| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | -if (!defined('WPO_VERSION')) die('No direct access allowed'); | |
| 3 | +if (!defined('ABSPATH')) die('No direct access allowed'); | |
| 4 | 4 | |
| 5 | 5 | use \WebPConvert\Convert\ConverterFactory; |
| 6 | 6 | |
| 7 | 7 | require_once WPO_PLUGIN_MAIN_PATH . 'vendor/autoload.php'; |
| @@ -10,31 +10,67 @@ | ||
| 10 | 10 | |
| 11 | 11 | class WPO_WebP_Utils { |
| 12 | 12 | |
| 13 | 13 | /** |
| 14 | - * Determines whether we can do webp conversion or not | |
| 14 | + * Minimum Firefox version that supports WebP images. | |
| 15 | 15 | * |
| 16 | + * @see https://caniuse.com/webp | |
| 17 | + */ | |
| 18 | + const MIN_FIREFOX_VERSION_FOR_WEBP = '65.0.0'; | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * Warning message patterns to suppress during WebP conversion. | |
| 22 | + * | |
| 23 | + * @var array<string> | |
| 24 | + */ | |
| 25 | + private static $suppressed_warning_patterns = array( | |
| 26 | + '/unlink\(.+\): No such file or directory/', | |
| 27 | + '/rename\(.+\): No such file or directory/', | |
| 28 | + '/filesize\(\): stat failed for/', | |
| 29 | + ); | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * Determines whether we can do webp conversion or not. | |
| 33 | + * | |
| 16 | 34 | * @return bool |
| 17 | 35 | */ |
| 18 | - public static function can_do_webp_conversion() { | |
| 19 | - $webp_conversion = WP_Optimize()->get_options()->get_option('webp_conversion', false); | |
| 20 | - $webp_converters = WP_Optimize()->get_options()->get_option('webp_converters', false); | |
| 36 | + public static function can_do_webp_conversion(): bool { | |
| 37 | + $options = WP_Optimize()->get_options(); | |
| 38 | + $webp_conversion = $options->get_option('webp_conversion'); | |
| 39 | + $webp_converters = $options->get_option('webp_converters'); | |
| 40 | + | |
| 21 | 41 | return $webp_conversion && !empty($webp_converters); |
| 22 | 42 | } |
| 23 | 43 | |
| 24 | 44 | /** |
| 25 | - * Convert given image file to webp format | |
| 45 | + * Convert given image file to webp format. | |
| 26 | 46 | * |
| 27 | - * @param string $source Path of image file | |
| 47 | + * @param string $source Path of image file. | |
| 48 | + * @param bool $convert_if_exists Whether to convert if the webp file already exists or not. | |
| 28 | 49 | * |
| 29 | 50 | * @return void |
| 30 | 51 | */ |
| 31 | - public static function do_webp_conversion($source) { | |
| 32 | - $webp_converter = new WPO_WebP_Convert(); | |
| 52 | + public static function do_webp_conversion($source, $convert_if_exists = false): void { | |
| 53 | + if (file_exists(self::get_destination_path($source)) && !$convert_if_exists) { | |
| 54 | + return; | |
| 55 | + } | |
| 56 | + | |
| 57 | + $webp_converter = WPO_WebP_Convert::get_instance(); | |
| 33 | 58 | $webp_converter->convert($source); |
| 34 | 59 | } |
| 35 | 60 | |
| 36 | 61 | /** |
| 62 | + * Returns the destination full path. | |
| 63 | + * | |
| 64 | + * @param string $source Path of the source file. | |
| 65 | + * | |
| 66 | + * @return string Path of destination file. | |
| 67 | + */ | |
| 68 | + public static function get_destination_path($source): string { | |
| 69 | + return dirname($source) . '/' . basename($source) . '.webp'; | |
| 70 | + } | |
| 71 | + | |
| 72 | + /** | |
| 37 | 73 | * Converts an image to WebP format using a specified converter. |
| 38 | 74 | * |
| 39 | 75 | * This method acts as a wrapper for the `WebPConvert\Convert\Converters\AbstractConverter::doConvert()` method |
| 40 | 76 | * from the `webp-convert` library. |
| @@ -41,46 +77,109 @@ | ||
| 41 | 77 | * |
| 42 | 78 | * @param string $converter The converter to be used for the conversion process. |
| 43 | 79 | * @param string $source The path to the source image file. |
| 44 | 80 | * @param string $destination The path to the destination WebP image file. |
| 81 | + * | |
| 82 | + * @return void | |
| 45 | 83 | */ |
| 46 | - public static function perform_webp_conversion($converter, $source, $destination) { | |
| 47 | - $converter_instance = ConverterFactory::makeConverter( | |
| 48 | - $converter, | |
| 49 | - $source, | |
| 50 | - $destination | |
| 51 | - ); | |
| 84 | + public static function perform_webp_conversion($converter, $source, $destination): void { | |
| 85 | + set_error_handler(array(__CLASS__, 'handle_webp_conversion_warnings'), E_WARNING); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- This is needed in order to suppress PHP warnings thrown by third party library | |
| 52 | 86 | |
| 53 | - set_error_handler(array(__CLASS__, 'handle_webp_conversion_warnings'), E_WARNING); | |
| 87 | + try { | |
| 88 | + $converter_instance = ConverterFactory::makeConverter( | |
| 89 | + $converter, | |
| 90 | + $source, | |
| 91 | + $destination | |
| 92 | + ); | |
| 54 | 93 | |
| 55 | - $converter_instance->doConvert(); | |
| 94 | + $converter_instance->doConvert(); | |
| 56 | 95 | |
| 57 | - restore_error_handler(); | |
| 96 | + restore_error_handler(); | |
| 97 | + } catch (Exception $e) { | |
| 98 | + restore_error_handler(); // Make sure we restore even if an exception occurs | |
| 99 | + | |
| 100 | + $log_details = 'Converter: '.$converter. ', '.WP_Optimize_Utils::get_wp_relative_path($source).' => '.WP_Optimize_Utils::get_wp_relative_path($destination); | |
| 101 | + WP_Optimize_WebP::get_instance()->log('Failed to convert to WebP: '. $log_details); | |
| 102 | + throw new Exception(esc_html(__('Failed to convert to WebP.', 'wp-optimize'))); | |
| 103 | + } | |
| 58 | 104 | } |
| 59 | 105 | |
| 60 | 106 | /** |
| 61 | 107 | * Custom error handler for handling PHP warnings during the WebP conversion process. |
| 62 | 108 | * |
| 63 | - * @param int $errno The level of the error raised, as an integer. | |
| 109 | + * @param int $errno The level of the error raised. | |
| 64 | 110 | * @param string $errstr The error message. |
| 65 | 111 | * |
| 66 | 112 | * @return bool |
| 67 | 113 | */ |
| 68 | - public static function handle_webp_conversion_warnings($errno, $errstr) { | |
| 69 | - $patterns = array( | |
| 70 | - '/unlink\(.+\): No such file or directory/', | |
| 71 | - '/rename\(.+\): No such file or directory/', | |
| 72 | - '/filesize\(\): stat failed for/', | |
| 114 | + public static function handle_webp_conversion_warnings($errno, $errstr): bool { | |
| 115 | + foreach (self::$suppressed_warning_patterns as $pattern) { | |
| 116 | + if (preg_match($pattern, $errstr)) { | |
| 117 | + return true; | |
| 118 | + } | |
| 119 | + } | |
| 120 | + | |
| 121 | + return false; | |
| 122 | + } | |
| 123 | + | |
| 124 | + /** | |
| 125 | + * Decide whether the browser requesting the URL can accept webp images or not. | |
| 126 | + * | |
| 127 | + * @return bool | |
| 128 | + */ | |
| 129 | + public static function is_browser_accepting_webp(): bool { | |
| 130 | + if (self::browser_accepts_webp_header()) { | |
| 131 | + return true; | |
| 132 | + } | |
| 133 | + | |
| 134 | + return self::user_agent_supports_webp(); | |
| 135 | + } | |
| 136 | + | |
| 137 | + /** | |
| 138 | + * Check if the HTTP Accept header indicates WebP support. | |
| 139 | + * | |
| 140 | + * @return bool | |
| 141 | + */ | |
| 142 | + private static function browser_accepts_webp_header(): bool { | |
| 143 | + $http_accept = TeamUpdraft\WP_Optimize\Includes\Fragments\fetch_superglobal( | |
| 144 | + 'server', | |
| 145 | + 'HTTP_ACCEPT', | |
| 146 | + 'string', | |
| 147 | + 'sanitize_text_field', | |
| 148 | + '' | |
| 73 | 149 | ); |
| 74 | 150 | |
| 75 | - foreach ($patterns as $pattern) { | |
| 76 | - if (preg_match($pattern, $errstr)) { | |
| 77 | - // Suppress the warning by returning true | |
| 151 | + if (!is_string($http_accept)) { | |
| 152 | + return false; | |
| 153 | + } | |
| 154 | + | |
| 155 | + return false !== strpos($http_accept, 'image/webp'); | |
| 156 | + } | |
| 157 | + | |
| 158 | + /** | |
| 159 | + * Check if the user agent indicates WebP support based on known browser versions. | |
| 160 | + * | |
| 161 | + * @return bool | |
| 162 | + */ | |
| 163 | + private static function user_agent_supports_webp(): bool { | |
| 164 | + $user_agent = TeamUpdraft\WP_Optimize\Includes\Fragments\fetch_superglobal( | |
| 165 | + 'server', | |
| 166 | + 'HTTP_USER_AGENT', | |
| 167 | + 'string', | |
| 168 | + 'sanitize_text_field', | |
| 169 | + '' | |
| 170 | + ); | |
| 171 | + | |
| 172 | + if (empty($user_agent) || !is_string($user_agent)) { | |
| 173 | + return false; | |
| 174 | + } | |
| 175 | + | |
| 176 | + if (preg_match('/Firefox\/([\d\.]+[a-z\d]*)/', $user_agent, $matches)) { | |
| 177 | + if (version_compare(self::MIN_FIREFOX_VERSION_FOR_WEBP, $matches[1], '<=')) { | |
| 78 | 178 | return true; |
| 79 | 179 | } |
| 80 | 180 | } |
| 81 | 181 | |
| 82 | - // For other PHP warnings, use the default PHP error handler by returning false | |
| 83 | 182 | return false; |
| 84 | 183 | } |
| 85 | 184 | } |
| 86 | 185 | |