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>
This commit is contained in:
436
lib/classes/HTAccess.php
Normal file
436
lib/classes/HTAccess.php
Normal file
@@ -0,0 +1,436 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Config;
|
||||
use \WebPExpress\FileHelper;
|
||||
use \WebPExpress\HTAccessRules;
|
||||
use \WebPExpress\Paths;
|
||||
use \WebPExpress\State;
|
||||
|
||||
class HTAccess
|
||||
{
|
||||
|
||||
public static function inlineInstructions($instructions, $marker)
|
||||
{
|
||||
if ($marker == 'WebP Express') {
|
||||
return [];
|
||||
} else {
|
||||
return $instructions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be parsed ie "wp-content", "index", etc. Not real dirs
|
||||
*/
|
||||
public static function addToActiveHTAccessDirsArray($dirId)
|
||||
{
|
||||
$activeHtaccessDirs = State::getState('active-htaccess-dirs', []);
|
||||
if (!in_array($dirId, $activeHtaccessDirs)) {
|
||||
$activeHtaccessDirs[] = $dirId;
|
||||
State::setState('active-htaccess-dirs', array_values($activeHtaccessDirs));
|
||||
}
|
||||
}
|
||||
|
||||
public static function removeFromActiveHTAccessDirsArray($dirId)
|
||||
{
|
||||
$activeHtaccessDirs = State::getState('active-htaccess-dirs', []);
|
||||
if (in_array($dirId, $activeHtaccessDirs)) {
|
||||
$activeHtaccessDirs = array_diff($activeHtaccessDirs, [$dirId]);
|
||||
State::setState('active-htaccess-dirs', array_values($activeHtaccessDirs));
|
||||
}
|
||||
}
|
||||
|
||||
public static function isInActiveHTAccessDirsArray($dirId)
|
||||
{
|
||||
$activeHtaccessDirs = State::getState('active-htaccess-dirs', []);
|
||||
return (in_array($dirId, $activeHtaccessDirs));
|
||||
}
|
||||
|
||||
public static function hasRecordOfSavingHTAccessToDir($dir) {
|
||||
$dirId = Paths::getAbsDirId($dir);
|
||||
if ($dirId !== false) {
|
||||
return self::isInActiveHTAccessDirsArray($dirId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|false Rules, or false if no rules found or file does not exist.
|
||||
*/
|
||||
public static function extractWebPExpressRulesFromHTAccess($filename) {
|
||||
if (FileHelper::fileExists($filename)) {
|
||||
$content = FileHelper::loadFile($filename);
|
||||
if ($content === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pos1 = strpos($content, '# BEGIN WebP Express');
|
||||
if ($pos1 === false) {
|
||||
return false;
|
||||
}
|
||||
$pos2 = strrpos($content, '# END WebP Express');
|
||||
if ($pos2 === false) {
|
||||
return false;
|
||||
}
|
||||
return substr($content, $pos1, $pos2 - $pos1);
|
||||
} else {
|
||||
// the .htaccess isn't even there. So there are no rules.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sneak peak into .htaccess to see if we have rules in it
|
||||
* This may not be possible (it requires read permission)
|
||||
* Return true, false, or null if we just can't tell
|
||||
*/
|
||||
public static function haveWeRulesInThisHTAccess($filename) {
|
||||
if (FileHelper::fileExists($filename)) {
|
||||
$content = FileHelper::loadFile($filename);
|
||||
if ($content === false) {
|
||||
return null;
|
||||
}
|
||||
$weRules = (self::extractWebPExpressRulesFromHTAccess($filename));
|
||||
if ($weRules === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (strpos($weRules, '<IfModule ') !== false);
|
||||
} else {
|
||||
// the .htaccess isn't even there. So there are no rules.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function haveWeRulesInThisHTAccessBestGuess($filename)
|
||||
{
|
||||
// First try to sneak peak. May return null if it cannot be determined.
|
||||
$result = self::haveWeRulesInThisHTAccess($filename);
|
||||
if ($result === true) {
|
||||
return true;
|
||||
}
|
||||
if ($result === null) {
|
||||
// We were not allowed to sneak-peak.
|
||||
// Well, good thing that we stored successful .htaccess write locations ;)
|
||||
// If we recorded a successful write, then we assume there are still rules there
|
||||
// If we did not, we assume there are no rules there
|
||||
$dir = FileHelper::dirName($filename);
|
||||
return self::hasRecordOfSavingHTAccessToDir($dir);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getRootsWithWebPExpressRulesIn()
|
||||
{
|
||||
$allIds = Paths::getImageRootIds();
|
||||
$allIds[] = 'cache';
|
||||
$result = [];
|
||||
foreach ($allIds as $imageRootId) {
|
||||
$filename = Paths::getAbsDirById($imageRootId) . '/.htaccess';
|
||||
if (self::haveWeRulesInThisHTAccessBestGuess($filename)) {
|
||||
$result[] = $imageRootId;
|
||||
}
|
||||
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function saveHTAccessRulesToFile($filename, $rules, $createIfMissing = false) {
|
||||
if (!@file_exists($filename)) {
|
||||
if (!$createIfMissing) {
|
||||
return false;
|
||||
}
|
||||
// insert_with_markers will create file if it doesn't exist, so we can continue...
|
||||
}
|
||||
|
||||
$existingFilePermission = null;
|
||||
$existingDirPermission = null;
|
||||
|
||||
// Try to make .htaccess writable if its not
|
||||
if (@file_exists($filename)) {
|
||||
if (!@is_writable($filename)) {
|
||||
$existingFilePermission = FileHelper::filePerm($filename);
|
||||
@chmod($filename, 0664); // chmod may fail, we know...
|
||||
}
|
||||
} else {
|
||||
$dir = FileHelper::dirName($filename);
|
||||
if (!@is_writable($dir)) {
|
||||
$existingDirPermission = FileHelper::filePerm($dir);
|
||||
@chmod($dir, 0775);
|
||||
}
|
||||
}
|
||||
|
||||
/* Add rules to .htaccess */
|
||||
if (!function_exists('insert_with_markers')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/misc.php';
|
||||
}
|
||||
|
||||
// Convert to array, because string version has bugs in Wordpress 4.3
|
||||
$rules = explode("\n", $rules);
|
||||
|
||||
add_filter('insert_with_markers_inline_instructions', array('\WebPExpress\HTAccess', 'inlineInstructions'), 10, 2);
|
||||
|
||||
$success = insert_with_markers($filename, 'WebP Express', $rules);
|
||||
|
||||
// Revert file or dir permissions
|
||||
if (!is_null($existingFilePermission)) {
|
||||
@chmod($filename, $existingFilePermission);
|
||||
}
|
||||
if (!is_null($existingDirPermission)) {
|
||||
@chmod($dir, $existingDirPermission);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
State::setState('htaccess-rules-saved-at-some-point', true);
|
||||
|
||||
//$containsRules = (strpos(implode('',$rules), '# Redirect images to webp-on-demand.php') != false);
|
||||
$containsRules = (strpos(implode('',$rules), '<IfModule mod_rewrite.c>') !== false);
|
||||
|
||||
$dir = FileHelper::dirName($filename);
|
||||
$dirId = Paths::getAbsDirId($dir);
|
||||
if ($dirId !== false) {
|
||||
if ($containsRules) {
|
||||
self::addToActiveHTAccessDirsArray($dirId);
|
||||
} else {
|
||||
self::removeFromActiveHTAccessDirsArray($dirId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
public static function saveHTAccessRules($rootId, $rules, $createIfMissing = true) {
|
||||
$filename = Paths::getAbsDirById($rootId) . '/.htaccess';
|
||||
return self::saveHTAccessRulesToFile($filename, $rules, $createIfMissing);
|
||||
}
|
||||
|
||||
/* only called in this file */
|
||||
public static function saveHTAccessRulesToFirstWritableHTAccessDir($dirs, $rules)
|
||||
{
|
||||
foreach ($dirs as $dir) {
|
||||
if (self::saveHTAccessRulesToFile($dir . '/.htaccess', $rules, true)) {
|
||||
return $dir;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Try to deactivate all .htaccess rules.
|
||||
* If success, we return true.
|
||||
* If we fail, we return an array of filenames that have problems
|
||||
* @return true|array
|
||||
*/
|
||||
public static function deactivateHTAccessRules($comment = '# Plugin is deactivated') {
|
||||
|
||||
$rootsToClean = Paths::getImageRootIds();
|
||||
$rootsToClean[] = 'home';
|
||||
$rootsToClean[] = 'cache';
|
||||
$failures = [];
|
||||
$successes = [];
|
||||
|
||||
foreach ($rootsToClean as $imageRootId) {
|
||||
$dir = Paths::getAbsDirById($imageRootId);
|
||||
$filename = $dir . '/.htaccess';
|
||||
if (!FileHelper::fileExists($filename)) {
|
||||
//error_log('exists not:' . $filename);
|
||||
continue;
|
||||
} else {
|
||||
if (self::haveWeRulesInThisHTAccessBestGuess($filename)) {
|
||||
if (self::saveHTAccessRulesToFile($filename, $comment, false)) {
|
||||
$successes[] = $imageRootId;
|
||||
} else {
|
||||
$failures[] = $imageRootId;
|
||||
}
|
||||
} else {
|
||||
//error_log('no rules:' . $filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
$success = (count($failures) == 0);
|
||||
return [$success, $failures, $successes];
|
||||
}
|
||||
|
||||
public static function testLinks($config) {
|
||||
/*
|
||||
if (isset($_SERVER['HTTP_ACCEPT']) && (strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') !== false )) {
|
||||
if ($config['operation-mode'] != 'no-conversion') {
|
||||
if ($config['image-types'] != 0) {
|
||||
$webpExpressRoot = Paths::getWebPExpressPluginUrlPath();
|
||||
$links = '';
|
||||
if ($config['enable-redirection-to-converter']) {
|
||||
$links = '<br>';
|
||||
$links .= '<a href="/' . $webpExpressRoot . '/test/test.jpg?debug&time=' . time() . '" target="_blank">Convert test image (show debug)</a><br>';
|
||||
$links .= '<a href="/' . $webpExpressRoot . '/test/test.jpg?' . time() . '" target="_blank">Convert test image</a><br>';
|
||||
}
|
||||
// TODO: webp-realizer test links (to missing webp)
|
||||
if ($config['enable-redirection-to-webp-realizer']) {
|
||||
}
|
||||
|
||||
// TODO: test link for testing redirection to existing
|
||||
if ($config['redirect-to-existing-in-htaccess']) {
|
||||
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
public static function getHTAccessDirRequirements() {
|
||||
$minRequired = 'index';
|
||||
if (Paths::isWPContentDirMovedOutOfAbsPath()) {
|
||||
$minRequired = 'wp-content';
|
||||
$pluginToo = Paths::isPluginDirMovedOutOfWpContent() ? 'yes' : 'no';
|
||||
$uploadToo = Paths::isUploadDirMovedOutOfWPContentDir() ? 'yes' : 'no';
|
||||
} else {
|
||||
// plugin requirement depends...
|
||||
// - if user grants access to 'index', the requirement is Paths::isPluginDirMovedOutOfAbsPath()
|
||||
// - if user grants access to 'wp-content', the requirement is Paths::isPluginDirMovedOutOfWpContent()
|
||||
$pluginToo = 'depends';
|
||||
|
||||
// plugin requirement depends...
|
||||
// - if user grants access to 'index', we should be fine, as UPLOADS is always in ABSPATH.
|
||||
// - if user grants access to 'wp-content', the requirement is Paths::isUploadDirMovedOutOfWPContentDir()
|
||||
$uploadToo = 'depends';
|
||||
}
|
||||
|
||||
// We need upload too for rewrite rules when destination structure is image-roots.
|
||||
// but it is also good otherwise. So lets always do it.
|
||||
|
||||
$uploadToo = 'yes';
|
||||
|
||||
return [
|
||||
$minRequired,
|
||||
$pluginToo, // 'yes', 'no' or 'depends'
|
||||
$uploadToo
|
||||
];
|
||||
}
|
||||
|
||||
public static function saveRules($config, $showMessage = true) {
|
||||
list($success, $failedDeactivations, $successfulDeactivations) = self::deactivateHTAccessRules('# The rules have left the building');
|
||||
|
||||
$rootsToPutRewritesIn = $config['scope'];
|
||||
if ($config['destination-structure'] == 'doc-root') {
|
||||
// Commented out to quickfix #338
|
||||
// $rootsToPutRewritesIn = Paths::filterOutSubRoots($rootsToPutRewritesIn);
|
||||
}
|
||||
|
||||
$dirsContainingWebps = [];
|
||||
|
||||
$mingled = ($config['destination-folder'] == 'mingled');
|
||||
if ($mingled) {
|
||||
$dirsContainingWebps[] = 'uploads';
|
||||
}
|
||||
$scopeOtherThanUpload = (str_replace('uploads', '', implode(',', $config['scope'])) != '');
|
||||
|
||||
if ($scopeOtherThanUpload || (!$mingled)) {
|
||||
$dirsContainingWebps[] = 'cache';
|
||||
}
|
||||
|
||||
$dirsToPutRewritesIn = array_unique(array_merge($rootsToPutRewritesIn, $dirsContainingWebps));
|
||||
|
||||
$failedWrites = [];
|
||||
$successfullWrites = [];
|
||||
foreach ($dirsToPutRewritesIn as $rootId) {
|
||||
$dirContainsSourceImages = in_array($rootId, $rootsToPutRewritesIn);
|
||||
$dirContainsWebPImages = in_array($rootId, $dirsContainingWebps);
|
||||
|
||||
$rules = HTAccessRules::generateHTAccessRulesFromConfigObj(
|
||||
$config,
|
||||
$rootId,
|
||||
$dirContainsSourceImages,
|
||||
$dirContainsWebPImages
|
||||
);
|
||||
$success = self::saveHTAccessRules(
|
||||
$rootId,
|
||||
$rules,
|
||||
true
|
||||
);
|
||||
if ($success) {
|
||||
$successfullWrites[] = $rootId;
|
||||
|
||||
// Remove it from $successfulDeactivations (if it is there)
|
||||
if (($key = array_search($rootId, $successfulDeactivations)) !== false) {
|
||||
unset($successfulDeactivations[$key]);
|
||||
}
|
||||
} else {
|
||||
$failedWrites[] = $rootId;
|
||||
|
||||
// Remove it from $failedDeactivations (if it is there)
|
||||
if (($key = array_search($rootId, $failedDeactivations)) !== false) {
|
||||
unset($failedDeactivations[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$success = ((count($failedDeactivations) == 0) && (count($failedWrites) == 0));
|
||||
|
||||
$return = [$success, $successfullWrites, $successfulDeactivations, $failedWrites, $failedDeactivations];
|
||||
if ($showMessage) {
|
||||
self::showSaveRulesMessages($return);
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
public static function showSaveRulesMessages($saveRulesResult)
|
||||
{
|
||||
list($success, $successfullWrites, $successfulDeactivations, $failedWrites, $failedDeactivations) = $saveRulesResult;
|
||||
|
||||
$msg = '';
|
||||
if (count($successfullWrites) > 0) {
|
||||
$msg .= '<p>Rewrite rules were saved to the following files:</p>';
|
||||
foreach ($successfullWrites as $rootId) {
|
||||
$rootIdName = $rootId;
|
||||
if ($rootIdName == 'cache') {
|
||||
$rootIdName = 'webp folder';
|
||||
}
|
||||
$msg .= '<i>' . Paths::getAbsDirById($rootId) . '/.htaccess</i> (' . $rootIdName . ')<br>';
|
||||
}
|
||||
}
|
||||
|
||||
if (count($successfulDeactivations) > 0) {
|
||||
$msg .= '<p>Rewrite rules were removed from the following files:</p>';
|
||||
foreach ($successfulDeactivations as $rootId) {
|
||||
$rootIdName = $rootId;
|
||||
if ($rootIdName == 'cache') {
|
||||
$rootIdName = 'webp folder';
|
||||
}
|
||||
$msg .= '<i>' . Paths::getAbsDirById($rootId) . '/.htaccess</i> (' . $rootIdName . ')<br>';
|
||||
}
|
||||
}
|
||||
|
||||
if ($msg != '') {
|
||||
Messenger::addMessage(
|
||||
($success ? 'success' : 'info'),
|
||||
$msg
|
||||
);
|
||||
}
|
||||
|
||||
if (count($failedWrites) > 0) {
|
||||
$msg = '<p>Failed writing rewrite rules to the following files:</p>';
|
||||
foreach ($failedWrites as $rootId) {
|
||||
$msg .= '<i>' . Paths::getAbsDirById($rootId) . '/.htaccess</i> (' . $rootId . ')<br>';
|
||||
}
|
||||
$msg .= 'You need to change the file permissions to allow WebP Express to save the rules.';
|
||||
Messenger::addMessage('error', $msg);
|
||||
} else {
|
||||
if (count($failedDeactivations) > 0) {
|
||||
$msg = '<p>Failed deleting unused rewrite rules in the following files:</p>';
|
||||
foreach ($failedDeactivations as $rootId) {
|
||||
$msg .= '<i>' . Paths::getAbsDirById($rootId) . '/.htaccess</i> (' . $rootId . ')<br>';
|
||||
}
|
||||
$msg .= 'You need to change the file permissions to allow WebP Express to remove the rules or ' .
|
||||
'remove them manually';
|
||||
Messenger::addMessage('error', $msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user