PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 1.10
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v1.10
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / classes / Webp / Picture / Display.php

Display.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 1.10, at classes/Webp/Picture/Display.php

799 lines 21.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Imagify\Webp\Picture;
3
4 defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' );
5
6 /**
7 * Display WebP images on the site with <picture> tags.
8 *
9 * @since 1.9
10 * @author Grégory Viguier
11 */
12 class Display {
13 use \Imagify\Traits\InstanceGetterTrait;
14
15 /**
16 * Option value.
17 *
18 * @var string
19 * @since 1.9
20 * @author Grégory Viguier
21 */
22 const OPTION_VALUE = 'picture';
23
24 /**
25 * Filesystem object.
26 *
27 * @var \Imagify_Filesystem
28 * @since 1.9
29 * @access protected
30 * @author Grégory Viguier
31 */
32 protected $filesystem;
33
34 /**
35 * Constructor.
36 *
37 * @since 1.9
38 * @access public
39 * @author Grégory Viguier
40 */
41 public function __construct() {
42 $this->filesystem = \Imagify_Filesystem::get_instance();
43 }
44
45 /**
46 * Init.
47 *
48 * @since 1.9
49 * @access public
50 * @author Grégory Viguier
51 */
52 public function init() {
53 add_action( 'template_redirect', [ $this, 'start_content_process' ], -1000 );
54 }
55
56 /** ----------------------------------------------------------------------------------------- */
57 /** ADD <PICTURE> TAGS TO THE PAGE ========================================================== */
58 /** ----------------------------------------------------------------------------------------- */
59
60 /**
61 * Start buffering the page content.
62 *
63 * @since 1.9
64 * @access public
65 * @author Grégory Viguier
66 */
67 public function start_content_process() {
68 if ( ! get_imagify_option( 'display_webp' ) ) {
69 return;
70 }
71
72 if ( self::OPTION_VALUE !== get_imagify_option( 'display_webp_method' ) ) {
73 return;
74 }
75
76 /**
77 * Prevent the replacement of <img> tags into <picture> tags.
78 *
79 * @since 1.9
80 * @author Grégory Viguier
81 *
82 * @param bool $allow True to allow the use of <picture> tags (default). False to prevent their use.
83 */
84 $allow = apply_filters( 'imagify_allow_picture_tags_for_webp', true );
85
86 if ( ! $allow ) {
87 return;
88 }
89
90 ob_start( [ $this, 'maybe_process_buffer' ] );
91 }
92
93 /**
94 * Maybe process the page content.
95 *
96 * @since 1.9
97 * @access public
98 * @author Grégory Viguier
99 *
100 * @param string $buffer The buffer content.
101 * @return string
102 */
103 public function maybe_process_buffer( $buffer ) {
104 if ( ! $this->is_html( $buffer ) ) {
105 return $buffer;
106 }
107
108 if ( strlen( $buffer ) <= 255 ) {
109 // Buffer length must be > 255 (IE does not read pages under 255 c).
110 return $buffer;
111 }
112
113 $buffer = $this->process_content( $buffer );
114
115 /**
116 * Filter the page content after Imagify.
117 *
118 * @since 1.9
119 * @author Grégory Viguier
120 *
121 * @param string $buffer The page content.
122 */
123 $buffer = (string) apply_filters( 'imagify_buffer', $buffer );
124
125 return $buffer;
126 }
127
128 /**
129 * Process the content.
130 *
131 * @since 1.9
132 * @access public
133 * @author Grégory Viguier
134 *
135 * @param string $content The content.
136 * @return string
137 */
138 public function process_content( $content ) {
139 $html_no_picture_tags = $this->remove_picture_tags( $content );
140 $images = $this->get_images( $html_no_picture_tags );
141
142 if ( ! $images ) {
143 return $content;
144 }
145
146 foreach ( $images as $image ) {
147 $tag = $this->build_picture_tag( $image );
148 $content = str_replace( $image['tag'], $tag, $content );
149 }
150
151 return $content; }
152
153 /**
154 * Remove pre-existing <picture> tags.
155 *
156 * We shouldn't replace images already nested inside picture tags
157 * that are already in the page.
158 *
159 * @since 1.10.0
160 *
161 * @param string $html Content of the page.
162 *
163 * @return string HTML content without pre-existing <picture> tags.
164 */
165 private function remove_picture_tags( $html ) {
166 $replace = preg_replace( '#<picture[^>]*>.*?<\/picture\s*>#mis', '', $html );
167
168 if ( null === $replace ) {
169 return $html;
170 }
171
172 return $replace;
173 }
174
175 /** ----------------------------------------------------------------------------------------- */
176 /** BUILD HTML TAGS AND ATTRIBUTES ========================================================== */
177 /** ----------------------------------------------------------------------------------------- */
178
179 /**
180 * Build the <picture> tag to insert.
181 *
182 * @since 1.9
183 * @see $this->process_image()
184 * @access protected
185 * @author Grégory Viguier
186 *
187 * @param array $image An array of data.
188 * @return string A <picture> tag.
189 */
190 protected function build_picture_tag( $image ) {
191 $to_remove = [
192 'alt' => '',
193 'height' => '',
194 'width' => '',
195 'data-lazy-src' => '',
196 'data-src' => '',
197 'src' => '',
198 'data-lazy-srcset' => '',
199 'data-srcset' => '',
200 'srcset' => '',
201 'data-lazy-sizes' => '',
202 'data-sizes' => '',
203 'sizes' => '',
204 ];
205
206 $attributes = array_diff_key( $image['attributes'], $to_remove );
207
208 /**
209 * Filter the attributes to be added to the <picture> tag.
210 *
211 * @since 1.9
212 * @author Grégory Viguier
213 *
214 * @param array $attributes A list of attributes to be added to the <picture> tag.
215 * @param array $data Data built from the originale <img> tag. See $this->process_image().
216 */
217 $attributes = apply_filters( 'imagify_picture_attributes', $attributes, $image );
218
219 /**
220 * Remove Gutenberg specific attributes from picture tag, leave them on img tag.
221 * Optional: $attributes['class'] = 'imagify-webp-cover-wrapper'; for website admin styling ease.
222 */
223 if ( ! empty( $image['attributes']['class'] ) && strpos( $image['attributes']['class'], 'wp-block-cover__image-background' ) !== false ) {
224 unset( $attributes['style'] );
225 unset( $attributes['class'] );
226 unset( $attributes['data-object-fit'] );
227 unset( $attributes['data-object-position'] );
228 }
229
230 $output = '<picture' . $this->build_attributes( $attributes ) . ">\n";
231 /**
232 * Allow to add more <source> tags to the <picture> tag.
233 *
234 * @since 1.9
235 * @author Grégory Viguier
236 *
237 * @param string $more_source_tags Additional <source> tags.
238 * @param array $data Data built from the originale <img> tag. See $this->process_image().
239 */
240 $output .= apply_filters( 'imagify_additional_source_tags', '', $image );
241 $output .= $this->build_source_tag( $image );
242 $output .= $this->build_img_tag( $image );
243 $output .= "</picture>\n";
244
245 return $output;
246 }
247
248 /**
249 * Build the <source> tag to insert in the <picture>.
250 *
251 * @since 1.9
252 * @see $this->process_image()
253 * @access protected
254 * @author Grégory Viguier
255 *
256 * @param array $image An array of data.
257 * @return string A <source> tag.
258 */
259 protected function build_source_tag( $image ) {
260 $srcset_source = ! empty( $image['srcset_attribute'] ) ? $image['srcset_attribute'] : $image['src_attribute'] . 'set';
261 $attributes = [
262 'type' => 'image/webp',
263 $srcset_source => [],
264 ];
265
266 if ( ! empty( $image['srcset'] ) ) {
267 foreach ( $image['srcset'] as $srcset ) {
268 if ( empty( $srcset['webp_url'] ) ) {
269 continue;
270 }
271
272 $attributes[ $srcset_source ][] = $srcset['webp_url'] . ' ' . $srcset['descriptor'];
273 }
274 }
275
276 if ( empty( $attributes[ $srcset_source ] ) ) {
277 $attributes[ $srcset_source ][] = $image['src']['webp_url'];
278 }
279
280 $attributes[ $srcset_source ] = implode( ', ', $attributes[ $srcset_source ] );
281
282 foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) {
283 if ( ! empty( $image['attributes'][ $srcset_attr ] ) && $srcset_attr !== $srcset_source ) {
284 $attributes[ $srcset_attr ] = $image['attributes'][ $srcset_attr ];
285 }
286 }
287
288 if ( 'srcset' !== $srcset_source && empty( $attributes['srcset'] ) && ! empty( $image['attributes']['src'] ) ) {
289 // Lazyload: the "src" attr should contain a placeholder (a data image or a blank.gif ).
290 $attributes['srcset'] = $image['attributes']['src'];
291 }
292
293 foreach ( [ 'data-lazy-sizes', 'data-sizes', 'sizes' ] as $sizes_attr ) {
294 if ( ! empty( $image['attributes'][ $sizes_attr ] ) ) {
295 $attributes[ $sizes_attr ] = $image['attributes'][ $sizes_attr ];
296 }
297 }
298
299 /**
300 * Filter the attributes to be added to the <source> tag.
301 *
302 * @since 1.9
303 * @author Grégory Viguier
304 *
305 * @param array $attributes A list of attributes to be added to the <source> tag.
306 * @param array $data Data built from the original <img> tag. See $this->process_image().
307 */
308 $attributes = apply_filters( 'imagify_picture_source_attributes', $attributes, $image );
309
310 return '<source' . $this->build_attributes( $attributes ) . "/>\n";
311 }
312
313 /**
314 * Build the <img> tag to insert in the <picture>.
315 *
316 * @since 1.9
317 * @see $this->process_image()
318 * @access protected
319 * @author Grégory Viguier
320 *
321 * @param array $image An array of data.
322 * @return string A <img> tag.
323 */
324 protected function build_img_tag( $image ) {
325 /**
326 * Gutenberg fix.
327 * Check for the 'wp-block-cover__image-background' class on the original image, and leave that class and style attributes if found.
328 */
329 if ( ! empty( $image['attributes']['class'] ) && strpos( $image['attributes']['class'], 'wp-block-cover__image-background' ) !== false ) {
330 $to_remove = [
331 'id' => '',
332 'title' => '',
333 ];
334
335 $attributes = array_diff_key( $image['attributes'], $to_remove );
336 } else {
337 $to_remove = [
338 'class' => '',
339 'id' => '',
340 'style' => '',
341 'title' => '',
342 ];
343
344 $attributes = array_diff_key( $image['attributes'], $to_remove );
345 }
346
347 /**
348 * Filter the attributes to be added to the <img> tag.
349 *
350 * @since 1.9
351 * @author Grégory Viguier
352 *
353 * @param array $attributes A list of attributes to be added to the <img> tag.
354 * @param array $data Data built from the originale <img> tag. See $this->process_image().
355 */
356 $attributes = apply_filters( 'imagify_picture_img_attributes', $attributes, $image );
357
358 return '<img' . $this->build_attributes( $attributes ) . "/>\n";
359 }
360
361 /**
362 * Create HTML attributes from an array.
363 *
364 * @since 1.9
365 * @access protected
366 * @author Grégory Viguier
367 *
368 * @param array $attributes A list of attribute pairs.
369 * @return string HTML attributes.
370 */
371 protected function build_attributes( $attributes ) {
372 if ( ! $attributes || ! is_array( $attributes ) ) {
373 return '';
374 }
375
376 $out = '';
377
378 foreach ( $attributes as $attribute => $value ) {
379 $out .= ' ' . $attribute . '="' . esc_attr( $value ) . '"';
380 }
381
382 return $out;
383 }
384
385 /** ----------------------------------------------------------------------------------------- */
386 /** VARIOUS TOOLS =========================================================================== */
387 /** ----------------------------------------------------------------------------------------- */
388
389 /**
390 * Get a list of images in a content.
391 *
392 * @since 1.9
393 * @access protected
394 * @author Grégory Viguier
395 *
396 * @param string $content The content.
397 * @return array
398 */
399 protected function get_images( $content ) {
400 // Remove comments.
401 $content = preg_replace( '/<!--(.*)-->/Uis', '', $content );
402
403 if ( ! preg_match_all( '/<img\s.*>/isU', $content, $matches ) ) {
404 return [];
405 }
406
407 $images = array_map( [ $this, 'process_image' ], $matches[0] );
408 $images = array_filter( $images );
409
410 /**
411 * Filter the images to display with a <picture> tag.
412 *
413 * @since 1.9
414 * @see $this->process_image()
415 * @author Grégory Viguier
416 *
417 * @param array $images A list of arrays.
418 * @param string $content The page content.
419 */
420 $images = apply_filters( 'imagify_webp_picture_images_to_display', $images, $content );
421
422 if ( ! $images || ! is_array( $images ) ) {
423 return [];
424 }
425
426 foreach ( $images as $i => $image ) {
427 if ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) {
428 unset( $images[ $i ] );
429 continue;
430 }
431
432 unset( $images[ $i ]['src']['webp_path'], $images[ $i ]['src']['webp_exists'] );
433
434 if ( empty( $image['srcset'] ) || ! is_array( $image['srcset'] ) ) {
435 unset( $images[ $i ]['srcset'] );
436 continue;
437 }
438
439 foreach ( $image['srcset'] as $j => $srcset ) {
440 if ( ! is_array( $srcset ) ) {
441 continue;
442 }
443
444 if ( empty( $srcset['webp_exists'] ) || empty( $srcset['webp_url'] ) ) {
445 unset( $images[ $i ]['srcset'][ $j ]['webp_url'] );
446 }
447
448 unset( $images[ $i ]['srcset'][ $j ]['webp_path'], $images[ $i ]['srcset'][ $j ]['webp_exists'] );
449 }
450 }
451
452 return $images;
453 }
454
455 /**
456 * Process an image tag and get an array containing some data.
457 *
458 * @since 1.9
459 * @access protected
460 * @author Grégory Viguier
461 *
462 * @param string $image An image html tag.
463 * @return array|false {
464 * An array of data if the image has a WebP version. False otherwise.
465 *
466 * @type string $tag The image tag.
467 * @type array $attributes The image attributes (minus src and srcset).
468 * @type array $src {
469 * @type string $url URL to the original image.
470 * @type string $webp_url URL to the WebP version.
471 * }
472 * @type array $srcset {
473 * An array or arrays. Not set if not applicable.
474 *
475 * @type string $url URL to the original image.
476 * @type string $webp_url URL to the WebP version. Not set if not applicable.
477 * @type string $descriptor A src descriptor.
478 * }
479 * }
480 */
481 protected function process_image( $image ) {
482 static $extensions;
483
484 $atts_pattern = '/(?<name>[^\s"\']+)\s*=\s*(["\'])\s*(?<value>.*?)\s*\2/s';
485
486 if ( ! preg_match_all( $atts_pattern, $image, $tmp_attributes, PREG_SET_ORDER ) ) {
487 // No attributes?
488 return false;
489 }
490
491 $attributes = [];
492
493 foreach ( $tmp_attributes as $attribute ) {
494 $attributes[ $attribute['name'] ] = $attribute['value'];
495 }
496
497 if ( ! empty( $attributes['class'] ) && strpos( $attributes['class'], 'imagify-no-webp' ) !== false ) {
498 // Has the 'imagify-no-webp' class.
499 return false;
500 }
501
502 // Deal with the src attribute.
503 $src_source = false;
504
505 foreach ( [ 'data-lazy-src', 'data-src', 'src' ] as $src_attr ) {
506 if ( ! empty( $attributes[ $src_attr ] ) ) {
507 $src_source = $src_attr;
508 break;
509 }
510 }
511
512 if ( ! $src_source ) {
513 // No src attribute.
514 return false;
515 }
516
517 if ( ! isset( $extensions ) ) {
518 $extensions = imagify_get_mime_types( 'image' );
519 $extensions = array_keys( $extensions );
520 $extensions = implode( '|', $extensions );
521 }
522
523 if ( ! preg_match( '@^(?<src>(?:(?:https?:)?//|/).+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $attributes[ $src_source ], $src ) ) {
524 // Not a supported image format.
525 return false;
526 }
527
528 $webp_url = imagify_path_to_webp( $src['src'] );
529 $webp_path = $this->url_to_path( $webp_url );
530 $webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
531
532 $data = [
533 'tag' => $image,
534 'attributes' => $attributes,
535 'src_attribute' => $src_source,
536 'src' => [
537 'url' => $attributes[ $src_source ],
538 'webp_url' => $webp_url,
539 'webp_path' => $webp_path,
540 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ),
541 ],
542 'srcset_attribute' => false,
543 'srcset' => [],
544 ];
545
546 // Deal with the srcset attribute.
547 $srcset_source = false;
548
549 foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) {
550 if ( ! empty( $attributes[ $srcset_attr ] ) ) {
551 $srcset_source = $srcset_attr;
552 break;
553 }
554 }
555
556 if ( $srcset_source ) {
557 $data['srcset_attribute'] = $srcset_source;
558
559 $srcset = explode( ',', $attributes[ $srcset_source ] );
560
561 foreach ( $srcset as $srcs ) {
562 $srcs = preg_split( '/\s+/', trim( $srcs ) );
563
564 if ( count( $srcs ) > 2 ) {
565 // Not a good idea to have space characters in file name.
566 $descriptor = array_pop( $srcs );
567 $srcs = [ implode( ' ', $srcs ), $descriptor ];
568 }
569
570 if ( empty( $srcs[1] ) ) {
571 $srcs[1] = '1x';
572 }
573
574 if ( ! preg_match( '@^(?<src>(?:https?:)?//.+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $srcs[0], $src ) ) {
575 // Not a supported image format.
576 $data['srcset'][] = [
577 'url' => $srcs[0],
578 'descriptor' => $srcs[1],
579 ];
580 continue;
581 }
582
583 $webp_url = imagify_path_to_webp( $src['src'] );
584 $webp_path = $this->url_to_path( $webp_url );
585 $webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
586
587 $data['srcset'][] = [
588 'url' => $srcs[0],
589 'descriptor' => $srcs[1],
590 'webp_url' => $webp_url,
591 'webp_path' => $webp_path,
592 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ),
593 ];
594 }
595 }
596
597 /**
598 * Filter a processed image tag.
599 *
600 * @since 1.9
601 * @author Grégory Viguier
602 *
603 * @param array $data An array of data for this image.
604 * @param string $image An image html tag.
605 */
606 $data = apply_filters( 'imagify_webp_picture_process_image', $data, $image );
607
608 if ( ! $data || ! is_array( $data ) ) {
609 return false;
610 }
611
612 if ( ! isset( $data['tag'], $data['attributes'], $data['src_attribute'], $data['src'], $data['srcset_attribute'], $data['srcset'] ) ) {
613 return false;
614 }
615
616 return $data;
617 }
618
619 /**
620 * Tell if a content is HTML.
621 *
622 * @since 1.9
623 * @access protected
624 * @author Grégory Viguier
625 *
626 * @param string $content The content.
627 * @return bool
628 */
629 protected function is_html( $content ) {
630 return preg_match( '/<\/html>/i', $content );
631 }
632
633 /**
634 * Convert a file URL to an absolute path.
635 *
636 * @since 1.9
637 * @access protected
638 * @author Grégory Viguier
639 *
640 * @param string $url A file URL.
641 * @return string|bool The file path. False on failure.
642 */
643 protected function url_to_path( $url ) {
644 static $uploads_url;
645 static $uploads_dir;
646 static $root_url;
647 static $root_dir;
648 static $cdn_url;
649 static $domain_url;
650
651 /**
652 * $url, $uploads_url, $root_url, and $cdn_url are passed through `set_url_scheme()` only to make sure `stripos()` doesn't fail over a stupid http/https difference.
653 */
654 if ( ! isset( $uploads_url ) ) {
655 $uploads_url = set_url_scheme( $this->filesystem->get_upload_baseurl() );
656 $uploads_dir = $this->filesystem->get_upload_basedir( true );
657 $root_url = set_url_scheme( $this->filesystem->get_site_root_url() );
658 $root_dir = $this->filesystem->get_site_root();
659 $cdn_url = $this->get_cdn_source();
660 $cdn_url = $cdn_url['url'] ? set_url_scheme( $cdn_url['url'] ) : false;
661 $domain_url = wp_parse_url( $root_url );
662
663 if ( ! empty( $domain_url['scheme'] ) && ! empty( $domain_url['host'] ) ) {
664 $domain_url = $domain_url['scheme'] . '://' . $domain_url['host'] . '/';
665 } else {
666 $domain_url = false;
667 }
668 }
669
670 // Get the right URL format.
671 if ( $domain_url && strpos( $url, '/' ) === 0 ) {
672 // URL like `/path/to/image.jpg.webp`.
673 $url = $domain_url . ltrim( $url, '/' );
674 }
675
676 $url = set_url_scheme( $url );
677
678 if ( $cdn_url && $domain_url && stripos( $url, $cdn_url ) === 0 ) {
679 // CDN.
680 $url = str_ireplace( $cdn_url, $domain_url, $url );
681 }
682
683 // Return the path.
684 if ( stripos( $url, $uploads_url ) === 0 ) {
685 return str_ireplace( $uploads_url, $uploads_dir, $url );
686 }
687
688 if ( stripos( $url, $root_url ) === 0 ) {
689 return str_ireplace( $root_url, $root_dir, $url );
690 }
691
692 return false;
693 }
694
695 /**
696 * Get the CDN "source".
697 *
698 * @since 1.9.3
699 * @access public
700 * @author Grégory Viguier
701 *
702 * @param string $option_url An URL to use instead of the one stored in the option. It is used only if no constant/filter.
703 * @return array {
704 * @type string $source Where does it come from? Possible values are 'constant', 'filter', or 'option'.
705 * @type string $name Who? Can be a constant name, a plugin name, or an empty string.
706 * @type string $url The CDN URL, with a trailing slash. An empty string if no URL is set.
707 * }
708 */
709 public function get_cdn_source( $option_url = '' ) {
710 if ( defined( 'IMAGIFY_CDN_URL' ) && IMAGIFY_CDN_URL && is_string( IMAGIFY_CDN_URL ) ) {
711 // Use a constant.
712 $source = [
713 'source' => 'constant',
714 'name' => 'IMAGIFY_CDN_URL',
715 'url' => IMAGIFY_CDN_URL,
716 ];
717 } else {
718 // Maybe use a filter.
719 $filter_source = [
720 'name' => null,
721 'url' => null,
722 ];
723
724 /**
725 * Provide a custom CDN source.
726 *
727 * @since 1.9.3
728 * @author Grégory Viguier
729 *
730 * @param array $filter_source {
731 * @type $name string The name of which provides the URL (plugin name, etc).
732 * @type $url string The CDN URL.
733 * }
734 */
735 $filter_source = apply_filters( 'imagify_cdn_source', $filter_source );
736
737 if ( ! empty( $filter_source['url'] ) ) {
738 $source = [
739 'source' => 'filter',
740 'name' => ! empty( $filter_source['name'] ) ? $filter_source['name'] : '',
741 'url' => $filter_source['url'],
742 ];
743 }
744 }
745
746 if ( empty( $source['url'] ) ) {
747 // No constant, no filter: use the option.
748 $source = [
749 'source' => 'option',
750 'name' => '',
751 'url' => $option_url && is_string( $option_url ) ? $option_url : get_imagify_option( 'cdn_url' ),
752 ];
753 }
754
755 if ( empty( $source['url'] ) ) {
756 // Nothing set.
757 return [
758 'source' => 'option',
759 'name' => '',
760 'url' => '',
761 ];
762 }
763
764 $source['url'] = $this->sanitize_cdn_url( $source['url'] );
765
766 if ( empty( $source['url'] ) ) {
767 // Not an URL.
768 return [
769 'source' => 'option',
770 'name' => '',
771 'url' => '',
772 ];
773 }
774
775 return $source;
776 }
777
778 /**
779 * Sanitize the CDN URL value.
780 *
781 * @since 1.9.3
782 * @access public
783 * @author Grégory Viguier
784 *
785 * @param string $url The URL to sanitize.
786 * @return string
787 */
788 public function sanitize_cdn_url( $url ) {
789 $url = sanitize_text_field( $url );
790
791 if ( ! $url || ! preg_match( '@^https?://.+\.[^.]+@i', $url ) ) {
792 // Not an URL.
793 return '';
794 }
795
796 return trailingslashit( $url );
797 }
798 }
799