PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 4.2.5
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v4.2.5
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 3.12.1 All 134 releases
optimole-wp / inc / url_replacer.php

url_replacer.php in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization 4.2.5, at inc/url_replacer.php

464 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
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
8 /**
9 * The class handles the url replacements.
10 *
11 * @package \Optml\Inc
12 * @author Optimole <friends@optimole.com>
13 */
14 final class Optml_Url_Replacer extends Optml_App_Replacer {
15
16 use Optml_Dam_Offload_Utils;
17 use Optml_Validator;
18 use Optml_Normalizer;
19
20 /**
21 * Cached object instance.
22 *
23 * @var Optml_Url_Replacer
24 */
25 protected static $instance = null;
26
27 /**
28 * Class instance method.
29 *
30 * @codeCoverageIgnore
31 * @static
32 * @return Optml_Url_Replacer
33 * @since 1.0.0
34 * @access public
35 */
36 public static function instance() {
37 if ( null === self::$instance ) {
38 self::$instance = new self();
39 add_action( 'optml_replacer_setup', [ self::$instance, 'init' ] );
40 }
41
42 return self::$instance;
43 }
44
45
46 /**
47 * The initialize method.
48 */
49 public function init() {
50
51 add_filter( 'optml_replace_image', [ $this, 'build_url' ], 10, 2 );
52 parent::init();
53
54 add_filter( 'optml_content_url', [ $this, 'build_url' ], 1, 2 );
55 }
56
57 /**
58 * Returns a signed image url authorized to be used in our CDN.
59 *
60 * @param string $url The url which should be signed.
61 * @param array $args Dimension params; Supports `width` and `height`.
62 * @param bool $retried Whether the url has been retried.
63 * @return string
64 */
65 public function build_url(
66 $url,
67 $args = [
68 'width' => 'auto',
69 'height' => 'auto',
70 ],
71 $retried = false
72 ) {
73 if ( apply_filters( 'optml_dont_replace_url', false, $url ) ) {
74 return $url;
75 }
76
77 if ( OPTML_DEBUG ) {
78 do_action( 'optml_log', 'building url: ' . $url . ' args: ' . print_r( $args, true ) );
79 }
80 $original_url = $url;
81
82 $is_slashed = strpos( $url, '\/' ) !== false;
83
84 // We do a little hack here, for json unicode chars we first replace them with html special chars,
85 // we then strip slashes to normalize the URL and last we convert html special chars back to get a clean URL
86 $url = $is_slashed ? html_entity_decode( stripslashes( preg_replace( '/\\\u([\da-fA-F]{4})/', '&#x\1;', $url ) ) ) : ( $url );
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 }
131 return $original_url;
132 }
133
134 if ( substr( $url, 0, 2 ) === '//' ) {
135 $url = ltrim( $url, '/' );
136 $url = sprintf( '%s://%s', is_ssl() ? 'https' : 'http', $url );
137 }
138
139 if ( ! $this->can_replace_url( $url ) ) {
140 return $original_url;
141 }
142 // Remove any query strings that might affect conversion.
143 $url = strtok( $url, '?' );
144 $ext = $this->is_valid_mimetype_from_url( $url, self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ] );
145 if ( false === $ext ) {
146 return $original_url;
147 }
148
149 if ( isset( $args['quality'] ) && ! empty( $args['quality'] ) ) {
150 $args['quality'] = $this->to_accepted_quality( $args['quality'] );
151 }
152
153 if ( isset( $args['minify'] ) && ! empty( $args['minify'] ) ) {
154 $args['minify'] = $this->to_accepted_minify( $args['minify'] );
155 }
156
157 // this will authorize the image
158 if ( ! empty( $this->site_mappings ) ) {
159 $url = str_replace( array_keys( $this->site_mappings ), array_values( $this->site_mappings ), $url );
160 }
161 if ( substr( $url, 0, 2 ) === '//' ) {
162 $url = ltrim( $url, '/' );
163 $url = sprintf( '%s://%s', is_ssl() ? 'https' : 'http', $url );
164 }
165 $normalized_ext = strtolower( $ext );
166 if ( isset( Optml_Config::$image_extensions[ $normalized_ext ] ) ) {
167 $new_url = $this->normalize_image( $url, $original_url, $args, $is_uploaded, $normalized_ext );
168 if ( $is_uploaded ) {
169 $new_url = str_replace( '/' . $url, $id_and_filename, $new_url );
170 }
171 } else {
172 $asset = Optimole::asset( $url, $this->active_cache_buster_assets );
173
174 if ( stripos( $url, '.css' ) || stripos( $url, '.js' ) ) {
175 $asset->quality();
176 }
177
178 $asset->minify( ( $this->is_css_minify_on && str_ends_with( strtolower( $url ), '.css' ) ) || ( $this->is_js_minify_on && str_ends_with( strtolower( $url ), '.js' ) ) );
179
180 return $asset->getUrl();
181 }
182 return $is_slashed ? addcslashes( $new_url, '/' ) : $new_url;
183 }
184
185 /**
186 * Apply extra normalization to image.
187 *
188 * @param string $url Image url.
189 * @param string $original_url Original image url.
190 * @param array $args Args.
191 *
192 * @return string
193 */
194 private function normalize_image( $url, $original_url, $args, $is_uploaded = false, $ext = '' ) {
195
196 $new_url = $this->strip_image_size_from_url( $url );
197
198 if ( $new_url !== $url || $is_uploaded === true ) {
199 if ( ! isset( $args['quality'] ) || $args['quality'] !== 'eco' ) {
200 if ( $is_uploaded === true ) {
201 $crop = false;
202 }
203 if ( $is_uploaded !== true ) {
204 list($args['width'], $args['height'], $crop) = $this->parse_dimensions_from_filename( $url );
205 }
206 if ( ! $crop ) {
207 $sizes2crop = self::size_to_crop();
208 $crop = isset( $sizes2crop[ $args['width'] . $args['height'] ] ) ? $sizes2crop[ $args['width'] . $args['height'] ] : false;
209 }
210 if ( $crop ) {
211 $args['resize'] = $this->to_optml_crop( $crop );
212 }
213 }
214 $url = $new_url;
215 }
216
217 if ( ! isset( $args['width'] ) ) {
218 $args['width'] = $this->limit_dimensions_enabled ? $this->limit_width : 'auto';
219 }
220 if ( ! isset( $args['height'] ) ) {
221 $args['height'] = $this->limit_dimensions_enabled ? $this->limit_height : 'auto';
222 }
223
224 $args['width'] = (int) $args['width'];
225 $args['height'] = (int) $args['height'];
226
227 if ( $this->limit_dimensions_enabled && $args['height'] !== 0 && $args['width'] !== 0 ) {
228 if ( $args['width'] > $this->limit_width || $args['height'] > $this->limit_height ) {
229 $scale = min( $this->limit_width / $args['width'], $this->limit_height / $args['height'] );
230
231 if ( $scale < 1 ) {
232 $args['width'] = (int) floor( $scale * $args['width'] );
233 $args['height'] = (int) floor( $scale * $args['height'] );
234 }
235 }
236
237 if ( isset( $args['resize'], $args['resize']['enlarge'] ) ) {
238 $args['resize']['enlarge'] = false;
239 }
240 }
241
242 if ( empty( $args['quality'] ) ) {
243 $args['quality'] = $this->to_accepted_quality( $this->settings->get_quality() );
244 }
245
246 if ( isset( $args['resize'], $args['resize']['gravity'] ) && $this->settings->is_smart_cropping() ) {
247 $args['resize']['gravity'] = GravityProperty::SMART;
248 }
249
250 $is_retina_enabled = $this->settings->get( 'retina_images' ) !== 'disabled';
251 if ( ! $is_retina_enabled ) {
252 $max_dimension = max( $args['width'], $args['height'] );
253 $should_apply_dpr = false;
254
255 if ( $max_dimension > 0 && $max_dimension < 150 ) {
256 $should_apply_dpr = true;
257 }
258
259 $should_apply_dpr = apply_filters( 'optml_should_apply_dpr', $should_apply_dpr, $args, $url );
260
261 if ( $should_apply_dpr && ! isset( $args['dpr'] ) ) {
262 $args['dpr'] = 2;
263 }
264 }
265
266 $args = apply_filters( 'optml_image_args', $args, $original_url );
267 $image = Optimole::image( apply_filters( 'optml_processed_url', $url ), self::get_active_cache_booster( $url, $this->active_cache_buster ) );
268
269 $image->width( ! empty( $args['width'] ) && is_int( $args['width'] ) ? $args['width'] : 'auto' );
270 $image->height( ! empty( $args['height'] ) && is_int( $args['height'] ) ? $args['height'] : 'auto' );
271
272 $image->quality( $args['quality'] );
273
274 if ( ! empty( $args['resize'] ) ) {
275 $this->apply_resize( $image, $args['resize'] );
276 } elseif ( $this->settings->is_smart_cropping() ) {
277 // If smart cropping is enabled and no resize is set, we apply smart focus since the resize can be triggered from the JS library.
278 $image->smartFocus();
279 }
280
281 if ( apply_filters( 'optml_apply_watermark_for', true, $url ) ) {
282 $this->apply_watermark( $image );
283 }
284
285 if ( ! empty( $args['format'] ) ) {
286 $image->format( (string) $args['format'] );
287 } elseif ( $this->settings->is_best_format() ) {
288 // If format is not already set, we use best format if it's enabled.
289 $image->format( 'best' );
290 }
291
292 if ( $this->settings->get( 'strip_metadata' ) === 'disabled' ) {
293 $image->stripMetadata( false );
294 }
295
296 if ( ! apply_filters( 'optml_should_avif_ext', true, $ext, $original_url ) || $this->settings->get( 'avif' ) === 'disabled' ) {
297 $image->ignoreAvif();
298 }
299
300 if ( apply_filters( 'optml_keep_copyright', false ) === true ) {
301 $image->keepCopyright();
302 }
303
304 if ( $this->is_dam_url( $image->getSource() ) ) {
305 return $this->get_dam_url( $image );
306 }
307
308 $offloaded_id = $this->is_offloaded_url( $image->getSource() );
309
310 if ( isset( $args['dpr'] ) && $args['dpr'] > 1 ) {
311 $image->dpr( $args['dpr'] );
312 }
313 if ( $offloaded_id !== 0 ) {
314 return $this->get_offloaded_url( $offloaded_id, $image->getUrl(), $image->getSource() );
315 }
316
317 return $image->getUrl();
318 }
319
320 /**
321 * Get the active cache booster.
322 *
323 * @param string $url The URL.
324 * @param string $main_cache_buster The default value.
325 *
326 * @return string
327 */
328 public static function get_active_cache_booster( $url, $main_cache_buster ) {
329 return ( get_transient( Optml_Settings::INDIVIDUAL_CACHE_TOKENS_KEY ) ?: [] )[ crc32( wp_basename( $url ) ) ] ?? $main_cache_buster;
330 }
331 /**
332 * Throw error on object clone
333 *
334 * The whole idea of the singleton design pattern is that there is a single
335 * object therefore, we don't want the object to be cloned.
336 *
337 * @codeCoverageIgnore
338 * @access public
339 * @return void
340 * @since 1.0.0
341 */
342 public function __clone() {
343 // Cloning instances of the class is forbidden.
344 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
345 }
346
347 /**
348 * Disable unserializing of the class
349 *
350 * @codeCoverageIgnore
351 * @access public
352 * @return void
353 * @since 1.0.0
354 */
355 public function __wakeup() {
356 // Unserializing instances of the class is forbidden.
357 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
358 }
359
360 /**
361 * Apply resize to image.
362 *
363 * @param Image $image Image object.
364 * @param mixed $resize Resize arguments.
365 */
366 private function apply_resize( Image $image, $resize ) {
367 if ( ! is_array( $resize ) || empty( $resize['type'] ) ) {
368 return;
369 }
370
371 $image->resize( $resize['type'], $resize['gravity'] ?? Position::CENTER, $resize['enlarge'] ?? false );
372 }
373
374 /**
375 * Apply watermark to image.
376 *
377 * @param Image $image Image object.
378 */
379 private function apply_watermark( Image $image ) {
380 $settings = $this->settings->get_site_settings();
381
382 if ( empty( $settings['watermark'] ) ) {
383 return;
384 }
385
386 $watermark = $settings['watermark'];
387
388 if ( ! isset( $watermark['id'], $watermark['opacity'], $watermark['position'] ) || $watermark['id'] <= 0 ) {
389 return;
390 }
391
392 $image->watermark( $watermark['id'], $watermark['opacity'], $watermark['position'], $watermark['x_offset'] ?? 0, $watermark['y_offset'] ?? 0, $watermark['scale'] ?? 0 );
393 }
394
395 /**
396 * Get the DAM image URL.
397 *
398 * @param Image $image Image object.
399 *
400 * @return string
401 */
402 private function get_dam_url( Image $image ) {
403 // Remove DAM flag.
404 $url = str_replace( Optml_Dam::URL_DAM_FLAG, '', $image->getSource() );
405
406 foreach ( $image->getProperties() as $property ) {
407 [ $name, $value ] = explode( ':', $property );
408
409 // Check if the property exists in the URL, if so, replace it with the current property value. Otherwise, add it before the /q: param.
410 $url = strpos( $url, '/' . $name . ':' ) !== false
411 ? preg_replace( '/\/' . $name . ':.*?\//', '/' . $name . ':' . $value . '/', $url )
412 : str_replace( '/q:', '/' . $name . ':' . $value . '/q:', $url );
413 }
414
415 return $url;
416 }
417
418
419 /**
420 * Check if the URL is offloaded.
421 *
422 * @param string $source_url The source image URL.
423 *
424 * @return int
425 */
426 private function is_offloaded_url( $source_url ) {
427 $attachment_id = 0;
428
429 if ( ! self::$offload_enabled ) {
430 return 0;
431 }
432 if ( strpos( $source_url, Optml_Media_Offload::KEYS['not_processed_flag'] ) !== false ) {
433 $attachment_id = (int) Optml_Media_Offload::get_attachment_id_from_url( $source_url );
434 } else {
435 $attachment_id = $this->attachment_url_to_post_id( $source_url );
436 }
437
438 if ( $attachment_id === 0 ) {
439 return 0;
440 }
441
442 if ( ! $this->is_completed_offload( $attachment_id ) ) {
443 return 0;
444 }
445
446 return (int) $attachment_id;
447 }
448
449 /**
450 * Get the offloaded URL for an image.
451 *
452 * @param int $id The attachment ID.
453 * @param string $optimized_url The optimized image URL.
454 * @param string $source_url The source image URL.
455 *
456 * @return string
457 */
458 private function get_offloaded_url( $id, $optimized_url, $source_url ) {
459 $suffix = wp_get_attachment_metadata( $id )['file'];
460
461 return str_replace( $source_url, ltrim( $suffix, '/' ), $optimized_url );
462 }
463 }
464