: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @package Smush\Core\Modules
namespace Smush\Core\Modules;
use Smush\Core\Media\Media_Item_Cache;
use Smush\Core\Media\Media_Item_Optimizer;
if ( ! defined( 'WPINC' ) ) {
class Backup extends Abstract_Module {
protected $slug = 'backup';
* Key for storing file path for image backup
private $backup_key = 'smush-full';
// Handle Restore operation.
//add_action( 'wp_ajax_smush_restore_image', array( $this, 'restore_image' ) );
// Handle bulk restore from modal.
add_action( 'wp_ajax_get_image_count', array( $this, 'get_image_count' ) );
add_action( 'wp_ajax_restore_step', array( $this, 'restore_step' ) );
* Check if the backup file exists.
* @param int $attachment_id Attachment ID.
* @param string $file_path Current file path.
* @return bool True if the backup file exists, false otherwise.
public function backup_exists( $attachment_id, $file_path = false ) {
$media_item = Media_Item_Cache::get_instance()->get( $attachment_id );
return $media_item->can_be_restored();
* Generate unique .bak file.
* @param string $bak_file The .bak file.
* @param int $attachment_id Attachment ID.
* @return string Returns a unique backup file.
private function generate_unique_bak_file( $bak_file, $attachment_id ) {
if ( strpos( $bak_file, '.bak' ) && Helper::file_exists( $bak_file, $attachment_id ) ) {
$ext = Helper::get_file_ext( $bak_file );
$file_without_ext = rtrim( $bak_file, $ext );
$bak_file = $file_without_ext . '-' . $count . $ext;
while ( Helper::file_exists( $bak_file, $attachment_id ) ) {
$bak_file = $file_without_ext . '-' . $count . $ext;
* Creates a backup of file for the given attachment path.
* Checks if there is an existing backup, else create one.
* @param string $file_path File path.
* @param int $attachment_id Attachment ID.
public function create_backup( $file_path, $attachment_id ) {
if ( empty( $file_path ) || empty( $attachment_id ) ) {
// If backup not enabled, return.
if ( ! $this->is_active() ) {
* If [ not compress original ]:
* elseif [ no-resize + no-png2jpg ]:
* We don't need to backup, let user try to use regenerate plugin
* to restore the compressed thumbnails size.
* else: continue as compress_original.
* We don't need to backup if we had a backup file for PNG2JPG,
* or .bak file. But if the .bak file is from third party, we will generate our new backup file.
// We might not need to backup the file if we're not compressing original.
if ( ! $this->settings->get( 'original' ) ) {
* Add WordPress 5.3 support for -scaled images size, and those can always be used to restore.
* Maybe user doesn't want to auto-scale JPG from WP for some images,
* so we allow user to restore it even we don't Smush this image.
if ( false !== strpos( $file_path, '-scaled.' ) && function_exists( 'wp_get_original_image_path' ) ) {
// Scaled images already have a backup. Use that and don't create a new one.
$file_path = Helper::get_attached_file( $attachment_id, 'backup' );// Supported S3.
if ( file_exists( $file_path ) ) {
* We do not need an additional backup file if we're not compressing originals.
* But we need to save the original file as a backup file in the metadata to allow restoring this image later.
$this->add_to_image_backup_sizes( $attachment_id, $file_path );
$mod = WP_Smush::get_instance()->core()->mod;
// If there is not *-scaled.jpg file, we don't need to backup the file if we don't work with original file.
if ( ! $mod->resize->is_active() && ! $mod->png2jpg->is_active() ) {
* In this case, we can add the meta to save the original file as a backup file,
* but if there is a lot of images, might take a lot of row for postmeta table,
* so leave it for user to use a "regenerate thumbnail" plugin instead.
Helper::logger()->backup()->info( sprintf( 'Not modify the original file [%s(%d)], skip the backup.', Helper::clean_file_path( $file_path ), $attachment_id ) );
// We should backup this image if we can resize it.
if ( $mod->resize->is_active() && $mod->resize->should_resize( $attachment_id ) ) {
// We should backup this image if we can convert it from PNG to JPEG.
! $should_backup && $mod->png2jpg->is_active() && Helper::get_file_ext( $file_path, 'png' )
&& $mod->png2jpg->can_be_converted( $attachment_id, 'full', 'image/png', $file_path )
// As we don't work with the original file, so we don't back it up.
if ( ! $should_backup ) {
Helper::logger()->backup()->info( sprintf( 'Not modify the original file [%s(%d)], skip the backup.', Helper::clean_file_path( $file_path ), $attachment_id ) );
* Check if exists backup file from meta,
* Because we will compress the original file,
* so we only keep the backup file if there is PNG2JPG or .bak file.
$backup_path = $this->get_backup_file( $attachment_id, $file_path );
* We will compress the original file so the backup file have to different from current file.
* And the backup file should be the same folder with the main file.
if ( $backup_path !== $file_path && dirname( $file_path ) === dirname( $backup_path ) ) {
// Check if there is a .bak file or PNG2JPG file.
if ( strpos( $backup_path, '.bak' ) || ( Helper::get_file_ext( $backup_path, 'png' ) && Helper::get_file_ext( $file_path, 'jpg' ) ) ) {
Helper::logger()->backup()->info( sprintf( 'Found backed up file [%s(%d)].', Helper::clean_file_path( $backup_path ), $attachment_id ) );
* To avoid the conflict with 3rd party, we will generate a new backup file.
* Because how about if 3rd party delete the backup file before trying to restore it from Smush?
* We only try to use their bak file while restoring the backup file.
$backup_path = $this->generate_unique_bak_file( $this->get_image_backup_path( $file_path ), $attachment_id );
* We need to save the .bak file to the meta. Because if there is a PNG, when we convert PNG2JPG,
* the converted file is .jpg, so the bak file will be .bak.jpg not .bak.png
// Store the backup path in image backup sizes.
if ( copy( $file_path, $backup_path ) ) {
$this->add_to_image_backup_sizes( $attachment_id, $backup_path );
Helper::logger()->backup()->error( sprintf( 'Cannot backup file [%s(%d)].', Helper::clean_file_path( $file_path ), $attachment_id ) );
* Store new backup path for the image.
* @param int $attachment_id Attachment ID.
* @param string $backup_path Backup path.
* @param string $backup_key Backup key.
public function add_to_image_backup_sizes( $attachment_id, $backup_path, $backup_key = '' ) {
if ( empty( $attachment_id ) || empty( $backup_path ) ) {
// Get the Existing backup sizes.
$backup_sizes = $this->get_backup_sizes( $attachment_id );
if ( empty( $backup_sizes ) ) {
// Prevent phar deserialization vulnerability.
if ( false !== stripos( $backup_path, 'phar://' ) ) {
Helper::logger()->backup()->info( sprintf( 'Prevent phar deserialization vulnerability [%s(%d)].', Helper::clean_file_path( $backup_path ), $attachment_id ) );
// Return if backup file doesn't exist.
if ( ! file_exists( $backup_path ) ) {
Helper::logger()->backup()->notice( sprintf( 'Back file [%s(%d)] does not exist.', Helper::clean_file_path( $backup_path ), $attachment_id ) );
list( $width, $height ) = getimagesize( $backup_path );
// Store our backup path.
$backup_key = empty( $backup_key ) ? $this->backup_key : $backup_key;
$backup_sizes[ $backup_key ] = array(
'file' => wp_basename( $backup_path ),
wp_cache_delete( 'images_with_backups', 'wp-smush' );
update_post_meta( $attachment_id, '_wp_attachment_backup_sizes', $backup_sizes );
* @param int $attachment_id Attachment ID.
* @return mixed False or an array of backup sizes.
public function get_backup_sizes( $attachment_id ) {
return get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
* Back up an image if it hasn't backed up yet.
* @param int $attachment_id Image id.
* @param string $backup_file File path to back up.
* Note, we used it to manage backup PNG2JPG to keep the backup file is the original file to avoid conflicts with a duplicate PNG file.
* If the backup file exists it will rename the original backup file to
* @return bool True if added this file to the backup sizes, false if the image was backed up before.
public function maybe_backup_image( $attachment_id, $backup_file ) {
if ( ! file_exists( $backup_file ) ) {
// We don't use .bak file from 3rd party while backing up.
$backed_up_file = $this->get_backup_file( $attachment_id, $backup_file );
if ( $backed_up_file && $backed_up_file !== $backup_file && dirname( $backed_up_file ) === dirname( $backup_file ) ) {
$was_backed_up = rename( $backed_up_file, $backup_file );
$this->add_to_image_backup_sizes( $attachment_id, $backup_file );
* Get the backup file from the meta.
* @param int $id Image ID.
* @param string $file_path Current file path.
* @return bool|null Backup file or false|null if the image doesn't exist.
public function get_backup_file( $id, $file_path = false ) {
if ( empty( $file_path ) ) {
// Get unfiltered path file.
$file_path = Helper::get_attached_file( $id, 'original' );
// If the file path is still empty, nothing to check here.
if ( empty( $file_path ) ) {
// Try to get the backup file from _wp_attachment_backup_sizes.
$backup_sizes = $this->get_backup_sizes( $id );
// Check if we have backup file from the metadata.
// Try to get the original file first.
if ( isset( $backup_sizes[ $this->backup_key ]['file'] ) ) {
$original_file = str_replace( wp_basename( $file_path ), wp_basename( $backup_sizes[ $this->backup_key ]['file'] ), $file_path );
if ( Helper::file_exists( $original_file, $id ) ) {
$backup_file = $original_file;
// Try to check it from legacy original file or from the resized PNG file.
// If we don't have the original backup path in backup sizes, check for legacy original file path. It's for old version < V.2.7.0.
$original_file = get_post_meta( $id, 'wp-smush-original_file', true );
if ( ! empty( $original_file ) ) {
// For old version < v.2.7.0, we are saving meta['file'] or _wp_attached_file.
$original_file = Helper::original_file( $original_file );
if ( Helper::file_exists( $original_file, $id ) ) {
$backup_file = $original_file;
// As we don't use this meta key so save it as a full backup file and delete the old metadata.
WP_Smush::get_instance()->core()->mod->backup->add_to_image_backup_sizes( $id, $backup_file );
delete_post_meta( $id, 'wp-smush-original_file' );
// Check the backup file from resized PNG file.
if ( ! $backup_file && isset( $backup_sizes['smush_png_path']['file'] ) ) {
$original_file = str_replace( wp_basename( $file_path ), wp_basename( $backup_sizes['smush_png_path']['file'] ), $file_path );
if ( Helper::file_exists( $original_file, $id ) ) {
$backup_file = $original_file;
* Restore the image and its sizes from backup
* @param string $attachment_id Attachment ID.
* @param bool $resp Send JSON response or not.
public function restore_image( $attachment_id = '', $resp = true ) {
// TODO: (stats refactor) handle properly
// If no attachment id is provided, check $_POST variable for attachment_id.
if ( empty( $attachment_id ) ) {
if ( empty( $_POST['attachment_id'] ) || empty( $_POST['_nonce'] ) ) {
'error_msg' => esc_html__( 'Error in processing restore action, fields empty.', 'wp-smushit' ),
$nonce_value = filter_input( INPUT_POST, '_nonce', FILTER_SANITIZE_SPECIAL_CHARS );
$attachment_id = filter_input( INPUT_POST, 'attachment_id', FILTER_SANITIZE_NUMBER_INT );
if ( ! wp_verify_nonce( $nonce_value, "wp-smush-restore-$attachment_id" ) ) {
'error_msg' => esc_html__( 'Image not restored, nonce verification failed.', 'wp-smushit' ),
if ( ! Helper::is_user_allowed( 'upload_files' ) ) {
'error_msg' => esc_html__( "You don't have permission to work with uploaded files.", 'wp-smushit' ),
$attachment_id = (int) $attachment_id;
$mod = WP_Smush::get_instance()->core()->mod;
// Set an option to avoid the smush-restore-smush loop.
set_transient( 'wp-smush-restore-' . $attachment_id, 1, HOUR_IN_SECONDS );
* Run WebP::delete_images always even when the module is deactivated.
$mod->webp->delete_images( $attachment_id );
// Restore Full size -> get other image sizes -> restore other images.
// Get the Original Path, supported S3.
$file_path = Helper::get_attached_file( $attachment_id, 'original' );
// Store the restore success/failure for full size image.
$backup_full_path = $this->get_backup_file( $attachment_id, $file_path );
// Is restoring the PNG which is converted to JPG or not.
* Fires before restoring a file.
* @param string|false $backup_full_path Full backup path.
* @param int $attachment_id Attachment id.
* @param string $file_path Original unfiltered file path.
* @hooked Smush\Core\Integrations\s3::maybe_download_file()
do_action( 'wp_smush_before_restore_backup', $backup_full_path, $attachment_id, $file_path );
// Finally, if we have the backup path, perform the restore operation.
if ( ! empty( $backup_full_path ) ) {
// If the backup file is the same as the main file, we only need to re-generate the metadata.
if ( $backup_full_path === $file_path ) {
// Is real backup file or .bak file.
$is_real_filename = false === strpos( $backup_full_path, '.bak' );
$restore_png = Helper::get_file_ext( trim( $backup_full_path ), 'png' ) && ! Helper::get_file_ext( $file_path, 'png' );
// Restore PNG full size.
$org_backup_full_path = $backup_full_path;
if ( ! $is_real_filename ) {
// Try to get a unique file name.
$dirname = dirname( $backup_full_path );
$new_file_name = wp_unique_filename( $dirname, wp_basename( str_replace( '.bak', '', $backup_full_path ) ) );
$new_png_file = path_join( $dirname, $new_file_name );
// Restore PNG full size.
$restored = copy( $backup_full_path, $new_png_file );
// Assign the new PNG file to the backup file.
$backup_full_path = $new_png_file;
// Restore all other image sizes.
$metadata = $this->restore_png( $attachment_id, $backup_full_path, $file_path );
$restored = ! empty( $metadata );
if ( $restored && ! $is_real_filename ) {
// Reset the backup file to delete it later.
$backup_full_path = $org_backup_full_path;
// If file exists, corresponding to our backup path - restore.
if ( ! $is_real_filename ) {
$restored = copy( $backup_full_path, $file_path );
// Remove the backup, if we were able to restore the image.