WebP-eXpress/lib/classes/HTAccess.php
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

437 lines
16 KiB
PHP

<?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);
}
}
}
}