Skip to content
170 changes: 170 additions & 0 deletions src/class-tiny-image.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}
68 changes: 37 additions & 31 deletions src/class-tiny-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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();
}

/**
Expand All @@ -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() {
Expand All @@ -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'] ) ) {
Expand Down
15 changes: 14 additions & 1 deletion src/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ input[type=number][name*="tinypng_resize_original"] {
}

.tiny-compression-details {
padding: 10px;
padding: 10px 0;
}

.tiny-compression-details table {
Expand Down Expand Up @@ -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;
}
53 changes: 52 additions & 1 deletion src/js/admin.js
Original file line number Diff line number Diff line change
@@ -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('<p>Could not restore backup. Please try again.</p>');
},
});
}

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();
Expand Down
Loading
Loading