✅ Fixed bulk conversion getting stuck on missing files ✅ Added robust error handling and timeout protection ✅ Improved JavaScript response parsing ✅ Added file existence validation ✅ Fixed missing PHP class imports ✅ Added comprehensive try-catch error recovery 🔧 Key fixes: - File existence checks before conversion attempts - 30-second timeout protection per file - Graceful handling of 500 errors and JSON parsing issues - Automatic continuation to next file on failures - Cache busting for JavaScript updates 🎯 Result: Bulk conversion now completes successfully even with missing files 🚀 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
253 lines
10 KiB
PHP
253 lines
10 KiB
PHP
<?php
|
|
/*
|
|
This class is used by wod/webp-on-demand.php, which does not do a Wordpress bootstrap, but does register an autoloader for
|
|
the WebPExpress classes.
|
|
|
|
Calling Wordpress functions will FAIL. Make sure not to do that in either this class or the helpers.
|
|
*/
|
|
//error_reporting(E_ALL);
|
|
//ini_set('display_errors', 1);
|
|
|
|
namespace WebPExpress;
|
|
|
|
use \WebPConvert\Convert\Converters\Ewww;
|
|
|
|
use \WebPExpress\ImageRoots;
|
|
use \WebPExpress\Sanitize;
|
|
use \WebPExpress\SanityCheck;
|
|
use \WebPExpress\SanityException;
|
|
use \WebPExpress\ValidateException;
|
|
use \WebPExpress\Validate;
|
|
|
|
class WodConfigLoader
|
|
{
|
|
|
|
protected static $docRoot;
|
|
protected static $checking;
|
|
protected static $wodOptions;
|
|
protected static $options;
|
|
protected static $usingDocRoot;
|
|
protected static $webExpressContentDirAbs;
|
|
|
|
public static function exitWithError($msg) {
|
|
header('X-WebP-Express-Error: ' . $msg, true);
|
|
echo $msg;
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Check if Apache handles the PHP requests (Note that duel setups are possible and ie Nginx could be handling the image requests).
|
|
*/
|
|
public static function isApache()
|
|
{
|
|
return (stripos($_SERVER['SERVER_SOFTWARE'], 'apache') !== false);
|
|
}
|
|
|
|
protected static function isNginxHandlingImages()
|
|
{
|
|
if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false) {
|
|
return true;
|
|
}
|
|
|
|
// On WP Engine, SERVER_SOFTWARE is "Apache", but images are handled by NGINX.
|
|
if (isset($_SERVER['WPENGINE_ACCOUNT'])) {
|
|
return true;
|
|
};
|
|
return false;
|
|
}
|
|
|
|
public static function preventDirectAccess($filename)
|
|
{
|
|
// Protect against directly accessing webp-on-demand.php
|
|
// Only protect on Apache. We know for sure that the method is not reliable on nginx.
|
|
// We have not tested on litespeed yet, so we dare not.
|
|
if (self::isApache() && (!self::isNginxHandlingImages())) {
|
|
if (strpos($_SERVER['REQUEST_URI'], $filename) !== false) {
|
|
self::exitWithError(
|
|
'It seems you are visiting this file (plugins/webp-express/wod/' . $filename . ') directly. We do not allow this.'
|
|
);
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get environment variable set with mod_rewrite module
|
|
* Return false if the environment variable isn't found
|
|
*/
|
|
protected static function getEnvPassedInRewriteRule($envName) {
|
|
// Envirenment variables passed through the REWRITE module have "REWRITE_" as a prefix (in Apache, not Litespeed, if I recall correctly)
|
|
// Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
|
|
// Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
|
|
// We simply look for an environment variable that ends with what we are looking for.
|
|
// (so make sure to make it unique)
|
|
$len = strlen($envName);
|
|
foreach ($_SERVER as $key => $item) {
|
|
if (substr($key, -$len) == $envName) {
|
|
return $item;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected static function getWebPExpressContentDirWithDocRoot()
|
|
{
|
|
// Get relative path to wp-content
|
|
// --------------------------------
|
|
self::$checking = 'Relative path to wp-content dir';
|
|
|
|
// Passed in env variable?
|
|
$wpContentDirRel = self::getEnvPassedInRewriteRule('WPCONTENT');
|
|
if ($wpContentDirRel === false) {
|
|
// Passed in QS?
|
|
if (isset($_GET['wp-content'])) {
|
|
$wpContentDirRel = SanityCheck::pathWithoutDirectoryTraversal($_GET['wp-content']);
|
|
} else {
|
|
// In case above fails, fall back to standard location
|
|
$wpContentDirRel = 'wp-content';
|
|
}
|
|
}
|
|
|
|
// Check WebP Express content dir
|
|
// ---------------------------------
|
|
self::$checking = 'WebP Express content dir';
|
|
|
|
$webExpressContentDirAbs = SanityCheck::absPathExistsAndIsDir(self::$docRoot . '/' . $wpContentDirRel . '/webp-express');
|
|
return $webExpressContentDirAbs;
|
|
}
|
|
|
|
protected static function getWebPExpressContentDirNoDocRoot() {
|
|
// Check wp-content
|
|
// ----------------------
|
|
self::$checking = 'relative path between webp-express plugin dir and wp-content dir';
|
|
|
|
// From v0.22.0, we pass relative to webp-express dir rather than to the general plugin dir.
|
|
// - this allows symlinking the webp-express dir.
|
|
$wpContentDirRelToWEPluginDir = self::getEnvPassedInRewriteRule('WE_WP_CONTENT_REL_TO_WE_PLUGIN_DIR');
|
|
if (!$wpContentDirRelToWEPluginDir) {
|
|
// Passed in QS?
|
|
if (isset($_GET['xwp-content-rel-to-we-plugin-dir'])) {
|
|
$xwpContentDirRelToWEPluginDir = SanityCheck::noControlChars($_GET['xwp-content-rel-to-we-plugin-dir']);
|
|
$wpContentDirRelToWEPluginDir = SanityCheck::pathDirectoryTraversalAllowed(substr($xwpContentDirRelToWEPluginDir, 1));
|
|
}
|
|
}
|
|
|
|
// Old .htaccess rules from before 0.22.0 passed relative path to general plugin dir.
|
|
// these rules must still be supported, which is what we do here:
|
|
if (!$wpContentDirRelToWEPluginDir) {
|
|
self::$checking = 'relative path between plugin dir and wp-content dir';
|
|
|
|
$wpContentDirRelToPluginDir = self::getEnvPassedInRewriteRule('WE_WP_CONTENT_REL_TO_PLUGIN_DIR');
|
|
if ($wpContentDirRelToPluginDir === false) {
|
|
// Passed in QS?
|
|
if (isset($_GET['xwp-content-rel-to-plugin-dir'])) {
|
|
$xwpContentDirRelToPluginDir = SanityCheck::noControlChars($_GET['xwp-content-rel-to-plugin-dir']);
|
|
$wpContentDirRelToPluginDir = SanityCheck::pathDirectoryTraversalAllowed(substr($xwpContentDirRelToPluginDir, 1));
|
|
|
|
} else {
|
|
throw new \Exception('Path to wp-content was not received in any way');
|
|
}
|
|
}
|
|
$wpContentDirRelToWEPluginDir = $wpContentDirRelToPluginDir . '..';
|
|
}
|
|
|
|
|
|
// Check WebP Express content dir
|
|
// ---------------------------------
|
|
self::$checking = 'WebP Express content dir';
|
|
|
|
$pathToWEPluginDir = dirname(dirname(__DIR__));
|
|
$webExpressContentDirAbs = SanityCheck::pathDirectoryTraversalAllowed($pathToWEPluginDir . '/' . $wpContentDirRelToWEPluginDir . '/webp-express');
|
|
|
|
//$pathToPluginDir = dirname(dirname(dirname(__DIR__)));
|
|
//$webExpressContentDirAbs = SanityCheck::pathDirectoryTraversalAllowed($pathToPluginDir . '/' . $wpContentDirRelToPluginDir . '/webp-express');
|
|
//echo $webExpressContentDirAbs; exit;
|
|
if (@!file_exists($webExpressContentDirAbs)) {
|
|
throw new \Exception('Dir not found');
|
|
}
|
|
$webExpressContentDirAbs = @realpath($webExpressContentDirAbs);
|
|
if ($webExpressContentDirAbs === false) {
|
|
throw new \Exception('WebP Express content dir is outside restricted open_basedir!');
|
|
}
|
|
return $webExpressContentDirAbs;
|
|
}
|
|
|
|
protected static function getImageRootsDef()
|
|
{
|
|
if (!isset(self::$wodOptions['image-roots'])) {
|
|
throw new \Exception('No image roots defined in config.');
|
|
}
|
|
return new ImageRoots(self::$wodOptions['image-roots']);
|
|
}
|
|
|
|
protected static function loadConfig() {
|
|
|
|
$usingDocRoot = !(
|
|
isset($_GET['xwp-content-rel-to-we-plugin-dir']) ||
|
|
self::getEnvPassedInRewriteRule('WE_WP_CONTENT_REL_TO_WE_PLUGIN_DIR') ||
|
|
isset($_GET['xwp-content-rel-to-plugin-dir']) ||
|
|
self::getEnvPassedInRewriteRule('WE_WP_CONTENT_REL_TO_PLUGIN_DIR')
|
|
);
|
|
self::$usingDocRoot = $usingDocRoot;
|
|
|
|
if ($usingDocRoot) {
|
|
// Check DOCUMENT_ROOT
|
|
// ----------------------
|
|
self::$checking = 'DOCUMENT_ROOT';
|
|
$docRootAvailable = PathHelper::isDocRootAvailableAndResolvable();
|
|
if (!$docRootAvailable) {
|
|
throw new \Exception(
|
|
'Document root is no longer available. It was available when the .htaccess rules was created and ' .
|
|
'the rules are based on that. You need to regenerate the rules (or fix your document root configuration)'
|
|
);
|
|
}
|
|
|
|
$docRoot = SanityCheck::absPath($_SERVER["DOCUMENT_ROOT"]);
|
|
$docRoot = rtrim($docRoot, '/');
|
|
self::$docRoot = $docRoot;
|
|
}
|
|
|
|
if ($usingDocRoot) {
|
|
self::$webExpressContentDirAbs = self::getWebPExpressContentDirWithDocRoot();
|
|
} else {
|
|
self::$webExpressContentDirAbs = self::getWebPExpressContentDirNoDocRoot();
|
|
}
|
|
|
|
// Check config file name
|
|
// ---------------------------------
|
|
self::$checking = 'config file';
|
|
|
|
$configFilename = self::$webExpressContentDirAbs . '/config/wod-options.json';
|
|
if (!file_exists($configFilename)) {
|
|
throw new \Exception('Configuration file was not found (wod-options.json)');
|
|
}
|
|
|
|
// Check config file
|
|
// --------------------
|
|
$configLoadResult = file_get_contents($configFilename);
|
|
if ($configLoadResult === false) {
|
|
throw new \Exception('Cannot open config file');
|
|
}
|
|
$json = SanityCheck::isJSONObject($configLoadResult);
|
|
|
|
self::$options = json_decode($json, true);
|
|
self::$wodOptions = self::$options['wod'];
|
|
}
|
|
|
|
/**
|
|
* Must be called after conversion.
|
|
*/
|
|
protected static function fixConfigIfEwwwDiscoveredNonFunctionalApiKeys()
|
|
{
|
|
if (isset(Ewww::$nonFunctionalApiKeysDiscoveredDuringConversion)) {
|
|
// We got an invalid or exceeded api key (at least one).
|
|
//error_log('look:' . print_r(Ewww::$nonFunctionalApiKeysDiscoveredDuringConversion, true));
|
|
EwwwTools::markApiKeysAsNonFunctional(
|
|
Ewww::$nonFunctionalApiKeysDiscoveredDuringConversion,
|
|
self::$webExpressContentDirAbs . '/config'
|
|
);
|
|
}
|
|
|
|
}
|
|
}
|