dvadf
File manager - Edit - /home/centroca/public_html/core.tar
Back
class-deprecated-hooks.php 0000644 00000007463 15252476777 0011641 0 ustar 00 <?php /** * Deprecated hooks. * It's created based on WC_Deprecated_Hooks. * * @since 3.9.6 * @package Deprecated_Hooks */ namespace Smush\Core; defined( 'ABSPATH' ) || exit; /** * Handles deprecation notices and triggering of legacy action hooks. */ class Deprecated_Hooks { /** * Array of deprecated actions hooks we need to handle. Format of 'new' => 'old'. * * @var array */ private $deprecated_action_hooks = array( 'wp_smush_before_smush_file' => 'smush_s3_integration_fetch_file', 'wp_smush_after_remove_file' => 'smush_s3_backup_remove', ); /** * Array of deprecated filters hooks we need to handle. Format of 'new' => 'old'. * * @var array */ private $deprecated_filter_hooks = array( 'wp_smush_backup_exists' => 'smush_backup_exists', 'wp_smush_file_exists' => 'smush_file_exists', ); /** * Array of versions on each hook has been deprecated. * * @var array */ private $deprecated_version = array( 'smush_backup_exists' => '3.9.6', 'smush_s3_integration_fetch_file' => '3.9.6', 'smush_s3_backup_remove' => '3.9.6', 'smush_file_exists' => '3.9.6', ); /** * Is action hook. * * @var bool */ private $is_action; /** * Constructor. * * Hook into the new hook so we can handle deprecated hooks once fired. */ public function __construct() { $deprecated_hooks = array_merge( array_keys( $this->deprecated_action_hooks ), array_keys( $this->deprecated_filter_hooks ) ); if ( $deprecated_hooks ) { foreach ( $deprecated_hooks as $new_action ) { add_filter( $new_action, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 ); } } } /** * Get old hooks to map to new hook. * * @param string $new_hook New hook name. * @return array */ private function get_old_hooks( $new_hook ) { $old_hooks = array(); if ( isset( $this->deprecated_action_hooks[ $new_hook ] ) ) { $old_hooks = $this->deprecated_action_hooks[ $new_hook ]; $this->is_action = true; } elseif ( isset( $this->deprecated_filter_hooks[ $new_hook ] ) ) { $old_hooks = $this->deprecated_filter_hooks[ $new_hook ]; // reset hook type. $this->is_action = null; } return is_array( $old_hooks ) ? $old_hooks : array( $old_hooks ); } /** * If the hook is Deprecated, call the old hooks here. */ public function maybe_handle_deprecated_hook() { $new_hook = current_filter(); $new_callback_args = func_get_args(); $return_value = $new_callback_args[0]; $old_hooks = $this->get_old_hooks( $new_hook ); if ( $old_hooks ) { foreach ( $old_hooks as $old_hook ) { if ( has_filter( $old_hook ) ) { $this->display_notice( $old_hook, $new_hook ); $return_value = $this->trigger_hook( $old_hook, $new_callback_args ); } } } return $return_value; } /** * Display a deprecated notice for old hooks. * * @param string $old_hook Old hook. * @param string $new_hook New hook. */ protected function display_notice( $old_hook, $new_hook ) { _deprecated_hook( esc_html( $old_hook ), esc_html( $this->get_deprecated_version( $old_hook ) ), esc_html( $new_hook ) ); } /** * Fire off a legacy hook with it's args. * * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @return mixed|void */ protected function trigger_hook( $old_hook, $new_callback_args ) { if ( $this->is_action ) { do_action_ref_array( $old_hook, $new_callback_args ); } else { return apply_filters_ref_array( $old_hook, $new_callback_args ); } } /** * Get deprecated version. * * @param string $old_hook Old hook name. * @return string */ protected function get_deprecated_version( $old_hook ) { return ! empty( $this->deprecated_version[ $old_hook ] ) ? $this->deprecated_version[ $old_hook ] : WP_SMUSH_VERSION; } } class-optimization-controller.php 0000644 00000016625 15252476777 0013327 0 ustar 00 <?php namespace Smush\Core; use Smush\Core\Media\Media_Item_Cache; use Smush\Core\Media\Media_Item_Optimizer; use Smush\Core\Media_Library\Media_Library_Row; use Smush\Core\Membership\Membership; use Smush\Core\Smush\Smush_Optimization; use Smush\Core\Smush\Smusher; use Smush\Core\Smush\Smusher_Options_Provider; use Smush\Core\Stats\Global_Stats; /** * // TODO: [WPMUDEV SMUSH UI] create tests */ class Optimization_Controller extends Controller { /** * @var Optimization_Controller */ private static $instance; /** * @var Global_Stats */ private $global_stats; private $membership; /** * @var Settings */ private $settings; private $media_item_cache; private $optimizer; private function __construct() { $this->global_stats = Global_Stats::get(); $this->membership = Membership::get_instance(); $this->settings = Settings::get_instance(); $this->media_item_cache = Media_Item_Cache::get_instance(); $this->optimizer = Optimizer::get_instance(); $this->register_action( 'wp_smush_image_sizes_changed', array( $this, 'mark_global_stats_as_outdated' ) ); $this->register_action( 'wp_ajax_optimize_attachment', array( $this, 'optimize_attachment' ) ); $this->register_action( 'wp_async_wp_generate_attachment_metadata', array( $this, 'auto_optimize_attachment_async', ) ); $this->register_filter( 'wp_generate_attachment_metadata', array( $this, 'maybe_auto_optimize_attachment_sync' ), 15, 2 ); $this->register_action( 'wp_async_wp_save_image_editor_file', array( $this, 'handle_editor_upload_async' ), '', 2 ); // Fix SSL CA certificates issue. $this->register_action( 'wp_smush_before_smush_file', array( $this, 'fix_ssl_ca_certificate_error' ) ); } public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } public function mark_global_stats_as_outdated() { $this->global_stats->mark_as_outdated(); } public function optimize_attachment() { if ( ! isset( $_REQUEST['attachment_id'] ) ) { wp_send_json_error( array( 'error_msg' => esc_html__( 'No attachment ID was provided.', 'wp-smushit' ) ) ); } if ( ! check_ajax_referer( 'wp-smush-ajax', '_nonce', false ) ) { wp_send_json_error( array( 'error_msg' => esc_html__( 'Nonce verification failed', 'wp-smushit' ) ) ); } if ( ! Helper::is_user_allowed( 'upload_files' ) ) { wp_send_json_error( array( 'error_msg' => esc_html__( "You don't have permission to work with uploaded files.", 'wp-smushit' ) ) ); } if ( $this->membership->is_api_hub_access_required() ) { wp_send_json_error( array( 'error_msg' => esc_html__( 'A WPMU DEV Hub connection is required to optimize images.', 'wp-smushit' ) ) ); } $attachment_id = (int) $_REQUEST['attachment_id']; $optimizer = $this->optimizer; $is_optimized = $optimizer->optimize( $attachment_id ); $media_lib_item = Media_Library_Row::get_instance( $attachment_id ); $markup = $media_lib_item->generate_markup(); if ( $is_optimized ) { wp_send_json_success( $markup ); } else { $errors = $optimizer->get_errors(); wp_send_json_error( array( 'error' => $errors->get_error_code(), 'error_msg' => $errors->get_error_message(), 'html_stats' => $markup, 'show_warning' => $this->membership->should_show_premium_status_warning( $attachment_id ), ) ); } } public function auto_optimize_attachment_async( $id ) { // If we don't have image id or auto Smush is disabled, return. if ( empty( $id ) || ! $this->optimizer->should_auto_optimize( $id ) ) { return; } $this->optimizer->optimize( $id ); } public function maybe_auto_optimize_attachment_sync( $meta, $id ) { // We need to check if this call originated from Gutenberg and allow only media. if ( Helper::is_non_rest_media() ) { // If not - return image metadata. return $meta; } $upload_attachment = filter_input( INPUT_POST, 'action', FILTER_SANITIZE_SPECIAL_CHARS ); $is_upload_attachment = 'upload-attachment' === $upload_attachment || isset( $_POST['post_id'] ); // Our async task runs when action is upload-attachment and post_id found. So do not run on these conditions. if ( $is_upload_attachment && defined( 'WP_SMUSH_ASYNC' ) && WP_SMUSH_ASYNC ) { return $meta; } $generating_metadata = doing_filter( 'wp_generate_attachment_metadata' ); if ( $generating_metadata && ! $this->optimizer->should_auto_optimize( $id ) ) { return $meta; } $this->optimizer->optimize( $id ); return $meta; } /** * This method runs when a media item is edited. * * TODO: this method has been replicated from another method but there are unanswered questions: * - Can't we optimize the full media item instead of just the full size? * - We should probably not do anything if the media item was not previously optimized, because in that case we can just treat it as the other unoptimized items and take care of it during bulk smush. */ public function handle_editor_upload_async( $id, $post_data ) { if ( ! $this->optimizer->should_auto_optimize( $id ) ) { return; } $filepath = empty( $post_data['filepath'] ) ? '' : $post_data['filepath']; if ( ! $filepath || ! file_exists( $filepath ) ) { return; } // Get before stats $before_file_size = filesize( $filepath ); $smusher_options = ( new Smusher_Options_Provider() )->get_options(); $smusher = new Smusher( $smusher_options ); $smusher->smush( array( $filepath ) ); if ( $smusher->has_errors() ) { return; } $media_item = Media_Item_Cache::get_instance()->get( $id ); $attached_file = $media_item->get_attached_file(); if ( $attached_file !== $filepath ) { return; } $after_file_size = filesize( $filepath ); $media_item_optimizer = new Media_Item_Optimizer( $media_item ); $smush_optimization = $media_item_optimizer->get_optimization( Smush_Optimization::get_key() ); $smush_optimization_total_stats = $media_item_optimizer->get_stats( Smush_Optimization::get_key() ); $smush_optimization_full_size_stats = $media_item_optimizer->get_size_stats( Smush_Optimization::get_key(), $media_item->get_main_size()->get_key() ); if ( $smush_optimization->is_optimized() ) { $smush_optimization_total_stats->set_size_before( $smush_optimization_total_stats->get_size_before() - $smush_optimization_full_size_stats->get_size_before() + $before_file_size ); $smush_optimization_total_stats->set_size_after( $smush_optimization_total_stats->get_size_after() - $smush_optimization_full_size_stats->get_size_after() + $after_file_size ); $smush_optimization_full_size_stats->set_size_before( $before_file_size ); $smush_optimization_full_size_stats->set_size_after( $after_file_size ); $smush_optimization->save(); } } /** * Fix SSL CA Certificate issue. * * @since 3.9.6 * * Check for use of http url (Hostgator mostly) - got it from smush_image. */ public function fix_ssl_ca_certificate_error() { // Return if the member defined it. if ( defined( 'WP_SMUSH_API_HTTP' ) ) { return; } static $use_http; /** * Fix for Hostgator. * Check for use of http url (Hostgator mostly). */ if ( is_null( $use_http ) ) { $use_http = $this->settings->get_setting( 'wp-smush-use_http' ); } if ( $use_http ) { define( 'WP_SMUSH_API_HTTP', 'http://smushpro.wpmudev.com/1.0/' ); } } } class-optimizer.php 0000644 00000006441 15252476777 0010435 0 ustar 00 <?php namespace Smush\Core; use Smush\Core\Media\Media_Item_Cache; use Smush\Core\Media\Media_Item_Optimizer; use Smush\Core\Membership\Membership; use Smush\Core\Smush\Smusher; use Smush\Core\Smush\Smusher_Options; use Smush\Core\Smush\Smusher_Options_Provider; use Smush\Core\Webp\Webp_Converter; use WP_Error; /** * This is a light weight facade that acts as the first entry point for optimization. The real work is done by {@see Media_Item_Optimizer}. */ class Optimizer { /** * Static instance * * @var self */ private static $instance; /** * @var bool */ private $optimization_in_progress; /** * @var Media_Item_Cache */ private $media_item_cache; /** * @var \WP_Error */ private $errors; private $membership; private $settings; public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->media_item_cache = Media_Item_Cache::get_instance(); $this->errors = new \WP_Error(); $this->membership = Membership::get_instance(); $this->settings = Settings::get_instance(); } public function should_auto_optimize( $attachment_id ) { if ( $this->membership->is_api_hub_access_required() ) { return false; } if ( ! $this->settings->is_automatic_compression_active() ) { return false; } $media_item = $this->media_item_cache->get( $attachment_id ); if ( ! $media_item->is_valid() ) { return false; } /** * Skip auto smush filter. * * @param bool $skip_auto_smush Whether to skip auto smush or not. */ $skip_auto_smush = apply_filters( 'wp_smush_should_skip_auto_smush', false, $attachment_id ); // We don't want very large files to be auto smushed. $skip_auto_smush = $skip_auto_smush || $media_item->is_large(); if ( $skip_auto_smush ) { return false; } return true; } public function optimize( $attachment_id ) { if ( $this->optimization_in_progress ) { $this->set_errors( new WP_Error( 'in_progress', 'Smush already in progress' ) ); return false; } $this->optimization_in_progress = true; // Reset the errors before starting $this->set_errors( null ); $media_item = $this->media_item_cache->get( $attachment_id ); $media_item_optimizer = new Media_Item_Optimizer( $media_item ); $optimized = $media_item_optimizer->optimize(); if ( ! $optimized ) { $errors = $media_item->has_errors() ? $media_item->get_errors() : $media_item_optimizer->get_errors(); $this->set_errors( $errors ); } $this->optimization_in_progress = false; return $optimized; } public function optimize_file( $file_path, $convert_to_webp = false, $options = null ) { $smusher_options = $options ?? ( new Smusher_Options_Provider() )->get_options(); $smusher = $convert_to_webp ? new Webp_Converter( $smusher_options ) : new Smusher( $smusher_options ); $data = $smusher->validate_and_smush_file( $file_path ); if ( $data ) { return array( 'success' => true, 'data' => $data ); } else { return $smusher->get_errors(); } } public function get_errors() { if ( is_null( $this->errors ) ) { $this->errors = new WP_Error(); } return $this->errors; } private function set_errors( $error ) { $this->errors = $error; } } rating-notification/class-rating-notification-controller.php 0000644 00000012517 15252476777 0020515 0 ustar 00 <?php namespace Smush\Core\Rating_Notification; use Smush\Core\Controller; use Smush\Core\Helper; use Smush\Core\Server_Utils; class Rating_Notification_Controller extends Controller { public function __construct() { $this->register_action( 'wp_ajax_smush_rating_completed', array( $this, 'handle_rating_completed' ) ); $this->register_action( 'wp_ajax_smush_rating_remind_later', array( $this, 'handle_rating_remind_later' ) ); $this->register_action( 'wp_ajax_smush_rating_dismissed', array( $this, 'handle_rating_dismissed' ) ); $this->register_action( 'wp_ajax_smush_rating_first_completion', array( $this, 'handle_rating_first_completion' ) ); $this->register_action( 'wp_smush_localize_ui_script_data', array( $this, 'localize_ui_script_data' ) ); } /** * Handle rating notification - user rated the plugin. * * @since 3.17.0 */ public function handle_rating_completed() { check_ajax_referer( 'wp-smush-ajax' ); // Check capability. if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 ); } // Store all rating statuses in a single option as an array. $rating_status = get_option( 'wp-smush-rating-status', array() ); $rating_status['completed'] = true; update_option( 'wp-smush-rating-status', $rating_status ); wp_send_json_success(); } /** * Handle rating notification - user wants to be reminded later. * * @since 3.17.0 */ public function handle_rating_remind_later() { check_ajax_referer( 'wp-smush-ajax' ); // Check capability. if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 ); } $rating_status = get_option( 'wp-smush-rating-status', array() ); $rating_status['remind_later'] = time(); update_option( 'wp-smush-rating-status', $rating_status ); wp_send_json_success(); } /** * Handle rating notification - user dismissed permanently. * * @since 3.17.0 */ public function handle_rating_dismissed() { check_ajax_referer( 'wp-smush-ajax' ); // Check capability. if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 ); } $rating_status = get_option( 'wp-smush-rating-status', array() ); $rating_status['dismissed'] = true; update_option( 'wp-smush-rating-status', $rating_status ); wp_send_json_success(); } /** * Handle rating notification - mark first completion. * * @since 3.17.0 */ public function handle_rating_first_completion() { check_ajax_referer( 'wp-smush-ajax' ); // Check capability. if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 ); } // Only set if not already set. $rating_status = get_option( 'wp-smush-rating-status', array() ); if ( empty( $rating_status['first_completion'] ) ) { $rating_status['first_completion'] = time(); update_option( 'wp-smush-rating-status', $rating_status ); } wp_send_json_success(); } public function localize_ui_script_data( $script_data ) { $script_data['ratingNotification'] = $this->get_rating_notification_data(); return $script_data; } /** * Get rating notification data for React component. * * @return array Rating notification state. * @since 3.17.0 */ private function get_rating_notification_data() { $rating_status = get_option( 'wp-smush-rating-status', array() ); $server_utils = new Server_Utils(); $dismissed = isset( $rating_status['dismissed'] ) ? $rating_status['dismissed'] : false; $completed = isset( $rating_status['completed'] ) ? $rating_status['completed'] : false; $remind_later = isset( $rating_status['remind_later'] ) ? $rating_status['remind_later'] : 0; $first_completion = isset( $rating_status['first_completion'] ) ? $rating_status['first_completion'] : 0; // Check if 7 days have passed since "remind later" was clicked. $should_show_after_reminder = false; if ( $remind_later > 0 ) { $seven_days_in_seconds = 7 * 24 * 60 * 60; $time_since_reminder = time() - $remind_later; $should_show_after_reminder = $time_since_reminder >= $seven_days_in_seconds; } // Check if 60 seconds have passed since first completion to show notification. $should_show_notification = false; $remaining_seconds = 0; if ( $first_completion > 0 && ! $completed && ! $dismissed ) { $sixty_seconds = 60; $time_since_completion = time() - $first_completion; // Show if 60+ seconds have passed and either no reminder or 7 days passed. if ( $time_since_completion >= $sixty_seconds ) { $should_show_notification = ( $remind_later === 0 ) || $should_show_after_reminder; } else { // Calculate remaining seconds until notification should show. $remaining_seconds = $sixty_seconds - $time_since_completion; } } return array( 'dismissed' => (bool) $dismissed, 'completed' => (bool) $completed, 'remindLater' => (int) $remind_later, 'firstCompletion' => (int) $first_completion, 'shouldShowAfterReminder' => $should_show_after_reminder, 'shouldShowNotification' => $should_show_notification, 'remainingSeconds' => (int) $remaining_seconds, 'isCurlMultiExecAvailable' => $server_utils->curl_multi_exec_available(), ); } } class-upload-dir.php 0000644 00000006553 15252476777 0010457 0 ustar 00 <?php namespace Smush\Core; class Upload_Dir { private $wp_upload_dir; private $root_path; private $upload_path; private $upload_rel_path; private $upload_url; /** * @return array */ public function get_wp_upload_dir() { if ( is_null( $this->wp_upload_dir ) ) { $this->wp_upload_dir = $this->prepare_wp_upload_dir(); } return $this->wp_upload_dir; } /** * @return mixed */ private function get_root_path() { if ( is_null( $this->root_path ) ) { $this->root_path = $this->prepare_root_path(); } return $this->root_path; } /** * @return mixed */ public function get_upload_path() { if ( is_null( $this->upload_path ) ) { $this->upload_path = $this->prepare_upload_path(); } return $this->upload_path; } /** * @return string */ public function get_upload_rel_path() { if ( is_null( $this->upload_rel_path ) ) { $this->upload_rel_path = $this->prepare_upload_rel_path(); } return $this->upload_rel_path; } /** * @return string */ public function get_upload_url() { if ( is_null( $this->upload_url ) ) { $this->upload_url = $this->prepare_upload_url(); } return $this->upload_url; } private function prepare_upload_path() { $upload = $this->get_wp_upload_dir(); return untrailingslashit( $upload['basedir'] ); } private function prepare_upload_rel_path() { $root_path = $this->get_root_path(); return str_replace( $root_path, '', $this->get_upload_path() ); } private function prepare_upload_url() { $upload = $this->get_wp_upload_dir(); return untrailingslashit( $upload['baseurl'] ); } private function prepare_wp_upload_dir() { return wp_upload_dir(); } protected function prepare_root_path() { // Is it possible that none of the following conditions are met? $root_path = ''; // Get the Document root path. There must be a better way to do this. // For example, /srv/www/site/public_html for /srv/www/site/public_html/wp-content/uploads. if ( 0 === strpos( $this->get_upload_path(), ABSPATH ) ) { // Environments like Flywheel have an ABSPATH that's not used in the paths. $root_path = ABSPATH; } elseif ( ! empty( $_SERVER['DOCUMENT_ROOT'] ) && 0 === strpos( $this->get_upload_path(), wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) ) { /** * This gets called when scanning for uncompressed images. * When ran from certain contexts, $_SERVER['DOCUMENT_ROOT'] might not be set. * * We are removing this part from the path later on. */ $root_path = realpath( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } elseif ( 0 === strpos( $this->get_upload_path(), dirname( WP_CONTENT_DIR ) ) ) { // We're assuming WP_CONTENT_DIR is only one level deep into the document root. // This might not be true in customized sites. A bit edgy. $root_path = dirname( WP_CONTENT_DIR ); } $root_path = untrailingslashit( $root_path ); /** * Filters the Document root path used to get relative paths for webp rules. * Hopefully of help for debugging and SLS. * * @since 3.9.0 */ return apply_filters( 'smush_webp_rules_root_path_base', $root_path ); } public function get_human_readable_path( $full_path ) { return str_replace( WP_CONTENT_DIR, '', $full_path ); } public function is_uploads_url( $url ) { return str_starts_with( $url, $this->get_upload_url() ); } } lazy-load/class-lazy-load-transform.php 0000644 00000040235 15252476777 0014213 0 ustar 00 <?php namespace Smush\Core\Lazy_Load; use Smush\Core\Array_Utils; use Smush\Core\Keyword_Exclusions; use Smush\Core\LCP\LCP_Transform; use Smush\Core\Parser\Composite_Element; use Smush\Core\Parser\Element; use Smush\Core\Parser\Element_Attribute; use Smush\Core\Parser\Page; use Smush\Core\Settings; use Smush\Core\Transform\Transform; use Smush\Core\Upload_Dir; use Smush\Core\Url_Utils; class Lazy_Load_Transform implements Transform { private static $lazyload_class = 'lazyload'; private static $temp_src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg=='; /** * @var Settings */ private $settings; /** * @var array */ private $lazy_load_options; /** * @var array */ private $excluded_keywords; /** * Keyword Exclusions. * * @var Keyword_Exclusions */ private $keyword_exclusions; /** * @var Lazy_Load_Helper */ private $helper; /** * @var Array_Utils */ private $array_utils; private $upload_dir; /** * @var Url_Utils */ private $url_utils; public function __construct() { $this->settings = Settings::get_instance(); $this->helper = Lazy_Load_Helper::get_instance(); $this->array_utils = new Array_Utils(); $this->upload_dir = new Upload_Dir(); $this->url_utils = new Url_Utils(); } public function should_transform() { return ! $this->helper->should_skip_lazyload(); } public function transform_page( $page ) { $this->transform_image_elements( $page ); if ( ! $this->helper->is_format_excluded( 'iframe' ) ) { $this->transform_iframes( $page ); } } /** * @param $page Page * * @return void */ private function transform_iframes( $page ) { foreach ( $page->get_iframe_elements() as $iframe_element ) { $this->transform_iframe( $iframe_element ); } } /** * @param Element $iframe_element * * @return void */ private function transform_iframe( $iframe_element ) { $src_attribute = $iframe_element->get_attribute( 'src' ); if ( ! $src_attribute ) { return; } $original_src_url = $src_attribute->get_value(); $original_iframe_markup = $iframe_element->get_markup(); if ( $this->is_element_excluded( $iframe_element ) || $this->is_iframe_skipped_through_filter( $original_src_url, $original_iframe_markup ) ) { return; } if ( esc_url_raw( $original_src_url ) !== $original_src_url ) { return; } if ( $this->helper->should_lazy_load_embed_video() ) { $lazy_load_video = new Lazy_Load_Video_Embed( $original_src_url, $iframe_element ); if ( $lazy_load_video->can_lazy_load() ) { $lazy_load_video->transform(); if ( $this->helper->is_noscript_fallback_enabled() ) { $iframe_element->set_postfix( "<noscript>$original_iframe_markup</noscript>" ); } return; } } if ( $this->helper->is_native_lazy_loading_enabled() ) { if ( ! $this->element_has_native_lazy_load_attribute( $iframe_element ) ) { $this->add_native_lazy_loading_attribute( $iframe_element ); } return; } $this->update_iframe_element_attributes_for_lazy_load( $iframe_element ); } private function update_iframe_element_attributes_for_lazy_load( $iframe_element ) { $this->remove_native_lazy_loading_attribute( $iframe_element ); $this->update_element_attributes_for_lazy_load( $iframe_element, array( 'src' ), 'about:blank' ); $iframe_element->add_attribute( new Element_Attribute( 'data-load-mode', '0' ) ); } private function update_element_attributes_for_lazy_load( $element, $replace_attributes, $placeholder = null ) { $this->replace_attributes_with_data_attributes( $element, $replace_attributes ); // We are adding a new src below, the original src is gone because we replaced it. $element->add_attribute( new Element_Attribute( 'src', $placeholder ?? self::$temp_src ) ); $this->add_lazy_load_class( $element ); } private function element_has_native_lazy_load_attribute( $element ) { return $element->has_attribute( 'loading' ); } private function is_element_excluded( $element ) { return $this->is_high_priority_element( $element ) || $element->is_lcp() || $this->element_has_excluded_keywords( $element ); } private function element_has_excluded_keywords( $element ) { $keyword_exclusions = $this->keyword_exclusions(); if ( ! $keyword_exclusions->has_excluded_keywords() ) { return false; } return $keyword_exclusions->is_markup_excluded( $element->get_markup() ) || $keyword_exclusions->is_id_attribute_excluded( $element->get_attribute_value( 'id' ) ) || $keyword_exclusions->is_class_attribute_excluded( $element->get_attribute_value( 'class' ) ); } private function is_iframe_skipped_through_filter( $src, $iframe ) { return apply_filters( 'smush_skip_iframe_from_lazy_load', false, $src, $iframe ); } private function get_lazy_load_options() { if ( ! $this->lazy_load_options ) { $setting = $this->settings->get_setting( 'wp-smush-lazy_load' ); $this->lazy_load_options = empty( $setting ) ? array() : $setting; } return $this->lazy_load_options; } private function get_excluded_keywords() { if ( ! $this->excluded_keywords ) { $this->excluded_keywords = $this->prepare_excluded_keywords(); } return $this->excluded_keywords; } private function prepare_excluded_keywords() { $default_exclude_keywords = $this->get_default_excluded_keywords(); $exclude_keywords = $this->helper->get_excluded_classes();// @since 3.13.0 used excluded ids, classes as excluded keywords. $exclude_keywords = array_merge( $default_exclude_keywords, $exclude_keywords ); return apply_filters( 'wp_smush_lazyload_excluded_keywords', array_unique( $exclude_keywords ) ); } private function replace_attributes_with_data_attributes( $element, $attribute_names ) { foreach ( $attribute_names as $attribute_name ) { $this->replace_attribute_with_data_attribute( $element, $attribute_name ); } } /** * @param Element $element * @param $original_attribute_name * * @return void */ private function replace_attribute_with_data_attribute( $element, $original_attribute_name ) { $attribute = $element->get_attribute( $original_attribute_name ); if ( $attribute ) { $original_value = $attribute->get_value(); $data_attribute = new Element_Attribute( "data-$original_attribute_name", $original_value ); $element->replace_attribute( $original_attribute_name, $data_attribute ); } } private function get_default_excluded_keywords() { return array( 'data-lazyload=', 'soliloquy-preload', // Soliloquy slider. 'no-lazyload', // Internal class to skip images. 'data-src=', 'data-no-lazy=', 'base64,R0lGOD', 'data-lazy-original=', 'data-lazy-src=', 'data-lazysrc=', 'data-bgposition=', 'fullurl=', 'jetpack-lazy-image', 'lazy-slider-img=', 'data-srcset=', 'class="ls-l', 'class="ls-bg', 'soliloquy-image', 'swatch-img', 'data-height-percentage', 'data-large_image', 'avia-bg-style-fixed', 'data-skip-lazy', 'skip-lazy', 'image-compare__', 'gform_ajax_frame', 'recaptcha/api/', 'google_ads_iframe_', ); } private function is_high_priority_element( $element ) { /** * An image should not be lazy-loaded and marked as high priority at the same time. * * @see wp_img_tag_add_loading_optimization_attrs() */ $fetch_priority = $element->get_attribute_value( 'fetchpriority' ); return $fetch_priority === 'high'; } /** * @param Element $element * * @return void */ private function add_native_lazy_loading_attribute( $element ) { $element->add_attribute( new Element_Attribute( 'loading', 'lazy' ) ); } private function remove_native_lazy_loading_attribute( $element ) { $native_lazyload_attr = $element->get_attribute( 'loading' ); if ( ! empty( $native_lazyload_attr ) ) { $element->remove_attribute( $native_lazyload_attr ); } } private function transform_image_elements( $page ) { /** * The following is being done in addition to the separate LCP_Transform just to save an extra re-parse in the transformer {@see Transformer::transform_content()}. * TODO: Remove this when re-parsing after every transform is not necessary. */ if ( $this->settings->is_lcp_preload_enabled() ) { $lcp_transform = new LCP_Transform(); $lcp_transform->transform_page( $page ); } foreach ( $page->get_composite_elements() as $composite_element ) { if ( ! $this->is_composite_element_excluded( $composite_element ) ) { $this->transform_elements( $composite_element->get_elements() ); } } $this->transform_elements( $page->get_elements() ); } private function transform_image_element( $element ) { if ( $element->get_tag() === 'source' ) { $this->maybe_lazy_load_source_element( $element ); } else { $attributes_updated = $this->maybe_lazy_load_image_element( $element ); if ( ! $attributes_updated ) { $this->maybe_lazy_load_background( $element ); } } } private function maybe_lazy_load_source_element( $element ) { $srcset_attribute = $element->get_attribute( 'srcset' ); if ( ! $srcset_attribute || empty( $srcset_attribute->get_image_urls() ) ) { return false; } $srcset_url = $srcset_attribute->get_single_image_url(); $srcset_image_url = $srcset_url->get_absolute_url(); $srcset_extension = $srcset_url->get_ext(); $original_markup = $element->get_markup(); if ( ! $srcset_image_url || ! $this->helper->is_image_extension_supported( $srcset_extension, $srcset_image_url ) || $this->is_element_excluded( $element ) || $this->is_image_element_skipped_through_filter( $srcset_image_url, $original_markup ) || $this->helper->is_native_lazy_loading_enabled() ) { return false; } $this->remove_native_lazy_loading_attribute( $element ); $this->replace_attributes_with_data_attributes( $element, array( 'src', 'srcset', 'sizes', ) ); return true; } private function maybe_lazy_load_image_element( $element ) { $src_attribute = $element->get_attribute( 'src' ); if ( ! $src_attribute ) { return false; } $src_image_url = ! empty( $src_attribute->get_single_image_url() ) ? $src_attribute->get_single_image_url()->get_absolute_url() : $src_attribute->get_value(); $src_extension = ! empty( $src_attribute->get_single_image_url() ) ? $src_attribute->get_single_image_url()->get_ext() : ''; $original_markup = $element->get_markup(); if ( ! $src_image_url || ! $this->helper->is_image_extension_supported( $src_extension, $src_image_url ) || $this->is_element_excluded( $element ) || $this->is_image_element_skipped_through_filter( $src_image_url, $original_markup ) ) { return false; } $is_tag_supported = in_array( $element->get_tag(), $this->get_lazy_load_image_tag_names(), true ); if ( ! $is_tag_supported ) { return false; } if ( $this->helper->is_native_lazy_loading_enabled() ) { if ( ! $this->element_has_native_lazy_load_attribute( $element ) ) { $this->add_native_lazy_loading_attribute( $element ); } } else { $this->remove_native_lazy_loading_attribute( $element ); $this->update_element_attributes_for_lazy_load( $element, array( 'src', 'srcset', 'sizes', ) ); $this->set_placeholder_width_and_height_in_style_attribute( $element, $src_image_url ); if ( $element->is_image_element() && $this->helper->is_noscript_fallback_enabled() ) { // TODO: Remove the duplicate <noscript> if it already exists before. $element->set_postfix( "<noscript>$original_markup</noscript>" ); } } return true; } private function maybe_lazy_load_background( $element ) { $background_property = $element->get_background_css_property(); $background_image_url = $background_property && ! empty( $background_property->get_single_image_url()->get_absolute_url() ) ? $background_property->get_single_image_url()->get_absolute_url() : ''; $background_extension = $background_property && ! empty( $background_property->get_single_image_url()->get_ext() ) ? $background_property->get_single_image_url()->get_ext() : ''; $original_markup = $element->get_markup(); if ( ! $background_image_url || ! $this->helper->is_image_extension_supported( $background_extension, $background_image_url ) || $this->is_element_excluded( $element ) || $this->is_image_element_skipped_through_filter( $background_image_url, $original_markup ) || $this->helper->is_native_lazy_loading_enabled() ) { return; } $data_attribute_name = 'data-' . str_replace( 'background', 'bg', $background_property->get_property() ); // data-bg|data-bg-image. $element->add_attribute( new Element_Attribute( $data_attribute_name, trim( $background_property->get_value() ) ) ); $background_property->set_value( 'inherit' ); $this->add_lazy_load_class( $element ); } private function get_lazy_load_image_tag_names() { $image_tag_names = array( 'img', ); if ( ! $this->helper->is_native_lazy_loading_enabled() ) { $image_tag_names[] = 'source'; } return (array) apply_filters( 'wp_smush_lazyload_image_tag_names', $image_tag_names ); } private function is_image_element_skipped_through_filter( $src_url, $markup ) { /** * Filter to skip a single image from lazy load. * * @param bool $skip Should skip? Default: false. * @param string $src_url Image url. * @param string $image Image. * * @since 3.3.0 Added $image param. * */ return apply_filters( 'smush_skip_image_from_lazy_load', false, $src_url, $markup ); } public function transform_image_url( $url ) { return $url; } /** * @param Element $element * * @return void */ private function add_lazy_load_class( $element ) { $class_attr = $element->get_attribute_value( 'class' ); if ( ! empty( $class_attr ) && strpos( $class_attr, self::$lazyload_class ) !== false ) { return; } $new_class_attr = empty( $class_attr ) ? self::$lazyload_class : $class_attr . ' ' . self::$lazyload_class; $new_class_attr = apply_filters( 'wp_smush_lazy_load_classes', $new_class_attr ); $element->add_or_update_attribute( new Element_Attribute( 'class', $new_class_attr ) ); } /** * @param Element $element * @param $src_image_url * * @return void */ private function set_placeholder_width_and_height_in_style_attribute( $element, $src_image_url ) { if ( strpos( $element->get_markup(), '--smush-image-aspect-ratio' ) ) { return; } // We need explicit values for width and height. First try attribute values. $raw_width = $element->get_attribute_value( 'width' ); $width = false === strpos($raw_width, '%') ? (int) $raw_width : 0; $raw_height = $element->get_attribute_value( 'height' ); $height = false === strpos($raw_height, '%') ? (int) $raw_height : 0; // If attributes are missing, check if the image file name has dimensions in it if ( empty( $width ) || empty( $height ) ) { list( $width, $height ) = $this->url_utils->get_image_dimensions( $src_image_url ); } if ( $width && $height ) { $original_style = $element->get_attribute_value( 'style' ); $new_style = "--smush-placeholder-width: {$width}px; --smush-placeholder-aspect-ratio: $width/$height;$original_style"; $element->add_or_update_attribute( new Element_Attribute( 'style', $new_style ) ); } } /** * @param Composite_Element $composite_element * * @return bool */ private function is_composite_element_excluded( $composite_element ) { foreach ( $composite_element->get_elements() as $sub_element ) { if ( $this->is_element_excluded( $sub_element ) ) { return true; } } return false; } /** * @param array $elements * * @return void */ private function transform_elements( $elements ) { foreach ( $elements as $element ) { $this->transform_image_element( $element ); } } /** * Get Keyword Exclusions. * * @return Keyword_Exclusions */ private function keyword_exclusions() { if ( ! $this->keyword_exclusions ) { $this->keyword_exclusions = new Keyword_Exclusions( $this->get_excluded_keywords() ); } return $this->keyword_exclusions; } /** * Get lazyload_class. * * @return string */ public static function get_lazyload_class() { return self::$lazyload_class; } /** * Get temp_src. * * @return string */ public static function get_temp_src() { return self::$temp_src; } } lazy-load/class-lazy-load-settings-dto.php 0000644 00000025673 15252476777 0014635 0 ustar 00 <?php /** * Lazy Load Settings DTO * * Handles conversion between PHP (snake_case/kebab-case) and React camelCase for lazy load settings. * * @package Smush\Core\Lazy_Load * @since 3.25.0 */ namespace Smush\Core\Lazy_Load; use Smush\Core\Abstract_Settings_DTO; if ( ! defined( 'WPINC' ) ) { die; } /** * Class Lazy_Load_Settings_DTO * * Converts lazy load settings from snake_case/kebab-case to camelCase for React. * * @since 3.25.0 */ class Lazy_Load_Settings_DTO extends Abstract_Settings_DTO { /** * Parent keys whose nested keys should be preserved as-is when unmapped. * * @return string[] */ protected static function get_keys_with_dynamic_array_values() { return array( 'include' ); } /** * Top-level keys mapping. * * @var array */ private static $top_level_keys = array( 'lazy_load' => 'lazyLoad', 'exclude-pages' => 'excludePages', 'exclude-classes' => 'excludeClasses', 'noscript_fallback' => 'noscriptFallback', 'native' => 'native', 'footer' => 'footer', 'animation' => 'animation', 'format' => 'format', 'include' => 'include', 'output' => 'output', ); /** * Animation object keys mapping. * Used for keys directly inside the 'animation' object. * * @var array */ private static $animation_keys = array( 'selected' => 'selected', 'fadein' => 'fadein', 'spinner' => 'spinner', 'placeholder' => 'placeholder', ); /** * Fadein nested keys mapping. * Used for keys inside the 'animation.fadein' object. * * @var array */ private static $fadein_keys = array( 'duration' => 'duration', 'delay' => 'delay', ); /** * Spinner nested keys mapping. * Used for keys inside the 'animation.spinner' object. * * @var array */ private static $spinner_keys = array( 'selected' => 'selected', 'custom' => 'custom', ); /** * Placeholder nested keys mapping. * Used for keys inside the 'animation.placeholder' object. * * @var array */ private static $placeholder_keys = array( 'selected' => 'selected', 'custom' => 'custom', 'color' => 'color', ); /** * Format nested keys mapping. * Used for keys inside the 'format' object. * * @var array */ private static $format_keys = array( 'embed_video' => 'embedVideo', 'jpeg' => 'jpeg', 'jpg' => 'jpg', 'png' => 'png', 'gif' => 'gif', 'svg' => 'svg', 'webp' => 'webp', 'iframe' => 'iframe', ); /** * Output nested keys mapping. * Used for keys inside the 'output' object. * * @var array */ private static $output_keys = array( 'content' => 'content', 'thumbnails' => 'thumbnails', 'gravatars' => 'gravatars', 'widgets' => 'widgets', ); /** * Keys that contain indexed arrays (lists of values) rather than nested settings objects. * These arrays should be preserved as-is without recursive conversion. * * @var array */ private static $indexed_array_keys = array( 'exclude-pages', 'exclude-classes', 'excludePages', // React version. 'excludeClasses', // React version. 'custom', // Used in spinner and placeholder. ); /** * Get the list of keys that contain indexed arrays. * * @return array List of keys containing indexed arrays. */ protected static function get_indexed_array_keys() { return self::$indexed_array_keys; } /** * Sanitization schema for lazy-load settings (PHP keys, post-conversion). * * @return array */ protected static function get_sanitization_schema() { return array( 'lazy_load' => array( 'sanitizer' => 'wp_validate_boolean' ), 'format' => array( 'sanitizer' => 'wp_validate_boolean' ), 'output' => array( 'sanitizer' => 'wp_validate_boolean' ), 'include' => array( 'sanitizer' => 'wp_validate_boolean' ), 'exclude-pages' => array( 'sanitizer' => 'sanitize_text_field', 'nonempty_list' => true ), 'exclude-classes' => array( 'sanitizer' => 'sanitize_text_field', 'nonempty_list' => true ), 'footer' => array( 'sanitizer' => 'wp_validate_boolean' ), 'native' => array( 'sanitizer' => 'wp_validate_boolean' ), 'noscript_fallback' => array( 'sanitizer' => 'wp_validate_boolean' ), 'animation' => array( 'selected' => array( 'sanitizer' => 'sanitize_text_field' ), 'fadein' => array( 'duration' => array( 'sanitizer' => 'intval' ), 'delay' => array( 'sanitizer' => 'intval' ), ), 'spinner' => array( 'selected' => array( 'sanitizer' => 'intval' ), 'custom' => array( 'sanitizer' => 'intval' ), ), 'placeholder' => array( 'selected' => array( 'sanitizer' => 'intval' ), 'custom' => array( 'sanitizer' => 'intval' ), 'color' => array( 'sanitizer' => array( __CLASS__, 'sanitize_color' ) ), ), ), ); } /** * Sanitize color value to ensure it's a valid color format. * * Supports hex colors, rgb/rgba. * * @param string $color The color value to sanitize. * * @return string Sanitized color value or empty string if invalid. */ public static function sanitize_color( $color ) { if ( ! is_string( $color ) ) { return ''; } // Remove any whitespace. $color = trim( $color ); // Check if it's a valid hex color (#fff, #ffffff, #ffffffff) via WordPress core. $hex = sanitize_hex_color( $color ); if ( null !== $hex ) { return $hex; } // Check for rgb/rgba colors. if ( preg_match( '/^rgba?\(\s*(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\s*(?:,\s*(?:1(?:\.0+)?|0(?:\.\d+)?|\.\d+)\s*)?\)$/', $color ) ) { return $color; } return ''; } /** * Get the appropriate key map based on context. * * @param string $parent_key The parent key to determine which nested map to use. * * @return array The appropriate key map. */ protected static function get_key_map( $parent_key = null ) { if ( null === $parent_key ) { return self::$top_level_keys; } switch ( $parent_key ) { case 'animation': return self::$animation_keys; case 'fadein': return self::$fadein_keys; case 'spinner': return self::$spinner_keys; case 'placeholder': return self::$placeholder_keys; case 'format': return self::$format_keys; case 'output': return self::$output_keys; default: return array(); } } /** * Convert React props back to PHP settings format. * * @param array $props React props with camelCase keys. * @param string $parent_key The parent key for nested arrays (used to determine which key map to use). * * @return array PHP settings with snake_case/kebab-case keys. */ public static function from_react_props( $props, $parent_key = null ) { if ( empty( $props ) || ! is_array( $props ) ) { return array(); } // Allow DTOs to hydrate/reshape values for storage before normalizing types. $props = static::prepare_for_storage( $props, $parent_key ); return parent::from_react_props( $props, $parent_key ); } /** * Prepare React props for storage. * * Converts UI-friendly attachment objects back into attachment ID arrays. * * @param array $props React props with camelCase keys. * @param string|null $parent_key Parent key context. * * @return array */ protected static function prepare_for_storage( $props, $parent_key = null ) { if ( 'spinner' === $parent_key || 'placeholder' === $parent_key ) { if ( ! empty( $props['custom'] ) && is_array( $props['custom'] ) ) { $props['custom'] = self::normalize_attachment_ids( $props['custom'] ); } } return $props; } /** * Normalize an array of attachment IDs (or arrays containing an `id`) into a clean list of IDs. * * @param array $items Attachment IDs or arrays containing 'id'. * * @return int[] */ private static function normalize_attachment_ids( $items ) { if ( empty( $items ) || ! is_array( $items ) ) { return array(); } $ids = array_map( function ( $attachment ) { if ( is_array( $attachment ) && isset( $attachment['id'] ) ) { return (int) $attachment['id']; } return is_numeric( $attachment ) ? (int) $attachment : 0; }, $items ); $ids = array_filter( $ids, static function ( $id ) { return $id > 0; } ); return array_values( $ids ); } /** * Convert settings to React props. * * @param array $settings Settings array with PHP keys. * @param string $parent_key The parent key for nested arrays (used to determine which key map to use). * * @return array Transformed settings with camelCase keys. */ public static function to_react_props( $settings, $parent_key = null ) { if ( empty( $settings ) || ! is_array( $settings ) ) { return array(); } // Allow DTOs to hydrate/reshape values for React before normalizing types. $settings = self::prepare_for_react( $settings, $parent_key ); return parent::to_react_props( $settings, $parent_key ); } /** * Prepare raw stored settings for React. * * Child DTOs can override this to hydrate/reshape values into UI-friendly structures * (e.g. turn attachment IDs into { id, url } objects) before key mapping occurs. * * @param array $settings Settings array with PHP keys. * @param string|null $parent_key Parent key context. * * @return array */ protected static function prepare_for_react( $settings, $parent_key = null ) { if ( 'spinner' === $parent_key || 'placeholder' === $parent_key ) { if ( ! empty( $settings['custom'] ) && is_array( $settings['custom'] ) ) { $settings['custom'] = self::hydrate_attachments_for_react( $settings['custom'], 'full' ); } return $settings; } return $settings; } /** * Hydrate an array of attachment IDs (or objects containing an `id`) into * an array of objects that include `id` and `url`. * * @param array $items Attachment IDs or arrays containing 'id'. * @param string $size Image size to retrieve URL for. * * @return array */ private static function hydrate_attachments_for_react( $items, $size = 'full' ) { if ( empty( $items ) || ! is_array( $items ) ) { return array(); } return array_values( array_filter( array_map( function ( $attachment_id ) use ( $size ) { $attachment_id = isset( $attachment_id['id'] ) ? $attachment_id['id'] : $attachment_id; $attachment_id = is_numeric( $attachment_id ) ? (int) $attachment_id : 0; $url = ''; if ( $attachment_id > 0 ) { $src = wp_get_attachment_image_src( $attachment_id, $size ); $url = is_array( $src ) && ! empty( $src[0] ) ? (string) $src[0] : ''; if ( empty( $url ) ) { $url = (string) wp_get_attachment_url( $attachment_id ); } } if ( $attachment_id <= 0 ) { return null; } return array( 'id' => $attachment_id, 'url' => $url, ); }, $items ), static function ( $item ) { return ! is_null( $item ); } ) ); } } lazy-load/class-lazy-load-video-embed.php 0000644 00000025732 15252476777 0014365 0 ustar 00 <?php namespace Smush\Core\Lazy_Load; use Smush\Core\Lazy_Load\Video_Embed\Video_Embed; use Smush\Core\Lazy_Load\Video_Embed\Video_Embed_Helper; use Smush\Core\Lazy_Load\Video_Embed\Video_Thumbnail; use Smush\Core\Next_Gen\Next_Gen_Manager; use Smush\Core\Parser\Element; use Smush\Core\Parser\Element_Attribute; use Smush\Core\Settings; use Smush\Core\Url_Utils; defined( 'WPINC' ) || exit; class Lazy_Load_Video_Embed { private static $class_smush_video = 'smush-lazyload-video'; /** * @var Video_Embed */ private $embed_provider; /** * @var string */ private $embed_url; /** * @var Element */ private $iframe_element; /** * @var Video_Embed_Helper */ protected $helper; private Url_Utils $url_utils; public function __construct( $embed_url, $iframe_element ) { $this->embed_url = $embed_url; $this->iframe_element = $iframe_element; $this->helper = Video_Embed_Helper::get_instance(); $this->url_utils = new Url_Utils(); } private function get_embed_provider() { if ( ! $this->embed_provider ) { $this->embed_provider = $this->prepare_embed_provider(); } return $this->embed_provider; } private function prepare_embed_provider() { return $this->helper->create_embed_object( $this->embed_url ); } public function can_lazy_load() { $embed_provider = $this->get_embed_provider(); $can_lazy_load = ! empty( $embed_provider ); return apply_filters( 'wp_smush_should_lazy_load_video', $can_lazy_load, $embed_provider, $this->iframe_element ); } private function is_auto_play_enabled() { $query_vars = $this->url_utils->get_query_vars( $this->embed_url ); return ! empty( $query_vars['autoplay'] ); } public function transform() { if ( ! $this->get_embed_provider() ) { return; } $wrapper_markup_parts = $this->generate_video_wrapper_parts(); if ( empty( $wrapper_markup_parts ) ) { return; } list( $wrapper_before, $wrapper_after ) = $wrapper_markup_parts; $this->iframe_element->set_wrapper_markup( $wrapper_before, $wrapper_after ); $this->convert_src_to_data_src(); $this->iframe_element->add_attribute( new Element_Attribute( 'src', 'about:blank' ) ); } private function generate_video_wrapper_parts() { // Try the cover attribute. $wrapper_markup = $this->generate_video_wrapper_parts_from_cover_attribute(); if ( ! empty( $wrapper_markup ) ) { return $wrapper_markup; } // Use cached thumbnail data. $wrapper_markup = $this->generate_video_wrapper_parts_from_cached_video_thumbnail(); if ( ! empty( $wrapper_markup ) ) { return $wrapper_markup; } // Generate a custom redirect URL. $wrapper_markup = $this->generate_video_wrapper_parts_with_custom_url(); if ( ! empty( $wrapper_markup ) ) { return $wrapper_markup; } return null; } private function convert_src_to_data_src() { $src_attribute = $this->iframe_element->get_attribute( 'src' ); if ( $src_attribute ) { $original_value = $src_attribute->get_value(); $data_attribute = new Element_Attribute( 'data-src', $original_value ); $this->iframe_element->replace_attribute( 'src', $data_attribute ); } } private function generate_video_wrapper_parts_with_custom_url() { list( $video_width, $video_height ) = $this->get_video_dimensions( $this->iframe_element ); if ( ! $video_width && ! $video_height ) { return null; } $video_thumbnail_url = $this->helper->make_video_thumbnail_url( $this->embed_url, $video_width, $video_height ); $aspect_ratio = $this->get_aspect_ratio( $video_width, $video_height ); return $this->generate_video_wrapper_markup_parts( $aspect_ratio, $video_thumbnail_url ); } private function generate_video_wrapper_parts_from_cached_video_thumbnail() { list( $video_width, $video_height ) = $this->get_video_dimensions( $this->iframe_element ); if ( ! $video_width && ! $video_height ) { return null; } $video_embed = $this->get_embed_provider(); $cached_video_thumbnail = $video_embed->get_cached_video_thumbnail( $video_width, $video_height ); if ( ! $cached_video_thumbnail ) { return null; } $video_thumbnail_url = $cached_video_thumbnail->get_url(); $aspect_ratio = $this->get_aspect_ratio( $video_width, $video_height ); $fallback_background_image_attribute = $this->get_next_gen_fallback_background_image_attribute( $cached_video_thumbnail ); return $this->generate_video_wrapper_markup_parts( $aspect_ratio, $video_thumbnail_url, $fallback_background_image_attribute ); } private function generate_video_wrapper_parts_from_cover_attribute() { $embed_provider = $this->get_embed_provider(); list( $video_width, $video_height ) = $this->get_video_dimensions( $this->iframe_element ); $video_thumbnail = $this->get_video_thumbnail_from_attribute( $this->iframe_element, $video_width, $video_height ); if ( empty( $video_thumbnail ) || empty( $embed_provider ) ) { return null; } // Generate markup components. $aspect_ratio = $this->get_aspect_ratio( $video_width, $video_height, $video_thumbnail ); $fallback_background_image_attribute = $this->get_next_gen_fallback_background_image_attribute( $video_thumbnail ); return $this->generate_video_wrapper_markup_parts( $aspect_ratio, $video_thumbnail->get_url(), $fallback_background_image_attribute ); } private function generate_video_wrapper_markup_parts( $aspect_ratio, $video_thumbnail_url, $fallback_background_image_attribute = '' ) { $embed_provider = $this->get_embed_provider(); $wrapper_classes = $this->generate_wrapper_classes(); $background_image_attribute = $this->get_background_image_attribute( $video_thumbnail_url ); // Build wrapper markup. $wrapper_markup_before = sprintf( '<div class="%1$s" style="--smush-video-aspect-ratio: %2$s" %3$s %4$s>', esc_attr( implode( ' ', $wrapper_classes ) ), esc_attr( $aspect_ratio ), $background_image_attribute, $fallback_background_image_attribute ); $wrapper_markup_before = apply_filters( 'wp_smush_lazy_load_video_wrapper_markup_before', $wrapper_markup_before, $embed_provider, $this->iframe_element ); $wrapper_markup_after = $this->get_play_button( $this->iframe_element ); $wrapper_markup_after .= '</div>'; $wrapper_markup_after = apply_filters( 'wp_smush_lazy_load_video_wrapper_markup_after', $wrapper_markup_after, $embed_provider, $this->iframe_element ); if ( empty( $wrapper_markup_before ) || empty( $wrapper_markup_after ) ) { return null; } return array( $wrapper_markup_before, $wrapper_markup_after ); } private function get_aspect_ratio( $video_width, $video_height, $video_thumbnail = null ) { $aspect_ratio = '16/9'; if ( $video_width && $video_height ) { $aspect_ratio = "{$video_width}/{$video_height}"; } else if ( $video_thumbnail ) { $aspect_ratio = $video_thumbnail->get_aspect_ratio(); } return apply_filters( 'wp_smush_lazy_load_video_aspect_ratio', $aspect_ratio, $this->get_embed_provider(), $this->iframe_element ); } private function generate_wrapper_classes() { $embed_provider = $this->get_embed_provider(); $classes = array( Lazy_Load_Transform::get_lazyload_class(), self::$class_smush_video, 'smush-lazyload-' . $embed_provider->get_name(), ); if ( $this->is_auto_play_enabled() ) { $classes[] = 'smush-lazyload-autoplay'; } return $classes; } private function get_background_image_attribute( $video_thumbnail_url ) { $thumb_url = apply_filters( 'wp_smush_lazy_load_video_thumbnail_url', $video_thumbnail_url, $this->get_embed_provider(), $this->iframe_element ); return sprintf( 'data-bg-image="url(%s)"', esc_url( $thumb_url ) ); } /** * @param Video_Thumbnail $video_thumbnail * * @return string */ private function get_next_gen_fallback_background_image_attribute( $video_thumbnail ) { $fallback_thumb_url = $video_thumbnail->get_fallback_url(); $has_next_gen_url = $video_thumbnail->has_next_gen_url(); // Return early if fallback URL or next-gen URL is not available. if ( empty( $fallback_thumb_url ) || ! $has_next_gen_url ) { return ''; } $next_gen_manager = Next_Gen_Manager::get_instance(); $is_next_gen_fallback_active = $next_gen_manager->is_active() && $next_gen_manager->is_fallback_activated(); if ( ! $is_next_gen_fallback_active ) { return ''; } $format_key = Next_Gen_Manager::get_instance()->get_active_format_key(); $fallback_data = wp_json_encode( array( 'data-bg-image' => sprintf( 'url(%s)', esc_url( $fallback_thumb_url ) ), ) ); return sprintf( 'data-smush-%s-fallback=\'%s\'', esc_attr( $format_key ), esc_attr( $fallback_data ) ); } private function get_video_thumbnail_from_attribute( $iframe_element, $video_width = false, $video_height = false ) { $poster = $iframe_element->get_attribute_value( 'data-poster' ); if ( ! $poster ) { return null; } if ( ! empty( $video_width ) && ! empty( $video_height ) ) { $width = $video_width; $height = $video_height; } else { list( $width, $height ) = $this->url_utils->get_image_dimensions( $poster ); } if ( ! $width || ! $height ) { return null; } $extension = $this->url_utils->get_extension( $poster ); $is_next_gen_format = 'avif' === $extension || 'webp' === $extension; $next_gen_url = $is_next_gen_format ? $poster : null; $fallback_url = $is_next_gen_format ? null : $poster; $video_thumbnail = new Video_Thumbnail(); $video_thumbnail->from_array( array( 'width' => $width, 'height' => $height, 'next_gen_url' => $next_gen_url, 'fallback_url' => $fallback_url, ) ); return $video_thumbnail; } private function get_play_button( $iframe_element ) { $play_label = $iframe_element->get_attribute_value( 'data-play-label' ); if ( ! $play_label ) { $play_label = esc_html__( 'Play', 'wp-smushit' ); } $player_button = sprintf( '<span class="smush-play-btn" role="button" aria-label="%1$s"> <span tabindex="0" class="smush-play-btn-inner"> <span>%2$s</span> </span> </span>', __( 'Play video', 'wp-smushit' ), $play_label, ); return apply_filters( 'wp_smush_lazy_load_video_player_button', $player_button, $iframe_element ); } private function get_video_dimensions( $iframe_element ) { $width = $iframe_element->get_attribute_value( 'width' ); $height = $iframe_element->get_attribute_value( 'height' ); $width = strpos( $width, '%' ) ? 0 : (int) $width; $height = strpos( $height, '%' ) ? 0 : (int) $height; if ( empty( $width ) && empty( $height ) ) { $width = $this->get_video_max_width(); } $video_dimensions = array( $width, $height, ); return (array) apply_filters( 'wp_smush_lazy_load_video_dimensions', $video_dimensions, $iframe_element ); } private function get_video_max_width() { if ( defined( 'WP_SMUSH_LAZYLOAD_MAX_VIDEO_WIDTH' ) && WP_SMUSH_LAZYLOAD_MAX_VIDEO_WIDTH > 0 ) { return WP_SMUSH_LAZYLOAD_MAX_VIDEO_WIDTH; } return Settings::get_instance()->max_content_width(); } } lazy-load/video-embed/class-video-thumbnail-cache.php 0000644 00000002204 15252476777 0016610 0 ustar 00 <?php namespace Smush\Core\Lazy_Load\Video_Embed; class Video_Thumbnail_Cache { private static $instance; public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } public function add( $video_id, $provider, $thumb_width, $thumb_height, $video_thumbnail ) { $transient_key = $this->get_transient_key( $video_id, $provider, $thumb_width, $thumb_height ); set_transient( $transient_key, $video_thumbnail->to_array() ); } public function get( $video_id, $provider, $thumb_width, $thumb_height ) { $transient_key = $this->get_transient_key( $video_id, $provider, $thumb_width, $thumb_height ); $video_thumbnail_data = get_transient( $transient_key ); if ( ! $video_thumbnail_data ) { return null; } $video_thumbnail = new Video_Thumbnail(); $video_thumbnail->from_array( $video_thumbnail_data ); return $video_thumbnail; } private function get_transient_key( $video_id, $provider, $thumb_width, $thumb_height ) { return sprintf( 'wp-smush-video-thumbnail-%s-%s-%d-%d', $provider, $video_id, (int) $thumb_width, (int) $thumb_height ); } } lazy-load/video-embed/class-vimeo-embed.php 0000644 00000017245 15252476777 0014664 0 ustar 00 <?php namespace Smush\Core\Lazy_Load\Video_Embed; use Smush\Core\Helper; if ( ! defined( 'WPINC' ) ) { die; } class Vimeo_Embed implements Video_Embed { private static $name = 'vimeo'; private static $video_id_regex = '#player\.vimeo\.com\/video\/(?<video_id>[\d]+)#i'; private static $vimeo_oembed_endpoint = 'https://vimeo.com/api/oembed.json'; /** * * @var string */ private $embed_url; /** * Video id. * * @var string */ private $video_id; /** * Thumbnail sizes. * * @var array */ private $thumb_sizes; private Video_Thumbnail_Cache $video_thumbnail_cache; public function __construct( $embed_url ) { $this->embed_url = $embed_url; $this->video_thumbnail_cache = Video_Thumbnail_Cache::get_instance(); } public function get_name() { return self::$name; } public function get_embed_url() { return $this->embed_url; } public function can_lazy_load() { return $this->is_valid_embed_url(); } private function is_valid_embed_url() { return ! empty( $this->get_video_id() ); } public function get_video_id() { if ( ! $this->video_id ) { $this->video_id = $this->prepare_video_id(); } return $this->video_id; } private function prepare_video_id() { $video_id_regex = apply_filters( 'wp_smush_lazy_load_vimeo_id_regex', self::$video_id_regex ); if ( preg_match( $video_id_regex, $this->embed_url, $matches ) ) { return $matches['video_id'] ?? ''; } return ''; } public function fetch_video_thumbnail( $video_width, $video_height ) { $video_id = $this->get_video_id(); if ( ! $video_id ) { return null; } list( $requested_thumb_width, $requested_thumb_height ) = $this->determine_best_thumbnail_size( $video_width, $video_height ); $cached = $this->video_thumbnail_cache->get( $video_id, self::$name, $requested_thumb_width, $requested_thumb_height ); if ( $cached ) { return $cached; } $thumbnail_info = $this->fetch_thumbnail_info( $requested_thumb_width, $requested_thumb_height ); if ( empty( $thumbnail_info ) ) { return null; } list( $thumb_url, $actual_thumb_width, $actual_thumb_height ) = $thumbnail_info; $video_thumbnail = new Video_Thumbnail(); $video_thumbnail->from_array( array( 'fallback_url' => $thumb_url, 'width' => $actual_thumb_width, 'height' => $actual_thumb_height, ) ); $this->video_thumbnail_cache->add( $video_id, self::$name, $requested_thumb_width, $requested_thumb_height, $video_thumbnail ); return $video_thumbnail; } /** * @param $video_width * @param $video_height * * @return array|int[]|mixed */ private function determine_best_non_retina_thumbnail_size( $video_width, $video_height ) { if ( $video_width > 0 && $video_height > 0 ) { return array( $video_width, $video_height ); } $thumb_sizes = $this->get_thumbnail_sizes(); $thumb_size = array(); foreach ( $thumb_sizes as $size ) { list( $thumb_width, $thumb_height ) = $size; if ( $this->is_smaller_than_video( $thumb_width, $thumb_height, $video_width, $video_height ) ) { continue; } $thumb_size = $size; break; } if ( empty( $thumb_size ) ) { $thumb_size = array( 640, 360 );// Standard Definition. } return $thumb_size; } private function determine_best_thumbnail_size( $video_width, $video_height ) { list( $non_retina_width, $non_retina_height ) = $this->determine_best_non_retina_thumbnail_size( $video_width, $video_height ); $retina_ratio = $this->get_retina_ratio( $non_retina_width, $non_retina_height ); $retina_width = $non_retina_width * $retina_ratio; $retina_height = $non_retina_height * $retina_ratio; return array( (int) ceil( $retina_width ), (int) ceil( $retina_height ) ); } private function is_smaller_than_video( $thumb_width, $thumb_height, $video_width, $video_height ) { return $thumb_width < $video_width || ( empty( $video_width ) && $thumb_height < $video_height ); } private function get_thumbnail_sizes() { if ( ! $this->thumb_sizes ) { $this->thumb_sizes = $this->prepare_thumbnail_sizes(); } return $this->thumb_sizes; } private function prepare_thumbnail_sizes() { $thumb_sizes = array( array( 295, 166 ), // Small 16:9. array( 640, 360 ), // Standard Definition SD) 16:9. array( 640, 480 ), // Standard Definition SD) 4:3. array( 1280, 720 ), // High Definition HD) 16:9. array( 1280, 960 ), // High Definition HD) 4:3. array( 1920, 1080 ), // Full High Definition Full HD. ); $thumb_sizes = apply_filters( 'wp_smush_lazy_load_vimeo_thumbnail_sizes', $thumb_sizes, $this->get_video_id(), $this->embed_url ); // Sort by width. usort( $thumb_sizes, function ( $a, $b ) { if ( isset( $a[1], $b[1] ) ) { return $a[1] - $b[1]; } else { return 0; } } ); return $thumb_sizes; } private function fetch_thumbnail_info( $width, $height ) { $embed_api_url = $this->get_oembed_api_url( $width, $height ); $timeout = apply_filters( 'wp_smush_lazy_load_oembed_api_timeout', 15, $this->get_video_id(), $this->embed_url ); $response = wp_remote_get( $embed_api_url, array( 'timeout' => $timeout, ) ); $video_info = wp_remote_retrieve_body( $response ); $thumb_info = array(); if ( ! empty( $video_info ) ) { $video_info = json_decode( $video_info, true ); if ( ! empty( $video_info['thumbnail_url'] ) && ! empty( $video_info['thumbnail_width'] ) && ! empty( $video_info['thumbnail_height'] ) ) { $thumb_info = array( $video_info['thumbnail_url'], $video_info['thumbnail_width'], $video_info['thumbnail_height'], ); } } elseif ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); $error_code = $response->get_error_code(); $error_code = $error_code ? $error_code : 'unknown_error'; $this->logger()->error( sprintf( 'Vimeo: Error fetching thumbnail info: %s (%s) | oEmbed API URL: %s', $error_message, $error_code, $embed_api_url ) ); } return $thumb_info; } private function get_retina_ratio( $width, $height ) { $retina_ratio = (int) apply_filters( 'wp_smush_lazy_load_vimeo_retina_ratio', null, $width, $height ); if ( $retina_ratio > 0 ) { return $retina_ratio; } if ( $this->thumbnail_size_exists( $width, $height ) ) { return 1; } // Increase the video dimensions because video thumbnails are often smaller than the original video dimensions, // typically by a factor of 1.25 to 1.69. $retina_ratio = 1.5; return $retina_ratio; } private function thumbnail_size_exists( $width, $height ) { $common_thumbnail_sizes = $this->get_thumbnail_sizes(); foreach ( $common_thumbnail_sizes as $size ) { if ( isset( $size[0], $size[1] ) && $size[0] === $width && $size[1] === $height ) { return true; } } return false; } private function get_oembed_api_url( $width, $height ) { $oembed_api_url = add_query_arg( array( 'width' => $width, 'height' => $height, 'url' => urlencode( $this->embed_url ), ), self::$vimeo_oembed_endpoint ); return apply_filters( 'wp_smush_lazyload_vimeo_oembed_api_url', $oembed_api_url, $this->get_video_id(), $this->embed_url ); } private function logger() { // Logger is a dynamic object, we will switch to another log file when point to another module, // so keep it as a function instead of a fixed variable to log into correct log file. return Helper::logger()->lazy(); } public function get_cached_video_thumbnail( $video_width, $video_height ) { list( $thumb_width, $thumb_height ) = $this->determine_best_thumbnail_size( $video_width, $video_height ); return $this->video_thumbnail_cache->get( $this->get_video_id(), self::$name, $thumb_width, $thumb_height ); } } lazy-load/video-embed/class-video-embed-helper.php 0000644 00000002330 15252476777 0016115 0 ustar 00 <?php namespace Smush\Core\Lazy_Load\Video_Embed; defined( 'ABSPATH' ) || exit; class Video_Embed_Helper { /** * @var self */ private static $instance; public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } public function make_video_thumbnail_url( $embed_url, $video_width, $video_height ) { return admin_url( sprintf( 'admin-ajax.php?action=smush_video_thumbnail&url=%s&video_width=%d&video_height=%d', urlencode( $embed_url ), (int) $video_width, (int) $video_height ) ); } public function create_embed_object( $embed_url ) { $provider_classes = self::get_embed_provider_classes(); if ( empty( $provider_classes ) || ! is_array( $provider_classes ) ) { return null; } foreach ( $provider_classes as $provider_class ) { $provider_instance = new $provider_class( $embed_url ); if ( $provider_instance->can_lazy_load() ) { return $provider_instance; } } return null; } private function get_embed_provider_classes() { $embed_provider_classes = array( Youtube_Embed::class, Vimeo_Embed::class, ); return apply_filters( 'wp_smush_lazy_load_embed_provider_classes', $embed_provider_classes ); } } lazy-load/video-embed/class-video-thumbnail-controller.php 0000644 00000005672 15252476777 0017744 0 ustar 00 <?php namespace Smush\Core\Lazy_Load\Video_Embed; use Smush\Core\Controller; use Smush\Core\Lazy_Load\Lazy_Load_Helper; use Smush\Core\Server_Utils; use Smush\Core\Settings; use Smush\Core\Url_Utils; use WP_Error; class Video_Thumbnail_Controller extends Controller { private $video_helper; private Lazy_Load_Helper $lazy_helper; /** * @var Settings */ private $settings; private Server_Utils $server_utils; private Url_Utils $url_utils; public function __construct() { $this->video_helper = Video_Embed_Helper::get_instance(); $this->lazy_helper = Lazy_Load_Helper::get_instance(); $this->settings = Settings::get_instance(); $this->server_utils = new Server_Utils(); $this->url_utils = new Url_Utils(); $this->register_action( 'wp_ajax_smush_video_thumbnail', array( $this, 'redirect_to_original_video_thumbnail' ) ); $this->register_action( 'wp_ajax_nopriv_smush_video_thumbnail', array( $this, 'redirect_to_original_video_thumbnail' ) ); } public function should_run() { return parent::should_run() && $this->settings->is_lazyload_active() && $this->lazy_helper->should_lazy_load_embed_video(); } public function redirect_to_original_video_thumbnail() { $thumbnail_url = $this->get_video_thumbnail_url_from_request( $_GET ); if ( is_wp_error( $thumbnail_url ) ) { status_header( 404 ); exit; } $expires = 31536000; header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $expires ) . ' GMT' ); header( "Cache-Control: public, max-age={$expires}, immutable" ); header( "Location: {$thumbnail_url}", true, 301 ); exit(); } public function get_video_thumbnail_url_from_request( $request ) { $embed_url = empty( $request['url'] ) ? '' : html_entity_decode( rawurldecode( $request['url'] ) ); $width = empty( $request['video_width'] ) ? '' : (int) $request['video_width']; $height = empty( $request['video_height'] ) ? '' : (int) $request['video_height']; if ( ! filter_var( $embed_url, FILTER_VALIDATE_URL ) || ( ! $width && ! $height ) ) { return new WP_Error( 'invalid_params', __( 'Invalid video URL or dimensions.', 'wp-smushit' ) ); } $embed = $this->video_helper->create_embed_object( $embed_url ); $video_thumbnail = $embed->fetch_video_thumbnail( $width, $height ); if ( ! $video_thumbnail ) { return new WP_Error( 'not_found', __( 'Video thumbnail not found.', 'wp-smushit' ) ); } $thumbnail_url = ''; $nextgen_url = $video_thumbnail->get_next_gen_url(); if ( $nextgen_url ) { $nextgen_extension = $this->url_utils->get_extension( $nextgen_url ); $nextgen_supported = $this->server_utils->browser_supports_nextgen_format( $nextgen_extension ); if ( $nextgen_supported ) { $thumbnail_url = $nextgen_url; } } if ( empty( $thumbnail_url ) ) { $thumbnail_url = $video_thumbnail->get_fallback_url(); } return $thumbnail_url ? $thumbnail_url : new WP_Error( 'not_found', __( 'Thumbnail URL not found.', 'wp-smushit' ) ); } } lazy-load/video-embed/class-youtube-embed.php 0000644 00000016350 15252476777 0015235 0 ustar 00 <?php namespace Smush\Core\Lazy_Load\Video_Embed; use Smush\Core\Next_Gen\Next_Gen_Manager; use Smush\Core\Settings; use Smush\Core\Url_Utils; if ( ! defined( 'WPINC' ) ) { die; } class Youtube_Embed implements Video_Embed { private static $name = 'youtube'; private static $video_id_regex = '#youtube(?:-nocookie)?\.com\/embed\/(?<video_id>[a-zA-Z0-9_-]{11})#i'; private static $thumbnail_url_format = 'https://i.ytimg.com/%1$s/%2$s/%3$s.%4$s'; private static $thumbnail_default = 'default'; private static $thumbnail_medium = 'mqdefault'; private static $thumbnail_high = 'hqdefault'; private static $thumbnail_sd = 'sddefault'; private static $thumbnail_max = 'maxresdefault'; /** * @var Settings */ private $settings; /** * * @var string */ private $embed_url; /** * Video id. * * @var string */ private $video_id; /** * @var Video_Embed_Helper */ protected $video_helper; /** * Thumbnail sizes. * * @var array */ private $thumb_sizes; private Video_Thumbnail_Cache $video_thumbnail_cache; private Url_Utils $url_utils; public function __construct( $embed_url ) { $this->embed_url = $embed_url; $this->settings = Settings::get_instance(); $this->video_helper = Video_Embed_Helper::get_instance(); $this->video_thumbnail_cache = Video_Thumbnail_Cache::get_instance(); $this->url_utils = new Url_Utils(); } public function get_name() { return self::$name; } public function get_embed_url() { return $this->embed_url; } public function can_lazy_load() { return $this->is_valid_embed_url(); } private function is_valid_embed_url() { $video_id = $this->get_video_id(); return ! empty( $video_id ); } public function get_video_id() { if ( ! $this->video_id ) { $this->video_id = $this->prepare_video_id(); } return $this->video_id; } private function prepare_video_id() { $video_id_regex = apply_filters( 'wp_smush_lazy_load_youtube_id_regex', self::$video_id_regex ); if ( ! preg_match( $video_id_regex, $this->embed_url, $matches ) ) { return null; } $video_id = $matches['video_id'] ?? ''; // TODO: check if we need to support data attribute for youtube playlist. $video_id = apply_filters( 'wp_smush_lazy_load_youtube_video_id', $video_id, $this->embed_url ); $is_playlist = 'videoseries' === $video_id; return $is_playlist ? null : $video_id; } public function fetch_video_thumbnail( $video_width, $video_height ) { $video_id = $this->get_video_id(); if ( ! $video_id ) { return null; } list( $thumb_size_name, $thumb_width, $thumb_height ) = $this->determine_best_thumbnail_size( $video_width, $video_height ); $cached = $this->video_thumbnail_cache->get( $video_id, self::$name, $thumb_width, $thumb_height ); if ( $cached ) { return $cached; } $next_gen_thumbnail_url = $this->get_thumbnail_url( $thumb_size_name, true ); $fallback_thumbnail_url = $this->get_thumbnail_url( $thumb_size_name, false ); $video_ratio = $video_width > 0 && $video_height > 0 ? "{$video_width}/{$video_height}" : "{$thumb_width}/{$thumb_height}"; $video_thumbnail = new Video_Thumbnail(); $video_thumbnail->from_array( array( 'width' => $thumb_width, 'height' => $thumb_height, 'next_gen_url' => $next_gen_thumbnail_url, 'fallback_url' => $fallback_thumbnail_url, 'aspect_ratio' => $video_ratio, ) ); $this->video_thumbnail_cache->add( $video_id, self::$name, $thumb_width, $thumb_height, $video_thumbnail ); return $video_thumbnail; } private function determine_best_thumbnail_size( $video_width, $video_height ) { $thumb_sizes = $this->get_thumbnail_sizes(); $thumb_size = array(); $larger_size = null; foreach ( $thumb_sizes as $size ) { list( , $thumb_width, $thumb_height ) = $size; if ( $this->is_smaller_than_video( $thumb_width, $thumb_height, $video_width, $video_height ) ) { continue; } if ( empty( $video_width ) || empty( $video_height ) ) { return $size; } if ( $this->is_aspect_ratio_match( $video_width, $video_height, $thumb_width, $thumb_height ) ) { return $size; } if ( empty( $larger_size ) ) { $larger_size = $size; } } if ( empty( $thumb_size ) ) { $thumb_size = $larger_size ?? end( $thumb_sizes ); } if ( empty( $thumb_size ) || ! isset( $thumb_size[0] ) || ! $this->is_thumbnail_available( $thumb_size[0] ) ) { // Try standard size which should exist for all video // due to Youtube will automatically generate three thumbnail sizes (Default, Medium, High-Resolution). $thumb_size = array( self::$thumbnail_high, 480, 360 ); } return $thumb_size; } private function is_smaller_than_video( $thumb_width, $thumb_height, $video_width, $video_height ) { return $thumb_width < $video_width || ( empty( $video_width ) && $thumb_height < $video_height ); } private function is_aspect_ratio_match( $video_width, $video_height, $thumb_width, $thumb_height ) { return wp_fuzzy_number_match( $video_width / $thumb_width, $video_height / $thumb_height, 0.1 ); } private function is_thumbnail_available( $thumb_size_name ) { $standard_sizes = array( self::$thumbnail_default, self::$thumbnail_medium, self::$thumbnail_high, ); if ( in_array( $thumb_size_name, $standard_sizes, true ) ) { return true; } $thumbnail_url = $this->get_thumbnail_url( $thumb_size_name ); return $this->url_utils->url_has_200_response( $thumbnail_url ); } private function get_thumbnail_sizes() { if ( ! $this->thumb_sizes ) { $this->thumb_sizes = $this->prepare_thumbnail_sizes(); } return $this->thumb_sizes; } private function prepare_thumbnail_sizes() { // @see https://gist.github.com/a1ip/be4514c1fd392a8c13b05e082c4da363. $thumb_sizes = array( array( self::$thumbnail_default, 120, 90 ), // - Default 4:3. array( self::$thumbnail_medium, 320, 180 ), // - Medium Quality 16:9. array( self::$thumbnail_high, 480, 360 ), // - High Quality 4:3. array( self::$thumbnail_sd, 640, 480 ), // - Standard Definition 4:3. array( self::$thumbnail_max, 1280, 720 ), // - Maximum Resolution 16:9. ); $thumb_sizes = apply_filters( 'wp_smush_lazy_load_youtube_thumbnail_sizes', $thumb_sizes, $this->get_video_id(), $this->embed_url ); // Sort by width. usort( $thumb_sizes, function ( $a, $b ) { if ( isset( $a[1], $b[1] ) ) { return $a[1] - $b[1]; } else { return 0; } } ); return $thumb_sizes; } private function get_thumbnail_url( $thumb_size_name, $use_next_gen_format = false ) { if ( $use_next_gen_format ) { $extension = 'webp'; $extension_uri = 'vi_webp'; } else { $extension = 'jpg'; $extension_uri = 'vi'; } return sprintf( self::$thumbnail_url_format, $extension_uri, $this->get_video_id(), $thumb_size_name, $extension ); } public function get_cached_video_thumbnail( $video_width, $video_height ) { list( , $thumb_width, $thumb_height ) = $this->determine_best_thumbnail_size( $video_width, $video_height ); return $this->video_thumbnail_cache->get( $this->get_video_id(), $this->get_name(), $thumb_width, $thumb_height ); } /** * Get thumbnail_url_format. * * @return string */ public static function get_thumbnail_url_format() { return self::$thumbnail_url_format; } } lazy-load/video-embed/class-video-embed.php 0000644 00000001354 15252476777 0014645 0 ustar 00 <?php namespace Smush\Core\Lazy_Load\Video_Embed; interface Video_Embed { /** * Get video provider name. * * @return string */ public function get_name(); /** * Get video id. * * @return string. */ public function get_video_id(); /** * Get embed url. * * @return string */ public function get_embed_url(); /** * Check if can lazy load video. * * @return bool. */ public function can_lazy_load(); /** * Get video thumbnail. * * @param int $video_width Max width. * @param int $video_height Max height. * * @return null|Video_Thumbnail */ public function fetch_video_thumbnail( $video_width, $video_height ); public function get_cached_video_thumbnail( $video_width, $video_height ); } lazy-load/video-embed/class-video-thumbnail.php 0000644 00000006411 15252476777 0015553 0 ustar 00 <?php namespace Smush\Core\Lazy_Load\Video_Embed; use Smush\Core\CDN\CDN_Helper; use Smush\Core\Next_Gen\Next_Gen_Manager; use Smush\Core\Settings; class Video_Thumbnail { /** * @var int */ private $width; /** * @var int */ private $height; /** * @var string */ private $next_gen_url; /** * @var string */ private $cdn_url; /** * @var string */ private $fallback_url; /** * @var string */ private $aspect_ratio; public function __construct() { } public function get_width() { return $this->width; } private function set_width( $width ) { $this->width = (int) $width; } public function get_height() { return $this->height; } private function set_height( $height ) { $this->height = (int) $height; } public function get_next_gen_url() { return $this->next_gen_url; } private function set_next_gen_url( $next_gen_url ) { $this->next_gen_url = $next_gen_url; } private function get_cdn_url() { if ( is_null( $this->cdn_url ) ) { $this->cdn_url = $this->prepare_cdn_url(); } return $this->cdn_url; } private function prepare_cdn_url() { $cdn_helper = CDN_Helper::get_instance(); $thumbnail_url = $this->get_fallback_url(); if ( ! $cdn_helper->is_cdn_active() || ! $cdn_helper->is_supported_url( $thumbnail_url ) || $cdn_helper->skip_image_url( $thumbnail_url ) ) { return false; } return $cdn_helper->generate_cdn_url( $thumbnail_url ); } private function set_cdn_url( $cdn_url ) { $this->cdn_url = $cdn_url; } public function get_fallback_url() { return $this->fallback_url; } private function set_fallback_url( $fallback_url ) { $this->fallback_url = $fallback_url; } public function get_url() { if ( $this->has_next_gen_url() ) { return $this->get_next_gen_url(); } $cdn_url = $this->get_cdn_url(); if ( $cdn_url ) { return $cdn_url; } return $this->get_fallback_url(); } public function has_next_gen_url() { if ( ! $this->should_use_next_gen_format() ) { return false; } return ! empty( $this->next_gen_url ); } private function should_use_next_gen_format() { return Next_Gen_Manager::get_instance()->is_active() || Settings::get_instance()->is_cdn_next_gen_conversion_active(); } public function get_aspect_ratio() { if ( ! $this->aspect_ratio ) { $this->aspect_ratio = $this->width && $this->height ? "{$this->width}/{$this->height}" : 'auto'; } return $this->aspect_ratio; } public function to_array() { return array( 'width' => $this->get_width(), 'height' => $this->get_height(), 'next_gen_url' => $this->get_next_gen_url(), 'cdn_url' => $this->get_cdn_url(), 'fallback_url' => $this->get_fallback_url(), ); } public function from_array( $array_values ) { $this->set_width( $this->get_array_value( $array_values, 'width' ) ); $this->set_height( $this->get_array_value( $array_values, 'height' ) ); $this->set_next_gen_url( $this->get_array_value( $array_values, 'next_gen_url' ) ); $this->set_cdn_url( $this->get_array_value( $array_values, 'cdn_url' ) ); $this->set_fallback_url( $this->get_array_value( $array_values, 'fallback_url' ) ); } private function get_array_value( $array_values, $key ) { return isset( $array_values[ $key ] ) ? $array_values[ $key ] : null; } } lazy-load/class-lazy-load-helper.php 0000644 00000014017 15252476777 0013456 0 ustar 00 <?php namespace Smush\Core\Lazy_Load; use Smush\Core\Array_Utils; use Smush\Core\Server_Utils; use Smush\Core\Settings; use Smush\Core\Urls_Exclusions; class Lazy_Load_Helper { private $settings; /** * @var array */ private $lazy_load_options; /** * @var Array_Utils */ private $array_utils; /** * @var Server_Utils */ private $server_utils; /** * Static instance * * @var self */ private static $instance; /** * Static instance getter */ public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } public function __construct() { $this->settings = Settings::get_instance(); $this->array_utils = new Array_Utils(); $this->server_utils = new Server_Utils(); } public function should_skip_lazyload() { return is_admin() || is_feed() || is_preview() || is_embed() || ! $this->settings->is_module_active( 'lazy_load' ) || $this->skip_lazy_load() || $this->is_excluded_uri() || $this->is_excluded_wp_location(); } /** * @return mixed|null */ private function skip_lazy_load() { /** * Internal filter to disable page parsing. * * Because the page parser module is universal, we need to make sure that all modules have the ability to skip * parsing of certain pages. For example, lazy loading should skip if_preview() pages. In order to achieve this * functionality, I've introduced this filter. Filter priority can be used to overwrite the $skip param. * * @param bool $skip Skip status. * * @since 3.2.2 * * Note: This is named weirdly, but we are keeping it like it is for backward compatibility. */ $skip_lazyload = apply_filters_deprecated( 'wp_smush_should_skip_parse', array( false ), '3.16.1', 'wp_smush_should_skip_lazy_load' ); return apply_filters( 'wp_smush_should_skip_lazy_load', $skip_lazyload ); } public function get_lazy_load_options() { if ( ! $this->lazy_load_options ) { $setting = $this->settings->get_setting( 'wp-smush-lazy_load' ); $this->lazy_load_options = array_merge( Settings::get_instance()->get_lazy_load_defaults(), $this->array_utils->ensure_array( $setting ) ); // Include the lazy_load toggle from main settings $main_settings = $this->settings->get(); if ( isset( $main_settings['lazy_load'] ) ) { $this->lazy_load_options['lazy_load'] = $main_settings['lazy_load']; } } return $this->lazy_load_options; } public function set_lazy_load_options( $options ) { $this->lazy_load_options = $options; } public function is_native_lazy_loading_enabled() { $options = $this->get_lazy_load_options(); return ! empty( $options['native'] ); } public function get_excluded_classes() { $exclude_classes = $this->array_utils->get_array_value( $this->get_lazy_load_options(), 'exclude-classes' ); return $this->array_utils->ensure_array( $exclude_classes ); } public function is_noscript_fallback_enabled() { $noscript_fallback = $this->array_utils->get_array_value( $this->get_lazy_load_options(), 'noscript_fallback' ); return ! empty( $noscript_fallback ); } private function get_excluded_pages() { $exclude_pages = $this->array_utils->get_array_value( $this->get_lazy_load_options(), 'exclude-pages' ); return array_filter( $this->array_utils->ensure_array( $exclude_pages ) ); } public function is_excluded_uri() { return ( new Urls_Exclusions() )->is_excluded_uri( $this->server_utils->get_request_uri(), $this->get_excluded_pages() ); } private function get_wp_location() { $blog_is_frontpage = ( 'posts' === get_option( 'show_on_front' ) && ! is_multisite() ) ? true : false; if ( is_front_page() ) { return 'frontpage'; } elseif ( is_home() && ! $blog_is_frontpage ) { return 'home'; } elseif ( is_page() ) { return 'page'; } elseif ( is_single() ) { return 'single'; } elseif ( is_category() ) { return 'category'; } elseif ( is_tag() ) { return 'tag'; } elseif ( is_archive() ) { return 'archive'; } else { return get_post_type(); } } private function get_included_locations() { $include = $this->array_utils->get_array_value( $this->get_lazy_load_options(), 'include' ); return $this->array_utils->ensure_array( $include ); } public function is_excluded_wp_location() { $included_locations = $this->get_included_locations(); if ( empty( $included_locations ) ) { // If not settings are set, probably, all are disabled. return true; } // Check if location is disabled. $wp_location = $this->get_wp_location(); return isset( $included_locations[ $wp_location ] ) && empty( $included_locations[ $wp_location ] ); } public function is_image_extension_supported( $ext, $src ) { if ( empty( $ext ) ) { return $this->is_image_without_extension_supported( $src ); } $ext = strtolower( $ext ); if ( ! in_array( $ext, array( 'jpg', 'jpeg', 'gif', 'png', 'svg', 'webp' ), true ) ) { return false; } return ! $this->is_format_excluded( $ext ); } private function is_image_without_extension_supported( $src ) { $pattern = '#(gravatar.com|googleusercontent.com)#is'; $pattern = apply_filters( 'wp_smush_without_extension_supported_regex', $pattern ); return preg_match( $pattern, $src ); } public function is_format_excluded( $needle ) { $supported_formats = $this->array_utils->get_array_value( $this->get_lazy_load_options(), 'format' ); $supported_formats = $this->array_utils->ensure_array( $supported_formats ); // Ensure 'jpeg' and 'jpg' are treated as the same format. if ( isset( $supported_formats['jpeg'] ) ) { $supported_formats['jpg'] = $supported_formats['jpeg']; } return in_array( false, $supported_formats, true ) && isset( $supported_formats[ $needle ] ) && ! $supported_formats[ $needle ]; } public function should_lazy_load_embed_video() { if ( $this->is_format_excluded( 'iframe' ) ) { return false; } $supported_formats = $this->array_utils->get_array_value( $this->get_lazy_load_options(), 'format' ); return ! empty( $supported_formats['embed_video'] ); } } lazy-load/class-lazy-load-controller.php 0000644 00000054431 15252476777 0014366 0 ustar 00 <?php /** * Lazy load images class: Lazy * * @since 3.2.0 * @package Smush\Core\Modules */ namespace Smush\Core\Lazy_Load; use Smush\Core\Array_Utils; use Smush\Core\Controller; use Smush\Core\Parser\Page_Parser; use Smush\Core\Server_Utils; use Smush\Core\Settings; if ( ! defined( 'WPINC' ) ) { die; } /** * Class Lazy */ class Lazy_Load_Controller extends Controller { private static $lazy_load_transform_priority = 20; /** * Module slug. * * @var string */ protected $slug = 'lazy_load'; /** * Lazy loading settings. * * @since 3.2.0 * @var array $settings */ private $options; /** * Excluded classes list. * * @since 3.6.2 * @var array */ private $excluded_classes = array( 'no-lazyload', // Internal class to skip images. 'skip-lazy', 'rev-slidebg', // Skip Revolution slider images. 'soliloquy-preload', // Soliloquy slider. ); /** * Static instance * * @var self */ private static $instance; /** * @var Settings */ private $settings; /** * @var Lazy_Load_Helper */ private $helper; private $array_utils; /** * Static instance getter */ public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Initialize module actions. * * @since 3.2.0 */ public function __construct() { $this->settings = Settings::get_instance(); $this->helper = Lazy_Load_Helper::get_instance(); $this->array_utils = new Array_Utils(); $this->register_action( 'wp_smush_content_transforms', array( $this, 'register_lazy_load_transform', ), self::$lazy_load_transform_priority ); // UI script data. $this->register_filter( 'wp_smush_localize_ui_script_data', array( $this, 'localize_lazy_load_script_data' ) ); // Hook into unified settings sync filter $this->register_filter( 'wp_smush_sync_settings', array( $this, 'handle_settings_sync' ), 10, 3 ); $this->register_action( 'wp_smush_lazy_load_updated', array( $this, 'reset_lazy_load_option_cache' ) ); // Only run on front end and if lazy loading is enabled. if ( is_admin() || ! $this->settings->is_module_active( 'lazy_load' ) ) { return; } $this->options = $this->helper->get_lazy_load_options(); // Disable WordPress native lazy load. $this->register_filter( 'wp_lazy_loading_enabled', array( $this, 'should_enable_wordpress_native_lazyload' ) ); $this->register_filter( 'wp_smush_transformed_page_markup', array( $this, 'add_has_smush_lazyload_video_class' ) ); // Load js file that is required in public facing pages. $this->register_action( 'wp_head', array( $this, 'add_inline_styles' ) ); $this->register_action( 'wp_head', array( $this, 'add_early_inline_styles' ), 5 ); $this->register_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ), 99 ); if ( defined( 'WP_SMUSH_ASYNC_LAZY' ) && WP_SMUSH_ASYNC_LAZY ) { $this->register_filter( 'script_loader_tag', array( $this, 'async_load' ), 10, 2 ); } // Allow lazy load attributes in img tag. $this->register_filter( 'wp_kses_allowed_html', array( $this, 'add_lazy_load_attributes' ) ); // Filter images. if ( ! isset( $this->options['output']['content'] ) || ! $this->options['output']['content'] ) { $this->register_filter( 'the_content', array( $this, 'exclude_from_lazy_loading' ), 100 ); } if ( ! isset( $this->options['output']['thumbnails'] ) || ! $this->options['output']['thumbnails'] ) { $this->register_filter( 'post_thumbnail_html', array( $this, 'exclude_from_lazy_loading' ), 100 ); } if ( ! isset( $this->options['output']['gravatars'] ) || ! $this->options['output']['gravatars'] ) { $this->register_filter( 'get_avatar', array( $this, 'exclude_from_lazy_loading' ), 100 ); } if ( ! isset( $this->options['output']['widgets'] ) || ! $this->options['output']['widgets'] ) { $this->register_action( 'dynamic_sidebar_before', array( $this, 'filter_sidebar_content_start' ), 0 ); $this->register_action( 'dynamic_sidebar_after', array( $this, 'filter_sidebar_content_end' ), 1000 ); } } /** * Reset lazy load options on update. */ public function reset_lazy_load_option_cache() { $this->helper->set_lazy_load_options( null ); } public function localize_lazy_load_script_data( $localize ) { if ( ! is_admin() ) { return $localize; } $lazy_load_options = $this->helper->get_lazy_load_options(); $localize['lazyloadSettings'] = Lazy_Load_Settings_DTO::to_react_props( $lazy_load_options ); $localize['metaData']['customPostTypes'] = $this->get_custom_post_types_for_ui(); return $localize; } /** * Get custom post types for UI. * * @return array<string|int, array{type: mixed, label: mixed}> */ private function get_custom_post_types_for_ui() { $custom_post_types = get_post_types( // custom post types. array( 'public' => true, '_builtin' => false, ), 'objects' ); $custom_post_types_data = array_map( function ( $post_type ) { return array( 'type' => $post_type->name, 'label' => $post_type->label, ); }, $custom_post_types ); return $custom_post_types_data; } public function add_early_inline_styles() { if ( $this->helper->should_skip_lazyload() ) { return; } ?> <style> .lazyload, .lazyloading { max-width: 100%; } </style> <?php } /** * Add inline styles at the top of the page for pre-loaders and effects. * * @since 3.2.0 */ public function add_inline_styles() { if ( $this->helper->should_skip_lazyload() ) { return; } // Lazy load embed video styles. $this->add_inline_embed_video_css(); // Fix for poorly coded themes that do not remove the no-js in the HTML class. ?> <script> document.documentElement.className = document.documentElement.className.replace('no-js', 'js'); </script> <?php if ( empty( $this->options['animation']['selected'] ) || 'none' === $this->options['animation']['selected'] ) { return; } $background_size = '16px auto !important'; // Spinner. if ( 'spinner' === $this->options['animation']['selected'] ) { $loader = WP_SMUSH_URL . 'app/assets/images/lazyloader-' . $this->options['animation']['spinner']['selected'] . '.svg'; if ( isset( $this->options['animation']['spinner']['selected'] ) && 3 < (int) $this->options['animation']['spinner']['selected'] ) { $loader = wp_get_attachment_image_src( $this->options['animation']['spinner']['selected'], 'full' ); if ( empty( $loader[0] ) ) { $loader = WP_SMUSH_URL . 'app/assets/images/lazyloader-1.svg'; } else { $loader = $loader[0]; } } $background = 'rgba(255, 255, 255, 0)'; } else { $background_size = 'max( 16px, min( var(--smush-placeholder-bg-max-width), 37.5% ) ) auto !important;'; // Placeholder. $loader = WP_SMUSH_URL . 'app/assets/images/placeholder.svg'; $background = '#F8F8F8'; if ( isset( $this->options['animation']['placeholder']['selected'] ) && 1 < (int) $this->options['animation']['placeholder']['selected'] ) { $loader = wp_get_attachment_image_src( (int) $this->options['animation']['placeholder']['selected'], 'full' ); // Can't find a loader on multisite? Try main site. if ( ! $loader && is_multisite() ) { switch_to_blog( 1 ); $loader = wp_get_attachment_image_src( (int) $this->options['animation']['placeholder']['selected'], 'full' ); restore_current_blog(); } if ( ! empty( $loader[0] ) ) { $loader = $loader[0]; } else { $loader = WP_SMUSH_URL . 'app/assets/images/placeholder.svg'; } } if ( isset( $this->options['animation']['placeholder']['color'] ) ) { $background = $this->options['animation']['placeholder']['color']; } } // Fade in. $fadein = isset( $this->options['animation']['fadein']['duration'] ) ? $this->options['animation']['fadein']['duration'] : 0; $delay = isset( $this->options['animation']['fadein']['delay'] ) ? $this->options['animation']['fadein']['delay'] : 0; ?> <style> .no-js img.lazyload { display: none; } figure.wp-block-image img.lazyloading { min-width: 150px; } .lazyload, .lazyloading { --smush-placeholder-width: 100px; --smush-placeholder-bg-max-width: 120px; --smush-placeholder-aspect-ratio: 1/1; width: var(--smush-image-width, var(--smush-placeholder-width)) !important; aspect-ratio: var(--smush-image-aspect-ratio, var(--smush-placeholder-aspect-ratio)) !important; } <?php if ( 'fadein' === $this->options['animation']['selected'] ) : ?> .lazyload, .lazyloading { opacity: 0; } .lazyloaded { opacity: 1; transition: opacity <?php echo esc_html( $fadein ); ?>ms; transition-delay: <?php echo esc_html( $delay ); ?>ms; } <?php else : ?> .lazyload { opacity: 0; } .lazyloading { border: 0 !important; opacity: 1; background: <?php echo esc_attr( $background ); ?> url('<?php echo esc_url( $loader ); ?>') no-repeat center !important; background-size: 16px auto !important; /* fallback for browsers without min/max */ background-size: <?php echo esc_attr( $background_size ); ?>; min-width: 16px; } <?php endif; ?> </style> <?php } private function add_inline_embed_video_css() { if ( ! $this->helper->should_lazy_load_embed_video() ) { return; } ?> <style> /* Thanks to https://github.com/paulirish/lite-youtube-embed and https://css-tricks.com/responsive-iframes/ */ .smush-lazyload-video { min-height: 240px; min-width: 320px; --smush-video-aspect-ratio: 16/9;background-color: #000;position: relative;display: block;contain: content;background-position: center center;background-size: cover;cursor: pointer; } .smush-lazyload-video.loading{cursor:progress} .smush-lazyload-video::before{content:'';display:block;position:absolute;top:0;background-image:linear-gradient(rgba(0,0,0,0.6),transparent);background-position:top;background-repeat:repeat-x;height:60px;width:100%;transition:all .2s cubic-bezier(0,0,0.2,1)} .smush-lazyload-video::after{content:"";display:block;padding-bottom:calc(100% / (var(--smush-video-aspect-ratio)))} .smush-lazyload-video > iframe{width:100%;height:100%;position:absolute;top:0;left:0;border:0;opacity:0;transition:opacity .5s ease-in} .smush-lazyload-video.smush-lazyloaded-video > iframe{opacity:1} .smush-lazyload-video > .smush-play-btn{z-index:10;position: absolute;top:0;left:0;bottom:0;right:0;} .smush-lazyload-video > .smush-play-btn > .smush-play-btn-inner{opacity:0.75;display:flex;align-items: center;width:68px;height:48px;position:absolute;cursor:pointer;transform:translate3d(-50%,-50%,0);top:50%;left:50%;z-index:1;background-repeat:no-repeat;background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 48"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="red"/><path d="M45 24 27 14v20" fill="white"/></svg>');filter:grayscale(100%);transition:filter .5s cubic-bezier(0,0,0.2,1), opacity .5s cubic-bezier(0,0,0.2,1);border:none} .smush-lazyload-video:hover .smush-play-btn-inner,.smush-lazyload-video .smush-play-btn-inner:focus{filter:none;opacity:1} .smush-lazyload-video > .smush-play-btn > .smush-play-btn-inner span{display:none;width:100%;text-align:center;} .smush-lazyload-video.smush-lazyloaded-video{cursor:unset} .smush-lazyload-video.video-loaded::before,.smush-lazyload-video.smush-lazyloaded-video > .smush-play-btn,.smush-lazyload-video.loading > .smush-play-btn{display:none;opacity:0;pointer-events:none} .smush-lazyload-video.smush-lazyload-vimeo > .smush-play-btn > .smush-play-btn-inner{background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 203 120' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m0.25116 9.0474c0-4.9968 4.0507-9.0474 9.0474-9.0474h184.4c4.997 0 9.048 4.0507 9.048 9.0474v101.91c0 4.996-4.051 9.047-9.048 9.047h-184.4c-4.9968 0-9.0474-4.051-9.0474-9.047v-101.91z' fill='%2317d5ff' fill-opacity='.7'/%3E%3Cpath d='m131.1 59.05c0.731 0.4223 0.731 1.4783 0 1.9006l-45.206 26.099c-0.7316 0.4223-1.646-0.1056-1.646-0.9504v-52.199c0-0.8448 0.9144-1.3727 1.646-0.9504l45.206 26.099z' fill='%23fff'/%3E%3C/svg%3E%0A");width:81px} <?php if ( get_theme_support( 'responsive-embeds' ) ) : ?> .wp-embed-responsive .wp-has-aspect-ratio .wp-block-embed__wrapper.has-smush-lazyload-video:before{padding-top:0!important;}.wp-embed-responsive .wp-embed-aspect-21-9 .smush-lazyload-video::after{padding-bottom:42.85%;}.wp-embed-responsive .wp-embed-aspect-18-9 .smush-lazyload-video::after{padding-bottom:50%;}.wp-embed-responsive .wp-embed-aspect-16-9 .smush-lazyload-video::after{padding-bottom:56.25%;}.wp-embed-responsive .wp-embed-aspect-4-3 .smush-lazyload-video::after{padding-bottom:75%;}.wp-embed-responsive .wp-embed-aspect-1-1 .smush-lazyload-video::after{padding-bottom:100%;}.wp-embed-responsive .wp-embed-aspect-9-16 .smush-lazyload-video::after{padding-bottom:177.77%;}.wp-embed-responsive .wp-embed-aspect-1-2 .smush-lazyload-video::after{padding-bottom:200%;} <?php endif; ?> </style> <?php } /** * Enqueue JS files required in public pages. * * @since 3.2.0 */ public function enqueue_assets() { if ( $this->helper->should_skip_lazyload() || ( $this->helper->is_native_lazy_loading_enabled() && ! $this->helper->should_lazy_load_embed_video() ) ) { return; } $script = WP_SMUSH_URL . 'app/assets/js/smush-lazy-load.min.js'; $in_footer = isset( $this->options['footer'] ) ? $this->options['footer'] : true; wp_enqueue_script( 'smush-lazy-load', $script, array(), WP_SMUSH_VERSION, $in_footer ); $lazy_load_script_options = apply_filters( 'smush_lazy_load_script_options', array( 'autoResizingEnabled' => $this->settings->is_auto_resizing_active(), 'autoResizeOptions' => array( 'precision' => 5, // 5px. 'skipAutoWidth' => true, // Whether to skip the image has 'auto' width. ), ) ); wp_add_inline_script( 'smush-lazy-load', 'var smushLazyLoadOptions = ' . wp_json_encode( $lazy_load_script_options ) . ';', 'before' ); $this->add_masonry_support(); if ( defined( 'WP_SMUSH_LAZY_LOAD_AVADA' ) && WP_SMUSH_LAZY_LOAD_AVADA ) { $this->add_avada_support(); } $this->add_divi_support(); $this->add_soliloquy_support(); } /** * Async load the lazy load scripts. * * @param string $tag The <script> tag for the enqueued script. * @param string $handle The script's registered handle. * * @return string * @since 3.7.0 */ public function async_load( $tag, $handle ) { if ( 'smush-lazy-load' === $handle ) { return str_replace( ' src', ' async="async" src', $tag ); } return $tag; } /** * Add support for plugins that use the masonry grid system (Block Gallery and CoBlocks plugins). * * @since 3.5.0 * * @see https://wordpress.org/plugins/coblocks/ * @see https://github.com/godaddy/block-gallery * @see https://masonry.desandro.com/methods.html#layout-masonry */ private function add_masonry_support() { if ( ! function_exists( 'has_block' ) ) { return; } // None of the supported blocks are active - exit. if ( ! has_block( 'blockgallery/masonry' ) && ! has_block( 'coblocks/gallery-masonry' ) ) { return; } $js = "var e = jQuery( '.wp-block-coblocks-gallery-masonry ul' );"; if ( has_block( 'blockgallery/masonry' ) ) { $js = "var e = jQuery( '.wp-block-blockgallery-masonry ul' );"; } $block_gallery_compat = "jQuery(document).on('lazyloaded', function(){{$js} if ('function' === typeof e.masonry) e.masonry();});"; wp_add_inline_script( 'smush-lazy-load', $block_gallery_compat ); } /** * Add fusion gallery support in Avada theme. * * @since 3.7.0 */ private function add_avada_support() { if ( ! defined( 'FUSION_BUILDER_VERSION' ) ) { return; } $js = "var e = jQuery( '.fusion-gallery' );"; $block_gallery_compat = "jQuery(document).on('lazyloaded', function(){{$js} if ('function' === typeof e.isotope) e.isotope();});"; wp_add_inline_script( 'smush-lazy-load', $block_gallery_compat ); } /** * Adds lazyload support to Divi & it's Waypoint library. * * @since 3.9.0 */ private function add_divi_support() { if ( ! defined( 'ET_BUILDER_THEME' ) || ! ET_BUILDER_THEME ) { return; } $script = "function rw() { Waypoint.refreshAll(); } window.addEventListener( 'lazybeforeunveil', rw, false); window.addEventListener( 'lazyloaded', rw, false);"; wp_add_inline_script( 'smush-lazy-load', $script ); } /** * Prevents the navigation from being missaligned in Soliloquy when lazy loading. * * @since 3.7.0 */ private function add_soliloquy_support() { if ( ! function_exists( 'soliloquy' ) ) { return; } $js = "var e = jQuery( '.soliloquy-image:not(.lazyloaded)' );"; $soliloquy = "jQuery(document).on('lazybeforeunveil', function(){{$js}e.each(function(){lazySizes.loader.unveil(this);});});"; wp_add_inline_script( 'smush-lazy-load', $soliloquy ); } /** * Make sure WordPress does not filter out img elements with lazy load attributes. * * @param array $allowedposttags Allowed post tags. * * @return mixed * @since 3.2.0 */ public function add_lazy_load_attributes( $allowedposttags ) { if ( ! isset( $allowedposttags['img'] ) ) { return $allowedposttags; } $smush_attributes = array( 'data-src' => true, 'data-srcset' => true, 'data-sizes' => true, ); $img_attributes = array_merge( $allowedposttags['img'], $smush_attributes ); $allowedposttags['img'] = $img_attributes; return $allowedposttags; } /** * Get images from content and add exclusion class. * * @param string $content Page/block content. * * @return string * @since 3.2.2 */ public function exclude_from_lazy_loading( $content ) { $server_utils = new Server_Utils(); $page = new Page_Parser( $server_utils->get_request_uri(), $content ); $parsed_page = $page->parse_page(); $images = $parsed_page->get_elements(); $composite_images = $parsed_page->get_composite_elements(); $all_image_elements = ! empty( $composite_images ) ? array_merge( $images, $this->extract_images_from_composite_elements( $composite_images ) ) : $images; if ( empty( $all_image_elements ) ) { return $content; } // Process all images. foreach ( $all_image_elements as $image ) { // Add .no-lazyload class. $image->append_attribute_value( 'class', 'no-lazyload' ); /** * Filters the no-lazyload image. * * @param string $text The image that can be filtered. * * @since 3.8.5 */ $new_markup = apply_filters( 'wp_smush_filter_no_lazyload_image', $image->get_updated_markup() ); $content = str_replace( $image->get_markup(), $new_markup, $content ); } return $content; } /** * Extracts individual image elements from composite elements. * * @param array $composite_elements Array of composite elements. * * @return array Array of individual image elements. * @since 3.18.0 */ private function extract_images_from_composite_elements( $composite_elements ) { $individual_images = array(); foreach ( $composite_elements as $composite_element ) { $element_images = $composite_element->get_elements(); if ( ! empty( $element_images ) ) { $individual_images = array_merge( $individual_images, $element_images ); } } return $individual_images; } /** * Buffer sidebar content. * * @since 3.2.0 */ public function filter_sidebar_content_start() { ob_start(); } /** * Process buffered content. * * @since 3.2.0 */ public function filter_sidebar_content_end() { $content = ob_get_clean(); echo $this->exclude_from_lazy_loading( $content ); unset( $content ); } public function register_lazy_load_transform( $transforms ) { $transforms['lazy_load'] = new Lazy_Load_Transform(); return $transforms; } public function should_enable_wordpress_native_lazyload() { return $this->helper->is_native_lazy_loading_enabled(); } public function add_has_smush_lazyload_video_class( $content ) { if ( ! $this->helper->should_lazy_load_embed_video() || ! get_theme_support( 'responsive-embeds' ) ) { return $content; } return preg_replace( '/<div class="wp-block-embed__wrapper">\s*<div class="lazyload smush-lazyload-video\b/', '<div class="wp-block-embed__wrapper has-smush-lazyload-video"><div class="lazyload smush-lazyload-video', $content ); } /** * Handle lazy load settings sync via unified endpoint. * * @param array|null $saved_settings Saved settings from previous filter, or null. * @param array $settings Incoming settings from React (camelCase). * @param string $context Context identifier. * * @return array|null Saved settings array if context matches, otherwise pass through. * * @since 3.25.0 */ public function handle_settings_sync( $saved_settings, $settings, $context ) { // Only handle lazyload context if ( 'lazyload' !== $context ) { return $saved_settings; } // Convert React camelCase to PHP format using DTO $db_settings = Lazy_Load_Settings_DTO::from_react_props( $settings ); // Get current settings $current_lazy_settings = $this->settings->get_setting( 'wp-smush-lazy_load', array() ); $current_lazy_settings = $this->array_utils->ensure_array( $current_lazy_settings ); $main_settings = $this->settings->get(); // Split settings: lazy_load goes to main settings, rest to lazy_load option $current_lazy_status = isset( $main_settings['lazy_load'] ) ? wp_validate_boolean( $main_settings['lazy_load'] ) : false; $new_lazy_load_status = isset( $db_settings['lazy_load'] ) ? wp_validate_boolean( $db_settings['lazy_load'] ) : $current_lazy_status; if ( $current_lazy_status !== $new_lazy_load_status ) { $this->settings->set( 'lazy_load', $new_lazy_load_status ); } unset( $db_settings['lazy_load'] ); // Merge and save remaining settings to wp-smush-lazy_load $updated_lazy_settings = array_merge( $current_lazy_settings, $db_settings ); $this->settings->set_setting( 'wp-smush-lazy_load', $updated_lazy_settings ); // Return transformed data (combine both for React) $combined_settings = array_merge( $updated_lazy_settings, array( 'lazy_load' => $new_lazy_load_status ) ); return Lazy_Load_Settings_DTO::to_react_props( $combined_settings ); } } class-installer.php 0000644 00000044227 15252476777 0010414 0 ustar 00 <?php /** * Smush installer (update/upgrade procedures): Installer class * * @package Smush\Core * @since 2.8.0 * * @author Anton Vanyukov <anton@incsub.com> * * @copyright (c) 2018, Incsub (http://incsub.com) */ namespace Smush\Core; use Smush\App\Abstract_Page; use Smush\Core\CDN\CDN_Controller; use Smush\Core\Smush\Smusher; use Smush\Core\Smush\Smusher_Options_Provider; use WP_Smush; if ( ! defined( 'WPINC' ) ) { die; } /** * Class Installer for handling updates and upgrades of the plugin. * * @since 2.8.0 */ class Installer { /** * Triggered on Smush deactivation. * * @since 3.1.0 */ public static function smush_deactivated() { if ( ! class_exists( '\\Smush\\Core\\Modules\\CDN_Controller' ) ) { $cdn_controller_path = __DIR__ . '/cdn/class-cdn-controller.php'; if ( file_exists( $cdn_controller_path ) ) { require_once $cdn_controller_path; } } Cron_Controller::get_instance()->unschedule_cron(); Settings::get_instance()->delete_setting( 'wp-smush-cdn_status' ); delete_site_option( 'wp_smush_api_auth' ); } /** * Redirect to Smush page after plugin activation if onboarding wizard has not been completed. * * @since 3.17.0 * * @param string $plugin Plugin basename. */ public static function redirect_to_setup_page( $plugin ) { // Check if this is the Smush plugin being activated. if ( WP_SMUSH_BASENAME !== $plugin ) { return; } // Check if onboarding wizard has been completed. $skip_quick_setup = ! empty( get_option( 'skip-smush-setup' ) ); if ( $skip_quick_setup ) { return; } // Don't redirect if activating multiple plugins. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['activate-multi'] ) ) { return; } // Don't redirect on AJAX, CLI, or network admin. if ( wp_doing_ajax() || ( defined( 'WP_CLI' ) && WP_CLI ) || is_network_admin() ) { return; } // Redirect to Smush page. wp_safe_redirect( admin_url( 'admin.php?page=smush' ) ); exit; } /** * Check if an existing install or new. * * @since 2.8.0 Moved to this class from wp-smush.php file. */ public static function smush_activated() { if ( ! defined( 'WP_SMUSH_ACTIVATING' ) ) { define( 'WP_SMUSH_ACTIVATING', true ); } $version = get_site_option( 'wp-smush-version' ); self::maybe_mark_as_pre_3_22_site( $version ); // Cache activated date time. $event_name = ! empty( $version ) ? 'plugin_activated' : 'plugin_installed'; self::cache_event_time( $event_name ); if ( ! class_exists( '\\Smush\\Core\\Settings' ) ) { require_once __DIR__ . '/class-settings.php'; } Settings::get_instance()->initial_default_site_settings(); // If the version is not saved or if the version is not same as the current version,. if ( ! $version || WP_SMUSH_VERSION !== $version ) { global $wpdb; // Check if there are any existing smush stats. $results = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE meta_key=%s LIMIT 1", 'wp-smpro-smush-data' ) ); // db call ok; no-cache ok. if ( $results || $version ) { update_site_option( 'wp-smush-install-type', 'existing' ); } // Create directory smush table. self::directory_smush_table(); // Store the plugin version in db. update_site_option( 'wp-smush-version', WP_SMUSH_VERSION ); } } /** * Handle plugin upgrades. * * @since 2.8.0 */ public static function upgrade_settings() { // Avoid executing this over an over in same thread. if ( defined( 'WP_SMUSH_ACTIVATING' ) || ( defined( 'WP_SMUSH_UPGRADING' ) && WP_SMUSH_UPGRADING ) ) { return; } if ( ! class_exists( '\\Smush\\Core\\Settings' ) ) { require_once __DIR__ . '/class-settings.php'; } $version = get_site_option( 'wp-smush-version' ); if ( false === $version ) { self::smush_activated(); } else { self::maybe_mark_as_pre_3_22_site( $version ); } if ( false !== $version && WP_SMUSH_VERSION !== $version ) { if ( ! defined( 'WP_SMUSH_UPGRADING' ) ) { define( 'WP_SMUSH_UPGRADING', true ); } // Cache last updated time. self::cache_event_time( 'plugin_upgraded' ); if ( version_compare( $version, '3.7.0', '<' ) ) { self::upgrade_3_7_0(); } if ( version_compare( $version, '3.8.0', '<' ) ) { // Delete the flag for hiding the BF modal because it was removed. delete_site_option( 'wp-smush-hide_blackfriday_modal' ); } if ( version_compare( $version, '3.8.3', '<' ) ) { // Delete this unused setting, leftover from old smush. delete_option( 'wp-smush-transparent_png' ); } if ( version_compare( $version, '3.9.5', '<' ) ) { delete_site_option( 'wp-smush-show-black-friday' ); } if ( version_compare( $version, '3.9.10', '<' ) ) { self::dir_smush_set_primary_key(); } if ( version_compare( $version, '3.10.0', '<' ) ) { self::upgrade_3_10_0(); } if ( version_compare( $version, '3.10.3', '<' ) ) { self::upgrade_3_10_3(); } if ( version_compare( $version, '3.16.0', '<' ) ) { self::regenerate_preset_configs_before_3_16_0(); } elseif ( version_compare( $version, '3.21.0', '<' ) ) { self::regenerate_preset_configs(); } if ( version_compare( $version, '3.21.0', '<' ) ) { self::upgrade_3_21_0(); } if ( version_compare( $version, '4.0', '<' ) ) { self::upgrade_4_0_0(); } if ( version_compare( $version, '4.2.0', '<' ) ) { self::upgrade_4_2_0(); } if ( version_compare( $version, '4.0', '<' ) ) { $hide_new_feature_highlight_modal = apply_filters( 'wpmudev_branding_hide_doc_link', false ); if ( ! $hide_new_feature_highlight_modal ) { // Add the flag to display the new feature background process modal. add_site_option( 'wp-smush-show_upgrade_modal', true ); } // Show new feature hotspot. // self::set_new_feature_hotspot_flag(); } // Create/upgrade directory smush table. self::directory_smush_table(); // Store the latest plugin version in db. update_site_option( 'wp-smush-version', WP_SMUSH_VERSION ); self::reset_smusher_error_counts(); } } /** * Create or upgrade custom table for directory Smush. * * After creating or upgrading the custom table, update the path_hash * column value and structure if upgrading from old version. * * @since 2.9.0 */ public static function directory_smush_table() { if ( ! class_exists( '\\Smush\\Core\\Modules\\Abstract_Module' ) ) { require_once __DIR__ . '/modules/class-abstract-module.php'; } if ( ! class_exists( '\\Smush\\Core\\Modules\\Dir' ) ) { require_once __DIR__ . '/modules/class-dir.php'; } // No need to continue on sub sites. if ( ! Modules\Dir::should_continue() ) { return; } // Create a class object, if doesn't exists. if ( ! is_object( WP_Smush::get_instance()->core()->mod->dir ) ) { WP_Smush::get_instance()->core()->mod->dir = new Modules\Dir(); } // Create/upgrade directory smush table. WP_Smush::get_instance()->core()->mod->dir->create_table(); } /** * Set primary key for directory smush table on upgrade to 3.9.10. * * @since 3.9.10 */ private static function dir_smush_set_primary_key() { global $wpdb; // Only call it after creating table smush_dir_images. If the table doesn't exist, returns. if ( ! Modules\Dir::table_exist() ) { return; } // If the table is already set the primary key, return. if ( $wpdb->query( $wpdb->prepare( "SHOW INDEXES FROM {$wpdb->base_prefix}smush_dir_images WHERE Key_name = %s;", 'PRIMARY' ) ) ) { return; } // Set column ID as a primary key. $wpdb->query( "ALTER TABLE {$wpdb->base_prefix}smush_dir_images ADD PRIMARY KEY (id);" ); } /** * Check if table needs to be created and create if not exists. * * @since 3.8.6 */ public static function maybe_create_table() { if ( ! function_exists( 'get_current_screen' ) ) { return; } if ( isset( get_current_screen()->id ) && false === strpos( get_current_screen()->id, 'page_smush' ) ) { return; } self::directory_smush_table(); } /** * Upgrade to 3.7.0 * * @since 3.7.0 */ private static function upgrade_3_7_0() { delete_site_option( 'wp-smush-run_recheck' ); // Fix the "None" animation in lazy-load options. $lazy = Settings::get_instance()->get_setting( 'wp-smush-lazy_load' ); if ( ! $lazy || ! isset( $lazy['animation'] ) || ! isset( $lazy['animation']['selected'] ) ) { return; } if ( '0' === $lazy['animation']['selected'] ) { $lazy['animation']['selected'] = 'none'; Settings::get_instance()->set_setting( 'wp-smush-lazy_load', $lazy ); } } /** * Upgrade to 3.10.0 * * @return void * @since 3.10.0 */ private static function upgrade_3_10_0() { // Remove unused options. delete_site_option( 'wp-smush-hide_pagespeed_suggestion' ); delete_site_option( 'wp-smush-hide_upgrade_notice' ); // Rename the default config. $stored_configs = get_site_option( 'wp-smush-preset_configs', false ); if ( is_array( $stored_configs ) && isset( $stored_configs[0] ) && isset( $stored_configs[0]['name'] ) && 'Basic config' === $stored_configs[0]['name'] ) { $stored_configs[0]['name'] = __( 'Smush', 'wp-smushit' ); update_site_option( 'wp-smush-preset_configs', $stored_configs ); } } /** * Upgrade to 4.0.0 * * @return void * @since 4.0.0 */ private static function upgrade_4_0_0() { $settings = Settings::get_instance(); $lazy_options = $settings->get_setting( 'wp-smush-lazy_load' ); if ( ! is_array( $lazy_options ) ) { return; } $changed = self::migrate_lazy_load_placeholder_in_options( $lazy_options ); $changed = self::migrate_lazy_load_spinner_in_options( $lazy_options ) || $changed; if ( $changed ) { $settings->set_setting( 'wp-smush-lazy_load', $lazy_options ); } } /** * Upgrade to 4.2.0 * * Migrates options that moved from PHP-serialized arrays / comma-separated * strings to raw JSON strings so that the new JSON_Record / JSON_Scalar_Array * classes can read them without a full reset. * * @return void * @since 4.2.0 */ private static function upgrade_4_2_0() { // Global_Stats option: PHP-serialized array → JSON object. self::migrate_serialized_option_to_json( 'wp_smush_global_stats', 'wp_smush_global_stats_json' ); // Attachment_Id_List options: comma-separated string → JSON array. $attachment_id_list_options = array( 'wp-smush-optimize-list', 'wp-smush-reoptimize-list', 'wp-smush-error-items-list', 'wp-smush-ignored-items-list', 'wp-smush-animated-items-list', ); foreach ( $attachment_id_list_options as $option_id ) { self::migrate_comma_separated_option_to_json_array( $option_id, $option_id . '-json' ); } } /** * Read an option whose value was written by update_option() as a * PHP-serialized array and re-save it as a raw JSON string. * * @param string $option_id WP option name. * @param $new_option_id * * @return void */ private static function migrate_serialized_option_to_json( $option_id, $new_option_id ) { $value = get_option( $option_id, null ); // get_option() calls maybe_unserialize; PHP-serialized array → array. if ( null === $value || ! is_array( $value ) ) { return; } update_option( $new_option_id, wp_json_encode( $value ), false ); } /** * Read an option whose value was written as a comma-separated string of * attachment IDs (e.g. "123,456,789") and re-save it as a JSON array * (e.g. [123,456,789]) so that JSON_Scalar_Array can read it. * * @param string $option_id WP option name. * @param $new_option_id * * @return void */ private static function migrate_comma_separated_option_to_json_array( $option_id, $new_option_id ) { $value = get_option( $option_id, null ); if ( null === $value ) { return; } // If get_option() already returned an array (e.g. PHP-serialized), encode directly. if ( is_array( $value ) ) { $ids = array_values( array_map( 'intval', $value ) ); update_option( $new_option_id, wp_json_encode( $ids ), false ); return; } $str = (string) $value; // Skip if value is already a valid JSON array. if ( '' !== $str && '[' === $str[0] ) { return; } if ( '' === trim( $str ) ) { update_option( $new_option_id, '[]', false ); return; } // Convert "123,456,789" → [123,456,789]. $ids = array_values( array_filter( array_map( 'intval', explode( ',', $str ) ) ) ); update_option( $new_option_id, wp_json_encode( $ids ), false ); } /** * Migrate lazy load placeholder settings. * * @param array $lazy_options Lazy load options. * * @return bool True when settings were changed. */ private static function migrate_lazy_load_placeholder_in_options( &$lazy_options ) { $selected_placeholder = isset( $lazy_options['animation']['placeholder']['selected'] ) ? (int) $lazy_options['animation']['placeholder']['selected'] : 1; if ( empty( $selected_placeholder ) || 2 !== $selected_placeholder ) { return false; } // Update lazy load settings. $lazy_options['animation']['placeholder']['selected'] = 1; return true; } /** * Migrate lazy load spinner settings. * * @param array $lazy_options Lazy load options. * * @return bool True when settings were changed. */ private static function migrate_lazy_load_spinner_in_options( &$lazy_options ) { $selected_spinner = isset( $lazy_options['animation']['spinner']['selected'] ) ? (int) $lazy_options['animation']['spinner']['selected'] : 1; if ( empty( $selected_spinner ) || $selected_spinner > 5 ) { return false; } $default_spinner = 1; $map_spinner = array( 1 => 2, 3 => 3, ); // Map the selected spinner to the new value. $selected_spinner = isset( $map_spinner[ $selected_spinner ] ) ? $map_spinner[ $selected_spinner ] : $default_spinner; // Update lazy load settings. $lazy_options['animation']['spinner']['selected'] = $selected_spinner; return true; } /** * Upgrade 3.10.3 * * @return void * @since 3.10.3 */ private static function upgrade_3_10_3() { delete_site_option( 'wp-smush-hide_smush_welcome' ); // Logger options. delete_site_option( 'wdev_logger_wp-smush-pro' ); delete_site_option( 'wdev_logger_wp-smushit' ); // Clean old cronjob (missing callback). if ( wp_next_scheduled( 'wdev_logger_clear_logs' ) ) { wp_clear_scheduled_hook( 'wdev_logger_clear_logs' ); } } private static function maybe_mark_as_pre_3_22_site( $version ) { if ( ! $version || false !== get_site_option( 'wp_smush_pre_3_22_site' ) ) { return; } if ( version_compare( $version, '3.21.1', '>' ) ) { $version = 0; } update_site_option( 'wp_smush_pre_3_22_site', $version ); } private static function regenerate_preset_configs_before_3_16_0() { // Update Smush mode for display on Configs page. $stored_configs = get_site_option( 'wp-smush-preset_configs', array() ); if ( empty( $stored_configs ) || ! is_array( $stored_configs ) ) { return; } $configs_handler = Configs::get_instance(); $new_settings = array( 'background_email' => false, ); foreach ( $stored_configs as $key => $preset_config ) { if ( empty( $preset_config['config']['configs']['settings'] ) ) { continue; } $preset_config ['config']['configs']['settings'] = array_merge( $new_settings, $preset_config['config']['configs']['settings'] ); $preset_config ['config'] = $configs_handler->sanitize_and_format_configs( $preset_config['config']['configs'] ); $stored_configs[ $key ] = $preset_config; } update_site_option( 'wp-smush-preset_configs', $stored_configs ); } private static function regenerate_preset_configs() { // Regenerate preset configs to update Next-Gen Formats. $stored_configs = get_site_option( 'wp-smush-preset_configs', array() ); if ( empty( $stored_configs ) || ! is_array( $stored_configs ) ) { return; } $configs_handler = Configs::get_instance(); foreach ( $stored_configs as $key => $preset_config ) { if ( empty( $preset_config['config']['configs'] ) ) { continue; } $preset_config ['config'] = $configs_handler->sanitize_and_format_configs( $preset_config['config']['configs'] ); $stored_configs[ $key ] = $preset_config; } update_site_option( 'wp-smush-preset_configs', $stored_configs ); } private static function upgrade_3_21_0() { self::migrate_auto_resize_to_new_settings(); self::migrate_auto_resize_to_new_settings_for_sub_sites(); } private static function migrate_auto_resize_to_new_settings_for_sub_sites() { if ( ! is_multisite() ) { return; } self::for_each_public_site( function() { self::migrate_auto_resize_to_new_settings(); } ); } private static function migrate_auto_resize_to_new_settings() { $settings = Settings::get_instance(); $is_auto_resizing_active = $settings->get( 'auto_resize' ); if ( ! $is_auto_resizing_active ) { return; } $settings->set( 'auto_resizing', $is_auto_resizing_active ); $settings->set( 'cdn_dynamic_sizes', $is_auto_resizing_active ); $settings->delete( 'auto_resize' ); } private static function cache_event_time( $event ) { $option_key = 'wp_smush_event_times'; $event_times = get_site_option( $option_key, array() ); $event_times[ $event ] = time(); update_site_option( $option_key, $event_times ); } /** * @return void */ private static function reset_smusher_error_counts() { $smusher_options = ( new Smusher_Options_Provider() )->get_options(); ( new Smusher( $smusher_options ) )->reset_error_counts(); } private static function set_new_feature_hotspot_flag() { add_option( 'wp-smush-show-new-feature-hotspot', true ); self::set_new_feature_hotspot_flag_for_sub_sites(); } private static function set_new_feature_hotspot_flag_for_sub_sites() { if ( ! is_multisite() ) { return; } self::for_each_public_site( function() { add_option( 'wp-smush-show-new-feature-hotspot', true ); } ); } private static function for_each_public_site( $callback ) { if ( ! is_multisite() ) { return; } $site_args = array( 'fields' => 'ids', 'public' => 1, 'number' => 250, // Limit to 250 sites to avoid performance issues. ); $site_ids = get_sites( $site_args ); if ( empty( $site_ids ) ) { return; } foreach ( $site_ids as $site_id ) { switch_to_blog( $site_id ); call_user_func( $callback ); restore_current_blog(); } } } class-modules.php 0000644 00000012445 15252476777 0010064 0 ustar 00 <?php /** * Class Modules. * * Used in Core to type hint the $mod variable. For example, this way any calls to * \Smush\WP_Smush::get_instance()->core()->mod->settings will be typehinted as a call to Settings module. * * @package Smush\Core */ namespace Smush\Core; use Smush\Core\Background\Background_Pre_Flight_Controller; use Smush\Core\Backups\Backups_Controller; use Smush\Core\Cache\Cache_Controller; use Smush\Core\Frontend\Frontend_Controller; use Smush\Core\Frontend\Multisite_Frontend_Controller; use Smush\Core\Lazy_Load\Lazy_Load_Controller; use Smush\Core\Lazy_Load\Video_Embed\Video_Thumbnail_Controller; use Smush\Core\Media\Attachment_Url_Cache_Controller; use Smush\Core\Media\Media_Item_Controller; use Smush\Core\Media_Library\Ajax_Media_Library_Scanner; use Smush\Core\Media_Library\Background_Media_Library_Scanner; use Smush\Core\Media_Library\Media_Library_Last_Process; use Smush\Core\Media_Library\Media_Library_Slice_Data_Fetcher; use Smush\Core\Media_Library\Media_Library_Watcher; use Smush\Core\Modules\CDN; use Smush\Core\Photon\Photon_Controller; use Smush\Core\Rating_Notification\Rating_Notification_Controller; use Smush\Core\Resize\Resize_Controller; use Smush\Core\Security\Security_Controller; use Smush\Core\Smush\Smush_Controller; use Smush\Core\Stats\Global_Stats_Controller; use Smush\Core\Transform\Transformation_Controller; use Smush\Core\Png2Jpg\Png2Jpg_Controller; if ( ! defined( 'WPINC' ) ) { die; } /** * Class Modules */ class Modules { /** * Directory Smush module. * * @var Modules\Dir */ public $dir; /** * Main Smush module. * * @var Modules\Smush */ public $smush; /** * Cache background optimization controller - Bulk_Smush_Controller * * @var \Smush\Core\Bulk\Background_Bulk_Smush_Controller */ public $bg_optimization; /** * @var Product_Analytics\Product_Analytics_Controller */ public $product_analytics; public $backward_compatibility; public static function get_instance() { return new self(); } /** * Modules constructor. */ public function __construct() { new Deprecated_Hooks();// Handle deprecated hooks. new Api\Hub(); // Init hub endpoints. new Rest(); if ( is_admin() ) { $this->dir = new Modules\Dir(); } $this->smush = $this->get_smush_module(); $transformation_controller = new Transformation_Controller(); $transformation_controller->init(); $this->product_analytics = Product_Analytics\Product_Analytics_Controller::get_instance(); $png2jpg_controller = Png2Jpg_Controller::get_instance(); $png2jpg_controller->init(); $this->bg_optimization = Bulk\Background_Bulk_Smush_Controller::get_instance(); $smush_controller = Smush_Controller::get_instance(); $smush_controller->init(); $resize_controller = new Resize_Controller(); $resize_controller->init(); $backups_controller = new Backups_Controller(); $backups_controller->init(); $library_scanner = new Ajax_Media_Library_Scanner(); $library_scanner->init(); $background_lib_scanner = Background_Media_Library_Scanner::get_instance(); $background_lib_scanner->init(); $media_library_watcher = new Media_Library_Watcher(); $media_library_watcher->init(); $global_stats_controller = Global_Stats_Controller::get_instance(); $global_stats_controller->init(); $plugin_settings_watcher = new Plugin_Settings_Watcher(); $plugin_settings_watcher->init(); $animated_status_controller = new Animated_Status_Controller(); $animated_status_controller->init(); $media_library_slice_data_fetcher = new Media_Library_Slice_Data_Fetcher( is_multisite(), get_current_blog_id() ); $media_library_slice_data_fetcher->init(); $media_item_controller = new Media_Item_Controller(); $media_item_controller->init(); $optimization_controller = Optimization_Controller::get_instance(); $optimization_controller->init(); $photon_controller = new Photon_Controller(); $photon_controller->init(); $cache_controller = new Cache_Controller(); $cache_controller->init(); $lazy_load_controller = Lazy_Load_Controller::get_instance(); $lazy_load_controller->init(); ( new Video_Thumbnail_Controller() )->init(); $background_health = Background_Pre_Flight_Controller::get_instance(); $background_health->init(); $media_lib_last_process = Media_Library_Last_Process::get_instance(); $media_lib_last_process->init(); $cron_controller = Cron_Controller::get_instance(); $cron_controller->init(); $security_controller = Security_Controller::get_instance(); $security_controller->init(); $attachment_url_cache_controller = new Attachment_Url_Cache_Controller(); $attachment_url_cache_controller->init(); $hub_connector = new Hub_Connector(); $hub_connector->init(); $frontend_controller = is_multisite() ? Multisite_Frontend_Controller::get_instance() : Frontend_Controller::get_instance(); $frontend_controller->init(); $settings_controller = new Settings_Controller(); $settings_controller->init(); $rating_notification_controller = new Rating_Notification_Controller(); $rating_notification_controller->init(); $activity_log_controller = Activity_Log_Controller::get_instance(); $activity_log_controller->init(); $configs_controller = Configs_Controller::get_instance(); $configs_controller->init(); } protected function get_smush_module() { return new Modules\Smush(); } } class-hub-connector.php 0000644 00000101337 15252476777 0011161 0 ustar 00 <?php /** * Hub_Connector class. * * @package Smush */ namespace Smush\Core; use Smush\Core\Membership\Membership; use WPMUDEV\Hub\Connector\API; use WPMUDEV\Hub\Connector\Data; use WPMUDEV_Dashboard; use WPMUDEV\Hub\Connector; use WP_Error; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class Hub_Connector * * Handles Hub connection functionality for the Smush plugin. */ class Hub_Connector extends Controller { /** * The identifier for the Smush plugin in the Hub. * * @const string */ private static $plugin_identifier = 'smush'; /** * The action name used for the Hub connection. * * @const string */ private static $connection_action = 'hub_connection'; /** * Valid screens for the Hub Connector. * * @var array */ private static array $valid_screens = array( 'smush_page_smush-bulk', 'smush-pro_page_smush-bulk', 'smush_page_smush-bulk-network', 'smush-pro_page_smush-bulk-network', ); /** * Array utilities instance. * * @var Array_Utils */ private $array_utils; /** * Hub_Connector constructor. * * Private constructor to enforce singleton pattern. */ public function __construct() { $this->initialize(); $this->array_utils = new Array_Utils(); $this->register_action( 'wpmudev_hub_connector_first_sync_completed', array( $this, 'sync_after_connect' ) ); $this->register_filter( 'wp_smush_modals', array( $this, 'register_hub_connection_success_modal' ) ); $this->register_filter( 'pre_site_option_wp-smush-networkwide', array( $this, 'disable_subsite_controls_for_unconnected_free_users' ) ); $this->register_action( 'wp_smush_render_general_setting_rows', array( $this, 'render_hub_connector_actions' ), 30 ); $this->register_filter( 'wp_smush_localize_script_messages', array( $this, 'add_site_disconnected_success_message' ) ); $this->register_action( 'wp_ajax_wp_smush_disconnect_site', array( $this, 'ajax_disconnect_site' ) ); $this->register_action( 'wp_ajax_wp_smush_check_hub_sync_status', array( $this, 'ajax_check_hub_sync_status' ) ); $this->register_filter( 'wp_smush_localize_ui_script_data', array( $this, 'localized_data_for_ui' ) ); if ( ! self::is_connection_flow() ) { return; } $this->register_action( 'admin_body_class', array( $this, 'admin_body_class' ), 11 ); $this->register_action( 'wpmudev_hub_connector_localize_text_vars', array( $this, 'customize_text_vars' ), 10, 2 ); $this->register_filter( 'wpmudev_hub_connector_localize_vars', array( $this, 'add_hub_connector_data' ), 10, 2 ); } public function localized_data_for_ui( $data ) { $dismissed_notices = get_option( 'wp-smush-dismissed-notices', array() ); $permission_level = is_multisite() ? 'manage_network' : 'manage_options'; // TODO: Maybe remove hubConnector if site is already connected. $data['hubConnector'] = array( 'is_available' => current_user_can( $permission_level ) && self::is_hub_connector_available(), 'is_syncing' => self::is_syncing(), // 'is_team_selection' => false, 'has_access' => current_user_can( $permission_level ), 'is_logged_in' => Membership::get_instance()->has_access_to_hub(), 'is_free_account_connected' => self::is_logged_in() && ! self::is_wpmudev_dashboard_connected(), 'should_redirect_to_dashboard' => self::should_redirect_to_dashboard(), // Not being used for literal output / DB insert. // phpcs:disable WordPress.Security.NonceVerification.Recommended 'current_tab' => isset( $_GET['hub_connector_callback'] ) ? 'login' : 'register', 'login_auth_url' => self::get_hub_site_login_auth_url(), 'hub_auth_url' => self::get_hub_google_login_url(), 'hub_signup_url' => self::get_hub_register_url(), 'redirect_url' => self::get_connect_site_url( 'smush' ), 'forgot_password_url' => $this->get_hub_forgot_password_url(), 'domain' => self::get_site_domain(), 'auth_nonce' => wp_create_nonce( 'auth_nonce' ), 'has_login_error' => self::has_error_in_login(), 'login_error_message' => self::get_auth_error(), 'hide_onboarding' => isset( $_GET['page_action'] ) && 'hub_connection' === sanitize_text_field( wp_unslash( $_GET['page_action'] ) ) && ! ( boolval( self::has_error_in_login() ) ), 'info_modal_dismissed' => ! empty( $dismissed_notices['hub_connect_info_modal'] ), 'profile_data' => $this->get_profile_data_for_ui(), ); return $data; } /** * Get profile data for UI. * * @return array Profile data. */ private function get_profile_data_for_ui() { $profile_data = $this->get_profile_data(); // Fallback to WP user data. if ( is_wp_error( $profile_data ) ) { return null; } $display_name = $this->array_utils->get_array_value( $profile_data, 'name' ); $user_name = $this->array_utils->get_array_value( $profile_data, 'user_name' ); $avatar = $this->array_utils->get_array_value( $profile_data, 'avatar' ); return array( 'initials' => $this->get_display_name_initial( $display_name ), 'avatar' => $avatar, 'profileBackgroundColor' => $avatar ? '#f8f8f8' : '#0059ff', 'profileFontColor' => '#ffffff', 'userName' => $user_name, 'email' => is_email( $user_name ) ? $user_name : '', 'displayName' => $display_name, ); } /** * Get the initial (first letter) from a display name. * * @param string $display_name The display name. * @return string Uppercase initial or empty string. */ protected function get_display_name_initial( $display_name ) { if ( empty( $display_name ) ) { return ''; } return strtoupper( substr( $display_name, 0, 1 ) ); } /** * Initialize the Hub Connector module and set its options. * * @return void */ private function initialize() { $this->load_hub_connector_library(); $this->configure_hub_connector(); } /** * Add Hub Connector specific classes to admin body. * * @param string $classes Existing CSS classes. * @return string Modified CSS classes. */ public function admin_body_class( $classes ) { if ( ! self::is_valid_screen() || self::is_logged_in() ) { return $classes; } $sui_version = $this->get_sui_version(); if ( ! empty( $sui_version ) ) { $classes .= ' ' . esc_attr( $sui_version ); } return $classes; } /** * Load the Hub Connector library. * * @return void * @throws \RuntimeException If library file doesn't exist. */ private function load_hub_connector_library() { $hub_connector_lib = WP_SMUSH_DIR . 'core/external/hub-connector/connector.php'; if ( ! file_exists( $hub_connector_lib ) ) { wp_die( esc_html__( 'Required library is missing. Please reinstall the plugin.', 'wp-smushit' ), esc_html__( 'Library Error', 'wp-smushit' ), array( 'response' => 500, 'back_link' => true, ) ); } require_once $hub_connector_lib; } /** * Configure Hub Connector options. * * @return void */ private function configure_hub_connector() { if ( ! class_exists( '\WPMUDEV\Hub\Connector' ) ) { return; } $options = array( 'screens' => self::$valid_screens, ); Connector::get()->set_options( self::$plugin_identifier, $options ); } /** * Get SUI version constant. * * @return string */ private function get_sui_version() { return defined( 'WPMUDEV_HUB_CONNECTOR_SUI_VERSION' ) ? WPMUDEV_HUB_CONNECTOR_SUI_VERSION : ''; } /** * Check if current screen is valid for Hub Connector. * * @return bool */ private static function is_valid_screen() { $current_screen = get_current_screen(); if ( ! $current_screen || ! isset( $current_screen->id ) ) { return false; } return in_array( $current_screen->id, self::$valid_screens, true ); } /** * Render the Hub Connector page. * * @return void */ public static function render() { do_action( 'wpmudev_hub_connector_ui', self::$plugin_identifier ); } /** * Checks if the current request is a Hub Connection flow. * * @return bool */ public static function is_connection_flow() { $action = self::get_sanitized_input( 'page_action' ); return ! empty( $action ) && self::$connection_action === $action; } /** * Checks if Hub Connector grants access to the page. * * @return bool */ public static function has_access() { return self::is_hub_connector_available() && self::is_logged_in(); } /** * Checks if Hub Connector is available. * * @return bool */ private static function is_hub_connector_available() { return class_exists( '\WPMUDEV\Hub\Connector' ); } /** * Checks if Hub Connector is logged in. * * @return bool */ public static function is_logged_in() { if ( ! class_exists( '\WPMUDEV\Hub\Connector\API' ) ) { return false; } $api = API::get(); return $api && method_exists( $api, 'is_logged_in' ) && $api->is_logged_in(); } /** * Sync site data with Hub. * * @return bool|WP_Error */ private function sync() { if ( ! class_exists( '\WPMUDEV\Hub\Connector\API' ) ) { return false; } $api = API::get(); if ( $api && method_exists( $api, 'sync_site' ) ) { $sync = $api->sync_site(); if ( is_wp_error( $sync ) ) { return $sync; } } return true; } /** * Disconnect site from Hub. * * @return bool */ public static function disconnect() { if ( ! class_exists( '\WPMUDEV\Hub\Connector\API' ) ) { return false; } $api = API::get(); return $api && method_exists( $api, 'logout' ) && $api->logout(); } /** * Get connection URL for Hub. * * @param string $target_page The target page to connect to. * @param string $utm_campaign The UTM campaign to append to the URL. * * @return string The connection URL. */ public static function get_connect_site_url( $target_page = 'smush', $utm_campaign = '' ) { $args = array(); if ( self::should_redirect_to_dashboard() ) { $args['page'] = 'wpmudev'; } else { $args = self::get_connection_args( $target_page ); } if ( ! empty( $utm_campaign ) ) { $args['utm_campaign'] = sanitize_text_field( $utm_campaign ); } $admin_url = self::get_admin_url(); return add_query_arg( $args, $admin_url ); } /** * Check if should redirect to WPMUDEV Dashboard. * * @return bool */ private static function should_redirect_to_dashboard() { return ! self::is_wpmudev_dashboard_connected() && class_exists( 'WPMUDEV_Dashboard' ); } /** * Get connection arguments for URL. * * @param string $target_page The target page. * @return array */ private static function get_connection_args( $target_page ) { return array( 'page' => sanitize_text_field( $target_page ), '_wpnonce' => wp_create_nonce( self::$connection_action ), 'page_action' => self::$connection_action, 'hub_connector_callback' => 1, ); } /** * Get appropriate admin URL. * * @return string */ private static function get_admin_url() { return is_network_admin() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ); } /** * Check if WPMUDEV Dashboard is connected. * * @return bool */ public static function is_wpmudev_dashboard_connected() { if ( ! class_exists( 'WPMUDEV_Dashboard' ) ) { return false; } $dashboard_api = WPMUDEV_Dashboard::$api ?? null; return is_object( $dashboard_api ) && method_exists( $dashboard_api, 'has_key' ) && $dashboard_api->has_key(); } /** * Checks if the Hub connector should render its UI. * * Verifies the nonce and login status to determine if the Hub connector should render its UI. * * @return bool True if should render, false otherwise. */ public static function should_render() { if ( self::is_logged_in() || ! self::is_valid_screen() ) { return false; } return self::verify_connection_nonce(); } /** * Verify the connection nonce. * * @return bool */ private static function verify_connection_nonce() { $nonce = self::get_sanitized_input( '_wpnonce' ); if ( empty( $nonce ) ) { return false; } return wp_verify_nonce( $nonce, self::$connection_action ) !== false; } /** * Get sanitized input from GET parameters. * * @param string $key The input key to retrieve. * @param mixed $default_value Default value if key doesn't exist. * @return mixed Sanitized input value or default. */ private static function get_sanitized_input( $key, $default_value = '' ) { $value = filter_input( INPUT_GET, $key, FILTER_UNSAFE_RAW ); if ( null === $value ) { return $default_value; } return sanitize_text_field( $value ); } /** * Modify text string vars. * * @param array $texts Vars. * @param string $plugin_id Plugin identifier. * * @return array */ public function customize_text_vars( $texts, $plugin_id ) { if ( self::$plugin_identifier === $plugin_id ) { $feature = $this->get_feature_name(); $feature_part = ucfirst( self::$plugin_identifier ) . ' - ' . esc_html( $feature ); $texts['create_account_desc'] = sprintf( /* translators: %1$s: Feature, %2$s: Opening italic tag, %3$s: Closing italic tag. */ esc_html__( 'Create a free account to connect your site to WPMU DEV and activate %1$s. %2$s It`s fast, seamless, and free. %3$s', 'wp-smushit' ), '<strong>' . $feature_part . '</strong>', '<i>', '</i>' ); $texts['login_desc'] = sprintf( /* translators: %s: Feature */ esc_html__( 'Log in with your WPMU DEV account credentials to activate %s.', 'wp-smushit' ), $feature_part ); } return $texts; } /** * Get the feature name for the current screen. * * @return string */ private function get_feature_name() { $feature_name = __( 'Bulk Smush', 'wp-smushit' ); $request_uri = ( new Server_Utils() )->get_request_uri(); if ( str_contains( $request_uri, 'smush_settings_permissions_subsite_controls' ) ) { $feature_name = __( 'Subsite Controls', 'wp-smushit' ); } return $feature_name; } /** * Adds the Hub connector data to the Smush data. * * @param array $extra_args The Smush data. * @param string $plugin_id Plugin identifier. * * @return array The Smush data with the Hub connector data. */ public function add_hub_connector_data( $extra_args, $plugin_id ) { if ( self::$plugin_identifier === $plugin_id ) { $register_url = $this->array_utils->get_array_value( $extra_args, array( 'login', 'register_url' ) ); if ( $register_url && is_string( $register_url ) ) { $extra_args['login']['register_url'] = $this->get_register_url_with_utm( $register_url ); } if ( is_multisite() ) { $this->remove_filter( 'pre_site_option_wp-smush-networkwide' ); $activated_subsite_modules = Settings::get_instance()->get_activated_subsite_modules_list(); $network_can_access_bulk = ! in_array( 'bulk', $activated_subsite_modules, true ); $current_url = $this->array_utils->get_array_value( $extra_args, array( 'login', 'current_url' ) ); if ( $current_url && ! $network_can_access_bulk ) { $dashboard_url = Helper::get_page_url( 'smush' ); // Update the redirect URL after the site is connected successfully. $extra_args['login']['current_url'] = $dashboard_url; } $this->restore_filter( 'pre_site_option_wp-smush-networkwide' ); } } return $extra_args; } /** * Get register URL with UTM parameters. * * @param string $register_url The base register URL. * @return string The register URL with UTM parameters. */ private function get_register_url_with_utm( $register_url ) { $utm_campaign = filter_input( INPUT_GET, 'utm_campaign', FILTER_UNSAFE_RAW ); return add_query_arg( array( 'utm_medium' => 'plugin', 'utm_source' => self::$plugin_identifier, 'utm_campaign' => empty( $utm_campaign ) ? 'smush_bulk_smush_connect' : esc_attr( $utm_campaign ), 'utm_content' => 'hub-connector', ), $register_url ); } /** * Sync data after successful connection. * * @return void */ public function sync_after_connect() { add_site_option( 'wp_smush_show_connected_modal', true ); delete_site_transient( 'wp_smush_hc_site_syncing' ); } /** * Register the hub connection success modal. * * @param array $modals Registered modals. * @return array */ public function register_hub_connection_success_modal( $modals ) { if ( get_site_option( 'wp_smush_show_connected_modal' ) ) { delete_site_option( 'wp_smush_show_connected_modal' ); $modals['hub-connection-success'] = array(); } if ( self::is_logged_in() ) { $modals['disconnect-site'] = array(); } return $modals; } /** * Disable Subsite Controls for Unconnected Free Users. * * @param mixed $pre_value Pre option value. * @return mixed */ public function disable_subsite_controls_for_unconnected_free_users( $pre_value ) { if ( Membership::get_instance()->is_api_hub_access_required() ) { // 0: None, 1: All, Array list modules: Custom. return 0; } return $pre_value; } /** * Renders the Hub Connector actions. */ public function render_hub_connector_actions() { $is_site_connected = self::is_logged_in(); $is_required_api_hub_access = Membership::get_instance()->is_api_hub_access_required(); if ( ! $is_site_connected && ! $is_required_api_hub_access ) { return; } ?> <div class="sui-box-settings-row" id="general-hub-connector-row"> <div class="sui-box-settings-col-1"> <span class="sui-settings-label "><?php esc_html_e( 'Hub Connector', 'wp-smushit' ); ?></span> <span class="sui-description"> <?php esc_html_e( "Connects your site to the WPMU DEV Free Plan, unlocking the plugin's Free plan features.", 'wp-smushit' ); ?> </span> </div> <div class="sui-box-settings-col-2"> <?php if ( $is_site_connected ) : ?> <button type="button" class="sui-button sui-button-ghost" data-esc-close="false" data-modal-open="smush-disconnect-site-modal" data-modal-open-focus="dialog-close-div" data-modal-mask="true"> <span class="sui-button-text-default"> <span class="sui-icon-plug-disconnected" aria-hidden="true"></span> <?php esc_html_e( 'Disconnect site', 'wp-smushit' ); ?> </span> </button> <?php else : ?> <a href="<?php echo esc_url( self::get_connect_site_url( 'smush-bulk', 'smush_settings_general_connect' ) ); ?>" class="sui-button sui-button-blue smush-button-dark-blue"> <span class="sui-icon-plug-connected" aria-hidden="true"></span> <?php esc_html_e( 'Connect site', 'wp-smushit' ); ?> </a> <?php endif; ?> <span class="sui-description"><?php esc_html_e( 'Note: disconnecting your site from WPMU DEV will disable other services that rely on this connection.', 'wp-smushit' ); ?></span> </div> </div> <?php } /** * AJAX handler to check Hub sync status. * * @return void */ public function ajax_check_hub_sync_status() { check_ajax_referer( 'auth_nonce', 'nonce' ); if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', 'wp-smushit' ) ), 403 ); } $sync = $this->sync(); if ( is_wp_error( $sync ) && 'not_logged_in' !== $sync->get_error_code() ) { delete_site_transient( 'wp_smush_hc_site_syncing' ); wp_send_json_error( array( 'is_synced' => false, 'error_code' => $sync->get_error_code(), 'error_message' => $sync->get_error_message(), ), 403 ); } $is_logged_in = self::is_logged_in(); delete_site_transient( 'wp_smush_hc_site_syncing' ); wp_send_json_success( array( 'is_synced' => $is_logged_in, ) ); } /** * Disconnect the site from the hub. * * @return void */ public function ajax_disconnect_site() { check_ajax_referer( 'wp-smush-ajax' ); // Check capability. if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 ); } $this->disconnect(); // No Need to send json response for other requests. wp_send_json_success(); } /** * Add site disconnected success message. * * @param mixed $messages Smush data messages. * @return array */ public function add_site_disconnected_success_message( $messages ) { $messages['site_disconnected_success'] = __( 'Site disconnected successfully.', 'wp-smushit' ); return $messages; } /** * Get the Hub forgot password URL. * * @return string Forgot password URL. */ private function get_hub_forgot_password_url() { if ( ! class_exists( '\WPMUDEV\Hub\Connector\Data' ) ) { return ''; } $current_page = 'smush'; $utm_campaign = 'smush_forgot_password'; return self::generate_hub_url( \WPMUDEV\Hub\Connector\Data::get()->server_url( 'forgot-password' ), array(), $current_page, $utm_campaign ); } /** * Get Hub register URL with site connection parameters. * * @return string */ public static function get_hub_register_url( $current_page = 'smush', $utm_campaign = 'smush_bulk_smush_connect' ) { // Check if hub connector Data class is available. if ( ! class_exists( '\WPMUDEV\Hub\Connector\Data' ) ) { return ''; } return self::generate_hub_url( \WPMUDEV\Hub\Connector\Data::get()->server_url( 'register' ), array( 'signup' => 'site-connect', ), $current_page, $utm_campaign ); } /** * Generate the Hub URL. * * @param mixed $hub_base_url * @param array $query_params * @param string $current_page * @param string $utm_campaign * * @return string */ private static function generate_hub_url( $hub_base_url, $query_params = array(), $current_page = 'smush', $utm_campaign = 'smush_bulk_smush_connect' ) { // Get the hub connection URL (includes page, nonce, page_action, utm_campaign). $hub_connect_url = self::get_connect_site_url( $current_page, $utm_campaign ); // Prepare redirect URL with callback and auth nonce. $auth_nonce = wp_create_nonce( 'auth_nonce' ); $redirect_url = add_query_arg( array( 'hub_connector_callback' => 1, 'auth_nonce' => $auth_nonce, ), $hub_connect_url ); $query_params = wp_parse_args( $query_params, array( 'site_connect_url' => rawurlencode( $redirect_url ), 'utm_medium' => 'plugin', 'utm_source' => 'smush', 'utm_campaign' => $utm_campaign, 'utm_content' => 'hub-connector', ) ); return add_query_arg( $query_params, $hub_base_url ); } /** * Get Hub Google login URL (https://wpmudev.com/api/dashboard/v2/google-auth). * * @return string */ public static function get_hub_google_login_url() { return \WPMUDEV\Hub\Connector\Data::get()->server_url( 'api/dashboard/v2/google-auth' ); } /** * Get Hub Login Auth URL (https://wpmudev.com/api/dashboard/v2/site-authenticate). * * @return string */ public static function get_hub_site_login_auth_url() { return \WPMUDEV\Hub\Connector\API::get()->rest_url( 'site-authenticate' ); } /** * Get the site domain. * * @return string */ public static function get_site_domain() { return \WPMUDEV\Hub\Connector\Data::get()->network_site_url(); } /** * Verify the auth nonce from request. * * @return bool */ public static function verify_nonce() { return wp_verify_nonce( ( sanitize_text_field( wp_unslash( $_REQUEST['auth_nonce'] ?? '' ) ) ), 'auth_nonce' ); } /** * Check if the site is syncing. * * @return bool */ public static function is_syncing() { $syncing = current_user_can( 'manage_options' ) && self::verify_nonce() && ! empty( $_REQUEST['page_action'] ) && 'hub_connection' === $_REQUEST['page_action'] && ! empty( $_REQUEST['set_apikey'] ); if ( $syncing ) { // HC removes params and in short window logged_in won't return the state. set_site_transient( 'wp_smush_hc_site_syncing', true ); } return get_site_transient( 'wp_smush_hc_site_syncing' ) ? true : false; } /** * Check if there is an error in login response from HUB. * * @return bool */ public static function has_error_in_login() { $page_action = sanitize_text_field( wp_unslash( $_GET['page_action'] ?? '' ) ); $api_error = sanitize_text_field( wp_unslash( $_GET['api_error'] ?? 0 ) ); $is_hub_callback = ! empty( $_GET['hub_connector_callback'] ); if ( 'hub_connection' === $page_action ) { return ! empty( $api_error ); } if ( ! $is_hub_callback ) { return false; } if ( ! self::verify_nonce() ) { return false; } return ! empty( $api_error ); } /** * Get authentication error messages. Copied from HC. * * Based on the error code, prepare different error messages. * * @return string */ public static function get_auth_error() { /** * Nonce is verified in `process_auth_callback` before this method called. * * @see self::process_auth_callback() */ // phpcs:disable WordPress.Security.NonceVerification.Recommended $error = ''; $reset_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'forgot-password' ); $skip_trial_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub/account/?skip_trial' ); $trial_info_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'docs/getting-started/how-free-trials-work/' ); $websites_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/' ); $security_info_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'manuals/hub-security/' ); $support_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub/support/' ); $account_details_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/account/details/' ); if ( isset( $_GET['api_error'] ) ) { // Get errors. $api_error = sanitize_key( wp_unslash( $_GET['api_error'] ) ); $auth_error = sanitize_key( wp_unslash( $_GET['auth_error'] ?? '' ) ); if ( 1 === (int) $api_error || 'auth' === $api_error ) { switch ( $auth_error ) { case 'google_linked': $error = sprintf( // translators: %s Account detail URL. __( 'You are currently using your Google account as your preferred login method. If you wish to login with your WPMU DEV email & password instead, please change the <strong>Login Method</strong> in <a href="%s" target="_blank">your WPMU DEV account</a>.', 'wp-smushit' ), $account_details_url ); break; case 'google_unlinked': $error = sprintf( // translators: %s Account detail URL. __( 'You are currently using your WPMU DEV email & password as your preferred login method. If you wish to login with your Google account instead, please change the <strong>Login Method</strong> in <a href="%s" target="_blank">your WPMU DEV account</a>.', 'wp-smushit' ), $account_details_url ); break; case 'reauth_google': $error = sprintf( // translators: %1$s Account detail URL, %2$s Reset URL. __( 'Due to security improvements, you will need to re-link your Google account in The Hub. Please log in with your WPMU DEV email & password for now, then set up your preferred <strong>Login Method</strong> in <a href="%1$s" target="_blank">your WPMU DEV account</a>. Forgot your password? You can <a href="%2$s" target="_blank"><strong>reset it here</strong></a>.', 'wp-smushit' ), $account_details_url, $reset_url ); break; default: // Invalid credentials. $error = sprintf( '%s<br><a href="%s" target="_blank"><strong>%s</strong></a>', esc_html__( 'Your login details were incorrect. Please make sure you\'re using your WPMU DEV email and password and try again.', 'wp-smushit' ), $reset_url, esc_html__( 'Forgot your password?', 'wp-smushit' ) ); break; } } else { switch ( $api_error ) { case 'in_trial': $error = sprintf( '%s<br><a href="%s" target="_blank">%s</a>', sprintf( // translators: %1$s Rest URL, %2$s Upgrade URL, %3$s Trial URL. __( 'This domain has previously been registered with us by the user %1$s. To use WPMU DEV on this domain, you can either log in with the original account (you can <a target="_blank" href="%2$s"><strong>reset your password</strong></a>) or <a target="_blank" href="%3$s">upgrade your trial</a> to a full membership. Trial accounts can\'t use previously registered domains - <a target="_blank" href="%4$s">here\'s why</a>.', 'wp-smushit' ), '<strong style="word-break: break-all;">' . esc_html( $_GET['display_name'] ) . '</strong>', // phpcs:ignore $reset_url, $skip_trial_url, $trial_info_url ), $support_url, __( 'Contact support if you need further assistance »', 'wp-smushit' ) ); break; case 'already_registered': $error = sprintf( // translators: %1$d Account name, %2$s Security info, %3$s Hub URL, %4$s Support URL. __( 'This site is currently registered to %1$s. For <a target="_blank" href="%2$s">security reasons</a> they will need to go to the <a target="_blank" href="%3$s">WPMU DEV Hub</a> and remove this domain before you can log in. If you do not have access to that account, and have no way of contacting that user, please <a target="_blank" href="%4$s">contact support for assistance</a>.', 'wp-smushit' ), ! isset( $_GET['display_name'] ) ? __( 'a different user', 'wp-smushit' ) : '<strong style="word-break: break-all;">' . esc_html( $_GET['display_name'] ) . '</strong>', // phpcs:ignore. $security_info_url, $websites_url, $support_url ); break; case 'banned_account': $error = sprintf( // translators: %s Support URL. __( 'This domain cannot be registered to your WPMU DEV account.<br><a href="%s">Contact Accounts & Billing if you need further assistance »</a>', 'wp-smushit' ), \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/#ask-question' ) ); break; case 'expired_membership': $error = sprintf( // translators: %1$s Hub Account URL, %2$s: Switch to Free URL. __( 'Login failed — your WPMU DEV membership has expired. Renew now to regain full access, or switch to our free plan to continue managing all your site in The Hub.<br/><br/><a class="sui-button sui-button-blue" href="%1$s" target="_blank">Renew Membership</a> <a class="sui-button sui-button-ghost" href="%2$s" target="_blank">Switch to Free</a>', 'wp-smushit' ), \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/account/ ' ), \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/?switch-free=1 ' ) ); break; case 'invalid_nonce': case 'invalid_double_submit_cookie': case 'invalid_google_creds': case '': $error = __( 'Google login failed. Please try again.', 'wp-smushit' ); break; default: // This in case we add new error types in the future. $error = __( 'Unknown error. Please update the WPMU DEV Dashboard plugin and try again.', 'wp-smushit' ); break; } } } elseif ( ! empty( $_REQUEST['connection_error'] ) ) { // Variable `$connection_error` is set by the UI function `render_dashboard`. $error = sprintf( '%s<br>%s<br><em>%s</em>', __( 'Your server had a problem connecting to WPMU DEV. Please try again.', 'wp-smushit' ), __( 'If this problem continues, please contact your host with this error message and ask:', 'wp-smushit' ), sprintf( // translators: url to API. __( '"Is PHP on my server properly configured to be able to contact %s with a POST HTTP request via fsockopen or CURL?"', 'wp-smushit' ), \WPMUDEV\Hub\Connector\Data::get()->server_url() ) ); } elseif ( ! empty( $_REQUEST['invalid_key'] ) ) { // Invalid API key. $error = __( 'Your API Key was invalid. Please try again.', 'wp-smushit' ); } /** * Filter to modify auth error text. * * @since 1.0.0 * * @param string $error Error message. * @param string $plugin Plugin identifier. */ return apply_filters( 'wpmudev_hub_connector_get_auth_error', $error, self::$plugin_identifier ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Get profile data. * * @return WP_Error|array */ private function get_profile_data() { // Hub Connector connected — use its profile data. if ( self::is_logged_in() && class_exists( '\WPMUDEV\Hub\Connector\Data' ) ) { $membership_data = Data::get()->profile_data( true ); if ( is_array( $membership_data ) && ! empty( $membership_data['user_name'] ) ) { return $membership_data; } } // Dashboard connected — use Dashboard profile. if ( self::is_wpmudev_dashboard_connected() && method_exists( WPMUDEV_Dashboard::$api, 'get_profile' ) ) { $profile = WPMUDEV_Dashboard::$api->get_profile(); if ( ! empty( $profile['profile'] ) && is_array( $profile['profile'] ) ) { return $profile['profile']; } } return new WP_Error( 'not_logged_in', __( 'Authentication required. Please log in.', 'wp-smushit' ) ); } } class-urls-exclusions.php 0000644 00000003720 15252476777 0011567 0 ustar 00 <?php namespace Smush\Core; /** * URL Exclusions helper. */ class Urls_Exclusions { /** * Regex delimiter used for preg_match/preg_quote. * * @var string */ private $delimiter; /** * Constructor. * * @param string $delimiter Regex delimiter. */ public function __construct( $delimiter = '~' ) { $this->delimiter = $delimiter; } /** * Checks if the current request URI matches any of the excluded pages. * * @return bool True if the current request URI is excluded, false otherwise. */ public function is_excluded_uri( $request_uri, $excluded_page_urls ) { $pattern = $this->get_excluded_uri_pattern( $excluded_page_urls ); if ( empty( $pattern ) ) { return false; } $request_uri = wp_parse_url( $request_uri, PHP_URL_PATH ); $regex = $this->delimiter . $pattern . $this->delimiter . 'i'; return (bool) preg_match( $regex, $request_uri ); } /** * Generate a regex pattern from excluded page URLs. * * @return string Regex pattern without delimiters or flags. */ private function get_excluded_uri_pattern( $excluded_page_urls ) { if ( empty( $excluded_page_urls ) ) { return ''; } $patterns = array_map( array( $this, 'build_url_pattern' ), $excluded_page_urls ); return implode( '|', array_filter( $patterns ) ); } /** * Build regex pattern for a single URL. * * @param string $url The URL to convert to regex pattern. * * @return string Regex pattern for the URL. */ private function build_url_pattern( $url ) { $url = trim( $url ); if ( empty( $url ) ) { return ''; } if ( '/' === $url ) { return $this->is_subsite() ? '^' . preg_quote( get_blog_details( get_current_blog_id() )->path, '~' ) . '$' : '^/$'; } return preg_quote( $url, '~' ); } /** * Checks if the current site is a subsite in a multisite network. * * @return bool True if the site is a subsite, false otherwise. */ private function is_subsite() { return is_multisite() && ! is_main_site(); } } class-error-handler.php 0000644 00000020704 15252476777 0011155 0 ustar 00 <?php /** * Error_Handler class. * * @package Smush\Core * @version 3.12.0 */ namespace Smush\Core; use Smush\Core\Media\Media_Item; use Smush\Core\Media\Media_Item_Cache; use Smush\Core\Media\Media_Item_Optimizer; use Smush\Core\Stats\Global_Stats; use WP_Error; if ( ! defined( 'WPINC' ) ) { die; } class Error_Handler { /** * Ignore meta key. */ private static $ignore_key = 'wp-smush-ignore-bulk'; /** * Error meta key. */ private static $error_key = 'wp-smush-error'; /** * Animated error code. */ private static $animated_error_code = 'animated'; /** * Handled error codes. * * @var array */ private static $locked_error_codes = array( 'ignored', // 'in_progress', // 'animated', ); /** * Skipped error codes. * * @var array */ private static $skipped_error_codes = array( 'skipped_filter', 'ignored', 'animated', 'size_limit', 'size_pro_limit', ); /** * Should regenerate thumbnail error codes. * * @var array */ private static $regenerate_error_codes = array( 'no_file_meta', 'file_not_found', ); /** * Get error message. * * @param string $error_code Error code. * @param int $image_id Attachment ID. * @return string */ public static function get_error_message( $error_code, $image_id = 0 ) { $error_messages = self::get_default_error_messages(); $error_message = ! empty( $error_messages[ $error_code ] ) ? $error_messages[ $error_code ] : ''; return self::format_error_message( $error_message, $error_code, $image_id ); } /** * Get sprintf error message. * * @param string $error_message Error message. * @param string $error_code Error code. * @param int $image_id Attachment ID. * @return string */ private static function format_error_message( $error_message, $error_code, $image_id ) { if ( empty( $image_id ) || empty( $error_message ) || false === strpos( $error_message, '%s' ) ) { return $error_message; } switch ( $error_code ) { case 'size_limit': case 'size_pro_limit': $size_exceeded = Helper::size_limit_exceeded( $image_id ); if ( $size_exceeded ) { $error_message = sprintf( $error_message, size_format( $size_exceeded ) ); } else { $error_message = null; } break; case 'not_writable': $file_path = Helper::get_attached_file( $image_id ); $error_message = sprintf( $error_message, Helper::clean_file_path( dirname( $file_path ) ) ); break; case 'file_not_found': $file_path = Helper::get_attached_file( $image_id ); $file_not_found = $file_path; if ( file_exists( $file_path ) ) { // Try go get the not found thumbnail file. $all_file_sizes = wp_get_attachment_metadata( $image_id ); $dir_path = dirname( $file_path ); if ( ! empty( $all_file_sizes['sizes'] ) ) { foreach ( $all_file_sizes['sizes'] as $size => $size_data ) { $size_file = $dir_path . '/' . $size_data['file']; if ( ! file_exists( $size_file ) ) { $file_not_found = $size_file; break; } } } } $error_message = sprintf( $error_message, basename( $file_not_found ) ); break; } return $error_message; } public static function get_all_failed_images() { $media_item_error_ids = Global_Stats::get()->get_error_list()->get_ids(); $optimization_error_ids = self::get_last_optimization_error_ids( PHP_INT_MAX ); return array_unique( array_merge( $media_item_error_ids, $optimization_error_ids ) ); } /** * Get last optimization errors. * * @param int $limit Query limit. * @return array */ private static function get_last_optimization_error_ids( $limit ) { global $wpdb; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $query = $wpdb->prepare( "SELECT DISTINCT post_meta_error.post_id FROM $wpdb->postmeta as post_meta_error LEFT JOIN $wpdb->postmeta as post_meta_ignore ON post_meta_ignore.post_id = post_meta_error.post_id AND post_meta_ignore.meta_key= %s WHERE post_meta_ignore.meta_value IS NULL AND post_meta_error.meta_key = %s ORDER BY post_meta_error.post_id DESC LIMIT %d;", Media_Item::get_ignored_meta_key(), Media_Item_Optimizer::get_error_meta_key(), $limit ); /** * Due to performance, we do not join with table wp_posts to exclude the deleted attachments, * leave a filter for third-party custom it. */ $query = apply_filters( 'wp_smush_query_get_last_optimize_errors', $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared return $wpdb->get_col( $query ); } private static function get_last_media_item_errors( $limit ) { $error_list_ids = Global_Stats::get()->get_error_list()->get_ids(); $last_errors = array(); if ( empty( $error_list_ids ) ) { return $last_errors; } foreach( $error_list_ids as $attachment_id ) { $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); if ( ! $media_item->has_errors() ) { continue; } $last_errors[ $attachment_id ] = self::get_error( $media_item->get_errors(), $media_item ); if ( count( $last_errors ) >= $limit ) { break; } } return $last_errors; } private static function get_last_optimize_errors( $limit ) { $last_errors_ids = self::get_last_optimization_error_ids( $limit ); $last_errors = array(); if ( empty( $last_errors_ids ) ) { return $last_errors; } foreach( $last_errors_ids as $attachment_id ) { $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); $optimizer = new Media_Item_Optimizer($media_item); if( ! $optimizer->has_errors() ) { continue; } $last_errors[ $attachment_id ] = self::get_error( $optimizer->get_errors(), $media_item ); if ( count( $last_errors ) >= $limit ) { break; } } return $last_errors; } /** * Get latest errors. * * @param int $limit Limit number of errors to return. * @return array */ public static function get_last_errors( $limit = 10 ) { $last_errors = self::get_last_media_item_errors( $limit ); $no_item_errors = count( $last_errors ); if ( $no_item_errors >= $limit ) { return $last_errors; } $optimize_errors = self::get_last_optimize_errors( $limit - $no_item_errors ); return $last_errors + $optimize_errors; } /** * @return array */ public static function get_error( $errors, $media_item ) { $thumbnail = $media_item->get_size('thumbnail' ); $media_item_size = $media_item->get_scaled_or_full_size(); return array( 'error_code' => $errors->get_error_code(), 'error_message' => $errors->get_error_message(), 'file_name' => $media_item_size ? $media_item_size->get_file_name() : '', 'thumbnail' => $thumbnail ? $thumbnail->get_file_url() : false, ); } /** * Get error messages. * * @return array */ private static function get_default_error_messages() { return apply_filters( 'wp_smush_error_messages', array( 'missing_id' => esc_html__( 'No attachment ID was received.', 'wp-smushit' ), 'ignored' => esc_html__( 'Skip ignored file.', 'wp-smushit' ), 'animated' => esc_html__( 'Skipped animated file.', 'wp-smushit' ), 'in_progress' => esc_html__( 'File processing is in progress.', 'wp-smushit' ), 'no_file_meta' => esc_html__( 'No file data found in image meta', 'wp-smushit' ), 'skipped_filter' => esc_html__( 'Skipped with wp_smush_image filter', 'wp-smushit' ), 'empty_path' => esc_html__( 'File path is empty', 'wp-smushit' ), 'empty_response' => esc_html__( 'Webp no response was received.', 'wp-smushit' ), 'not_processed' => esc_html__( 'Not processed', 'wp-smushit' ), /* translators: %s: image size */ 'size_limit' => __( 'Skipped (%s). File size limit of 5MB exceeded.', 'wp-smushit' ), /* translators: %s: image size */ 'size_pro_limit' => __( 'Skipped (%s). File size limit of 256MB exceeded.', 'wp-smushit' ), /* translators: %s: Directory path */ 'not_writable' => __( '%s is not writable', 'wp-smushit' ), /* translators: %s: File path */ 'file_not_found' => __( 'Skipped (%s). File not found.', 'wp-smushit' ), ) ); } /** * Get animated_error_code. * * @return string */ public static function get_animated_error_code() { return self::$animated_error_code; } /** * Get error_key. * * @return string */ public static function get_error_key() { return self::$error_key; } /** * Get ignore_key. * * @return string */ public static function get_ignore_key() { return self::$ignore_key; } } class-settings-sanitizer.php 0000644 00000011562 15252476777 0012261 0 ustar 00 <?php /** * Settings Sanitizer Utility * * Generic helpers to sanitize settings received from UI/API before persisting to the database. * Includes schema-based sanitization and small helpers for common shapes (e.g. non-empty string lists). * * @package Smush\Core */ namespace Smush\Core; if ( ! defined( 'WPINC' ) ) { die; } /** * Class Settings_Sanitizer * * Provides schema-driven sanitization for settings arrays and helper methods for common settings shapes. */ class Settings_Sanitizer { /** * Sanitize settings using a schema. Works for both partial updates and full settings arrays. * * Rules: * - 'key' => 'callback' : sanitize that key. If value is array, apply callback to all leaves (map_deep). * - 'key' => [ ... ] : nested schema for arrays. * - Keys missing from schema are sanitized with $fallback_callback (deep). * - Nested keys missing from nested schema are also sanitized with $fallback_callback (deep). * * @param array $input Incoming settings (partial or full). * @param array $schema Sanitization schema. * @param string|callable $fallback_callback Fallback sanitizer applied via deep() when rule is missing/invalid. * * @return array Sanitized settings (same shape/keys as $input). */ public static function sanitize( $input, $schema = array(), $fallback_callback = 'sanitize_text_field' ) { $input = is_array( $input ) ? $input : array(); $schema = is_array( $schema ) ? $schema : array(); $out = array(); foreach ( $input as $key => $value ) { if ( array_key_exists( $key, $schema ) ) { $out[ $key ] = self::sanitize_value_by_rule( $value, $schema[ $key ], $fallback_callback ); continue; } $out[ $key ] = self::deep( $value, $fallback_callback ); } return $out; } /** * Sanitize a value using a rule (callback or nested schema). * * @param mixed $value Value to sanitize. * @param mixed $rule Rule describing how to sanitize the value. * @param string|callable $fallback_callback Fallback sanitizer applied via deep() when rule is missing/invalid. * * @return mixed */ private static function sanitize_value_by_rule( $value, $rule, $fallback_callback ) { /** * Security note: * Only allow executables via the explicit config-schema: * array( 'sanitizer' => (callable) ) * * This avoids treating arbitrary input arrays/strings as callables. */ if ( is_array( $rule ) && array_key_exists( 'sanitizer', $rule ) ) { $callback = is_callable( $rule['sanitizer'] ) ? $rule['sanitizer'] : $fallback_callback; $sanitized = self::deep( $value, $callback ); // Optionally normalise the whole list: trim, remove empties, deduplicate. if ( ! empty( $rule['nonempty_list'] ) ) { $sanitized = self::sanitize_nonempty_string_list( $sanitized ); } return $sanitized; } // Nested schema (no callables allowed here). if ( is_array( $rule ) ) { $value = is_array( $value ) ? $value : array(); $out = array(); foreach ( $value as $child_key => $child_value ) { if ( array_key_exists( $child_key, $rule ) ) { $out[ $child_key ] = self::sanitize_value_by_rule( $child_value, $rule[ $child_key ], $fallback_callback ); } else { $out[ $child_key ] = self::deep( $child_value, $fallback_callback ); } } return $out; } // Invalid rule => fallback. return self::deep( $value, $fallback_callback ); } /** * Sanitize a list of strings. * * Normalizes typical "list" inputs coming from UI components: * - Ensures the result is always an array. * - Trims each item. * - Drops empty items. * - Removes duplicates. * * @param mixed $sanitized_value Incoming value. * * @return array Normalized list of non-empty, unique strings. */ public static function sanitize_nonempty_string_list( $sanitized_value ) { if ( ! is_array( $sanitized_value ) ) { $sanitized_value = array(); } $items = array(); foreach ( $sanitized_value as $item ) { $item = is_string( $item ) ? trim( $item ) : ''; if ( '' === $item ) { continue; } $items[] = $item; } $items = array_values( array_unique( $items ) ); return $items; } /** * Deep sanitizer: apply callback to all leaf values. * * @param mixed $value Value to sanitize (scalar|array|object). * @param callable $callback Callback to apply to leaf values. * * @return mixed */ public static function deep( $value, $callback ) { if ( function_exists( 'map_deep' ) ) { return map_deep( $value, $callback ); } if ( is_array( $value ) ) { foreach ( $value as $k => $v ) { $value[ $k ] = self::deep( $v, $callback ); } return $value; } if ( is_object( $value ) ) { foreach ( get_object_vars( $value ) as $k => $v ) { $value->$k = self::deep( $v, $callback ); } return $value; } return call_user_func( $callback, $value ); } } smush/class-smush-request.php 0000644 00000007334 15252476777 0012401 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Array_Utils; use Smush\Core\File_System; use Smush\Core\File_Utils; /** * Calls the API and returns the response. */ abstract class Smush_Request { /** * @var int */ private $timeout; /** * @var int */ private $connect_timeout = 5; /** * @var string */ private $user_agent; /** * @var */ private $on_complete = '__return_false'; /** * @var Array_Utils */ private $array_utils; /** * @var File_Utils */ private $file_utils; /** * @var bool */ private $streaming_enabled; /** * @var File_System */ private $fs; /** * @var array */ private $extra_headers; /** * @var Smusher_Options */ private $options; public function __construct( $options ) { $this->options = $options; $this->streaming_enabled = $options->is_streaming_enabled(); $this->extra_headers = $options->get_extra_headers(); $this->array_utils = new Array_Utils(); $this->file_utils = new File_Utils(); $this->fs = new File_System(); $this->user_agent = WP_SMUSH_UA; $this->timeout = WP_SMUSH_TIMEOUT; } public function get_on_complete() { return $this->on_complete; } public function set_on_complete( $on_complete ) { $this->on_complete = $on_complete; return $this; } public function get_connect_timeout() { return $this->connect_timeout; } public function get_timeout() { return $this->timeout; } public function get_user_agent() { return $this->user_agent; } public function get_url() { return $this->options->get_api_url(); } /** * @return string[] */ public function get_api_request_headers( $file_path ) { $headers = array_merge( array( 'accept' => 'application/json', // The API returns JSON. 'exif' => $this->options->strip_exif() ? 'false' : 'true', ), $this->get_extra_headers() ); if ( $this->streaming_enabled ) { $headers['response'] = 'image_url'; } else { $headers['response'] = 'image_full'; } $headers['content-type'] = 'application/binary'; $headers['lossy'] = $this->options->get_lossy_level(); // Check if premium member, add API key. $api_key = $this->options->get_api_key(); if ( ! empty( $api_key ) ) { $headers['apikey'] = $api_key; $is_large_file = $this->file_utils->is_large_file( $file_path ); if ( $is_large_file ) { $headers['islarge'] = 1; } } return $headers; } public function get_full_file_contents( $file_path ) { // Temporary increase the limit because we are about to read a full file into memory. wp_raise_memory_limit( 'image' ); $contents = $this->fs->file_get_contents( $file_path ); return empty( $contents ) ? '' : $contents; } /** * @param $file_data string|array * * @return array */ protected function get_file_path_and_url( $file_data ) { if ( is_string( $file_data ) ) { $file_path = $file_data; $file_url = ''; } else { $file_path = $this->array_utils->get_array_value( $file_data, 'path' ); $file_url = $this->array_utils->get_array_value( $file_data, 'url' ); } return array( $file_path, $file_url ); } public function get_extra_headers() { return $this->extra_headers; } public function set_extra_headers( $extra_headers ) { $this->extra_headers = $extra_headers; return $this; } public function do_request( $file_path, $size_key ) { return false; } public function set_streaming_enabled( $streaming_enabled ) { $this->streaming_enabled = $streaming_enabled; return $this; } public function is_streaming_enabled() { return $this->streaming_enabled; } /** * @param $file_paths array * * @return mixed */ abstract public function do_requests( $file_paths ); abstract public function is_supported(); } smush/class-smusher-options.php 0000644 00000004730 15252476777 0012730 0 ustar 00 <?php namespace Smush\Core\Smush; class Smusher_Options { private $lossy_level; private $strip_exif; private $api_key; private $api_url; private $streaming_enabled; private $extra_headers; private $parallel_optimization_enabled; private $protocol; private $max_size; /** * @var callable|null */ private $on_disable_streaming; /** * @var callable|null */ private $on_switch_to_http; public function set_on_disable_streaming( $callback ) { $this->on_disable_streaming = $callback; return $this; } public function set_on_switch_to_http( $callback ) { $this->on_switch_to_http = $callback; return $this; } public function disable_streaming() { if ( is_callable( $this->on_disable_streaming ) ) { ( $this->on_disable_streaming )(); } } public function switch_to_http() { if ( is_callable( $this->on_switch_to_http ) ) { ( $this->on_switch_to_http )(); } } public function get_lossy_level() { return $this->lossy_level; } public function set_lossy_level( $lossy_level ) { $this->lossy_level = $lossy_level; return $this; } public function strip_exif() { return $this->strip_exif; } public function set_strip_exif( $strip_exif ) { $this->strip_exif = $strip_exif; return $this; } public function get_api_key() { return $this->api_key; } public function set_api_key( $api_key ) { $this->api_key = $api_key; return $this; } public function get_api_url() { return $this->api_url; } public function set_api_url( $api_url ) { $this->api_url = $api_url; return $this; } public function is_streaming_enabled() { return $this->streaming_enabled; } public function set_streaming_enabled( $enabled ) { $this->streaming_enabled = $enabled; return $this; } public function get_extra_headers() { return $this->extra_headers; } public function set_extra_headers( $extra_headers ) { $this->extra_headers = $extra_headers; return $this; } public function is_parallel_optimization_enabled() { return $this->parallel_optimization_enabled; } public function set_parallel_optimization_enabled( $enabled ) { $this->parallel_optimization_enabled = $enabled; return $this; } public function get_protocol() { return $this->protocol; } public function set_protocol( $protocol ) { $this->protocol = $protocol; return $this; } public function get_max_size() { return $this->max_size; } public function set_max_size( $max_size ) { $this->max_size = $max_size; return $this; } } smush/class-smush-request-wp-multiple.php 0000644 00000005634 15252476777 0014657 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Server_Utils; use WP_Error; class Smush_Request_WP_Multiple extends Smush_Request { /** * @var Server_Utils */ private $server_utils; public function __construct( $options ) { parent::__construct( $options ); $this->server_utils = new Server_Utils(); } public function do_requests( $file_paths ) { $responses = array(); $requests = $this->prepare_requests( $file_paths ); self::request_multiple( $requests, array( 'timeout' => $this->get_timeout(), 'connect_timeout' => $this->get_connect_timeout(), 'user-agent' => $this->get_user_agent(), 'complete' => function ( $response, $size_key ) use ( $file_paths, $requests, &$responses ) { // Convert to a response that looks like standard WP HTTP API responses $response = $this->multi_to_singular_response( $response ); $request = $requests[ $size_key ]; do_action( 'smush_http_api_debug', $response, $request ); // Call the actual on complete callback $file_path = $file_paths[ $size_key ]; $requests[ $size_key ] = null; $responses[ $size_key ] = call_user_func( $this->get_on_complete(), $response, $size_key, $file_path ); }, ) ); return $responses; } private function multi_to_singular_response( $multi_response ) { if ( is_a( $multi_response, self::get_requests_exception_class_name() ) ) { return new WP_Error( $multi_response->getType(), $multi_response->getMessage() ); } else { return array( 'body' => $multi_response->body, 'response' => array( 'code' => $multi_response->status_code ), ); } } /** \Requests lib are deprecated on WP 6.2.0 */ private static function get_wp_requests_class_name() { return class_exists( '\WpOrg\Requests\Requests' ) ? '\WpOrg\Requests\Requests' : '\Requests'; } private static function request_multiple( $requests, $options = array() ) { $wp_requests_class_name = self::get_wp_requests_class_name(); return $wp_requests_class_name::request_multiple( $requests, $options ); } private static function get_requests_exception_class_name() { return class_exists( '\WpOrg\Requests\Exception' ) ? '\WpOrg\Requests\Exception' : '\Requests_Exception'; } /** * @param array $file_paths * * @return array */ private function prepare_requests( $file_paths ) { $requests = array(); foreach ( $file_paths as $size_key => $file_path ) { $requests[ $size_key ] = array( 'url' => $this->get_url(), 'headers' => $this->get_api_request_headers( $file_path ), 'data' => $this->get_full_file_contents( $file_path ), 'type' => 'POST', ); } return $requests; } public function is_supported() { $wp_requests_class_name = self::get_wp_requests_class_name(); return $this->server_utils->is_function_supported( 'curl_multi_exec' ) && method_exists( $wp_requests_class_name, "request_multiple" ); } } smush/class-smusher.php 0000644 00000071675 15252476777 0011253 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Array_Utils; use Smush\Core\File_System; use Smush\Core\Helper; use Smush\Core\Product_Analytics\Product_Analytics; use Smush\Core\Threads\JSON_Record; use Smush\Core\Timer; use Smush\Core\Upload_Dir; use Smush_Vendor\GuzzleHttp\Client; use WP_Error; /** * Takes raw image file paths and processes them through the Smush API. Replaces originals with the optimized versions. */ class Smusher { private static $error_ssl_cert = 'ssl_cert_error'; private static $image_not_saved_from_url = 'image_not_saved_from_url'; private static $default_chunk_size = 5 * 1024 * 1024; private static $error_time_out = 'time_out'; private static $error_gateway_time_out = 'gateway_time_out'; private static $error_posting_to_api = 'error_posting_to_api'; private static $response_code_non_200 = 'response_code_non_200'; private static $option_id_smush_error_counts = 'wp_smush_error_counts'; /** * @var Smusher_Options */ private $options; /** * @var Smush_Request */ private $request_multiple; /** * @var Smush_Request */ private $request_sequential; /** * @var \WDEV_Logger|null */ private $logger; /** * @var boolean */ private $smush_parallel; /** * @var WP_Error */ private $errors; /** * @var WP_Error */ private $warnings; /** * @var File_System */ private $fs; /** * @var Upload_Dir */ private $upload_dir; /** * @var Array_Utils */ private $array_utils; /** * @var bool */ private $streaming_enabled; /** * @var Product_Analytics */ private $product_analytics; /** * @var JSON_Record */ private $error_counts; /** * @var int|null */ private $max_size; public function __construct( $options ) { $this->options = $options; $this->logger = Helper::logger(); $this->errors = new WP_Error(); $this->warnings = new WP_Error(); $this->fs = new File_System(); $this->upload_dir = new Upload_Dir(); $this->array_utils = new Array_Utils(); $this->product_analytics = Product_Analytics::get_instance(); $this->error_counts = new JSON_Record( self::$option_id_smush_error_counts ); $this->smush_parallel = $options->is_parallel_optimization_enabled(); $this->streaming_enabled = $options->is_streaming_enabled(); $this->max_size = $options->get_max_size(); $this->request_multiple = new Smush_Request_Guzzle_Multiple( $options ); $this->request_sequential = new Smush_Request_WP_Sequential( $options ); } /** * @param $file_paths string[] * * @return boolean[]|object[] */ public function smush( $file_paths ) { $file_paths = $this->normalize_file_paths( $file_paths ); $this->set_errors( new WP_Error() ); $this->set_warnings( new WP_Error() ); if ( $this->smush_parallel && $this->parallel_available_on_server() ) { return $this->smush_parallel( $file_paths ); } else { return $this->smush_sequential( $file_paths ); } } /** * @param $file_paths string[] * * @return boolean[]|object[] */ private function smush_parallel( $file_paths ) { $timer = new Timer(); $timer->start(); $retry = array(); $responses = array(); $this->request_multiple ->set_on_complete( function ( $response, $response_size_key, $size_file_path ) use ( &$responses, &$retry ) { $parsed_response = $this->parse_response( $response, $size_file_path ); if ( $this->is_network_error( $parsed_response ) ) { $retry[ $response_size_key ] = $size_file_path; $this->add_warnings( $parsed_response, $response_size_key ); } else { $is_success_response = $this->handle_response( $parsed_response, $response_size_key, $size_file_path ); // If the network request was successful, there are still some cases where it's best to retry if ( ! $is_success_response && $this->has_error_worth_retrying() ) { $retry[ $response_size_key ] = $size_file_path; } else { $responses[ $response_size_key ] = $is_success_response; } } } )->do_requests( $file_paths ); foreach ( $retry as $retry_size_key => $retry_file_path ) { $responses[ $retry_size_key ] = $this->smush_file( $retry_file_path, $retry_size_key ); } $time_elapsed = $timer->end(); $this->maybe_disable_streaming(); $this->maybe_change_http_setting(); $this->maybe_track_image_url_error( $time_elapsed ); $this->maybe_track_network_errors( $time_elapsed ); return $responses; } /** * Normalizes file paths to the current flat string[] format. * * Supports the legacy format: * array( 'key' => array( 'url' => string, 'path' => string ) ) * * Converts to current format: * array( 'key' => string ) * * @param array $file_paths * * @return string[] */ private function normalize_file_paths( $file_paths ) { $normalized = array(); foreach ( $file_paths as $key => $value ) { if ( is_array( $value ) && isset( $value['path'] ) ) { $normalized[ $key ] = $value['path']; } else { $normalized[ $key ] = $value; } } return $normalized; } private function maybe_change_http_setting() { $codes = array_merge( $this->errors->get_error_codes(), $this->warnings->get_error_codes() ); if ( in_array( self::$error_ssl_cert, $codes, true ) ) { // Switch to http protocol. $this->options->switch_to_http(); } } /** * @param $file_paths string[] * * @return boolean[]|object[] */ private function smush_sequential( $file_paths ) { return $this->request_sequential ->set_streaming_enabled( $this->streaming_enabled ) ->set_on_complete( function ( $response, $response_size_key, $size_file_path ) { $parsed_response = $this->parse_response( $response, $size_file_path ); return $this->handle_response( $parsed_response, $response_size_key, $size_file_path ); } )->do_requests( $file_paths ); } /** * @param $file_path string * @param $size_key string * * @return bool|object */ public function smush_file( $file_path, $size_key = '' ) { return $this->request_sequential ->set_streaming_enabled( false ) ->set_on_complete( function ( $response, $size_key, $file_path ) { $parsed_response = $this->parse_response( $response, $file_path ); return $this->handle_response( $parsed_response, $size_key, $file_path ); } ) ->do_request( $file_path, $size_key ); } /** * Validates and smushes a single file. Use this for standalone files without attachment IDs. * * @param string $file_path Absolute path to the file. * @param string $size_key Size key identifier. * * @return bool|object Returns response object on success, false on failure. */ public function validate_and_smush_file( $file_path, $size_key = '' ) { // Validate the file before processing $validation_error = $this->validate_file( $file_path ); if ( is_wp_error( $validation_error ) ) { $this->add_error( $size_key, $validation_error->get_error_code(), $validation_error->get_error_message(), $validation_error->get_error_data() ); return false; } return $this->smush_file( $file_path, $size_key ); } /** * Validates a file before processing. * * @param string $file_path Absolute path to the file. * * @return true|WP_Error Returns true if valid, WP_Error otherwise. */ private function validate_file( $file_path ) { $dir_name = trailingslashit( dirname( $file_path ) ); // Check if file exists and the directory is writable. if ( empty( $file_path ) ) { return new WP_Error( 'empty_path', esc_html__( 'File path is empty', 'wp-smushit' ) ); } if ( ! file_exists( $file_path ) || ! is_file( $file_path ) ) { // Check that the file exists. /* translators: %s: file path */ return new WP_Error( 'file_not_found', /* translators: %s: file path */ sprintf( __( 'Skipped (%s). File not found.', 'wp-smushit' ), basename( $file_path ) ) ); } if ( ! is_writable( $dir_name ) ) { // Check that the file is writable. /* translators: %s: directory name */ return new WP_Error( 'not_writable', /* translators: %s: directory name */ sprintf( __( '%s is not writable', 'wp-smushit' ), $dir_name ) ); } $file_size = filesize( $file_path ); // Check if file exists. if ( 0 === (int) $file_size ) { return new WP_Error( 'file_not_found', /* translators: %s: file path */ sprintf( __( 'Skipped (%s). File not found.', 'wp-smushit' ), basename( $file_path ) ) ); } // Check size limit. $max_size = $this->max_size; $size_limit_code = 'size_limit'; if ( $file_size > $max_size ) { /* translators: %s: image size */ return new WP_Error( $size_limit_code, /* translators: %s: file path */ sprintf( __( 'Skipped (%s). File size limit of 5MB exceeded', 'wp-smushit' ), size_format( $file_size, 1 ) ), array( 'file_name' => basename( $file_path ) ) ); } return true; } public function set_request_sequential( $request_sequential ) { $this->request_sequential = $request_sequential; return $this; } public function get_request_sequential() { return $this->request_sequential; } /** * Set the maximum file size for validation. * * @param int $max_size Maximum file size in bytes. * * @return $this */ public function set_max_size( $max_size ) { $this->max_size = $max_size; return $this; } /** * @param $parsed_response WP_Error|object * @param $size_key string * @param $file_path string * * @return bool|object */ private function handle_response( $parsed_response, $size_key, $file_path ) { if ( is_wp_error( $parsed_response ) ) { $this->add_error( $size_key, $parsed_response->get_error_code(), $parsed_response->get_error_message(), $parsed_response->get_error_data() ); return false; } $data = $parsed_response; if ( $data->bytes_saved > 0 ) { if ( ! empty( $data->image_url ) ) { $saved_from_image_url = $this->save_from_image_url( $data->image_url, $file_path, $data->image_md5 ); if ( is_wp_error( $saved_from_image_url ) ) { $this->add_error( $size_key, self::$image_not_saved_from_url, /* translators: %s: Error message. */ sprintf( __( 'Smush was successful but we were unable to save from URL: %s.', 'wp-smushit' ), $saved_from_image_url->get_error_message() ), array( 'original_code' => $saved_from_image_url->get_error_code(), 'original_message' => $saved_from_image_url->get_error_message(), ) ); return false; } } else { $optimized_image_saved = $this->save_smushed_image_file( $file_path, $data->image ); if ( ! $optimized_image_saved ) { $this->add_error( $size_key, 'image_not_saved', /* translators: %s: File path. */ sprintf( __( 'Smush was successful but we were unable to save the file due to a file system error: [%s].', 'wp-smushit' ), $this->upload_dir->get_human_readable_path( $file_path ) ) ); return false; } } } // No need to pass image data any further if ( isset( $data->image ) ) { $data->image = null; } if ( isset( $data->image_md5 ) ) { $data->image_md5 = null; } // Check for API message and store in db. if ( ! empty( $data->api_message ) ) { $this->add_api_message( (array) $data->api_message ); } return $data; } /** * @param $input_stream resource * @param $target_file_path * @param $file_md5 * @param $chunk_size * * @return true|WP_Error */ protected function save_from_resource( $input_stream, $target_file_path, $file_md5, $chunk_size ) { if ( ! function_exists( 'wp_tempnam' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $timer = new Timer(); $timer->start(); $error = false; $temp_name = wp_tempnam(); do { if ( empty( $temp_name ) ) { $error = new WP_Error( 'temp-file-creation-error', 'Error creating temporary file' ); break; } $output_stream = fopen( $temp_name, "wb" ); do { $chunk_copied_successfully = stream_copy_to_stream( $input_stream, $output_stream, $chunk_size ); if ( $chunk_copied_successfully === false ) { break; } } while ( ! feof( $input_stream ) ); // Close the input and output streams fclose( $input_stream ); fclose( $output_stream ); if ( $chunk_copied_successfully === false ) { $error = new WP_Error( 'temp-file-save-error', 'Error saving temp file' ); break; } $hash_equals = hash_equals( $file_md5, md5_file( $temp_name ) ); if ( ! $hash_equals ) { $error = new WP_Error( 'file-hash-mismatch', 'File hash mismatch' ); break; } $target_file_name = basename( $target_file_path ); $type = $this->wp_get_image_mime( $temp_name ); if ( ! str_starts_with( $type, 'image/' ) ) { $error = new WP_Error( 'invalid-file-type', sprintf( 'Invalid file type. Calculated type for file named %s at %s is %s', $target_file_name, $temp_name, $type ) ); break; } $file_copied = copy( $temp_name, $target_file_path ); if ( ! $file_copied ) { $error = new WP_Error( 'error-moving-file', 'Error moving file' ); break; } $permissions = $this->get_permissions_for_image( $target_file_path ); chmod( $target_file_path, $permissions ); } while ( 0 ); @unlink( $temp_name ); $time = $timer->end(); if ( $error ) { $this->logger->notice( sprintf( 'File could not be saved: %s', $error->get_error_message() ) ); return $error; } else { $this->logger->notice( sprintf( 'File saved successfully in %s seconds', $time ) ); return true; } } public function save_from_image_url( $image_url, $target_file_path, $file_md5, $chunk_size = null ) { if ( is_null( $chunk_size ) ) { $chunk_size = self::$default_chunk_size; } try { $client = new Client(); $response = $client->get( $image_url, [ 'stream' => true, ] ); $input_stream = $response->getBody()->detach(); return $this->save_from_resource( $input_stream, $target_file_path, $file_md5, $chunk_size ); } catch ( \Exception $exception ) { $this->logger->error( sprintf( 'Error fetching image from URL: %s', $exception->getMessage() ) ); $code = $exception->getCode(); $code = empty( $code ) ? 'timeout' : $code; return new WP_Error( $code, 'Error fetching image from URL' ); } } protected function save_smushed_image_file( $file_path, $image ) { $pre = apply_filters( 'wp_smush_pre_image_write', false, $file_path, $image ); if ( $pre !== false ) { $this->logger->notice( 'Another plugin/theme short circuited the image write operation using the wp_smush_pre_image_write filter.' ); // Assume that the plugin/theme responsible took care of it return true; } $permissions = $this->get_permissions_for_image( $file_path ); // Save the new file $success = $this->put_smushed_image_file( $file_path, $image ); chmod( $file_path, $permissions ); return $success; } private function put_smushed_image_file( $file_path, $image ) { $temp_file = $file_path . '.tmp'; $success = $this->put_image_using_temp_file( $file_path, $image, $temp_file ); // Clean up if ( $this->fs->file_exists( $temp_file ) ) { $this->fs->unlink( $temp_file ); } return $success; } private function put_image_using_temp_file( $file_path, $image, $temp_file ) { $file_written = file_put_contents( $temp_file, $image ); if ( ! $file_written ) { return false; } $renamed = rename( $temp_file, $file_path ); if ( $renamed ) { return true; } $copied = $this->fs->copy( $temp_file, $file_path ); if ( $copied ) { return true; } return false; } private function add_api_message( $api_message = array() ) { if ( empty( $api_message ) || ! count( $api_message ) || empty( $api_message['timestamp'] ) || empty( $api_message['message'] ) ) { return; } $o_api_message = get_site_option( 'wp-smush-api_message', array() ); if ( array_key_exists( $api_message['timestamp'], $o_api_message ) ) { return; } $message = array(); $message[ $api_message['timestamp'] ] = array( 'message' => sanitize_text_field( $api_message['message'] ), 'type' => sanitize_text_field( $api_message['type'] ), 'status' => 'show', ); update_site_option( 'wp-smush-api_message', $message ); } /** * @param $response * @param $file_path string * * @return object|WP_Error */ private function parse_response( $response, $file_path ) { $error = new WP_Error(); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); if ( strpos( $error_message, 'SSL CA cert' ) !== false ) { $error->add( self::$error_ssl_cert, $error_message, array( 'original_code' => $response->get_error_code(), 'original_message' => $error_message, ) ); return $error; } else if ( strpos( $error_message, 'timed out' ) !== false ) { $error->add( self::$error_time_out, esc_html__( "Skipped due to a timeout error. You can increase the request timeout to make sure Smush has enough time to process larger files. define('WP_SMUSH_TIMEOUT', 150);", 'wp-smushit' ), array( 'original_code' => $response->get_error_code(), 'original_message' => $error_message, ) ); return $error; } else { $error->add( self::$error_posting_to_api, /* translators: %s: Error message. */ sprintf( __( 'Error posting to API: %s', 'wp-smushit' ), $error_message ), array( 'original_code' => $response->get_error_code(), 'original_message' => $error_message, ) ); return $error; } } $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $response_code ) { $non_200_body = wp_remote_retrieve_body( $response ); $non_200_json = $non_200_body ? json_decode( $non_200_body ) : null; if ( ! empty( $non_200_json->data ) ) { // We got a pre-formatted error from the API $error_message = $non_200_json->data; } else if ( strpos( wp_remote_retrieve_response_message( $response ), 'Gateway Timeout' ) !== false ) { $error->add( self::$error_gateway_time_out, esc_html__( 'The request is taking longer than expected. Please check back in a few moments.', 'wp-smushit' ), array( 'original_code' => $response_code, 'original_message' => wp_remote_retrieve_response_message( $response ), ) ); return $error; } else { // Make an error from the response message $error_message = sprintf( /* translators: 1: Error code, 2: Error message. */ __( 'Error posting to API: %1$s %2$s', 'wp-smushit' ), $response_code, wp_remote_retrieve_response_message( $response ) ); } $error->add( self::$response_code_non_200, $error_message, array( 'original_code' => $response_code, 'original_message' => "Received response code $response_code", ) ); return $error; } $json = json_decode( wp_remote_retrieve_body( $response ) ); if ( empty( $json->success ) ) { $error_message = ! empty( $json->data ) ? $json->data : __( "Image couldn't be smushed", 'wp-smushit' ); $error->add( 'unsuccessful_smush', $error_message ); return $error; } if ( empty( $json->data ) || empty( $json->data->before_size ) || empty( $json->data->after_size ) ) { $error->add( 'no_data', __( 'Unknown API error', 'wp-smushit' ) ); return $error; } $data = $json->data; $data->bytes_saved = isset( $data->bytes_saved ) ? (int) $data->bytes_saved : 0; $optimized_image_larger = $data->after_size > $data->before_size; if ( $optimized_image_larger ) { $error->add( 'optimized_image_larger', /* translators: 1: File path, 2: Savings bytes. */ sprintf( 'The smushed image is larger than the original image [%s] (bytes saved %d), keep original image.', $this->upload_dir->get_human_readable_path( $file_path ), $data->bytes_saved ) ); return $error; } if ( empty( $data->image_url ) ) { $image = empty( $data->image ) ? '' : $data->image; if ( $data->bytes_saved > 0 ) { // Because of the API response structure, the following should only be done when there are some bytes_saved. if ( $data->image_md5 !== md5( $image ) ) { $error_message = __( 'Smush data corrupted, try again.', 'wp-smushit' ); $error->add( 'data_corrupted', $error_message ); return $error; } if ( ! empty( $image ) ) { $data->image = base64_decode( $data->image ); } } } return $data; } /** * @param $response WP_Error|object * * @return bool */ private function is_network_error( $response ) { if ( ! is_wp_error( $response ) ) { return false; } $network_error_codes = $this->get_network_error_codes(); foreach ( $response->get_error_codes() as $error_code ) { if ( in_array( $error_code, $network_error_codes, true ) ) { return true; } } return false; } /** * @return bool */ public function parallel_available_on_server() { return $this->request_multiple->is_supported(); } /** * @param bool $smush_parallel * * @return Smusher */ public function set_smush_parallel( $smush_parallel ) { $this->smush_parallel = $smush_parallel; return $this; } public function get_request_multiple() { return $this->request_multiple; } /** * @param Smush_Request $request_multiple * * @return Smusher */ public function set_request_multiple( $request_multiple ) { $this->request_multiple = $request_multiple; return $this; } public function has_errors() { return $this->get_errors()->has_errors(); } public function get_errors() { return $this->errors; } /** * @param $errors WP_Error * * @return void */ private function set_errors( $errors ) { $this->errors = $errors; } /** * @param $size_key string * @param $code string * @param $message string * * @return void */ private function add_error( $size_key, $code, $message, $data = array() ) { $size_key_format = empty( $size_key ) ? '' : "[$size_key] "; // Log the error $this->logger->error( $size_key_format . $message ); // Add the error $this->errors->add( $code, $size_key_format . $message ); if ( ! empty( $data ) ) { $this->errors->add_data( $data, $code ); } } /** * @param $size_key string * @param $code string * @param $message string * * @return void */ private function add_warning( $size_key, $code, $message, $data = array() ) { // Log the warning $this->logger->warning( "[$size_key] $message" ); // Add the warning $this->warnings->add( $code, "[$size_key] $message" ); if ( ! empty( $data ) ) { $this->warnings->add_data( $data, $code ); } } private function has_warning( $code ) { return ! empty( $this->warnings->get_error_message( $code ) ); } /** * @param $warnings WP_Error * * @return void */ private function set_warnings( $warnings ) { $this->warnings = $warnings; } public function get_warnings() { return $this->warnings; } /** * @param $code string * * @return bool */ private function has_error( $code ) { return ! empty( $this->errors->get_error_message( $code ) ); } /** * @param $file_data string|array * * @return array */ private function get_file_path_and_url( $file_data ) { if ( is_string( $file_data ) ) { $file_path = $file_data; $file_url = ''; } else { $file_path = $this->array_utils->get_array_value( $file_data, 'path' ); $file_url = $this->array_utils->get_array_value( $file_data, 'url' ); } return array( $file_path, $file_url ); } private function get_permissions_for_image( $file_path ) { clearstatcache(); $perms = fileperms( $file_path ) & 0777; // Some servers are having issue with file permission, this should fix it. if ( empty( $perms ) ) { // Source: WordPress Core. $stat = stat( dirname( $file_path ) ); $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits. } return $perms; } private function maybe_track_image_url_error( $time_elapsed ) { if ( $this->has_error( self::$image_not_saved_from_url ) ) { $this->track_error( $this->errors, self::$image_not_saved_from_url, $time_elapsed ); } } private function maybe_disable_streaming() { // If the constant is defined or disabled, do nothing. if ( defined( 'WP_SMUSH_USE_STREAMS' ) || ! $this->streaming_enabled ) { return; } $error_counts = $this->error_counts->get( array() ); $max_occurrences = empty( $error_counts ) ? 0 : max( $error_counts ); if ( $max_occurrences < 3 ) { $this->count_error_types(); } else { $this->options->disable_streaming(); } } /** * @return bool */ private function has_error_worth_retrying() { $errors_that_should_be_retried = array( self::$image_not_saved_from_url, ); foreach ( $errors_that_should_be_retried as $error_code ) { if ( $this->has_error( $error_code ) ) { return true; } } return false; } protected function get_type_label() { return 'Classic'; } private function add_warnings( $response, $size_key ) { if ( is_wp_error( $response ) ) { /** * @var WP_Error $error */ $error = $response; $this->add_warning( $size_key, $error->get_error_code(), $error->get_error_message(), $error->get_error_data() ); } } private function maybe_track_network_errors( $time_elapsed ) { foreach ( $this->get_network_error_codes() as $error_code ) { if ( $this->has_warning( $error_code ) ) { $this->track_error( $this->warnings, $error_code, $time_elapsed ); } elseif ( $this->has_error( $error_code ) ) { $this->track_error( $this->errors, $error_code, $time_elapsed ); } } } /** * @param $haystack WP_Error * @param $error_code string * @param $time_elapsed * * @return void */ private function track_error( $haystack, $error_code, $time_elapsed ) { $error_data = $haystack->get_error_data( $error_code ); $original_code = $this->array_utils->get_array_value( $error_data, 'original_code' ); $original_message = $this->array_utils->get_array_value( $error_data, 'original_message' ); if ( $original_code && $original_message ) { $this->product_analytics->maybe_track_error( $error_code, $original_code, $original_message, array( 'Smush Type' => $this->get_type_label(), 'Time Elapsed' => $time_elapsed, ) ); } } /** * @return string[] */ private function get_network_error_codes() { return array( self::$error_posting_to_api, self::$error_time_out, self::$error_ssl_cert, self::$response_code_non_200, ); } /** * @return void */ private function count_error_types() { $increment_keys = array(); $errors_and_warnings = array_merge( $this->errors->get_error_codes(), $this->warnings->get_error_codes() ); if ( empty( $errors_and_warnings ) ) { return; } foreach ( $errors_and_warnings as $code ) { $error_data = $this->warnings->get_error_data( $code ); $original_code = $this->array_utils->get_array_value( $error_data, 'original_code' ); $full_code = $code; if ( $original_code ) { $full_code .= "_$original_code"; } $increment_keys[ $full_code ] = $full_code; } if ( ! empty( $increment_keys ) ) { $this->error_counts->increment_values( array_values( $increment_keys ) ); } } public function reset_error_counts() { $this->error_counts->delete(); } /** * @param $file * * @return string * @see \wp_get_image_mime() */ function wp_get_image_mime( $file ) { /* * Use exif_imagetype() to check the mimetype if available or fall back to * getimagesize() if exif isn't available. If either function throws an Exception * we assume the file could not be validated. */ try { if ( is_callable( 'exif_imagetype' ) ) { $imagetype = exif_imagetype( $file ); $mime = ( $imagetype ) ? image_type_to_mime_type( $imagetype ) : false; } elseif ( function_exists( 'getimagesize' ) ) { // Don't silence errors when in debug mode, unless running unit tests. if ( defined( 'WP_DEBUG' ) && WP_DEBUG && ! defined( 'WP_RUN_CORE_TESTS' ) ) { // Not using wp_getimagesize() here to avoid an infinite loop. $imagesize = getimagesize( $file ); } else { $imagesize = @getimagesize( $file ); } $mime = ( isset( $imagesize['mime'] ) ) ? $imagesize['mime'] : false; } else { $mime = false; } if ( false !== $mime ) { return $mime; } $magic = file_get_contents( $file, false, null, 0, 12 ); if ( false === $magic ) { return false; } /* * Add WebP fallback detection when image library doesn't support WebP. * Note: detection values come from LibWebP, see * https://github.com/webmproject/libwebp/blob/master/imageio/image_dec.c#L30 */ $magic = bin2hex( $magic ); if ( // RIFF. ( str_starts_with( $magic, '52494646' ) ) && // WEBP. ( 16 === strpos( $magic, '57454250' ) ) ) { $mime = 'image/webp'; } /** Custom Code Start */ if ( strpos( $magic, '6674797061766966' ) !== false ) { $mime = 'image/avif'; } /** Custom Code End */ } catch ( Exception $e ) { $mime = false; } return $mime; } /** * Get option_id_smush_error_counts. * * @return string */ public static function get_smush_error_counts_option_id() { return self::$option_id_smush_error_counts; } } smush/class-smush-media-item-stats.php 0000644 00000001224 15252476777 0014050 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Media\Media_Item_Stats; class Smush_Media_Item_Stats extends Media_Item_Stats { private $lossy = false; /** * @return mixed */ public function is_lossy() { return $this->lossy; } /** * @param mixed $lossy * * @return Smush_Media_Item_Stats */ public function set_lossy( $lossy ) { $this->lossy = $lossy; return $this; } public function to_array() { $array = parent::to_array(); $array['lossy'] = $this->is_lossy(); return $array; } public function from_array( $array ) { parent::from_array( $array ); $this->set_lossy( ! empty( $array['lossy'] ) ); } } smush/class-dir-smusher-options-provider.php 0000644 00000000475 15252476777 0015336 0 ustar 00 <?php namespace Smush\Core\Smush; class Dir_Smusher_Options_Provider extends Smusher_Options_Provider { public function get_options() { return parent::get_options() ->set_lossy_level( $this->settings->get_dir_lossy_level_setting() ) ->set_strip_exif( $this->settings->get_dir_strip_exif_setting() ); } } smush/class-smush-request-guzzle-multiple.php 0000644 00000013405 15252476777 0015544 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\File_System; use Smush\Core\Server_Utils; use Smush_Vendor\GuzzleHttp\Client; use Smush_Vendor\GuzzleHttp\Pool; use Smush_Vendor\GuzzleHttp\Psr7\Response; use Smush_Vendor\GuzzleHttp\Psr7\Utils; use Smush_Vendor\GuzzleHttp\Exception\RequestException; use Smush_Vendor\GuzzleHttp\Exception\ConnectException; use Smush_Vendor\GuzzleHttp\Exception\ClientException; use Smush_Vendor\GuzzleHttp\Exception\ServerException; use WP_Error; class Smush_Request_Guzzle_Multiple extends Smush_Request { /** * @var Client */ private $client; /** * @var Server_Utils */ private $server_utils; public function __construct( $options ) { $this->client = new Client(); $this->server_utils = new Server_Utils(); parent::__construct( $options ); } public function do_requests( $file_paths ) { $responses = array(); $request_generator = $this->make_request_generator(); $pool = new Pool( $this->client, $request_generator( $file_paths ), array( 'concurrency' => count( $file_paths ), 'fulfilled' => function ( $response, $size_key ) use ( $file_paths, &$responses ) { $file_path = $file_paths[ $size_key ]; // Convert to a response that looks like standard WP HTTP API responses $response = $this->multi_to_singular_response( $response ); $this->do_action( $response, $file_path ); // Call the actual on complete callback $responses[ $size_key ] = call_user_func( $this->get_on_complete(), $response, $size_key, $file_path ); }, 'rejected' => function ( $reason, $size_key ) use ( $file_paths, &$responses ) { list( $reason_code, $reason_message ) = $this->extract_error_details( $reason ); $file_path = $file_paths[ $size_key ]; $response = new WP_Error( $reason_code, $reason_message ); $this->do_action( $response, $file_path ); // Call the actual on complete callback $responses[ $size_key ] = call_user_func( $this->get_on_complete(), $response, $size_key, $file_path ); }, ) ); $pool->promise()->wait(); return $responses; } private function extract_error_details( $error ) { $error_code = ''; $error_message = ''; if ( is_a( $error, '\Exception' ) ) { $error_code = $error->getCode(); $error_message = $error->getMessage(); } elseif ( is_string( $error ) ) { $error_code = $error; $error_message = $error; } if ( empty( $error_code ) && ! empty( $error_message ) ) { $error_message_lowercase = strtolower( $error_message ); if ( $error instanceof ConnectException ) { $error_code = $this->map_connect_exception_error_code( $error_message_lowercase ); } elseif ( $error instanceof ClientException ) { $error_code = 'client-error'; } elseif ( $error instanceof ServerException ) { $error_code = 'server-error'; } elseif ( $error instanceof RequestException ) { $error_code = 'request-error'; } } if ( empty( $error_code ) ) { $error_code = 'unknown-error'; $error_message = $error_message ? $error_message : 'An unknown error occurred when trying to send the request.'; } return array( $error_code, $error_message ); } private function map_connect_exception_error_code( $error_message_lowercase ) { $error_map = array( 'curl error 35' => 'ssl-error', 'ssl' => 'ssl-error', 'curl error 28' => 'timeout-error', 'timed out' => 'timeout-error', 'curl error 6' => 'host-resolution-error', 'could not resolve host' => 'host-resolution-error', 'curl error 7' => 'connection-failed-error', 'failed to connect' => 'connection-failed-error', ); foreach ( $error_map as $error_string => $code ) { if ( false !== strpos( $error_message_lowercase, $error_string ) ) { return $code; } } return 'connection-error'; } /** * @param $guzzle_response Response * * @return array */ private function multi_to_singular_response( $guzzle_response ) { return array( 'body' => $guzzle_response->getBody()->getContents(), 'response' => array( 'code' => $guzzle_response->getStatusCode() ), ); } /** * @return \Closure */ private function make_request_generator() { return function ( $file_paths ) { foreach ( $file_paths as $size_key => $file_path ) { yield $size_key => function () use ( $file_path ) { return $this->client->postAsync( $this->get_url(), array( 'headers' => $this->get_api_request_headers( $file_path ), 'body' => $this->get_body( $file_path ), 'timeout' => $this->get_timeout(), 'user-agent' => $this->get_user_agent(), ) ); }; } }; } private function get_body( $file_path ) { if ( $this->is_streaming_enabled() ) { return Utils::streamFor( fopen( $file_path, 'rb' ) ); } else { return $this->get_full_file_contents( $file_path ); } } /** * @param $response * @param $file_path * * @return void */ private function do_action( $response, $file_path ) { do_action( 'smush_http_api_debug', $response, array( 'url' => $this->get_url(), 'headers' => $this->get_api_request_headers( $file_path ), 'type' => 'POST', 'data' => "[streamed $file_path]", 'timeout' => $this->get_timeout(), 'user-agent' => $this->get_user_agent(), ) ); } public function is_supported() { $curl_version = function_exists( 'curl_version' ) ? curl_version() : array( 'version' => 0 ); $curl_version_supported = version_compare( $curl_version['version'], '7.19.4', '>=' ); $allow_url_fopen_supported = $this->server_utils->is_function_supported( 'allow_url_fopen' ); $php_version_supported = version_compare( PHP_VERSION, '7.2.5', '>=' ); return $php_version_supported && ( $allow_url_fopen_supported || $curl_version_supported ); } } smush/class-smush-controller.php 0000644 00000004401 15252476777 0013064 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Array_Utils; use Smush\Core\Backups\Backups; use Smush\Core\Controller; use Smush\Core\Media\Media_Item; use Smush\Core\Security\Security_Utils; use Smush\Core\Settings; use Smush\Core\Stats\Global_Stats; use Smush\Core\Stats\Media_Item_Optimization_Global_Stats_Persistable; use Smush\Core\Webp\Webp_Converter; use WP_Smush; class Smush_Controller extends Controller { private static $global_stats_option_id = 'wp-smush-optimization-global-stats'; private static $smush_optimization_order = 40; private $global_stats; /** * Static instance * * @var self */ private static $instance; public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->global_stats = Global_Stats::get(); $this->register_filter( 'wp_smush_optimizations', array( $this, 'add_smush_optimization', ), self::$smush_optimization_order, 2 ); $this->register_filter( 'wp_smush_global_optimization_stats', array( $this, 'add_smush_global_stats' ) ); $this->register_filter( 'wp_smush_optimization_global_stats_instance', array( $this, 'create_global_stats_instance', ), 10, 2 ); $this->register_action( 'wp_smush_image_sizes_deleted', array( $this->global_stats, 'mark_as_outdated' ) ); $this->register_action( 'wp_smush_image_sizes_added', array( $this->global_stats, 'mark_as_outdated' ) ); } /** * @param $optimizations array * @param $media_item Media_Item * * @return array */ public function add_smush_optimization( $optimizations, $media_item ) { $optimization = new Smush_Optimization( $media_item ); $optimizations[ $optimization->get_key() ] = $optimization; return $optimizations; } public function add_smush_global_stats( $stats ) { $stats[ Smush_Optimization::get_key() ] = new Media_Item_Optimization_Global_Stats_Persistable( self::$global_stats_option_id, new Smush_Optimization_Global_Stats() ); return $stats; } public function create_global_stats_instance( $original, $key ) { if ( $key === Smush_Optimization::get_key() ) { return new Smush_Optimization_Global_Stats(); } return $original; } } smush/class-smush-request-wp-sequential.php 0000644 00000004461 15252476777 0015173 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Api\Backoff; class Smush_Request_WP_Sequential extends Smush_Request { /** * @var Backoff */ private $backoff; /** * @var int */ private $retry_attempts; /** * @var int */ private $retry_wait; public function __construct( $options ) { $this->backoff = new Backoff(); $this->retry_attempts = WP_SMUSH_RETRY_ATTEMPTS; $this->retry_wait = WP_SMUSH_RETRY_WAIT; parent::__construct( $options ); } public function do_requests( $file_paths ) { $responses = array(); foreach ( $file_paths as $size_key => $file_path ) { $responses[ $size_key ] = $this->do_request( $file_path, $size_key ); } return $responses; } private function get_api_request_args( $file_path ) { return array( 'headers' => $this->get_api_request_headers( $file_path ), 'body' => $this->get_full_file_contents( $file_path ), 'timeout' => $this->get_timeout(), 'user-agent' => $this->get_user_agent(), ); } /** * @param array $request * * @return array|\WP_Error */ private function make_request_with_backoff( $request ) { return $this->backoff->set_wait( $this->retry_wait ) ->set_max_attempts( $this->retry_attempts ) ->enable_jitter() ->set_decider( array( $this, 'should_retry' ) ) ->run( function () use ( $request ) { return wp_remote_post( $this->get_url(), $request ); } ); } public function should_retry( $response ) { return $this->retry_attempts > 0 && ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ); } /** * @param $file_path * @param $size_key * * @return mixed */ public function do_request( $file_path, $size_key ) { $request = $this->get_api_request_args( $file_path ); $response = $this->make_request_with_backoff( $request ); do_action( 'smush_http_api_debug', $response, $request ); return call_user_func( $this->get_on_complete(), $response, $size_key, $file_path ); } /** * @param int $retry_attempts */ public function set_retry_attempts( $retry_attempts ) { $this->retry_attempts = $retry_attempts; } public function is_supported() { return function_exists( 'wp_remote_post' ); } } smush/class-smush-optimization-global-stats.php 0000644 00000004713 15252476777 0016027 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Media\Media_Item_Optimization_Global_Stats; class Smush_Optimization_Global_Stats extends Media_Item_Optimization_Global_Stats { private $lossy_count = 0; public function from_array( $array ) { parent::from_array( $array ); $this->set_lossy_count( (int) $this->get_array_value( $array, 'lossy_count' ) ); } public function to_array() { $array = parent::to_array(); $array['lossy_count'] = $this->get_lossy_count(); return $array; } /** * @param $attachment_id int * @param $item_stats Smush_Media_Item_Stats * * @return boolean */ public function add_item_stats( $attachment_id, $item_stats ) { $added = parent::add_item_stats( $attachment_id, $item_stats ); if ( $added && $item_stats->is_lossy() ) { $this->set_lossy_count( $this->get_lossy_count() + 1 ); } return $added; } /** * @param $attachment_id int * @param $item_stats Smush_Media_Item_Stats * * @return boolean */ public function subtract_item_stats( $attachment_id, $item_stats ) { $subtracted = parent::subtract_item_stats( $attachment_id, $item_stats ); if ( $subtracted && $item_stats->is_lossy() ) { // Assuming that we added to the lossy count $this->set_lossy_count( max( $this->get_lossy_count() - 1, 0 ) ); } return $subtracted; } /** * @param $addend Smush_Optimization_Global_Stats * * @return void */ public function add( $addend ) { parent::add( $addend ); $this->set_lossy_count( $this->get_lossy_count() + $addend->get_lossy_count() ); } /** * @param $subtrahend Smush_Optimization_Global_Stats * * @return void */ public function subtract( $subtrahend ) { parent::subtract( $subtrahend ); $this->set_lossy_count( max( $this->get_lossy_count() - $subtrahend->get_lossy_count(), 0 ) ); } /** * @return int */ public function get_lossy_count() { return $this->lossy_count; } /** * @param int $lossy_count * * @return Smush_Optimization_Global_Stats */ public function set_lossy_count( $lossy_count ) { $this->lossy_count = $lossy_count; return $this; } /** * Get key. * * @return mixed */ public static function get_key() { return self::$key; } /** * Get lossy_meta_key. * * @return mixed */ public static function get_lossy_meta_key() { return self::$lossy_meta_key; } /** * Get smush_meta_key. * * @return mixed */ public static function get_smush_meta_key() { return self::$smush_meta_key; } } smush/class-smush-optimization.php 0000644 00000027324 15252476777 0013440 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Media\Media_Item; use Smush\Core\Media\Media_Item_Optimization; use Smush\Core\Media\Media_Item_Size; use Smush\Core\Media\Media_Item_Stats; use Smush\Core\Modules\Helpers\WhiteLabel; use Smush\Core\Settings; use Smush\Core\Smush\Smusher_Options_Provider; use WP_Error; /** * Smushes a media item and updates the stats. */ class Smush_Optimization extends Media_Item_Optimization { private static $key = 'smush_optimization'; private static $smush_meta_key = 'wp-smpro-smush-data'; private static $lossy_meta_key = 'wp-smush-lossy'; /** * White label helper used for replacing branding terms. * * @var WhiteLabel */ private $whitelabel; /** * @var Media_Item_Stats */ private $stats; /** * @var Media_Item_Stats[] */ private $size_stats = array(); /** * @var Media_Item */ private $media_item; /** * @var array */ private $smush_meta; /** * @var int */ private $keep_exif; /** * @var bool */ private $lossy_level; /** * @var string */ private $api_version; /** * @var Settings */ private $settings; private $reset_properties = array( 'stats', 'size_stats', 'smush_meta', 'keep_exif', 'lossy_level', 'api_version', ); /** * @var Smusher */ private $smusher; /** * Each Smush API call returns a flag indicating whether the user is a pro user * @var bool */ private $is_premium; public function __construct( $media_item ) { $this->media_item = $media_item; $this->settings = Settings::get_instance(); $smusher_options = ( new Smusher_Options_Provider() )->get_options(); $this->smusher = new Smusher( $smusher_options ); $this->whitelabel = new WhiteLabel(); } public static function get_smush_meta_key() { return self::$smush_meta_key; } public static function get_lossy_meta_key() { return self::$lossy_meta_key; } public static function get_key() { return self::$key; } public function get_name() { return $this->whitelabel->replace_branding_terms( __( 'Smush', 'wp-smushit' ) ); } public function get_stats() { if ( is_null( $this->stats ) ) { $this->stats = $this->prepare_stats(); } return $this->stats; } public function set_stats( $stats ) { $this->stats = $stats; } private function get_meta_sizes() { $smush_meta = $this->get_smush_meta(); return empty( $smush_meta['sizes'] ) ? array() : $smush_meta['sizes']; } private function get_size_meta( $size_key ) { $sizes = $this->get_meta_sizes(); $size = empty( $sizes[ $size_key ] ) ? array() : (array) $sizes[ $size_key ]; return empty( $size ) ? array() : $size; } private function size_meta_exists( $size_key ) { return ! empty( $this->get_size_meta( $size_key ) ); } public function get_size_stats( $size_key ) { if ( empty( $this->size_stats[ $size_key ] ) ) { $this->size_stats[ $size_key ] = $this->prepare_size_stats( $size_key ); } return $this->size_stats[ $size_key ]; } private function prepare_size_stats( $size_key ) { $stats = new Media_Item_Stats(); $stats->from_array( $this->get_size_meta( $size_key ) ); return $stats; } public function save() { $meta = $this->make_smush_meta(); if ( ! empty( $meta ) ) { update_post_meta( $this->media_item->get_id(), self::$smush_meta_key, $meta ); // TODO: the separate lossy meta is only necessary for the backup global stats, if enough time has passed and enough people have moved to the new stats then we can remove it if ( $this->get_lossy_level() ) { update_post_meta( $this->media_item->get_id(), self::$lossy_meta_key, 1 ); } else { delete_post_meta( $this->media_item->get_id(), self::$lossy_meta_key ); } $this->reset(); } } public function is_optimized() { return ! $this->get_stats()->is_empty(); } public function should_optimize() { if ( $this->media_item->is_skipped() || $this->media_item->has_errors() ) { return false; } return ! empty( $this->get_sizes_to_smush() ); } public function should_reoptimize() { return $this->should_resmush(); } public function optimize() { if ( ! $this->should_optimize() ) { return false; } $media_item = $this->media_item; $file_paths = array_map( function ( $size ) { return $size->get_file_path(); }, $this->get_sizes_to_smush() ); $responses = $this->smusher->smush( $file_paths ); $success_responses = array_filter( $responses ); if ( count( $success_responses ) !== count( $responses ) ) { return false; } $media_item_stats = $this->create_media_item_stats_instance(); foreach ( $responses as $size_key => $data ) { $this->update_from_response( $size_key, $data, $media_item_stats ); } $this->set_stats( $media_item_stats ); if ( $media_item_stats->get_bytes() >= 0 ) { do_action( 'wp_smush_image_optimised', $this->media_item->get_id(), $this->make_smush_meta(), $this->media_item->get_wp_metadata() ); } // Update media item $media_item->save(); // Update smush meta $this->save(); return true; } private function prepare_stats() { $smush_meta = $this->get_smush_meta(); $stats = $this->create_media_item_stats_instance(); $stats_data = empty( $smush_meta['stats'] ) ? array() : $smush_meta['stats']; $stats->from_array( $stats_data ); $stats->set_lossy( (bool) $this->get_lossy_level() ); return $stats; } private function get_smush_meta() { if ( is_null( $this->smush_meta ) ) { $this->smush_meta = $this->fetch_smush_meta(); } return $this->smush_meta; } private function fetch_smush_meta() { $post_meta = get_post_meta( $this->media_item->get_id(), self::$smush_meta_key, true ); return empty( $post_meta ) || ! is_array( $post_meta ) ? array() : $post_meta; } public function keep_exif() { if ( is_null( $this->keep_exif ) ) { $this->keep_exif = $this->prepare_keep_exif(); } return $this->keep_exif; } private function prepare_keep_exif() { $smush_meta = $this->get_smush_meta(); return isset( $smush_meta['stats']['keep_exif'] ) ? (int) $smush_meta['stats']['keep_exif'] : 0; } public function set_keep_exif( $keep_exif ) { $this->keep_exif = (int) $keep_exif; } public function get_lossy_level() { if ( is_null( $this->lossy_level ) ) { $this->lossy_level = $this->prepare_lossy_level(); } return $this->lossy_level; } private function prepare_lossy_level() { $smush_meta = $this->get_smush_meta(); return empty( $smush_meta['stats']['lossy'] ) ? 0 : (int) $smush_meta['stats']['lossy']; } public function set_lossy_level( $lossy ) { $this->lossy_level = (int) $lossy; } public function get_api_version() { if ( is_null( $this->api_version ) ) { $this->api_version = $this->prepare_api_version(); } return $this->api_version; } private function prepare_api_version() { $smush_meta = $this->get_smush_meta(); return empty( $smush_meta['stats']['api_version'] ) ? '' : $smush_meta['stats']['api_version']; } public function set_api_version( $api_version ) { $this->api_version = $api_version; } private function make_smush_meta() { $smush_meta = $this->get_smush_meta(); // Stats $media_item_stats = $this->get_stats(); if ( ! $media_item_stats->is_empty() ) { $smush_meta['stats'] = array_merge( empty( $smush_meta['stats'] ) ? array() : $smush_meta['stats'], $media_item_stats->to_array(), array( 'keep_exif' => $this->keep_exif(), 'lossy' => $this->get_lossy_level(), 'api_version' => $this->get_api_version(), 'is_premium' => $this->is_premium(), ) ); } // Sizes foreach ( $this->size_stats as $size_key => $size_stats ) { if ( ! $size_stats->is_empty() ) { $smush_meta['sizes'][ $size_key ] = (object) $size_stats->to_array(); } } return $smush_meta; } private function should_resmush() { if ( ! $this->should_optimize() ) { return false; } if ( $this->is_next_level_available() ) { return true; } if ( $this->settings->get( 'strip_exif' ) && $this->keep_exif() ) { return true; } foreach ( $this->get_sizes_to_smush() as $size_key => $size ) { $is_smushed = $this->size_meta_exists( $size_key ) || $this->is_file_smushed( $size->get_file_path() ); if ( ! $is_smushed ) { return true; } } return false; } public function is_next_level_available() { $current_lossy_level = $this->get_lossy_level(); $required_lossy_level = $this->settings->get_lossy_level_setting(); return $current_lossy_level < $required_lossy_level; } private function is_file_smushed( $file_path ) { foreach ( $this->media_item->get_sizes() as $size_key => $size ) { if ( $size->get_file_path() === $file_path && $this->size_meta_exists( $size_key ) ) { return true; } } return false; } /** * @param $size_key * @param object $data * @param $media_item_stats Smush_Media_Item_Stats */ private function update_from_response( $size_key, $data, $media_item_stats ) { $size_stats = $this->get_size_stats( $size_key ); $this->set_api_version( $data->api_version ); $this->set_lossy_level( (int) $data->lossy ); $this->set_keep_exif( empty( $data->keep_exif ) ? 0 : $data->keep_exif ); $this->set_is_premium( $data->is_premium ); // Update the size stats $size_stats->from_array( $this->size_stats_from_response( $size_stats, $data ) ); // Add the size stats to the media item stats $media_item_stats->add( $size_stats ); // TODO: maybe remove the lossy count from smush stats $media_item_stats->set_lossy( (bool) $this->get_lossy_level() ); } /** * @param $existing_stats Media_Item_Stats * @param $data * * @return array */ private function size_stats_from_response( $existing_stats, $data ) { $size_before = max( $existing_stats->get_size_before(), $data->before_size ); // We want to use the oldest before size return array( 'size_before' => $size_before, 'size_after' => $data->after_size, 'time' => $data->time, ); } /** * @return WP_Error */ public function get_errors() { return $this->get_smusher()->get_errors(); } protected function reset() { foreach ( $this->reset_properties as $property ) { $this->$property = null; } } public function delete_data() { delete_post_meta( $this->media_item->get_id(), self::$smush_meta_key ); $this->reset(); } /** * @param $size Media_Item_Size * * @return bool */ public function should_optimize_size( $size ) { if ( ! $this->should_optimize() ) { return false; } return array_key_exists( $size->get_key(), $this->get_sizes_to_smush() ); } /** * @return Media_Item_Size[] */ private function get_sizes_to_smush() { return $this->media_item->get_smushable_sizes(); } /** * @return Smusher */ public function get_smusher() { return $this->smusher; } /** * @return Smush_Media_Item_Stats */ private function create_media_item_stats_instance() { return new Smush_Media_Item_Stats(); } public function get_optimized_sizes_count() { $count = 0; $sizes = $this->get_meta_sizes(); foreach ( $sizes as $size ) { if ( ! empty( $size->bytes ) ) { $count++; } } return $count; } /** * // TODO: [WPMUDEV SMUSH UI] it's probably best to get rid of this method because it's an extra pro check to care about */ public function is_premium() { if ( is_null( $this->is_premium ) ) { $this->is_premium = $this->prepare_is_premium(); } return $this->is_premium; } private function prepare_is_premium() { $smush_meta = $this->get_smush_meta(); return isset( $smush_meta['stats']['is_premium'] ) && $smush_meta['stats']['is_premium']; } private function set_is_premium( $is_premium ) { $this->is_premium = ! empty( $is_premium ); } /** * @param $smusher Smusher * * @return void */ public function set_smusher( $smusher ) { $this->smusher = $smusher; } } smush/class-smusher-options-provider.php 0000644 00000002411 15252476777 0014552 0 ustar 00 <?php namespace Smush\Core\Smush; use Smush\Core\Settings; class Smusher_Options_Provider { /** * @var Settings */ protected $settings; public function __construct() { $this->settings = Settings::get_instance(); } public function get_options() { $use_http = $this->settings->get_setting( 'wp-smush-use_http' ); $api_url = defined( 'WP_SMUSH_API_HTTP' ) ? WP_SMUSH_API_HTTP : WP_SMUSH_API; $protocol = $use_http ? 'http' : 'https'; $settings = $this->settings; return ( new Smusher_Options() ) ->set_lossy_level( $this->settings->get_lossy_level_setting() ) ->set_strip_exif( $this->settings->get( 'strip_exif' ) ) ->set_api_key( $this->settings->get_api_key() ) ->set_api_url( $api_url ) ->set_streaming_enabled( $this->settings->streaming_enabled() ) ->set_extra_headers( array() ) ->set_parallel_optimization_enabled( apply_filters( 'wp_smush_parallel_optimization', WP_SMUSH_PARALLEL ) ) ->set_protocol( $protocol ) ->set_max_size( $this->settings->get_file_size_limit() ) ->set_on_disable_streaming( function () use ( $settings ) { $settings->set( 'disable_streams', WP_SMUSH_VERSION ); } ) ->set_on_switch_to_http( function () use ( $settings ) { $settings->set_setting( 'wp-smush-use_http', 1 ); } ); } } media-library/class-media-library-last-process.php 0000644 00000020321 15252476777 0016263 0 ustar 00 <?php namespace Smush\Core\Media_Library; use Smush\Core\Array_Utils; use Smush\Core\Bulk\Background_Bulk_Smush_Controller; use Smush\Core\Controller; use Smush\Core\Helper; use Smush\Core\Settings; use Smush\Core\Threads\JSON_Record; class Media_Library_Last_Process extends Controller { private static $process_key = 'wp_smush_media_library_last_process_json'; private static $start_time = 'start_time'; private static $end_time = 'end_time'; private static $last_attachment = 'last_attachment'; private static $first_stuck_attachment = 'first_stuck_attachment'; private static $process_time_out = 120;// 2 mins. /** * @var Array_Utils */ private $array_utils; /** * @var JSON_Record */ private $record; /** * Static instance * * @var self */ private static $instance; public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } public function __construct() { $this->array_utils = new Array_Utils(); $this->record = new JSON_Record( self::$process_key ); // Register actions to cache data for displaying stuck notice of background process. $this->register_action( 'wp_smush_bulk_smush_start', array( $this, 'record_process_start_time' ), 5 ); $this->register_action( 'wp_smush_before_smush_file', array( $this, 'record_bulk_smush_last_processed_attachment' ), 5 ); if ( ! $this->should_track() ) { return; } $scan_background_process = Background_Media_Library_Scanner::get_instance()->get_background_process(); $this->register_action( $scan_background_process->action_name( 'started' ), array( $this, 'record_process_start_time' ), 5 ); $this->register_action( $scan_background_process->action_name( 'dead' ), array( $this, 'record_process_end_time' ), 5 ); $this->register_action( 'wp_smush_after_smush_file', array( $this, 'record_last_processed_attachment_elapsed_time' ), 5 ); $this->register_action( 'wp_ajax_bulk_smush_get_status', array( $this, 'check_bulk_smush_process_stuck_on_ajax_get_status' ), 5 ); // Background Bulk Smush. $this->register_action( 'wp_smush_bulk_smush_dead', array( $this, 'record_process_end_time' ), 5 ); $bulk_smush_background_process = Background_Bulk_Smush_Controller::get_instance()->get_background_process(); $this->register_action( $bulk_smush_background_process->action_name( 'cron' ), array( $this, 'check_bulk_smush_process' ), 5 ); } public function should_run() { return true; } public function should_track() { return Settings::get_instance()->get( 'usage' ); } private function get_ajax_nonce( $query_arg = '_ajax_nonce' ) { $nonce = ''; if ( $query_arg && isset( $_REQUEST[ $query_arg ] ) ) { $nonce = wp_unslash( $_REQUEST[ $query_arg ] ); } elseif ( isset( $_REQUEST['_ajax_nonce'] ) ) { $nonce = wp_unslash( $_REQUEST['_ajax_nonce'] ); } elseif ( isset( $_REQUEST['_wpnonce'] ) ) { $nonce = wp_unslash( $_REQUEST['_wpnonce'] ); } return $nonce; } public function record_bulk_smush_last_processed_attachment( $attachment_id ) { if ( ! $this->is_bulk_smush_processing() ) { return; } $this->set_last_processed_attachment( $attachment_id ); } private function is_bulk_smush_processing() { if ( ! wp_doing_ajax() || empty( $_REQUEST['action'] ) ) { return false; } $bulk_process_actions = array( 'wp_smush_bulk_smush_background_process', 'wp_smushit_bulk', ); $action = wp_unslash( $_REQUEST['action'] ); foreach ( $bulk_process_actions as $bulk_action ) { if ( str_starts_with( $action, $bulk_action ) ) { return true; } } return false; } public function check_bulk_smush_process() { if ( $this->should_check_stuck() && $this->is_process_stuck() ) { $this->set_first_stuck_attachment(); do_action( 'wp_smush_bulk_smush_stuck', $this ); Helper::logger()->warning( sprintf( 'The Bulk Smush process has been stuck for %1$s minutes at image %2$d ( %3$s minutes )', round( $this->get_seconds_since_last_image_processing_started() / 60, 2 ), $this->get_last_process_attachment_id(), round( $this->get_last_process_attachment_elapsed_time() / 60, 2 ) ) ); } } private function should_check_stuck() { $first_stuck_attachment = $this->get_process_item( self::$first_stuck_attachment ); return empty( $first_stuck_attachment ); } public function check_bulk_smush_process_stuck_on_ajax_get_status() { $nonce = $this->get_ajax_nonce(); // Check capability. if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp-smush-ajax' ) || ! Helper::is_user_allowed( 'manage_options' ) ) { return; } $this->check_bulk_smush_process(); } private function set_last_processed_attachment( $attachment_id ) { $this->set_process_item( self::$last_attachment, array( 'id' => $attachment_id, 'start_time' => time(), ) ); } private function set_first_stuck_attachment() { $last_process_attachment = $this->get_last_processed_attachment(); $last_process_attachment['elapsed_time'] = $this->get_seconds_since_last_image_processing_started(); $this->set_process_item( self::$first_stuck_attachment, $last_process_attachment ); } public function is_process_stuck() { $elapsed_time = $this->get_seconds_since_last_image_processing_started(); return $elapsed_time > self::$process_time_out; } public function record_last_processed_attachment_elapsed_time() { $last_process_attachment = $this->get_last_processed_attachment(); $last_process_attachment['attachment_elapsed_time'] = $this->get_last_process_attachment_elapsed_time(); $this->set_process_item( self::$last_attachment, $last_process_attachment ); } public function get_last_process_attachment_elapsed_time() { $last_process_attachment = $this->get_last_processed_attachment(); $attachment_elapsed_time = (int) $this->array_utils->get_array_value( $last_process_attachment, 'attachment_elapsed_time', - 1 ); if ( $attachment_elapsed_time > - 1 ) { return $attachment_elapsed_time; } return $this->get_seconds_since_last_image_processing_started(); } public function get_seconds_since_last_image_processing_started() { $last_process_attachment = $this->get_last_processed_attachment(); $start_time = (int) $this->array_utils->get_array_value( $last_process_attachment, 'start_time' ); if ( empty( $start_time ) ) { return 0; } $end_time = time(); return $end_time - $start_time; } public function get_last_process_attachment_id() { $last_process_attachment = $this->get_last_processed_attachment(); return $this->array_utils->get_array_value( $last_process_attachment, 'id', 0 ); } private function get_last_processed_attachment() { return $this->get_process_item( self::$last_attachment, array() ); } public function record_process_start_time() { $this->reset_process_option(); $this->set_process_start_time(); } public function record_process_end_time() { $this->set_process_end_time(); } private function reset_process_option() { $this->record->delete(); wp_cache_delete( self::$process_key, 'options' ); } private function set_process_start_time() { $this->set_process_item( self::$start_time, microtime( true ) ); } private function set_process_end_time() { $this->set_process_item( self::$end_time, microtime( true ) ); } public function get_process_elapsed_time() { $start_time = $this->get_process_start_time(); $end_time = $this->get_process_end_time(); return (int) ( $end_time - $start_time ); } public function get_process_start_time() { return $this->get_process_item( self::$start_time ); } private function get_process_end_time() { return $this->get_process_item( self::$end_time, time() ); } private function get_process_item( $item, $default_value = false ) { $process_option = $this->get_process_option(); return $this->array_utils->get_array_value( $process_option, $item, $default_value ); } private function set_process_item( $item, $value ) { $this->record->set_values( array( $item => $value ) ); } private function get_process_option() { // JSON_Record::get() queries DB directly (bypasses cache) and json_decodes. $last_process = $this->record->get( array() ); return $this->array_utils->ensure_array( $last_process ); } } media-library/class-media-library-scanner.php 0000644 00000005556 15252476777 0015312 0 ustar 00 <?php namespace Smush\Core\Media_Library; use Smush\Core\Media\Media_Item_Query; /** * An un-opinionated scanner. * All it does is traverse attachments, the real work is supposed to be done by other controllers through actions and filters. * Supposed to handle parallel requests, each request handling a 'slice' of the total media items. */ class Media_Library_Scanner { private static $slice_size_max = 2500; private static $slice_size_min = 500; private static $slice_size_factor = 40; private static $slice_size_option_id = 'wp_smush_scan_slice_size'; public function before_scan_library() { do_action( 'wp_smush_before_scan_library' ); } public function scan_library_slice( $slice ) { $slice_size = $this->get_slice_size(); $query = new Media_Item_Query(); $attachment_ids = $query->fetch_slice_ids( $slice, $slice_size ); $slice_data = apply_filters( 'wp_smush_before_scan_library_slice', array(), $slice, $slice_size ); foreach ( $attachment_ids as $attachment_id ) { $slice_data = apply_filters( 'wp_smush_scan_library_slice_handle_attachment', $slice_data, $attachment_id, $slice, $slice_size ); } return apply_filters( 'wp_smush_after_scan_library_slice', $slice_data, $slice, $slice_size ); } public function after_scan_library() { do_action( 'wp_smush_after_scan_library' ); } public function get_slice_size() { $constant_value = $this->get_slice_size_constant(); if ( $constant_value ) { return $constant_value; } $option_value = $this->get_slice_size_option(); if ( $option_value ) { return $option_value; } return $this->calculate_default_slice_size(); } private function calculate_default_slice_size() { $query = new Media_Item_Query(); $attachment_count = $query->get_image_attachment_count(); $default_slice_size = (int) ceil( $attachment_count / self::$slice_size_factor ); if ( $default_slice_size > self::$slice_size_max ) { $default_slice_size = self::$slice_size_max; } elseif ( $default_slice_size < self::$slice_size_min ) { $default_slice_size = self::$slice_size_min; } return $default_slice_size; } public function reduce_slice_size_option() { $this->set_slice_size( self::$slice_size_min ); } private function get_slice_size_option() { $option_value = (int) get_option( self::$slice_size_option_id, 0 ); return max( $option_value, 0 ); } private function get_slice_size_constant() { if ( ! defined( 'WP_SMUSH_SCAN_SLICE_SIZE' ) ) { return 0; } $constant_value = (int) WP_SMUSH_SCAN_SLICE_SIZE; return max( $constant_value, 0 ); } /** * @param $value * * @return void */ private function set_slice_size( $value ) { update_option( self::$slice_size_option_id, $value ); } /** * Get slice_size_option_id. * * @return string */ public static function get_slice_size_option_id() { return self::$slice_size_option_id; } } media-library/class-ajax-media-library-scanner.php 0000644 00000004154 15252476777 0016224 0 ustar 00 <?php namespace Smush\Core\Media_Library; use Smush\Core\Controller; use Smush\Core\Helper; use Smush\Core\Media\Media_Item_Query; class Ajax_Media_Library_Scanner extends Controller { private static $parallel_requests = 5; /** * @var Media_Library_Scanner */ private $scanner; public function __construct() { $this->scanner = new Media_Library_Scanner(); $this->register_action( 'wp_ajax_wp_smush_before_scan_library', array( $this, 'before_scan_library' ) ); $this->register_action( 'wp_ajax_wp_smush_scan_library_slice', array( $this, 'scan_library_slice' ) ); $this->register_action( 'wp_ajax_wp_smush_after_scan_library', array( $this, 'after_scan_library' ) ); } public function before_scan_library() { check_ajax_referer( 'wp_smush_media_library_scanner' ); if ( ! Helper::is_user_allowed() ) { wp_send_json_error(); } $this->scanner->before_scan_library(); $slice_size = $this->scanner->get_slice_size(); $parallel_requests = $this->get_parallel_requests(); $query = new Media_Item_Query(); $image_attachment_count = $query->get_image_attachment_count(); $slice_count = $query->get_slice_count( $slice_size ); wp_send_json_success( array( 'image_attachment_count' => $image_attachment_count, 'slice_count' => $slice_count, 'slice_size' => $slice_size, 'parallel_requests' => $parallel_requests, ) ); } public function scan_library_slice() { check_ajax_referer( 'wp_smush_media_library_scanner' ); if ( ! Helper::is_user_allowed() ) { wp_send_json_error(); } $data = stripslashes_deep( $_POST ); if ( ! isset( $data['slice'] ) ) { wp_send_json_error(); } $slice = (int) $data['slice']; wp_send_json_success( $this->scanner->scan_library_slice( $slice ) ); } public function after_scan_library() { check_ajax_referer( 'wp_smush_media_library_scanner' ); if ( ! Helper::is_user_allowed() ) { wp_send_json_error(); } $this->scanner->after_scan_library(); wp_send_json_success(); } public function get_parallel_requests() { return self::$parallel_requests; } } media-library/class-media-library-slice-data-fetcher.php 0000644 00000014413 15252476777 0017275 0 ustar 00 <?php namespace Smush\Core\Media_Library; use Smush\Core\Array_Utils; use Smush\Core\Controller; use Smush\Core\Helper; use Smush\Core\Media\Media_Item_Query; class Media_Library_Slice_Data_Fetcher extends Controller { private $slice_post_meta = array(); private $slice_post_ids = array(); private $query; /** * @var \WDEV_Logger|null */ private $logger; private $is_multisite; private $current_site_id; /** * @var Array_Utils */ private $array_utils; public function __construct( $is_multisite = false, $current_site_id = 0 ) { $this->is_multisite = $is_multisite; $this->current_site_id = $current_site_id; $this->query = new Media_Item_Query(); $this->logger = Helper::logger(); $this->array_utils = new Array_Utils(); $this->register_filter( 'wp_smush_before_scan_library_slice', array( $this, 'prefetch_slice_data' ), 10, 3 ); $this->register_filter( 'wp_smush_before_scan_library_slice', array( $this, 'hook_meta_filters' ), 20, 3 ); $this->register_filter( 'wp_smush_after_scan_library_slice', array( $this, 'unhook_meta_filters' ) ); $this->register_filter( 'wp_smush_after_scan_library_slice', array( $this, 'reset_slice_data' ) ); } public function hook_meta_filters() { add_filter( 'get_post_metadata', array( $this, 'maybe_serve_post_meta' ), 10, 3 ); add_filter( 'add_post_meta', array( $this, 'update_post_meta_on_add' ), 10, 3 ); add_filter( 'update_post_meta', array( $this, 'update_post_meta_on_update' ), 10, 4 ); add_action( 'delete_post_meta', array( $this, 'purge_post_meta_on_delete' ), 10, 3 ); } public function unhook_meta_filters() { remove_filter( 'get_post_metadata', array( $this, 'maybe_serve_post_meta' ) ); remove_filter( 'add_post_meta', array( $this, 'update_post_meta_on_add' ) ); remove_filter( 'update_post_meta', array( $this, 'update_post_meta_on_update' ) ); remove_action( 'delete_post_meta', array( $this, 'purge_post_meta_on_delete' ) ); } public function prefetch_slice_data( $slice_data, $slice, $slice_size ) { $this->prefetch_slice_post_meta( $slice, $slice_size ); $this->prefetch_slice_posts( $slice, $slice_size ); return $slice_data; } public function maybe_serve_post_meta( $meta_value, $attachment_id, $meta_key ) { $slice_post_meta = $this->get_slice_post_meta(); if ( empty( $slice_post_meta ) ) { return $meta_value; } $cache_key = $this->get_post_meta_cache_key( $attachment_id, $meta_key ); $cached_value = ''; if ( isset( $slice_post_meta[ $cache_key ]->meta_value ) ) { $cached_value = maybe_unserialize( $slice_post_meta[ $cache_key ]->meta_value ); } return array( $cached_value ); } public function update_post_meta_on_add( $attachment_id, $meta_key, $meta_value ) { $this->update_post_meta( $attachment_id, $meta_key, $meta_value ); } public function update_post_meta_on_update( $meta_id, $attachment_id, $meta_key, $meta_value ) { $this->update_post_meta( $attachment_id, $meta_key, $meta_value ); } public function purge_post_meta_on_delete( $meta_ids, $attachment_id, $meta_key ) { $cache_key = $this->get_post_meta_cache_key( $attachment_id, $meta_key ); $slice_post_meta = $this->get_slice_post_meta(); if ( isset( $slice_post_meta[ $cache_key ] ) ) { unset( $slice_post_meta[ $cache_key ] ); $this->set_slice_post_meta( $slice_post_meta ); } } public function reset_slice_data( $slice_data ) { $this->set_slice_post_meta( array() ); $this->reset_slice_posts(); return $slice_data; } private function prefetch_slice_post_meta( $slice, $slice_size ) { $fetched_post_meta = $this->query->fetch_slice_post_meta( $slice, $slice_size ); $fetched_post_meta = $this->array_utils->ensure_array( $fetched_post_meta ); $this->set_slice_post_meta( $fetched_post_meta ); } private function prefetch_slice_posts( $slice, $slice_size ) { $slice_posts = $this->query->fetch_slice_posts( $slice, $slice_size ); if ( ! empty( $slice_posts ) && is_array( $slice_posts ) ) { $slice_post_ids = array(); foreach ( $slice_posts as $slice_post_key => $slice_post ) { $slice_post_ids[] = $slice_post_key; // Sanitize before adding to cache otherwise the post is going to be sanitized every time it is fetched from the cache $sanitized_post = sanitize_post( $slice_post, 'raw' ); wp_cache_add( $slice_post_key, $sanitized_post, 'posts' ); } $this->set_slice_post_ids( $slice_post_ids ); } } private function reset_slice_posts() { foreach ( $this->get_slice_post_ids() as $slice_post_id ) { wp_cache_delete( $slice_post_id, 'posts' ); } $this->set_slice_post_ids( array() ); } /** * @param $attachment_id * @param $meta_key * * @return string */ private function get_post_meta_cache_key( $attachment_id, $meta_key ) { return "$attachment_id-$meta_key"; } private function get_slice_post_meta() { $slice_post_meta = $this->slice_post_meta; if ( $this->is_multisite ) { $slice_post_meta = $this->array_utils->get_array_value( $slice_post_meta, $this->current_site_id ); } return $this->array_utils->ensure_array( $slice_post_meta ); } private function set_slice_post_meta( $slice_post_meta ) { if ( $this->is_multisite ) { $this->slice_post_meta[ $this->current_site_id ] = $slice_post_meta; } else { $this->slice_post_meta = $slice_post_meta; } } private function get_slice_post_ids() { $slice_post_ids = $this->slice_post_ids; if ( $this->is_multisite ) { $slice_post_ids = $this->array_utils->get_array_value( $slice_post_ids, $this->current_site_id ); } return $this->array_utils->ensure_array( $slice_post_ids ); } private function set_slice_post_ids( $slice_post_ids ) { if ( $this->is_multisite ) { $this->slice_post_ids[ $this->current_site_id ] = $slice_post_ids; } else { $this->slice_post_ids = $slice_post_ids; } } /** * @param $attachment_id * @param $meta_key * @param $meta_value * * @return void */ private function update_post_meta( $attachment_id, $meta_key, $meta_value ) { $cache_key = $this->get_post_meta_cache_key( $attachment_id, $meta_key ); $slice_post_meta = $this->get_slice_post_meta(); if ( empty( $slice_post_meta[ $cache_key ] ) ) { $slice_post_meta[ $cache_key ] = new \stdClass(); } $slice_post_meta[ $cache_key ]->meta_value = $meta_value; $this->set_slice_post_meta( $slice_post_meta ); } } media-library/class-media-library-watcher.php 0000644 00000004274 15252476777 0015312 0 ustar 00 <?php namespace Smush\Core\Media_Library; use Smush\Core\Array_Utils; use Smush\Core\Controller; use Smush\Core\Helper; class Media_Library_Watcher extends Controller { private static $wp_smush_image_sizes_state = 'wp_smush_image_sizes_state'; /** * @var Array_Utils */ private $array_utils; public function __construct() { $this->array_utils = new Array_Utils(); } public function init() { parent::init(); add_action( 'add_attachment', array( $this, 'wait_for_generate_metadata' ) ); add_action( 'admin_init', array( $this, 'watch_image_sizes' ), PHP_INT_MAX ); } public function wait_for_generate_metadata() { add_filter( 'wp_generate_attachment_metadata', array( $this, 'trigger_custom_add_attachment' ), 10, 2 ); } public function trigger_custom_add_attachment( $metadata, $attachment_id ) { do_action( 'wp_smush_after_attachment_upload', $attachment_id ); remove_filter( 'wp_generate_attachment_metadata', array( $this, 'trigger_custom_add_attachment' ) ); return $metadata; } public function watch_image_sizes() { $skip = get_transient( 'wp_smush_skip_image_sizes_recheck' ); if ( $skip ) { return; } $new_sizes = Helper::fetch_image_sizes(); $new_hash = $this->array_utils->array_hash( $new_sizes ); $old_state = $this->get_image_sizes_state(); $old_sizes = $old_state['sizes']; $old_hash = $old_state['hash']; if ( $new_hash !== $old_hash ) { do_action( 'wp_smush_image_sizes_changed', $old_sizes, $new_sizes ); $this->update_image_sizes_state( $new_sizes, $new_hash ); } set_transient( 'wp_smush_skip_image_sizes_recheck', true, HOUR_IN_SECONDS ); } private function get_image_sizes_state() { $state = get_option( self::$wp_smush_image_sizes_state ); if ( empty( $state ) ) { $state = array(); } if ( empty( $state['sizes'] ) || ! is_array( $state['sizes'] ) ) { $state['sizes'] = array(); } if ( empty( $state['hash'] ) ) { $state['hash'] = ''; } return $state; } private function update_image_sizes_state( $sizes, $hash ) { update_option( self::$wp_smush_image_sizes_state, array( 'sizes' => empty( $sizes ) || ! is_array( $sizes ) ? array() : $sizes, 'hash' => empty( $hash ) ? '' : $hash, ) ); } } media-library/class-media-library-scan-background-process.php 0000644 00000002400 15252476777 0020357 0 ustar 00 <?php namespace Smush\Core\Media_Library; use Smush\Core\Background\Background_Process; class Media_Library_Scan_Background_Process extends Background_Process { /** * Cron Interval. * * @overwrite parent. * @var int */ protected $cron_interval = 2; /** * @var Media_Library_Scanner */ private $scanner; public function __construct( $identifier, $scanner ) { parent::__construct( $identifier ); $this->scanner = $scanner; } protected function task( $task ) { if ( ! empty( $task['slice'] ) ) { $this->scanner->scan_library_slice( $task['slice'] ); } return true; } protected function get_instance_expiry_duration_seconds() { $expire_duration = 0; if ( defined( 'WP_SMUSH_SCAN_EXPIRE_DURATION' ) ) { $expire_duration = (int) WP_SMUSH_SCAN_EXPIRE_DURATION; } return $expire_duration > 0 ? $expire_duration : MINUTE_IN_SECONDS; } protected function get_revival_limit() { $constant_value = $this->get_revival_limit_constant(); return $constant_value ? $constant_value : parent::get_revival_limit(); } private function get_revival_limit_constant() { if ( ! defined( 'WP_SMUSH_SCAN_REVIVAL_LIMIT' ) ) { return 0; } $constant_value = (int) WP_SMUSH_SCAN_REVIVAL_LIMIT; return max( $constant_value, 0 ); } } media-library/class-media-library-last-process-pro.php 0000644 00000000324 15252476777 0017062 0 ustar 00 <?php namespace Smush\Core\Media_Library; class Media_Library_Last_Process_Pro extends Media_Library_Last_Process { public function __call( $name, $arguments ) { _deprecated_function( $name, '4.1.0' ); } } media-library/class-background-media-library-scanner.php 0000644 00000024553 15252476777 0017425 0 ustar 00 <?php namespace Smush\Core\Media_Library; use Smush\Core\Background\Process_Status_DTO; use Smush\Core\Controller; use Smush\Core\Helper; use Smush\Core\Media\Media_Item_Query; use Smush\Core\Stats\Global_Stats; use WP_Error; class Background_Media_Library_Scanner extends Controller { private static $optimize_on_completed_option_key = 'wp_smush_run_optimize_on_scan_completed'; private static $last_scan_completed_option_key = 'wp_smush_last_scan_completed'; /** * @var Media_Library_Scanner */ private $scanner; /** * @var Media_Library_Scan_Background_Process */ private $background_process; private $logger; /** * @var bool */ private $optimize_on_scan_completed; /** * @var Global_Stats */ private $global_stats; /** * Static instance * * @var self */ private static $instance; public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->scanner = new Media_Library_Scanner(); $this->logger = Helper::logger(); $this->global_stats = Global_Stats::get(); $identifier = $this->make_identifier(); $this->background_process = new Media_Library_Scan_Background_Process( $identifier, $this->scanner ); $this->background_process->set_logger( Helper::logger() ); $this->register_action( 'wp_ajax_wp_smush_start_background_scan', array( $this, 'start_background_scan' ) ); $this->register_action( 'wp_ajax_wp_smush_cancel_background_scan', array( $this, 'cancel_background_scan' ) ); $this->register_action( 'wp_ajax_wp_smush_get_background_scan_status', array( $this, 'send_status' ) ); $this->register_action( 'wp_ajax_wp_smush_reset_background_scan_status', array( $this, 'reset_background_scan_status' ) ); $this->register_action( "{$identifier}_completed", array( $this, 'background_process_completed' ) ); $this->register_action( "{$identifier}_dead", array( $this, 'background_process_dead' ) ); $this->register_filter( 'wp_smush_frontend_poll_data', array( $this, 'add_scan_progress_to_poll' ) ); // TODO: [WPMUDEV SMUSH UI] None is localized via old script data, need to implement localization. add_filter( 'wp_smush_script_data', array( $this, 'localize_media_library_scan_script_data' ) ); add_filter( 'wp_smush_localize_ui_script_data', array( $this, 'localize_scan_stats' ) ); } public function start_background_scan() { check_ajax_referer( 'wp_smush_media_library_scanner' ); if ( ! Helper::is_user_allowed() ) { wp_send_json_error(); } $status = $this->start_background_scan_direct(); if ( is_wp_error( $status ) ) { wp_send_json_error( array( 'message' => $status->get_error_message() ) ); } wp_send_json_success( $this->get_scan_status() ); } public function start_background_scan_direct() { $in_processing = $this->background_process->get_status()->is_in_processing(); if ( $in_processing ) { // Already in progress return new WP_Error( 'in_processing', __( 'Background scan is already in processing.', 'wp-smushit' ) ); } $this->set_optimize_on_scan_completed( ! empty( $_REQUEST['optimize_on_scan_completed'] ) ); if ( $this->background_process->get_status()->is_dead() ) { $this->scanner->reduce_slice_size_option(); } $this->scanner->before_scan_library(); $slice_size = $this->scanner->get_slice_size(); $query = new Media_Item_Query(); $slice_count = $query->get_slice_count( $slice_size ); $tasks = array_map( function ( $slice_number ) { return array( 'slice' => $slice_number ); }, range( 1, $slice_count ) ); $this->background_process->start( $tasks ); return $this->background_process->get_status()->to_array(); } public function cancel_background_scan() { check_ajax_referer( 'wp_smush_media_library_scanner' ); if ( ! Helper::is_user_allowed() ) { wp_send_json_error(); } if ( ! $this->background_process->get_status()->is_cancelled() ) { $this->background_process->cancel(); } $this->set_optimize_on_scan_completed( false ); wp_send_json_success( $this->get_scan_status() ); } public function send_status() { check_ajax_referer( 'wp_smush_media_library_scanner' ); if ( ! Helper::is_user_allowed() ) { wp_send_json_error(); } $this->background_process->maybe_do_healthcheck(); wp_send_json_success( $this->get_scan_status() ); } public function background_process_completed() { $this->scanner->after_scan_library(); // Cache latest scan completion status. update_option( self::$last_scan_completed_option_key, time() ); } /** * Get the last scan completed time in "time ago" format. * * @return string|null */ private function get_last_scan_completed_human() { $timestamp = $this->get_last_scan_completed(); if ( ! $timestamp ) { return __( 'No scans yet', 'wp-smushit' ); } $now = isset( $_GET['smush-current-time'] ) ? (int) $_GET['smush-current-time'] : time(); $from = (int) $timestamp; $diff = (int) abs( $now - $from ); if ( $diff < 60 ) { return __( 'Scanned just now', 'wp-smushit' ); } if ( $diff < 120 ) { return __( 'Scanned a min ago', 'wp-smushit' ); } if ( $diff < HOUR_IN_SECONDS ) { $mins = floor( $diff / 60 ); /* translators: Time difference between two dates, in minutes. %s: Number of minutes. */ return sprintf( __( 'Scanned %d mins ago', 'wp-smushit' ), $mins ); } if ( $diff < 2 * HOUR_IN_SECONDS ) { return __( 'Scanned 1h ago', 'wp-smushit' ); } if ( $diff < DAY_IN_SECONDS ) { $hours = floor( $diff / HOUR_IN_SECONDS ); /* translators: Time difference between two dates, in hours. %s: Number of hours. */ return sprintf( __( 'Scanned %dh ago', 'wp-smushit' ), $hours ); } if ( $diff < 2 * DAY_IN_SECONDS ) { return __( 'Scanned a day ago', 'wp-smushit' ); } if ( $diff < WEEK_IN_SECONDS ) { $days = floor( $diff / DAY_IN_SECONDS ); /* translators: Time difference between two dates, in days. %s: Number of days. */ return sprintf( __( 'Scanned %d days ago', 'wp-smushit' ), $days ); } if ( $diff < 2 * WEEK_IN_SECONDS ) { return __( 'Scanned last week', 'wp-smushit' ); } if ( $diff < MONTH_IN_SECONDS ) { $weeks = floor( $diff / WEEK_IN_SECONDS ); /* translators: Time difference between two dates, in weeks. %s: Number of weeks. */ return sprintf( __( 'Scanned %d weeks ago', 'wp-smushit' ), $weeks ); } if ( $diff < YEAR_IN_SECONDS ) { $months = floor( $diff / MONTH_IN_SECONDS ); /* translators: Time difference between two dates, in months. %s: Number of months. */ return sprintf( __( 'Scanned %d mo ago', 'wp-smushit' ), $months ); } return __( 'Scanned over a year ago', 'wp-smushit' ); } /** * Get the last scan completion timestamp. * * @return mixed|null */ private function get_last_scan_completed() { return get_option( self::$last_scan_completed_option_key, 0 ); } public function background_process_dead() { $this->global_stats->mark_as_outdated(); } private function make_identifier() { $identifier = 'wp_smush_background_scan_process'; if ( is_multisite() ) { $post_fix = '_' . get_current_blog_id(); $identifier .= $post_fix; } return $identifier; } public function localize_media_library_scan_script_data( $script_data ) { $scan_script_data = $this->background_process->get_status()->to_array(); $scan_script_data['nonce'] = wp_create_nonce( 'wp_smush_media_library_scanner' ); $script_data['media_library_scan'] = $scan_script_data; return $script_data; } /** * Localize scan stats * * @param array $script_data Script data. * * @return array */ public function localize_scan_stats( $script_data ) { $scan_status = $this->get_scan_data(); $scan_status['nonce'] = wp_create_nonce( 'wp_smush_media_library_scanner' ); $script_data['scanStatus'] = Process_Status_DTO::to_react_props( $scan_status ); return $script_data; } private function set_optimize_on_scan_completed( $status ) { $this->optimize_on_scan_completed = $status; if ( $this->optimize_on_scan_completed ) { update_option( self::$optimize_on_completed_option_key, 1, false ); } else { delete_option( self::$optimize_on_completed_option_key ); } } public function enabled_optimize_on_scan_completed() { if ( null === $this->optimize_on_scan_completed ) { $this->optimize_on_scan_completed = get_option( self::$optimize_on_completed_option_key ); } return ! empty( $this->optimize_on_scan_completed ); } private function get_scan_status() { $status = $this->background_process->get_status()->to_array(); $status['optimize_on_scan_completed'] = $this->enabled_optimize_on_scan_completed(); $status['lastScanRun'] = $this->get_last_scan_completed_human(); return $status; } public function get_background_process() { return $this->background_process; } /** * Add scan progress data to frontend poll response * * @param array $data Polling data array. * * @return array Modified polling data with scan progress. */ public function add_scan_progress_to_poll( $data ) { $status = $this->get_scan_data(); $data['scan-progress'] = Process_Status_DTO::to_react_props( $status ); return $data; } /** * Get the last scan completed time in "time ago" format. * * @return string|null */ private function get_last_scan_date_time() { $timestamp = $this->get_last_scan_completed(); if ( ! $timestamp ) { return ''; } $now = current_time( 'timestamp' ); $diff = $now - $timestamp; // <24 hours: "00:00 AM/PM" or "Yesterday, 00:00 AM/PM" if ( $diff < DAY_IN_SECONDS ) { return date_i18n( 'g:i A', $timestamp ); } // 48+ hours but less than 12 months: "Day, DD Month 00:00 AM/PM" if ( $diff < YEAR_IN_SECONDS ) { return date_i18n( 'D, j M g:i A', $timestamp ); } // 12+ months: "DD Month YYYY, 00:00 AM/PM" return date_i18n( 'j M Y, g:i A', $timestamp ); } public function get_scan_data() { $status = $this->background_process->get_status()->to_array(); $status['lastScanRun'] = $this->get_last_scan_completed_human(); $status['scanDateTime'] = $this->get_last_scan_date_time(); return $status; } public function reset_background_scan_status() { check_ajax_referer( 'wp_smush_media_library_scanner' ); if ( ! Helper::is_user_allowed() ) { wp_send_json_error(); } $this->background_process->get_status()->reset(); wp_send_json_success(); } } media-library/class-media-library-row.php 0000644 00000057547 15252476777 0014477 0 ustar 00 <?php namespace Smush\Core\Media_Library; use Smush\Core\Helper; use Smush\Core\Media\Media_Item; use Smush\Core\Media\Media_Item_Cache; use Smush\Core\Media\Media_Item_Optimization; use Smush\Core\Media\Media_Item_Optimizer; use Smush\Core\Media\Media_Item_Stats; use Smush\Core\Resize\Resize_Optimization; use Smush\Core\Settings; use Smush\Core\Smush\Smush_Optimization; use Smush\Core\Stats\Global_Stats; use Smush\Core\Modules\Helpers\WhiteLabel; use WP_Error; use WP_Smush; class Media_Library_Row { /** * @var int */ protected $attachment_id; /** * @var WP_Error */ protected $errors; /** * @var Media_Item_Optimizer */ protected $optimizer; /** * @var Media_Item */ protected $media_item; /** * @var Global_Stats */ protected $global_stats; /** * @var Settings */ protected $settings; protected $total_stats; protected $sizes_stats; /** * @var Media_Item_Optimization[] */ protected $applied_optimizations; /** * @var WhiteLabel */ private $whitelabel; public static function get_instance( $attachment_id ) { return new self( $attachment_id ); } public function __construct( $attachment_id ) { $this->attachment_id = $attachment_id; $this->media_item = Media_Item_Cache::get_instance()->get( $this->attachment_id ); $this->global_stats = Global_Stats::get(); $this->optimizer = new Media_Item_Optimizer( $this->media_item ); $this->errors = $this->prepare_errors(); $this->settings = Settings::get_instance(); $this->whitelabel = new WhiteLabel(); } private function prepare_errors() { $error_list = $this->global_stats->get_error_list(); if ( $error_list->has_id( $this->attachment_id ) || ( ! $this->media_item->has_wp_metadata() && $this->media_item->is_mime_type_supported() ) ) { return $this->media_item->get_errors(); } if ( $this->optimizer->has_errors() ) { $optimization_errors = $this->optimizer->get_errors(); if ( $optimization_errors->get_error_message( 'in_progress' ) ) { $optimization_errors->remove( 'in_progress' ); } return $optimization_errors; } return new WP_Error(); } /** * @return string */ public function generate_markup() { if ( ! $this->media_item->is_image() || ! $this->media_item->is_mime_type_supported() ) { return esc_html__( 'Not processed', 'wp-smushit' ); } if ( $this->optimizer->in_progress() || $this->optimizer->restore_in_progress() ) { return esc_html__( 'File processing is in progress.', 'wp-smushit' ); } if ( $this->media_item->is_animated() ) { return $this->generate_markup_for_animated_item(); } $has_error = $this->errors->has_errors(); if ( $has_error && $this->media_item->size_limit_exceeded() ) { return $this->generate_markup_for_size_limited_item(); } // Render ignored after animated/size limited to show upsell even ignored the image. // And render ignored before media item failed to show Ignored message when the image is ignored. if ( $this->media_item->is_ignored() ) { return $this->generate_markup_for_ignored_item(); } if ( $has_error && $this->media_item->has_errors() ) { return $this->generate_markup_for_failed_item(); } if ( $this->is_first_optimization_required() && ! $has_error ) { return $this->generate_markup_for_unsmushed_item(); } return $this->generate_markup_for_smushed_item(); } private function is_first_optimization_required() { return ! $this->optimizer->is_optimized() && $this->optimizer->should_optimize(); } private function generate_markup_for_animated_item() { $error_message = esc_html__( 'Skipped animated file.', 'wp-smushit' ); $utm_link = $this->get_animated_html_utm_link(); return $this->get_html_markup_for_failed_item_with_utm_link( $error_message, $utm_link ); } protected function get_animated_html_utm_link() { return $this->get_html_utm_link( __( 'Upgrade to Serve GIFs faster with CDN.', 'wp-smushit' ), 'smush_bulksmush_library_gif_cdn' ); } protected function get_html_utm_link( $utm_message, $utm_campain ) { $upgrade_url = 'https://wpmudev.com/project/wp-smush-pro/'; $args = array( 'utm_source' => 'smush', 'utm_medium' => 'plugin', 'utm_campaign' => $utm_campain, ); $utm_link = add_query_arg( $args, $upgrade_url ); return sprintf( '<a class="smush-upgrade-link" href="%1$s" target="_blank">%2$s</a>', esc_url( $utm_link ), esc_html( $utm_message ) ); } private function get_html_markup_for_failed_item_with_utm_link( $error_message, $utm_link = '' ) { if ( $this->media_item->is_ignored() ) { $links = $this->get_revert_with_utm_link( $utm_link ); } else { $links = $this->get_ignore_with_utm_link( $utm_link ); } return $this->get_html_markup_for_failed_item( $error_message, $links ); } private function get_revert_with_utm_link( $utm_link = '' ) { $class_names = array(); $links = $utm_link; if ( ! empty( $utm_link ) ) { $class_names[] = 'smush-revert-utm'; } $links .= $this->get_revert_link( $class_names ); return $links; } private function get_revert_link( $class_names = array() ) { $nonce = wp_create_nonce( 'wp-smush-remove-skipped' ); $class_names[] = 'wp-smush-remove-skipped'; // smush-revert-utm return sprintf( '<a href="#" class="%1$s" data-id="%2$d" data-nonce="%3$s">%4$s</a>', esc_attr( join( ' ', $class_names ) ), $this->attachment_id, $nonce, esc_html__( 'Revert back to previous state', 'wp-smushit' ) . '</a>' ); } private function get_ignore_with_utm_link( $utm_link = '' ) { $class_names = array(); $links = $utm_link; if ( ! empty( $utm_link ) ) { $class_names[] = ' smush-ignore-utm'; } $links .= $this->get_ignore_link( $class_names ); return $links; } private function get_ignore_link( $class_names = array() ) { $class_names[] = 'smush-ignore-image'; return sprintf( '<a href="#" class="%s" data-id="%d">%s</a>', esc_attr( join( ' ', $class_names ) ), $this->attachment_id, esc_html__( 'Ignore', 'wp-smushit' ) ); } private function get_html_markup_for_failed_item( $error_message, $links ) { $html = $this->get_html_markup_optimization_status_for_failed_item( $error_message ); $html .= $this->get_html_markup_action_links( $links ); return $html; } private function get_html_markup_optimization_status_for_failed_item( $error_message ) { if ( $this->media_item->is_ignored() ) { $class_name = 'smush-ignored'; } else { $class_name = 'smush-warning'; } return $this->get_html_markup_optimization_status( $error_message, $class_name ); } private function get_html_markup_optimization_status( $message, $class_names = array() ) { return sprintf( '<p class="smush-status %s">%s</p>', join( ' ', (array) $class_names ), $message ); } private function get_html_markup_action_links( $links, $separator = ' | ' ) { $links = (array) $links; $max_links = 4; if ( count( $links ) > $max_links ) { $links = array_splice( $links, count( $links ) - $max_links ); } return sprintf( '<div class="sui-smush-media smush-status-links">%s</div>', join( $links ) ); } private function generate_markup_for_size_limited_item() { $utm_link = $this->get_filesize_limit_utm_link(); if ( $this->media_item->is_ignored() ) { $error_message = esc_html__( 'Ignored.', 'wp-smushit' ); } else { $error_message = $this->errors->get_error_message(); } return $this->get_html_markup_for_failed_item_with_utm_link( $error_message, $utm_link ); } /** * Get UTM link for file size limit upsell. * * @return string */ protected function get_filesize_limit_utm_link() { return $this->get_html_utm_link( __( 'Upgrade to Pro to Smush larger images.', 'wp-smushit' ), 'smush_bulksmush_library_filesizelimit' ); } private function generate_markup_for_ignored_item() { return $this->get_html_markup_for_failed_item_with_suggestion_link( esc_html__( 'Ignored.', 'wp-smushit' ) ); } private function generate_markup_for_failed_item() { $error_suggestion = $this->get_error_suggestion(); $suggestion_link = $this->get_array_value( $error_suggestion, 'link' ); $suggestion_message = $this->get_array_value( $error_suggestion, 'message' ); $error_message = $this->errors->get_error_message(); if ( $suggestion_message ) { $error_message = sprintf( '%s. %s', rtrim( $error_message, '.' ), $suggestion_message ); } return $this->get_html_markup_for_failed_item_with_suggestion_link( $error_message, $suggestion_link ); } private function get_error_suggestion() { $error_suggestion = array( 'message' => '', 'link' => '', ); if ( ! $this->errors->has_errors() ) { return $error_suggestion; } switch ( $this->errors->get_error_code() ) { case 'file_not_found': case 'no_file_meta': if ( $this->media_item->can_be_restored() ) { $error_suggestion['message'] = esc_html__( 'We recommend using the restore image function to regenerate the thumbnails.', 'wp-smushit' ); } else { $error_suggestion['message'] = esc_html__( 'We recommend regenerating the thumbnails.', 'wp-smushit' ); $error_suggestion['link'] = $this->get_html_markup_for_regenerate_doc_link(); } break; } return $error_suggestion; } private function get_html_markup_for_regenerate_doc_link() { if ( $this->whitelabel->should_hide_doc_link() ) { return ''; } return sprintf( '<a target="_blank" href="%s" class="wp-smush-learnmore" data-id="%d">%s</a>', esc_url( $this->get_regenerate_doc_link() ), $this->attachment_id, esc_html__( 'Learn more', 'wp-smushit' ) ); } private function get_regenerate_doc_link() { return Helper::get_utm_link( array( 'utm_campaign' => 'smush_pluginlist_docs' ), 'https://wpmudev.com/docs/wpmu-dev-plugins/smush/#restoring-images' ); } private function get_html_markup_for_failed_item_with_suggestion_link( $error_message, $suggestion_link = '' ) { $links = array(); if ( $suggestion_link ) { $links[] = $suggestion_link; } if ( $this->media_item->is_ignored() ) { $links[] = $this->get_revert_link(); } else { $resmush_link = $this->get_resmush_link(); if ( $resmush_link ) { $links[] = $resmush_link; } $restore_link = $this->get_restore_link(); if ( $restore_link ) { $links[] = $restore_link; } $links[] = $this->get_ignore_link(); } return $this->get_html_markup_for_failed_item( $error_message, $links ); } private function generate_markup_for_unsmushed_item() { $action_links = array( $this->get_smush_link(), $this->get_ignore_link(), ); $html = $this->get_html_markup_optimization_status( esc_html__( 'Not processed', 'wp-smushit' ) ); $html .= $this->get_html_markup_action_links( $action_links ); return $html; } private function generate_markup_for_smushed_item() { $error_class = $this->errors->has_errors() ? 'smush-warning' : ''; $html = $this->get_html_markup_optimization_status( $this->get_optimization_status(), $error_class ); $html .= $this->get_html_markup_action_links( $this->get_action_links() ); $html .= sprintf( '<div id="smush-stats-%d" class="sui-smush-media smush-stats-wrapper hidden">', $this->attachment_id ); $html .= $this->get_html_markup_detailed_stats(); $html .= '</div>'; return $html; } private function get_optimization_status() { $error_message = $this->errors->get_error_message(); if ( $error_message ) { return $error_message; } if ( $this->is_no_savings() ) { return esc_html__( 'Skipped: Image is already optimized.', 'wp-smushit' ); } return $this->get_optimized_status_text(); } private function is_no_savings() { $total_stats = $this->get_total_stats(); return $total_stats->get_size_after() >= $total_stats->get_size_before(); } private function get_total_stats() { if ( is_null( $this->total_stats ) ) { $this->total_stats = $this->prepare_total_stats(); } return $this->total_stats; } private function prepare_total_stats() { $total_stats = new Media_Item_Stats(); $optimizations = $this->get_applied_optimizations(); if ( empty( $optimizations ) ) { return $total_stats; } $size_before = $this->get_size_before(); $size_after = $this->get_size_after(); $total_stats->from_array( array( 'size_before' => $size_before, 'size_after' => $size_after, ) ); return $total_stats; } private function get_size_before() { $optimizations = $this->get_applied_optimizations(); $size_before = max( array_map( function ( $optimization ) { return $optimization->get_stats()->get_size_before(); }, $optimizations ) ); return $size_before; } private function get_size_after() { $optimizations = $this->get_applied_optimizations(); $size_after = min( array_map( function ( $optimization ) { return $optimization->get_stats()->get_size_after(); }, $optimizations ) ); return $size_after; } private function get_sizes_stats() { if ( is_null( $this->sizes_stats ) ) { $this->sizes_stats = $this->prepare_sizes_stats(); } return $this->sizes_stats; } private function prepare_sizes_stats() { $sizes_stats = array(); foreach ( $this->media_item->get_sizes() as $size ) { $sizes_stats[ $size->get_key() ] = $this->get_size_stats( $size ); } return $sizes_stats; } private function get_size_stats( $size ) { $optimizations = $this->get_applied_optimizations(); $size_stats = new Media_Item_Stats(); if ( empty( $optimizations ) ) { return $size_stats; } $size_before = max( array_map( function ( $optimization ) use ( $size ) { return $optimization->get_size_stats( $size->get_key() )->get_size_before(); }, $optimizations ) ); $size_after = min( array_map( function ( $optimization ) use ( $size ) { return $optimization->get_size_stats( $size->get_key() )->get_size_after(); }, $optimizations ) ); $size_stats->from_array( array( 'size_before' => $size_before, 'size_after' => $size_after, ) ); return $size_stats; } /** * @return Media_Item_Optimization */ private function get_primary_optimization() { $optimizations = $this->get_applied_optimizations(); return array_shift( $optimizations ); } private function get_applied_optimizations() { if ( is_null( $this->applied_optimizations ) ) { $this->applied_optimizations = $this->prepare_applied_optimizations(); } return $this->applied_optimizations; } private function prepare_applied_optimizations() { $applied_ordered_optimizations = array(); $nextgen_optimization = $this->get_active_nextgen_optimization(); if ( $nextgen_optimization ) { $applied_ordered_optimizations[] = $nextgen_optimization; } $applied_ordered_optimizations = array_merge( $applied_ordered_optimizations, $this->get_classic_optimizations() ); return array_filter( $applied_ordered_optimizations, function ( $optimization ) { return $optimization && $optimization->is_optimized() && $optimization->get_stats()->get_bytes() > 0; } ); } private function get_classic_optimizations() { $ordered_optimizations = $this->get_ordered_optimization_keys(); return array_map( array( $this->optimizer, 'get_optimization' ), $ordered_optimizations ); } /** * Get the ordered optimization keys for classic optimizations. * * @return array */ protected function get_ordered_optimization_keys() { return array( Smush_Optimization::get_key(), Resize_Optimization::get_key(), ); } private function get_optimized_status_text() { $total_stats = $this->get_total_stats(); $sizes_stats = $this->get_sizes_stats(); $count_images = 0; foreach ( $sizes_stats as $size_stats ) { if ( ! empty( $size_stats->get_bytes() ) ) { $count_images++; } } $status_text = ''; if ( 1 < $count_images ) { $status_text .= sprintf( /* translators: %1$s: bytes savings, %2$s: percentage savings, %3$d: number of images */ esc_html__( '%3$d images reduced by %1$s (%2$s)', 'wp-smushit' ), $total_stats->get_human_bytes(), sprintf( '%01.1f%%', $total_stats->get_percent() ), $count_images ); } else { $status_text .= sprintf( /* translators: %1$s: bytes savings, %2$s: percentage savings */ esc_html__( 'Reduced by %1$s (%2$s)', 'wp-smushit' ), $total_stats->get_human_bytes(), sprintf( '%01.1f%%', $total_stats->get_percent() ) ); } // Do we need to show the main image size? $main_size = $this->media_item->get_scaled_or_full_size(); /** * @var Media_Item_Stats $main_size_stats */ $main_size_stats = $this->get_array_value( $sizes_stats, $main_size->get_key() ); $main_file_size = ( $main_size_stats && $main_size_stats->get_size_after() > 0 ) ? $main_size_stats->get_size_after() : $main_size->get_filesize(); $status_text .= sprintf( /* translators: 1: <br/> tag, 2: Image file size */ esc_html__( '%1$sMain Image size: %2$s', 'wp-smushit' ), '<br />', size_format( $main_file_size, 2 ) ); return $status_text; } /** * @return array */ private function get_action_links() { if ( $this->is_first_optimization_required() ) { return array( $this->get_smush_link(), $this->get_ignore_link() ); } $links = array(); $restore_link = $this->get_restore_link(); if ( $restore_link ) { $links[] = $restore_link; } $resmush_link = $this->get_resmush_link(); if ( $resmush_link ) { $links[] = $resmush_link; } if ( ! $this->is_no_savings() ) { $links[] = $this->get_view_stats_link(); } // Add ignore button while showing resmush button. if ( $resmush_link ) { $links[] = $this->get_ignore_link(); } return $links; } private function get_html_markup_detailed_stats() { if ( $this->is_no_savings() ) { return; } $primary_optimization = $this->get_primary_optimization(); return sprintf( ' <table class="wp-smush-stats-holder"> <thead> <tr> <th class="smush-stats-header">%s</th> <th class="smush-stats-header">%s</th> </tr> </thead> <tbody>%s</tbody> </table> ', esc_html__( 'Image size', 'wp-smushit' ), sprintf( /* translators: %s: Optimization name */ esc_html__( '%s Savings', 'wp-smushit' ), $primary_optimization->get_name() ), $this->get_detailed_stats_content() ); } private function get_detailed_stats_content() { $primary_optimization = $this->get_primary_optimization(); $sizes_stats = $this->get_sizes_stats(); $stats_rows = array(); $savings_sizes = array(); // Show Sizes and their compression. foreach ( $this->media_item->get_sizes() as $size_key => $size ) { $size_stats = $this->get_array_value( $sizes_stats, $size_key ); if ( $size_stats->is_empty() || empty( $size_stats->get_bytes() ) ) { continue; } $dimensions = "{$size->get_width()}x{$size->get_height()}"; $optimized_file_url = $primary_optimization->get_optimized_file_url( $size->get_file_url() ); if ( empty( $optimized_file_url ) ) { $optimized_file_url = $size->get_file_url(); } $stats_rows[ $size_key ] = sprintf( '<tr> <td><a href="%1$s">%2$s</a><br/>(%3$s)</td> <td>%4$s ( %5$s%% )</td> </tr>', $optimized_file_url ? $optimized_file_url : '#', strtoupper( $size_key ), $dimensions, $size_stats->get_human_bytes(), $size_stats->get_percent() ); $savings_sizes[ $size_key ] = $size_stats->get_bytes(); } uksort( $stats_rows, function ( $size_key1, $size_key2 ) use ( $savings_sizes ) { return $savings_sizes[ $size_key2 ] - $savings_sizes[ $size_key1 ]; } ); return join( '', $stats_rows ); } private function get_smush_link() { return sprintf( '<a href="#" class="wp-smush-send button" data-id="%d">%s</a>', $this->attachment_id, $this->whitelabel->get_whitelabel_text( esc_html__( 'Smush', 'wp-smushit' ), esc_html__( 'Optimize', 'wp-smushit' ) ) ); } private function should_reoptimize() { $reoptimize_list = $this->global_stats->get_reoptimize_list(); $error_list = $this->global_stats->get_error_list(); $should_reoptimize = $reoptimize_list->has_id( $this->attachment_id ) || $error_list->has_id( $this->attachment_id ); if ( $should_reoptimize && $this->optimizer->has_errors() ) { return $this->optimizer->should_reoptimize(); } return $should_reoptimize; } /** * @return string|void */ private function get_resmush_link() { if ( ! $this->should_reoptimize() || ! $this->media_item->has_wp_metadata() ) { return; } $next_level_smush_link = $this->get_next_level_smush_link(); if ( ! empty( $next_level_smush_link ) ) { return $next_level_smush_link; } return sprintf( '<a href="#" data-tooltip="%s" data-id="%d" data-nonce="%s" class="wp-smush-action wp-smush-title sui-tooltip sui-tooltip-constrained wp-smush-resmush button">%s</a>', $this->whitelabel->get_whitelabel_text( esc_html__( 'Smush image including original file', 'wp-smushit' ), esc_html__( 'Optimize image including original file', 'wp-smushit' ) ), $this->attachment_id, wp_create_nonce( 'wp-smush-ajax' ), $this->whitelabel->get_whitelabel_text( esc_html__( 'Resmush', 'wp-smushit' ), esc_html__( 'Reoptimize', 'wp-smushit' ) ) ); } /** * @return string|void */ private function get_next_level_smush_link() { if ( $this->errors->has_errors() || $this->is_first_optimization_required() || ! $this->is_next_level_smush_required() ) { return; } $anchor_text = $this->get_next_level_smush_anchor_text(); if ( ! $anchor_text ) { return; } return sprintf( '<a href="#" class="wp-smush-send button" data-id="%d">%s</a>', $this->attachment_id, $anchor_text ); } /** * @return bool */ private function is_next_level_smush_required() { $smush_optimization = $this->get_smush_optimization(); return $smush_optimization && $smush_optimization->is_next_level_available(); } private function get_next_level_smush_anchor_text() { $required_level = $this->settings->get_lossy_level_setting(); switch ( $required_level ) { case Settings::get_level_ultra_lossy(): return $this->whitelabel->get_whitelabel_text( esc_html__( 'Ultra Smush', 'wp-smushit' ), esc_html__( 'Ultra Optimize', 'wp-smushit' ) ); case Settings::get_level_super_lossy(): return $this->whitelabel->get_whitelabel_text( esc_html__( 'Super Smush', 'wp-smushit' ), esc_html__( 'Super Optimize', 'wp-smushit' ) ); default: return false; } } /** * @return Smush_Optimization|null */ private function get_smush_optimization() { /** * @var $smush_optimization Smush_Optimization|null */ $smush_optimization = $this->optimizer->get_optimization( Smush_Optimization::get_key() ); return $smush_optimization; } /** * @return string|void */ private function get_restore_link() { if ( ! empty( $this->media_item->can_be_restored() ) ) { return sprintf( '<a href="#" data-tooltip="%s" data-id="%d" data-nonce="%s" class="wp-smush-action wp-smush-title sui-tooltip wp-smush-restore button">%s</a>', esc_html__( 'Restore original image', 'wp-smushit' ), $this->attachment_id, wp_create_nonce( 'wp-smush-restore-' . $this->attachment_id ), esc_html__( 'Restore original', 'wp-smushit' ) ); } return sprintf( '<a href="#" data-tooltip="%s" class="wp-smush-title wp-smush-restore sui-tooltip sui-tooltip-constrained button disabled">%s</a>', esc_html__( 'No backup image available. Enable Back up original images to restore them in the future.', 'wp-smushit' ), esc_html__( 'Restore original', 'wp-smushit' ) ); } private function get_view_stats_link() { return sprintf( '<a href="#" class="wp-smush-action smush-stats-details wp-smush-title sui-tooltip sui-tooltip-top-right" data-tooltip="%s">%s</a>', esc_html__( 'Detailed stats for all the image sizes', 'wp-smushit' ), '<span class="stats-toggle"></span>' ); } private function get_array_value( $array, $key ) { return isset( $array[ $key ] ) ? $array[ $key ] : null; } /** * @return Media_Item_Optimization|null */ protected function get_active_nextgen_optimization() { return null; } } class-file-utils.php 0000644 00000001250 15252476777 0010461 0 ustar 00 <?php namespace Smush\Core; class File_Utils { private $file_sizes_cache = array(); /** * @var Settings|null */ private $settings; public function __construct() { $this->settings = Settings::get_instance(); } public function is_large_file( $file_path ) { $file_size = $this->get_file_size( $file_path ); $cut_off = $this->settings->get_large_file_cutoff(); return $file_size > $cut_off; } public function get_file_size( $file_path ) { if ( ! isset( $this->file_sizes_cache[ $file_path ] ) ) { $this->file_sizes_cache[ $file_path ] = file_exists( $file_path ) ? filesize( $file_path ) : 0; } return $this->file_sizes_cache[ $file_path ]; } } class-format-utils.php 0000644 00000000363 15252476777 0011036 0 ustar 00 <?php namespace Smush\Core; class Format_Utils { public function convert_to_megabytes( $size_in_bytes ) { if ( empty( $size_in_bytes ) ) { return 0; } $unit_mb = pow( 1024, 2 ); return round( $size_in_bytes / $unit_mb, 2 ); } } class-shim.php 0000644 00000004344 15252476777 0007353 0 ustar 00 <?php namespace Smush\Core; class Shim implements \Countable { private static function is_string_item( $name ) { return self::starts_with_get( $name ) && self::ends_with_string_type( $name ); } private static function is_array_type( $name ) { return $name === 'to_array'; } private static function starts_with_get( $name ) { return str_starts_with( $name, 'get' ); } private static function ends_with_string_type( $name ) { return (bool) preg_match( '/(?:_name|_key|_option_id)$/', $name ); } public function __call( $name, $arguments ) { if ( self::log_all() ) { error_log( sprintf( 'Smush Shim: Missing method %s called with args: %s', $name, json_encode( $arguments ) ) ); } if ( self::is_boolean_item( $name ) ) { return false; } if ( self::is_string_item( $name ) ) { return ''; } if ( self::is_array_type( $name ) ) { return []; } if ( self::log_risky() ) { error_log( sprintf( 'Smush Shim: Returning Shim object for method %s. This could be risky.', $name ) ); } return new self(); } public function __get( $name ) { if ( self::is_boolean_item( $name ) ) { return false; } return new self(); } public static function __callStatic( $name, $arguments ) { if ( self::log_all() ) { error_log( sprintf( 'Smush Shim: Missing static method %s called with args: %s', $name, json_encode( $arguments ) ) ); } if ( self::is_boolean_item( $name ) ) { return false; } if ( self::is_string_item( $name ) ) { return ''; } if ( self::log_risky() ) { error_log( sprintf( 'Smush Shim: Returning Shim object for static method %s. This could be risky.', $name ) ); } return new self(); } #[\ReturnTypeWillChange] public function count() { return 0; } public function __toString() { return ''; } /** * @param string $name * * @return bool */ private static function is_boolean_item( $name ) { return str_starts_with( $name, 'is_' ); } private static function log_all() { return self::get_log_type() === 'all'; } private static function log_risky() { return self::get_log_type() === 'risky'; } private static function get_log_type() { return defined( 'WP_SMUSH_LOG_SHIM_CALLS' ) ? constant( 'WP_SMUSH_LOG_SHIM_CALLS' ) : ''; } } class-time-utils.php 0000644 00000000423 15252476777 0010501 0 ustar 00 <?php namespace Smush\Core; class Time_Utils { private $time; public function get_time() { if ( is_null( $this->time ) ) { return time(); } return $this->time; } /** * ONLY FOR TESTING */ public function set_time( $time ) { $this->time = $time; } } media/class-media-item-stats.php 0000644 00000007614 15252476777 0012644 0 ustar 00 <?php namespace Smush\Core\Media; class Media_Item_Stats { /** * @var int */ private $size_before = 0; /** * @var int */ private $size_after = 0; /** * @var float */ private $time = 0.0; /** * @return float */ public function get_percent() { return $this->calculate_percentage( $this->get_size_before(), $this->get_size_after() ); } public function get_human_bytes() { $bytes = $this->get_bytes(); return size_format( $bytes, $bytes >= 1024 ? 1 : 0 ); } /** * @return int */ public function get_bytes() { $size_before = $this->get_size_before(); $size_after = $this->get_size_after(); return $size_before > $size_after ? $size_before - $size_after : 0; } /** * @return int */ public function get_size_before() { return $this->size_before; } /** * @param int $size_before */ public function set_size_before( $size_before ) { $this->size_before = (int) $size_before; } /** * @return int */ public function get_size_after() { return $this->size_after; } /** * @param int $size_after */ public function set_size_after( $size_after ) { $this->size_after = (int) $size_after; } /** * @return float */ public function get_time() { return $this->time; } /** * @param float $time */ public function set_time( $time ) { $this->time = (float) $time; } public function from_array( $array ) { $this->set_time( (float) $this->get_array_value( $array, 'time' ) ); $this->set_size_before( (int) $this->get_array_value( $array, 'size_before' ) ); $this->set_size_after( (int) $this->get_array_value( $array, 'size_after' ) ); } public function is_empty() { return empty( $this->get_size_before() ) && empty( $this->get_size_after() ); } public function to_array() { return array( 'time' => $this->get_time(), 'bytes' => $this->get_bytes(), 'percent' => $this->get_percent(), 'size_before' => $this->get_size_before(), 'size_after' => $this->get_size_after(), ); } protected function get_array_value( $array, $key ) { return isset( $array[ $key ] ) ? $array[ $key ] : null; } /** * Add values from the passed stats object to the current object * * @param $addend Media_Item_Stats * * @return void */ public function add( $addend ) { $new_size_before = $this->get_size_before() + $addend->get_size_before(); $new_size_after = $this->get_size_after() + $addend->get_size_after(); $new_time = $this->get_time() + $addend->get_time(); // Update with new values $this->set_time( $new_time ); $this->set_size_before( $new_size_before ); $this->set_size_after( $new_size_after ); } /** * @param $subtrahend Media_Item_Stats * * @return void */ public function subtract( $subtrahend ) { $new_size_before = $this->get_size_before() - $subtrahend->get_size_before(); $new_size_after = $this->get_size_after() - $subtrahend->get_size_after(); $new_time = $this->get_time() - $subtrahend->get_time(); // Update with new values $this->set_time( max( $new_time, 0 ) ); $this->set_size_before( max( $new_size_before, 0 ) ); $this->set_size_after( max( $new_size_after, 0 ) ); } /** * @param $to_check Media_Item_Stats * * @return boolean */ public function equals( $to_check ) { return $this->get_size_before() === $to_check->get_size_before() && $this->get_size_after() === $to_check->get_size_after() && $this->get_time() === $to_check->get_time(); } private function calculate_percentage( $size_before, $size_after ) { $savings = $size_before - $size_after; if ( $savings > 0 && $size_before > 0 ) { $percentage = ( $savings / $size_before ) * 100; return $percentage > 0 ? round( $percentage, 2 ) : $percentage; } return 0; } /** * @param $to_copy Media_Item_Stats * * @return void */ public function copy( $to_copy ) { $this->from_array( $to_copy->to_array() ); } } media/class-attachment-url-cache.php 0000644 00000002265 15252476777 0013463 0 ustar 00 <?php namespace Smush\Core\Media; class Attachment_Url_Cache { private $cache = array(); /** * Static instance * * @var self */ private static $instance; private $fetch_in_advance = false; /** * Static instance getter */ public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } public function has_cached( $url ) { return isset( $this->cache[ trim( $url ) ] ); } public function get_id_for_url( $url, $fetch = false ) { if ( ! isset( $this->cache[ trim( $url ) ] ) ) { $attachment_id = 0; if ( $fetch ) { $attachment_id = attachment_url_to_postid( $url ); } $this->set_id_for_url( $url, $attachment_id ); } return $this->cache[ trim( $url ) ] ?? 0; } public function set_id_for_url( $url, $attachment_id ) { $this->cache[ trim( $url ) ] = $attachment_id; } public function reset() { $this->cache = array(); } public function get_all() { return $this->cache; } public function set_fetch_in_advance( $fetch_in_advance ) { $this->fetch_in_advance = $fetch_in_advance; } public function fetch_in_advance() { return $this->fetch_in_advance; } } media/class-media-item-controller.php 0000644 00000005635 15252476777 0013672 0 ustar 00 <?php namespace Smush\Core\Media; use Smush\Core\Controller; use Smush\Core\Error_Handler; use Smush\Core\Helper; use Smush\Core\Stats\Global_Stats; use WP_Smush; /** * Performs operations on the media item */ class Media_Item_Controller extends Controller { public function __construct() { $this->register_action( 'wp_ajax_ignore_bulk_image', array( $this, 'ignore_bulk_image' ) ); $this->register_action( 'wp_ajax_remove_from_skip_list', array( $this, 'remove_from_skip_list' ) ); $this->register_action( 'wp_ajax_wp_smush_ignore_all_failed_items', array( $this, 'ignore_all_failed_items', ) ); } public function remove_from_skip_list() { check_ajax_referer( 'wp-smush-remove-skipped' ); if ( ! Helper::is_user_allowed( 'upload_files' ) ) { wp_send_json_error( array( 'error_message' => esc_html__( "You don't have permission to work with uploaded files.", 'wp-smushit' ), ), 403 ); } if ( ! isset( $_POST['id'] ) ) { wp_send_json_error(); } $attachment_id = absint( $_POST['id'] ); $changed = $this->change_attachment_ignored_status( $attachment_id, false ); if ( ! $changed ) { wp_send_json_error(); } wp_send_json_success( array( 'html' => WP_Smush::get_instance()->library()->generate_markup( $attachment_id ), ) ); } public function ignore_bulk_image() { check_ajax_referer( 'wp-smush-ajax' ); if ( ! Helper::is_user_allowed( 'upload_files' ) ) { wp_send_json_error( array( 'error_msg' => esc_html__( "You don't have permission to work with uploaded files.", 'wp-smushit' ), ), 403 ); } if ( ! isset( $_POST['id'] ) ) { wp_send_json_error(); } $attachment_id = absint( $_POST['id'] ); $changed = $this->change_attachment_ignored_status( $attachment_id, true ); if ( ! $changed ) { wp_send_json_error(); } wp_send_json_success( array( 'html' => WP_Smush::get_instance()->library()->generate_markup( $attachment_id ), ) ); } public function ignore_all_failed_items() { check_ajax_referer( 'wp-smush-ajax' ); if ( ! Helper::is_user_allowed() ) { wp_send_json_error( array( 'message' => __( "You don't have permission to do this.", 'wp-smushit' ), ), 403 ); } $failed_images = Error_Handler::get_all_failed_images(); if ( empty( $failed_images ) ) { wp_send_json_error( array( 'message' => __( 'Not found any failed items.', 'wp-smushit' ) ) ); } foreach ( $failed_images as $failed_image_id ) { $this->change_attachment_ignored_status( $failed_image_id, true ); } wp_send_json_success(); } private function change_attachment_ignored_status( $attachment_id, $new_status ) { $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); if ( ! $media_item->is_mime_type_supported() ) { return false; } $media_item->set_ignored( $new_status ); $media_item->save(); do_action( 'wp_smush_attachment_ignored_status_changed', $attachment_id, $new_status ); return true; } } media/class-media-item-cache.php 0000644 00000005135 15252476777 0012545 0 ustar 00 <?php namespace Smush\Core\Media; /** * TODO: maybe reset the media item when: * - a new size is added */ class Media_Item_Cache { private static $cache_group = 'wp-smushit'; /** * Static instance * * @var self */ private static $instance; /** * @var Media_Item[] */ private $media_items; /** * Static instance getter */ public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } public function has( $id ) { $media_item = $this->get_from_cache( $id ); return ! empty( $media_item ); } /** * @param $id * * @return Media_Item|null */ public function get( $id ) { $media_item = $this->get_from_cache( $id ); if ( ! $media_item ) { $media_item = new Media_Item( $id ); $this->save_to_cache( $id, $media_item ); } return $media_item; } /** * @param $id * * @return Media_Item|null */ private function get_from_cache( $id ) { return $this->get_array_value( $this->media_items, $this->make_key( $id ) ); } private function make_key( $id ) { return "wp-smush-media-item-$id"; } private function save_to_cache( $id, $media_item ) { $this->media_items[ $this->make_key( $id ) ] = $media_item; } public function remove( $id ) { unset( $this->media_items[ $this->make_key( $id ) ] ); } private function get_array_value( $array, $key ) { return $array && isset( $array[ $key ] ) ? $array[ $key ] : null; } public function reset_all() { foreach ( $this->media_items as $media_item ) { $media_item->reset(); } } /** * Get animated_meta_key. * * @return mixed */ public static function get_animated_meta_key() { return self::$animated_meta_key; } /** * Get backup_sizes_meta_key. * * @return mixed */ public static function get_backup_sizes_meta_key() { return self::$backup_sizes_meta_key; } /** * Get default_backup_key. * * @return mixed */ public static function get_default_backup_key() { return self::$default_backup_key; } /** * Get ignored_meta_key. * * @return mixed */ public static function get_ignored_meta_key() { return self::$ignored_meta_key; } /** * Get size_key_full. * * @return mixed */ public static function get_size_key_full() { return self::$size_key_full; } /** * Get size_key_scaled. * * @return mixed */ public static function get_size_key_scaled() { return self::$size_key_scaled; } /** * Get transparent_meta_key. * * @return mixed */ public static function get_transparent_meta_key() { return self::$transparent_meta_key; } } media/class-attachment-url-cache-controller.php 0000644 00000014015 15252476777 0015640 0 ustar 00 <?php namespace Smush\Core\Media; use Smush\Core\Array_Utils; use Smush\Core\CDN\CDN_Helper; use Smush\Core\Controller; use Smush\Core\Parser\Element; use Smush\Core\Parser\Page; use Smush\Core\Settings; use Smush\Core\Upload_Dir; use Smush\Core\Url_Utils; class Attachment_Url_Cache_Controller extends Controller { private $cache; private $bulk_image_urls = array(); /** * @var Upload_Dir */ private $upload_dir; /** * @var Media_Item_Query */ private $media_item_query; private $element_urls = array(); private $url_elements = array(); /** * @var Array_Utils */ private $array_utils; /** * @var Url_Utils */ private $url_utils; /** * @var Settings */ private $settings; /** * @var CDN_Helper */ private $cdn_helper; public function __construct() { $this->cache = Attachment_Url_Cache::get_instance(); $this->upload_dir = new Upload_Dir(); $this->media_item_query = new Media_Item_Query(); $this->array_utils = new Array_Utils(); $this->url_utils = new Url_Utils(); $this->settings = Settings::get_instance(); $this->cdn_helper = CDN_Helper::get_instance(); $this->register_filter( 'wp_get_attachment_image_src', array( $this, 'save__wp_get_attachment_image_src' ), 10, 2 ); $this->register_filter( 'wp_calculate_image_srcset', array( $this, 'save__wp_calculate_image_srcset' ), 10, 5 ); $this->register_filter( 'wp_get_attachment_metadata', array( $this, 'save__wp_get_attachment_metadata' ), 10, 2 ); $this->register_filter( 'wp_smush_pre_transform_page', array( $this, 'pre_transform_bulk_cache_page_urls' ) ); } public function should_run() { return parent::should_run() && ! is_admin(); } public function save__wp_get_attachment_image_src( $image, $attachment_id ) { if ( ! empty( $image ) ) { $this->cache->set_id_for_url( $image[0], $attachment_id ); } return $image; } public function save__wp_calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) { if ( ! empty( $sources ) ) { foreach ( $sources as $source ) { $this->cache->set_id_for_url( $source['url'], $attachment_id ); } } return $sources; } public function save__wp_get_attachment_metadata( $meta_data, $attachment_id ) { $original_file = $this->array_utils->get_array_value( $meta_data, 'original_image' ); if ( $original_file ) { $upload_dir = wp_upload_dir(); $upload_dir_url = untrailingslashit( $upload_dir['baseurl'] ); $file_dir = untrailingslashit( dirname( $meta_data['file'] ) ); $original_file_url = "$upload_dir_url/$file_dir/$original_file"; $this->cache->set_id_for_url( $original_file_url, $attachment_id ); } return $meta_data; } /** * * @param $page Page * * @return void */ public function pre_transform_bulk_cache_page_urls( $page ) { if ( ! $this->cache->fetch_in_advance() ) { // Run only if a component has asked for the cache to be primed in advance return; } foreach ( $page->get_composite_elements() as $composite_element ) { $this->collect_bulk_image_urls( $composite_element->get_elements() ); } $this->collect_bulk_image_urls( $page->get_elements() ); if ( ! empty( $this->bulk_image_urls ) ) { $urls_to_ids = $this->media_item_query->attachment_urls_to_ids( $this->bulk_image_urls ); foreach ( $urls_to_ids as $url => $attachment_id ) { $element_key = $this->array_utils->get_array_value( $this->url_elements, $url ); $element_urls = $this->array_utils->get_array_value( $this->element_urls, $element_key ); if ( ! empty( $element_urls ) && is_array( $element_urls ) ) { foreach ( $element_urls as $element_url ) { $this->cache->set_id_for_url( $element_url, $attachment_id ); } } } } $this->element_urls = array(); $this->url_elements = array(); $this->bulk_image_urls = array(); } /** * @param $elements Element[] * * @return void */ private function collect_bulk_image_urls( $elements ) { foreach ( $elements as $element ) { $original_url = $this->get_original_url( $element ); if ( ! $original_url ) { continue; } $element_key = md5( $original_url ); if ( $element->has_attribute( 'src' ) ) { $src_url = $element->get_attribute( 'src' )->get_single_image_url(); if ( $src_url ) { $src_absolute_url = $src_url->get_absolute_url(); $urls = array( $src_absolute_url, $original_url, $this->url_utils->get_scaled_image_url( $original_url ), ); foreach ( $urls as $url ) { if ( $this->should_add_url( $url ) ) { $this->collect_url( $url, $element_key ); } } } } if ( $element->has_attribute( 'srcset' ) ) { $src_set_urls = $element->get_attribute( 'srcset' )->get_image_urls(); $src_set_urls = $this->array_utils->ensure_array( $src_set_urls ); foreach ( $src_set_urls as $image_url ) { $srcset_url = $image_url->get_absolute_url(); if ( $this->should_add_url( $srcset_url ) ) { $this->collect_url( $srcset_url, $element_key ); } } } } } private function get_original_url( $element ) { $image_attributes = array( 'src', 'srcset' ); foreach ( $image_attributes as $attribute ) { if ( $element->has_attribute( $attribute ) ) { $image_url = $element->get_attribute( $attribute )->get_single_image_url(); if ( $image_url ) { $absolute_url = $image_url->get_absolute_url(); if ( $this->upload_dir->is_uploads_url( $absolute_url ) ) { return $this->url_utils->get_url_without_dimensions( $absolute_url ); } } } } return null; } private function should_add_url( $url ) { return ! empty( $url ) && ! in_array( $url, $this->bulk_image_urls, true ) && $this->upload_dir->is_uploads_url( $url ); } /** * @param string $src_url * @param string $element_key * * @return void */ private function collect_url( $src_url, $element_key ) { $this->bulk_image_urls[] = $src_url; $this->element_urls[ $element_key ][] = $src_url; $this->url_elements[ $src_url ] = $element_key; } } media/class-media-item-size.php 0000644 00000011747 15252476777 0012462 0 ustar 00 <?php namespace Smush\Core\Media; use Smush\Core\File_System; use Smush\Core\Settings; use WP_Smush; class Media_Item_Size { /** * @var string */ private $key; /** * @var string */ private $file_name; /** * @var int */ private $width; /** * @var int */ private $height; /** * @var string */ private $mime_type; /** * @var int */ private $filesize; /** * @var int */ private $attachment_id; /** * @var Settings */ private $settings; /** * @var array */ private $wp_metadata; /** * @var int */ private $size_limit; /** * @var string */ private $dir; /** * @var string */ private $base_url; /** * @var string */ private $extension; /** * @var File_System */ private $fs; /** * @param $key string * @param $attachment_id int * @param $wp_size_metadata array */ public function __construct( $key, $attachment_id, $dir, $base_url, $wp_size_metadata ) { $this->key = $key; $this->attachment_id = $attachment_id; $this->dir = $dir; $this->base_url = $base_url; $this->wp_metadata = $wp_size_metadata; $this->fs = new File_System(); $this->settings = Settings::get_instance(); $this->from_array( $wp_size_metadata ); } /** * @param $size_data array Typically an item from 'sizes' array returned by wp_get_attachment_metadata * * @return void */ private function from_array( $size_data ) { $this->set_file_name( (string) $this->get_array_value( $size_data, 'file' ) ); $this->set_width( (int) $this->get_array_value( $size_data, 'width' ) ); $this->set_height( (int) $this->get_array_value( $size_data, 'height' ) ); $this->set_mime_type( (string) $this->get_array_value( $size_data, 'mime-type' ) ); $this->set_filesize( (int) $this->get_array_value( $size_data, 'filesize' ) ); } private function get_array_value( $array, $key ) { return isset( $array[ $key ] ) ? $array[ $key ] : null; } public function get_file_name_without_extension() { return mb_substr( $this->get_file_name(), 0, mb_strlen( $this->get_file_name() ) - mb_strlen( '.' . $this->get_extension() ) ); } public function get_file_name() { return $this->file_name; } public function set_file_name( $file_name ) { $this->file_name = $file_name; } /** * @return string */ public function get_file_path() { return path_join( $this->dir, $this->get_file_name() ); } public function get_file_url() { $base_url = $this->base_url; $file_name = $this->get_file_name(); return "$base_url$file_name"; } /** * @return int */ public function get_width() { return $this->width; } /** * @param int $width */ public function set_width( $width ) { $this->width = $width; } /** * @return int */ public function get_height() { return $this->height; } /** * @param int $height */ public function set_height( $height ) { $this->height = $height; } /** * @return string */ public function get_mime_type() { return $this->mime_type; } /** * @param string $mime_type */ public function set_mime_type( $mime_type ) { $this->mime_type = $mime_type; } /** * @return int */ public function get_filesize() { return $this->filesize; } /** * @param int $filesize */ public function set_filesize( $filesize ) { $this->filesize = $filesize; } /** * @return string */ public function get_key() { return $this->key; } public function has_wp_metadata() { return ! empty( $this->wp_metadata ); } public function is_smushable() { return $this->is_size_selected_in_settings() && $this->media_image_filter(); } public function exceeds_size_limit() { return $this->get_filesize() > $this->get_size_limit(); } private function media_image_filter() { return apply_filters( 'wp_smush_media_image', true, $this->get_key(), $this->get_file_path(), $this->get_attachment_id() ); } public function file_exists() { return $this->fs->file_exists( $this->get_file_path() ); } private function is_size_selected_in_settings() { if ( $this->get_key() === 'full' ) { return $this->settings->get( 'original' ); } $selected = $this->settings->get_setting( 'wp-smush-image_sizes' ); if ( ! is_array( $selected ) ) { return true; } return in_array( $this->get_key(), $selected ); } /** * @return int */ public function get_size_limit() { if ( is_null( $this->size_limit ) ) { $this->size_limit = $this->settings->get_file_size_limit(); } return $this->size_limit; } /** * @param int $size_limit */ public function set_size_limit( $size_limit ) { $this->size_limit = $size_limit; } public function get_dir() { return $this->dir; } public function get_extension() { if ( is_null( $this->extension ) ) { $this->extension = $this->prepare_extension(); } return $this->extension; } public function prepare_extension() { return pathinfo( $this->get_file_path(), PATHINFO_EXTENSION ); } /** * @return int */ public function get_attachment_id() { return $this->attachment_id; } } media/class-media-item-optimization.php 0000644 00000002520 15252476777 0014223 0 ustar 00 <?php namespace Smush\Core\Media; use WP_Error; abstract class Media_Item_Optimization { /** * @param $media_item Media_Item */ abstract public function __construct( $media_item ); abstract public static function get_key(); abstract public function get_name(); /** * @return Media_Item_Stats */ abstract public function get_stats(); /** * @return Media_Item_Stats */ abstract public function get_size_stats( $size_key ); abstract public function get_optimized_sizes_count(); abstract public function save(); abstract public function is_optimized(); abstract public function should_optimize(); abstract public function should_reoptimize(); /** * @param $size Media_Item_Size */ abstract public function should_optimize_size( $size ); /** * @return mixed */ abstract public function delete_data(); /** * @return boolean */ abstract public function optimize(); public function can_restore() { return false; } public function restore() { return false; } public function has_errors() { $wp_error = $this->get_errors(); return $wp_error && is_a( $wp_error, '\WP_Error' ) && $wp_error->has_errors(); } /** * @return WP_Error */ abstract public function get_errors(); public function get_optimized_file_url( $original_file_url ) { return $original_file_url; } } media/class-media-item.php 0000644 00000073327 15252476777 0011514 0 ustar 00 <?php namespace Smush\Core\Media; use Smush\Core\Animated_Status_Controller; use Smush\Core\Array_Utils; use Smush\Core\Backup_Size; use Smush\Core\File_System; use Smush\Core\Helper; use Smush\Core\Settings; use Smush\Core\Smush_File; use WP_Error; use WP_Smush; class Media_Item extends Smush_File { private static $animated_meta_key = 'wp-smush-animated'; private static $transparent_meta_key = 'wp-smush-transparent'; private static $ignored_meta_key = 'wp-smush-ignore-bulk'; private static $size_key_scaled = 'wp_scaled'; private static $size_key_full = 'full'; private static $backup_sizes_meta_key = '_wp_attachment_backup_sizes'; private static $default_backup_key = 'smush-full'; private $id; /** * @var array|false */ private $metadata; /** * @var string */ private $edit_url; /** * @var string */ private $file; /** * @var Media_Item_Size[] */ private $sizes; /** * @var Settings */ private $plugin_settings; /** * @var \WP_Post */ private $post; /** * @var string[] */ private $animated_mime_types = array( 'image/gif' ); /** * @var bool */ private $animated; /** * @var bool */ private $transparent; /** * @var int */ private $ignored; /** * @var int */ private $size_limit; /** * @var WP_Error */ private $errors; /** * @var array */ private $smushable_sizes; /** * @var bool */ private $is_image; /** * @var bool */ private $mime_type_supported; /** * @var array[] */ private $missing_sizes; private $reset_properties = array( 'metadata', 'file', 'sizes', 'post', 'animated', 'ignored', 'errors', 'smushable_sizes', 'is_image', 'mime_type_supported', 'missing_sizes', 'backup_sizes', 'mime_type', 'attached_file', 'original_image_path', 'outdated_meta_values', ); /** * @var Backup_Size[] */ private $backup_sizes; /** * @var string */ private $mime_type; /** * @var string */ private $attached_file; /** * @var false|string */ private $original_image_path; /** * @var Array_Utils */ private $array_utils; private $registered_wp_sizes; private $outdated_meta_values = array(); /** * @var File_System */ private $fs; public function __construct( $id ) { $this->id = $id; $this->set_settings( Settings::get_instance() ); $this->array_utils = new Array_Utils(); $this->fs = new File_System(); } public static function get_animated_meta_key() { return self::$animated_meta_key; } public static function get_size_key_scaled() { return self::$size_key_scaled; } public static function get_default_backup_key() { return self::$default_backup_key; } public static function get_ignored_meta_key() { return self::$ignored_meta_key; } public static function get_backup_sizes_meta_key() { return self::$backup_sizes_meta_key; } public static function get_size_key_full() { return self::$size_key_full; } public static function get_transparent_meta_key() { return self::$transparent_meta_key; } public function size_limit_exceeded() { foreach ( $this->get_smushable_sizes() as $size ) { if ( $size->exceeds_size_limit() ) { return true; } } return false; } private function get_file_name_exceeding_limit() { foreach ( $this->get_smushable_sizes() as $size ) { if ( $size->exceeds_size_limit() ) { return $size->get_file_name(); } } return ''; } public function set_size_limit( $size_limit ) { $this->size_limit = $size_limit; } public function get_size_limit() { if ( is_null( $this->size_limit ) ) { $this->size_limit = Settings::get_instance()->get_file_size_limit(); } return $this->size_limit; } public function get_human_size_limit() { return size_format( $this->get_size_limit() ); } public function get_id() { return $this->id; } /** * Checks whether important metadata exists for the media item. * * Missing metadata for a size does not mean there is a problem, for example data * is not generated if the image is too small to generate a 'large' version. * So we don't check metadata for sizes. * * @return bool */ public function has_wp_metadata() { $metadata = $this->get_wp_metadata(); return is_array( $metadata ) && ! empty( $metadata ) && ! empty( $metadata['file'] ); } private function get_missing_sizes() { if ( is_null( $this->missing_sizes ) ) { $this->missing_sizes = wp_get_missing_image_subsizes( $this->get_id() ); } return $this->missing_sizes; } /** * TODO: maybe add an error for this * @return bool */ public function has_missing_sizes() { return ! empty( $this->get_missing_sizes() ); } public function get_wp_metadata() { if ( empty( $this->metadata ) ) { $this->metadata = $this->fetch_wp_metadata(); } return $this->metadata; } private function fetch_wp_metadata() { $attachment_metadata = wp_get_attachment_metadata( $this->get_id() ); return $this->array_utils->ensure_array( $attachment_metadata ); } /** * @return void */ private function update_wp_metadata() { $updated_attachment_meta = $this->make_attachment_meta(); if ( ! $this->arrays_same( $this->get_wp_metadata(), $updated_attachment_meta ) ) { wp_update_attachment_metadata( $this->get_id(), $updated_attachment_meta ); } } private function file_name_from_path( $file_path ) { return wp_basename( $file_path ); } /** * TODO: use this instead of Helper::get_image_media_link * @return string */ public function get_edit_url() { if ( is_null( $this->edit_url ) ) { $this->edit_url = $this->prepare_edit_url(); } return $this->edit_url; } private function prepare_edit_url() { $mode = get_user_option( 'media_library_mode' ); $image_id = $this->get_id(); if ( 'grid' === $mode ) { $edit_link = admin_url( "upload.php?item={$image_id}" ); } else { $edit_link = admin_url( "post.php?post={$image_id}&action=edit" ); } return $edit_link; } public function get_edit_link() { $name = $this->get_full_or_scaled_size()->get_file_name(); $url = $this->get_edit_url(); return "<a href='$url'>$name</a>"; } /** * File dir relative to the uploads directory e.g. 2023/05/. Includes trailing slash. * @return string */ public function get_relative_file_dir() { $relative_file_dir = dirname( $this->get_relative_file_path() ); if ( '.' === $relative_file_dir ) { return ''; } return trailingslashit( $relative_file_dir ); } /** * The relative file path e.g. 2023/05/image.png * * @return string */ public function get_relative_file_path() { if ( is_null( $this->file ) ) { $this->file = $this->prepare_relative_file_path(); } return $this->file; } private function prepare_relative_file_path() { $file = (string) $this->get_array_value( $this->get_wp_metadata(), 'file' ); if ( empty( $file ) ) { /** * If metadata is missing we still want some of our functions to work, e.g. backup and restore * * Using _wp_attached_file meta because: * 1. get_attached_file returns the full path but the _wp_attached_file meta has the relative path we need * 2. get_attached_file is filtered which can interfere with our code e.g. the S3 module changes attached file, but we don't want that */ $file = $this->get_post_meta( '_wp_attached_file' ); } return $file; } private function get_post_meta( $key ) { return get_post_meta( $this->get_id(), $key, true ); } public function has_size( $key ) { $sizes = $this->get_sizes(); return ! empty( $sizes[ $key ] ); } public function has_scaled_size() { return $this->has_size( self::$size_key_scaled ); } public function has_full_size() { return $this->has_size( self::$size_key_full ); } /** * @param $key * * @return Media_Item_Size */ public function get_size( $key ) { return $this->get_array_value( $this->get_sizes(), $key ); } public function get_scaled_size() { return $this->get_size( self::$size_key_scaled ); } public function get_full_size() { return $this->get_size( self::$size_key_full ); } public function get_sizes() { if ( is_null( $this->sizes ) ) { $this->sizes = $this->prepare_sizes(); } return $this->sizes; } /** * The 'main' size is the size has get_attached_file as the file path * * @return Media_Item_Size */ public function get_main_size() { return $this->get_scaled_or_full_size(); } private function prepare_sizes() { $media_item_sizes = array(); $metadata_sizes = $this->get_wp_metadata_sizes(); foreach ( $metadata_sizes as $size_key => $metadata_size ) { $registered_size = $this->array_utils->ensure_array( $this->get_registered_wp_size( $size_key ) ); $metadata_size = $this->array_utils->ensure_array( $metadata_size ); $size = $this->initialize_size( $size_key, array_merge( $registered_size, $metadata_size ) ); if ( $size ) { $media_item_sizes[ $size_key ] = $size; } } $scaled_size = $this->prepare_scaled_size(); if ( $scaled_size ) { $media_item_sizes[ self::$size_key_scaled ] = $scaled_size; } $full_size = $this->prepare_full_size(); if ( $full_size ) { $media_item_sizes[ self::$size_key_full ] = $full_size; } return $media_item_sizes; } public function prepare_scaled_size() { $file = $this->get_attached_file(); if ( $file && $this->separate_original_image_path_exists() ) { $wp_size_metadata = $this->attachment_metadata_as_size_metadata( $file ); return $this->initialize_size( self::$size_key_scaled, $wp_size_metadata ); } return null; } private function separate_original_image_path_exists() { $original_image = $this->get_original_image_path(); $main_file = $this->get_attached_file(); return $original_image !== $main_file; } public function prepare_full_size() { $original_image_exists = $this->separate_original_image_path_exists(); if ( $original_image_exists ) { $original_image_file = $this->get_original_image_path(); $image_size = $this->fs->file_exists( $original_image_file ) ? $this->fs->getimagesize( $original_image_file ) : false; if ( ! $image_size ) { return null; } return $this->initialize_size( self::$size_key_full, array( 'file' => $this->file_name_from_path( $original_image_file ), 'width' => $image_size[0], 'height' => $image_size[1], 'mime-type' => $this->get_mime_type(), 'filesize' => $this->fs->filesize( $original_image_file ), ) ); } else { $main_file = $this->get_attached_file(); $wp_size_metadata = $this->attachment_metadata_as_size_metadata( $main_file ); return $this->initialize_size( self::$size_key_full, $wp_size_metadata ); } } public function has_smushable_sizes() { return ! empty( $this->get_smushable_sizes() ); } /** * @return Media_Item_Size[] */ public function get_smushable_sizes() { if ( is_null( $this->smushable_sizes ) ) { $this->smushable_sizes = $this->prepare_smushable_sizes(); } return $this->smushable_sizes; } private function prepare_smushable_sizes() { $sizes = array(); foreach ( $this->get_sizes() as $size_key => $size ) { if ( $size->is_smushable() ) { $sizes[ $size_key ] = $size; } } return $sizes; } private function get_array_value( $array, $key ) { return $array && isset( $array[ $key ] ) ? $array[ $key ] : null; } /** * @return array|mixed */ private function get_wp_metadata_sizes() { // TODO: media items created before a certain WP version might not have the scaled size so that needs to be normalized for all wp versions $metadata = $this->get_wp_metadata(); return empty( $metadata['sizes'] ) ? array() : $metadata['sizes']; } private function get_wp_metadata_size( $size_key ) { $metadata = $this->get_wp_metadata_sizes(); return empty( $metadata[ $size_key ] ) ? array() : $metadata[ $size_key ]; } public function is_skipped() { return $this->is_ignored() || $this->is_animated(); } public function is_mime_type_supported() { if ( is_null( $this->mime_type_supported ) ) { $this->mime_type_supported = $this->check_is_mime_type_supported(); } return $this->mime_type_supported; } private function check_is_mime_type_supported() { $mime_type = $this->get_mime_type(); $supported = in_array( $mime_type, $this->get_supported_mime_types(), true ); return apply_filters( 'wp_smush_resmush_mime_supported', $supported, $mime_type ); } public function is_image() { if ( is_null( $this->is_image ) ) { $this->is_image = $this->check_is_image(); } return $this->is_image; } private function check_is_image() { return wp_attachment_is_image( $this->get_id() ); } private function is_smushable_filter() { return apply_filters( 'wp_smush_is_smushable', true, $this->get_id(), $this->get_supported_mime_types() ); } public function is_ignored() { if ( is_null( $this->ignored ) ) { $this->ignored = $this->prepare_ignored(); } return $this->ignored; } private function prepare_ignored() { return (bool) $this->get_post_meta( self::$ignored_meta_key ); } public function set_ignored( $ignored ) { $this->ignored = $ignored; $this->set_outdated( self::$ignored_meta_key ); } /** * @return void */ private function update_ignored_meta() { if ( ! $this->is_outdated( self::$ignored_meta_key ) ) { return; } if ( $this->is_ignored() ) { update_post_meta( $this->get_id(), self::$ignored_meta_key, true ); } else { delete_post_meta( $this->get_id(), self::$ignored_meta_key ); } } private function smush_image_filter() { return apply_filters( 'wp_smush_image', true, $this->get_id() ); } /** * Checking if a file is really animated is an expensive operation because we look at file frames, so here we check only a meta value and do the actual checking right before bulk smush. * * @return bool * @see Animated_Status_Controller */ public function is_animated() { if ( ! $this->has_animated_mime_type() ) { return false; } if ( is_null( $this->animated ) ) { $this->animated = (bool) $this->get_post_meta( self::$animated_meta_key ); } return $this->animated; } /** * @param $animated * * @return bool */ public function set_animated( $animated ) { if ( ! $this->has_animated_mime_type() ) { return false; } $this->animated = (bool) $animated; $this->set_outdated( self::$animated_meta_key ); return true; } /** * @return void */ private function update_animated_meta() { if ( $this->is_outdated( self::$animated_meta_key ) ) { update_post_meta( $this->get_id(), self::$animated_meta_key, $this->is_animated() ? 1 : 0 ); } } public function animated_meta_exists() { $animated_meta_value = $this->get_post_meta( self::$animated_meta_key ); // Post meta default is empty string so a bool means there is a row in the meta table return is_numeric( $animated_meta_value ); } /** * Checking if a file is really transparent is an expensive operation because we look at file contents, so here we check only a meta value and do the actual checking elsewhere. */ public function is_transparent() { if ( ! $this->is_png() ) { return false; } if ( is_null( $this->transparent ) ) { $this->transparent = (bool) $this->get_post_meta( self::$transparent_meta_key ); } return $this->transparent; } public function set_transparent( $transparent ) { if ( ! $this->is_png() ) { return false; } $this->transparent = (bool) $transparent; $this->set_outdated( self::$transparent_meta_key ); return true; } /** * @return void */ private function update_transparent_meta() { if ( ! $this->is_png() ) { // Maybe the mime type has changed, and we should delete the transparent meta value added when the mime type was PNG if ( $this->transparent_meta_exists() ) { delete_post_meta( $this->get_id(), self::$transparent_meta_key ); } } else { if ( $this->is_outdated( self::$transparent_meta_key ) ) { // Unlike most other meta values we will not delete the meta because even a false value is useful: it tells us we have checked transparency before. update_post_meta( $this->get_id(), self::$transparent_meta_key, $this->is_transparent() ? 1 : 0 ); } } } public function transparent_meta_exists() { $transparent_meta_value = $this->get_post_meta( self::$transparent_meta_key ); // Post meta default is empty string so a bool means there is a row in the meta table return is_numeric( $transparent_meta_value ); } public function is_valid() { return ! empty( $this->get_wp_metadata() ) && $this->has_attached_file(); } private function has_attached_file() { return ! empty( $this->get_attached_file() ); } /** * @return bool */ public function has_animated_mime_type() { return in_array( $this->get_mime_type(), $this->animated_mime_types, true ); } private function get_missing_file_name() { foreach ( $this->get_smushable_sizes() as $size ) { if ( ! $size->file_exists() ) { return $size->get_file_name(); } } return ''; } private function files_exist() { foreach ( $this->get_smushable_sizes() as $size ) { if ( ! $size->file_exists() ) { return false; } } return true; } public function set_settings( $settings ) { $this->plugin_settings = $settings; } private function get_post() { if ( is_null( $this->post ) ) { $this->post = get_post( $this->get_id() ); } return $this->post; } public function get_mime_type() { if ( is_null( $this->mime_type ) ) { $this->mime_type = $this->fetch_post_mime_type(); } return $this->mime_type; } private function fetch_post_mime_type() { $post = $this->get_post(); return empty( $post ) ? '' : $post->post_mime_type; } public function set_mime_type( $mime_type ) { $this->mime_type = $mime_type; } /** * @return void */ private function update_post_mime_type() { if ( $this->get_mime_type() !== $this->fetch_post_mime_type() ) { wp_update_post( array( 'ID' => $this->get_id(), 'post_mime_type' => $this->get_mime_type(), ) ); } } public function save() { $this->update_ignored_meta(); if ( $this->is_valid() ) { // We don't want to touch the rest of the stuff if the item is not valid. // For example if we don't have metadata to begin with then don't try to update it now. $this->update_animated_meta(); $this->update_transparent_meta(); $this->update_attached_file(); $this->update_post_mime_type(); $this->update_wp_metadata(); $this->update_backup_sizes(); } // Force everything to be reloaded from DB $this->reset(); } public function prepare_errors() { $errors = new WP_Error(); if ( ! $this->is_image() ) { $errors->add( 'not_an_image', esc_html__( "Attachment is not an image so it can't be smushed.", 'wp-smushit' ) ); } if ( ! $this->is_mime_type_supported() ) { $errors->add( 'unsupported_mime_type', /* translators: %s: Image mime type */ sprintf( esc_html__( 'The mime type %s is not supported by Smush.', 'wp-smushit' ), $this->get_mime_type() ) ); } if ( ! $this->has_wp_metadata() ) { $errors->add( 'no_file_meta', esc_html__( 'No file data found in image meta', 'wp-smushit' ) ); } if ( ! $this->has_attached_file() ) { $errors->add( 'no_attached_file', esc_html__( 'Missing attached file data in image metadata', 'wp-smushit' ) ); } // Verify missing the full size due to the original image not found for wp.com since we only allowed the full size. // @see Photon_Controller::only_handle_full_size(). if ( ! $this->get_scaled_or_full_size() ) { $original_file = $this->get_original_image_path(); $errors->add( 'file_not_found', /* translators: %s: The missing file name */ sprintf( esc_html__( 'Skipped (%s). File not found.', 'wp-smushit' ), basename( $original_file ) ) ); } elseif ( ! $this->files_exist() ) { $errors->add( 'file_not_found', /* translators: %s: The missing file name */ sprintf( esc_html__( 'Skipped (%s). File not found.', 'wp-smushit' ), $this->get_missing_file_name() ) ); } if ( $this->size_limit_exceeded() ) { $errors->add( 'size_limit', /* translators: 1: Exceeded size limit file name, 2: Image size limit */ sprintf( esc_html__( 'Skipped (%1$s). File size limit of %2$s exceeded', 'wp-smushit' ), $this->get_file_name_exceeding_limit(), $this->get_human_size_limit() ) ); } if ( ! $this->smush_image_filter() || ! $this->is_smushable_filter() ) { $errors->add( 'skipped_filter', /* translators: %s: Smush image filter */ sprintf( esc_html__( 'Skipped with %s filter.', 'wp-smushit' ), ! $this->smush_image_filter() ? 'wp_smush_image' : 'wp_smush_is_smushable' ) ); } return $errors; } /** * @return WP_Error */ public function get_errors() { if ( is_null( $this->errors ) ) { $this->errors = $this->prepare_errors(); } return $this->errors; } public function has_errors() { return $this->get_errors()->has_errors(); } /** * @param $file * * @return bool */ private function file_path_has_scaled_postfix( $file ) { return false !== strpos( $file, '-scaled.' ); } public function get_dir() { $upload_dir = wp_upload_dir(); $basedir = untrailingslashit( $upload_dir['basedir'] ); $file_dir = $this->get_relative_file_dir(); return "$basedir/$file_dir"; } public function get_base_url() { $upload_dir = wp_upload_dir(); $upload_dir_url = untrailingslashit( $upload_dir['baseurl'] ); $file_dir = $this->get_relative_file_dir(); return "$upload_dir_url/$file_dir"; } /** * Transform the metadata for a size, so it can be used to initialize a size object * @return array|false */ private function attachment_metadata_as_size_metadata( $file_path ) { $size_metadata = array( // Size data is expected to have just the file name instead of path. 'file' => $this->file_name_from_path( $file_path ), // Size data is expected to have 'mime-type'. 'mime-type' => $this->get_mime_type(), ); if ( $this->fs->file_exists( $file_path ) ) { // Some older WP versions don't have filesize in wp_metadata. $size_metadata['filesize'] = $this->fs->filesize( $file_path ); } return array_merge( $this->get_wp_metadata(), $size_metadata ); } private function initialize_size( $key, $metadata ) { $size = new Media_Item_Size( $key, $this->get_id(), $this->get_dir(), $this->get_base_url(), $metadata ); return apply_filters( 'wp_smush_media_item_size', $size, $key, $metadata, $this ); } /** * @return array */ private function get_registered_wp_sizes() { if ( is_null( $this->registered_wp_sizes ) ) { $this->registered_wp_sizes = Helper::get_image_sizes(); } return $this->registered_wp_sizes; } private function get_registered_wp_size( $size_key ) { return $this->array_utils->get_array_value( $this->get_registered_wp_sizes(), $size_key ); } public function set_registered_wp_sizes( $registered_wp_sizes ) { $this->registered_wp_sizes = $registered_wp_sizes; } public function reset() { foreach ( $this->reset_properties as $property ) { $this->$property = null; } } /** * @return false|string */ public function get_attached_file() { if ( is_null( $this->attached_file ) ) { $this->attached_file = get_attached_file( $this->get_id() ); } return $this->attached_file; } /** * @return void */ private function update_attached_file() { $main_size = $this->get_main_size(); $updated_attached_file = $main_size->get_file_path(); if ( $updated_attached_file !== $this->get_attached_file() ) { update_attached_file( $this->get_id(), $updated_attached_file ); } } private function make_attachment_meta() { $sizes = array(); foreach ( $this->get_sizes() as $size_key => $size ) { if ( $size_key === self::$size_key_full || $size_key === self::$size_key_scaled ) { continue; } $sizes[ $size_key ] = array( 'file' => $size->get_file_name(), 'width' => $size->get_width(), 'height' => $size->get_height(), 'mime-type' => $size->get_mime_type(), 'filesize' => $size->get_filesize(), ); } $short_dir = $this->get_relative_file_dir(); $main_size = $this->get_main_size(); $new_meta = array( 'file' => "$short_dir{$main_size->get_file_name()}", 'width' => $main_size->get_width(), 'height' => $main_size->get_height(), 'filesize' => $main_size->get_filesize(), 'sizes' => $sizes, ); if ( $this->separate_original_image_path_exists() && $this->has_full_size() ) { // If the original image exists then we must have used it when preparing the full size, // use it now to update the original_image value in the meta $new_meta['original_image'] = $this->get_full_size()->get_file_name(); } return array_merge( $this->get_wp_metadata(), $new_meta ); } private function arrays_same( $array1, $array2 ) { if ( ! is_array( $array1 ) || ! is_array( $array2 ) || count( $array1 ) !== count( $array2 ) ) { return false; } return $this->array_utils->array_hash( $array1 ) === $this->array_utils->array_hash( $array2 ); } /** * @return false|string */ public function get_original_image_path() { if ( is_null( $this->original_image_path ) ) { $this->original_image_path = wp_get_original_image_path( $this->get_id() ); } return $this->original_image_path; } public function get_backup_sizes() { if ( is_null( $this->backup_sizes ) ) { $this->backup_sizes = $this->prepare_backup_sizes(); } return $this->backup_sizes; } /** * @param $backup_sizes Backup_Size[] * * @return void */ private function set_backup_sizes( $backup_sizes ) { $this->backup_sizes = $backup_sizes; $this->set_outdated( self::$backup_sizes_meta_key ); } /** * @return void */ private function update_backup_sizes() { if ( ! $this->is_outdated( self::$backup_sizes_meta_key ) ) { return; } $updated_backup_sizes_meta = $this->make_backup_sizes_meta(); if ( ! $this->arrays_same( $this->get_backup_sizes_meta(), $updated_backup_sizes_meta ) ) { update_post_meta( $this->get_id(), self::$backup_sizes_meta_key, $updated_backup_sizes_meta ); } } private function prepare_backup_sizes() { $backup_sizes = array(); $backup_sizes_meta = $this->get_backup_sizes_meta(); foreach ( $backup_sizes_meta as $backup_size_key => $backup_size_meta ) { $backup_size = new Backup_Size( $this->get_dir() ); $backup_size->from_array( $backup_size_meta ); $backup_sizes[ $backup_size_key ] = $backup_size; } return $backup_sizes; } /** * @return Backup_Size|null */ public function get_default_backup_size() { return $this->get_backup_size( self::$default_backup_key ); } /** * @param $file_name * @param $width * @param $height * @param $key * * @return void */ public function add_backup_size( $file_name, $width, $height, $key = null ) { if ( is_null( $key ) ) { $key = self::$default_backup_key; } $backup_sizes = $this->get_backup_sizes(); $dir = $this->get_dir(); $backup_size = ( new Backup_Size( $dir ) )->set_file( $file_name ) ->set_width( $width ) ->set_height( $height ); $backup_sizes[ $key ] = $backup_size; $this->set_backup_sizes( $backup_sizes ); } public function make_backup_sizes_meta() { return array_map( function ( $backup_size ) { return $backup_size->to_array(); }, $this->get_backup_sizes() ); } /** * @return array|mixed */ private function get_backup_sizes_meta() { $backup_sizes_meta = $this->get_post_meta( self::$backup_sizes_meta_key ); return empty( $backup_sizes_meta ) ? array() : $backup_sizes_meta; } /** * @param $key * * @return Backup_Size|null */ public function get_backup_size( $key ) { return $this->get_array_value( $this->get_backup_sizes(), $key ); } public function remove_default_backup_size() { $this->remove_backup_size( self::$default_backup_key ); } public function remove_backup_size( $key ) { $backup_sizes = $this->get_backup_sizes(); if ( isset( $backup_sizes[ $key ] ) ) { unset( $backup_sizes[ $key ] ); } $this->set_backup_sizes( $backup_sizes ); $this->set_outdated( self::$backup_sizes_meta_key ); } public function get_scaled_or_full_size() { return $this->has_scaled_size() ? $this->get_scaled_size() : $this->get_full_size(); } public function get_full_or_scaled_size() { return $this->has_full_size() ? $this->get_full_size() : $this->get_scaled_size(); } public function get_size_urls() { return array_map( function ( $size ) { return $size->get_file_url(); }, $this->get_sizes() ); } public function get_size_paths() { return array_map( function ( $size ) { return $size->get_file_path(); }, $this->get_sizes() ); } private function is_outdated( $key ) { $outdated_values = empty( $this->outdated_meta_values ) ? array() : $this->outdated_meta_values; return ! empty( $outdated_values[ $key ] ); } private function set_outdated( $key ) { if ( empty( $this->outdated_meta_values ) ) { $this->outdated_meta_values = array(); } $this->outdated_meta_values[ $key ] = true; } public function is_png() { $mime = $this->get_mime_type(); return 'image/png' === $mime || 'image/x-png' === $mime; } public function can_be_restored() { // Note that we don't check if file exists because the file might be on a remote server e.g. s3 return ! empty( $this->get_default_backup_size() ); } public function is_large() { $file_size = $this->get_full_or_scaled_size()->get_filesize(); $cut_off = $this->plugin_settings->get_large_file_cutoff(); return $file_size > $cut_off; } public function set_wp_metadata( $metadata ) { $this->metadata = $metadata; } public function set_attached_file( $attached_file ) { $this->attached_file = $attached_file; } public function set_original_image_path( $original_image_path ) { $this->original_image_path = $original_image_path; } } media/class-media-item-query.php 0000644 00000023033 15252476777 0012644 0 ustar 00 <?php namespace Smush\Core\Media; use Smush\Core\Array_Utils; use Smush\Core\Smush\Smush_Optimization; use Smush\Core\Smush_File; use Smush\Core\Url_Utils; class Media_Item_Query { /** * @var Url_Utils */ private $url_utils; /** * @var Array_Utils */ private $array_utils; public function __construct() { $this->url_utils = new Url_Utils(); $this->array_utils = new Array_Utils(); } public function fetch( $offset = 0, $limit = - 1 ) { global $wpdb; $query = $this->make_query( 'ID', $offset, $limit ); return $wpdb->get_col( $query ); } public function fetch_slice_post_meta( $slice, $slice_size ) { global $wpdb; $offset = $this->get_offset( $slice, $slice_size ); $limit = (int) $slice_size; $ids_query = $this->make_query( 'ID', $offset, $limit ); $query = "SELECT CONCAT(post_id, '-', meta_key), post_id, meta_key, meta_value FROM $wpdb->postmeta WHERE post_id IN (SELECT * FROM ($ids_query) AS slice_ids);"; return $wpdb->get_results( $query, OBJECT_K ); } public function fetch_slice_posts( $slice, $slice_size ) { global $wpdb; $offset = $this->get_offset( $slice, $slice_size ); $limit = (int) $slice_size; $posts_query = $this->make_query( '*', $offset, $limit ); return $wpdb->get_results( $posts_query, OBJECT_K ); } public function fetch_slice_ids( $slice, $slice_size ) { $offset = $this->get_offset( $slice, $slice_size ); $limit = (int) $slice_size; return $this->fetch( $offset, $limit ); } public function get_slice_count( $slice_size ) { if ( empty( $slice_size ) ) { return 0; } $image_attachment_count = $this->get_image_attachment_count(); return (int) ceil( $image_attachment_count / $slice_size ); } public function get_image_attachment_count() { global $wpdb; $query = $this->make_query( 'COUNT(*)' ); return (int) $wpdb->get_var( $query ); } /** * @param $select * @param $offset * @param $limit * * @return string|null */ private function make_query( $select, $offset = 0, $limit = - 1 ) { global $wpdb; $mime_types = ( new Smush_File() )->get_supported_mime_types(); $placeholders = implode( ',', array_fill( 0, count( $mime_types ), '%s' ) ); $column = $select; $query = "SELECT %s FROM $wpdb->posts WHERE post_type = 'attachment' AND post_mime_type IN (%s)"; $query = sprintf( $query, $column, $placeholders ); $args = $mime_types; if ( $limit > 0 ) { $query = "$query LIMIT %d"; $args[] = $limit; if ( $offset >= 0 ) { $query = "$query OFFSET %d"; $args[] = $offset; } } return $wpdb->prepare( $query, $args ); } public function get_lossy_count() { global $wpdb; $query = $wpdb->prepare( "SELECT COUNT(DISTINCT post_id) FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = 1", Smush_Optimization::get_lossy_meta_key() ); return $wpdb->get_var( $query ); } public function get_smushed_count() { global $wpdb; $query = $wpdb->prepare( "SELECT COUNT(DISTINCT post_meta_optimized.post_id) FROM $wpdb->postmeta as post_meta_optimized LEFT JOIN $wpdb->postmeta as post_meta_ignored ON post_meta_optimized.post_id = post_meta_ignored.post_id AND post_meta_ignored.meta_key= %s WHERE post_meta_optimized.meta_key = %s AND post_meta_ignored.meta_value IS NULL", Media_Item::get_ignored_meta_key(), Smush_Optimization::get_smush_meta_key() ); return $wpdb->get_var( $query ); } public function get_ignored_count() { global $wpdb; $query = $wpdb->prepare( "SELECT COUNT(DISTINCT post_id) FROM $wpdb->postmeta WHERE meta_key = %s", Media_Item::get_ignored_meta_key() ); return $wpdb->get_var( $query ); } /** * @param $slice * @param $slice_size * * @return float|int */ private function get_offset( $slice, $slice_size ) { $slice = (int) $slice; $slice_size = (int) $slice_size; return ( $slice - 1 ) * $slice_size; } /** * @see attachment_url_to_postid() */ public function attachment_urls_to_ids( $absolute_urls ) { if ( empty( $absolute_urls ) ) { return array(); } $absolute_key_relative_value = array(); $relative_key_absolute_value = array(); foreach ( $absolute_urls as $absolute_url ) { $relative_url = $this->convert_attachment_url_to_relative( $absolute_url ); $absolute_key_relative_value[ $absolute_url ] = $relative_url; $relative_key_absolute_value[ $relative_url ] = $absolute_url; } $escaped_relative_urls = array_map( function ( $relative_url ) { return "'" . esc_sql( $relative_url ) . "'"; }, $absolute_key_relative_value ); global $wpdb; /** * Maximum number of URLs per IN() clause when resolving attachment IDs. * Lower this on hosts that kill long queries (e.g. WP Engine). * 100 is summing to good enough number of characters usually. * * @param int $chunk_size Default 100. */ $chunk_size = (int) apply_filters( 'wp_smush_attachment_urls_chunk_size', 100 ); $chunks = array_chunk( $escaped_relative_urls, $chunk_size ); $results = array(); foreach ( $chunks as $chunk ) { $in = join( ',', $chunk ); $sql = "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value IN ({$in})"; $chunk_result = $wpdb->get_results( $sql, ARRAY_A ); if ( ! empty( $chunk_result ) ) { $results = array_merge( $results, $chunk_result ); } } if ( empty( $results ) ) { return array(); } $ids = array(); foreach ( $results as $result ) { $meta_value = $result['meta_value']; $original_absolute_url = $relative_key_absolute_value[ $meta_value ]; $ids[ $original_absolute_url ] = $result['post_id']; } return $ids; } public function urls_to_size_data( $urls ) { if ( empty( $urls ) || ! is_array( $urls ) ) { return array(); } global $wpdb; $wild = '%'; $meta_value_likes = []; foreach ( $urls as $url ) { $meta_value_likes[] = $wpdb->prepare( "meta_value LIKE %s", $wild . $wpdb->esc_like( basename( $url ) ) . $wild ); } $where = join( ' OR ', $meta_value_likes ); $sql = "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = '_wp_attachment_metadata' AND ({$where})"; $db_results = $wpdb->get_results( $sql, ARRAY_A ); if ( empty( $db_results ) ) { return array(); } return $this->prepare_urls_to_size_data_result( $urls, $db_results ); } private function get_main_file_data_from_wp_attachment_metadata( $attachment_id, $meta_value, $absolute_url ) { if ( empty( $meta_value ) ) { return array(); } $file = $this->array_utils->get_array_value( $meta_value, 'file' ); if ( $this->convert_attachment_url_to_relative( $absolute_url ) === $file ) { $width = $this->array_utils->get_array_value( $meta_value, 'width' ); $height = $this->array_utils->get_array_value( $meta_value, 'height' ); if ( $width && $height ) { return array( 'id' => (int) $attachment_id, 'width' => (int) $width, 'height' => (int) $height, ); } } return array(); } private function get_size_data_from_wp_attachment_metadata( $attachment_id, $meta_value, $absolute_url ) { if ( empty( $meta_value ) ) { return array(); } $sizes = $this->array_utils->get_array_value( $meta_value, 'sizes' ); $sizes = $this->array_utils->ensure_array( $sizes ); if ( empty( $sizes ) ) { return array(); } foreach ( $sizes as $size ) { $file = $this->array_utils->get_array_value( $size, 'file' ); if ( basename( $absolute_url ) === $file ) { $width = $this->array_utils->get_array_value( $size, 'width' ); $height = $this->array_utils->get_array_value( $size, 'height' ); if ( $file && $width && $height ) { return array( 'id' => (int) $attachment_id, 'width' => (int) $width, 'height' => (int) $height, ); } } } return array(); } private function convert_attachment_url_to_relative( $url ) { return $this->url_utils->make_media_url_relative( $url ); } /** * @param $urls * @param array $db_results * * @return array */ private function prepare_urls_to_size_data_result( $urls, $db_results ) { $return = array(); foreach ( $urls as $url ) { if ( $this->is_non_media_library_url( $url ) ) { continue; } foreach ( $db_results as $result ) { $attachment_id = $this->array_utils->get_array_value( $result, 'post_id' ); $meta_value = $this->array_utils->get_array_value( $result, 'meta_value' ); if ( empty( $attachment_id ) || empty( $meta_value ) ) { continue; } $file_name = basename( $url ); if ( strpos( $meta_value, $file_name ) === false ) { continue; } $meta_value = maybe_unserialize( $meta_value ); $main_file_data = $this->get_main_file_data_from_wp_attachment_metadata( $attachment_id, $meta_value, $url ); if ( ! empty( $main_file_data ) ) { $return[ $url ] = $main_file_data; break; } else { // Look for a size $size_data = $this->get_size_data_from_wp_attachment_metadata( $attachment_id, $meta_value, $url ); if ( ! empty( $size_data ) ) { $return[ $url ] = $size_data; break; } } } } return $return; } /** * @param $url * * @return bool */ private function is_non_media_library_url( $url ) { return $this->convert_attachment_url_to_relative( $url ) === $url; } /** * Get the count of optimization errors. * * @return int */ public function get_optimization_errors_count() { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT post_id) FROM $wpdb->postmeta WHERE meta_key = %s", 'wp-smush-optimization-errors' ) ); } } media/class-media-item-optimizer.php 0000644 00000027764 15252476777 0013540 0 ustar 00 <?php namespace Smush\Core\Media; use Smush\Core\Backups\Backups; use Smush\Core\Helper; use Smush\Core\Smush\Smush_Optimization; use Smush\Core\Stats\Global_Stats; use WDEV_Logger; use WP_Error; class Media_Item_Optimizer { private static $error_meta_key = 'wp-smush-optimization-errors'; /** * @var Media_Item_Optimization[] */ private $optimizations; /** * @var Media_Item */ private $media_item; /** * @var Backups */ private $backups; /** * @var WDEV_Logger */ private $logger; /** * @var Global_Stats */ private $global_stats; /** * @var WP_Error */ private $errors; /** * Restoration errors. * * @var WP_Error */ private $restoration_errors; /** * @param $media_item Media_Item */ public function __construct( $media_item ) { $this->media_item = $media_item; $this->backups = new Backups(); $this->logger = Helper::logger(); $this->global_stats = Global_Stats::get(); } /** * @return Media_Item_Optimization[] */ public function get_optimizations() { if ( is_null( $this->optimizations ) ) { $this->optimizations = $this->initialize_optimizations(); } return $this->optimizations; } public function set_optimizations( $optimizations ) { $this->optimizations = $optimizations; } private function initialize_optimizations() { return apply_filters( 'wp_smush_optimizations', array(), $this->media_item ); } /** * TODO: check the uses for this method to make sure they are prepared to receive null * * @param $key * * @return Media_Item_Optimization|null */ public function get_optimization( $key ) { return $this->get_array_value( $this->get_optimizations(), $key ); } /** * @param $key * * @return Media_Item_Stats */ public function get_stats( $key ) { $optimization = $this->get_optimization( $key ); if ( $optimization ) { return $optimization->get_stats(); } return new Media_Item_Stats(); } public function get_total_stats() { $total_stats = new Media_Item_Stats(); foreach ( $this->get_optimizations() as $optimization ) { $total_stats->add( $optimization->get_stats() ); } return $total_stats; } /** * @param $optimization_key * @param $size_key * * @return Media_Item_Stats */ public function get_size_stats( $optimization_key, $size_key ) { $optimization = $this->get_optimization( $optimization_key ); if ( $optimization ) { return $optimization->get_size_stats( $size_key ); } return new Media_Item_Stats(); } public function get_total_size_stats( $size_key ) { $total_stats = new Media_Item_Stats(); foreach ( $this->get_optimizations() as $optimization ) { $total_stats->add( $optimization->get_size_stats( $size_key ) ); } return $total_stats; } public function get_optimized_sizes_count() { $size_count = 0; foreach ( $this->get_optimizations() as $optimization ) { $optimized_sizes_count = $optimization->get_optimized_sizes_count(); if ( $optimized_sizes_count > $size_count ) { $size_count = $optimized_sizes_count; } } return $size_count; } /** * Whether the media item was optimized at some point. It may need to be reoptimized. * * @return bool */ public function is_optimized() { foreach ( $this->get_optimizations() as $optimization ) { if ( $optimization->is_optimized() ) { return true; } } return false; } public function should_optimize() { foreach ( $this->get_optimizations() as $optimization ) { if ( $optimization->should_optimize() ) { return true; } } return false; } public function should_reoptimize() { $should_reoptimize = false; foreach ( $this->get_optimizations() as $optimization ) { if ( $optimization->should_reoptimize() ) { $should_reoptimize = true; } } return apply_filters( 'wp_smush_should_resmush', $should_reoptimize, $this->media_item->get_id() ); } public function optimize() { if ( $this->restore_in_progress() ) { $this->logger->log( 'Prevented auto-smush during restore.' ); return false; } if ( $this->in_progress() ) { $this->handle_error( 'in_progress', 'Smush already in progress' ); return false; } $third_party_errors = new WP_Error(); $media_item = $this->media_item; /** * Fires before Smushing a file. * * @param int $attachment_id Attachment ID. * @param array $ref_meta Metadata. * @param WP_Error $third_party_errors A WP_Error object allowing third-parties to stop the smush process. */ do_action( 'wp_smush_before_smush_attempt', $media_item->get_id(), $media_item->get_wp_metadata(), $third_party_errors ); if ( $third_party_errors->has_errors() ) { $this->logger->log( 'Got errors from a third-party while executing the wp_smush_before_smush_file action.' ); return false; } if ( $media_item->has_errors() || $media_item->is_skipped() ) { $this->adjust_global_stats_lists(); return false; } do_action( 'wp_smush_before_smush_file', $media_item->get_id(), $media_item->get_wp_metadata() ); $this->set_in_progress_transient(); $this->backups->maybe_create_backup( $media_item, $this ); $optimized = $this->run_optimizations(); if ( $optimized ) { do_action( 'wp_smush_after_smush_successful', $media_item->get_id(), $media_item->get_wp_metadata() ); $this->delete_previous_optimization_errors(); } else { $this->handle_optimization_errors(); } // This needs to be triggered after handle_optimization_errors so that get_errors will return correct errors do_action( 'wp_smush_after_smush_file', $media_item->get_id(), $media_item->get_wp_metadata(), $optimized ? array() : $this->get_errors() ); $this->delete_in_progress_transient(); return $optimized; } public function restore() { if ( $this->in_progress() || $this->restore_in_progress() ) { return false; } $this->set_restore_in_progress_transient(); $this->reset_restoration_errors(); $restoration_attempted = false; $restored = false; // First, allow one of the optimizations to handle the restoration process foreach ( $this->get_optimizations() as $optimization ) { if ( $optimization->can_restore() ) { $restoration_attempted = true; $restored = $optimization->restore(); break; } } if ( ! $restoration_attempted ) { // Try the standard restoration $restored = $this->backups->restore_backup( $this->media_item ); } if ( $restored ) { // Before deleting all data subtract the stats $this->global_stats->subtract_item_stats( $this->media_item ); $this->global_stats->subtract_optimized_images_count( $this->get_optimized_sizes_count() ); // Delete all the optimization data $this->delete_data(); // Delete optimization errors. $this->delete_previous_optimization_errors(); // Once all data has been deleted, adjust the lists $this->global_stats->adjust_lists_for_media_item( $this->media_item ); } else { $this->set_restoration_errors( $this->backups->get_errors() ); } $this->delete_restore_in_progress_transient(); return $restored; } public function save() { foreach ( $this->get_optimizations() as $optimization ) { $optimization->save(); } } private function get_array_value( $array, $key ) { return $array && isset( $array[ $key ] ) ? $array[ $key ] : null; } /** * @param Media_Item_Size $full_size * * @return boolean */ public function should_optimize_size( $full_size ) { $should_optimize_size = false; foreach ( $this->get_optimizations() as $optimization ) { if ( $optimization->should_optimize_size( $full_size ) ) { $should_optimize_size = true; break; } } return $should_optimize_size; } public function delete_data() { foreach ( $this->get_optimizations() as $optimization ) { $optimization->delete_data(); } } /** * @return bool */ private function run_optimizations() { $all_optimized = true; foreach ( $this->get_optimizations() as $optimization ) { if ( $optimization->should_optimize() ) { $current_optimized = $optimization->optimize(); $all_optimized = $all_optimized && $current_optimized; } } return $all_optimized; } private function adjust_global_stats_lists() { $this->global_stats->adjust_lists_for_media_item( $this->media_item ); } private function set_in_progress_transient() { set_transient( $this->in_progress_transient_key(), 1, HOUR_IN_SECONDS ); } private function delete_in_progress_transient() { delete_transient( $this->in_progress_transient_key() ); } public function in_progress() { return (bool) get_transient( $this->in_progress_transient_key() ); } private function in_progress_transient_key() { return 'smush-in-progress-' . $this->media_item->get_id(); } private function set_restore_in_progress_transient() { set_transient( $this->restore_in_progress_transient_key(), 1, HOUR_IN_SECONDS ); } private function delete_restore_in_progress_transient() { delete_transient( $this->restore_in_progress_transient_key() ); } public function restore_in_progress() { return (bool) get_transient( $this->restore_in_progress_transient_key() ); } private function restore_in_progress_transient_key() { return 'wp-smush-restore-' . $this->media_item->get_id(); } /** * @param $code * @param $error_message * * @return void */ private function handle_error( $code, $error_message ) { $this->logger->error( $error_message ); $this->set_errors( new WP_Error( $code, $error_message ) ); $this->update_errors_meta(); } public function get_errors() { if ( is_null( $this->errors ) ) { $this->errors = $this->fetch_errors_from_meta(); } return $this->errors; } private function set_errors( $errors ) { $this->errors = $errors; } public function has_errors() { return $this->get_errors()->has_errors(); } private function set_optimization_errors() { $errors = new WP_Error(); // Add optimization errors foreach ( $this->get_optimizations() as $optimization ) { if ( $optimization->has_errors() ) { $errors->merge_from( $optimization->get_errors() ); } } $this->set_errors( $errors ); } private function fetch_errors_from_meta() { $wp_error = new WP_Error(); $errors = get_post_meta( $this->media_item->get_id(), self::$error_meta_key, true ); if ( empty( $errors ) || ! is_array( $errors ) ) { return $wp_error; } foreach ( $errors as $error_code => $error_message ) { if ( empty( $error_message ) ) { continue; } if ( is_array( $error_message ) ) { foreach ( $error_message as $error ) { $wp_error->add( $error_code, $error ); } } else { $wp_error->add( $error_code, $error_message ); } } return $wp_error; } private function update_errors_meta() { $errors_array = array(); foreach ( $this->errors->get_error_codes() as $error_code ) { $errors_array[ $error_code ] = $this->errors->get_error_messages( $error_code ); } if ( ! empty( $errors_array ) ) { update_post_meta( $this->media_item->get_id(), self::$error_meta_key, $errors_array ); } } /** * @return void */ private function handle_optimization_errors() { $this->set_optimization_errors(); $this->update_errors_meta(); } private function delete_previous_optimization_errors() { if ( $this->has_errors() ) { delete_post_meta( $this->media_item->get_id(), self::$error_meta_key ); $this->set_errors( null ); } } /** * Reset restoration errors. * * @return void */ private function reset_restoration_errors() { $this->restoration_errors = null; } /** * Set restoration errors. * * @param WP_Error $errors Restoration errors. */ private function set_restoration_errors( $errors ) { $this->restoration_errors = $errors; } /** * Get restoration errors. * * @return WP_Error */ public function get_restoration_errors() { if ( ! $this->restoration_errors ) { $this->restoration_errors = new WP_Error(); } return $this->restoration_errors; } /** * Get error_meta_key. * * @return string */ public static function get_error_meta_key() { return self::$error_meta_key; } } media/class-media-item-optimization-global-stats.php 0000644 00000007264 15252476777 0016627 0 ustar 00 <?php namespace Smush\Core\Media; use Smush\Core\Array_Utils; class Media_Item_Optimization_Global_Stats extends Media_Item_Stats { /** * @var int How many media *items* are included in this instance. */ private $count = 0; /** * @var int[] Ids of the attachments included in this instance. */ private $attachment_ids = array(); private $array_utils; public function __construct() { $this->array_utils = new Array_Utils(); } public function to_array() { $array = parent::to_array(); $array['count'] = $this->get_count(); $array['attachment_ids'] = join( ',', $this->get_attachment_ids() ); return $array; } public function from_array( $array ) { parent::from_array( $array ); $this->set_count( (int) $this->get_array_value( $array, 'count' ) ); $attachment_ids = $this->get_array_value( $array, 'attachment_ids' ); $attachment_ids = empty( $attachment_ids ) ? array() : explode( ',', $attachment_ids ); $this->set_attachment_ids( $attachment_ids ); } /** * @param $attachment_id int * @param $item_stats Media_Item_Stats * * @return boolean */ public function add_item_stats( $attachment_id, $item_stats ) { if ( $this->has_attachment_id( $attachment_id ) ) { return false; } else { parent::add( $item_stats ); $this->set_count( $this->get_count() + 1 ); $this->add_attachment_id( $attachment_id ); return true; } } /** * @param $attachment_id int * @param $item_stats Media_Item_Stats * * @return boolean */ public function subtract_item_stats( $attachment_id, $item_stats ) { if ( $this->has_attachment_id( $attachment_id ) ) { parent::subtract( $item_stats ); $this->set_count( $this->get_count() - 1 ); $this->remove_attachment_id( $attachment_id ); return true; } else { return false; } } /** * @param $addend Media_Item_Optimization_Global_Stats * * @return void */ public function add( $addend ) { parent::add( $addend ); $this->set_count( $this->get_count() + $addend->get_count() ); $this->set_attachment_ids( $this->array_utils->fast_array_unique( array_merge( $this->get_attachment_ids(), $addend->get_attachment_ids() ) ) ); } /** * @param $subtrahend Media_Item_Optimization_Global_Stats * * @return void */ public function subtract( $subtrahend ) { parent::subtract( $subtrahend ); $this->set_count( max( $this->get_count() - $subtrahend->get_count(), 0 ) ); $this->set_attachment_ids( array_diff( $this->get_attachment_ids(), $subtrahend->get_attachment_ids() ) ); } /** * @return mixed */ public function get_count() { return $this->count; } /** * @param mixed $count * * @return Media_Item_Optimization_Global_Stats */ public function set_count( $count ) { $this->count = $count; return $this; } private function add_attachment_id( $attachment_id ) { $this->attachment_ids[] = $attachment_id; } private function remove_attachment_id( $attachment_id ) { $attachment_ids = $this->get_attachment_ids(); $index = array_search( $attachment_id, $attachment_ids ); if ( $index !== false ) { unset( $attachment_ids[ $index ] ); $this->set_attachment_ids( $attachment_ids ); } } public function has_attachment_id( $attachment_id ) { return in_array( $attachment_id, $this->get_attachment_ids() ); } private function get_attachment_ids() { $attachment_ids = $this->attachment_ids; return empty( $attachment_ids ) || ! is_array( $attachment_ids ) ? array() : $attachment_ids; } private function set_attachment_ids( $attachment_ids ) { $this->attachment_ids = empty( $attachment_ids ) || ! is_array( $attachment_ids ) ? array() : $attachment_ids; } } class-core.php 0000644 00000024471 15252476777 0007346 0 ustar 00 <?php /** * Core class: Core class. * * @since 2.9.0 * @package Smush\Core */ namespace Smush\Core; use WP_Smush; if ( ! defined( 'WPINC' ) ) { die; } /** * Class Core */ class Core extends Stats { /** * Animated status. * * @var int */ private static $status_animated = 2; /** * Modules array. * * @var Modules */ public $mod; /** * Allowed mime types of image. * * @var array $mime_types */ public static $mime_types = array( 'image/jpg', 'image/jpeg', 'image/x-citrix-jpeg', 'image/gif', 'image/png', 'image/x-png', ); /** * List of external pages where smush needs to be loaded. * * @var array $pages */ public static $external_pages = array( 'nggallery-manage-images', 'gallery_page_nggallery-manage-gallery', 'gallery_page_wp-smush-nextgen-bulk', 'nextgen-gallery_page_nggallery-manage-gallery', // Different since NextGen 3.3.6. 'nextgen-gallery_page_wp-smush-nextgen-bulk', // Different since NextGen 3.3.6. 'post', 'post-new', 'page', 'edit-page', 'upload', ); /** * Attachment IDs which are smushed. * * @var array $smushed_attachments */ public $smushed_attachments = array(); /** * Unsmushed image IDs. * * @var array $unsmushed_attachments */ public $unsmushed_attachments = array(); /** * Skipped attachment IDs. * * @since 3.0 * * @var array $skipped_attachments */ public $skipped_attachments = array(); /** * Smushed attachments out of total attachments. * * @var int $smushed_count */ public $smushed_count = 0; /** * Smushed attachments out of total attachments. * * @var int $remaining_count */ public $remaining_count = 0; /** * Images with errors that have been skipped from bulk smushing. * * @since 3.0 * @var int $skipped_count */ public $skipped_count = 0; /** * Super Smushed attachments count. * * @var int $super_smushed */ public $super_smushed = 0; /** * Total count of attachments for smushing. * * @var int $total_count */ public $total_count = 0; /** * Initialize modules. * * @since 2.9.0 */ protected function init() { $this->mod = Modules::get_instance(); // Enqueue scripts and initialize variables. add_action( 'admin_init', array( $this, 'init_settings' ) ); // Load integrations. add_action( 'init', array( $this, 'load_integrations' ) ); // Big image size threshold (WordPress 5.3+). add_filter( 'big_image_size_threshold', array( $this, 'big_image_size_threshold' ), 10 ); /** * Load NextGen Gallery, instantiate the Async class. if hooked too late or early, auto Smush doesn't * work, also load after settings have been saved on init action. */ add_action( 'plugins_loaded', array( $this, 'load_libs' ), 90 ); /** * Maybe need to load some modules in REST API mode. * E.g. S3. */ add_action( 'rest_api_init', array( $this, 'load_libs_for_rest_api' ), 99 ); } public function __call( $method_name, $arguments ) { _deprecated_function( esc_html( $method_name ), '4.0' ); } /** * Load integrations class. * * @since 2.8.0 */ public function load_integrations() { new Integrations\Common(); } /** * Load plugin modules. */ public function load_libs() { $this->wp_smush_async(); new Integrations\Gutenberg(); new Integrations\Composer(); new Integrations\Gravity_Forms(); $avada = new Integrations\Avada(); $avada->init(); $divi = new Integrations\Divi(); $divi->init(); $envira = new Integrations\Envira(); $envira->init(); $hummingbird = new Integrations\Hummingbird_Integration(); $hummingbird->init(); $woo = new Integrations\WooCommerce(); $woo->init(); $amp = new Integrations\AMP_Integration(); $amp->init(); $essential_grid = new Integrations\Essential_Grid_Integration(); $essential_grid->init(); $elementor = new Integrations\Elementor_Integration(); $elementor->init(); $wp_rocket_integration = new Integrations\WP_Rocket_Integration(); $wp_rocket_integration->init(); $w3tc_integration = new Integrations\W3_Total_Cache_Integration(); $w3tc_integration->init(); $litespeed_integration = new Integrations\Litespeed_Cache_Integration(); $litespeed_integration->init(); $wp_fastest_cache_integration = new Integrations\WP_Fastest_Cache_Integration(); $wp_fastest_cache_integration->init(); $wp_optimize_integration = new Integrations\WP_Optimize_Integration(); $wp_optimize_integration->init(); $wp_super_cache_integration = new Integrations\WP_Super_Cache_Integration(); $wp_super_cache_integration->init(); $oxygen_builder = new Integrations\Oxygen_Builder_Integration(); $oxygen_builder->init(); // Register logger to schedule cronjob. Helper::logger(); } /** * Load lib for REST API. */ public function load_libs_for_rest_api() { } /** * Initialize the Smush Async class. */ private function wp_smush_async() { // Check if Async is disabled. if ( defined( 'WP_SMUSH_ASYNC' ) && ! WP_SMUSH_ASYNC ) { return; } // Instantiate class. new Modules\Async\Async(); // Load the Editor Async task only if user logged in or in backend. if ( is_admin() && is_user_logged_in() ) { new Modules\Async\Editor(); } } /** * Init settings. */ public function init_settings() { // Initialize Image dimensions. $this->mod->smush->image_sizes = $this->image_dimensions(); } public function get_localize_strings() { $upgrade_url = add_query_arg( array( 'utm_source' => 'smush', 'utm_medium' => 'plugin', 'utm_campaign' => 'smush_bulksmush_inline_filesizelimit', ), 'https://wpmudev.com/project/wp-smush-pro/' ); $wp_smush_msgs = array( 'nonce' => wp_create_nonce( 'wp-smush-ajax' ), 'webp_nonce' => wp_create_nonce( 'wp-smush-webp-nonce' ), 'resmush' => esc_html__( 'Super-Smush', 'wp-smushit' ), 'error_in_bulk' => esc_html__( '{{smushed}}/{{total}} images optimized successfully, {{errors}} images were not optimized, find out why and how to resolve the issue(s) below.', 'wp-smushit' ), 'all_failed' => esc_html__( 'All of your images failed to optimize. Find out why and how to resolve the issue(s) below.', 'wp-smushit' ), 'all_resmushed' => esc_html__( 'All images are fully optimized.', 'wp-smushit' ), 'all_smushed' => esc_html__( 'All attachments have been optimized. Awesome!', 'wp-smushit' ), 'restore' => esc_html__( 'Restoring image...', 'wp-smushit' ), 'smushing' => esc_html__( 'Smushing...', 'wp-smushit' ), 'btn_ignore' => esc_html__( 'Ignore', 'wp-smushit' ), 'view_detail' => esc_html__( 'View Details', 'wp-smushit' ), 'failed_item_smushed' => esc_html__( 'Images optimized successfully. No further action required.', 'wp-smushit' ), // Used by Directory Smush. 'generic_ajax_error' => esc_html__( 'Something went wrong with the request. Please reload the page and try again.', 'wp-smushit' ), // Errors. 'error_ignore' => esc_html__( 'Ignore this image from bulk smushing', 'wp-smushit' ), // Ignore text. 'ignored' => esc_html__( 'Ignored', 'wp-smushit' ), 'not_processed' => esc_html__( 'Not processed', 'wp-smushit' ), // Notices. 'noticeDismiss' => esc_html__( 'Dismiss', 'wp-smushit' ), 'noticeDismissTooltip' => esc_html__( 'Dismiss notice', 'wp-smushit' ), // URLs. 'smush_url' => network_admin_url( 'admin.php?page=smush' ), 'bulk_smush_url' => Helper::get_page_url( 'smush-bulk' ), 'nextGenURL' => network_admin_url( 'admin.php?page=smush-next-gen' ), 'edit_link' => Helper::get_image_media_link( '{{id}}', null, true ), 'debug_mode' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'cancel' => esc_html__( 'Cancel', 'wp-smushit' ), 'cancelling' => esc_html__( 'Cancelling ...', 'wp-smushit' ), ); return apply_filters( 'wp_smush_localize_script_messages', $wp_smush_msgs ); } /** * Get registered image sizes with dimension * * @return array */ public function image_dimensions() { return Helper::get_image_sizes(); } /** * Get the Maximum Width and Height settings for WrodPress * * @return array, Array of Max. Width and Height for image. */ public function get_max_image_dimensions() { global $_wp_additional_image_sizes; $width = 0; $height = 0; $limit = 9999; // Post-thumbnail. $image_sizes = get_intermediate_image_sizes(); // If image sizes are filtered and no image size list is returned. if ( empty( $image_sizes ) ) { return array( 'width' => $width, 'height' => $height, ); } // Create the full array with sizes and crop info. foreach ( $image_sizes as $size ) { if ( in_array( $size, array( 'thumbnail', 'medium', 'medium_large', 'large' ), true ) ) { $size_width = get_option( "{$size}_size_w" ); $size_height = get_option( "{$size}_size_h" ); } elseif ( isset( $_wp_additional_image_sizes[ $size ] ) ) { $size_width = $_wp_additional_image_sizes[ $size ]['width']; $size_height = $_wp_additional_image_sizes[ $size ]['height']; } // Skip if no width and height. if ( ! isset( $size_width, $size_height ) ) { continue; } // If within te limit, check for a max value. if ( $size_width <= $limit ) { $width = max( $width, $size_width ); } if ( $size_height <= $limit ) { $height = max( $height, $size_height ); } } return array( 'width' => $width, 'height' => $height, ); } /** * Set the big image threshold. * * @param int $threshold The threshold value in pixels. Default 2560. * * @return int|bool New threshold. False if scaling is disabled. * @since 3.3.2 */ public function big_image_size_threshold( $threshold ) { if ( Settings::get_instance()->get( 'no_scale' ) ) { return false; } if ( ! Settings::get_instance()->is_resize_module_active() ) { return $threshold; } $resize_sizes = Settings::get_instance()->get_setting( 'wp-smush-resize_sizes' ); if ( ! $resize_sizes || ! is_array( $resize_sizes ) ) { return $threshold; } return $resize_sizes['width'] > $resize_sizes['height'] ? $resize_sizes['width'] : $resize_sizes['height']; } /** * Get status_animated. * * @return int */ public static function get_status_animated() { return self::$status_animated; } } parser/class-replaceable.php 0000644 00000000304 15252476777 0012136 0 ustar 00 <?php namespace Smush\Core\Parser; interface Replaceable { public function get_original(); public function get_updated(); public function get_position(); public function has_updates(); } parser/class-element-attribute.php 0000644 00000004174 15252476777 0013342 0 ustar 00 <?php namespace Smush\Core\Parser; class Element_Attribute { private $attribute; private $name; private $value; /** * @var Image_URL[] */ private $image_urls; public function __construct( $name, $value, $attribute = '', $image_urls = array() ) { $this->name = $name; $this->value = new Value( $value ); $this->image_urls = $image_urls; $this->attribute = empty( $attribute ) ? sprintf( '%s="%s"', $name, $value ) : $attribute; } /** * @return mixed */ public function get_attribute() { return $this->attribute; } /** * @return mixed */ public function get_name() { return $this->name; } /** * @return mixed */ public function get_value() { return $this->value->get(); } public function set_value( $value ) { $this->value->set( $value ); } public function has_updates() { if ( $this->value->has_updates() ) { return true; } foreach ( $this->image_urls as $image_url ) { if ( $image_url->has_updates() ) { return true; } } return false; } public function get_updated() { $updated = $this->attribute; if ( $this->value->has_updates() ) { // Replace whole value $updated = $this->replace_value( $updated ); } else { // Replace the image URLs within the value $updated = $this->replace_image_urls( $updated ); } return $updated; } /** * @return Image_URL[] */ public function get_image_urls() { return $this->image_urls; } public function get_single_image_url() { $image_urls = $this->get_image_urls(); return empty( $image_urls ) ? null : $image_urls[0]; } /** * @param $updated * * @return string */ private function replace_image_urls( $updated ) { foreach ( $this->image_urls as $image_url ) { if ( $image_url->has_updates() ) { $updated = str_replace( $image_url->get_previous_url(), esc_url_raw( $image_url->get_url() ), $updated ); } } return $updated; } /** * @param $updated * * @return string */ private function replace_value( $updated ) { return str_replace( $this->value->get_previous(), esc_attr( $this->value->get() ), $updated ); } } parser/class-image-url.php 0000644 00000007076 15252476777 0011576 0 ustar 00 <?php namespace Smush\Core\Parser; class Image_URL { /** * @var Value */ private $url; private $ext; private $base_url; private $absolute_url; private $scheme; public function __construct( $url, $ext, $base_url ) { $this->url = new Value( $url ); $this->ext = $ext; $this->base_url = $base_url; } /** * @return string */ public function get_url() { return $this->url->get(); } /** * @param $url * * @return bool */ public function set_url( $url ) { /** * If the new value matches the absolute URL then there is no need to update. * The url class {@see Value::set()} also internally checks if the value is the same as before. */ $current_absolute_url = $this->get_absolute_url(); if ( $url === $current_absolute_url ) { return false; } return $this->url->set( $url ); } public function get_base_url() { return $this->base_url; } public function get_previous_url() { return $this->url->get_previous(); } /** * @return mixed */ public function get_ext() { return $this->ext; } public function has_updates() { return $this->url->has_updates(); } public function get_scheme() { if ( is_null( $this->scheme ) ) { $this->scheme = $this->prepare_scheme(); } return $this->scheme; } private function prepare_scheme() { $url_parts = wp_parse_url( $this->get_absolute_url() ); return $url_parts ? $url_parts['scheme'] : ''; } public function get_absolute_url() { if ( empty( $this->get_base_url() ) ) { // If a base URL is not provided we don't try to make an absolute URL return $this->get_url(); } if ( is_null( $this->absolute_url ) ) { $this->absolute_url = $this->prepare_absolute_url(); } return $this->absolute_url; } private function prepare_absolute_url() { if ( $this->is_scheme_missing_from_original() ) { $scheme = is_ssl() ? 'https:' : 'http:'; $full_url = $scheme . $this->url->get(); } else if ( $this->is_original_url_absolute() ) { $full_url = $this->url->get(); } else if ( $this->original_url_starts_with_slash() ) { $full_url = $this->make_url_relative_to_host(); } else { $full_url = $this->make_url_relative_to_base(); } return $this->resolve_relative_url( $full_url ); } private function is_original_url_absolute() { $scheme = parse_url( $this->url->get(), PHP_URL_SCHEME ); return ! empty( $scheme ); } private function is_scheme_missing_from_original() { return str_starts_with( $this->url->get(), '//' ); } /** * @param $full_url * * @return string */ private function resolve_relative_url( $full_url ) { $path = parse_url( $full_url, PHP_URL_PATH ); $resolved_path = str_replace( '/./', '/', $path ); // TODO: in the following regex [a-zA-Z0-9-_.] is too narrow, what about non-english characters? $pattern = '@/[a-zA-Z0-9-_.]*/\.{2}/@i'; while ( preg_match( $pattern, $resolved_path ) ) { $resolved_path = preg_replace( $pattern, '/', $resolved_path ); } return str_replace( $path, $resolved_path, $full_url ); } /** * @return bool */ private function original_url_starts_with_slash() { return str_starts_with( $this->url->get(), '/' ); } /** * @return string */ private function make_url_relative_to_host() { $scheme = parse_url( $this->base_url, PHP_URL_SCHEME ); $host = parse_url( $this->base_url, PHP_URL_HOST ); return trailingslashit( "$scheme://$host" ) . ltrim( $this->url->get(), '/' ); } /** * @return string */ private function make_url_relative_to_base() { return trailingslashit( $this->base_url ) . ltrim( $this->url->get(), '/' ); } } parser/class-value.php 0000644 00000001460 15252476777 0011017 0 ustar 00 <?php namespace Smush\Core\Parser; class Value { private $previous_value; private $value; private $has_updates = false; public function __construct( $value ) { $this->previous_value = ''; $this->value = $value; } /** * @param $value * * @return bool */ public function set( $value ) { if ( $value === $this->value ) { /** * Don't do anything if the value hasn't changed. * We don't want to do unnecessary string replacements. */ return false; } $this->previous_value = $this->value; $this->value = $value; $this->has_updates = true; return true; } public function get() { return $this->value; } public function get_previous() { return $this->previous_value; } public function has_updates() { return $this->has_updates; } } parser/class-rest-content.php 0000644 00000001611 15252476777 0012326 0 ustar 00 <?php namespace Smush\Core\Parser; class Rest_Content { /** * @var string */ private $content; /** * @var Image_URL[] */ private $image_urls; public function __construct( $content, $image_urls ) { $this->content = $content; $this->image_urls = $image_urls; } /** * @return string */ public function get_content() { return $this->content; } public function get_image_urls() { return $this->image_urls; } public function has_updates() { foreach ( $this->image_urls as $image_url ) { if ( $image_url->has_updates() ) { return true; } } return false; } public function get_updated() { $updated = $this->content; foreach ( $this->image_urls as $image_url ) { if ( $image_url->has_updates() ) { $updated = str_replace( $image_url->get_previous_url(), $image_url->get_url(), $updated ); } } return $updated; } } parser/class-placeholder-replacement.php 0000644 00000004035 15252476777 0014463 0 ustar 00 <?php namespace Smush\Core\Parser; class Placeholder_Replacement { private $placeholders = array(); private $counts = array(); private $prefix = 'smush-placeholder-'; public function add_placeholders( $markup, $blocks ) { foreach ( $blocks as $block ) { $markup = $this->add_placeholder( $markup, $block ); } return $markup; } public function add_placeholder( $markup, $block ) { $key = $this->make_key( $block ); $this->placeholders[ $key ] = $block; $new_markup = str_replace( $block, $key, $markup, $count ); if ( ! isset( $this->counts[ $key ] ) ) { $this->counts[ $key ] = 0; } $this->counts[ $key ] += $count; return $new_markup; } public function remove_placeholder( $markup, $key ) { if ( isset( $this->placeholders[ $key ] ) && strpos( $markup, $key ) !== false ) { $markup = str_replace( $key, $this->placeholders[ $key ], $markup, $count ); $this->counts[ $key ] -= $count; if ( empty( $this->counts[ $key ] ) ) { unset( $this->placeholders[ $key ] ); unset( $this->counts[ $key ] ); } } return $markup; } public function remove_placeholders( $markup ) { foreach ( $this->placeholders as $key => $original ) { $markup = $this->remove_placeholder( $markup, $key ); } return $markup; } public function remove_placeholders_recursively( $markup ) { $markup = $this->remove_placeholders( $markup ); if ( $this->has_some_key( $markup ) ) { return $this->remove_placeholders_recursively( $markup ); } return $markup; } /** * @param $block * * @return string */ private function make_key( $block ) { return $this->prefix . md5( $block ); } public function has_some_key( $markup ) { return strpos( $markup, $this->prefix ) !== false; } public function get_placeholders_from_markup( $markup ) { $matches = array(); if ( preg_match_all( '/smush-placeholder-[a-f0-9]{32}/', $markup, $matches ) ) { return empty( $matches[0] ) ? array() : $matches[0]; } else { return array(); } } } parser/class-style.php 0000644 00000002156 15252476777 0011046 0 ustar 00 <?php namespace Smush\Core\Parser; class Style implements Replaceable { /** * @var string */ private $css; /** * @var Image_URL[] */ private $image_urls; /** * @var int */ private $position; public function __construct( $css, $image_urls, $position = - 1 ) { $this->css = $css; $this->image_urls = $image_urls; $this->position = $position; } /** * @return string */ public function get_css() { return $this->css; } public function get_image_urls() { return $this->image_urls; } public function has_updates() { foreach ( $this->image_urls as $image_url ) { if ( $image_url->has_updates() ) { return true; } } return false; } public function get_updated() { $updated = $this->css; foreach ( $this->image_urls as $image_url ) { if ( $image_url->has_updates() ) { $updated = str_replace( $image_url->get_previous_url(), esc_url_raw( $image_url->get_url() ), $updated ); } } return $updated; } public function get_original() { return $this->get_css(); } public function get_position() { return $this->position; } } parser/class-composite-element.php 0000644 00000003115 15252476777 0013333 0 ustar 00 <?php namespace Smush\Core\Parser; class Composite_Element implements Replaceable { /** * @var string */ private $markup; /** * @var string */ private $tag; /** * @var Element[] */ private $elements; /** * @var int */ private $position; /** * @var bool */ private $has_lcp; public function __construct( $markup, $tag, $elements, $position = - 1, $has_lcp = false ) { $this->markup = $markup; $this->tag = $tag; $this->elements = $elements; $this->position = $position; $this->has_lcp = $has_lcp; } /** * @return string */ public function get_markup() { return $this->markup; } /** * @return string */ public function get_tag() { return $this->tag; } /** * @return Element[] */ public function get_elements() { return $this->elements; } public function has_updates() { foreach ( $this->elements as $element ) { if ( $element->has_updates() ) { return true; } } return false; } public function get_updated() { $updated = $this->markup; foreach ( $this->elements as $element ) { if ( $element->has_updates() ) { $updated = str_replace( $element->get_markup(), $element->get_updated_markup(), $updated ); } } return $updated; } public function has_lcp() { return $this->has_lcp; } public function set_has_lcp( $is_lcp ) { $this->has_lcp = $is_lcp; } public function get_position() { return $this->position; } public function set_position( $position ) { $this->position = $position; } public function get_original() { return $this->get_markup(); } } parser/class-page-parser.php 0000644 00000007274 15252476777 0012122 0 ustar 00 <?php namespace Smush\Core\Parser; use Smush\Core\LCP\LCP_Data; use Smush\Core\LCP\LCP_Locator; class Page_Parser { /** * @var string */ private $page_url; /** * @var string */ private $page_markup; /** * @var Parser */ private $parser; /** * @var LCP_Data */ private $lcp_data; public function __construct( $page_url, $page_markup, $lcp_data = null ) { $this->page_url = $page_url; $this->page_markup = $page_markup; $this->parser = new Parser(); $this->lcp_data = $lcp_data; } /** * TODO: make sure this method is called as few times as possible * * @return Page */ public function parse_page() { $page_markup = $this->page_markup; $base_tag_url = $this->parser->get_base_url( $page_markup ); $base_url = $base_tag_url ?: $this->page_url; $styles = $this->parser->get_inline_styles( $page_markup, $base_url ); if ( empty( $this->lcp_data ) ) { $lcp_position = - 1; } else { $lcp_locator = new LCP_Locator( $this->lcp_data, $page_markup, $this->page_url ); $lcp_position = $lcp_locator->get_lcp_position(); } $sub_element_positions = array(); $script_elements = $this->parser->get_composite_elements( $page_markup, $base_url, array( 'script', 'noscript' ), $lcp_position ); $sub_element_positions = $this->get_composite_sub_element_positions( $script_elements, $sub_element_positions ); $picture_elements = $this->parser->get_composite_elements( $page_markup, $base_url, array( 'picture' ), $lcp_position ); $sub_element_positions = $this->get_composite_sub_element_positions( $picture_elements, $sub_element_positions ); $elements = $this->parser->get_elements_with_image_attributes( $page_markup, $base_url, $lcp_position ); $elements = $this->remove_composite_sub_elements( $elements, $sub_element_positions ); $iframe_elements = $this->parser->get_iframe_elements( $page_markup, $base_url ); return new Page( $this->page_url, $this->page_markup, $styles, $picture_elements, $elements, $iframe_elements ); } /** * @param $markup * @param $composite_elements Composite_Element[] * * @return string */ private function replace_composites_with_placeholders( $markup, $composite_elements ) { $placeholder_replacement = new Placeholder_Replacement(); if ( empty( $composite_elements ) ) { return $markup; } $html_elements = array_map( function ( $composite_element ) { return $composite_element->get_markup(); }, $composite_elements ); return $placeholder_replacement->add_placeholders( $markup, $html_elements ); } /** * @param array $composite_elements Composite_Element[] * * @return int[] */ private function get_composite_sub_element_positions( $composite_elements, $sub_element_positions ) { foreach ( $composite_elements as $composite_element ) { foreach ( $composite_element->get_elements() as $sub_element ) { $sub_element_position = $sub_element->get_position(); if ( ! in_array( $sub_element_position, $sub_element_positions ) ) { $sub_element_positions[] = $sub_element_position; } } } return $sub_element_positions; } /** * @param Element[] $elements * @param int[] $composite_sub_element_positions * * @return array */ private function remove_composite_sub_elements( $elements, $composite_sub_element_positions ) { if ( empty( $composite_sub_element_positions ) || ! is_array( $composite_sub_element_positions ) || ! is_array( $elements ) ) { return $elements; } $filtered = array_filter( $elements, function ( $element ) use ( $composite_sub_element_positions ) { return ! in_array( $element->get_position(), $composite_sub_element_positions ); } ); return array_values( $filtered ); } } parser/class-parser.php 0000644 00000044260 15252476777 0011204 0 ustar 00 <?php namespace Smush\Core\Parser; use Smush\Core\Array_Utils; class Parser { /** * @var Array_Utils */ private $array_utils; public function __construct() { $this->array_utils = new Array_Utils(); } public function get_base_url( $markup ) { $pattern = "/<base[^>]*?href\s*=\s*['\"](.*?)['\"]\s*\/\s*>/is"; if ( preg_match( $pattern, $markup, $matches ) && ! empty( $matches[1] ) ) { return $matches[1]; } return ''; } public function get_inline_style_blocks( $markup ) { $pattern = '/<style\b[^>]*>(.*?)<\/style>/msi'; if ( ! preg_match_all( $pattern, $markup, $matches, PREG_OFFSET_CAPTURE ) ) { return array(); } return $matches[1]; } /** * @param $markup * @param $base_url * * @return Style[] */ public function get_inline_styles( $markup, $base_url ) { $styles = array(); $inline_style_blocks = $this->get_inline_style_blocks( $markup ); foreach ( $inline_style_blocks as $style_block ) { if ( empty( $style_block ) || ! is_array( $style_block ) || count( $style_block ) < 2 ) { continue; } list( $inline_style_block, $inline_style_block_position ) = $style_block; $image_urls = $this->get_image_urls( $inline_style_block, $base_url ); $style = new Style( $inline_style_block, $image_urls, $inline_style_block_position ); $styles[] = $style; } return $styles; } /** * @param $markup string * @param $base_url * * @return Image_URL[] */ public function get_image_urls( $markup, $base_url ) { // IMPORTANT: the following regex is a copy of the one in the JS function SmushLCPDetector.getBackgroundDataForPropertyValue(). Remember to keep them synced. $pattern = '@(?<src>(?:https?:/|\.+)?/[^\'",\s\(\)]+\.(?<ext>jpe?g|png|gif|webp|svg|avif)(?:\?[^\s\'",?)]+)?)\b@is'; $pattern = apply_filters( 'wp_smush_image_urls_regex', $pattern ); if ( ! preg_match_all( $pattern, $markup, $matches, PREG_SET_ORDER ) ) { return array(); } $image_urls = array(); foreach ( $matches as $match ) { if ( ! isset( $match['src'], $match['ext'] ) ) { continue; } $src = $match['src']; $image_urls[ $src ] = new Image_URL( $this->remove_quote_entities( $src ), $match['ext'], $base_url ); } return array_values( $image_urls ); } public function get_tags( $markup, $tags ) { $tags_string = join( '|', $tags ); $matches = array(); if ( preg_match_all( '/<(' . $tags_string . ').*?\/\1>/s', $markup, $matches, PREG_PATTERN_ORDER ) ) { return $matches[0]; } return array(); } public function get_block_by_tag( $markup, $tag ) { $pattern = "/<$tag\b([^>]*>.*)<\/$tag>/is"; if ( ! preg_match( $pattern, $markup, $matches ) ) { return $markup; } return $matches[1]; } /** * @param $markup * @param $base_url * @param $tag_names * @param $lcp_location * * @return Composite_Element[] */ public function get_composite_elements( $markup, $base_url, $tag_names, $lcp_location = - 1 ) { $composite_elements = array(); foreach ( $tag_names as $tag_name ) { $match_found = preg_match_all( '/<(' . $tag_name . ').*?\/\1>/s', $markup, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE ); if ( ! $match_found ) { continue; } $html_elements = empty( $matches[0] ) ? array() : $matches[0]; foreach ( $html_elements as $html_element ) { $composite_element_markup = $html_element[0]; $composite_element_position = $html_element[1]; $elements = $this->get_elements_with_image_attributes( $composite_element_markup, $base_url ); if ( ! empty( $elements ) ) { $has_lcp_element = false; foreach ( $elements as $element ) { $actual_position = $composite_element_position + $element->get_position(); $element->set_position( $actual_position ); $element->set_lcp( $lcp_location === $actual_position ); if ( $element->is_lcp() ) { $has_lcp_element = true; } } $composite_elements[] = new Composite_Element( $composite_element_markup, $tag_name, $elements, $composite_element_position, $has_lcp_element ); } } } return $composite_elements; } /** * @param $markup * @param $base_url * @param int $lcp_position * * @return Element[] */ public function get_elements_with_image_attributes( $markup, $base_url, $lcp_position = - 1 ) { $pattern = '@(?<element><(?:(?<img>img)\b[^>]+|(?<tag>[a-zA-Z]+)\b[^>]+\.(?:jpe?g|png|gif|webp|svg|avif)[^>]+)>)@is'; $pattern = apply_filters( 'wp_smush_images_from_content_regex', $pattern ); return $this->get_elements_matching_pattern( $pattern, $markup, $base_url, $lcp_position ); } public function get_elements_with_id_attribute( $markup, $id, $base_url ) { $pattern = '@(?<element><(?<tag>[a-zA-Z]+)\b[^>]+(?<!\S)id=(["\'])' . preg_quote( $id ) . '\3[^>]+>)@is'; return $this->get_elements_matching_pattern( $pattern, $markup, $base_url ); } public function get_elements_with_class_attribute( $markup, $class, $base_url ) { $pattern = '@(?<element><(?<tag>[a-zA-Z]+)\b[^>]+(?<!\S)class=(["\'])' . preg_quote( $class ) . '\3[^>]+>)@is'; return $this->get_elements_matching_pattern( $pattern, $markup, $base_url ); } /** * @param $markup * @param $image_url * @param $base_url * * @return Element[] */ public function get_elements_with_image_url( $markup, $image_url, $base_url ) { $pattern = '@(?<element><(?<tag>[a-zA-Z]+)\b[^>]+' . preg_quote( $image_url ) . '[^>]+>)@is'; return $this->get_elements_matching_pattern( $pattern, $markup, $base_url ); } private function get_elements_matching_pattern( $pattern, $markup, $base_url, $lcp_position = - 1 ) { if ( ! preg_match_all( $pattern, $markup, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE ) ) { return array(); } $elements = array(); foreach ( $matches as $item ) { $element = (string) $this->array_utils->get_array_value( $item, [ 'element', 0 ] ); $element_position = (int) $this->array_utils->get_array_value( $item, [ 'element', 1 ] ); if ( empty( $element ) ) { continue; } $img_tag_name = (string) $this->array_utils->get_array_value( $item, [ 'img', 0 ] ); $tag_name = (string) $this->array_utils->get_array_value( $item, [ 'tag', 0 ] ); $tag_name = ! empty( $img_tag_name ) ? $img_tag_name : $tag_name; $attributes = $this->get_element_attributes( $element, $base_url ); $background = $this->get_element_background_image( $element, $base_url ); $css_properties = $background ? array( $background ) : array(); // TODO: Support CSS variable with images. if ( empty( $attributes ) && empty( $css_properties ) ) { continue; } $is_lcp_element = $lcp_position === $element_position; $elements[] = new Element( $element, $tag_name, $attributes, $css_properties, $element_position, $is_lcp_element ); } return array_values( $elements ); } /** * @param $element * @param $base_url * * @return Element_Attribute[] */ public function get_element_attributes( $element, $base_url ) { $image_attributes = array(); $pattern = '#\b(?<name>(?:data-(?:[a-z0-9_-]+-)?)?(?:[a-z0-9_-]+))\s*=\s*(["\'])(?<value>[^\'"]+)\2#is'; $pattern = apply_filters( 'wp_smush_image_attributes_regex', $pattern ); if ( ! preg_match_all( $pattern, $element, $matches, PREG_SET_ORDER ) ) { return $image_attributes; } foreach ( $matches as $attr_data ) { if ( ! isset( $attr_data['name'], $attr_data['value'] ) ) { continue; } $attr_name = $attr_data['name']; $attr_value = trim( $attr_data['value'] ); if ( ! $this->is_safe( $attr_name ) || ! $this->is_safe( $attr_value ) ) { continue; } $image_urls = $this->get_image_urls( $attr_value, $base_url ); $image_attributes[ $attr_name ] = new Element_Attribute( $attr_name, $attr_value, $attr_data[0], $image_urls ); } return $image_attributes; } public function get_element_background_image( $element, $base_url ) { if ( ! strpos( $element, 'background' ) ) { return null; } $style = $this->get_element_attribute_value( $element, 'style' ); if ( empty( $style ) ) { return null; } /** * Background image regex supports: * * 1. background or background-image. * 2. Multiple background images. * * background: url(img_flwr.gif) right bottom no-repeat, url(paper.gif) left top repeat; * background-image: url("image1.png"), url(https://sample.com/image2.png?lossy=2&strip=1&webp=1), linear-gradient(to right, rgba(30, 75, 115, 1), rgba(255, 255, 255, 0)); */ // Regex rule to get all inline style after background(-image) property. $pattern = '#(?<!-)\b(?<property>background(?:-image)?):(?<value>[^;:]*?url\s*\([^>]+\)[^=:;]*);{0,1}#is'; $pattern = apply_filters( 'wp_smush_background_images_regex', $pattern ); if ( ! preg_match_all( $pattern, $style, $matches, PREG_SET_ORDER ) ) { return null; } $bg_property = null; foreach ( $matches as $bg_image_data ) { if ( ! isset( $bg_image_data['property'], $bg_image_data['value'] ) ) { continue; } $bg_image_property = $bg_image_data['property']; $bg_image_value = $bg_image_data['value']; if ( ! $this->is_safe( $bg_image_property ) || ! $this->is_safe( $bg_image_value ) ) { continue; } $image_urls = $this->get_image_urls( $bg_image_value, $base_url ); if ( empty( $image_urls ) ) { continue; } // background-image:url("image1.png"); background-repeat: no-repeat. if ( substr_count( $bg_image_value, ';' ) ) { $bg_image_value = $this->extract_background_image( $bg_image_value ); } // An element only has one background, so let try to get the latest one. $bg_property = new Element_CSS_Property( $bg_image_data[0], $bg_image_property, $bg_image_value, $image_urls ); } return $bg_property; } /** * Extract background image value from the inline style. * * Input: linear-gradient(to right, rgba(30, 75, 115, 1), rgba(255, 255, 255, 0)), url("image1.png") right bottom no-repeat, * url(https://sample.com/image2.png?lossy=2&strip=1&webp=1) left top repeat; background-color: #fff; * Output: linear-gradient(to right, rgba(30, 75, 115, 1), rgba(255, 255, 255, 0)), url("image1.png") right bottom no-repeat, * url(https://sample.com/image2.png?lossy=2&strip=1&webp=1) left top repeat; */ public function extract_background_image( $bg_image_value ) { $urls = array(); $bg_image_value_without_url = preg_replace_callback( '#url\s*\(([^\)]+)\)#is', function ( $matches ) use ( &$urls ) { if ( ! empty( $matches[1] ) ) { $url = $matches[1]; $urls[] = $url; $count = count( $urls ); $matches[0] = str_replace( $url, "[URL{$count}]", $matches[0] ); } return $matches[0]; }, $bg_image_value ); if ( ! empty( $urls ) && preg_match( '/[^;]+/is', $bg_image_value_without_url, $matches ) ) { $bg_image_value = $matches[0]; foreach ( $urls as $index => $url ) { $index ++; $bg_image_value = str_replace( "[URL{$index}]", $url, $bg_image_value ); } } return $bg_image_value; } private function remove_quote_entities( $image ) { // Quote entities. $quotes = apply_filters( 'wp_smush_background_image_quotes', array( '"', '"', ''', ''' ) ); $image = trim( $image ); if ( empty( $image ) || strlen( $image ) < 6 ) { return $image; } // Remove the starting quotes. if ( in_array( substr( $image, 0, 6 ), $quotes, true ) ) { $image = substr( $image, 6 ); } // Remove the ending quotes. if ( in_array( substr( $image, - 6 ), $quotes, true ) ) { $image = substr( $image, 0, - 6 ); } return $image; } public function add_attribute_to_element( $element_markup, $tag_name, $attribute_name, $attribute_value = null ) { $pattern = '~<' . $tag_name . '\b[^>]*>~'; if ( preg_match_all( $pattern, $element_markup, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE ) ) { $starting_tag = $this->array_utils->get_array_value( $matches, [ 0, 0, 0 ] ); $starting_tag_position = $this->array_utils->get_array_value( $matches, [ 0, 0, 1 ] ); $updated_starting_tag = $this->add_attribute_to_self_closing_element( $starting_tag, $attribute_name, $attribute_value ); if ( $starting_tag !== $updated_starting_tag ) { $before = substr( $element_markup, 0, $starting_tag_position ); $after = substr( $element_markup, $starting_tag_position + strlen( $starting_tag ) ); $element_markup = $before . $updated_starting_tag . $after; } } return $element_markup; } public function add_element_attribute( $element, $name, $value = null ) { _deprecated_function( __METHOD__, '3.19.1', '\Smush\Core\Parser\Parser::add_attribute_to_element()' ); return $this->add_attribute_to_self_closing_element( $element, $name, $value ); } private function add_attribute_to_self_closing_element( $element, $name, $value = null ) { $closing = false === strpos( $element, '/>' ) ? '>' : ' />'; $quotes = false === strpos( $element, '"' ) ? '\'' : '"'; if ( ! is_null( $value ) ) { $element = rtrim( $element, $closing ) . " {$name}={$quotes}{$value}{$quotes}{$closing}"; } else { $element = rtrim( $element, $closing ) . " {$name}{$closing}"; } return $element; } public function remove_element_attribute( $element, $attribute ) { return preg_replace( '/\s' . $attribute . '=[\'"](.*?)[\'"]/i', '', $element ); } /** * @param $markup * @param $base_url * * @return Element[] */ public function get_iframe_elements( $markup, $base_url ) { if ( strpos( $markup, '<iframe' ) === false ) { return array(); } // Iframe tag has srcdocs attribute which might contains HTML code. $pattern = '#<iframe\b[^>]*\s(?<attr>src\s*=\s*(\'|")(?<src>[^\'"]+)\2)[^>]*>(.*?)</iframe>#is'; $pattern = apply_filters( 'wp_smush_iframes_regex', $pattern ); $iframes = array(); if ( ! preg_match_all( $pattern, $markup, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE ) ) { return $iframes; } foreach ( $matches as $iframe_data ) { if ( empty( $iframe_data[0][0] ) || ! isset( $iframe_data[0][1] ) || empty( $iframe_data['attr'] ) || empty( $iframe_data['src'] ) ) { continue; } $iframe_markup = $iframe_data[0][0]; $iframe_position = $iframe_data[0][1]; $attributes = $this->get_element_attributes( $iframe_markup, $base_url ); $iframe_element = new Element( $iframe_markup, 'iframe', $attributes, array(), $iframe_position ); $iframes[] = $iframe_element; } return $iframes; } public function get_element_attribute_value( $element_markup, $attribute_name ) { if ( strpos( $element_markup, $attribute_name ) === false ) { return ''; } $pattern = '#' . $attribute_name . '\s*=\s*([\'|"])(?<value>(?:(?!\1).)*)\1#is'; if ( ! preg_match_all( $pattern, $element_markup, $matches, PREG_SET_ORDER ) ) { return ''; } return empty( $matches[0]['value'] ) ? '' : $matches[0]['value']; } public function markup_contains_noscript( $markup ) { return false !== strpos( $markup, '<noscript>' ); } private function is_safe( $str ) { $str = trim( $str ); return $this->sanitize_value( $str ) === $str; } /** * This is almost the same as {@see _sanitize_text_fields()} but it doesn't remove percent encoded values because they are valid. */ private function sanitize_value( $str ) { if ( is_object( $str ) || is_array( $str ) ) { return ''; } $str = (string) $str; $filtered = wp_check_invalid_utf8( $str ); if ( str_contains( $filtered, '<' ) ) { $filtered = wp_pre_kses_less_than( $filtered ); // This will strip extra whitespace for us. $filtered = wp_strip_all_tags( $filtered ); /* * Use HTML entities in a special case to make sure that * later newline stripping stages cannot lead to a functional tag. */ $filtered = str_replace( "<\n", "<\n", $filtered ); } /** * Skip removal of percent encoded values {@see _sanitize_text_fields} */ return trim( $filtered ); } public function get_self_closing_element_and_position( $tag, $markup, $index ) { $html_element_markup = null; $html_element_position = null; $pattern = '/<(' . $tag . ')[^>]+>/is'; $tags_found = preg_match_all( $pattern, $markup, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE ); if ( $tags_found ) { $html_element_markup = $this->array_utils->get_array_value( $matches, [ 0, $index, 0 ] ); $html_element_position = $this->array_utils->get_array_value( $matches, [ 0, $index, 1 ] ); } return [ $html_element_markup, $html_element_position ]; } public function get_top_level_element_and_position( $tag, $markup, $index ) { $html_element_markup = null; $html_element_position = null; $html_element_inner_markup = null; $html_element_inner_position = null; if ( substr_count( $markup, '<' . $tag ) < 2 ) { $pattern = '~<(?<tag>' . $tag . ')\b[^>]*>(.*?)</' . $tag . '>~is'; } else { $pattern = '~<(?<tag>[a-zA-Z][a-zA-Z0-9]*)\b[^>]*>((?>(?:[^<]+|<(?!/?\1\b[^>]*>)+|(?R))*))</\1>~is'; } $tags_found = preg_match_all( $pattern, $markup, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE ); if ( $tags_found ) { $matches = $this->filter_out_irrelevant_elements( $matches, $tag ); if ( $matches ) { $html_element_markup = $this->array_utils->get_array_value( $matches, [ 0, $index, 0 ] ); $html_element_position = $this->array_utils->get_array_value( $matches, [ 0, $index, 1 ] ); $html_element_inner_markup = $this->array_utils->get_array_value( $matches, [ 1, $index, 0 ] ); $html_element_inner_position = $this->array_utils->get_array_value( $matches, [ 1, $index, 1 ] ); } } return [ $html_element_markup, $html_element_position, $html_element_inner_markup, $html_element_inner_position ]; } private function filter_out_irrelevant_elements( $matches, $target_tag ) { $outer = $matches[0]; $tags = $matches[1]; $inner = $matches[2]; $filtered_outer = array(); $filtered_inner = array(); foreach ( $outer as $index => $outer_element ) { $tag = $tags[ $index ][0]; if ( $tag !== $target_tag ) { continue; } $filtered_outer[] = $outer_element; $filtered_inner[] = $inner[ $index ]; } return array( $filtered_outer, $filtered_inner ); } } parser/class-page.php 0000644 00000007340 15252476777 0010622 0 ustar 00 <?php namespace Smush\Core\Parser; class Page { /** * @var string */ private $page_url; /** * @var string */ private $page_markup; /** * @var Style[] */ private $styles; /** * @var Element[] */ private $elements; /** * @var Parser */ private $parser; /** * @var Element[] */ private $iframe_elements; /** * @var Composite_Element[] */ private $composite_elements; /** * @var Composite_Element|Element|null */ private $lcp_element; /** * @param $page_url string * @param $page_markup string * @param $styles Style[] * @param $elements Element[] */ public function __construct( $page_url, $page_markup, $styles, $composite_elements, $elements, $iframe_elements ) { $this->page_url = $page_url; $this->page_markup = $page_markup; $this->styles = $styles; $this->composite_elements = $composite_elements; $this->elements = $elements; $this->iframe_elements = $iframe_elements; $this->parser = new Parser(); } /** * @return Style[] */ public function get_styles() { return $this->styles; } /** * @return Composite_Element[] */ public function get_composite_elements() { return $this->composite_elements; } /** * @return Element[] */ public function get_elements() { return $this->elements; } public function has_updates() { foreach ( $this->styles as $style ) { if ( $style->has_updates() ) { return true; } } foreach ( $this->composite_elements as $composite_element ) { if ( $composite_element->has_updates() ) { return true; } } foreach ( $this->elements as $element ) { if ( $element->has_updates() ) { return true; } } foreach ( $this->iframe_elements as $iframe_element ) { if ( $iframe_element->has_updates() ) { return true; } } return false; } /** * @return string */ public function get_page_markup() { return $this->page_markup; } /** * ASSUMPTIONS: * - All elements handled by this method have correct positions, not the default -1 * - The same element is not included in the elements array as well as a composite element * * @return string */ public function get_updated_markup() { $updated = $this->page_markup; $replaceable = $this->get_sorted_items(); foreach ( $replaceable as $replaceable_item ) { if ( $replaceable_item->has_updates() ) { $before = substr( $updated, 0, $replaceable_item->get_position() ); $after = substr( $updated, $replaceable_item->get_position() + strlen( $replaceable_item->get_original() ) ); $updated = $before . $replaceable_item->get_updated() . $after; } } return $updated; } public function get_iframe_elements() { return $this->iframe_elements; } public function get_lcp_element() { if ( is_null( $this->lcp_element ) ) { $this->lcp_element = $this->find_lcp_element(); } return $this->lcp_element; } /** * @return Composite_Element|Element|null */ private function find_lcp_element() { foreach ( $this->get_composite_elements() as $composite_element ) { if ( $composite_element->has_lcp() ) { return $composite_element; } } foreach ( $this->get_elements() as $element ) { if ( $element->is_lcp() ) { return $element; } } return null; } /** * @return Replaceable[] */ private function get_sorted_items() { /** * @var Replaceable[] $replaceable */ $replaceable = array_merge( $this->styles, $this->composite_elements, $this->elements, $this->iframe_elements ); // Replace elements starting from the end of the markup so that positions don't change usort( $replaceable, function ( $a, $b ) { return $b->get_position() <=> $a->get_position(); } ); return $replaceable; } } parser/class-element.php 0000644 00000025066 15252476777 0011344 0 ustar 00 <?php namespace Smush\Core\Parser; class Element implements Replaceable { private $markup; private $tag; /** * @var Element_Attribute[] */ private $attributes; /** * @var Element_CSS_Property[] */ private $css_properties; private $has_updates = false; /** * @var Element_Attribute[] */ private $added_attributes = array(); /** * @var Element_Attribute[] */ private $replaced_attributes = array(); /** * @var Element_Attribute[] */ private $removed_attributes = array(); /** * @var Parser */ private $parser; private $postfix; /** * @var Element_Attribute[] */ private $image_attributes; /** * @var bool */ private $is_lcp; /** * @var int */ private $position; /** * @var string */ private $wrapper_markup_before; /** * @var string */ private $wrapper_markup_after; public function __construct( $markup, $tag, $attributes, $css_properties, $position = - 1, $is_lcp = false ) { $this->markup = $markup; $this->tag = $tag; $this->attributes = $attributes; $this->css_properties = $css_properties; $this->parser = new Parser(); $this->is_lcp = $is_lcp; $this->position = $position; } /** * @return mixed */ public function get_markup() { return $this->markup; } /** * @return mixed */ public function get_tag() { return $this->tag; } /** * @return Element_Attribute[] */ public function get_attributes() { return $this->attributes; } public function get_image_attributes() { if ( is_null( $this->image_attributes ) ) { $this->image_attributes = $this->prepare_image_attributes(); } return $this->image_attributes; } private function prepare_image_attributes() { $image_attributes = array(); foreach ( $this->get_image_attribute_names() as $image_attribute_name ) { $image_attribute = $this->get_attribute( $image_attribute_name ); if ( $image_attribute ) { $image_attributes[] = $image_attribute; } } return $image_attributes; } private function get_image_attribute_names() { $attribute_names = apply_filters( 'wp_smush_get_image_attribute_names', /** * TODO: break down this list and move to integration classes, only keep the bare minimum here */ array( 'href', 'data-href', 'src', 'data-src', 'srcset', 'data-srcset', 'data-thumb', 'data-thumbnail', 'data-back', 'data-lazyload', // WP Rocket lazy loading: 'data-lazy-src', 'data-lazy-srcset', 'data-original', // We need the following to support webp *after* lazy load. 'data-bg', 'data-bg-image', 'poster', ) ); $attribute_names = array_filter( (array) $attribute_names, function ( $attribute_names ) { return $attribute_names && is_string( $attribute_names ); } ); return array_unique( $attribute_names ); } /** * @param Element_Attribute $attribute */ public function add_attribute( $attribute ) { $this->added_attributes[ $attribute->get_name() ] = $attribute; $this->set_has_updates( true ); } /** * @param $name * * @return Element_Attribute */ public function get_attribute( $name ) { foreach ( $this->attributes as $attribute ) { if ( $attribute->get_name() === $name ) { return $attribute; } } return null; } /** * @param $original_name string * @param $new_attribute Element_Attribute * * @return void */ public function replace_attribute( $original_name, $new_attribute ) { $this->replaced_attributes[ $original_name ] = $new_attribute; $this->set_has_updates( true ); } /** * @param Element_Attribute $attribute Attribute to remove * * @return void */ public function remove_attribute( $attribute ) { if ( ! $attribute instanceof Element_Attribute ) { return; } $name = $attribute->get_name(); if ( isset( $this->added_attributes[ $name ] ) ) { unset( $this->added_attributes[ $name ] ); } if ( ! $this->has_attribute( $name ) ) { return; } $this->removed_attributes[ $name ] = $attribute; $this->set_has_updates( true ); } /** * @param $attribute Element_Attribute * * @return void */ public function add_or_update_attribute( $attribute ) { if ( $this->has_attribute( $attribute->get_name() ) ) { $this->replace_attribute( $attribute->get_name(), $attribute ); } else { $this->add_attribute( $attribute ); } } private function set_has_updates( $has_updates ) { $this->has_updates = $has_updates; } /** * @return Element_CSS_Property[] */ public function get_css_properties() { return $this->css_properties; } public function get_background_css_property() { foreach ( $this->get_css_properties() as $css_property ) { if ( strpos( $css_property->get_property(), 'background' ) !== false ) { return $css_property; } } return null; } /** * @param Element_CSS_Property $css_property */ public function add_css_property( $css_property ) { // TODO: this won't work as of now $this->css_properties[] = $css_property; $this->set_has_updates( true ); } public function has_updates() { $has_updates = $this->has_updates; foreach ( $this->attributes as $attribute ) { $has_updates = $has_updates || $attribute->has_updates(); } foreach ( $this->css_properties as $css_property ) { $has_updates = $has_updates || $css_property->has_updates(); } return $has_updates; } public function get_updated_markup() { $updated = $this->get_markup(); $updated = $this->update_attributes( $updated ); $updated = $this->update_css_properties( $updated ); $updated = $this->replace_attributes( $updated ); $updated = $this->remove_attributes( $updated ); $updated = $this->add_new_attributes( $updated ); $updated = $this->add_postfix( $updated ); if ( $this->has_wrapper_markup() && $this->can_wrap_element() ) { $updated = $this->wrapper_markup_before . $updated . $this->wrapper_markup_after; } // TODO: this is a temporary way to support the old filters, remove this in the release that comes after 3.16.0 $updated = apply_filters_deprecated( 'smush_cdn_image_tag', array( $updated ), '3.16.0', 'wp_smush_updated_element_markup' ); $updated = apply_filters_deprecated( 'smush_cdn_bg_image_tag', array( $updated ), '3.16.0', 'wp_smush_updated_element_markup' ); return apply_filters( 'wp_smush_updated_element_markup', $updated ); } public function set_postfix( $postfix ) { $this->postfix = $postfix; } private function add_postfix( $markup ) { return $markup . $this->postfix; } private function replace_attributes( $markup ) { foreach ( $this->replaced_attributes as $original_attribute_name => $replaced_attribute ) { $original = $this->get_attribute( $original_attribute_name ); if ( $original ) { $replaced_attribute_string = $this->change_attribute_quote_character( $replaced_attribute->get_attribute(), $this->find_quote_character( $original->get_attribute() ) ); $markup = str_replace( $original->get_attribute(), $replaced_attribute_string, $markup ); } } return $markup; } private function find_quote_character( $string ) { return false === strpos( $string, '"' ) ? '\'' : '"'; } private function change_attribute_quote_character( $full_attribute, $new_quote_character ) { $current_quote_character = $this->find_quote_character( $full_attribute ); if ( $current_quote_character === $new_quote_character ) { return $full_attribute; } return str_replace( $current_quote_character, $new_quote_character, $full_attribute ); } private function update_attributes( $markup ) { foreach ( $this->get_attributes() as $attribute ) { if ( $attribute->has_updates() ) { $markup = str_replace( $attribute->get_attribute(), $attribute->get_updated(), $markup ); } } return $markup; } private function update_css_properties( $markup ) { foreach ( $this->get_css_properties() as $css_property ) { if ( $css_property->has_updates() ) { $markup = str_replace( $css_property->get_full(), $css_property->get_updated(), $markup ); } } return $markup; } /** * Remove atributes from an element. * * @param string $markup Element markup html. * * @return string Updated markup. */ private function remove_attributes( $markup ) { foreach ( $this->removed_attributes as $removed_attribute ) { $attribute_name = $removed_attribute->get_name(); $markup = $this->parser->remove_element_attribute( $markup, $attribute_name ); } return $markup; } private function add_new_attributes( $markup ) { foreach ( $this->added_attributes as $added_attribute ) { $attribute_name = $added_attribute->get_name(); // Remove the attribute first, important for removing any empty or invalid values before adding again $markup = $this->parser->remove_element_attribute( $markup, $attribute_name ); $markup = $this->parser->add_attribute_to_element( $markup, $this->get_tag(), $attribute_name, esc_attr( $added_attribute->get_value() ) ); } return $markup; } public function get_attribute_value( $attribute_name ) { $attribute = $this->get_attribute( $attribute_name ); return $attribute ? $attribute->get_value() : ''; } public function append_attribute_value( $attribute_name, $appendage ) { $attribute = $this->get_attribute( $attribute_name ); if ( $attribute ) { $attribute->set_value( $attribute->get_value() . " " . $appendage ); } else { $this->add_attribute( new Element_Attribute( $attribute_name, $appendage ) ); } } public function is_image_element() { return 'img' === $this->get_tag(); } public function has_attribute( $name ) { return ! empty( $this->get_attribute( $name ) ); } private function has_wrapper_markup() { return ! empty( $this->wrapper_markup_before ) && ! empty( $this->wrapper_markup_after ); } public function set_wrapper_markup( $wrapper_markup_before, $wrapper_markup_after ) { $this->wrapper_markup_before = $wrapper_markup_before; $this->wrapper_markup_after = $wrapper_markup_after; } private function can_wrap_element() { // Only wrap if the markup of the element is fully, included closing tag. $allowed_tags = array( 'iframe' ); return in_array( $this->get_tag(), $allowed_tags, true ); } public function is_lcp() { return $this->is_lcp; } public function set_lcp( $is_lcp ) { $this->is_lcp = $is_lcp; } public function get_position() { return $this->position; } public function set_position( $position ) { $this->position = $position; } public function get_original() { return $this->get_markup(); } public function get_updated() { return $this->get_updated_markup(); } } parser/class-element-css-property.php 0000644 00000004022 15252476777 0014001 0 ustar 00 <?php namespace Smush\Core\Parser; class Element_CSS_Property { private $full; private $property; /** * @var Value */ private $value; /** * @var Image_URL[] */ private $image_urls; public function __construct( $full, $property, $value, $image_urls ) { $this->full = $full; $this->property = $property; $this->value = new Value( $value ); $this->image_urls = $image_urls; } /** * @return mixed */ public function get_full() { return $this->full; } /** * @return mixed */ public function get_property() { return $this->property; } /** * @return mixed */ public function get_value() { return $this->value->get(); } public function set_value( $new_value ) { $this->value->set( $new_value ); } /** * @return Image_URL[] */ public function get_image_urls() { return $this->image_urls; } public function has_updates() { if ( $this->value->has_updates() ) { return true; } foreach ( $this->image_urls as $image_url ) { if ( $image_url->has_updates() ) { return true; } } return false; } public function get_updated() { $updated = $this->full; if ( $this->value->has_updates() ) { // Replace whole value $updated = $this->replace_value( $updated ); } else { // Replace the image URLs within the value $updated = $this->replace_image_urls( $updated ); } return $updated; } /** * @param $updated * * @return string */ private function replace_image_urls( $updated ) { foreach ( $this->image_urls as $image_url ) { if ( $image_url->has_updates() ) { $updated = str_replace( $image_url->get_previous_url(), esc_url_raw( $image_url->get_url() ), $updated ); } } return $updated; } public function get_single_image_url() { $image_urls = $this->get_image_urls(); return empty( $image_urls ) ? null : $image_urls[0]; } private function replace_value( $updated ) { return str_replace( $this->value->get_previous(), esc_attr( $this->value->get() ), $updated ); } } cache/class-cache-helper.php 0000644 00000002542 15252476777 0011774 0 ustar 00 <?php namespace Smush\Core\Cache; class Cache_Helper { private static $clear_cache_action = 'wp_smush_clear_page_cache'; private static $show_cache_notice_transient = 'wp_smush_show_cache_notice'; /** * Static instance * * @var self */ private static $instance; /** * Static instance getter */ public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } public function clear_post_cache( $post_id ) { do_action( 'wp_smush_post_cache_flush_required', $post_id ); } public function clear_home_cache( $url ) { do_action( 'wp_smush_home_cache_flush_required', $url ); } public function clear_full_cache( $notice_key = 'generic' ) { if ( ! has_action( self::$clear_cache_action ) && ! empty( $notice_key ) ) { // If no one is handling the cache clearing then show a notice set_transient( self::$show_cache_notice_transient, $notice_key ); } else { do_action( self::$clear_cache_action ); } } public function delete_notice_key() { delete_transient( self::$show_cache_notice_transient ); } public function get_notice_key() { return get_transient( self::$show_cache_notice_transient ); } /** * Get clear_cache_action. * * @return string */ public static function get_clear_cache_action() { return self::$clear_cache_action; } } cache/class-cache-controller.php 0000644 00000006412 15252476777 0012700 0 ustar 00 <?php namespace Smush\Core\Cache; use Smush\Core\Controller; use Smush\Core\Helper; use Smush\Core\Settings; class Cache_Controller extends Controller { private Cache_Helper $helper; public function __construct() { $this->helper = Cache_Helper::get_instance(); $this->register_action( 'wp_smush_avif_status_changed', array( $this, 'avif_status_changed' ) ); $this->register_action( 'wp_smush_webp_status_changed', array( $this, 'webp_status_changed' ) ); $this->register_action( 'wp_smush_webp_method_changed', array( $this, 'webp_method_changed' ) ); $this->register_action( 'wp_smush_cdn_status_changed', array( $this, 'cdn_status_changed' ) ); // TODO: identify other cases where cache should be cleared and call the clear_third_party_cache method $this->register_action( 'wp_ajax_smush_dismiss_cache_notice', array( $this, 'dismiss_cache_notice' ) ); $this->register_action( 'wp_smush_header_notices', array( $this, 'maybe_show_cache_notice' ) ); } public function cdn_status_changed() { $this->helper->clear_full_cache( 'cdn' ); } public function webp_method_changed() { $this->helper->clear_full_cache( 'next_gen_method' ); } public function webp_status_changed() { $this->helper->clear_full_cache( 'next_gen' ); } public function avif_status_changed() { $this->helper->clear_full_cache( 'next_gen' ); } public function dismiss_cache_notice() { check_ajax_referer( 'wp-smush-ajax' ); // Check for permission. if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 ); } $this->helper->delete_notice_key(); wp_send_json_success(); } public function maybe_show_cache_notice() { $notice = $this->get_cache_notice(); if ( empty( $notice ) ) { return; } ?> <div class="sui-notice sui-notice-info" id="wp-smush-cache-notice"> <div class="sui-notice-content"> <div class="sui-notice-message"> <i class="sui-notice-icon sui-icon-info" aria-hidden="true"></i> <p><?php echo wp_kses_post( $notice ); ?></p> </div> <div class="sui-notice-actions"> <button class="sui-button-icon smush-dismiss-notice-button"> <i class="sui-icon-check" aria-hidden="true"></i> <span class="sui-screen-reader-text"><?php esc_html_e( 'Dismiss', 'wp-smushit' ); ?></span> </button> </div> </div> </div> <?php } private function get_cache_notice() { $notice_key = $this->helper->get_notice_key(); if ( empty( $notice_key ) ) { return; } $settings = Settings::get_instance(); if ( 'cdn' === $notice_key ) { return $settings->has_cdn_page() ? __( 'CDN status has changed.<br/>If you have a page caching plugin or server caching, please clear it to ensure everything works as expected.', 'wp-smushit' ) : ''; } if ( 'next_gen' === $notice_key || 'next_gen_method' === $notice_key ) { $notice = 'next_gen' === $notice_key ? __( 'Next-Gen Formats status has changed.<br/>If you have a page caching plugin or server caching, please clear it to ensure everything works as expected.', 'wp-smushit' ) : __( 'Next-Gen conversion method has been updated.<br/>If you have a page caching plugin or server caching, please clear it to ensure everything works as expected.', 'wp-smushit' ); return $settings->has_next_gen_page() ? $notice : ''; } } } security/class-security-utils.php 0000644 00000005442 15252476777 0013267 0 ustar 00 <?php namespace Smush\Core\Security; use Smush\Core\Array_Utils; use Smush\Core\Threads\JSON_Object_Map; class Security_Utils { private static $expected_nonces_option_id = 'wp_smush_public_expected_nonces'; /** * @var Array_Utils */ private $array_utils; /** * @var JSON_Object_Map */ private $object_map; public function __construct() { $this->array_utils = new Array_Utils(); $this->object_map = new JSON_Object_Map( self::$expected_nonces_option_id ); } public function create_public_nonce( $action = - 1 ) { $nonce = wp_hash( wp_nonce_tick() . '|' . $action, 'nonce' ); $added = $this->add_expected_nonce( $nonce ); if ( ! $added ) { return false; } return $nonce; } public function verify_public_nonce( $nonce, $action = - 1 ) { $nonce_valid = hash_equals( wp_hash( wp_nonce_tick() . '|' . $action, 'nonce' ), $nonce ); $nonce_expected = $this->is_nonce_expected( $nonce ); return $nonce_valid && $nonce_expected; } public function clean_public_nonce( $nonce ) { $this->object_map->remove( $this->expected_nonce_key( $nonce ) ); } private function add_expected_nonce( $nonce ) { return $this->object_map->add( $this->expected_nonce_key( $nonce ), array( 'time' => time(), 'nonce' => $nonce ) ); } private function is_nonce_expected( $nonce ) { $expected_nonces = $this->get_expected_nonces(); foreach ( $expected_nonces as $data ) { $now = time(); $time = (int) $this->array_utils->get_array_value( $data, 'time' ); $is_fresh = ( $now - $time ) < $this->get_expected_nonce_expiry(); $expected_nonce = $this->array_utils->get_array_value( $data, 'nonce' ); if ( $is_fresh && $expected_nonce === $nonce ) { return true; } } return false; } public function clean_expected_nonces() { $expected_nonces = $this->clean_expected( $this->get_expected_nonces() ); $this->object_map->unsafe_set( $expected_nonces ); } private function clean_expected( $expected_nonces ) { $now = time(); foreach ( $expected_nonces as $key => $data ) { $time = (int) $this->array_utils->get_array_value( $data, 'time' ); if ( ( $now - $time ) > $this->get_expected_nonce_expiry() ) { unset( $expected_nonces[ $key ] ); } } return $expected_nonces; } /** * @return array */ public function get_expected_nonces() { $nonces = $this->object_map->get( array() ); return $this->array_utils->ensure_array( $nonces ); } /** * @return float|int */ private function get_expected_nonce_expiry() { // 15 minutes return MINUTE_IN_SECONDS * 15; } private function expected_nonce_key( $nonce ) { return "nonce_$nonce"; } /** * Get expected_nonces_option. * * @return string */ public static function get_expected_nonces_option_id() { return self::$expected_nonces_option_id; } } security/class-security-controller.php 0000644 00000000727 15252476777 0014313 0 ustar 00 <?php namespace Smush\Core\Security; use Smush\Core\Controller; use Smush\Core\Cron_Controller; class Security_Controller extends Controller { /** * @var Security_Controller */ private static $instance; private $security_utils; public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } public function __construct() { $this->security_utils = new Security_Utils(); } } class-stats.php 0000644 00000037425 15252476777 0007557 0 ustar 00 <?php /** * Class that is responsible for all stats calculations. * * @since 3.4.0 * @package Smush\Core */ namespace Smush\Core; use Smush\Core\Media\Media_Item; use Smush\Core\Media\Media_Item_Query; use Smush\Core\Png2Jpg\Png2Jpg_Optimization; use Smush\Core\Resize\Resize_Optimization; use Smush\Core\Smush\Smush_Optimization; use Smush\Core\Smush\Smush_Optimization_Global_Stats; use Smush\Core\Stats\Global_Stats; use stdClass; use WP_Query; if ( ! defined( 'WPINC' ) ) { die; } /** * Class Stats */ class Stats { /** * Stores the stats for all the images. * * @var array $stats */ public $stats; /** * Compressed attachments from selected directories. * * @var array $dir_stats */ public $dir_stats; /** * Set a limit of MySQL query. Default: 3000. * * @var int $query_limit */ private $query_limit; /** * Set a limit to max number of rows in MySQL query. Default: 5000. * * @var int $max_rows */ private $max_rows; /** * Attachment IDs. * * @var array $attachments */ public $attachments = array(); /** * Image ids that needs to be resmushed. * * @var array $resmush_ids */ public $resmush_ids = array(); /** * Percentage of the smushed images. * * @var float */ public $percent_optimized; /** * Percentage metric. * * @var float */ public $percent_metric; /** * Class name of grade type. * * @var string */ public $percent_grade; /** * Protected init class, used in child methods instead of constructor. * * @since 3.4.0 */ protected function init() {} public function __call( $method_name, $arguments ) { _deprecated_function( esc_html( $method_name ), '4.2.0' ); } /** * Stats constructor. */ public function __construct() { $this->init(); $this->query_limit = apply_filters( 'wp_smush_query_limit', 3000 ); $this->max_rows = apply_filters( 'wp_smush_max_rows', 5000 ); // Recalculate resize savings. add_action( 'wp_smush_image_resized', function() { return $this->get_savings( 'resize' ); } ); // Update Conversion savings. add_action( 'wp_smush_png_jpg_converted', function() { return $this->get_savings( 'pngjpg' ); } ); // Update the media_attachments list. add_action( 'add_attachment', array( $this, 'add_to_media_attachments_list' ) ); add_action( 'delete_attachment', array( $this, 'update_lists' ), 12 ); } /** * Get the savings from image resizing or PNG -> JPG conversion savings. * * @param string $type Savings type. Accepts: resize, pngjpg. * @param bool $force_update Force update to re-calculate all stats. Default: false. * @param bool $format Format the bytes in readable format. Default: false. * @param bool $return_count Return the resized image count. Default: false. * * @return int|array */ public function get_savings( $type, $force_update = true, $format = false, $return_count = false ) { $key = 'wp-smush-' . $type . '_savings'; $key_count = 'wp-smush-resize_count'; if ( ! $force_update ) { $savings = wp_cache_get( $key, 'wp-smush' ); if ( ! $return_count && $savings ) { return $savings; } $count = wp_cache_get( $key_count, 'wp-smush' ); if ( $return_count && false !== $count ) { return $count; } } // If savings or resize image count is not stored in db, recalculate. $count = 0; $offset = 0; $query_next = true; $savings = array( 'resize' => array( 'bytes' => 0, 'size_before' => 0, 'size_after' => 0, ), 'pngjpg' => array( 'bytes' => 0, 'size_before' => 0, 'size_after' => 0, ), ); global $wpdb; while ( $query_next ) { $query_data = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key=%s LIMIT %d, %d", $key, $offset, $this->query_limit ) ); // Db call ok. // No results - break out of loop. if ( empty( $query_data ) ) { break; } foreach ( $query_data as $data ) { // Skip resmush IDs. if ( ! empty( $this->resmush_ids ) && in_array( $data->post_id, $this->resmush_ids, true ) ) { continue; } $count++; if ( empty( $data ) ) { continue; } $meta = maybe_unserialize( $data->meta_value ); // Resize mete already contains all the stats. if ( ! empty( $meta ) && ! empty( $meta['bytes'] ) ) { $savings['resize']['bytes'] += $meta['bytes']; $savings['resize']['size_before'] += $meta['size_before']; $savings['resize']['size_after'] += $meta['size_after']; } // PNG - JPG conversion meta contains stats by attachment size. if ( is_array( $meta ) ) { foreach ( $meta as $size ) { $savings['pngjpg']['bytes'] += isset( $size['bytes'] ) ? $size['bytes'] : 0; $savings['pngjpg']['size_before'] += isset( $size['size_before'] ) ? $size['size_before'] : 0; $savings['pngjpg']['size_after'] += isset( $size['size_after'] ) ? $size['size_after'] : 0; } } } // Update the offset. $offset += $this->query_limit; // Compare the offset value to total images. $query_next = $this->total_count > $offset; } if ( $format ) { $savings[ $type ]['bytes'] = size_format( $savings[ $type ]['bytes'], 1 ); } wp_cache_set( 'wp-smush-resize_savings', $savings['resize'], 'wp-smush' ); wp_cache_set( 'wp-smush-pngjpg_savings', $savings['pngjpg'], 'wp-smush' ); wp_cache_set( $key_count, $count, 'wp-smush' ); return $return_count ? $count : $savings[ $type ]; } /** * Adds the ID of the smushed image to the media_attachments list. * * @since 3.7.1 * * @param int $id Attachment's ID. */ public function add_to_media_attachments_list( $id ) { $posts = wp_cache_get( 'media_attachments', 'wp-smush' ); // Return if there's no list to update. if ( ! $posts ) { return; } $mime_type = get_post_mime_type( $id ); $id_string = (string) $id; // Add the ID if the mime type is allowed and the ID isn't in the list already. if ( $mime_type && in_array( $mime_type, Core::$mime_types, true ) && ! in_array( $id_string, $posts, true ) ) { $posts[] = $id_string; wp_cache_set( 'media_attachments', $posts, 'wp-smush' ); } } /** * Updates the IDs lists when an attachment is deleted. * * @since 3.7.2 * * @param integer $id Deleted attachment ID. */ public function update_lists( $id ) { $this->remove_from_media_attachments_list( $id ); self::remove_from_smushed_list( $id ); } /** * Removes the ID of the deleted image from the media_attachments list. * * @since 3.7.1 * * @param int $id Attachment's ID. */ private function remove_from_media_attachments_list( $id ) { $posts = wp_cache_get( 'media_attachments', 'wp-smush' ); // Return if there's no list to update. if ( ! $posts ) { return; } $index = array_search( (string) $id, $posts, true ); if ( false !== $index ) { unset( $posts[ $index ] ); wp_cache_set( 'media_attachments', $posts, 'wp-smush' ); } } /** * Removes an ID from the smushed IDs list from the object cache. * * @since 3.7.2 * * @param integer $attachment_id ID of the smushed attachment. */ public static function remove_from_smushed_list( $attachment_id ) { $smushed_ids = wp_cache_get( 'wp-smush-smushed_ids', 'wp-smush' ); if ( ! empty( $smushed_ids ) ) { $index = array_search( strval( $attachment_id ), $smushed_ids, true ); if ( false !== $index ) { unset( $smushed_ids[ $index ] ); wp_cache_set( 'wp-smush-smushed_ids', $smushed_ids, 'wp-smush' ); } } } /** * Temporary remove Smush metadata. * * We use this in order to temporary remove the stats metadata, * e.g While generating thumbnail or wp_generate_ when disabled auto smush. * * Note, if member's site allows compression of the original file, * when we remove stats, we might lose a large amount of storage (stats) that we saved for the member's site. * => TODO: Delete stats or just update new stats with re-smush? * * @since 3.9.6 * * @param int $attachment_id Attachment ID. */ public function remove_stats( $attachment_id ) { // Main stats. delete_post_meta( $attachment_id, Modules\Smush::$smushed_meta_key ); // Lossy flag. delete_post_meta( $attachment_id, 'wp-smush-lossy' ); // Finally, remove the attachment ID from cache. self::remove_from_smushed_list( $attachment_id ); } /** * Get unsmushed meta query. * * @return array */ public static function get_unsmushed_meta_query() { $unsmushed_query = array( 'relation' => 'AND', array( 'key' => Smush_Optimization::get_smush_meta_key(), 'compare' => 'NOT EXISTS', ), array( 'key' => Media_Item::get_ignored_meta_key(), 'compare' => 'NOT EXISTS', ), ); return $unsmushed_query; } /** * Smush and Resizing Stats Combined together. * * @param array $smush_stats Smush stats. * @param array $resize_savings Resize savings. * * @return array Array of all the stats */ public function combined_stats( $smush_stats, $resize_savings ) { if ( empty( $smush_stats ) || empty( $resize_savings ) ) { return $smush_stats; } // Initialize key full if not there already. if ( ! isset( $smush_stats['sizes']['full'] ) ) { $smush_stats['sizes']['full'] = new stdClass(); $smush_stats['sizes']['full']->bytes = 0; $smush_stats['sizes']['full']->size_before = 0; $smush_stats['sizes']['full']->size_after = 0; $smush_stats['sizes']['full']->percent = 0; } // Full Image. if ( ! empty( $smush_stats['sizes']['full'] ) ) { $smush_stats['sizes']['full']->bytes = ! empty( $resize_savings['bytes'] ) ? $smush_stats['sizes']['full']->bytes + $resize_savings['bytes'] : $smush_stats['sizes']['full']->bytes; $smush_stats['sizes']['full']->size_before = ! empty( $resize_savings['size_before'] ) && ( $resize_savings['size_before'] > $smush_stats['sizes']['full']->size_before ) ? $resize_savings['size_before'] : $smush_stats['sizes']['full']->size_before; $smush_stats['sizes']['full']->percent = ! empty( $smush_stats['sizes']['full']->bytes ) && $smush_stats['sizes']['full']->size_before > 0 ? ( $smush_stats['sizes']['full']->bytes / $smush_stats['sizes']['full']->size_before ) * 100 : $smush_stats['sizes']['full']->percent; $smush_stats['sizes']['full']->size_after = $smush_stats['sizes']['full']->size_before - $smush_stats['sizes']['full']->bytes; $smush_stats['sizes']['full']->percent = round( $smush_stats['sizes']['full']->percent, 1 ); } return $this->total_compression( $smush_stats ); } /** * Combine Savings from PNG to JPG conversion with smush stats * * @param array $stats Savings from Smushing the image. * @param array $conversion_savings Savings from converting the PNG to JPG. * * @return Object|array Total Savings */ public function combine_conversion_stats( $stats, $conversion_savings ) { if ( empty( $stats ) || empty( $conversion_savings ) ) { return $stats; } foreach ( $conversion_savings as $size_k => $savings ) { // Initialize Object for size. if ( empty( $stats['sizes'][ $size_k ] ) ) { $stats['sizes'][ $size_k ] = new stdClass(); $stats['sizes'][ $size_k ]->bytes = 0; $stats['sizes'][ $size_k ]->size_before = 0; $stats['sizes'][ $size_k ]->size_after = 0; $stats['sizes'][ $size_k ]->percent = 0; } if ( ! empty( $stats['sizes'][ $size_k ] ) && ! empty( $savings ) ) { $stats['sizes'][ $size_k ]->bytes = $stats['sizes'][ $size_k ]->bytes + $savings['bytes']; $stats['sizes'][ $size_k ]->size_before = $stats['sizes'][ $size_k ]->size_before > $savings['size_before'] ? $stats['sizes'][ $size_k ]->size_before : $savings['size_before']; $stats['sizes'][ $size_k ]->percent = ! empty( $stats['sizes'][ $size_k ]->bytes ) && $stats['sizes'][ $size_k ]->size_before > 0 ? ( $stats['sizes'][ $size_k ]->bytes / $stats['sizes'][ $size_k ]->size_before ) * 100 : $stats['sizes'][ $size_k ]->percent; $stats['sizes'][ $size_k ]->percent = round( $stats['sizes'][ $size_k ]->percent, 1 ); } } return $this->total_compression( $stats ); } /** * Iterate over all the size stats and calculate the total stats * * @param array $stats Stats array. * * @return mixed */ public function total_compression( $stats ) { $stats['stats']['size_before'] = 0; $stats['stats']['size_after'] = 0; $stats['stats']['time'] = 0; foreach ( $stats['sizes'] as $size_stats ) { $stats['stats']['size_before'] += ! empty( $size_stats->size_before ) ? $size_stats->size_before : 0; $stats['stats']['size_after'] += ! empty( $size_stats->size_after ) ? $size_stats->size_after : 0; $stats['stats']['time'] += ! empty( $size_stats->time ) ? $size_stats->time : 0; } $stats['stats']['bytes'] = ! empty( $stats['stats']['size_before'] ) && $stats['stats']['size_before'] > $stats['stats']['size_after'] ? $stats['stats']['size_before'] - $stats['stats']['size_after'] : 0; if ( ! empty( $stats['stats']['bytes'] ) && ! empty( $stats['stats']['size_before'] ) ) { $stats['stats']['percent'] = ( $stats['stats']['bytes'] / $stats['stats']['size_before'] ) * 100; } return $stats; } /** * Returns an array that can be consumed by the JS * * TODO: When we have rewritten the frontend of the plugin we can directly use {@see Global_Stats::to_array()} instead * * @return array */ public function get_global_stats() { $global_stats = Global_Stats::get(); $total_stats = $global_stats->get_sum_of_optimization_global_stats(); /** * @var $smush_stats Smush_Optimization_Global_Stats */ $smush_stats = $global_stats->get_persistable_stats_for_optimization( Smush_Optimization::get_key() ) ->get_stats(); $resize_stats = $global_stats->get_persistable_stats_for_optimization( Resize_Optimization::get_key() ) ->get_stats(); $png2jpg_stats = $global_stats->get_persistable_stats_for_optimization( Png2Jpg_Optimization::get_key() ) ->get_stats(); return array( 'stats_updated_timestamp' => $global_stats->get_stats_updated_timestamp(), 'is_outdated' => $global_stats->is_outdated(), 'count_supersmushed' => $smush_stats->get_lossy_count(), 'count_smushed' => $smush_stats->get_count(), 'count_total' => $global_stats->get_total_optimizable_items_count(), 'count_images' => $global_stats->get_optimized_images_count(), 'count_resize' => $resize_stats->get_count(), 'count_skipped' => $global_stats->get_skipped_count(), 'unsmushed' => $global_stats->get_optimize_list()->get_ids(), 'count_unsmushed' => $global_stats->get_optimize_list()->get_count(), 'resmush' => $global_stats->get_redo_ids(), 'count_resmush' => $global_stats->get_redo_count(), 'size_before' => $total_stats->get_size_before(), 'size_after' => $total_stats->get_size_after(), 'savings_bytes' => $total_stats->get_bytes(), 'human_bytes' => $total_stats->get_human_bytes(), 'savings_resize' => $resize_stats->get_bytes(), 'savings_resize_human' => $resize_stats->get_human_bytes(), 'savings_conversion' => $png2jpg_stats->get_bytes(), 'savings_conversion_human' => $png2jpg_stats->get_human_bytes(), 'savings_dir_smush' => $this->dir_stats, 'savings_percent' => $total_stats->get_percent() > 0 ? number_format_i18n( $total_stats->get_percent(), 1 ) : 0, 'percent_grade' => $global_stats->get_grade_class(), 'percent_metric' => $global_stats->get_percent_metric(), 'percent_optimized' => $global_stats->get_percent_optimized(), 'remaining_count' => $global_stats->get_remaining_count(), ); } } cli/class-cli-optimizer.php 0000644 00000023237 15252476777 0011753 0 ustar 00 <?php /** * Class CLI * * @since 3.1 * @package Smush\Core */ namespace Smush\Core\CLI; use Smush\Core\Array_Utils; use Smush\Core\Media\Media_Item_Cache; use Smush\Core\Media\Media_Item_Optimizer; use WP_CLI; use WP_Smush; if ( ! defined( 'WPINC' ) ) { die; } class CLI_Optimizer { private static $image_id_key = 'ID'; private static $edit_link_key = 'Edit Link'; private static $error_message_key = 'Error Message'; private static $image_link_key = 'IMAGE LINK'; private static $mime_type_key = 'MIME TYPE'; /** * @var int */ private $limit; /** * @var array */ private $errors; /** * @var array */ private $ids; /** * @var Array_Utils */ private $array_utils; public function __construct( $array_utils ) { $this->array_utils = $array_utils; } public function bulk_restore( $start_message ) { WP_CLI::log( $start_message ); $total_images = $this->get_count(); if ( $total_images < 1 ) { WP_CLI::success( __( 'No images available to restore', 'wp-smushit' ) ); return; } $progress = WP_CLI\Utils\make_progress_bar( __( 'Progress:', 'wp-smushit' ), $total_images ); $optimize_ids = $this->get_ids(); WP_CLI::log( sprintf( __( 'Found %d attachments that need to be restored!', 'wp-smushit' ), $total_images ) ); $this->log_start_restore(); foreach ( $optimize_ids as $attachment_id ) { $this->restore( (int) $attachment_id ); $progress->tick(); } $progress->finish(); $this->render_restore_status(); $this->reset(); } private function log_start_restore() { if ( 1 === $this->get_limit() ) { return WP_CLI::log( __( 'Starting restoration ...', 'wp-smushit' ) ); } return WP_CLI::log( __( 'Starting bulk restoration ...', 'wp-smushit' ) ); } private function restore( $attachment_id ) { $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); $optimizer = new Media_Item_Optimizer( $media_item ); $restored = $optimizer->restore(); if ( ! $restored ) { $error_message = sprintf( /* translators: %d - attachment ID */ esc_html__( 'Image %d cannot be restored.', 'wp-smushit' ), (int) $attachment_id ); $this->add_error( $this->get_error_item( $attachment_id, $error_message ) ); } return $restored; } private function render_restore_status() { $total_images = $this->get_count(); $errors = $this->get_errors(); if ( 1 === $this->get_limit() ) { $this->render_single_restore_status( $errors ); return; } $this->render_bulk_restore_status( $total_images, $errors ); } private function render_single_restore_status( $errors ) { if ( empty( $errors ) ) { WP_CLI::success( __( 'Image restored successfully!', 'wp-smushit' ) ); return; } WP_CLI::warning( sprintf( /* translators: %s: Error message */ __( 'Image could not be restored: %s', 'wp-smushit' ), $this->array_utils->get_array_value( $errors[0], self::$error_message_key ) ) ); } private function render_bulk_restore_status( $total_images, $errors ) { $no_errors = count( $errors ); $bulk_restore_message = $this->get_bulk_restore_message( $no_errors, $total_images ); if ( empty( $no_errors ) ) { WP_CLI::success( $bulk_restore_message ); return; } WP_CLI::warning( $bulk_restore_message ); WP_CLI\Utils\format_items( 'table', $errors, array( self::$image_id_key, self::$edit_link_key, self::$error_message_key ) ); } private function get_bulk_restore_message( $no_errors, $total_images ) { if ( $no_errors === $total_images ) { return esc_html__( 'All of your images failed to restore. Find out why and how to resolve the issue(s) below.', 'wp-smushit' ); } elseif ( $no_errors > 0 ) { $no_restored = $total_images - $no_errors; $bulk_restore_message = esc_html__( '{{smushed}}/{{total}} images restored successfully, {{errors}} images were not restored. Find out why and how to resolve the issue(s) below.', 'wp-smushit' ); $bulk_restore_message = str_replace( array( '{{smushed}}', '{{total}}', '{{errors}}' ), array( $no_restored, $total_images, $no_errors ), $bulk_restore_message ); return $bulk_restore_message; } return WP_CLI::success( __( 'All images restored.', 'wp-smushit' ) ); } public function bulk_optimize( $start_message ) { WP_CLI::log( $start_message ); $total_images = $this->get_count(); if ( $total_images < 1 ) { WP_CLI::success( __( 'No images available to smush.', 'wp-smushit' ) ); return; } $progress = WP_CLI\Utils\make_progress_bar( __( 'Progress:', 'wp-smushit' ), $total_images ); $optimize_ids = $this->get_ids(); WP_CLI::log( sprintf( __( 'Found %d attachments that need smushing!', 'wp-smushit' ), $total_images ) ); $this->log_start_smush(); foreach ( $optimize_ids as $attachment_id ) { $this->optimize( (int) $attachment_id ); $progress->tick(); } $progress->finish(); $this->render_smush_status(); $this->reset(); } private function log_start_smush() { if ( 1 === $this->get_limit() ) { return WP_CLI::log( __( 'Starting smush ...', 'wp-smushit' ) ); } return WP_CLI::log( __( 'Starting smush ...', 'wp-smushit' ) ); } private function optimize( $attachment_id ) { $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); $optimizer = new Media_Item_Optimizer( $media_item ); $optimized = $optimizer->optimize(); if ( $optimized ) { return true; } if ( $media_item->has_errors() ) { $this->add_error( $this->get_error_item( $attachment_id, $media_item->get_errors()->get_error_message() ) ); } else { $this->add_error( $this->get_error_item( $attachment_id, $optimizer->get_errors()->get_error_message() ) ); } return false; } private function get_error_item( $attachment_id, $error_message ) { $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); return array( self::$image_id_key => $attachment_id, self::$edit_link_key => $media_item->get_edit_url(), self::$error_message_key => $error_message, ); } private function render_smush_status() { $total_images = $this->get_count(); $errors = $this->get_errors(); if ( 1 === $this->get_limit() ) { $this->render_single_smush_status( $errors ); return; } $this->render_bulk_smush_status( $total_images, $errors ); } private function render_single_smush_status( $errors ) { if ( empty( $errors ) ) { WP_CLI::success( __( 'Image smushed.', 'wp-smushit' ) ); return; } WP_CLI::warning( sprintf( /* translators: %s: Error message */ __( 'Image could not be smushed: %s', 'wp-smushit' ), $this->array_utils->get_array_value( $errors[0], self::$error_message_key ) ) ); } private function render_bulk_smush_status( $total_images, $errors ) { $no_errors = count( $errors ); $bulk_smush_message = $this->get_bulk_smush_message( $no_errors, $total_images ); if ( empty( $no_errors ) ) { WP_CLI::success( $bulk_smush_message ); return; } WP_CLI::warning( $bulk_smush_message ); WP_CLI\Utils\format_items( 'table', $errors, array( self::$image_id_key, self::$edit_link_key, self::$error_message_key ) ); } private function get_bulk_smush_message( $no_errors, $total_images ) { $localize_strings = WP_Smush::get_instance()->core()->get_localize_strings(); if ( $no_errors === $total_images ) { return $this->array_utils->get_array_value( $localize_strings, 'all_failed' ); } elseif ( $no_errors > 0 ) { $no_smushed = $total_images - $no_errors; $bulk_smush_message = $this->array_utils->get_array_value( $localize_strings, 'error_in_bulk' ); $bulk_smush_message = str_replace( array( '{{smushed}}', '{{total}}', '{{errors}}' ), array( $no_smushed, $total_images, $no_errors ), $bulk_smush_message ); return $bulk_smush_message; } return $this->array_utils->get_array_value( $localize_strings, 'all_smushed' ); } public function render_optimize_list( $title ) { $optimize_list = $this->get_optimize_list(); if ( empty( $optimize_list ) ) { WP_CLI::success( __( 'We did not find any images that need smushing.', 'wp-smushit' ) ); return; } WP_CLI::log( $title ); WP_CLI\Utils\format_items( 'table', $this->get_optimize_list(), array( self::$image_id_key, self::$image_link_key, self::$mime_type_key ) ); $this->reset(); } private function get_optimize_list() { $optimize_list = array(); $optimize_ids = $this->get_ids(); foreach ( $optimize_ids as $attachment_id ) { $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); if ( ! $media_item->is_valid() || ! $media_item->get_main_size() ) { continue; } $optimize_list[] = $this->get_optimize_item( $media_item ); } return $optimize_list; } private function get_ids() { return $this->ids; } public function set_ids( $ids ) { $limit = $this->get_limit(); $ids = (array) $ids; if ( $limit && $limit < count( $ids ) ) { $ids = array_slice( $ids, 0, $limit ); } $this->ids = $ids; return $this; } public function get_optimize_item( $media_item ) { return array( self::$image_id_key => $media_item->get_id(), self::$image_link_key => $media_item->get_main_size()->get_file_url(), self::$mime_type_key => $media_item->get_mime_type(), ); } private function get_count() { return count( $this->get_ids() ); } private function get_limit() { return $this->limit; } public function set_limit( $limit ) { $this->limit = $limit > 0 ? $limit : 0; return $this; } private function add_error( $error_item ) { $this->errors[] = $error_item; } public function get_errors() { return (array) $this->errors; } private function set_errors( $errors ) { $this->errors = $errors; return $this; } public function reset() { $this->set_limit( 0 ); $this->set_errors( array() ); $this->set_ids( array() ); } } cli/class-cli.php 0000644 00000025407 15252476777 0007734 0 ustar 00 <?php /** * Class CLI * * @since 3.1 * @package Smush\Core */ namespace Smush\Core\CLI; use Smush\Core\Array_Utils; use Smush\Core\Backups\Backups; use Smush\Core\Helper; use Smush\Core\Media_Library\Background_Media_Library_Scanner; use Smush\Core\Membership\Membership; use Smush\Core\Stats\Global_Stats; use WP_CLI; use WP_CLI_Command; use WP_Smush; if ( ! defined( 'WPINC' ) ) { die; } /** * Reduce image file sizes, improve performance and boost your SEO using the free WPMU DEV Smush API. */ class CLI extends WP_CLI_Command { /** * @var Array_Utils */ private $array_utils; /** * @var CLI_Optimizer */ private $cli_optimizer; public function __construct() { parent::__construct(); $this->array_utils = new Array_Utils(); $this->cli_optimizer = new CLI_Optimizer( $this->array_utils ); } /** * Optimize image. * * ## OPTIONS * * [--type=<type>] * : Optimize single image, batch or all images. * --- * default: all * options: * - all * - single * - multiple * - batch * --- * * [--image=<ID>] * : Attachment ID to compress. * --- * default: 0 * --- * * ## EXAMPLES * * # Smush all images. * $ wp smush compress * * # Smush single image with ID = 10. * $ wp smush compress --type=single --image=10 * * # Smush multiple image IDs. * $ wp smush compress --type=multiple --image=10,15,16 * * # Smush first 5 images. * $ wp smush compress --type=batch --image=5 * * @param array $args All the positional arguments. * @param array $assoc_args All the arguments defined like --key=value or --flag or --no-flag. */ public function compress( $args, $assoc_args ) { if ( Membership::get_instance()->is_api_hub_access_required() ) { WP_CLI::warning( __( 'Super 2X Smush requires your site to be connected to a free WPMU DEV account. Connect your site via plugin and try again.', 'wp-smushit' ) ); return; } $type = $this->array_utils->get_array_value( $assoc_args, 'type' ); $image = $this->array_utils->get_array_value( $assoc_args, 'image' ); if ( 'single' !== $type && Global_Stats::get()->is_outdated() ) { WP_CLI::warning( 'Smush needs to scan the media library for changes before starting optimization. Running a scan now.', 'wp-smushit' ); WP_CLI::runcommand( 'smush scan' ); } switch ( $type ) { case 'single': case 'multiple': if ( empty( $image ) ) { WP_CLI::warning( __( 'Missing image id(s).', 'wp-smushit' ) ); return; } $image_ids = explode( ',', $image ); $count = count( $image_ids ); $this->cli_optimizer->set_limit( $count ) ->set_ids( $image_ids ) ->bulk_optimize( sprintf( /* translators: %s Smush image Id(s) */ _n( 'Smushing image ID: %d', 'Smushing images %s', $count, 'wp-smushit' ), $image ) ); $count_limit = 25; $this->_list( array( $count_limit ) ); break; case 'batch': $limit = absint( $image ); $this->cli_optimizer->set_limit( $limit ) ->set_ids( $this->get_all_optimize_ids() ) /* translators: %d - number of images */ ->bulk_optimize( sprintf( __( 'Smushing first %d images', 'wp-smushit' ), absint( $image ) ) ); break; case 'all': default: $this->cli_optimizer->set_ids( $this->get_all_optimize_ids() ) ->bulk_optimize( __( 'Smushing all images', 'wp-smushit' ) ); break; } } /** * List unoptimized images. * * ## OPTIONS * * [<count>] * : Limit number of images to get. * * ## EXAMPLES * * # Get all unoptimized images. * $ wp smush list * * # Get the first 100 images that are not optimized. * $ wp smush list 100 * * @subcommand list * @when after_wp_load * * @param array $args All the positional arguments. */ public function _list( $args = array() ) { if ( ! empty( $args ) ) { list( $count ) = $args; } else { $count = PHP_INT_MAX; } if ( Global_Stats::get()->is_outdated() ) { WP_CLI::warning( 'Smush needs to scan the media library for changes before starting optimization. Running a scan now.', 'wp-smushit' ); WP_CLI::runcommand( 'smush scan' ); } $this->cli_optimizer->set_limit( $count ) ->set_ids( $this->get_all_optimize_ids() ) ->render_optimize_list( __( 'Images that need to be smushed:', 'wp-smushit' ) ); } private function get_all_optimize_ids() { $global_stats = Global_Stats::get(); $optimize_list = $global_stats->get_optimize_list(); return $this->array_utils->fast_array_unique( array_merge( $optimize_list->get_ids(), $global_stats->get_redo_ids() ) ); } /** * Restore image. * * ## OPTIONS * * [--id=<ID>] * : Attachment ID to restore. * --- * default: all * --- * * ## EXAMPLES * * # Restore all images that have backups. * $ wp smush restore * * # Restore single image with ID = 10. * $ wp smush restore --id=10 * * @param array $args All the positional arguments. * @param array $assoc_args All the arguments defined like --key=value or --flag or --no-flag. */ public function restore( $args, $assoc_args ) { $id = $this->array_utils->get_array_value( $assoc_args, 'id' ); if ( 'all' === $id ) { $restore_ids = ( new Backups() )->get_attachments_with_backups(); $this->cli_optimizer->set_ids( $restore_ids ) ->bulk_restore( __( 'Restoring all images', 'wp-smushit' ) ); return; } $restore_ids = explode( ',', $id ); $total_restore_images = count( $restore_ids ); $this->cli_optimizer->set_limit( $total_restore_images ) ->set_ids( $restore_ids ) ->bulk_restore( sprintf( /* translators: %s Restore image Id(s) */ _n( 'Restoring %s image', 'Restoring %s images', $total_restore_images, 'wp-smushit' ), $id ) ); } /** * Scan Media. * * ## EXAMPLES * * # Scan media library. * $ wp smush scan */ public function scan() { if ( ! Helper::loopback_supported() ) { WP_CLI::warning( esc_html__( 'Your site seems to have an issue with loopback requests. Please try again and if the problem persists find out more here: https://wpmudev.com/docs/wpmu-dev-plugins/smush/#background-processing', 'wp-smushit' ) ); return; } $background_scan = Background_Media_Library_Scanner::get_instance(); $status = $background_scan->start_background_scan_direct(); if ( is_wp_error( $status ) ) { WP_CLI::warning( $status->get_error_message() ); return; } WP_CLI::log( __( 'Starting media library scan', 'wp-smushit' ) ); $background_scan_status = $background_scan->get_background_process()->get_status(); $progress = WP_CLI\Utils\make_progress_bar( __( 'Progress:', 'wp-smushit' ), $this->array_utils->get_array_value( $status, 'total_items' ) ); $processed_items = $this->array_utils->get_array_value( $status, 'processed_items' ); $this->update_progress( $progress, $processed_items ); do { $prev_processed_items = $processed_items; $processed_items = $background_scan_status->get_processed_items(); $this->update_progress( $progress, $processed_items - $prev_processed_items ); sleep( 2 ); } while ( $background_scan_status->is_in_processing() ); $progress->finish(); if ( $background_scan_status->is_dead() ) { WP_CLI::warning( esc_html__( 'Unfortunately the scan could not be completed due to an unknown error. Please restart the scan.', 'wp-smushit' ) ); return; } if ( $background_scan_status->is_cancelled() ) { WP_CLI::warning( esc_html__( 'The background process is cancelled.', 'wp-smushit' ) ); return; } WP_CLI::success( esc_html__( 'Media library scan complete.', 'wp-smushit' ) ); // Reset notoptions cache to fetch the latest stats. wp_cache_delete( 'notoptions', 'options' ); // Get new instance to avoid the cache. $global_stats = new Global_Stats(); $total_stats = $global_stats->get_sum_of_optimization_global_stats(); $remaining_count = $global_stats->get_remaining_count(); $redo_count = $global_stats->get_redo_count(); $optimize_count = $global_stats->get_optimize_list()->get_count(); WP_CLI::log( $this->get_pending_bulk_smush_content( $remaining_count, $redo_count, $optimize_count ) ); $global_stats = array( esc_html__( 'Total Savings', 'wp-smushit' ) => $total_stats->get_human_bytes(), esc_html__( 'Savings Percent(%)', 'wp-smushit' ) => $total_stats->get_percent(), esc_html__( 'Images Smushed', 'wp-smushit' ) => $global_stats->get_optimized_images_count(), esc_html__( 'Optimized Percent(%)', 'wp-smushit' ) => $global_stats->get_percent_optimized(), esc_html__( 'Unsmushed Count', 'wp-smushit' ) => $optimize_count, esc_html__( 'Resmush Count', 'wp-smushit' ) => $redo_count, ); WP_CLI\Utils\format_items( 'table', array( $global_stats ), array_keys( $global_stats ) ); } private function update_progress( $progress, $new_processed_items ) { if ( $new_processed_items < 1 ) { return; } for ( $i = 0; $i < $new_processed_items; $i ++ ) { $progress->tick(); } } private function get_pending_bulk_smush_content( $remaining_count, $reoptimize_count, $optimize_count ) { if ( $remaining_count < 1 ) { return esc_html__( 'Yay! All images are optimized as per your current settings.', 'wp-smushit' ); } $optimize_message = ''; if ( 0 < $optimize_count ) { $optimize_message = sprintf( esc_html( /* translators: %d - number of attachments. */ _n( 'Found %d attachment that needs smushing', 'Found %d attachments that need smushing', $optimize_count, 'wp-smushit' ) ), absint( $optimize_count ) ); } $reoptimize_message = ''; if ( 0 < $reoptimize_count ) { $reoptimize_message = sprintf( esc_html( /* translators: %d - number of attachments. */ _n( 'Found %d attachment that needs re-smushing', 'Found %d attachments that need re-smushing', $reoptimize_count, 'wp-smushit' ) ), absint( $reoptimize_count ) ); } $bulk_smush_suggestion = ''; if ( $remaining_count ) { $bulk_smush_suggestion = __( 'Run "wp smush compress" to smush all images.', 'wp-smushit' ); } return sprintf( /* translators: 1. unsmushed images message, 2. 'and' text for when having both unsmushed and re-smush images, 3. re-smush images message. */ __( 'You have %1$s%2$s%3$s. %4$s', 'wp-smushit' ), $optimize_message, ( $optimize_message && $reoptimize_message ? esc_html__( ', and ', 'wp-smushit' ) : '' ), $reoptimize_message, $bulk_smush_suggestion ); } } class-activity-log-controller.php 0000644 00000025041 15252476777 0013204 0 ustar 00 <?php namespace Smush\Core; use Smush\Core\Bulk\Background_Bulk_Smush_Controller; use Smush\Core\Helper; use Smush\Core\Media_Library\Background_Media_Library_Scanner; use Smush\Core\Threads\JSON_Object_Array; use Smush\Core\Modules\Helpers\WhiteLabel; class Activity_Log_Controller extends Controller { /** * Notification data key. * * @var string */ private static $notification_data_key = 'wp_smush_notifications'; /** * Maximum number of notifications. * * @var int */ private static $max_notification = 50; public static function get_max_notifications() { return self::$max_notification; } /** * @var Media_Library_Scan_Background_Process */ protected $scan_background_process; /** * @var Bulk_Smush_Background_Process */ protected $bulk_background_process; /** * @var String_Utils */ protected $string_utils; /** * @var JSON_Object_Array */ private $object_array; /** * Activity_Log_Controller instance. * * @var mixed */ private static $instance; /** * @var WhiteLabel */ private $whitelabel; public static function get_instance() { if ( ! isset( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } public function __construct() { $this->string_utils = new String_Utils(); $this->object_array = new JSON_Object_Array( self::$notification_data_key ); $this->whitelabel = new WhiteLabel(); $this->scan_background_process = Background_Media_Library_Scanner::get_instance()->get_background_process(); $this->bulk_background_process = Background_Bulk_Smush_Controller::get_instance()->get_background_process(); $this->register_filter( 'wp_smush_localize_ui_script_data', array( $this, 'localized_data_for_ui' ) ); $this->register_action( 'wp_ajax_smush_add_notification', array( $this, 'ajax_add_notification' ) ); $this->register_action( 'wp_ajax_smush_get_notifications', array( $this, 'ajax_get_notifications' ) ); $log_priority = 100; // $this->register_action( 'wp_smush_config_applied', array( $this, 'log_config_applied' ), $log_priority ); // Scan. $identifier = $this->scan_background_process->get_identifier(); $scan_dead_action = "{$identifier}_dead"; /** * Track early to ensure we capture the scan completed timing correctly, * since Bulk Smush can be auto-started when a scan is completed. */ $log_scan_completed_priority = 5; $this->register_action( "{$identifier}_completed", array( $this, 'log_scan_completed' ), $log_scan_completed_priority ); $this->register_action( $scan_dead_action, array( $this, 'log_scan_process_death' ), $log_priority ); // Bulk Smush. $identifier = $this->bulk_background_process->get_identifier(); $bulkd_smush_dead_action = "{$identifier}_dead"; $this->register_action( 'wp_smush_bulk_smush_completed', array( $this, 'log_bulk_smush_completed' ), $log_priority ); $this->register_action( $bulkd_smush_dead_action, array( $this, 'log_bulk_smush_process_death' ), $log_priority ); $this->register_action( 'wp_smush_cdn_activated', array( $this, 'log_cdn_activated' ), $log_priority ); } /** * Localize data for the UI. * * @param array $data * @return array */ public function localized_data_for_ui( $data ) { $data['activityLog'] = array( 'notifications' => $this->get_notifications_for_ui(), ); return $data; } /** * AJAX handler for adding a notification from the UI. * * @return void */ public function ajax_add_notification() { check_ajax_referer( 'wp-smush-ajax' ); if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'wp-smushit' ) ), 403 ); } $raw = isset( $_POST['notification'] ) ? wp_unslash( $_POST['notification'] ) : ''; $notification = is_string( $raw ) ? json_decode( $raw, true ) : $raw; if ( empty( $notification ) || ! is_array( $notification ) ) { wp_send_json_error( array( 'message' => __( 'Invalid notification data.', 'wp-smushit' ) ) ); } $success = $this->add_notification( $notification ); if ( ! $success ) { wp_send_json_error( array( 'message' => __( 'Failed to save notification.', 'wp-smushit' ) ) ); } wp_send_json_success(); } /** * AJAX handler for fetching the latest notifications for the UI. * Called after a long-running background process completes or dies on-page, * so the frontend can replace optimistic entries with server-written ones. * * @return void */ public function ajax_get_notifications() { check_ajax_referer( 'wp-smush-ajax' ); if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'wp-smushit' ) ), 403 ); } wp_send_json_success( array( 'notifications' => $this->get_notifications_for_ui(), ) ); } private function get_notifications_for_ui() { $this->enforce_notification_limit(); $notifications = $this->get_notifications(); if ( empty( $notifications ) ) { $this->add_notification( array( 'module' => 'smush', 'content' => $this->string_utils->get_raw_string( 'Welcome to Smush', $this->whitelabel->replace_branding_terms( __( 'Welcome to Smush', 'wp-smushit' ) ) ), ) ); $notifications = $this->get_notifications(); } return array_map( array( $this, 'sanitize_notification_for_ui' ), $notifications ); } /** * Track the completion of a scan process. * * @return void */ public function log_scan_completed() { $this->add_notification( array( 'module' => 'scan', 'content' => $this->string_utils->get_raw_string( 'Scan completed.', __( 'Scan completed.', 'wp-smushit' ) ), ) ); } /** * Track the death of a scan process. * * @return void */ public function log_scan_process_death() { $this->add_notification( array( 'module' => 'scan', 'content' => $this->string_utils->get_raw_string( 'Scan failed.', __( 'Scan failed.', 'wp-smushit' ) ), ) ); } /** * Track the completion of a bulk smush process. * * @return void */ public function log_bulk_smush_completed() { $this->add_notification( array( 'module' => 'bulk_smush', 'content' => $this->string_utils->get_raw_string( 'Bulk Optimization completed.', __( 'Bulk Optimization completed.', 'wp-smushit' ) ), ) ); } /** * Track the death of a bulk smush process. * * @return void */ public function log_bulk_smush_process_death() { $this->add_notification( array( 'module' => 'bulk_smush', 'content' => $this->string_utils->get_raw_string( 'Bulk Optimization failed.', __( 'Bulk Optimization failed.', 'wp-smushit' ) ), ) ); } /** * Track CDN going fully active after provisioning completes. * * @return void */ public function log_cdn_activated() { $this->add_notification( array( 'module' => 'cdn', 'content' => $this->string_utils->get_raw_string( 'CDN setup complete.', __( 'CDN setup complete.', 'wp-smushit' ) ), ) ); } /** * Get the notifications. * * Reads directly from the database (bypasses object cache) to ensure * writes from other background processes are visible. * * @return array */ public function get_notifications() { $notifications = $this->object_array->get( array() ); return is_array( $notifications ) ? $notifications : array(); } /** * Add a notification atomically. * * Uses a single JSON_ARRAY_APPEND database query so concurrent background * processes never overwrite each other's entries. * * @param mixed $notification Notification data. * * @return bool True if the notification was successfully stored, false otherwise. */ public function add_notification( $notification ) { $sanitized_notification = $this->sanitize_notification( $notification ); if ( empty( $sanitized_notification ) ) { return false; } $result = $this->object_array->append( $sanitized_notification ); return $result !== false && $result > 0; } /** * Sanitize a notification for display in the UI. * Translates the notification content. * * @param array $notification Notification data. * * @return array Sanitized notification data. */ private function sanitize_notification_for_ui( $notification ) { $sanitized = $this->sanitize_notification( $notification ); if ( empty( $sanitized['content'] ) ) { return array(); } $sanitized['content'] = $this->string_utils->get_translated_string( $sanitized['content'] ); return $sanitized; } /** * Sanitize a notification. * * @param mixed $notification Notification data. * * @return array{id: string, timestamp: string|int, type: string, content: string, url: string} */ private function sanitize_notification( $notification ) { if ( empty( $notification['content'] ) ) { return array(); } // Sanitize and ensure all expected fields exist. $sanitized = array(); $sanitized['id'] = isset( $notification['id'] ) ? sanitize_text_field( $notification['id'] ) : uniqid( 'smush_notification_' ); $sanitized['timestamp'] = isset( $notification['timestamp'] ) ? sanitize_text_field( $notification['timestamp'] ) : microtime( true ); $sanitized['type'] = isset( $notification['type'] ) ? sanitize_text_field( $notification['type'] ) : 'info'; $sanitized['module'] = isset( $notification['module'] ) ? sanitize_text_field( $notification['module'] ) : ''; $sanitized['content'] = isset( $notification['content'] ) ? sanitize_text_field( $notification['content'] ) : ''; $sanitized['url'] = isset( $notification['url'] ) ? sanitize_text_field( $notification['url'] ) : ''; return $sanitized; } /** * Trim the stored list to the maximum allowed size, keeping the most recent entries. * * Called once per UI read (localize / poll), so background processes can append * atomically without any per-write overhead. The worst-case outcome of deferring * this to read-time is storing a few extra entries between writes. * * @return void */ private function enforce_notification_limit() { $notifications = $this->get_notifications(); if ( count( $notifications ) <= self::$max_notification ) { return; } // Sort newest-first, then keep only the allowed maximum. usort( $notifications, function ( $a, $b ) { return $b['timestamp'] <=> $a['timestamp']; } ); $notifications = array_slice( $notifications, 0, self::$max_notification ); // Overwrite the option with the trimmed, sorted list. // Uses replace_array() so the value is stored as JSON, keeping it // consistent with the JSON-based read in get_notifications(). $this->object_array->unsafe_replace( $notifications ); } } product-analytics/class-product-analytics.php 0000644 00000020535 15252476777 0015525 0 ustar 00 <?php namespace Smush\Core\Product_Analytics; use Smush\Core\Array_Utils; use Smush\Core\Format_Utils; use Smush\Core\Helper; use Smush\Core\Membership\Membership; use Smush\Core\Server_Utils; use Smush\Core\Settings; use Smush\Core\Threads\JSON_Record; use Smush\Core\Time_Utils; use Smush\Core\Url_Utils; use WPMUDEV_Analytics; use WPMUDEV_Analytics_V4; class Product_Analytics { private static $project_token = '5d545622e3a040aca63f2089b0e6cae7'; private static $event_data_option_id = 'wp_smush_event_data'; private static $event_count_key = 'wp_smush_event_count_%s'; /** * @var WPMUDEV_Analytics */ private $analytics; /** * @var Server_Utils */ private $server_utils; /** * Static instance * * @var self */ private static $instance; /** * @var Format_Utils */ private $format_utils; /** * @var Settings|null */ private $settings; /** * @var Array_Utils */ private $array_utils; /** * @var Time_Utils */ private $time_utils; /** * @var Membership */ private $membership; /** * @var Url_Utils */ private $url_utils; /** * @var JSON_Record */ private $event_data; /** * Static instance getter */ public static function get_instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->server_utils = new Server_Utils(); $this->format_utils = new Format_Utils(); $this->array_utils = new Array_Utils(); $this->time_utils = new Time_Utils(); $this->url_utils = new Url_Utils(); $this->settings = Settings::get_instance(); $this->membership = Membership::get_instance(); $this->event_data = new JSON_Record( self::$event_data_option_id ); } /** * @return WPMUDEV_Analytics */ private function get_analytics() { if ( is_null( $this->analytics ) ) { $this->analytics = $this->prepare_analytics_instance(); } return $this->analytics; } /** * @param $analytics WPMUDEV_Analytics * * @return void */ public function set_analytics( $analytics ) { $this->analytics = $analytics; } private function prepare_analytics_instance() { if ( ! class_exists( 'WPMUDEV_Analytics_V4' ) ) { require_once WP_SMUSH_DIR . 'core/external/wpmudev-analytics/autoload.php'; } $mixpanel = new WPMUDEV_Analytics_V4( 'smush', 'Smush', 55, $this->get_token() ); $mixpanel->identify( $this->get_unique_id() ); $mixpanel->registerAll( $this->get_super_properties() ); return $mixpanel; } public function get_unique_id() { $site_url = home_url(); $has_valid_domain = $this->has_valid_domain( $site_url ); if ( ! $has_valid_domain ) { $site_url = site_url(); $has_valid_domain = $this->has_valid_domain( $site_url ); } return $has_valid_domain ? $this->normalize_url( $site_url ) : ''; } private function get_token() { if ( empty( $this->get_unique_id() ) ) { return ''; } return self::$project_token; } private function has_valid_domain( $url ) { $pattern = '/^(https?:\/\/)?([a-z0-9-]+\.)*[a-z0-9-]+(\.[a-z]{2,})/i'; $is_valid = preg_match( $pattern, $url ); if ( $is_valid ) { return true; } return preg_match( '/^(https?:\/\/)?localhost/i', $url ); } private function normalize_url( $url ) { return $this->url_utils->normalize_url( $url ); } private function get_super_properties() { global $wp_version; $super_properties = array( 'active_theme' => get_stylesheet(), 'locale' => get_locale(), 'mysql_version' => $this->server_utils->get_mysql_version(), 'php_version' => phpversion(), 'plugin' => 'Smush', 'plugin_type' => $this->membership->get_member_value('pro', 'free'), 'plugin_version' => WP_SMUSH_VERSION, 'server_type' => $this->server_utils->get_server_type(), 'memory_limit' => $this->format_utils->convert_to_megabytes( $this->server_utils->get_memory_limit() ), 'max_execution_time' => $this->server_utils->get_max_execution_time(), 'wp_type' => is_multisite() ? 'multisite' : 'single', 'wp_version' => $wp_version, 'device' => $this->server_utils->get_device_type(), 'user_agent' => $this->server_utils->get_user_agent(), 'streams_status' => $this->settings->streaming_enabled() ? 'enabled' : 'disabled', ); return array_merge( $super_properties, $this->get_date_time_properties() ); } private function get_date_time_properties() { $properties = array(); $event_times = get_site_option( 'wp_smush_event_times', array() ); $time_events = array( 'Installation Date' => 'plugin_installed', 'Activation Date' => 'plugin_activated', 'Last Updated' => 'plugin_upgraded', ); foreach ( $time_events as $event_name => $event_key ) { if ( ! empty( $event_times[ $event_key ] ) ) { $properties[ $event_name ] = date( 'c', $event_times[ $event_key ] ); } } return $properties; } public function maybe_track( $event, $properties = array(), $limit_per_day = 0 ) { if ( ! $this->tracking_enabled() ) { return; } if ( $this->event_has_limit( $limit_per_day ) ) { $this->track_with_limit( $event, $properties, $limit_per_day ); } else { $this->track( $event, $properties ); } } private function tracking_enabled() { return (bool) $this->settings->get( 'usage' ); } private function get_event_count_key( $event, $properties ) { if ( method_exists( $this, "get_event_count_key_$event" ) ) { return call_user_func( array( $this, "get_event_count_key_$event" ), $event, $properties ); } else { return sprintf( self::$event_count_key, $event ); } } public function track( $event, $properties = array() ) { $debug_mode = defined( 'WP_SMUSH_MIXPANEL_DEBUG' ) && WP_SMUSH_MIXPANEL_DEBUG; if ( $debug_mode ) { Helper::logger()->track()->info( sprintf( 'Track Event %1$s: %2$s', $event, print_r( $properties, true ) ) ); } else { $this->get_analytics()->track( $event, $properties ); } } public function maybe_track_error( $type, $code, $message, $extra_properties = array() ) { $limit_per_day = 1; $this->maybe_track( 'smush_error_encountered', array_merge( array( 'Error Type' => $type, 'Error Code' => $code, 'Error Message' => $message, ), $extra_properties ), $limit_per_day ); } protected function get_event_count_key_smush_error_encountered( $event, $properties ) { $event_key = $event; $error_type = $this->array_utils->get_array_value( $properties, 'Error Type' ); $error_code = $this->array_utils->get_array_value( $properties, 'Error Code' ); if ( ! empty( $error_type ) && ! empty( $error_code ) ) { $event_key = $error_type . '_' . $error_code; } return sprintf( self::$event_count_key, sanitize_key( $event_key ) ); } private function track_with_limit( $event, $properties, $limit_per_day ) { $event_count_key = $this->get_event_count_key( $event, $properties ); $event_count_timestamp_key = $event_count_key . '_timestamp'; $event_count = (int) $this->event_data->get_value( $event_count_key, 0 ); $event_count_timestamp = (int) $this->event_data->get_value( $event_count_timestamp_key, 0 ); $not_tracked_in_24_hours = $this->time_utils->get_time() - $event_count_timestamp > DAY_IN_SECONDS; if ( $not_tracked_in_24_hours || $event_count < $limit_per_day ) { if ( 'smush_error_encountered' === $event ) { $properties['Total Error Count'] = empty( $event_count_timestamp ) ? 1 : $event_count; } $this->track( $event, $properties ); if ( $not_tracked_in_24_hours ) { // Reset the count if it has been more than 24 hours $this->event_data->set_values( array( $event_count_key => 1, $event_count_timestamp_key => $this->time_utils->get_time(), ) ); } else { $this->event_data->increment_values( array( $event_count_key ) ); } } else { $this->event_data->increment_values( array( $event_count_key ) ); } } /** * @param $limit_per_day * * @return bool */ private function event_has_limit( $limit_per_day ) { return 0 !== (int) $limit_per_day; } public function set_settings( $settings ) { $this->settings = $settings; } /** * @return Time_Utils */ public function get_time_utils() { return $this->time_utils; } /** * Get event_data_option_id. * * @return string */ public static function get_event_data_option_id() { return self::$event_data_option_id; } } product-analytics/class-product-analytics-controller.php 0000644 00000101720 15252476777 0017702 0 ustar 00 <?php namespace Smush\Core\Product_Analytics; use Smush\Core\Array_Utils; use Smush\Core\Background\Background_Pre_Flight_Controller; use Smush\Core\Background\Background_Process; use Smush\Core\Helper; use Smush\Core\Hub_Connector; use Smush\Core\Media\Media_Item_Cache; use Smush\Core\Media\Media_Item_Query; use Smush\Core\Media_Library\Background_Media_Library_Scanner; use Smush\Core\Media_Library\Media_Library_Last_Process; use Smush\Core\Media_Library\Media_Library_Scan_Background_Process; use Smush\Core\Media_Library\Media_Library_Scanner; use Smush\Core\Membership\Membership; use Smush\Core\Settings; use Smush\Core\Stats\Global_Stats; use WP_Smush; class Product_Analytics_Controller { /** * @var Settings */ protected $settings; /** * @var Media_Library_Scan_Background_Process */ protected $scan_background_process; protected $scanner_slice_size; /** * @var Media_Library_Last_Process */ protected $media_library_last_process; /** * @var bool */ protected $scan_background_process_dead = false; /** * @var Product_Analytics */ protected $product_analytics; /** * @var Array_Utils */ protected $array_utils; public function __construct() { $this->settings = Settings::get_instance(); $this->scan_background_process = Background_Media_Library_Scanner::get_instance()->get_background_process(); $this->media_library_last_process = Media_Library_Last_Process::get_instance(); $this->product_analytics = Product_Analytics::get_instance(); $this->array_utils = new Array_Utils(); $this->hook_actions(); } public static function get_instance() { return new self(); } public function __call( $method_name, $arguments ) { _deprecated_function( esc_html( $method_name ), '3.24.0' ); } private function hook_actions() { // Setting events. add_action( 'wp_smush_settings_updated', array( $this, 'track_opt_toggle' ), 10, 2 ); add_action( 'wp_smush_settings_updated', array( $this, 'intercept_settings_update' ), 10, 2 ); add_action( 'wp_smush_settings_deleted', array( $this, 'intercept_reset' ) ); add_action( 'wp_smush_settings_updated', array( $this, 'track_integrations_saved' ), 10, 2 ); add_action( 'wp_ajax_smush_track_deactivate', array( $this, 'ajax_track_deactivation_survey' ) ); add_action( 'wp_ajax_smush_analytics_track_event', array( $this, 'ajax_handle_track_request' ) ); if ( ! $this->is_usage_tracking_enabled() ) { return; } // Other events. add_action( 'wp_smush_directory_smush_start', array( $this, 'track_directory_smush' ) ); add_action( 'wp_smush_bulk_smush_start', array( $this, 'track_bulk_smush_start' ), 20 ); add_action( 'wp_smush_bulk_smush_completed', array( $this, 'track_background_bulk_smush_completed' ) ); add_action( 'wp_smush_bulk_smush_dead', array( $this, 'track_bulk_smush_background_process_death' ) ); add_action( 'wp_smush_config_applied', array( $this, 'track_config_applied' ) ); $identifier = $this->scan_background_process->get_identifier(); $scan_started_action = "{$identifier}_started"; $scan_dead_action = "{$identifier}_dead"; add_action( "{$identifier}_before_start", array( $this, 'record_scan_death' ), 10, 2 ); add_action( $scan_started_action, array( $this, 'track_background_scan_start' ), 10, 2 ); add_action( "{$identifier}_completed", array( $this, 'track_background_scan_end' ), 10, 2 ); add_action( $scan_dead_action, array( $this, 'track_background_scan_process_death' ) ); add_action( 'wp_smush_plugin_activated', array( $this, 'track_plugin_activation' ) ); if ( defined( 'WP_SMUSH_BASENAME' ) ) { $plugin_basename = WP_SMUSH_BASENAME; add_action( "deactivate_$plugin_basename", array( $this, 'track_plugin_deactivation' ) ); } add_action( 'wp_smush_bulk_smush_stuck', array( $this, 'track_bulk_smush_progress_stuck' ) ); add_action( 'wp_smush_lazy_load_updated', array( $this, 'track_lazy_load_settings_updated' ), 10, 2 ); add_action( 'wp_smush_bulk_restore_completed', array( $this, 'track_bulk_restore_completed' ) ); } protected function is_usage_tracking_enabled() { return $this->settings->get( 'usage' ); } protected function track( $event, $properties = array() ) { $this->product_analytics->track( $event, $properties ); } public function intercept_settings_update( $old_settings, $settings ) { if ( empty( $settings['usage'] ) ) { // Use the most up-to-data value of 'usage' return; } $settings = $this->remove_unchanged_settings( $old_settings, $settings ); $handled = $this->maybe_track_feature_toggle( $settings ); if ( ! $handled ) { $this->maybe_track_cdn_update( $settings ); } } private function maybe_track_feature_toggle( $settings ) { $has_tracked = false; foreach ( $settings as $setting_key => $setting_value ) { $handler = "track_{$setting_key}_feature_toggle"; if ( method_exists( $this, $handler ) ) { call_user_func( array( $this, $handler ), $setting_value ); $has_tracked = true; } } return $has_tracked; } protected function remove_unchanged_settings( $old_settings, $settings ) { $default_settings = $this->settings->get_defaults(); $not_null_callback = function ( $value ) { return ! is_null( $value ); }; $old_settings = array_filter( $old_settings, $not_null_callback ); $old_settings = array_merge( $default_settings, $old_settings ); $settings = array_filter( $settings, $not_null_callback ); $settings = array_merge( $default_settings, $settings ); $changed = array(); foreach ( $settings as $setting_key => $setting_value ) { $old_setting_value = isset( $old_settings[ $setting_key ] ) ? $old_settings[ $setting_key ] : ''; if ( $old_setting_value !== $setting_value ) { $changed[ $setting_key ] = $setting_value; } } return $changed; } public function get_bulk_properties() { $bulk_property_labels = array( 'auto' => 'Automatic Compression', 'strip_exif' => 'Metadata', 'resize' => 'Resize Original Images', 'original' => 'Compress original images', 'backup' => 'Backup original images', 'png_to_jpg' => 'Auto-convert PNGs to JPEGs (lossy)', 'no_scale' => 'Disable scaled images', 'background_email' => 'Email notification', ); $core = WP_Smush::get_instance()->core(); $sizes = $core->image_dimensions(); $image_sizes = Settings::get_instance()->get_setting( 'wp-smush-image_sizes' ); $all_selected = ! is_array( $image_sizes ) || count( $image_sizes ) === count( $sizes ); $bulk_properties = array( 'Image Sizes' => $all_selected ? 'All' : 'Custom', 'Mode' => $this->get_current_lossy_level_label(), 'Parallel Processing' => $this->get_parallel_processing_status(), 'Smush Type' => $this->get_smush_type(), ); foreach ( $bulk_property_labels as $bulk_setting => $bulk_property_label ) { $property_value = Settings::get_instance()->get( $bulk_setting ) ? 'Enabled' : 'Disabled'; $bulk_properties[ $bulk_property_label ] = $property_value; } return $bulk_properties; } private function get_parallel_processing_status() { return defined( 'WP_SMUSH_PARALLEL' ) && WP_SMUSH_PARALLEL ? 'Enabled' : 'Disabled'; } protected function get_smush_type() { if ( $this->settings->is_webp_module_active() ) { return 'WebP'; } if ( $this->settings->is_avif_module_active() ) { return 'AVIF'; } return 'Classic'; } protected function get_current_lossy_level_label() { $lossy_level = $this->settings->get_lossy_level_setting(); $smush_modes = array( Settings::get_level_lossless() => 'Basic', Settings::get_level_super_lossy() => 'Super', Settings::get_level_ultra_lossy() => 'Ultra', ); if ( ! isset( $smush_modes[ $lossy_level ] ) ) { $lossy_level = Settings::get_level_lossless(); } return $smush_modes[ $lossy_level ]; } private function track_lazy_load_feature_toggle( $setting_value ) { $this->track_lazy_load_feature_updated_on_toggle( $setting_value ); return $this->track_feature_toggle( $setting_value, 'Lazy Load' ); } private function track_lazy_load_feature_updated_on_toggle( $activate ) { $this->track_lazy_load_updated( array( 'update_type' => $activate ? 'activate' : 'deactivate', 'modified_settings' => 'na', ), $this->settings->get_setting( 'wp-smush-lazy_load', array() ) ); } protected function track_feature_toggle( $active, $feature ) { $event = $active ? 'Feature Activated' : 'Feature Deactivated'; $this->track( $event, array( 'Feature' => $feature, 'Triggered From' => $this->identify_referrer(), ) ); return true; } protected function identify_referrer() { $page = $this->get_referer_page(); $triggered_from = array( 'smush' => 'Dashboard', 'smush-lazy-preload' => 'Lazy Load', 'lazyload' => 'Lazy Load', 'preload' => 'Preload', 'smush-cdn' => 'CDN', 'smush-directory' => 'Directory Smush', 'smush-settings' => 'Tools', 'tools' => 'Tools', 'nextgen' => 'Next-Gen Formats', 'integrations' => 'Integrations', 'config' => 'Configs', 'other-settings' => 'Other Settings', ); return empty( $triggered_from[ $page ] ) ? '' : $triggered_from[ $page ]; } protected function maybe_track_cdn_update( $settings ) { return false; } public function track_directory_smush() { $this->track( 'Directory Smushed' ); } public function track_bulk_smush_start() { $properties = $this->get_bulk_properties(); $properties = array_merge( $properties, array( 'process_id' => $this->get_process_id(), 'Background Optimization' => $this->get_background_optimization_status(), 'Cron' => $this->get_cron_healthy_status(), ) ); $this->track( 'Bulk Smush Started', $properties ); } /** * Track the event on background optimization completed. * Note: For ajax Bulk Smush, we will track it via js. * * @return void */ public function track_background_bulk_smush_completed() { $bg_optimization = WP_Smush::get_instance()->core()->mod->bg_optimization; $total_items = $bg_optimization->get_total_items(); $failed_items = $bg_optimization->get_failed_items(); $failure_percentage = $total_items > 0 ? round( $failed_items * 100 / $total_items ) : 0; $properties = array_merge( $this->get_bulk_smush_stats(), array( 'Total Enqueued Images' => $total_items, 'Failure Percentage' => $failure_percentage, ) ); $properties = $this->filter_bulk_smush_completed_properties( $properties ); $this->track( 'Bulk Smush Completed', $properties ); } private function get_bulk_smush_stats() { $global_stats = WP_Smush::get_instance()->core()->get_global_stats(); return array( 'Total Savings' => $this->convert_to_megabytes( (int) $this->array_utils->get_array_value( $global_stats, 'savings_bytes' ) ), 'Total Images' => (int) $this->array_utils->get_array_value( $global_stats, 'count_images' ), 'Media Optimization Percentage' => (float) $this->array_utils->get_array_value( $global_stats, 'percent_optimized' ), 'Percentage of Savings' => (float) $this->array_utils->get_array_value( $global_stats, 'savings_percent' ), 'Images Resized' => (int) $this->array_utils->get_array_value( $global_stats, 'count_resize' ), 'Resize Savings' => $this->convert_to_megabytes( (int) $this->array_utils->get_array_value( $global_stats, 'savings_resize' ) ), ); } public function track_bulk_smush_background_process_death() { $this->track( 'Background Process Dead', array_merge( array( 'Process Type' => 'Smush', 'Slice Size' => 0, 'Time Elapsed' => $this->media_library_last_process->get_process_elapsed_time(), 'Smush Type' => $this->get_smush_type(), 'Mode' => $this->get_current_lossy_level_label(), ), $this->get_bulk_background_process_properties() ) ); } protected function get_bulk_background_process_properties() { $bg_optimization = WP_Smush::get_instance()->core()->mod->bg_optimization; $process_id = $this->get_process_id(); if ( ! $bg_optimization->is_background_enabled() ) { return array( 'process_id' => $process_id, ); } $total_items = $bg_optimization->get_total_items(); $processed_items = $bg_optimization->get_processed_items(); return array( 'process_id' => $process_id, 'Retry Attempts' => $bg_optimization->get_revival_count(), 'Total Enqueued Images' => $total_items, 'Completion Percentage' => $this->get_background_process_completion_percentage( $total_items, $processed_items ), 'Total Processed Images' => $processed_items, ); } protected function get_process_id() { return md5( $this->media_library_last_process->get_process_start_time() ); } /** * Add extra properties to the bulk smush completed event for Bulk Smush include ajax method. * * @param array $properties Bulk Smush completed properties. */ protected function filter_bulk_smush_completed_properties( $properties ) { return array_merge( $properties, array( 'process_id' => $this->get_process_id(), 'Background Optimization' => $this->get_background_optimization_status(), 'Cron' => $this->get_cron_healthy_status(), 'Time Elapsed' => $this->media_library_last_process->get_process_elapsed_time(), 'Smush Type' => $this->get_smush_type(), 'Mode' => $this->get_current_lossy_level_label(), ) ); } public function track_config_applied( $config_name ) { $properties = $config_name ? array( 'Config Name' => $config_name ) : array(); $properties['Triggered From'] = $this->identify_referrer(); $this->track( 'Config Applied', $properties ); } public function track_opt_toggle( $old_settings, $settings ) { $settings = $this->remove_unchanged_settings( $old_settings, $settings ); if ( isset( $settings['usage'] ) ) { $location = $this->identify_referrer(); $location = 'Dashboard' === $location ? 'Share Usage Notice' : $location; $this->track( $settings['usage'] ? 'Opt In' : 'Opt Out', array( 'Location' => $location, 'active_plugins' => $this->get_active_plugins(), ) ); } } public function track_integrations_saved( $old_settings, $settings ) { if ( empty( $settings['usage'] ) ) { return; } $settings = $this->remove_unchanged_settings( $old_settings, $settings ); if ( empty( $settings ) ) { return; } $this->maybe_track_integrations_toggle( $settings ); } private function maybe_track_integrations_toggle( $settings ) { $integrations = array( 'gutenberg' => 'Gutenberg', 'gform' => 'Gravity Forms', 'js_builder' => 'WP Bakery', 's3' => 'Amazon S3', ); foreach ( $settings as $integration_slug => $is_activated ) { if ( ! array_key_exists( $integration_slug, $integrations ) ) { continue; } if ( $is_activated ) { $this->track( 'Integration Activated', array( 'Integration' => $integrations[ $integration_slug ], ) ); } else { $this->track( 'Integration Deactivated', array( 'Integration' => $integrations[ $integration_slug ], ) ); } } } public function intercept_reset() { if ( $this->is_usage_tracking_enabled() ) { $this->track( 'Opt Out', array( 'Location' => 'reset', 'active_plugins' => $this->get_active_plugins(), ) ); } } public function record_scan_death() { $this->scan_background_process_dead = $this->scan_background_process->get_status()->is_dead(); } public function track_background_scan_start( $identifier, $background_process ) { $type = $this->scan_background_process_dead ? 'Retry' : 'New'; $this->_track_background_scan_start( $type, $background_process ); } private function _track_background_scan_start( $type, $background_process ) { $properties = array( 'Scan Type' => $type, ); $this->track( 'Scan Started', array_merge( $properties, $this->get_bulk_properties(), $this->get_scan_properties() ) ); } /** * @param $identifier * @param $background_process Background_Process * * @return void */ public function track_background_scan_end( $identifier, $background_process ) { $properties = array( 'Retry Attempts' => $background_process->get_revival_count(), 'Time Elapsed' => $this->media_library_last_process->get_process_elapsed_time(), ); $this->track( 'Scan Ended', array_merge( $properties, $this->get_bulk_properties(), $this->get_scan_properties() ) ); } public function track_background_scan_process_death() { $this->track( 'Background Process Dead', array_merge( array( 'Process Type' => 'Scan', 'Slice Size' => $this->get_scanner_slice_size(), 'Time Elapsed' => $this->media_library_last_process->get_process_elapsed_time(), 'Smush Type' => $this->get_smush_type(), 'Mode' => $this->get_current_lossy_level_label(), ), $this->get_scan_background_process_properties() ) ); } private function get_scan_properties() { $global_stats = Global_Stats::get(); $global_stats_array = $global_stats->to_array(); $properties = array( 'process_id' => $this->get_process_id(), 'Slice Size' => $this->get_scanner_slice_size(), ); $labels = array( 'image_attachment_count' => 'Image Attachment Count', 'optimized_images_count' => 'Optimized Images Count', 'optimize_count' => 'Optimize Count', 'reoptimize_count' => 'Reoptimize Count', 'ignore_count' => 'Ignore Count', 'animated_count' => 'Animated Count', 'error_count' => 'Error Count', 'percent_optimized' => 'Percent Optimized', 'size_before' => 'Size Before', 'size_after' => 'Size After', 'savings_percent' => 'Savings Percent', ); $savings_keys = array( 'size_before', 'size_after', ); foreach ( $labels as $key => $label ) { if ( isset( $global_stats_array[ $key ] ) ) { $properties[ $label ] = $global_stats_array[ $key ]; if ( in_array( $key, $savings_keys, true ) ) { $properties[ $label ] = $this->convert_to_megabytes( $properties[ $label ] ); } } } return $properties; } protected function get_scan_background_process_properties() { $query = new Media_Item_Query(); $total_enqueued_images = $query->get_image_attachment_count(); $total_items = $this->scan_background_process->get_status()->get_total_items(); $processed_items = $this->scan_background_process->get_status()->get_processed_items(); $scanner_slice_size = $this->get_scanner_slice_size(); $total_processed_images = $processed_items * $scanner_slice_size; $total_processed_images = min( $total_processed_images, $total_enqueued_images ); return array( 'process_id' => $this->get_process_id(), 'Retry Attempts' => $this->scan_background_process->get_revival_count(), 'Total Enqueued Images' => $total_enqueued_images, 'Completion Percentage' => $this->get_background_process_completion_percentage( $total_items, $processed_items ), 'Total Processed Images' => $total_processed_images, ); } protected function get_background_process_completion_percentage( $total_items, $processed_items ) { if ( $total_items < 1 ) { return 0; } return ceil( $processed_items * 100 / $total_items ); } protected function convert_to_megabytes( $size_in_bytes ) { if ( empty( $size_in_bytes ) ) { return 0; } $unit_mb = pow( 1024, 2 ); return round( $size_in_bytes / $unit_mb, 2 ); } protected function get_scanner_slice_size() { if ( is_null( $this->scanner_slice_size ) ) { $this->scanner_slice_size = ( new Media_Library_Scanner() )->get_slice_size(); } return $this->scanner_slice_size; } protected function get_referer_page() { $referer = wp_get_referer(); if ( empty( $referer ) ) { $referer = $_SERVER['REQUEST_URI']; } $query = wp_parse_url( $referer, PHP_URL_QUERY ); if ( empty( $query ) ) { return ''; } $query_vars = array(); parse_str( $query, $query_vars ); $page = empty( $query_vars['page'] ) ? '' : sanitize_key( $query_vars['page'] ); $page_has_tabs = in_array( $page, array( 'smush-settings', 'smush-lazy-preload' ), true ); if ( $page_has_tabs && isset( $query_vars['view'] ) ) { $page = sanitize_key( $query_vars['view'] ); } return $page; } public function track_plugin_activation() { $this->track( 'Opt In', array( 'Location' => 'reactivate', 'active_plugins' => $this->get_active_plugins(), ) ); } public function track_plugin_deactivation() { $location = $this->get_deactivation_location(); $this->track( 'Opt Out', array( 'Location' => $location, 'active_plugins' => $this->get_active_plugins(), ) ); } private function get_deactivation_location() { $is_hub_request = ! empty( $_REQUEST['wpmudev-hub'] ); if ( $is_hub_request ) { return 'deactivate_hub'; } $is_dashboard_request = wp_doing_ajax() && ! empty( $_REQUEST['action'] ) && 'wdp-project-deactivate' === wp_unslash( $_REQUEST['action'] ); if ( $is_dashboard_request ) { return 'deactivate_dashboard'; } return 'deactivate_pluginlist'; } private function get_active_plugins() { $active_plugins = array(); $active_plugin_files = $this->get_active_and_valid_plugin_files(); foreach ( $active_plugin_files as $plugin_file ) { $plugin_name = $this->get_plugin_name( $plugin_file ); if ( $plugin_name ) { $active_plugins[] = $plugin_name; } } return $active_plugins; } private function get_active_and_valid_plugin_files() { $active_plugins = is_multisite() ? wp_get_active_network_plugins() : array(); $active_plugins = array_merge( $active_plugins, wp_get_active_and_valid_plugins() ); return array_unique( $active_plugins ); } private function get_plugin_name( $plugin_file ) { $plugin_data = get_plugin_data( $plugin_file ); return ! empty( $plugin_data['Name'] ) ? $plugin_data['Name'] : ''; } private function get_cron_healthy_status() { $is_cron_healthy = Background_Pre_Flight_Controller::get_instance()->is_cron_healthy(); return $is_cron_healthy ? 'Enabled' : 'Disabled'; } protected function get_background_optimization_status() { return 'Disabled'; } public function ajax_handle_track_request() { $event_name = $this->get_event_name(); if ( ! check_ajax_referer( 'wp-smush-ajax' ) || ! Helper::is_user_allowed() || empty( $event_name ) ) { wp_send_json_error(); } $properties = $this->get_event_properties( $event_name ); if ( ! $this->allow_to_track( $event_name, $properties ) ) { wp_send_json_error(); } $this->track( $event_name, $properties ); wp_send_json_success(); } private function allow_to_track( $event_name, $properties ) { $trackable_events = array( 'smush_pro_upsell' => isset( $properties['Location'] ) && 'wizard' === $properties['Location'], 'Disconnect Site' => true, ); $is_trackable_event = ! empty( $trackable_events[ $event_name ] ); return $is_trackable_event || $this->is_usage_tracking_enabled(); } private function get_event_name() { return isset( $_POST['event'] ) ? sanitize_text_field( wp_unslash( $_POST['event'] ) ) : ''; } private function get_event_properties( $event_name ) { $properties = isset( $_POST['properties'] ) && is_array( $_POST['properties'] ) ? wp_unslash( $_POST['properties'] ) : array(); $properties = map_deep( $properties, 'sanitize_text_field' ); $filter_callback = $this->get_filter_properties_callback( $event_name ); if ( method_exists( $this, $filter_callback ) ) { $properties = call_user_func( array( $this, $filter_callback ), $properties ); } return $properties; } private function get_filter_properties_callback( $event_name ) { $event_name = str_replace( ' ', '_', $event_name ); $event_name = sanitize_key( $event_name ); return "filter_{$event_name}_properties"; } /** * Filter properties for Scan Interrupted event. * * @param array $properties JS properties. */ protected function filter_scan_interrupted_properties( $properties ) { return array_merge( $properties, array( 'Slice Size' => $this->get_scanner_slice_size(), 'Background Optimization' => $this->get_background_optimization_status(), 'Cron' => $this->get_cron_healthy_status(), 'Time Elapsed' => $this->media_library_last_process->get_process_elapsed_time(), 'Smush Type' => $this->get_smush_type(), 'Mode' => $this->get_current_lossy_level_label(), 'WP Loopback Status' => $this->get_wp_loopback_status( $properties ), ), $this->get_scan_background_process_properties(), $this->get_last_image_process_properties() ); } private function get_last_image_process_properties() { $last_image_id = $this->media_library_last_process->get_last_process_attachment_id(); if ( ! $last_image_id ) { return array(); } $media_item = Media_Item_Cache::get_instance()->get( $last_image_id ); $last_image_time_elapsed = $this->media_library_last_process->get_last_process_attachment_elapsed_time(); $properties = array( 'Last Image Time Elapsed' => $last_image_time_elapsed, ); if ( ! $media_item->is_valid() ) { return $properties; } $full_size = $media_item->get_full_or_scaled_size(); if ( ! $full_size ) { return $properties; } $file_size = $this->convert_to_megabytes( $full_size->get_filesize() ); $image_width = $full_size->get_width(); $image_height = $full_size->get_height(); $image_type = strtoupper( $full_size->get_extension() ); return array( 'Last Image Time Elapsed' => $last_image_time_elapsed, 'Last Image Size' => $file_size, 'Last Image Width' => $image_width, 'Last Image Height' => $image_height, 'Last Image Type' => $image_type, ); } /** * Filter properties for Bulk Smush interrupted event. * * @param array $properties JS properties. */ protected function filter_bulk_smush_interrupted_properties( $properties ) { return array_merge( $properties, array( 'Background Optimization' => $this->get_background_optimization_status(), 'Cron' => $this->get_cron_healthy_status(), 'Parallel Processing' => $this->get_parallel_processing_status(), 'Time Elapsed' => $this->media_library_last_process->get_process_elapsed_time(), 'Smush Type' => $this->get_smush_type(), 'Mode' => $this->get_current_lossy_level_label(), 'WP Loopback Status' => $this->get_wp_loopback_status( $properties ), ), $this->get_bulk_background_process_properties(), $this->get_last_image_process_properties() ); } public function ajax_track_deactivation_survey() { $event_name = $this->get_event_name(); if ( ! check_ajax_referer( 'wp-smush-ajax' ) || ! Helper::is_user_allowed() || empty( $event_name ) ) { wp_send_json_error(); } $properties = $this->get_event_properties( $event_name ); $properties = array_merge( $properties, array( 'active_features' => $this->get_active_features(), 'active_plugins' => $this->get_active_plugins(), 'Smush API Connection' => $this->get_api_connection_status(), ) ); $this->track( $event_name, $properties ); wp_send_json_success(); } private function get_api_connection_status() { if ( Hub_Connector::is_logged_in() ) { return 'connected'; } if ( Membership::get_instance()->is_api_hub_access_required() ) { return 'disconnected'; } return 'na'; } private function get_active_features() { $lossy_level = $this->settings->get_lossy_level_setting(); $features = array( 'lazy_load' => $this->settings->is_lazyload_active(), 'smush_basic' => Settings::get_level_lossless() === $lossy_level, 'smush_super' => Settings::get_level_super_lossy() === $lossy_level, 'wp_bakery' => $this->settings->get( 'js_builder' ), 'gravity_forms' => $this->settings->get( 'gform' ), 'gutenberg_blocks' => $this->settings->get( 'gutenberg' ), ); // Merge in pro features. $features = array_merge( $features, $this->get_active_pro_features() ); return array_keys( array_filter( $features ) ); } protected function get_active_pro_features() { return array(); } private function get_wp_loopback_status( $properties ) { $is_loopback_error = ! empty( $properties['Trigger'] ) && 'loopback_error' === $properties['Trigger']; if ( $is_loopback_error ) { $loopback_status = Helper::loopback_supported() ? 'Pass' : 'Fail'; } else { $loopback_status = 'na'; } return $loopback_status; } public function track_bulk_smush_progress_stuck() { $properties = array( 'Trigger' => 'stuck_notice', 'Modal Action' => 'na', 'Troubleshoot' => 'na', ); $properties = $this->filter_bulk_smush_interrupted_properties( $properties ); $this->track( 'Bulk Smush Interrupted', $properties ); } public function track_lazy_load_settings_updated( $old_settings, $settings ) { $changed_settings = $this->remove_unchanged_settings( (array) $old_settings, (array) $settings ); $modified_settings = 'na'; if ( ! empty( $changed_settings ) ) { $modified_settings_map = array( 'format' => 'media_type', 'output' => 'output_location', 'animation' => 'display_animation', 'include' => 'include_exclude_posttype', 'exclude-pages' => 'include_exclude_url', 'exclude-classes' => 'include_exclude_keyword', 'footer' => 'script_method', 'native' => 'native_lazyload', 'noscript_fallback' => 'noscript', ); $modified_settings = array_intersect_key( $modified_settings_map, $changed_settings ); $modified_settings = ! empty( $modified_settings ) ? array_values( $modified_settings ) : 'na'; } $this->track_lazy_load_updated( array( 'update_type' => 'modify', 'modified_settings' => $modified_settings, ), $settings ); } protected function track_lazy_load_updated( $properties, $settings ) { $exclusion_enabled = $this->is_lazy_load_exclusion_enabled( $settings ); $native_lazyload_enabled = ! empty( $settings['native'] ); $noscript_fallback_enabled = ! empty( $settings['noscript_fallback'] ); $embed_content = empty( $settings['format']['iframe'] ) ? 'Disabled' : ( empty( $settings['format']['embed_video'] ) ? 'Enabled' : 'Preview Images' ); $properties = array_merge( array( 'Location' => $this->identify_referrer(), 'embed_content' => $embed_content, 'exclusions' => $exclusion_enabled ? 'Enabled' : 'Disabled', 'native_lazy_status' => $native_lazyload_enabled ? 'Enabled' : 'Disabled', 'noscript_status' => $noscript_fallback_enabled ? 'Enabled' : 'Disabled', 'auto_resizing_status' => $this->settings->get( 'auto_resizing' ) ? 'Enabled' : 'Disabled', 'image_dimensions_status' => $this->settings->get( 'image_dimensions' ) ? 'Enabled' : 'Disabled', ), $properties ); $this->track( 'lazy_load_updated_new', $properties ); } private function is_lazy_load_exclusion_enabled( $settings ) { if ( ! empty( $settings['exclude-pages'] ) || ! empty( $settings['exclude-classes'] ) ) { return true; } if ( empty( $settings['include'] ) || ! is_array( $settings['include'] ) ) { return false; } $included_post_types = $settings['include']; // By default, we activated for all post types, so this option is changed when any post type is unchecked. return in_array( false, $included_post_types, true ); } /** * Track the completion of a bulk restore process. * * @param array $args Restore arguments. */ public function track_bulk_restore_completed( $args ) { $this->track( 'Bulk Restore Triggered', $this->filter_bulk_restore_triggered_properties( array( 'Type' => 'Bulk', 'Total images restored' => (int) $this->array_utils->get_array_value( $args, 'restored_count', 0 ), 'Total images' => (int) $this->array_utils->get_array_value( $args, 'total_count', 0 ), 'Backup not found' => (int) $this->array_utils->get_array_value( $args, 'missing_backup_count', 0 ), ) ) ); } /** * Filter the properties for the bulk restore triggered event. * * @param mixed $properties Properties. * * @return array */ public function filter_bulk_restore_triggered_properties( $properties ) { return array_merge( $properties, array( 'Backup Status' => $this->settings->is_backup_active() ? 'Enabled' : 'Disabled', ) ); } protected function is_syncing_settings() { return wp_doing_ajax() && ! empty( $_REQUEST['action'] ) && 'smush_sync_settings' === wp_unslash( $_REQUEST['action'] ); } } product-analytics/class-product-analytics-controller-pro.php 0000644 00000026642 15252476777 0020511 0 ustar 00 <?php namespace Smush\Core\Product_Analytics; use Smush\Core\CDN\CDN_Helper; use Smush\Core\Next_Gen\Next_Gen_Manager; use Smush\Core\Settings; use Smush\Core\Stats\Global_Stats; use Smush\Core\Webp\Webp_Configuration; use WP_Smush; class Product_Analytics_Controller_Pro extends Product_Analytics_Controller { /** * @var Next_Gen_Manager */ private $next_gen_manager; public function __construct() { parent::__construct(); $this->next_gen_manager = Next_Gen_Manager::get_instance(); add_action( 'wp_smush_settings_updated', array( $this, 'track_toggle_next_gen_fallback' ), 10, 2 ); if ( $this->is_usage_tracking_enabled() ) { add_action( 'wp_smush_webp_method_changed', array( $this, 'track_webp_method_changed' ) ); add_action( 'wp_smush_webp_status_changed', array( $this, 'track_next_gen_status_changed' ) ); add_action( 'wp_smush_avif_status_changed', array( $this, 'track_next_gen_status_changed' ) ); add_action( 'wp_smush_after_delete_all_webp_files', array( $this, 'track_deleting_all_next_gen_files', ) ); add_action( 'wp_smush_after_delete_all_avif_files', array( $this, 'track_deleting_all_next_gen_files', ) ); add_action( 'shutdown', array( $this, 'maybe_track_next_gen_format_changed' ) ); } } protected function track_webp_mod_feature_toggle( $setting_value ) { if ( $this->is_switching_next_gen_format() ) { return; } return $this->track_feature_toggle( $setting_value, 'Next-Gen' ); } protected function track_avif_mod_feature_toggle( $setting_value ) { if ( $this->is_switching_next_gen_format() ) { return; } return $this->track_feature_toggle( $setting_value, 'Next-Gen' ); } protected function is_switching_next_gen_format() { return did_action( 'wp_smush_next_gen_before_format_switch' ); } protected function track_cdn_feature_toggle( $setting_value ) { return $this->track_feature_toggle( $setting_value, 'CDN' ); } protected function track_preload_images_feature_toggle( $setting_value ) { return $this->track_feature_toggle( $setting_value, 'Preload Critical Images' ); } public function track_deleting_all_next_gen_files() { $auto_deleting_old_next_gen_files = wp_doing_cron(); if ( $auto_deleting_old_next_gen_files ) { return; } $next_gen_properties = $this->get_next_gen_properties(); $this->track( 'next_gen_updated', array_merge( $next_gen_properties, array( 'update_type' => 'delete_files', ) ) ); } public function track_toggle_next_gen_fallback( $old_settings, $settings ) { if ( empty( $settings['usage'] ) ) { return; } $webp_activated = ! empty( $settings['webp_mod'] ); $avif_activated = ! empty( $settings['avif_mod'] ); $next_gen_activated = $webp_activated || $avif_activated; // Do not track when Next Gen is not activated. if ( ! $next_gen_activated ) { return; } $modified_settings = $this->remove_unchanged_settings( $old_settings, $settings ); $next_gen_fallback_changed = isset( $modified_settings['webp_fallback'] ) || isset( $modified_settings['avif_fallback'] ); // Do not track if both WebP and AVIF fallbacks are not changed. if ( ! $next_gen_fallback_changed ) { return; } $webp_fallback_activated = ! empty( $settings['webp_fallback'] ); $avif_fallback_activated = ! empty( $settings['avif_fallback'] ); // Do not track if both WebP and AVIF fallbacks have the same status while switching the Next-Gen formats. if ( $this->is_switching_next_gen_format() && ( $webp_fallback_activated === $avif_fallback_activated ) ) { return; } $next_gen_fallback_activated = ( $webp_activated && $webp_fallback_activated ) || ( $avif_activated && $avif_fallback_activated ); $update_type = $next_gen_fallback_activated ? 'browser_support_on' : 'browser_support_off'; $next_gen_properties = $this->get_next_gen_properties(); $next_gen_method = 'avif_direct'; if ( $webp_activated ) { $direct_conversion_enabled = ! empty( $settings['webp_direct_conversion'] );// WebP method might or might not be changed. $next_gen_method = $direct_conversion_enabled ? 'webp_direct' : 'server_redirect'; } $this->track( 'next_gen_updated', array_merge( $next_gen_properties, array( 'update_type' => $update_type, 'Method' => $next_gen_method, ) ) ); } public function track_webp_method_changed() { $next_gen_properties = $this->get_next_gen_properties(); $this->track( 'next_gen_updated', array_merge( $next_gen_properties, array( 'update_type' => 'switch_webp_method', ) ) ); } public function track_next_gen_status_changed() { if ( $this->is_switching_next_gen_format() ) { return; } $next_gen_properties = $this->get_next_gen_properties(); $update_type = $this->next_gen_manager->is_active() ? 'activate' : 'deactivate'; $this->track( 'next_gen_updated', array_merge( $next_gen_properties, array( 'update_type' => $update_type, ) ) ); } /** * Note: Uses shutdown action to ensure all new settings are updated. */ public function maybe_track_next_gen_format_changed() { $switched_next_gen_format = did_action( 'wp_smush_next_gen_after_format_switch' ); if ( ! $switched_next_gen_format ) { return; } $next_gen_properties = $this->get_next_gen_properties(); $this->track( 'next_gen_updated', array_merge( $next_gen_properties, array( 'update_type' => 'switch_next_gen_format', ) ) ); } private function get_next_gen_referer() { $page = $this->get_referer_page(); $webp_configuration = Webp_Configuration::get_instance(); $is_user_on_wizard_webp = 'smush-next-gen' === $page && $webp_configuration->should_show_wizard() && ! $webp_configuration->direct_conversion_enabled(); if ( $is_user_on_wizard_webp ) { return 'Wizard'; } return $this->identify_referrer(); } private function get_next_gen_properties() { $location = $this->get_next_gen_referer(); $active_format_configuration = $this->next_gen_manager->get_active_format_configuration(); $next_gen_status_notice = $this->get_next_gen_status_notice(); $next_gen_method = 'avif_direct'; if ( Webp_Configuration::get_format_key() === $active_format_configuration->get_format_key() ) { // Directly check webp_direct_conversion option to identify webp method even webp module is disabled. $direct_conversion_enabled = $this->settings->get( 'webp_direct_conversion' ); $next_gen_method = $direct_conversion_enabled ? 'webp_direct' : 'webp_server'; } return array( 'Location' => $location, 'Method' => $next_gen_method, 'status_notice' => $next_gen_status_notice, ); } private function get_next_gen_status_notice() { if ( ! $this->next_gen_manager->is_active() ) { return 'na'; } if ( ! $this->next_gen_manager->is_configured() ) { $webp_configuration = Webp_Configuration::get_instance(); return $webp_configuration->server_configuration()->get_configuration_error_code(); } if ( is_multisite() ) { return 'active_subsite';// Activated but required run Bulk Smush on subsites. } $required_bulk_smush = Global_Stats::get()->is_outdated() || Global_Stats::get()->get_remaining_count() > 0; if ( $required_bulk_smush ) { return 'active_need_smush'; } $auto_smush_enabled = $this->settings->is_automatic_compression_active(); if ( $auto_smush_enabled ) { return 'active_automatic_enabled'; } return 'active_automatic_disabled'; } protected function maybe_track_cdn_update( $settings ) { $cdn_properties = array(); $cdn_property_labels = $this->cdn_property_labels(); foreach ( $settings as $setting_key => $setting_value ) { if ( array_key_exists( $setting_key, $cdn_property_labels ) ) { $property_label = $cdn_property_labels[ $setting_key ]; $property_value = $setting_value ? 'Enabled' : 'Disabled'; $cdn_properties[ $property_label ] = $property_value; } } if ( isset( $settings[ Settings::get_next_gen_cdn_key() ] ) ) { $cdn_next_gen_conversions_mode = $this->settings->sanitize_cdn_next_gen_conversion_mode( $settings[ Settings::get_next_gen_cdn_key() ] ); $cdn_next_gen_conversions = array( Settings::get_none_cdn_mode() => 'None', Settings::get_webp_cdn_mode() => 'WebP', Settings::get_avif_cdn_mode() => 'AVIF', ); if ( ! isset( $cdn_next_gen_conversions[ $cdn_next_gen_conversions_mode ] ) ) { $cdn_next_gen_conversions_mode = Settings::get_none_cdn_mode(); } $cdn_properties['Next-Gen Conversions'] = $cdn_next_gen_conversions[ $cdn_next_gen_conversions_mode ]; } if ( $cdn_properties ) { $this->track( 'CDN Updated', $cdn_properties ); return true; } return false; } private function cdn_property_labels() { return array( 'background_images' => 'Background Images', 'cdn_dynamic_sizes' => 'Dynamic Image Sizing', 'rest_api_support' => 'Rest API', ); } protected function get_background_optimization_status() { $bg_optimization = WP_Smush::get_instance()->core()->mod->bg_optimization; return $bg_optimization->is_background_enabled() ? 'Enabled' : 'Disabled'; } protected function get_active_pro_features() { $lossy_level = $this->settings->get_lossy_level_setting(); $cdn_module_activated = CDN_Helper::get_instance()->is_cdn_active(); $webp_module_activated = ! $cdn_module_activated && $this->settings->is_webp_module_active(); $avif_module_activated = ! $cdn_module_activated && $this->settings->is_avif_module_active(); $webp_direct_activated = $webp_module_activated && $this->settings->is_webp_direct_conversion_active(); $webp_server_activated = $webp_module_activated && ! $webp_direct_activated; return array( 'smush_ultra' => Settings::get_level_ultra_lossy() === $lossy_level, 'cdn' => $cdn_module_activated, 'avif' => $avif_module_activated, 'webp_direct' => $webp_direct_activated, 'webp_server' => $webp_server_activated, 's3_offload' => $this->settings->is_s3_active(), 'nextgen_gallery' => $this->settings->get( 'nextgen' ), 'preload_images' => $this->settings->is_lcp_preload_enabled(), ); } /** * Track lazy load updated event on toggle auto resizing. * * @param bool $setting_value Setting value. * * @return void */ protected function track_auto_resizing_feature_toggle( $setting_value ) { if ( ! $this->is_syncing_settings() ) { return; } $lazyload_settings = $this->settings->get_setting( 'wp-smush-lazy_load', array() ); $this->track_lazy_load_updated( array( 'update_type' => 'modify', 'modified_settings' => 'na', 'auto_resizing_status' => $setting_value ? 'Enabled' : 'Disabled', ), $lazyload_settings ); } /** * Track lazy load updated event on toggle auto resizing. * * @param bool $setting_value Setting value. * * @return void */ protected function track_image_dimensions_feature_toggle( $setting_value ) { if ( ! $this->is_syncing_settings() ) { return; } $lazyload_settings = $this->settings->get_setting( 'wp-smush-lazy_load', array() ); $this->track_lazy_load_updated( array( 'update_type' => 'modify', 'modified_settings' => 'na', 'image_dimensions_status' => $setting_value ? 'Enabled' : 'Disabled', ), $lazyload_settings ); } } external/plugins-cross-sell-page/vendor/composer/ClassLoader.php 0000644 00000037764 15252476777 0021145 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier <fabien@symfony.com> * @author Jordi Boggiano <j.boggiano@seld.be> * @see https://www.php-fig.org/psr/psr-0/ * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { /** @var \Closure(string):void */ private static $includeFile; /** @var string|null */ private $vendorDir; // PSR-4 /** * @var array<string, array<string, int>> */ private $prefixLengthsPsr4 = array(); /** * @var array<string, list<string>> */ private $prefixDirsPsr4 = array(); /** * @var list<string> */ private $fallbackDirsPsr4 = array(); // PSR-0 /** * List of PSR-0 prefixes * * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) * * @var array<string, array<string, list<string>>> */ private $prefixesPsr0 = array(); /** * @var list<string> */ private $fallbackDirsPsr0 = array(); /** @var bool */ private $useIncludePath = false; /** * @var array<string, string> */ private $classMap = array(); /** @var bool */ private $classMapAuthoritative = false; /** * @var array<string, bool> */ private $missingClasses = array(); /** @var string|null */ private $apcuPrefix; /** * @var array<string, self> */ private static $registeredLoaders = array(); /** * @param string|null $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; self::initializeIncludeClosure(); } /** * @return array<string, list<string>> */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } /** * @return array<string, list<string>> */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } /** * @return list<string> */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } /** * @return list<string> */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } /** * @return array<string, string> Array of classname => path */ public function getClassMap() { return $this->classMap; } /** * @param array<string, string> $classMap Class to filename map * * @return void */ public function addClassMap($classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param list<string>|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param list<string>|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param list<string>|string $paths The PSR-0 base directories * * @return void */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param list<string>|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * * @return void */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath * * @return void */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative * * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix * * @return void */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not * * @return void */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { return; } if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); self::$registeredLoaders[$this->vendorDir] = $this; } } /** * Unregisters this instance as an autoloader. * * @return void */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); if (null !== $this->vendorDir) { unset(self::$registeredLoaders[$this->vendorDir]); } } /** * Loads the given class or interface. * * @param string $class The name of the class * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { $includeFile = self::$includeFile; $includeFile($file); return true; } return null; } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } /** * Returns the currently registered loaders keyed by their corresponding vendor directories. * * @return array<string, self> */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } /** * @param string $class * @param string $ext * @return string|false */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } /** * @return void */ private static function initializeIncludeClosure() { if (self::$includeFile !== null) { return; } /** * Scope isolated include. * * Prevents access to $this/self from included files. * * @param string $file * @return void */ self::$includeFile = \Closure::bind(static function($file) { include $file; }, null, null); } } external/plugins-cross-sell-page/vendor/composer/installed.json 0000644 00000000106 15252476777 0021066 0 ustar 00 { "packages": [], "dev": false, "dev-package-names": [] } external/plugins-cross-sell-page/vendor/composer/autoload_psr4.php 0000644 00000000205 15252476777 0021505 0 ustar 00 <?php // autoload_psr4.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( ); external/plugins-cross-sell-page/vendor/composer/LICENSE 0000644 00000002056 15252476777 0017227 0 ustar 00 Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. external/plugins-cross-sell-page/vendor/composer/autoload_namespaces.php 0000644 00000000213 15252476777 0022733 0 ustar 00 <?php // autoload_namespaces.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( ); external/plugins-cross-sell-page/vendor/composer/installed.php 0000644 00000001433 15252476777 0020710 0 ustar 00 <?php return array( 'root' => array( 'name' => 'wpmudev/plugin-cross-sell', 'pretty_version' => 'dev-development', 'version' => 'dev-development', 'reference' => 'c28c8f4054046899664bdbb41e4b84942c009f05', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false, ), 'versions' => array( 'wpmudev/plugin-cross-sell' => array( 'pretty_version' => 'dev-development', 'version' => 'dev-development', 'reference' => 'c28c8f4054046899664bdbb41e4b84942c009f05', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), ), ); external/plugins-cross-sell-page/vendor/composer/autoload_static.php 0000644 00000002612 15252476777 0022110 0 ustar 00 <?php // autoload_static.php @generated by Composer namespace Composer\Autoload; class ComposerStaticInit65a0a6b53636acc8bd93737adac79345 { public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\App\\Rest_Endpoints\\Activate_Plugin' => __DIR__ . '/../..' . '/app/rest-endpoints/class-activate-plugin.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\App\\Rest_Endpoints\\Install_Plugin' => __DIR__ . '/../..' . '/app/rest-endpoints/class-install-plugin.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\App\\Submenus\\CrossSell' => __DIR__ . '/../..' . '/app/submenus/class-cross-sell.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\Container' => __DIR__ . '/../..' . '/core/class-container.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\Loader' => __DIR__ . '/../..' . '/core/class-loader.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\Rest_Api' => __DIR__ . '/../..' . '/core/class-rest-api.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\Utilities' => __DIR__ . '/../..' . '/core/class-utilities.php', ); public static function getInitializer($loader) { return \Closure::bind(function () use ($loader) { $loader->classMap = ComposerStaticInit65a0a6b53636acc8bd93737adac79345::$classMap; }, null, ClassLoader::class); } } external/plugins-cross-sell-page/vendor/composer/autoload_real.php 0000644 00000002077 15252476777 0021551 0 ustar 00 <?php // autoload_real.php @generated by Composer class ComposerAutoloaderInit65a0a6b53636acc8bd93737adac79345 { private static $loader; public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } } /** * @return \Composer\Autoload\ClassLoader */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } spl_autoload_register(array('ComposerAutoloaderInit65a0a6b53636acc8bd93737adac79345', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInit65a0a6b53636acc8bd93737adac79345', 'loadClassLoader')); require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit65a0a6b53636acc8bd93737adac79345::getInitializer($loader)); $loader->register(true); return $loader; } } external/plugins-cross-sell-page/vendor/composer/autoload_classmap.php 0000644 00000001752 15252476777 0022430 0 ustar 00 <?php // autoload_classmap.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\App\\Rest_Endpoints\\Activate_Plugin' => $baseDir . '/app/rest-endpoints/class-activate-plugin.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\App\\Rest_Endpoints\\Install_Plugin' => $baseDir . '/app/rest-endpoints/class-install-plugin.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\App\\Submenus\\CrossSell' => $baseDir . '/app/submenus/class-cross-sell.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\Container' => $baseDir . '/core/class-container.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\Loader' => $baseDir . '/core/class-loader.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\Rest_Api' => $baseDir . '/core/class-rest-api.php', 'WPMUDEV\\Modules\\Plugin_Cross_Sell\\Utilities' => $baseDir . '/core/class-utilities.php', ); external/plugins-cross-sell-page/vendor/composer/InstalledVersions.php 0000644 00000041745 15252476777 0022413 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\ClassLoader; use Composer\Semver\VersionParser; /** * This class is copied in every Composer installed project and available to all * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require its presence, you can require `composer-runtime-api ^2.0` * * @final */ class InstalledVersions { /** * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to * @internal */ private static $selfDir = null; /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null */ private static $installed; /** * @var bool */ private static $installedIsLocalDir; /** * @var bool|null */ private static $canGetVendors; /** * @var array[] * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ private static $installedByVendor = array(); /** * Returns a list of all package names which are present, either by being installed, replaced or provided * * @return string[] * @psalm-return list<string> */ public static function getInstalledPackages() { $packages = array(); foreach (self::getInstalled() as $installed) { $packages[] = array_keys($installed['versions']); } if (1 === \count($packages)) { return $packages[0]; } return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } /** * Returns a list of all package names with a specific type e.g. 'library' * * @param string $type * @return string[] * @psalm-return list<string> */ public static function getInstalledPackagesByType($type) { $packagesByType = array(); foreach (self::getInstalled() as $installed) { foreach ($installed['versions'] as $name => $package) { if (isset($package['type']) && $package['type'] === $type) { $packagesByType[] = $name; } } } return $packagesByType; } /** * Checks whether the given package is installed * * This also returns true if the package name is provided or replaced by another package * * @param string $packageName * @param bool $includeDevRequirements * @return bool */ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; } } return false; } /** * Checks whether the given package satisfies a version constraint * * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: * * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package * @return bool */ public static function satisfies($parser, $packageName, $constraint) { $constraint = $parser->parseConstraints((string) $constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); } /** * Returns a version constraint representing all the range(s) which are installed for a given package * * It is easier to use this via isInstalled() with the $constraint argument if you need to check * whether a given version of a package is installed, and not just whether it exists * * @param string $packageName * @return string Version constraint usable with composer/semver */ public static function getVersionRanges($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } $ranges = array(); if (isset($installed['versions'][$packageName]['pretty_version'])) { $ranges[] = $installed['versions'][$packageName]['pretty_version']; } if (array_key_exists('aliases', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); } if (array_key_exists('replaced', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); } if (array_key_exists('provided', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); } return implode(' || ', $ranges); } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['version'])) { return null; } return $installed['versions'][$packageName]['version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getPrettyVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['pretty_version'])) { return null; } return $installed['versions'][$packageName]['pretty_version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference */ public static function getReference($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['reference'])) { return null; } return $installed['versions'][$packageName]['reference']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. */ public static function getInstallPath($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @return array * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} */ public static function getRootPackage() { $installed = self::getInstalled(); return $installed[0]['root']; } /** * Returns the raw installed.php data for custom implementations * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} */ public static function getRawData() { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); } } return self::$installed; } /** * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ public static function getAllRawData() { return self::getInstalled(); } /** * Lets you reload the static array from another file * * This is only useful for complex integrations in which a project needs to use * this class but then also needs to execute another project's autoloader in process, * and wants to ensure both projects have access to their version of installed.php. * * A typical case would be PHPUnit, where it would need to make sure it reads all * the data it needs from this class, then call reload() with * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure * the project in which it runs can then also use this class safely, without * interference between PHPUnit's dependencies and the project's dependencies. * * @param array[] $data A vendor/composer/installed.php data set * @return void * * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data */ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); // when using reload, we disable the duplicate protection to ensure that self::$installed data is // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, // so we have to assume it does not, and that may result in duplicate data being returned when listing // all installed packages for example self::$installedIsLocalDir = false; } /** * @return string */ private static function getSelfDir() { if (self::$selfDir === null) { self::$selfDir = strtr(__DIR__, '\\', '/'); } return self::$selfDir; } /** * @return array[] * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ private static function getInstalled() { if (null === self::$canGetVendors) { self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); } $installed = array(); $copiedLocalDir = false; if (self::$canGetVendors) { $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ $required = require $vendorDir.'/composer/installed.php'; self::$installedByVendor[$vendorDir] = $required; $installed[] = $required; if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { self::$installed = $required; self::$installedIsLocalDir = true; } } if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { $copiedLocalDir = true; } } } if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ $required = require __DIR__ . '/installed.php'; self::$installed = $required; } else { self::$installed = array(); } } if (self::$installed !== array() && !$copiedLocalDir) { $installed[] = self::$installed; } return $installed; } } external/plugins-cross-sell-page/vendor/autoload.php 0000644 00000001354 15252476777 0016714 0 ustar 00 <?php // autoload.php @generated by Composer if (PHP_VERSION_ID < 50600) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, $err); } elseif (!headers_sent()) { echo $err; } } throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit65a0a6b53636acc8bd93737adac79345::getLoader(); external/plugins-cross-sell-page/core/class-rest-api.php 0000644 00000012062 15252476777 0017364 0 ustar 00 <?php /** * Controller for rest endpoints. * * @link https://wpmudev.com/ * @since 1.0.0 * * @author WPMUDEV (https://wpmudev.com) * @package WPMUDEV\Plugin_Cross_Sell * * @copyright (c) 2025, WPMU DEV (http://wpmudev.com) */ namespace WPMUDEV\Modules\Plugin_Cross_Sell; // Abort if called directly. use WP_REST_Controller; use WPMUDEV\Modules\Plugin_Cross_Sell\Container; defined( 'WPINC' ) || die; /** * Class Rest_Api * * @package WPMUDEV\Plugin_Cross_Sell */ abstract class Rest_Api extends WP_REST_Controller { /** * Holds the request param. * * @var array|object */ protected $request_action; /** * The version. * * @var string */ protected $version = 'v1'; /** * The namespace. * * @var string */ protected $namespace = 'wpmudev_pcs/v1'; /** * API endpoint for the current endpoint. * * @since 1.0.0 * * @var string $endpoint */ protected $endpoint = ''; /** * Dependency container. * * @since 1.0.0 * * @var Container */ protected $di_container = null; /** * Utilities object. * * @since 1.0.0 * * @var Utilities */ protected $utilities = null; /** * Prepares the class properties. * * @param Container $container Dependency container. * @return void */ public function init( $container = null ) { $this->di_container = $container; $this->utilities = $container->get( 'utilities' ); if ( ! $this->utilities instanceof Utilities ) { $this->utilities = new Utilities(); } // Allow to prepare some params early if required. $this->prepare_endpoint_params(); // If the single instance hasn't been set, set it now. $this->register_hooks(); } /** * Set up WordPress hooks and filters * * @since 1.0.0 * * @return void */ protected function register_hooks() { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } /** * Formatting the response * * @since 1.0.0 * * @param mixed $item The item to format. * @param WP_REST_Request $request request object. * * @return array|WP_Error|WP_REST_Response */ public function prepare_item_for_response( $item, $request ) { $fields = $this->get_fields_for_response( $request ); $data = array(); foreach ( $fields as $field_key ) { if ( rest_is_field_included( $field_key, $fields ) ) { $data[ $field_key ] = isset( $item[ $field_key ] ) ? $item[ $field_key ] : ''; } } return $data; } /** * Sets up the proper HTTP status code for authorization. * * @since 2.0.0 * * @return int */ public function authorization_status_code() { $status = 401; if ( is_user_logged_in() ) { $status = 403; } return $status; } /** * Check if a given request has access to manage settings. * * @param \WP_REST_Request $request Request object. * @param string $capability Capability to check. * * @return bool * @since 1.0.0 */ public function has_permission( $request = null, $capability = 'manage_options' ) { $capable = current_user_can( $capability ); /** * Filter to modify settings rest capability. * * @param WP_REST_Request $request Request object. * * @param bool $capable Is user capable?. * * @since 1.0.0 */ return boolval( apply_filters( 'wpmudev_pluginscrosssell_rest_settings_permission', $capable, $request ) ); } /** * Get formatted response for the current request. * * @param array $data Response data. * @param bool $success Is request success. * * @return \WP_REST_Response * @since 1.0.0 */ public function get_response( $data = array(), $success = true ) { // Response status. $status = $success ? 200 : 400; return new \WP_REST_Response( array( 'success' => $success, 'message' => $data['message'] ?? '', 'data' => $data, ), $status ); } /** * Get the Endpoint's namespace. * * @return string */ public function get_namespace() { return $this->namespace; } /** * Set the Endpoint's namespace. * * @param string $api_namespace The namespace. * @return void */ protected function set_namespace( $api_namespace = '' ) { $this->namespace = $api_namespace; } /** * Get the Endpoint's endpoint part * * @return string */ public function get_endpoint() { return $this->endpoint; } /** * Gives the full url of the Rest endpoint (with site url). * * @return string */ public function get_endpoint_url() { return trailingslashit( rest_url() ) . trailingslashit( $this->get_namespace() ) . $this->get_endpoint(); } /** * Gives the path of the Rest endpoint without site url * * @return string */ public function get_endpoint_path() { return trailingslashit( $this->get_namespace() ) . $this->get_endpoint(); } /** * Register the routes for the objects of the controller. This should be defined in extending class. * * @since 1.0.0 * * @return void */ public function register_routes() { } /** * A helper function called early to allow prepare some variables * * @since 1.0.0 * * @return void */ public function prepare_endpoint_params() { } } external/plugins-cross-sell-page/core/plugins-list.php 0000644 00000030437 15252476777 0017175 0 ustar 00 <?php /** * Plugin list of WPMU DEV for the cross-sell sub-module. Includes Free and Pro plugins. * * @link https://wpmudev.com/ * @since 1.0.0 * * @author WPMUDEV (https://wpmudev.com) * @package WPMUDEV\Plugin_Cross_Sell * * @copyright (c) 2025, WPMU DEV (http://wpmudev.com) */ return array( 'free-plugins' => array( array( 'slug' => 'wp-smushit', 'path' => 'wp-smushit/wp-smush.php', 'url' => 'https://wordpress.org/plugins/wp-smushit/', 'utm_source' => 'smush', 'utm_campaign' => 'cross-sell_plugin_smush', 'admin_url_page' => 'smush', 'logo' => 'wp-smushit.png', 'title' => __( 'Smush', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Smush images down to size for faster page loading.', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '1M +', ), array( 'slug' => 'forminator', 'path' => 'forminator/forminator.php', 'url' => 'https://wordpress.org/plugins/forminator/', 'utm_source' => 'forminator', 'utm_campaign' => 'cross-sell_plugin_forminator', 'admin_url_page' => 'forminator', 'logo' => 'forminator.png', 'title' => __( 'Forminator', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Surveys, subscriptions, feedback, quizzes: Forminator makes it easy.', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 4.5, 'active_installs' => '500k +', ), array( 'slug' => 'defender-security', 'path' => 'defender-security/wp-defender.php', 'url' => 'https://wordpress.org/plugins/defender-security/', 'utm_source' => 'defender', 'utm_campaign' => 'cross-sell_plugin_defender', 'admin_url_page' => 'wp-defender', 'logo' => 'defender-security.png', 'title' => __( 'Defender', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Automated and reliable WordPress security in a matter of clicks.', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '90k +', ), array( 'slug' => 'hummingbird-performance', 'path' => 'hummingbird-performance/wp-hummingbird.php', 'url' => 'https://wordpress.org/plugins/hummingbird-performance/', 'utm_source' => 'hummingbird', 'utm_campaign' => 'cross-sell_plugin_hummingbird', 'admin_url_page' => 'wphb', 'logo' => 'hummingbird-performance.png', 'title' => __( 'Hummingbird', 'plugin-cross-sell-textdomain' ), 'description' => __( 'For sites that load in one flap of a hummingbird’s wing. ', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 4.7, 'active_installs' => '100k +', ), array( 'slug' => 'smartcrawl-seo', 'path' => 'smartcrawl-seo/wpmu-dev-seo.php', 'url' => 'https://wordpress.org/plugins/smartcrawl-seo/', 'utm_source' => 'smartcrawl', 'utm_campaign' => 'cross-sell_plugin_smartcrawl', 'admin_url_page' => 'wds_wizard', 'logo' => 'smartcrawl-seo.png', 'title' => __( 'SmartCrawl', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Take the mystery out of optimizing your site.', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '20k +', ), array( 'slug' => 'wordpress-popup', 'path' => 'wordpress-popup/popover.php', 'url' => 'https://wordpress.org/plugins/wordpress-popup/', 'utm_source' => 'hustle', 'utm_campaign' => 'cross-sell_plugin_hustle', 'admin_url_page' => 'hustle', 'logo' => 'wordpress-popup.png', 'title' => __( 'Hustle', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Create high-converting and targeted marketing campaigns in minutes.', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 4.6, 'active_installs' => '100k +', ), array( 'slug' => 'branda-white-labeling', 'path' => 'branda-white-labeling/ultimate-branding.php', 'url' => 'https://wordpress.org/plugins/branda-white-labeling/', 'utm_source' => 'branda', 'utm_campaign' => 'cross-sell_plugin_branda', 'admin_url_page' => 'branding', 'logo' => 'branda-white-labeling.png', 'title' => __( 'Branda', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Rebrand, customize, and white label WordPress without code.', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 4.5, 'active_installs' => '20k +', ), array( 'slug' => 'beehive-analytics', 'path' => 'beehive-analytics/beehive-analytics.php', 'url' => 'https://wordpress.org/plugins/beehive-analytics/', 'utm_source' => 'beehive', 'utm_campaign' => 'cross-sell_plugin_beehive', 'admin_url_page' => 'beehive', 'logo' => 'beehive-analytics.png', 'title' => __( 'Beehive', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Customizable Google Analytics dashboards, statistics, and reports.', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '30k +', ), array( 'slug' => 'broken-link-checker', 'path' => 'broken-link-checker/broken-link-checker.php', 'url' => 'https://wordpress.org/plugins/broken-link-checker/', 'utm_source' => 'blc', 'utm_campaign' => 'cross-sell_plugin_blc', 'admin_url_page' => 'blc_dash', 'logo' => 'broken-link-checker.png', 'title' => __( 'Broken Link Checker', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Check posts, pages & content for broken links to improve SEO. ', 'plugin-cross-sell-textdomain' ), 'features' => array(), 'installed' => false, 'active' => false, 'rating' => 3.8, 'active_installs' => '600k +', ), ), 'pro-plugins' => array( array( 'slug' => 'forminator-pro', 'path' => 'forminator-pro/forminator.php', 'url' => 'https://wpmudev.com/project/forminator-pro/', 'utm_source' => 'forminator', 'utm_campaign' => 'cross-sell_plugin_forminator', 'admin_url_page' => 'forminator', 'logo' => 'forminator.png', 'title' => __( 'Forminator Pro', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Surveys, subscriptions, feedback, quizzes: Forminator makes it easy.', 'plugin-cross-sell-textdomain' ), 'features' => array( __( 'Generate and send PDF files', 'plugin-cross-sell-textdomain' ), __( 'Accept recurring payment', 'plugin-cross-sell-textdomain' ), __( 'Get form submitter’s geolocation', 'plugin-cross-sell-textdomain' ), ), 'installed' => false, 'active' => false, 'rating' => 4.5, 'active_installs' => '550k +', ), array( 'slug' => 'smush-pro', 'path' => 'wp-smush-pro/wp-smush.php', 'url' => 'https://wpmudev.com/project/wp-smush-pro/', 'utm_source' => 'smush', 'utm_campaign' => 'cross-sell_plugin_smush', 'admin_url_page' => 'smush', 'logo' => 'wp-smushit.png', 'title' => __( 'Smush Pro', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Smush images down to size for faster page loading.', 'plugin-cross-sell-textdomain' ), 'features' => array( __( 'Ultra Smush with 5X compression', 'plugin-cross-sell-textdomain' ), __( '123 point CDN + 50GB bandwidth', 'plugin-cross-sell-textdomain' ), __( 'Unlimited bulk and auto smushing', 'plugin-cross-sell-textdomain' ), ), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '1M +', ), array( 'slug' => 'defender-pro', 'path' => 'wp-defender/wp-defender.php', 'url' => 'https://wpmudev.com/project/wp-defender/', 'utm_source' => 'defender', 'utm_campaign' => 'cross-sell_plugin_defender', 'admin_url_page' => 'wp-defender', 'logo' => 'defender-security.png', 'title' => __( 'Defender Pro', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Automated and reliable WordPress security in a matter of clicks.', 'plugin-cross-sell-textdomain' ), 'features' => array( __( 'Scheduled malware scanning', 'plugin-cross-sell-textdomain' ), __( 'AntiBot Global Firewall', 'plugin-cross-sell-textdomain' ), __( 'Comprehensive audit logging', 'plugin-cross-sell-textdomain' ), ), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '260k +', ), array( 'slug' => 'hummingbird-pro', 'path' => 'wp-hummingbird/wp-hummingbird.php', 'url' => 'https://wpmudev.com/project/wp-hummingbird/', 'utm_source' => 'hummingbird', 'utm_campaign' => 'cross-sell_plugin_hummingbird', 'admin_url_page' => 'wphb', 'logo' => 'hummingbird-performance.png', 'title' => __( 'Hummingbird Pro', 'plugin-cross-sell-textdomain' ), 'description' => __( 'For sites that load in one flap of a hummingbird’s wing.', 'plugin-cross-sell-textdomain' ), 'features' => array( __( 'Delay JavaScript execution', 'plugin-cross-sell-textdomain' ), __( 'Automated critical CSS generation', 'plugin-cross-sell-textdomain' ), __( 'Turbocharged asset optimization', 'plugin-cross-sell-textdomain' ), ), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '185k +', ), array( 'slug' => 'smartcrawl-pro', 'path' => 'wpmu-dev-seo/wpmu-dev-seo.php', 'url' => 'https://wpmudev.com/project/smartcrawl-wordpress-seo/', 'utm_source' => 'smartcrawl', 'utm_campaign' => 'cross-sell_plugin_smartcrawl', 'admin_url_page' => 'wds_wizard', 'logo' => 'smartcrawl-seo.png', 'title' => __( 'SmartCrawl Pro', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Take the mystery out of optimizing your site.', 'plugin-cross-sell-textdomain' ), 'features' => array( __( 'Site crawler SEO scan & reports', 'plugin-cross-sell-textdomain' ), __( 'SEO health audit reports', 'plugin-cross-sell-textdomain' ), __( 'Automatic integrated linking', 'plugin-cross-sell-textdomain' ), ), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '100k +', ), array( 'slug' => 'snapshot-pro', 'path' => 'snapshot-backups/snapshot-backups.php', 'url' => 'https://wpmudev.com/project/snapshot/', 'utm_source' => 'snapshot', 'utm_campaign' => 'cross-sell_plugin_snapshot', 'admin_url_page' => 'snapshot', 'logo' => 'snapshot.png', 'title' => __( 'Snapshot Pro', 'plugin-cross-sell-textdomain' ), 'description' => __( 'Space-efficient backups that happen effortlessly while you sleep.', 'plugin-cross-sell-textdomain' ), 'features' => array( __( 'Our PRO-only backup plugin', 'plugin-cross-sell-textdomain' ), __( 'Comes with up to 50GB storage', 'plugin-cross-sell-textdomain' ), __( 'Store backups for 50 days', 'plugin-cross-sell-textdomain' ), ), 'installed' => false, 'active' => false, 'rating' => 4.8, 'active_installs' => '50k +', ), ), ); external/plugins-cross-sell-page/core/class-container.php 0000644 00000002316 15252476777 0017623 0 ustar 00 <?php /** * The Container class used for DI. * * @link https://wpmudev.com/ * @since 1.0.0 * * @author WPMUDEV (https://wpmudev.com) * @package WPMUDEV/Plugin_Cross_Sell * * @copyright (c) 2025, Incsub (http://incsub.com) */ namespace WPMUDEV\Modules\Plugin_Cross_Sell; /** * Dependency Injection Container class. * * @since 1.0.0 */ class Container { /** * Services list. * * @var array */ private $services = array(); /** * Pushes a service into the container. * * @param string $key Service key. * @param mixed $value Service. * @return void */ public function set( $key, $value ) { $this->services[ $key ] = $value; } /** * Fetches a service from the container. * * @param string $key Service key. * @throws \InvalidArgumentException If service not found. */ public function get( $key ) { if ( ! isset( $this->services[ $key ] ) ) { throw new \InvalidArgumentException( esc_html( "Service '{$key}' not found in container." ) ); } return $this->services[ $key ]; } /** * Checks if a service is registered. * * @param string $key Service key. * @return bool */ public function has( $key ) { return isset( $this->services[ $key ] ); } } external/plugins-cross-sell-page/core/class-loader.php 0000644 00000004124 15252476777 0017106 0 ustar 00 <?php /** * Class to boot up module. * * @link https://wpmudev.com/ * @since 1.0.0 * * @author WPMUDEV (https://wpmudev.com) * @package WPMUDEV/Plugin_Cross_Sell * * @copyright (c) 2025, Incsub (http://incsub.com) */ namespace WPMUDEV\Modules\Plugin_Cross_Sell; // If this file is called directly, abort. defined( 'WPINC' ) || die; /** * The Loader class is responsible for initializing the module. */ final class Loader { /** * Settings helper class instance. * * @since 1.0.0 * @var object */ public $settings; /** * Minimum supported php version. * * @since 1.0.0 * @var float */ public $php_version = '7.4'; /** * Minimum WordPress version. * * @since 1.0.0 * @var float */ public $wp_version = '6.3'; /** * The dependency container. * * @since 1.0.0 * @var Container */ private $container; /** * Initialize the loader. * * @since 1.0.0 * @param Container $container The dependency container. * @return void */ public function __construct( $container ) { $this->container = $container; } /** * Initialize functionality if requirements are met. * * @return void */ public function init() { if ( ! $this->can_boot() ) { return; } $this->setup_components(); } /** * Main condition that checks if plugin parts should continue loading. * * @return bool */ private function can_boot() { /** * Checks * - PHP version * - WP Version * If not then return. */ global $wp_version; return ( version_compare( PHP_VERSION, $this->php_version, '>=' ) && version_compare( $wp_version, $this->wp_version, '>=' ) ); } /** * Register all the actions and filters. * * @since 1.0.0 * @access private * @return void */ private function setup_components() { $submenus = new App\Submenus\CrossSell(); $submenus->init( $this->container ); $install_endpoint = new App\Rest_Endpoints\Install_Plugin(); $install_endpoint->init( $this->container ); $activation_endpoint = new App\Rest_Endpoints\Activate_Plugin(); $activation_endpoint->init( $this->container ); } } external/plugins-cross-sell-page/core/class-utilities.php 0000644 00000021355 15252476777 0017660 0 ustar 00 <?php /** * Utilities class contains a list of common useful helper methods. * * @link https://wpmudev.com/ * @since 1.0.0 * * @author WPMUDEV (https://wpmudev.com) * @package WPMUDEV/Plugin_Cross_Sell * * @copyright (c) 2025, Incsub (http://incsub.com) */ namespace WPMUDEV\Modules\Plugin_Cross_Sell; // If this file is called directly, abort. if ( ! defined( 'WPINC' ) ) { die; } /** * A list of general purpose utility methods. */ class Utilities { /** * Returns the list of all plugins. * * @return array */ public function get_plugins_list() { static $plugins_list = null; if ( is_null( $plugins_list ) ) { $plugins_file = untrailingslashit( WPMUDEV_MODULE_PLUGIN_CROSS_SELL_DIR ) . '/core/plugins-list.php'; if ( file_exists( $plugins_file ) ) { $plugins_list = require_once $plugins_file; } } return is_array( $plugins_list ) ? $plugins_list : array(); } /** * Returns the list of free plugins. * * @return array */ public function get_free_plugins() { $plugins = $this->get_plugins_list(); return is_array( $plugins ) && ! empty( $plugins['free-plugins'] ) ? $plugins['free-plugins'] : array(); } /** * Returns the list of pro plugins. * * @return array */ public function get_pro_plugins() { $plugins = $this->get_plugins_list(); return is_array( $plugins ) && ! empty( $plugins['pro-plugins'] ) ? $plugins['pro-plugins'] : array(); } /** * Retrieves the path of a plugin by its slug. * * @param string $plugin_slug The plugin slug. * @return string */ public function get_plugin_path_by_slug( $plugin_slug = '' ) { $free_plugins = $this->get_free_plugins(); $free_plugins = is_array( $free_plugins ) && ! empty( $free_plugins['free-plugins'] ) ? $free_plugins['free-plugins'] : $free_plugins; return $this->get_value_from_associative_array( 'path', $free_plugins, array( 'slug' => $plugin_slug ) ); } /** * Extracts the value of a key from the first associative array in an array that matches the given criteria. * * @param string $key The key whose value you want to extract. * @param array $input_list The array of associative arrays to filter. * @param array $args The criteria for filtering (key-value pairs). * @param string $operator How to combine the criteria ('AND' or 'OR'). * @return mixed The value for the specified key if found, or null. */ public function get_value_from_associative_array( $key = '', $input_list = array(), $args = array(), $operator = 'AND' ) { if ( empty( $key ) || empty( $input_list ) ) { return null; } $filtered = wp_list_filter( $input_list, $args, $operator ); if ( ! empty( $filtered ) ) { $first_item = current( $filtered ); // Check if $first_item is an array. if ( is_array( $first_item ) ) { return isset( $first_item[ $key ] ) ? $first_item[ $key ] : ''; } // Check if $first_item is an object. if ( is_object( $first_item ) ) { return isset( $first_item->$key ) ? $first_item->$key : ''; } } return null; } /** * Checks if a plugin is installed. * * @param string $file The plugin file path. * @return bool */ public function is_plugin_installed( $file = '' ) { // Include necessary plugin functions. if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $all_plugins = get_plugins(); return is_array( $all_plugins ) ? isset( $all_plugins[ $file ] ) : false; } /** * Get plugin statistics from WordPress.org API * * @param string $plugin_slug The plugin slug from wordpress.org. * @param bool $force_refresh Optional. Whether to force a refresh of the data from the API. * @return array|false Plugin data or false on failure. */ public function get_plugin_stats( $plugin_slug = '', $force_refresh = false ) { if ( empty( $plugin_slug ) ) { return false; } // Set cache key - add a prefix for easy identification/deletion. $transient_key = 'wpmudev_pcs_plugin_stats_' . sanitize_key( $plugin_slug ); // Try to get cached data first (unless force refresh is requested). if ( ! $force_refresh ) { $cached_data = get_transient( $transient_key ); if ( false !== $cached_data ) { return $cached_data; } } // If not in cache or force refresh, fetch from API. $url = "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug={$plugin_slug}"; $response = wp_remote_get( $url, array( 'timeout' => 15, // Increased timeout for potentially slow API responses. ) ); if ( is_wp_error( $response ) ) { return false; } $status_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $status_code ) { return false; } $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( empty( $data ) || ! is_array( $data ) ) { return false; } // Format the data (with fallbacks for each field). $plugin_data = array( 'name' => $data['name'] ?? '', 'version' => $data['version'] ?? '', 'active_installs' => $data['active_installs'] ?? 0, 'rating' => isset( $data['rating'] ) ? round( $data['rating'] / 20, 1 ) : 0, // Convert to 5-star scale. 'num_ratings' => $data['num_ratings'] ?? 0, 'last_updated' => $data['last_updated'] ?? '', 'requires_wp' => $data['requires'] ?? '', 'tested_wp' => $data['tested'] ?? '', 'author' => isset( $data['author'] ) ? wp_strip_all_tags( $data['author'] ) : '', 'homepage' => $data['homepage'] ?? '', ); // Cache the data. $expiration = DAY_IN_SECONDS * 3; // Store in transient. set_transient( $transient_key, $plugin_data, $expiration ); return $plugin_data; } /** * Clear plugin stats cache for a specific plugin or all plugin stats * * @param string $plugin_slug Optional. Clear cache for specific plugin. If empty, clears all plugin stats. * @return bool True on success */ public function clear_plugin_stats_cache( $plugin_slug = '' ) { global $wpdb; // If plugin slug provided, clear only that plugin's cache. if ( ! empty( $plugin_slug ) ) { $transient_key = 'wpmudev_pcs_plugin_stats_' . sanitize_key( $plugin_slug ); return delete_transient( $transient_key ); } // Otherwise clear all plugin stats transients. $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE '_transient_wpmudev_pcs_plugin_stats_%'" ); return true; } /** * Validate a value against a given schema. * * @param mixed $value The value to check. * @param mixed $schema The schema that defines the expected type or structure. * @param bool $strict If true, value must not have extra keys/properties. * @return bool */ public function validate_schema( $value, $schema, $strict = false ) { // If schema is a simple type (string), perform a type check. if ( is_string( $schema ) ) { return $this->validate_type( $value, $schema ); } // If schema is an array, we expect the value to be an array or object. if ( is_array( $schema ) ) { if ( ! is_array( $value ) && ! is_object( $value ) ) { return false; } // Convert objects to an associative array of properties. $value_array = is_object( $value ) ? get_object_vars( $value ) : $value; // Check each key defined in the schema. foreach ( $schema as $key => $expected_type ) { if ( $strict && ! array_key_exists( $key, $value_array ) ) { // In strict mode, all keys in the schema must be present. return false; } if ( array_key_exists( $key, $value_array ) ) { // Recursively validate the value for this key. if ( ! $this->validate_schema( $value_array[ $key ], $expected_type, $strict ) ) { return false; } } } // In strict mode, also ensure that there are no extra keys. if ( $strict && count( $value_array ) !== count( $schema ) ) { return false; } return true; } // If the schema is neither a string nor an array, we don't know how to validate. return false; } /** * Check if a value is of the expected type. * * @param mixed $value The value to check. * @param string $type The expected type (e.g., 'int', 'string', 'bool', 'array', 'object'). * @return bool */ public function validate_type( $value, $type ) { switch ( $type ) { case 'int': case 'integer': return is_int( $value ); case 'string': return is_string( $value ); case 'bool': case 'boolean': return is_bool( $value ); case 'float': case 'double': return is_float( $value ); case 'array': return is_array( $value ); case 'object': return is_object( $value ); default: // If $type is a class name, check if $value is an instance of that class. if ( class_exists( $type ) ) { return $value instanceof $type; } // Unknown type. return false; } } } external/plugins-cross-sell-page/languages/plugin-cross-sell-textdomain.pot 0000644 00000025211 15252476777 0023322 0 ustar 00 msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "POT-Creation-Date: 2025-04-22T13:36:58+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.11.0\n" "X-Domain: plugin-cross-sell-textdomain\n" #: assets/js/crosssellpage.js:137 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:65 msgid "We heard you like plugins...😉" msgstr "" #: assets/js/crosssellpage.js:144 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:71 msgid "Check out these top-rated tools for securing, optimizing and growing your site." msgstr "" #: assets/js/crosssellpage.js:1249 #: assets/js/crosssellpage.js:1254 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:697 #: assets/js/crosssellpage.js:703 msgid "4.9/5" msgstr "" #: assets/js/crosssellpage.js:1250 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:698 msgid "3,002" msgstr "" #: assets/js/crosssellpage.js:1255 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:704 msgid "1,746" msgstr "" #: assets/js/crosssellpage.js:1259 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:709 msgid "5/5" msgstr "" #: assets/js/crosssellpage.js:1260 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:710 msgid "592" msgstr "" #: assets/js/crosssellpage.js:1302 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:720 msgid "About WPMU DEV" msgstr "" #: assets/js/crosssellpage.js:1309 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:722 msgid "Made for web developers, by web developers" msgstr "" #: assets/js/crosssellpage.js:1324 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:730 msgid "Since 2006, our award-winning WordPress plugins, hosting, world beating support and site management tools have helped hundreds of thousands of web developers, freelancers and agencies run and grow their businesses. " msgstr "" #: assets/js/crosssellpage.js:1336 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:739 msgid "Learn more" msgstr "" #: assets/js/crosssellpage.js:1370 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:753 msgid "Everything Wordpress found in one place. " msgstr "" #: assets/js/crosssellpage.js:1382 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:762 msgid "WPMU DEV" msgstr "" #: assets/js/crosssellpage.js:1394 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:781 msgid "Trusted by 60,000+ businesses worldwide" msgstr "" #: assets/js/crosssellpage.js:1397 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:788 msgid "67,233,282 plugin downloads" msgstr "" #: assets/js/crosssellpage.js:1400 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:792 msgid "7,260+ 5 star reviews" msgstr "" #: assets/js/crosssellpage.js:2168 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1447 msgid " plugin has been installed successfully!" msgstr "" #: assets/js/crosssellpage.js:2227 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1495 msgid " plugin has been activated successfully!" msgstr "" #: assets/js/crosssellpage.js:2321 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1551 msgid "Installing..." msgstr "" #: assets/js/crosssellpage.js:2321 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1552 msgid "Install" msgstr "" #: assets/js/crosssellpage.js:2332 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1562 msgid "Activating..." msgstr "" #: assets/js/crosssellpage.js:2332 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1563 msgid "Activate" msgstr "" #: assets/js/crosssellpage.js:2343 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1571 msgid "Active" msgstr "" #: assets/js/crosssellpage.js:2452 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1627 msgid "Try our other highly-rated free WordPress plugins" msgstr "" #: assets/js/crosssellpage.js:2453 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1631 msgid "From security to SEO to marketing, we’ve got you covered." msgstr "" #: assets/js/crosssellpage.js:2499 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1663 msgid "Get a high-powered web-building suite, at no extra cost" msgstr "" #: assets/js/crosssellpage.js:2548 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1683 msgid "View all pro plugins" msgstr "" #: assets/js/crosssellpage.js:2602 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1733 msgid "Fully managed hosting" msgstr "" #: assets/js/crosssellpage.js:2603 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1734 msgid "Deliver lightning-fast, secure websites with a 99.9% SLA—complete with isolated environments, global server locations, and expert support." msgstr "" #: assets/js/crosssellpage.js:2609 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1742 msgid "Site management" msgstr "" #: assets/js/crosssellpage.js:2610 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1743 msgid "Take command of all your WordPress sites with one simple dashboard to automate updates, monitor performance, and generate white-label client reports." msgstr "" #: assets/js/crosssellpage.js:2616 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1751 msgid "Domains" msgstr "" #: assets/js/crosssellpage.js:2617 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1752 msgid "With access to over 250 TLDs, seamless integration and free privacy protection, you can offer domain registration services at unbeatable prices." msgstr "" #: assets/js/crosssellpage.js:2623 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1760 msgid "100+ template library" msgstr "" #: assets/js/crosssellpage.js:2624 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1761 msgid "Create beautiful sites in seconds with pre-configured site templates for you and your client projects—compatible with every plugin, theme builder and tool." msgstr "" #: assets/js/crosssellpage.js:2630 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1769 msgid "Get Pro Email" msgstr "" #: assets/js/crosssellpage.js:2631 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1770 msgid "Add-on private, ad-free IMAP webmail for easily managed, auto-synced, professional emails for you and your clients with 5-50GB storage options." msgstr "" #: assets/js/crosssellpage.js:2637 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1778 msgid "Unparalleled support" msgstr "" #: assets/js/crosssellpage.js:2638 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1779 msgid "Chat with our support team anytime—24/7, 365 days a year—with an average response time of 2 minutes. We’ll even log in and fix issues for you and your clients!" msgstr "" #: assets/js/crosssellpage.js:2645 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1790 msgid "Everything you need to grow a successful agency - at an unrivaled value" msgstr "" #: assets/js/crosssellpage.js:2737 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1835 msgid "On-Demand Development" msgstr "" #: assets/js/crosssellpage.js:2738 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1836 msgid "Need assistance with CSS or custom functionality? Our experts create scripts to solve WordPress issues and enhance your site." msgstr "" #: assets/js/crosssellpage.js:2744 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1844 msgid "Proactive Monitoring" msgstr "" #: assets/js/crosssellpage.js:2745 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1845 msgid "We monitor WPMU DEV hosted sites 24/7 and fix them fast if they go down, you don’t have to do anything." msgstr "" #: assets/js/crosssellpage.js:2751 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1853 msgid "Speed Optimization" msgstr "" #: assets/js/crosssellpage.js:2752 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1854 msgid "Page Speed lagging behind? Our experts will give your site a guaranteed scores of 90+ on desktop and 75+ on mobile." msgstr "" #: assets/js/crosssellpage.js:2758 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1862 msgid "Malware Removal" msgstr "" #: assets/js/crosssellpage.js:2759 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1863 msgid "Need help with CSS or custom functionality? Our experts create scripts to solve WordPress issues and enhance your site." msgstr "" #: assets/js/crosssellpage.js:2766 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1874 msgid "Expert services" msgstr "" #: assets/js/crosssellpage.js:2768 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1876 msgid "Hand off any site issues to our WordPress expert team’s 20+ years of expertise." msgstr "" #: assets/js/crosssellpage.js:2777 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1882 msgid "Add-on services" msgstr "" #: assets/js/crosssellpage.js:2890 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1939 msgid "Find your plan (From $3/m <highlight>$15/m</highlight>)" msgstr "" #: assets/js/crosssellpage.js:2910 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1952 msgid "Did you know WPMU DEV Membership includes " msgstr "" #: assets/js/crosssellpage.js:2911 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1956 msgid "ALL our Pro Plugins?" msgstr "" #: assets/js/crosssellpage.js:2912 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1960 msgid "Plus, everything your agency needs for hosting, reselling and more." msgstr "" #: assets/js/crosssellpage.js:2983 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1979 msgid "Your comprehensive toolkit for WordPress website management." msgstr "" #: assets/js/crosssellpage.js:3018 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1989 msgid "30 day guarantee: If you don’t love it, we’ll give you your money back - no questions asked." msgstr "" #: assets/js/crosssellpage.js:3085 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:2020 msgid "Free Plugins you’ll like" msgstr "" #: assets/js/crosssellpage.js:3092 #: assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:2023 msgid "Pro Plugins You’ll LOVE (Bundle Deal)" msgstr "" external/plugins-cross-sell-page/languages/plugin-cross-sell-textdomain-en_US.po 0000644 00000032655 15252476777 0024157 0 ustar 00 msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "POT-Creation-Date: 2025-04-22T13:36:58+00:00\n" "PO-Revision-Date: 2025-04-22T13:36:58+00:00\n" "X-Generator: WP-CLI 2.11.0\n" "X-Domain: plugin-cross-sell-textdomain\n" "Language: en_US\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: assets/js/crosssellpage.js:137 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:65 msgid "We heard you like plugins...😉" msgstr "We heard you like plugins...😉" #: assets/js/crosssellpage.js:144 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:71 msgid "Check out these top-rated tools for securing, optimizing and growing your site." msgstr "Check out these top-rated tools for securing, optimizing and growing your site." #: assets/js/crosssellpage.js:1249 assets/js/crosssellpage.js:1254 #: assets/js/crosssellpage.min.js:1 assets/js/crosssellpage.js:697 #: assets/js/crosssellpage.js:703 msgid "4.9/5" msgstr "4.9/5" #: assets/js/crosssellpage.js:1250 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:698 msgid "3,002" msgstr "3,002" #: assets/js/crosssellpage.js:1255 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:704 msgid "1,746" msgstr "1,746" #: assets/js/crosssellpage.js:1259 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:709 msgid "5/5" msgstr "5/5" #: assets/js/crosssellpage.js:1260 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:710 msgid "592" msgstr "592" #: assets/js/crosssellpage.js:1302 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:720 msgid "About WPMU DEV" msgstr "About WPMU DEV" #: assets/js/crosssellpage.js:1309 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:722 msgid "Made for web developers, by web developers" msgstr "Made for web developers, by web developers" #: assets/js/crosssellpage.js:1324 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:730 msgid "Since 2006, our award-winning WordPress plugins, hosting, world beating support and site management tools have helped hundreds of thousands of web developers, freelancers and agencies run and grow their businesses. " msgstr "Since 2006, our award-winning WordPress plugins, hosting, world beating support and site management tools have helped hundreds of thousands of web developers, freelancers and agencies run and grow their businesses. " #: assets/js/crosssellpage.js:1336 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:739 msgid "Learn more" msgstr "Learn more" #: assets/js/crosssellpage.js:1370 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:753 msgid "Everything Wordpress found in one place. " msgstr "Everything Wordpress found in one place. " #: assets/js/crosssellpage.js:1382 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:762 msgid "WPMU DEV" msgstr "WPMU DEV" #: assets/js/crosssellpage.js:1394 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:781 msgid "Trusted by 60,000+ businesses worldwide" msgstr "Trusted by 60,000+ businesses worldwide" #: assets/js/crosssellpage.js:1397 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:788 msgid "67,233,282 plugin downloads" msgstr "67,233,282 plugin downloads" #: assets/js/crosssellpage.js:1400 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:792 msgid "7,260+ 5 star reviews" msgstr "7,260+ 5 star reviews" #: assets/js/crosssellpage.js:2168 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1447 msgid " plugin has been installed successfully!" msgstr " plugin has been installed successfully!" #: assets/js/crosssellpage.js:2227 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1495 msgid " plugin has been activated successfully!" msgstr " plugin has been activated successfully!" #: assets/js/crosssellpage.js:2321 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1551 msgid "Installing..." msgstr "Installing..." #: assets/js/crosssellpage.js:2321 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1552 msgid "Install" msgstr "Install" #: assets/js/crosssellpage.js:2332 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1562 msgid "Activating..." msgstr "Activating..." #: assets/js/crosssellpage.js:2332 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1563 msgid "Activate" msgstr "Activate" #: assets/js/crosssellpage.js:2343 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1571 msgid "Active" msgstr "Active" #: assets/js/crosssellpage.js:2452 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1627 msgid "Try our other highly-rated free WordPress plugins" msgstr "Try our other highly-rated free WordPress plugins" #: assets/js/crosssellpage.js:2453 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1631 msgid "From security to SEO to marketing, we’ve got you covered." msgstr "From security to SEO to marketing, we’ve got you covered." #: assets/js/crosssellpage.js:2499 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1663 msgid "Get a high-powered web-building suite, at no extra cost" msgstr "Get a high-powered web-building suite, at no extra cost" #: assets/js/crosssellpage.js:2548 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1683 msgid "View all pro plugins" msgstr "View all pro plugins" #: assets/js/crosssellpage.js:2602 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1733 msgid "Fully managed hosting" msgstr "Fully managed hosting" #: assets/js/crosssellpage.js:2603 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1734 msgid "Deliver lightning-fast, secure websites with a 99.9% SLA—complete with isolated environments, global server locations, and expert support." msgstr "Deliver lightning-fast, secure websites with a 99.9% SLA—complete with isolated environments, global server locations, and expert support." #: assets/js/crosssellpage.js:2609 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1742 msgid "Site management" msgstr "Site management" #: assets/js/crosssellpage.js:2610 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1743 msgid "Take command of all your WordPress sites with one simple dashboard to automate updates, monitor performance, and generate white-label client reports." msgstr "Take command of all your WordPress sites with one simple dashboard to automate updates, monitor performance, and generate white-label client reports." #: assets/js/crosssellpage.js:2616 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1751 msgid "Domains" msgstr "Domains" #: assets/js/crosssellpage.js:2617 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1752 msgid "With access to over 250 TLDs, seamless integration and free privacy protection, you can offer domain registration services at unbeatable prices." msgstr "With access to over 250 TLDs, seamless integration and free privacy protection, you can offer domain registration services at unbeatable prices." #: assets/js/crosssellpage.js:2623 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1760 msgid "100+ template library" msgstr "100+ template library" #: assets/js/crosssellpage.js:2624 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1761 msgid "Create beautiful sites in seconds with pre-configured site templates for you and your client projects—compatible with every plugin, theme builder and tool." msgstr "Create beautiful sites in seconds with pre-configured site templates for you and your client projects—compatible with every plugin, theme builder and tool." #: assets/js/crosssellpage.js:2630 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1769 msgid "Get Pro Email" msgstr "Get Pro Email" #: assets/js/crosssellpage.js:2631 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1770 msgid "Add-on private, ad-free IMAP webmail for easily managed, auto-synced, professional emails for you and your clients with 5-50GB storage options." msgstr "Add-on private, ad-free IMAP webmail for easily managed, auto-synced, professional emails for you and your clients with 5-50GB storage options." #: assets/js/crosssellpage.js:2637 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1778 msgid "Unparalleled support" msgstr "Unparalleled support" #: assets/js/crosssellpage.js:2638 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1779 msgid "Chat with our support team anytime—24/7, 365 days a year—with an average response time of 2 minutes. We’ll even log in and fix issues for you and your clients!" msgstr "Chat with our support team anytime—24/7, 365 days a year—with an average response time of 2 minutes. We’ll even log in and fix issues for you and your clients!" #: assets/js/crosssellpage.js:2645 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1790 msgid "Everything you need to grow a successful agency - at an unrivaled value" msgstr "Everything you need to grow a successful agency - at an unrivaled value" #: assets/js/crosssellpage.js:2737 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1835 msgid "On-Demand Development" msgstr "On-Demand Development" #: assets/js/crosssellpage.js:2738 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1836 msgid "Need assistance with CSS or custom functionality? Our experts create scripts to solve WordPress issues and enhance your site." msgstr "Need assistance with CSS or custom functionality? Our experts create scripts to solve WordPress issues and enhance your site." #: assets/js/crosssellpage.js:2744 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1844 msgid "Proactive Monitoring" msgstr "Proactive Monitoring" #: assets/js/crosssellpage.js:2745 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1845 msgid "We monitor WPMU DEV hosted sites 24/7 and fix them fast if they go down, you don’t have to do anything." msgstr "We monitor WPMU DEV hosted sites 24/7 and fix them fast if they go down, you don’t have to do anything." #: assets/js/crosssellpage.js:2751 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1853 msgid "Speed Optimization" msgstr "Speed Optimization" #: assets/js/crosssellpage.js:2752 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1854 msgid "Page Speed lagging behind? Our experts will give your site a guaranteed scores of 90+ on desktop and 75+ on mobile." msgstr "Page Speed lagging behind? Our experts will give your site a guaranteed scores of 90+ on desktop and 75+ on mobile." #: assets/js/crosssellpage.js:2758 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1862 msgid "Malware Removal" msgstr "Malware Removal" #: assets/js/crosssellpage.js:2759 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1863 msgid "Need help with CSS or custom functionality? Our experts create scripts to solve WordPress issues and enhance your site." msgstr "Need help with CSS or custom functionality? Our experts create scripts to solve WordPress issues and enhance your site." #: assets/js/crosssellpage.js:2766 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1874 msgid "Expert services" msgstr "Expert services" #: assets/js/crosssellpage.js:2768 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1876 msgid "Hand off any site issues to our WordPress expert team’s 20+ years of expertise." msgstr "Hand off any site issues to our WordPress expert team’s 20+ years of expertise." #: assets/js/crosssellpage.js:2777 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1882 msgid "Add-on services" msgstr "Add-on services" #: assets/js/crosssellpage.js:2890 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1939 msgid "Find your plan (From $3/m <highlight>$15/m</highlight>)" msgstr "Find your plan (From $3/m <highlight>$15/m</highlight>)" #: assets/js/crosssellpage.js:2910 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1952 msgid "Did you know WPMU DEV Membership includes " msgstr "Did you know WPMU DEV Membership includes " #: assets/js/crosssellpage.js:2911 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1956 msgid "ALL our Pro Plugins?" msgstr "ALL our Pro Plugins?" #: assets/js/crosssellpage.js:2912 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1960 msgid "Plus, everything your agency needs for hosting, reselling and more." msgstr "Plus, everything your agency needs for hosting, reselling and more." #: assets/js/crosssellpage.js:2983 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1979 msgid "Your comprehensive toolkit for WordPress website management." msgstr "Your comprehensive toolkit for WordPress website management." #: assets/js/crosssellpage.js:3018 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:1989 msgid "30 day guarantee: If you don’t love it, we’ll give you your money back - no questions asked." msgstr "30 day guarantee: If you don’t love it, we’ll give you your money back - no questions asked." #: assets/js/crosssellpage.js:3085 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:2020 msgid "Free Plugins you’ll like" msgstr "Free Plugins you’ll like" #: assets/js/crosssellpage.js:3092 assets/js/crosssellpage.min.js:1 #: assets/js/crosssellpage.js:2023 msgid "Pro Plugins You’ll LOVE (Bundle Deal)" msgstr "Pro Plugins You’ll LOVE (Bundle Deal)" external/plugins-cross-sell-page/plugin-cross-sell.php 0000644 00000005610 15252476777 0017170 0 ustar 00 <?php /** * WPMUDEV Plugin Cross-Sell module for free plugins. * * Used in free plugins to get a glimpse of other plugins offered by WPMU DEV. * * @since 1.0.0 * @author Panos Lyrakis * @link https://wpmudev.com * @package WPMUDEV\Plugin_Cross_Sell */ namespace WPMUDEV\Modules; // If this file is called directly, abort. if ( ! defined( 'WPINC' ) ) { die; } // Support for site-level autoloading. if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) { require_once __DIR__ . '/vendor/autoload.php'; } // Sub-module version. if ( ! defined( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_VERSION' ) ) { define( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_VERSION', '1.0.0' ); } // Sub-module directory. if ( ! defined( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_DIR' ) ) { define( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_DIR', plugin_dir_path( __FILE__ ) ); } // Sub-module url. if ( ! defined( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_URL' ) ) { define( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_URL', plugin_dir_url( __FILE__ ) ); } // Sub-module Assets url. if ( ! defined( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_ASSETS_URL' ) ) { define( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_ASSETS_URL', untrailingslashit( WPMUDEV_MODULE_PLUGIN_CROSS_SELL_URL ) . '/assets' ); } // Shared UI Version. if ( ! defined( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_SUI_VERSION' ) ) { define( 'WPMUDEV_MODULE_PLUGIN_CROSS_SELL_SUI_VERSION', '2.12.24' ); } /** * Sub-module Cross-Sell class. * * @since 1.0.0 */ if ( ! class_exists( 'WPMUDEV\Modules\Plugin_Cross_Sell' ) ) { /** * Module main class. */ final class Plugin_Cross_Sell { /** * The DI container. * * @var Plugin_Cross_Sell\Container */ private $container = null; /** * Initialize the module. * * @param array $props Module properties. * @since 1.0.0 * * @return void */ public function __construct( $props = array() ) { // Prepare the translation directory. $dir = ! empty( $props['translation_dir'] ) ? realpath( $props['translation_dir'] ) : false; $props['translation_dir'] = $dir ? wp_normalize_path( $dir ) : WPMUDEV_MODULE_PLUGIN_CROSS_SELL_DIR . 'languages/'; // Self-initialization of DI container. $this->container = new Plugin_Cross_Sell\Container(); $this->container->set( 'submenu_data', $props ); $this->container->set( 'utilities', new Plugin_Cross_Sell\Utilities() ); $this->load(); } /** * Class initializer. */ public function load() { $submenu_params = $this->container->get( 'submenu_data' ); $translation_dir = ! empty( $submenu_params['translation_dir'] ) ? $submenu_params['translation_dir'] : WPMUDEV_MODULE_PLUGIN_CROSS_SELL_DIR . 'languages/'; load_plugin_textdomain( 'plugin-cross-sell-textdomain', false, $translation_dir ); // Create a new Loader instance and pass the DI container. $loader = new Plugin_Cross_Sell\Loader( $this->container ); $loader->init(); } } } external/plugins-cross-sell-page/assets/images/smartcrawl-seo.png 0000644 00000002407 15252476777 0021316 0 ustar 00 �PNG IHDR ( ) G�k� pHYs �� sRGB ��� gAMA ���a �IDATx͙OLU�ov�l��46� ��6iI���� �{ж�(��ڪ-%Zj"�ޤU��J��xЃI7z�iMd��KK 1�2��vw���cg�Ζ�a��&d�̼7���y��P��P�|"7Z"-<�iN���: HW�{���ܹ�O���I3���?z<[��6In ����q�ى @�Ê!E�2X�0��i�[2��6B�&u����)��P ��� �҆�A�,E�/��ŗD)��M�AFo�����,`|G�c�a�IU AJWx|!�vv���"U��2H)8�=��>���EW��+�Bǩ�ľ'��x[�"�� ߾��ݵ��@�<������.�Fk|QJ� �Ա�`��^;F����m���u���fϝ�����T�a�H�%�N'a����.��%h�ן(Ϳ��F5�{Ȼs/�M�dAo�'�8Ԩ�'�%�,�B7=t���4�xOZ�J[�o�;'�������Ɂ4 ڞ`X�a��N}I��_�/X���v��幙��|�`s�oR[,��lY﮽��[Ou�g}τ�tO?���#�l �\�� K8�jc��5��i9�f�YQ��w?����,�#�4�,�*ja�ą/d;��TQ@/G%����b8�� %cWeD�k������BvdPm\�BDk��X�A���rI_�!��b�f�Hۄ{��Ǯ���RVpj���Ojn�d�'��� j+TӾ�,����Zi�Kj�ّ�)6+�۷全�:u�3K�e�80�rPr� U�mf�rpjp5�tDN3 ��%� ^I/V��&@@��8!?n�p�`<��9&�N�m@��iE�d%XW.�W>�-nZ�Xp ���X������Wh�J� �4�H8�L�t?�ͅ� w��N�G�!�b1��LӃ'�`e�3�Q]fZ���Ղ�!�́���R�:�?��i���3�¨���O�J?���UN-[���pB�W��j21(�>�:�(�є��%�|�K�!w�L���k��L�S�Id�r{3UdE�{#m㋇��%j5�:�J�-fPToo�0O�.�z�&��+|�Ψy����P��M�$�rTR�ñ�I�ѻx�� �4��{ގb�̵�B�d��_���J��jnVZ� ���R��ɩ� �h%72����_�T͐����Vl2R��X# IEND�B`� external/plugins-cross-sell-page/assets/images/wp-smushit.png 0000644 00000002661 15252476777 0020475 0 ustar 00 �PNG IHDR ( ) G�k� pHYs �� sRGB ��� gAMA ���a FIDATx�YKo[E��^?b�q�&�4Q+�Pm� �H�$�X���UWM�u� V,)ˊE�B �,�h� ��&jܤ~�w�Ό�;ߛ4��dy��9�o��D|2�Eh?h��To,妋h� ��4]�B�I p����!)E�sa���b���XƸ*-9r`R�dMq���ݑ`|"9P�+.F�h0O���F,Q_a�q�����-::r��2�M�]_Q� H)?��Art�?���� �w����`�g_���\]b&w����?~+���^������F�Z'MP��JMλ����Sr� �7` ��%))[��^�� @,z�4��Dn2�L��Ub��[�6J��/��[��{�\�\�=�V��y������r��r�!�ϓh���+�Z�L��۫���n��{M����e��+*�vl��E��If�V]��FA��>�lr˅2��-��~���u��K�~=�B�c��86#�@tr�^��s�ـ�U3k�X奼V.���!� �ӆX.,U���&��z�>�9�Su[��2E�^/P��F9��q�ɿ�ȟ����]~=�Ŝ�a,���!z5�W�¥���z�fc�m�l�g�B��g:֙��#�5V�;�����a�� ����>&�6�!J<H�A,�n��ݬ~�Azs���3p+��?�R%wc9��p/]��JF�m�n����?�z��Q+��0 # ��tMFء `߭fU��ͺ�]���ـv�+�^�䮜>N�w����cˏ�˅G�,ܶ�է�.����{H��Y���Oы!_M{�x�L��j҇��*���ʤ �WG7*˂�6+˝�,V.��!6G�V��G9U7�q�>��Pu�j�o<����9]vF�Q"����v3�� |�\��Ƙ�(��v������vb�M1x2H]-5�XE�� a���2�����O� ��Vԃ<�s�K�ƂA�n���$oqH�p/��P��[۾�:W�Р�%0�xhv�K�^F�f7t�]{�]�Mr~�v�M��L�U������.��� F��� 3yE��Bȯ���t�L��m���f���_���r����G~@p��V������0��-5�pr=O7�[��Tn�m#0�i�2��}�q ��L��Lѕv�)d�JPnσ� 5��hL?K�)5�М���RQP���;(V�i�ܤ��8����y/� �!�a�F�?^��������I~� V��i(�1��۶:���0����]���zr�jTr$�e�p0�b_V�GO���2�� �M��KL�5%�5m�躓�L��lnV�c�{ IEND�B`� external/plugins-cross-sell-page/assets/images/hummingbird-performance.png 0000644 00000002073 15252476777 0023156 0 ustar 00 �PNG IHDR ( ) G�k� pHYs �� sRGB ��� gAMA ���a �IDATx͙=LA�s���o�F��#j���F嬌�Z\��&~\��A*(������ ��3�A%P@�����DQ��wv��8��=8��\fvg��ϛ���= D�G�K���P��D!/��p%�]GHUB# ��,2kI��pBÏ@c��;"��*�ʡ@5 ��� U�ŝ�B�f�EP(��xW[�HK�Ո�:K'Ν5�[ݹ3PY�=�z�u\��`/��С��*�>����p+�����Ό@����-�9f�ĺ�` t�|锅��W(�0�������u9��q�i��GN��� 0�\��0�Ƈ�D#0��P�����i�!�����> l�g�l)�S�ӂ�Ј(]wNc��w����e��6p2����v�mmίr�7+pR�Yn�h=2>h��&#��|~%e��� ��.U˒�M�D��L��?&U�]j^w7�P�HFel����_^�OVā��GzMk�Y�Z� $�}�b�Tk�ޛ�E1gn,��}Ĵ�>�Y�[y1�n��e���Un�g.������=��O`���b���բ�{�� aXZ���'ПSa�>��<ߴ��l�sv-}p��iG�9.R�Kϊ9�=�>��M-��C���Z�K�Ȧ`�]d�Q�������V��s��'�����ߓ@�)�e���P��h�"(\`r^D��Q�/0${�]&���"�z'��Z��%�8B��R�Z�v�Eo�~\!�Gs1� `4�����3�@�a��� ?���ӓ���4z$�N:Q���47w��U�ѧL��� ��lEd�WZJ K1���$<\M)�� YQ�Mj3��Mً�4+�W�_�L�*\�����BI�I�l}쵷�N3�r*�Œ���Ȓ㖩�/J�J����Y�)��͑@͚����X���<�bb��J��&�O�/���+%�� ����Pk6�>� IEND�B`� external/plugins-cross-sell-page/assets/images/broken-link-checker.png 0000644 00000002147 15252476777 0022171 0 ustar 00 �PNG IHDR ( ) G�k� pHYs �� sRGB ��� gAMA ���a �IDATx�YKOQ=S�e1�h0��nL������čE7>b��K� ���(Qd�p�v 1��T��hL�����;w:ô��L*�$ý�{�s�=��~5θ M�BsvW!�ƶ'��n � �Ӳ�\ !h��&�5�o��EгT/P���@�9��6#�1Ag,-Ćefag� ��oO�n$�u�����#�Y�q��ը��G�!vʒ��`�M��Y4J6�y��.G� 1�%�O�y�L��8�����[��E�Aq2�<�Ϻ��lګ���?�Jv����:a��U5&g&Q$2�b�BDX�0x�k�7Z>sqݺ&:�`9�&d�����<�Ҷg����n;����$��ln��� ��aum���>!�b�pߩ�����������Z|���8hBLd塩�V��Q����G��<���8�-��ۂ�<Z�$�gصDo�i�6�C��U�o����\033���L��xZ%�����$�ۨ�� -�.�h�)���ŪX�Κ*�;�65'H�k~J��-�&HˑG�Yx�]�<a?�� �J�{���Э>����H�ֳܴ��)qI�ׅ6�T�A5-.kt5��R��;۪J!��%n�}��{��v���$���ճ0��>��0 &��{��em����V�{YƸƀ���D�}P��N �C���@���b$g�dk���J(d+��g}Fi&"�v�T�}���"�D�9_?ï��=�� �O4Թ��U&