<?php
defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' );

require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';

/**
 * Class that enhance the WP filesystem class.
 *
 * @since  1.7.1
 * @author Grégory Viguier
 */
class Imagify_Filesystem extends WP_Filesystem_Direct {

	/**
	 * Class version.
	 *
	 * @var string
	 */
	const VERSION = '1.2';

	/**
	 * Delimiter used for regex patterns.
	 *
	 * @var    string
	 * @since  1.8
	 * @author Grégory Viguier
	 */
	const PATTERN_DELIMITER = '@';

	/**
	 * The single instance of the class.
	 *
	 * @var    object
	 * @access protected
	 */
	protected static $_instance;


	/** ----------------------------------------------------------------------------------------- */
	/** INSTANCIATION =========================================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Constructor.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 */
	public function __construct() {
		// Define the permission constants if not already done.
		if ( ! defined( 'FS_CHMOD_DIR' ) ) {
			define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
		}
		if ( ! defined( 'FS_CHMOD_FILE' ) ) {
			define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
		}

		parent::__construct( '' );
	}

	/**
	 * Get the main Instance.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @return object Main instance.
	 */
	public static function get_instance() {
		if ( ! isset( self::$_instance ) ) {
			self::$_instance = new self();
		}

		return self::$_instance;
	}


	/** ----------------------------------------------------------------------------------------- */
	/** CUSTOM TOOLS ============================================================================ */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Get the file name.
	 * Replacement for basename().
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return string|bool       The base name of the given path. False on failure.
	 */
	public function file_name( $file_path ) {
		if ( ! $file_path ) {
			return false;
		}

		return wp_basename( $file_path );
	}

	/**
	 * Get the parent directory's path.
	 * Replacement for dirname().
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return string|bool       The directory path with a trailing slash. False on failure.
	 */
	public function dir_path( $file_path ) {
		if ( ! $file_path ) {
			return false;
		}

		$file_path = dirname( $file_path );

		return $this->is_root( $file_path ) ? $this->get_root() : trailingslashit( $file_path );
	}

	/**
	 * Get information about a file path.
	 * Replacement for pathinfo().
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @param  string $option    If present, specifies a specific element to be returned; one of 'dir_path', 'file_name', 'extension' or 'file_base'.
	 *                           If option is not specified, returns all available elements.
	 * @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).
	 */
	public function path_info( $file_path, $option = null ) {
		if ( ! $file_path ) {
			if ( isset( $option ) ) {
				return '';
			}

			return array(
				'dir_path'  => '',
				'file_name' => '',
				'extension' => null,
				'file_base' => '',
			);
		}

		if ( isset( $option ) ) {
			$options = array(
				'dir_path'  => PATHINFO_DIRNAME,
				'file_name' => PATHINFO_BASENAME,
				'extension' => PATHINFO_EXTENSION,
				'file_base' => PATHINFO_FILENAME,
			);

			if ( ! isset( $options[ $option ] ) ) {
				return '';
			}

			$output = pathinfo( $file_path, $options[ $option ] );

			if ( 'dir_path' !== $option ) {
				return $output;
			}

			return $this->is_root( $output ) ? $this->get_root() : trailingslashit( $output );
		}

		$output = pathinfo( $file_path );

		$output['dirname']   = $this->is_root( $output['dirname'] ) ? $this->get_root()    : trailingslashit( $output['dirname'] );
		$output['extension'] = isset( $output['extension'] )        ? $output['extension'] : null;

		// '/www/htdocs/inc/lib.inc.php'
		return array(
			'dir_path'  => $output['dirname'],   // '/www/htdocs/inc/'
			'file_name' => $output['basename'],  // 'lib.inc.php'
			'extension' => $output['extension'], // 'php'
			'file_base' => $output['filename'],  // 'lib.inc'
		);
	}

