getArray() as $i => $imageRoot) { // in $obj, "rel-path" is only set when document root can be used for relative paths. // So, if it is set, we can use it (beware: we cannot neccessarily use realpath on document root, // but we do not need to - see the long comment in Paths::canUseDocRootForRelPaths()) $rootPath = $imageRoot->getAbsPath(); /* if (isset($obj['rel-path'])) { $docRoot = rtrim($_SERVER["DOCUMENT_ROOT"], '/'); $rootPath = $docRoot . '/' . $obj['rel-path']; } else { // If "rel-path" isn't set, then abs-path is, and we can use that. $rootPath = $obj['abs-path']; }*/ // $source may be resolved or not. Same goes for $rootPath. // We can assume that $rootPath is resolvable using realpath (it ought to exist and be within open_basedir for WP to function) // We can also assume that $source is resolvable (it ought to exist and within open_basedir) // So: Resolve both! and test if the resolved source begins with the resolved rootPath. if (strpos($sourceResolved, realpath($rootPath)) !== false) { $relPath = substr($sourceResolved, strlen(realpath($rootPath)) + 1); $relPath = self::appendOrSetExtension($relPath, $destinationFolder, $destinationExt, false); $destination = $webExpressContentDirAbs . '/webp-images/' . $imageRoot->id . '/' . $relPath; break; } } if ($destination == '') { return false; } } } } catch (SanityException $e) { return false; } return $destination; } /** * Find source corresponding to destination, separate. * * We can rely on destinationExt being "append" for separate. * Returns false if source file is not found or if a path is not sane. Otherwise returns path to source * destination does not have to exist. * * @param string $destination Path to destination file (does not have to exist) * @param string $destinationStructure "doc-root" or "image-roots" * @param string $webExpressContentDirAbs * @param ImageRoots $imageRoots An image roots object * * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned */ private static function findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots) { try { if ($destinationStructure == 'doc-root') { // Check that destination path is sane and inside document root // -------------------------- $destination = SanityCheck::absPathIsInDocRoot($destination); // Check that calculated image root is sane and inside document root // -------------------------- $imageRoot = SanityCheck::absPathIsInDocRoot($webExpressContentDirAbs . '/webp-images/doc-root'); // Calculate source and check that it is sane and exists // ----------------------------------------------------- // TODO: This does not work on Windows yet. if (strpos($destination, $imageRoot . '/') === 0) { // "Eat" the left part off the $destination parameter. $destination is for example: // "/var/www/webp-express-tests/we0/wp-content-moved/webp-express/webp-images/doc-root/wordpress/uploads-moved/2018/12/tegning5-300x265.jpg.webp" // We also eat the slash (+1) $sourceRel = substr($destination, strlen($imageRoot) + 1); $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/'); $source = $docRoot . '/' . $sourceRel; $source = preg_replace('/\\.(webp)$/', '', $source); } else { // Try with symlinks resolved // This is not trivial as this must also work when the destination path doesn't exist, and // realpath can only be used to resolve symlinks for files that exists. // But here is how we achieve it anyway: // // 1. We make sure imageRoot exists (if not, create it) - this ensures that we can resolve it. // 2. Find closest folder existing folder (resolved) of destination - using PathHelper::findClosestExistingFolderSymLinksExpanded() // 3. Test that resolved closest existing folder starts with resolved imageRoot // 4. If it does, we could create a dummy file at the destination to get its real path, but we want to avoid that, so instead // we can create the containing directory. // 5. We can now use realpath to get the resolved path of the containing directory. The rest is simple enough. if (!file_exists($imageRoot)) { mkdir($imageRoot, 0777, true); } $closestExistingResolved = PathHelper::findClosestExistingFolderSymLinksExpanded($destination); if ($closestExistingResolved == '') { return false; } else { $imageRootResolved = realpath($imageRoot); if (strpos($closestExistingResolved . '/', $imageRootResolved . '/') === 0) { // echo $destination . '
' . $closestExistingResolved . '
' . $imageRootResolved . '/'; exit; // Create containing dir for destination $containingDir = PathHelper::dirname($destination); if (!file_exists($containingDir)) { mkdir($containingDir, 0777, true); } $containingDirResolved = realpath($containingDir); $filename = PathHelper::basename($destination); $destinationResolved = $containingDirResolved . '/' . $filename; $sourceRel = substr($destinationResolved, strlen($imageRootResolved) + 1); $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/'); $source = $docRoot . '/' . $sourceRel; $source = preg_replace('/\\.(webp)$/', '', $source); return $source; } else { return false; } } } return SanityCheck::absPathExistsAndIsFileInDocRoot($source); } else { // Mission: To find source corresponding to destination (separate) - using the "image-roots" structure. // How can we do that? // We got the destination (unresolved) - ie '/website-symlinked/wp-content/webp-express/webp-images/uploads/2018/07/hello.jpg.webp' // If we were lazy and unprecise, we could simply: // - search for "webp-express/webp-images/" // - strip anything before that - result: 'uploads/2018/07/hello.jpg.webp' // - the first path component is the root id. // - the rest of the path is the relative path to the source - if we strip the ".webp" ending // So, are we lazy? - what is the alternative? // - Get closest existing resolved folder of destination (ie "/var/www/website/wp-content-moved/webp-express/webp-images/wp-content") // - Check if that folder is below the cache root (resolved) (cache root is the "wp-content" image root + 'webp-express/webp-images') // - Create dir for destination (if missing) // - We can now resolve destination. With cache root also being resolved, we can get the relative dir. // ie 'uploads/2018/07/hello.jpg.webp'. // The first path component is the root id, the rest is the relative path to the source. $closestExistingResolved = PathHelper::findClosestExistingFolderSymLinksExpanded($destination); $cacheRoot = $webExpressContentDirAbs . '/webp-images'; if ($closestExistingResolved == '') { return false; } else { $cacheRootResolved = realpath($cacheRoot); if (strpos($closestExistingResolved . '/', $cacheRootResolved . '/') === 0) { // Create containing dir for destination $containingDir = PathHelper::dirname($destination); if (!file_exists($containingDir)) { mkdir($containingDir, 0777, true); } $containingDirResolved = realpath($containingDir); $filename = PathHelper::basename($destination); $destinationResolved = $containingDirResolved . '/' . $filename; $destinationRelToCacheRoot = substr($destinationResolved, strlen($cacheRootResolved) + 1); $parts = explode('/', $destinationRelToCacheRoot); $imageRoot = array_shift($parts); $sourceRel = implode('/', $parts); $source = $imageRoots->byId($imageRoot)->getAbsPath() . '/' . $sourceRel; $source = preg_replace('/\\.(webp)$/', '', $source); return $source; } else { return false; } } return false; } } catch (SanityException $e) { return false; } return $source; } /** * Find source corresponding to destination (mingled) * Returns false if not found. Otherwise returns path to source * * @param string $destination Path to destination file (does not have to exist) * @param string $destinationExt Extension ('append' or 'set') * @param string $destinationStructure "doc-root" or "image-roots" * * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned */ private static function findSourceMingled($destination, $destinationExt, $destinationStructure) { try { if ($destinationStructure == 'doc-root') { // Check that destination path is sane and inside document root // -------------------------- $destination = SanityCheck::absPathIsInDocRoot($destination); } else { // The following will fail if path contains directory traversal. TODO: Is that ok? $destination = SanityCheck::absPath($destination); } // Calculate source and check that it is sane and exists // ----------------------------------------------------- if ($destinationExt == 'append') { $source = preg_replace('/\\.(webp)$/', '', $destination); } else { $source = preg_replace('#\\.webp$#', '.jpg', $destination); // TODO! // Also check for "Jpeg", "JpEg" etc. if (!@file_exists($source)) { $source = preg_replace('/\\.webp$/', '.jpeg', $destination); } if (!@file_exists($source)) { $source = preg_replace('/\\.webp$/', '.JPG', $destination); } if (!@file_exists($source)) { $source = preg_replace('/\\.webp$/', '.JPEG', $destination); } if (!@file_exists($source)) { $source = preg_replace('/\\.webp$/', '.png', $destination); } if (!@file_exists($source)) { $source = preg_replace('/\\.webp$/', '.PNG', $destination); } } if ($destinationStructure == 'doc-root') { $source = SanityCheck::absPathExistsAndIsFileInDocRoot($source); } else { $source = SanityCheck::absPathExistsAndIsFile($source); } } catch (SanityException $e) { return false; } return $source; } /** * Get source from destination (and some configurations) * Returns false if not found. Otherwise returns path to source * * @param string $destination Path to destination file (does not have to exist). May not contain directory traversal * @param string $destinationFolder 'mingled' or 'separate' * @param string $destinationExt Extension ('append' or 'set') * @param string $destinationStructure "doc-root" or "image-roots" * @param string $webExpressContentDirAbs * @param ImageRoots $imageRoots An image roots object * * @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned */ public static function findSource($destination, $destinationFolder, $destinationExt, $destinationStructure, $webExpressContentDirAbs, $imageRoots) { try { if ($destinationStructure == 'doc-root') { // Check that destination path is sane and inside document root // -------------------------- $destination = SanityCheck::absPathIsInDocRoot($destination); } else { // The following will fail if path contains directory traversal. TODO: Is that ok? $destination = SanityCheck::absPath($destination); } } catch (SanityException $e) { return false; } if ($destinationFolder == 'mingled') { $result = self::findSourceMingled($destination, $destinationExt, $destinationStructure); if ($result === false) { $result = self::findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots); } return $result; } else { return self::findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots); } } /** * * @param string $source Path to source file * @param string $logDir The folder where log files are kept * * @return string|false Returns computed filename of log - or false if a path is not sane * */ public static function getLogFilename($source, $logDir) { try { // Check that source path is sane and inside document root // ------------------------------------------------------- $source = SanityCheck::absPathIsInDocRoot($source); // Check that log path is sane and inside document root // ------------------------------------------------------- $logDir = SanityCheck::absPathIsInDocRoot($logDir); // Compute and check log path // -------------------------- $logDirForConversions = $logDir .= '/conversions'; // We store relative to document root. // "Eat" the left part off the source parameter which contains the document root. // and also eat the slash (+1) $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/'); $sourceRel = substr($source, strlen($docRoot) + 1); $logFileName = $logDir . '/doc-root/' . $sourceRel . '.md'; SanityCheck::absPathIsInDocRoot($logFileName); } catch (SanityException $e) { return false; } return $logFileName; } /** * Create the directory for log files and put a .htaccess file into it, which prevents * it to be viewed from the outside (not that it contains any sensitive information btw, but for good measure). * * @param string $logDir The folder where log files are kept * * @return boolean Whether it was created successfully or not. * */ private static function createLogDir($logDir) { if (!is_dir($logDir)) { @mkdir($logDir, 0775, true); @chmod($logDir, 0775); @file_put_contents(rtrim($logDir . '/') . '/.htaccess', << Require all denied Order deny,allow Deny from all APACHE ); @chmod($logDir . '/.htaccess', 0664); } return is_dir($logDir); } /** * Saves the log file corresponding to a conversion. * * @param string $source Path to the source file that was converted * @param string $logDir The folder where log files are kept * @param string $text Content of the log file * @param string $msgTop A message that is printed before the conversion log (containing version info) * * */ private static function saveLog($source, $logDir, $text, $msgTop) { if (!file_exists($logDir)) { self::createLogDir($logDir); } $text = preg_replace('#' . preg_quote($_SERVER["DOCUMENT_ROOT"]) . '#', '[doc-root]', $text); // TODO: Put version number somewhere else. Ie \WebPExpress\VersionNumber::version $text = 'WebP Express 0.25.9. ' . $msgTop . ', ' . date("Y-m-d H:i:s") . "\n\r\n\r" . $text; $logFile = self::getLogFilename($source, $logDir); if ($logFile === false) { return; } $logFolder = @dirname($logFile); if (!@file_exists($logFolder)) { mkdir($logFolder, 0777, true); } if (@file_exists($logFolder)) { file_put_contents($logFile, $text); } } /** * Trigger an actual conversion with webp-convert. * * PS: To convert with a specific converter, set it in the $converter param. * * @param string $source Full path to the source file that was converted. * @param string $destination Full path to the destination file (may exist or not). * @param array $convertOptions Conversion options. * @param string $logDir The folder where log files are kept or null for no logging * @param string $converter (optional) Set it to convert with a specific converter. */ public static function convert($source, $destination, $convertOptions, $logDir = null, $converter = null) { include_once __DIR__ . '/../../vendor/autoload.php'; // At this point, everything has already been checked for sanity. But for good meassure, lets // check the most important parts again. This is after all a public method. // ------------------------------------------------------------------ try { // Check that source path is sane, exists, is a file and is inside document root // ------------------------------------------------------- // First check if file exists before doing any other validations if (!file_exists($source)) { return [ 'success' => false, 'msg' => 'Source file does not exist: ' . $source, 'log' => '', ]; } $source = SanityCheck::absPathExistsAndIsFileInDocRoot($source); // Check that destination path is sane and is inside document root // ------------------------------------------------------- $destination = SanityCheck::absPathIsInDocRoot($destination); $destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Destination does not end with .webp'); // Check that log path is sane and inside document root // ------------------------------------------------------- if (!is_null($logDir)) { $logDir = SanityCheck::absPathIsInDocRoot($logDir); } // PS: No need to check $logMsgTop. Log files are markdown and stored as ".md". They can do no harm. } catch (SanityException $e) { return [ 'success' => false, 'msg' => $e->getMessage(), 'log' => '', ]; } $success = false; $msg = ''; $logger = new BufferLogger(); try { if (!is_null($converter)) { //if (isset($convertOptions['converter'])) { //print_r($convertOptions);exit; $logger->logLn('Converter set to: ' . $converter); $logger->logLn(''); $converter = ConverterFactory::makeConverter($converter, $source, $destination, $convertOptions, $logger); $converter->doConvert(); } else { //error_log('options:' . print_r(json_encode($convertOptions,JSON_PRETTY_PRINT), true)); WebPConvert::convert($source, $destination, $convertOptions, $logger); } $success = true; } catch (\WebpConvert\Exceptions\WebPConvertException $e) { $msg = $e->getMessage(); } catch (\Exception $e) { //$msg = 'An exception was thrown!'; $msg = $e->getMessage(); } catch (\Throwable $e) { //Executed only in PHP 7 and 8, will not match in PHP 5 $msg = $e->getMessage(); } if (!is_null($logDir)) { self::saveLog($source, $logDir, $logger->getMarkDown("\n\r"), 'Conversion triggered using bulk conversion'); } return [ 'success' => $success, 'msg' => $msg, 'log' => $logger->getMarkDown("\n"), ]; } /** * Serve a converted file (if it does not already exist, a conversion is triggered - all handled in webp-convert). * */ public static function serveConverted($source, $destination, $serveOptions, $logDir = null, $logMsgTop = '') { include_once __DIR__ . '/../../vendor/autoload.php'; // At this point, everything has already been checked for sanity. But for good meassure, lets // check again. This is after all a public method. // --------------------------------------------- try { // Check that source path is sane, exists, is a file. // ------------------------------------------------------- //$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source); $source = SanityCheck::absPathExistsAndIsFile($source); // Check that destination path is sane // ------------------------------------------------------- //$destination = SanityCheck::absPathIsInDocRoot($destination); $destination = SanityCheck::absPath($destination); $destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Destination does not end with .webp'); // Check that log path is sane // ------------------------------------------------------- //$logDir = SanityCheck::absPathIsInDocRoot($logDir); if ($logDir != null) { $logDir = SanityCheck::absPath($logDir); } // PS: No need to check $logMsgTop. Log files are markdown and stored as ".md". They can do no harm. } catch (SanityException $e) { $msg = $e->getMessage(); echo $msg; header('X-WebP-Express-Error: ' . $msg, true); // TODO: error_log() ? exit; } $convertLogger = new BufferLogger(); WebPConvert::serveConverted($source, $destination, $serveOptions, null, $convertLogger); if (!is_null($logDir)) { $convertLog = $convertLogger->getMarkDown("\n\r"); if ($convertLog != '') { self::saveLog($source, $logDir, $convertLog, $logMsgTop); } } } }