Files
2026-04-13 15:50:16 +02:00

335 lines
14 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 getPassedHash() {
// First check if it is passed in as an environment variable
$hash = self::getEnvPassedInRewriteRule('HASH');
if ($hash !== false) {
return $hash;
}
// Then check if it is passed in the query string
if (isset($_GET['hash'])) {
return $_GET['hash'];
}
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']);
}
/**
* Find the hash from config.[hash].json inside a directory.
*
* @param string $configDir Absolute path to the config directory (no trailing slash)
*/
protected static function findConfigFileByInspection($configDir)
{
if (!is_dir($configDir)) {
throw new \Exception(
'WebP Express configuration directory was not found. ' .
'Please check that wp-content/webp-express/config exists and is readable.'
);
}
if (!is_readable($configDir)) {
throw new \Exception('WebP Express configuration directory is not readable. Please fix the permissions (wp-content/webp-express/config)');
}
$files = glob($configDir . '/wod-options.*.json');
if ($files === false || empty($files)) {
// Failed finding the pattern. Lets see if old plain "wod-options.json" is there due to migration not complete
$oldFileName = $configDir . '/wod-options.json';
if (file_exists($oldFileName)) {
return $oldFileName;
}
throw new \Exception('Could not find a file that matches the pattern "wod-options.[hash].json" in the config dir. Is it there? Please check manually in this folder: wp-content/webp-express/config');
}
// We expect only one match. But failed migrations / manual restores could perhaps cause there to be several
// So therefore, lets make sure to pick the newest file, in case there are several
usort($files, function ($a, $b) {
return filemtime($b) <=> filemtime($a);
});
$configFilename = $files[0];
if (!file_exists($configFilename)) {
throw new \Exception('Configuration file found in filesystem, but at the same, it seems not to exist!');
}
return $configFilename;
// Take the first match
//$filename = basename($files[0]);
// Extract hash between "config." and ".json"
/*
if (preg_match('/^wod-options\.([a-f0-9]+)\.json$/i', $filename, $matches)) {
return $matches[1];
}*/
// throw new \Exception('The filename Could not find a file that matches the pattern "wod-options.[hash].json" in the config dir. Is it there? Please check manually in this folder: wp-content/webp-express/config');
}
protected static function loadConfig() {
self::$checking = 'config folder';
$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';
$hash = self::getPassedHash();
if ($hash === false) {
self::$checking = 'finding config file by introspecting config dir';
$configFilename = self::findConfigFileByInspection(self::$webExpressContentDirAbs . '/config');
} else {
$hash = SanityCheck::noDirectoryTraversal($hash);
if (!preg_match('/^[a-f0-9]{32}$/i', $hash)) {
//throw new \Exception('Provided hash does not match correct pattern. Hash is expected to be 32 chars of letters/digits. Check the .htaccess files, its where the hash comes from (unless you are using Nginx - then check your Nginx rewrite rules)');
self::$checking = 'finding config file by introspecting config dir (as no hash passed in did not conform to expected pattern, which is 32 chars of letters/digits)';
$configFilename = self::findConfigFileByInspection(self::$webExpressContentDirAbs . '/config');
}
else {
$filename = 'wod-options.' . $hash . '.json';
$configFilename = self::$webExpressContentDirAbs . '/config/' . $filename;
if (!file_exists($configFilename)) {
self::$checking = 'finding config file by introspecting config dir (as no config file was found using hash passed in)';
$configFilename = self::findConfigFileByInspection(self::$webExpressContentDirAbs . '/config');
}
}
}
// Check config file
// --------------------
$configLoadResult = file_get_contents($configFilename);
if ($configLoadResult === false) {
throw new \Exception('Cannot open config file: ' . $configFilename);
}
$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'
);
}
}
}