* Copyright (C) 2024 Frédéric France * * == BEGIN LICENSE == * * Licensed under the terms of any of the following licenses at your * choice: * * - GNU General Public License Version 2 or later (the "GPL") * https://www.gnu.org/licenses/gpl.html * * - GNU Lesser General Public License Version 2.1 or later (the "LGPL") * https://www.gnu.org/licenses/lgpl.html * * - Mozilla Public License Version 1.1 or later (the "MPL") * http://www.mozilla.org/MPL/MPL-1.1.html * * == END LICENSE == * * These functions are used by the connector.php script. */ /** * SetXmlHeaders * * @return void */ function SetXmlHeaders() { ob_end_clean(); // Prevent the browser from caching the result. // Date in the past header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // always modified header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); // HTTP/1.1 header('Cache-Control: no-store, no-cache, must-revalidate'); header('Cache-Control: post-check=0, pre-check=0', false); // HTTP/1.0 header('Pragma: no-cache'); // Set the response format. header('Content-Type: text/xml; charset=utf-8'); } /** * CreateXmlHeader * * @param string $command Command * @param string $resourceType Resource type * @param string $currentFolder Current folder * @return void */ function CreateXmlHeader($command, $resourceType, $currentFolder) { SetXmlHeaders(); // Create the XML document header. echo ''; // Create the main "Connector" node. echo ''; // Add the current folder node. echo ''; $GLOBALS['HeaderSent'] = true; } /** * CreateXmlFooter * * @return void */ function CreateXmlFooter() { echo ''; } /** * SendError * * @param integer $number Number * @param string $text Text * @return never */ function SendError($number, $text) { if ($_GET['Command'] == 'FileUpload') { SendUploadResults((string) $number, "", "", $text); } if (isset($GLOBALS['HeaderSent']) && $GLOBALS['HeaderSent']) { SendErrorNode($number, $text); CreateXmlFooter(); } else { SetXmlHeaders(); dol_syslog('Error: '.$number.' '.$text, LOG_ERR); // Create the XML document header echo ''; echo ''; SendErrorNode($number, $text); echo ''; } exit; } /** * SendErrorNode * * @param integer $number Number * @param string $text Text of error * @return string Error node */ function SendErrorNode($number, $text) { if ($text) { echo ''; } else { echo ''; } return ''; } /** * GetFolders * * @param string $resourceType Resource type * @param string $currentFolder Current folder * @return void */ function GetFolders($resourceType, $currentFolder) { // Map the virtual path to the local server path. $sServerDir = ServerMapFolder($resourceType, $currentFolder, 'GetFolders'); // Array that will hold the folders names. $aFolders = array(); $oCurrentFolder = @opendir($sServerDir); if ($oCurrentFolder !== false) { while ($sFile = readdir($oCurrentFolder)) { if ($sFile != '.' && $sFile != '..' && is_dir($sServerDir.$sFile)) { $aFolders[] = ''; } } closedir($oCurrentFolder); } // Open the "Folders" node. echo ""; natcasesort($aFolders); foreach ($aFolders as $sFolder) { echo $sFolder; } // Close the "Folders" node. echo ""; } /** * GetFoldersAndFiles * * @param string $resourceType Resource type * @param string $currentFolder Current folder * @return void */ function GetFoldersAndFiles($resourceType, $currentFolder) { // Map the virtual path to the local server path. $sServerDir = ServerMapFolder($resourceType, $currentFolder, 'GetFoldersAndFiles'); // Arrays that will hold the folders and files names. $aFolders = array(); $aFiles = array(); $oCurrentFolder = @opendir($sServerDir); if ($oCurrentFolder !== false) { while ($sFile = readdir($oCurrentFolder)) { if ($sFile != '.' && $sFile != '..') { if (is_dir($sServerDir.$sFile)) { $aFolders[] = ''; } else { $iFileSize = @filesize($sServerDir.$sFile); if (!$iFileSize) { $iFileSize = 0; } if ($iFileSize > 0) { $iFileSize = round($iFileSize / 1024); if ($iFileSize < 1) { $iFileSize = 1; } } $aFiles[] = ''; } } } closedir($oCurrentFolder); } // Send the folders natcasesort($aFolders); echo ''; foreach ($aFolders as $sFolder) { echo $sFolder; } echo ''; // Send the files natcasesort($aFiles); echo ''; foreach ($aFiles as $sFiles) { echo $sFiles; } echo ''; } /** * Create folder * * @param string $resourceType Resource type * @param string $currentFolder Current folder * @return void */ function CreateFolder($resourceType, $currentFolder) { $sErrorNumber = '0'; $sErrorMsg = ''; if (isset($_GET['NewFolderName'])) { $sNewFolderName = GETPOST('NewFolderName'); $sNewFolderName = SanitizeFolderName($sNewFolderName); if (strpos($sNewFolderName, '..') !== false) { $sErrorNumber = '102'; // Invalid folder name. } else { // Map the virtual path to the local server path of the current folder. $sServerDir = ServerMapFolder($resourceType, $currentFolder, 'CreateFolder'); if (is_writable($sServerDir)) { $sServerDir .= $sNewFolderName; $sErrorMsg = CreateServerFolder($sServerDir); switch ($sErrorMsg) { case '': $sErrorNumber = '0'; break; case 'Invalid argument': case 'No such file or directory': $sErrorNumber = '102'; // Path too long. break; default: $sErrorNumber = '110'; break; } } else { $sErrorNumber = '103'; } } } else { $sErrorNumber = '102'; } // Create the "Error" node. echo ''; } /** * FileUpload * * @param string $resourceType Resource type * @param string $currentFolder Current folder * @param string $sCommand Command * @param string $CKEcallback Callback * @return never */ function FileUpload($resourceType, $currentFolder, $sCommand, $CKEcallback = '') { global $user; if (!isset($_FILES)) { global $_FILES; // @phan-suppress-current-line PhanPluginConstantVariableNull } $sErrorNumber = '0'; $sFileName = ''; // (_FILES indexes:) @phan-suppress-next-line PhanTypeInvalidDimOffset if (isset($_FILES['NewFile']) && !is_null($_FILES['NewFile']['tmp_name']) && !is_null($_FILES['NewFile']['name']) || (isset($_FILES['upload']) && !is_null($_FILES['upload']['tmp_name']) && !is_null($_FILES['upload']['name']))) { global $Config; $oFile = isset($_FILES['NewFile']) ? $_FILES['NewFile'] : $_FILES['upload']; // $resourceType should be 'Image'; $detectHtml = 0; // Map the virtual path to the local server path. $sServerDir = ServerMapFolder($resourceType, $currentFolder, $sCommand); // Get the uploaded file name. $sFileName = $oFile['name']; //$sFileName = SanitizeFileName($sFileName); $sFileName = dol_sanitizeFileName($sFileName); $sOriginalFileName = $sFileName; // Get the extension. $sExtension = substr($sFileName, (strrpos($sFileName, '.') + 1)); $sExtension = strtolower($sExtension); // Check permission $permissiontouploadmediaisok = 1; if (!empty($user->socid)) { $permissiontouploadmediaisok = 0; } /*if (!$user->hasRight('website', 'write') && !$user->hasRight('mailing', 'write')) { $permissiontouploadmediaisok = 0; }*/ if (!$permissiontouploadmediaisok) { dol_syslog("connector.lib.php Try to upload a file with no permission"); $sErrorNumber = '202'; } include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php'; //var_dump($sFileName); var_dump(image_format_supported($sFileName));exit; $imgsupported = image_format_supported($sFileName); $isImageValid = ($imgsupported >= 0); if (!$isImageValid) { $sErrorNumber = '202'; } // Check if it is an allowed extension. if (!$sErrorNumber) { if (IsAllowedExt($sExtension, $resourceType)) { $iCounter = 0; while (true) { $sFilePath = $sServerDir.$sFileName; if (is_file($sFilePath)) { $iCounter++; $sFileName = RemoveExtension($sOriginalFileName).'('.$iCounter.').'.$sExtension; $sErrorNumber = '201'; } else { include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; dol_move_uploaded_file($oFile['tmp_name'], $sFilePath, 0, 0); if (is_file($sFilePath)) { if (isset($Config['ChmodOnUpload']) && !$Config['ChmodOnUpload']) { break; } $permissions = '0777'; if (isset($Config['ChmodOnUpload']) && $Config['ChmodOnUpload']) { $permissions = (string) $Config['ChmodOnUpload']; } $permissionsdec = octdec($permissions); dol_syslog("connector.lib.php permission = ".$permissions." ".$permissionsdec." ".decoct($permissionsdec)); $oldumask = umask(0); chmod($sFilePath, $permissionsdec); umask($oldumask); } break; } } if (file_exists($sFilePath)) { //previous checks failed, try once again if (isset($isImageValid) && $imgsupported === -1 && IsImageValid($sFilePath, $sExtension) === false) { dol_syslog("connector.lib.php IsImageValid is ko"); @unlink($sFilePath); $sErrorNumber = '202'; } else { $detectHtml = DetectHtml($sFilePath); if ($detectHtml === true || $detectHtml == -1) { // Note that is is a simple test and not reliable. Security does not rely on this. dol_syslog("connector.lib.php DetectHtml is ko"); @unlink($sFilePath); $sErrorNumber = '202'; } } } } else { $sErrorNumber = '202'; } } } else { $sErrorNumber = '203'; } $sFileUrl = CombinePaths(GetResourceTypePath($resourceType, $sCommand), $currentFolder); $sFileUrl = CombinePaths($sFileUrl, $sFileName); // @CHANGE //SendUploadResults( $sErrorNumber, $sFileUrl, $sFileName ); if ($CKEcallback == '') { // this line already exists so wrap the if block around it SendUploadResults($sErrorNumber, $sFileUrl, $sFileName); } else { //issue the CKEditor Callback SendCKEditorResults( $CKEcallback, $sFileUrl, ($sErrorNumber != 0 ? 'Error '.$sErrorNumber.' upload failed.' : 'Upload Successful') ); } exit; } /** * CombinePaths * * @param string $sBasePath sBasePath * @param string $sFolder sFolder * @return string Combined path */ function CombinePaths($sBasePath, $sFolder) { return RemoveFromEnd($sBasePath, '/').'/'.RemoveFromStart($sFolder, '/'); } /** * GetResourceTypePath * * @param string $resourceType Resource type * @param string $sCommand Command * @return string Config */ function GetResourceTypePath($resourceType, $sCommand) { global $Config; if ($sCommand == "QuickUpload") { return $Config['QuickUploadPath'][$resourceType]; } else { return $Config['FileTypesPath'][$resourceType]; } } /** * GetResourceTypeDirectory * * @param string $resourceType Resource type * @param string $sCommand Command * @return string */ function GetResourceTypeDirectory($resourceType, $sCommand) { global $Config; if ($sCommand == "QuickUpload") { if (strlen($Config['QuickUploadAbsolutePath'][$resourceType]) > 0) { return $Config['QuickUploadAbsolutePath'][$resourceType]; } // Map the "UserFiles" path to a local directory. return Server_MapPath($Config['QuickUploadPath'][$resourceType]); } else { if (strlen($Config['FileTypesAbsolutePath'][$resourceType]) > 0) { return $Config['FileTypesAbsolutePath'][$resourceType]; } // Map the "UserFiles" path to a local directory. return Server_MapPath($Config['FileTypesPath'][$resourceType]); } } /** * GetUrlFromPath * * @param string $resourceType Resource type * @param string $folderPath Path * @param string $sCommand Command * @return string Full url */ function GetUrlFromPath($resourceType, $folderPath, $sCommand) { return CombinePaths(GetResourceTypePath($resourceType, $sCommand), $folderPath); } /** * RemoveExtension * * @param string $fileName Filename * @return string String without extension */ function RemoveExtension($fileName) { return substr($fileName, 0, strrpos($fileName, '.')); } /** * ServerMapFolder * * @param string $resourceType Resource type * @param string $folderPath Folder * @param string $sCommand Command * @return string */ function ServerMapFolder($resourceType, $folderPath, $sCommand) { // Get the resource type directory. $sResourceTypePath = GetResourceTypeDirectory($resourceType, $sCommand); // Ensure that the directory exists. $sErrorMsg = CreateServerFolder($sResourceTypePath); if ($sErrorMsg != '') { SendError(1, "Error creating folder \"$sResourceTypePath\" ($sErrorMsg)"); } // Return the resource type directory combined with the required path. return CombinePaths($sResourceTypePath, $folderPath); } /** * GetParentFolder * * @param string $folderPath Folder path * @return string Parent folder */ function GetParentFolder($folderPath) { $sPattern = "-[/\\\\][^/\\\\]+[/\\\\]?$-"; return preg_replace($sPattern, '', $folderPath); } /** * CreateServerFolder * * @param string $folderPath Folder - Folder to create (recursively) * @param ?string $lastFolder Internal - Child Folder we are creating, prevents recursion * @return string ''=success, error message otherwise */ function CreateServerFolder($folderPath, $lastFolder = null) { global $user; global $Config; $sParent = GetParentFolder($folderPath); // Ensure the folder path has no double-slashes, or mkdir may fail on certain platforms while (strpos($folderPath, '//') !== false) { $folderPath = str_replace('//', '/', $folderPath); } $permissiontouploadmediaisok = 1; if (!empty($user->socid)) { $permissiontouploadmediaisok = 0; } /*if (!$user->hasRight('website', 'write') && !$user->hasRight('mailing', 'write')) { $permissiontouploadmediaisok = 0; }*/ if (!$permissiontouploadmediaisok) { return 'Bad permissions to create a folder in media directory'; } // Check if the parent exists, or create it. if (!empty($sParent) && !file_exists($sParent)) { //prevents against infinite loop when we can't create root folder if (!is_null($lastFolder) && $lastFolder === $sParent) { return "Can't create $folderPath directory"; } // @phan-suppress-next-line PhanPluginSuspiciousParamPosition $sErrorMsg = CreateServerFolder($sParent, $folderPath); if ($sErrorMsg != '') { return $sErrorMsg; } } if (!file_exists($folderPath)) { // Turn off all error reporting. error_reporting(0); $php_errormsg = ''; // Enable error tracking to catch the error. ini_set('track_errors', '1'); if (isset($Config['ChmodOnFolderCreate']) && !$Config['ChmodOnFolderCreate']) { mkdir($folderPath); } else { $permissions = '0777'; if (isset($Config['ChmodOnFolderCreate']) && $Config['ChmodOnFolderCreate']) { $permissions = (string) $Config['ChmodOnFolderCreate']; } $permissionsdec = octdec($permissions); $permissionsdec |= octdec('0111'); // Set x bit required for directories dol_syslog("connector.lib.php permission = ".$permissions." ".$permissionsdec." ".decoct($permissionsdec)); // To create the folder with 0777 permissions, we need to set umask to zero. $oldumask = umask(0); mkdir($folderPath, $permissionsdec); umask($oldumask); } $sErrorMsg = $php_errormsg; // Restore the configurations. ini_restore('track_errors'); ini_restore('error_reporting'); return $sErrorMsg; } else { return ''; } } /** * Get Root Path * * @return string real path */ function GetRootPath() { if (!isset($_SERVER)) { global $_SERVER; // @phan-suppress-current-line PhanPluginConstantVariableNull } $sRealPath = realpath('./'); // #2124 ensure that no slash is at the end $sRealPath = rtrim($sRealPath, "\\/"); $sSelfPath = $_SERVER['PHP_SELF']; $sSelfPath = substr($sSelfPath, 0, strrpos($sSelfPath, '/')); $sSelfPath = str_replace('/', DIRECTORY_SEPARATOR, $sSelfPath); $position = strpos($sRealPath, $sSelfPath); // This can check only that this script isn't run from a virtual dir // But it avoids the problems that arise if it isn't checked if ($position === false || $position != strlen($sRealPath) - strlen($sSelfPath)) { SendError(1, 'Sorry, can\'t map "UserFilesPath" to a physical path. You must set the "UserFilesAbsolutePath" value in "editor/filemanager/connectors/php/config.inc.php".'); } return substr($sRealPath, 0, $position); } /** * Emulate the asp Server.mapPath function. * @param string $path given an url path return the physical directory that it corresponds to * @return string Path */ function Server_MapPath($path) { // This function is available only for Apache if (function_exists('apache_lookup_uri')) { $info = apache_lookup_uri($path); return $info->filename.$info->path_info; } // This isn't correct but for the moment there's no other solution // If this script is under a virtual directory or symlink it will detect the problem and stop return GetRootPath().$path; } /** * Is Allowed Extension * * @param string $sExtension File extension * @param string $resourceType resource type * @return boolean true or false */ function IsAllowedExt($sExtension, $resourceType) { global $Config; // Get the allowed and denied extensions arrays. $arAllowed = $Config['AllowedExtensions'][$resourceType]; $arDenied = $Config['DeniedExtensions'][$resourceType]; if (count($arAllowed) > 0 && !in_array($sExtension, $arAllowed)) { return false; } if (count($arDenied) > 0 && in_array($sExtension, $arDenied)) { return false; } return true; } /** * Is Allowed Type * * @param string $resourceType resource type * @return boolean true or false */ function IsAllowedType($resourceType) { global $Config; if (!in_array($resourceType, $Config['ConfigAllowedTypes'])) { return false; } return true; } /** * IsAllowedCommand * * @param string $sCommand Command * @return boolean True or false */ function IsAllowedCommand($sCommand) { global $Config; if (!in_array($sCommand, $Config['ConfigAllowedCommands'])) { return false; } return true; } /** * GetCurrentFolder * * @return string current folder */ function GetCurrentFolder() { $sCurrentFolder = isset($_GET['CurrentFolder']) ? GETPOST('CurrentFolder', 'alphanohtml', 1) : '/'; // Check the current folder syntax (must begin and start with a slash). if (!preg_match('|/$|', $sCurrentFolder)) { $sCurrentFolder .= '/'; } if (strpos($sCurrentFolder, '/') !== 0) { $sCurrentFolder = '/'.$sCurrentFolder; } // Ensure the folder path has no double-slashes while (strpos($sCurrentFolder, '//') !== false) { $sCurrentFolder = str_replace('//', '/', $sCurrentFolder); } // Check for invalid folder paths (..) if (strpos($sCurrentFolder, '..') || strpos($sCurrentFolder, "\\")) { SendError(102, ''); } if (preg_match(",(/\.)|[[:cntrl:]]|(//)|(\\\\)|([\:\*\?\"\<\>\|]),", $sCurrentFolder)) { SendError(102, ''); } return $sCurrentFolder; } /** * Do a cleanup of the folder name to avoid possible problems * * @param string $sNewFolderName Folder * @return string Folder sanitized */ function SanitizeFolderName($sNewFolderName) { $sNewFolderName = stripslashes($sNewFolderName); // Remove . \ / | : ? * " < > $sNewFolderName = preg_replace('/\\.|\\\\|\\/|\\||\\:|\\?|\\*|"|<|>|[[:cntrl:]]/', '_', $sNewFolderName); return $sNewFolderName; } /** * Do a cleanup of the file name to avoid possible problems * * @param string $sNewFileName Folder * @return string Folder sanitized */ function SanitizeFileName($sNewFileName) { global $Config; $sNewFileName = stripslashes($sNewFileName); // Replace dots in the name with underscores (only one dot can be there... security issue). if ($Config['ForceSingleExtension']) { $sNewFileName = preg_replace('/\\.(?![^.]*$)/', '_', $sNewFileName); } // Remove \ / | : ? * " < > $sNewFileName = preg_replace('/\\\\|\\/|\\||\\:|\\?|\\*|"|<|>|[[:cntrl:]]/', '_', $sNewFileName); return $sNewFileName; } /** * This is the function that sends the results of the uploading process. * * @param string $errorNumber errorNumber * @param string $fileUrl fileUrl * @param string $fileName fileName * @param string $customMsg customMsg * @return never */ function SendUploadResults($errorNumber, $fileUrl = '', $fileName = '', $customMsg = '') { // Minified version of the document.domain automatic fix script (#1919). // The original script can be found at _dev/domain_fix_template.js echo << (function(){var d=document.domain;while (true){try{var A=window.parent.document.domain;break;}catch(e) {};d=d.replace(/.*?(?:\.|$)/,'');if (d.length==0) break;try{document.domain=d;}catch (e){break;}}})(); EOF; if ($errorNumber && $errorNumber != '201') { $fileUrl = ""; $fileName = ""; } $rpl = array('\\' => '\\\\', '"' => '\\"'); echo 'console.log('.$errorNumber.');'; echo 'window.parent.OnUploadCompleted('.$errorNumber.', "'.strtr($fileUrl, $rpl).'", "'.strtr($fileName, $rpl).'", "'.strtr($customMsg, $rpl).'");'; echo ''; exit; } // @CHANGE // This is the function that sends the results of the uploading process to CKE. /** * SendCKEditorResults * * @param string $callback callback * @param string $sFileUrl sFileUrl * @param string $customMsg customMsg * @return void */ function SendCKEditorResults($callback, $sFileUrl, $customMsg = '') { echo ''; } /** * RemoveFromStart * * @param string $sourceString Source * @param string $charToRemove Char to remove * @return string Result */ function RemoveFromStart($sourceString, $charToRemove) { $sPattern = '|^'.$charToRemove.'+|'; return preg_replace($sPattern, '', $sourceString); } /** * RemoveFromEnd * * @param string $sourceString Source * @param string $charToRemove Rhar to remove * @return string Result */ function RemoveFromEnd($sourceString, $charToRemove) { $sPattern = '|'.$charToRemove.'+$|'; return preg_replace($sPattern, '', $sourceString); } /** * FindBadUtf8 * * @param string $string String * @return boolean */ function FindBadUtf8($string) { $regex = '([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]'; $regex .= '|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2}|(.{1}))'; $matches = array(); while (preg_match('/'.$regex.'/S', $string, $matches)) { if (isset($matches[2])) { return true; } $string = substr($string, strlen($matches[0])); } return false; } /** * ConvertToXmlAttribute * * @param string $value Value * @return string */ function ConvertToXmlAttribute($value) { if (defined('PHP_OS')) { $os = PHP_OS; } else { $os = php_uname(); } if (strtoupper(substr($os, 0, 3)) === 'WIN' || FindBadUtf8($value)) { return (mb_convert_encoding(htmlspecialchars($value), 'UTF-8', 'ISO-8859-1')); } else { return (htmlspecialchars($value)); } } /** * Check whether given extension is in html extensions list * * @param string $ext Extension (Will only match if lowercase) * @param string[] $formExtensions Array of extensions (Internally lowercased) * @return boolean */ function IsHtmlExtension($ext, $formExtensions) { if (!$formExtensions || !is_array($formExtensions)) { return false; } $lcaseHtmlExtensions = array(); foreach ($formExtensions as $key => $val) { $lcaseHtmlExtensions[$key] = strtolower($val); } return in_array($ext, $lcaseHtmlExtensions); } /** * Detect HTML in the first KB to prevent against potential security issue with * IE/Safari/Opera file type auto detection bug. * * @param string $filePath Absolute path to file * @return bool|-1 Returns true if the file contains insecure HTML code at the beginning or false, or -1 if error */ function DetectHtml($filePath) { $fp = @fopen($filePath, 'rb'); //open_basedir restriction, see #1906 if ($fp === false || !flock($fp, LOCK_SH)) { return -1; } $chunk = fread($fp, 1024); flock($fp, LOCK_UN); fclose($fp); $chunk = strtolower($chunk); if (!$chunk) { return false; } $chunk = trim($chunk); if (preg_match("/