PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / trunk
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization vtrunk
4.2.14 4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 3.12.0 All 135 releases
← All changes | inc/url_replacer.php +298 -71 2.5.7 → trunk View file →
@@ -1,6 +1,11 @@
1 1 <?php
2 2
3 +use Optimole\Sdk\Optimole;
4 +use Optimole\Sdk\Resource\Image;
5 +use Optimole\Sdk\Resource\ImageProperty\GravityProperty;
6 +use Optimole\Sdk\ValueObject\Position;
7 +
3 8 /**
4 9 * The class handles the url replacements.
5 10 *
6 11 * @package \Optml\Inc
@@ -7,8 +12,9 @@
7 12 * @author Optimole <[email protected]>
8 13 */
9 14 final class Optml_Url_Replacer extends Optml_App_Replacer {
10 15
16 + use Optml_Dam_Offload_Utils;
11 17 use Optml_Validator;
12 18 use Optml_Normalizer;
13 19
14 20 /**
@@ -35,51 +41,9 @@
35 41
36 42 return self::$instance;
37 43 }
38 44
39 - /**
40 - * A filter which turns a local url into an optimized CDN image url or an array of image urls.
41 - *
42 - * @param string $url The url which should be replaced.
43 - *
44 - * @return string Replaced url.
45 - */
46 - public function replace_option_url( $url ) {
47 - if ( empty( $url ) ) {
48 - return $url;
49 - }
50 - // $url might be an array or an json encoded array with urls.
51 - if ( is_array( $url ) || filter_var( $url, FILTER_VALIDATE_URL ) === false ) {
52 - $array = $url;
53 - $encoded = false;
54 45
55 - // it might a json encoded array
56 - if ( is_string( $url ) ) {
57 - $array = json_decode( $url, true );
58 - $encoded = true;
59 - }
60 -
61 - // in case there is an array, apply it recursively.
62 - if ( is_array( $array ) ) {
63 - foreach ( $array as $index => $value ) {
64 - $array[ $index ] = $this->replace_option_url( $value );
65 - }
66 -
67 - if ( $encoded ) {
68 - return json_encode( $array );
69 - }
70 -
71 - return $array;
72 - }
73 -
74 - if ( filter_var( $url, FILTER_VALIDATE_URL ) === false ) {
75 - return $url;
76 - }
77 - }
78 -
79 - return apply_filters( 'optml_content_url', $url );
80 - }
81 -
82 46 /**
83 47 * The initialize method.
84 48 */
85 49 public function init() {
@@ -86,13 +50,9 @@
86 50
87 51 add_filter( 'optml_replace_image', [ $this, 'build_url' ], 10, 2 );
88 52 parent::init();
89 53
90 - Optml_Quality::$default_quality = $this->to_accepted_quality( $this->settings->get_quality() );
91 - Optml_Image::$watermark = new Optml_Watermark( $this->settings->get_site_settings()['watermark'] );
92 - Optml_Resize::$default_enlarge = apply_filters( 'optml_always_enlarge', false );
93 54 add_filter( 'optml_content_url', [ $this, 'build_url' ], 1, 2 );
94 -
95 55 }
96 56
97 57 /**
98 58 * Returns a signed image url authorized to be used in our CDN.
@@ -98,21 +58,26 @@
98 58 * Returns a signed image url authorized to be used in our CDN.
99 59 *
100 60 * @param string $url The url which should be signed.
101 61 * @param array $args Dimension params; Supports `width` and `height`.
102 - *
62 + * @param bool $retried Whether the url has been retried.
103 63 * @return string
104 64 */
105 65 public function build_url(
106 - $url, $args = [
66 + $url,
67 + $args = [
107 68 'width' => 'auto',
108 69 'height' => 'auto',
109 - ]
70 + ],
71 + $retried = false
110 72 ) {
111 73 if ( apply_filters( 'optml_dont_replace_url', false, $url ) ) {
112 74 return $url;
113 75 }
114 76
77 + if ( OPTML_DEBUG ) {
78 + do_action( 'optml_log', 'building url: ' . $url . ' args: ' . print_r( $args, true ) );
79 + }
115 80 $original_url = $url;
116 81
117 82 $is_slashed = strpos( $url, '\/' ) !== false;
118 83
@@ -118,16 +83,63 @@
118 83
119 84 // We do a little hack here, for json unicode chars we first replace them with html special chars,
120 85 // we then strip slashes to normalize the URL and last we convert html special chars back to get a clean URL
121 86 $url = $is_slashed ? html_entity_decode( stripslashes( preg_replace( '/\\\u([\da-fA-F]{4})/', '&#x\1;', $url ) ) ) : ( $url );
122 - if ( strpos( $url, Optml_Config::$service_url ) !== false ) {
87 + $is_uploaded = Optml_Media_Offload::is_not_processed_image( $url );
88 + if ( $is_uploaded === true ) {
89 + $attachment_id = [];
90 + preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id );
91 + if ( ! isset( $attachment_id[1] ) ) {
92 + return $url;
93 + }
94 + $id_and_filename = [];
95 + preg_match( '/\/(' . Optml_Media_Offload::KEYS['uploaded_flag'] . '.*)/', $url, $id_and_filename );
96 + if ( isset( $id_and_filename[0] ) ) {
97 + $id_and_filename = $id_and_filename[0];
98 + }
99 + $sizes = $this->parse_dimension_from_optimized_url( $url );
100 + if ( isset( $sizes[0] ) && $sizes[0] !== false ) {
101 + $args['width'] = $sizes[0];
102 + }
103 + if ( isset( $sizes[1] ) && $sizes[1] !== false ) {
104 + $args['height'] = $sizes[1];
105 + }
106 + $unoptimized_url = false;
107 + if ( $attachment_id[1] === 'media_cloud' ) {
108 + $tmp_url = explode( '/', $id_and_filename, 3 );
109 + if ( isset( $tmp_url[2] ) ) {
110 + $unoptimized_url = $tmp_url[2];
111 + }
112 + } else {
113 + $unoptimized_url = Optml_Media_Offload::get_original_url( (int) $attachment_id[1] );
114 + }
115 + if ( $unoptimized_url !== false ) {
116 + $url = $unoptimized_url;
117 + }
118 + }
119 + if ( isset( $args['force'] ) && $args['force'] === true && strpos( $url, Optml_Config::$service_url ) !== false && ! $this->url_has_dam_flag( $url ) ) {
120 + if ( OPTML_DEBUG ) {
121 + do_action( 'optml_log', 'url is already using optimole: ' . $url );
122 + }
123 + if ( $retried === true ) {
124 + return $original_url;
125 + }
126 + // We retry because the url is already using optimole, but we need to rebuild it because the args might be different.
127 + $pos = strpos( $url, 'http', 8 ); // skip 'https://' or 'http://'
128 + if ( $pos !== false ) {
129 + return $this->build_url( substr( $url, $pos ), $args, true );
130 + }
123 131 return $original_url;
124 132 }
125 133
134 + if ( substr( $url, 0, 2 ) === '//' ) {
135 + $url = ltrim( $url, '/' );
136 + $url = sprintf( '%s://%s', is_ssl() ? 'https' : 'http', $url );
137 + }
138 +
126 139 if ( ! $this->can_replace_url( $url ) ) {
127 140 return $original_url;
128 141 }
129 -
130 142 // Remove any query strings that might affect conversion.
131 143 $url = strtok( $url, '?' );
132 144 $ext = $this->is_valid_mimetype_from_url( $url, self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ] );
133 145 if ( false === $ext ) {
@@ -146,14 +158,31 @@
146 158 if ( ! empty( $this->site_mappings ) ) {
147 159 $url = str_replace( array_keys( $this->site_mappings ), array_values( $this->site_mappings ), $url );
148 160 }
149 161 if ( substr( $url, 0, 2 ) === '//' ) {
150 - $url = sprintf( '%s:%s', is_ssl() ? 'https' : 'http', $url );
162 + $url = ltrim( $url, '/' );
163 + $url = sprintf( '%s://%s', is_ssl() ? 'https' : 'http', $url );
151 164 }
152 - if ( isset( Optml_Config::$image_extensions[ strtolower( $ext ) ] ) ) {
153 - $new_url = $this->normalize_image( $url, $original_url, $args );
165 + $normalized_ext = strtolower( $ext );
166 + $url = esc_url( $url );
167 + if ( empty( $url ) ) {
168 + return $original_url;
169 + }
170 + if ( isset( Optml_Config::$image_extensions[ $normalized_ext ] ) ) {
171 + $new_url = $this->normalize_image( $url, $original_url, $args, $is_uploaded, $normalized_ext );
172 + if ( $is_uploaded ) {
173 + $new_url = str_replace( '/' . $url, $id_and_filename, $new_url );
174 + }
154 175 } else {
155 - $new_url = ( new Optml_Asset( $url, $args, $this->active_cache_buster, $this->is_css_minify_on, $this->is_js_minify_on ) )->get_url();
176 + $asset = Optimole::asset( $url, $this->active_cache_buster_assets );
177 +
178 + if ( stripos( $url, '.css' ) || stripos( $url, '.js' ) ) {
179 + $asset->quality();
180 + }
181 +
182 + $asset->minify( ( $this->is_css_minify_on && str_ends_with( strtolower( $url ), '.css' ) ) || ( $this->is_js_minify_on && str_ends_with( strtolower( $url ), '.js' ) ) );
183 +
184 + return $asset->getUrl();
156 185 }
157 186 return $is_slashed ? addcslashes( $new_url, '/' ) : $new_url;
158 187 }
159 188
@@ -165,15 +194,20 @@
165 194 * @param array $args Args.
166 195 *
167 196 * @return string
168 197 */
169 - private function normalize_image( $url, $original_url, $args ) {
198 + private function normalize_image( $url, $original_url, $args, $is_uploaded = false, $ext = '' ) {
170 199
171 200 $new_url = $this->strip_image_size_from_url( $url );
172 201
173 - if ( $new_url !== $url ) {
202 + if ( $new_url !== $url || $is_uploaded === true ) {
174 203 if ( ! isset( $args['quality'] ) || $args['quality'] !== 'eco' ) {
175 - list( $args['width'], $args['height'], $crop ) = $this->parse_dimensions_from_filename( $url );
204 + if ( $is_uploaded === true ) {
205 + $crop = false;
206 + }
207 + if ( $is_uploaded !== true ) {
208 + list($args['width'], $args['height'], $crop) = $this->parse_dimensions_from_filename( $url );
209 + }
176 210 if ( ! $crop ) {
177 211 $sizes2crop = self::size_to_crop();
178 212 $crop = isset( $sizes2crop[ $args['width'] . $args['height'] ] ) ? $sizes2crop[ $args['width'] . $args['height'] ] : false;
179 213 }
@@ -182,35 +216,124 @@
182 216 }
183 217 }
184 218 $url = $new_url;
185 219 }
220 +
221 + if ( ! isset( $args['width'] ) ) {
222 + $args['width'] = $this->limit_dimensions_enabled ? $this->limit_width : 'auto';
223 + }
224 + if ( ! isset( $args['height'] ) ) {
225 + $args['height'] = $this->limit_dimensions_enabled ? $this->limit_height : 'auto';
226 + }
227 +
186 228 $args['width'] = (int) $args['width'];
187 229 $args['height'] = (int) $args['height'];
188 - if ( $args['width'] > 0 && $args['height'] > 0 ) {
189 - list( $args['width'], $args['height'] ) = wp_constrain_dimensions( $args['width'], $args['height'], $this->max_width, $this->max_height );
190 - } elseif ( $args['width'] > 0 ) {
191 - $args['width'] = $args['width'] > $this->max_width ? $this->max_width : $args['width'];
192 - } elseif ( $args['height'] > 0 ) {
193 - $args['height'] = $args['height'] > $this->max_height ? $this->max_height : $args['height'];
230 +
231 + if ( $this->limit_dimensions_enabled && $args['height'] !== 0 && $args['width'] !== 0 ) {
232 + if ( $args['width'] > $this->limit_width || $args['height'] > $this->limit_height ) {
233 + $scale = min( $this->limit_width / $args['width'], $this->limit_height / $args['height'] );
234 +
235 + if ( $scale < 1 ) {
236 + $args['width'] = (int) floor( $scale * $args['width'] );
237 + $args['height'] = (int) floor( $scale * $args['height'] );
238 + }
239 + }
240 +
241 + if ( isset( $args['resize'], $args['resize']['enlarge'] ) ) {
242 + $args['resize']['enlarge'] = false;
243 + }
194 244 }
245 +
246 + if ( empty( $args['quality'] ) ) {
247 + $args['quality'] = $this->to_accepted_quality( $this->settings->get_quality() );
248 + }
249 +
195 250 if ( isset( $args['resize'], $args['resize']['gravity'] ) && $this->settings->is_smart_cropping() ) {
196 - $args['resize']['gravity'] = Optml_Resize::GRAVITY_SMART;
251 + $args['resize']['gravity'] = GravityProperty::SMART;
197 252 }
253 +
254 + $is_retina_enabled = $this->settings->get( 'retina_images' ) !== 'disabled';
255 + if ( ! $is_retina_enabled ) {
256 + $max_dimension = max( $args['width'], $args['height'] );
257 + $should_apply_dpr = false;
258 +
259 + if ( $max_dimension > 0 && $max_dimension < 150 ) {
260 + $should_apply_dpr = true;
261 + }
262 +
263 + $should_apply_dpr = apply_filters( 'optml_should_apply_dpr', $should_apply_dpr, $args, $url );
264 +
265 + if ( $should_apply_dpr && ! isset( $args['dpr'] ) ) {
266 + $args['dpr'] = 2;
267 + }
268 + }
269 +
198 270 $args = apply_filters( 'optml_image_args', $args, $original_url );
271 + $image = Optimole::image( apply_filters( 'optml_processed_url', $url ), self::get_active_cache_booster( $url, $this->active_cache_buster ) );
199 272
200 - $arguments = [
201 - 'apply_watermark' => apply_filters( 'optml_apply_watermark_for', true, $url ),
202 - ];
273 + $image->width( ! empty( $args['width'] ) && is_int( $args['width'] ) ? $args['width'] : 'auto' );
274 + $image->height( ! empty( $args['height'] ) && is_int( $args['height'] ) ? $args['height'] : 'auto' );
203 275
204 - if ( isset( $args['format'] ) && ! empty( $args['format'] ) ) {
205 - $arguments['format'] = $args['format'];
276 + $image->quality( $args['quality'] );
277 +
278 + if ( ! empty( $args['resize'] ) ) {
279 + $this->apply_resize( $image, $args['resize'] );
280 + } elseif ( $this->settings->is_smart_cropping() ) {
281 + // If smart cropping is enabled and no resize is set, we apply smart focus since the resize can be triggered from the JS library.
282 + $image->smartFocus();
206 283 }
207 284
208 - return ( new Optml_Image( $url, $args, $this->active_cache_buster ) )->get_url( $arguments );
285 + if ( apply_filters( 'optml_apply_watermark_for', true, $url ) ) {
286 + $this->apply_watermark( $image );
287 + }
209 288
289 + if ( ! empty( $args['format'] ) ) {
290 + $image->format( (string) $args['format'] );
291 + } elseif ( $this->settings->is_best_format() ) {
292 + // If format is not already set, we use best format if it's enabled.
293 + $image->format( 'best' );
294 + }
295 +
296 + if ( $this->settings->get( 'strip_metadata' ) === 'disabled' ) {
297 + $image->stripMetadata( false );
298 + }
299 +
300 + if ( ! apply_filters( 'optml_should_avif_ext', true, $ext, $original_url ) || $this->settings->get( 'avif' ) === 'disabled' ) {
301 + $image->ignoreAvif();
302 + }
303 +
304 + if ( apply_filters( 'optml_keep_copyright', false ) === true ) {
305 + $image->keepCopyright();
306 + }
307 +
308 + if ( $this->is_dam_url( $image->getSource() ) ) {
309 + return $this->get_dam_url( $image );
310 + }
311 +
312 + $offloaded_id = $this->is_offloaded_url( $image->getSource() );
313 +
314 + if ( isset( $args['dpr'] ) && $args['dpr'] > 1 ) {
315 + $image->dpr( $args['dpr'] );
316 + }
317 + if ( $offloaded_id !== 0 ) {
318 + return $this->get_offloaded_url( $offloaded_id, $image->getUrl(), $image->getSource() );
319 + }
320 +
321 + return $image->getUrl();
210 322 }
211 323
212 324 /**
325 + * Get the active cache booster.
326 + *
327 + * @param string $url The URL.
328 + * @param string $main_cache_buster The default value.
329 + *
330 + * @return string
331 + */
332 + public static function get_active_cache_booster( $url, $main_cache_buster ) {
333 + return ( get_transient( Optml_Settings::INDIVIDUAL_CACHE_TOKENS_KEY ) ?: [] )[ crc32( wp_basename( $url ) ) ] ?? $main_cache_buster;
334 + }
335 + /**
213 336 * Throw error on object clone
214 337 *
215 338 * The whole idea of the singleton design pattern is that there is a single
216 339 * object therefore, we don't want the object to be cloned.
@@ -235,6 +358,110 @@
235 358 */
236 359 public function __wakeup() {
237 360 // Unserializing instances of the class is forbidden.
238 361 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
362 + }
363 +
364 + /**
365 + * Apply resize to image.
366 + *
367 + * @param Image $image Image object.
368 + * @param mixed $resize Resize arguments.
369 + */
370 + private function apply_resize( Image $image, $resize ) {
371 + if ( ! is_array( $resize ) || empty( $resize['type'] ) ) {
372 + return;
373 + }
374 +
375 + $image->resize( $resize['type'], $resize['gravity'] ?? Position::CENTER, $resize['enlarge'] ?? false );
376 + }
377 +
378 + /**
379 + * Apply watermark to image.
380 + *
381 + * @param Image $image Image object.
382 + */
383 + private function apply_watermark( Image $image ) {
384 + $settings = $this->settings->get_site_settings();
385 +
386 + if ( empty( $settings['watermark'] ) ) {
387 + return;
388 + }
389 +
390 + $watermark = $settings['watermark'];
391 +
392 + if ( ! isset( $watermark['id'], $watermark['opacity'], $watermark['position'] ) || $watermark['id'] <= 0 ) {
393 + return;
394 + }
395 +
396 + $image->watermark( $watermark['id'], $watermark['opacity'], $watermark['position'], $watermark['x_offset'] ?? 0, $watermark['y_offset'] ?? 0, $watermark['scale'] ?? 0 );
397 + }
398 +
399 + /**
400 + * Get the DAM image URL.
401 + *
402 + * @param Image $image Image object.
403 + *
404 + * @return string
405 + */
406 + private function get_dam_url( Image $image ) {
407 + // Remove DAM flag.
408 + $url = str_replace( Optml_Dam::URL_DAM_FLAG, '', $image->getSource() );
409 +
410 + foreach ( $image->getProperties() as $property ) {
411 + [ $name, $value ] = explode( ':', $property );
412 +
413 + // Check if the property exists in the URL, if so, replace it with the current property value. Otherwise, add it before the /q: param.
414 + $url = strpos( $url, '/' . $name . ':' ) !== false
415 + ? preg_replace( '/\/' . $name . ':.*?\//', '/' . $name . ':' . $value . '/', $url )
416 + : str_replace( '/q:', '/' . $name . ':' . $value . '/q:', $url );
417 + }
418 +
419 + return $url;
420 + }
421 +
422 +
423 + /**
424 + * Check if the URL is offloaded.
425 + *
426 + * @param string $source_url The source image URL.
427 + *
428 + * @return int
429 + */
430 + private function is_offloaded_url( $source_url ) {
431 + $attachment_id = 0;
432 +
433 + if ( ! self::$offload_enabled ) {
434 + return 0;
435 + }
436 + if ( strpos( $source_url, Optml_Media_Offload::KEYS['not_processed_flag'] ) !== false ) {
437 + $attachment_id = (int) Optml_Media_Offload::get_attachment_id_from_url( $source_url );
438 + } else {
439 + $attachment_id = $this->attachment_url_to_post_id( $source_url );
440 + }
441 +
442 + if ( $attachment_id === 0 ) {
443 + return 0;
444 + }
445 +
446 + if ( ! $this->is_completed_offload( $attachment_id ) ) {
447 + return 0;
448 + }
449 +
450 + return (int) $attachment_id;
451 + }
452 +
453 + /**
454 + * Get the offloaded URL for an image.
455 + *
456 + * @param int $id The attachment ID.
457 + * @param string $optimized_url The optimized image URL.
458 + * @param string $source_url The source image URL.
459 + *
460 + * @return string
461 + */
462 + private function get_offloaded_url( $id, $optimized_url, $source_url ) {
463 + $suffix = wp_get_attachment_metadata( $id )['file'];
464 +
465 + return str_replace( $source_url, ltrim( $suffix, '/' ), $optimized_url );
239 466 }
240 467 }