diff --git a/src/class-tiny-image.php b/src/class-tiny-image.php index 4b349eab..07e2435e 100644 --- a/src/class-tiny-image.php +++ b/src/class-tiny-image.php @@ -675,4 +675,174 @@ public function mark_as_compressed() { $this->update_tiny_post_meta(); } + + /** + * Retrieves the original image of the Tiny_Image + * + * + * @return Tiny_Image_Size|false the image or false if does not exist + */ + private function get_original_image() { + $original_image = $this->get_image_size( self::ORIGINAL_UNSCALED ); + if ( null === $original_image ) { + $original_image = $this->get_image_size(); + } + + if ( null === $original_image ) { + return false; + } + + return $original_image; + } + + /** + * Builds the filesystem path where the backup of the original image is + * (or would be) stored. + * + * @return string|false the backup file path, or false if there is no original image + */ + private function get_backup_path() { + $original_image = $this->get_original_image(); + if ( false === $original_image ) { + return false; + } + + $file_path = $original_image->filename; + $upload_dir = wp_upload_dir(); + $basedir = trailingslashit( $upload_dir['basedir'] ); + if ( Tiny_Helpers::str_starts_with( $file_path, $basedir ) ) { + $file_path = substr( $file_path, strlen( $basedir ) ); + } + + return $basedir . 'tinify_backup/' . $file_path; + } + + /** + * Creates a backup copy of the original image, if one does not already exist. + * + * @return bool true on success, false on failure or if a backup already exists + */ + public function create_backup() { + + $backup_file_path = $this->get_backup_path(); + if ( false === $backup_file_path ) { + return false; + } + + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + + if ( $wp_filesystem->exists( $backup_file_path ) ) { + return false; + } + + $backup_dir = dirname( $backup_file_path ); + + if ( ! wp_mkdir_p( $backup_dir ) ) { + return false; + } + + $original_image = $this->get_original_image(); + + return $wp_filesystem->copy( $original_image->filename, $backup_file_path ); + } + + + /** + * Retrieves the public URL of the backup of the original image. + * + * @return string|false the backup URL, or false if no backup exists + */ + public function get_backup() { + $backup_file_path = $this->get_backup_path(); + if ( false === $backup_file_path ) { + return false; + } + + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + + if ( ! $wp_filesystem->exists( $backup_file_path ) ) { + return false; + } + + $upload_dir = wp_upload_dir(); + $basedir = trailingslashit( $upload_dir['basedir'] ); + $baseurl = trailingslashit( $upload_dir['baseurl'] ); + + return str_replace( $basedir, $baseurl, $backup_file_path ); + } + + /** + * Restores the original image from its backup. + * + * - Copies the backup file over the current original. + * - Clears compression metadata for all image sizes. + * - Regenerates all thumbnail sizes from the restored image. + * - Updates the WordPress attachment metadata. + * + * @since 3.7.0 + * + * @return bool True on success, false if no backup exists or the copy fails. + */ + public function restore_backup() { + $backup_file_path = $this->get_backup_path(); + if ( false === $backup_file_path ) { + return false; + } + + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + + if ( ! $wp_filesystem->exists( $backup_file_path ) ) { + return false; + } + + $original_size_key = null !== $this->get_image_size( self::ORIGINAL_UNSCALED ) + ? self::ORIGINAL_UNSCALED + : self::ORIGINAL; + + $original_image = $this->get_image_size( $original_size_key ); + if ( null === $original_image ) { + return false; + } + + if ( ! $wp_filesystem->copy( $backup_file_path, $original_image->filename, true ) ) { + return false; + } + + // Clear compression metadata for all image sizes. + foreach ( $this->sizes as $size ) { + $size->meta = array(); + } + $this->update_tiny_post_meta(); + + // Regenerate all thumbnail sizes from the restored image. + $new_metadata = wp_generate_attachment_metadata( $this->id, $original_image->filename ); + if ( $new_metadata ) { + $this->wp_metadata = $new_metadata; + wp_update_attachment_metadata( $this->id, $this->wp_metadata ); + } + + return true; + } + + /** + * Deletes the backup file of the original image, if it exists. + * + * @since 3.7.0 + * + * @return bool True on success or if no backup exists, false on deletion failure. + */ + public function delete_backup() { + $backup_file_path = $this->get_backup_path(); + if ( false === $backup_file_path ) { + return true; + } + + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + + if ( ! $wp_filesystem->exists( $backup_file_path ) ) { + return true; + } + + return $wp_filesystem->delete( $backup_file_path ); + } } diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index a96b9f4f..6555d606 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -119,6 +119,11 @@ public function ajax_init() { $this->get_method( 'mark_image_as_compressed' ) ); + add_action( + 'wp_ajax_tiny_restore_backup', + $this->get_method( 'restore_backup_image' ) + ); + /* When touching any functionality linked to image compressions when uploading images make sure it also works with XML-RPC. See README. */ @@ -909,6 +914,7 @@ public function friendly_user_name() { public function clean_attachment( $post_id ) { $tiny_image = new Tiny_Image( $this->settings, $post_id ); $tiny_image->delete_converted_image(); + $tiny_image->delete_backup(); } /** @@ -934,37 +940,7 @@ public function backup_original_image( $attachment_id ) { $tiny_image = new Tiny_Image( $this->settings, $attachment_id ); - $original_image = $tiny_image->get_image_size( Tiny_Image::ORIGINAL_UNSCALED ); - if ( null === $original_image ) { - $original_image = $tiny_image->get_image_size(); - } - - if ( null === $original_image ) { - return false; - } - - $file_path = $original_image->filename; - $upload_dir = wp_upload_dir(); - $basedir = trailingslashit( $upload_dir['basedir'] ); - if ( Tiny_Helpers::str_starts_with( $file_path, $basedir ) ) { - $file_path = substr( $file_path, strlen( $basedir ) ); - } - - $backup_file = $basedir . 'tinify_backup/' . $file_path; - - $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); - - if ( $wp_filesystem->exists( $backup_file ) ) { - return false; - } - - $backup_dir = dirname( $backup_file ); - - if ( ! wp_mkdir_p( $backup_dir ) ) { - return false; - } - - return $wp_filesystem->copy( $original_image->filename, $backup_file ); + return $tiny_image->create_backup(); } public static function request_review() { @@ -989,6 +965,36 @@ public static function uninstall() { Tiny_Apache_Rewrite::uninstall_rules(); } + /** + * Restores the original image from its backup via AJAX. + * + * Validates the request, calls restore_backup() on the image, then + * re-renders the compression details partial in the response. + * + * @since 3.7.0 + * + * @return void + */ + public function restore_backup_image() { + $response = $this->validate_ajax_attachment_request(); + if ( isset( $response['error'] ) ) { + echo esc_html( $response['error'] ); + exit(); + } + + list($id, $metadata) = $response['data']; + $tiny_image = new Tiny_Image( $this->settings, $id, $metadata ); + + if ( ! $tiny_image->restore_backup() ) { + echo esc_html__( 'Could not restore backup. The backup file may not exist or could not be written.', 'tiny-compress-images' ); + exit(); + } + + $this->render_compress_details( $tiny_image ); + + exit(); + } + public function mark_image_as_compressed() { $response = $this->validate_ajax_attachment_request(); if ( isset( $response['error'] ) ) { diff --git a/src/css/admin.css b/src/css/admin.css index 977dadf1..ad3aaf34 100644 --- a/src/css/admin.css +++ b/src/css/admin.css @@ -412,7 +412,7 @@ input[type=number][name*="tinypng_resize_original"] { } .tiny-compression-details { - padding: 10px; + padding: 10px 0; } .tiny-compression-details table { @@ -481,4 +481,17 @@ fieldset.tinypng_convert_fields[disabled] { .tiny-mt-2 { margin-top: 10px; +} + +.tiny-dialog { + padding: 20px; + border: 1px solid #ccd0d4; + border-radius: 4px; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3); +} + +.tiny-dialog-actions { + display: flex; + gap: 10px; + justify-content: flex-end; } \ No newline at end of file diff --git a/src/js/admin.js b/src/js/admin.js index 03312c0a..f136f87a 100644 --- a/src/js/admin.js +++ b/src/js/admin.js @@ -1,4 +1,55 @@ -(function() { +(function () { + function restoreBackup(attachmentId, container) { + container.css('opacity', '0.5'); + jQuery.ajax({ + url: ajaxurl, + type: 'POST', + data: { + _nonce: tinyCompress.nonce, + action: 'tiny_restore_backup', + id: attachmentId, + }, + success: function (data) { + container.css('opacity', '1'); + container.html(data); + }, + error: function () { + container.css('opacity', '1'); + // TODO: Replace when actual message + container.html('

Could not restore backup. Please try again.

'); + }, + }); + } + + jQuery(document).on('click', 'a[data-dialog-id]', function (e) { + e.preventDefault(); + const trigger = jQuery(e.currentTarget); + const dialogID = trigger.data('dialog-id'); + if (!dialogID) { + return; + } + + const dialog = document.getElementById(dialogID); + if (!dialog) { + return; + } + + const attachmentId = trigger.data('id'); + const container = jQuery('#modal_' + attachmentId).closest('.tiny-ajax-container'); + + dialog.showModal(); + + dialog.addEventListener('close', function onClose() { + dialog.removeEventListener('close', onClose); + if (dialog.returnValue === 'confirm') { + if (typeof tb_remove === 'function') { + tb_remove(); + } + restoreBackup(attachmentId, container); + } + }); + }); + function downloadDiagnostics() { try { jQuery('#download-diagnostics-spinner').show(); diff --git a/src/views/compress-details-backup.php b/src/views/compress-details-backup.php new file mode 100644 index 00000000..573b0af0 --- /dev/null +++ b/src/views/compress-details-backup.php @@ -0,0 +1,43 @@ +settings->get_backup_enabled(); + +?> +get_backup(); + $modal_id = 'modal_' . absint( $tiny_image->get_id() ) . '_backup'; + ?> +

+ + + + + + + +

+

+ +
+ + +
+
+ + + + + +

+ \ No newline at end of file diff --git a/src/views/compress-details.php b/src/views/compress-details.php index 7c3152f9..c27c3b3f 100644 --- a/src/views/compress-details.php +++ b/src/views/compress-details.php @@ -10,6 +10,7 @@ $available_sizes = array_keys( $this->settings->get_sizes() ); $conversion_enabled = $this->settings->get_conversion_enabled(); $active_sizes = $this->settings->get_sizes(); +$backup_enabled = $this->settings->get_backup_enabled(); $active_tinify_sizes = $this->settings->get_active_tinify_sizes(); $error = $tiny_image->get_latest_error(); $total = $tiny_image->get_count( array( 'modified', 'missing', 'has_been_compressed', 'compressed', 'has_been_converted' ) ); @@ -111,8 +112,12 @@
- - + get_name() ) ); + ?> + @@ -138,12 +143,6 @@