Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6af270d
Set title of modal to image title
tijmenbruggeman Jul 23, 2026
86e8030
move backup func to image
tijmenbruggeman Jul 23, 2026
88bc995
show backup info and actions on details modal
tijmenbruggeman Jul 23, 2026
7a90ef7
tiny ui fix
tijmenbruggeman Jul 23, 2026
8fc5564
add ajax action for restoring
tijmenbruggeman Jul 24, 2026
38e0029
restore backup js
tijmenbruggeman Jul 24, 2026
39fb07b
add template
tijmenbruggeman Jul 24, 2026
2521b11
style: apply WordPress PHP coding standards
tijmenbruggeman Jul 24, 2026
834e024
delete backup when deleting attachment
tijmenbruggeman Jul 24, 2026
8b380e3
remove unused variables
tijmenbruggeman Jul 24, 2026
c8b54f5
handle error cases
tijmenbruggeman Jul 24, 2026
a3fb73a
test(backup): add restore and clean attachment tests
tijmenbruggeman Jul 24, 2026
befe6d5
refactor(backup): use invoker API for restore dialog
tijmenbruggeman Jul 25, 2026
de2e855
refactor(admin): async restoreBackup with spinner feedback
tijmenbruggeman Jul 26, 2026
a252aab
feat(plugin): add data-tiny-media-id to image elements
tijmenbruggeman Jul 26, 2026
c77f3ea
refactor(backup): improve restore dialog UX and markup structure
tijmenbruggeman Jul 26, 2026
2feb2f3
refactor(compress-details): show backup section only when compressed
tijmenbruggeman Jul 26, 2026
5b1193c
style: fix WPCS spacing in plugin and views
tijmenbruggeman Jul 26, 2026
498c6f1
git commit -m "fix(backup): show error in dialog on restore failure
tijmenbruggeman Jul 26, 2026
70b68bb
fix(admin): prevent Thickbox content duplication on restore
tijmenbruggeman Jul 26, 2026
959d0a4
refactor(helpers): consolidate filesystem init into one method
tijmenbruggeman Jul 30, 2026
af5cdc6
simplify get original image and reset meta data
tijmenbruggeman Jul 30, 2026
7018b13
prevent double escaping
tijmenbruggeman Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/class-tiny-diagnostics.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ public function create_diagnostic_zip() {
}

