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

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