$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; } } } } return $backup_file; } /** * Restore the image and its sizes from backup * * @param string $attachment_id Attachment ID. * @param bool $resp Send JSON response or not. * * @return bool */ 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 ) ) { // Check Empty fields. if ( empty( $_POST['attachment_id'] ) || empty( $_POST['_nonce'] ) ) { wp_send_json_error( array( '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" ) ) { wp_send_json_error( array( 'error_msg' => esc_html__( 'Image not restored, nonce verification failed.', 'wp-smushit' ), ) ); } // Check capability. 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' ), ) ); } } $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 ); /** * Delete WebP. * * Run WebP::delete_images always even when the module is deactivated. * * @since 3.8.0 */ $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. $restored = false; // Retrieve backup file. $backup_full_path = $this->get_backup_file( $attachment_id, $file_path ); // Is restoring the PNG which is converted to JPG or not. $restore_png = false; /** * Fires before restoring a file. * * @since 3.9.6 * * @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 ) { $restored = true; } else { // 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' ); if ( $restore_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 ); if ( $restored ) { // Assign the new PNG file to the backup file. $backup_full_path = $new_png_file; } } else { $restored = true; } // Restore all other image sizes. if ( $restored ) { $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; } } } else { // If file exists, corresponding to our backup path - restore. if ( ! $is_real_filename ) { $restored = copy( $backup_full_path, $file_path ); } else { $restored = true; } } // Remove the backup, if we were able to restore the image. if ( $restored ) { // Remove our backup file. $this->remove_from_backup_sizes( $attachment_id ); /** * Delete our backup file if it's .bak file, we will try to backup later when running Smush. */ if ( ! $is_real_filename ) { // It will also delete file from the cloud, e.g. S3. Helper::delete_permanently( array( $this->backup_key => $backup_full_path ), $attachment_id, false ); } } } } else { Helper::logger()->backup()->warning( sprintf( 'Backup file [%s(%d)] does not exist.', Helper::clean_file_path( $backup_full_path ), $attachment_id ) ); } /** * Regenerate thumbnails * * All this is handled in self::restore_png(). */ if ( $restored ) { if ( ! $restore_png ) { // Generate all other image size, and update attachment metadata. $metadata = wp_generate_attachment_metadata( $attachment_id, $file_path ); } // Update metadata to db if it was successfully generated. if ( ! empty( $metadata ) && ! is_wp_error( $metadata ) ) { Helper::wp_update_attachment_metadata( $attachment_id, $metadata ); } else { Helper::logger()->backup()->warning( sprintf( 'Meta file [%s(%d)] is empty.', Helper::clean_file_path( $file_path ), $attachment_id ) ); } } /** * Fires before restoring a file. * * @since 3.9.6 * * @param bool $restored Restore status. * @param string|false $backup_full_path Full backup path. * @param int $attachment_id Attachment id. * @param string $file_path Original unfiltered file path. */ do_action( 'wp_smush_after_restore_backup', $restored, $backup_full_path, $attachment_id, $file_path ); // If any of the image is restored, we count it as success. if ( $restored ) { // Remove the Meta, And send json success. delete_post_meta( $attachment_id, Smush::$smushed_meta_key ); // Remove PNG to JPG conversion savings. delete_post_meta( $attachment_id, 'wp-smush-pngjpg_savings' ); // Remove Original File. delete_post_meta( $attachment_id, 'wp-smush-original_file' ); // Delete resize savings. delete_post_meta( $attachment_id, 'wp-smush-resize_savings' ); // Remove lossy flag. delete_post_meta( $attachment_id, 'wp-smush-lossy' ); // Clear backups cache. wp_cache_delete( 'images_with_backups', 'wp-smush' ); Core::remove_from_smushed_list( $attachment_id ); // Get the Button html without wrapper. $button_html = WP_Smush::get_instance()->library()->generate_markup( $attachment_id ); // Release the attachment after restoring. delete_transient( 'wp-smush-restore-' . $attachment_id ); if ( ! $resp ) { return true; } $size = file_exists( $file_path ) ? filesize( $file_path ) : 0; if ( $size > 0 ) { $update_size = size_format( $size ); // Used in js to update image stat. } wp_send_json_success( array( 'stats' => $button_html, 'new_size' => isset( $update_size ) ? $update_size : 0, ) ); } // Release the attachment after restoring. delete_transient( 'wp-smush-restore-' . $attachment_id ); if ( $resp ) { wp_send_json_error( array( 'error_msg' => esc_html__( 'Unable to restore image', 'wp-smushit' ) ) ); } return false; } /** * Restore PNG. * * @param int $attachment_id Attachment ID. * @param string $backup_file_path Full backup file, the result of self::get_backup_file(). * @param string $file_path File path. * * @since 3.9.10 Moved wp_update_attachment_metadata into self::restore_image() after deleting the backup file, * in order to support S3 - @see SMUSH-1141. * * @return bool|array */ private function restore_png( $attachment_id, $backup_file_path, $file_path ) { if ( empty( $attachment_id ) || empty( $backup_file_path ) || empty( $file_path ) ) { return false; } $meta = array(); // Else get the Attachment details. /** * For Full Size * 1. Get the original file path * 2. Update the attachment metadata and all other meta details * 3. Delete the JPEG * 4. And we're done * 5. Add an action after updating the URLs, that'd allow the users to perform an additional search, replace action */ if ( file_exists( $backup_file_path ) ) { $mod = WP_Smush::get_instance()->core()->mod; // Update the path details in meta and attached file, replace the image. $meta = $mod->png2jpg->update_image_path( $attachment_id, $file_path, $backup_file_path, $meta, 'full', 'restore' ); $files_to_remove = array(); // Unlink JPG after updating attached file. if ( ! empty( $meta['file'] ) && wp_basename( $backup_file_path ) === wp_basename( $meta['file'] ) ) { /** * Note, we use size key smush-png2jpg-full for PNG2JPG file to support S3 private media, * to remove converted JPG file after restoring in private folder. * * @see Smush\Core\Integrations\S3::get_object_key() */ $files_to_remove['smush-png2jpg-full'] = $file_path; } $jpg_meta = wp_get_attachment_metadata( $attachment_id ); foreach ( $jpg_meta['sizes'] as $size_key => $size_data ) { $size_path = str_replace( wp_basename( $backup_file_path ), wp_basename( $size_data['file'] ), $backup_file_path ); // Add to delete the thumbnails jpg. $files_to_remove[ $size_key ] = $size_path; } // Re-generate metadata for PNG file. $metadata = wp_generate_attachment_metadata( $attachment_id, $backup_file_path ); // Perform an action after the image URL is updated in post content. do_action( 'wp_smush_image_url_updated', $attachment_id, $file_path, $backup_file_path ); } else { Helper::logger()->backup()->warning( sprintf( 'Backup file [%s(%d)] does not exist.', Helper::clean_file_path( $backup_file_path ), $attachment_id ) ); } if ( ! empty( $metadata ) ) { // Delete jpg files, we also try to delete these files on cloud, e.g S3. Helper::delete_permanently( $files_to_remove, $attachment_id, false ); return $metadata; } else { Helper::logger()->backup()->warning( sprintf( 'Meta file [%s(%d)] is empty.', Helper::clean_file_path( $backup_file_path ), $attachment_id ) ); } return false; } /** * Remove a specific backup key from the backup size array. * * @param int $attachment_id Attachment ID. */ private function remove_from_backup_sizes( $attachment_id ) { // Get backup sizes. $backup_sizes = $this->get_backup_sizes( $attachment_id ); // If we don't have any backup sizes list or if the particular key is not set, return. if ( empty( $backup_sizes ) || ! isset( $backup_sizes[ $this->backup_key ] ) ) { return; } unset( $backup_sizes[ $this->backup_key ] ); if ( empty( $backup_sizes ) ) { delete_post_meta( $attachment_id, '_wp_attachment_backup_sizes' ); } else { update_post_meta( $attachment_id, '_wp_attachment_backup_sizes', $backup_sizes ); } } /** * Get the attachments that can be restored. * * @since 3.6.0 Changed from private to public. * * @return array Array of attachments IDs. */ public function get_attachments_with_backups() { global $wpdb; $images_to_restore = $wpdb->get_col( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='_wp_attachment_backup_sizes' AND (`meta_value` LIKE '%smush-full%' OR `meta_value` LIKE '%smush_png_path%')" ); return $images_to_restore; } /** * Get the number of attachments that can be restored. * * @since 3.2.2 */ public function get_image_count() { check_ajax_referer( 'smush_bulk_restore' ); // Check for permission. if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 ); } wp_send_json_success( array( 'items' => $this->get_attachments_with_backups(), ) ); } /** * Bulk restore images from the modal. * * @since 3.2.2 */ public function restore_step() { check_ajax_referer( 'smush_bulk_restore' ); // Check for permission. if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 ); } $id = filter_input( INPUT_POST, 'item', FILTER_SANITIZE_NUMBER_INT, FILTER_NULL_ON_FAILURE ); $media_item = Media_Item_Cache::get_instance()->get( $id ); if ( ! $media_item->is_mime_type_supported() ) { wp_send_json_error( array( /* translators: %s: Error message */ 'error_msg' => sprintf( esc_html__( 'Image not restored. %s', 'wp-smushit' ), $media_item->get_errors()->get_error_message() ), ) ); } $optimizer = new Media_Item_Optimizer( $media_item ); $status = $id && $optimizer->restore(); $file_name = $media_item->get_full_or_scaled_size()->get_file_name(); wp_send_json_success( array( 'success' => $status, 'src' => ! empty( $file_name ) ? $file_name : __( 'Error getting file name', 'wp-smushit' ), 'thumb' => wp_get_attachment_image( $id ), 'link' => Helper::get_image_media_link( $id, $file_name, true ), ) ); } /** * Returns the backup path for attachment * * @param string $attachment_path Attachment path. * * @return string */ public function get_image_backup_path( $attachment_path ) { if ( empty( $attachment_path ) ) { return ''; } $path = pathinfo( $attachment_path ); if ( empty( $path['extension'] ) ) { return ''; } return trailingslashit( $path['dirname'] ) . $path['filename'] . '.bak.' . $path['extension']; } /** * Clear up all the backup files for the image while deleting the image. * * @since 3.9.6 * Note, we only call this method while deleting the image, as it will delete * .bak file and might be the original file too. * * Note, for the old version < 3.9.6 we also save all PNG files (original file and thumbnails) * when the site doesn't compress original file. * But it's not safe to remove them if the user add another image with the same PNG file name, and didn't convert it. * So we still leave them there. * * @param int $attachment_id Attachment ID. */ public function delete_backup_files( $attachment_id ) { $smush_meta = get_post_meta( $attachment_id, Smush::$smushed_meta_key, true ); if ( empty( $smush_meta ) ) { return; } // Save list files to remove. $files_to_remove = array(); $unfiltered = false; $file_path = get_attached_file( $attachment_id, false ); // We only work with the real file path, not cloud URL like S3. if ( false === strpos( $file_path, ABSPATH ) ) { $unfiltered = true; $file_path = get_attached_file( $attachment_id, true ); } // Remove from the cache. wp_cache_delete( 'images_with_backups', 'wp-smush' ); /** * We only remove the backup file from the metadata, * keep the backup file from 3rd-party. */ $backup_path = null;// Reset backup file. $backup_sizes = $this->get_backup_sizes( $attachment_id ); if ( isset( $backup_sizes[ $this->backup_key ]['file'] ) ) { $backup_path = str_replace( wp_basename( $file_path ), wp_basename( $backup_sizes[ $this->backup_key ]['file'] ), $file_path ); // Add to remove the backup file. $files_to_remove[ $this->backup_key ] = $backup_path; } // Check the backup file from resized PNG file (< 3.9.6). if ( isset( $backup_sizes['smush_png_path']['file'] ) ) { $backup_path = str_replace( wp_basename( $file_path ), wp_basename( $backup_sizes['smush_png_path']['file'] ), $file_path ); // Add to remove the backup file. $files_to_remove['smush_png_path'] = $backup_path; } if ( ! $backup_path ) { // Check for legacy original file path. It's for old version < V.2.7.0. $original_file = get_post_meta( $attachment_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. $backup_path = Helper::original_file( $original_file ); // Add to remove the backup file. $files_to_remove[] = $backup_path; } } // Check meta for rest of the sizes. $meta = wp_get_attachment_metadata( $attachment_id, $unfiltered ); if ( empty( $meta ) || empty( $meta['sizes'] ) ) { Helper::logger()->backup()->info( sprintf( 'Empty meta sizes [%s(%d)]', $file_path, $attachment_id ) ); return; } foreach ( $meta['sizes'] as $size ) { if ( empty( $size['file'] ) ) { continue; } // Image path and backup path. $image_size_path = path_join( dirname( $file_path ), $size['file'] ); $image_backup_path = $this->get_image_backup_path( $image_size_path ); // Add to remove the backup file. $files_to_remove[] = $image_backup_path; } // We also try to delete this file on cloud, e.g. S3. Helper::delete_permanently( $files_to_remove, $attachment_id, false ); } }