$wp_filesystem = Tiny_Helpers::get_wp_filesystem();
if ( false === $wp_filesystem ) {
return new WP_Error(
'filesystem_unavailable',
__( 'WordPress filesystem could not be initialized.', 'tiny-compress-images' )
);
}
$temp_dir = trailingslashit( get_temp_dir() ) . 'tiny-compress-temp';
if ( ! $wp_filesystem->exists( $temp_dir ) ) {
wp_mkdir_p( $temp_dir );
Expand Down Expand Up @@ -249,6 +255,9 @@ public function create_diagnostic_zip() {
*/
public static function download_zip( $zip_path ) {
$wp_filesystem = Tiny_Helpers::get_wp_filesystem();
if ( false === $wp_filesystem ) {
wp_die( esc_html__( 'WordPress filesystem could not be initialized.', 'tiny-compress-images' ) );
}
if ( ! $wp_filesystem->exists( $zip_path ) ) {
wp_die( esc_html__( 'Diagnostic file not found.', 'tiny-compress-images' ) );
}
Expand Down
9 changes: 4 additions & 5 deletions src/class-tiny-helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,10 @@ public static function get_mimetype( $input ) {
* Gets or initializes the WordPress filesystem instance.
*
* Returns the global WP_Filesystem instance, initializing it if necessary.
* This helper prevents repeated initialization code throughout the plugin.
*
* @since 3.7.0
*
* @return WP_Filesystem_Base The WP_Filesystem instance.
* @throws Exception If the filesystem cannot be initialized.
* @return WP_Filesystem_Base|false The WP_Filesystem instance, or false on failure.
*/
public static function get_wp_filesystem() {
global $wp_filesystem;
Expand All @@ -129,14 +127,15 @@ public static function get_wp_filesystem() {
return $wp_filesystem;
}

// Initialize the filesystem only if the function isn't available yet.
if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();

if ( ! ( $wp_filesystem instanceof WP_Filesystem_Base ) ) {
throw new Exception( 'Unable to initialize WordPress filesystem.' );
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'Tiny Compress: Unable to initialize WordPress filesystem.' );
return false;
}

return $wp_filesystem;
Expand Down
180 changes: 180 additions & 0 deletions src/class-tiny-image.php
Original file line number Diff line number Diff line change
Expand Up @@ -675,4 +675,184 @@ 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 ( false === $wp_filesystem ) {
return false;
}

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 ( false === $wp_filesystem ) {
return false;
}

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 );
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* 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 ( false === $wp_filesystem ) {
return false;
}

if ( ! $wp_filesystem->exists( $backup_file_path ) ) {
return false;
}

$original_image = $this->get_original_image();
if ( false === $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 );
$this->sizes = array();
$this->parse_wp_metadata();
}

return true;
}
Comment thread
tijmenbruggeman marked this conversation as resolved.

/**
* 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 ( false === $wp_filesystem ) {
return false;
}

if ( ! $wp_filesystem->exists( $backup_file_path ) ) {
return true;
}

return $wp_filesystem->delete( $backup_file_path );
}
}
12 changes: 12 additions & 0 deletions src/class-tiny-logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ private function log( $level, $message, $context = array() ) {
// Ensure log directory exists.
$log_dir = dirname( $this->log_file_path );
$wp_filesystem = Tiny_Helpers::get_wp_filesystem();
if ( false === $wp_filesystem ) {
return;
}
if ( ! $wp_filesystem->exists( $log_dir ) ) {
wp_mkdir_p( $log_dir );
self::create_blocking_files( $log_dir );
Expand All @@ -201,6 +204,9 @@ private function log( $level, $message, $context = array() ) {
*/
private function rotate_logs() {
$wp_filesystem = Tiny_Helpers::get_wp_filesystem();
if ( false === $wp_filesystem ) {
return;
}
if ( ! $wp_filesystem->exists( $this->log_file_path ) ) {
return;
}
Expand All @@ -222,6 +228,9 @@ public static function clear_logs() {
$instance = self::get_instance();
$log_path = $instance->get_log_file_path();
$wp_filesystem = Tiny_Helpers::get_wp_filesystem();
if ( false === $wp_filesystem ) {
return false;
}
$file_exits = $wp_filesystem->exists( $log_path );
if ( $file_exits ) {
return $wp_filesystem->delete( $log_path );
Expand All @@ -239,6 +248,9 @@ public static function clear_logs() {
*/
private static function create_blocking_files( $log_dir ) {
$wp_filesystem = Tiny_Helpers::get_wp_filesystem();
if ( false === $wp_filesystem ) {
return;
}

$index_file = trailingslashit( $log_dir ) . 'index.html';
if ( ! $wp_filesystem->exists( $index_file ) ) {
Expand Down
76 changes: 43 additions & 33 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 @@ -746,7 +751,7 @@ public function render_media_column( $column, $id ) {
if ( self::MEDIA_COLUMN === $column ) {
$tiny_image = new Tiny_Image( $this->settings, $id );
if ( $tiny_image->file_type_allowed() ) {
echo '<div class="tiny-ajax-container">';
echo '<div class="tiny-ajax-container" data-tiny-media-id="' . absint( $id ) . '">';
$this->render_compress_details( $tiny_image );
echo '</div>';
}
Expand All @@ -761,7 +766,8 @@ public function show_media_info() {
echo '<h4>';
esc_html_e( 'JPEG, PNG, & WebP optimization', 'tiny-compress-images' );
echo '</h4>';
echo '<div class="tiny-ajax-container">';
echo '<div class="tiny-ajax-container" data-tiny-media-id="';
echo absint( $post->ID ) . '">';
$this->render_compress_details( $tiny_image );
echo '</div>';
echo '</div>';
Expand Down Expand Up @@ -909,6 +915,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 +941,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 +966,39 @@ 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
Loading
Loading