	/**
	 * Recursive directory creation based on full path. Will attempt to set permissions on folders.
	 * Replacement for recursive mkdir().
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $path Full path to attempt to create.
	 * @return bool         Whether the path was created. True if path already exists.
	 */
	public function make_dir( $path ) {
		/*
		* Safe mode fails with a trailing slash under certain PHP versions.
		*/
		$path = untrailingslashit( wp_normalize_path( $path ) );

		if ( $this->is_root( $path ) ) {
			return $this->is_dir( $this->get_root() ) && $this->is_writable( $this->get_root() );
		}

		if ( $this->exists( $path ) ) {
			return $this->is_dir( $path ) && $this->is_writable( $path );
		}

		$site_root = $this->get_site_root();

		if ( strpos( $path, $site_root ) !== 0 ) {
			return false;
		}

		$bits = preg_replace( '@^' . preg_quote( $site_root, '@' ) . '@i', '', $path );
		$bits = explode( '/', trim( $bits, '/' ) );
		$path = untrailingslashit( $site_root );

		foreach ( $bits as $bit ) {
			$parent_path = $path;
			$path       .= '/' . $bit;

			if ( $this->exists( $path ) ) {
				if ( ! $this->is_dir( $path ) ) {
					return false;
				}

				continue;
			}

			if ( ! $this->is_writable( $parent_path ) ) {
				$this->chmod_dir( $parent_path );

				if ( ! $this->is_writable( $parent_path ) ) {
					return false;
				}
			}

			$this->mkdir( $path );

			if ( ! $this->exists( $path ) ) {
				return false;
			}

			$this->touch( trailingslashit( $path ) . 'index.php' );
		}

		return true;
	}

	/**
	 * Set a file permissions using FS_CHMOD_FILE.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return bool              True on success, false on failure.
	 */
	public function chmod_file( $file_path ) {
		if ( ! $file_path ) {
			return false;
		}

		return $this->chmod( $file_path, FS_CHMOD_FILE );
	}

	/**
	 * Set a directory permissions using FS_CHMOD_DIR.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the directory.
	 * @return bool              True on success, false on failure.
	 */
	public function chmod_dir( $file_path ) {
		if ( ! $file_path ) {
			return false;
		}

		return $this->chmod( $file_path, FS_CHMOD_DIR );
	}

	/**
	 * Get a file mime type.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path A file path (prefered) or a filename.
	 * @return string|bool       A mime type. False on failure: the test is limited to mime types supported by Imagify.
	 */
	public function get_mime_type( $file_path ) {
		if ( ! $file_path ) {
			return false;
		}

		$file_type = wp_check_filetype( $file_path, imagify_get_mime_types() );

		return $file_type['type'];
	}

	/**
	 * Get a file modification date, formated as "mysql". Fallback to current date.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return string            The date.
	 */
	public function get_date( $file_path ) {
		static $offset;

		if ( ! $file_path ) {
			return current_time( 'mysql' );
		}

		$date = $this->mtime( $file_path );

		if ( ! $date ) {
			return current_time( 'mysql' );
		}

		if ( ! isset( $offset ) ) {
			$offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
		}

		return gmdate( 'Y-m-d H:i:s', $date + $offset );
	}

