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

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