* * @copyright (c) 2017, Incsub (http://incsub.com) */ namespace Smush\Core; use finfo; use Smush\Core\Media\Media_Item_Cache; use Smush\Core\Media\Media_Item_Stats; use Smush\Core\Png2Jpg\Png2Jpg_Optimization; use WP_Smush; use WDEV_Logger; if ( ! defined( 'WPINC' ) ) { die; } /** * Class Helper */ class Helper { /** * Temporary cache. * * We use this instead of WP_Object_Cache to avoid save data to memory cache (persistent caching). * * And to avoid it take memory space, we also reset the group cache each we get a new key, * it means one group only has one key. * It's useful when we want to save result a function. * * Leave group is null to set and get the value by unique key. * * It's useful to avoid checking something multiple times. * * @since 3.9.6 * * @var array */ private static $temp_cache = array(); /** * WPMUDEV Logger lib. * * @access private * * @var null|WDEV_Logger */ private static $logger; /** * Get WDEV_Logger instance. * * @return WDEV_Logger */ public static function logger() { if ( null === self::$logger ) { $swiched_blog = false; // On MU site, move all log files into the log folder [wp-content/uploads/smush] on the main site. if ( is_multisite() && ! is_main_site() ) { switch_to_blog( get_main_site_id() ); $swiched_blog = true; } $upload_dir = wp_get_upload_dir(); $log_dir = 'smush'; if ( false !== strpos( $upload_dir['basedir'], WP_CONTENT_DIR ) ) { $log_dir = str_replace( trailingslashit( WP_CONTENT_DIR ), '', $upload_dir['basedir'] ) . '/smush'; } if ( $swiched_blog ) { restore_current_blog(); } self::$logger = WDEV_Logger::create( array( 'log_dir' => $log_dir, 'is_private' => true, 'modules' => array( 'smush' => array( 'is_global_module' => true, ), 'cdn' => array(), 'lazy' => array(), 'webp' => array(), 'png2jpg' => array(), 'resize' => array(), 'dir' => array(), 'backup' => array(), 'api' => array(), 'integrations' => array(), ), ) ); } return self::$logger; } /** * Clean file path. * * @param string $file File path. * @return string */ public static function clean_file_path( $file ) { return str_replace( WP_CONTENT_DIR, '', $file ); } /** * Get value from temporary cache. * * @param string $key Key name. * @param string|null $group Group name. * @param mixed $default Default value, default is NULL. * * Uses: * if( null !== Helper::cache_get( 'your_key', 'your_group' ) ){ * // Do your something with temporary cache value. * } * // Maybe setting it with Helper::cache_set. * * @since 3.9.6 * * @return mixed The cached result. */ public static function cache_get( $key, $group = null, $default = null ) { // Add current blog id to support MU site. $current_blog_id = get_current_blog_id(); // Get cache for current blog. $temp_cache = array(); if ( isset( self::$temp_cache[ $current_blog_id ] ) ) { $temp_cache = self::$temp_cache[ $current_blog_id ]; } /** * Add a filter to force cache. * It might be helpful when we debug. */ if ( apply_filters( 'wp_smush_force_cache', false, $key, $group, $temp_cache ) ) { $locked_groups = array( // Required for cache png2jpg()->can_be_converted() before resizing. 'png2jpg_can_be_converted', // Required for cache unique file name of png2jpg()->convert_to_jpg(). 'convert_to_jpg', ); if ( ! in_array( $group, $locked_groups, true ) ) { return null; } } $value = $default; if ( isset( $group ) ) { if ( isset( $temp_cache[ $group ][ $key ] ) ) { $value = $temp_cache[ $group ][ $key ]; } elseif ( isset( $temp_cache[ $group ] ) ) { // Get a new key, reset group. unset( $temp_cache[ $group ] ); } } elseif ( isset( $temp_cache[ $key ] ) ) { // Get the value by key. $value = $temp_cache[ $key ]; } return $value; } /** * Save value to temporary cache. * * @since 3.9.6 * * @param string $key Key name. * @param mixed $value Data to cache. * @param string|null $group Group name. * * Note, we return the provided value to use it inside some methods. * @return mixed Returns the provided value. */ public static function cache_set( $key, $value, $group = null ) { // Add current blog id to support MU site. $current_blog_id = get_current_blog_id(); if ( isset( $group ) ) { // Reset group and set the value. self::$temp_cache[ $current_blog_id ][ $group ] = array( $key => $value ); } else { // Save value by unique key. self::$temp_cache[ $current_blog_id ][ $key ] = $value; } return $value; } /** * Clear cache by group or key. * * @since 3.9.6 * * @param string $cache_key Group name or unique key name. */ public static function cache_delete( $cache_key ) { // Add current blog id to support MU site. $current_blog_id = get_current_blog_id(); // Delete temp cache by cache key. if ( isset( $cache_key, self::$temp_cache[ $current_blog_id ][ $cache_key ] ) ) { unset( self::$temp_cache[ $current_blog_id ][ $cache_key ] ); } return true; } /** * Get mime type for file. * * @since 3.1.0 Moved here as a helper function. * * @param string $path Image path. * * @return bool|string */ public static function get_mime_type( $path ) { // These mime functions only work on local files/streams. if ( ! stream_is_local( $path ) ) { return false; } // Get the File mime. if ( class_exists( 'finfo' ) ) { $file_info = new finfo( FILEINFO_MIME_TYPE ); } else { $file_info = false; } if ( $file_info ) { $mime = file_exists( $path ) ? $file_info->file( $path ) : ''; } elseif ( function_exists( 'mime_content_type' ) ) { $mime = mime_content_type( $path ); } else { $mime = false; } return $mime; } /** * Filter the Posts object as per mime type. * * @param array $posts Object of Posts. * * @return array Array of post IDs. */ public static function filter_by_mime( $posts ) { if ( empty( $posts ) ) { return $posts; } foreach ( $posts as $post_k => $post ) { if ( ! isset( $post->post_mime_type ) || ! in_array( $post->post_mime_type, Core::$mime_types, true ) ) { unset( $posts[ $post_k ] ); } else { $posts[ $post_k ] = $post->ID; } } return $posts; } /** * Iterate over PNG->JPG Savings to return cummulative savings for an image * * @param string $attachment_id Attachment ID. * * @return array */ public static function get_pngjpg_savings( $attachment_id = '' ) { $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); $png2jpg_optimization = new Png2Jpg_Optimization( $media_item ); $stats = $png2jpg_optimization->is_optimized() ? $png2jpg_optimization->get_stats() : new Media_Item_Stats(); return $stats->to_array(); } /** * Get the link to the media library page for the image. * * @since 2.9.0 * * @param int $id Image ID. * @param string $name Image file name. * @param bool $src Return only src. Default - return link. * * @return string */ public static function get_image_media_link( $id, $name, $src = false ) { $mode = get_user_option( 'media_library_mode' ); if ( 'grid' === $mode ) { $link = admin_url( "upload.php?item=$id" ); } else { $link = admin_url( "post.php?post=$id&action=edit" ); } if ( ! $src ) { return "$name"; } return $link; } /** * Returns current user name to be displayed * * @return string */ public static function get_user_name() { $current_user = wp_get_current_user(); return ! empty( $current_user->first_name ) ? $current_user->first_name : $current_user->display_name; } /** * Allows to filter the error message sent to the user * * @param string $error Error message. * @param string $attachment_id Attachment ID. * * @return mixed|null|string */ public static function filter_error( $error = '', $attachment_id = '' ) { if ( empty( $error ) ) { return null; } /** * Replace the 500 server error with a more appropriate error message. */ if ( false !== strpos( $error, '500 Internal Server Error' ) ) { $error = esc_html__( "Couldn't process image due to bad headers. Try re-saving the image in an image editor, then upload it again.", 'wp-smushit' ); } elseif ( strpos( $error, 'timed out' ) ) { $error = esc_html__( "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' ); } /** * Used internally to modify the error message */ return apply_filters( 'wp_smush_error', $error, $attachment_id ); } /** * Format metadata from $_POST request. * * Post request in WordPress will convert all values * to string. Make sure image height and width are int. * This is required only when Async requests are used. * See - https://wordpress.org/support/topic/smushit-overwrites-image-meta-crop-sizes-as-string-instead-of-int/ * * @since 2.8.0 * * @param array $meta Metadata of attachment. * * @return array */ public static function format_meta_from_post( $meta = array() ) { // Do not continue in case meta is empty. if ( empty( $meta ) ) { return $meta; } // If metadata is array proceed. if ( is_array( $meta ) ) { // Walk through each items and format. array_walk_recursive( $meta, array( self::class, 'format_attachment_meta_item' ) ); } return $meta; } /** * If current item is width or height, make sure it is int. * * @since 2.8.0 * * @param mixed $value Meta item value. * @param string $key Meta item key. */ public static function format_attachment_meta_item( &$value, $key ) { if ( 'height' === $key || 'width' === $key ) { $value = (int) $value; } /** * Allows to format single item in meta. * * This filter will be used only for Async, post requests. * * @param mixed $value Meta item value. * @param string $key Meta item key. */ $value = apply_filters( 'wp_smush_format_attachment_meta_item', $value, $key ); } /** * Check to see if file is animated. * * @since 3.0 Moved from class-resize.php * @since 3.9.6 Add a new param $mime_type. * * @param string $file_path Image file path. * @param int $id Attachment ID. * @param false|string $mime_type Mime type. * * @return bool|int */ public static function check_animated_status( $file_path, $id, $mime_type = false ) { $media_item = Media_Item_Cache::get_instance()->get( $id ); return $media_item->is_animated(); } public static function check_animated_file_contents( $file_path ) { $filecontents = file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $str_loc = 0; $count = 0; // There is no point in continuing after we find a 2nd frame. while ( $count < 2 ) { $where1 = strpos( $filecontents, "\x00\x21\xF9\x04", $str_loc ); if ( false === $where1 ) { break; } else { $str_loc = $where1 + 1; $where2 = strpos( $filecontents, "\x00\x2C", $str_loc ); if ( false === $where2 ) { break; } else { if ( $where2 === $where1 + 8 ) { $count ++; } $str_loc = $where2 + 1; } } } return $count > 1; } /** * Verify the file size limit. * * @param int $attachment_id Attachment ID. * * Note: We only use this method to verify an image before smushing it, * we still need to verify the file size of every thumbnail files while smushing them. * * @return bool|int Return the file size if the size limit is exceeded, otherwise return FALSE. */ public static function size_limit_exceeded( $attachment_id ) { $original_file_path = self::get_attached_file( $attachment_id, 'original' ); if ( ! file_exists( $original_file_path ) ) { $original_file_path = self::get_attached_file( $attachment_id ); } if ( ! file_exists( $original_file_path ) ) { return false; } $max_file_size = WP_Smush::is_pro() ? WP_SMUSH_PREMIUM_MAX_BYTES : WP_SMUSH_MAX_BYTES; $file_size = filesize( $original_file_path ); return $file_size > $max_file_size ? $file_size : false; } /** * Original File path * * @param string $original_file Original file. * * @return string File Path */ public static function original_file( $original_file = '' ) { $uploads = wp_get_upload_dir(); $upload_path = $uploads['basedir']; return path_join( $upload_path, $original_file ); } /** * Gets the WPMU DEV API key. * * @since 3.8.6 * * @return string|false */ public static function get_wpmudev_apikey() { // If API key defined manually, get that. if ( defined( 'WPMUDEV_APIKEY' ) && WPMUDEV_APIKEY ) { return WPMUDEV_APIKEY; } // If dashboard plugin is active, get API key from db. if ( class_exists( 'WPMUDEV_Dashboard' ) ) { return get_site_option( 'wpmudev_apikey' ); } return false; } /** * Get upsell URL. * * @since 3.9.1 * * @param string $utm_campaign Campaing string. * * @return string */ public static function get_url( $utm_campaign = '' ) { $upgrade_url = 'https://wpmudev.com/project/wp-smush-pro/'; return add_query_arg( array( 'utm_source' => 'smush', 'utm_medium' => 'plugin', 'utm_campaign' => $utm_campaign, ), $upgrade_url ); } /** * Get Smush page URL. * * @param string $page Page URL. * * @return string */ public static function get_page_url( $page = 'smush-bulk' ) { if ( is_multisite() && is_network_admin() ) { return network_admin_url( 'admin.php?page=' . $page ); } return admin_url( 'admin.php?page=' . $page ); } /** * Get the extension of a file. * * @param string $file File path or file name. * @param string $expected_ext The expected extension. * * @return bool|string Returns extension of the file, or false if it's not the same as the expected extension. */ public static function get_file_ext( $file, $expected_ext = '' ) { $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ); if ( ! empty( $expected_ext ) ) { return $expected_ext === $ext ? $ext : false; } else { return $ext; } } /** * Returns TRUE if the current request is REST API but is not media endpoint. * * @since 3.9.7 */ public static function is_non_rest_media() { static $is_not_rest_media; if ( null === $is_not_rest_media ) { $is_not_rest_media = false; // We need to check if this call originated from Gutenberg and allow only media. if ( ! empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) { $route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] ); // Only allow media routes. if ( empty( $route ) || '/wp/v2/media' !== $route ) { // If not - return image metadata. $is_not_rest_media = true; } } } return $is_not_rest_media; } /** * Checks if user is allowed to perform the ajax actions. * As previous we allowed for logged in user, so add a hook filter to allow * user can custom the capability. It might also helpful when user custom admin menu via Branda. * * @since 3.13.0 * * @param string $capability Capability default is manage_options. * @return boolean */ public static function is_user_allowed( $capability = 'manage_options' ) { $capability = empty( $capability ) ? 'manage_options' : $capability; return current_user_can( apply_filters( 'wp_smush_admin_cap', $capability ) ); } /*------ S3 Compatible Methods ------*/ /** * Return unfiltered path for Smush or restore. * * @since 3.9.6 * * @param int $attachment_id Attachment ID. * @param string $type false|original|scaled|smush|backup|resize|check-resize. * @param bool $unfiltered Whether to get unfiltered path or not. * * $type = original|backup => Try to get the original image file path. * $type = false|smush => Get the file path base on the setting "compress original". * $type = scaled|resize => Get the full file path, for large jpg it's scaled file not the original file. * * @return bool|string */ public static function get_raw_attached_file( $attachment_id, $type = 'smush', $unfiltered = false ) { if ( function_exists( 'wp_get_original_image_path' ) ) { if ( 'backup' === $type ) { $type = 'original'; } elseif ( 'resize' === $type || 'check-resize' === $type ) { $type = 'scaled'; } // We will get the original file if we are doing for backup or restore, or smush original file. if ( 'original' === $type || 'scaled' !== $type && Settings::get_instance()->get( 'original' ) ) { $file_path = wp_get_original_image_path( $attachment_id, $unfiltered ); } else { $file_path = get_attached_file( $attachment_id, $unfiltered ); } } else { $file_path = get_attached_file( $attachment_id, $unfiltered ); } return $file_path; } /** * Return file path for Smush, restore or checking resize. * * Add a hook for third party download the file, * if it's not available on the server. * * @param int $attachment_id Attachment ID. * @param string $type false|original|smush|backup|resize * $type = smush|backup => Get the file path and download the attached file if it doesn't exist. * $type = check-resize => Get the file path ( if it exists ), or filtered file path if it doesn't exist. * $type = original => Only get the original file path (not scaled file). * $type = scaled|resize => Get the full file path, for large jpg it's scaled file not the original file. * $type = false => Get the file path base on the setting "compress original". * * @since 3.9.6 Moved S3 to S3 integration. * Add a hook filter to allow 3rd party to custom the result. * * @return bool|string */ public static function get_attached_file( $attachment_id, $type = 'smush' ) { if ( empty( $attachment_id ) ) { return false; } /** * Add a hook to allow 3rd party to custom the result. * * @param null|string $file_path File path or file url(checking resize). * @param int $attachment_id Attachment ID. * @param bool $should_download Should download the file if it doesn't exist. * @param bool $should_real_path Expecting a real file path instead an URL. * @param string $type false|original|smush|backup|resize|scaled|check-resize. * * @usedby Smush\Core\Integrations\S3::get_attached_file */ // If the site is using S3, we only need to download the file when doing smush, backup or resizing. $should_download = in_array( $type, array( 'smush', 'backup', 'resize' ), true ); // But when restoring/smushing we are expecting a real file path. $should_real_path = 'check-resize' !== $type; $file_path = apply_filters( 'wp_smush_get_attached_file', null, $attachment_id, $should_download, $should_real_path, $type ); if ( is_null( $file_path ) ) { $file_path = self::get_raw_attached_file( $attachment_id, $type ); } return $file_path; } /** * Custom for function wp_update_attachment_metadata * We use this method to reset our S3 config before updating the metadata. * * @param int $attachment_id Attachment ID. * @param array $meta Metadata. * @return bool */ public static function wp_update_attachment_metadata( $attachment_id, $meta ) { /** * Fire before calling wp_update_attachment_metadata. * * @param int $attachment_id Attachment ID. * @param array $meta Metadata. * * @hooked Smush\Core\Integrations\S3::release_smush_mode() * This will help we to upload the attachments, and remove them if it's required. */ do_action( 'wp_smush_before_update_attachment_metadata', $attachment_id, $meta ); return wp_update_attachment_metadata( $attachment_id, $meta ); } /** * Check if the file exists on the server or cloud (S3). * * @since 3.9.6 * * @param string|int $file File path or File ID. * @param int|null $attachment_id File ID. * @param bool $should_download Whether to download the file or not. * @param bool $force_cache Whether check for result from the cache for full image or not. * * @return bool */ public static function file_exists( $file, $attachment_id = null, $should_download = false, $force_cache = false ) { // If file is an attachment id we will reset the arguments. // Use is_numeric for common case. if ( $file && is_numeric( $file ) ) { $attachment_id = $file; $file = null; } // If the file path is not empty we will try to check file_exists first. if ( empty( $file ) ) { $file_exists = null; } else { $file_exists = file_exists( $file ); if ( $file_exists ) { return true; } } // Only continue if provided Attachment ID. if ( $attachment_id < 1 ) { return false; } /** * Check if there is a cached for full image. */ if ( null === $file && ! $force_cache ) { // Use different key for the download case. $cache_key = 'helper_file_exists' . intval( $should_download ); $cached_file_exists = self::cache_get( $attachment_id, $cache_key ); if ( null !== $cached_file_exists ) { return $cached_file_exists; } } /** * Add a hook to allow 3rd party to custom the result. * * @param bool|null $file_exists Current status. * @param string|null $file Full file path. * @param int $attachment_id Attachment ID. * @param bool $should_download Whether to download the file if it's missing on the server or not. * * @usedby Smush\Core\Integrations\S3::file_exists_on_s3 */ $file_exists = apply_filters( 'wp_smush_file_exists', $file_exists, $file, $attachment_id, $should_download ); // If it doesn't check and file is null, we will try to get the attached file from $attachment_id to check. if ( is_null( $file_exists ) && ! $file ) { $file = get_attached_file( $attachment_id ); if ( $file ) { $file_exists = file_exists( $file ); } } /** * Cache the result for full image, * It also avoid we download again the not found image when enabling S3. */ if ( isset( $cache_key ) ) { return self::cache_set( $attachment_id, $file_exists, $cache_key ); } return $file_exists; } /** * Check if the file exists, will try to download if it is not on the server (e.g s3). * * @since 3.9.6 * * @param string|int $file File path or File ID. * @param int|null $attachment_id File ID. * * @return bool Returns TRUE if file exists on the server. */ public static function exists_or_downloaded( $file, $attachment_id = null ) { return self::file_exists( $file, $attachment_id, true ); } /** * Check if the file is an image, is supported in Smush and exists, and then cache the result. * * @since 3.9.6 * * @param int|null $attachment_id File ID. * * @return bool|0 Returns TRUE if file is smushable, FALSE If the image does not exist, and 0 is not an image or is not supported */ public static function is_smushable( $attachment_id ) { if ( empty( $attachment_id ) ) { return null;// Nothing to check. } $is_smushable = self::cache_get( $attachment_id, 'is_smushable' ); if ( ! is_null( $is_smushable ) ) { return $is_smushable; } // Set is_smushable is 0 (not false) to detect is not an image or image not found. $is_smushable = 0; $mime = get_post_mime_type( $attachment_id ); if ( apply_filters( 'wp_smush_resmush_mime_supported', in_array( $mime, Core::$mime_types, true ), $mime ) && wp_attachment_is_image( $attachment_id ) ) { $is_smushable = self::file_exists( $attachment_id ); } /** * Cache and returns the result. * Also added a hook for third-party. * * @param bool $is_smushable 0 if is not an image or mime type not supported | TRUE if image exists and otherwise is FALSE. * @param int $attachment_id Attachment ID. * @param array $mime_types List supported mime types. */ return apply_filters( 'wp_smush_is_smushable', self::cache_set( $attachment_id, $is_smushable, 'is_smushable' ), $attachment_id, Core::$mime_types ); } /** * Delete a file path from server and cloud (e.g s3). * * @since 3.9.6 * * @param string|array $file_paths File path or list of file paths to remove. * @param int $attachment_id Attachment ID. * @param bool $only_exists_file Whether to call the action wp_smush_after_remove_file even the file doesn't exits or not. * * Current we only use this method to delete the file when after converting PNG to JPG or after restore, or when delete the files. */ public static function delete_permanently( $file_paths, $attachment_id, $only_exists_file = true ) { if ( empty( $file_paths ) ) { return; } $file_paths = (array) $file_paths; $removed = true; foreach ( $file_paths as $file_path ) { if ( file_exists( $file_path ) ) { if ( ! unlink( $file_path ) ) { $removed = false; // Log the error. self::logger()->error( sprintf( 'Cannot delete file [%s(%d)].', self::clean_file_path( $file_path ), $attachment_id ) ); } } } if ( $removed || ! $only_exists_file ) { /** * Fires after removing a file on server. * * @param int $attachment_id Attachment ID. * @param string|array $file_paths File path or list of file paths. * @param bool $removed Unlink status. */ do_action( 'wp_smush_after_remove_file', $attachment_id, $file_paths, $removed ); } } /*------ End S3 Compatible Methods ------*/ public static function get_image_sizes() { // Get from cache if available to avoid duplicate looping. $sizes = wp_cache_get( 'get_image_sizes', 'smush_image_sizes' ); if ( $sizes ) { return $sizes; } return self::fetch_image_sizes(); } public static function fetch_image_sizes() { global $_wp_additional_image_sizes; $additional_sizes = get_intermediate_image_sizes(); $sizes = array(); if ( empty( $additional_sizes ) ) { return $sizes; } // Create the full array with sizes and crop info. foreach ( $additional_sizes as $_size ) { if ( in_array( $_size, array( 'thumbnail', 'medium', 'large' ), true ) ) { $sizes[ $_size ]['width'] = get_option( $_size . '_size_w' ); $sizes[ $_size ]['height'] = get_option( $_size . '_size_h' ); $sizes[ $_size ]['crop'] = (bool) get_option( $_size . '_crop' ); } elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) { $sizes[ $_size ] = array( 'width' => $_wp_additional_image_sizes[ $_size ]['width'], 'height' => $_wp_additional_image_sizes[ $_size ]['height'], 'crop' => $_wp_additional_image_sizes[ $_size ]['crop'], ); } } // Medium Large. if ( ! isset( $sizes['medium_large'] ) || empty( $sizes['medium_large'] ) ) { $width = (int) get_option( 'medium_large_size_w' ); $height = (int) get_option( 'medium_large_size_h' ); $sizes['medium_large'] = array( 'width' => $width, 'height' => $height, ); } // Set cache to avoid this loop next time. wp_cache_set( 'get_image_sizes', $sizes, 'smush_image_sizes' ); return $sizes; } public static function loopback_supported() { $method_available = class_exists( '\WP_Site_Health' ) && method_exists( '\WP_Site_Health', 'get_instance' ) && method_exists( \WP_Site_Health::get_instance(), 'can_perform_loopback' ); if ( $method_available ) { $loopback = \WP_Site_Health::get_instance()->can_perform_loopback(); return $loopback->status === 'good'; } return true; } public static function get_recheck_images_link() { if ( is_network_admin() ) { // Users can't run re-check images on the network admin side at the moment, @see: SMUSH-369. return ''; } $recheck_images_link = add_query_arg( array( 'smush-action' => 'start-scan-media' ), self::get_page_url( 'smush-bulk' ) ); return $recheck_images_link; } }