	/**
	 * Tell if a file is symlinked.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path An absolute path.
	 * @return bool
	 */
	public function is_symlinked( $file_path ) {
		static $site_root;
		static $plugin_paths = array();
		global $wp_plugin_paths;

		if ( ! $file_path ) {
			return false;
		}

		$real_path = realpath( $file_path );

		if ( ! $real_path ) {
			return false;
		}

		if ( ! isset( $site_root ) ) {
			$site_root = $this->normalize_path_for_comparison( $this->get_site_root() );
		}

		$lower_file_path = $this->normalize_path_for_comparison( $real_path );

		if ( strpos( $lower_file_path, $site_root ) !== 0 ) {
			return true;
		}

		if ( $wp_plugin_paths && is_array( $wp_plugin_paths ) ) {
			if ( ! $plugin_paths ) {
				foreach ( $wp_plugin_paths as $dir => $real_dir ) {
					$dir = $this->normalize_path_for_comparison( $dir );
					$plugin_paths[ $dir ] = $this->normalize_path_for_comparison( $real_dir );
				}
			}

			$lower_file_path = $this->normalize_path_for_comparison( $file_path );

			foreach ( $plugin_paths as $dir => $real_dir ) {
				if ( strpos( $lower_file_path, $dir ) === 0 ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Tell if a file is a pdf.
	 *
	 * @since  1.8
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return bool
	 */
	public function is_pdf( $file_path ) {
		if ( function_exists( 'finfo_fopen' ) ) {
			$finfo = finfo_open( FILEINFO_MIME );

			if ( $finfo ) {
				$mimetype = finfo_file( $finfo, $file_path );

				if ( false !== $mimetype ) {
					return 'application/pdf' === $mimetype;
				}
			}
		}

		if ( function_exists( 'mime_content_type' ) ) {
			$mimetype = mime_content_type( $file_path );
			return 'application/pdf' === $mimetype;
		}

		return false;
	}


	/** ----------------------------------------------------------------------------------------- */
	/** CLASS OVERWRITES ======================================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Move a file and apply chmod.
	 * If the file failed to be moved once, a 2nd attempt is made after applying chmod.
	 *
	 * @since  1.8
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $source      Path to the file to move.
	 * @param  string $destination Path to the destination.
	 * @param  bool   $overwrite   Allow to overwrite existing file at destination.
	 * @return bool                True on success, false on failure.
	 */
	public function move( $source, $destination, $overwrite = false ) {
		if ( parent::move( $source, $destination, $overwrite ) ) {
			return $this->chmod_file( $destination );
		}

		if ( ! $this->chmod_file( $destination ) ) {
			return false;
		}

		if ( parent::move( $source, $destination, $overwrite ) ) {
			return $this->chmod_file( $destination );
		}

		return false;
	}

	/**
	 * Determine if a file or directory is writable.
	 * This function is used to work around certain ACL issues in PHP primarily affecting Windows Servers.
	 * Replacement for is_writable().
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return bool
	 */
	public function is_writable( $file_path ) {
		if ( ! $file_path ) {
			return false;
		}

		return wp_is_writable( $file_path );
	}


	/** ----------------------------------------------------------------------------------------- */
	/** WORK WITH IMAGES ======================================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Tell if a file is an image.
	 *
	 * @since  1.8
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return bool
	 */
	public function is_image( $file_path ) {
		if ( function_exists( 'finfo_fopen' ) ) {
			$finfo = finfo_open( FILEINFO_MIME );

			if ( $finfo ) {
				$mimetype = finfo_file( $finfo, $file_path );

				if ( false !== $mimetype ) {
					return strpos( $mimetype, 'image/' ) === 0;
				}
			}
		}

		if ( function_exists( 'exif_imagetype' ) ) {
			$mimetype = exif_imagetype( $file_path );
			return (bool) $mimetype;
		}

		if ( function_exists( 'mime_content_type' ) ) {
			$mimetype = mime_content_type( $file_path );
			return strpos( $mimetype, 'image/' ) === 0;
		}

		return false;
	}

	/**
	 * Get an image data.
	 * Replacement for getimagesize().
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return array             The image data. An empty array on failure.
	 */
	public function get_image_size( $file_path ) {
		if ( ! $file_path ) {
			return array();
		}

		$size = @getimagesize( $file_path );

		if ( ! $size || ! isset( $size[0], $size[1] ) ) {
			return array();
		}

		return array(
			0          => (int) $size[0],
			1          => (int) $size[1],
			'width'    => (int) $size[0],
			'height'   => (int) $size[1],
			'type'     => (int) $size[2],
			'attr'     => $size[3],
			'channels' => isset( $size['channels'] ) ? (int) $size['channels'] : null,
			'bits'     => isset( $size['bits'] )     ? (int) $size['bits']     : null,
			'mime'     => $size['mime'],
		);
	}

	/**
	 * Tell if exif_read_data() is available.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @return bool
	 */
	public function can_get_exif() {
		static $callable;

		if ( ! isset( $callable ) ) {
			$callable = is_callable( 'exif_read_data' );
		}

		return $callable;
	}

	/**
	 * Get the EXIF headers from an image file.
	 * Replacement for exif_read_data().
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 * @see    https://secure.php.net/manual/en/function.exif-read-data.php
	 *
	 * @param  string $file_path Path to the file.
	 * @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.
	 * @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.
	 * @param  bool   $thumbnail When set to TRUE the thumbnail itself is read. Otherwise, only the tagged data is read.
	 * @return array             The EXIF headers. An empty array on failure.
	 */
	public function get_image_exif( $file_path, $sections = null, $arrays = false, $thumbnail = false ) {
		if ( ! $file_path || ! $this->can_get_exif() ) {
			return array();
		}

		$exif = @exif_read_data( $file_path, $sections, $arrays, $thumbnail );

		return is_array( $exif ) ? $exif : array();
	}

	/**
	 * Tell if a file is an animated gif.
	 *
	 * @since  1.9.5
	 * @access public
	 * @source https://www.php.net/manual/en/function.imagecreatefromgif.php#104473
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path Path to the file.
	 * @return bool|null         Null if the file cannot be read.
	 */
	public function is_animated_gif( $file_path ) {
		if ( $this->path_info( $file_path, 'extension' ) !== 'gif' ) {
			// Not a gif file.
			return false;
		}

		$fh = @fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen

		if ( ! $fh ) {
			// Could not open the file.
			return null;
		}

		/**
		 * An animated gif contains multiple "frames", with each frame having a header made up of:
		 * - a static 4-byte sequence (\x00\x21\xF9\x04),
		 * - 4 variable bytes,
		 * - a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?).
		 */
		$count = 0;

		// We read through the file til we reach the end of the file, or we've found at least 2 frame headers.
		while ( ! feof( $fh ) && $count < 2 ) {
			// Read 100kb at a time.
			$chunk  = fread( $fh, 1024 * 100 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fread
			$count += preg_match_all( '#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches );
		}

		fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose

		return $count > 1;
	}


	/** ----------------------------------------------------------------------------------------- */
	/** WORK WITH PATHS ========================================================================= */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Make an absolute path relative to WordPress' root folder.
	 * Also works for files from registered symlinked plugins.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path An absolute path.
	 * @param  string $base      A base path to use instead of ABSPATH.
	 * @return string|bool       A relative path. Can return the absolute path or false in case of a failure.
	 */
	public function make_path_relative( $file_path, $base = '' ) {
		global $wp_plugin_paths;

		if ( ! $file_path ) {
			return false;
		}

		$file_path = wp_normalize_path( $file_path );
		$base      = $base ? $this->normalize_dir_path( $base ) : $this->get_site_root();
		$pos       = strpos( $file_path, $base );

		if ( false === $pos && $wp_plugin_paths && is_array( $wp_plugin_paths ) ) {
			// The file is probably part of a symlinked plugin.
			arsort( $wp_plugin_paths );

			foreach ( $wp_plugin_paths as $dir => $real_dir ) {
				if ( strpos( $file_path, $real_dir ) === 0 ) {
					$file_path = wp_normalize_path( $dir . substr( $file_path, strlen( $real_dir ) ) );
				}
			}

			$pos = strpos( $file_path, $base );
		}

		if ( false === $pos ) {
			// We're in trouble.
			return $file_path;
		}

		return substr_replace( $file_path, '', 0, $pos + strlen( $base ) );
	}

	/**
	 * Normalize a directory path.
	 * The path is normalized and a trailing slash is added.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path The file path.
	 * @return string            The normalized dir path.
	 */
	public function normalize_dir_path( $file_path ) {
		return wp_normalize_path( trailingslashit( $file_path ) );
	}

	/**
	 * Normalize a file path, aiming for path comparison.
	 * The path is normalized, case-lowered, and a trailing slash is added.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $file_path The file path.
	 * @return string            The normalized file path.
	 */
	public function normalize_path_for_comparison( $file_path ) {
		return strtolower( $this->normalize_dir_path( $file_path ) );
	}


	/** ----------------------------------------------------------------------------------------- */
	/** SOME WELL KNOWN PATHS AND URLS ========================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Tell if WordPress is installed in its own directory: aka WP's path !== site's path.
	 *
	 * @since  1.8.1
	 * @access public
	 * @see    https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory
	 * @author Grégory Viguier
	 *
	 * @return string
	 */
	public function has_wp_its_own_directory() {
		return $this->get_abspath() !== $this->get_site_root();
	}

	/**
	 * The path to the server's root is not always '/', it can also be '//' or 'C://'.
	 * I am get_root.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @return string The path to the server's root.
	 */
	public function get_root() {
		static $groot;

		if ( isset( $groot ) ) {
			return $groot;
		}

		$groot = preg_replace( '@^((?:.:)?/+).*@', '$1', $this->get_site_root() );

		return $groot;
	}

	/**
	 * Tell if a path is the server's root.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $path The path.
	 * @return bool
	 */
	public function is_root( $path ) {
		$path = rtrim( $path, '/\\' );
		return '.' === $path || '' === $path || preg_match( '@^.:$@', $path );
	}

	/**
	 * Get the path to the site's root.
	 * This is an improved version of get_home_path() that *should* work in almost every cases.
	 * Because creating a constant like ABSPATH was too simple.
	 *
	 * @since  1.8.1
	 * @access public
	 * @see    get_home_path()
	 * @author Grégory Viguier
	 *
	 * @return string
	 */
	public function get_site_root() {
		static $root_path;

		if ( isset( $root_path ) ) {
			return $root_path;
		}

		/**
		 * Filter the path to the site's root.
		 *
		 * @since  1.8.1
		 * @author Grégory Viguier
		 *
		 * @param string $root_path Path to the site's root. Default is null.
		 */
		$root_path = apply_filters( 'imagify_site_root', null );

		if ( is_string( $root_path ) ) {
			$root_path = trailingslashit( wp_normalize_path( $root_path ) );

			return $root_path;
		}

		$home    = set_url_scheme( untrailingslashit( get_option( 'home' ) ), 'http' );
		$siteurl = set_url_scheme( untrailingslashit( get_option( 'siteurl' ) ), 'http' );

		if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
			$wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */
			$pos                 = strripos( str_replace( '\\', '/', ABSPATH ), trailingslashit( $wp_path_rel_to_home ) );
			$root_path           = substr( ABSPATH, 0, $pos );
			$root_path           = trailingslashit( wp_normalize_path( $root_path ) );
			return $root_path;
		}

		if ( ! defined( 'PATH_CURRENT_SITE' ) || ! is_multisite() || is_main_site() ) {
			$root_path = $this->get_abspath();
			return $root_path;
		}

		/**
		 * For a multisite in its own directory, get_home_path() returns the expected path only for the main site.
		 *
		 * Friend, each time an attempt is made to improve this method, and especially this part, please increment the following counter.
		 * Improvement attempts: 3.
		 */
		$document_root     = realpath( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ); // `realpath()` is needed for those cases where $_SERVER['DOCUMENT_ROOT'] is totally different from ABSPATH.
		$document_root     = trailingslashit( str_replace( '\\', '/', $document_root ) );
		$path_current_site = trim( str_replace( '\\', '/', PATH_CURRENT_SITE ), '/' );
		$root_path         = trailingslashit( wp_normalize_path( $document_root . $path_current_site ) );

		return $root_path;
	}

	/**
	 * Get the URL of the site's root. It corresponds to the main site's home page URL.
	 *
	 * @since  1.8.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @return string
	 */
	public function get_site_root_url() {
		static $root_url;

		if ( isset( $root_url ) ) {
			return $root_url;
		}

		if ( ! is_multisite() || is_main_site() ) {
			$root_url = home_url( '/' );
			return $root_url;
		}

		$current_network = false;

		if ( function_exists( 'get_network' ) ) {
			$current_network = get_network();
		} elseif ( function_exists( 'get_current_site' ) ) {
			$current_network = get_current_site();
		}

		if ( ! $current_network ) {
			$root_url = home_url( '/' );
			return $root_url;
		}

		$root_url = is_ssl() ? 'https' : 'http';
		$root_url = set_url_scheme( 'http://' . $current_network->domain . $current_network->path, $root_url );
		$root_url = trailingslashit( $root_url );

		return $root_url;
	}

	/**
	 * Tell if a path is the site's root.
	 *
	 * @since  1.8.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $path The path.
	 * @return bool
	 */
	public function is_site_root( $path ) {
		return $this->normalize_dir_path( $path ) === $this->get_site_root();
	}

	/**
	 * Get a clean value of ABSPATH.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @return string The path to WordPress' root folder.
	 */
	public function get_abspath() {
		static $abspath;

		if ( isset( $abspath ) ) {
			return $abspath;
		}

		$abspath = wp_normalize_path( ABSPATH );

		// 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).
		$test_file = wp_normalize_path( IMAGIFY_FILE );
		$pos       = strpos( $test_file, $abspath );

		if ( $pos > 0 ) {
			// ABSPATH has a wrong value.
			$abspath = substr( $test_file, 0, $pos ) . $abspath;

		} elseif ( false === $pos && class_exists( 'ReflectionClass' ) ) {
			// Imagify is symlinked (dude, you look for trouble).
			$reflector = new ReflectionClass( 'WP' );
			$test_file = $reflector->getFileName();
			$pos       = strpos( $test_file, $abspath );

			if ( 0 < $pos ) {
				// ABSPATH has a wrong value.
				$abspath = substr( $test_file, 0, $pos ) . $abspath;
			}
		}

		$abspath = trailingslashit( $abspath );

		if ( '/' !== substr( $abspath, 0, 1 ) && ':' !== substr( $abspath, 1, 1 ) ) {
			$abspath = '/' . $abspath;
		}

		return $abspath;
	}

	/**
	 * Tell if a path is WP's root (ABSPATH).
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @param  string $path The path.
	 * @return bool
	 */
	public function is_abspath( $path ) {
		return $this->normalize_dir_path( $path ) === $this->get_abspath();
	}

	/**
	 * Get the upload basedir.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @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.
	 * @return string|bool        The path. False on failure.
	 */
	public function get_upload_basedir( $bypass_error = false ) {
		static $upload_basedir;
		static $upload_basedir_or_error;

		if ( isset( $upload_basedir ) ) {
			return $bypass_error ? $upload_basedir : $upload_basedir_or_error;
		}

		$uploads        = wp_upload_dir();
		$upload_basedir = $this->normalize_dir_path( $uploads['basedir'] );

		if ( false !== $uploads['error'] ) {
			$upload_basedir_or_error = false;
		} else {
			$upload_basedir_or_error = $upload_basedir;
		}

		return $bypass_error ? $upload_basedir : $upload_basedir_or_error;
	}

	/**
	 * Get the upload baseurl.
	 *
	 * @since  1.7.1
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @return string|bool The URL. False on failure.
	 */
	public function get_upload_baseurl() {
		static $upload_baseurl;

		if ( isset( $upload_baseurl ) ) {
			return $upload_baseurl;
		}

		$uploads = wp_upload_dir();

		if ( false !== $uploads['error'] ) {
			$upload_baseurl = false;
			return $upload_baseurl;
		}

		$upload_baseurl = trailingslashit( $uploads['baseurl'] );

		return $upload_baseurl;
	}

	/**
	 * Get the path to the uploads base directory of the main site.
	 *
	 * @since  1.8
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @return string
	 */
	public function get_main_upload_basedir() {
		static $basedir;

		if ( isset( $basedir ) ) {
			return $basedir;
		}

		$basedir = get_imagify_upload_basedir( true );

		if ( is_multisite() ) {
			$pattern = '/' . $this->get_multisite_uploads_subdir_pattern() . '$';
			$basedir = preg_replace( self::PATTERN_DELIMITER . $pattern . self::PATTERN_DELIMITER, '/', $basedir );
		}

		return $basedir;
	}

	/**
	 * Get the URL of the uploads base directory of the main site.
	 *
	 * @since  1.8
	 * @access public
	 * @author Grégory Viguier
	 *
	 * @return string
	 */
	public function get_main_upload_baseurl() {
		static $baseurl;

		if ( isset( $baseurl ) ) {
			return $baseurl;
		}

		$baseurl = get_imagify_upload_baseurl( true );

		if ( is_multisite() ) {
			$pattern = '/' . $this->get_multisite_uploads_subdir_pattern() . '$';
			$baseurl = preg_replace( self::PATTERN_DELIMITER . $pattern . self::PATTERN_DELIMITER, '/', $baseurl );
		}

		return $baseurl;
	}

	/**
	 * Get the regex pattern used to match the uploads subdir on multisite in a file path.
	 * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`.
	 * Paths tested against these patterns are lower-cased.
	 *
	 * @since  1.8
	 * @access public
	 * @see    _wp_upload_dir()
	 * @author Grégory Viguier
	 *
	 * @return string
	 */
	public function get_multisite_uploads_subdir_pattern() {
		static $pattern;

		if ( isset( $pattern ) ) {
			return $pattern;
		}

		$pattern = '';

		if ( ! is_multisite() ) {
			return $pattern;
		}

		if ( ! get_site_option( 'ms_files_rewriting' ) ) {
			if ( defined( 'MULTISITE' ) ) {
				$pattern = 'sites/\d+/';
			} else {
				$pattern = '\d+/';
			}
		} elseif ( defined( 'UPLOADS' ) ) {
			$site_id = (string) get_current_blog_id();
			$path    = $this->get_upload_basedir( true ); // Something like `/absolute/path/to/wp-content/blogs.dir/3/files/`, also for site 1.
			$path    = strrev( $path );

			if ( preg_match( self::PATTERN_DELIMITER . '^.*' . strrev( $site_id ) . '[^/]*/' . self::PATTERN_DELIMITER . 'U', $path, $matches ) ) {
				$pattern = end( $matches );
				$pattern = ltrim( strtolower( strrev( $pattern ) ), '/' );
				$pattern = str_replace( $site_id, '\d+', $pattern );
			}
		}

		/**
		 * Filter the regex pattern used to match the uploads subdir on multisite in a file path.
		 * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`.
		 * Important: lowercase, no heading slash, mandatory trailing slash.
		 *
		 * @since  1.8
		 * @author Grégory Viguier
		 *
		 * @param string $pattern The regex pattern.
		 */
		$pattern = apply_filters( 'imagify_multisite_uploads_subdir_pattern', $pattern );

		return $pattern;
	}
}
