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:
46
lib/classes/Actions.php
Normal file
46
lib/classes/Actions.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Option;
|
||||
use \WebPExpress\State;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class Actions
|
||||
{
|
||||
/**
|
||||
* $action: identifier
|
||||
*/
|
||||
public static function procastinate($action) {
|
||||
Option::updateOption('webp-express-actions-pending', true, true);
|
||||
|
||||
$pendingActions = State::getState('pendingActions', []);
|
||||
$pendingActions[] = $action;
|
||||
State::setState('pendingActions', $pendingActions);
|
||||
}
|
||||
|
||||
public static function takeAction($action) {
|
||||
switch ($action) {
|
||||
case 'deactivate':
|
||||
add_action('admin_init', function () {
|
||||
deactivate_plugins(plugin_basename(WEBPEXPRESS_PLUGIN));
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static function processQueuedActions() {
|
||||
$actions = State::getState('pendingActions', []);
|
||||
|
||||
foreach ($actions as $action) {
|
||||
self::takeAction($action);
|
||||
}
|
||||
|
||||
State::setState('pendingActions', []);
|
||||
Option::updateOption('webp-express-actions-pending', false, true);
|
||||
|
||||
}
|
||||
}
|
||||
147
lib/classes/AdminInit.php
Normal file
147
lib/classes/AdminInit.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class AdminInit
|
||||
{
|
||||
public static function init() {
|
||||
|
||||
// uncomment next line to debug an error during activation
|
||||
//include __DIR__ . "/../debug.php";
|
||||
|
||||
if (Option::getOption('webp-express-actions-pending')) {
|
||||
\WebPExpress\Actions::processQueuedActions();
|
||||
}
|
||||
|
||||
self::addHooks();
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static function runMigrationIfNeeded()
|
||||
{
|
||||
// When an update requires a migration, the number should be increased
|
||||
define('WEBPEXPRESS_MIGRATION_VERSION', '14');
|
||||
|
||||
if (WEBPEXPRESS_MIGRATION_VERSION != Option::getOption('webp-express-migration-version', 0)) {
|
||||
// run migration logic
|
||||
include WEBPEXPRESS_PLUGIN_DIR . '/lib/migrate/migrate.php';
|
||||
}
|
||||
|
||||
// uncomment next line to test-run a migration
|
||||
//include WEBPEXPRESS_PLUGIN_DIR . '/lib/migrate/migrate14.php';
|
||||
}
|
||||
|
||||
public static function pageNowIs($pageId)
|
||||
{
|
||||
global $pagenow;
|
||||
|
||||
if ((!isset($pagenow)) || (empty($pagenow))) {
|
||||
return false;
|
||||
}
|
||||
return ($pageId == $pagenow);
|
||||
}
|
||||
|
||||
|
||||
public static function addHooksAfterAdminInit()
|
||||
{
|
||||
|
||||
if (current_user_can('manage_options')) {
|
||||
|
||||
// Hooks related to conversion page (in media)
|
||||
//if (self::pageNowIs('upload.php')) {
|
||||
if (isset($_GET['page']) && ('webp_express_conversion_page' === $_GET['page'])) {
|
||||
//add_action('admin_enqueue_scripts', array('\WebPExpress\WCFMPage', 'enqueueScripts'));
|
||||
add_action('admin_head', array('\WebPExpress\WCFMPage', 'addToHead'));
|
||||
}
|
||||
//}
|
||||
|
||||
// Hooks related to options page
|
||||
if (self::pageNowIs('options-general.php') || self::pageNowIs('settings.php')) {
|
||||
if (isset($_GET['page']) && ('webp_express_settings_page' === $_GET['page'])) {
|
||||
add_action('admin_enqueue_scripts', array('\WebPExpress\OptionsPage', 'enqueueScripts'));
|
||||
}
|
||||
}
|
||||
|
||||
// Hooks related to plugins page
|
||||
if (self::pageNowIs('plugins.php')) {
|
||||
add_action('admin_enqueue_scripts', array('\WebPExpress\PluginPageScript', 'enqueueScripts'));
|
||||
}
|
||||
|
||||
add_action("admin_post_webpexpress_settings_submit", array('\WebPExpress\OptionsPageHooks', 'submitHandler'));
|
||||
|
||||
|
||||
// Ajax actions
|
||||
add_action('wp_ajax_list_unconverted_files', array('\WebPExpress\BulkConvert', 'processAjaxListUnconvertedFiles'));
|
||||
add_action('wp_ajax_convert_file', array('\WebPExpress\Convert', 'processAjaxConvertFile'));
|
||||
add_action('wp_ajax_webpexpress_view_log', array('\WebPExpress\ConvertLog', 'processAjaxViewLog'));
|
||||
add_action('wp_ajax_webpexpress_purge_cache', array('\WebPExpress\CachePurge', 'processAjaxPurgeCache'));
|
||||
add_action('wp_ajax_webpexpress_purge_log', array('\WebPExpress\LogPurge', 'processAjaxPurgeLog'));
|
||||
add_action('wp_ajax_webpexpress_dismiss_message', array('\WebPExpress\DismissableMessages', 'processAjaxDismissMessage'));
|
||||
add_action('wp_ajax_webpexpress_dismiss_global_message', array('\WebPExpress\DismissableGlobalMessages', 'processAjaxDismissGlobalMessage'));
|
||||
add_action('wp_ajax_webpexpress_self_test', array('\WebPExpress\SelfTest', 'processAjax'));
|
||||
add_action('wp_ajax_webpexpress-wcfm-api', array('\WebPExpress\WCFMApi', 'processRequest'));
|
||||
|
||||
|
||||
// Add settings link on the plugins list page
|
||||
add_filter('plugin_action_links_' . plugin_basename(WEBPEXPRESS_PLUGIN), array('\WebPExpress\AdminUi', 'pluginActionLinksFilter'), 10, 2);
|
||||
|
||||
// Add settings link in multisite
|
||||
add_filter('network_admin_plugin_action_links_' . plugin_basename(WEBPEXPRESS_PLUGIN), array('\WebPExpress\AdminUi', 'networkPluginActionLinksFilter'), 10, 2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function addHooks()
|
||||
{
|
||||
|
||||
// Plugin activation, deactivation and uninstall
|
||||
register_activation_hook(WEBPEXPRESS_PLUGIN, array('\WebPExpress\PluginActivate', 'activate'));
|
||||
register_deactivation_hook(WEBPEXPRESS_PLUGIN, array('\WebPExpress\PluginDeactivate', 'deactivate'));
|
||||
register_uninstall_hook(WEBPEXPRESS_PLUGIN, array('\WebPExpress\PluginUninstall', 'uninstall'));
|
||||
|
||||
/*$start = microtime(true);
|
||||
BiggerThanSourceDummyFilesBulk::updateStatus(Config::loadConfig());
|
||||
echo microtime(true) - $start;*/
|
||||
|
||||
|
||||
// Some hooks must be registered AFTER admin_init...
|
||||
add_action("admin_init", array('\WebPExpress\AdminInit', 'addHooksAfterAdminInit'));
|
||||
|
||||
// Run migration AFTER admin_init hook (important, as insert_with_markers injection otherwise fails, see #394)
|
||||
// PS: "plugins_loaded" is to early, as insert_with_markers fails.
|
||||
// PS: Unfortunately Message::addMessage doesnt print until next load now, we should look into that.
|
||||
// PPS: It does run. It must be the Option that does not react
|
||||
//add_action("admin_init", array('\WebPExpress\AdminInit', 'runMigrationIfNeeded'));
|
||||
|
||||
add_action("admin_init", array('\WebPExpress\AdminInit', 'runMigrationIfNeeded'));
|
||||
|
||||
add_action("admin_notices", array('\WebPExpress\DismissableGlobalMessages', 'printMessages'));
|
||||
|
||||
if (Multisite::isNetworkActivated()) {
|
||||
if (is_network_admin()) {
|
||||
add_action("network_admin_menu", array('\WebPExpress\AdminUi', 'networAdminMenuHook'));
|
||||
} else {
|
||||
add_action("admin_menu", array('\WebPExpress\AdminUi', 'adminMenuHookMultisite'));
|
||||
}
|
||||
|
||||
} else {
|
||||
add_action("admin_menu", array('\WebPExpress\AdminUi', 'adminMenuHook'));
|
||||
}
|
||||
|
||||
// Print pending messages, if any
|
||||
if (Option::getOption('webp-express-messages-pending')) {
|
||||
add_action(Multisite::isNetworkActivated() ? 'network_admin_notices' : 'admin_notices', array('\WebPExpress\Messenger', 'printPendingMessages'));
|
||||
}
|
||||
|
||||
|
||||
// PS:
|
||||
// Filters for processing upload hooks in order to convert images upon upload (wp_handle_upload / image_make_intermediate_size)
|
||||
// are located in webp-express.php
|
||||
|
||||
}
|
||||
}
|
||||
106
lib/classes/AdminUi.php
Normal file
106
lib/classes/AdminUi.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Multisite;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class AdminUi
|
||||
{
|
||||
|
||||
// Add settings link on the plugins page
|
||||
// The hook was registred in AdminInit
|
||||
public static function pluginActionLinksFilter($links)
|
||||
{
|
||||
if (Multisite::isNetworkActivated()) {
|
||||
$mylinks= [
|
||||
'<a href="https://ko-fi.com/rosell" target="_blank">donate?</a>',
|
||||
];
|
||||
} else {
|
||||
$mylinks = array(
|
||||
'<a href="' . admin_url('options-general.php?page=webp_express_settings_page') . '">Settings</a>',
|
||||
'<a href="https://wordpress.org/plugins/webp-express/#%0Ahow%20do%20i%20buy%20you%20a%20cup%20of%20coffee%3F%0A" target="_blank">Provide coffee for the developer</a>',
|
||||
);
|
||||
|
||||
}
|
||||
return array_merge($links, $mylinks);
|
||||
}
|
||||
|
||||
// Add settings link in multisite
|
||||
// The hook was registred in AdminInit
|
||||
public static function networkPluginActionLinksFilter($links)
|
||||
{
|
||||
$mylinks = array(
|
||||
'<a href="' . network_admin_url('settings.php?page=webp_express_settings_page') . '">Settings</a>',
|
||||
'<a href="https://ko-fi.com/rosell" target="_blank">donate?</a>',
|
||||
);
|
||||
return array_merge($links, $mylinks);
|
||||
}
|
||||
|
||||
|
||||
// callback for 'network_admin_menu' (registred in AdminInit)
|
||||
public static function networAdminMenuHook()
|
||||
{
|
||||
add_submenu_page(
|
||||
'settings.php', // Parent element
|
||||
'WebP Express settings (for network)', // Text in browser title bar
|
||||
'WebP Express', // Text to be displayed in the menu.
|
||||
'manage_network_options', // Capability
|
||||
'webp_express_settings_page', // slug
|
||||
array('\WebPExpress\OptionsPage', 'display') // Callback function which displays the page
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'settings.php', // Parent element
|
||||
'WebP Express File Manager', //Page Title
|
||||
'WebP Express File Manager', //Menu Title
|
||||
'manage_network_options', //capability
|
||||
'webp_express_conversion_page', // slug
|
||||
array('\WebPExpress\WCFMPage', 'display') //The function to be called to output the content for this page.
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public static function adminMenuHookMultisite()
|
||||
{
|
||||
// Add Media page
|
||||
/*
|
||||
not ready - it should not display images for the other blogs!
|
||||
|
||||
add_submenu_page(
|
||||
'upload.php', // Parent element
|
||||
'WebP Express', //Page Title
|
||||
'WebP Express', //Menu Title
|
||||
'manage_network_options', //capability
|
||||
'webp_express_conversion_page', // slug
|
||||
array('\WebPExpress\WCFMPage', 'display') //The function to be called to output the content for this page.
|
||||
);
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
public static function adminMenuHook()
|
||||
{
|
||||
//Add Settings Page
|
||||
add_options_page(
|
||||
'WebP Express Settings', //Page Title
|
||||
'WebP Express', //Menu Title
|
||||
'manage_options', //capability
|
||||
'webp_express_settings_page', // slug
|
||||
array('\WebPExpress\OptionsPage', 'display') //The function to be called to output the content for this page.
|
||||
);
|
||||
|
||||
// Add Media page
|
||||
add_media_page(
|
||||
'WebP Express', //Page Title
|
||||
'WebP Express', //Menu Title
|
||||
'manage_options', //capability
|
||||
'webp_express_conversion_page', // slug
|
||||
array('\WebPExpress\WCFMPage', 'display') //The function to be called to output the content for this page.
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
377
lib/classes/AlterHtmlHelper.php
Normal file
377
lib/classes/AlterHtmlHelper.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
//use AlterHtmlInit;
|
||||
use \WebPExpress\Config;
|
||||
use \WebPExpress\Paths;
|
||||
use \WebPExpress\PathHelper;
|
||||
use \WebPExpress\Multisite;
|
||||
use \WebPExpress\Option;
|
||||
|
||||
class AlterHtmlHelper
|
||||
{
|
||||
|
||||
public static $options;
|
||||
/*
|
||||
public static function hasWebP($src)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function inUploadDir($src)
|
||||
{
|
||||
$upload_dir = wp_upload_dir();
|
||||
$src_url = parse_url($upload_dir['baseurl']);
|
||||
$upload_path = $src_url['path'];
|
||||
|
||||
return (strpos($src, $upload_path) !== false );
|
||||
|
||||
}
|
||||
|
||||
public static function checkSrc($src)
|
||||
{
|
||||
self::$options = \WebPExpress\AlterHtmlInit::self::$options();
|
||||
|
||||
|
||||
if (self::$options['destination-folder'] == 'mingled') {
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public static function getOptions() {
|
||||
if (!isset(self::$options)) {
|
||||
self::$options = json_decode(Option::getOption('webp-express-alter-html-options', null), true);
|
||||
if (!isset(self::$options['prevent-using-webps-larger-than-original'])) {
|
||||
self::$options['prevent-using-webps-larger-than-original'] = true;
|
||||
}
|
||||
// Set scope if it isn't there (it wasn't cached until 0.17.5)
|
||||
if (!isset(self::$options['scope'])) {
|
||||
$config = Config::loadConfig();
|
||||
if ($config) {
|
||||
$config = Config::fix($config, false);
|
||||
self::$options['scope'] = $config['scope'];
|
||||
|
||||
Option::updateOption(
|
||||
'webp-express-alter-html-options',
|
||||
json_encode(self::$options, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets relative path between a base url and another.
|
||||
* Returns false if the url isn't a subpath
|
||||
*
|
||||
* @param $imageUrl (ie "http://example.com/wp-content/image.jpg")
|
||||
* @param $baseUrl (ie "http://example.com/wp-content")
|
||||
* @return path or false (ie "/image.jpg")
|
||||
*/
|
||||
public static function getRelUrlPath($imageUrl, $baseUrl)
|
||||
{
|
||||
$baseUrlComponents = parse_url($baseUrl);
|
||||
/* ie:
|
||||
(
|
||||
[scheme] => http
|
||||
[host] => we0
|
||||
[path] => /wordpress/uploads-moved
|
||||
)*/
|
||||
|
||||
$imageUrlComponents = parse_url($imageUrl);
|
||||
/* ie:
|
||||
(
|
||||
[scheme] => http
|
||||
[host] => we0
|
||||
[path] => /wordpress/uploads-moved/logo.jpg
|
||||
)*/
|
||||
if ($baseUrlComponents['host'] != $imageUrlComponents['host']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if path begins with base path
|
||||
if (strpos($imageUrlComponents['path'], $baseUrlComponents['path']) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove base path from path (we know it begins with basepath, from previous check)
|
||||
return substr($imageUrlComponents['path'], strlen($baseUrlComponents['path']));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks if $imageUrl is rooted in $baseUrl and if the file is there
|
||||
* PS: NOT USED ANYMORE!
|
||||
*
|
||||
* @param $imageUrl (ie http://example.com/wp-content/image.jpg)
|
||||
* @param $baseUrl (ie http://example.com/wp-content)
|
||||
* @param $baseDir (ie /var/www/example.com/wp-content)
|
||||
*/
|
||||
public static function isImageUrlHere($imageUrl, $baseUrl, $baseDir)
|
||||
{
|
||||
|
||||
$srcPathRel = self::getRelUrlPath($imageUrl, $baseUrl);
|
||||
|
||||
if ($srcPathRel === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate file path to src
|
||||
$srcPathAbs = $baseDir . $srcPathRel;
|
||||
//return 'dyt:' . $srcPathAbs;
|
||||
|
||||
// Check that src file exists
|
||||
if (!@file_exists($srcPathAbs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
// NOT USED ANYMORE
|
||||
public static function isSourceInUpload($src)
|
||||
{
|
||||
/* $src is ie http://we0/wp-content-moved/themes/twentyseventeen/assets/images/header.jpg */
|
||||
|
||||
$uploadDir = wp_upload_dir();
|
||||
/* ie:
|
||||
|
||||
[path] => /var/www/webp-express-tests/we0/wordpress/uploads-moved
|
||||
[url] => http://we0/wordpress/uploads-moved
|
||||
[subdir] =>
|
||||
[basedir] => /var/www/webp-express-tests/we0/wordpress/uploads-moved
|
||||
[baseurl] => http://we0/wordpress/uploads-moved
|
||||
[error] =>
|
||||
*/
|
||||
|
||||
return self::isImageUrlHere($src, $uploadDir['baseurl'], $uploadDir['basedir']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get url for webp from source url, (if ), given a certain baseUrl / baseDir.
|
||||
* Base can for example be uploads or wp-content.
|
||||
*
|
||||
* returns false:
|
||||
* - if no source file found in that base
|
||||
* - if source file is found but webp file isn't there and the `only-for-webps-that-exists` option is set
|
||||
* - if webp is marked as bigger than source
|
||||
*
|
||||
* @param string $sourceUrl Url of source image (ie http://example.com/wp-content/image.jpg)
|
||||
* @param string $rootId Id (created in Config::updateAutoloadedOptions). Ie "uploads", "content" or any image root id
|
||||
* @param string $baseUrl Base url of source image (ie http://example.com/wp-content)
|
||||
* @param string $baseDir Base dir of source image (ie /var/www/example.com/wp-content)
|
||||
*/
|
||||
public static function getWebPUrlInImageRoot($sourceUrl, $rootId, $baseUrl, $baseDir)
|
||||
{
|
||||
|
||||
|
||||
$srcPathRel = self::getRelUrlPath($sourceUrl, $baseUrl);
|
||||
|
||||
if ($srcPathRel === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate file path to source
|
||||
$srcPathAbs = $baseDir . $srcPathRel;
|
||||
|
||||
// Check that source file exists
|
||||
if (!@file_exists($srcPathAbs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_exists($srcPathAbs . '.do-not-convert')) {
|
||||
return false;
|
||||
}
|
||||
if (file_exists($srcPathAbs . '.dontreplace')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate destination of webp (both path and url)
|
||||
// ----------------------------------------
|
||||
|
||||
// We are calculating: $destPathAbs and $destUrl.
|
||||
|
||||
// Make sure the options are loaded (and fixed)
|
||||
self::getOptions();
|
||||
$destinationOptions = new DestinationOptions(
|
||||
self::$options['destination-folder'] == 'mingled',
|
||||
self::$options['destination-structure'] == 'doc-root',
|
||||
self::$options['destination-extension'] == 'set',
|
||||
self::$options['scope']
|
||||
);
|
||||
|
||||
if (!isset(self::$options['scope']) || !in_array($rootId, self::$options['scope'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$destinationRoot = Paths::destinationRoot($rootId, $destinationOptions);
|
||||
|
||||
$relPathFromImageRootToSource = PathHelper::getRelDir(
|
||||
realpath(Paths::getAbsDirById($rootId)), // note: In multisite (subfolders), it contains ie "/site/2/"
|
||||
realpath($srcPathAbs)
|
||||
);
|
||||
$relPathFromImageRootToDest = ConvertHelperIndependent::appendOrSetExtension(
|
||||
$relPathFromImageRootToSource,
|
||||
self::$options['destination-folder'],
|
||||
self::$options['destination-extension'],
|
||||
($rootId == 'uploads')
|
||||
);
|
||||
$destPathAbs = $destinationRoot['abs-path'] . '/' . $relPathFromImageRootToDest;
|
||||
$webpMustExist = self::$options['only-for-webps-that-exists'];
|
||||
if ($webpMustExist && (!@file_exists($destPathAbs))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if webp is marked as bigger than source
|
||||
/*
|
||||
$biggerThanSourcePath = Paths::getBiggerThanSourceDirAbs() . '/' . $rootId . '/' . $relPathFromImageRootToDest;
|
||||
if (@file_exists($biggerThanSourcePath)) {
|
||||
return false;
|
||||
}*/
|
||||
|
||||
// check if webp is larger than original
|
||||
if (self::$options['prevent-using-webps-larger-than-original']) {
|
||||
if (BiggerThanSource::bigger($srcPathAbs, $destPathAbs)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$destUrl = $destinationRoot['url'] . '/' . $relPathFromImageRootToDest;
|
||||
|
||||
// Fix scheme (use same as source)
|
||||
$sourceUrlComponents = parse_url($sourceUrl);
|
||||
$destUrlComponents = parse_url($destUrl);
|
||||
$port = isset($sourceUrlComponents['port']) ? ":" . $sourceUrlComponents['port'] : "";
|
||||
$result = $sourceUrlComponents['scheme'] . '://' . $sourceUrlComponents['host'] . $port . $destUrlComponents['path'];
|
||||
|
||||
/*
|
||||
error_log(
|
||||
"getWebPUrlInImageRoot:\n" .
|
||||
"- url: " . $sourceUrl . "\n" .
|
||||
"- baseUrl: " . $baseUrl . "\n" .
|
||||
"- baseDir: " . $baseDir . "\n" .
|
||||
"- root id: " . $rootId . "\n" .
|
||||
"- root abs: " . Paths::getAbsDirById($rootId) . "\n" .
|
||||
"- destination root (abs): " . $destinationRoot['abs-path'] . "\n" .
|
||||
"- destination root (url): " . $destinationRoot['url'] . "\n" .
|
||||
"- rel: " . $srcPathRel . "\n" .
|
||||
"- srcPathAbs: " . $srcPathAbs . "\n" .
|
||||
'- relPathFromImageRootToSource: ' . $relPathFromImageRootToSource . "\n" .
|
||||
'- get_blog_details()->path: ' . get_blog_details()->path . "\n" .
|
||||
"- result: " . $result . "\n"
|
||||
);*/
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get url for webp
|
||||
* returns second argument if no webp
|
||||
*
|
||||
* @param $sourceUrl
|
||||
* @param $returnValueOnFail
|
||||
*/
|
||||
public static function getWebPUrl($sourceUrl, $returnValueOnFail)
|
||||
{
|
||||
// Get the options
|
||||
self::getOptions();
|
||||
|
||||
// Fail for webp-disabled browsers (when "only-for-webp-enabled-browsers" is set)
|
||||
if (self::$options['only-for-webp-enabled-browsers']) {
|
||||
if (!isset($_SERVER['HTTP_ACCEPT']) || (strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') === false)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
}
|
||||
|
||||
// Fail for relative urls. Wordpress doesn't use such very much anyway
|
||||
if (!preg_match('#^https?://#', $sourceUrl)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
|
||||
// Fail if the image type isn't enabled
|
||||
switch (self::$options['image-types']) {
|
||||
case 0:
|
||||
return $returnValueOnFail;
|
||||
case 1:
|
||||
if (!preg_match('#(jpe?g)$#', $sourceUrl)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (!preg_match('#(png)$#', $sourceUrl)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (!preg_match('#(jpe?g|png)$#', $sourceUrl)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
//error_log('source url:' . $sourceUrl);
|
||||
|
||||
// Try all image roots
|
||||
foreach (self::$options['scope'] as $rootId) {
|
||||
$baseDir = Paths::getAbsDirById($rootId);
|
||||
$baseUrl = Paths::getUrlById($rootId);
|
||||
|
||||
if (Multisite::isMultisite() && ($rootId == 'uploads')) {
|
||||
$baseUrl = Paths::getUploadUrl();
|
||||
$baseDir = Paths::getUploadDirAbs();
|
||||
}
|
||||
|
||||
$result = self::getWebPUrlInImageRoot($sourceUrl, $rootId, $baseUrl, $baseDir);
|
||||
if ($result !== false) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Try the hostname aliases.
|
||||
if (!isset(self::$options['hostname-aliases'])) {
|
||||
continue;
|
||||
}
|
||||
$hostnameAliases = self::$options['hostname-aliases'];
|
||||
|
||||
$hostname = Paths::getHostNameOfUrl($baseUrl);
|
||||
$baseUrlComponents = parse_url($baseUrl);
|
||||
$sourceUrlComponents = parse_url($sourceUrl);
|
||||
// ie: [scheme] => http, [host] => we0, [path] => /wordpress/uploads-moved
|
||||
|
||||
if ((!isset($baseUrlComponents['host'])) || (!isset($sourceUrlComponents['host']))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($hostnameAliases as $hostnameAlias) {
|
||||
|
||||
if ($sourceUrlComponents['host'] != $hostnameAlias) {
|
||||
continue;
|
||||
}
|
||||
//error_log('hostname alias:' . $hostnameAlias);
|
||||
|
||||
$baseUrlOnAlias = $baseUrlComponents['scheme'] . '://' . $hostnameAlias . $baseUrlComponents['path'];
|
||||
//error_log('baseurl (alias):' . $baseUrlOnAlias);
|
||||
|
||||
$result = self::getWebPUrlInImageRoot($sourceUrl, $rootId, $baseUrlOnAlias, $baseDir);
|
||||
if ($result !== false) {
|
||||
$resultUrlComponents = parse_url($result);
|
||||
return $sourceUrlComponents['scheme'] . '://' . $hostnameAlias . $resultUrlComponents['path'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
|
||||
/*
|
||||
public static function getWebPUrlOrSame($sourceUrl, $returnValueOnFail)
|
||||
{
|
||||
return self::getWebPUrl($sourceUrl, $sourceUrl);
|
||||
}*/
|
||||
|
||||
}
|
||||
33
lib/classes/AlterHtmlImageUrls.php
Normal file
33
lib/classes/AlterHtmlImageUrls.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\AlterHtmlInit;
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
/**
|
||||
* Class AlterHtmlImageUrls - convert image urls to webp
|
||||
* Based this code on code from the Cache Enabler plugin
|
||||
*/
|
||||
|
||||
use \WebPExpress\AlterHtmlHelper;
|
||||
//use \WebPExpress\ImageUrlsReplacer;
|
||||
use DOMUtilForWebP\ImageUrlReplacer;
|
||||
|
||||
class AlterHtmlImageUrls extends ImageUrlReplacer
|
||||
{
|
||||
public function replaceUrl($url) {
|
||||
return AlterHtmlHelper::getWebPUrl($url, null);
|
||||
}
|
||||
|
||||
public function attributeFilter($attrName) {
|
||||
// Allow "src", "srcset" and data-attributes that smells like they are used for images
|
||||
// The following rule matches all attributes used for lazy loading images that we know of
|
||||
return preg_match('#^(src|srcset|poster|(data-[^=]*(lazy|small|slide|img|large|src|thumb|source|set|bg-url)[^=]*))$#i', $attrName);
|
||||
|
||||
// If you want to limit it further, only allowing attributes known to be used for lazy load,
|
||||
// use the following regex instead:
|
||||
//return preg_match('#^(src|srcset|data-(src|srcset|cvpsrc|cvpset|thumb|bg-url|large_image|lazyload|source-url|srcsmall|srclarge|srcfull|slide-img|lazy-original))$#i', $attrName);
|
||||
}
|
||||
|
||||
}
|
||||
154
lib/classes/AlterHtmlInit.php
Normal file
154
lib/classes/AlterHtmlInit.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use AlterHtmlHelper;
|
||||
|
||||
use \WebPExpress\Option;
|
||||
|
||||
class AlterHtmlInit
|
||||
{
|
||||
public static $options = null;
|
||||
|
||||
public static function startOutputBuffer()
|
||||
{
|
||||
if (!is_admin() || (function_exists("wp_doing_ajax") && wp_doing_ajax()) || (defined( 'DOING_AJAX' ) && DOING_AJAX)) {
|
||||
// note: "self::alterHtml" does for some reason not work on hhvm (#226)
|
||||
ob_start('\\WebPExpress\\AlterHtmlInit::alterHtml');
|
||||
}
|
||||
}
|
||||
|
||||
public static function alterHtml($content)
|
||||
{
|
||||
// Don't do anything with the RSS feed.
|
||||
if (is_feed()) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if (is_admin()) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Exit if it doesn't look like HTML (see #228)
|
||||
if (!preg_match("#^\\s*<#", $content)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if (Option::getOption('webp-express-alter-html-replacement') == 'picture') {
|
||||
if(function_exists('is_amp_endpoint') && is_amp_endpoint()) {
|
||||
//for AMP pages the <picture> tag is not allowed
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset(self::$options)) {
|
||||
self::$options = json_decode(Option::getOption('webp-express-alter-html-options', null), true);
|
||||
//AlterHtmlHelper::$options = self::$options;
|
||||
}
|
||||
|
||||
if (self::$options == null) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if (Option::getOption('webp-express-alter-html-replacement') == 'picture') {
|
||||
require_once __DIR__ . "/../../vendor/autoload.php";
|
||||
require_once __DIR__ . '/AlterHtmlHelper.php';
|
||||
require_once __DIR__ . '/AlterHtmlPicture.php';
|
||||
return \WebPExpress\AlterHtmlPicture::replace($content);
|
||||
} else {
|
||||
require_once __DIR__ . "/../../vendor/autoload.php";
|
||||
require_once __DIR__ . '/AlterHtmlHelper.php';
|
||||
require_once __DIR__ . '/AlterHtmlImageUrls.php';
|
||||
|
||||
return \WebPExpress\AlterHtmlImageUrls::replace($content);
|
||||
}
|
||||
}
|
||||
|
||||
public static function addPictureFillJs()
|
||||
{
|
||||
// Don't do anything with the RSS feed.
|
||||
// - and no need for PictureJs in the admin
|
||||
if ( is_feed() || is_admin() ) { return; }
|
||||
|
||||
echo '<script>'
|
||||
. 'document.createElement( "picture" );'
|
||||
. 'if(!window.HTMLPictureElement && document.addEventListener) {'
|
||||
. 'window.addEventListener("DOMContentLoaded", function() {'
|
||||
. 'var s = document.createElement("script");'
|
||||
. 's.src = "' . plugins_url('/js/picturefill.min.js', WEBPEXPRESS_PLUGIN) . '";'
|
||||
. 'document.body.appendChild(s);'
|
||||
. '});'
|
||||
. '}'
|
||||
. '</script>';
|
||||
}
|
||||
|
||||
public static function sidebarBeforeAlterHtml()
|
||||
{
|
||||
ob_start();
|
||||
}
|
||||
|
||||
public static function sidebarAfterAlterHtml()
|
||||
{
|
||||
$content = ob_get_clean();
|
||||
|
||||
echo self::alterHtml($content);
|
||||
|
||||
unset($content);
|
||||
}
|
||||
|
||||
public static function setHooks() {
|
||||
|
||||
if (Option::getOption('webp-express-alter-html-add-picturefill-js')) {
|
||||
add_action( 'wp_head', '\\WebPExpress\\AlterHtmlInit::addPictureFillJs');
|
||||
}
|
||||
|
||||
if (Option::getOption('webp-express-alter-html-hooks', 'ob') == 'ob') {
|
||||
/* TODO:
|
||||
Which hook should we use, and should we make it optional?
|
||||
- Cache enabler uses 'template_redirect'
|
||||
- ShortPixes uses 'init'
|
||||
|
||||
We go with template_redirect now, because it is the "innermost".
|
||||
This lowers the risk of problems with plugins used rewriting URLs to point to CDN.
|
||||
(We need to process the output *before* the other plugin has rewritten the URLs,
|
||||
if the "Only for webps that exists" feature is enabled)
|
||||
*/
|
||||
add_action( 'init', '\\WebPExpress\\AlterHtmlInit::startOutputBuffer', 1 );
|
||||
add_action( 'template_redirect', '\\WebPExpress\\AlterHtmlInit::startOutputBuffer', 10000 );
|
||||
|
||||
} else {
|
||||
add_filter( 'the_content', '\\WebPExpress\\AlterHtmlInit::alterHtml', 99999 ); // priority big, so it will be executed last
|
||||
add_filter( 'the_excerpt', '\\WebPExpress\\AlterHtmlInit::alterHtml', 99999 );
|
||||
add_filter( 'post_thumbnail_html', '\\WebPExpress\\AlterHtmlInit::alterHtml', 99999);
|
||||
add_filter( 'woocommerce_product_get_image', '\\WebPExpress\\AlterHtmlInit::alterHtml', 99999 );
|
||||
add_filter( 'get_avatar', '\\WebPExpress\\AlterHtmlInit::alterHtml', 99999 );
|
||||
add_filter( 'acf_the_content', '\\WebPExpress\\AlterHtmlInit::alterHtml', 99999 );
|
||||
add_action( 'dynamic_sidebar_before', '\\WebPExpress\\AlterHtmlInit::sidebarBeforeAlterHtml', 0 );
|
||||
add_action( 'dynamic_sidebar_after', '\\WebPExpress\\AlterHtmlInit::sidebarAfterAlterHtml', 1000 );
|
||||
|
||||
|
||||
/*
|
||||
TODO:
|
||||
check out these hooks (used by Jetpack, in class.photon.php)
|
||||
|
||||
// Images in post content and galleries
|
||||
add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
|
||||
add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
|
||||
add_filter( 'widget_media_image_instance', array( __CLASS__, 'filter_the_image_widget' ), 999999 );
|
||||
|
||||
// Core image retrieval
|
||||
add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
|
||||
add_filter( 'rest_request_before_callbacks', array( $this, 'should_rest_photon_image_downsize' ), 10, 3 );
|
||||
add_filter( 'rest_request_after_callbacks', array( $this, 'cleanup_rest_photon_image_downsize' ) );
|
||||
|
||||
// Responsive image srcset substitution
|
||||
add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 5 );
|
||||
add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 2 ); // Early so themes can still easily filter.
|
||||
|
||||
// Helpers for maniuplated images
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
18
lib/classes/AlterHtmlPicture.php
Normal file
18
lib/classes/AlterHtmlPicture.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
/**
|
||||
* Class AlterHtmlPicture - convert an <img> tag to a <picture> tag and add the webp versions of the images
|
||||
* Based this code on code from the ShortPixel plugin, which used code from Responsify WP plugin
|
||||
*/
|
||||
|
||||
use \WebPExpress\AlterHtmlHelper;
|
||||
use DOMUtilForWebP\PictureTags;
|
||||
|
||||
class AlterHtmlPicture extends PictureTags
|
||||
{
|
||||
public function replaceUrl($url) {
|
||||
return AlterHtmlHelper::getWebPUrl($url, null);
|
||||
}
|
||||
}
|
||||
32
lib/classes/BiggerThanSource.php
Normal file
32
lib/classes/BiggerThanSource.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
This class is made to not be dependent on Wordpress functions and must be kept like that.
|
||||
It is used by webp-on-demand.php. It is also used for bulk conversion.
|
||||
*/
|
||||
namespace WebPExpress;
|
||||
|
||||
|
||||
class BiggerThanSource
|
||||
{
|
||||
/**
|
||||
* Check if webp is bigger than original.
|
||||
*
|
||||
* @return boolean|null True if it is bigger than original, false if not. NULL if it cannot be determined
|
||||
*/
|
||||
public static function bigger($source, $destination)
|
||||
{
|
||||
if ((!@file_exists($source)) || (!@file_exists($destination))) {
|
||||
return null;
|
||||
}
|
||||
$filesizeDestination = @filesize($destination);
|
||||
$filesizeSource = @filesize($source);
|
||||
|
||||
// sizes are FALSE on failure (ie if file does not exists)
|
||||
if (($filesizeSource === false) || ($filesizeDestination === false)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ($filesizeDestination > $filesizeSource);
|
||||
}
|
||||
}
|
||||
135
lib/classes/BiggerThanSourceDummyFiles.php
Normal file
135
lib/classes/BiggerThanSourceDummyFiles.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
This class is made to not be dependent on Wordpress functions and must be kept like that.
|
||||
It is used by webp-on-demand.php. It is also used for bulk conversion.
|
||||
*/
|
||||
namespace WebPExpress;
|
||||
|
||||
|
||||
class BiggerThanSourceDummyFiles
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* Create the directory for log files and put a .htaccess file into it, which prevents
|
||||
* it to be viewed from the outside (not that it contains any sensitive information btw, but for good measure).
|
||||
*
|
||||
* @param string $logDir The folder where log files are kept
|
||||
*
|
||||
* @return boolean Whether it was created successfully or not.
|
||||
*
|
||||
*/
|
||||
private static function createBiggerThanSourceBaseDir($dir)
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0775, true);
|
||||
@chmod($dir, 0775);
|
||||
@file_put_contents(rtrim($dir . '/') . '/.htaccess', <<<APACHE
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
APACHE
|
||||
);
|
||||
@chmod($dir . '/.htaccess', 0664);
|
||||
}
|
||||
return is_dir($dir);
|
||||
}
|
||||
|
||||
public static function pathToDummyFile($source, $basedir, $imageRoots, $destinationFolder, $destinationExt)
|
||||
{
|
||||
$sourceResolved = realpath($source);
|
||||
|
||||
// Check roots until we (hopefully) get a match.
|
||||
// (that is: find a root which the source is inside)
|
||||
foreach ($imageRoots->getArray() as $i => $imageRoot) {
|
||||
$rootPath = $imageRoot->getAbsPath();
|
||||
|
||||
// We can assume that $rootPath is resolvable using realpath (it ought to exist and be within open_basedir for WP to function)
|
||||
// We can also assume that $source is resolvable (it ought to exist and within open_basedir)
|
||||
// So: Resolve both! and test if the resolved source begins with the resolved rootPath.
|
||||
if (strpos($sourceResolved, realpath($rootPath)) !== false) {
|
||||
$relPath = substr($sourceResolved, strlen(realpath($rootPath)) + 1);
|
||||
$relPath = ConvertHelperIndependent::appendOrSetExtension($relPath, $destinationFolder, $destinationExt, false);
|
||||
|
||||
return $basedir . '/' . $imageRoot->id . '/' . $relPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function pathToDummyFileRootAndRelKnown($source, $basedir, $rootId, $destinationFolder, $destinationExt)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if webp is bigger than original.
|
||||
*
|
||||
* @return boolean|null True if it is bigger than original, false if not. NULL if it cannot be determined
|
||||
*/
|
||||
public static function bigger($source, $destination)
|
||||
{
|
||||
/*
|
||||
if ((!@file_exists($source)) || (!@file_exists($destination) {
|
||||
return null;
|
||||
}*/
|
||||
$filesizeDestination = @filesize($destination);
|
||||
$filesizeSource = @filesize($source);
|
||||
|
||||
// sizes are FALSE on failure (ie if file does not exists)
|
||||
if (($filesizeDestination === false) || ($filesizeDestination === false)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ($filesizeDestination > $filesizeSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status for a single image (when rootId is unknown)
|
||||
*
|
||||
* Checks if webp is bigger than original. If it is, a dummy file is placed. Otherwise, it is
|
||||
* removed (if exists)
|
||||
*
|
||||
* @param string $source Path to the source file that was converted
|
||||
*
|
||||
*
|
||||
*/
|
||||
public static function updateStatus($source, $destination, $webExpressContentDirAbs, $imageRoots, $destinationFolder, $destinationExt)
|
||||
{
|
||||
$basedir = $webExpressContentDirAbs . '/webp-images-bigger-than-source';
|
||||
if (!file_exists($basedir)) {
|
||||
self::createBiggerThanSourceBaseDir($basedir);
|
||||
}
|
||||
$bigWebP = BiggerThanSource::bigger($source, $destination);
|
||||
|
||||
$file = self::pathToDummyFile($source, $basedir, $imageRoots, $destinationFolder, $destinationExt);
|
||||
if ($file === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($bigWebP === true) {
|
||||
// place dummy file, which marks that webp is bigger than source
|
||||
|
||||
$folder = @dirname($file);
|
||||
if (!@file_exists($folder)) {
|
||||
mkdir($folder, 0777, true);
|
||||
}
|
||||
if (@file_exists($folder)) {
|
||||
file_put_contents($file, '');
|
||||
}
|
||||
|
||||
} else {
|
||||
// remove dummy file (if exists)
|
||||
if (@file_exists($file)) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
120
lib/classes/BiggerThanSourceDummyFilesBulk.php
Normal file
120
lib/classes/BiggerThanSourceDummyFilesBulk.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
|
||||
class BiggerThanSourceDummyFilesBulk
|
||||
{
|
||||
|
||||
private static $settings;
|
||||
|
||||
/**
|
||||
* Update the status for a all images.
|
||||
*
|
||||
*/
|
||||
public static function updateStatus($config = null)
|
||||
{
|
||||
if (is_null($config)) {
|
||||
$config = Config::loadConfigAndFix(false);
|
||||
}
|
||||
self::$settings = [
|
||||
'ext' => $config['destination-extension'],
|
||||
'destination-folder' => $config['destination-folder'], /* hm, "destination-folder" is a bad name... */
|
||||
'webExpressContentDirAbs' => Paths::getWebPExpressContentDirAbs(),
|
||||
'uploadDirAbs' => Paths::getUploadDirAbs(),
|
||||
'useDocRootForStructuringCacheDir' => (($config['destination-structure'] == 'doc-root') && (Paths::canUseDocRootForStructuringCacheDir())),
|
||||
//'imageRoots' => new ImageRoots(Paths::getImageRootsDefForSelectedIds($config['scope'])), // (Paths::getImageRootsDef()
|
||||
'imageRoots' => new ImageRoots(Paths::getImageRootsDefForSelectedIds(Paths::getImageRootIds())), // (Paths::getImageRootsDef()
|
||||
'image-types' => $config['image-types'],
|
||||
];
|
||||
|
||||
|
||||
//$rootIds = Paths::filterOutSubRoots($config['scope']);
|
||||
|
||||
// We want to update status on ALL root dirs (so we don't have to re-run when user changes scope)
|
||||
$rootIds = Paths::filterOutSubRoots(Paths::getImageRootIds());
|
||||
//$rootIds = ['uploads'];
|
||||
//$rootIds = ['uploads', 'themes'];
|
||||
|
||||
foreach ($rootIds as $rootId) {
|
||||
self::updateStatusForRoot($rootId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-requirement: self::$settings is set.
|
||||
*
|
||||
* Idea for improvement: Traverse destination dirs instead. This will be quicker, as there will not be
|
||||
* as many images (unless all have been converted), and not as many folders (non-image folders will not be present.
|
||||
* however, index does not take too long to traverse, even though it has many non-image folders, so it will only
|
||||
* be a problem if there are plugins or themes with extremely many folders).
|
||||
*/
|
||||
private static function updateStatusForRoot($rootId, $dir = '')
|
||||
{
|
||||
if ($dir == '') {
|
||||
$dir = Paths::getAbsDirById($rootId);
|
||||
}
|
||||
|
||||
// Canonicalize because dir might contain "/./", which causes file_exists to fail (#222)
|
||||
$dir = PathHelper::canonicalize($dir);
|
||||
|
||||
if (!@file_exists($dir) || !@is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fileIterator = new \FilesystemIterator($dir);
|
||||
|
||||
$results = [];
|
||||
|
||||
while ($fileIterator->valid()) {
|
||||
$filename = $fileIterator->getFilename();
|
||||
|
||||
if (($filename != ".") && ($filename != "..")) {
|
||||
if (@is_dir($dir . "/" . $filename)) {
|
||||
$newDir = $dir . "/" . $filename;
|
||||
|
||||
// The new dir might have its own root id
|
||||
$newRootId = Paths::findImageRootOfPath($newDir, Paths::getImageRootIds());
|
||||
//echo $newRootId . ': ' . $newDir . "\n";
|
||||
self::updateStatusForRoot($newRootId, $newDir);
|
||||
} else {
|
||||
// its a file - check if its a valid image type (jpeg or png)
|
||||
$regex = '#\.(jpe?g|png)$#';
|
||||
if (preg_match($regex, $filename)) {
|
||||
|
||||
$source = $dir . "/" . $filename;
|
||||
|
||||
$destination = ConvertHelperIndependent::getDestination(
|
||||
$source,
|
||||
self::$settings['destination-folder'],
|
||||
self::$settings['ext'],
|
||||
self::$settings['webExpressContentDirAbs'],
|
||||
self::$settings['uploadDirAbs'],
|
||||
self::$settings['useDocRootForStructuringCacheDir'],
|
||||
self::$settings['imageRoots'],
|
||||
//$rootId
|
||||
|
||||
);
|
||||
$webpExists = @file_exists($destination);
|
||||
|
||||
//echo ($webpExists ? 'YES' : 'NO') . ' ' . $rootId . ': ' . $source . "\n";
|
||||
|
||||
BiggerThanSourceDummyFiles::updateStatus(
|
||||
$source,
|
||||
$destination,
|
||||
self::$settings['webExpressContentDirAbs'],
|
||||
self::$settings['imageRoots'],
|
||||
self::$settings['destination-folder'],
|
||||
self::$settings['ext'],
|
||||
// TODO: send rootId so the function doesn't need to try all
|
||||
// $rootId,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileIterator->next();
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
337
lib/classes/BulkConvert.php
Normal file
337
lib/classes/BulkConvert.php
Normal file
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
//use \Onnov\DetectEncoding\EncodingDetector;
|
||||
|
||||
use \WebPExpress\Config;
|
||||
use \WebPExpress\ConvertHelperIndependent;
|
||||
use \WebPExpress\ImageRoots;
|
||||
use \WebPExpress\PathHelper;
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
class BulkConvert
|
||||
{
|
||||
|
||||
public static function defaultListOptions($config)
|
||||
{
|
||||
return [
|
||||
//'root' => Paths::getUploadDirAbs(),
|
||||
'ext' => $config['destination-extension'],
|
||||
'destination-folder' => $config['destination-folder'], /* hm, "destination-folder" is a bad name... */
|
||||
'webExpressContentDirAbs' => Paths::getWebPExpressContentDirAbs(),
|
||||
'uploadDirAbs' => Paths::getUploadDirAbs(),
|
||||
'useDocRootForStructuringCacheDir' => (($config['destination-structure'] == 'doc-root') && (Paths::canUseDocRootForStructuringCacheDir())),
|
||||
'imageRoots' => new ImageRoots(Paths::getImageRootsDefForSelectedIds($config['scope'])), // (Paths::getImageRootsDef()
|
||||
'filter' => [
|
||||
'only-converted' => false,
|
||||
'only-unconverted' => true,
|
||||
'image-types' => $config['image-types'],
|
||||
'max-depth' => 100,
|
||||
],
|
||||
'flattenList' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grouped list of files. They are grouped by image roots.
|
||||
*
|
||||
*/
|
||||
public static function getList($config, $listOptions = null)
|
||||
{
|
||||
|
||||
/*
|
||||
isUploadDirMovedOutOfWPContentDir
|
||||
isUploadDirMovedOutOfAbsPath
|
||||
isPluginDirMovedOutOfAbsPath
|
||||
isPluginDirMovedOutOfWpContent
|
||||
isWPContentDirMovedOutOfAbsPath */
|
||||
|
||||
if (is_null($listOptions)) {
|
||||
$listOptions = self::defaultListOptions($config);
|
||||
}
|
||||
|
||||
$rootIds = Paths::filterOutSubRoots($config['scope']);
|
||||
|
||||
$groups = [];
|
||||
foreach ($rootIds as $rootId) {
|
||||
$groups[] = [
|
||||
'groupName' => $rootId,
|
||||
'root' => Paths::getAbsDirById($rootId)
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($groups as $i => &$group) {
|
||||
$listOptions['root'] = $group['root'];
|
||||
/*
|
||||
No use, because if uploads is in wp-content, the cache root will be different for the files in uploads (if mingled)
|
||||
$group['image-root'] = ConvertHelperIndependent::getDestinationFolder(
|
||||
$group['root'],
|
||||
$listOptions['destination-folder'],
|
||||
$listOptions['ext'],
|
||||
$listOptions['webExpressContentDirAbs'],
|
||||
$listOptions['uploadDirAbs']
|
||||
);*/
|
||||
$group['files'] = self::getListRecursively('.', $listOptions);
|
||||
//'image-root' => ConvertHelperIndependent::getDestinationFolder()
|
||||
}
|
||||
|
||||
return $groups;
|
||||
//self::moveRecursively($toDir, $fromDir, $srcDir, $fromExt, $toExt);
|
||||
}
|
||||
|
||||
/**
|
||||
* $filter: all | converted | not-converted. "not-converted" for example returns paths to images that has not been converted
|
||||
*/
|
||||
public static function getListRecursively($relDir, &$listOptions, $depth = 0)
|
||||
{
|
||||
$dir = $listOptions['root'] . '/' . $relDir;
|
||||
|
||||
// Canonicalize because dir might contain "/./", which causes file_exists to fail (#222)
|
||||
$dir = PathHelper::canonicalize($dir);
|
||||
|
||||
if (!@file_exists($dir) || !@is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fileIterator = new \FilesystemIterator($dir);
|
||||
|
||||
$results = [];
|
||||
$filter = &$listOptions['filter'];
|
||||
|
||||
while ($fileIterator->valid()) {
|
||||
$filename = $fileIterator->getFilename();
|
||||
|
||||
if (($filename != ".") && ($filename != "..")) {
|
||||
|
||||
if (@is_dir($dir . "/" . $filename)) {
|
||||
if ($listOptions['flattenList']) {
|
||||
$results = array_merge($results, self::getListRecursively($relDir . "/" . $filename, $listOptions, $depth+1));
|
||||
} else {
|
||||
$r = [
|
||||
'name' => $filename,
|
||||
'isDir' => true,
|
||||
];
|
||||
if ($depth > $listOptions['max-depth']) {
|
||||
return $r; // one item is enough to determine that it is not empty
|
||||
}
|
||||
if ($depth < $listOptions['max-depth']) {
|
||||
$r['children'] = self::getListRecursively($relDir . "/" . $filename, $listOptions, $depth+1);
|
||||
$r['isEmpty'] = (count($r['children']) == 0);
|
||||
} else if ($depth == $listOptions['max-depth']) {
|
||||
$c = self::getListRecursively($relDir . "/" . $filename, $listOptions, $depth+1);
|
||||
$r['isEmpty'] = (count($c) == 0);
|
||||
//$r['isEmpty'] = !(new \FilesystemIterator($dir))->valid();
|
||||
}
|
||||
$results[] = $r;
|
||||
}
|
||||
} else {
|
||||
// its a file - check if its a jpeg or png
|
||||
|
||||
if (!isset($filter['_regexPattern'])) {
|
||||
$imageTypes = $filter['image-types'];
|
||||
$fileExtensions = [];
|
||||
if ($imageTypes & 1) {
|
||||
$fileExtensions[] = 'jpe?g';
|
||||
}
|
||||
if ($imageTypes & 2) {
|
||||
$fileExtensions[] = 'png';
|
||||
}
|
||||
$filter['_regexPattern'] = '#\.(' . implode('|', $fileExtensions) . ')$#';
|
||||
}
|
||||
|
||||
if (preg_match($filter['_regexPattern'], $filename)) {
|
||||
$addThis = true;
|
||||
|
||||
$destination = ConvertHelperIndependent::getDestination(
|
||||
$dir . "/" . $filename,
|
||||
$listOptions['destination-folder'],
|
||||
$listOptions['ext'],
|
||||
$listOptions['webExpressContentDirAbs'],
|
||||
$listOptions['uploadDirAbs'],
|
||||
$listOptions['useDocRootForStructuringCacheDir'],
|
||||
$listOptions['imageRoots']
|
||||
);
|
||||
$webpExists = @file_exists($destination);
|
||||
|
||||
if (($filter['only-converted']) || ($filter['only-unconverted'])) {
|
||||
//$cacheDir = $listOptions['image-root'] . '/' . $relDir;
|
||||
|
||||
// Check if corresponding webp exists
|
||||
/*
|
||||
if ($listOptions['ext'] == 'append') {
|
||||
$webpExists = @file_exists($cacheDir . "/" . $filename . '.webp');
|
||||
} else {
|
||||
$webpExists = @file_exists(preg_replace("/\.(jpe?g|png)\.webp$/", '.webp', $filename));
|
||||
}*/
|
||||
|
||||
if (!$webpExists && ($filter['only-converted'])) {
|
||||
$addThis = false;
|
||||
}
|
||||
if ($webpExists && ($filter['only-unconverted'])) {
|
||||
$addThis = false;
|
||||
}
|
||||
} else {
|
||||
$addThis = true;
|
||||
}
|
||||
|
||||
if ($addThis) {
|
||||
|
||||
$path = substr($relDir . "/", 2) . $filename; // (we cut the leading "./" off with substr)
|
||||
|
||||
// Additional safety check: verify the file actually exists before adding to list
|
||||
$fullPath = $dir . "/" . $filename;
|
||||
if (!file_exists($fullPath)) {
|
||||
continue; // Skip this file if it doesn't exist
|
||||
}
|
||||
|
||||
// Check if the string can be encoded to json (if not: change it to a string that can)
|
||||
if (json_encode($path, JSON_UNESCAPED_UNICODE) === false) {
|
||||
/*
|
||||
json_encode failed. This means that the string was not UTF-8.
|
||||
Lets see if we can convert it to UTF-8.
|
||||
This is however tricky business (see #471)
|
||||
*/
|
||||
|
||||
$encodedToUTF8 = false;
|
||||
|
||||
// First try library that claims to do better than mb_detect_encoding
|
||||
/*
|
||||
DISABLED, because Onnov EncodingDetector requires PHP 7.2
|
||||
https://wordpress.org/support/topic/get-http-error-500-after-new-update-2/
|
||||
|
||||
if (!$encodedToUTF8) {
|
||||
$detector = new EncodingDetector();
|
||||
|
||||
$dectedEncoding = $detector->getEncoding($path);
|
||||
|
||||
if ($dectedEncoding !== 'utf-8') {
|
||||
if (function_exists('iconv')) {
|
||||
$res = iconv($dectedEncoding, 'utf-8//TRANSLIT', $path);
|
||||
if ($res !== false) {
|
||||
$path = $res;
|
||||
$encodedToUTF8 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// iconvXtoEncoding should work now hm, issue #5 has been fixed
|
||||
$path = $detector->iconvXtoEncoding($path);
|
||||
$encodedToUTF8 = true;
|
||||
} catch (\Exception $e) {
|
||||
|
||||
}
|
||||
}*/
|
||||
|
||||
// Try mb_detect_encoding
|
||||
if (!$encodedToUTF8) {
|
||||
if (function_exists('mb_convert_encoding')) {
|
||||
$encoding = mb_detect_encoding($path, mb_detect_order(), true);
|
||||
if ($encoding) {
|
||||
$path = mb_convert_encoding($path, 'UTF-8', $encoding);
|
||||
$encodedToUTF8 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$encodedToUTF8) {
|
||||
/*
|
||||
We haven't yet succeeded in encoding to UTF-8.
|
||||
What should we do?
|
||||
1. Skip the file? (no, the user will not know about the problem then)
|
||||
2. Add it anyway? (no, if this string causes problems to json_encode, then we will have
|
||||
the same problem when encoding the entire list - result: an empty list)
|
||||
3. Try wp_json_encode? (no, it will fall back on "wp_check_invalid_utf8", which has a number of
|
||||
things we do not want)
|
||||
4. Encode it to UTF-8 assuming that the string is encoded in the most common encoding (Windows-1252) ?
|
||||
(yes, if we are lucky with the guess, it will work. If it is in another encoding, the conversion
|
||||
will not be correct, and the user will then know about the problem. And either way, we will
|
||||
have UTF-8 string, which will not break encoding of the list)
|
||||
*/
|
||||
|
||||
// https://stackoverflow.com/questions/6606713/json-encode-non-utf-8-strings
|
||||
if (function_exists('mb_convert_encoding')) {
|
||||
$path = mb_convert_encoding($path, "UTF-8", "Windows-1252");
|
||||
} elseif (function_exists('iconv')) {
|
||||
$path = iconv("CP1252", "UTF-8", $path);
|
||||
} elseif (function_exists('utf8_encode')) {
|
||||
// utf8_encode converts from ISO-8859-1 to UTF-8
|
||||
$path = utf8_encode($path);
|
||||
} else {
|
||||
$path = '[cannot encode this filename to UTF-8]';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
if ($listOptions['flattenList']) {
|
||||
$results[] = $path;
|
||||
} else {
|
||||
$results[] = [
|
||||
'name' => basename($path),
|
||||
'isConverted' => $webpExists
|
||||
];
|
||||
if ($depth > $listOptions['max-depth']) {
|
||||
return $results; // one item is enough to determine that it is not empty
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileIterator->next();
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/*
|
||||
public static function convertFile($source)
|
||||
{
|
||||
$config = Config::loadConfigAndFix();
|
||||
$options = Config::generateWodOptionsFromConfigObj($config);
|
||||
|
||||
$destination = ConvertHelperIndependent::getDestination(
|
||||
$source,
|
||||
$options['destination-folder'],
|
||||
$options['destination-extension'],
|
||||
Paths::getWebPExpressContentDirAbs(),
|
||||
Paths::getUploadDirAbs()
|
||||
);
|
||||
$result = ConvertHelperIndependent::convert($source, $destination, $options);
|
||||
|
||||
//$result['destination'] = $destination;
|
||||
if ($result['success']) {
|
||||
$result['filesize-original'] = @filesize($source);
|
||||
$result['filesize-webp'] = @filesize($destination);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
*/
|
||||
|
||||
public static function processAjaxListUnconvertedFiles()
|
||||
{
|
||||
if (!check_ajax_referer('webpexpress-ajax-list-unconverted-files-nonce', 'nonce', false)) {
|
||||
wp_send_json_error('The security nonce has expired. You need to reload the settings page (press F5) and try again)');
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$config = Config::loadConfigAndFix();
|
||||
$arr = self::getList($config);
|
||||
|
||||
// We use "wp_json_encode" rather than "json_encode" because it handles problems if there is non UTF-8 characters
|
||||
// There should be none, as we have taken our measures, but no harm in taking extra precautions
|
||||
$json = wp_json_encode($arr, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
if ($json === false) {
|
||||
// TODO: We can do better error handling than this!
|
||||
echo '';
|
||||
} else {
|
||||
echo $json;
|
||||
}
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
}
|
||||
272
lib/classes/CLI.php
Normal file
272
lib/classes/CLI.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class CLI extends \WP_CLI_Command
|
||||
{
|
||||
|
||||
private static function printableSize($bytes) {
|
||||
return ($bytes < 10000) ? $bytes . " bytes" : round($bytes / 1024) . ' kb';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert images to webp
|
||||
*
|
||||
* ## OPTIONS
|
||||
* [<location>]
|
||||
* : Limit which folders to process to a single location. Ie "uploads/2021". The first part is the
|
||||
* "image root", which must be "uploads", "themes", "plugins", "wp-content" or "index"
|
||||
*
|
||||
* [--reconvert]
|
||||
* : Even convert images that are already converted (new conversions replaces the old conversions)
|
||||
*
|
||||
* [--only-png]
|
||||
* : Only convert PNG images
|
||||
*
|
||||
* [--only-jpeg]
|
||||
* : Only convert jpeg images
|
||||
*
|
||||
* [--quality]
|
||||
* : Override quality with specified (0-100)
|
||||
*
|
||||
* [--near-lossless]
|
||||
* : Override near-lossless quality with specified (0-100)
|
||||
*
|
||||
* [--alpha-quality]
|
||||
* : Override alpha-quality quality with specified (0-100)
|
||||
*
|
||||
* [--encoding]
|
||||
* : Override encoding quality with specified ("auto", "lossy" or "lossless")
|
||||
*
|
||||
* [--converter=<converter>]
|
||||
* : Specify the converter to use (default is to use the stack). Valid options: cwebp | vips | ewww | imagemagick | imagick | gmagick | graphicsmagick | ffmpeg | gd | wpc | ewww
|
||||
*/
|
||||
public function convert($args, $assoc_args)
|
||||
{
|
||||
$config = Config::loadConfigAndFix();
|
||||
$override = [];
|
||||
|
||||
if (isset($assoc_args['quality'])) {
|
||||
$override['max-quality'] = intval($assoc_args['quality']);
|
||||
$override['png-quality'] = intval($assoc_args['quality']);
|
||||
}
|
||||
if (isset($assoc_args['near-lossless'])) {
|
||||
$override['png-near-lossless'] = intval($assoc_args['near-lossless']);
|
||||
$override['jpeg-near-lossless'] = intval($assoc_args['near-lossless']);
|
||||
}
|
||||
if (isset($assoc_args['alpha-quality'])) {
|
||||
$override['alpha-quality'] = intval($assoc_args['alpha-quality']);
|
||||
}
|
||||
if (isset($assoc_args['encoding'])) {
|
||||
if (!in_array($assoc_args['encoding'], ['auto', 'lossy', 'lossless'])) {
|
||||
\WP_CLI::error('encoding must be auto, lossy or lossless');
|
||||
}
|
||||
$override['png-encoding'] = $assoc_args['encoding'];
|
||||
$override['jpeg-encoding'] = $assoc_args['encoding'];
|
||||
}
|
||||
if (isset($assoc_args['converter'])) {
|
||||
if (!in_array($assoc_args['converter'], ConvertersHelper::getDefaultConverterNames())) {
|
||||
\WP_CLI::error(
|
||||
'"' . $assoc_args['converter'] . '" is not a valid converter id. ' .
|
||||
'Valid converters are: ' . implode(', ', ConvertersHelper::getDefaultConverterNames())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$config = array_merge($config, $override);
|
||||
|
||||
\WP_CLI::log('Converting with the following settings:');
|
||||
\WP_CLI::log('- Lossless quality: ' . $config['png-quality'] . ' for PNG, ' . $config['max-quality'] . " for jpeg");
|
||||
\WP_CLI::log(
|
||||
'- Near lossless: ' .
|
||||
($config['png-enable-near-lossless'] ? $config['png-near-lossless'] : 'disabled') . ' for PNG, ' .
|
||||
($config['jpeg-enable-near-lossless'] ? $config['jpeg-near-lossless'] : 'disabled') . ' for jpeg, '
|
||||
);
|
||||
\WP_CLI::log('- Alpha quality: ' . $config['alpha-quality']);
|
||||
\WP_CLI::log('- Encoding: ' . $config['png-encoding'] . ' for PNG, ' . $config['jpeg-encoding'] . " for jpeg");
|
||||
|
||||
if (count($override) == 0) {
|
||||
\WP_CLI::log('Note that you can override these with --quality=<quality>, etc');
|
||||
}
|
||||
\WP_CLI::log('');
|
||||
|
||||
|
||||
$listOptions = BulkConvert::defaultListOptions($config);
|
||||
if (isset($assoc_args['reconvert'])) {
|
||||
$listOptions['filter']['only-unconverted'] = false;
|
||||
}
|
||||
if (isset($assoc_args['only-png'])) {
|
||||
$listOptions['filter']['image-types'] = 2;
|
||||
}
|
||||
if (isset($assoc_args['only-jpeg'])) {
|
||||
$listOptions['filter']['image-types'] = 1;
|
||||
}
|
||||
|
||||
if (!isset($args[0])) {
|
||||
$groups = BulkConvert::getList($config, $listOptions);
|
||||
foreach($groups as $group){
|
||||
\WP_CLI::log($group['groupName'] . ' contains ' . count($group['files']) . ' ' .
|
||||
(isset($assoc_args['reconvert']) ? '' : 'unconverted ') .
|
||||
'files');
|
||||
}
|
||||
\WP_CLI::log('');
|
||||
} else {
|
||||
$location = $args[0];
|
||||
if (strpos($location, '/') === 0) {
|
||||
$location = substr($location, 1);
|
||||
}
|
||||
if (strpos($location, '/') === false) {
|
||||
$rootId = $location;
|
||||
$path = '.';
|
||||
} else {
|
||||
list($rootId, $path) = explode('/', $location, 2);
|
||||
}
|
||||
|
||||
if (!in_array($rootId, Paths::getImageRootIds())) {
|
||||
\WP_CLI::error(
|
||||
'"' . $args[0] . '" is not a valid image root. ' .
|
||||
'Valid roots are: ' . implode(', ', Paths::getImageRootIds())
|
||||
);
|
||||
}
|
||||
|
||||
$root = Paths::getAbsDirById($rootId) . '/' . $path;
|
||||
if (!file_exists($root)) {
|
||||
\WP_CLI::error(
|
||||
'"' . $args[0] . '" does not exist. '
|
||||
);
|
||||
}
|
||||
$listOptions['root'] = $root;
|
||||
$groups = [
|
||||
[
|
||||
'groupName' => $args[0],
|
||||
'root' => $root,
|
||||
'files' => BulkConvert::getListRecursively('.', $listOptions)
|
||||
]
|
||||
];
|
||||
if (count($groups[0]['files']) == 0) {
|
||||
\WP_CLI::log('Nothing to convert in ' . $args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
$orgTotalFilesize = 0;
|
||||
$webpTotalFilesize = 0;
|
||||
|
||||
$converter = null;
|
||||
$convertOptions = null;
|
||||
|
||||
if (isset($assoc_args['converter'])) {
|
||||
|
||||
$converter = $assoc_args['converter'];
|
||||
$convertOptions = Config::generateWodOptionsFromConfigObj($config)['webp-convert']['convert'];
|
||||
|
||||
// find the converter
|
||||
$optionsForThisConverter = null;
|
||||
foreach ($convertOptions['converters'] as $c) {
|
||||
if ($c['converter'] == $converter) {
|
||||
$optionsForThisConverter = (isset($c['options']) ? $c['options'] : []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_array($optionsForThisConverter)) {
|
||||
\WP_CLI::error('Failed handling options');
|
||||
}
|
||||
|
||||
$convertOptions = array_merge($convertOptions, $optionsForThisConverter);
|
||||
unset($convertOptions['converters']);
|
||||
}
|
||||
|
||||
foreach($groups as $group){
|
||||
if (count($group['files']) == 0) continue;
|
||||
|
||||
\WP_CLI::log('Converting ' . count($group['files']) . ' files in ' . $group['groupName']);
|
||||
\WP_CLI::log('------------------------------');
|
||||
$root = $group['root'];
|
||||
|
||||
$files = array_reverse($group['files']);
|
||||
//echo count($group["files"]);
|
||||
foreach($files as $key => $file)
|
||||
{
|
||||
$path = trailingslashit($group['root']) . $file;
|
||||
\WP_CLI::log('Converting: ' . $file);
|
||||
|
||||
$result = Convert::convertFile($path, $config, $convertOptions, $converter);
|
||||
|
||||
if ($result['success']) {
|
||||
$orgSize = $result['filesize-original'];
|
||||
$webpSize = $result['filesize-webp'];
|
||||
|
||||
$orgTotalFilesize += $orgSize;
|
||||
$webpTotalFilesize += $webpSize;
|
||||
|
||||
//$percentage = round(($orgSize - $webpSize)/$orgSize * 100);
|
||||
$percentage = ($orgSize == 0 ? 100 : round(($webpSize/$orgSize) * 100));
|
||||
|
||||
\WP_CLI::log(
|
||||
\WP_CLI::colorize(
|
||||
"%GOK%n. " .
|
||||
"Size: " .
|
||||
($percentage<90 ? "%G" : ($percentage<100 ? "%Y" : "%R")) .
|
||||
$percentage .
|
||||
"% %nof original" .
|
||||
" (" . self::printableSize($orgSize) . ' => ' . self::printableSize($webpSize) .
|
||||
") "
|
||||
)
|
||||
);
|
||||
//print_r($result);
|
||||
} else {
|
||||
\WP_CLI::log(
|
||||
\WP_CLI::colorize("%RConversion failed. " . $result['msg'] . "%n")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($orgTotalFilesize > 0) {
|
||||
$percentage = ($orgTotalFilesize == 0 ? 100 : round(($webpTotalFilesize/$orgTotalFilesize) * 100));
|
||||
\WP_CLI::log(
|
||||
\WP_CLI::colorize(
|
||||
"Done. " .
|
||||
"Size of webps: " .
|
||||
($percentage<90 ? "%G" : ($percentage<100 ? "%Y" : "%R")) .
|
||||
$percentage .
|
||||
"% %nof original" .
|
||||
" (" . self::printableSize($orgTotalFilesize) . ' => ' . self::printableSize($webpTotalFilesize) .
|
||||
") "
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush webps
|
||||
*
|
||||
* ## OPTIONS
|
||||
* [--only-png]
|
||||
* : Only flush webps that are conversions of a PNG)
|
||||
*/
|
||||
public function flushwebp($args, $assoc_args)
|
||||
{
|
||||
$config = Config::loadConfigAndFix();
|
||||
|
||||
$onlyPng = isset($assoc_args['only-png']);
|
||||
|
||||
if ($onlyPng) {
|
||||
\WP_CLI::log('Flushing webp files that are conversions of PNG images');
|
||||
} else {
|
||||
\WP_CLI::log('Flushing all webp files');
|
||||
}
|
||||
|
||||
$result = CachePurge::purge($config, $onlyPng);
|
||||
|
||||
\WP_CLI::log(
|
||||
\WP_CLI::colorize("%GFlushed " . $result['delete-count'] . " webp files%n")
|
||||
);
|
||||
if ($result['fail-count'] > 0) {
|
||||
\WP_CLI::log(
|
||||
\WP_CLI::colorize("%RFailed deleting " . $result['fail-count'] . " webp files%n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
235
lib/classes/CacheMover.php
Normal file
235
lib/classes/CacheMover.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\FileHelper;
|
||||
use \WebPExpress\PathHelper;
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
class CacheMover
|
||||
{
|
||||
|
||||
public static function getUploadFolder($destinationFolder)
|
||||
{
|
||||
switch ($destinationFolder) {
|
||||
case 'mingled':
|
||||
return Paths::getUploadDirAbs();
|
||||
case 'separate':
|
||||
return Paths::getCacheDirAbs() . '/doc-root/' . Paths::getUploadDirRel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets permission, uid and gid of all subfolders/files of a dir to same as the dir
|
||||
* (but for files, do not set executable flag)
|
||||
*/
|
||||
public static function chmodFixSubDirs($dir, $alsoSetOnDirs)
|
||||
{
|
||||
$dirPerm = FileHelper::filePermWithFallback($dir, 0775);
|
||||
$filePerm = $dirPerm & 0666; // set executable flags to 0
|
||||
/*echo 'dir:' . $dir . "\n";
|
||||
echo 'Dir perm:' . FileHelper::humanReadableFilePerm($dirPerm) . "\n";
|
||||
echo 'File perm:' . FileHelper::humanReadableFilePerm($filePerm) . "\n";*/
|
||||
//return;
|
||||
|
||||
$stat = @stat($dir);
|
||||
$uid = null;
|
||||
$gid = null;
|
||||
if ($stat !== false) {
|
||||
if (isset($stat['uid'])) {
|
||||
$uid = $stat['uid'];
|
||||
}
|
||||
if (isset($stat['gid'])) {
|
||||
$uid = $stat['gid'];
|
||||
}
|
||||
}
|
||||
FileHelper::chmod_r($dir, $dirPerm, $filePerm, $uid, $gid, '#\.webp$#', ($alsoSetOnDirs ? null : '#^$#'));
|
||||
}
|
||||
|
||||
public static function getDestinationFolderForImageRoot($config, $imageRootId)
|
||||
{
|
||||
return Paths::getCacheDirForImageRoot($config['destination-folder'], $config['destination-structure'], $imageRootId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cache because of change in options.
|
||||
* If structure is unchanged, only move the upload folder
|
||||
* Only move those that has an original
|
||||
* Only move those that can be moved.
|
||||
* @return [$numFilesMoved, $numFilesFailedMoving]
|
||||
*/
|
||||
public static function move($newConfig, $oldConfig)
|
||||
{
|
||||
if (!Paths::canUseDocRootForStructuringCacheDir()) {
|
||||
if (($oldConfig['destination-structure'] == 'doc-root') || ($newConfig['destination-structure'] == 'doc-root')) {
|
||||
// oh, well. Seems document root is not available.
|
||||
// so we cannot move from or to that kind of structure
|
||||
// This could happen if document root once was available but now is unavailable
|
||||
return [0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
$changeStructure = ($newConfig['destination-structure'] != $oldConfig['destination-structure']);
|
||||
|
||||
if ($changeStructure) {
|
||||
$rootIds = Paths::getImageRootIds();
|
||||
} else {
|
||||
$rootIds = ['uploads'];
|
||||
}
|
||||
|
||||
$numFilesMovedTotal = 0;
|
||||
$numFilesFailedMovingTotal = 0;
|
||||
foreach ($rootIds as $rootId) {
|
||||
|
||||
$isUploadsMingled = (($newConfig['destination-folder'] == 'mingled') && ($rootId == 'uploads'));
|
||||
|
||||
$fromDir = self::getDestinationFolderForImageRoot($oldConfig, $rootId);
|
||||
$fromExt = $oldConfig['destination-extension'];
|
||||
|
||||
$toDir = self::getDestinationFolderForImageRoot($newConfig, $rootId);
|
||||
$toExt = $newConfig['destination-extension'];
|
||||
|
||||
$srcDir = Paths::getAbsDirById($rootId);
|
||||
|
||||
list($numFilesMoved, $numFilesFailedMoving) = self::moveRecursively($fromDir, $toDir, $srcDir, $fromExt, $toExt);
|
||||
if (!$isUploadsMingled) {
|
||||
FileHelper::removeEmptySubFolders($fromDir);
|
||||
}
|
||||
|
||||
$numFilesMovedTotal += $numFilesMoved;
|
||||
$numFilesFailedMovingTotal += $numFilesFailedMoving;
|
||||
|
||||
$chmodFixFoldersToo = !$isUploadsMingled;
|
||||
self::chmodFixSubDirs($toDir, $chmodFixFoldersToo);
|
||||
}
|
||||
return [$numFilesMovedTotal, $numFilesFailedMovingTotal];
|
||||
/*
|
||||
$fromDir = self::getUploadFolder($oldConfig['destination-folder']);
|
||||
$fromExt = $oldConfig['destination-extension'];
|
||||
|
||||
$toDir = self::getUploadFolder($newConfig['destination-folder']);
|
||||
$toExt = $newConfig['destination-extension'];
|
||||
|
||||
$srcDir = self::getUploadFolder('mingled');
|
||||
|
||||
$result = self::moveRecursively($fromDir, $toDir, $srcDir, $fromExt, $toExt);
|
||||
self::chmodFixSubDirs($toDir, ($newConfig['destination-folder'] == 'separate'));
|
||||
*/
|
||||
|
||||
//return $result;
|
||||
|
||||
// for testing!
|
||||
/*
|
||||
$fromDir = self::getUploadFolder('mingled'); // separate | mingled
|
||||
$toDir = self::getUploadFolder('mingled');
|
||||
$fromExt = 'set'; // set | append
|
||||
$toExt = 'append';
|
||||
|
||||
echo '<pre>';
|
||||
echo 'from: ' . $fromDir . '<br>';
|
||||
echo 'to: ' . $toDir . '<br>';
|
||||
echo 'ext:' . $fromExt . ' => ' . $toExt . '<br>';
|
||||
echo '</pre>';*/
|
||||
|
||||
//error_log('move to:' . $toDir . ' ( ' . (file_exists($toDir) ? 'exists' : 'does not exist ') . ')');
|
||||
|
||||
//self::moveRecursively($toDir, $fromDir, $srcDir, $fromExt, $toExt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [$numFilesMoved, $numFilesFailedMoving]
|
||||
*/
|
||||
public static function moveRecursively($fromDir, $toDir, $srcDir, $fromExt, $toExt)
|
||||
{
|
||||
if (!@is_dir($fromDir)) {
|
||||
return [0, 0];
|
||||
}
|
||||
if (!@file_exists($toDir)) {
|
||||
// Note: 0777 is default. Default umask is 0022, so the default result is 0755
|
||||
if (!@mkdir($toDir, 0777, true)) {
|
||||
return [0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
$numFilesMoved = 0;
|
||||
$numFilesFailedMoving = 0;
|
||||
|
||||
//$filenames = @scandir($fromDir);
|
||||
$fileIterator = new \FilesystemIterator($fromDir);
|
||||
|
||||
//foreach ($filenames as $filename) {
|
||||
while ($fileIterator->valid()) {
|
||||
$filename = $fileIterator->getFilename();
|
||||
|
||||
if (($filename != ".") && ($filename != "..")) {
|
||||
//$filePerm = FileHelper::filePermWithFallback($filename, 0777);
|
||||
|
||||
if (@is_dir($fromDir . "/" . $filename)) {
|
||||
list($r1, $r2) = self::moveRecursively($fromDir . "/" . $filename, $toDir . "/" . $filename, $srcDir . "/" . $filename, $fromExt, $toExt);
|
||||
$numFilesMoved += $r1;
|
||||
$numFilesFailedMoving += $r2;
|
||||
|
||||
// Remove dir, if its empty. But do not remove dirs in srcDir
|
||||
if ($fromDir != $srcDir) {
|
||||
$fileIterator2 = new \FilesystemIterator($fromDir . "/" . $filename);
|
||||
$dirEmpty = !$fileIterator2->valid();
|
||||
if ($dirEmpty) {
|
||||
@rmdir($fromDir . "/" . $filename);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// its a file.
|
||||
// check if its a webp
|
||||
if (strpos($filename, '.webp', strlen($filename) - 5) !== false) {
|
||||
|
||||
$filenameWithoutWebp = substr($filename, 0, strlen($filename) - 5);
|
||||
$srcFilePathWithoutWebp = $srcDir . "/" . $filenameWithoutWebp;
|
||||
|
||||
// check if a corresponding source file exists
|
||||
$newFilename = null;
|
||||
if (($fromExt == 'append') && (@file_exists($srcFilePathWithoutWebp))) {
|
||||
if ($toExt == 'append') {
|
||||
$newFilename = $filename;
|
||||
} else {
|
||||
// remove ".jpg" part of filename (or ".png")
|
||||
$newFilename = preg_replace("/\.(jpe?g|png)\.webp$/", '.webp', $filename);
|
||||
}
|
||||
} elseif ($fromExt == 'set') {
|
||||
if ($toExt == 'set') {
|
||||
if (
|
||||
@file_exists($srcFilePathWithoutWebp . ".jpg") ||
|
||||
@file_exists($srcFilePathWithoutWebp . ".jpeg") ||
|
||||
@file_exists($srcFilePathWithoutWebp . ".png")
|
||||
) {
|
||||
$newFilename = $filename;
|
||||
}
|
||||
} else {
|
||||
// append
|
||||
if (@file_exists($srcFilePathWithoutWebp . ".jpg")) {
|
||||
$newFilename = $filenameWithoutWebp . ".jpg.webp";
|
||||
} elseif (@file_exists($srcFilePathWithoutWebp . ".jpeg")) {
|
||||
$newFilename = $filenameWithoutWebp . ".jpeg.webp";
|
||||
} elseif (@file_exists($srcFilePathWithoutWebp . ".png")) {
|
||||
$newFilename = $filenameWithoutWebp . ".png.webp";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($newFilename !== null) {
|
||||
//echo 'moving to: ' . $toDir . '/' .$newFilename . "<br>";
|
||||
$toFilename = $toDir . "/" . $newFilename;
|
||||
if (@rename($fromDir . "/" . $filename, $toFilename)) {
|
||||
$numFilesMoved++;
|
||||
} else {
|
||||
$numFilesFailedMoving++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileIterator->next();
|
||||
}
|
||||
return [$numFilesMoved, $numFilesFailedMoving];
|
||||
}
|
||||
|
||||
}
|
||||
171
lib/classes/CachePurge.php
Normal file
171
lib/classes/CachePurge.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Convert;
|
||||
use \WebPExpress\FileHelper;
|
||||
use \WebPExpress\DismissableMessages;
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
// TODO! Needs to be updated to work with the new "destination-structure" setting
|
||||
|
||||
class CachePurge
|
||||
{
|
||||
|
||||
/**
|
||||
* - Removes cache dir
|
||||
* - Removes all files with ".webp" extension in upload dir (if set to mingled)
|
||||
*/
|
||||
public static function purge($config, $onlyPng)
|
||||
{
|
||||
DismissableMessages::dismissMessage('0.14.0/suggest-wipe-because-lossless');
|
||||
|
||||
$filter = [
|
||||
'only-png' => $onlyPng,
|
||||
'only-with-corresponding-original' => false
|
||||
];
|
||||
|
||||
$numDeleted = 0;
|
||||
$numFailed = 0;
|
||||
|
||||
list($numDeleted, $numFailed) = self::purgeWebPFilesInDir(Paths::getCacheDirAbs(), $filter, $config);
|
||||
FileHelper::removeEmptySubFolders(Paths::getCacheDirAbs());
|
||||
|
||||
if ($config['destination-folder'] == 'mingled') {
|
||||
list($d, $f) = self::purgeWebPFilesInDir(Paths::getUploadDirAbs(), $filter, $config);
|
||||
|
||||
$numDeleted += $d;
|
||||
$numFailed += $f;
|
||||
}
|
||||
|
||||
// Now, purge dummy files too
|
||||
$dir = Paths::getBiggerThanSourceDirAbs();
|
||||
self::purgeWebPFilesInDir($dir, $filter, $config);
|
||||
FileHelper::removeEmptySubFolders($dir);
|
||||
|
||||
return [
|
||||
'delete-count' => $numDeleted,
|
||||
'fail-count' => $numFailed
|
||||
];
|
||||
|
||||
//$successInRemovingCacheDir = FileHelper::rrmdir(Paths::getCacheDirAbs());
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Purge webp files in a dir
|
||||
* Warning: the "only-png" option only works for mingled mode.
|
||||
* (when not mingled, you can simply delete the whole cache dir instead)
|
||||
*
|
||||
* @param $filter.
|
||||
* only-png: If true, it will only be deleted if extension is .png.webp or a corresponding png exists.
|
||||
*
|
||||
* @return [num files deleted, num files failed to delete]
|
||||
*/
|
||||
private static function purgeWebPFilesInDir($dir, &$filter, &$config)
|
||||
{
|
||||
if (!@file_exists($dir) || !@is_dir($dir)) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
$numFilesDeleted = 0;
|
||||
$numFilesFailedDeleting = 0;
|
||||
|
||||
$fileIterator = new \FilesystemIterator($dir);
|
||||
while ($fileIterator->valid()) {
|
||||
$filename = $fileIterator->getFilename();
|
||||
|
||||
if (($filename != ".") && ($filename != "..")) {
|
||||
|
||||
if (@is_dir($dir . "/" . $filename)) {
|
||||
list($r1, $r2) = self::purgeWebPFilesInDir($dir . "/" . $filename, $filter, $config);
|
||||
$numFilesDeleted += $r1;
|
||||
$numFilesFailedDeleting += $r2;
|
||||
} else {
|
||||
|
||||
// its a file
|
||||
// Run through filters, which each may set "skipThis" to true
|
||||
|
||||
$skipThis = false;
|
||||
|
||||
// filter: It must be a webp
|
||||
if (!$skipThis && !preg_match('#\.webp$#', $filename)) {
|
||||
$skipThis = true;
|
||||
}
|
||||
|
||||
// filter: only with corresponding original
|
||||
$source = '';
|
||||
if (!$skipThis && $filter['only-with-corresponding-original']) {
|
||||
$source = Convert::findSource($dir . "/" . $filename, $config);
|
||||
if ($source === false) {
|
||||
$skipThis = true;
|
||||
}
|
||||
}
|
||||
|
||||
// filter: only png
|
||||
if (!$skipThis && $filter['only-png']) {
|
||||
|
||||
// turn logic around - we skip deletion, unless we deem it a png
|
||||
$skipThis = true;
|
||||
|
||||
// If extension is "png.webp", its a png
|
||||
if (preg_match('#\.png\.webp$#', $filename)) {
|
||||
// its a png
|
||||
$skipThis = false;
|
||||
} else {
|
||||
if (preg_match('#\.jpe?g\.webp$#', $filename)) {
|
||||
// It is a jpeg, no need to investigate further.
|
||||
} else {
|
||||
|
||||
if (!$filter['only-with-corresponding-original']) {
|
||||
$source = Convert::findSource($dir . "/" . $filename, $config);
|
||||
}
|
||||
if ($source === false) {
|
||||
// We could not find corresponding source.
|
||||
// Should we delete?
|
||||
// No, I guess we need more evidence, so we skip
|
||||
// In the future, we could detect mime
|
||||
} else {
|
||||
if (preg_match('#\.png$#', $source)) {
|
||||
// its a png
|
||||
$skipThis = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!$skipThis) {
|
||||
if (@unlink($dir . "/" . $filename)) {
|
||||
$numFilesDeleted++;
|
||||
} else {
|
||||
$numFilesFailedDeleting++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileIterator->next();
|
||||
}
|
||||
return [$numFilesDeleted, $numFilesFailedDeleting];
|
||||
}
|
||||
|
||||
public static function processAjaxPurgeCache()
|
||||
{
|
||||
|
||||
if (!check_ajax_referer('webpexpress-ajax-purge-cache-nonce', 'nonce', false)) {
|
||||
wp_send_json_error('The security nonce has expired. You need to reload the settings page (press F5) and try again)');
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$onlyPng = (sanitize_text_field($_POST['only-png']) == 'true');
|
||||
|
||||
$config = Config::loadConfigAndFix();
|
||||
$result = self::purge($config, $onlyPng);
|
||||
|
||||
echo json_encode($result, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
|
||||
wp_die();
|
||||
}
|
||||
}
|
||||
103
lib/classes/CapabilityTest.php
Normal file
103
lib/classes/CapabilityTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/*
|
||||
This functionality will be moved to a separate project.
|
||||
|
||||
Btw:
|
||||
Seems someone else got similar idea:
|
||||
http://christian.roy.name/blog/detecting-modrewrite-using-php
|
||||
*/
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\FileHelper;
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
class CapabilityTest
|
||||
{
|
||||
|
||||
public static function copyCapabilityTestsToWpContent()
|
||||
{
|
||||
return FileHelper::cpdir(Paths::getWebPExpressPluginDirAbs() . '/htaccess-capability-tests', Paths::getWebPExpressContentDirAbs() . '/htaccess-capability-tests');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run one of the tests in wp-content/webp-express/capability-tests
|
||||
* Three possible outcomes: true, false or null (null if request fails)
|
||||
*/
|
||||
public static function runTest($testDir)
|
||||
{
|
||||
//echo 'running test:' . $testDir . '<br>';
|
||||
if (!@file_exists(Paths::getWebPExpressPluginDirAbs() . '/htaccess-capability-tests/' . $testDir)) {
|
||||
// test does not even exist
|
||||
//echo 'test does not exist: ' . $testDir . '<br>';
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!@file_exists(Paths::getWebPExpressContentDirAbs() . '/htaccess-capability-tests/' . $testDir)) {
|
||||
self::copyCapabilityTestsToWpContent();
|
||||
}
|
||||
|
||||
// If copy failed, we can use the test in plugin path
|
||||
if (!@file_exists(Paths::getWebPExpressContentDirAbs() . '/htaccess-capability-tests/' . $testDir)) {
|
||||
$testUrl = Paths::getContentUrl() . '/' . 'webp-express/htaccess-capability-tests/' . $testDir . '/test.php';
|
||||
} else {
|
||||
$testUrl = Paths::getWebPExpressPluginUrl() . '/' . 'htaccess-capability-tests/' . $testDir . '/test.php';
|
||||
}
|
||||
|
||||
//echo 'test url: ' . $testUrl . '<br>';
|
||||
// TODO: Should we test if wp_remote_get exists first? - and if not, include wp-includes/http.php ?
|
||||
|
||||
$response = wp_remote_get($testUrl, ['timeout' => 10]);
|
||||
//echo '<pre>' . print_r($response, true) . '</pre>';
|
||||
if (wp_remote_retrieve_response_code($response) != '200') {
|
||||
return null;
|
||||
}
|
||||
$responseBody = wp_remote_retrieve_body($response);
|
||||
if ($responseBody == '') {
|
||||
return null; // Some failure
|
||||
}
|
||||
if ($responseBody == '0') {
|
||||
return false;
|
||||
}
|
||||
if ($responseBody == '1') {
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Three possible outcomes: true, false or null (null if failed to run test)
|
||||
*/
|
||||
public static function modRewriteWorking()
|
||||
{
|
||||
return self::runTest('has-mod-rewrite');
|
||||
}
|
||||
|
||||
/**
|
||||
* Three possible outcomes: true, false or null (null if failed to run test)
|
||||
*/
|
||||
public static function modHeaderWorking()
|
||||
{
|
||||
return self::runTest('has-mod-header');
|
||||
}
|
||||
|
||||
/**
|
||||
* Three possible outcomes: true, false or null (null if failed to run test)
|
||||
*/
|
||||
public static function passThroughEnvWorking()
|
||||
{
|
||||
return self::runTest('pass-through-environment-var');
|
||||
}
|
||||
|
||||
/**
|
||||
* Three possible outcomes: true, false or null (null if failed to run test)
|
||||
*/
|
||||
public static function passThroughHeaderWorking()
|
||||
{
|
||||
// pretend it fails because .htaccess rules aren't currently generated correctly
|
||||
return false;
|
||||
return self::runTest('pass-server-var-through-header');
|
||||
}
|
||||
|
||||
}
|
||||
755
lib/classes/Config.php
Normal file
755
lib/classes/Config.php
Normal file
@@ -0,0 +1,755 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class Config
|
||||
{
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return object|false Returns config object if config file exists and can be read. Otherwise it returns false
|
||||
*/
|
||||
public static function loadConfig()
|
||||
{
|
||||
return FileHelper::loadJSONOptions(Paths::getConfigFileName());
|
||||
}
|
||||
|
||||
public static function getDefaultConfig($skipQualityAuto = false) {
|
||||
if ($skipQualityAuto) {
|
||||
$qualityAuto = null;
|
||||
} else {
|
||||
$qualityAuto = TestRun::isLocalQualityDetectionWorking();
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
'operation-mode' => 'varied-image-responses',
|
||||
|
||||
// general
|
||||
'image-types' => 3,
|
||||
'destination-folder' => 'separate',
|
||||
'destination-extension' => 'append',
|
||||
'destination-structure' => (PlatformInfo::isNginx() ? 'doc-root' : 'image-roots'),
|
||||
'cache-control' => 'no-header', /* can be "no-header", "set" or "custom" */
|
||||
'cache-control-custom' => 'public, max-age=31536000, stale-while-revalidate=604800, stale-if-error=604800',
|
||||
'cache-control-max-age' => 'one-week',
|
||||
'cache-control-public' => false,
|
||||
'scope' => ['themes', 'uploads'],
|
||||
'enable-logging' => false,
|
||||
'prevent-using-webps-larger-than-original' => true,
|
||||
|
||||
// redirection rules
|
||||
'enable-redirection-to-converter' => true,
|
||||
'only-redirect-to-converter-on-cache-miss' => false,
|
||||
'only-redirect-to-converter-for-webp-enabled-browsers' => true,
|
||||
'do-not-pass-source-in-query-string' => false, // In 0.13 we can remove this. migration7.php depends on it
|
||||
'redirect-to-existing-in-htaccess' => true,
|
||||
'forward-query-string' => false,
|
||||
'enable-redirection-to-webp-realizer' => true,
|
||||
|
||||
// conversion options
|
||||
'jpeg-encoding' => 'auto',
|
||||
'jpeg-enable-near-lossless' => true,
|
||||
'jpeg-near-lossless' => 60,
|
||||
'quality-auto' => $qualityAuto,
|
||||
'max-quality' => 80,
|
||||
'quality-specific' => 70,
|
||||
|
||||
'png-encoding' => 'auto',
|
||||
'png-enable-near-lossless' => true,
|
||||
'png-near-lossless' => 60,
|
||||
'png-quality' => 85,
|
||||
'alpha-quality' => 80,
|
||||
|
||||
'converters' => [],
|
||||
'metadata' => 'none',
|
||||
//'log-call-arguments' => true,
|
||||
'convert-on-upload' => false,
|
||||
|
||||
// serve options
|
||||
'fail' => 'original',
|
||||
'success-response' => 'converted',
|
||||
|
||||
// alter html options
|
||||
'alter-html' => [
|
||||
'enabled' => false,
|
||||
'replacement' => 'picture', // "picture" or "url"
|
||||
'hooks' => 'ob', // "content-hooks" or "ob"
|
||||
'only-for-webp-enabled-browsers' => true, // If true, there will be two HTML versions of each page
|
||||
'only-for-webps-that-exists' => false,
|
||||
'alter-html-add-picturefill-js' => true,
|
||||
'hostname-aliases' => []
|
||||
],
|
||||
|
||||
// web service
|
||||
'web-service' => [
|
||||
'enabled' => false,
|
||||
'whitelist' => [
|
||||
/*[
|
||||
'uid' => '', // for internal purposes
|
||||
'label' => '', // ie website name. It is just for display
|
||||
'ip' => '', // restrict to these ips. * pattern is allowed.
|
||||
'api-key' => '', // Api key for the entry. Not neccessarily unique for the entry
|
||||
//'quota' => 60
|
||||
]
|
||||
*/
|
||||
]
|
||||
],
|
||||
|
||||
'environment-when-config-was-saved' => [
|
||||
'doc-root-available' => null, // null means unavailable
|
||||
'doc-root-resolvable' => null,
|
||||
'doc-root-usable-for-structuring' => null,
|
||||
'image-roots' => null,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply operation mode (set the hidden defaults that comes along with the mode)
|
||||
* @return An altered configuration array
|
||||
*/
|
||||
public static function applyOperationMode($config)
|
||||
{
|
||||
if (!isset($config['operation-mode'])) {
|
||||
$config['operation-mode'] = 'varied-image-responses';
|
||||
}
|
||||
|
||||
if ($config['operation-mode'] == 'varied-image-responses') {
|
||||
$config = array_merge($config, [
|
||||
//'redirect-to-existing-in-htaccess' => true, // this can now be configured, so do not apply
|
||||
//'enable-redirection-to-converter' => true, // this can now be configured, so do not apply
|
||||
'only-redirect-to-converter-for-webp-enabled-browsers' => true,
|
||||
'only-redirect-to-converter-on-cache-miss' => false,
|
||||
'do-not-pass-source-in-query-string' => true, // Will be removed in 0.13
|
||||
'fail' => 'original',
|
||||
'success-response' => 'converted',
|
||||
]);
|
||||
} elseif ($config['operation-mode'] == 'cdn-friendly') {
|
||||
$config = array_merge($config, [
|
||||
'redirect-to-existing-in-htaccess' => false,
|
||||
'enable-redirection-to-converter' => false,
|
||||
/*
|
||||
'only-redirect-to-converter-for-webp-enabled-browsers' => false,
|
||||
'only-redirect-to-converter-on-cache-miss' => true,
|
||||
*/
|
||||
'do-not-pass-source-in-query-string' => true, // Will be removed in 0.13
|
||||
'fail' => 'original',
|
||||
'success-response' => 'original',
|
||||
// cache-control => 'no-header' (we do not need this, as it is not important what it is set to in cdn-friendly mode, and we dont the value to be lost when switching operation mode)
|
||||
]);
|
||||
} elseif ($config['operation-mode'] == 'no-conversion') {
|
||||
|
||||
// TODO: Go through these...
|
||||
|
||||
$config = array_merge($config, [
|
||||
'enable-redirection-to-converter' => false,
|
||||
'destination-folder' => 'mingled',
|
||||
'enable-redirection-to-webp-realizer' => false,
|
||||
]);
|
||||
$config['alter-html']['only-for-webps-that-exists'] = true;
|
||||
$config['web-service']['enabled'] = false;
|
||||
$config['scope'] = ['uploads'];
|
||||
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix config.
|
||||
*
|
||||
* Among other things, the config is merged with default config, to ensure all options are present
|
||||
*
|
||||
*/
|
||||
public static function fix($config, $checkQualityDetection = true)
|
||||
{
|
||||
if ($config === false) {
|
||||
$config = self::getDefaultConfig(!$checkQualityDetection);
|
||||
} else {
|
||||
if ($checkQualityDetection) {
|
||||
if (isset($config['quality-auto']) && ($config['quality-auto'])) {
|
||||
$qualityDetectionWorking = TestRun::isLocalQualityDetectionWorking();
|
||||
if (!TestRun::isLocalQualityDetectionWorking()) {
|
||||
$config['quality-auto'] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
$defaultConfig = self::getDefaultConfig(true);
|
||||
$config = array_merge($defaultConfig, $config);
|
||||
|
||||
// Make sure new defaults below "alter-html" are added into the existing array
|
||||
// (note that this will not remove old unused properties, if some key should become obsolete)
|
||||
$config['alter-html'] = array_replace_recursive($defaultConfig['alter-html'], $config['alter-html']);
|
||||
|
||||
// Make sure new defaults below "environment-when-config-was-saved" are added into the existing array
|
||||
$config['environment-when-config-was-saved'] = array_replace_recursive($defaultConfig['environment-when-config-was-saved'], $config['environment-when-config-was-saved']);
|
||||
}
|
||||
|
||||
if (!isset($config['base-htaccess-on-these-capability-tests'])) {
|
||||
self::runAndStoreCapabilityTests($config);
|
||||
}
|
||||
|
||||
// Apparently, migrate7 did not fix old "operation-mode" values for all.
|
||||
// So fix here
|
||||
if ($config['operation-mode'] == 'just-redirect') {
|
||||
$config['operation-mode'] = 'no-conversion';
|
||||
}
|
||||
if ($config['operation-mode'] == 'no-varied-responses') {
|
||||
$config['operation-mode'] = 'cdn-friendly';
|
||||
}
|
||||
if ($config['operation-mode'] == 'varied-responses') {
|
||||
$config['operation-mode'] = 'varied-image-responses';
|
||||
}
|
||||
|
||||
// In case doc root no longer can be used, use image-roots
|
||||
// Or? No, changing here will not fix it for WebPOnDemand.php.
|
||||
// An invalid setting requires that config is saved again and .htaccess files regenerated.
|
||||
/*
|
||||
if (($config['operation-mode'] == 'doc-root') && (!Paths::canUseDocRootForRelPaths())) {
|
||||
$config['destination-structure'] = 'image-roots';
|
||||
}*/
|
||||
|
||||
$config = self::applyOperationMode($config);
|
||||
|
||||
// Fix scope: Remove invalid and put in correct order
|
||||
$fixedScope = [];
|
||||
foreach (Paths::getImageRootIds() as $rootId) {
|
||||
if (in_array($rootId, $config['scope'])) {
|
||||
$fixedScope[] = $rootId;
|
||||
}
|
||||
}
|
||||
$config['scope'] = $fixedScope;
|
||||
|
||||
if (!isset($config['web-service'])) {
|
||||
$config['web-service'] = [
|
||||
'enabled' => false
|
||||
];
|
||||
}
|
||||
if (!is_array($config['web-service']['whitelist'])) {
|
||||
$config['web-service']['whitelist'] = [];
|
||||
}
|
||||
// remove whitelist entries without required fields (label, ip)
|
||||
$config['web-service']['whitelist'] = array_filter($config['web-service']['whitelist'], function($var) {
|
||||
return (isset($var['label']) && (isset($var['ip'])));
|
||||
});
|
||||
|
||||
if (($config['cache-control'] == 'set') && ($config['cache-control-max-age'] == '')) {
|
||||
$config['cache-control-max-age'] = 'one-week';
|
||||
}
|
||||
|
||||
/*if (is_null($config['alter-html']['hostname-aliases'])) {
|
||||
$config['alter-html']['hostname-aliases'] = [];
|
||||
}*/
|
||||
|
||||
if (!is_array($config['converters'])) {
|
||||
$config['converters'] = [];
|
||||
}
|
||||
|
||||
if (count($config['converters']) > 0) {
|
||||
// merge missing converters in
|
||||
$config['converters'] = ConvertersHelper::mergeConverters(
|
||||
$config['converters'],
|
||||
ConvertersHelper::$defaultConverters
|
||||
);
|
||||
} else {
|
||||
// This is first time visit!
|
||||
$config['converters'] = ConvertersHelper::$defaultConverters;
|
||||
}
|
||||
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
|
||||
public static function runAndStoreCapabilityTests(&$config)
|
||||
{
|
||||
$config['base-htaccess-on-these-capability-tests'] = [
|
||||
'passThroughHeaderWorking' => HTAccessCapabilityTestRunner::passThroughHeaderWorking(),
|
||||
'passThroughEnvWorking' => HTAccessCapabilityTestRunner::passThroughEnvWorking(),
|
||||
'modHeaderWorking' => HTAccessCapabilityTestRunner::modHeaderWorking(),
|
||||
//'grantAllAllowed' => HTAccessCapabilityTestRunner::grantAllAllowed(),
|
||||
'canRunTestScriptInWOD' => HTAccessCapabilityTestRunner::canRunTestScriptInWOD(),
|
||||
'canRunTestScriptInWOD2' => HTAccessCapabilityTestRunner::canRunTestScriptInWOD2(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Config (if available), fills in the rest with defaults
|
||||
* also applies operation mode.
|
||||
* If config is not saved yet, the default config will be returned
|
||||
*/
|
||||
public static function loadConfigAndFix($checkQualityDetection = true)
|
||||
{
|
||||
// PS: Yes, loadConfig may return false. "fix" handles this by returning default config
|
||||
return self::fix(Config::loadConfig(), $checkQualityDetection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a fresh test on all converters and update their statuses in the config.
|
||||
*
|
||||
* @param object config to be updated
|
||||
* @return object Updated config
|
||||
*/
|
||||
public static function updateConverterStatusWithFreshTest($config) {
|
||||
// Test converters
|
||||
$testResult = TestRun::getConverterStatus();
|
||||
|
||||
// Set "working" and "error" properties
|
||||
if ($testResult) {
|
||||
foreach ($config['converters'] as &$converter) {
|
||||
$converterId = $converter['converter'];
|
||||
$hasError = isset($testResult['errors'][$converterId]);
|
||||
$hasWarning = isset($testResult['warnings'][$converterId]);
|
||||
$working = !$hasError;
|
||||
|
||||
/*
|
||||
Don't print this stuff here. It can end up in the head tag.
|
||||
TODO: Move it somewhere
|
||||
if (isset($converter['working']) && ($converter['working'] != $working)) {
|
||||
|
||||
// TODO: webpexpress_converterName($converterId)
|
||||
if ($working) {
|
||||
Messenger::printMessage(
|
||||
'info',
|
||||
'Hurray! - The <i>' . $converterId . '</i> conversion method is working now!'
|
||||
);
|
||||
} else {
|
||||
Messenger::printMessage(
|
||||
'warning',
|
||||
'Sad news. The <i>' . $converterId . '</i> conversion method is not working anymore. What happened?'
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
$converter['working'] = $working;
|
||||
if ($hasError) {
|
||||
$error = $testResult['errors'][$converterId];
|
||||
if ($converterId == 'wpc') {
|
||||
if (preg_match('/Missing URL/', $error)) {
|
||||
$error = 'Not configured';
|
||||
}
|
||||
if ($error == 'No remote host has been set up') {
|
||||
$error = 'Not configured';
|
||||
}
|
||||
|
||||
if (preg_match('/cloud service is not enabled/', $error)) {
|
||||
$error = 'The server is not enabled. Click the "Enable web service" on WebP Express settings on the site you are trying to connect to.';
|
||||
}
|
||||
}
|
||||
$converter['error'] = $error;
|
||||
} else {
|
||||
unset($converter['error']);
|
||||
}
|
||||
if ($hasWarning) {
|
||||
$converter['warnings'] = $testResult['warnings'][$converterId];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
|
||||
public static $configForOptionsPage = null; // cache the result (called twice, - also in enqueue_scripts)
|
||||
public static function getConfigForOptionsPage()
|
||||
{
|
||||
if (isset(self::$configForOptionsPage)) {
|
||||
return self::$configForOptionsPage;
|
||||
}
|
||||
|
||||
|
||||
$config = self::loadConfigAndFix();
|
||||
|
||||
// Remove keys in whitelist (so they cannot easily be picked up by examining the html)
|
||||
foreach ($config['web-service']['whitelist'] as &$whitelistEntry) {
|
||||
unset($whitelistEntry['api-key']);
|
||||
}
|
||||
|
||||
// Remove keys from WPC converters
|
||||
foreach ($config['converters'] as &$converter) {
|
||||
if (isset($converter['converter']) && ($converter['converter'] == 'wpc')) {
|
||||
if (isset($converter['options']['api-key'])) {
|
||||
if ($converter['options']['api-key'] != '') {
|
||||
$converter['options']['_api-key-non-empty'] = true;
|
||||
}
|
||||
unset($converter['options']['api-key']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($config['operation-mode'] != 'no-conversion') {
|
||||
$config = self::updateConverterStatusWithFreshTest($config);
|
||||
}
|
||||
|
||||
self::$configForOptionsPage = $config; // cache the result
|
||||
return $config;
|
||||
}
|
||||
|
||||
public static function isConfigFileThere()
|
||||
{
|
||||
return (FileHelper::fileExists(Paths::getConfigFileName()));
|
||||
}
|
||||
|
||||
public static function isConfigFileThereAndOk()
|
||||
{
|
||||
return (self::loadConfig() !== false);
|
||||
}
|
||||
|
||||
public static function loadWodOptions()
|
||||
{
|
||||
return FileHelper::loadJSONOptions(Paths::getWodOptionsFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Some of the options in config needs to be quickly accessible
|
||||
* These are stored in wordpress autoloaded options
|
||||
*/
|
||||
public static function updateAutoloadedOptions($config)
|
||||
{
|
||||
$config = self::fix($config, false);
|
||||
|
||||
Option::updateOption('webp-express-alter-html', $config['alter-html']['enabled'], true);
|
||||
Option::updateOption('webp-express-alter-html-hooks', $config['alter-html']['hooks'], true);
|
||||
Option::updateOption('webp-express-alter-html-replacement', $config['alter-html']['replacement'], true);
|
||||
Option::updateOption('webp-express-alter-html-add-picturefill-js', (($config['alter-html']['replacement'] == 'picture') && (isset($config['alter-html']['alter-html-add-picturefill-js']) && $config['alter-html']['alter-html-add-picturefill-js'])), true);
|
||||
|
||||
|
||||
//Option::updateOption('webp-express-alter-html', $config['alter-html']['enabled'], true);
|
||||
|
||||
$obj = $config['alter-html'];
|
||||
unset($obj['enabled']);
|
||||
$obj['destination-folder'] = $config['destination-folder'];
|
||||
$obj['destination-extension'] = $config['destination-extension'];
|
||||
$obj['destination-structure'] = $config['destination-structure'];
|
||||
$obj['scope'] = $config['scope'];
|
||||
$obj['image-types'] = $config['image-types']; // 0=none,1=jpg, 2=png, 3=both
|
||||
$obj['prevent-using-webps-larger-than-original'] = $config['prevent-using-webps-larger-than-original'];
|
||||
|
||||
Option::updateOption(
|
||||
'webp-express-alter-html-options',
|
||||
json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration file. Also updates autoloaded options (such as alter html options)
|
||||
*/
|
||||
public static function saveConfigurationFile($config)
|
||||
{
|
||||
$config['paths-used-in-htaccess'] = [
|
||||
'wod-url-path' => Paths::getWodUrlPath(),
|
||||
];
|
||||
|
||||
if (Paths::createConfigDirIfMissing()) {
|
||||
$success = FileHelper::saveJSONOptions(Paths::getConfigFileName(), $config);
|
||||
if ($success) {
|
||||
State::setState('configured', true);
|
||||
self::updateAutoloadedOptions($config);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getCacheControlHeader($config) {
|
||||
$cacheControl = $config['cache-control'];
|
||||
switch ($cacheControl) {
|
||||
case 'custom':
|
||||
return $config['cache-control-custom'];
|
||||
case 'no-header':
|
||||
return '';
|
||||
default:
|
||||
$public = (isset($config['cache-control-public']) ? $config['cache-control-public'] : true);
|
||||
$maxAge = (isset($config['cache-control-max-age']) ? $config['cache-control-max-age'] : $cacheControl);
|
||||
$maxAgeOptions = [
|
||||
'' => 'max-age=604800', // it has happened, but I don't think it can happen again...
|
||||
'one-second' => 'max-age=1',
|
||||
'one-minute' => 'max-age=60',
|
||||
'one-hour' => 'max-age=3600',
|
||||
'one-day' => 'max-age=86400',
|
||||
'one-week' => 'max-age=604800',
|
||||
'one-month' => 'max-age=2592000',
|
||||
'one-year' => 'max-age=31536000',
|
||||
];
|
||||
return ($public ? 'public, ' : 'private, ') . $maxAgeOptions[$maxAge];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function generateWodOptionsFromConfigObj($config)
|
||||
{
|
||||
|
||||
// WebP convert options
|
||||
// --------------------
|
||||
$wc = [
|
||||
'converters' => []
|
||||
];
|
||||
|
||||
// Add active converters
|
||||
foreach ($config['converters'] as $converter) {
|
||||
if (isset($converter['deactivated']) && ($converter['deactivated'])) {
|
||||
continue;
|
||||
}
|
||||
$wc['converters'][] = $converter;
|
||||
}
|
||||
|
||||
// Clean the converter options from junk
|
||||
foreach ($wc['converters'] as &$c) {
|
||||
|
||||
// In cwebp converter options (here in webp express), we have a checkbox "set size"
|
||||
// - there is no such option in webp-convert - so remove.
|
||||
if ($c['converter'] == 'cwebp') {
|
||||
if (isset($c['options']['set-size']) && $c['options']['set-size']) {
|
||||
unset($c['options']['set-size']);
|
||||
} else {
|
||||
unset($c['options']['set-size']);
|
||||
unset($c['options']['size-in-percentage']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($c['converter'] == 'ewww') {
|
||||
$c['options']['check-key-status-before-converting'] = false;
|
||||
}
|
||||
|
||||
// 'id', 'working' and 'error' attributes are used internally in webp-express,
|
||||
// no need to have it in the wod configuration file.
|
||||
unset ($c['id']);
|
||||
unset($c['working']);
|
||||
unset($c['error']);
|
||||
|
||||
if (isset($c['options']['quality']) && ($c['options']['quality'] == 'inherit')) {
|
||||
unset ($c['options']['quality']);
|
||||
}
|
||||
/*
|
||||
if (!isset($c['options'])) {
|
||||
$c = $c['converter'];
|
||||
}*/
|
||||
}
|
||||
|
||||
// Create jpeg options
|
||||
// https://github.com/rosell-dk/webp-convert/blob/master/docs/v2.0/converting/introduction-for-converting.md#png-og-jpeg-specific-options
|
||||
|
||||
$auto = (isset($config['quality-auto']) && $config['quality-auto']);
|
||||
$wc['jpeg'] = [
|
||||
'encoding' => $config['jpeg-encoding'],
|
||||
'quality' => ($auto ? 'auto' : $config['quality-specific']),
|
||||
];
|
||||
if ($auto) {
|
||||
$wc['jpeg']['default-quality'] = $config['quality-specific'];
|
||||
$wc['jpeg']['max-quality'] = $config['max-quality'];
|
||||
}
|
||||
if ($config['jpeg-encoding'] != 'lossy') {
|
||||
if ($config['jpeg-enable-near-lossless']) {
|
||||
$wc['jpeg']['near-lossless'] = $config['jpeg-near-lossless'];
|
||||
} else {
|
||||
$wc['jpeg']['near-lossless'] = 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Create png options
|
||||
// ---
|
||||
$wc['png'] = [
|
||||
'encoding' => $config['png-encoding'],
|
||||
'quality' => $config['png-quality'],
|
||||
];
|
||||
if ($config['png-encoding'] != 'lossy') {
|
||||
if ($config['png-enable-near-lossless']) {
|
||||
$wc['png']['near-lossless'] = $config['png-near-lossless'];
|
||||
} else {
|
||||
$wc['png']['near-lossless'] = 100;
|
||||
}
|
||||
}
|
||||
if ($config['png-encoding'] != 'lossless') {
|
||||
// Only relevant for pngs, and only for "lossy" (and thus also "auto")
|
||||
$wc['png']['alpha-quality'] = $config['alpha-quality'];
|
||||
}
|
||||
|
||||
// Other convert options
|
||||
$wc['metadata'] = $config['metadata'];
|
||||
$wc['log-call-arguments'] = true; // $config['log-call-arguments'];
|
||||
|
||||
// Serve options
|
||||
// -------------
|
||||
$serve = [
|
||||
'serve-image' => [
|
||||
'headers' => [
|
||||
'cache-control' => false,
|
||||
'content-length' => true,
|
||||
'content-type' => true,
|
||||
'expires' => false,
|
||||
'last-modified' => true,
|
||||
//'vary-accept' => false // This must be different for webp-on-demand and webp-realizer
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($config['cache-control'] != 'no-header') {
|
||||
$serve['serve-image']['cache-control-header'] = self::getCacheControlHeader($config);
|
||||
$serve['serve-image']['headers']['cache-control'] = true;
|
||||
$serve['serve-image']['headers']['expires'] = true;
|
||||
}
|
||||
$serve['fail'] = $config['fail'];
|
||||
|
||||
|
||||
// WOD options
|
||||
// -------------
|
||||
$wod = [
|
||||
'enable-logging' => $config['enable-logging'],
|
||||
'enable-redirection-to-converter' => $config['enable-redirection-to-converter'],
|
||||
'enable-redirection-to-webp-realizer' => $config['enable-redirection-to-webp-realizer'],
|
||||
'base-htaccess-on-these-capability-tests' => $config['base-htaccess-on-these-capability-tests'],
|
||||
'destination-extension' => $config['destination-extension'],
|
||||
'destination-folder' => $config['destination-folder'],
|
||||
'forward-query-string' => $config['forward-query-string'],
|
||||
//'method-for-passing-source' => $config['method-for-passing-source'],
|
||||
'image-roots' => Paths::getImageRootsDef(),
|
||||
'success-response' => $config['success-response'],
|
||||
];
|
||||
|
||||
|
||||
// Put it all together
|
||||
// -------------
|
||||
|
||||
//$options = array_merge($wc, $serve, $wod);
|
||||
|
||||
// I'd like to put the webp-convert options in its own key,
|
||||
// but it requires some work. Postponing it to another day that I can uncomment the two next lines (and remove the one above)
|
||||
//$wc = array_merge($wc, $serve);
|
||||
//$options = array_merge($wod, ['webp-convert' => $wc]);
|
||||
|
||||
//$options = array_merge($wod, array_merge($serve, ['conversion' => $wc]));
|
||||
|
||||
$options = [
|
||||
'wod' => $wod,
|
||||
'webp-convert' => array_merge($serve, ['convert' => $wc])
|
||||
];
|
||||
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public static function saveWodOptionsFile($options)
|
||||
{
|
||||
if (Paths::createConfigDirIfMissing()) {
|
||||
return FileHelper::saveJSONOptions(Paths::getWodOptionsFileName(), $options);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save both configuration files, but do not update htaccess
|
||||
* Returns success (boolean)
|
||||
*/
|
||||
public static function saveConfigurationFileAndWodOptions($config)
|
||||
{
|
||||
if (!isset($config['base-htaccess-on-these-capability-tests'])) {
|
||||
self::runAndStoreCapabilityTests($config);
|
||||
}
|
||||
if (!(self::saveConfigurationFile($config))) {
|
||||
return false;
|
||||
}
|
||||
$options = self::generateWodOptionsFromConfigObj($config);
|
||||
return (self::saveWodOptionsFile($options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate config and .htaccess files
|
||||
*
|
||||
* It will only happen if configuration file exists. So the method is meant for updating - ie upon migration.
|
||||
* It updates:
|
||||
* - config files (both) - and ensures that capability tests have been run
|
||||
* - autoloaded options (such as alter html options)
|
||||
* - .htaccess files (all)
|
||||
*/
|
||||
public static function regenerateConfigAndHtaccessFiles() {
|
||||
self::regenerateConfig(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate config and .htaccess files
|
||||
*
|
||||
* It will only happen if configuration file exists. So the method is meant for updating - ie upon migration.
|
||||
* It updates:
|
||||
* - config files (both) - and ensures that capability tests have been run
|
||||
* - autoloaded options (such as alter html options)
|
||||
* - .htaccess files - but only if needed due to configuration changes
|
||||
*/
|
||||
public static function regenerateConfig($forceRuleUpdating = false) {
|
||||
if (!self::isConfigFileThere()) {
|
||||
return;
|
||||
}
|
||||
$config = self::loadConfig();
|
||||
$config = self::fix($config, false); // fix. We do not need examining if quality detection is working
|
||||
if ($config === false) {
|
||||
return;
|
||||
}
|
||||
self::saveConfigurationAndHTAccess($config, $forceRuleUpdating);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* $rewriteRulesNeedsUpdate:
|
||||
*/
|
||||
public static function saveConfigurationAndHTAccess($config, $forceRuleUpdating = false)
|
||||
{
|
||||
// Important to do this check before saving config, because the method
|
||||
// compares against existing config.
|
||||
|
||||
if ($forceRuleUpdating) {
|
||||
$rewriteRulesNeedsUpdate = true;
|
||||
} else {
|
||||
$rewriteRulesNeedsUpdate = HTAccessRules::doesRewriteRulesNeedUpdate($config);
|
||||
}
|
||||
|
||||
if (!isset($config['base-htaccess-on-these-capability-tests']) || $rewriteRulesNeedsUpdate) {
|
||||
self::runAndStoreCapabilityTests($config);
|
||||
}
|
||||
|
||||
if (self::saveConfigurationFile($config)) {
|
||||
$options = self::generateWodOptionsFromConfigObj($config);
|
||||
if (self::saveWodOptionsFile($options)) {
|
||||
if ($rewriteRulesNeedsUpdate) {
|
||||
$rulesResult = HTAccess::saveRules($config, false);
|
||||
return [
|
||||
'saved-both-config' => true,
|
||||
'saved-main-config' => true,
|
||||
'rules-needed-update' => true,
|
||||
'htaccess-result' => $rulesResult
|
||||
];
|
||||
}
|
||||
else {
|
||||
$rulesResult = HTAccess::saveRules($config, false);
|
||||
return [
|
||||
'saved-both-config' => true,
|
||||
'saved-main-config' => true,
|
||||
'rules-needed-update' => false,
|
||||
'htaccess-result' => $rulesResult
|
||||
];
|
||||
}
|
||||
} else {
|
||||
return [
|
||||
'saved-both-config' => false,
|
||||
'saved-main-config' => true,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
return [
|
||||
'saved-both-config' => false,
|
||||
'saved-main-config' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public static function getConverterByName($config, $converterName)
|
||||
{
|
||||
foreach ($config['converters'] as $i => $converter) {
|
||||
if ($converter['converter'] == $converterName) {
|
||||
return $converter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
349
lib/classes/Convert.php
Normal file
349
lib/classes/Convert.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
739
lib/classes/ConvertHelperIndependent.php
Normal file
739
lib/classes/ConvertHelperIndependent.php
Normal file
@@ -0,0 +1,739 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
This class is made to not be dependent on Wordpress functions and must be kept like that.
|
||||
It is used by webp-on-demand.php. It is also used for bulk conversion.
|
||||
*/
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPConvert\WebPConvert;
|
||||
use \WebPConvert\Convert\ConverterFactory;
|
||||
use \WebPConvert\Exceptions\WebPConvertException;
|
||||
use \WebPConvert\Loggers\BufferLogger;
|
||||
|
||||
use \WebPExpress\FileHelper;
|
||||
use \WebPExpress\PathHelper;
|
||||
use \WebPExpress\SanityCheck;
|
||||
use \WebPExpress\SanityException;
|
||||
|
||||
class ConvertHelperIndependent
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* @return boolean Whether or not the destination corresponding to a given source should be stored in the same folder or the separate (in wp-content/webp-express)
|
||||
*/
|
||||
private static function storeMingledOrNot($source, $destinationFolder, $uploadDirAbs)
|
||||
{
|
||||
if ($destinationFolder != 'mingled') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Option is set for mingled, but this does not neccessarily means we should store "mingled".
|
||||
// - because the mingled option only applies to upload folder, the rest is stored in separate cache folder
|
||||
// So, return true, if $source is located in upload folder
|
||||
return (strpos($source, $uploadDirAbs) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if source is inside in document root
|
||||
* Note: This function relies on the existence of both.
|
||||
*
|
||||
* @return true if windows; false if not.
|
||||
*/
|
||||
public static function sourceIsInsideDocRoot($source, $docRoot){
|
||||
|
||||
$normalizedSource = realpath($source);
|
||||
$normalizedDocRoot = realpath($docRoot);
|
||||
|
||||
return strpos($normalizedSource, $normalizedDocRoot) === 0;
|
||||
}
|
||||
|
||||
public static function getSource()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Append ".webp" to path or replace extension with "webp", depending on what is appropriate.
|
||||
*
|
||||
* If destination-folder is set to mingled and destination-extension is set to "set" and
|
||||
* the path is inside upload folder, the appropriate thing is to SET the extension.
|
||||
* Otherwise, it is to APPEND.
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $destinationFolder
|
||||
* @param string $destinationExt
|
||||
* @param boolean $inUploadFolder
|
||||
*/
|
||||
public static function appendOrSetExtension($path, $destinationFolder, $destinationExt, $inUploadFolder)
|
||||
{
|
||||
if (($destinationFolder == 'mingled') && ($destinationExt == 'set') && $inUploadFolder) {
|
||||
return preg_replace('/\\.(jpe?g|png)$/i', '', $path) . '.webp';
|
||||
} else {
|
||||
return $path . '.webp';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination path corresponding to the source path given (and some configurations)
|
||||
*
|
||||
* If for example Operation mode is set to "mingled" and extension is set to "Append .webp",
|
||||
* the result of finding the destination path that corresponds to "/path/to/logo.jpg" will be "/path/to/logo.jpg.webp".
|
||||
*
|
||||
* @param string $source Path to source file
|
||||
* @param string $destinationFolder 'mingled' or 'separate'
|
||||
* @param string $destinationExt Extension ('append' or 'set')
|
||||
* @param string $webExpressContentDirAbs
|
||||
* @param string $uploadDirAbs
|
||||
* @param boolean $useDocRootForStructuringCacheDir
|
||||
* @param ImageRoots $imageRoots An image roots object
|
||||
*
|
||||
* @return string|false Returns path to destination corresponding to source, or false on failure
|
||||
*/
|
||||
public static function getDestination(
|
||||
$source,
|
||||
$destinationFolder,
|
||||
$destinationExt,
|
||||
$webExpressContentDirAbs,
|
||||
$uploadDirAbs,
|
||||
$useDocRootForStructuringCacheDir,
|
||||
$imageRoots)
|
||||
{
|
||||
// At this point, everything has already been checked for sanity. But for good meassure, lets
|
||||
// check the most important parts again. This is after all a public method.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
try {
|
||||
// Check source
|
||||
// --------------
|
||||
// TODO: make this check work with symlinks
|
||||
//$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
|
||||
|
||||
// Calculate destination and check that the result is sane
|
||||
// -------------------------------------------------------
|
||||
if (self::storeMingledOrNot($source, $destinationFolder, $uploadDirAbs)) {
|
||||
$destination = self::appendOrSetExtension($source, $destinationFolder, $destinationExt, true);
|
||||
} else {
|
||||
|
||||
if ($useDocRootForStructuringCacheDir) {
|
||||
// We must find the relative path from document root to source.
|
||||
// However, we dont know if document root is resolved or not.
|
||||
// We also do not know if source begins with a resolved or unresolved document root.
|
||||
// And we cannot be sure that document root is resolvable.
|
||||
|
||||
// Lets say:
|
||||
// 1. document root is unresolvable.
|
||||
// 2. document root is configured to something unresolved ("/my-website")
|
||||
// 3. source is resolved and within an image root ("/var/www/my-website/wp-content/uploads/test.jpg")
|
||||
// 4. all image roots are resolvable.
|
||||
// 5. Paths::canUseDocRootForRelPaths()) returned true
|
||||
|
||||
// Can the relative path then be found?
|
||||
// Actually, yes.
|
||||
// We can loop through the image roots.
|
||||
// When we get to the "uploads" root, it must neccessarily contain the unresolved document root.
|
||||
// It will in other words be: "my-website/wp-content/uploads"
|
||||
// It can not be configured to the resolved path because canUseDocRootForRelPaths would have then returned false as
|
||||
// It would not be possible to establish that "/var/www/my-website/wp-content/uploads/" is within document root, as
|
||||
// document root is "/my-website" and unresolvable.
|
||||
// To sum up, we have:
|
||||
// If document root is unresolvable while canUseDocRootForRelPaths() succeeded, then the image roots will all begin with
|
||||
// the unresolved path.
|
||||
// In this method, if $useDocRootForStructuringCacheDir is true, then it is assumed that canUseDocRootForRelPaths()
|
||||
// succeeded.
|
||||
// OH!
|
||||
// I realize that the image root can be passed as well:
|
||||
// $imageRoot = $webExpressContentDirAbs . '/webp-images';
|
||||
// So the question is: Will $webExpressContentDirAbs also be the unresolved path?
|
||||
// That variable is calculated in WodConfigLoader based on various methods available.
|
||||
// I'm not digging into it, but would expect it to in some cases be resolved. Which means that relative path can not
|
||||
// be found.
|
||||
// So. Lets play it safe and require that document root is resolvable in order to use docRoot for structure
|
||||
|
||||
if (!PathHelper::isDocRootAvailable()) {
|
||||
throw new \Exception(
|
||||
'Can not calculate destination using "doc-root" structure as document root is not available. $_SERVER["DOCUMENT_ROOT"] is empty. ' .
|
||||
'This is probably a misconfiguration on the server. ' .
|
||||
'However, WebP Express can function without using documument root. If you resave options and regenerate the .htaccess files, it should ' .
|
||||
'automatically start to structure the webp files in subfolders that are relative the image root folders rather than document-root.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!PathHelper::isDocRootAvailableAndResolvable()) {
|
||||
throw new \Exception(
|
||||
'Can not calculate destination using "doc-root" structure as document root cannot be resolved for symlinks using "realpath". The ' .
|
||||
'reason for that is probably that open_basedir protection has been set up and that document root is outside outside that open_basedir. ' .
|
||||
'WebP Express can function in that setting, however you will need to resave options and regenerate the .htaccess files. It should then ' .
|
||||
'automatically stop to structure the webp files as relative to document root and instead structure them as relative to image root folders.'
|
||||
);
|
||||
}
|
||||
$docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
|
||||
$imageRoot = $webExpressContentDirAbs . '/webp-images';
|
||||
|
||||
// TODO: make this check work with symlinks
|
||||
//SanityCheck::absPathIsInDocRoot($imageRoot);
|
||||
|
||||
$sourceRel = substr(realpath($source), strlen($docRoot) + 1);
|
||||
$destination = $imageRoot . '/doc-root/' . $sourceRel;
|
||||
$destination = self::appendOrSetExtension($destination, $destinationFolder, $destinationExt, false);
|
||||
|
||||
|
||||
// TODO: make this check work with symlinks
|
||||
//$destination = SanityCheck::absPathIsInDocRoot($destination);
|
||||
} else {
|
||||
$destination = '';
|
||||
|
||||
$sourceResolved = realpath($source);
|
||||
|
||||
|
||||
// Check roots until we (hopefully) get a match.
|
||||
// (that is: find a root which the source is inside)
|
||||
foreach ($imageRoots->getArray() as $i => $imageRoot) {
|
||||
// in $obj, "rel-path" is only set when document root can be used for relative paths.
|
||||
// So, if it is set, we can use it (beware: we cannot neccessarily use realpath on document root,
|
||||
// but we do not need to - see the long comment in Paths::canUseDocRootForRelPaths())
|
||||
|
||||
$rootPath = $imageRoot->getAbsPath();
|
||||
/*
|
||||
if (isset($obj['rel-path'])) {
|
||||
$docRoot = rtrim($_SERVER["DOCUMENT_ROOT"], '/');
|
||||
$rootPath = $docRoot . '/' . $obj['rel-path'];
|
||||
} else {
|
||||
// If "rel-path" isn't set, then abs-path is, and we can use that.
|
||||
$rootPath = $obj['abs-path'];
|
||||
}*/
|
||||
|
||||
// $source may be resolved or not. Same goes for $rootPath.
|
||||
// We can assume that $rootPath is resolvable using realpath (it ought to exist and be within open_basedir for WP to function)
|
||||
// We can also assume that $source is resolvable (it ought to exist and within open_basedir)
|
||||
// So: Resolve both! and test if the resolved source begins with the resolved rootPath.
|
||||
if (strpos($sourceResolved, realpath($rootPath)) !== false) {
|
||||
$relPath = substr($sourceResolved, strlen(realpath($rootPath)) + 1);
|
||||
$relPath = self::appendOrSetExtension($relPath, $destinationFolder, $destinationExt, false);
|
||||
|
||||
$destination = $webExpressContentDirAbs . '/webp-images/' . $imageRoot->id . '/' . $relPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($destination == '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (SanityException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find source corresponding to destination, separate.
|
||||
*
|
||||
* We can rely on destinationExt being "append" for separate.
|
||||
* Returns false if source file is not found or if a path is not sane. Otherwise returns path to source
|
||||
* destination does not have to exist.
|
||||
*
|
||||
* @param string $destination Path to destination file (does not have to exist)
|
||||
* @param string $destinationStructure "doc-root" or "image-roots"
|
||||
* @param string $webExpressContentDirAbs
|
||||
* @param ImageRoots $imageRoots An image roots object
|
||||
*
|
||||
* @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
|
||||
*/
|
||||
private static function findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots)
|
||||
{
|
||||
try {
|
||||
|
||||
if ($destinationStructure == 'doc-root') {
|
||||
|
||||
// Check that destination path is sane and inside document root
|
||||
// --------------------------
|
||||
$destination = SanityCheck::absPathIsInDocRoot($destination);
|
||||
|
||||
|
||||
// Check that calculated image root is sane and inside document root
|
||||
// --------------------------
|
||||
$imageRoot = SanityCheck::absPathIsInDocRoot($webExpressContentDirAbs . '/webp-images/doc-root');
|
||||
|
||||
|
||||
// Calculate source and check that it is sane and exists
|
||||
// -----------------------------------------------------
|
||||
|
||||
// TODO: This does not work on Windows yet.
|
||||
if (strpos($destination, $imageRoot . '/') === 0) {
|
||||
|
||||
// "Eat" the left part off the $destination parameter. $destination is for example:
|
||||
// "/var/www/webp-express-tests/we0/wp-content-moved/webp-express/webp-images/doc-root/wordpress/uploads-moved/2018/12/tegning5-300x265.jpg.webp"
|
||||
// We also eat the slash (+1)
|
||||
$sourceRel = substr($destination, strlen($imageRoot) + 1);
|
||||
|
||||
$docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
|
||||
$source = $docRoot . '/' . $sourceRel;
|
||||
$source = preg_replace('/\\.(webp)$/', '', $source);
|
||||
} else {
|
||||
// Try with symlinks resolved
|
||||
// This is not trivial as this must also work when the destination path doesn't exist, and
|
||||
// realpath can only be used to resolve symlinks for files that exists.
|
||||
// But here is how we achieve it anyway:
|
||||
//
|
||||
// 1. We make sure imageRoot exists (if not, create it) - this ensures that we can resolve it.
|
||||
// 2. Find closest folder existing folder (resolved) of destination - using PathHelper::findClosestExistingFolderSymLinksExpanded()
|
||||
// 3. Test that resolved closest existing folder starts with resolved imageRoot
|
||||
// 4. If it does, we could create a dummy file at the destination to get its real path, but we want to avoid that, so instead
|
||||
// we can create the containing directory.
|
||||
// 5. We can now use realpath to get the resolved path of the containing directory. The rest is simple enough.
|
||||
if (!file_exists($imageRoot)) {
|
||||
mkdir($imageRoot, 0777, true);
|
||||
}
|
||||
$closestExistingResolved = PathHelper::findClosestExistingFolderSymLinksExpanded($destination);
|
||||
if ($closestExistingResolved == '') {
|
||||
return false;
|
||||
} else {
|
||||
$imageRootResolved = realpath($imageRoot);
|
||||
if (strpos($closestExistingResolved . '/', $imageRootResolved . '/') === 0) {
|
||||
// echo $destination . '<br>' . $closestExistingResolved . '<br>' . $imageRootResolved . '/'; exit;
|
||||
// Create containing dir for destination
|
||||
$containingDir = PathHelper::dirname($destination);
|
||||
if (!file_exists($containingDir)) {
|
||||
mkdir($containingDir, 0777, true);
|
||||
}
|
||||
$containingDirResolved = realpath($containingDir);
|
||||
|
||||
$filename = PathHelper::basename($destination);
|
||||
$destinationResolved = $containingDirResolved . '/' . $filename;
|
||||
|
||||
$sourceRel = substr($destinationResolved, strlen($imageRootResolved) + 1);
|
||||
|
||||
$docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
|
||||
$source = $docRoot . '/' . $sourceRel;
|
||||
$source = preg_replace('/\\.(webp)$/', '', $source);
|
||||
return $source;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SanityCheck::absPathExistsAndIsFileInDocRoot($source);
|
||||
} else {
|
||||
|
||||
// Mission: To find source corresponding to destination (separate) - using the "image-roots" structure.
|
||||
|
||||
// How can we do that?
|
||||
// We got the destination (unresolved) - ie '/website-symlinked/wp-content/webp-express/webp-images/uploads/2018/07/hello.jpg.webp'
|
||||
// If we were lazy and unprecise, we could simply:
|
||||
// - search for "webp-express/webp-images/"
|
||||
// - strip anything before that - result: 'uploads/2018/07/hello.jpg.webp'
|
||||
// - the first path component is the root id.
|
||||
// - the rest of the path is the relative path to the source - if we strip the ".webp" ending
|
||||
|
||||
// So, are we lazy? - what is the alternative?
|
||||
// - Get closest existing resolved folder of destination (ie "/var/www/website/wp-content-moved/webp-express/webp-images/wp-content")
|
||||
// - Check if that folder is below the cache root (resolved) (cache root is the "wp-content" image root + 'webp-express/webp-images')
|
||||
// - Create dir for destination (if missing)
|
||||
// - We can now resolve destination. With cache root also being resolved, we can get the relative dir.
|
||||
// ie 'uploads/2018/07/hello.jpg.webp'.
|
||||
// The first path component is the root id, the rest is the relative path to the source.
|
||||
|
||||
$closestExistingResolved = PathHelper::findClosestExistingFolderSymLinksExpanded($destination);
|
||||
$cacheRoot = $webExpressContentDirAbs . '/webp-images';
|
||||
if ($closestExistingResolved == '') {
|
||||
return false;
|
||||
} else {
|
||||
$cacheRootResolved = realpath($cacheRoot);
|
||||
if (strpos($closestExistingResolved . '/', $cacheRootResolved . '/') === 0) {
|
||||
|
||||
// Create containing dir for destination
|
||||
$containingDir = PathHelper::dirname($destination);
|
||||
if (!file_exists($containingDir)) {
|
||||
mkdir($containingDir, 0777, true);
|
||||
}
|
||||
$containingDirResolved = realpath($containingDir);
|
||||
|
||||
$filename = PathHelper::basename($destination);
|
||||
$destinationResolved = $containingDirResolved . '/' . $filename;
|
||||
$destinationRelToCacheRoot = substr($destinationResolved, strlen($cacheRootResolved) + 1);
|
||||
|
||||
$parts = explode('/', $destinationRelToCacheRoot);
|
||||
$imageRoot = array_shift($parts);
|
||||
$sourceRel = implode('/', $parts);
|
||||
|
||||
$source = $imageRoots->byId($imageRoot)->getAbsPath() . '/' . $sourceRel;
|
||||
$source = preg_replace('/\\.(webp)$/', '', $source);
|
||||
return $source;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (SanityException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find source corresponding to destination (mingled)
|
||||
* Returns false if not found. Otherwise returns path to source
|
||||
*
|
||||
* @param string $destination Path to destination file (does not have to exist)
|
||||
* @param string $destinationExt Extension ('append' or 'set')
|
||||
* @param string $destinationStructure "doc-root" or "image-roots"
|
||||
*
|
||||
* @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
|
||||
*/
|
||||
private static function findSourceMingled($destination, $destinationExt, $destinationStructure)
|
||||
{
|
||||
try {
|
||||
|
||||
if ($destinationStructure == 'doc-root') {
|
||||
// Check that destination path is sane and inside document root
|
||||
// --------------------------
|
||||
$destination = SanityCheck::absPathIsInDocRoot($destination);
|
||||
} else {
|
||||
// The following will fail if path contains directory traversal. TODO: Is that ok?
|
||||
$destination = SanityCheck::absPath($destination);
|
||||
}
|
||||
|
||||
// Calculate source and check that it is sane and exists
|
||||
// -----------------------------------------------------
|
||||
if ($destinationExt == 'append') {
|
||||
$source = preg_replace('/\\.(webp)$/', '', $destination);
|
||||
} else {
|
||||
$source = preg_replace('#\\.webp$#', '.jpg', $destination);
|
||||
// TODO!
|
||||
// Also check for "Jpeg", "JpEg" etc.
|
||||
if (!@file_exists($source)) {
|
||||
$source = preg_replace('/\\.webp$/', '.jpeg', $destination);
|
||||
}
|
||||
if (!@file_exists($source)) {
|
||||
$source = preg_replace('/\\.webp$/', '.JPG', $destination);
|
||||
}
|
||||
if (!@file_exists($source)) {
|
||||
$source = preg_replace('/\\.webp$/', '.JPEG', $destination);
|
||||
}
|
||||
if (!@file_exists($source)) {
|
||||
$source = preg_replace('/\\.webp$/', '.png', $destination);
|
||||
}
|
||||
if (!@file_exists($source)) {
|
||||
$source = preg_replace('/\\.webp$/', '.PNG', $destination);
|
||||
}
|
||||
}
|
||||
if ($destinationStructure == 'doc-root') {
|
||||
$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
|
||||
} else {
|
||||
$source = SanityCheck::absPathExistsAndIsFile($source);
|
||||
}
|
||||
|
||||
|
||||
} catch (SanityException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source from destination (and some configurations)
|
||||
* Returns false if not found. Otherwise returns path to source
|
||||
*
|
||||
* @param string $destination Path to destination file (does not have to exist). May not contain directory traversal
|
||||
* @param string $destinationFolder 'mingled' or 'separate'
|
||||
* @param string $destinationExt Extension ('append' or 'set')
|
||||
* @param string $destinationStructure "doc-root" or "image-roots"
|
||||
* @param string $webExpressContentDirAbs
|
||||
* @param ImageRoots $imageRoots An image roots object
|
||||
*
|
||||
* @return string|false Returns path to source, if found. If not - or a path is not sane, false is returned
|
||||
*/
|
||||
public static function findSource($destination, $destinationFolder, $destinationExt, $destinationStructure, $webExpressContentDirAbs, $imageRoots)
|
||||
{
|
||||
|
||||
try {
|
||||
|
||||
if ($destinationStructure == 'doc-root') {
|
||||
// Check that destination path is sane and inside document root
|
||||
// --------------------------
|
||||
$destination = SanityCheck::absPathIsInDocRoot($destination);
|
||||
} else {
|
||||
// The following will fail if path contains directory traversal. TODO: Is that ok?
|
||||
$destination = SanityCheck::absPath($destination);
|
||||
}
|
||||
|
||||
} catch (SanityException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($destinationFolder == 'mingled') {
|
||||
$result = self::findSourceMingled($destination, $destinationExt, $destinationStructure);
|
||||
if ($result === false) {
|
||||
$result = self::findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots);
|
||||
}
|
||||
return $result;
|
||||
} else {
|
||||
return self::findSourceSeparate($destination, $destinationStructure, $webExpressContentDirAbs, $imageRoots);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $source Path to source file
|
||||
* @param string $logDir The folder where log files are kept
|
||||
*
|
||||
* @return string|false Returns computed filename of log - or false if a path is not sane
|
||||
*
|
||||
*/
|
||||
public static function getLogFilename($source, $logDir)
|
||||
{
|
||||
try {
|
||||
|
||||
// Check that source path is sane and inside document root
|
||||
// -------------------------------------------------------
|
||||
$source = SanityCheck::absPathIsInDocRoot($source);
|
||||
|
||||
|
||||
// Check that log path is sane and inside document root
|
||||
// -------------------------------------------------------
|
||||
$logDir = SanityCheck::absPathIsInDocRoot($logDir);
|
||||
|
||||
|
||||
// Compute and check log path
|
||||
// --------------------------
|
||||
$logDirForConversions = $logDir .= '/conversions';
|
||||
|
||||
// We store relative to document root.
|
||||
// "Eat" the left part off the source parameter which contains the document root.
|
||||
// and also eat the slash (+1)
|
||||
|
||||
$docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
|
||||
$sourceRel = substr($source, strlen($docRoot) + 1);
|
||||
$logFileName = $logDir . '/doc-root/' . $sourceRel . '.md';
|
||||
SanityCheck::absPathIsInDocRoot($logFileName);
|
||||
|
||||
} catch (SanityException $e) {
|
||||
return false;
|
||||
}
|
||||
return $logFileName;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the directory for log files and put a .htaccess file into it, which prevents
|
||||
* it to be viewed from the outside (not that it contains any sensitive information btw, but for good measure).
|
||||
*
|
||||
* @param string $logDir The folder where log files are kept
|
||||
*
|
||||
* @return boolean Whether it was created successfully or not.
|
||||
*
|
||||
*/
|
||||
private static function createLogDir($logDir)
|
||||
{
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0775, true);
|
||||
@chmod($logDir, 0775);
|
||||
@file_put_contents(rtrim($logDir . '/') . '/.htaccess', <<<APACHE
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
APACHE
|
||||
);
|
||||
@chmod($logDir . '/.htaccess', 0664);
|
||||
}
|
||||
return is_dir($logDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the log file corresponding to a conversion.
|
||||
*
|
||||
* @param string $source Path to the source file that was converted
|
||||
* @param string $logDir The folder where log files are kept
|
||||
* @param string $text Content of the log file
|
||||
* @param string $msgTop A message that is printed before the conversion log (containing version info)
|
||||
*
|
||||
*
|
||||
*/
|
||||
private static function saveLog($source, $logDir, $text, $msgTop)
|
||||
{
|
||||
|
||||
if (!file_exists($logDir)) {
|
||||
self::createLogDir($logDir);
|
||||
}
|
||||
|
||||
$text = preg_replace('#' . preg_quote($_SERVER["DOCUMENT_ROOT"]) . '#', '[doc-root]', $text);
|
||||
|
||||
// TODO: Put version number somewhere else. Ie \WebPExpress\VersionNumber::version
|
||||
$text = 'WebP Express 0.25.9. ' . $msgTop . ', ' . date("Y-m-d H:i:s") . "\n\r\n\r" . $text;
|
||||
|
||||
$logFile = self::getLogFilename($source, $logDir);
|
||||
|
||||
if ($logFile === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logFolder = @dirname($logFile);
|
||||
if (!@file_exists($logFolder)) {
|
||||
mkdir($logFolder, 0777, true);
|
||||
}
|
||||
if (@file_exists($logFolder)) {
|
||||
file_put_contents($logFile, $text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an actual conversion with webp-convert.
|
||||
*
|
||||
* PS: To convert with a specific converter, set it in the $converter param.
|
||||
*
|
||||
* @param string $source Full path to the source file that was converted.
|
||||
* @param string $destination Full path to the destination file (may exist or not).
|
||||
* @param array $convertOptions Conversion options.
|
||||
* @param string $logDir The folder where log files are kept or null for no logging
|
||||
* @param string $converter (optional) Set it to convert with a specific converter.
|
||||
*/
|
||||
public static function convert($source, $destination, $convertOptions, $logDir = null, $converter = null) {
|
||||
include_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
// At this point, everything has already been checked for sanity. But for good meassure, lets
|
||||
// check the most important parts again. This is after all a public method.
|
||||
// ------------------------------------------------------------------
|
||||
try {
|
||||
|
||||
// Check that source path is sane, exists, is a file and is inside document root
|
||||
// -------------------------------------------------------
|
||||
|
||||
// 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::absPathExistsAndIsFileInDocRoot($source);
|
||||
|
||||
|
||||
// Check that destination path is sane and is inside document root
|
||||
// -------------------------------------------------------
|
||||
$destination = SanityCheck::absPathIsInDocRoot($destination);
|
||||
$destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Destination does not end with .webp');
|
||||
|
||||
|
||||
// Check that log path is sane and inside document root
|
||||
// -------------------------------------------------------
|
||||
if (!is_null($logDir)) {
|
||||
$logDir = SanityCheck::absPathIsInDocRoot($logDir);
|
||||
}
|
||||
|
||||
|
||||
// PS: No need to check $logMsgTop. Log files are markdown and stored as ".md". They can do no harm.
|
||||
|
||||
} catch (SanityException $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'msg' => $e->getMessage(),
|
||||
'log' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$success = false;
|
||||
$msg = '';
|
||||
$logger = new BufferLogger();
|
||||
try {
|
||||
if (!is_null($converter)) {
|
||||
//if (isset($convertOptions['converter'])) {
|
||||
//print_r($convertOptions);exit;
|
||||
$logger->logLn('Converter set to: ' . $converter);
|
||||
$logger->logLn('');
|
||||
$converter = ConverterFactory::makeConverter($converter, $source, $destination, $convertOptions, $logger);
|
||||
$converter->doConvert();
|
||||
} else {
|
||||
//error_log('options:' . print_r(json_encode($convertOptions,JSON_PRETTY_PRINT), true));
|
||||
WebPConvert::convert($source, $destination, $convertOptions, $logger);
|
||||
}
|
||||
$success = true;
|
||||
} catch (\WebpConvert\Exceptions\WebPConvertException $e) {
|
||||
$msg = $e->getMessage();
|
||||
} catch (\Exception $e) {
|
||||
//$msg = 'An exception was thrown!';
|
||||
$msg = $e->getMessage();
|
||||
} catch (\Throwable $e) {
|
||||
//Executed only in PHP 7 and 8, will not match in PHP 5
|
||||
$msg = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!is_null($logDir)) {
|
||||
self::saveLog($source, $logDir, $logger->getMarkDown("\n\r"), 'Conversion triggered using bulk conversion');
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $success,
|
||||
'msg' => $msg,
|
||||
'log' => $logger->getMarkDown("\n"),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve a converted file (if it does not already exist, a conversion is triggered - all handled in webp-convert).
|
||||
*
|
||||
*/
|
||||
public static function serveConverted($source, $destination, $serveOptions, $logDir = null, $logMsgTop = '')
|
||||
{
|
||||
include_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
// At this point, everything has already been checked for sanity. But for good meassure, lets
|
||||
// check again. This is after all a public method.
|
||||
// ---------------------------------------------
|
||||
try {
|
||||
|
||||
// Check that source path is sane, exists, is a file.
|
||||
// -------------------------------------------------------
|
||||
//$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
|
||||
$source = SanityCheck::absPathExistsAndIsFile($source);
|
||||
|
||||
|
||||
// Check that destination path is sane
|
||||
// -------------------------------------------------------
|
||||
//$destination = SanityCheck::absPathIsInDocRoot($destination);
|
||||
$destination = SanityCheck::absPath($destination);
|
||||
$destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Destination does not end with .webp');
|
||||
|
||||
|
||||
// Check that log path is sane
|
||||
// -------------------------------------------------------
|
||||
//$logDir = SanityCheck::absPathIsInDocRoot($logDir);
|
||||
if ($logDir != null) {
|
||||
$logDir = SanityCheck::absPath($logDir);
|
||||
}
|
||||
|
||||
// PS: No need to check $logMsgTop. Log files are markdown and stored as ".md". They can do no harm.
|
||||
|
||||
} catch (SanityException $e) {
|
||||
$msg = $e->getMessage();
|
||||
echo $msg;
|
||||
header('X-WebP-Express-Error: ' . $msg, true);
|
||||
// TODO: error_log() ?
|
||||
exit;
|
||||
}
|
||||
|
||||
$convertLogger = new BufferLogger();
|
||||
WebPConvert::serveConverted($source, $destination, $serveOptions, null, $convertLogger);
|
||||
if (!is_null($logDir)) {
|
||||
$convertLog = $convertLogger->getMarkDown("\n\r");
|
||||
if ($convertLog != '') {
|
||||
self::saveLog($source, $logDir, $convertLog, $logMsgTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
lib/classes/ConvertLog.php
Normal file
48
lib/classes/ConvertLog.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\ConvertHelperIndependent;
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
class ConvertLog
|
||||
{
|
||||
public static function processAjaxViewLog()
|
||||
{
|
||||
if (!check_ajax_referer('webpexpress-ajax-view-log-nonce', 'nonce', false)) {
|
||||
wp_send_json_error('The security nonce has expired. You need to reload the settings page (press F5) and try again)');
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// We need to be absolute certain that this feature cannot be misused.
|
||||
// - so disabling until I get the time...
|
||||
|
||||
$msg = 'This feature is on the road map...';
|
||||
echo json_encode($msg, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
|
||||
/*
|
||||
$source = sanitize_text_field($_POST['source']);
|
||||
$logFile = ConvertHelperIndependent::getLogFilename($source, Paths::getLogDirAbs());
|
||||
$msg = 'Log file: <i>' . $logFile . '</i><br><br><hr>';
|
||||
|
||||
if (!file_exists($logFile)) {
|
||||
$msg .= '<b>No log file found on that location</b>';
|
||||
|
||||
} else {
|
||||
$log = file_get_contents($logFile);
|
||||
if ($log === false) {
|
||||
$msg .= '<b>Could not read log file</b>';
|
||||
} else {
|
||||
$msg .= nl2br($log);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//$log = $source;
|
||||
//file_get_contents
|
||||
|
||||
echo json_encode($msg, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
|
||||
*/
|
||||
wp_die();
|
||||
}
|
||||
|
||||
}
|
||||
287
lib/classes/ConvertersHelper.php
Normal file
287
lib/classes/ConvertersHelper.php
Normal file
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class ConvertersHelper
|
||||
{
|
||||
public static $defaultConverters = [
|
||||
['converter' => 'cwebp', 'options' => [
|
||||
'use-nice' => true,
|
||||
'try-common-system-paths' => true,
|
||||
'try-supplied-binary-for-os' => true,
|
||||
'method' => 6,
|
||||
'low-memory' => true,
|
||||
'command-line-options' => '',
|
||||
]],
|
||||
['converter' => 'vips', 'options' => [
|
||||
'smart-subsample' => false,
|
||||
'preset' => 'none'
|
||||
]],
|
||||
['converter' => 'imagemagick', 'options' => [
|
||||
'use-nice' => true,
|
||||
]],
|
||||
['converter' => 'graphicsmagick', 'options' => [
|
||||
'use-nice' => true,
|
||||
]],
|
||||
['converter' => 'ffmpeg', 'options' => [
|
||||
'use-nice' => true,
|
||||
'method' => 4,
|
||||
]],
|
||||
['converter' => 'wpc', 'options' => []], // we should not set api-version default - it is handled in the javascript
|
||||
['converter' => 'ewww', 'options' => []],
|
||||
['converter' => 'imagick', 'options' => []],
|
||||
['converter' => 'gmagick', 'options' => []],
|
||||
['converter' => 'gd', 'options' => [
|
||||
'skip-pngs' => false,
|
||||
]],
|
||||
];
|
||||
|
||||
public static function getDefaultConverterNames()
|
||||
{
|
||||
$availableConverterIDs = [];
|
||||
foreach (self::$defaultConverters as $converter) {
|
||||
$availableConverterIDs[] = $converter['converter'];
|
||||
}
|
||||
return $availableConverterIDs;
|
||||
|
||||
// PS: In a couple of years:
|
||||
//return array_column(self::$defaultConverters, 'converter');
|
||||
}
|
||||
|
||||
public static function getConverterNames($converters)
|
||||
{
|
||||
return array_column(self::normalize($converters), 'converter');
|
||||
}
|
||||
|
||||
public static function normalize($converters)
|
||||
{
|
||||
foreach ($converters as &$converter) {
|
||||
if (!isset($converter['converter'])) {
|
||||
$converter = ['converter' => $converter];
|
||||
}
|
||||
if (!isset($converter['options'])) {
|
||||
$converter['options'] = [];
|
||||
}
|
||||
}
|
||||
return $converters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Those converters in second array, but not in first will be appended to first
|
||||
*/
|
||||
public static function mergeConverters($first, $second)
|
||||
{
|
||||
$namesInFirst = self::getConverterNames($first);
|
||||
$second = self::normalize($second);
|
||||
|
||||
foreach ($second as $converter) {
|
||||
// migrate9 and this functionality could create two converters.
|
||||
// so, for a while, skip graphicsmagick and imagemagick
|
||||
|
||||
if ($converter['converter'] == 'graphicsmagick') {
|
||||
if (in_array('gmagickbinary', $namesInFirst)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ($converter['converter'] == 'imagemagick') {
|
||||
if (in_array('imagickbinary', $namesInFirst)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!in_array($converter['converter'], $namesInFirst)) {
|
||||
$first[] = $converter;
|
||||
}
|
||||
}
|
||||
return $first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get converter by id
|
||||
*
|
||||
* @param object $config
|
||||
* @return array|false converter object
|
||||
*/
|
||||
public static function getConverterById($config, $id) {
|
||||
if (!isset($config['converters'])) {
|
||||
return false;
|
||||
}
|
||||
$converters = $config['converters'];
|
||||
|
||||
if (!is_array($converters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($converters as $c) {
|
||||
if (!isset($c['converter'])) {
|
||||
continue;
|
||||
}
|
||||
if ($c['converter'] == $id) {
|
||||
return $c;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get working converters.
|
||||
*
|
||||
* @param object $config
|
||||
* @return array
|
||||
*/
|
||||
public static function getWorkingConverters($config) {
|
||||
if (!isset($config['converters'])) {
|
||||
return [];
|
||||
}
|
||||
$converters = $config['converters'];
|
||||
|
||||
if (!is_array($converters)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($converters as $c) {
|
||||
if (isset($c['working']) && !$c['working']) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $c;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of working converter ids. Same order as configured.
|
||||
*/
|
||||
public static function getWorkingConverterIds($config)
|
||||
{
|
||||
$converters = self::getWorkingConverters($config);
|
||||
$result = [];
|
||||
foreach ($converters as $converter) {
|
||||
$result[] = $converter['converter'];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get working and active converters.
|
||||
*
|
||||
* @param object $config
|
||||
* @return array Array of converter objects
|
||||
*/
|
||||
public static function getWorkingAndActiveConverters($config)
|
||||
{
|
||||
if (!isset($config['converters'])) {
|
||||
return [];
|
||||
}
|
||||
$converters = $config['converters'];
|
||||
|
||||
if (!is_array($converters)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($converters as $c) {
|
||||
if (isset($c['deactivated']) && $c['deactivated']) {
|
||||
continue;
|
||||
}
|
||||
if (isset($c['working']) && !$c['working']) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $c;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active converters.
|
||||
*
|
||||
* @param object $config
|
||||
* @return array Array of converter objects
|
||||
*/
|
||||
public static function getActiveConverters($config)
|
||||
{
|
||||
if (!isset($config['converters'])) {
|
||||
return [];
|
||||
}
|
||||
$converters = $config['converters'];
|
||||
if (!is_array($converters)) {
|
||||
return [];
|
||||
}
|
||||
$result = [];
|
||||
foreach ($converters as $c) {
|
||||
if (isset($c['deactivated']) && $c['deactivated']) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $c;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function getWorkingAndActiveConverterIds($config)
|
||||
{
|
||||
$converters = self::getWorkingAndActiveConverters($config);
|
||||
$result = [];
|
||||
foreach ($converters as $converter) {
|
||||
$result[] = $converter['converter'];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function getActiveConverterIds($config)
|
||||
{
|
||||
$converters = self::getActiveConverters($config);
|
||||
$result = [];
|
||||
foreach ($converters as $converter) {
|
||||
$result[] = $converter['converter'];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get converter id by converter object
|
||||
*
|
||||
* @param object $converter
|
||||
* @return string converter name, or empty string if not set (it should always be set, however)
|
||||
*/
|
||||
public static function getConverterId($converter) {
|
||||
if (!isset($converter['converter'])) {
|
||||
return '';
|
||||
}
|
||||
return $converter['converter'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first working and active converter.
|
||||
*
|
||||
* @param object $config
|
||||
* @return object|false
|
||||
*/
|
||||
public static function getFirstWorkingAndActiveConverter($config) {
|
||||
|
||||
$workingConverters = self::getWorkingAndActiveConverters($config);
|
||||
|
||||
if (count($workingConverters) == 0) {
|
||||
return false;
|
||||
}
|
||||
return $workingConverters[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first working and active converter (name)
|
||||
*
|
||||
* @param object $config
|
||||
* @return string|false id of converter, or false if no converter is working and active
|
||||
*/
|
||||
public static function getFirstWorkingAndActiveConverterId($config) {
|
||||
$c = self::getFirstWorkingAndActiveConverter($config);
|
||||
if ($c === false) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($c['converter'])) {
|
||||
return false;
|
||||
}
|
||||
return $c['converter'];
|
||||
}
|
||||
|
||||
}
|
||||
208
lib/classes/Destination.php
Normal file
208
lib/classes/Destination.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
This class is made to not be dependent on Wordpress functions and must be kept like that.
|
||||
It is used by webp-on-demand.php, which does not register an auto loader. It is also used for bulk conversion.
|
||||
*/
|
||||
namespace WebPExpress;
|
||||
|
||||
class Destination
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* @return boolean Whether or not the destination corresponding to a given source should be stored in the same folder
|
||||
* or the separate folder (in wp-content/webp-express)
|
||||
*/
|
||||
private static function storeMingledOrNot($source, $mingled, $uploadDirAbs)
|
||||
{
|
||||
if ($mingled == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Option is set for mingled, but this does not neccessarily means we should store "mingled".
|
||||
// - because the mingled option only applies to upload folder, the rest is stored in separate cache folder
|
||||
// So, return true, if $source is located in upload folder
|
||||
return (strpos($source, $uploadDirAbs) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append ".webp" to path or replace extension with "webp", depending on what is appropriate.
|
||||
*
|
||||
* If destination-folder is set to mingled and destination-extension is set to "set" and
|
||||
* the path is inside upload folder, the appropriate thing is to SET the extension.
|
||||
* Otherwise, it is to APPEND.
|
||||
*
|
||||
* @param string $path
|
||||
* @param boolean $mingled Mingled setting (notice that mingled only applies to uploads)
|
||||
* @param string $replaceExt If file extension should be replaced with ".webp". If false, ".webp" is appended.
|
||||
* @param boolean $inUploadFolder
|
||||
*/
|
||||
public static function appendOrSetExtension($path, $mingled, $replaceExt, $inUploadFolder)
|
||||
{
|
||||
if ($mingled && $replaceExt && $inUploadFolder) {
|
||||
return preg_replace('/\\.(jpe?g|png)$/i', '', $path) . '.webp';
|
||||
} else {
|
||||
return $path . '.webp';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination path corresponding to the source path given (and some configurations)
|
||||
*
|
||||
* If for example Operation mode is set to "mingled" and extension is set to "Append .webp",
|
||||
* the result of finding the destination path that corresponds to "/path/to/logo.jpg" will be "/path/to/logo.jpg.webp".
|
||||
*
|
||||
* The imageRoots are tried in order.
|
||||
* This means that ie "uploads" is preferred over "wp-content" even though the source resides in both (when uploads is inside wp-content)
|
||||
* So destination is ie [..]/wp-content/webp-express/uploads/[..]", rather than same but with "wp-content"
|
||||
*
|
||||
* @param string $source Path to source file
|
||||
* @param string $webExpressContentDirAbs
|
||||
* @param string $uploadDirAbs
|
||||
* @param DestinationOptions $destinationOptions
|
||||
* @param ImageRoots $imageRoots An image roots object
|
||||
*
|
||||
* @return string|false Returns path to destination corresponding to source, or false on failure
|
||||
*/
|
||||
public static function getDestinationPathCorrespondingToSource(
|
||||
$source,
|
||||
$webExpressContentDirAbs,
|
||||
$uploadDirAbs,
|
||||
$destinationOptions,
|
||||
$imageRoots
|
||||
) {
|
||||
// At this point, everything has already been checked for sanity. But for good meassure, lets
|
||||
// check the most important parts again. This is after all a public method.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
$mingled = $destinationOptions->mingled;
|
||||
$replaceExt = $destinationOptions->replaceExt;
|
||||
$useDocRoot = $destinationOptions->useDocRoot;
|
||||
|
||||
try {
|
||||
// Check source
|
||||
// --------------
|
||||
// TODO: make this check work with symlinks
|
||||
//$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
|
||||
|
||||
// Calculate destination and check that the result is sane
|
||||
// -------------------------------------------------------
|
||||
if (self::storeMingledOrNot($source, $mingled, $uploadDirAbs)) {
|
||||
$destination = self::appendOrSetExtension($source, $mingled, $replaceExt, true);
|
||||
} else {
|
||||
|
||||
if ($useDocRoot) {
|
||||
// We must find the relative path from document root to source.
|
||||
// However, we dont know if document root is resolved or not.
|
||||
// We also do not know if source begins with a resolved or unresolved document root.
|
||||
// And we cannot be sure that document root is resolvable.
|
||||
|
||||
// Lets say:
|
||||
// 1. document root is unresolvable.
|
||||
// 2. document root is configured to something unresolved ("/my-website")
|
||||
// 3. source is resolved and within an image root ("/var/www/my-website/wp-content/uploads/test.jpg")
|
||||
// 4. all image roots are resolvable.
|
||||
// 5. Paths::canUseDocRootForRelPaths()) returned true
|
||||
|
||||
// Can the relative path then be found?
|
||||
// Actually, yes.
|
||||
// We can loop through the image roots.
|
||||
// When we get to the "uploads" root, it must neccessarily contain the unresolved document root.
|
||||
// It will in other words be: "my-website/wp-content/uploads"
|
||||
// It can not be configured to the resolved path because canUseDocRootForRelPaths would have then returned false as
|
||||
// It would not be possible to establish that "/var/www/my-website/wp-content/uploads/" is within document root, as
|
||||
// document root is "/my-website" and unresolvable.
|
||||
// To sum up, we have:
|
||||
// If document root is unresolvable while canUseDocRootForRelPaths() succeeded, then the image roots will all begin with
|
||||
// the unresolved path.
|
||||
// In this method, if $useDocRootForStructuringCacheDir is true, then it is assumed that canUseDocRootForRelPaths()
|
||||
// succeeded.
|
||||
// OH!
|
||||
// I realize that the image root can be passed as well:
|
||||
// $imageRoot = $webExpressContentDirAbs . '/webp-images';
|
||||
// So the question is: Will $webExpressContentDirAbs also be the unresolved path?
|
||||
// That variable is calculated in WodConfigLoader based on various methods available.
|
||||
// I'm not digging into it, but would expect it to in some cases be resolved. Which means that relative path can not
|
||||
// be found.
|
||||
// So. Lets play it safe and require that document root is resolvable in order to use docRoot for structure
|
||||
|
||||
if (!PathHelper::isDocRootAvailable()) {
|
||||
throw new \Exception(
|
||||
'Can not calculate destination using "doc-root" structure as document root is not available. $_SERVER["DOCUMENT_ROOT"] is empty. ' .
|
||||
'This is probably a misconfiguration on the server. ' .
|
||||
'However, WebP Express can function without using documument root. If you resave options and regenerate the .htaccess files, it should ' .
|
||||
'automatically start to structure the webp files in subfolders that are relative the image root folders rather than document-root.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!PathHelper::isDocRootAvailableAndResolvable()) {
|
||||
throw new \Exception(
|
||||
'Can not calculate destination using "doc-root" structure as document root cannot be resolved for symlinks using "realpath". The ' .
|
||||
'reason for that is probably that open_basedir protection has been set up and that document root is outside outside that open_basedir. ' .
|
||||
'WebP Express can function in that setting, however you will need to resave options and regenerate the .htaccess files. It should then ' .
|
||||
'automatically stop to structure the webp files as relative to document root and instead structure them as relative to image root folders.'
|
||||
);
|
||||
}
|
||||
$docRoot = rtrim(realpath($_SERVER["DOCUMENT_ROOT"]), '/');
|
||||
$imageRoot = $webExpressContentDirAbs . '/webp-images';
|
||||
|
||||
// TODO: make this check work with symlinks
|
||||
//SanityCheck::absPathIsInDocRoot($imageRoot);
|
||||
|
||||
$sourceRel = substr(realpath($source), strlen($docRoot) + 1);
|
||||
$destination = $imageRoot . '/doc-root/' . $sourceRel;
|
||||
$destination = self::appendOrSetExtension($destination, $mingled, $replaceExt, false);
|
||||
|
||||
|
||||
// TODO: make this check work with symlinks
|
||||
//$destination = SanityCheck::absPathIsInDocRoot($destination);
|
||||
} else {
|
||||
$destination = '';
|
||||
|
||||
$sourceResolved = realpath($source);
|
||||
|
||||
|
||||
// Check roots until we (hopefully) get a match.
|
||||
// (that is: find a root which the source is inside)
|
||||
foreach ($imageRoots->getArray() as $i => $imageRoot) {
|
||||
// in $obj, "rel-path" is only set when document root can be used for relative paths.
|
||||
// So, if it is set, we can use it (beware: we cannot neccessarily use realpath on document root,
|
||||
// but we do not need to - see the long comment in Paths::canUseDocRootForRelPaths())
|
||||
|
||||
$rootPath = $imageRoot->getAbsPath();
|
||||
/*
|
||||
if (isset($obj['rel-path'])) {
|
||||
$docRoot = rtrim($_SERVER["DOCUMENT_ROOT"], '/');
|
||||
$rootPath = $docRoot . '/' . $obj['rel-path'];
|
||||
} else {
|
||||
// If "rel-path" isn't set, then abs-path is, and we can use that.
|
||||
$rootPath = $obj['abs-path'];
|
||||
}*/
|
||||
|
||||
// $source may be resolved or not. Same goes for $rootPath.
|
||||
// We can assume that $rootPath is resolvable using realpath (it ought to exist and be within open_basedir for WP to function)
|
||||
// We can also assume that $source is resolvable (it ought to exist and within open_basedir)
|
||||
// So: Resolve both! and test if the resolved source begins with the resolved rootPath.
|
||||
if (strpos($sourceResolved, realpath($rootPath)) !== false) {
|
||||
$relPath = substr($sourceResolved, strlen(realpath($rootPath)) + 1);
|
||||
$relPath = self::appendOrSetExtension($relPath, $mingled, $replaceExt, false);
|
||||
|
||||
$destination = $webExpressContentDirAbs . '/webp-images/' . $imageRoot->id . '/' . $relPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($destination == '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (SanityException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
}
|
||||
42
lib/classes/DestinationOptions.php
Normal file
42
lib/classes/DestinationOptions.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class DestinationOptions
|
||||
{
|
||||
|
||||
public $mingled;
|
||||
public $useDocRoot;
|
||||
public $replaceExt;
|
||||
public $scope;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $imageRootDef assoc array containing "id", "url" and either "abs-path", "rel-path" or both.
|
||||
*/
|
||||
public function __construct($mingled, $useDocRoot, $replaceExt, $scope)
|
||||
{
|
||||
$this->mingled = $mingled;
|
||||
$this->useDocRoot = $useDocRoot;
|
||||
$this->replaceExt = $replaceExt;
|
||||
$this->scope = $scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set properties from config file
|
||||
*
|
||||
* @param array $config WebP Express configuration object
|
||||
*/
|
||||
public static function createFromConfig(&$config)
|
||||
{
|
||||
return new DestinationOptions(
|
||||
$config['destination-folder'] == 'mingled', // "mingled" or "separate"
|
||||
$config['destination-structure'] == 'doc-root', // "doc-root" or "image-roots"
|
||||
$config['destination-extension'] == 'set', // "set" or "append"
|
||||
$config['scope']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
229
lib/classes/DestinationUrl.php
Normal file
229
lib/classes/DestinationUrl.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
/**
|
||||
* This class is not used yet! - and not finished!
|
||||
* It is the beginning of a refactor, where code is to be moved from AlterHtmlHelper to here
|
||||
*/
|
||||
class DestinationUrl
|
||||
{
|
||||
|
||||
/**
|
||||
* Gets relative path between a base url and another.
|
||||
* Returns false if the url isn't a subpath
|
||||
*
|
||||
* @param $imageUrl (ie "http://example.com/wp-content/image.jpg")
|
||||
* @param $baseUrl (ie "http://example.com/wp-content")
|
||||
* @return path or false (ie "/image.jpg")
|
||||
*/
|
||||
public static function getRelUrlPath($imageUrl, $baseUrl)
|
||||
{
|
||||
$baseUrlComponents = parse_url($baseUrl);
|
||||
/* ie:
|
||||
(
|
||||
[scheme] => http
|
||||
[host] => we0
|
||||
[path] => /wordpress/uploads-moved
|
||||
)*/
|
||||
|
||||
$imageUrlComponents = parse_url($imageUrl);
|
||||
/* ie:
|
||||
(
|
||||
[scheme] => http
|
||||
[host] => we0
|
||||
[path] => /wordpress/uploads-moved/logo.jpg
|
||||
)*/
|
||||
if ($baseUrlComponents['host'] != $imageUrlComponents['host']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if path begins with base path
|
||||
if (strpos($imageUrlComponents['path'], $baseUrlComponents['path']) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove base path from path (we know it begins with basepath, from previous check)
|
||||
return substr($imageUrlComponents['path'], strlen($baseUrlComponents['path']));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url for webp from source url, (if ), given a certain baseUrl / baseDir.
|
||||
* Base can for example be uploads or wp-content.
|
||||
*
|
||||
* returns false
|
||||
* - if no source file found in that base
|
||||
* - or source file is found but webp file isn't there and the `only-for-webps-that-exists` option is set
|
||||
*
|
||||
* @param string $sourceUrl Url of source image (ie http://example.com/wp-content/image.jpg)
|
||||
* @param string $rootId Id (created in Config::updateAutoloadedOptions). Ie "uploads", "content" or any image root id
|
||||
* @param string $baseUrl Base url of source image (ie http://example.com/wp-content)
|
||||
* @param string $baseDir Base dir of source image (ie /var/www/example.com/wp-content)
|
||||
* @param object $destinationOptions
|
||||
*/
|
||||
public static function getWebPUrlInImageRoot($sourceUrl, $rootId, $baseUrl, $baseDir, $destinationOptions)
|
||||
{
|
||||
//error_log('getWebPUrlInImageRoot:' . $sourceUrl . ':' . $baseUrl . ':' . $baseDir);
|
||||
|
||||
|
||||
$srcPathRel = self::getRelUrlPath($sourceUrl, $baseUrl);
|
||||
|
||||
if ($srcPathRel === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate file path to source
|
||||
$srcPathAbs = $baseDir . $srcPathRel;
|
||||
|
||||
// Check that source file exists
|
||||
if (!@file_exists($srcPathAbs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate destination of webp (both path and url)
|
||||
// ----------------------------------------
|
||||
|
||||
// We are calculating: $destPathAbs and $destUrl.
|
||||
|
||||
if (!isset($destinationOptions->scope) || !in_array($rootId, $destinationOptions->scope)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$destinationRoot = Paths::destinationRoot(
|
||||
$rootId,
|
||||
$destinationOptions
|
||||
);
|
||||
|
||||
$relPathFromImageRootToSource = PathHelper::getRelDir(
|
||||
realpath(Paths::getAbsDirById($rootId)),
|
||||
realpath($srcPathAbs)
|
||||
);
|
||||
$relPathFromImageRootToDest = Destination::appendOrSetExtension(
|
||||
$relPathFromImageRootToSource,
|
||||
$destinationOptions->mingled,
|
||||
$destinationOptions->replaceExt,
|
||||
($rootId == 'uploads')
|
||||
);
|
||||
$destPathAbs = $destinationRoot['abs-path'] . '/' . $relPathFromImageRootToDest;
|
||||
$webpMustExist = self::$options['only-for-webps-that-exists'];
|
||||
if ($webpMustExist && (!@file_exists($destPathAbs))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$destUrl = $destinationRoot['url'] . '/' . $relPathFromImageRootToDest;
|
||||
|
||||
// Fix scheme (use same as source)
|
||||
$sourceUrlComponents = parse_url($sourceUrl);
|
||||
$destUrlComponents = parse_url($destUrl);
|
||||
$port = isset($sourceUrlComponents['port']) ? ":" . $sourceUrlComponents['port'] : "";
|
||||
return $sourceUrlComponents['scheme'] . '://' . $sourceUrlComponents['host'] . $port . $destUrlComponents['path'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get url for webp
|
||||
* returns second argument if no webp
|
||||
*
|
||||
* @param $sourceUrl
|
||||
* @param $returnValueOnFail
|
||||
*/
|
||||
public static function getWebPUrl($sourceUrl, $returnValueOnFail)
|
||||
{
|
||||
// Get the options
|
||||
self::getOptions();
|
||||
|
||||
// Fail for webp-disabled browsers (when "only-for-webp-enabled-browsers" is set)
|
||||
if (self::$options['only-for-webp-enabled-browsers']) {
|
||||
if (!isset($_SERVER['HTTP_ACCEPT']) || (strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') === false)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
}
|
||||
|
||||
// Fail for relative urls. Wordpress doesn't use such very much anyway
|
||||
if (!preg_match('#^https?://#', $sourceUrl)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
|
||||
// Fail if the image type isn't enabled
|
||||
switch (self::$options['image-types']) {
|
||||
case 0:
|
||||
return $returnValueOnFail;
|
||||
case 1:
|
||||
if (!preg_match('#(jpe?g)$#', $sourceUrl)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (!preg_match('#(png)$#', $sourceUrl)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (!preg_match('#(jpe?g|png)$#', $sourceUrl)) {
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
//error_log('source url:' . $sourceUrl);
|
||||
|
||||
// Try all image roots
|
||||
foreach (self::$options['scope'] as $rootId) {
|
||||
$baseDir = Paths::getAbsDirById($rootId);
|
||||
$baseUrl = Paths::getUrlById($rootId);
|
||||
|
||||
//error_log('baseurl: ' . $baseUrl);
|
||||
if (Multisite::isMultisite() && ($rootId == 'uploads')) {
|
||||
$baseUrl = Paths::getUploadUrl();
|
||||
$baseDir = Paths::getUploadDirAbs();
|
||||
}
|
||||
|
||||
$result = self::getWebPUrlInImageRoot($sourceUrl, $rootId, $baseUrl, $baseDir);
|
||||
if ($result !== false) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Try the hostname aliases.
|
||||
if (!isset(self::$options['hostname-aliases'])) {
|
||||
continue;
|
||||
}
|
||||
$hostnameAliases = self::$options['hostname-aliases'];
|
||||
|
||||
$hostname = Paths::getHostNameOfUrl($baseUrl);
|
||||
$baseUrlComponents = parse_url($baseUrl);
|
||||
$sourceUrlComponents = parse_url($sourceUrl);
|
||||
// ie: [scheme] => http, [host] => we0, [path] => /wordpress/uploads-moved
|
||||
|
||||
if ((!isset($baseUrlComponents['host'])) || (!isset($sourceUrlComponents['host']))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($hostnameAliases as $hostnameAlias) {
|
||||
|
||||
if ($sourceUrlComponents['host'] != $hostnameAlias) {
|
||||
continue;
|
||||
}
|
||||
//error_log('hostname alias:' . $hostnameAlias);
|
||||
|
||||
$baseUrlOnAlias = $baseUrlComponents['scheme'] . '://' . $hostnameAlias . $baseUrlComponents['path'];
|
||||
//error_log('baseurl (alias):' . $baseUrlOnAlias);
|
||||
|
||||
$result = self::getWebPUrlInImageRoot($sourceUrl, $rootId, $baseUrlOnAlias, $baseDir);
|
||||
if ($result !== false) {
|
||||
$resultUrlComponents = parse_url($result);
|
||||
return $sourceUrlComponents['scheme'] . '://' . $hostnameAlias . $resultUrlComponents['path'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $returnValueOnFail;
|
||||
}
|
||||
|
||||
/*
|
||||
public static function getWebPUrlOrSame($sourceUrl, $returnValueOnFail)
|
||||
{
|
||||
return self::getWebPUrl($sourceUrl, $sourceUrl);
|
||||
}*/
|
||||
|
||||
}
|
||||
100
lib/classes/DismissableGlobalMessages.php
Normal file
100
lib/classes/DismissableGlobalMessages.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Option;
|
||||
use \WebPExpress\State;
|
||||
use \WebPExpress\Messenger;
|
||||
|
||||
class DismissableGlobalMessages
|
||||
{
|
||||
|
||||
/**
|
||||
* Add dismissible message.
|
||||
*
|
||||
* @param string $id An identifier, ie "suggest_enable_pngs"
|
||||
*/
|
||||
public static function addDismissableMessage($id)
|
||||
{
|
||||
$dismissableGlobalMessageIds = State::getState('dismissableGlobalMessageIds', []);
|
||||
|
||||
// Ensure we do not add a message that is already there
|
||||
if (in_array($id, $dismissableGlobalMessageIds)) {
|
||||
return;
|
||||
}
|
||||
$dismissableGlobalMessageIds[] = $id;
|
||||
State::setState('dismissableGlobalMessageIds', $dismissableGlobalMessageIds);
|
||||
}
|
||||
|
||||
public static function printDismissableMessage($level, $msg, $id, $buttons)
|
||||
{
|
||||
$msg .= '<br><br>';
|
||||
foreach ($buttons as $i => $button) {
|
||||
$javascript = "jQuery(this).closest('div.notice').slideUp();";
|
||||
//$javascript = "console.log(jQuery(this).closest('div.notice'));";
|
||||
$javascript .= "jQuery.post(ajaxurl, " .
|
||||
"{'action': 'webpexpress_dismiss_global_message', " .
|
||||
"'id': '" . $id . "'})";
|
||||
if (isset($button['javascript'])) {
|
||||
$javascript .= ".done(function() {" . $button['javascript'] . "});";
|
||||
}
|
||||
if (isset($button['redirect-to-settings'])) {
|
||||
$javascript .= ".done(function() {location.href='" . Paths::getSettingsUrl() . "'});";
|
||||
}
|
||||
|
||||
$msg .= '<button type="button" class="button ' .
|
||||
(($i == 0) ? 'button-primary' : '') .
|
||||
'" onclick="' . $javascript . '" ' .
|
||||
'style="display:inline-block; margin-top:20px; margin-right:20px; ' . (($i > 0) ? 'float:right;' : '') .
|
||||
'">' . $button['text'] . '</button>';
|
||||
|
||||
}
|
||||
Messenger::printMessage($level, $msg);
|
||||
}
|
||||
|
||||
public static function printMessages()
|
||||
{
|
||||
$ids = State::getState('dismissableGlobalMessageIds', []);
|
||||
foreach ($ids as $id) {
|
||||
include_once __DIR__ . '/../dismissable-global-messages/' . $id . '.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss message
|
||||
*
|
||||
* @param string $id An identifier, ie "suggest_enable_pngs"
|
||||
*/
|
||||
public static function dismissMessage($id) {
|
||||
$messages = State::getState('dismissableGlobalMessageIds', []);
|
||||
$newQueue = [];
|
||||
foreach ($messages as $mid) {
|
||||
if ($mid == $id) {
|
||||
|
||||
} else {
|
||||
$newQueue[] = $mid;
|
||||
}
|
||||
}
|
||||
State::setState('dismissableGlobalMessageIds', $newQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss message
|
||||
*
|
||||
* @param string $id An identifier, ie "suggest_enable_pngs"
|
||||
*/
|
||||
public static function dismissAll() {
|
||||
State::setState('dismissableGlobalMessageIds', []);
|
||||
}
|
||||
|
||||
public static function processAjaxDismissGlobalMessage() {
|
||||
/*
|
||||
We have no security nonce here
|
||||
Dismissing a message is not harmful and dismissMessage($id) do anything harmful, no matter what you send in the "id"
|
||||
*/
|
||||
$id = sanitize_text_field($_POST['id']);
|
||||
self::dismissMessage($id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
86
lib/classes/DismissableMessages.php
Normal file
86
lib/classes/DismissableMessages.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Option;
|
||||
use \WebPExpress\State;
|
||||
use \WebPExpress\Messenger;
|
||||
|
||||
class DismissableMessages
|
||||
{
|
||||
|
||||
/**
|
||||
* Add dismissible message.
|
||||
*
|
||||
* @param string $id An identifier, ie "suggest_enable_pngs"
|
||||
*/
|
||||
public static function addDismissableMessage($id)
|
||||
{
|
||||
$dismissableMessageIds = State::getState('dismissableMessageIds', []);
|
||||
|
||||
// Ensure we do not add a message that is already there
|
||||
if (in_array($id, $dismissableMessageIds)) {
|
||||
return;
|
||||
}
|
||||
$dismissableMessageIds[] = $id;
|
||||
State::setState('dismissableMessageIds', $dismissableMessageIds);
|
||||
}
|
||||
|
||||
public static function printDismissableMessage($level, $msg, $id, $gotItText = '')
|
||||
{
|
||||
if ($gotItText != '') {
|
||||
$javascript = "jQuery(this).closest('div.notice').slideUp();";
|
||||
//$javascript = "console.log(jQuery(this).closest('div.notice'));";
|
||||
$javascript .= "jQuery.post(ajaxurl, {'action': 'webpexpress_dismiss_message', 'id': '" . $id . "'});";
|
||||
|
||||
$msg .= '<button type="button" class="button button-primary" onclick="' . $javascript . '" style="display:block; margin-top:20px">' . $gotItText . '</button>';
|
||||
}
|
||||
Messenger::printMessage($level, $msg);
|
||||
}
|
||||
|
||||
public static function printMessages()
|
||||
{
|
||||
$ids = State::getState('dismissableMessageIds', []);
|
||||
foreach ($ids as $id) {
|
||||
include_once __DIR__ . '/../dismissable-messages/' . $id . '.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss message
|
||||
*
|
||||
* @param string $id An identifier, ie "suggest_enable_pngs"
|
||||
*/
|
||||
public static function dismissMessage($id) {
|
||||
$messages = State::getState('dismissableMessageIds', []);
|
||||
$newQueue = [];
|
||||
foreach ($messages as $mid) {
|
||||
if ($mid == $id) {
|
||||
|
||||
} else {
|
||||
$newQueue[] = $mid;
|
||||
}
|
||||
}
|
||||
State::setState('dismissableMessageIds', $newQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss message
|
||||
*
|
||||
* @param string $id An identifier, ie "suggest_enable_pngs"
|
||||
*/
|
||||
public static function dismissAll() {
|
||||
State::setState('dismissableMessageIds', []);
|
||||
}
|
||||
|
||||
public static function processAjaxDismissMessage() {
|
||||
/*
|
||||
We have no security nonce here
|
||||
Dismissing a message is not harmful and dismissMessage($id) do anything harmful, no matter what you send in the "id"
|
||||
*/
|
||||
$id = sanitize_text_field($_POST['id']);
|
||||
self::dismissMessage($id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
113
lib/classes/EwwwTools.php
Normal file
113
lib/classes/EwwwTools.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
// This class does NOT, and MAY NOT rely on Wordpress functions (it is used in WebPOnDemand)
|
||||
class EwwwTools
|
||||
{
|
||||
/**
|
||||
* Mark ewww api keys as non functional in config.json
|
||||
*
|
||||
* @return boolean If it went well.
|
||||
*/
|
||||
private static function markApiKeysAsNonFunctionalInConfig($apiKeysToMarkAsNonFunctional, $configDir)
|
||||
{
|
||||
$config = FileHelper::loadJSONOptions($configDir . '/config.json');
|
||||
if ($config === false) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($config['converters'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_array($config['converters'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($config['converters'] as &$c) {
|
||||
if (!isset($c['converter'])) {
|
||||
continue;
|
||||
}
|
||||
if ($c['converter'] == 'ewww') {
|
||||
if (!isset($c['non-functional-api-keys'])) {
|
||||
$c['non-functional-api-keys'] = [];
|
||||
}
|
||||
$c['non-functional-api-keys'] = array_unique(
|
||||
array_merge($c['non-functional-api-keys'], $apiKeysToMarkAsNonFunctional)
|
||||
);
|
||||
|
||||
// Do we have an api-key-2 which is not "blacklisted"?
|
||||
$haveBackupKey = (isset($c['options']['api-key-2']) && !empty($c['options']['api-key-2']));
|
||||
$switchToBackupKey = $haveBackupKey && (!in_array($c['options']['api-key-2'], $c['non-functional-api-keys']));
|
||||
|
||||
if ($switchToBackupKey) {
|
||||
$temp = $c['options']['api-key'];
|
||||
$c['options']['api-key'] = $c['options']['api-key-2'];
|
||||
$c['options']['api-key-2'] = $temp;
|
||||
} else {
|
||||
// deactivate converter, we must then.
|
||||
$c['deactivated'] = true;
|
||||
$c['working'] = false;
|
||||
}
|
||||
|
||||
//$successfulWrite = Config::saveConfigurationFileAndWodOptions($config);
|
||||
$successfulWrite = FileHelper::saveJSONOptions($configDir . '/config.json', $config);
|
||||
return $successfulWrite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove ewww in wod-options.json
|
||||
*
|
||||
* @return boolean If it went well.
|
||||
*/
|
||||
private static function removeEwwwFromWodOptions($apiKeysToMarkAsNonFunctional, $configDir)
|
||||
{
|
||||
$wodOptions = FileHelper::loadJSONOptions($configDir . '/wod-options.json');
|
||||
if ($config === false) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($wodOptions['webp-convert']['convert']['converters'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_array($wodOptions['webp-convert']['convert']['converters'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($wodOptions['webp-convert']['convert']['converters'] as $i => $c) {
|
||||
if (!isset($c['converter'])) {
|
||||
continue;
|
||||
}
|
||||
if ($c['converter'] == 'ewww') {
|
||||
//unset($wodOptions['webp-convert']['convert']['converters'][$i]);
|
||||
array_splice($wodOptions['webp-convert']['convert']['converters'], $i, 1);
|
||||
|
||||
//$successfulWrite = Config::saveConfigurationFileAndWodOptions($config);
|
||||
$successfulWrite = FileHelper::saveJSONOptions($configDir . '/wod-options.json', $wodOptions);
|
||||
return $successfulWrite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark ewww api keys as non functional.
|
||||
*
|
||||
* Current implementation simply removes ewww from wod-options.json.
|
||||
* It will reappear when options are saved - but be removed again upon next failure
|
||||
*
|
||||
* @return boolean If it went well.
|
||||
*/
|
||||
public static function markApiKeysAsNonFunctional($apiKeysToMarkAsNonFunctional, $configDir)
|
||||
{
|
||||
//self::markApiKeysAsNonFunctionalInConfig($apiKeysToMarkAsNonFunctional, $configDir);
|
||||
|
||||
// TODO: We should update the key to api-key-2 the first time.
|
||||
// But I am going to change the structure of wod-options so ewww becomes a stack converter, so
|
||||
// I don't bother implementing this right now.
|
||||
self::removeEwwwFromWodOptions($apiKeysToMarkAsNonFunctional, $configDir);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
395
lib/classes/FileHelper.php
Normal file
395
lib/classes/FileHelper.php
Normal file
@@ -0,0 +1,395 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class FileHelper
|
||||
{
|
||||
|
||||
public static function fileExists($filename) {
|
||||
return @file_exists($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file permission of a file (integer). Only get the last part, ie 0644
|
||||
* If failure, it returns false
|
||||
*/
|
||||
public static function filePerm($filename) {
|
||||
if (!self::fileExists($filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// fileperms can still fail. In that case, it returns false
|
||||
$perm = @fileperms($filename);
|
||||
if ($perm === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return octdec(substr(decoct($perm), -4));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get file permission of a file (integer). Only get the last part, ie 0644
|
||||
* If failure, it returns $fallback
|
||||
*/
|
||||
public static function filePermWithFallback($filename, $fallback) {
|
||||
$perm = self::filePerm($filename);
|
||||
if ($perm === false) {
|
||||
return $fallback;
|
||||
}
|
||||
return $perm;
|
||||
}
|
||||
|
||||
public static function humanReadableFilePerm($mode) {
|
||||
return substr(decoct($mode), -4);
|
||||
}
|
||||
|
||||
public static function humanReadableFilePermOfFile($filename) {
|
||||
return self::humanReadableFilePerm(self::filePerm($filename));
|
||||
}
|
||||
|
||||
/**
|
||||
* As the return value of the PHP function isn't reliable,
|
||||
* we have our own chmod.
|
||||
*/
|
||||
public static function chmod($filename, $mode) {
|
||||
// In case someone carelessly passed the result of a filePerm call, which was false:
|
||||
if ($mode === false) {
|
||||
return false;
|
||||
}
|
||||
$existingPermission = self::filePerm($filename);
|
||||
if ($mode === $existingPermission) {
|
||||
return true;
|
||||
}
|
||||
if (@chmod($filename, $mode)) {
|
||||
// in some cases chmod returns true, even though it did not succeed!
|
||||
// - so we test if our operation had the desired effect.
|
||||
if (self::filePerm($filename) !== $mode) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function chmod_r($dir, $dirPerm = null, $filePerm = null, $uid = null, $gid = null, $regexFileMatchPattern = null, $regexDirMatchPattern = null) {
|
||||
if (!@file_exists($dir) || (!@is_dir($dir))) {
|
||||
return;
|
||||
}
|
||||
$fileIterator = new \FilesystemIterator($dir);
|
||||
|
||||
while ($fileIterator->valid()) {
|
||||
$filename = $fileIterator->getFilename();
|
||||
$filepath = $dir . "/" . $filename;
|
||||
|
||||
// echo $filepath . "\n";
|
||||
|
||||
$isDir = @is_dir($filepath);
|
||||
|
||||
if ((!$isDir && (is_null($regexFileMatchPattern) || preg_match($regexFileMatchPattern, $filename))) ||
|
||||
($isDir && (is_null($regexDirMatchPattern) || preg_match($regexDirMatchPattern, $filename)))) {
|
||||
// chmod
|
||||
if ($isDir) {
|
||||
if (!is_null($dirPerm)) {
|
||||
self::chmod($filepath, $dirPerm);
|
||||
//echo '. chmod dir to:' . self::humanReadableFilePerm($dirPerm) . '. result:' . self::humanReadableFilePermOfFile($filepath) . "\n";
|
||||
}
|
||||
} else {
|
||||
if (!is_null($filePerm)) {
|
||||
self::chmod($filepath, $filePerm);
|
||||
//echo '. chmod file to:' . self::humanReadableFilePerm($filePerm) . '. result:' . self::humanReadableFilePermOfFile($filepath) . "\n";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// chown
|
||||
if (!is_null($uid)) {
|
||||
@chown($filepath, $uid);
|
||||
}
|
||||
|
||||
// chgrp
|
||||
if (!is_null($gid)) {
|
||||
@chgrp($filepath, $gid);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// recurse
|
||||
if ($isDir) {
|
||||
self::chmod_r($filepath, $dirPerm, $filePerm, $uid, $gid, $regexFileMatchPattern, $regexDirMatchPattern);
|
||||
}
|
||||
|
||||
// next!
|
||||
$fileIterator->next();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a dir using same permissions as parent.
|
||||
* If
|
||||
*/
|
||||
/*
|
||||
public static function mkdirSamePermissionsAsParent($pathname) {
|
||||
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Get directory part of filename.
|
||||
* Ie '/var/www/.htaccess' => '/var/www'
|
||||
* Also works with backslashes
|
||||
*/
|
||||
public static function dirName($filename) {
|
||||
return preg_replace('/[\/\\\\][^\/\\\\]*$/', '', $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a file can be created.
|
||||
* BEWARE: It requires that the containing folder already exists
|
||||
*/
|
||||
public static function canCreateFile($filename) {
|
||||
$dirName = self::dirName($filename);
|
||||
if (!@file_exists($dirName)) {
|
||||
return false;
|
||||
}
|
||||
if (@is_writable($dirName) && @is_executable($dirName) || self::isWindows() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$existingPermission = self::filePerm($dirName);
|
||||
|
||||
// we need to make sure we got the existing permission, so we can revert correctly later
|
||||
if ($existingPermission !== false) {
|
||||
if (self::chmod($dirName, 0775)) {
|
||||
// change back
|
||||
self::chmod($filename, $existingPermission);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Do not use for directories
|
||||
*/
|
||||
public static function canEditFile($filename) {
|
||||
if (!@file_exists($filename)) {
|
||||
return false;
|
||||
}
|
||||
if (@is_writable($filename) && @is_readable($filename)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// As a last desperate try, lets see if we can give ourself write permissions.
|
||||
// If possible, then it will also be possible when actually writing
|
||||
$existingPermission = self::filePerm($filename);
|
||||
|
||||
// we need to make sure we got the existing permission, so we can revert correctly later
|
||||
if ($existingPermission !== false) {
|
||||
if (self::chmod($filename, 0664)) {
|
||||
// change back
|
||||
self::chmod($filename, $existingPermission);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
// Idea: Perhaps we should also try to actually open the file for writing?
|
||||
|
||||
}
|
||||
|
||||
public static function canEditOrCreateFileHere($filename) {
|
||||
if (@file_exists($filename)) {
|
||||
return self::canEditFile($filename);
|
||||
} else {
|
||||
return self::canCreateFile($filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to read from a file. Tries hard.
|
||||
* Returns content, or false if read error.
|
||||
*/
|
||||
public static function loadFile($filename) {
|
||||
$changedPermission = false;
|
||||
if (!@is_readable($filename)) {
|
||||
$existingPermission = self::filePerm($filename);
|
||||
|
||||
// we need to make sure we got the existing permission, so we can revert correctly later
|
||||
if ($existingPermission !== false) {
|
||||
$changedPermission = self::chmod($filename, 0664);
|
||||
}
|
||||
}
|
||||
|
||||
$return = false;
|
||||
try {
|
||||
$handle = @fopen($filename, "r");
|
||||
} catch (\ErrorException $exception) {
|
||||
$handle = false;
|
||||
error_log($exception->getMessage());
|
||||
}
|
||||
if ($handle !== false) {
|
||||
// Return value is either file content or false
|
||||
if (filesize($filename) == 0) {
|
||||
$return = '';
|
||||
} else {
|
||||
$return = @fread($handle, filesize($filename));
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
if ($changedPermission) {
|
||||
// change back
|
||||
self::chmod($filename, $existingPermission);
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/* Remove dir and files in it recursively.
|
||||
No warnings
|
||||
returns $success
|
||||
*/
|
||||
public static function rrmdir($dir) {
|
||||
if (@is_dir($dir)) {
|
||||
$objects = @scandir($dir);
|
||||
foreach ($objects as $object) {
|
||||
if ($object != "." && $object != "..") {
|
||||
if (@is_dir($dir . "/" . $object))
|
||||
self::rrmdir($dir . "/" . $object);
|
||||
else
|
||||
@unlink($dir . "/" . $object);
|
||||
}
|
||||
}
|
||||
return @rmdir($dir);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Copy dir and all its files.
|
||||
* Existing files are overwritten.
|
||||
*
|
||||
* @return $success
|
||||
*/
|
||||
public static function cpdir($sourceDir, $destinationDir)
|
||||
{
|
||||
if (!@is_dir($sourceDir)) {
|
||||
return false;
|
||||
}
|
||||
if (!@file_exists($destinationDir)) {
|
||||
if (!@mkdir($destinationDir)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$fileIterator = new \FilesystemIterator($sourceDir);
|
||||
$success = true;
|
||||
|
||||
while ($fileIterator->valid()) {
|
||||
$filename = $fileIterator->getFilename();
|
||||
|
||||
if (($filename != ".") && ($filename != "..")) {
|
||||
//$filePerm = FileHelper::filePermWithFallback($filename, 0777);
|
||||
|
||||
if (@is_dir($sourceDir . "/" . $filename)) {
|
||||
if (!self::cpdir($sourceDir . "/" . $filename, $destinationDir . "/" . $filename)) {
|
||||
$success = false;
|
||||
}
|
||||
} else {
|
||||
// its a file.
|
||||
if (!copy($sourceDir . "/" . $filename, $destinationDir . "/" . $filename)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileIterator->next();
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove empty subfolders.
|
||||
*
|
||||
* Got it here: https://stackoverflow.com/a/1833681/842756
|
||||
*
|
||||
* @return boolean If folder is (was) empty
|
||||
*/
|
||||
public static function removeEmptySubFolders($path, $removeEmptySelfToo = false)
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
$empty = true;
|
||||
foreach (scandir($path) as $file) {
|
||||
if (($file == '.') || ($file == '..')) {
|
||||
continue;
|
||||
}
|
||||
$file = $path . DIRECTORY_SEPARATOR . $file;
|
||||
if (is_dir($file)) {
|
||||
if (!self::removeEmptySubFolders($file, true)) {
|
||||
$empty=false;
|
||||
}
|
||||
} else {
|
||||
$empty=false;
|
||||
}
|
||||
}
|
||||
if ($empty && $removeEmptySelfToo) {
|
||||
rmdir($path);
|
||||
}
|
||||
return $empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if OS is Windows
|
||||
*
|
||||
*
|
||||
* @return true if windows; false if not.
|
||||
*/
|
||||
public static function isWindows(){
|
||||
return preg_match('/^win/i', PHP_OS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalize separators of directory paths
|
||||
*
|
||||
*
|
||||
* @return $normalized_path
|
||||
*/
|
||||
public static function normalizeSeparator($path, $newSeparator = DIRECTORY_SEPARATOR){
|
||||
return preg_replace("#[\\\/]+#", $newSeparator, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object|false Returns parsed file the file exists and can be read. Otherwise it returns false
|
||||
*/
|
||||
public static function loadJSONOptions($filename)
|
||||
{
|
||||
$json = self::loadFile($filename);
|
||||
if ($json === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$options = json_decode($json, true);
|
||||
if ($options === null) {
|
||||
return false;
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
public static function saveJSONOptions($filename, $obj)
|
||||
{
|
||||
$result = @file_put_contents(
|
||||
$filename,
|
||||
json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT)
|
||||
);
|
||||
/*if ($result === false) {
|
||||
echo 'COULD NOT' . $filename;
|
||||
}*/
|
||||
return ($result !== false);
|
||||
}
|
||||
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
187
lib/classes/HTAccessCapabilityTestRunner.php
Normal file
187
lib/classes/HTAccessCapabilityTestRunner.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/*
|
||||
This functionality will be moved to a separate project.
|
||||
|
||||
Btw:
|
||||
Seems someone else got similar idea:
|
||||
http://christian.roy.name/blog/detecting-modrewrite-using-php
|
||||
*/
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\FileHelper;
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
use \HtaccessCapabilityTester\HtaccessCapabilityTester;
|
||||
|
||||
include_once WEBPEXPRESS_PLUGIN_DIR . '/vendor/autoload.php';
|
||||
|
||||
class HTAccessCapabilityTestRunner
|
||||
{
|
||||
|
||||
public static $cachedResults;
|
||||
|
||||
/**
|
||||
* Tests if a test script responds with "pong"
|
||||
*/
|
||||
private static function canRunPingPongTestScript($url)
|
||||
{
|
||||
$response = wp_remote_get($url, ['timeout' => 10]);
|
||||
//echo '<pre>' . print_r($response, true) . '</pre>';
|
||||
if (is_wp_error($response)) {
|
||||
return null;
|
||||
}
|
||||
if (wp_remote_retrieve_response_code($response) != '200') {
|
||||
return false;
|
||||
}
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
return ($body == 'pong');
|
||||
}
|
||||
|
||||
private static function runNamedTest($testName)
|
||||
{
|
||||
switch ($testName) {
|
||||
case 'canRunTestScriptInWOD':
|
||||
$url = Paths::getWebPExpressPluginUrl() . '/wod/ping.php';
|
||||
return self::canRunPingPongTestScript($url);
|
||||
|
||||
case 'canRunTestScriptInWOD2':
|
||||
$url = Paths::getWebPExpressPluginUrl() . '/wod2/ping.php';
|
||||
return self::canRunPingPongTestScript($url);
|
||||
|
||||
case 'htaccessEnabled':
|
||||
return self::runTestInWebPExpressContentDir('htaccessEnabled');
|
||||
|
||||
case 'modHeadersLoaded':
|
||||
return self::runTestInWebPExpressContentDir('modHeadersLoaded');
|
||||
|
||||
case 'modHeaderWorking':
|
||||
return self::runTestInWebPExpressContentDir('headerSetWorks');
|
||||
|
||||
case 'modRewriteWorking':
|
||||
return self::runTestInWebPExpressContentDir('rewriteWorks');
|
||||
|
||||
case 'passThroughEnvWorking':
|
||||
return self::runTestInWebPExpressContentDir('passingInfoFromRewriteToScriptThroughEnvWorks');
|
||||
|
||||
case 'passThroughHeaderWorking':
|
||||
// pretend it fails because .htaccess rules aren't currently generated correctly
|
||||
return false;
|
||||
return self::runTestInWebPExpressContentDir('passingInfoFromRewriteToScriptThroughRequestHeaderWorks');
|
||||
|
||||
case 'grantAllAllowed':
|
||||
return self::runTestInWebPExpressContentDir('grantAllCrashTester');
|
||||
}
|
||||
}
|
||||
|
||||
private static function runOrGetCached($testName)
|
||||
{
|
||||
if (!isset(self::$cachedResults)) {
|
||||
self::$cachedResults = [];
|
||||
}
|
||||
if (!isset(self::$cachedResults[$testName])) {
|
||||
self::$cachedResults[$testName] = self::runNamedTest($testName);
|
||||
}
|
||||
return self::$cachedResults[$testName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one of the htaccess capability tests.
|
||||
* Three possible outcomes: true, false or null (null if request fails)
|
||||
*/
|
||||
private static function runTestInWebPExpressContentDir($testName)
|
||||
{
|
||||
$baseDir = Paths::getWebPExpressContentDirAbs() . '/htaccess-capability-tests';
|
||||
$baseUrl = Paths::getContentUrl() . '/webp-express/htaccess-capability-tests';
|
||||
|
||||
$hct = new HtaccessCapabilityTester($baseDir, $baseUrl);
|
||||
$hct->setHttpRequester(new WPHttpRequester());
|
||||
|
||||
try {
|
||||
switch ($testName) {
|
||||
case 'htaccessEnabled':
|
||||
return $hct->htaccessEnabled();
|
||||
case 'rewriteWorks':
|
||||
return $hct->rewriteWorks();
|
||||
case 'addTypeWorks':
|
||||
return $hct->addTypeWorks();
|
||||
case 'modHeadersLoaded':
|
||||
return $hct->moduleLoaded('headers');
|
||||
case 'headerSetWorks':
|
||||
return $hct->headerSetWorks();
|
||||
case 'requestHeaderWorks':
|
||||
return $hct->requestHeaderWorks();
|
||||
case 'passingInfoFromRewriteToScriptThroughRequestHeaderWorks':
|
||||
return $hct->passingInfoFromRewriteToScriptThroughRequestHeaderWorks();
|
||||
case 'passingInfoFromRewriteToScriptThroughEnvWorks':
|
||||
return $hct->passingInfoFromRewriteToScriptThroughEnvWorks();
|
||||
case 'grantAllCrashTester':
|
||||
$rules = <<<'EOD'
|
||||
<FilesMatch "(webp-on-demand\.php|webp-realizer\.php|ping\.php|ping\.txt)$">
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Allow from all
|
||||
</IfModule>
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all granted
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
EOD;
|
||||
return $hct->crashTest($rules, 'grant-all');
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
//error_log('test: ' . $testName . ':' . (($testResult === true) ? 'ok' : ($testResult === false ? 'failed' : 'hm')));
|
||||
|
||||
throw new \Exception('Unknown test:' . $testName);
|
||||
}
|
||||
|
||||
|
||||
public static function modRewriteWorking()
|
||||
{
|
||||
return self::runOrGetCached('modRewriteWorking');
|
||||
}
|
||||
|
||||
public static function htaccessEnabled()
|
||||
{
|
||||
return self::runOrGetCached('htaccessEnabled');
|
||||
}
|
||||
|
||||
public static function modHeadersLoaded()
|
||||
{
|
||||
return self::runOrGetCached('modHeadersLoaded');
|
||||
}
|
||||
|
||||
public static function modHeaderWorking()
|
||||
{
|
||||
return self::runOrGetCached('modHeaderWorking');
|
||||
}
|
||||
|
||||
public static function passThroughEnvWorking()
|
||||
{
|
||||
return self::runOrGetCached('passThroughEnvWorking');
|
||||
}
|
||||
|
||||
public static function passThroughHeaderWorking()
|
||||
{
|
||||
return self::runOrGetCached('passThroughHeaderWorking');
|
||||
}
|
||||
|
||||
public static function grantAllAllowed()
|
||||
{
|
||||
return self::runOrGetCached('grantAllAllowed');
|
||||
}
|
||||
|
||||
public static function canRunTestScriptInWOD()
|
||||
{
|
||||
return self::runOrGetCached('canRunTestScriptInWOD');
|
||||
}
|
||||
|
||||
public static function canRunTestScriptInWOD2()
|
||||
{
|
||||
return self::runOrGetCached('canRunTestScriptInWOD2');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
1200
lib/classes/HTAccessRules.php
Normal file
1200
lib/classes/HTAccessRules.php
Normal file
File diff suppressed because it is too large
Load Diff
42
lib/classes/HandleDeleteFileHook.php
Normal file
42
lib/classes/HandleDeleteFileHook.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
use \WebPExpress\Convert;
|
||||
use \WebPExpress\Mime;
|
||||
use \WebPExpress\SanityCheck;
|
||||
|
||||
class HandleDeleteFileHook
|
||||
{
|
||||
|
||||
/**
|
||||
* hook: wp_delete_file
|
||||
*/
|
||||
public static function deleteAssociatedWebP($filename)
|
||||
{
|
||||
try {
|
||||
$filename = SanityCheck::absPathExistsAndIsFileInDocRoot($filename);
|
||||
|
||||
$mimeTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
];
|
||||
if (!Mime::isOneOfTheseImageMimeTypes($filename, $mimeTypes)) {
|
||||
return $filename;
|
||||
}
|
||||
|
||||
$config = Config::loadConfigAndFix();
|
||||
$destination = Convert::getDestination($filename, $config);
|
||||
if (@file_exists($destination)) {
|
||||
if (@unlink($destination)) {
|
||||
Convert::updateBiggerThanOriginalMark($filename, $destination, $config);
|
||||
} else {
|
||||
error_log('WebP Express failed deleting webp:' . $destination);
|
||||
}
|
||||
}
|
||||
} catch (SanityException $e) {
|
||||
// fail silently. (maybe we should write to debug log instead?)
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
90
lib/classes/HandleUploadHooks.php
Normal file
90
lib/classes/HandleUploadHooks.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Config;
|
||||
use \WebPExpress\Convert;
|
||||
use \WebPExpress\Mime;
|
||||
use \WebPExpress\SanityCheck;
|
||||
use \WebPExpress\SanityException;
|
||||
|
||||
class HandleUploadHooks
|
||||
{
|
||||
|
||||
private static $config;
|
||||
|
||||
/**
|
||||
* Convert if:
|
||||
* - Option has been enabled
|
||||
* - We are not in "No conversion" mode
|
||||
* - The mime type is one of the ones the user has activated (in config)
|
||||
*/
|
||||
private static function convertIf($filename)
|
||||
{
|
||||
if (!isset(self::$config)) {
|
||||
self::$config = Config::loadConfigAndFix();
|
||||
}
|
||||
|
||||
$config = &self::$config;
|
||||
|
||||
if (!$config['convert-on-upload']) {
|
||||
return;
|
||||
}
|
||||
if ($config['operation-mode'] == 'no-conversion') {
|
||||
return;
|
||||
}
|
||||
|
||||
//$mimeType = getimagesize($filename)['mime'];
|
||||
|
||||
$allowedMimeTypes = [];
|
||||
$imageTypes = $config['image-types'];
|
||||
if ($imageTypes & 1) {
|
||||
$allowedMimeTypes[] = 'image/jpeg';
|
||||
$allowedMimeTypes[] = 'image/jpg'; /* don't think "image/jpg" is necessary, but just in case */
|
||||
}
|
||||
if ($imageTypes & 2) {
|
||||
$allowedMimeTypes[] = 'image/png';
|
||||
}
|
||||
|
||||
if (!in_array(Mime::getMimeTypeOfMedia($filename), $allowedMimeTypes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Convert::convertFile($filename, $config);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* hook: handle_upload
|
||||
* $filename is ie "/var/www/webp-express-tests/we0/wordpress/uploads-moved/image4-10-150x150.jpg"
|
||||
*/
|
||||
public static function handleUpload($filearray, $overrides = false, $ignore = false)
|
||||
{
|
||||
if (isset($filearray['file'])) {
|
||||
try {
|
||||
$filename = SanityCheck::absPathExistsAndIsFileInDocRoot($filearray['file']);
|
||||
self::convertIf($filename);
|
||||
} catch (SanityException $e) {
|
||||
// fail silently. (maybe we should write to debug log instead?)
|
||||
}
|
||||
}
|
||||
return $filearray;
|
||||
}
|
||||
|
||||
/**
|
||||
* hook: image_make_intermediate_size
|
||||
* $filename is ie "/var/www/webp-express-tests/we0/wordpress/uploads-moved/image4-10-150x150.jpg"
|
||||
*/
|
||||
public static function handleMakeIntermediateSize($filename)
|
||||
{
|
||||
if (!is_null($filename)) {
|
||||
try {
|
||||
$filenameToConvert = SanityCheck::absPathExistsAndIsFileInDocRoot($filename);
|
||||
self::convertIf($filenameToConvert);
|
||||
} catch (SanityException $e) {
|
||||
// fail silently. (maybe we should write to debug log instead?)
|
||||
}
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
53
lib/classes/ImageRoot.php
Normal file
53
lib/classes/ImageRoot.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\PathHelper;
|
||||
|
||||
class ImageRoot
|
||||
{
|
||||
public $id;
|
||||
private $imageRootDef;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $imageRootDef assoc array containing "id", "url" and either "abs-path", "rel-path" or both.
|
||||
*/
|
||||
public function __construct($imageRootDef)
|
||||
{
|
||||
$this->imageRootDef = $imageRootDef;
|
||||
$this->id = $imageRootDef['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get / calculate abs path.
|
||||
*
|
||||
* If "rel-path" is set and document root is available, the abs path will be calculated from the relative path.
|
||||
* Otherwise the "abs-path" is returned.
|
||||
* @throws Exception In case rel-path is not
|
||||
*/
|
||||
public function getAbsPath()
|
||||
{
|
||||
$def = $this->imageRootDef;
|
||||
if (isset($def['rel-path']) && PathHelper::isDocRootAvailable()) {
|
||||
return rtrim($_SERVER["DOCUMENT_ROOT"], '/') . '/' . $def['rel-path'];
|
||||
} elseif (isset($def['abs-path'])) {
|
||||
return $def['abs-path'];
|
||||
} else {
|
||||
if (!isset($def['rel-path'])) {
|
||||
throw new \Exception(
|
||||
'Image root definition in config file is must either have a "rel-path" or "abs-path" property defined. ' .
|
||||
'Probably your system setup has changed. Please re-save WebP Express options and regenerate .htaccess'
|
||||
);
|
||||
} else {
|
||||
throw new \Exception(
|
||||
'Image root definition in config file is defined by "rel-path". However, DOCUMENT_ROOT is unavailable so we ' .
|
||||
'cannot use that (as the rel-path is relative to that. ' .
|
||||
'Probably your system setup has changed. Please re-save WebP Express options and regenerate .htaccess'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
52
lib/classes/ImageRoots.php
Normal file
52
lib/classes/ImageRoots.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\ImageRoot;
|
||||
|
||||
class ImageRoots
|
||||
{
|
||||
private $imageRootsDef;
|
||||
private $imageRoots;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $imageRoots Array representation of image roots
|
||||
*/
|
||||
public function __construct($imageRootsDef)
|
||||
{
|
||||
$this->imageRootsDef = $imageRootsDef;
|
||||
|
||||
$this->imageRoots = [];
|
||||
foreach ($imageRootsDef as $i => $def)
|
||||
{
|
||||
$this->imageRoots[] = new ImageRoot($def);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image root by id.
|
||||
*
|
||||
* @return \WebPExpress\ImageRoot An image root object
|
||||
*/
|
||||
public function byId($id)
|
||||
{
|
||||
foreach ($this->imageRoots as $i => $imageRoot) {
|
||||
if ($imageRoot->id == $id) {
|
||||
return $imageRoot;
|
||||
}
|
||||
}
|
||||
throw new \Exception('Image root not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image roots array
|
||||
*
|
||||
* @return array An array of ImageRoot objects
|
||||
*/
|
||||
public function getArray()
|
||||
{
|
||||
return $this->imageRoots;
|
||||
}
|
||||
}
|
||||
60
lib/classes/KeepEwwwSubscriptionAlive.php
Normal file
60
lib/classes/KeepEwwwSubscriptionAlive.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Config;
|
||||
use \WebPExpress\Messenger;
|
||||
use \WebPExpress\State;
|
||||
use \WebPConvert\Converters\Ewww;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class KeepEwwwSubscriptionAlive
|
||||
{
|
||||
public static function keepAlive($config = null) {
|
||||
include_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
if (is_null($config)) {
|
||||
$config = Config::loadConfigAndFix(false); // false, because we do not need to test if quality detection is working
|
||||
}
|
||||
|
||||
$ewww = Config::getConverterByName($config, 'ewww');
|
||||
if (!isset($ewww['options']['key'])) {
|
||||
return;
|
||||
}
|
||||
if (!$ewww['working']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ewwwConvertResult = Ewww::keepSubscriptionAlive(__DIR__ . '/../../test/very-small.jpg', $ewww['options']['key']);
|
||||
if ($ewwwConvertResult === true) {
|
||||
Messenger::addMessage(
|
||||
'info',
|
||||
'Successfully optimized regular jpg with <i>ewww</i> converter in order to keep the subscription alive'
|
||||
);
|
||||
State::setState('last-ewww-optimize', time());
|
||||
} else {
|
||||
Messenger::addMessage(
|
||||
'warning',
|
||||
'Failed optimizing regular jpg with <i>ewww</i> converter in order to keep the subscription alive'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function keepAliveIfItIsTime($config = null) {
|
||||
|
||||
$timeSinseLastSuccesfullOptimize = time() - State::getState('last-ewww-optimize', 0);
|
||||
if ($timeSinseLastSuccesfullOptimize > 3 * 30 * 24 * 60 * 60) {
|
||||
|
||||
$timeSinseLastOptimizeAttempt = time() - State::getState('last-ewww-optimize-attempt', 0);
|
||||
if ($timeSinseLastOptimizeAttempt > 14 * 24 * 60 * 60) {
|
||||
State::setState('last-ewww-optimize-attempt', time());
|
||||
self::keepAlive($config);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
99
lib/classes/LogPurge.php
Normal file
99
lib/classes/LogPurge.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class LogPurge
|
||||
{
|
||||
|
||||
/**
|
||||
* - Removes cache dir
|
||||
* - Removes all files with ".webp" extension in upload dir (if set to mingled)
|
||||
*/
|
||||
public static function purge()
|
||||
{
|
||||
DismissableMessages::dismissMessage('0.14.0/suggest-wipe-because-lossless');
|
||||
|
||||
$filter = [
|
||||
'only-png' => $onlyPng,
|
||||
'only-with-corresponding-original' => false
|
||||
];
|
||||
|
||||
$numDeleted = 0;
|
||||
$numFailed = 0;
|
||||
|
||||
$dir = Paths::getLogDirAbs();
|
||||
list($numDeleted, $numFailed) = self::purgeLogFilesInDir($dir);
|
||||
FileHelper::removeEmptySubFolders($dir);
|
||||
|
||||
return [
|
||||
'delete-count' => $numDeleted,
|
||||
'fail-count' => $numFailed
|
||||
];
|
||||
|
||||
//$successInRemovingCacheDir = FileHelper::rrmdir(Paths::getCacheDirAbs());
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Purge log files in a dir
|
||||
*
|
||||
* @return [num files deleted, num files failed to delete]
|
||||
*/
|
||||
private static function purgeLogFilesInDir($dir)
|
||||
{
|
||||
if (!@file_exists($dir) || !@is_dir($dir)) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
$numFilesDeleted = 0;
|
||||
$numFilesFailedDeleting = 0;
|
||||
|
||||
$fileIterator = new \FilesystemIterator($dir);
|
||||
while ($fileIterator->valid()) {
|
||||
$filename = $fileIterator->getFilename();
|
||||
|
||||
if (($filename != ".") && ($filename != "..")) {
|
||||
|
||||
if (@is_dir($dir . "/" . $filename)) {
|
||||
list($r1, $r2) = self::purgeLogFilesInDir($dir . "/" . $filename);
|
||||
$numFilesDeleted += $r1;
|
||||
$numFilesFailedDeleting += $r2;
|
||||
} else {
|
||||
|
||||
// its a file
|
||||
// Run through filters, which each may set "skipThis" to true
|
||||
|
||||
$skipThis = false;
|
||||
|
||||
// filter: It must have ".md" extension
|
||||
if (!$skipThis && !preg_match('#\.md$#', $filename)) {
|
||||
$skipThis = true;
|
||||
}
|
||||
|
||||
if (!$skipThis) {
|
||||
if (@unlink($dir . "/" . $filename)) {
|
||||
$numFilesDeleted++;
|
||||
} else {
|
||||
$numFilesFailedDeleting++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileIterator->next();
|
||||
}
|
||||
return [$numFilesDeleted, $numFilesFailedDeleting];
|
||||
}
|
||||
|
||||
public static function processAjaxPurgeLog()
|
||||
{
|
||||
|
||||
if (!check_ajax_referer('webpexpress-ajax-purge-log-nonce', 'nonce', false)) {
|
||||
wp_send_json_error('The security nonce has expired. You need to reload the settings page (press F5) and try again)');
|
||||
wp_die();
|
||||
}
|
||||
$result = self::purge($config);
|
||||
echo json_encode($result, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
|
||||
wp_die();
|
||||
}
|
||||
}
|
||||
96
lib/classes/Messenger.php
Normal file
96
lib/classes/Messenger.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Option;
|
||||
use \WebPExpress\State;
|
||||
|
||||
class Messenger
|
||||
{
|
||||
private static $printedStyles = false;
|
||||
|
||||
/**
|
||||
* @param string $level (info | success | warning | error)
|
||||
* @param string $msg the message (not translated)
|
||||
*
|
||||
* Hm... we should add some sprintf-like support
|
||||
* $msg = sprintf(__( 'You are on a very old version of PHP (%s). WebP Express may not work as intended.', 'webp-express' ), phpversion());
|
||||
*/
|
||||
public static function addMessage($level, $msg) {
|
||||
//error_log('add message:' . $msg);
|
||||
|
||||
Option::updateOption('webp-express-messages-pending', true, true); // We want this option to be autoloaded
|
||||
$pendingMessages = State::getState('pendingMessages', []);
|
||||
|
||||
// Ensure we do not add a message that is already pending.
|
||||
foreach ($pendingMessages as $i => $entry) {
|
||||
if ($entry['message'] == $msg) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$pendingMessages[] = ['level' => $level, 'message' => $msg];
|
||||
State::setState('pendingMessages', $pendingMessages);
|
||||
}
|
||||
|
||||
public static function printMessage($level, $msg) {
|
||||
if (!(self::$printedStyles)) {
|
||||
global $wp_version;
|
||||
if (floatval(substr($wp_version, 0, 3)) < 4.1) {
|
||||
// Actually, I don't know precisely what version the styles were introduced.
|
||||
// They are there in 4.1. They are not there in 4.0
|
||||
self::printMessageStylesForOldWordpress();
|
||||
}
|
||||
self::$printedStyles = true;
|
||||
}
|
||||
|
||||
//$msg = __( $msg, 'webp-express'); // uncommented. We should add some sprintf-like functionality before making the plugin translatable
|
||||
printf(
|
||||
'<div class="%1$s"><div style="margin:10px 0">%2$s</div></div>',
|
||||
//esc_attr('notice notice-' . $level . ' is-dismissible'),
|
||||
esc_attr('notice notice-' . $level),
|
||||
$msg
|
||||
);
|
||||
}
|
||||
|
||||
private static function printMessageStylesForOldWordpress() {
|
||||
?>
|
||||
<style>
|
||||
/* In Older Wordpress (ie 4.0), .notice is not declared */
|
||||
.notice {
|
||||
background: #fff;
|
||||
border-left: 4px solid #fff;
|
||||
-webkit-box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
|
||||
box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
|
||||
margin: 10px 15px 2px 2px;
|
||||
padding: 1px 12px;
|
||||
}
|
||||
.notice-error {
|
||||
border-left-color: #dc3232;
|
||||
}
|
||||
.notice-success {esc_attr('notice notice-' . $level . ' is-dismissible'),
|
||||
border-left-color: #46b450;
|
||||
}
|
||||
.notice-info {
|
||||
border-left-color: #00a0d2;
|
||||
}
|
||||
.notice-warning {
|
||||
border-left-color: #ffb900;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
|
||||
public static function printPendingMessages() {
|
||||
|
||||
$messages = State::getState('pendingMessages', []);
|
||||
|
||||
foreach ($messages as $message) {
|
||||
self::printMessage($message['level'], $message['message']);
|
||||
}
|
||||
|
||||
State::setState('pendingMessages', []);
|
||||
|
||||
Option::updateOption('webp-express-messages-pending', false, true);
|
||||
}
|
||||
|
||||
}
|
||||
55
lib/classes/Mime.php
Normal file
55
lib/classes/Mime.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Config;
|
||||
use \WebPExpress\Convert;
|
||||
|
||||
class Mime
|
||||
{
|
||||
|
||||
public static function getMimeTypeOfMedia($filename)
|
||||
{
|
||||
// ensure filename is not empty, as wp_get_image_mime() goes fatal if it is
|
||||
if ($filename === '') {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// First try the Wordpress function if available (it was introduced in 4.7.1)
|
||||
if (function_exists('wp_get_image_mime')) {
|
||||
|
||||
// PS: wp_get_image_mime tries exif_imagetype and getimagesize and returns false if no methods are available
|
||||
$mimeType = wp_get_image_mime($filename);
|
||||
if ($mimeType !== false) {
|
||||
return $mimeType;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Try mime_content_type
|
||||
if (function_exists('mime_content_type')) {
|
||||
$mimeType = mime_content_type($filename);
|
||||
if ($mimeType !== false) {
|
||||
return $mimeType;
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('wp_check_filetype')) { // introduced in 2.0.4
|
||||
// Try wordpress method, which simply uses the file extension and a map
|
||||
$mimeType = wp_check_filetype($filename)['type'];
|
||||
if ($mimeType !== false) {
|
||||
return $mimeType;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't say we didn't try!
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
public static function isOneOfTheseImageMimeTypes($filename, $imageMimeTypes)
|
||||
{
|
||||
$detectedMimeType = self::getMimeTypeOfMedia($filename);
|
||||
return in_array($detectedMimeType, $imageMimeTypes);
|
||||
}
|
||||
|
||||
}
|
||||
36
lib/classes/Multisite.php
Normal file
36
lib/classes/Multisite.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class Multisite
|
||||
{
|
||||
public static $networkActive;
|
||||
|
||||
/*
|
||||
Needed because is_plugin_active_for_network() does not return true right after network activation
|
||||
*/
|
||||
public static function overrideIsNetworkActivated($networkActive)
|
||||
{
|
||||
self::$networkActive = $networkActive;
|
||||
}
|
||||
|
||||
public static function isNetworkActivated()
|
||||
{
|
||||
if (!is_null(self::$networkActive)) {
|
||||
return self::$networkActive;
|
||||
}
|
||||
if (!self::isMultisite()) {
|
||||
return false;
|
||||
}
|
||||
if (!function_exists( 'is_plugin_active_for_network')) {
|
||||
require_once(ABSPATH . '/wp-admin/includes/plugin.php');
|
||||
}
|
||||
return is_plugin_active_for_network('webp-express/webp-express.php');
|
||||
}
|
||||
|
||||
public static function isMultisite()
|
||||
{
|
||||
return is_multisite();
|
||||
}
|
||||
|
||||
}
|
||||
39
lib/classes/Option.php
Normal file
39
lib/classes/Option.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Multisite;
|
||||
|
||||
class Option
|
||||
{
|
||||
|
||||
public static function getOption($optionName, $default = false)
|
||||
{
|
||||
if (Multisite::isNetworkActivated()) {
|
||||
return get_site_option($optionName, $default);
|
||||
} else {
|
||||
return get_option($optionName, $default);
|
||||
}
|
||||
}
|
||||
|
||||
public static function updateOption($optionName, $value, $autoload = null)
|
||||
{
|
||||
if (Multisite::isNetworkActivated()) {
|
||||
//error_log('update option (network):' . $optionName . ':' . $value);
|
||||
return update_site_option($optionName, $value);
|
||||
} else {
|
||||
//error_log('update option:' . $optionName . ':' . $value);
|
||||
return update_option($optionName, $value, $autoload);
|
||||
}
|
||||
}
|
||||
|
||||
public static function deleteOption($optionName)
|
||||
{
|
||||
if (Multisite::isNetworkActivated()) {
|
||||
return delete_site_option($optionName);
|
||||
} else {
|
||||
return delete_option($optionName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
20
lib/classes/OptionsPage.php
Normal file
20
lib/classes/OptionsPage.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class OptionsPage
|
||||
{
|
||||
|
||||
// callback (registred in AdminUi)
|
||||
public static function display() {
|
||||
include WEBPEXPRESS_PLUGIN_DIR . '/lib/options/page.php';
|
||||
}
|
||||
|
||||
public static function enqueueScripts() {
|
||||
include WEBPEXPRESS_PLUGIN_DIR . '/lib/options/enqueue_scripts.php';
|
||||
}
|
||||
}
|
||||
16
lib/classes/OptionsPageHooks.php
Normal file
16
lib/classes/OptionsPageHooks.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class OptionsPageHooks
|
||||
{
|
||||
|
||||
// callback for 'admin_post_webpexpress_settings_submit' (registred in AdminInit::addHooks)
|
||||
public static function submitHandler() {
|
||||
include WEBPEXPRESS_PLUGIN_DIR . '/lib/options/submit.php';
|
||||
}
|
||||
}
|
||||
481
lib/classes/PathHelper.php
Normal file
481
lib/classes/PathHelper.php
Normal file
@@ -0,0 +1,481 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class PathHelper
|
||||
{
|
||||
|
||||
public static function isDocRootAvailable() {
|
||||
|
||||
// BTW:
|
||||
// Note that DOCUMENT_ROOT does not end with trailing slash on old litespeed servers:
|
||||
// https://www.litespeedtech.com/support/forum/threads/document_root-trailing-slash.5304/
|
||||
|
||||
if (!isset($_SERVER['DOCUMENT_ROOT'])) {
|
||||
return false;
|
||||
}
|
||||
if ($_SERVER['DOCUMENT_ROOT'] == '') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a path exists as is resolvable (will be unless it is outside open_basedir)
|
||||
*
|
||||
* @param string $absPath The path to test (must be absolute. symlinks allowed)
|
||||
* @return boolean The result
|
||||
*/
|
||||
public static function pathExistsAndIsResolvable($absPath) {
|
||||
if (!@realpath($absPath)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if document root is available, exists and symlinks are resolvable (resolved path is within open basedir)
|
||||
*
|
||||
* @return boolean The result
|
||||
*/
|
||||
public static function isDocRootAvailableAndResolvable() {
|
||||
return (
|
||||
self::isDocRootAvailable() &&
|
||||
self::pathExistsAndIsResolvable($_SERVER['DOCUMENT_ROOT'])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the rewrite rules are using the absolute dir, the rewrite rules does not work if that dir
|
||||
* is outside document root. This poses a problem if some part of the document root has been symlinked.
|
||||
*
|
||||
* This method "unresolves" the document root part of a dir.
|
||||
* That is: It takes an absolute url, looks to see if it begins with the resolved document root.
|
||||
* In case it does, it replaces the resolved document root with the unresolved document root.
|
||||
*
|
||||
* Unfortunately we can only unresolve when document root is available and resolvable.
|
||||
* - which is sad, because the image-roots was introduced in order to get it to work on setups
|
||||
*/
|
||||
public static function fixAbsPathToUseUnresolvedDocRoot($absPath) {
|
||||
if (self::isDocRootAvailableAndResolvable()) {
|
||||
if (strpos($absPath, realpath($_SERVER['DOCUMENT_ROOT'])) === 0) {
|
||||
return $_SERVER['DOCUMENT_ROOT'] . substr($absPath, strlen(realpath($_SERVER['DOCUMENT_ROOT'])));
|
||||
}
|
||||
}
|
||||
return $absPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out if path is below - or equal to a path.
|
||||
*
|
||||
* "/var/www" below/equal to "/var"? : Yes
|
||||
* "/var/www" below/equal to "/var/www"? : Yes
|
||||
* "/var/www2" below/equal to "/var/www"? : No
|
||||
*/
|
||||
/*
|
||||
public static function isPathBelowOrEqualToPath($path1, $path2)
|
||||
{
|
||||
return (strpos($path1 . '/', $path2 . '/') === 0);
|
||||
//$rel = self::getRelDir($path2, $path1);
|
||||
//return (substr($rel, 0, 3) != '../');
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Calculate relative path from document root to a given absolute path (must exist and be resolvable) - if possible AND
|
||||
* if it can be done without directory traversal.
|
||||
*
|
||||
* The function is designed with the usual folders in mind (index, uploads, wp-content, plugins), which all presumably
|
||||
* exists and are within open_basedir.
|
||||
*
|
||||
* @param string $dir An absolute path (may contain symlinks). The path must exist and be resolvable.
|
||||
* @throws \Exception If it is not possible to get such path (ie if doc-root is unavailable or the dir is outside doc-root)
|
||||
* @return string Relative path to document root or empty string if document root is unavailable
|
||||
*/
|
||||
public static function getRelPathFromDocRootToDirNoDirectoryTraversalAllowed($dir)
|
||||
{
|
||||
if (!self::isDocRootAvailable()) {
|
||||
throw new \Exception('Cannot calculate relative path from document root to dir, as document root is not available');
|
||||
}
|
||||
|
||||
// First try unresolved.
|
||||
// This will even work when ie wp-content is symlinked to somewhere outside document root, while the symlink itself is within document root)
|
||||
$relPath = self::getRelDir($_SERVER['DOCUMENT_ROOT'], $dir);
|
||||
if (strpos($relPath, '../') !== 0) { // Check if relPath starts with "../" (if it does, we cannot use it)
|
||||
return $relPath;
|
||||
}
|
||||
|
||||
if (self::isDocRootAvailableAndResolvable()) {
|
||||
if (self::pathExistsAndIsResolvable($dir)) {
|
||||
// Try with both resolved
|
||||
$relPath = self::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), realpath($dir));
|
||||
if (strpos($relPath, '../') !== 0) {
|
||||
return $relPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Try with just document root resolved
|
||||
$relPath = self::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), $dir);
|
||||
if (strpos($relPath, '../') !== 0) {
|
||||
return $relPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (self::pathExistsAndIsResolvable($dir)) {
|
||||
// Try with dir resolved
|
||||
$relPath = self::getRelDir($_SERVER['DOCUMENT_ROOT'], realpath($dir));
|
||||
if (strpos($relPath, '../') !== 0) {
|
||||
return $relPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Problem:
|
||||
// - dir is already resolved (ie: /disk/the-content)
|
||||
// - document root is ie. /var/www/website/wordpress
|
||||
// - the unresolved symlink is ie. /var/www/website/wordpress/wp-content
|
||||
// - we do not know what the unresolved symlink is
|
||||
// The result should be "wp-content". But how do we get to that result?
|
||||
// I guess we must check out all folders below document root to see if anyone resolves to dir
|
||||
// we could start out trying usual suspects such as "wp-content" and "wp-content/uploads"
|
||||
//foreach (glob($dir . DIRECTORY_SEPARATOR . $filePattern) as $filename)
|
||||
/*
|
||||
$iter = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($_SERVER['DOCUMENT_ROOT'], \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::SELF_FIRST,
|
||||
\RecursiveIteratorIterator::CATCH_GET_CHILD // Ignore "Permission denied"
|
||||
);
|
||||
|
||||
foreach ($iter as $path => $dirObj) {
|
||||
if ($dirObj->isDir()) {
|
||||
if (realpath($path) == $dir) {
|
||||
//return $path;
|
||||
$relPath = self::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), $path);
|
||||
if (strpos($relPath, '../') !== 0) {
|
||||
return $relPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
// Ok, the above works - but when subfolders to the symlink is referenced. Ie referencing uploads when wp-content is symlinked
|
||||
// - dir is already resolved (ie: /disk/the-content/uploads)
|
||||
// - document root is ie. /var/www/website/wordpress
|
||||
// - the unresolved symlink is ie. /var/www/website/wordpress/wp-content/uploads
|
||||
// - we do not know what the unresolved symlink is
|
||||
// The result should be "wp-content/uploads". But how do we get to that result?
|
||||
|
||||
// What if we collect all symlinks below document root in a assoc array?
|
||||
// ['/disk/the-content' => 'wp-content']
|
||||
// Input is: '/disk/the-content/uploads'
|
||||
// 1. We check the symlinks and substitute. We get: 'wp-content/uploads'.
|
||||
// 2. We test if realpath($_SERVER['DOCUMENT_ROOT'] . '/' . 'wp-content/uploads') equals input.
|
||||
// It seems I have a solution!
|
||||
// - I shall continue work soon! - for a 0.15.1 release (test instance #26)
|
||||
// PS: cache the result of the symlinks in docroot collector.
|
||||
|
||||
throw new \Exception(
|
||||
'Cannot get relative path from document root to dir without resolving to directory traversal. ' .
|
||||
'It seems the dir is not below document root'
|
||||
);
|
||||
|
||||
/*
|
||||
if (!self::pathExistsAndIsResolvable($dir)) {
|
||||
throw new \Exception('Cannot calculate relative path from document root to dir. The path given is not resolvable (realpath fails)');
|
||||
}
|
||||
|
||||
|
||||
// Check if relPath starts with "../"
|
||||
if (strpos($relPath, '../') === 0) {
|
||||
|
||||
// Unresolved failed. Try with document root resolved
|
||||
$relPath = self::getRelDir(realpath($_SERVER['DOCUMENT_ROOT']), $dir);
|
||||
|
||||
if (strpos($relPath, '../') === 0) {
|
||||
|
||||
// Try with both resolved
|
||||
$relPath = self::getRelDir($dir, $dir);
|
||||
throw new \Exception('Cannot calculate relative path from document root to dir. The path given is not within document root');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return $relPath;
|
||||
} else {
|
||||
// We cannot get the resolved doc-root.
|
||||
// This might be ok as long as the (resolved) path we are examining begins with the configured doc-root.
|
||||
$relPath = self::getRelDir($_SERVER['DOCUMENT_ROOT'], $dir);
|
||||
|
||||
// Check if relPath starts with "../" (it may not)
|
||||
if (strpos($relPath, '../') === 0) {
|
||||
|
||||
// Well, that did not work. We can try the resolved path instead.
|
||||
if (!self::pathExistsAndIsResolvable($dir)) {
|
||||
throw new \Exception('Cannot calculate relative path from document root to dir. The path given is not resolvable (realpath fails)');
|
||||
}
|
||||
|
||||
$relPath = self::getRelDir($_SERVER['DOCUMENT_ROOT'], realpath($dir));
|
||||
if (strpos($relPath, '../') === 0) {
|
||||
|
||||
// That failed too.
|
||||
// Either it is in fact outside document root or it is because of a special setup.
|
||||
throw new \Exception(
|
||||
'Cannot calculate relative path from document root to dir. Either the path given is not within the configured document root or ' .
|
||||
'it is because of a special setup. The document root is outside open_basedir. If it is also symlinked, but the other Wordpress paths ' .
|
||||
'are not using that same symlink, it will not be possible to calculate the relative path.'
|
||||
);
|
||||
}
|
||||
}
|
||||
return $relPath;
|
||||
}*/
|
||||
}
|
||||
|
||||
public static function canCalculateRelPathFromDocRootToDir($dir)
|
||||
{
|
||||
try {
|
||||
$relPath = self::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed($dir);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find closest existing folder with symlinks expandend, using realpath.
|
||||
*
|
||||
* Note that if the input or the closest existing folder is outside open_basedir, no folder will
|
||||
* be found and an empty string will be returned.
|
||||
*
|
||||
* @return string closest existing path or empty string if none found (due to open_basedir restriction)
|
||||
*/
|
||||
public static function findClosestExistingFolderSymLinksExpanded($input) {
|
||||
|
||||
// The strategy is to first try the supplied directory. If it fails, try the parent, etc.
|
||||
$dir = $input;
|
||||
|
||||
// We count the levels up to avoid infinite loop - as good practice. It ought not to get that far
|
||||
$levelsUp = 0;
|
||||
|
||||
while ($levelsUp < 100) {
|
||||
// We suppress warning because we are aware that we might get a
|
||||
// open_basedir restriction warning.
|
||||
$realPathResult = @realpath($dir);
|
||||
if ($realPathResult !== false) {
|
||||
return $realPathResult;
|
||||
}
|
||||
// Stop at root. This will happen if the original path is outside basedir.
|
||||
if (($dir == '/') || (strlen($dir) < 4)) {
|
||||
return '';
|
||||
}
|
||||
// Peal off one directory
|
||||
$dir = @dirname($dir);
|
||||
$levelsUp++;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Look if filepath is within a dir path (both by string matching and by using realpath, see notes).
|
||||
*
|
||||
* Note that the naive string match does not resolve '..'. You might want to call ::canonicalize first.
|
||||
* Note that the realpath match requires: 1. that the dir exist and is within open_basedir
|
||||
* 2. that the closest existing folder within filepath is within open_basedir
|
||||
*
|
||||
* @param string $filePath Path to file. It may be non-existing.
|
||||
* @param string $dirPath Path to dir. It must exist and be within open_basedir in order for the realpath match to execute.
|
||||
*/
|
||||
public static function isFilePathWithinDirPath($filePath, $dirPath)
|
||||
{
|
||||
// See if $filePath begins with $dirPath + '/'.
|
||||
if (strpos($filePath, $dirPath . '/') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strpos(self::canonicalize($filePath), self::canonicalize($dirPath) . '/') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Also try with symlinks expanded.
|
||||
// As symlinks can only be retrieved with realpath and realpath fails with non-existing paths,
|
||||
// we settle with checking if closest existing folder in the filepath is within the dir.
|
||||
// If that is the case, then surely, the complete filepath is also within the dir.
|
||||
// Note however that it might be that the closest existing folder is not within the dir, while the
|
||||
// file would be (if it existed)
|
||||
// For WebP Express, we are pretty sure that the dirs we are checking against (uploads folder,
|
||||
// wp-content, plugins folder) exists. So getting the closest existing folder should be sufficient.
|
||||
// but could it be that these are outside open_basedir on some setups? Perhaps on a few systems.
|
||||
if (self::pathExistsAndIsResolvable($dirPath)) {
|
||||
$closestExistingDirOfFile = PathHelper::findClosestExistingFolderSymLinksExpanded($filePath);
|
||||
if (strpos($closestExistingDirOfFile, realpath($dirPath) . '/') === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look if path is within a dir path. Also tries expanding symlinks
|
||||
*
|
||||
* @param string $path Path to examine. It may be non-existing.
|
||||
* @param string $dirPath Path to dir. It must exist in order for symlinks to be expanded.
|
||||
*/
|
||||
public static function isPathWithinExistingDirPath($path, $dirPath)
|
||||
{
|
||||
if ($path == $dirPath) {
|
||||
return true;
|
||||
}
|
||||
// See if $filePath begins with $dirPath + '/'.
|
||||
if (strpos($path, $dirPath . '/') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also try with symlinks expanded (see comments in ::isFilePathWithinDirPath())
|
||||
$closestExistingDir = PathHelper::findClosestExistingFolderSymLinksExpanded($path);
|
||||
if (strpos($closestExistingDir . '/', $dirPath . '/') === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function frontslasher($str)
|
||||
{
|
||||
// TODO: replace backslash with frontslash
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace double slash with single slash. ie '/var//www/' => '/var/www/'
|
||||
* This allows you to lazely concatenate paths with '/' and then call this method to clean up afterwards.
|
||||
* Also removes triple slash etc.
|
||||
*/
|
||||
public static function fixDoubleSlash($str)
|
||||
{
|
||||
return preg_replace('/\/\/+/', '/', $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove trailing slash, if any
|
||||
*/
|
||||
public static function untrailSlash($str)
|
||||
{
|
||||
return rtrim($str, '/');
|
||||
//return preg_replace('/\/$/', '', $str);
|
||||
}
|
||||
|
||||
public static function backslashesToForwardSlashes($path) {
|
||||
return str_replace( "\\", '/', $path);
|
||||
}
|
||||
|
||||
// Canonicalize a path by resolving '../' and './'. It also replaces backslashes with forward slash
|
||||
// Got it from a comment here: http://php.net/manual/en/function.realpath.php
|
||||
// But fixed it (it could not handle './../')
|
||||
public static function canonicalize($path) {
|
||||
|
||||
$parts = explode('/', $path);
|
||||
|
||||
// Remove parts containing just '.' (and the empty holes afterwards)
|
||||
$parts = array_values(array_filter($parts, function($var) {
|
||||
return ($var != '.');
|
||||
}));
|
||||
|
||||
// Remove parts containing '..' and the preceding
|
||||
$keys = array_keys($parts, '..');
|
||||
foreach($keys as $keypos => $key) {
|
||||
array_splice($parts, $key - ($keypos * 2 + 1), 2);
|
||||
}
|
||||
return implode('/', $parts);
|
||||
}
|
||||
|
||||
public static function dirname($path) {
|
||||
return self::canonicalize($path . '/..');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base name of a path (the last component of a path - ie the filename).
|
||||
*
|
||||
* This function operates natively on the string and is not locale aware.
|
||||
* It only works with "/" path separators.
|
||||
*
|
||||
* @return string the last component of a path
|
||||
*/
|
||||
public static function basename($path) {
|
||||
$parts = explode('/', $path);
|
||||
return array_pop($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns absolute path from a relative path and root
|
||||
* The result is canonicalized (dots and double-dots are resolved)
|
||||
*
|
||||
* @param $path Absolute path or relative path
|
||||
* @param $root What the path is relative to, if its relative
|
||||
*/
|
||||
public static function relPathToAbsPath($path, $root)
|
||||
{
|
||||
return self::canonicalize(self::fixDoubleSlash($root . '/' . $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* isAbsPath
|
||||
* If path starts with '/', it is considered an absolute path (no Windows support)
|
||||
*
|
||||
* @param $path Path to inspect
|
||||
*/
|
||||
public static function isAbsPath($path)
|
||||
{
|
||||
return (substr($path, 0, 1) == '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns absolute path from a path which can either be absolute or relative to second argument.
|
||||
* If path starts with '/', it is considered an absolute path.
|
||||
* The result is canonicalized (dots and double-dots are resolved)
|
||||
*
|
||||
* @param $path Absolute path or relative path
|
||||
* @param $root What the path is relative to, if its relative
|
||||
*/
|
||||
public static function pathToAbsPath($path, $root)
|
||||
{
|
||||
if (self::isAbsPath($path)) {
|
||||
// path is already absolute
|
||||
return $path;
|
||||
} else {
|
||||
return self::relPathToAbsPath($path, $root);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative path between two absolute paths
|
||||
* Examples:
|
||||
* from '/var/www' to 'var/ddd'. Result: '../ddd'
|
||||
* from '/var/www' to 'var/www/images'. Result: 'images'
|
||||
* from '/var/www' to 'var/www'. Result: '.'
|
||||
*/
|
||||
public static function getRelDir($fromPath, $toPath)
|
||||
{
|
||||
$fromDirParts = explode('/', str_replace('\\', '/', self::canonicalize(self::untrailSlash($fromPath))));
|
||||
$toDirParts = explode('/', str_replace('\\', '/', self::canonicalize(self::untrailSlash($toPath))));
|
||||
$i = 0;
|
||||
while (($i < count($fromDirParts)) && ($i < count($toDirParts)) && ($fromDirParts[$i] == $toDirParts[$i])) {
|
||||
$i++;
|
||||
}
|
||||
$rel = "";
|
||||
for ($j = $i; $j < count($fromDirParts); $j++) {
|
||||
$rel .= "../";
|
||||
}
|
||||
|
||||
for ($j = $i; $j < count($toDirParts); $j++) {
|
||||
$rel .= $toDirParts[$j];
|
||||
if ($j < count($toDirParts)-1) {
|
||||
$rel .= '/';
|
||||
}
|
||||
}
|
||||
if ($rel == '') {
|
||||
$rel = '.';
|
||||
}
|
||||
return $rel;
|
||||
}
|
||||
|
||||
}
|
||||
879
lib/classes/Paths.php
Normal file
879
lib/classes/Paths.php
Normal file
@@ -0,0 +1,879 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\FileHelper;
|
||||
use \WebPExpress\Multisite;
|
||||
use \WebPExpress\PathHelper;
|
||||
|
||||
class Paths
|
||||
{
|
||||
public static function areAllImageRootsWithinDocRoot() {
|
||||
if (!PathHelper::isDocRootAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$roots = self::getImageRootIds();
|
||||
foreach ($roots as $dirId) {
|
||||
$dir = self::getAbsDirById($dirId);
|
||||
if (!PathHelper::canCalculateRelPathFromDocRootToDir($dir)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can use document root for calculating relative paths (which may not contain "/.." directory traversal)
|
||||
*
|
||||
* Note that this method allows document root to be outside open_basedir as long as document root is
|
||||
* non-empty AND it is possible to calculate relative paths to all image roots (including "index").
|
||||
* Here is a case when a relative CAN be calculated:
|
||||
* - Document root is configured to "/var/www/website" - which is also the absolute file path.
|
||||
* - open_basedir is set to "/var/www/website/wordpress"
|
||||
* - uploads is in "/var/www/website/wordpress/wp-content/uploads" (within open_basedir, as it should)
|
||||
* - "/wp-uploads" symlinks to "/var/www/website/wordpress")
|
||||
* - Wordpress has been configured to use "/wp-uploads" path for uploads.
|
||||
*
|
||||
* What happens?
|
||||
* First, it is tested if the configured upload path ("/wp-uploads") begins with the configured document root ("/var/www/website").
|
||||
* This fails.
|
||||
* Next, it is tested if the uploads path can be resolved. It can, as it is within the open_basedir.
|
||||
* Next, it is tested if the *resolved* the uploads path begins with the configured document root.
|
||||
* As "/var/www/website/wordpress/wp-content/uploads" begins with "/var/www/website", we have a match.
|
||||
* The relative path can be calculated to be "wordpress/wp-content/uploads".
|
||||
* Later, when the relative path is used, it will be used as $docRoot + "/" + $relPath, which
|
||||
* will be "/var/www/website/wordpress/wp-content/uploads". All is well.
|
||||
*
|
||||
* Here is a case where it CAN NOT be calculated:
|
||||
* - Document root is configured to "/the-website", which symlinks to "/var/www/website"
|
||||
* - open_basedir is set to "/var/www/website/wordpress"
|
||||
* - uploads is in "/var/www/website/wordpress/wp-content/uploads" and wordpress is configured to use that upload path.
|
||||
*
|
||||
* What happens?
|
||||
* First, it is tested if the configured upload path begins with the configured document root
|
||||
* "/var/www/website/wordpress/wp-content/uploads" does not begin with "/the-website", so it fails.
|
||||
* Next, it is tested if the *resolved* the uploads path begins with the configured document root.
|
||||
* The resolved uploads path is the same as the configured so it also fails.
|
||||
* Next, it is tested if Document root can be resolved. It can not, as the resolved path is not within open_basedir.
|
||||
* If it could, it would have been tested if the resolved path begins with the resolved document root and we would have
|
||||
* gotten a yes, and the relative path would have been "wordpress/wp-content/uploads" and it would work.
|
||||
* However: Document root could not be resolved and we could not get a result.
|
||||
* To sum the scenario up:
|
||||
* If document root is configured to a symlink which cannot be resolved then it will only be possible to get relative paths
|
||||
* when all other configured paths begins are relative to that symlink.
|
||||
*/
|
||||
public static function canUseDocRootForRelPaths() {
|
||||
if (!PathHelper::isDocRootAvailable()) {
|
||||
return false;
|
||||
}
|
||||
return self::areAllImageRootsWithinDocRoot();
|
||||
}
|
||||
|
||||
public static function canCalculateRelPathFromDocRootToDir($absPath) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can use document root for structuring the cache dir.
|
||||
*
|
||||
* In order to structure the images by doc-root, WebP Express needs all images to be within document root.
|
||||
* Does WebP Express in addition to this need to be able to resolve document root?
|
||||
* Short answer is yes.
|
||||
* The long answer is available as a comment inside ConvertHelperIndependent::getDestination()
|
||||
*
|
||||
*/
|
||||
public static function canUseDocRootForStructuringCacheDir() {
|
||||
return (PathHelper::isDocRootAvailableAndResolvable() && self::canUseDocRootForRelPaths());
|
||||
}
|
||||
|
||||
public static function docRootStatusText()
|
||||
{
|
||||
if (!PathHelper::isDocRootAvailable()) {
|
||||
if (!isset($_SERVER['DOCUMENT_ROOT'])) {
|
||||
return 'Unavailable (DOCUMENT_ROOT is not set in the global $_SERVER var)';
|
||||
}
|
||||
if ($_SERVER['DOCUMENT_ROOT'] == '') {
|
||||
return 'Unavailable (empty string)';
|
||||
}
|
||||
return 'Unavailable';
|
||||
}
|
||||
|
||||
$imageRootsWithin = self::canUseDocRootForRelPaths();
|
||||
if (!PathHelper::isDocRootAvailableAndResolvable()) {
|
||||
$status = 'Available, but either non-existing or not within open_basedir.' .
|
||||
($imageRootsWithin ? '' : ' And not all image roots are within that document root.');
|
||||
} elseif (!$imageRootsWithin) {
|
||||
$status = 'Available, but not all image roots are within that document root.';
|
||||
} else {
|
||||
$status = 'Available and its "realpath" is available too.';
|
||||
}
|
||||
if (self::canUseDocRootForStructuringCacheDir()) {
|
||||
$status .= ' Can be used for structuring cache dir.';
|
||||
} else {
|
||||
$status .= ' Cannot be used for structuring cache dir.';
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
public static function getAbsDirId($absDir) {
|
||||
switch ($absDir) {
|
||||
case self::getContentDirAbs():
|
||||
return 'wp-content';
|
||||
case self::getIndexDirAbs():
|
||||
return 'index';
|
||||
case self::getHomeDirAbs():
|
||||
return 'home';
|
||||
case self::getPluginDirAbs():
|
||||
return 'plugins';
|
||||
case self::getUploadDirAbs():
|
||||
return 'uploads';
|
||||
case self::getThemesDirAbs():
|
||||
return 'themes';
|
||||
case self::getCacheDirAbs():
|
||||
return 'cache';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getAbsDirById($dirId) {
|
||||
switch ($dirId) {
|
||||
case 'wp-content':
|
||||
return self::getContentDirAbs();
|
||||
case 'index':
|
||||
return self::getIndexDirAbs();
|
||||
case 'home':
|
||||
// "home" is still needed (used in PluginDeactivate.php)
|
||||
return self::getHomeDirAbs();
|
||||
case 'plugins':
|
||||
return self::getPluginDirAbs();
|
||||
case 'uploads':
|
||||
return self::getUploadDirAbs();
|
||||
case 'themes':
|
||||
return self::getThemesDirAbs();
|
||||
case 'cache':
|
||||
return self::getCacheDirAbs();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ids for folders where SOURCE images may reside
|
||||
*/
|
||||
public static function getImageRootIds() {
|
||||
return ['uploads', 'themes', 'plugins', 'wp-content', 'index'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find which rootId a path belongs to.
|
||||
*
|
||||
* Note: If the root ids passed are ordered the way getImageRootIds() returns them, the root id
|
||||
* returned will be the "deepest"
|
||||
*/
|
||||
public static function findImageRootOfPath($path, $rootIdsToSearch) {
|
||||
foreach ($rootIdsToSearch as $rootId) {
|
||||
if (PathHelper::isPathWithinExistingDirPath($path, self::getAbsDirById($rootId))) {
|
||||
return $rootId;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getImageRootsDefForSelectedIds($ids) {
|
||||
$canUseDocRootForRelPaths = self::canUseDocRootForRelPaths();
|
||||
|
||||
$mapping = [];
|
||||
foreach ($ids as $rootId) {
|
||||
$obj = [
|
||||
'id' => $rootId,
|
||||
];
|
||||
$absPath = self::getAbsDirById($rootId);
|
||||
if ($canUseDocRootForRelPaths) {
|
||||
$obj['rel-path'] = PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed($absPath);
|
||||
} else {
|
||||
$obj['abs-path'] = $absPath;
|
||||
}
|
||||
$obj['url'] = self::getUrlById($rootId);
|
||||
$mapping[] = $obj;
|
||||
}
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
public static function getImageRootsDef()
|
||||
{
|
||||
return self::getImageRootsDefForSelectedIds(self::getImageRootIds());
|
||||
}
|
||||
|
||||
public static function filterOutSubRoots($rootIds)
|
||||
{
|
||||
// Get dirs of enabled roots
|
||||
$dirs = [];
|
||||
foreach ($rootIds as $rootId) {
|
||||
$dirs[] = self::getAbsDirById($rootId);
|
||||
}
|
||||
|
||||
// Filter out dirs which are below other dirs
|
||||
$dirsToSkip = [];
|
||||
foreach ($dirs as $dirToExamine) {
|
||||
foreach ($dirs as $dirToCompareAgainst) {
|
||||
if ($dirToExamine == $dirToCompareAgainst) {
|
||||
continue;
|
||||
}
|
||||
if (self::isDirInsideDir($dirToExamine, $dirToCompareAgainst)) {
|
||||
$dirsToSkip[] = $dirToExamine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$dirs = array_diff($dirs, $dirsToSkip);
|
||||
|
||||
// back to ids
|
||||
$result = [];
|
||||
foreach ($dirs as $dir) {
|
||||
$result[] = self::getAbsDirId($dir);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function createDirIfMissing($dir)
|
||||
{
|
||||
if (!@file_exists($dir)) {
|
||||
// We use the wp_mkdir_p, because it takes care of setting folder
|
||||
// permissions to that of parent, and handles creating deep structures too
|
||||
wp_mkdir_p($dir);
|
||||
}
|
||||
return file_exists($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out if $dir1 is inside - or equal to - $dir2
|
||||
*/
|
||||
public static function isDirInsideDir($dir1, $dir2)
|
||||
{
|
||||
$rel = PathHelper::getRelDir($dir2, $dir1);
|
||||
return (substr($rel, 0, 3) != '../');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return absolute dir.
|
||||
*
|
||||
* - Path is canonicalized (without resolving symlinks)
|
||||
* - trailing dash is removed - we don't use that around here.
|
||||
*
|
||||
* We do not resolve symlinks anymore. Information was lost that way.
|
||||
* And in some cases we needed the unresolved path - for example in the .htaccess.
|
||||
*/
|
||||
public static function getAbsDir($dir)
|
||||
{
|
||||
$dir = PathHelper::canonicalize($dir);
|
||||
return rtrim($dir, '/');
|
||||
/*
|
||||
$result = realpath($dir);
|
||||
if ($result === false) {
|
||||
$dir = PathHelper::canonicalize($dir);
|
||||
} else {
|
||||
$dir = $result;
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
// ------------ Home Dir -------------
|
||||
|
||||
// PS: Home dir is not the same as index dir.
|
||||
// For example, if Wordpress folder has been moved (method 2), the home dir could be below.
|
||||
public static function getHomeDirAbs()
|
||||
{
|
||||
if (!function_exists('get_home_path')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
return self::getAbsDir(get_home_path());
|
||||
}
|
||||
|
||||
// ------------ Index Dir (WP root dir) -------------
|
||||
// (The Wordpress installation dir- where index.php and wp-load.php resides)
|
||||
|
||||
public static function getIndexDirAbs()
|
||||
{
|
||||
// We used to return self::getAbsDir(ABSPATH), which used realpath.
|
||||
// It has been changed now, as it seems we do not need realpath for ABSPATH, as it is defined
|
||||
// (in wp-load.php) as dirname(__FILE__) . "/" and according to this link, __FILE__ returns resolved paths:
|
||||
// https://stackoverflow.com/questions/3221771/how-do-you-get-php-symlinks-and-file-to-work-together-nicely
|
||||
// AND a user reported an open_basedir restriction problem thrown by realpath($_SERVER['DOCUMENT_ROOT']),
|
||||
// due to symlinking and opendir restriction (see #322)
|
||||
|
||||
return rtrim(ABSPATH, '/');
|
||||
|
||||
// TODO: read up on this, regarding realpath:
|
||||
// https://github.com/twigphp/Twig/issues/2707
|
||||
|
||||
}
|
||||
|
||||
// ------------ .htaccess dir -------------
|
||||
// (directory containing the relevant .htaccess)
|
||||
// (see https://github.com/rosell-dk/webp-express/issues/36)
|
||||
|
||||
|
||||
|
||||
public static function canWriteHTAccessRulesHere($dirName) {
|
||||
return FileHelper::canEditOrCreateFileHere($dirName . '/.htaccess');
|
||||
}
|
||||
|
||||
public static function canWriteHTAccessRulesInDir($dirId) {
|
||||
return self::canWriteHTAccessRulesHere(self::getAbsDirById($dirId));
|
||||
}
|
||||
|
||||
public static function returnFirstWritableHTAccessDir($dirs)
|
||||
{
|
||||
foreach ($dirs as $dir) {
|
||||
if (self::canWriteHTAccessRulesHere($dir)) {
|
||||
return $dir;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------ Content Dir (the "WP" content dir) -------------
|
||||
|
||||
public static function getContentDirAbs()
|
||||
{
|
||||
return self::getAbsDir(WP_CONTENT_DIR);
|
||||
}
|
||||
public static function getContentDirRel()
|
||||
{
|
||||
return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getContentDirAbs());
|
||||
}
|
||||
public static function getContentDirRelToPluginDir()
|
||||
{
|
||||
return PathHelper::getRelDir(self::getPluginDirAbs(), self::getContentDirAbs());
|
||||
}
|
||||
public static function getContentDirRelToWebPExpressPluginDir()
|
||||
{
|
||||
return PathHelper::getRelDir(self::getWebPExpressPluginDirAbs(), self::getContentDirAbs());
|
||||
}
|
||||
|
||||
|
||||
public static function isWPContentDirMoved()
|
||||
{
|
||||
return (self::getContentDirAbs() != (ABSPATH . 'wp-content'));
|
||||
}
|
||||
|
||||
public static function isWPContentDirMovedOutOfAbsPath()
|
||||
{
|
||||
return !(self::isDirInsideDir(self::getContentDirAbs(), ABSPATH));
|
||||
}
|
||||
|
||||
// ------------ Themes Dir -------------
|
||||
|
||||
public static function getThemesDirAbs()
|
||||
{
|
||||
return self::getContentDirAbs() . '/themes';
|
||||
}
|
||||
|
||||
// ------------ WebPExpress Content Dir -------------
|
||||
// (the "webp-express" directory inside wp-content)
|
||||
|
||||
public static function getWebPExpressContentDirAbs()
|
||||
{
|
||||
return self::getContentDirAbs() . '/webp-express';
|
||||
}
|
||||
|
||||
public static function getWebPExpressContentDirRel()
|
||||
{
|
||||
return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getWebPExpressContentDirAbs());
|
||||
}
|
||||
|
||||
public static function createContentDirIfMissing()
|
||||
{
|
||||
return self::createDirIfMissing(self::getWebPExpressContentDirAbs());
|
||||
}
|
||||
|
||||
// ------------ Upload Dir -------------
|
||||
public static function getUploadDirAbs()
|
||||
{
|
||||
$upload_dir = wp_upload_dir(null, false);
|
||||
return self::getAbsDir($upload_dir['basedir']);
|
||||
}
|
||||
public static function getUploadDirRel()
|
||||
{
|
||||
return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getUploadDirAbs());
|
||||
}
|
||||
|
||||
/*
|
||||
public static function getUploadDirAbs()
|
||||
{
|
||||
if ( defined( 'UPLOADS' ) ) {
|
||||
return ABSPATH . rtrim(UPLOADS, '/');
|
||||
} else {
|
||||
return self::getContentDirAbs() . '/uploads';
|
||||
}
|
||||
}*/
|
||||
|
||||
public static function isUploadDirMovedOutOfWPContentDir()
|
||||
{
|
||||
return !(self::isDirInsideDir(self::getUploadDirAbs(), self::getContentDirAbs()));
|
||||
}
|
||||
|
||||
public static function isUploadDirMovedOutOfAbsPath()
|
||||
{
|
||||
return !(self::isDirInsideDir(self::getUploadDirAbs(), ABSPATH));
|
||||
}
|
||||
|
||||
// ------------ Config Dir -------------
|
||||
|
||||
public static function getConfigDirAbs()
|
||||
{
|
||||
return self::getWebPExpressContentDirAbs() . '/config';
|
||||
}
|
||||
|
||||
public static function getConfigDirRel()
|
||||
{
|
||||
return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getConfigDirAbs());
|
||||
}
|
||||
|
||||
public static function createConfigDirIfMissing()
|
||||
{
|
||||
$configDir = self::getConfigDirAbs();
|
||||
// Using code from Wordfence bootstrap.php...
|
||||
// Why not simply use wp_mkdir_p ? - it sets the permissions to same as parent. Isn't that better?
|
||||
// or perhaps not... - Because we need write permissions in the config dir.
|
||||
if (!is_dir($configDir)) {
|
||||
@mkdir($configDir, 0775);
|
||||
@chmod($configDir, 0775);
|
||||
@file_put_contents(rtrim($configDir . '/') . '/.htaccess', <<<APACHE
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
APACHE
|
||||
);
|
||||
@chmod($configDir . '/.htaccess', 0664);
|
||||
}
|
||||
return is_dir($configDir);
|
||||
}
|
||||
|
||||
public static function getConfigFileName()
|
||||
{
|
||||
return self::getConfigDirAbs() . '/config.json';
|
||||
}
|
||||
|
||||
public static function getWodOptionsFileName()
|
||||
{
|
||||
return self::getConfigDirAbs() . '/wod-options.json';
|
||||
}
|
||||
|
||||
// ------------ Cache Dir -------------
|
||||
|
||||
public static function getCacheDirAbs()
|
||||
{
|
||||
return self::getWebPExpressContentDirAbs() . '/webp-images';
|
||||
}
|
||||
|
||||
public static function getCacheDirRelToDocRoot()
|
||||
{
|
||||
return PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(self::getCacheDirAbs());
|
||||
}
|
||||
|
||||
public static function getCacheDirForImageRoot($destinationFolder, $destinationStructure, $imageRootId)
|
||||
{
|
||||
if (($destinationFolder == 'mingled') && ($imageRootId == 'uploads')) {
|
||||
return self::getUploadDirAbs();
|
||||
}
|
||||
|
||||
if ($destinationStructure == 'doc-root') {
|
||||
$relPath = PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(
|
||||
self::getAbsDirById($imageRootId)
|
||||
);
|
||||
return self::getCacheDirAbs() . '/doc-root/' . $relPath;
|
||||
} else {
|
||||
return self::getCacheDirAbs() . '/' . $imageRootId;
|
||||
}
|
||||
}
|
||||
|
||||
public static function createCacheDirIfMissing()
|
||||
{
|
||||
return self::createDirIfMissing(self::getCacheDirAbs());
|
||||
}
|
||||
|
||||
// ------------ Log Dir -------------
|
||||
|
||||
public static function getLogDirAbs()
|
||||
{
|
||||
return self::getWebPExpressContentDirAbs() . '/log';
|
||||
}
|
||||
|
||||
// ------------ Bigger-than-source dir -------------
|
||||
|
||||
public static function getBiggerThanSourceDirAbs()
|
||||
{
|
||||
return self::getWebPExpressContentDirAbs() . '/webp-images-bigger-than-source';
|
||||
}
|
||||
|
||||
// ------------ Plugin Dir (all plugins) -------------
|
||||
|
||||
public static function getPluginDirAbs()
|
||||
{
|
||||
return self::getAbsDir(WP_PLUGIN_DIR);
|
||||
}
|
||||
|
||||
|
||||
public static function isPluginDirMovedOutOfAbsPath()
|
||||
{
|
||||
return !(self::isDirInsideDir(self::getPluginDirAbs(), ABSPATH));
|
||||
}
|
||||
|
||||
public static function isPluginDirMovedOutOfWpContent()
|
||||
{
|
||||
return !(self::isDirInsideDir(self::getPluginDirAbs(), self::getContentDirAbs()));
|
||||
}
|
||||
|
||||
// ------------ WebP Express Plugin Dir -------------
|
||||
|
||||
public static function getWebPExpressPluginDirAbs()
|
||||
{
|
||||
return self::getAbsDir(WEBPEXPRESS_PLUGIN_DIR);
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// --------- Url paths ----------
|
||||
// ------------------------------------
|
||||
|
||||
/**
|
||||
* Get url path (relative to domain) from absolute url.
|
||||
* Ie: "http://example.com/blog" => "blog"
|
||||
* Btw: By "url path" we shall always mean relative to domain
|
||||
* By "url" we shall always mean complete URL (with domain and everything)
|
||||
* (or at least something that starts with it...)
|
||||
*
|
||||
* Also note that in this library, we never returns trailing or leading slashes.
|
||||
*/
|
||||
public static function getUrlPathFromUrl($url)
|
||||
{
|
||||
$parsed = parse_url($url);
|
||||
if (!isset($parsed['path'])) {
|
||||
return '';
|
||||
}
|
||||
if (is_null($parsed['path'])) {
|
||||
return '';
|
||||
}
|
||||
$path = untrailingslashit($parsed['path']);
|
||||
return ltrim($path, '/\\');
|
||||
}
|
||||
|
||||
public static function getUrlById($dirId) {
|
||||
switch ($dirId) {
|
||||
case 'wp-content':
|
||||
return self::getContentUrl();
|
||||
case 'index':
|
||||
return self::getHomeUrl();
|
||||
case 'home':
|
||||
return self::getHomeUrl();
|
||||
case 'plugins':
|
||||
return self::getPluginsUrl();
|
||||
case 'uploads':
|
||||
return self::getUploadUrl();
|
||||
case 'themes':
|
||||
return self::getThemesUrl();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination root url and path, provided rootId and some configuration options
|
||||
*
|
||||
* This method kind of establishes the overall structure of the cache dir.
|
||||
* (but not quite, as the logic is also in ConverterHelperIndependent::getDestination).
|
||||
*
|
||||
* @param string $rootId
|
||||
* @param DestinationOptions $destinationOptions
|
||||
*
|
||||
* @return array url and abs-path of destination root
|
||||
*/
|
||||
public static function destinationRoot($rootId, $destinationOptions)
|
||||
{
|
||||
if (($destinationOptions->mingled) && ($rootId == 'uploads')) {
|
||||
return [
|
||||
'url' => self::getUrlById('uploads'),
|
||||
'abs-path' => self::getUploadDirAbs()
|
||||
];
|
||||
} else {
|
||||
|
||||
// Its within these bases:
|
||||
$destUrl = self::getUrlById('wp-content') . '/webp-express/webp-images';
|
||||
$destPath = self::getAbsDirById('wp-content') . '/webp-express/webp-images';
|
||||
|
||||
if (($destinationOptions->useDocRoot) && self::canUseDocRootForStructuringCacheDir()) {
|
||||
$relPathFromDocRootToSourceImageRoot = PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed(
|
||||
self::getAbsDirById($rootId)
|
||||
);
|
||||
return [
|
||||
'url' => $destUrl . '/doc-root/' . $relPathFromDocRootToSourceImageRoot,
|
||||
'abs-path' => $destPath . '/doc-root/' . $relPathFromDocRootToSourceImageRoot
|
||||
];
|
||||
} else {
|
||||
$extraPath = '';
|
||||
if (is_multisite() && (get_current_blog_id() != 1)) {
|
||||
$extraPath = '/sites/' . get_current_blog_id(); // #510
|
||||
}
|
||||
return [
|
||||
'url' => $destUrl . '/' . $rootId . $extraPath,
|
||||
'abs-path' => $destPath . '/' . $rootId . $extraPath
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getRootAndRelPathForDestination($destinationPath, $imageRoots) {
|
||||
foreach ($imageRoots->getArray() as $i => $imageRoot) {
|
||||
$rootPath = $imageRoot->getAbsPath();
|
||||
if (strpos($destinationPath, realpath($rootPath)) !== false) {
|
||||
$relPath = substr($destinationPath, strlen(realpath($rootPath)) + 1);
|
||||
return [$imageRoot->id, $relPath];
|
||||
}
|
||||
}
|
||||
return ['', ''];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// PST:
|
||||
// appendOrSetExtension() have been copied from ConvertHelperIndependent.
|
||||
// TODO: I should complete the move ASAP.
|
||||
|
||||
/**
|
||||
* Append ".webp" to path or replace extension with "webp", depending on what is appropriate.
|
||||
*
|
||||
* If destination-folder is set to mingled and destination-extension is set to "set" and
|
||||
* the path is inside upload folder, the appropriate thing is to SET the extension.
|
||||
* Otherwise, it is to APPEND.
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $destinationFolder
|
||||
* @param string $destinationExt
|
||||
* @param boolean $inUploadFolder
|
||||
*/
|
||||
public static function appendOrSetExtension($path, $destinationFolder, $destinationExt, $inUploadFolder)
|
||||
{
|
||||
if (($destinationFolder == 'mingled') && ($destinationExt == 'set') && $inUploadFolder) {
|
||||
return preg_replace('/\\.(jpe?g|png)$/i', '', $path) . '.webp';
|
||||
} else {
|
||||
return $path . '.webp';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination root url and path, provided rootId and some configuration options
|
||||
*
|
||||
* This method kind of establishes the overall structure of the cache dir.
|
||||
* (but not quite, as the logic is also in ConverterHelperIndependent::getDestination).
|
||||
*
|
||||
* @param string $rootId
|
||||
* @param string $relPath
|
||||
* @param string $destinationFolder ("mingled" or "separate")
|
||||
* @param string $destinationExt ('append' or 'set')
|
||||
* @param string $destinationStructure ("doc-root" or "image-roots")
|
||||
*
|
||||
* @return array url and abs-path of destination
|
||||
*/
|
||||
/*
|
||||
public static function destinationPath($rootId, $relPath, $destinationFolder, $destinationExt, $destinationStructure) {
|
||||
|
||||
// TODO: Current logic will not do!
|
||||
// We must use ConvertHelper::getDestination for the abs path.
|
||||
// And we must use logic from AlterHtmlHelper to get the URL
|
||||
// Perhaps this method must be abandonned
|
||||
|
||||
$root = self::destinationRoot($rootId, $destinationFolder, $destinationStructure);
|
||||
$inUploadFolder = ($rootId == 'upload');
|
||||
$relPath = ConvertHelperIndependent::appendOrSetExtension($relPath, $destinationFolder, $destinationExt, $inUploadFolder);
|
||||
|
||||
return [
|
||||
'abs-path' => $root['abs-path'] . '/' . $relPath,
|
||||
'url' => $root['url'] . '/' . $relPath,
|
||||
];
|
||||
}
|
||||
|
||||
public static function destinationPathConvenience($rootId, $relPath, $config) {
|
||||
return self::destinationPath(
|
||||
$rootId,
|
||||
$relPath,
|
||||
$config['destination-folder'],
|
||||
$config['destination-extension'],
|
||||
$config['destination-structure']
|
||||
);
|
||||
}*/
|
||||
|
||||
public static function getDestinationPathCorrespondingToSource($source, $destinationOptions) {
|
||||
return Destination::getDestinationPathCorrespondingToSource(
|
||||
$source,
|
||||
Paths::getWebPExpressContentDirAbs(),
|
||||
Paths::getUploadDirAbs(),
|
||||
$destinationOptions,
|
||||
new ImageRoots(self::getImageRootsDef())
|
||||
);
|
||||
}
|
||||
|
||||
public static function getUrlPathById($dirId) {
|
||||
return self::getUrlPathFromUrl(self::getUrlById($dirId));
|
||||
}
|
||||
|
||||
public static function getHostNameOfUrl($url) {
|
||||
$urlComponents = parse_url($url);
|
||||
/* ie:
|
||||
(
|
||||
[scheme] => http
|
||||
[host] => we0
|
||||
[path] => /wordpress/uploads-moved
|
||||
)*/
|
||||
|
||||
if (!isset($urlComponents['host'])) {
|
||||
return '';
|
||||
} else {
|
||||
return $urlComponents['host'];
|
||||
}
|
||||
}
|
||||
|
||||
// Get complete home url (no trailing slash). Ie: "http://example.com/blog"
|
||||
public static function getHomeUrl()
|
||||
{
|
||||
if (!function_exists('home_url')) {
|
||||
// silence is golden?
|
||||
// bad joke. Need to handle this...
|
||||
}
|
||||
return untrailingslashit(home_url());
|
||||
}
|
||||
|
||||
/** Get home url, relative to domain. Ie "" or "blog"
|
||||
* If home url is for example http://example.com/blog/, the result is "blog"
|
||||
*/
|
||||
public static function getHomeUrlPath()
|
||||
{
|
||||
return self::getUrlPathFromUrl(self::getHomeUrl());
|
||||
}
|
||||
|
||||
|
||||
public static function getUploadUrl()
|
||||
{
|
||||
$uploadDir = wp_upload_dir(null, false);
|
||||
return untrailingslashit($uploadDir['baseurl']);
|
||||
}
|
||||
|
||||
public static function getUploadUrlPath()
|
||||
{
|
||||
return self::getUrlPathFromUrl(self::getUploadUrl());
|
||||
}
|
||||
|
||||
public static function getContentUrl()
|
||||
{
|
||||
return untrailingslashit(content_url());
|
||||
}
|
||||
|
||||
public static function getContentUrlPath()
|
||||
{
|
||||
return self::getUrlPathFromUrl(self::getContentUrl());
|
||||
}
|
||||
|
||||
public static function getThemesUrl()
|
||||
{
|
||||
return self::getContentUrl() . '/themes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Url to plugins (base)
|
||||
*/
|
||||
public static function getPluginsUrl()
|
||||
{
|
||||
return untrailingslashit(plugins_url());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Url to WebP Express plugin (this is in fact an incomplete URL, you need to append ie '/webp-on-demand.php' to get a full URL)
|
||||
*/
|
||||
public static function getWebPExpressPluginUrl()
|
||||
{
|
||||
return untrailingslashit(plugins_url('', WEBPEXPRESS_PLUGIN));
|
||||
}
|
||||
|
||||
public static function getWebPExpressPluginUrlPath()
|
||||
{
|
||||
return self::getUrlPathFromUrl(self::getWebPExpressPluginUrl());
|
||||
}
|
||||
|
||||
public static function getWodFolderUrlPath()
|
||||
{
|
||||
return
|
||||
self::getWebPExpressPluginUrlPath() .
|
||||
'/wod';
|
||||
}
|
||||
|
||||
public static function getWod2FolderUrlPath()
|
||||
{
|
||||
return
|
||||
self::getWebPExpressPluginUrlPath() .
|
||||
'/wod2';
|
||||
}
|
||||
|
||||
public static function getWodUrlPath()
|
||||
{
|
||||
return
|
||||
self::getWodFolderUrlPath() .
|
||||
'/webp-on-demand.php';
|
||||
}
|
||||
|
||||
public static function getWod2UrlPath()
|
||||
{
|
||||
return
|
||||
self::getWod2FolderUrlPath() .
|
||||
'/webp-on-demand.php';
|
||||
}
|
||||
|
||||
public static function getWebPRealizerUrlPath()
|
||||
{
|
||||
return
|
||||
self::getWodFolderUrlPath() .
|
||||
'/webp-realizer.php';
|
||||
}
|
||||
|
||||
public static function getWebPRealizer2UrlPath()
|
||||
{
|
||||
return
|
||||
self::getWod2FolderUrlPath() .
|
||||
'/webp-realizer.php';
|
||||
}
|
||||
|
||||
public static function getWebServiceUrl()
|
||||
{
|
||||
//return self::getWebPExpressPluginUrl() . '/wpc.php';
|
||||
//return self::getHomeUrl() . '/webp-express-server';
|
||||
return self::getHomeUrl() . '/webp-express-web-service';
|
||||
}
|
||||
|
||||
public static function getUrlsAndPathsForTheJavascript()
|
||||
{
|
||||
return [
|
||||
'urls' => [
|
||||
'webpExpressRoot' => self::getWebPExpressPluginUrlPath(),
|
||||
'content' => self::getContentUrlPath(),
|
||||
],
|
||||
'filePaths' => [
|
||||
'webpExpressRoot' => self::getWebPExpressPluginDirAbs(),
|
||||
'destinationRoot' => self::getCacheDirAbs(),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function getSettingsUrl()
|
||||
{
|
||||
if (!function_exists('admin_url')) {
|
||||
require_once ABSPATH . 'wp-includes/link-template.php';
|
||||
}
|
||||
if (Multisite::isNetworkActivated()) {
|
||||
// network_admin_url is also defined in link-template.php.
|
||||
return network_admin_url('settings.php?page=webp_express_settings_page');
|
||||
} else {
|
||||
return admin_url('options-general.php?page=webp_express_settings_page');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
122
lib/classes/PlatformInfo.php
Normal file
122
lib/classes/PlatformInfo.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class PlatformInfo
|
||||
{
|
||||
|
||||
public static function isMicrosoftIis()
|
||||
{
|
||||
$server = strtolower($_SERVER['SERVER_SOFTWARE']);
|
||||
return ( strpos( $server, 'microsoft-iis') !== false );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
public static function isLiteSpeed()
|
||||
{
|
||||
$server = strtolower($_SERVER['SERVER_SOFTWARE']);
|
||||
return ( strpos( $server, 'litespeed') !== false );
|
||||
}
|
||||
|
||||
public static function isNginx()
|
||||
{
|
||||
return (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false);
|
||||
}
|
||||
|
||||
public static function isApacheOrLiteSpeed()
|
||||
{
|
||||
return self::isApache() || self::isLiteSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an Apache module is available.
|
||||
*
|
||||
* If apache_get_modules() exists, it is used. That function is however only available in mod_php installs.
|
||||
* Otherwise the Wordpress function "apache_mod_loaded" is tried, which examines phpinfo() output.
|
||||
* However, it seems there is no module output on php-fpm setups.
|
||||
* So on php-fpm, we cannot come with an answer.
|
||||
* https://stackoverflow.com/questions/9021425/how-to-check-if-mod-rewrite-is-enabled-in-php
|
||||
*
|
||||
* @param string $mod Name of module - ie "mod_rewrite"
|
||||
* @return boolean|null Return if module is available, or null if indeterminate
|
||||
*/
|
||||
public static function gotApacheModule($mod)
|
||||
{
|
||||
if (function_exists('apache_get_modules')) {
|
||||
return in_array($mod, apache_get_modules());
|
||||
}
|
||||
|
||||
// Revert to Wordpress method, which examines output from phpinfo as well
|
||||
if (function_exists('apache_mod_loaded')) {
|
||||
$result = apache_mod_loaded($mod, null);
|
||||
|
||||
// If we got a real result, return it.
|
||||
if ($result != null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// We could run shell_exec("apachectl -l"), as suggested here:
|
||||
// https://stackoverflow.com/questions/9021425/how-to-check-if-mod-rewrite-is-enabled-in-php
|
||||
// But it does not seem to return all modules in my php-fpm setup.
|
||||
|
||||
// Currently we got no more tools in this function...
|
||||
// you might want to take a look at the "htaccess_capability_tester" library...
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* It is not always possible to determine if apache has a given module...
|
||||
* We shall not fool anyone into thinking otherwise by providing a "got" method like Wordpress does...
|
||||
*/
|
||||
public static function definitelyGotApacheModule($mod)
|
||||
{
|
||||
return (self::gotApacheModule($mod) === true);
|
||||
}
|
||||
|
||||
public static function definitelyNotGotApacheModule($mod)
|
||||
{
|
||||
return (self::gotApacheModule($mod) === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mod_rewrite or IIS rewrite is available.
|
||||
*
|
||||
* @return boolean|null Return bool if it can be determined, or null if not
|
||||
*/
|
||||
public static function gotRewriteModule()
|
||||
{
|
||||
$gotModRewrite = self::gotApacheModule('mod_rewrite');
|
||||
if (!is_null($gotModRewrite)) {
|
||||
return $gotModRewrite;
|
||||
}
|
||||
|
||||
// Got the IIS check here: https://stackoverflow.com/a/21249745/842756
|
||||
// but have not tested it...
|
||||
if (isset($_SERVER['IIS_UrlRewriteModule'])) {
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static function definitelyNotGotModRewrite()
|
||||
{
|
||||
return self::definitelyNotGotApacheModule('mod_rewrite');
|
||||
}
|
||||
|
||||
public static function definitelyGotModEnv()
|
||||
{
|
||||
return self::definitelyGotApacheModule('mod_env');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
111
lib/classes/PluginActivate.php
Normal file
111
lib/classes/PluginActivate.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Config;
|
||||
use \WebPExpress\HTAccess;
|
||||
use \WebPExpress\Messenger;
|
||||
use \WebPExpress\Multisite;
|
||||
use \WebPExpress\Paths;
|
||||
use \WebPExpress\PlatformInfo;
|
||||
use \WebPExpress\State;
|
||||
|
||||
class PluginActivate
|
||||
{
|
||||
// callback for 'register_activation_hook' (registred in AdminInit)
|
||||
public static function activate($network_active) {
|
||||
|
||||
Multisite::overrideIsNetworkActivated($network_active);
|
||||
|
||||
// Test if plugin is activated for the first time or reactivated
|
||||
if (State::getState('configured', false)) {
|
||||
self::reactivate();
|
||||
} else {
|
||||
self::activateFirstTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static function reactivate()
|
||||
{
|
||||
$config = Config::loadConfigAndFix(false); // false, because we do not need to test if quality detection is working
|
||||
|
||||
if ($config === false) {
|
||||
Messenger::addMessage(
|
||||
'error',
|
||||
'The config file seems to have gone missing. You will need to reconfigure WebP Express ' .
|
||||
'<a href="' . Paths::getSettingsUrl() . '">(here)</a>.'
|
||||
);
|
||||
} else {
|
||||
$rulesResult = HTAccess::saveRules($config, false);
|
||||
|
||||
$rulesSaveSuccess = $rulesResult[0];
|
||||
if ($rulesSaveSuccess) {
|
||||
Messenger::addMessage(
|
||||
'success',
|
||||
'WebP Express re-activated successfully.<br>' .
|
||||
'The image redirections are in effect again.<br><br>' .
|
||||
'Just a quick reminder: If you at some point change the upload directory or move Wordpress, ' .
|
||||
'the <i>.htaccess</i> files will need to be regenerated.<br>' .
|
||||
'You do that by re-saving the settings ' .
|
||||
'<a href="' . Paths::getSettingsUrl() . '">(here)</a>'
|
||||
);
|
||||
} else {
|
||||
Messenger::addMessage(
|
||||
'warning',
|
||||
'WebP Express could not regenerate the rewrite rules<br>' .
|
||||
'You need to change some permissions. Head to the ' .
|
||||
'<a href="' . Paths::getSettingsUrl() . '">settings page</a> ' .
|
||||
'and try to save the settings there (it will provide more information about the problem)'
|
||||
);
|
||||
}
|
||||
|
||||
HTAccess::showSaveRulesMessages($rulesResult);
|
||||
}
|
||||
}
|
||||
|
||||
private static function activateFirstTime()
|
||||
{
|
||||
// First check basic requirements.
|
||||
// -------------------------------
|
||||
|
||||
if (PlatformInfo::isMicrosoftIis()) {
|
||||
Messenger::addMessage(
|
||||
'warning',
|
||||
'You are on Microsoft IIS server. ' .
|
||||
'WebP Express <a href="https://github.com/rosell-dk/webp-express/pull/213">should work on Windows now</a>, but it has not been tested thoroughly.'
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
if (!version_compare(PHP_VERSION, '5.5.0', '>=')) {
|
||||
Messenger::addMessage(
|
||||
'warning',
|
||||
'You are on a very old version of PHP. WebP Express may not work correctly. Your PHP version:' . phpversion()
|
||||
);
|
||||
}
|
||||
|
||||
// Next issue warnings, if any
|
||||
// -------------------------------
|
||||
|
||||
if (PlatformInfo::isApache() || PlatformInfo::isLiteSpeed()) {
|
||||
// all is well.
|
||||
} else {
|
||||
Messenger::addMessage(
|
||||
'warning',
|
||||
'You are not on Apache server, nor on LiteSpeed. WebP Express only works out of the box on Apache and LiteSpeed.<br>' .
|
||||
'But you may get it to work. WebP Express will print you rewrite rules for Apache. You could try to configure your server to do similar routing.<br>' .
|
||||
'Btw: your server is: ' . $_SERVER['SERVER_SOFTWARE']
|
||||
);
|
||||
}
|
||||
|
||||
// Welcome!
|
||||
// -------------------------------
|
||||
Messenger::addMessage(
|
||||
'info',
|
||||
'WebP Express was installed successfully. To start using it, you must ' .
|
||||
'<a href="' . Paths::getSettingsUrl() . '">configure it here</a>.'
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
36
lib/classes/PluginDeactivate.php
Normal file
36
lib/classes/PluginDeactivate.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class PluginDeactivate
|
||||
{
|
||||
// The hook was registred in AdminInit
|
||||
public static function deactivate() {
|
||||
|
||||
list($success, $failures, $successes) = HTAccess::deactivateHTAccessRules();
|
||||
|
||||
if ($success) {
|
||||
// Oh, it would be nice to be able to add a goodbye message here...
|
||||
// But well, that cannot be done here.
|
||||
} else {
|
||||
// Oh no. We failed removing the rules
|
||||
$msg = "<b>Sorry, can't let you disable WebP Express!</b><br>" .
|
||||
'There are rewrite rules in the <i>.htaccess</i> that could not be removed. If these are not removed, it would break all images.<br>' .
|
||||
'Please make your <i>.htaccess</i> writable and then try to disable WebPExpress again.<br>Alternatively, remove the rules manually in your <i>.htaccess</i> file and try disabling again.' .
|
||||
'<br>It concerns the following files:<br>';
|
||||
|
||||
|
||||
foreach ($failures as $rootId) {
|
||||
$msg .= '- ' . Paths::getAbsDirById($rootId) . '/.htaccess<br>';
|
||||
}
|
||||
|
||||
Messenger::addMessage(
|
||||
'error',
|
||||
$msg
|
||||
);
|
||||
|
||||
wp_redirect(admin_url('options-general.php?page=webp_express_settings_page'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
lib/classes/PluginPageScript.php
Normal file
26
lib/classes/PluginPageScript.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class PluginPageScript
|
||||
{
|
||||
// The hook was registred in AdminInit
|
||||
public static function enqueueScripts() {
|
||||
$ver = '1'; // note: Minimum 1
|
||||
$jsDir = 'js/0.16.0'; // We change dir when it is critical that no-one gets the cached version (there is a plugin that strips version strings out there...)
|
||||
|
||||
if (!function_exists('webp_express_add_inline_script')) {
|
||||
function webp_express_add_inline_script($id, $script, $position) {
|
||||
if (function_exists('wp_add_inline_script')) {
|
||||
// wp_add_inline_script is available from Wordpress 4.5
|
||||
wp_add_inline_script($id, $script, $position);
|
||||
} else {
|
||||
echo '<script>' . $script . '</script>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wp_register_script('webpexpress-plugin-page', plugins_url($jsDir . '/plugin-page.js', dirname(dirname(__FILE__))), [], '1.9.0');
|
||||
wp_enqueue_script('webpexpress-plugin-page');
|
||||
}
|
||||
}
|
||||
33
lib/classes/PluginUninstall.php
Normal file
33
lib/classes/PluginUninstall.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\FileHelper;
|
||||
use \WebPExpress\Option;
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class PluginUninstall
|
||||
{
|
||||
// The hook was registred in AdminInit
|
||||
public static function uninstall() {
|
||||
|
||||
$optionsToDelete = [
|
||||
'webp-express-messages-pending',
|
||||
'webp-express-action-pending',
|
||||
'webp-express-state',
|
||||
'webp-express-version',
|
||||
'webp-express-activation-error',
|
||||
'webp-express-migration-version'
|
||||
];
|
||||
foreach ($optionsToDelete as $i => $optionName) {
|
||||
Option::deleteOption($optionName);
|
||||
}
|
||||
|
||||
// remove content dir (config plus images plus htaccess-tests)
|
||||
FileHelper::rrmdir(Paths::getWebPExpressContentDirAbs());
|
||||
}
|
||||
}
|
||||
31
lib/classes/Sanitize.php
Normal file
31
lib/classes/Sanitize.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class Sanitize
|
||||
{
|
||||
|
||||
/**
|
||||
* The NUL character is a demon, because it can be used to bypass other tests
|
||||
* See https://st-g.de/2011/04/doing-filename-checks-securely-in-PHP.
|
||||
*
|
||||
* @param string $string string remove NUL characters in
|
||||
*/
|
||||
public static function removeNUL($string)
|
||||
{
|
||||
return str_replace(chr(0), '', $string);
|
||||
}
|
||||
|
||||
public static function removeStreamWrappers($string)
|
||||
{
|
||||
return preg_replace('#^\\w+://#', '', $string);
|
||||
}
|
||||
|
||||
public static function path($string)
|
||||
{
|
||||
$string = self::removeNUL($string);
|
||||
$string = self::removeStreamWrappers($string);
|
||||
return $string;
|
||||
}
|
||||
|
||||
}
|
||||
412
lib/classes/SanityCheck.php
Normal file
412
lib/classes/SanityCheck.php
Normal file
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\PathHelper;
|
||||
use \WebPExpress\Sanitize;
|
||||
use \WebPExpress\SanityException;
|
||||
|
||||
class SanityCheck
|
||||
{
|
||||
|
||||
private static function fail($errorMsg, $input)
|
||||
{
|
||||
// sanitize input before calling error_log(), it might be sent to file, mail, syslog etc.
|
||||
//error_log($errorMsg . '. input:' . Sanitize::removeNUL($input) . 'backtrace: ' . print_r(debug_backtrace(), true));
|
||||
error_log($errorMsg . '. input:' . Sanitize::removeNUL($input));
|
||||
|
||||
//error_log(get_magic_quotes_gpc() ? 'on' :'off');
|
||||
throw new SanityException($errorMsg); // . '. Check debug.log for details (and make sure debugging is enabled)'
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $input string to test for NUL char
|
||||
*/
|
||||
public static function mustBeString($input, $errorMsg = 'String expected')
|
||||
{
|
||||
if (gettype($input) !== 'string') {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* The NUL character is a demon, because it can be used to bypass other tests
|
||||
* See https://st-g.de/2011/04/doing-filename-checks-securely-in-PHP.
|
||||
*
|
||||
* @param string $input string to test for NUL char
|
||||
*/
|
||||
public static function noNUL($input, $errorMsg = 'NUL character is not allowed')
|
||||
{
|
||||
self::mustBeString($input);
|
||||
if (strpos($input, chr(0)) !== false) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent control chararters (#00 - #20).
|
||||
*
|
||||
* This prevents line feed, new line, tab, charater return, tab, ets.
|
||||
* https://www.rapidtables.com/code/text/ascii-table.html
|
||||
*
|
||||
* @param string $input string to test for control characters
|
||||
*/
|
||||
public static function noControlChars($input, $errorMsg = 'Control characters are not allowed')
|
||||
{
|
||||
self::mustBeString($input);
|
||||
self::noNUL($input);
|
||||
if (preg_match('#[\x{0}-\x{1f}]#', $input)) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mixed $input something that may not be empty
|
||||
*/
|
||||
public static function notEmpty($input, $errorMsg = 'Must be non-empty')
|
||||
{
|
||||
if (empty($input)) {
|
||||
self::fail($errorMsg, '');
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function noDirectoryTraversal($input, $errorMsg = 'Directory traversal is not allowed')
|
||||
{
|
||||
self::mustBeString($input);
|
||||
self::noControlChars($input);
|
||||
if (preg_match('#\.\.\/#', $input)) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function noStreamWrappers($input, $errorMsg = 'Stream wrappers are not allowed')
|
||||
{
|
||||
self::mustBeString($input);
|
||||
self::noControlChars($input);
|
||||
|
||||
// Prevent stream wrappers ("phar://", "php://" and the like)
|
||||
// https://www.php.net/manual/en/wrappers.phar.php
|
||||
if (preg_match('#^\\w+://#', Sanitize::removeNUL($input))) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function pathDirectoryTraversalAllowed($input)
|
||||
{
|
||||
self::notEmpty($input);
|
||||
self::mustBeString($input);
|
||||
self::noControlChars($input);
|
||||
self::noStreamWrappers($input);
|
||||
|
||||
// PS: The following sanitize has no effect, as we have just tested that there are no NUL and
|
||||
// no stream wrappers. It is here to avoid false positives on coderisk.com
|
||||
$input = Sanitize::path($input);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function pathWithoutDirectoryTraversal($input)
|
||||
{
|
||||
self::pathDirectoryTraversalAllowed($input);
|
||||
self::noDirectoryTraversal($input);
|
||||
$input = Sanitize::path($input);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function path($input)
|
||||
{
|
||||
return self::pathWithoutDirectoryTraversal($input);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Beware: This does not take symlinks into account.
|
||||
* I should make one that does. Until then, you should probably not call this method from outside this class
|
||||
*/
|
||||
private static function pathBeginsWith($input, $beginsWith, $errorMsg = 'Path is outside allowed path')
|
||||
{
|
||||
self::path($input);
|
||||
if (!(strpos($input, $beginsWith) === 0)) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
private static function pathBeginsWithSymLinksExpanded($input, $beginsWith, $errorMsg = 'Path is outside allowed path') {
|
||||
$closestExistingFolder = PathHelper::findClosestExistingFolderSymLinksExpanded($input);
|
||||
self::pathBeginsWith($closestExistingFolder, $beginsWith, $errorMsg);
|
||||
}
|
||||
|
||||
private static function absPathMicrosoftStyle($input, $errorMsg = 'Not an fully qualified Windows path')
|
||||
{
|
||||
// On microsoft we allow [drive letter]:\
|
||||
if (!preg_match("#^[A-Z]:\\\\|/#", $input)) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
private static function isOnMicrosoft()
|
||||
{
|
||||
if (isset($_SERVER['SERVER_SOFTWARE'])) {
|
||||
if (strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'microsoft') !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
switch (PHP_OS) {
|
||||
case "WINNT":
|
||||
case "WIN32":
|
||||
case "INTERIX":
|
||||
case "UWIN":
|
||||
case "UWIN-W7":
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function absPath($input, $errorMsg = 'Not an absolute path')
|
||||
{
|
||||
// first make sure there are no nasty things like control chars, phar wrappers, etc.
|
||||
// - and no directory traversal either.
|
||||
self::path($input);
|
||||
|
||||
// For non-windows, we require that an absolute path begins with "/"
|
||||
// On windows, we also accept that a path starts with a drive letter, ie "C:\"
|
||||
if ((strpos($input, '/') !== 0)) {
|
||||
if (self::isOnMicrosoft()) {
|
||||
self::absPathMicrosoftStyle($input);
|
||||
} else {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function absPathInOneOfTheseRoots()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Look if filepath is within a dir path.
|
||||
* Also tries expanding symlinks
|
||||
*
|
||||
* @param string $filePath Path to file. It may be non-existing.
|
||||
* @param string $dirPath Path to dir. It must exist in order for symlinks to be expanded.
|
||||
*/
|
||||
private static function isFilePathWithinExistingDirPath($filePath, $dirPath)
|
||||
{
|
||||
// sanity-check input. It must be a valid absolute filepath. It is allowed to be non-existing
|
||||
self::absPath($filePath);
|
||||
|
||||
// sanity-check dir and that it exists.
|
||||
self::absPathExistsAndIsDir($dirPath);
|
||||
|
||||
return PathHelper::isFilePathWithinDirPath($filePath, $dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look if filepath is within multiple dir paths.
|
||||
* Also tries expanding symlinks
|
||||
*
|
||||
* @param string $input Path to file. It may be non-existing.
|
||||
* @param array $roots Allowed root dirs. Note that they must exist in order for symlinks to be expanded.
|
||||
*/
|
||||
public static function filePathWithinOneOfTheseRoots($input, $roots, $errorMsg = 'The path is outside allowed roots.')
|
||||
{
|
||||
self::absPath($input);
|
||||
|
||||
foreach ($roots as $root) {
|
||||
if (self::isFilePathWithinExistingDirPath($input, $root)) {
|
||||
return $input;
|
||||
}
|
||||
}
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
|
||||
/*
|
||||
public static function sourcePath($input, $errorMsg = 'The source path is outside allowed roots. It is only allowed to convert images that resides in: home dir, content path, upload dir and plugin dir.')
|
||||
{
|
||||
$validPaths = [
|
||||
Paths::getHomeDirAbs(),
|
||||
Paths::getIndexDirAbs(),
|
||||
Paths::getContentDirAbs(),
|
||||
Paths::getUploadDirAbs(),
|
||||
Paths::getPluginDirAbs()
|
||||
];
|
||||
return self::filePathWithinOneOfTheseRoots($input, $validPaths, $errorMsg);
|
||||
}
|
||||
|
||||
public static function destinationPath($input, $errorMsg = 'The destination path is outside allowed roots. The webps may only be stored in the upload folder and in the folder that WebP Express stores converted images in')
|
||||
{
|
||||
self::absPath($input);
|
||||
|
||||
// Webp Express only store converted images in upload folder and in its "webp-images" folder
|
||||
// Check that destination path is within one of these.
|
||||
$validPaths = [
|
||||
'/var/www/webp-express-tests/we1'
|
||||
//Paths::getUploadDirAbs(),
|
||||
//Paths::getWebPExpressContentDirRel() . '/webp-images'
|
||||
];
|
||||
return self::filePathWithinOneOfTheseRoots($input, $validPaths, $errorMsg);
|
||||
}*/
|
||||
|
||||
|
||||
/**
|
||||
* Test that path is an absolute path and it is in document root.
|
||||
*
|
||||
* If DOCUMENT_ROOT is not available, then only the absPath check will be done.
|
||||
*
|
||||
* TODO: Instead of this method, we shoud check
|
||||
*
|
||||
*
|
||||
* It is acceptable if the absolute path does not exist
|
||||
*/
|
||||
public static function absPathIsInDocRoot($input, $errorMsg = 'Path is outside document root')
|
||||
{
|
||||
self::absPath($input);
|
||||
|
||||
if (!isset($_SERVER["DOCUMENT_ROOT"])) {
|
||||
return $input;
|
||||
}
|
||||
if ($_SERVER["DOCUMENT_ROOT"] == '') {
|
||||
return $input;
|
||||
}
|
||||
|
||||
$docRoot = self::absPath($_SERVER["DOCUMENT_ROOT"]);
|
||||
$docRoot = rtrim($docRoot, '/');
|
||||
|
||||
try {
|
||||
$docRoot = self::absPathExistsAndIsDir($docRoot);
|
||||
} catch (SanityException $e) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
// Use realpath to expand symbolic links and check if it exists
|
||||
$docRootSymLinksExpanded = @realpath($docRoot);
|
||||
if ($docRootSymLinksExpanded === false) {
|
||||
// probably outside open basedir restriction.
|
||||
//$errorMsg = 'Cannot resolve document root';
|
||||
//self::fail($errorMsg, $input);
|
||||
|
||||
// Cannot resolve document root, so cannot test if in document root
|
||||
return $input;
|
||||
}
|
||||
|
||||
// See if $filePath begins with the realpath of the $docRoot + '/'. If it does, we are done and OK!
|
||||
// (pull #429)
|
||||
if (strpos($input, $docRootSymLinksExpanded . '/') === 0) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
$docRootSymLinksExpanded = rtrim($docRootSymLinksExpanded, '\\/');
|
||||
$docRootSymLinksExpanded = self::absPathExists($docRootSymLinksExpanded, 'Document root does not exist!');
|
||||
$docRootSymLinksExpanded = self::absPathExistsAndIsDir($docRootSymLinksExpanded, 'Document root is not a directory!');
|
||||
|
||||
$directorySeparator = self::isOnMicrosoft() ? '\\' : '/';
|
||||
$errorMsg = 'Path is outside resolved document root (' . $docRootSymLinksExpanded . ')';
|
||||
self::pathBeginsWithSymLinksExpanded($input, $docRootSymLinksExpanded . $directorySeparator, $errorMsg);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function absPathExists($input, $errorMsg = 'Path does not exist or it is outside restricted basedir')
|
||||
{
|
||||
self::absPath($input);
|
||||
if (@!file_exists($input)) {
|
||||
// TODO: We might be able to detect if the problem is that the path does not exist or if the problem
|
||||
// is that it is outside restricted basedir.
|
||||
// ie by creating an error handler or inspecting the php ini "open_basedir" setting
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function absPathExistsAndIsDir(
|
||||
$input,
|
||||
$errorMsg = 'Path points to a file (it should point to a directory)'
|
||||
) {
|
||||
self::absPathExists($input, 'Directory does not exist or is outside restricted basedir');
|
||||
if (!is_dir($input)) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function absPathExistsAndIsFile(
|
||||
$input,
|
||||
$errorMsg = 'Path points to a directory (it should not do that)'
|
||||
) {
|
||||
self::absPathExists($input, 'File does not exist or is outside restricted basedir');
|
||||
if (@is_dir($input)) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function absPathExistsAndIsFileInDocRoot($input)
|
||||
{
|
||||
self::absPathExistsAndIsFile($input);
|
||||
self::absPathIsInDocRoot($input);
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function absPathExistsAndIsNotDir(
|
||||
$input,
|
||||
$errorMsg = 'Path points to a directory (it should point to a file)'
|
||||
) {
|
||||
self::absPathExistsAndIsFile($input, $errorMsg);
|
||||
return $input;
|
||||
}
|
||||
|
||||
|
||||
public static function pregMatch($pattern, $input, $errorMsg = 'Does not match expected pattern')
|
||||
{
|
||||
self::noNUL($input);
|
||||
self::mustBeString($input);
|
||||
if (!preg_match($pattern, $input)) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function isJSONArray($input, $errorMsg = 'Not a JSON array')
|
||||
{
|
||||
self::noNUL($input);
|
||||
self::mustBeString($input);
|
||||
self::notEmpty($input);
|
||||
if ((strpos($input, '[') !== 0) || (!is_array(json_decode($input)))) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function isJSONObject($input, $errorMsg = 'Not a JSON object')
|
||||
{
|
||||
self::noNUL($input);
|
||||
self::mustBeString($input);
|
||||
self::notEmpty($input);
|
||||
if ((strpos($input, '{') !== 0) || (!is_object(json_decode($input)))) {
|
||||
self::fail($errorMsg, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
}
|
||||
7
lib/classes/SanityException.php
Normal file
7
lib/classes/SanityException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class SanityException extends \Exception
|
||||
{
|
||||
}
|
||||
118
lib/classes/SelfTest.php
Normal file
118
lib/classes/SelfTest.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class SelfTest
|
||||
{
|
||||
|
||||
private static $next;
|
||||
|
||||
public static function allInfo()
|
||||
{
|
||||
self::$next = 'done';
|
||||
$config = Config::loadConfigAndFix(false);
|
||||
return SelfTestHelper::allInfo($config);
|
||||
}
|
||||
|
||||
|
||||
public static function systemInfo()
|
||||
{
|
||||
self::$next = 'configInfo';
|
||||
return SelfTestHelper::systemInfo();
|
||||
}
|
||||
|
||||
public static function configInfo()
|
||||
{
|
||||
self::$next = 'capabilityTests';
|
||||
$config = Config::loadConfigAndFix(false);
|
||||
return SelfTestHelper::configInfo($config);
|
||||
}
|
||||
|
||||
public static function capabilityTests()
|
||||
{
|
||||
self::$next = 'done';
|
||||
$config = Config::loadConfigAndFix(false);
|
||||
return SelfTestHelper::capabilityTests($config);
|
||||
}
|
||||
|
||||
public static function redirectToExisting()
|
||||
{
|
||||
self::$next = 'done';
|
||||
list ($success, $result) = SelfTestRedirectToExisting::runTest();
|
||||
return $result;
|
||||
/*
|
||||
$result = [];
|
||||
$result[] = '# Redirection tests';
|
||||
$modRewriteWorking = HTAccessCapabilityTestRunner::modRewriteWorking();
|
||||
$modHeaderWorking = HTAccessCapabilityTestRunner::modHeaderWorking();
|
||||
|
||||
if (($modRewriteWorking === false) && ($modHeaderWorking)) {
|
||||
//$result[] = 'mod_rewrite is not working';
|
||||
|
||||
if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') !== false) {
|
||||
|
||||
$result[] = 'You are on Nginx and the rules that WebP Express stores in the .htaccess files does not ' .
|
||||
'have any effect. '
|
||||
|
||||
}
|
||||
// if (stripos($_SERVER["SERVER_SOFTWARE"], 'apache') !== false && stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
|
||||
|
||||
}
|
||||
|
||||
return [$result, 'done'];*/
|
||||
}
|
||||
|
||||
public static function redirectToConverter()
|
||||
{
|
||||
self::$next = 'done';
|
||||
list ($success, $result) = SelfTestRedirectToConverter::runTest();
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function redirectToWebPRealizer()
|
||||
{
|
||||
self::$next = 'done';
|
||||
list ($success, $result) = SelfTestRedirectToWebPRealizer::runTest();
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public static function processAjax()
|
||||
{
|
||||
if (!check_ajax_referer('webpexpress-ajax-self-test-nonce', 'nonce', false)) {
|
||||
wp_send_json_error('The security nonce has expired. You need to reload the settings page (press F5) and try again)');
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// Check input
|
||||
// --------------
|
||||
try {
|
||||
// Check "testId"
|
||||
$checking = '"testId" argument';
|
||||
Validate::postHasKey('testId');
|
||||
|
||||
$testId = sanitize_text_field(stripslashes($_POST['testId']));
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error('Validation failed for ' . $checking . ': '. $e->getMessage());
|
||||
wp_die();
|
||||
}
|
||||
$result = '';
|
||||
if (method_exists(__CLASS__, $testId)) {
|
||||
|
||||
// The following call sets self::$next.
|
||||
$result = call_user_func(array(__CLASS__, $testId));
|
||||
} else {
|
||||
$result = ['Unknown test: ' . $testId];
|
||||
self::$next = 'break';
|
||||
}
|
||||
|
||||
$response = [
|
||||
'result' => $result,
|
||||
'next' => self::$next
|
||||
];
|
||||
echo json_encode($response, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
}
|
||||
792
lib/classes/SelfTestHelper.php
Normal file
792
lib/classes/SelfTestHelper.php
Normal file
@@ -0,0 +1,792 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Paths;
|
||||
|
||||
class SelfTestHelper
|
||||
{
|
||||
|
||||
public static function deleteFilesInDir($dir, $filePattern = "*")
|
||||
{
|
||||
foreach (glob($dir . DIRECTORY_SEPARATOR . $filePattern) as $filename) {
|
||||
unlink($filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove files in dir and the dir. Does not remove files recursively.
|
||||
*/
|
||||
public static function deleteDir($dir)
|
||||
{
|
||||
if (@file_exists($dir)) {
|
||||
self::deleteFilesInDir($dir);
|
||||
rmdir($dir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function deleteTestImagesInFolder($rootId)
|
||||
{
|
||||
$testDir = Paths::getAbsDirById($rootId) . '/webp-express-test-images';
|
||||
self::deleteDir($testDir);
|
||||
}
|
||||
|
||||
public static function cleanUpTestImages($rootId, $config)
|
||||
{
|
||||
// Clean up test images in source folder
|
||||
self::deleteTestImagesInFolder($rootId);
|
||||
|
||||
// Clean up dummy webp images in cache folder for the root
|
||||
$cacheDirForRoot = Paths::getCacheDirForImageRoot(
|
||||
$config['destination-folder'],
|
||||
$config['destination-structure'],
|
||||
$rootId
|
||||
);
|
||||
|
||||
$testDir = $cacheDirForRoot . '/webp-express-test-images';
|
||||
self::deleteDir($testDir);
|
||||
}
|
||||
|
||||
public static function copyFile($source, $destination)
|
||||
{
|
||||
$log = [];
|
||||
if (@copy($source, $destination)) {
|
||||
return [true, $log];
|
||||
} else {
|
||||
$log[] = 'Failed to copy *' . $source . '* to *' . $destination . '*';
|
||||
if (!@file_exists($source)) {
|
||||
$log[] = 'The source file was not found';
|
||||
} else {
|
||||
if (!@file_exists(dirname($destination))) {
|
||||
$log[] = 'The destination folder does not exist!';
|
||||
} else {
|
||||
$log[] = 'This is probably a permission issue. Check that your webserver has permission to ' .
|
||||
'write files in the directory (*' . dirname($destination) . '*)';
|
||||
}
|
||||
}
|
||||
return [false, $log];
|
||||
}
|
||||
}
|
||||
|
||||
public static function randomDigitsAndLetters($length)
|
||||
{
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$charactersLength = strlen($characters);
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomString .= $characters[rand(0, $charactersLength - 1)];
|
||||
}
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
public static function copyTestImageToRoot($rootId, $imageType = 'jpeg')
|
||||
{
|
||||
// TODO: Copy to a subfolder instead
|
||||
// TODO: Use smaller jpeg / pngs please.
|
||||
$log = [];
|
||||
switch ($imageType) {
|
||||
case 'jpeg':
|
||||
$fileNameToCopy = 'very-small.jpg';
|
||||
break;
|
||||
case 'png':
|
||||
$fileNameToCopy = 'test.png';
|
||||
break;
|
||||
}
|
||||
$testSource = Paths::getPluginDirAbs() . '/webp-express/test/' . $fileNameToCopy;
|
||||
$filenameOfDestination = self::randomDigitsAndLetters(6) . '.' . strtoupper($imageType);
|
||||
//$filenameOfDestination = self::randomDigitsAndLetters(6) . '.' . $imageType;
|
||||
$log[] = 'Copying ' . strtoupper($imageType) . ' to ' . $rootId . ' folder (*webp-express-test-images/' . $filenameOfDestination . '*)';
|
||||
|
||||
$destDir = Paths::getAbsDirById($rootId) . '/webp-express-test-images';
|
||||
$destination = $destDir . '/' . $filenameOfDestination;
|
||||
|
||||
if (!@file_exists($destDir)) {
|
||||
if (!@mkdir($destDir)) {
|
||||
$log[count($log) - 1] .= '. FAILED';
|
||||
$log[] = 'Failed to create folder for test images: ' . $destDir;
|
||||
return [$log, false, ''];
|
||||
}
|
||||
}
|
||||
|
||||
list($success, $errors) = self::copyFile($testSource, $destination);
|
||||
if (!$success) {
|
||||
$log[count($log) - 1] .= '. FAILED';
|
||||
$log = array_merge($log, $errors);
|
||||
return [$log, false, ''];
|
||||
} else {
|
||||
$log[count($log) - 1] .= '. ok!';
|
||||
$log[] = 'We now have a ' . $imageType . ' stored here:';
|
||||
$log[] = '*' . $destination . '*';
|
||||
}
|
||||
return [$log, true, $filenameOfDestination];
|
||||
}
|
||||
|
||||
public static function copyTestImageToUploadFolder($imageType = 'jpeg')
|
||||
{
|
||||
return self::copyTestImageToRoot('uploads', $imageType);
|
||||
}
|
||||
|
||||
public static function copyDummyWebPToCacheFolder($rootId, $destinationFolder, $destinationExtension, $destinationStructure, $sourceFileName, $imageType = 'jpeg')
|
||||
{
|
||||
$log = [];
|
||||
$dummyWebP = Paths::getPluginDirAbs() . '/webp-express/test/test.jpg.webp';
|
||||
|
||||
$log[] = 'Copying dummy webp to the cache root for ' . $rootId;
|
||||
$destDir = Paths::getCacheDirForImageRoot($destinationFolder, $destinationStructure, $rootId);
|
||||
if (!file_exists($destDir)) {
|
||||
$log[] = 'The folder did not exist. Creating folder at: ' . $destinationFolder;
|
||||
if (!mkdir($destDir, 0777, true)) {
|
||||
$log[] = 'Failed creating folder!';
|
||||
return [$log, false, ''];
|
||||
}
|
||||
}
|
||||
$destDir .= '/webp-express-test-images';
|
||||
if (!file_exists($destDir)) {
|
||||
if (!mkdir($destDir, 0755, false)) {
|
||||
$log[] = 'Failed creating the folder for the test images:';
|
||||
$log[] = $destDir;
|
||||
$log[] = 'To run this test, you must grant write permissions';
|
||||
return [$log, false, ''];
|
||||
}
|
||||
}
|
||||
|
||||
$filenameOfDestination = ConvertHelperIndependent::appendOrSetExtension(
|
||||
$sourceFileName,
|
||||
$destinationFolder,
|
||||
$destinationExtension,
|
||||
($rootId == 'uploads')
|
||||
);
|
||||
|
||||
//$filenameOfDestination = $destinationFileNameNoExt . ($destinationExtension == 'append' ? '.' . $imageType : '') . '.webp';
|
||||
$destination = $destDir . '/' . $filenameOfDestination;
|
||||
|
||||
list($success, $errors) = self::copyFile($dummyWebP, $destination);
|
||||
if (!$success) {
|
||||
$log[count($log) - 1] .= '. FAILED';
|
||||
$log = array_merge($log, $errors);
|
||||
return [$log, false, ''];
|
||||
} else {
|
||||
$log[count($log) - 1] .= '. ok!';
|
||||
$log[] = 'We now have a webp file stored here:';
|
||||
$log[] = '*' . $destination . '*';
|
||||
$log[] = '';
|
||||
}
|
||||
return [$log, true, $destination];
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform HTTP request.
|
||||
*
|
||||
* @param string $requestUrl URL
|
||||
* @param array $args Args to pass to wp_remote_get. Note however that "redirection" is set to 0
|
||||
* @param int $maxRedirects For internal use
|
||||
* @return array The result
|
||||
* $success (boolean): If we got a 200 response in the end (after max 2 redirects)
|
||||
* $log (array) : Message log
|
||||
* $results : Array of results from wp_remote_get. If no redirection occured, it will only contain one item.
|
||||
*
|
||||
*/
|
||||
public static function remoteGet($requestUrl, $args = [], $maxRedirects = 2)
|
||||
{
|
||||
$log = [];
|
||||
$args['redirection'] = 0;
|
||||
|
||||
if (defined('WP_DEBUG') && WP_DEBUG ) {
|
||||
// Prevent errors with unverified certificates (#379)
|
||||
$args['sslverify'] = false;
|
||||
}
|
||||
|
||||
$log[] = 'Request URL: ' . $requestUrl;
|
||||
|
||||
$results = [];
|
||||
$wpResult = wp_remote_get($requestUrl, $args);
|
||||
if (is_wp_error($wpResult)) {
|
||||
$log[] = 'The remote request errored';
|
||||
$log[] = $wpResult->get_error_message();
|
||||
//$log[] = print_r($wpResult, true);
|
||||
return [false, $log, $results];
|
||||
}
|
||||
if (!is_wp_error($wpResult) && !isset($wpResult['headers'])) {
|
||||
$wpResult['headers'] = [];
|
||||
}
|
||||
$results[] = $wpResult;
|
||||
$responseCode = $wpResult['response']['code'];
|
||||
|
||||
$log[] = 'Response: ' . $responseCode . ' ' . $wpResult['response']['message'];
|
||||
$log = array_merge($log, SelfTestHelper::printHeaders($wpResult['headers']));
|
||||
|
||||
if (isset($wpResult['headers']['content-type'])) {
|
||||
if (strpos($wpResult['headers']['content-type'], 'text/html') !== false) {
|
||||
if (isset($wpResult['body']) && (!empty($wpResult['body']))) {
|
||||
$log[] = 'Body:';
|
||||
$log[] = print_r($wpResult['body'], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (($responseCode == '302') || ($responseCode == '301')) {
|
||||
if ($maxRedirects > 0) {
|
||||
if (isset($wpResult['headers']['location'])) {
|
||||
$url = $wpResult['headers']['location'];
|
||||
if (strpos($url, 'http') !== 0) {
|
||||
$url = $requestUrl . $url;
|
||||
}
|
||||
$log[] = 'Following that redirect';
|
||||
|
||||
list($success, $newLog, $newResult) = self::remoteGet($url, $args, $maxRedirects - 1);
|
||||
$log = array_merge($log, $newLog);
|
||||
$results = array_merge($results, $newResult);
|
||||
|
||||
return [$success, $log, $results];
|
||||
|
||||
}
|
||||
} else {
|
||||
$log[] = 'Not following the redirect (max redirects exceeded)';
|
||||
}
|
||||
}
|
||||
|
||||
$success = ($responseCode == '200');
|
||||
return [$success, $log, $results];
|
||||
}
|
||||
|
||||
public static function hasHeaderContaining($headers, $headerToInspect, $containString)
|
||||
{
|
||||
if (!isset($headers[$headerToInspect])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there are multiple headers, check all
|
||||
if (gettype($headers[$headerToInspect]) == 'string') {
|
||||
$h = [$headers[$headerToInspect]];
|
||||
} else {
|
||||
$h = $headers[$headerToInspect];
|
||||
}
|
||||
foreach ($h as $headerValue) {
|
||||
if (stripos($headerValue, $containString) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function hasVaryAcceptHeader($headers)
|
||||
{
|
||||
if (!isset($headers['vary'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// There may be multiple Vary headers. Or they might be combined in one.
|
||||
// Both are acceptable, according to https://stackoverflow.com/a/28799169/842756
|
||||
if (gettype($headers['vary']) == 'string') {
|
||||
$varyHeaders = [$headers['vary']];
|
||||
} else {
|
||||
$varyHeaders = $headers['vary'];
|
||||
}
|
||||
foreach ($varyHeaders as $headerValue) {
|
||||
$values = explode(',', $headerValue);
|
||||
foreach ($values as $value) {
|
||||
if (strtolower($value) == 'accept') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $rule existing|webp-on-demand|webp-realizer
|
||||
*/
|
||||
public static function diagnoseNoVaryHeader($rootId, $rule)
|
||||
{
|
||||
$log = [];
|
||||
$log[] = '**However, we did not receive a Vary:Accept header. ' .
|
||||
'That header should be set in order to tell proxies that the response varies depending on the ' .
|
||||
'Accept header. Otherwise browsers not supporting webp might get a cached webp and vice versa.**{: .warn}';
|
||||
|
||||
$log[] = 'Too technical? ';
|
||||
$log[] = 'Here is an explanation of what this means: ' .
|
||||
'Some companies have set up proxies which caches resources. This way, if employee A have downloaded an ' .
|
||||
'image and employee B requests it, the proxy can deliver the image directly to employee B without needing to ' .
|
||||
'send a request to the server. ' .
|
||||
'This is clever, but it can go wrong. If B for some reason is meant to get another image than A, it will not ' .
|
||||
'happen, as the server does not get the request. That is where the Vary header comes in. It tells the proxy ' .
|
||||
'that the image is dependent upon something. In this case, we need to signal proxies that the image depends upon ' .
|
||||
'the "Accept" header, as this is the one browsers use to tell the server if it accepts webps or not. ' .
|
||||
'We do that using the "Vary:Accept" header. However - it is missing :( ' .
|
||||
'Which means that employees at (larger) companies might experience problems if some are using browsers ' .
|
||||
'that supports webp and others are using browsers that does not. Worst case is that the request to an image ' .
|
||||
'is done with a browser that supports webp, as this will cache the webp in the proxy, and deliver webps to ' .
|
||||
'all employees - even to those who uses browsers that does not support webp. These employees will get blank images.';
|
||||
|
||||
if ($rule == 'existing') {
|
||||
$log[] = 'So, what should you do? **I would recommend that you either try to fix the problem with the missing Vary:Accept ' .
|
||||
'header or change to "CDN friendly" mode.**{: .warn}';
|
||||
} elseif ($rule == 'webp-on-demand') {
|
||||
$log[] = 'So, what should you do? **I would recommend that you either try to fix the problem with the missing Vary:Accept ' .
|
||||
'header or disable the "Enable redirection to converter?" option and use another way to get the images converted - ie ' .
|
||||
'Bulk Convert or Convert on Upload**{: .warn}';
|
||||
}
|
||||
|
||||
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function hasCacheControlOrExpiresHeader($headers)
|
||||
{
|
||||
if (isset($headers['cache-control'])) {
|
||||
return true;
|
||||
}
|
||||
if (isset($headers['expires'])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static function flattenHeaders($headers)
|
||||
{
|
||||
$log = [];
|
||||
foreach ($headers as $headerName => $headerValue) {
|
||||
if (gettype($headerValue) == 'array') {
|
||||
foreach ($headerValue as $i => $value) {
|
||||
$log[] = [$headerName, $value];
|
||||
}
|
||||
} else {
|
||||
$log[] = [$headerName, $headerValue];
|
||||
}
|
||||
}
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function printHeaders($headers)
|
||||
{
|
||||
$log = [];
|
||||
$log[] = '#### Response headers:';
|
||||
|
||||
$headersFlat = self::flattenHeaders($headers);
|
||||
//
|
||||
foreach ($headersFlat as $i => list($headerName, $headerValue)) {
|
||||
if ($headerName == 'x-webp-express-error') {
|
||||
$headerValue = '**' . $headerValue . '**{: .error}';
|
||||
}
|
||||
$log[] = '- ' . $headerName . ': ' . $headerValue;
|
||||
}
|
||||
$log[] = '';
|
||||
return $log;
|
||||
}
|
||||
|
||||
private static function trueFalseNullString($var)
|
||||
{
|
||||
if ($var === true) {
|
||||
return 'yes';
|
||||
}
|
||||
if ($var === false) {
|
||||
return 'no';
|
||||
}
|
||||
return 'could not be determined';
|
||||
}
|
||||
|
||||
public static function systemInfo()
|
||||
{
|
||||
$log = [];
|
||||
$log[] = '#### System info:';
|
||||
$log[] = '- PHP version: ' . phpversion();
|
||||
$log[] = '- OS: ' . PHP_OS;
|
||||
$log[] = '- Server software: ' . $_SERVER["SERVER_SOFTWARE"];
|
||||
$log[] = '- Document Root status: ' . Paths::docRootStatusText();
|
||||
if (PathHelper::isDocRootAvailable()) {
|
||||
$log[] = '- Document Root: ' . $_SERVER['DOCUMENT_ROOT'];
|
||||
}
|
||||
if (PathHelper::isDocRootAvailableAndResolvable()) {
|
||||
if ($_SERVER['DOCUMENT_ROOT'] != realpath($_SERVER['DOCUMENT_ROOT'])) {
|
||||
$log[] = '- Document Root (symlinked resolved): ' . realpath($_SERVER['DOCUMENT_ROOT']);
|
||||
}
|
||||
}
|
||||
|
||||
$log[] = '- Document Root: ' . Paths::docRootStatusText();
|
||||
$log[] = '- Apache module "mod_rewrite" enabled?: ' . self::trueFalseNullString(PlatformInfo::gotApacheModule('mod_rewrite'));
|
||||
$log[] = '- Apache module "mod_headers" enabled?: ' . self::trueFalseNullString(PlatformInfo::gotApacheModule('mod_headers'));
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function wordpressInfo()
|
||||
{
|
||||
$log = [];
|
||||
$log[] = '#### Wordpress info:';
|
||||
$log[] = '- Version: ' . get_bloginfo('version');
|
||||
$log[] = '- Multisite?: ' . self::trueFalseNullString(is_multisite());
|
||||
$log[] = '- Is wp-content moved?: ' . self::trueFalseNullString(Paths::isWPContentDirMoved());
|
||||
$log[] = '- Is uploads moved out of wp-content?: ' . self::trueFalseNullString(Paths::isUploadDirMovedOutOfWPContentDir());
|
||||
$log[] = '- Is plugins moved out of wp-content?: ' . self::trueFalseNullString(Paths::isPluginDirMovedOutOfWpContent());
|
||||
|
||||
$log[] = '';
|
||||
|
||||
$log[] = '#### Image roots (absolute paths)';
|
||||
foreach (Paths::getImageRootIds() as $rootId) {
|
||||
$absDir = Paths::getAbsDirById($rootId);
|
||||
|
||||
if (PathHelper::pathExistsAndIsResolvable($absDir) && ($absDir != realpath($absDir))) {
|
||||
$log[] = '*' . $rootId . '*: ' . $absDir . ' (resolved for symlinks: ' . realpath($absDir) . ')';
|
||||
} else {
|
||||
$log[] = '*' . $rootId . '*: ' . $absDir;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$log[] = '#### Image roots (relative to document root)';
|
||||
foreach (Paths::getImageRootIds() as $rootId) {
|
||||
$absPath = Paths::getAbsDirById($rootId);
|
||||
if (PathHelper::canCalculateRelPathFromDocRootToDir($absPath)) {
|
||||
$log[] = '*' . $rootId . '*: ' . PathHelper::getRelPathFromDocRootToDirNoDirectoryTraversalAllowed($absPath);
|
||||
} else {
|
||||
$log[] = '*' . $rootId . '*: ' . 'n/a (not within document root)';
|
||||
}
|
||||
}
|
||||
|
||||
$log[] = '#### Image roots (URLs)';
|
||||
foreach (Paths::getImageRootIds() as $rootId) {
|
||||
$url = Paths::getUrlById($rootId);
|
||||
$log[] = '*' . $rootId . '*: ' . $url;
|
||||
}
|
||||
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function configInfo($config)
|
||||
{
|
||||
$log = [];
|
||||
$log[] = '#### WebP Express configuration info:';
|
||||
$log[] = '- Destination folder: ' . $config['destination-folder'];
|
||||
$log[] = '- Destination extension: ' . $config['destination-extension'];
|
||||
$log[] = '- Destination structure: ' . $config['destination-structure'];
|
||||
//$log[] = 'Image types: ' . ;
|
||||
//$log[] = '';
|
||||
$log[] = '(To view all configuration, take a look at the config file, which is stored in *' . Paths::getConfigFileName() . '*)';
|
||||
//$log[] = '- Config file: (config.json)';
|
||||
//$log[] = "'''\n" . json_encode($config, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT) . "\n'''\n";
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function htaccessInfo($config, $printRules = true)
|
||||
{
|
||||
$log = [];
|
||||
//$log[] = '*.htaccess info:*';
|
||||
//$log[] = '- Image roots with WebP Express rules: ' . implode(', ', HTAccess::getRootsWithWebPExpressRulesIn());
|
||||
$log[] = '#### .htaccess files that WebP Express have placed rules in the following files:';
|
||||
$rootIds = HTAccess::getRootsWithWebPExpressRulesIn();
|
||||
foreach ($rootIds as $imageRootId) {
|
||||
$log[] = '- ' . Paths::getAbsDirById($imageRootId) . '/.htaccess';
|
||||
}
|
||||
|
||||
foreach ($rootIds as $imageRootId) {
|
||||
$log = array_merge($log, self::rulesInImageRoot($config, $imageRootId));
|
||||
}
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function rulesInImageRoot($config, $imageRootId)
|
||||
{
|
||||
$log = [];
|
||||
$file = Paths::getAbsDirById($imageRootId) . '/.htaccess';
|
||||
$log[] = '#### WebP rules in *' .
|
||||
($imageRootId == 'cache' ? 'webp image cache' : $imageRootId) . '*:';
|
||||
$log[] = 'File: ' . $file;
|
||||
if (!HTAccess::haveWeRulesInThisHTAccess($file)) {
|
||||
$log[] = '**NONE!**{: .warn}';
|
||||
} else {
|
||||
$weRules = HTAccess::extractWebPExpressRulesFromHTAccess($file);
|
||||
// remove unindented comments
|
||||
//$weRules = preg_replace('/^\#\s[^\n\r]*[\n\r]+/ms', '', $weRules);
|
||||
|
||||
// remove comments in the beginning
|
||||
$weRulesArr = preg_split("/\r\n|\n|\r/", $weRules); // https://stackoverflow.com/a/11165332/842756
|
||||
while ((strlen($weRulesArr[0]) > 0) && ($weRulesArr[0][0] == '#')) {
|
||||
array_shift($weRulesArr);
|
||||
}
|
||||
$weRules = implode("\n", $weRulesArr);
|
||||
|
||||
$log[] = '```' . $weRules . '```';
|
||||
}
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function rulesInUpload($config)
|
||||
{
|
||||
return self::rulesInImageRoot($config, 'uploads');
|
||||
}
|
||||
|
||||
public static function allInfo($config)
|
||||
{
|
||||
$log = [];
|
||||
|
||||
$log = array_merge($log, self::systemInfo());
|
||||
$log = array_merge($log, self::wordpressInfo());
|
||||
$log = array_merge($log, self::configInfo($config));
|
||||
$log = array_merge($log, self::capabilityTests($config));
|
||||
$log = array_merge($log, self::htaccessInfo($config, true));
|
||||
//$log = array_merge($log, self::rulesInImageRoot($config, 'upload'));
|
||||
//$log = array_merge($log, self::rulesInImageRoot($config, 'wp-content'));
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function capabilityTests($config)
|
||||
{
|
||||
$capTests = $config['base-htaccess-on-these-capability-tests'];
|
||||
$log = [];
|
||||
$log[] = '#### Live tests of .htaccess capabilities / system configuration:';
|
||||
$log[] = 'Unless noted otherwise, the tests are run in *wp-content/webp-express/htaccess-capability-tester*. ';
|
||||
$log[] = 'WebPExpress currently treats the results as they neccessarily applies to all scopes (upload, themes, etc), ';
|
||||
$log[] = 'but note that a server might be configured to have mod_rewrite disallowed in some folders and allowed in others.';
|
||||
/*$log[] = 'Exactly what you can do in a *.htaccess* depends on the server setup. WebP Express ' .
|
||||
'makes some live tests to verify if a certain feature in fact works. This is done by creating ' .
|
||||
'test files (*.htaccess* files and php files) in a dir inside the content dir and running these. ' .
|
||||
'These test results are used when creating the rewrite rules. Here are the results:';*/
|
||||
|
||||
// $log[] = '';
|
||||
$log[] = '- .htaccess files enabled?: ' . self::trueFalseNullString(HTAccessCapabilityTestRunner::htaccessEnabled());
|
||||
$log[] = '- mod_rewrite working?: ' . self::trueFalseNullString(HTAccessCapabilityTestRunner::modRewriteWorking());
|
||||
$log[] = '- mod_headers loaded?: ' . self::trueFalseNullString(HTAccessCapabilityTestRunner::modHeadersLoaded());
|
||||
$log[] = '- mod_headers working (header set): ' . self::trueFalseNullString(HTAccessCapabilityTestRunner::modHeaderWorking());
|
||||
//$log[] = '- passing variables from *.htaccess* to PHP script through environment variable working?: ' . self::trueFalseNullString($capTests['passThroughEnvWorking']);
|
||||
$log[] = '- passing variables from *.htaccess* to PHP script through environment variable working?: ' . self::trueFalseNullString(HTAccessCapabilityTestRunner::passThroughEnvWorking());
|
||||
$log[] = '- Can run php test file in plugins/webp-express/wod/ ?: ' . self::trueFalseNullString(HTAccessCapabilityTestRunner::canRunTestScriptInWOD());
|
||||
$log[] = '- Can run php test file in plugins/webp-express/wod2/ ?: ' . self::trueFalseNullString(HTAccessCapabilityTestRunner::canRunTestScriptInWOD2());
|
||||
$log[] = '- Directives for granting access like its done in wod/.htaccess allowed?: ' . self::trueFalseNullString(HTAccessCapabilityTestRunner::grantAllAllowed());
|
||||
/*$log[] = '- pass variable from *.htaccess* to script through header working?: ' .
|
||||
self::trueFalseNullString($capTests['passThroughHeaderWorking']);*/
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function diagnoseFailedRewrite($config, $headers)
|
||||
{
|
||||
if (($config['destination-structure'] == 'image-roots') && (!PathHelper::isDocRootAvailableAndResolvable())) {
|
||||
$log[] = 'The problem is probably this combination:';
|
||||
if (!PathHelper::isDocRootAvailable()) {
|
||||
$log[] = '1. Your document root isn`t available';
|
||||
} else {
|
||||
$log[] = '1. Your document root isn`t resolvable for symlinks (it is probably subject to open_basedir restriction)';
|
||||
}
|
||||
$log[] = '2. Your document root is symlinked';
|
||||
$log[] = '3. The wordpress function that tells the path of the uploads folder returns the symlink resolved path';
|
||||
|
||||
$log[] = 'I cannot check if your document root is in fact symlinked (as document root isnt resolvable). ' .
|
||||
'But if it is, there you have it. The line beginning with "RewriteCond %{REQUEST_FILENAME}"" points to your resolved root, ' .
|
||||
'but it should point to your symlinked root. WebP Express cannot do that for you because it cannot discover what the symlink is. ' .
|
||||
'Try changing the line manually. When it works, you can move the rules outside the WebP Express block so they dont get ' .
|
||||
'overwritten. OR you can change your server configuration (document root / open_basedir restrictions)';
|
||||
}
|
||||
|
||||
//$log[] = '## Diagnosing';
|
||||
|
||||
//if (PlatformInfo::isNginx()) {
|
||||
if (strpos($headers['server'], 'nginx') === 0) {
|
||||
|
||||
// Nginx
|
||||
$log[] = 'Notice that you are on Nginx and the rules that WebP Express stores in the *.htaccess* files probably does not ' .
|
||||
'have any effect. ';
|
||||
$log[] = 'Please read the "I am on Nginx" section in the FAQ (https://wordpress.org/plugins/webp-express/)';
|
||||
$log[] = 'And did you remember to restart the nginx service after updating the configuration?';
|
||||
|
||||
$log[] = 'PS: If you cannot get the redirect to work, you can simply rely on Alter HTML as described in the FAQ.';
|
||||
return $log;
|
||||
}
|
||||
|
||||
$modRewriteWorking = HTAccessCapabilityTestRunner::modRewriteWorking();
|
||||
if ($modRewriteWorking !== null) {
|
||||
$log[] = 'Running a special designed capability test to test if rewriting works with *.htaccess* files';
|
||||
}
|
||||
if ($modRewriteWorking === true) {
|
||||
$log[] = 'Result: Yes, rewriting works.';
|
||||
$log[] = 'It seems something is wrong with the *.htaccess* rules then. You could try ' .
|
||||
'to change "Destination structure" - the rules there are quite different.';
|
||||
$log[] = 'It could also be that the server has cached the configuration a while. Some servers ' .
|
||||
'does that. In that case, simply give it a few minutes and try again.';
|
||||
} elseif ($modRewriteWorking === false) {
|
||||
$log[] = 'Result: No, rewriting does not seem to work within *.htaccess* rules.';
|
||||
if (PlatformInfo::definitelyNotGotModRewrite()) {
|
||||
$log[] = 'It actually seems "mod_write" is disabled on your server. ' .
|
||||
'**You must enable mod_rewrite on the server**';
|
||||
} elseif (PlatformInfo::definitelyGotApacheModule('mod_rewrite')) {
|
||||
$log[] = 'However, "mod_write" *is* enabled on your server. This seems to indicate that ' .
|
||||
'*.htaccess* files has been disabled for configuration on your server. ' .
|
||||
'In that case, you need to copy the WebP Express rules from the *.htaccess* files into your virtual host configuration files. ' .
|
||||
'(WebP Express generates multiple *.htaccess* files. Look in the upload folder, the wp-content folder, etc).';
|
||||
$log[] = 'It could however alse simply be that your server simply needs some time. ' .
|
||||
'Some servers caches the *.htaccess* rules for a bit. In that case, simply give it a few minutes and try again.';
|
||||
} else {
|
||||
$log[] = 'However, this could be due to your server being a bit slow on picking up changes in *.htaccess*.' .
|
||||
'Give it a few minutes and try again.';
|
||||
}
|
||||
} else {
|
||||
// The mod_rewrite test could not conclude anything.
|
||||
if (PlatformInfo::definitelyNotGotApacheModule('mod_rewrite')) {
|
||||
$log[] = 'It actually seems "mod_write" is disabled on your server. ' .
|
||||
'**You must enable mod_rewrite on the server**';
|
||||
} elseif (PlatformInfo::definitelyGotApacheModule('mod_rewrite')) {
|
||||
$log[] = '"mod_write" is enabled on your server, so rewriting ought to work. ' .
|
||||
'However, it could be that your server setup has disabled *.htaccess* files for configuration. ' .
|
||||
'In that case, you need to copy the WebP Express rules from the *.htaccess* files into your virtual host configuration files. ' .
|
||||
'(WebP Express generates multiple *.htaccess* files. Look in the upload folder, the wp-content folder, etc). ';
|
||||
} else {
|
||||
$log[] = 'It seems something is wrong with the *.htaccess* rules. ';
|
||||
$log[] = 'Or perhaps the server has cached the configuration a while. Some servers ' .
|
||||
'does that. In that case, simply give it a few minutes and try again.';
|
||||
}
|
||||
}
|
||||
$log[] = 'Note that if you cannot get redirection to work, you can switch to "CDN friendly" mode and ' .
|
||||
'rely on the "Alter HTML" functionality to point to the webp images. If you do a bulk conversion ' .
|
||||
'and make sure that "Convert upon upload" is activated, you should be all set. Alter HTML even handles ' .
|
||||
'inline css (unless you select "picture tag" syntax). It does however not handle images in external css or ' .
|
||||
'which is added dynamically with javascript.';
|
||||
|
||||
$log[] = '## Info for manually diagnosing';
|
||||
$log = array_merge($log, self::allInfo($config));
|
||||
return $log;
|
||||
}
|
||||
|
||||
public static function diagnoseWod403or500($config, $rootId, $responseCode)
|
||||
{
|
||||
$log = [];
|
||||
|
||||
$htaccessRules = SelfTestHelper::rulesInImageRoot($config, $rootId);
|
||||
$rulesText = implode('', $htaccessRules);
|
||||
$rulesPointsToWod = (strpos($rulesText, '/wod/') > 0);
|
||||
$rulesPointsToWod2 = (strpos($rulesText, '/wod2/') !== false);
|
||||
|
||||
$log[] = '';
|
||||
$log[] = '**diagnosing**';
|
||||
$canRunTestScriptInWod = HTAccessCapabilityTestRunner::canRunTestScriptInWOD();
|
||||
$canRunTestScriptInWod2 = HTAccessCapabilityTestRunner::canRunTestScriptInWOD2();
|
||||
$canRunInAnyWod = ($canRunTestScriptInWod || $canRunTestScriptInWod2);
|
||||
|
||||
$responsePingPhp = wp_remote_get(Paths::getPluginsUrl() . '/webp-express/wod/ping.php', ['timeout' => 7]);
|
||||
$pingPhpResponseCode = wp_remote_retrieve_response_code($responsePingPhp);
|
||||
|
||||
$responsePingText = wp_remote_get(Paths::getPluginsUrl() . '/webp-express/wod/ping.txt', ['timeout' => 7]);
|
||||
$pingTextResponseCode = wp_remote_retrieve_response_code($responsePingText);
|
||||
|
||||
if ($responseCode == 500) {
|
||||
$log[] = 'The response was a *500 Internal Server Error*. There can be different reasons for that. ' .
|
||||
'Lets dig a bit deeper...';
|
||||
}
|
||||
|
||||
$log[] = 'Examining where the *.htaccess* rules in the ' . $rootId . ' folder points to. ';
|
||||
|
||||
if ($rulesPointsToWod) {
|
||||
$log[] = 'They point to **wod**/webp-on-demand.php';
|
||||
} elseif ($rulesPointsToWod2) {
|
||||
$log[] = 'They point to **wod2**/webp-on-demand.php';
|
||||
} else {
|
||||
$log[] = '**There are no redirect rule to *webp-on-demand.php* in the .htaccess!**{: .warn}';
|
||||
$log[] = 'Here is the rules:';
|
||||
$log = array_merge($log, $htaccessRules);
|
||||
}
|
||||
|
||||
if ($rulesPointsToWod) {
|
||||
$log[] = 'Requesting simple test script "wod/ping.php"... ' .
|
||||
'Result: ' . ($pingPhpResponseCode == '200' ? 'ok' : 'failed (response code: ' . $pingPhpResponseCode . ')');
|
||||
//'Result: ' . ($canRunTestScriptInWod ? 'ok' : 'failed');
|
||||
|
||||
if ($canRunTestScriptInWod) {
|
||||
if ($responseCode == '500') {
|
||||
$log[] = '';
|
||||
$log[] = '**As the test script works, it would seem that the explanation for the 500 internal server ' .
|
||||
'error is that the PHP script (webp-on-demand.php) crashes. ' .
|
||||
'You can help me by enabling debugging and post the error on the support forum on Wordpress ' .
|
||||
'(https://wordpress.org/support/plugin/webp-express/), or create an issue on github ' .
|
||||
'(https://github.com/rosell-dk/webp-express/issues)**';
|
||||
$log[] = '';
|
||||
}
|
||||
} else {
|
||||
$log[] = 'Requesting simple test file "wod/ping.txt". ' .
|
||||
'Result: ' . ($pingTextResponseCode == '200' ? 'ok' : 'failed (response code: ' . $pingTextResponseCode . ')');
|
||||
|
||||
if ($canRunTestScriptInWod2) {
|
||||
if ($responseCode == 500) {
|
||||
if ($pingTextResponseCode == '500') {
|
||||
$log[] = 'The problem appears to be that the *.htaccess* placed in *plugins/webp-express/wod/.htaccess*' .
|
||||
' contains auth directives ("Allow" and "Request") and your server is set up to go fatal about it. ' .
|
||||
'Luckily, it seems that running scripts in the "wod2" folder works. ' .
|
||||
'**What you need to do is simply to click the "Save settings and force new .htacess rules"' .
|
||||
' button. WebP Express wil then change the .htaccess rules to point to the "wod2" folder**';
|
||||
} else {
|
||||
$log[] = 'The problem appears to be running PHP scripts in the "wod". ' .
|
||||
'Luckily, it seems that running scripts in the "wod2" folder works ' .
|
||||
'(it has probably something to do with the *.htaccess* file placed in "wod"). ' .
|
||||
'**What you need to do is simply to click the "Save settings and force new .htacess rules"' .
|
||||
' button. WebP Express wil then change the .htaccess rules to point to the "wod2" folder**';
|
||||
}
|
||||
} elseif ($responseCode == 403) {
|
||||
$log[] = 'The problem appears to be running PHP scripts in the "wod". ' .
|
||||
'Luckily, it seems that running scripts in the "wod2" folder works ' .
|
||||
'(it could perhaps have something to do with the *.htaccess* file placed in "wod", ' .
|
||||
'although it ought not result in a 403). **What you need to do is simply to click the "Save settings and force new .htacess rules"' .
|
||||
' button. WebP Express wil then change the .htaccess rules to point to the "wod2" folder**';
|
||||
}
|
||||
|
||||
return $log;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$log[] = 'Requesting simple test script "wod2/ping.php". Result: ' . ($canRunTestScriptInWod2 ? 'ok' : 'failed');
|
||||
$responsePingText2 = wp_remote_get(Paths::getPluginsUrl() . '/webp-express/wod2/ping.txt', ['timeout' => 7]);
|
||||
$pingTextResponseCode2 = wp_remote_retrieve_response_code($responsePingText2);
|
||||
$log[] = 'Requesting simple test file "wod2/ping.txt". ' .
|
||||
'Result: ' . ($pingTextResponseCode == '200' ? 'ok' : 'failed (response code: ' . $pingTextResponseCode2 . ')');
|
||||
|
||||
if ($rulesPointsToWod2) {
|
||||
if ($canRunTestScriptInWod2) {
|
||||
if ($responseCode == '500') {
|
||||
$log[] = '';
|
||||
$log[] = '**As the test script works, it would seem that the explanation for the 500 internal server ' .
|
||||
'error is that the PHP script (webp-on-demand.php) crashes. ' .
|
||||
'You can help me by enabling debugging and post the error on the support forum on Wordpress ' .
|
||||
'(https://wordpress.org/support/plugin/webp-express/), or create an issue on github ' .
|
||||
'(https://github.com/rosell-dk/webp-express/issues)**';
|
||||
$log[] = '';
|
||||
}
|
||||
} else {
|
||||
if ($canRunTestScriptInWod) {
|
||||
$log[] = '';
|
||||
$log[] = 'The problem appears to be running PHP scripts in the "wod2" folder. ' .
|
||||
'Luckily, it seems that running scripts in the "wod" folder works ' .
|
||||
'**What you need to do is simply to click the "Save settings and force new .htacess rules"' .
|
||||
' button. WebP Express wil then change the .htaccess rules to point to the "wod" folder**';
|
||||
$log[] = '';
|
||||
} else {
|
||||
if ($responseCode == 500) {
|
||||
|
||||
if ($pingTextResponseCode2 == '500') {
|
||||
$log[] = 'All our requests results in 500 Internal Error. Even ' .
|
||||
'the request to plugins/webp-express/wod2/ping.txt. ' .
|
||||
'Surprising!';
|
||||
} else {
|
||||
$log[] = 'The internal server error happens for php files, but not txt files. ' .
|
||||
'It could be the result of a restrictive server configuration or the works of a security plugin. ' .
|
||||
'Try to examine the .htaccess file in the plugins folder and its parent folders. ' .
|
||||
'Or try to look in the httpd.conf. Look for the "AllowOverride" and the "AllowOverrideList" directives. ';
|
||||
}
|
||||
|
||||
//$log[] = 'We get *500 Internal Server Error*';
|
||||
/*
|
||||
It can for example be that the *.htaccess* ' .
|
||||
'in the ' . $rootId . ' folder (or a parent folder) contains directives that the server either ' .
|
||||
'doesnt support or has not allowed (using AllowOverride in ie httpd.conf). It could also be that the redirect succeded, ' .
|
||||
'but the *.htaccess* in the folder of the script (or a parent folder) results in such problems. Also, ' .
|
||||
'it could be that the script (webp-on-demand.php) for some reason fails.';
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
return $log;
|
||||
}
|
||||
}
|
||||
115
lib/classes/SelfTestRedirectAbstract.php
Normal file
115
lib/classes/SelfTestRedirectAbstract.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
abstract class SelfTestRedirectAbstract
|
||||
{
|
||||
protected $config;
|
||||
|
||||
public function __construct($config) {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run test for either jpeg or png
|
||||
*
|
||||
* @param string $rootId (ie "uploads" or "themes")
|
||||
* @param string $imageType ("jpeg" or "png")
|
||||
* @return array [$success, $result, $createdTestFiles]
|
||||
*/
|
||||
abstract protected function runTestForImageType($rootId, $imageType);
|
||||
|
||||
abstract protected function getSuccessMessage();
|
||||
|
||||
private function doRunTestForRoot($rootId)
|
||||
{
|
||||
// return [true, ['hello'], false];
|
||||
// return [false, SelfTestHelper::diagnoseFailedRewrite($this->config, $headers), false];
|
||||
|
||||
$result = [];
|
||||
|
||||
//$result[] = '*hello* with *you* and **you**. ok! FAILED';
|
||||
$result[] = '## ' . $rootId;
|
||||
//$result[] = 'This test examines image responses "from the outside".';
|
||||
|
||||
$createdTestFiles = false;
|
||||
|
||||
if ($this->config['image-types'] & 1) {
|
||||
list($success, $subResult, $createdTestFiles) = $this->runTestForImageType($rootId, 'jpeg');
|
||||
$result = array_merge($result, $subResult);
|
||||
|
||||
if ($success) {
|
||||
if ($this->config['image-types'] & 2) {
|
||||
$result[] = '### Performing same tests for PNG';
|
||||
list($success, $subResult, $createdTestFiles2) = $this->runTestForImageType($rootId, 'png');
|
||||
$createdTestFiles = $createdTestFiles || $createdTestFiles2;
|
||||
if ($success) {
|
||||
//$result[count($result) - 1] .= '. **ok**{: .ok}';
|
||||
$result[] .= 'All tests passed for PNG as well.';
|
||||
$result[] = '(I shall spare you for the report, which is almost identical to the one above)';
|
||||
} else {
|
||||
$result = array_merge($result, $subResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
list($success, $subResult, $createdTestFiles) = $this->runTestForImageType($rootId, 'png');
|
||||
$result = array_merge($result, $subResult);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$result[] = '### Results for ' . strtoupper($rootId);
|
||||
|
||||
$result[] = $this->getSuccessMessage();
|
||||
}
|
||||
return [true, $result, $createdTestFiles];
|
||||
}
|
||||
|
||||
private function runTestForRoot($rootId)
|
||||
{
|
||||
// TODO: move that method to here
|
||||
SelfTestHelper::cleanUpTestImages($rootId, $this->config);
|
||||
|
||||
// Run the actual test
|
||||
list($success, $result, $createdTestFiles) = $this->doRunTestForRoot($rootId);
|
||||
|
||||
// Clean up test images again. We are very tidy around here
|
||||
if ($createdTestFiles) {
|
||||
$result[] = 'Deleting test images';
|
||||
SelfTestHelper::cleanUpTestImages($rootId, $this->config);
|
||||
}
|
||||
|
||||
return [$success, $result];
|
||||
}
|
||||
|
||||
abstract protected function startupTests();
|
||||
|
||||
protected function startTest()
|
||||
{
|
||||
|
||||
list($success, $result) = $this->startupTests();
|
||||
|
||||
if (!$success) {
|
||||
return [false, $result];
|
||||
}
|
||||
|
||||
if (!file_exists(Paths::getConfigFileName())) {
|
||||
$result[] = 'Hold on. You need to save options before you can run this test. There is no config file yet.';
|
||||
return [true, $result];
|
||||
}
|
||||
|
||||
if ($this->config['image-types'] == 0) {
|
||||
$result[] = 'No image types have been activated, nothing to test';
|
||||
return [true, $result];
|
||||
}
|
||||
|
||||
foreach ($this->config['scope'] as $rootId) {
|
||||
list($success, $subResult) = $this->runTestForRoot($rootId);
|
||||
$result = array_merge($result, $subResult);
|
||||
}
|
||||
//list($success, $result) = self::runTestForRoot('uploads', $this->config);
|
||||
|
||||
return [$success, $result];
|
||||
}
|
||||
|
||||
}
|
||||
239
lib/classes/SelfTestRedirectToConverter.php
Normal file
239
lib/classes/SelfTestRedirectToConverter.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class SelfTestRedirectToConverter extends SelfTestRedirectAbstract
|
||||
{
|
||||
|
||||
/**
|
||||
* Run test for either jpeg or png
|
||||
*
|
||||
* @param string $rootId (ie "uploads" or "themes")
|
||||
* @param string $imageType ("jpeg" or "png")
|
||||
* @return array [$success, $log, $createdTestFiles]
|
||||
*/
|
||||
protected function runTestForImageType($rootId, $imageType)
|
||||
{
|
||||
$log = [];
|
||||
$createdTestFiles = false;
|
||||
$noWarningsYet = true;
|
||||
|
||||
$htaccessFile = Paths::getAbsDirById($rootId) . '/.htaccess';
|
||||
if (!FileHelper::fileExists($htaccessFile)) {
|
||||
$log[] = '**Warning: There is no .htaccess file in the ' . $rootId . ' folder!**{: .warn} (did you save settings yet?)';
|
||||
$noWarningsYet = false;
|
||||
} elseif (!HTAccess::haveWeRulesInThisHTAccess($htaccessFile)) {
|
||||
$log[] = '**Warning: There are no WebP Express rules in the .htaccess file in the ' . $rootId . ' folder!**{: .warn}';
|
||||
$noWarningsYet = false;
|
||||
}
|
||||
|
||||
// Copy test image (jpeg)
|
||||
list($subResult, $success, $sourceFileName) = SelfTestHelper::copyTestImageToRoot($rootId, $imageType);
|
||||
$log = array_merge($log, $subResult);
|
||||
if (!$success) {
|
||||
$log[] = 'The test cannot be completed';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
$createdTestFiles = true;
|
||||
|
||||
$requestUrl = Paths::getUrlById($rootId) . '/webp-express-test-images/' . $sourceFileName;
|
||||
|
||||
$log[] = '### Lets check that browsers supporting webp gets a freshly converted WEBP ' .
|
||||
'when the ' . $imageType . ' is requested';
|
||||
$log[] = 'Making a HTTP request for the test image (pretending to be a client that supports webp, by setting the "Accept" header to "image/webp")';
|
||||
$requestArgs = [
|
||||
'headers' => [
|
||||
'ACCEPT' => 'image/webp'
|
||||
],
|
||||
];
|
||||
list($success, $remoteGetLog, $results) = SelfTestHelper::remoteGet($requestUrl, $requestArgs);
|
||||
$headers = $results[count($results)-1]['headers'];
|
||||
$log = array_merge($log, $remoteGetLog);
|
||||
|
||||
if (!$success) {
|
||||
//$log[count($log) - 1] .= '. FAILED';
|
||||
$log[] = 'The request FAILED';
|
||||
//$log = array_merge($log, $remoteGetLog);
|
||||
|
||||
if (isset($results[0]['response']['code'])) {
|
||||
$responseCode = $results[0]['response']['code'];
|
||||
if (($responseCode == 500) || ($responseCode == 403)) {
|
||||
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseWod403or500($this->config, $rootId, $responseCode));
|
||||
|
||||
//$log[] = 'or that there is an .htaccess file in the ';
|
||||
}
|
||||
// $log[] = print_r($results[0]['response']['code'], true);
|
||||
}
|
||||
//$log[] = 'The test cannot be completed';
|
||||
//$log[count($log) - 1] .= '. FAILED';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
//$log[count($log) - 1] .= '. ok!';
|
||||
//$log[] = '*' . $requestUrl . '*';
|
||||
|
||||
//$log = array_merge($log, SelfTestHelper::printHeaders($headers));
|
||||
|
||||
if (!isset($headers['content-type'])) {
|
||||
$log[] = 'Bummer. There is no "content-type" response header. The test FAILED';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($headers['content-type'] == 'image/' . $imageType) {
|
||||
$log[] = 'Bummer. As the "content-type" header reveals, we got the ' . $imageType . '.';
|
||||
$log[] = 'The test **failed**{: .error}.';
|
||||
$log[] = 'Now, what went wrong?';
|
||||
|
||||
if (isset($headers['x-webp-convert-log'])) {
|
||||
//$log[] = 'Inspect the "x-webp-convert-log" headers above, and you ' .
|
||||
// 'should have your answer (it is probably because you do not have any conversion methods working).';
|
||||
if (SelfTestHelper::hasHeaderContaining($headers, 'x-webp-convert-log', 'Performing fail action: original')) {
|
||||
$log[] = 'The answer lies in the "x-convert-log" response headers: ' .
|
||||
'**The conversion failed**{: .error}. ';
|
||||
}
|
||||
} else {
|
||||
$log[] = 'Well, there is indication that the redirection isnt working. ' .
|
||||
'The PHP script should set "x-webp-convert-log" response headers, but there are none. ';
|
||||
'While these headers could have been eaten in a Cloudflare-like setup, the problem is ';
|
||||
'probably that the redirection simply failed';
|
||||
|
||||
$log[] = '### Diagnosing redirection problems';
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseFailedRewrite($this->config, $headers));
|
||||
}
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($headers['content-type'] != 'image/webp') {
|
||||
$log[] = 'However. As the "content-type" header reveals, we did not get a webp' .
|
||||
'Surprisingly we got: "' . $headers['content-type'] . '"';
|
||||
$log[] = 'The test FAILED.';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if (isset($headers['x-webp-convert-log'])) {
|
||||
$log[] = 'Alrighty. We got a webp, and we got it from the PHP script. **Great!**{: .ok}';
|
||||
} else {
|
||||
if (count($results) > 1) {
|
||||
if (isset($results[0]['headers']['x-webp-convert-log'])) {
|
||||
$log[] = '**Great!**{: .ok}. The PHP script created a webp and redirected the image request ' .
|
||||
'back to itself. A refresh, if you wish. The refresh got us the webp (relying on there being ' .
|
||||
'a rule which redirect images to existing converted images for webp-enabled browsers - which there is!). ' .
|
||||
(SelfTestHelper::hasVaryAcceptHeader($headers) ? 'And we got the Vary:Accept header set too. **Super!**{: .ok}!' : '');
|
||||
}
|
||||
} else {
|
||||
$log[] = 'We got a webp. However, it seems we did not get it from the PHP script.';
|
||||
|
||||
}
|
||||
|
||||
//$log[] = print_r($return, true);
|
||||
//error_log(print_r($return, true));
|
||||
}
|
||||
|
||||
if (!SelfTestHelper::hasVaryAcceptHeader($headers)) {
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseNoVaryHeader($rootId, 'webp-on-demand'));
|
||||
$noWarningsYet = false;
|
||||
}
|
||||
if (!SelfTestHelper::hasCacheControlOrExpiresHeader($headers)) {
|
||||
$log[] = '**Notice: No cache-control or expires header has been set. ' .
|
||||
'It is recommended to do so. Set it nice and big once you are sure the webps have a good quality/compression compromise.**{: .warn}';
|
||||
}
|
||||
$log[] = '';
|
||||
|
||||
|
||||
// Check browsers NOT supporting webp
|
||||
// -----------------------------------
|
||||
$log[] = '### Now lets check that browsers *not* supporting webp gets the ' . strtoupper($imageType);
|
||||
$log[] = 'Making a HTTP request for the test image (without setting the "Accept" header)';
|
||||
list($success, $remoteGetLog, $results) = SelfTestHelper::remoteGet($requestUrl);
|
||||
$headers = $results[count($results)-1]['headers'];
|
||||
$log = array_merge($log, $remoteGetLog);
|
||||
|
||||
if (!$success) {
|
||||
$log[] = 'The request FAILED';
|
||||
$log[] = 'The test cannot be completed';
|
||||
//$log[count($log) - 1] .= '. FAILED';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
//$log[count($log) - 1] .= '. ok!';
|
||||
//$log[] = '*' . $requestUrl . '*';
|
||||
|
||||
//$log = array_merge($log, SelfTestHelper::printHeaders($headers));
|
||||
|
||||
if (!isset($headers['content-type'])) {
|
||||
$log[] = 'Bummer. There is no "content-type" response header. The test FAILED';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($headers['content-type'] == 'image/webp') {
|
||||
$log[] = '**Bummer**{: .error}. As the "content-type" header reveals, we got the webp. ' .
|
||||
'So even browsers not supporting webp gets webp. Not good!';
|
||||
$log[] = 'The test FAILED.';
|
||||
|
||||
$log[] = '### What to do now?';
|
||||
// TODO: We could examine the headers for common CDN responses
|
||||
|
||||
$log[] = 'First, examine the response headers above. Is there any indication that ' .
|
||||
'the image is returned from a CDN cache? ' .
|
||||
$log[] = 'If there is: Check out the ' .
|
||||
'*How do I configure my CDN in “Varied image responses” operation mode?* section in the FAQ ' .
|
||||
'(https://wordpress.org/plugins/webp-express/)';
|
||||
|
||||
if (PlatformInfo::isApache()) {
|
||||
$log[] = 'If not: please report this in the forum, as it seems the .htaccess rules ';
|
||||
$log[] = 'just arent working on your system.';
|
||||
} elseif (PlatformInfo::isNginx()) {
|
||||
$log[] = 'Also, as you are on Nginx, check out the ' .
|
||||
' "I am on Nginx" section in the FAQ (https://wordpress.org/plugins/webp-express/)';
|
||||
} else {
|
||||
$log[] = 'If not: please report this in the forum, as it seems that there is something ' .
|
||||
'in the *.htaccess* rules generated by WebP Express that are not working.';
|
||||
}
|
||||
|
||||
$log[] = '### System info (for manual diagnosing):';
|
||||
$log = array_merge($log, SelfTestHelper::allInfo($this->config));
|
||||
|
||||
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($headers['content-type'] != 'image/' . $imageType) {
|
||||
$log[] = 'Bummer. As the "content-type" header reveals, we did not get the ' . $imageType .
|
||||
'Surprisingly we got: "' . $headers['content-type'] . '"';
|
||||
$log[] = 'The test FAILED.';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
$log[] = 'Alrighty. We got the ' . $imageType . '. **Great!**{: .ok}.';
|
||||
|
||||
if (!SelfTestHelper::hasVaryAcceptHeader($headers)) {
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseNoVaryHeader($rootId, 'webp-on-demand'));
|
||||
$noWarningsYet = false;
|
||||
}
|
||||
|
||||
return [$noWarningsYet, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
protected function getSuccessMessage()
|
||||
{
|
||||
return 'Everything **seems to work**{: .ok} as it should. ' .
|
||||
'However, a check is on the TODO: ' .
|
||||
'TODO: Check that disabled image types does not get converted. ';
|
||||
}
|
||||
|
||||
public function startupTests()
|
||||
{
|
||||
$log[] = '# Testing redirection to converter';
|
||||
if (!$this->config['enable-redirection-to-converter']) {
|
||||
$log[] = 'Turned off, nothing to test (if you just turned it on without saving, remember: this is a live test so you need to save settings)';
|
||||
return [false, $log];
|
||||
}
|
||||
return [true, $log];
|
||||
}
|
||||
|
||||
public static function runTest()
|
||||
{
|
||||
$config = Config::loadConfigAndFix(false);
|
||||
$me = new SelfTestRedirectToConverter($config);
|
||||
return $me->startTest();
|
||||
}
|
||||
|
||||
}
|
||||
250
lib/classes/SelfTestRedirectToExisting.php
Normal file
250
lib/classes/SelfTestRedirectToExisting.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class SelfTestRedirectToExisting extends SelfTestRedirectAbstract
|
||||
{
|
||||
/**
|
||||
* Run test for either jpeg or png
|
||||
*
|
||||
* @param string $rootId (ie "uploads" or "themes")
|
||||
* @param string $imageType ("jpeg" or "png")
|
||||
* @return array [$success, $log, $createdTestFiles]
|
||||
*/
|
||||
protected function runTestForImageType($rootId, $imageType)
|
||||
{
|
||||
$log = [];
|
||||
$createdTestFiles = false;
|
||||
$noWarningsYet = true;
|
||||
|
||||
$log[] = '### Copying files for testing';
|
||||
|
||||
// Copy test image
|
||||
list($subResult, $success, $sourceFileName) = SelfTestHelper::copyTestImageToRoot($rootId, $imageType);
|
||||
$log = array_merge($log, $subResult);
|
||||
if (!$success) {
|
||||
$log[] = 'The test cannot be completed';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
$createdTestFiles = true;
|
||||
|
||||
$log[] = '';
|
||||
|
||||
// Copy dummy webp
|
||||
list($subResult, $success, $destinationFile) = SelfTestHelper::copyDummyWebPToCacheFolder(
|
||||
$rootId,
|
||||
$this->config['destination-folder'],
|
||||
$this->config['destination-extension'],
|
||||
$this->config['destination-structure'],
|
||||
$sourceFileName,
|
||||
$imageType
|
||||
);
|
||||
$log = array_merge($log, $subResult);
|
||||
if (!$success) {
|
||||
$log[] = 'The test cannot be completed';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
$requestUrl = Paths::getUrlById($rootId) . '/webp-express-test-images/' . $sourceFileName;
|
||||
$log[] = '### Lets check that browsers supporting webp gets the WEBP when the ' . strtoupper($imageType) . ' is requested';
|
||||
$log[] = 'Making a HTTP request for the test image (pretending to be a client that supports webp, by setting the "Accept" header to "image/webp")';
|
||||
$requestArgs = [
|
||||
'headers' => [
|
||||
'ACCEPT' => 'image/webp'
|
||||
]
|
||||
];
|
||||
|
||||
list($success, $remoteGetLog, $results) = SelfTestHelper::remoteGet($requestUrl, $requestArgs);
|
||||
$headers = $results[count($results)-1]['headers'];
|
||||
$log = array_merge($log, $remoteGetLog);
|
||||
|
||||
if (!$success) {
|
||||
$log[] = 'The test cannot be completed, as the HTTP request failed. This does not neccesarily mean that the redirections ' .
|
||||
"aren't" . ' working, but it means you will have to check it manually. Check out the FAQ on how to do this. ' .
|
||||
'You might also want to check out why a simple HTTP request could not be issued. WebP Express uses such requests ' .
|
||||
'for detecting system capabilities, which are used when generating .htaccess files. These tests are not essential, but ' .
|
||||
'it would be best to have them working. I can inform that the Wordpress function *wp_remote_get* was used for the HTTP request ' .
|
||||
'and the URL was: ' . $requestUrl;
|
||||
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
//$log[count($log) - 1] .= '. ok!';
|
||||
//$log[] = '*' . $requestUrl . '*';
|
||||
|
||||
//$log = array_merge($log, SelfTestHelper::printHeaders($headers));
|
||||
|
||||
if (!isset($headers['content-type'])) {
|
||||
$log[] = 'Bummer. There is no "content-type" response header. The test FAILED';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
if ($headers['content-type'] != 'image/webp') {
|
||||
|
||||
if ($headers['content-type'] == 'image/' . $imageType) {
|
||||
$log[] = 'Bummer. As the "content-type" header reveals, we got the ' . $imageType . '. ';
|
||||
} else {
|
||||
$log[] = 'Bummer. As the "content-type" header reveals, we did not get a webp' .
|
||||
'Surprisingly we got: "' . $headers['content-type'] . '"';
|
||||
}
|
||||
|
||||
if (isset($headers['content-length'])) {
|
||||
if ($headers['content-length'] == '6964') {
|
||||
$log[] = 'However, the content-length reveals that we actually GOT the webp ' .
|
||||
'(we know that the file we put is exactly 6964 bytes). ' .
|
||||
'So it is "just" the content-type header that was not set correctly.';
|
||||
|
||||
if (PlatformInfo::isNginx()) {
|
||||
$log[] = 'As you are on Nginx, you probably need to add the following line ' .
|
||||
'in your *mime.types* configuration file: ';
|
||||
$log[] = '```image/webp webp;```';
|
||||
} else {
|
||||
$log[] = 'Perhaps you dont have *mod_mime* installed, or the following lines are not in a *.htaccess* ' .
|
||||
'in the folder containing the webp (or a parent):';
|
||||
$log[] = "```<IfModule mod_mime.c>\n AddType image/webp .webp\n</IfModule>```";
|
||||
|
||||
$log[] = '### .htaccess status';
|
||||
$log = array_merge($log, SelfTestHelper::htaccessInfo($this->config, true));
|
||||
}
|
||||
|
||||
$log[] = 'The test **FAILED**{: .error}.';
|
||||
} else {
|
||||
$log[] = 'Additionally, the content-length reveals that we did not get the webp ' .
|
||||
'(we know that the file we put is exactly 6964 bytes). ' .
|
||||
'So we can conclude that the rewrite did not happen';
|
||||
$log[] = 'The test **FAILED**{: .error}.';
|
||||
$log[] = '#### Diagnosing rewrites';
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseFailedRewrite($this->config, $headers));
|
||||
}
|
||||
} else {
|
||||
$log[] = 'In addition, we did not get a *content-length* header either.' .
|
||||
$log[] = 'It seems we can conclude that the rewrite did not happen.';
|
||||
$log[] = 'The test **FAILED**{: .error}.';
|
||||
$log[] = '#### Diagnosing rewrites';
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseFailedRewrite($this->config, $headers));
|
||||
}
|
||||
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if (isset($headers['x-webp-convert-log'])) {
|
||||
$log[] = 'Bummer. Although we did get a webp, we did not get it as a result of a direct ' .
|
||||
'redirection. This webp was returned by the PHP script. Although this works, it takes more ' .
|
||||
'resources to ignite the PHP engine for each image request than redirecting directly to the image.';
|
||||
$log[] = 'The test FAILED.';
|
||||
|
||||
$log[] = 'It seems something went wrong with the redirection.';
|
||||
$log[] = '#### Diagnosing redirects';
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseFailedRewrite($this->config, $headers));
|
||||
|
||||
return [false, $log, $createdTestFiles];
|
||||
} else {
|
||||
$log[] = 'Alrighty. We got a webp. Just what we wanted. **Great!**{: .ok}';
|
||||
}
|
||||
|
||||
if (!SelfTestHelper::hasVaryAcceptHeader($headers)) {
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseNoVaryHeader($rootId, 'existing'));
|
||||
$noWarningsYet = false;
|
||||
}
|
||||
|
||||
if (!SelfTestHelper::hasCacheControlOrExpiresHeader($headers)) {
|
||||
$log[] = '**Notice: No cache-control or expires header has been set. ' .
|
||||
'It is recommended to do so. Set it nice and big once you are sure the webps have a good quality/compression compromise.**{: .warn}';
|
||||
}
|
||||
$log[] = '';
|
||||
|
||||
|
||||
// Check browsers NOT supporting webp
|
||||
// -----------------------------------
|
||||
$log[] = '### Now lets check that browsers *not* supporting webp gets the ' . strtoupper($imageType);
|
||||
$log[] = 'Making a HTTP request for the test image (without setting the "Accept" header)';
|
||||
list($success, $remoteGetLog, $results) = SelfTestHelper::remoteGet($requestUrl);
|
||||
$headers = $results[count($results)-1]['headers'];
|
||||
$log = array_merge($log, $remoteGetLog);
|
||||
|
||||
if (!$success) {
|
||||
$log[] = 'The request FAILED';
|
||||
$log[] = 'The test cannot be completed';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
//$log[count($log) - 1] .= '. ok!';
|
||||
//$log[] = '*' . $requestUrl . '*';
|
||||
|
||||
//$log = array_merge($log, SelfTestHelper::printHeaders($headers));
|
||||
|
||||
if (!isset($headers['content-type'])) {
|
||||
$log[] = 'Bummer. There is no "content-type" response header. The test FAILED';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($headers['content-type'] == 'image/webp') {
|
||||
$log[] = '**Bummer**{: .error}. As the "content-type" header reveals, we got the webp. ' .
|
||||
'So even browsers not supporting webp gets webp. Not good!';
|
||||
$log[] = 'The test FAILED.';
|
||||
|
||||
$log[] = '#### What to do now?';
|
||||
// TODO: We could examine the headers for common CDN responses
|
||||
|
||||
$log[] = 'First, examine the response headers above. Is there any indication that ' .
|
||||
'the image is returned from a CDN cache? ' .
|
||||
$log[] = 'If there is: Check out the ' .
|
||||
'*How do I configure my CDN in “Varied image responses” operation mode?* section in the FAQ ' .
|
||||
'(https://wordpress.org/plugins/webp-express/)';
|
||||
|
||||
if (PlatformInfo::isApache()) {
|
||||
$log[] = 'If not: please report this in the forum, as it seems the .htaccess rules ';
|
||||
$log[] = 'just arent working on your system.';
|
||||
} elseif (PlatformInfo::isNginx()) {
|
||||
$log[] = 'Also, as you are on Nginx, check out the ' .
|
||||
' "I am on Nginx" section in the FAQ (https://wordpress.org/plugins/webp-express/)';
|
||||
} else {
|
||||
$log[] = 'If not: please report this in the forum, as it seems that there is something ' .
|
||||
'in the *.htaccess* rules generated by WebP Express that are not working.';
|
||||
}
|
||||
|
||||
$log[] = '### System info (for manual diagnosing):';
|
||||
$log = array_merge($log, SelfTestHelper::allInfo($this->config));
|
||||
|
||||
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($headers['content-type'] != 'image/' . $imageType) {
|
||||
$log[] = 'Bummer. As the "content-type" header reveals, we did not get the ' . $imageType .
|
||||
'Surprisingly we got: "' . $headers['content-type'] . '"';
|
||||
$log[] = 'The test FAILED.';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
$log[] = 'Alrighty. We got the ' . $imageType . '. **Great!**{: .ok}.';
|
||||
|
||||
if (!SelfTestHelper::hasVaryAcceptHeader($headers)) {
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseNoVaryHeader($rootId, 'existing'));
|
||||
$noWarningsYet = false;
|
||||
}
|
||||
|
||||
return [$noWarningsYet, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
protected function getSuccessMessage()
|
||||
{
|
||||
return 'Everything **seems to work**{: .ok} as it should. ' .
|
||||
'However, a couple of things were not tested (it is on the TODO). ' .
|
||||
'TODO 1: If one image type is disabled, check that it does not redirect to webp (unless redirection to converter is set up). ' .
|
||||
'TODO 2: Test that redirection to webp only is triggered when the webp exists. ';
|
||||
}
|
||||
|
||||
public function startupTests()
|
||||
{
|
||||
$log[] = '# Testing redirection to existing webp';
|
||||
if (!$this->config['redirect-to-existing-in-htaccess']) {
|
||||
$log[] = 'Turned off, nothing to test (if you just turned it on without saving, remember: this is a live test so you need to save settings)';
|
||||
return [false, $log];
|
||||
}
|
||||
return [true, $log];
|
||||
}
|
||||
|
||||
public static function runTest()
|
||||
{
|
||||
$config = Config::loadConfigAndFix(false);
|
||||
$me = new SelfTestRedirectToExisting($config);
|
||||
return $me->startTest();
|
||||
}
|
||||
}
|
||||
257
lib/classes/SelfTestRedirectToWebPRealizer.php
Normal file
257
lib/classes/SelfTestRedirectToWebPRealizer.php
Normal file
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
use \WebPExpress\Option;
|
||||
|
||||
|
||||
class SelfTestRedirectToWebPRealizer extends SelfTestRedirectAbstract
|
||||
{
|
||||
|
||||
/**
|
||||
* Run test for either jpeg or png
|
||||
*
|
||||
* @param string $rootId (ie "uploads" or "themes")
|
||||
* @param string $imageType ("jpeg" or "png")
|
||||
* @return array [$success, $log, $createdTestFiles]
|
||||
*/
|
||||
protected function runTestForImageType($rootId, $imageType)
|
||||
{
|
||||
$log = [];
|
||||
$createdTestFiles = false;
|
||||
$noWarningsYet = true;
|
||||
|
||||
// Copy test image
|
||||
list($subResult, $success, $sourceFileName) = SelfTestHelper::copyTestImageToRoot($rootId, $imageType);
|
||||
$log = array_merge($log, $subResult);
|
||||
if (!$success) {
|
||||
$log[] = 'The test cannot be completed';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
$createdTestFiles = true;
|
||||
|
||||
//$requestUrl = Paths::getUploadUrl() . '/' . $sourceFileName;
|
||||
|
||||
// Hacky, I know.
|
||||
// AlterHtmlHelper was not meant to be used like this, but it is the only place where we currently
|
||||
// have logic for finding destination url from source url.
|
||||
|
||||
//$sourceUrl = Paths::getUploadUrl() . '/' . $sourceFileName;
|
||||
$sourceUrl = Paths::getUrlById($rootId) . '/webp-express-test-images/' . $sourceFileName;
|
||||
|
||||
AlterHtmlHelper::$options = json_decode(Option::getOption('webp-express-alter-html-options', null), true);
|
||||
AlterHtmlHelper::$options['only-for-webps-that-exists'] = false;
|
||||
|
||||
// TODO: Check that AlterHtmlHelper::$options['scope'] is not empty
|
||||
// - it has been seen to happen
|
||||
|
||||
$requestUrl = AlterHtmlHelper::getWebPUrlInImageRoot(
|
||||
$sourceUrl,
|
||||
$rootId,
|
||||
Paths::getUrlById($rootId),
|
||||
Paths::getAbsDirById($rootId)
|
||||
);
|
||||
|
||||
if ($requestUrl === false) {
|
||||
// PS: this has happened due to AlterHtmlHelper::$options['scope'] being empty...
|
||||
|
||||
$log[] = 'Hm, strange. The source URL does not seem to be in the base root';
|
||||
$log[] = 'Source URL:' . $sourceUrl;
|
||||
//$log[] = 'Root ID:' . $rootId;
|
||||
$log[] = 'Root Url:' . Paths::getUrlById($rootId);
|
||||
$log[] = 'Request Url:' . $requestUrl;
|
||||
$log[] = 'parsed url:' . print_r(parse_url($sourceUrl), true);
|
||||
$log[] = 'parsed url:' . print_r(parse_url(Paths::getUrlById($rootId)), true);
|
||||
$log[] = 'scope:' . print_r(AlterHtmlHelper::$options['scope'], true);
|
||||
$log[] = 'cached options:' . print_r(AlterHtmlHelper::$options, true);
|
||||
$log[] = 'cached options: ' . print_r(Option::getOption('webp-express-alter-html-options', 'not there!'), true);
|
||||
}
|
||||
|
||||
|
||||
$log[] = '### Lets check that browsers supporting webp gets a freshly converted WEBP ' .
|
||||
'when a non-existing WEBP is requested, which has a corresponding source';
|
||||
$log[] = 'Making a HTTP request for the test image (pretending to be a client that supports webp, by setting the "Accept" header to "image/webp")';
|
||||
$requestArgs = [
|
||||
'headers' => [
|
||||
'ACCEPT' => 'image/webp'
|
||||
]
|
||||
];
|
||||
list($success, $remoteGetLog, $results) = SelfTestHelper::remoteGet($requestUrl, $requestArgs);
|
||||
$headers = $results[count($results)-1]['headers'];
|
||||
$log = array_merge($log, $remoteGetLog);
|
||||
|
||||
|
||||
if (!$success) {
|
||||
//$log[count($log) - 1] .= '. FAILED';
|
||||
//$log[] = '*' . $requestUrl . '*';
|
||||
|
||||
$log[] = 'The test **failed**{: .error}';
|
||||
|
||||
if (isset($results[0]['response']['code'])) {
|
||||
$responseCode = $results[0]['response']['code'];
|
||||
if (($responseCode == 500) || ($responseCode == 403)) {
|
||||
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseWod403or500($this->config, $rootId, $responseCode));
|
||||
return [false, $log, $createdTestFiles];
|
||||
//$log[] = 'or that there is an .htaccess file in the ';
|
||||
}
|
||||
// $log[] = print_r($results[0]['response']['code'], true);
|
||||
}
|
||||
|
||||
$log[] = 'Why did it fail? It could either be that the redirection rule did not trigger ' .
|
||||
'or it could be that the PHP script could not locate a source image corresponding to the destination URL. ' .
|
||||
'Currently, this analysis cannot dertermine which was the case and it cannot be helpful ' .
|
||||
'if the latter is the case (sorry!). However, if the redirection rules are the problem, here is some info:';
|
||||
|
||||
$log[] = '### Diagnosing redirection problems (presuming it is the redirection to the script that is failing)';
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseFailedRewrite($this->config, $headers));
|
||||
|
||||
|
||||
//$log[count($log) - 1] .= '. FAILED';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
//$log[count($log) - 1] .= '. ok!';
|
||||
//$log[] = '*' . $requestUrl . '*';
|
||||
|
||||
//$log = array_merge($log, SelfTestHelper::printHeaders($headers));
|
||||
|
||||
if (!isset($headers['content-type'])) {
|
||||
$log[] = 'Bummer. There is no "content-type" response header. The test FAILED';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($headers['content-type'] == 'image/' . $imageType) {
|
||||
$log[] = 'Bummer. As the "content-type" header reveals, we got the ' . $imageType . '.';
|
||||
$log[] = 'The test **failed**{: .error}.';
|
||||
$log[] = 'Now, what went wrong?';
|
||||
|
||||
if (isset($headers['x-webp-convert-log'])) {
|
||||
//$log[] = 'Inspect the "x-webp-convert-log" headers above, and you ' .
|
||||
// 'should have your answer (it is probably because you do not have any conversion methods working).';
|
||||
if (SelfTestHelper::hasHeaderContaining($headers, 'x-webp-convert-log', 'Performing fail action: original')) {
|
||||
$log[] = 'The answer lies in the "x-convert-log" response headers: ' .
|
||||
'**The conversion failed**{: .error}. ';
|
||||
}
|
||||
} else {
|
||||
$log[] = 'Well, there is indication that the redirection isnt working. ' .
|
||||
'The PHP script should set "x-webp-convert-log" response headers, but there are none. ';
|
||||
'While these headers could have been eaten in a Cloudflare-like setup, the problem is ';
|
||||
'probably that the redirection simply failed';
|
||||
|
||||
$log[] = '### Diagnosing redirection problems';
|
||||
$log = array_merge($log, SelfTestHelper::diagnoseFailedRewrite($this->config, $headers));
|
||||
}
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($headers['content-type'] != 'image/webp') {
|
||||
$log[] = 'However. As the "content-type" header reveals, we did not get a webp' .
|
||||
'Surprisingly we got: "' . $headers['content-type'] . '"';
|
||||
$log[] = 'The test FAILED.';
|
||||
return [false, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
$log[] = '**Alrighty**{: .ok}. We got a webp.';
|
||||
if (isset($headers['x-webp-convert-log'])) {
|
||||
$log[] = 'The "x-webp-convert-log" headers reveals we got the webp from the PHP script. **Great!**{: .ok}';
|
||||
} else {
|
||||
$log[] = 'Interestingly, there are no "x-webp-convert-log" headers even though ' .
|
||||
'the PHP script always produces such. Could it be you have some weird setup that eats these headers?';
|
||||
}
|
||||
|
||||
if (SelfTestHelper::hasVaryAcceptHeader($headers)) {
|
||||
$log[] = 'All is however not super-duper:';
|
||||
|
||||
$log[] = '**Notice: We received a Vary:Accept header. ' .
|
||||
'That header need not to be set. Actually, it is a little bit bad for performance ' .
|
||||
'as proxies are currently doing a bad job maintaining several caches (in many cases they simply do not)**{: .warn}';
|
||||
$noWarningsYet = false;
|
||||
}
|
||||
if (!SelfTestHelper::hasCacheControlOrExpiresHeader($headers)) {
|
||||
$log[] = '**Notice: No cache-control or expires header has been set. ' .
|
||||
'It is recommended to do so. Set it nice and big once you are sure the webps have a good quality/compression compromise.**{: .warn}';
|
||||
}
|
||||
$log[] = '';
|
||||
|
||||
return [$noWarningsYet, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
/*
|
||||
private static function doRunTest($this->config)
|
||||
{
|
||||
$log = [];
|
||||
$log[] = '# Testing redirection to converter';
|
||||
|
||||
$createdTestFiles = false;
|
||||
if (!file_exists(Paths::getConfigFileName())) {
|
||||
$log[] = 'Hold on. You need to save options before you can run this test. There is no config file yet.';
|
||||
return [true, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
|
||||
if ($this->config['image-types'] == 0) {
|
||||
$log[] = 'No image types have been activated, nothing to test';
|
||||
return [true, $log, $createdTestFiles];
|
||||
}
|
||||
|
||||
if ($this->config['image-types'] & 1) {
|
||||
list($success, $subResult, $createdTestFiles) = self::runTestForImageType($this->config, 'jpeg');
|
||||
$log = array_merge($log, $subResult);
|
||||
|
||||
if ($success) {
|
||||
if ($this->config['image-types'] & 2) {
|
||||
$log[] = '### Performing same tests for PNG';
|
||||
list($success, $subResult, $createdTestFiles2) = self::runTestForImageType($this->config, 'png');
|
||||
$createdTestFiles = $createdTestFiles || $createdTestFiles2;
|
||||
if ($success) {
|
||||
//$log[count($log) - 1] .= '. **ok**{: .ok}';
|
||||
$log[] .= 'All tests passed for PNG as well.';
|
||||
$log[] = '(I shall spare you for the report, which is almost identical to the one above)';
|
||||
} else {
|
||||
$log = array_merge($log, $subResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
list($success, $subResult, $createdTestFiles) = self::runTestForImageType($this->config, 'png');
|
||||
$log = array_merge($log, $subResult);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$log[] = '### Conclusion';
|
||||
$log[] = 'Everything **seems to work**{: .ok} as it should. ' .
|
||||
'However, notice that this test only tested an image which was placed in the *uploads* folder. ' .
|
||||
'The rest of the image roots (such as theme images) have not been tested (it is on the TODO). ' .
|
||||
'Also on the TODO: If one image type is disabled, check that it does not redirect to the conversion script. ' .
|
||||
'These things probably work, though.';
|
||||
}
|
||||
|
||||
|
||||
return [true, $log, $createdTestFiles];
|
||||
}*/
|
||||
|
||||
protected function getSuccessMessage()
|
||||
{
|
||||
return 'Everything **seems to work**{: .ok} as it should. ' .
|
||||
'However, a check is on the TODO: ' .
|
||||
'TODO: Check that disabled image types does not get converted. ';
|
||||
}
|
||||
|
||||
public function startupTests()
|
||||
{
|
||||
$log[] = '# Testing "WebP Realizer" functionality';
|
||||
if (!$this->config['enable-redirection-to-webp-realizer']) {
|
||||
$log[] = 'Turned off, nothing to test (if you just turned it on without saving, remember: this is a live test so you need to save settings)';
|
||||
return [false, $log];
|
||||
}
|
||||
return [true, $log];
|
||||
}
|
||||
|
||||
public static function runTest()
|
||||
{
|
||||
$config = Config::loadConfigAndFix(false);
|
||||
$me = new SelfTestRedirectToWebPRealizer($config);
|
||||
return $me->startTest();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
45
lib/classes/State.php
Normal file
45
lib/classes/State.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Option;
|
||||
|
||||
/**
|
||||
* Store state in db
|
||||
* We are using update_option WITHOUT autoloading.
|
||||
* So this class is not intended for storing stuff that is needed on every page load.
|
||||
* For such things, use update_option / get_option directly
|
||||
*/
|
||||
|
||||
|
||||
class State
|
||||
{
|
||||
|
||||
public static function getStateObj() {
|
||||
// TODO: cache
|
||||
$json = Option::getOption('webp-express-state', '[]');
|
||||
return json_decode($json, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return state by key. Returns supplied default if key doesn't exist, or state object is corrupt
|
||||
*/
|
||||
public static function getState($key, $default = null) {
|
||||
$obj = self::getStateObj();
|
||||
if ($obj != false) {
|
||||
if (isset($obj[$key])) {
|
||||
return $obj[$key];
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
public static function setState($key, $value) {
|
||||
$currentStateObj = self::getStateObj();
|
||||
$currentStateObj[$key] = $value;
|
||||
$json = json_encode($currentStateObj, JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
|
||||
|
||||
// Store in db. No autoloading.
|
||||
Option::updateOption('webp-express-state', $json, false);
|
||||
}
|
||||
}
|
||||
152
lib/classes/TestRun.php
Normal file
152
lib/classes/TestRun.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\Config;
|
||||
use \WebPExpress\ConvertersHelper;
|
||||
use \WebPExpress\Paths;
|
||||
use \WebPExpress\FileHelper;
|
||||
|
||||
use \WebPConvert\Convert\ConverterFactory;
|
||||
use \WebPConvert\Convert\Helpers\JpegQualityDetector;
|
||||
|
||||
include_once WEBPEXPRESS_PLUGIN_DIR . '/vendor/autoload.php';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class TestRun
|
||||
{
|
||||
|
||||
|
||||
public static $converterStatus = null; // to cache the result
|
||||
|
||||
private static $warnings;
|
||||
|
||||
public static function warningHandler($errno, $errstr, $errfile, $errline, $errcontext = null)
|
||||
{
|
||||
$errorTypes = [
|
||||
E_WARNING => "Warning",
|
||||
E_NOTICE => "Notice",
|
||||
E_STRICT => "Strict Notice",
|
||||
E_DEPRECATED => "Deprecated",
|
||||
E_USER_DEPRECATED => "User Deprecated",
|
||||
];
|
||||
|
||||
if (isset($errorTypes[$errno])) {
|
||||
$errType = $errorTypes[$errno];
|
||||
} else {
|
||||
$errType = "Warning ($errno)";
|
||||
}
|
||||
|
||||
$msg = $errType . ': ' . $errstr . ' in ' . $errfile . ', line ' . $errline;
|
||||
self::$warnings[] = $msg;
|
||||
|
||||
// suppress!
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Get a test result object OR false, if tests cannot be made.
|
||||
*
|
||||
* @return object|false
|
||||
*/
|
||||
public static function getConverterStatus() {
|
||||
//return false;
|
||||
|
||||
// Is result cached?
|
||||
if (isset(self::$converterStatus)) {
|
||||
return self::$converterStatus;
|
||||
}
|
||||
$source = Paths::getWebPExpressPluginDirAbs() . '/test/small-q61.jpg';
|
||||
$destination = Paths::getUploadDirAbs() . '/webp-express-test-conversion.webp';
|
||||
if (!FileHelper::canCreateFile($destination)) {
|
||||
$destination = Paths::getContentDirAbs() . '/webp-express-test-conversion.webp';
|
||||
}
|
||||
if (!FileHelper::canCreateFile($destination)) {
|
||||
self::$converterStatus = false; // // cache the result
|
||||
return false;
|
||||
}
|
||||
$workingConverters = [];
|
||||
$errors = [];
|
||||
self::$warnings = [];
|
||||
|
||||
// We need wod options.
|
||||
// But we cannot simply use loadWodOptions - because that would leave out the deactivated
|
||||
// converters. And we need to test all converters - even the deactivated ones.
|
||||
// So we load config, set "deactivated" to false, and generate Wod options from the config
|
||||
$config = Config::loadConfigAndFix();
|
||||
|
||||
// set deactivated to false on all converters
|
||||
foreach($config['converters'] as &$converter) {
|
||||
$converter['deactivated'] = false;
|
||||
}
|
||||
|
||||
$options = Config::generateWodOptionsFromConfigObj($config);
|
||||
$options['converters'] = ConvertersHelper::normalize($options['webp-convert']['convert']['converters']);
|
||||
|
||||
$previousErrorHandler = set_error_handler(
|
||||
array('\WebPExpress\TestRun', "warningHandler"),
|
||||
E_WARNING | E_USER_WARNING | E_NOTICE | E_USER_NOTICE
|
||||
);
|
||||
|
||||
$warnings = [];
|
||||
//echo '<pre>' . print_r($options, true) . '</pre>';
|
||||
foreach ($options['converters'] as $converter) {
|
||||
$converterId = $converter['converter'];
|
||||
self::$warnings = [];
|
||||
try {
|
||||
$converterOptions = array_merge($options, $converter['options']);
|
||||
unset($converterOptions['converters']);
|
||||
|
||||
//ConverterHelper::runConverter($converterId, $source, $destination, $converterOptions);
|
||||
$converterInstance = ConverterFactory::makeConverter(
|
||||
$converterId,
|
||||
$source,
|
||||
$destination,
|
||||
$converterOptions
|
||||
);
|
||||
// Note: We now suppress warnings.
|
||||
// WebPConvert logs warnings but purposefully does not stop them - warnings should generally not be
|
||||
// stopped. However, as these warnings are logged in conversion log, it is preferable not to make them
|
||||
// bubble here. #
|
||||
$converterInstance->doConvert();
|
||||
|
||||
if (count(self::$warnings) > 0) {
|
||||
$warnings[$converterId] = self::$warnings;
|
||||
}
|
||||
$workingConverters[] = $converterId;
|
||||
} catch (\Exception $e) {
|
||||
$errors[$converterId] = $e->getMessage();
|
||||
} catch (\Throwable $e) {
|
||||
$errors[$converterId] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
//print_r($errors);
|
||||
|
||||
// cache the result
|
||||
self::$converterStatus = [
|
||||
'workingConverters' => $workingConverters,
|
||||
'errors' => $errors,
|
||||
'warnings' => $warnings,
|
||||
];
|
||||
return self::$converterStatus;
|
||||
}
|
||||
|
||||
|
||||
public static $localQualityDetectionWorking = null; // to cache the result
|
||||
|
||||
public static function isLocalQualityDetectionWorking() {
|
||||
if (isset(self::$localQualityDetectionWorking)) {
|
||||
return self::$localQualityDetectionWorking;
|
||||
} else {
|
||||
$q = JpegQualityDetector::detectQualityOfJpg(
|
||||
Paths::getWebPExpressPluginDirAbs() . '/test/small-q61.jpg'
|
||||
);
|
||||
self::$localQualityDetectionWorking = ($q === 61);
|
||||
return self::$localQualityDetectionWorking;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
lib/classes/Validate.php
Normal file
27
lib/classes/Validate.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPExpress\ConvertersHelper;
|
||||
use \WebPExpress\ValidateException;
|
||||
use \WebPExpress\SanityCheck;
|
||||
|
||||
class Validate
|
||||
{
|
||||
|
||||
public static function postHasKey($key)
|
||||
{
|
||||
if (!isset($_POST[$key])) {
|
||||
throw new ValidateException('Expected parameter in POST missing: ' . $key);
|
||||
}
|
||||
}
|
||||
|
||||
public static function isConverterId($converterId, $errorMsg = 'Not a valid converter id')
|
||||
{
|
||||
SanityCheck::pregMatch('#^[a-z]+$#', $converterId, $errorMsg);
|
||||
if (!in_array($converterId, ConvertersHelper::getDefaultConverterNames())) {
|
||||
throw new ValidateException($errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
7
lib/classes/ValidateException.php
Normal file
7
lib/classes/ValidateException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
class ValidateException extends \Exception
|
||||
{
|
||||
}
|
||||
659
lib/classes/WCFMApi.php
Normal file
659
lib/classes/WCFMApi.php
Normal file
@@ -0,0 +1,659 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \WebPConvert\Convert\Converters\Stack;
|
||||
use \WebPConvert\WebPConvert;
|
||||
use \ImageMimeTypeGuesser\ImageMimeTypeGuesser;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class WCFMApi
|
||||
{
|
||||
private static function doProcessRequest() {
|
||||
if (!check_ajax_referer('webpexpress-wcfm-nonce', 'nonce', false)) {
|
||||
throw new \Exception('The security nonce has expired. You need to reload (press F5) and try again)');
|
||||
}
|
||||
Validate::postHasKey('command');
|
||||
$command = sanitize_text_field(stripslashes($_POST['command']));
|
||||
|
||||
switch ($command) {
|
||||
/*
|
||||
case 'get-tree':
|
||||
$result = self::processGetTree();
|
||||
break;*/
|
||||
case 'get-folder':
|
||||
$result = self::processGetFolder();
|
||||
break;
|
||||
case 'conversion-settings':
|
||||
$result = self::processConversionSettings();
|
||||
break;
|
||||
case 'info':
|
||||
$result = self::processInfo();
|
||||
break;
|
||||
case 'convert':
|
||||
$result = self::processConvert();
|
||||
break;
|
||||
case 'delete-converted':
|
||||
$result = self::processDeleteConverted();
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Unknown command');
|
||||
}
|
||||
if (!isset($result)) {
|
||||
throw new \Exception('Command: ' . $command . ' gave no result');
|
||||
}
|
||||
|
||||
$json = wp_json_encode($result, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
if ($json === false) {
|
||||
// TODO: We can do better error handling than this!
|
||||
throw new \Exception('Failed encoding result to JSON');
|
||||
} else {
|
||||
echo $json;
|
||||
}
|
||||
wp_die();
|
||||
}
|
||||
|
||||
public static function processRequest() {
|
||||
try {
|
||||
self::doProcessRequest();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
wp_send_json_error($e->getMessage());
|
||||
wp_die();
|
||||
}
|
||||
}
|
||||
/*
|
||||
{
|
||||
"converters": [
|
||||
{
|
||||
"converter": "cwebp",
|
||||
"options": {
|
||||
"use-nice": true,
|
||||
"try-common-system-paths": true,
|
||||
"try-supplied-binary-for-os": true,
|
||||
"method": 6,
|
||||
"low-memory": true,
|
||||
"command-line-options": ""
|
||||
},
|
||||
"working": true
|
||||
},
|
||||
{
|
||||
"converter": "vips",
|
||||
"options": {
|
||||
"smart-subsample": false,
|
||||
"preset": "none"
|
||||
},
|
||||
"working": false
|
||||
},
|
||||
{
|
||||
"converter": "imagemagick",
|
||||
"options": {
|
||||
"use-nice": true
|
||||
},
|
||||
"working": true,
|
||||
"deactivated": true
|
||||
},
|
||||
{
|
||||
"converter": "graphicsmagick",
|
||||
"options": {
|
||||
"use-nice": true
|
||||
},
|
||||
"working": false
|
||||
},
|
||||
{
|
||||
"converter": "ffmpeg",
|
||||
"options": {
|
||||
"use-nice": true,
|
||||
"method": 4
|
||||
},
|
||||
"working": false
|
||||
},
|
||||
{
|
||||
"converter": "wpc",
|
||||
"working": false,
|
||||
"options": {
|
||||
"api-key": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"converter": "ewww",
|
||||
"working": false
|
||||
},
|
||||
{
|
||||
"converter": "imagick",
|
||||
"working": false
|
||||
},
|
||||
{
|
||||
"converter": "gmagick",
|
||||
"working": false
|
||||
},
|
||||
{
|
||||
"converter": "gd",
|
||||
"options": {
|
||||
"skip-pngs": false
|
||||
},
|
||||
"working": false
|
||||
}
|
||||
]
|
||||
}*/
|
||||
public static function processConversionSettings() {
|
||||
require_once __DIR__ . "/../../vendor/autoload.php";
|
||||
$availableConverters = Stack::getAvailableConverters();
|
||||
|
||||
/*
|
||||
$converters = [];
|
||||
//$supportsEncoding = [];
|
||||
foreach ($availableConverters as $converter) {
|
||||
$converters[] = [
|
||||
'id' => $converter,
|
||||
'name' => $converter
|
||||
];
|
||||
/*if () {
|
||||
$supportsEncoding[] = $converter;
|
||||
}*/
|
||||
//}
|
||||
|
||||
$webpConvertOptionDefinitions = WebPConvert::getConverterOptionDefinitions();
|
||||
|
||||
$config = Config::loadConfigAndFix();
|
||||
$defaults = [
|
||||
'auto-limit' => (isset($config['quality-auto']) && $config['quality-auto']),
|
||||
'alpha-quality' => $config['alpha-quality'],
|
||||
'quality' => $config['max-quality'],
|
||||
'encoding' => $config['jpeg-encoding'],
|
||||
'near-lossless' => ($config['jpeg-enable-near-lossless'] ? $config['jpeg-near-lossless'] : 100),
|
||||
'metadata' => $config['metadata'],
|
||||
'stack-converters' => ConvertersHelper::getActiveConverterIds($config),
|
||||
|
||||
// 'method' (I could copy from cwebp...)
|
||||
// 'sharp-yuv' (n/a)
|
||||
// low-memory (n/a)
|
||||
// auto-filter (n/a)
|
||||
// preset (n/a)
|
||||
// size-in-percentage (I could copy from cwebp...)
|
||||
];
|
||||
|
||||
$good = ConvertersHelper::getWorkingAndActiveConverterIds($config);
|
||||
if (isset($good[0])) {
|
||||
$defaults['converter'] = $good[0];
|
||||
}
|
||||
//'converter' => 'ewww',
|
||||
|
||||
|
||||
// TODO:add PNG options
|
||||
$pngDefaults = [
|
||||
'encoding' => $config['png-encoding'],
|
||||
'near-lossless' => ($config['png-enable-near-lossless'] ? $config['png-near-lossless'] : 100),
|
||||
'quality' => $config['png-quality'],
|
||||
];
|
||||
|
||||
|
||||
// Filter active converters
|
||||
foreach ($config['converters'] as $converter) {
|
||||
/*if (isset($converter['deactivated']) && ($converter['deactivated'])) {
|
||||
//continue;
|
||||
}*/
|
||||
if (isset($converter['options'])) {
|
||||
foreach ($converter['options'] as $optionName => $optionValue) {
|
||||
$defaults[$converter['converter'] . '-' . $optionName] = $optionValue;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$systemStatus = [
|
||||
'converterRequirements' => [
|
||||
'gd' => [
|
||||
'extensionLoaded' => extension_loaded('gd'),
|
||||
'compiledWithWebP' => function_exists('imagewebp'),
|
||||
]
|
||||
// TODO: Add more!
|
||||
]
|
||||
];
|
||||
|
||||
//getUnsupportedDefaultOptions
|
||||
//supportedStandardOptions: {
|
||||
$defaults['png'] = $pngDefaults;
|
||||
|
||||
return [
|
||||
//'converters' => $converters,
|
||||
'defaults' => $defaults,
|
||||
//'pngDefaults' => $pngDefaults,
|
||||
'options' => $webpConvertOptionDefinitions,
|
||||
'systemStatus' => $systemStatus
|
||||
];
|
||||
|
||||
/*
|
||||
$config = Config::loadConfigAndFix();
|
||||
// 'working', 'deactivated'
|
||||
$foundFirstWorkingAndActive = false;
|
||||
foreach ($config['converters'] as $converter) {
|
||||
$converters[] = [
|
||||
'id' => $converter['converter'],
|
||||
'name' => $converter['converter']
|
||||
];
|
||||
if ($converter['working']) {
|
||||
if
|
||||
}
|
||||
if (!$foundFirstWorkingAndActive) {
|
||||
|
||||
}
|
||||
}*/
|
||||
|
||||
return [
|
||||
'converters' => $converters
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* Get mime
|
||||
* @return string
|
||||
*/
|
||||
private static function setMime($path, &$info) {
|
||||
require_once __DIR__ . "/../../vendor/autoload.php";
|
||||
$mimeResult = ImageMimeTypeGuesser::detect($path);
|
||||
if (!$mimeResult) {
|
||||
return;
|
||||
}
|
||||
$info['mime'] = $mimeResult;
|
||||
if ($mimeResult == 'image/webp') {
|
||||
$handle = @fopen($path, 'r');
|
||||
if ($handle !== false) {
|
||||
// 20 bytes is sufficient for all our sniffers, except image/svg+xml.
|
||||
// The svg sniffer takes care of reading more
|
||||
$sampleBin = @fread($handle, 20);
|
||||
if ($sampleBin !== false) {
|
||||
if (preg_match("/^RIFF.{4}WEBPVP8\ /", $sampleBin) === 1) {
|
||||
$info['mime'] .= ' (lossy)';
|
||||
} else if (preg_match("/^RIFF.{4}WEBPVP8L/", $sampleBin) === 1) {
|
||||
$info['mime'] .= ' (lossless)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static function processInfo() {
|
||||
|
||||
Validate::postHasKey('args');
|
||||
|
||||
//$args = json_decode(sanitize_text_field(stripslashes($_POST['args'])), true);
|
||||
|
||||
//$args = $_POST['args'];
|
||||
$args = self::getArgs();
|
||||
if (!array_key_exists('path', $args)) {
|
||||
throw new \Exception('"path" argument missing for command');
|
||||
}
|
||||
|
||||
$path = SanityCheck::pathWithoutDirectoryTraversal($args['path']);
|
||||
$path = ltrim($path, '/');
|
||||
$pathTokens = explode('/', $path);
|
||||
|
||||
$rootId = array_shift($pathTokens); // Shift off the first item, which is the scope
|
||||
$relPath = implode('/', $pathTokens);
|
||||
$config = Config::loadConfigAndFix();
|
||||
/*$rootIds = Paths::filterOutSubRoots($config['scope']);
|
||||
if (!in_array($rootId, $rootIds)) {
|
||||
throw new \Exception('Invalid scope (have you perhaps changed the scope setting after igniting the file manager?)');
|
||||
}*/
|
||||
$rootIds = $rootIds = Paths::getImageRootIds();
|
||||
|
||||
$absPath = Paths::getAbsDirById($rootId) . '/' . $relPath;
|
||||
//absPathExistsAndIsFile
|
||||
SanityCheck::absPathExists($absPath);
|
||||
|
||||
$result = [
|
||||
'original' => [
|
||||
//'filename' => $absPath,
|
||||
//'abspath' => $absPath,
|
||||
'size' => filesize($absPath),
|
||||
// PS: I keep "&original" because some might have set up Nginx rules for ?original
|
||||
'url' => Paths::getUrlById($rootId) . '/' . $relPath . '?' . SelfTestHelper::randomDigitsAndLetters(8) . '&dontreplace&original',
|
||||
]
|
||||
];
|
||||
self::setMime($absPath, $result['original']);
|
||||
|
||||
// TODO: NO!
|
||||
// We must use ConvertHelper::getDestination for the abs path.
|
||||
// And we must use logic from AlterHtmlHelper to get the URL
|
||||
//error_log('path:' . $absPathDest);
|
||||
|
||||
$destinationOptions = DestinationOptions::createFromConfig($config);
|
||||
if ($destinationOptions->useDocRoot) {
|
||||
if (!(Paths::canUseDocRootForStructuringCacheDir())) {
|
||||
$destinationOptions->useDocRoot = false;
|
||||
}
|
||||
}
|
||||
$imageRoots = new ImageRoots(Paths::getImageRootsDef());
|
||||
$destinationPath = Paths::getDestinationPathCorrespondingToSource($absPath, $destinationOptions);
|
||||
list($rootId, $destRelPath) = Paths::getRootAndRelPathForDestination($destinationPath, $imageRoots);
|
||||
if ($rootId != '') {
|
||||
$absPathDest = Paths::getAbsDirById($rootId) . '/' . $destRelPath;
|
||||
$destinationUrl = Paths::getUrlById($rootId) . '/' . $destRelPath;
|
||||
|
||||
SanityCheck::absPath($absPathDest);
|
||||
|
||||
if (@file_exists($absPathDest)) {
|
||||
$result['converted'] = [
|
||||
//'abspath' => $absPathDest,
|
||||
'size' => filesize($absPathDest),
|
||||
'url' => $destinationUrl . '?' . SelfTestHelper::randomDigitsAndLetters(8),
|
||||
];
|
||||
self::setMime($absPathDest, $result['converted']);
|
||||
}
|
||||
|
||||
// Get log, if exists. Ignore errors.
|
||||
$log = '';
|
||||
try {
|
||||
$logFile = ConvertHelperIndependent::getLogFilename($absPath, Paths::getLogDirAbs());
|
||||
if (@file_exists($logFile)) {
|
||||
$logContent = file_get_contents($logFile);
|
||||
if ($log !== false) {
|
||||
$log = $logContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
//throw $e;
|
||||
}
|
||||
|
||||
$result['log'] = $log;
|
||||
}
|
||||
|
||||
|
||||
//$destinationUrl = DestinationUrl::
|
||||
|
||||
/*
|
||||
error_log('dest:' . $destinationPath);
|
||||
error_log('dest root:' . $rootId);
|
||||
error_log('dest path:' . $destRelPath);
|
||||
error_log('dest abs-dir:' . Paths::getAbsDirById($rootId) . '/' . $destRelPath);
|
||||
error_log('dest url:' . Paths::getUrlById($rootId) . '/' . $destRelPath);
|
||||
*/
|
||||
|
||||
//error_log('url:' . $destinationPath);
|
||||
//error_log('destinationOptions' . print_r($destinationOptions, true));
|
||||
|
||||
/*
|
||||
$destination = Paths::destinationPathConvenience($rootId, $relPath, $config);
|
||||
$absPathDest = $destination['abs-path'];
|
||||
SanityCheck::absPath($absPathDest);
|
||||
error_log('path:' . $absPathDest);
|
||||
|
||||
if (@file_exists($absPathDest)) {
|
||||
$result['converted'] = [
|
||||
'abspath' => $destination['abs-path'],
|
||||
'size' => filesize($destination['abs-path']),
|
||||
'url' => $destination['url'],
|
||||
'log' => ''
|
||||
];
|
||||
}
|
||||
*/
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate path received (ie "/uploads/2021/...") to absolute path.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return array [$absPath, $relPath, $rootId]
|
||||
* @throws \Exception if root id is invalid or path doesn't pass sanity check
|
||||
*/
|
||||
private static function analyzePathReceived($path) {
|
||||
try {
|
||||
$path = SanityCheck::pathWithoutDirectoryTraversal($path);
|
||||
$path = ltrim($path, '/');
|
||||
$pathTokens = explode('/', $path);
|
||||
|
||||
$rootId = array_shift($pathTokens);
|
||||
$relPath = implode('/', $pathTokens);
|
||||
|
||||
$rootIds = Paths::getImageRootIds();
|
||||
if (!in_array($rootId, $rootIds)) {
|
||||
throw new \Exception('Invalid rootId');
|
||||
}
|
||||
if ($relPath == '') {
|
||||
$relPath = '.';
|
||||
}
|
||||
|
||||
$absPath = PathHelper::canonicalize(Paths::getAbsDirById($rootId) . '/' . $relPath);
|
||||
SanityCheck::absPathExists($absPath);
|
||||
|
||||
return [$absPath, $relPath, $rootId];
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
//throw new \Exception('Invalid path received (' . $e->getMessage() . ')');
|
||||
throw new \Exception('Invalid path');
|
||||
}
|
||||
}
|
||||
|
||||
public static function processGetFolder() {
|
||||
|
||||
Validate::postHasKey('args');
|
||||
|
||||
//$args = json_decode(sanitize_text_field(stripslashes($_POST['args'])), true);
|
||||
|
||||
$args = self::getArgs();
|
||||
if (!array_key_exists('path', $args)) {
|
||||
throw new \Exception('"path" argument missing for command');
|
||||
}
|
||||
|
||||
$path = SanityCheck::noStreamWrappers($args['path']);
|
||||
//$pathTokens = explode('/', $path);
|
||||
if ($path == '') {
|
||||
$result = [
|
||||
'children' => [
|
||||
[
|
||||
'name' => '/',
|
||||
'isDir' => true,
|
||||
'nickname' => 'scope'
|
||||
]
|
||||
]
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
$config = Config::loadConfigAndFix();
|
||||
$rootIds = Paths::getImageRootIds();
|
||||
if ($path == '/') {
|
||||
$rootIds = Paths::filterOutSubRoots($config['scope']);
|
||||
$result = ['children'=>[]];
|
||||
foreach ($rootIds as $rootId) {
|
||||
$result['children'][] = [
|
||||
'name' => $rootId,
|
||||
'isDir' => true,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
list($absPath, $relPath, $rootId) = self::analyzePathReceived($path);
|
||||
|
||||
$listOptions = BulkConvert::defaultListOptions($config);
|
||||
$listOptions['root'] = Paths::getAbsDirById($rootId);
|
||||
|
||||
$listOptions['filter']['only-unconverted'] = false;
|
||||
$listOptions['flattenList'] = false;
|
||||
$listOptions['max-depth'] = 0;
|
||||
|
||||
//throw new \Exception('Invalid rootId' . print_r($listOptions));
|
||||
|
||||
$list = BulkConvert::getListRecursively($relPath, $listOptions);
|
||||
|
||||
return ['children' => $list];
|
||||
}
|
||||
|
||||
public static function processGetTree() {
|
||||
$config = Config::loadConfigAndFix();
|
||||
$rootIds = Paths::filterOutSubRoots($config['scope']);
|
||||
|
||||
$listOptions = [
|
||||
//'root' => Paths::getUploadDirAbs(),
|
||||
'ext' => $config['destination-extension'],
|
||||
'destination-folder' => $config['destination-folder'], /* hm, "destination-folder" is a bad name... */
|
||||
'webExpressContentDirAbs' => Paths::getWebPExpressContentDirAbs(),
|
||||
'uploadDirAbs' => Paths::getUploadDirAbs(),
|
||||
'useDocRootForStructuringCacheDir' => (($config['destination-structure'] == 'doc-root') && (Paths::canUseDocRootForStructuringCacheDir())),
|
||||
'imageRoots' => new ImageRoots(Paths::getImageRootsDefForSelectedIds($config['scope'])), // (Paths::getImageRootsDef()
|
||||
'filter' => [
|
||||
'only-converted' => false,
|
||||
'only-unconverted' => false,
|
||||
'image-types' => $config['image-types'],
|
||||
],
|
||||
'flattenList' => false
|
||||
];
|
||||
|
||||
$children = [];
|
||||
foreach ($rootIds as $rootId) {
|
||||
$listOptions['root'] = Paths::getAbsDirById($rootId);
|
||||
$grandChildren = BulkConvert::getListRecursively('.', $listOptions);
|
||||
$children[] = [
|
||||
'name' => $rootId,
|
||||
'isDir' => true,
|
||||
'children' => $grandChildren
|
||||
];
|
||||
}
|
||||
return ['name' => '', 'isDir' => true, 'isOpen' => true, 'children' => $children];
|
||||
|
||||
}
|
||||
|
||||
private static function getArgs() {
|
||||
//return $_POST['args'];
|
||||
|
||||
$args = $_POST['args'];
|
||||
// $args = '{\"path\":\"\"}';
|
||||
//$args = '{"path":"hollo"}';
|
||||
|
||||
//error_log('get args:' . gettype($args));
|
||||
//error_log(print_r($args, true));
|
||||
//error_log(print_r(($_POST['args'] + ''), true));
|
||||
|
||||
//error_log('type:' . gettype($_POST['args']));
|
||||
$args = json_decode('"' . $args . '"', true);
|
||||
$args = json_decode($args, true);
|
||||
//error_log('decoded:' . gettype($args));
|
||||
//error_log(print_r($args, true));
|
||||
//$args = json_decode($args, true);
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
public static function processConvert() {
|
||||
|
||||
Validate::postHasKey('args');
|
||||
|
||||
//$args = json_decode(sanitize_text_field(stripslashes($_POST['args'])), true);
|
||||
|
||||
$args = self::getArgs();
|
||||
if (!array_key_exists('path', $args)) {
|
||||
throw new \Exception('"path" argument missing for command');
|
||||
}
|
||||
|
||||
$path = SanityCheck::noStreamWrappers($args['path']);
|
||||
|
||||
$convertOptions = null;
|
||||
if (isset($args['convertOptions'])) {
|
||||
$convertOptions = $args['convertOptions'];
|
||||
$convertOptions['log-call-arguments'] = true;
|
||||
//unset($convertOptions['converter']);
|
||||
//$convertOptions['png'] = ['quality' => 7];
|
||||
//$convertOptions['png-quality'] = 8;
|
||||
}
|
||||
|
||||
//error_log(print_r(json_encode($convertOptions, JSON_PRETTY_PRINT), true));
|
||||
|
||||
list($absPath, $relPath, $rootId) = self::analyzePathReceived($path);
|
||||
|
||||
$convertResult = Convert::convertFile($absPath, null, $convertOptions);
|
||||
|
||||
$result = [
|
||||
'success' => $convertResult['success'],
|
||||
'data' => $convertResult['msg'],
|
||||
'log' => $convertResult['log'],
|
||||
'args' => $args, // for debugging. TODO
|
||||
];
|
||||
$info = [];
|
||||
if (isset($convertResult['filesize-webp'])) {
|
||||
$info['size'] = $convertResult['filesize-webp'];
|
||||
}
|
||||
if (isset($convertResult['destination-url'])) {
|
||||
$info['url'] = $convertResult['destination-url'] . '?' . SelfTestHelper::randomDigitsAndLetters(8);
|
||||
}
|
||||
if (isset($convertResult['destination-path'])) {
|
||||
self::setMime($convertResult['destination-path'], $info);
|
||||
}
|
||||
|
||||
$result['converted'] = $info;
|
||||
return $result;
|
||||
|
||||
/*if (!array_key_exists('convertOptions', $args)) {
|
||||
throw new \Exception('"convertOptions" argument missing for command');
|
||||
}
|
||||
//return ['success' => true, 'optionsReceived' => $args['convertOptions']];
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
$path = SanityCheck::pathWithoutDirectoryTraversal($args['path']);
|
||||
$path = ltrim($path, '/');
|
||||
$pathTokens = explode('/', $path);
|
||||
|
||||
$rootId = array_shift($pathTokens); // Shift off the first item, which is the scope
|
||||
$relPath = implode('/', $pathTokens);
|
||||
$config = Config::loadConfigAndFix();
|
||||
$rootIds = Paths::filterOutSubRoots($config['scope']);
|
||||
if (!in_array($rootId, $rootIds)) {
|
||||
throw new \Exception('Invalid scope');
|
||||
}
|
||||
|
||||
$absPath = Paths::getAbsDirById($rootId) . '/' . $relPath;
|
||||
//absPathExistsAndIsFile
|
||||
SanityCheck::absPathExists($absPath); */
|
||||
}
|
||||
|
||||
public static function processDeleteConverted() {
|
||||
|
||||
Validate::postHasKey('args');
|
||||
|
||||
//$args = json_decode(sanitize_text_field(stripslashes($_POST['args'])), true);
|
||||
|
||||
//$args = $_POST['args'];
|
||||
$args = self::getArgs();
|
||||
if (!array_key_exists('path', $args)) {
|
||||
throw new \Exception('"path" argument missing for command');
|
||||
}
|
||||
|
||||
$path = SanityCheck::noStreamWrappers($args['path']);
|
||||
list($absPath, $relPath, $rootId) = self::analyzePathReceived($path);
|
||||
|
||||
$config = Config::loadConfigAndFix();
|
||||
$destinationOptions = DestinationOptions::createFromConfig($config);
|
||||
if ($destinationOptions->useDocRoot) {
|
||||
if (!(Paths::canUseDocRootForStructuringCacheDir())) {
|
||||
$destinationOptions->useDocRoot = false;
|
||||
}
|
||||
}
|
||||
$destinationPath = Paths::getDestinationPathCorrespondingToSource($absPath, $destinationOptions);
|
||||
|
||||
if (@!file_exists($destinationPath)) {
|
||||
throw new \Exception('file not found: ' . $destinationPath);
|
||||
}
|
||||
|
||||
if (@!unlink($destinationPath)) {
|
||||
throw new \Exception('failed deleting file');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'success' => true,
|
||||
'data' => $destinationPath
|
||||
];
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
54
lib/classes/WCFMPage.php
Normal file
54
lib/classes/WCFMPage.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
use \WebPConvert\WebPConvert;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
class WCFMPage
|
||||
{
|
||||
|
||||
// callback (registred in AdminUi)
|
||||
public static function display() {
|
||||
echo '<div id="wcfmintro">' .
|
||||
'<h1>WebP Express Conversion Browser</h1>' .
|
||||
'</div>';
|
||||
|
||||
echo '<div id="webpconvert-filemanager" style="position:relative; min-height:400px">loading</div>';
|
||||
//include WEBPEXPRESS_PLUGIN_DIR . '/lib/options/page.php';
|
||||
|
||||
/* require_once __DIR__ . "/../../vendor/autoload.php";
|
||||
// print_r(WebPConvert::getConverterOptionDefinitions('png', false, true));
|
||||
echo '<pre>' .
|
||||
print_r(
|
||||
json_encode(
|
||||
WebPConvert::getConverterOptionDefinitions('png', false, true),
|
||||
JSON_PRETTY_PRINT
|
||||
),
|
||||
true
|
||||
) . '</pre>';*/
|
||||
}
|
||||
|
||||
/* We add directly to head instead, to get the type="module"
|
||||
public static function enqueueScripts() {
|
||||
$ver = '0';
|
||||
wp_register_script('wcfileman', plugins_url('js/wcfm/index.js', WEBPEXPRESS_PLUGIN), [], $ver);
|
||||
wp_enqueue_script('wcfileman');
|
||||
}*/
|
||||
|
||||
public static function addToHead() {
|
||||
$baseUrl = plugins_url('lib/wcfm', WEBPEXPRESS_PLUGIN);
|
||||
//$url = plugins_url('js/conversion-manager/index.be5d792e.js ', WEBPEXPRESS_PLUGIN);
|
||||
|
||||
$wcfmNonce = wp_create_nonce('webpexpress-wcfm-nonce');
|
||||
echo '<scr' . 'ipt>window.webpExpressWCFMNonce = "' . $wcfmNonce . '";</scr' . 'ipt>';
|
||||
|
||||
echo '<scr' . 'ipt src="' . $baseUrl . '/wcfm-options.js?25"></scr' . 'ipt>';
|
||||
//echo '<scr' . 'ipt type="module" src="' . $baseUrl . '/vendor.js?1"></scr' . 'ipt>';
|
||||
|
||||
echo '<scr' . 'ipt type="module" src="' . $baseUrl . '/index.be5d792e.js"></scr' . 'ipt>';
|
||||
echo '<link rel="stylesheet" href="' . $baseUrl . '/index.0c25b0fb.css">';
|
||||
}
|
||||
|
||||
}
|
||||
36
lib/classes/WPHttpRequester.php
Normal file
36
lib/classes/WPHttpRequester.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace WebPExpress;
|
||||
|
||||
use \HtaccessCapabilityTester\HttpRequesterInterface;
|
||||
use \HtaccessCapabilityTester\HttpResponse;
|
||||
|
||||
class WPHttpRequester implements HttpRequesterInterface
|
||||
{
|
||||
/**
|
||||
* Make a HTTP request to a URL.
|
||||
*
|
||||
* @param string $url The URL to make the HTTP request to
|
||||
*
|
||||
* @return HttpResponse A HttpResponse object, which simply contains body, status code
|
||||
* and response headers
|
||||
*/
|
||||
public function makeHTTPRequest($url) {
|
||||
$response = wp_remote_get($url, ['timeout' => 10]);
|
||||
//echo '<pre>' . print_r($response, true) . '</pre>';
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new HttpResponse($response->get_error_message(), '0', []);
|
||||
} else {
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
$statusCode = wp_remote_retrieve_response_code($response);
|
||||
$headersDict = wp_remote_retrieve_headers($response);
|
||||
if (method_exists($headersDict, 'getAll')) {
|
||||
$headersMap = $headersDict->getAll();
|
||||
} else {
|
||||
$headersMap = [];
|
||||
}
|
||||
return new HttpResponse($body, $statusCode, $headersMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
285
lib/classes/WebPOnDemand.php
Normal file
285
lib/classes/WebPOnDemand.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?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 \WebPExpress\ConvertHelperIndependent;
|
||||
use \WebPExpress\Sanitize;
|
||||
use \WebPExpress\SanityCheck;
|
||||
use \WebPExpress\SanityException;
|
||||
use \WebPExpress\ValidateException;
|
||||
use \WebPExpress\Validate;
|
||||
use \WebPExpress\WodConfigLoader;
|
||||
use WebPConvert\Loggers\EchoLogger;
|
||||
|
||||
class WebPOnDemand extends WodConfigLoader
|
||||
{
|
||||
private static function getSourceDocRoot() {
|
||||
|
||||
|
||||
//echo 't:' . $_GET['test'];exit;
|
||||
// Check if it is in an environment variable
|
||||
$source = self::getEnvPassedInRewriteRule('REQFN');
|
||||
if ($source !== false) {
|
||||
self::$checking = 'source (passed through env)';
|
||||
return SanityCheck::absPathExistsAndIsFile($source);
|
||||
}
|
||||
|
||||
// Check if it is in header (but only if .htaccess was configured to send in header)
|
||||
if (isset($wodOptions['base-htaccess-on-these-capability-tests'])) {
|
||||
$capTests = $wodOptions['base-htaccess-on-these-capability-tests'];
|
||||
$passThroughHeaderDefinitelyUnavailable = ($capTests['passThroughHeaderWorking'] === false);
|
||||
$passThrougEnvVarDefinitelyAvailable =($capTests['passThroughEnvWorking'] === true);
|
||||
// This determines if .htaccess was configured to send in querystring
|
||||
$headerMagicAddedInHtaccess = ((!$passThrougEnvVarDefinitelyAvailable) && (!$passThroughHeaderDefinitelyUnavailable));
|
||||
} else {
|
||||
$headerMagicAddedInHtaccess = true; // pretend its true
|
||||
}
|
||||
if ($headerMagicAddedInHtaccess && (isset($_SERVER['HTTP_REQFN']))) {
|
||||
self::$checking = 'source (passed through request header)';
|
||||
return SanityCheck::absPathExistsAndIsFile($_SERVER['HTTP_REQFN']);
|
||||
}
|
||||
|
||||
if (!isset(self::$docRoot)) {
|
||||
//$source = self::getEnvPassedInRewriteRule('REQFN');
|
||||
if (isset($_GET['root-id']) && isset($_GET['xsource-rel-to-root-id'])) {
|
||||
$xsrcRelToRootId = SanityCheck::noControlChars($_GET['xsource-rel-to-root-id']);
|
||||
$srcRelToRootId = SanityCheck::pathWithoutDirectoryTraversal(substr($xsrcRelToRootId, 1));
|
||||
//echo $srcRelToRootId; exit;
|
||||
|
||||
$rootId = SanityCheck::noControlChars($_GET['root-id']);
|
||||
SanityCheck::pregMatch('#^[a-z]+$#', $rootId, 'Not a valid root-id');
|
||||
|
||||
$source = self::getRootPathById($rootId) . '/' . $srcRelToRootId;
|
||||
return SanityCheck::absPathExistsAndIsFile($source);
|
||||
}
|
||||
}
|
||||
|
||||
// Check querystring (relative path to docRoot) - when docRoot is available
|
||||
if (isset(self::$docRoot) && isset($_GET['xsource-rel'])) {
|
||||
self::$checking = 'source (passed as relative path, through querystring)';
|
||||
$xsrcRel = SanityCheck::noControlChars($_GET['xsource-rel']);
|
||||
$srcRel = SanityCheck::pathWithoutDirectoryTraversal(substr($xsrcRel, 1));
|
||||
return SanityCheck::absPathExistsAndIsFile(self::$docRoot . '/' . $srcRel);
|
||||
}
|
||||
|
||||
// Check querystring (relative path to plugin) - when docRoot is unavailable
|
||||
/*TODO
|
||||
if (!isset(self::$docRoot) && isset($_GET['xsource-rel-to-plugin-dir'])) {
|
||||
self::$checking = 'source (passed as relative path to plugin dir, through querystring)';
|
||||
$xsrcRelPlugin = SanityCheck::noControlChars($_GET['xsource-rel-to-plugin-dir']);
|
||||
$srcRelPlugin = SanityCheck::pathWithoutDirectoryTraversal(substr($xsrcRelPlugin, 1));
|
||||
return SanityCheck::absPathExistsAndIsFile(self::$docRoot . '/' . $srcRel);
|
||||
}*/
|
||||
|
||||
|
||||
// Check querystring (full path)
|
||||
// - But only on Nginx (our Apache .htaccess rules never passes absolute url)
|
||||
if (
|
||||
(self::isNginxHandlingImages()) &&
|
||||
(isset($_GET['source']) || isset($_GET['xsource']))
|
||||
) {
|
||||
self::$checking = 'source (passed as absolute path on nginx)';
|
||||
if (isset($_GET['source'])) {
|
||||
$source = SanityCheck::absPathExistsAndIsFile($_GET['source']);
|
||||
} else {
|
||||
$xsrc = SanityCheck::noControlChars($_GET['xsource']);
|
||||
return SanityCheck::absPathExistsAndIsFile(substr($xsrc, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort is to use $_SERVER['REQUEST_URI'], well knowing that it does not give the
|
||||
// correct result in all setups (ie "folder method 1")
|
||||
if (isset(self::$docRoot)) {
|
||||
self::$checking = 'source (retrieved by the request_uri server var)';
|
||||
$srcRel = SanityCheck::pathWithoutDirectoryTraversal(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
|
||||
return SanityCheck::absPathExistsAndIsFile(self::$docRoot . $srcRel);
|
||||
}
|
||||
}
|
||||
|
||||
private static function getSourceNoDocRoot()
|
||||
{
|
||||
$dirIdOfHtaccess = self::getEnvPassedInRewriteRule('WE_HTACCESS_ID');
|
||||
if ($dirIdOfHtaccess === false) {
|
||||
$dirIdOfHtaccess = SanityCheck::noControlChars($_GET['htaccess-id']);
|
||||
}
|
||||
|
||||
if (!in_array($dirIdOfHtaccess, ['uploads', 'themes', 'wp-content', 'plugins', 'index'])) {
|
||||
throw new \Exception('invalid htaccess directory id argument.');
|
||||
}
|
||||
|
||||
// First try ENV
|
||||
$sourceRelHtaccess = self::getEnvPassedInRewriteRule('WE_SOURCE_REL_HTACCESS');
|
||||
|
||||
// Otherwise use query-string
|
||||
if ($sourceRelHtaccess === false) {
|
||||
if (isset($_GET['xsource-rel-htaccess'])) {
|
||||
$x = SanityCheck::noControlChars($_GET['xsource-rel-htaccess']);
|
||||
$sourceRelHtaccess = SanityCheck::pathWithoutDirectoryTraversal(substr($x, 1));
|
||||
} else {
|
||||
throw new \Exception('Argument for source path is missing');
|
||||
}
|
||||
}
|
||||
|
||||
$sourceRelHtaccess = SanityCheck::pathWithoutDirectoryTraversal($sourceRelHtaccess);
|
||||
|
||||
|
||||
$imageRoots = self::getImageRootsDef();
|
||||
|
||||
$source = $imageRoots->byId($dirIdOfHtaccess)->getAbsPath() . '/' . $sourceRelHtaccess;
|
||||
return $source;
|
||||
}
|
||||
|
||||
private static function getSource() {
|
||||
if (self::$usingDocRoot) {
|
||||
$source = self::getSourceDocRoot();
|
||||
} else {
|
||||
$source = self::getSourceNoDocRoot();
|
||||
}
|
||||
return $source;
|
||||
}
|
||||
|
||||
private static function processRequestNoTryCatch() {
|
||||
|
||||
self::loadConfig();
|
||||
|
||||
$options = self::$options;
|
||||
$wodOptions = self::$wodOptions;
|
||||
$serveOptions = $options['webp-convert'];
|
||||
$convertOptions = &$serveOptions['convert'];
|
||||
//echo '<pre>' . print_r($wodOptions, true) . '</pre>'; exit;
|
||||
|
||||
|
||||
// Validate that WebPExpress was configured to redirect to this conversion script
|
||||
// (but do not require that for Nginx)
|
||||
// ------------------------------------------------------------------------------
|
||||
self::$checking = 'settings';
|
||||
if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
|
||||
if (!isset($wodOptions['enable-redirection-to-converter']) || ($wodOptions['enable-redirection-to-converter'] === false)) {
|
||||
throw new ValidateException('Redirection to conversion script is not enabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Check source (the image to be converted)
|
||||
// --------------------------------------------
|
||||
self::$checking = 'source';
|
||||
|
||||
// Decode URL in case file contains encoded symbols (#413)
|
||||
$source = urldecode(self::getSource());
|
||||
|
||||
//self::exitWithError($source);
|
||||
|
||||
$imageRoots = self::getImageRootsDef();
|
||||
|
||||
// Get upload dir
|
||||
$uploadDirAbs = $imageRoots->byId('uploads')->getAbsPath();
|
||||
|
||||
// Check destination path
|
||||
// --------------------------------------------
|
||||
self::$checking = 'destination path';
|
||||
$destination = ConvertHelperIndependent::getDestination(
|
||||
$source,
|
||||
$wodOptions['destination-folder'],
|
||||
$wodOptions['destination-extension'],
|
||||
self::$webExpressContentDirAbs,
|
||||
$uploadDirAbs,
|
||||
self::$usingDocRoot,
|
||||
self::getImageRootsDef()
|
||||
);
|
||||
|
||||
//$destination = SanityCheck::absPathIsInDocRoot($destination);
|
||||
$destination = SanityCheck::pregMatch('#\.webp$#', $destination, 'Does not end with .webp');
|
||||
|
||||
//self::exitWithError($destination);
|
||||
|
||||
// Done with sanitizing, lets get to work!
|
||||
// ---------------------------------------
|
||||
self::$checking = 'done';
|
||||
|
||||
if (isset($wodOptions['success-response']) && ($wodOptions['success-response'] == 'original')) {
|
||||
$serveOptions['serve-original'] = true;
|
||||
$serveOptions['serve-image']['headers']['vary-accept'] = false;
|
||||
} else {
|
||||
$serveOptions['serve-image']['headers']['vary-accept'] = true;
|
||||
}
|
||||
//echo $source . '<br>' . $destination; exit;
|
||||
|
||||
/*
|
||||
// No caching!
|
||||
// - perhaps this will solve it for WP engine.
|
||||
// but no... Perhaps a 302 redirect to self then? (if redirect to existing is activated).
|
||||
// TODO: try!
|
||||
//$serveOptions['serve-image']['headers']['vary-accept'] = false;
|
||||
|
||||
*/
|
||||
/*
|
||||
include_once __DIR__ . '/../../vendor/autoload.php';
|
||||
$convertLogger = new \WebPConvert\Loggers\BufferLogger();
|
||||
\WebPConvert\WebPConvert::convert($source, $destination, $serveOptions['convert'], $convertLogger);
|
||||
header('Location: ?fresh' , 302);
|
||||
*/
|
||||
|
||||
if (isset($_SERVER['WPENGINE_ACCOUNT'])) {
|
||||
// Redirect to self rather than serve directly for WP Engine.
|
||||
// This overcomes that Vary:Accept header set from PHP is lost on WP Engine.
|
||||
// To prevent endless loop in case "redirect to existing webp" isn't set up correctly,
|
||||
// only activate when destination is missing.
|
||||
// (actually it does not prevent anything on wpengine as the first request is cached!
|
||||
// -even though we try to prevent it:)
|
||||
// Well well. Those users better set up "redirect to existing webp" as well!
|
||||
$serveOptions['serve-image']['headers']['cache-control'] = true;
|
||||
$serveOptions['serve-image']['headers']['expires'] = false;
|
||||
$serveOptions['serve-image']['cache-control-header'] = 'no-store, no-cache, must-revalidate, max-age=0';
|
||||
//header("Pragma: no-cache", true);
|
||||
|
||||
if (!@file_exists($destination)) {
|
||||
$serveOptions['redirect-to-self-instead-of-serving'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$loggingEnabled = (isset($wodOptions['enable-logging']) ? $wodOptions['enable-logging'] : true);
|
||||
$logDir = ($loggingEnabled ? self::$webExpressContentDirAbs . '/log' : null);
|
||||
|
||||
ConvertHelperIndependent::serveConverted(
|
||||
$source,
|
||||
$destination,
|
||||
$serveOptions,
|
||||
$logDir,
|
||||
'Conversion triggered with the conversion script (wod/webp-on-demand.php)'
|
||||
);
|
||||
|
||||
BiggerThanSourceDummyFiles::updateStatus(
|
||||
$source,
|
||||
$destination,
|
||||
self::$webExpressContentDirAbs,
|
||||
self::getImageRootsDef(),
|
||||
$wodOptions['destination-folder'],
|
||||
$wodOptions['destination-extension']
|
||||
);
|
||||
|
||||
self::fixConfigIfEwwwDiscoveredNonFunctionalApiKeys();
|
||||
}
|
||||
|
||||
public static function processRequest() {
|
||||
try {
|
||||
self::processRequestNoTryCatch();
|
||||
} catch (SanityException $e) {
|
||||
self::exitWithError('Sanity check failed for ' . self::$checking . ': '. $e->getMessage());
|
||||
} catch (ValidateException $e) {
|
||||
self::exitWithError('Validation failed for ' . self::$checking . ': '. $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
if (self::$checking == 'done') {
|
||||
self::exitWithError('Error occured during conversion/serving:' . $e->getMessage());
|
||||
} else {
|
||||
self::exitWithError('Error occured while calculating ' . self::$checking . ': '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
276
lib/classes/WebPRealizer.php
Normal file
276
lib/classes/WebPRealizer.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/*
|
||||
This class is used by wod/webp-realizer.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 \WebPExpress\ConvertHelperIndependent;
|
||||
use \WebPExpress\Sanitize;
|
||||
use \WebPExpress\SanityCheck;
|
||||
use \WebPExpress\SanityException;
|
||||
use \WebPExpress\ValidateException;
|
||||
use \WebPExpress\Validate;
|
||||
use \WebPExpress\WodConfigLoader;
|
||||
|
||||
class WebPRealizer extends WodConfigLoader
|
||||
{
|
||||
private static function getDestinationDocRoot() {
|
||||
$docRoot = self::$docRoot;
|
||||
|
||||
// Check if it is in an environment variable
|
||||
$destRel = self::getEnvPassedInRewriteRule('DESTINATIONREL');
|
||||
if ($destRel !== false) {
|
||||
return SanityCheck::absPath($docRoot . '/' . $destRel);
|
||||
}
|
||||
|
||||
// Check querystring (relative path)
|
||||
if (isset($_GET['xdestination-rel'])) {
|
||||
$xdestRel = SanityCheck::noControlChars($_GET['xdestination-rel']);
|
||||
$destRel = SanityCheck::pathWithoutDirectoryTraversal(substr($xdestRel, 1));
|
||||
$destination = SanityCheck::absPath($docRoot . '/' . $destRel);
|
||||
return SanityCheck::absPathIsInDocRoot($destination);
|
||||
}
|
||||
|
||||
// Check querystring (full path)
|
||||
// - But only on Nginx (our Apache .htaccess rules never passes absolute url)
|
||||
if (self::isNginxHandlingImages()) {
|
||||
if (isset($_GET['destination'])) {
|
||||
return SanityCheck::absPathIsInDocRoot($_GET['destination']);
|
||||
}
|
||||
if (isset($_GET['xdestination'])) {
|
||||
$xdest = SanityCheck::noControlChars($_GET['xdestination']);
|
||||
return SanityCheck::absPathIsInDocRoot(substr($xdest, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort is to use $_SERVER['REQUEST_URI'], well knowing that it does not give the
|
||||
// correct result in all setups (ie "folder method 1").
|
||||
// On nginx, it can even return the path to webp-realizer.php. TODO: Handle that better than now
|
||||
$destRel = SanityCheck::pathWithoutDirectoryTraversal(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
|
||||
if ($destRel) {
|
||||
if (preg_match('#webp-realizer\.php$#', $destRel)) {
|
||||
throw new \Exception(
|
||||
'webp-realizer.php need to know the file path and cannot simply use $_SERVER["REQUEST_URI"] ' .
|
||||
'as that points to itself rather than the image requested. ' .
|
||||
'On Nginx, please add: "&xdestination=x$request_filename" to the URL in the rules in the nginx config ' .
|
||||
'(sorry, the parameter was missing in the rules in the README for a while, but it is back)'
|
||||
);
|
||||
}
|
||||
}
|
||||
$destination = SanityCheck::absPath($docRoot . $destRel);
|
||||
return SanityCheck::absPathIsInDocRoot($destination);
|
||||
}
|
||||
|
||||
private static function getDestinationNoDocRoot() {
|
||||
|
||||
$dirIdOfHtaccess = self::getEnvPassedInRewriteRule('WE_HTACCESS_ID');
|
||||
if ($dirIdOfHtaccess === false) {
|
||||
$dirIdOfHtaccess = SanityCheck::noControlChars($_GET['htaccess-id']);
|
||||
}
|
||||
|
||||
if (!in_array($dirIdOfHtaccess, ['uploads', 'cache'])) {
|
||||
throw new \Exception('invalid htaccess directory id argument. It must be either "uploads" or "cache".');
|
||||
}
|
||||
|
||||
|
||||
// First try ENV
|
||||
$destinationRelHtaccess = self::getEnvPassedInRewriteRule('WE_DESTINATION_REL_HTACCESS');
|
||||
|
||||
// Otherwise use query-string
|
||||
if ($destinationRelHtaccess === false) {
|
||||
if (isset($_GET['xdestination-rel-htaccess'])) {
|
||||
$x = SanityCheck::noControlChars($_GET['xdestination-rel-htaccess']);
|
||||
$destinationRelHtaccess = SanityCheck::pathWithoutDirectoryTraversal(substr($x, 1));
|
||||
} else {
|
||||
throw new \Exception('Argument for destination path is missing');
|
||||
}
|
||||
}
|
||||
|
||||
$destinationRelHtaccess = SanityCheck::pathWithoutDirectoryTraversal($destinationRelHtaccess);
|
||||
|
||||
$imageRoots = self::getImageRootsDef();
|
||||
if ($dirIdOfHtaccess == 'uploads') {
|
||||
return $imageRoots->byId('uploads')->getAbsPath() . '/' . $destinationRelHtaccess;
|
||||
} elseif ($dirIdOfHtaccess == 'cache') {
|
||||
return $imageRoots->byId('wp-content')->getAbsPath() . '/webp-express/webp-images/' . $destinationRelHtaccess;
|
||||
}
|
||||
/*
|
||||
$pathTokens = explode('/', $destinationRelCacheRoot);
|
||||
$imageRootId = array_shift($pathTokens);
|
||||
$destinationRelSpecificCacheRoot = implode('/', $pathTokens);
|
||||
|
||||
$imageRootId = SanityCheck::pregMatch(
|
||||
'#^[a-z\-]+$#',
|
||||
$imageRootId,
|
||||
'The image root ID is not a valid root id'
|
||||
);
|
||||
|
||||
// TODO: Validate that the root id is in scope
|
||||
|
||||
if (count($pathTokens) == 0) {
|
||||
throw new \Exception('invalid destination argument. It must contain dashes.');
|
||||
}
|
||||
|
||||
return $imageRoots->byId($imageRootId)->getAbsPath() . '/' . $destinationRelSpecificCacheRoot;
|
||||
|
||||
/*
|
||||
if ($imageRootId !== false) {
|
||||
|
||||
//$imageRootId = self::getEnvPassedInRewriteRule('WE_IMAGE_ROOT_ID');
|
||||
if ($imageRootId !== false) {
|
||||
$imageRootId = SanityCheck::pregMatch('#^[a-z\-]+$#', $imageRootId, 'The image root ID passed in ENV is not a valid root-id');
|
||||
|
||||
$destinationRelImageRoot = self::getEnvPassedInRewriteRule('WE_DESTINATION_REL_IMAGE_ROOT');
|
||||
if ($destinationRelImageRoot !== false) {
|
||||
$destinationRelImageRoot = SanityCheck::pathWithoutDirectoryTraversal($destinationRelImageRoot);
|
||||
}
|
||||
$imageRoots = self::getImageRootsDef();
|
||||
return $imageRoots->byId($imageRootId)->getAbsPath() . '/' . $destinationRelImageRoot;
|
||||
}
|
||||
|
||||
if (isset($_GET['xdestination-rel-image-root'])) {
|
||||
$xdestinationRelImageRoot = SanityCheck::noControlChars($_GET['xdestination-rel-image-root']);
|
||||
$destinationRelImageRoot = SanityCheck::pathWithoutDirectoryTraversal(substr($xdestinationRelImageRoot, 1));
|
||||
|
||||
$imageRootId = SanityCheck::noControlChars($_GET['image-root-id']);
|
||||
SanityCheck::pregMatch('#^[a-z\-]+$#', $imageRootId, 'Not a valid root-id');
|
||||
|
||||
$imageRoots = self::getImageRootsDef();
|
||||
return $imageRoots->byId($imageRootId)->getAbsPath() . '/' . $destinationRelImageRoot;
|
||||
}
|
||||
|
||||
throw new \Exception('Argument for destination file missing');
|
||||
//WE_DESTINATION_REL_IMG_ROOT*/
|
||||
|
||||
/*
|
||||
$destAbs = SanityCheck::noControlChars(self::getEnvPassedInRewriteRule('WEDESTINATIONABS'));
|
||||
if ($destAbs !== false) {
|
||||
return SanityCheck::pathWithoutDirectoryTraversal($destAbs);
|
||||
}
|
||||
|
||||
// Check querystring (relative path)
|
||||
if (isset($_GET['xdest-rel-to-root-id'])) {
|
||||
$xdestRelToRootId = SanityCheck::noControlChars($_GET['xdest-rel-to-root-id']);
|
||||
$destRelToRootId = SanityCheck::pathWithoutDirectoryTraversal(substr($xdestRelToRootId, 1));
|
||||
|
||||
$rootId = SanityCheck::noControlChars($_GET['root-id']);
|
||||
SanityCheck::pregMatch('#^[a-z]+$#', $rootId, 'Not a valid root-id');
|
||||
return self::getRootPathById($rootId) . '/' . $destRelToRootId;
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
private static function getDestination() {
|
||||
self::$checking = 'destination path';
|
||||
if (self::$usingDocRoot) {
|
||||
$destination = self::getDestinationDocRoot();
|
||||
} else {
|
||||
$destination = self::getDestinationNoDocRoot();
|
||||
}
|
||||
SanityCheck::pregMatch('#\.webp$#', $destination, 'Does not end with .webp');
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
private static function processRequestNoTryCatch() {
|
||||
|
||||
self::loadConfig();
|
||||
|
||||
$options = self::$options;
|
||||
$wodOptions = self::$wodOptions;
|
||||
$serveOptions = $options['webp-convert'];
|
||||
$convertOptions = &$serveOptions['convert'];
|
||||
//echo '<pre>' . print_r($wodOptions, true) . '</pre>'; exit;
|
||||
|
||||
|
||||
// Validate that WebPExpress was configured to redirect to this conversion script
|
||||
// (but do not require that for Nginx)
|
||||
// ------------------------------------------------------------------------------
|
||||
self::$checking = 'settings';
|
||||
if (stripos($_SERVER["SERVER_SOFTWARE"], 'nginx') === false) {
|
||||
if (!isset($wodOptions['enable-redirection-to-webp-realizer']) || ($wodOptions['enable-redirection-to-webp-realizer'] === false)) {
|
||||
throw new ValidateException('Redirection to webp realizer is not enabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Get destination
|
||||
// --------------------------------------------
|
||||
self::$checking = 'destination';
|
||||
// Decode URL in case file contains encoded symbols (#413)
|
||||
$destination = urldecode(self::getDestination());
|
||||
|
||||
//self::exitWithError($destination);
|
||||
|
||||
// Validate source path
|
||||
// --------------------------------------------
|
||||
$checking = 'source path';
|
||||
$source = ConvertHelperIndependent::findSource(
|
||||
$destination,
|
||||
$wodOptions['destination-folder'],
|
||||
$wodOptions['destination-extension'],
|
||||
self::$usingDocRoot ? 'doc-root' : 'image-roots',
|
||||
self::$webExpressContentDirAbs,
|
||||
self::getImageRootsDef()
|
||||
);
|
||||
//self::exitWithError('source:' . $source);
|
||||
//echo '<h3>destination:</h3> ' . $destination . '<h3>source:</h3>' . $source; exit;
|
||||
|
||||
if ($source === false) {
|
||||
header('X-WebP-Express-Error: webp-realizer.php could not find an existing jpg/png that corresponds to the webp requested', true);
|
||||
|
||||
$protocol = isset($_SERVER["SERVER_PROTOCOL"]) ? $_SERVER["SERVER_PROTOCOL"] : 'HTTP/1.0';
|
||||
header($protocol . " 404 Not Found");
|
||||
die();
|
||||
//echo 'destination requested:<br><i>' . $destination . '</i>';
|
||||
}
|
||||
//$source = SanityCheck::absPathExistsAndIsFileInDocRoot($source);
|
||||
|
||||
// Done with sanitizing, lets get to work!
|
||||
// ---------------------------------------
|
||||
$serveOptions['add-vary-header'] = false;
|
||||
$serveOptions['fail'] = '404';
|
||||
$serveOptions['fail-when-fail-fails'] = '404';
|
||||
$serveOptions['serve-image']['headers']['vary-accept'] = false;
|
||||
|
||||
$loggingEnabled = (isset($wodOptions['enable-logging']) ? $wodOptions['enable-logging'] : true);
|
||||
$logDir = ($loggingEnabled ? self::$webExpressContentDirAbs . '/log' : null);
|
||||
|
||||
ConvertHelperIndependent::serveConverted(
|
||||
$source,
|
||||
$destination,
|
||||
$serveOptions,
|
||||
$logDir,
|
||||
'Conversion triggered with the conversion script (wod/webp-realizer.php)'
|
||||
);
|
||||
|
||||
BiggerThanSourceDummyFiles::updateStatus(
|
||||
$source,
|
||||
$destination,
|
||||
self::$webExpressContentDirAbs,
|
||||
self::getImageRootsDef(),
|
||||
$wodOptions['destination-folder'],
|
||||
$wodOptions['destination-extension']
|
||||
);
|
||||
|
||||
self::fixConfigIfEwwwDiscoveredNonFunctionalApiKeys();
|
||||
}
|
||||
|
||||
public static function processRequest() {
|
||||
try {
|
||||
self::processRequestNoTryCatch();
|
||||
} catch (SanityException $e) {
|
||||
self::exitWithError('Sanity check failed for ' . self::$checking . ': '. $e->getMessage());
|
||||
} catch (ValidateException $e) {
|
||||
self::exitWithError('Validation failed for ' . self::$checking . ': '. $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
self::exitWithError('Error occured while calculating ' . self::$checking . ': '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
252
lib/classes/WodConfigLoader.php
Normal file
252
lib/classes/WodConfigLoader.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?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'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user