* * @copyright (c) 2016, Incsub (http://incsub.com) */ require_once 'utils/class-wp-smush-directory-scanner.php'; require_once 'ui/class-wp-smush-dir-ui.php'; if ( ! class_exists( 'WP_Smush_Dir' ) ) { /** * Class WP_Smush_Dir */ class WP_Smush_Dir { /** * Contains a list of optimised images. * * @var $optimised_images */ public $optimised_images; /** * Flag to check if dir smush table exist. * * @var $table_exist */ public $table_exist; /** * Total Stats for the image optimisation. * * @var $stats */ public $stats; /** * Directory scanner. * * @var WP_Smush_Directory_Scanner */ public $scanner; /** * Directory Smush UI. * * @since 2.8.1 * * @var WP_Smush_Dir_UI */ private $ui; /** * WP_Smush_Dir constructor. */ public function __construct() { if ( ! $this->should_continue() ) { // Remove directory smush from tabs if not required. add_filter( 'smush_setting_tabs', array( $this, 'remove_directory_tab' ) ); return; } $this->ui = new WP_Smush_Dir_UI(); $this->scanner = new WP_Smush_Directory_Scanner(); if ( ! $this->scanner->is_scanning() ) { $this->scanner->reset_scan(); } // Check directory smush table after screen is set. add_action( 'current_screen', array( $this, 'check_table' ) ); // Check to see if the scanner should be running. add_action( 'admin_footer', array( $this, 'check_scan' ) ); // Handle Ajax request 'smush_get_directory_list'. add_action( 'wp_ajax_smush_get_directory_list', array( $this, 'directory_list' ) ); // Scan the given directory path for the list of images. add_action( 'wp_ajax_image_list', array( $this, 'image_list' ) ); // Handle Ajax Request to optimise images. add_action( 'wp_ajax_optimise', array( $this, 'optimise' ) ); /** * Scanner ajax actions. * * @since 2.8.1 */ add_action( 'wp_ajax_directory_smush_start', array( $this, 'directory_smush_start' ) ); add_action( 'wp_ajax_directory_smush_check_step', array( $this, 'directory_smush_check_step' ) ); add_action( 'wp_ajax_directory_smush_finish', array( $this, 'directory_smush_finish' ) ); add_action( 'wp_ajax_directory_smush_cancel', array( $this, 'directory_smush_cancel' ) ); } /** * Run the scanner on page refresh (if it's running). * * @since 2.8.1 */ public function check_scan() { if ( $this->scanner->is_scanning() ) { ?> scanner->init_scan(); wp_send_json_success(); } /** * Directory Smush: Smush step. * * @since 2.8.1 */ public function directory_smush_check_step() { $urls = $this->get_scanned_images(); $current_step = absint( $_POST['step'] ); // Input var ok. $this->scanner->update_current_step( $current_step ); if ( isset( $urls[ $current_step ] ) ) { $this->optimise_image( $urls[ $current_step ]['id'] ); } wp_send_json_success(); } /** * Directory Smush: Finish smush. * * @since 2.8.1 */ public function directory_smush_finish() { $items = isset( $_POST['items'] ) ? absint( $_POST['items'] ) : 0; // Input var ok. $failed = isset( $_POST['failed'] ) ? absint( $_POST['failed'] ) : 0; // Input var ok. // If any images failed to smush, store count. if ( $failed > 0 ) { set_transient( 'wp-smush-dir-scan-failed-items', $failed, 60 * 5 ); // 5 minutes max. } // Store optimized items count. set_transient( 'wp-smush-show-dir-scan-notice', $items, 60 * 5 ); // 5 minutes max. $this->scanner->reset_scan(); wp_send_json_success(); } /** * Directory Smush: Cancel smush. * * @since 2.8.1 */ public function directory_smush_cancel() { $this->scanner->reset_scan(); wp_send_json_success(); } /** * Handles the ajax request for image optimisation in a folder * * @param int $image_id Image ID. */ private function optimise_image( $image_id ) { global $wpdb, $wp_smush, $wpsmushit_admin; $error_msg = ''; // No image ID. if ( ! isset( $image_id ) ) { $error_msg = esc_html__( 'Incorrect image id', 'wp-smushit' ); wp_send_json_error( $error_msg ); } // Check smush limit for free users. if ( ! $wp_smush->validate_install() ) { /** * Free version bulk smush, check the transient counter value. * * @var WpSmushitAdmin $wpsmushit_admin */ $should_continue = $wpsmushit_admin->check_bulk_limit( false, 'dir_sent_count' ); // Send a error for the limit. if ( ! $should_continue ) { wp_send_json_error( array( 'error' => 'dir_smush_limit_exceeded', 'continue' => false, ) ); } } $id = intval( $image_id ); if ( ! $scanned_images = wp_cache_get( 'wp_smush_scanned_images' ) ) { $scanned_images = $this->get_scanned_images(); } $image = $this->get_image( $id, '', $scanned_images ); if ( empty( $image ) ) { // If there are no stats. $error_msg = esc_html__( 'Could not find image id in last scanned images', 'wp-smushit' ); wp_send_json_error( $error_msg ); } $path = $image['path']; // We have the image path, optimise. $smush_results = $wp_smush->do_smushit( $path ); if ( is_wp_error( $smush_results ) ) { /* @var WP_Error $smush_results */ $error_msg = $smush_results->get_error_message(); } elseif ( empty( $smush_results['data'] ) ) { // If there are no stats. $error_msg = esc_html__( "Image couldn't be optimized", 'wp-smushit' ); } if ( ! empty( $error_msg ) ) { // Store the error in DB. All good, Update the stats. $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}smush_dir_images SET error=%s WHERE id=%d LIMIT 1", $error_msg, $id ) ); // Db call ok; no-cache ok. wp_send_json_error( array( 'error' => $error_msg, 'image' => array( 'id' => $id, ), ) ); } // Get file time. $file_time = @filectime( $path ); // If super-smush enabled, update supersmushed meta value also. $lossy = $wp_smush->lossy_enabled ? 1 : 0; // All good, Update the stats. $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}smush_dir_images SET image_size=%d, file_time=%d, lossy=%s WHERE id=%d LIMIT 1", $smush_results['data']->after_size, $file_time, $lossy, $id ) ); // Db call ok; no-cache ok. // Update bulk limit transient. $wpsmushit_admin->update_smush_count( 'dir_sent_count' ); } /** * Do not display Directory smush for subsites. * * @return bool True/False, whether to display the Directory smush or not */ public function should_continue() { // Do not show directory smush, if not main site in a network. if ( ! is_main_site() || is_network_admin() ) { return false; } return true; } /** * Create the Smush image table to store the paths of scanned images, and stats */ public function create_table() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); /** * Table: wp_smush_dir_images * Columns: * id -> Auto Increment ID * path -> Absolute path to the image file * resize -> Whether the image was resized or not * lossy -> Whether the image was super-smushed/lossy or not * image_size -> Current image size post optimisation * orig_size -> Original image size before optimisation * file_time -> Unix time for the file creation, to match it against the current creation time, * in order to confirm if it is optimised or not * last_scan -> Timestamp, Get images form last scan by latest timestamp * are from latest scan only and not the whole list from db * meta -> For any future use */ $sql = "CREATE TABLE {$wpdb->base_prefix}smush_dir_images ( id mediumint(9) NOT NULL AUTO_INCREMENT, path text NOT NULL, path_hash CHAR(32), resize varchar(55), lossy varchar(55), error varchar(55) DEFAULT NULL, image_size int(10) unsigned, orig_size int(10) unsigned, file_time int(10) unsigned, last_scan timestamp DEFAULT '0000-00-00 00:00:00', meta text, UNIQUE KEY id (id), UNIQUE KEY path_hash (path_hash), KEY image_size (image_size) ) $charset_collate;"; // Include the upgrade library to initialize a table. require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); // Set flag. $this->table_exist = true; } /** * Update path_hash, and store a flag if all the rows were updated * * @return null * * @todo, Stop running this function after 2-3 updates using version check */ public function update_dir_path_hash() { // If we've already performed the update. if ( get_option( 'smush-directory-path-hash-updated', false ) ) { return null; } global $wpsmush_helper, $wpdb; // Check if column exists. if ( ! $wpsmush_helper->table_column_exists( $wpdb->prefix . 'smush_dir_images', 'path_hash' ) ) { return null; } // Update the rows. $wpdb->query( "UPDATE {$wpdb->prefix}smush_dir_images SET path_hash = MD5(path) WHERE path IS NOT NULL" ); // Check if there are any pending rows that needs to be updated. $pending_rows = "SELECT count(*) FROM {$wpdb->prefix}smush_dir_images WHERE path_hash is NULL AND path IS NOT NULL"; $index_exists = "SHOW INDEX FROM {$wpdb->prefix}smush_dir_images WHERE KEY_NAME = 'path'"; // If all the rows are updated and Index exists. if ( ! $wpdb->get_var( $pending_rows ) && $wpdb->get_var( $index_exists ) != null ) { $wpsmush_helper->drop_index( $wpdb->prefix . 'smush_dir_images', 'path' ); update_option( 'smush-directory-path-hash-updated', 1 ); } } /** * Get the image ids and path for last scanned images * * @return array Array of last scanned images containing image id and path */ public function get_scanned_images() { global $wpdb; $results = $wpdb->get_results( "SELECT id, path, orig_size FROM {$wpdb->prefix}smush_dir_images WHERE last_scan = (SELECT MAX(last_scan) FROM {$wpdb->prefix}smush_dir_images ) GROUP BY id ORDER BY id", ARRAY_A ); // Db call ok; no-cache ok. // Return image ids. if ( is_wp_error( $results ) ) { error_log( sprintf( 'WP Smush Query Error in %s at %s: %s', __FILE__, __LINE__, $results->get_error_message() ) ); $results = array(); } return $results; } /** * Check if the image file is media library file * * @param string $file_path File path. * * @return bool */ private function is_media_library_file( $file_path ) { $upload_dir = wp_upload_dir(); $upload_path = $upload_dir['path']; // Get the base path of file. $base_dir = dirname( $file_path ); if ( $base_dir === $upload_path ) { return true; } return false; } /** * Return a directory/File list * * PHP Connector */ public function directory_list() { // Check For permission. if ( ! current_user_can( 'manage_options' ) || ! is_user_logged_in() ) { wp_send_json_error( __( 'Unauthorized', 'wp-smushit' ) ); } // Verify nonce. check_ajax_referer( 'smush_get_dir_list', 'list_nonce' ); // Get the root path for a main site or subsite. $root = realpath( $this->get_root_path() ); $dir = ( isset( $_GET['dir'] ) && ! is_array( $_GET['dir'] ) ) ? ltrim( sanitize_text_field( wp_unslash( $_GET['dir'] ) ), '/' ) : null; // Input var ok. $post_dir = strlen( $dir ) > 1 ? path_join( $root, $dir ) : $root . $dir; $post_dir = realpath( rawurldecode( $post_dir ) ); // If the final path doesn't contains the root path, bail out. if ( ! $root || false === $post_dir || 0 !== strpos( $post_dir, $root ) ) { wp_send_json_error( __( 'Unauthorized', 'wp-smushit' ) ); } $supported_image = array( 'gif', 'jpg', 'jpeg', 'png', ); if ( file_exists( $post_dir ) ) { $files = scandir( $post_dir ); // Exclude hidden files. if ( ! empty( $files ) ) { $files = preg_grep( '/^([^.])/', $files ); } $return_dir = substr( $post_dir, strlen( $root ) ); natcasesort( $files ); if ( count( $files ) !== 0 && ! $this->skip_dir( $post_dir ) ) { $tree = array(); foreach ( $files as $file ) { $html_rel = htmlentities( ltrim( path_join( $return_dir, $file ), '/' ) ); $html_name = htmlentities( $file ); $ext = preg_replace( '/^.*\./', '', $file ); $file_path = path_join( $post_dir, $file ); if ( ! file_exists( $file_path ) || '.' === $file || '..' === $file ) { continue; } // Skip unsupported files and files that are already in the media library. if ( ! is_dir( $file_path ) && ( ! in_array( $ext, $supported_image, true ) || $this->is_media_library_file( $file_path ) ) ) { continue; } $skip_path = $this->skip_dir( $file_path ); $tree[] = array( 'title' => $html_name, 'key' => $html_rel, 'folder' => is_dir( $file_path ), 'lazy' => ! $skip_path, 'checkbox' => true, 'unselectable' => $skip_path, // Skip Uploads folder - Media Files. ); } wp_send_json_success( $tree ); } } // End if(). } /** * Get root path of the installation. * * @return string Root path. */ public function get_root_path() { // If main site. if ( is_main_site() ) { /** * Sometimes content directories may reside outside * the installation sub directory. We need to make sure * we are selecting the root directory, not installation * directory. * * @see https://xnau.com/finding-the-wordpress-root-path-for-an-alternate-directory-structure/ * @see https://app.asana.com/0/14491813218786/487682361460247/f */ $content_path = explode( '/', WP_CONTENT_DIR ); // Get root path and explod. $root_path = explode( '/', get_home_path() ); // Find the length of the shortest one. $end = min( count( $content_path ), count( $root_path ) ); $i = 0; $common_path = array(); // Add the component if they are the same in both paths. while ( $content_path[ $i ] === $root_path[ $i ] && $i < $end ) { $common_path[] = $content_path[ $i ]; $i++; } return implode( '/', $common_path ); } else { $up = wp_upload_dir(); return $up['basedir']; } } /** * Get the image list in a specified directory path. * * @since 2.8.1 Added support for selecting files. * * @param string|array $paths Path where to look for images, or selected images. * * @return array */ private function get_image_list( $paths = '' ) { global $wpsmush_helper; // Error with directory tree. if ( ! is_array( $paths ) ) { wp_send_json_error( array( 'message' => __( 'There was a problem getting the selected directories', 'wp-smushit' ), ) ); } $count = 0; $images = array(); $values = array(); $timestamp = gmdate( 'Y-m-d H:i:s' ); /** * Temporary increase the limit. * * @var WpSmushHelper $wpsmush_helper */ $wpsmush_helper->increase_memory_limit(); // Iterate over all the selected items (can be either an image or directory). foreach ( $paths as $path ) { /** * Path is an image. */ if ( ! is_dir( $path ) && ! $this->is_media_library_file( $path ) && ! strpos( $path, '.bak' ) ) { if ( ! $this->is_image( $path ) ) { continue; } // Image already added. Skip. if ( in_array( $path, $images, true ) ) { continue; } $images[] = $path; $images[] = md5( $path ); $images[] = @filesize( $path ); // Get the file size. $images[] = @filectime( $path ); // Get the file modification time. $images[] = $timestamp; $values[] = '(%s, %s, %d, %d, %s)'; $count++; // Store the images in db at an interval of 5k. if ( $count >= 5000 ) { $count = 0; $this->store_images( $values, $images ); $images = $values = array(); } continue; } /** * Path is a directory. */ $base_dir = realpath( rawurldecode( $path ) ); if ( ! $base_dir ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', 'wp-smushit' ), ) ); } // Directory Iterator, Exclude . and .. $filtered_dir = new WPSmushRecursiveFilterIterator( new RecursiveDirectoryIterator( $base_dir ) ); // File Iterator. $iterator = new RecursiveIteratorIterator( $filtered_dir, RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $iterator as $file ) { // Used in place of Skip Dots, For php 5.2 compatibility. if ( basename( $file ) === '..' || basename( $file ) === '.' ) { continue; } // Not a file. Skip. if ( ! $file->isFile() ) { continue; } $file_path = $file->getPathname(); if ( $this->is_image( $file_path ) && ! $this->is_media_library_file( $file_path ) && strpos( $file, '.bak' ) === false ) { /** To be stored in DB, Part of code inspired from Ewwww Optimiser */ $images[] = $file_path; $images[] = md5( $file_path ); $images[] = $file->getSize(); $images[] = @filectime( $file_path ); // Get the file modification time. $images[] = $timestamp; $values[] = '(%s, %s, %d, %d, %s)'; $count++; } // Store the images in db at an interval of 5k. if ( $count >= 5000 ) { $count = 0; $this->store_images( $values, $images ); $images = $values = array(); } } // End foreach(). } // End foreach(). // Update rest of the images. if ( ! empty( $images ) && $count > 0 ) { $this->store_images( $values, $images ); } // Remove scanned images from cache. wp_cache_delete( 'wp_smush_scanned_images' ); // Get the image ids. $images = $this->get_scanned_images(); // Store scanned images in cache. wp_cache_add( 'wp_smush_scanned_images', $images ); return $images; } /** * Write to the database. * * @since 2.8.1 * * @param array $values Values for query build. * @param array $images Array of images. */ private function store_images( $values, $images ) { global $wpdb; $query = $this->build_query( $values, $images ); $wpdb->query( $query ); // Db call ok; no-cache ok. } /** * Build and prepare query from the given values and image array. * * @param array $values Values. * @param array $images Images. * * @return bool|string */ private function build_query( $values, $images ) { if ( empty( $images ) || empty( $values ) ) { return false; } global $wpdb; $values = implode( ',', $values ); // Replace with image path and respective parameters. $query = "INSERT INTO {$wpdb->prefix}smush_dir_images (path, path_hash, orig_size,file_time,last_scan) VALUES $values ON DUPLICATE KEY UPDATE image_size = IF( file_time < VALUES(file_time), NULL, image_size ), file_time = IF( file_time < VALUES(file_time), VALUES(file_time), file_time ), last_scan = VALUES( last_scan )"; $query = $wpdb->prepare( $query, $images ); // Db call ok; no-cache ok. return $query; } /** * Sends a Ajax response if no images are found in selected directory. */ private function send_error() { $message = sprintf( "
%s
'; $notice .= esc_html__( 'Directory smushing requires custom tables and it seems there was an error creating tables. For help, please contact our team on the support forums', 'wp-smushit' ); $notice .= '