: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* Smush core class: Smush class
* @package Smush\Core\Modules
namespace Smush\Core\Modules;
use Smush\Core\Api\Backoff;
use Smush\Core\Api\Request_Multiple;
use Smush\Core\Error_Handler;
use Smush\Core\Media\Media_Item_Cache;
use Smush\Core\Media\Media_Item_Optimizer;
use Smush\Core\Smush\Smusher;
use Smush\Core\Smush\Smush_Optimization;
use Smush\Core\Webp\Webp_Converter;
use Smush\Core\Webp\Webp_Optimization;
if ( ! defined( 'WPINC' ) ) {
class Smush extends Abstract_Module {
const ERROR_SSL_CERT = 'ssl_cert_error';
* Meta key to save smush result to db.
* @var string $smushed_meta_key
public static $smushed_meta_key = 'wp-smpro-smush-data';
* Images dimensions array.
* @var array $image_sizes
public $image_sizes = array();
* Stores the headers returned by the latest API call.
* @var array $api_headers
protected $api_headers = array();
* Prevent third party try to run another smush while it's running.
private $prevent_infinite_loop;
private $request_multiple;
// Update the Super Smush count, after the Smush'ing.
//add_action( 'wp_smush_image_optimised', array( $this, 'update_lists' ), '', 2 );
// Smush image (Auto Smush) when `wp_generate_attachment_metadata` filter is fired.
add_filter( 'wp_generate_attachment_metadata', array( $this, 'smush_image' ), 15, 2 );
//add_action( 'delete_attachment', array( $this, 'delete_images' ), 12 );
// Handle the Async optimisation.
add_action( 'wp_async_wp_generate_attachment_metadata', array( $this, 'wp_smush_handle_async' ) );
add_action( 'wp_async_wp_save_image_editor_file', array( $this, 'wp_smush_handle_editor_async' ), '', 2 );
// Make sure we treat scaled images as additional size.
//add_filter( 'wp_smush_add_scaled_images_to_meta', array( $this, 'add_scaled_to_meta' ), 10, 2 );
// Fix SSL CA certificates issue.
add_action( 'wp_smush_before_smush_file', array( $this, 'fix_ssl_ca_certificate_error' ) );
$this->request_multiple = new Request_Multiple();
$this->backoff = new Backoff();
* Check whether to show warning or not for Pro users, if they don't have a valid install
public function show_warning() {
// If it's a free setup, Go back right away!
if ( ! WP_Smush::is_pro() ) {
// Return. If we don't have any headers.
if ( ! isset( $this->api_headers ) ) {
// Show warning, if function says it's premium and api says not premium.
if ( isset( $this->api_headers['is_premium'] ) && ! (int) $this->api_headers['is_premium'] ) {
* Add/Remove image id from Super Smushed images count.
* @param int $id Image id.
* @param string $op_type Add/remove, whether to add the image id or remove it from the list.
* @param string $key Options key.
* @return bool Whether the Super Smushed option was update or not
public function update_super_smush_count( $id, $op_type = 'add', $key = 'wp-smush-super_smushed' ) {
// Get the existing count.
$super_smushed = get_option( $key, false );
// Initialize if it doesn't exists.
if ( ! $super_smushed || empty( $super_smushed['ids'] ) ) {
// Insert the id, if not in there already.
if ( 'add' === $op_type && ! in_array( $id, $super_smushed['ids'] ) ) {
$super_smushed['ids'][] = $id;
} elseif ( 'remove' === $op_type && false !== ( $k = array_search( $id, $super_smushed['ids'] ) ) ) {
// Else remove the id from the list.
unset( $super_smushed['ids'][ $k ] );
// Reset all the indexes.
$super_smushed['ids'] = array_values( $super_smushed['ids'] );
$super_smushed['timestamp'] = time();
update_option( $key, $super_smushed, false );
* Checks if the image compression is lossy, stores the image id in options table
* @param int $id Image Id.
* @param array $stats Compression Stats.
* @param string $key Meta Key for storing the Super Smushed ids (Optional for Media Library).
* Need To be specified for NextGen.
public function update_lists( $id, $stats, $key = '' ) {
// If Stats are empty or the image id is not provided, return.
if ( empty( $stats ) || empty( $id ) || empty( $stats['stats'] ) ) {
// Update Super Smush count.
if ( isset( $stats['stats']['lossy'] ) && 1 == $stats['stats']['lossy'] ) {
update_post_meta( $id, 'wp-smush-lossy', 1 );
$this->update_super_smush_count( $id, 'add', $key );
// Check and update re-smush list for media gallery.
if ( ! empty( $this->resmush_ids ) && in_array( $id, $this->resmush_ids ) ) {
$this->update_resmush_list( $id );
* Remove the given attachment id from resmush list and updates it to db
* @param string $attachment_id Attachment ID.
* @param string $mkey Option key.
public function update_resmush_list( $attachment_id, $mkey = 'wp-smush-resmush-list' ) {
$resmush_list = get_option( $mkey );
// If there are any items in the resmush list, Unset the Key.
if ( ! empty( $resmush_list ) && count( $resmush_list ) > 0 ) {
$key = array_search( $attachment_id, $resmush_list );
unset( $resmush_list[ $key ] );
$resmush_list = array_values( $resmush_list );
// If Resmush List is empty.
if ( empty( $resmush_list ) || 0 === count( $resmush_list ) ) {
update_option( $mkey, $resmush_list, false );
* Remove the Update info.
* @param bool $remove_notice Remove notice.
public function dismiss_update_info( $remove_notice = false ) {
if ( isset( $_GET['dismiss_smush_update_info'] ) && 1 == $_GET['dismiss_smush_update_info'] ) {
if ( ! empty( $_REQUEST['action'] ) && 'dismiss_update_info' === $_REQUEST['action'] ) {
update_site_option( 'wp-smush-hide_update_info', 1 );
* Check whether to skip a specific image size or not.
* @param string $size Registered image size.
* @return bool Skip the image size or not.
public function skip_image_size( $size ) {
// No image size specified, Don't skip.
$image_sizes = $this->settings->get_setting( 'wp-smush-image_sizes' );
// If image sizes aren't set, don't skip any of the image size.
if ( false === $image_sizes ) {
// Check if the size is in the smush list.
return is_array( $image_sizes ) && ! in_array( $size, $image_sizes, true );
private function validate_file( $file_path ) {
$errors = new WP_Error();
$dir_name = trailingslashit( dirname( $file_path ) );
// Check if file exists and the directory is writable.
if ( empty( $file_path ) ) {
$errors->add( 'empty_path', Error_Handler::get_error_message( 'empty_path' ) );
} elseif ( ! file_exists( $file_path ) || ! is_file( $file_path ) ) {
// Check that the file exists.
/* translators: %s: file path */
$errors->add( 'file_not_found', sprintf( Error_Handler::get_error_message( 'file_not_found' ), basename( $file_path ) ) );
} elseif ( ! is_writable( $dir_name ) ) {
// Check that the file is writable.
/* translators: %s: directory name */
$errors->add( 'not_writable', sprintf( Error_Handler::get_error_message( 'not_writable' ), $dir_name ) );
$file_size = file_exists( $file_path ) ? filesize( $file_path ) : '';
// Check if premium user.
if ( WP_Smush::is_pro() ) {
$max_size = WP_SMUSH_PREMIUM_MAX_BYTES;
$size_limit_code = 'size_pro_limit';
$size_limit_code = 'size_limit';
$max_size = WP_SMUSH_MAX_BYTES;
if ( 0 === (int) $file_size ) {
$errors->add( 'file_not_found', sprintf( Error_Handler::get_error_message( 'file_not_found' ), basename( $file_path ) ) );
} elseif ( $file_size > $max_size ) {
$errors->add( $size_limit_code, sprintf( Error_Handler::get_error_message( $size_limit_code ), size_format( $file_size, 1 ) ), array(
'file_name' => basename( $file_path )
private function smush_parallel( $file_paths, $convert_to_webp = false ) {
foreach ( $file_paths as $file_key => $file_path ) {
$error = $this->validate_file( $file_path );
if ( $error->has_errors() ) {
$file_errors[ $file_key ] = $error;
$requests[ $file_key ] = $this->get_multi_api_request_args( $convert_to_webp, $file_path );
// Send off the valid paths to the API
$this->request_multiple->do_requests( $requests, array(
'timeout' => WP_SMUSH_TIMEOUT,
'user-agent' => WP_SMUSH_UA,
'complete' => function ( $response, $response_key ) use ( &$requests, &$responses, &$retry, $file_paths, $convert_to_webp ) {
$requests[ $response_key ] = null;
$file_path = $file_paths[ $response_key ];
if ( $this->should_retry_smush( $response ) ) {
$retry[ $response_key ] = $file_path;
$responses[ $response_key ] = $this->handle_response(
// Retry failures with exponential backoff
foreach ( $retry as $retry_key => $retry_file_path ) {
$responses[ $retry_key ] = $this->do_smushit(
return array_merge( $responses, $file_errors );
private function smush_sequential( $file_paths, $convert_to_webp = false ) {
foreach ( $file_paths as $file_size => $file_path ) {
$responses[ $file_size ] = $this->do_smushit( $file_path, $convert_to_webp, WP_SMUSH_RETRY_ATTEMPTS );
* @param $convert_to_webp
private function get_multi_api_request_args( $convert_to_webp, $file_path ) {
'url' => $this->get_api_url(),
'headers' => $this->get_api_request_headers( $convert_to_webp ),
'data' => file_get_contents( $file_path ),
* Process an image with Smush.
* @since 3.8.0 Added new param $convert_to_webp.
* @param string $file_path Absolute path to the image.
* @param bool $convert_to_webp Convert the image to webp.
* @param int $retries Number of times to retry the operation
* @return array|bool|WP_Error
public function do_smushit( $file_path = '', $convert_to_webp = false, $retries = 0 ) {
// TODO: (stats refactor) handle properly
return $this->do_smushit_optimization( $file_path, $convert_to_webp, $retries );
$errors = $this->validate_file( $file_path );
if ( count( $errors->get_error_messages() ) ) {
sprintf( 'Skipped file [%s] due to error:', Helper::clean_file_path( $file_path ) ),
$errors->get_error_messages(),
// Optimize image, and fetch the response.
$response = $this->backoff->set_wait( WP_SMUSH_RETRY_WAIT )
->set_max_attempts( $retries )
->set_decider( array( $this, 'should_retry_smush' ) )
->run( function () use ( $file_path, $convert_to_webp ) {
return $this->_post( $file_path, $convert_to_webp );
return $this->handle_response( $response, $file_path, $convert_to_webp );
public function should_retry_smush( $response ) {
return WP_SMUSH_RETRY_ATTEMPTS > 0 && (
|| 200 !== wp_remote_retrieve_response_code( $response )
* Takes the raw response from the API and performs all the necessary file operations etc.
* @param $response array|WP_Error
* @param $file_path string
* @param $convert_to_webp boolean
private function handle_response( $response, $file_path, $convert_to_webp ) {
$data = $this->parse_response( $response );
if ( is_wp_error( $data ) ) {
if ( $data->get_error_code() === self::ERROR_SSL_CERT ) {
// Switch to http protocol.
$this->settings->set_setting( 'wp-smush-use_http', 1 );
$error_format = $convert_to_webp
? 'Cannot convert to webp for image [%s].'
: 'Cannot smush image [%s].';
sprintf( $error_format, Helper::clean_file_path( $file_path ) ),
$data->get_error_messages(),
$bytes_saved = empty( $data->bytes_saved ) ? 0 : $data->bytes_saved;
if ( $bytes_saved > 0 ) {
$this->save_smushed_image_file(
// No savings, just add an entry to the log
Helper::logger()->notice(
'The smushed image is larger than the original image [%s] (bytes saved %d), keep original image.',
Helper::clean_file_path( $file_path ),
// No need to pass image data any further
// Check for API message and store in db.
if ( ! empty( $data->api_message ) ) {
$this->add_api_message( (array) $data->api_message );
// If is_premium is set in response, send it over to check for member validity.
if ( ! empty( $data->is_premium ) ) {
$this->api_headers['is_premium'] = $data->is_premium;
* Posts an image to Smush.
* @since 3.8.0 Added new param $convert_to_webp.