Malin 37cf714058 WebP Express CloudHost.es Fix v0.25.9-cloudhost
 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>
2025-09-23 10:22:32 +02:00

350 lines
14 KiB
PHP

<?php
namespace WebPExpress;
use \WebPConvert\Convert\Converters\Ewww;
use \WebPExpress\BiggerThanSourceDummyFiles;
use \WebPExpress\ConvertHelperIndependent;
use \WebPExpress\Config;
use \WebPExpress\ConvertersHelper;
use \WebPExpress\DestinationOptions;
use \WebPExpress\EwwwTools;
use \WebPExpress\ImageRoots;
use \WebPExpress\PathHelper;
use \WebPExpress\Paths;
use \WebPExpress\SanityCheck;
use \WebPExpress\SanityException;
use \WebPExpress\Validate;
use \WebPExpress\ValidateException;
class Convert
{
public static function getDestination($source, &$config = null)
{
if (is_null($config)) {
$config = Config::loadConfigAndFix();
}
return ConvertHelperIndependent::getDestination(
$source,
$config['destination-folder'],
$config['destination-extension'],
Paths::getWebPExpressContentDirAbs(),
Paths::getUploadDirAbs(),
(($config['destination-structure'] == 'doc-root') && (Paths::canUseDocRootForStructuringCacheDir())),
new ImageRoots(Paths::getImageRootsDef())
);
}
public static function updateBiggerThanOriginalMark($source, $destination = null, &$config = null)
{
if (is_null($config)) {
$config = Config::loadConfigAndFix();
}
if (is_null($destination)) {
$destination = self::getDestination($config);
}
BiggerThanSourceDummyFiles::updateStatus(
$source,
$destination,
Paths::getWebPExpressContentDirAbs(),
new ImageRoots(Paths::getImageRootsDef()),
$config['destination-folder'],
$config['destination-extension']
);
}
public static function convertFile($source, $config = null, $convertOptions = null, $converter = null)
{
try {
// Check source
// ---------------
$checking = 'source path';
// 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::absPathExistsAndIsFile($source);
//$filename = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
// PS: No need to check mime type as the WebPConvert library does that (it only accepts image/jpeg and image/png)
// Check that source is within a valid image root
$activeRootIds = Paths::getImageRootIds(); // Currently, root ids cannot be selected, so all root ids are active.
$rootId = Paths::findImageRootOfPath($source, $activeRootIds);
if ($rootId === false) {
throw new \Exception('Path of source is not within a valid image root');
}
// Check config
// --------------
$checking = 'configuration file';
if (is_null($config)) {
$config = Config::loadConfigAndFix(); // ps: if this fails to load, default config is returned.
}
if (!is_array($config)) {
throw new SanityException('configuration file is corrupt');
}
// Check convert options
// -------------------------------
$checking = 'configuration file (options)';
if (is_null($convertOptions)) {
$wodOptions = Config::generateWodOptionsFromConfigObj($config);
if (!isset($wodOptions['webp-convert']['convert'])) {
throw new SanityException('conversion options are missing');
}
$convertOptions = $wodOptions['webp-convert']['convert'];
}
if (!is_array($convertOptions)) {
throw new SanityException('conversion options are missing');
}
// Check destination
// -------------------------------
$checking = 'destination';
$destination = self::getDestination($source, $config);
$destination = SanityCheck::absPath($destination);
// Check log dir
// -------------------------------
$checking = 'conversion log dir';
if (isset($config['enable-logging']) && $config['enable-logging']) {
$logDir = SanityCheck::absPath(Paths::getWebPExpressContentDirAbs() . '/log');
} else {
$logDir = null;
}
} catch (\Exception $e) {
return [
'success' => false,
'msg' => 'Check failed for ' . $checking . ': '. $e->getMessage(),
'log' => '',
];
}
// Done with sanitizing, lets get to work!
// ---------------------------------------
//return false;
$result = ConvertHelperIndependent::convert($source, $destination, $convertOptions, $logDir, $converter);
//error_log('looki:' . $source . $converter);
// If we are using stack converter, check if Ewww discovered invalid api key
//if (is_null($converter)) {
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,
Paths::getConfigDirAbs()
);
}
//}
self::updateBiggerThanOriginalMark($source, $destination, $config);
if ($result['success'] === true) {
$result['filesize-original'] = @filesize($source);
$result['filesize-webp'] = @filesize($destination);
$result['destination-path'] = $destination;
$destinationOptions = DestinationOptions::createFromConfig($config);
$rootOfDestination = Paths::destinationRoot($rootId, $destinationOptions);
$relPathFromImageRootToSource = PathHelper::getRelDir(
realpath(Paths::getAbsDirById($rootId)),
realpath($source)
);
$relPathFromImageRootToDest = ConvertHelperIndependent::appendOrSetExtension(
$relPathFromImageRootToSource,
$config['destination-folder'],
$config['destination-extension'],
($rootId == 'uploads')
);
$result['destination-url'] = $rootOfDestination['url'] . '/' . $relPathFromImageRootToDest;
}
return $result;
}
/**
* Determine the location of a source from the location of a destination.
*
* If for example Operation mode is set to "mingled" and extension is set to "Append .webp",
* the result of looking passing "/path/to/logo.jpg.webp" will be "/path/to/logo.jpg".
*
* Additionally, it is tested if the source exists. If not, false is returned.
* The destination does not have to exist.
*
* @return string|null The source path corresponding to a destination path
* - or false on failure (if the source does not exist or $destination is not sane)
*
*/
public static function findSource($destination, &$config = null)
{
try {
// Check that destination path is sane and inside document root
$destination = SanityCheck::absPathIsInDocRoot($destination);
} catch (SanityException $e) {
return false;
}
// Load config if not already loaded
if (is_null($config)) {
$config = Config::loadConfigAndFix();
}
return ConvertHelperIndependent::findSource(
$destination,
$config['destination-folder'],
$config['destination-extension'],
$config['destination-structure'],
Paths::getWebPExpressContentDirAbs(),
new ImageRoots(Paths::getImageRootsDef())
);
}
public static function processAjaxConvertFile()
{
if (!check_ajax_referer('webpexpress-ajax-convert-nonce', 'nonce', false)) {
//if (true) {
//wp_send_json_error('The security nonce has expired. You need to reload the settings page (press F5) and try again)');
//wp_die();
$result = [
'success' => false,
'msg' => 'The security nonce has expired. You need to reload the settings page (press F5) and try again)',
'stop' => true
];
echo json_encode($result, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
wp_die();
}
// Check input
// --------------
try {
// Check "filename"
$checking = '"filename" argument';
Validate::postHasKey('filename');
$filename = sanitize_text_field(stripslashes($_POST['filename']));
// holy moly! Wordpress automatically adds slashes to the global POST vars - https://stackoverflow.com/questions/2496455/why-are-post-variables-getting-escaped-in-php
$filename = wp_unslash($_POST['filename']);
//$filename = SanityCheck::absPathExistsAndIsFileInDocRoot($filename);
// PS: No need to check mime version as webp-convert does that.
// Check converter id
// ---------------------
$checking = '"converter" argument';
if (isset($_POST['converter'])) {
$converterId = sanitize_text_field($_POST['converter']);
Validate::isConverterId($converterId);
}
// Check "config-overrides"
// ---------------------------
$checking = '"config-overrides" argument';
if (isset($_POST['config-overrides'])) {
$configOverridesJSON = SanityCheck::noControlChars($_POST['config-overrides']);
$configOverridesJSON = preg_replace('/\\\\"/', '"', $configOverridesJSON); // We got crazy encoding, perhaps by jQuery. This cleans it up
$configOverridesJSON = SanityCheck::isJSONObject($configOverridesJSON);
$configOverrides = json_decode($configOverridesJSON, true);
// PS: We do not need to validate the overrides.
// webp-convert checks all options. Nothing can be passed to webp-convert which causes harm.
}
} catch (SanityException $e) {
wp_send_json_error('Sanitation check failed for ' . $checking . ': '. $e->getMessage());
wp_die();
} catch (ValidateException $e) {
wp_send_json_error('Validation failed for ' . $checking . ': '. $e->getMessage());
wp_die();
}
// Input has been processed, now lets get to work!
// -----------------------------------------------
if (isset($configOverrides)) {
$config = Config::loadConfigAndFix();
// convert using specific converter
if (!is_null($converterId)) {
// Merge in the config-overrides (config-overrides only have effect when using a specific converter)
$config = array_merge($config, $configOverrides);
$converter = ConvertersHelper::getConverterById($config, $converterId);
if ($converter === false) {
wp_send_json_error('Converter could not be loaded');
wp_die();
}
// the converter options stored in config.json is not precisely the same as the ones
// we send to webp-convert.
// We need to "regenerate" webp-convert options in order to use the ones specified in the config-overrides
// And we need to merge the general options (such as quality etc) into the option for the specific converter
$generalWebpConvertOptions = Config::generateWodOptionsFromConfigObj($config)['webp-convert']['convert'];
$converterSpecificWebpConvertOptions = isset($converter['options']) ? $converter['options'] : [];
$webpConvertOptions = array_merge($generalWebpConvertOptions, $converterSpecificWebpConvertOptions);
unset($webpConvertOptions['converters']);
// what is this? - I forgot why!
//$config = array_merge($config, $converter['options']);
$result = self::convertFile($filename, $config, $webpConvertOptions, $converterId);
} else {
$result = self::convertFile($filename, $config);
}
} else {
$result = self::convertFile($filename);
}
$nonceTick = wp_verify_nonce($_REQUEST['nonce'], 'webpexpress-ajax-convert-nonce');
if ($nonceTick == 2) {
$result['new-convert-nonce'] = wp_create_nonce('webpexpress-ajax-convert-nonce');
// wp_create_nonce('webpexpress-ajax-convert-nonce')
}
$result['nonce-tick'] = $nonceTick;
$result = self::utf8ize($result);
echo json_encode($result, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
wp_die();
}
private static function utf8ize($d) {
if (is_array($d)) {
foreach ($d as $k => $v) {
$d[$k] = self::utf8ize($v);
}
} else if (is_string ($d)) {
return utf8_encode($d);
}
return $d;
}
}