PluginProbe ʕ •ᴥ•ʔ
Robin Image Optimizer – Unlimited Image Optimization, WebP & AVIF / trunk
Robin Image Optimizer – Unlimited Image Optimization, WebP & AVIF vtrunk
2.0.5 trunk 1.3.7 1.4.0 1.4.1 1.4.2 1.4.6 1.5.0 1.5.3 1.5.6 1.5.8 1.6.5 1.6.6 1.6.9 1.7.0 1.7.4 1.8.1 1.8.2 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4
robin-image-optimizer / libs / addons / includes / classes / webp / class-webp-delivery.php
robin-image-optimizer / libs / addons / includes / classes / webp Last commit date
vendor 5 months ago class-webp-api.php 5 months ago class-webp-delivery.php 3 months ago class-webp-html-image-urls-replacer.php 5 months ago class-webp-html-picture-tags.php 5 months ago class-webp-listener.php 3 months ago class-webp-server.php 5 months ago composer.json 3 years ago composer.lock 3 years ago
class-webp-delivery.php
393 lines
1 <?php
2
3 namespace WRIO\WEBP\HTML;
4
5 // Exit if accessed directly
6 use WRIO_Logger;
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Class WRIO\WEBP\HTML\Delivery converts and replace JPEG & PNG images within HTML doc.
14 *
15 * Images converted via third-party service, saved locally and then replaced based on parsed DOM <img>, or other elements.
16 *
17 * @link https://css-tricks.com/using-webp-images/
18 * @link https://dev.opera.com/articles/responsive-images/#different-image-types-use-case
19 * @link https://ru.wordpress.org/plugins/webp-express/
20 * @link https://github.com/rosell-dk/
21 * @version 1.0
22 */
23 class Delivery {
24
25 /**
26 * Legacy constant for no delivery mode.
27 *
28 * @var string
29 */
30 const NONE_DELIVERY_MODE = 'none';
31 const DEFAULT_DELIVERY_MODE = 'picture';
32 const PICTURE_DELIVERY_MODE = 'picture';
33 const URL_DELIVERY_MODE = 'url';
34 const REDIRECT_DELIVERY_MODE = 'redirect';
35
36 /**
37 * WRIO_Webp constructor.
38 */
39 public function __construct() {
40 $this->init();
41 }
42
43 /**
44 * Initiate the class.
45 */
46 public function init() {
47
48 if ( ! static::should_use_converted_images() ) {
49 return;
50 }
51
52 if ( \WRIO_Plugin::app()->is_keep_error_log_on_frontend() ) {
53 \WRIO_Plugin::app()->logger->info( sprintf( 'WebP/AVIF option enabled and browser "%s" is supported, ready to process buffer', isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '*undefined*' ) );
54 \WRIO_Plugin::app()->logger->info( sprintf( 'WebP/AVIF delivery mode: %s', static::get_delivery_mode() ) );
55 }
56
57 /*
58 TODO:
59 Which hook should we use, and should we make it optional?
60 - Cache enabler uses 'template_redirect'
61 - ShortPixes uses 'init'
62
63 We go with template_redirect now, because it is the "innermost".
64 This lowers the risk of problems with plugins used rewriting URLs to point to CDN.
65 (We need to process the output *before* the other plugin has rewritten the URLs,
66 if the "Only for webps that exists" feature is enabled)
67 */
68
69 if ( static::is_delivery_mode_enabled() ) {
70 // Use template_redirect for frontend output buffering (fires during page render)
71 // This is more reliable than 'init' as it only fires on frontend requests
72 add_action( 'template_redirect', [ $this, 'process_buffer' ], 1 );
73 }
74
75 if ( static::is_picture_delivery_mode() ) {
76 add_action( 'wp_head', [ $this, 'add_picture_fill_js' ] );
77 }
78 }
79
80 /**
81 * Check whether any format conversion is enabled (WebP or AVIF).
82 *
83 * @return bool
84 */
85 public static function should_use_converted_images() {
86 return \WRIO_Format_Converter_Factory::is_format_conversion_enabled();
87 }
88
89 /**
90 * @return bool
91 * @since 1.0.4
92 */
93 public static function is_redirect_delivery_mode() {
94 return self::REDIRECT_DELIVERY_MODE === static::get_delivery_mode();
95 }
96
97 /**
98 * @return bool
99 * @since 1.0.4
100 */
101 public static function is_picture_delivery_mode() {
102 return self::PICTURE_DELIVERY_MODE === static::get_delivery_mode();
103 }
104
105 /**
106 * @return bool
107 * @since 1.0.4
108 */
109 public static function is_url_delivery_mode() {
110 return self::URL_DELIVERY_MODE === static::get_delivery_mode();
111 }
112
113 /**
114 * Check whether any delivery mode is enabled that modifies HTML output.
115 *
116 * @return bool
117 */
118 public static function is_delivery_mode_enabled() {
119 $delivery_mode = static::get_delivery_mode();
120
121 return in_array(
122 $delivery_mode,
123 [
124 self::PICTURE_DELIVERY_MODE,
125 self::URL_DELIVERY_MODE,
126 self::NONE_DELIVERY_MODE,
127 ],
128 true
129 );
130 }
131
132 /**
133 * @return string
134 * @since 1.0.4
135 */
136 public static function get_delivery_mode() {
137 $delivery_mode = \WRIO_Plugin::app()->getPopulateOption( 'webp_delivery_mode' );
138
139 if ( ! empty( $delivery_mode ) ) {
140 return $delivery_mode;
141 }
142
143 return self::DEFAULT_DELIVERY_MODE;
144 }
145
146 /**
147 * @since 1.0.4
148 */
149 public function add_picture_fill_js() {
150 // Don't do anything with the RSS feed.
151 // - and no need for PictureJs in the admin
152 if ( is_feed() || is_admin() ) {
153 return;
154 }
155
156 echo '<script>' . 'document.createElement( "picture" );' . 'if(!window.HTMLPictureElement && document.addEventListener) {' . 'window.addEventListener("DOMContentLoaded", function() {' . 'var s = document.createElement("script");' . 's.src = "' . WRIOP_PLUGIN_URL . '/assets/js/picturefill.min.js' . '";' . 'document.body.appendChild(s);' . '});' . '}' . '</script>';
157 }
158
159 /**
160 * Process HTML template buffer.
161 */
162 public function process_buffer() {
163 // template_redirect only fires on frontend, so no need to check is_admin() or AJAX
164 ob_start( [ $this, 'process_alter_html' ] );
165 }
166
167 /**
168 * Process tags to replace those elements which match converted to WebP within buffer.
169 *
170 * @param string $content HTML buffer.
171 *
172 * @return string
173 */
174 public function process_alter_html( $content ) {
175 $raw_content = $content;
176
177 // Don't do anything with the RSS feed.
178 if ( is_feed() || empty( $content ) || ! is_null( json_decode( $content ) ) ) {
179 // WRIO_Plugin::app()->logger->info( "Buffer content is empty, skipping processing" );
180 return $content;
181 }
182 if ( static::is_picture_delivery_mode() ) {
183 if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) {
184 // for AMP pages the <picture> tag is not allowed
185 return $content;
186 }
187
188 require_once WRIOP_PLUGIN_DIR . '/includes/classes/webp/class-webp-html-picture-tags.php';
189 $content = Picture_Tags::replace( $content );
190 } elseif ( static::is_url_delivery_mode() ) {
191 if ( ! is_admin() ) {
192 require_once WRIOP_PLUGIN_DIR . '/includes/classes/webp/class-webp-html-image-urls-replacer.php';
193 $content = Urls_Replacer::replace( $content );
194 }
195 }
196
197 // If the search and replacement are completed with an error, then return the raw content.
198 // If this is not prevented, in case of an error the user will receive a white screen.
199 if ( empty( $content ) ) {
200 if ( \WRIO_Plugin::app()->is_keep_error_log_on_frontend() ) {
201 \WRIO_Plugin::app()->logger->warning( sprintf( 'Failed search and replace urls. Empty result received (%s).', base64_encode( $content ) ) );
202 }
203
204 return $raw_content;
205 }
206
207 return $content;
208 }
209
210 /**
211 * Get url for webp
212 * returns second argument if no webp
213 *
214 * @param string $source_url (ie http://example.com/wp-content/image.jpg)
215 * @param string $return_value_on_fail
216 *
217 * @return string
218 */
219 /**
220 * Get URL for converted format (WebP or AVIF).
221 *
222 * Checks for available converted formats in order of preference (AVIF first, then WebP).
223 *
224 * @param string $source_url Original image URL.
225 * @param string $return_value_on_fail Value to return if conversion not found.
226 *
227 * @return string Converted image URL or fallback value.
228 * @since 1.9.0
229 */
230 public static function get_converted_url( $source_url, $return_value_on_fail ) {
231 $enabled_formats = \WRIO_Format_Converter_Factory::get_enabled_formats();
232
233 if ( empty( $enabled_formats ) || ! static::is_support_format( $source_url ) ) {
234 if ( \WRIO_Plugin::app()->is_keep_error_log_on_frontend() ) {
235 \WRIO_Plugin::app()->logger->warning( sprintf( "Skipped converted image lookup. Original image format is not supported for conversion, or converted image delivery is disabled.\r\nSource url: %s", $source_url ) );
236 }
237
238 return $return_value_on_fail;
239 }
240
241 if ( ! preg_match( '#^https?://#', $source_url ) ) {
242 $source_url = wrio_rel_to_abs_url( $source_url );
243 }
244
245 $is_wpmedia_url = static::is_wpmedia_url( $source_url );
246
247 // If the image is stored on a remote server, need to skip it
248 if ( static::is_external_url( $source_url ) && ! $is_wpmedia_url ) {
249 if ( \WRIO_Plugin::app()->is_keep_error_log_on_frontend() ) {
250 \WRIO_Plugin::app()->logger->warning( sprintf( "Skipped converted image lookup. Image is hosted on a remote server.\r\nSource url: %s", $source_url ) );
251 }
252
253 return $return_value_on_fail;
254 }
255
256 if ( $is_wpmedia_url ) {
257 $upload_dir = wp_get_upload_dir();
258
259 $source_parts = wp_parse_url( $source_url );
260 $base_parts = wp_parse_url( $upload_dir['baseurl'] );
261
262 // If URL parsing fails, bail
263 if ( empty( $source_parts['path'] ) || empty( $base_parts['path'] ) ) {
264 return $return_value_on_fail;
265 }
266
267 // Must be same host to treat it as local upload (ignore scheme)
268 $source_host = $source_parts['host'] ?? '';
269 $base_host = $base_parts['host'] ?? '';
270
271 if ( $source_host && $base_host && strtolower( $source_host ) !== strtolower( $base_host ) ) {
272 return $return_value_on_fail;
273 }
274
275 // Path must start with uploads base path
276 $base_path = rtrim( $base_parts['path'], '/' ); // eg /app/uploads
277 $source_path = $source_parts['path']; // eg /app/uploads/2025/12/demo.png
278
279 if ( strpos( $source_path, $base_path . '/' ) !== 0 ) {
280 return $return_value_on_fail;
281 }
282
283 // Convert URL path to filesystem path
284 $relative = ltrim( substr( $source_path, strlen( $base_path ) ), '/' );
285 $file_path = trailingslashit( $upload_dir['basedir'] ) . $relative;
286 } else {
287 $file_path = wrio_url_to_abs_path( $source_url );
288 }
289
290 // If you could not find original image, skip it. Perhaps an error
291 // in absolute path formation to the directory where the
292 // image is stored.
293 if ( empty( $file_path ) || ! file_exists( $file_path ) ) {
294 if ( \WRIO_Plugin::app()->is_keep_error_log_on_frontend() ) {
295 \WRIO_Plugin::app()->logger->warning( sprintf( "Skipped converted image lookup. Unable to find the original image on disk.\r\nRelative path: (%s)\r\nSource url: (%s)", $file_path, $source_url ) );
296 }
297
298 return $return_value_on_fail;
299 }
300
301 // Check AVIF first if enabled, then WebP.
302 foreach ( $enabled_formats as $format ) {
303 $extension = '.' . strtolower( $format );
304 $converted_file_path = $file_path . $extension;
305
306 if ( file_exists( $converted_file_path ) ) {
307 return $source_url . $extension;
308 }
309 }
310
311 if ( \WRIO_Plugin::app()->is_keep_error_log_on_frontend() ) {
312 \WRIO_Plugin::app()->logger->warning( sprintf( "Skipped converted image delivery. No converted file was found for the original image.\r\nSource url: %s\r\nChecked formats: %s", $source_url, implode( ', ', $enabled_formats ) ) );
313 }
314
315 return $return_value_on_fail;
316 }
317
318 /**
319 * Get WebP URL (backward compatibility wrapper).
320 *
321 * @param string $source_url Original image URL.
322 * @param string $return_value_on_fail Value to return if WebP not found.
323 *
324 * @return string WebP image URL or fallback value.
325 * @deprecated Use get_converted_url() instead.
326 */
327 public static function get_webp_url( $source_url, $return_value_on_fail ) {
328 return static::get_converted_url( $source_url, $return_value_on_fail );
329 }
330
331 /**
332 * @param string $source_url
333 *
334 * @return bool
335 * @since 1.4.0
336 */
337 protected static function is_wpmedia_url( $source_url ) {
338 $upload_dir = wp_get_upload_dir();
339
340 if ( isset( $upload_dir['error'] ) && $upload_dir['error'] !== false ) {
341 return false;
342 }
343
344 // Normalize both URLs to https for comparison.
345 $source_url_normalized = set_url_scheme( $source_url, 'https' );
346 $baseurl_normalized = set_url_scheme( $upload_dir['baseurl'], 'https' );
347
348 return false !== strpos( $source_url_normalized, $baseurl_normalized );
349 }
350
351 /**
352 * @param string $source_url
353 *
354 * @return bool
355 * @since 1.4.0
356 */
357 protected static function is_support_format( $source_url ) {
358 // Match .jpg, .jpeg, or .png at end of URL (before optional query string)
359 if ( ! preg_match( '#\.(jpe?g|png)($|\?)#i', $source_url ) ) {
360 return false;
361 }
362
363 return true;
364 }
365
366 /**
367 * @param string $source_url
368 *
369 * @return bool
370 * @since 1.4.0
371 */
372 protected static function is_external_url( $source_url ) {
373 if ( strpos( $source_url, get_site_url() ) === false ) {
374 return true;
375 }
376
377 return false;
378 }
379
380 /**
381 * Check whether browser supports WebP or not.
382 *
383 * @return bool
384 */
385 protected static function is_supported_browser() {
386 if ( isset( $_SERVER['HTTP_ACCEPT'] ) && strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' ) !== false || isset( $_SERVER['HTTP_USER_AGENT'] ) && strpos( $_SERVER['HTTP_USER_AGENT'], ' Chrome/' ) !== false ) {
387 return true;
388 }
389
390 return false;
391 }
392 }
393