PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.3
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.3
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 / inc / classes / class-imagify-filesystem.php

class-imagify-filesystem.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.3, at inc/classes/class-imagify-filesystem.php

1,138 lines 30.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use Imagify\Traits\InstanceGetterTrait;
4
5 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
6 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
7
8 /**
9 * Class that enhance the WP filesystem class.
10 *
11 * @since 1.7.1
12 * @author Grégory Viguier
13 */
14 class Imagify_Filesystem extends WP_Filesystem_Direct {
15 use InstanceGetterTrait;
16
17 /**
18 * Class version.
19 *
20 * @var string
21 */
22 const VERSION = '1.2';
23
24 /**
25 * Delimiter used for regex patterns.
26 *
27 * @var string
28 * @since 1.8
29 * @author Grégory Viguier
30 */
31 const PATTERN_DELIMITER = '@';
32
33 /**
34 * Cached site root URLs, keyed by blog ID.
35 *
36 * @since 2.3.1
37 *
38 * @var array<int,string>
39 */
40 private $site_root_urls = [];
41
42 /** ----------------------------------------------------------------------------------------- */
43 /** INSTANCIATION =========================================================================== */
44 /** ----------------------------------------------------------------------------------------- */
45
46 /**
47 * Constructor.
48 *
49 * @since 1.7.1
50 * @access public
51 * @author Grégory Viguier
52 */
53 public function __construct() {
54 // Define the permission constants if not already done.
55 if ( ! defined( 'FS_CHMOD_DIR' ) ) {
56 define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
57 }
58 if ( ! defined( 'FS_CHMOD_FILE' ) ) {
59 define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
60 }
61
62 parent::__construct( '' );
63 }
64
65
66 /** ----------------------------------------------------------------------------------------- */
67 /** CUSTOM TOOLS ============================================================================ */
68 /** ----------------------------------------------------------------------------------------- */
69
70 /**
71 * Get the file name.
72 * Replacement for basename().
73 *
74 * @since 1.7.1
75 * @access public
76 * @author Grégory Viguier
77 *
78 * @param string $file_path Path to the file.
79 * @return string|bool The base name of the given path. False on failure.
80 */
81 public function file_name( $file_path ) {
82 if ( ! $file_path ) {
83 return false;
84 }
85
86 return wp_basename( $file_path );
87 }
88
89 /**
90 * Get the parent directory's path.
91 * Replacement for dirname().
92 *
93 * @since 1.7.1
94 * @access public
95 * @author Grégory Viguier
96 *
97 * @param string $file_path Path to the file.
98 * @return string|bool The directory path with a trailing slash. False on failure.
99 */
100 public function dir_path( $file_path ) {
101 if ( ! $file_path ) {
102 return false;
103 }
104
105 $file_path = dirname( $file_path );
106
107 return $this->is_root( $file_path ) ? $this->get_root() : trailingslashit( $file_path );
108 }
109
110 /**
111 * Get information about a file path.
112 * Replacement for pathinfo().
113 *
114 * @since 1.7.1
115 * @access public
116 * @author Grégory Viguier
117 *
118 * @param string $file_path Path to the file.
119 * @param string $option If present, specifies a specific element to be returned; one of 'dir_path', 'file_name', 'extension' or 'file_base'.
120 * If option is not specified, returns all available elements.
121 * @return array|string|null If the option parameter is not passed, an associative array containing the following elements is returned: 'dir_path' (with trailing slash), 'file_name' (with extension), 'extension' (if any), and 'file_base' (without extension).
122 */
123 public function path_info( $file_path, $option = null ) {
124 if ( ! $file_path ) {
125 if ( isset( $option ) ) {
126 return '';
127 }
128
129 return [
130 'dir_path' => '',
131 'file_name' => '',
132 'extension' => null,
133 'file_base' => '',
134 ];
135 }
136
137 if ( isset( $option ) ) {
138 $options = [
139 'dir_path' => PATHINFO_DIRNAME,
140 'file_name' => PATHINFO_BASENAME,
141 'extension' => PATHINFO_EXTENSION,
142 'file_base' => PATHINFO_FILENAME,
143 ];
144
145 if ( ! isset( $options[ $option ] ) ) {
146 return '';
147 }
148
149 $output = pathinfo( $file_path, $options[ $option ] );
150
151 if ( 'dir_path' !== $option ) {
152 return $output;
153 }
154
155 return $this->is_root( $output ) ? $this->get_root() : trailingslashit( $output );
156 }
157
158 $output = pathinfo( $file_path );
159
160 $output['dirname'] = $this->is_root( $output['dirname'] ) ? $this->get_root() : trailingslashit( $output['dirname'] );
161 $output['extension'] = isset( $output['extension'] ) ? $output['extension'] : null;
162
163 // '/www/htdocs/inc/lib.inc.php'
164 return [
165 'dir_path' => $output['dirname'], // '/www/htdocs/inc/'
166 'file_name' => $output['basename'], // 'lib.inc.php'
167 'extension' => $output['extension'], // 'php'
168 'file_base' => $output['filename'], // 'lib.inc'
169 ];
170 }
171
172 /**
173 * Recursive directory creation based on full path. Will attempt to set permissions on folders.
174 * Replacement for recursive mkdir().
175 *
176 * @since 1.7.1
177 * @access public
178 * @author Grégory Viguier
179 *
180 * @param string $path Full path to attempt to create.
181 * @return bool Whether the path was created. True if path already exists.
182 */
183 public function make_dir( $path ) {
184 /*
185 * Safe mode fails with a trailing slash under certain PHP versions.
186 */
187 $path = untrailingslashit( wp_normalize_path( $path ) );
188
189 if ( $this->is_root( $path ) ) {
190 return $this->is_dir( $this->get_root() ) && $this->is_writable( $this->get_root() );
191 }
192
193 if ( $this->exists( $path ) ) {
194 return $this->is_dir( $path ) && $this->is_writable( $path );
195 }
196
197 $site_root = $this->get_site_root();
198
199 if ( strpos( $path, $site_root ) !== 0 ) {
200 return false;
201 }
202
203 $bits = preg_replace( '@^' . preg_quote( $site_root, '@' ) . '@i', '', $path );
204 $bits = explode( '/', trim( $bits, '/' ) );
205 $path = untrailingslashit( $site_root );
206
207 foreach ( $bits as $bit ) {
208 $parent_path = $path;
209 $path .= '/' . $bit;
210
211 if ( $this->exists( $path ) ) {
212 if ( ! $this->is_dir( $path ) ) {
213 return false;
214 }
215
216 continue;
217 }
218
219 if ( ! $this->is_writable( $parent_path ) ) {
220 $this->chmod_dir( $parent_path );
221
222 if ( ! $this->is_writable( $parent_path ) ) {
223 return false;
224 }
225 }
226
227 $this->mkdir( $path );
228
229 if ( ! $this->exists( $path ) ) {
230 return false;
231 }
232
233 $this->touch( trailingslashit( $path ) . 'index.php' );
234 }
235
236 return true;
237 }
238
239 /**
240 * Set a file permissions using FS_CHMOD_FILE.
241 *
242 * @since 1.7.1
243 * @access public
244 * @author Grégory Viguier
245 *
246 * @param string $file_path Path to the file.
247 * @return bool True on success, false on failure.
248 */
249 public function chmod_file( $file_path ) {
250 if ( ! $file_path ) {
251 return false;
252 }
253
254 return $this->chmod( $file_path, FS_CHMOD_FILE );
255 }
256
257 /**
258 * Set a directory permissions using FS_CHMOD_DIR.
259 *
260 * @since 1.7.1
261 * @access public
262 * @author Grégory Viguier
263 *
264 * @param string $file_path Path to the directory.
265 * @return bool True on success, false on failure.
266 */
267 public function chmod_dir( $file_path ) {
268 if ( ! $file_path ) {
269 return false;
270 }
271
272 return $this->chmod( $file_path, FS_CHMOD_DIR );
273 }
274
275 /**
276 * Get a file mime type.
277 *
278 * @since 1.7.1
279 * @access public
280 * @author Grégory Viguier
281 *
282 * @param string $file_path A file path (prefered) or a filename.
283 * @return string|bool A mime type. False on failure: the test is limited to mime types supported by Imagify.
284 */
285 public function get_mime_type( $file_path ) {
286 if ( ! $file_path ) {
287 return false;
288 }
289
290 $file_type = wp_check_filetype( $file_path, imagify_get_mime_types() );
291
292 return $file_type['type'];
293 }
294
295 /**
296 * Get a file modification date, formated as "mysql". Fallback to current date.
297 *
298 * @since 1.7.1
299 * @access public
300 * @author Grégory Viguier
301 *
302 * @param string $file_path Path to the file.
303 * @return string The date.
304 */
305 public function get_date( $file_path ) {
306 static $offset;
307
308 if ( ! $file_path ) {
309 return current_time( 'mysql' );
310 }
311
312 $date = $this->mtime( $file_path );
313
314 if ( ! $date ) {
315 return current_time( 'mysql' );
316 }
317
318 if ( ! isset( $offset ) ) {
319 $offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
320 }
321
322 return gmdate( 'Y-m-d H:i:s', $date + $offset );
323 }
324
325 /**
326 * Tell if a file is symlinked.
327 *
328 * @since 1.7.1
329 * @access public
330 * @author Grégory Viguier
331 *
332 * @param string $file_path An absolute path.
333 * @return bool
334 */
335 public function is_symlinked( $file_path ) {
336 static $site_root;
337 static $plugin_paths = [];
338 global $wp_plugin_paths;
339
340 if ( ! $file_path ) {
341 return false;
342 }
343
344 $real_path = realpath( $file_path );
345
346 if ( ! $real_path ) {
347 return false;
348 }
349
350 if ( ! isset( $site_root ) ) {
351 $site_root = $this->normalize_path_for_comparison( $this->get_site_root() );
352 }
353
354 $lower_file_path = $this->normalize_path_for_comparison( $real_path );
355
356 if ( strpos( $lower_file_path, $site_root ) !== 0 ) {
357 return true;
358 }
359
360 if ( $wp_plugin_paths && is_array( $wp_plugin_paths ) ) {
361 if ( ! $plugin_paths ) {
362 foreach ( $wp_plugin_paths as $dir => $real_dir ) {
363 $dir = $this->normalize_path_for_comparison( $dir );
364 $plugin_paths[ $dir ] = $this->normalize_path_for_comparison( $real_dir );
365 }
366 }
367
368 $lower_file_path = $this->normalize_path_for_comparison( $file_path );
369
370 foreach ( $plugin_paths as $dir => $real_dir ) {
371 if ( strpos( $lower_file_path, $dir ) === 0 ) {
372 return true;
373 }
374 }
375 }
376
377 return false;
378 }
379
380 /**
381 * Tell if a file is a pdf.
382 *
383 * @since 1.8
384 * @access public
385 * @author Grégory Viguier
386 *
387 * @param string $file_path Path to the file.
388 * @return bool
389 */
390 public function is_pdf( $file_path ) {
391 if ( function_exists( 'finfo_fopen' ) ) {
392 $finfo = finfo_open( FILEINFO_MIME );
393
394 if ( $finfo ) {
395 $mimetype = finfo_file( $finfo, $file_path );
396
397 if ( false !== $mimetype ) {
398 return 'application/pdf' === $mimetype;
399 }
400 }
401 }
402
403 if ( function_exists( 'mime_content_type' ) ) {
404 $mimetype = mime_content_type( $file_path );
405 return 'application/pdf' === $mimetype;
406 }
407
408 return false;
409 }
410
411
412 /** ----------------------------------------------------------------------------------------- */
413 /** CLASS OVERWRITES ======================================================================== */
414 /** ----------------------------------------------------------------------------------------- */
415
416 /**
417 * Move a file and apply chmod.
418 * If the file failed to be moved once, a 2nd attempt is made after applying chmod.
419 *
420 * @since 1.8
421 * @access public
422 * @author Grégory Viguier
423 *
424 * @param string $source Path to the file to move.
425 * @param string $destination Path to the destination.
426 * @param bool $overwrite Allow to overwrite existing file at destination.
427 * @return bool True on success, false on failure.
428 */
429 public function move( $source, $destination, $overwrite = false ) {
430 if ( parent::move( $source, $destination, $overwrite ) ) {
431 return $this->chmod_file( $destination );
432 }
433
434 if ( ! $this->chmod_file( $destination ) ) {
435 return false;
436 }
437
438 if ( parent::move( $source, $destination, $overwrite ) ) {
439 return $this->chmod_file( $destination );
440 }
441
442 return false;
443 }
444
445 /**
446 * Determine if a file or directory is writable.
447 * This function is used to work around certain ACL issues in PHP primarily affecting Windows Servers.
448 * Replacement for is_writable().
449 *
450 * @since 1.7.1
451 * @access public
452 * @author Grégory Viguier
453 *
454 * @param string $file_path Path to the file.
455 * @return bool
456 */
457 public function is_writable( $file_path ) {
458 if ( ! $file_path ) {
459 return false;
460 }
461
462 return wp_is_writable( $file_path );
463 }
464
465
466 /** ----------------------------------------------------------------------------------------- */
467 /** WORK WITH IMAGES ======================================================================== */
468 /** ----------------------------------------------------------------------------------------- */
469
470 /**
471 * Tell if a file is an image.
472 *
473 * @since 1.8
474 * @access public
475 * @author Grégory Viguier
476 *
477 * @param string $file_path Path to the file.
478 * @return bool
479 */
480 public function is_image( $file_path ) {
481 if ( function_exists( 'finfo_fopen' ) ) {
482 $finfo = finfo_open( FILEINFO_MIME );
483
484 if ( $finfo ) {
485 $mimetype = finfo_file( $finfo, $file_path );
486
487 if ( false !== $mimetype ) {
488 return strpos( $mimetype, 'image/' ) === 0;
489 }
490 }
491 }
492
493 if ( function_exists( 'exif_imagetype' ) ) {
494 $mimetype = exif_imagetype( $file_path );
495 return (bool) $mimetype;
496 }
497
498 if ( function_exists( 'mime_content_type' ) ) {
499 $mimetype = mime_content_type( $file_path );
500 return strpos( $mimetype, 'image/' ) === 0;
501 }
502
503 return false;
504 }
505
506 /**
507 * Get an image data.
508 * Replacement for getimagesize().
509 *
510 * @since 1.7.1
511 * @access public
512 * @author Grégory Viguier
513 *
514 * @param string $file_path Path to the file.
515 * @return array The image data. An empty array on failure.
516 */
517 public function get_image_size( $file_path ) {
518 if ( ! $file_path ) {
519 return [];
520 }
521
522 $size = @getimagesize( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
523
524 if ( ! $size || ! isset( $size[0], $size[1] ) ) {
525 return [];
526 }
527
528 return [
529 0 => (int) $size[0],
530 1 => (int) $size[1],
531 'width' => (int) $size[0],
532 'height' => (int) $size[1],
533 'type' => (int) $size[2],
534 'attr' => $size[3],
535 'channels' => isset( $size['channels'] ) ? (int) $size['channels'] : null,
536 'bits' => isset( $size['bits'] ) ? (int) $size['bits'] : null,
537 'mime' => $size['mime'],
538 ];
539 }
540
541 /**
542 * Tell if exif_read_data() is available.
543 *
544 * @since 1.7.1
545 * @access public
546 * @author Grégory Viguier
547 *
548 * @return bool
549 */
550 public function can_get_exif() {
551 static $callable;
552
553 if ( ! isset( $callable ) ) {
554 $callable = is_callable( 'exif_read_data' );
555 }
556
557 return $callable;
558 }
559
560 /**
561 * Get the EXIF headers from an image file.
562 * Replacement for exif_read_data().
563 *
564 * @since 1.7.1
565 * @access public
566 * @author Grégory Viguier
567 * @see https://secure.php.net/manual/en/function.exif-read-data.php
568 *
569 * @param string $file_path Path to the file.
570 * @param string $sections A comma separated list of sections that need to be present in file to produce a result array. See exif_read_data() documentation for values: FILE, COMPUTED, ANY_TAG, IFD0, THUMBNAIL, COMMENT, EXIF.
571 * @param bool $arrays Specifies whether or not each section becomes an array. The sections COMPUTED, THUMBNAIL, and COMMENT always become arrays as they may contain values whose names conflict with other sections.
572 * @param bool $thumbnail When set to TRUE the thumbnail itself is read. Otherwise, only the tagged data is read.
573 * @return array The EXIF headers. An empty array on failure.
574 */
575 public function get_image_exif( $file_path, $sections = null, $arrays = false, $thumbnail = false ) {
576 if ( ! $file_path || ! $this->can_get_exif() ) {
577 return [];
578 }
579
580 $exif = @exif_read_data( $file_path, $sections, $arrays, $thumbnail ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
581
582 return is_array( $exif ) ? $exif : [];
583 }
584
585 /**
586 * Tell if a file is an animated gif.
587 *
588 * @since 1.9.5
589 * @access public
590 * @source https://www.php.net/manual/en/function.imagecreatefromgif.php#104473
591 * @author Grégory Viguier
592 *
593 * @param string $file_path Path to the file.
594 * @return bool|null Null if the file cannot be read.
595 */
596 public function is_animated_gif( $file_path ) {
597 if ( $this->path_info( $file_path, 'extension' ) !== 'gif' ) {
598 // Not a gif file.
599 return false;
600 }
601
602 $fh = @fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_fopen
603
604 if ( ! $fh ) {
605 // Could not open the file.
606 return null;
607 }
608
609 /**
610 * An animated gif contains multiple "frames", with each frame having a header made up of:
611 * - a static 4-byte sequence (\x00\x21\xF9\x04),
612 * - 4 variable bytes,
613 * - a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?).
614 */
615 $count = 0;
616
617 // We read through the file til we reach the end of the file, or we've found at least 2 frame headers.
618 while ( ! feof( $fh ) && $count < 2 ) {
619 // Read 100kb at a time.
620 $chunk = fread( $fh, 1024 * 100 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
621 $count += preg_match_all( '#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches );
622 }
623
624 fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
625
626 return $count > 1;
627 }
628
629
630 /** ----------------------------------------------------------------------------------------- */
631 /** WORK WITH PATHS ========================================================================= */
632 /** ----------------------------------------------------------------------------------------- */
633
634 /**
635 * Make an absolute path relative to WordPress' root folder.
636 * Also works for files from registered symlinked plugins.
637 *
638 * @since 1.7.1
639 * @access public
640 * @author Grégory Viguier
641 *
642 * @param string $file_path An absolute path.
643 * @param string $base A base path to use instead of ABSPATH.
644 * @return string|bool A relative path. Can return the absolute path or false in case of a failure.
645 */
646 public function make_path_relative( $file_path, $base = '' ) {
647 global $wp_plugin_paths;
648
649 if ( ! $file_path ) {
650 return false;
651 }
652
653 $file_path = wp_normalize_path( $file_path );
654 $base = $base ? $this->normalize_dir_path( $base ) : $this->get_site_root();
655 $pos = strpos( $file_path, $base );
656
657 if ( false === $pos && $wp_plugin_paths && is_array( $wp_plugin_paths ) ) {
658 // The file is probably part of a symlinked plugin.
659 arsort( $wp_plugin_paths );
660
661 foreach ( $wp_plugin_paths as $dir => $real_dir ) {
662 if ( strpos( $file_path, $real_dir ) === 0 ) {
663 $file_path = wp_normalize_path( $dir . substr( $file_path, strlen( $real_dir ) ) );
664 }
665 }
666
667 $pos = strpos( $file_path, $base );
668 }
669
670 if ( false === $pos ) {
671 // We're in trouble.
672 return $file_path;
673 }
674
675 return substr_replace( $file_path, '', 0, $pos + strlen( $base ) );
676 }
677
678 /**
679 * Normalize a directory path.
680 * The path is normalized and a trailing slash is added.
681 *
682 * @since 1.7.1
683 * @access public
684 * @author Grégory Viguier
685 *
686 * @param string $file_path The file path.
687 * @return string The normalized dir path.
688 */
689 public function normalize_dir_path( $file_path ) {
690 return wp_normalize_path( trailingslashit( $file_path ) );
691 }
692
693 /**
694 * Normalize a file path, aiming for path comparison.
695 * The path is normalized, case-lowered, and a trailing slash is added.
696 *
697 * @since 1.7.1
698 * @access public
699 * @author Grégory Viguier
700 *
701 * @param string $file_path The file path.
702 * @return string The normalized file path.
703 */
704 public function normalize_path_for_comparison( $file_path ) {
705 return strtolower( $this->normalize_dir_path( $file_path ) );
706 }
707
708
709 /** ----------------------------------------------------------------------------------------- */
710 /** SOME WELL KNOWN PATHS AND URLS ========================================================== */
711 /** ----------------------------------------------------------------------------------------- */
712
713 /**
714 * Tell if WordPress is installed in its own directory: aka WP's path !== site's path.
715 *
716 * @since 1.8.1
717 * @access public
718 * @see https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory
719 * @author Grégory Viguier
720 *
721 * @return string
722 */
723 public function has_wp_its_own_directory() {
724 return $this->get_abspath() !== $this->get_site_root();
725 }
726
727 /**
728 * The path to the server's root is not always '/', it can also be '//' or 'C://'.
729 * I am get_root.
730 *
731 * @since 1.7.1
732 * @access public
733 * @author Grégory Viguier
734 *
735 * @return string The path to the server's root.
736 */
737 public function get_root() {
738 static $groot;
739
740 if ( isset( $groot ) ) {
741 return $groot;
742 }
743
744 $groot = preg_replace( '@^((?:.:)?/+).*@', '$1', $this->get_site_root() );
745
746 return $groot;
747 }
748
749 /**
750 * Tell if a path is the server's root.
751 *
752 * @since 1.7.1
753 * @access public
754 * @author Grégory Viguier
755 *
756 * @param string $path The path.
757 * @return bool
758 */
759 public function is_root( $path ) {
760 $path = rtrim( $path, '/\\' );
761 return '.' === $path || '' === $path || preg_match( '@^.:$@', $path );
762 }
763
764 /**
765 * Get the path to the site's root.
766 * This is an improved version of get_home_path() that *should* work in almost every cases.
767 * Because creating a constant like ABSPATH was too simple.
768 *
769 * @since 1.8.1
770 * @access public
771 * @see get_home_path()
772 * @author Grégory Viguier
773 *
774 * @return string
775 */
776 public function get_site_root() {
777 static $root_path;
778
779 if ( isset( $root_path ) ) {
780 return $root_path;
781 }
782
783 /**
784 * Filter the path to the site's root.
785 *
786 * @since 1.8.1
787 * @author Grégory Viguier
788 *
789 * @param string $root_path Path to the site's root. Default is null.
790 */
791 $root_path = apply_filters( 'imagify_site_root', null );
792
793 if ( is_string( $root_path ) ) {
794 $root_path = trailingslashit( wp_normalize_path( $root_path ) );
795
796 return $root_path;
797 }
798
799 $home = set_url_scheme( untrailingslashit( get_option( 'home' ) ), 'http' );
800 $siteurl = set_url_scheme( untrailingslashit( get_option( 'siteurl' ) ), 'http' );
801
802 if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
803 $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */
804 $pos = strripos( str_replace( '\\', '/', ABSPATH ), trailingslashit( $wp_path_rel_to_home ) );
805 $root_path = substr( ABSPATH, 0, $pos );
806 $root_path = trailingslashit( wp_normalize_path( $root_path ) );
807 return $root_path;
808 }
809
810 if ( ! defined( 'PATH_CURRENT_SITE' ) || ! is_multisite() || is_main_site() ) {
811 $root_path = $this->get_abspath();
812 return $root_path;
813 }
814
815 if ( empty( $_SERVER['DOCUMENT_ROOT'] ) ) {
816 return $root_path;
817 }
818
819 /**
820 * For a multisite in its own directory, get_home_path() returns the expected path only for the main site.
821 *
822 * Friend, each time an attempt is made to improve this method, and especially this part, please increment the following counter.
823 * Improvement attempts: 3.
824 */
825 $document_root = realpath( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
826 // `realpath()` is needed for those cases where $_SERVER['DOCUMENT_ROOT'] is totally different from ABSPATH.
827 $document_root = trailingslashit( str_replace( '\\', '/', $document_root ) );
828 $path_current_site = trim( str_replace( '\\', '/', PATH_CURRENT_SITE ), '/' );
829 $root_path = trailingslashit( wp_normalize_path( $document_root . $path_current_site ) );
830
831 return $root_path;
832 }
833
834 /**
835 * Get the URL of the site's root. It corresponds to the main site's home page URL.
836 *
837 * @since 1.8.1
838 * @since 2.3.1 Added the imagify_site_root_url filter.
839 * @access public
840 * @author Grégory Viguier
841 *
842 * @return string
843 */
844 public function get_site_root_url() {
845 $blog_id = get_current_blog_id();
846
847 if ( isset( $this->site_root_urls[ $blog_id ] ) ) {
848 return $this->site_root_urls[ $blog_id ];
849 }
850
851 $root_url = home_url( '/' );
852
853 if ( is_multisite() && ! is_main_site() ) {
854 $current_network = false;
855
856 if ( function_exists( 'get_network' ) ) {
857 $current_network = get_network();
858 } elseif ( function_exists( 'get_current_site' ) ) {
859 $current_network = get_current_site();
860 }
861
862 if ( $current_network ) {
863 $scheme = is_ssl() ? 'https' : 'http';
864 $root_url = set_url_scheme( 'http://' . $current_network->domain . $current_network->path, $scheme );
865 $root_url = trailingslashit( $root_url );
866 }
867 }
868
869 /**
870 * Filters the URL of the site's root.
871 *
872 * On a multisite, this URL is built from the current network's domain and path. Sites using
873 * domain mapping are served from a domain that does not match it, which prevents Imagify from
874 * recognizing their image URLs as internal. Use this filter to return the URL actually used to
875 * serve the current site.
876 *
877 * The returned value is cached per blog for the rest of the request: callbacks must be
878 * deterministic for a given blog ID.
879 *
880 * @since 2.3.1
881 *
882 * @param string $root_url URL of the site's root, with a trailing slash.
883 * @param int $blog_id ID of the blog the URL was built for.
884 */
885 $filtered_url = apply_filters( 'imagify_site_root_url', $root_url, $blog_id );
886
887 // Ignore anything a callback returns that is not a usable URL, rather than casting it blindly.
888 $this->site_root_urls[ $blog_id ] = is_string( $filtered_url ) && '' !== trim( $filtered_url )
889 ? trailingslashit( trim( $filtered_url ) )
890 : home_url( '/' );
891
892 return $this->site_root_urls[ $blog_id ];
893 }
894
895 /**
896 * Tell if a path is the site's root.
897 *
898 * @since 1.8.1
899 * @access public
900 * @author Grégory Viguier
901 *
902 * @param string $path The path.
903 * @return bool
904 */
905 public function is_site_root( $path ) {
906 return $this->normalize_dir_path( $path ) === $this->get_site_root();
907 }
908
909 /**
910 * Get a clean value of ABSPATH.
911 *
912 * @since 1.7.1
913 * @access public
914 * @author Grégory Viguier
915 *
916 * @return string The path to WordPress' root folder.
917 */
918 public function get_abspath() {
919 static $abspath;
920
921 if ( isset( $abspath ) ) {
922 return $abspath;
923 }
924
925 $abspath = wp_normalize_path( ABSPATH );
926
927 // Make sure ABSPATH is not messed up: it could be defined as a relative path for example (yeah, I know, but we've seen it).
928 $test_file = wp_normalize_path( IMAGIFY_FILE );
929 $pos = strpos( $test_file, $abspath );
930
931 if ( $pos > 0 ) {
932 // ABSPATH has a wrong value.
933 $abspath = substr( $test_file, 0, $pos ) . $abspath;
934
935 } elseif ( false === $pos && class_exists( 'ReflectionClass' ) ) {
936 // Imagify is symlinked (dude, you look for trouble).
937 $reflector = new ReflectionClass( 'WP' );
938 $test_file = $reflector->getFileName();
939 $pos = strpos( $test_file, $abspath );
940
941 if ( 0 < $pos ) {
942 // ABSPATH has a wrong value.
943 $abspath = substr( $test_file, 0, $pos ) . $abspath;
944 }
945 }
946
947 $abspath = trailingslashit( $abspath );
948
949 if ( '/' !== substr( $abspath, 0, 1 ) && ':' !== substr( $abspath, 1, 1 ) ) {
950 $abspath = '/' . $abspath;
951 }
952
953 return $abspath;
954 }
955
956 /**
957 * Tell if a path is WP's root (ABSPATH).
958 *
959 * @since 1.7.1
960 * @access public
961 * @author Grégory Viguier
962 *
963 * @param string $path The path.
964 * @return bool
965 */
966 public function is_abspath( $path ) {
967 return $this->normalize_dir_path( $path ) === $this->get_abspath();
968 }
969
970 /**
971 * Get the upload basedir.
972 *
973 * @since 1.7.1
974 * @access public
975 * @author Grégory Viguier
976 *
977 * @param bool $bypass_error True to return the path even if there is an error. This is used when we want to display this path in a message for example.
978 * @return string|bool The path. False on failure.
979 */
980 public function get_upload_basedir( $bypass_error = false ) {
981 static $upload_basedir;
982 static $upload_basedir_or_error;
983
984 if ( isset( $upload_basedir ) ) {
985 return $bypass_error ? $upload_basedir : $upload_basedir_or_error;
986 }
987
988 $uploads = wp_upload_dir();
989 $upload_basedir = $this->normalize_dir_path( $uploads['basedir'] );
990
991 if ( false !== $uploads['error'] ) {
992 $upload_basedir_or_error = false;
993 } else {
994 $upload_basedir_or_error = $upload_basedir;
995 }
996
997 return $bypass_error ? $upload_basedir : $upload_basedir_or_error;
998 }
999
1000 /**
1001 * Get the upload baseurl.
1002 *
1003 * @since 1.7.1
1004 * @access public
1005 * @author Grégory Viguier
1006 *
1007 * @return string|bool The URL. False on failure.
1008 */
1009 public function get_upload_baseurl() {
1010 static $upload_baseurl;
1011
1012 if ( isset( $upload_baseurl ) ) {
1013 return $upload_baseurl;
1014 }
1015
1016 $uploads = wp_upload_dir();
1017
1018 if ( false !== $uploads['error'] ) {
1019 $upload_baseurl = false;
1020 return $upload_baseurl;
1021 }
1022
1023 $upload_baseurl = trailingslashit( $uploads['baseurl'] );
1024
1025 return $upload_baseurl;
1026 }
1027
1028 /**
1029 * Get the path to the uploads base directory of the main site.
1030 *
1031 * @since 1.8
1032 * @access public
1033 * @author Grégory Viguier
1034 *
1035 * @return string
1036 */
1037 public function get_main_upload_basedir() {
1038 static $basedir;
1039
1040 if ( isset( $basedir ) ) {
1041 return $basedir;
1042 }
1043
1044 $basedir = get_imagify_upload_basedir( true );
1045
1046 if ( is_multisite() ) {
1047 $pattern = '/' . $this->get_multisite_uploads_subdir_pattern() . '$';
1048 $basedir = preg_replace( self::PATTERN_DELIMITER . $pattern . self::PATTERN_DELIMITER, '/', $basedir );
1049 }
1050
1051 return $basedir;
1052 }
1053
1054 /**
1055 * Get the URL of the uploads base directory of the main site.
1056 *
1057 * @since 1.8
1058 * @access public
1059 * @author Grégory Viguier
1060 *
1061 * @return string
1062 */
1063 public function get_main_upload_baseurl() {
1064 static $baseurl;
1065
1066 if ( isset( $baseurl ) ) {
1067 return $baseurl;
1068 }
1069
1070 $baseurl = get_imagify_upload_baseurl( true );
1071
1072 if ( is_multisite() ) {
1073 $pattern = '/' . $this->get_multisite_uploads_subdir_pattern() . '$';
1074 $baseurl = preg_replace( self::PATTERN_DELIMITER . $pattern . self::PATTERN_DELIMITER, '/', $baseurl );
1075 }
1076
1077 return $baseurl;
1078 }
1079
1080 /**
1081 * Get the regex pattern used to match the uploads subdir on multisite in a file path.
1082 * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`.
1083 * Paths tested against these patterns are lower-cased.
1084 *
1085 * @since 1.8
1086 * @access public
1087 * @see _wp_upload_dir()
1088 * @author Grégory Viguier
1089 *
1090 * @return string
1091 */
1092 public function get_multisite_uploads_subdir_pattern() {
1093 static $pattern;
1094
1095 if ( isset( $pattern ) ) {
1096 return $pattern;
1097 }
1098
1099 $pattern = '';
1100
1101 if ( ! is_multisite() ) {
1102 return $pattern;
1103 }
1104
1105 if ( ! get_site_option( 'ms_files_rewriting' ) ) {
1106 if ( defined( 'MULTISITE' ) ) {
1107 $pattern = 'sites/\d+/';
1108 } else {
1109 $pattern = '\d+/';
1110 }
1111 } elseif ( defined( 'UPLOADS' ) ) {
1112 $site_id = (string) get_current_blog_id();
1113 $path = $this->get_upload_basedir( true ); // Something like `/absolute/path/to/wp-content/blogs.dir/3/files/`, also for site 1.
1114 $path = strrev( $path );
1115
1116 if ( preg_match( self::PATTERN_DELIMITER . '^.*' . strrev( $site_id ) . '[^/]*/' . self::PATTERN_DELIMITER . 'U', $path, $matches ) ) {
1117 $pattern = end( $matches );
1118 $pattern = ltrim( strtolower( strrev( $pattern ) ), '/' );
1119 $pattern = str_replace( $site_id, '\d+', $pattern );
1120 }
1121 }
1122
1123 /**
1124 * Filter the regex pattern used to match the uploads subdir on multisite in a file path.
1125 * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`.
1126 * Important: lowercase, no heading slash, mandatory trailing slash.
1127 *
1128 * @since 1.8
1129 * @author Grégory Viguier
1130 *
1131 * @param string $pattern The regex pattern.
1132 */
1133 $pattern = apply_filters( 'imagify_multisite_uploads_subdir_pattern', $pattern );
1134
1135 return $pattern;
1136 }
1137 }
1138