File "ConvertHelperIndependent.php"

Full Path: /home/amervokv/ecomlive.net/wp-content/plugins/webp-express/lib/classes/ConvertHelperIndependent.php
File size: 33.5 KB
MIME-type: text/x-php
Charset: utf-8

<?php

/*
This class is made to not be dependent on Wordpress functions and must be kept like that.
It is used by webp-on-demand.php. It is also used for bulk conversion.
*/
namespace WebPExpress;

use \WebPConvert\WebPConvert;
use \WebPConvert\Convert\ConverterFactory;
use \WebPConvert\Exceptions\WebPConvertException;
use \WebPConvert\Loggers\BufferLogger;

use \WebPExpress\FileHelper;
use \WebPExpress\SanityCheck;
use \WebPExpress\SanityException;

class ConvertHelperIndependent
{

    /**
     *
     * @return boolean  Whether or not the destination corresponding to a given source should be stored in the same folder or the separate (in wp-content/webp-express)
     */
    private static function storeMingledOrNot($source, $destinationFolder, $uploadDirAbs)
    {
        if ($destinationFolder != 'mingled') {
            return false;
        }

        // Option is set for mingled, but this does not neccessarily means we should store "mingled".
        // - because the mingled option only applies to upload folder, the rest is stored in separate cache folder
        // So, return true, if $source is located in upload folder
        return (strpos($source, $uploadDirAbs) === 0);
    }

    /**
     *  Verify if source is inside in document root
     *  Note: This function relies on the existence of both.
     *
     *  @return true if windows; false if not.
     */
    public static function sourceIsInsideDocRoot($source, $docRoot){

        $normalizedSource = realpath($source);
        $normalizedDocRoot = realpath($docRoot);

        return strpos($normalizedSource, $normalizedDocRoot) === 0;
    }

    public static function getSource()
    {

    }

    /**
     * Append ".webp" to path or replace extension with "webp", depending on what is appropriate.
     *
     * If destination-folder is set to mingled and destination-extension is set to "set" and
     * the path is inside upload folder, the appropriate thing is to SET the extension.
     * Otherwise, it is to APPEND.
     *
     * @param  string  $path
     * @param  string  $destinationFolder
     * @param  string  $destinationExt
     * @param  boolean $inUploadFolder
     */
    public static function appendOrSetExtension($path, $destinationFolder, $destinationExt, $inUploadFolder)
    {
        if (($destinationFolder == 'mingled') && ($destinationExt == 'set') && $inUploadFolder) {
            return preg_replace('/\\.(jpe?g|png)$/i', '', $path) . '.webp';
        } else {
            return $path . '.webp';
        }
    }

