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:
2025-09-23 10:22:32 +02:00
commit 37cf714058
553 changed files with 55249 additions and 0 deletions

46
lib/classes/Actions.php Normal file
View 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
View 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
View 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.
);
}
}

View 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);
}*/
}

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

View 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 );
*/
}
}
}

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

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

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

View 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
View 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
View 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
View 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
View 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();
}
}

View 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
View 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
View 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;
}
}

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

View 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();
}
}

View 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
View 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;
}
}

View 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']
);
}
}

View 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);
}*/
}

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

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

View 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');
}
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

View 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
View 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'
);
}
}
}
}

View 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;
}
}

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

View 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';
}
}

View 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
View 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
View 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');
}
}
}

View 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');
}
}

View 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>.'
);
}
}

View 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;
}
}
}

View 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');
}
}

View 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
View 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
View 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;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace WebPExpress;
class SanityException extends \Exception
{
}

118
lib/classes/SelfTest.php Normal file
View 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();
}
}

View 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;
}
}

View 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];
}
}

View 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();
}
}

View 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();
}
}

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

View File

@@ -0,0 +1,7 @@
<?php
namespace WebPExpress;
class ValidateException extends \Exception
{
}

659
lib/classes/WCFMApi.php Normal file
View 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
View 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">';
}
}

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

View 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());
}
}
}
}

View 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());
}
}
}

View 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'
);
}
}
}