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);
}
}
}
}