    /**
     * Get destination path corresponding to the source path given (and some configurations)
     *
     * If for example Operation mode is set to "mingled" and extension is set to "Append .webp",
     * the result of finding the destination path that corresponds to "/path/to/logo.jpg" will be "/path/to/logo.jpg.webp".
     *
     * @param  string   $source                     Path to source file
     * @param  string   $destinationFolder          'mingled' or 'separate'
     * @param  string   $destinationExt             Extension ('append' or 'set')
     * @param  string   $webExpressContentDirAbs
     * @param  string   $uploadDirAbs
     * @param  boolean  $useDocRootForStructuringCacheDir
     * @param  ImageRoots  $imageRoots                An image roots object
     *
     * @return string|false   Returns path to destination corresponding to source, or false on failure
     */
    public static function getDestination(
        $source,
        $destinationFolder,
        $destinationExt,
        $webExpressContentDirAbs,
        $uploadDirAbs,
        $useDocRootForStructuringCacheDir,
        $imageRoots)
    {
        // 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 source
            // --------------
            // TODO: make this check work with symlinks
            //$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);

            // Calculate destination and check that the result is sane
            // -------------------------------------------------------
            if (self::storeMingledOrNot($source, $destinationFolder, $uploadDirAbs)) {
                $destination = self::appendOrSetExtension($source, $destinationFolder, $destinationExt, true);
            } else {

                if ($useDocRootForStructuringCacheDir) {
                    // We must find the relative path from document root to source.
                    // However, we dont know if document root is resolved or not.
                    // We also do not know if source begins with a resolved or unresolved document root.
                    // And we cannot be sure that document root is resolvable.

                    // Lets say:
                    // 1. document root is unresolvable.
                    // 2. document root is configured to something unresolved ("/my-website")
                    // 3. source is resolved and within an image root ("/var/www/my-website/wp-content/uploads/test.jpg")
                    // 4. all image roots are resolvable.
                    // 5. Paths::canUseDocRootForRelPaths()) returned true

                    // Can the relative path then be found?
                    // Actually, yes.
                    // We can loop through the image roots.
                    // When we get to the "uploads" root, it must neccessarily contain the unresolved document root.
                    // It will in other words be: "my-website/wp-content/uploads"
                    // It can not be configured to the resolved path because canUseDocRootForRelPaths would have then returned false as
                    // It would not be possible to establish that "/var/www/my-website/wp-content/uploads/" is within document root, as
                    // document root is "/my-website" and unresolvable.
                    // To sum up, we have:
                    // If document root is unresolvable while canUseDocRootForRelPaths() succeeded, then the image roots will all begin with
                    // the unresolved path.
                    // In this method, if $useDocRootForStructuringCacheDir is true, then it is assumed that canUseDocRootForRelPaths()
                    // succeeded.
                    // OH!
                    // I realize that the image root can be passed as well:
                    // $imageRoot = $webExpressContentDirAbs . '/webp-images';
                    // So the question is: Will $webExpressContentDirAbs also be the unresolved path?
                    // That variable is calculated in WodConfigLoader based on various methods available.
                    // I'm not digging into it, but would expect it to in some cases be resolved. Which means that relative path can not
                    // be found.
                    // So. Lets play it safe and require that document root is resolvable in order to use docRoot for structure

                    if (!PathHelper::isDocRootAvailable()) {
                        throw new \Exception(
                            'Can not calculate destination using "doc-root" structure as document root is not available. $_SERVER["DOCUMENT_ROOT"] is empty. ' .
                            'This is probably a misconfiguration on the server. ' .
                            'However, WebP Express can function without using documument root. If you resave options and regenerate the .htaccess files, it should ' .
                            'automatically start to structure the webp files in subfolders that are relative the image root folders rather than document-root.'
                        );
                    }

                    if (!PathHelper::isDocRootAvailableAndResolvable()) {
                        throw new \Exception(
                            'Can not calculate destination using "doc-root" structure as document root cannot be resolved for symlinks using "realpath". The ' .
                            'reason for that is probably that open_basedir protection has been set up and that document root is outside outside that open_basedir. ' .
                            'WebP Express can function in that setting, however you will need to resave options and regenerate the .htaccess files. It should then ' .
                            'automatically stop to structure the webp files as relative to document root and instead structure them as relative to image root folders.'
                        );
                    }
                    $docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
                    $imageRoot = $webExpressContentDirAbs . '/webp-images';

                    // TODO: make this check work with symlinks
                    //SanityCheck::absPathIsInDocRoot($imageRoot);

                    $sourceRel = substr(realpath($source), strlen($docRoot) + 1);
                    $destination = $imageRoot . '/doc-root/' . $sourceRel;
                    $destination = self::appendOrSetExtension($destination, $destinationFolder, $destinationExt, false);


                    // TODO: make this check work with symlinks
                    //$destination = SanityCheck::absPathIsInDocRoot($destination);
                } else {
                    $destination = '';

                    $sourceResolved = realpath($source);


                    // Check roots until we (hopefully) get a match.
                    // (that is: find a root which the source is inside)
                    foreach ($imageRoots->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 . '<br>' . $closestExistingResolved . '<br>' . $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', <<<APACHE
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
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
            // -------------------------------------------------------
            $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);
            }
        }
    }
}