OwlCyberSecurity - MANAGER
Edit File: class-imagify-files-scan.php
<?php defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Class handling everything that is related to "custom folders optimization". * * @since 1.7 * @author Grégory Viguier */ class Imagify_Files_Scan { /** * Class version. * * @var string * @since 1.7 * @author Grégory Viguier */ const VERSION = '1.1.1'; /** * Get files (optimizable by Imagify) recursively from a specific folder. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $folder An absolute path to a folder. * @return array|object An array of absolute paths. A WP_Error object on error. */ public static function get_files_from_folder( $folder ) { $filesystem = imagify_get_filesystem(); // Formate and validate the folder path. if ( ! is_string( $folder ) ) { return new WP_Error( 'invalid_folder', __( 'Invalid folder.', 'imagify' ) ); } $folder = realpath( $folder ); if ( ! $folder ) { return new WP_Error( 'folder_not_exists', __( 'This folder does not exist.', 'imagify' ) ); } if ( ! $filesystem->is_dir( $folder ) ) { return new WP_Error( 'not_a_folder', __( 'This file is not a folder.', 'imagify' ) ); } if ( self::is_path_forbidden( trailingslashit( $folder ) ) ) { return new WP_Error( 'folder_forbidden', __( 'This folder is not allowed.', 'imagify' ) ); } // Finally we made all our validations. if ( $filesystem->is_site_root( $folder ) ) { // For the site's root, we don't look in sub-folders. $dir = new DirectoryIterator( $folder ); $dir = new Imagify_Files_Iterator( $dir, false ); $images = array(); foreach ( new IteratorIterator( $dir ) as $file ) { $images[] = $file->getPathname(); } return $images; } /** * 4096 stands for FilesystemIterator::SKIP_DOTS, which was introduced in php 5.3.0. * 8192 stands for FilesystemIterator::UNIX_PATHS, which was introduced in php 5.3.0. */ $dir = new RecursiveDirectoryIterator( $folder, 4096 | 8192 ); $dir = new Imagify_Files_Recursive_Iterator( $dir ); $images = new RecursiveIteratorIterator( $dir ); $images = array_keys( iterator_to_array( $images ) ); return $images; } /** ----------------------------------------------------------------------------------------- */ /** FORBIDDEN FOLDERS AND FILES ============================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if a path is autorized. * When testing a folder, the path MUST have a trailing slash. * * @since 1.7.1 * @since 1.8 The path must have a trailing slash if for a folder. * @access public * @author Grégory Viguier * * @param string $file_path A file or folder absolute path. * @return bool */ public static function is_path_autorized( $file_path ) { return ! self::is_path_forbidden( $file_path ); } /** * Tell if a path is forbidden. * When testing a folder, the path MUST have a trailing slash. * * @since 1.7 * @since 1.8 The path must have a trailing slash if for a folder. * @access public * @author Grégory Viguier * * @param string $file_path A file or folder absolute path. * @return bool */ public static function is_path_forbidden( $file_path ) { static $folders; $filesystem = imagify_get_filesystem(); if ( self::is_filename_forbidden( $filesystem->file_name( $file_path ) ) ) { return true; } if ( $filesystem->is_symlinked( $file_path ) ) { // Files outside the site's folder are forbidden. return true; } if ( ! isset( $folders ) ) { $folders = self::get_forbidden_folders(); $folders = array_map( 'strtolower', $folders ); $folders = array_flip( $folders ); } $file_path = self::normalize_path_for_comparison( $file_path ); if ( isset( $folders[ $file_path ] ) ) { return true; } $delim = Imagify_Filesystem::PATTERN_DELIMITER; foreach ( self::get_forbidden_folder_patterns() as $pattern ) { if ( preg_match( $delim . '^' . $pattern . $delim, $file_path ) ) { return true; } } foreach ( $folders as $folder => $i ) { if ( strpos( $file_path, $folder ) === 0 ) { return true; } } return false; } /** * Get the list of folders where Imagify won't look for files to optimize. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array A list of absolute paths. */ public static function get_forbidden_folders() { static $folders; if ( isset( $folders ) ) { return $folders; } $filesystem = imagify_get_filesystem(); $site_root = $filesystem->get_site_root(); $abspath = $filesystem->get_abspath(); $folders = array( // Server. $site_root . 'cgi-bin', // `cgi-bin` // WordPress. $abspath . 'wp-admin', // `wp-admin` $abspath . WPINC, // `wp-includes` WP_CONTENT_DIR . '/mu-plugins', // MU plugins. WP_CONTENT_DIR . '/upgrade', // Upgrade. // Plugins. WP_CONTENT_DIR . '/bps-backup', // BulletProof Security. self::get_ewww_tools_path(), // EWWW: /wp-content/ewww. WP_CONTENT_DIR . '/ngg', // NextGen Gallery. WP_CONTENT_DIR . '/ngg_styles', // NextGen Gallery. WP_CONTENT_DIR . '/w3tc-config', // W3 Total Cache. WP_CONTENT_DIR . '/wfcache', // WP Fastest Cache. WP_CONTENT_DIR . '/wp-rocket-config', // WP Rocket. Imagify_Custom_Folders::get_backup_dir_path(), // Imagify "Custom folders" backup: /imagify-backup. IMAGIFY_PATH, // Imagify plugin: /wp-content/plugins/imagify. self::get_shortpixel_path(), // ShortPixel: /wp-content/uploads/ShortpixelBackups. ); if ( ! is_multisite() ) { $uploads_dir = $filesystem->get_upload_basedir( true ); $ngg_galleries = self::get_ngg_galleries_path(); if ( $ngg_galleries ) { $folders[] = $ngg_galleries; // NextGen Gallery: /wp-content/gallery. } $folders[] = $uploads_dir . 'formidable'; // Formidable Forms: /wp-content/uploads/formidable. $folders[] = get_imagify_backup_dir_path( true ); // Imagify Media Library backup: /wp-content/uploads/backup. $folders[] = self::get_wc_logs_path(); // WooCommerce Logs: /wp-content/uploads/wc-logs. $folders[] = $uploads_dir . 'woocommerce_uploads'; // WooCommerce uploads: /wp-content/uploads/woocommerce_uploads. } $folders = array_map( array( $filesystem, 'normalize_dir_path' ), $folders ); /** * Add folders to the list of forbidden ones. * * @since 1.7 * @author Grégory Viguier * * @param array $added_folders List of absolute paths. * @param array $folders List of folders already forbidden. */ $added_folders = apply_filters( 'imagify_add_forbidden_folders', array(), $folders ); $added_folders = array_filter( (array) $added_folders ); $added_folders = array_filter( $added_folders, 'is_string' ); if ( ! $added_folders ) { return $folders; } $added_folders = array_map( array( $filesystem, 'normalize_dir_path' ), $added_folders ); $folders = array_merge( $folders, $added_folders ); $folders = array_flip( array_flip( $folders ) ); return $folders; } /** * Get the list of folder patterns where Imagify won't look for files to optimize. This is meant for paths that are dynamic. * `^` will be prepended to each pattern (aka, the pattern must match an absolute path). * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`. * Paths tested against these patterns are lower-cased. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array A list of regex patterns. */ public static function get_forbidden_folder_patterns() { static $folders; if ( isset( $folders ) ) { return $folders; } $folders = array(); // Media Library: /wp\-content/uploads/(sites/\d+/)?\d{4}/\d{2}/. $folders[] = self::get_media_library_pattern(); if ( is_multisite() ) { /** * On multisite we can't exclude Imagify's library backup folders, or any other folder located in the uploads folders (created by other plugins): there are too many ways it can fail. * Only exception we're aware of so far is NextGen Gallery, because it provides a clear pattern to use. */ $ngg_galleries = self::get_ngg_galleries_multisite_pattern(); if ( $ngg_galleries ) { // NextGen Gallery: /wp\-content/uploads/sites/\d+/nggallery/. $folders[] = $ngg_galleries; } } /** * Add folder patterns to the list of forbidden ones. * Don't forget to use `Imagify_Files_Scan::normalize_path_for_regex( $path )`! * * @since 1.7 * @author Grégory Viguier * * @param array $added_folders List of patterns. * @param array $folders List of patterns already forbidden. */ $added_folders = apply_filters( 'imagify_add_forbidden_folder_patterns', array(), $folders ); $added_folders = array_filter( (array) $added_folders ); $added_folders = array_filter( $added_folders, 'is_string' ); if ( ! $added_folders ) { return $folders; } $folders = array_merge( $folders, $added_folders ); $folders = array_flip( array_flip( $folders ) ); return $folders; } /** * Tell if a file/folder name is forbidden. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_name A file or folder name. * @return bool */ public static function is_filename_forbidden( $file_name ) { static $file_names; if ( ! isset( $file_names ) ) { $file_names = array_flip( self::get_forbidden_file_names() ); } return isset( $file_names[ strtolower( $file_name ) ] ); } /** * Get the list of file names that Imagify won't optimize. * It can contain folder names. Names are case-lowered. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array A list of file names */ public static function get_forbidden_file_names() { static $file_names; if ( isset( $file_names ) ) { return $file_names; } $file_names = array( '.', '..', '.DS_Store', '.git', '.svn', 'backup', 'backups', 'cache', 'lang', 'langs', 'languages', 'node_modules', 'Thumbs.db', ); $file_names = array_map( 'strtolower', $file_names ); /** * Add file names to the list of forbidden ones. * * @since 1.7 * @author Grégory Viguier * * @param array $added_file_names List of file names. * @param array $file_names List of file names already forbidden. */ $added_file_names = apply_filters( 'imagify_add_forbidden_file_names', array(), $file_names ); if ( ! $added_file_names || ! is_array( $added_file_names ) ) { return $file_names; } $added_file_names = array_filter( $added_file_names, 'is_string' ); $added_file_names = array_map( 'strtolower', $added_file_names ); $file_names = array_merge( $file_names, $added_file_names ); $file_names = array_flip( array_flip( $file_names ) ); return $file_names; } /** ----------------------------------------------------------------------------------------- */ /** PLACEHOLDERS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Add a placeholder to a path. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_path An absolute path. * @return string A "placeholdered" path. */ public static function add_placeholder( $file_path ) { $file_path = wp_normalize_path( $file_path ); $locations = self::get_placeholder_paths(); foreach ( $locations as $placeholder => $location_path ) { if ( strpos( $file_path, $location_path ) === 0 ) { return preg_replace( '@^' . preg_quote( $location_path, '@' ) . '@', $placeholder, $file_path ); } } // Should not happen. return $file_path; } /** * Change a path with a placeholder into a real path or URL. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_path A path with a placeholder. * @param string $type What to return: 'path' or 'url'. * @return string An absolute path or a URL. */ public static function remove_placeholder( $file_path, $type = 'path' ) { if ( 'path' === $type ) { $locations = self::get_placeholder_paths(); } else { $locations = self::get_placeholder_urls(); } foreach ( $locations as $placeholder => $location_path ) { if ( strpos( $file_path, $placeholder ) === 0 ) { return preg_replace( '@^' . preg_quote( $placeholder, '@' ) . '@', $location_path, $file_path ); } } // Should not happen. return $file_path; } /** * Get array of pairs of placeholder => corresponding path. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public static function get_placeholder_paths() { static $replacements; if ( isset( $replacements ) ) { return $replacements; } $filesystem = imagify_get_filesystem(); $replacements = array( '{{PLUGINS}}/' => WP_PLUGIN_DIR, '{{MU_PLUGINS}}/' => WPMU_PLUGIN_DIR, '{{THEMES}}/' => WP_CONTENT_DIR . '/themes', '{{UPLOADS}}/' => $filesystem->get_main_upload_basedir(), '{{CONTENT}}/' => WP_CONTENT_DIR, '{{ROOT}}/' => $filesystem->get_site_root(), ); $replacements = array_map( array( $filesystem, 'normalize_dir_path' ), $replacements ); return $replacements; } /** * Get array of pairs of placeholder => corresponding URL. * * @since 1.7 * @access public * @author Grégory Viguier * * @return array */ public static function get_placeholder_urls() { static $replacements; if ( isset( $replacements ) ) { return $replacements; } $filesystem = imagify_get_filesystem(); $replacements = array( '{{PLUGINS}}/' => plugins_url( '/' ), '{{MU_PLUGINS}}/' => plugins_url( '/', WPMU_PLUGIN_DIR . '/.' ), '{{THEMES}}/' => content_url( 'themes/' ), '{{UPLOADS}}/' => $filesystem->get_main_upload_baseurl(), '{{CONTENT}}/' => content_url( '/' ), '{{ROOT}}/' => $filesystem->get_site_root_url(), ); return $replacements; } /** * A file_exists() for paths with a placeholder. * * @since 1.7 * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return bool */ public static function placeholder_path_exists( $file_path ) { return imagify_get_filesystem()->is_readable( self::remove_placeholder( $file_path ) ); } /** ----------------------------------------------------------------------------------------- */ /** PATHS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the path to NextGen galleries on monosites. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string|bool An absolute path. False if it can't be retrieved. */ public static function get_ngg_galleries_path() { $galleries_path = get_site_option( 'ngg_options' ); if ( empty( $galleries_path['gallerypath'] ) ) { return false; } $filesystem = imagify_get_filesystem(); $galleries_path = $filesystem->normalize_dir_path( $galleries_path['gallerypath'] ); $galleries_path = trim( $galleries_path, '/' ); // Something like `wp-content/gallery`. $ngg_root = defined( 'NGG_GALLERY_ROOT_TYPE' ) ? NGG_GALLERY_ROOT_TYPE : 'site'; if ( $galleries_path && 'content' === $ngg_root ) { $ngg_root = $filesystem->normalize_dir_path( WP_CONTENT_DIR ); $ngg_root = trim( $ngg_root, '/' ); // Something like `abs-path/to/wp-content`. $exploded_root = explode( '/', $ngg_root ); $exploded_galleries = explode( '/', $galleries_path ); $first_gallery_dirname = reset( $exploded_galleries ); $last_root_dirname = end( $exploded_root ); if ( $last_root_dirname === $first_gallery_dirname ) { array_shift( $exploded_galleries ); $galleries_path = implode( '/', $exploded_galleries ); } } if ( 'content' === $ngg_root ) { $ngg_root = $filesystem->normalize_dir_path( WP_CONTENT_DIR ); } else { $ngg_root = $filesystem->get_abspath(); } if ( strpos( $galleries_path, $ngg_root ) !== 0 ) { $galleries_path = $ngg_root . $galleries_path; } return $galleries_path . '/'; } /** * Get the path to WooCommerce logs on monosites. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string An absolute path. */ public static function get_wc_logs_path() { if ( defined( 'WC_LOG_DIR' ) ) { return WC_LOG_DIR; } return imagify_get_filesystem()->get_upload_basedir( true ) . 'wc-logs/'; } /** * Get the path to EWWW optimization tools. * It is the same for all sites on multisite. * * @since 1.7 * @access public * @author Grégory Viguier * * @return string An absolute path. */ public static function get_ewww_tools_path() { if ( defined( 'EWWW_IMAGE_OPTIMIZER_TOOL_PATH' ) ) { return EWWW_IMAGE_OPTIMIZER_TOOL_PATH; } return WP_CONTENT_DIR . '/ewww/'; } /** * Get the path to ShortPixel backup folder. * It is the same for all sites on multisite (and yes, you'll get a surprise if your upload base dir -aka uploads/sites/12/- is not 2 folders deeper than theuploads folder). * * @since 1.8 * @access public * @author Grégory Viguier * * @return string An absolute path. */ public static function get_shortpixel_path() { if ( defined( 'SHORTPIXEL_BACKUP_FOLDER' ) ) { return trailingslashit( SHORTPIXEL_BACKUP_FOLDER ); } $filesystem = imagify_get_filesystem(); $path = $filesystem->get_upload_basedir( true ); $path = is_main_site() ? $path : $filesystem->dir_path( $filesystem->dir_path( $path ) ); return $path . 'ShortpixelBackups/'; } /** ----------------------------------------------------------------------------------------- */ /** REGEX PATTERNS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the regex pattern used to match the paths to the media library. * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`. * Paths tested against these patterns are lower-cased. * * @since 1.8 * @access public * @author Grégory Viguier * * @return string Something like `/wp\-content/uploads/(sites/\d+/)?\d{4}/\d{2}/`. */ public static function get_media_library_pattern() { $filesystem = imagify_get_filesystem(); $uploads_dir = self::normalize_path_for_regex( $filesystem->get_main_upload_basedir() ); if ( ! is_multisite() ) { if ( get_option( 'uploads_use_yearmonth_folders' ) ) { // In year/month folders. return $uploads_dir . '\d{4}/\d{2}/'; } // Not in year/month folders. return $uploads_dir . '[^/]+$'; } $pattern = $filesystem->get_multisite_uploads_subdir_pattern(); if ( get_option( 'uploads_use_yearmonth_folders' ) ) { // In year/month folders. return $uploads_dir . '(' . $pattern . ')?\d{4}/\d{2}/'; } // Not in year/month folders. return $uploads_dir . '(' . $pattern . ')?[^/]+$'; } /** * Get the regex pattern used to match the paths to NextGen galleries on multisite. * Pattern delimiter is `Imagify_Filesystem::PATTERN_DELIMITER`. * Paths tested against these patterns are lower-cased. * * @since 1.8 * @access public * @author Grégory Viguier * * @return string|bool Something like `/wp-content/uploads/sites/\d+/nggallery/`. False if it can't be retrieved. */ public static function get_ngg_galleries_multisite_pattern() { $galleries_path = self::get_ngg_galleries_path(); // Something like `wp-content/uploads/sites/%BLOG_ID%/nggallery/`. if ( ! $galleries_path ) { return false; } $galleries_path = self::normalize_path_for_regex( $galleries_path ); $galleries_path = str_replace( array( '%blog_name%', '%blog_id%' ), array( '.+', '\d+' ), $galleries_path ); return $galleries_path; } /** ----------------------------------------------------------------------------------------- */ /** NORMALIZATION TOOLS ===================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Normalize a file path, aiming for path comparison. * The path is normalized and case-lowered. * * @since 1.7 * @since 1.8 No trailing slash anymore, because it can be used for files. * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return string The normalized file path. */ public static function normalize_path_for_comparison( $file_path ) { return strtolower( wp_normalize_path( $file_path ) ); } /** * Normalize a file path, aiming for use in a regex pattern. * The path is normalized, case-lowered, and escaped. * * @since 1.8 * @access public * @author Grégory Viguier * * @param string $file_path The file path. * @return string The normalized file path. */ public static function normalize_path_for_regex( $file_path ) { return preg_quote( imagify_get_filesystem()->normalize_path_for_comparison( $file_path ), Imagify_Filesystem::PATTERN_DELIMITER ); } }