class-backup.php 29.2 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878
<?php
/**
 * Smush backup class
 *
 * @package Smush\Core\Modules
 */

namespace Smush\Core\Modules;

use Smush\Core\Core;
use Smush\Core\Helper;
use WP_Smush;

if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class Backup
 */
class Backup extends Abstract_Module {

	/**
	 * Module slug.
	 *
	 * @var string
	 */
	protected $slug = 'backup';

	/**
	 * Key for storing file path for image backup
	 *
	 * @var string
	 */
	private $backup_key = 'smush-full';

	/**
	 * Backup constructor.
	 */
	public function init() {
		// 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 ) {
		return apply_filters( 'wp_smush_backup_exists', $this->get_backup_file( $attachment_id, $file_path ), $attachment_id, $file_path );
	}

	/**
	 * 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 ) ) {
			$count            = 1;
			$ext              = Helper::get_file_ext( $bak_file );
			$ext              = ".bak.$ext";
			$file_without_ext = rtrim( $bak_file, $ext );
			$bak_file         = $file_without_ext . '-' . $count . $ext;

			while ( Helper::file_exists( $bak_file, $attachment_id ) ) {
				$count++;
				$bak_file = $file_without_ext . '-' . $count . $ext;
			}

			return $bak_file;
		}
		return $bak_file;
	}

	/**
	 * 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.
	 *
	 * @return void
	 */
	public function create_backup( $file_path, $attachment_id ) {
		if ( empty( $file_path ) || empty( $attachment_id ) ) {
			return;
		}

		// If backup not enabled, return.
		if ( ! $this->is_active() ) {
			return;
		}

		/**
		 * If [ not compress original ]:
		 *    if [ is-scaled.file ]:
		 *          Backup original file.
		 *    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.
		 * else:
		 *      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.
		 * end.
		 */

		// 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 );
					return;
				}
			}

			$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 ) );
				return;
			}

			$should_backup = false;

			// We should backup this image if we can resize it.
			if ( $mod->resize->is_active() && $mod->resize->should_resize( $attachment_id ) ) {
				$should_backup = true;
			}

			// We should backup this image if we can convert it from PNG to JPEG.
			if (
				! $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 )
			) {
				$should_backup = true;
			}

			// 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 ) );
				return;
			}
		}

		/**
		 * 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 );
		if ( $backup_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 ) );
					return;
				}
			}
		}

		/**
		 * 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 );
		} else {
			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 ) ) {
			return;
		}

		// Get the Existing backup sizes.
		$backup_sizes = $this->get_backup_sizes( $attachment_id );
		if ( empty( $backup_sizes ) ) {
			$backup_sizes = array();
		}

		// 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;
		}

		// 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 ) );
			return;
		}

		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 ),
			'width'  => $width,
			'height' => $height,
		);

		wp_cache_delete( 'images_with_backups', 'wp-smush' );
		update_post_meta( $attachment_id, '_wp_attachment_backup_sizes', $backup_sizes );
	}

	/**
	 * Get 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.
	 *
	 * @since 3.9.6
	 *
	 * @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
	 * the new backup file.
	 *
	 * @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 ) ) {
			return false;
		}

		// We don't use .bak file from 3rd party while backing up.
		$backed_up_file = $this->get_backup_file( $attachment_id, $backup_file );
		$was_backed_up  = true;

		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 );
		}

		// Backup the image.
		if ( $was_backed_up ) {
			$this->add_to_image_backup_sizes( $attachment_id, $backup_file );
		}

		return $was_backed_up;
	}

	/**
	 * Get the backup file from the meta.
	 *
	 * @since 3.9.6
	 *
	 * @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( $id ) ) {
			return null;
		}

		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 ) ) {
				return null;
			}
		}

		// Initial result.
		$backup_file = false;
		// 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.
		if ( $backup_sizes ) {
			// 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 ( ! $backup_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;
					}
				}
			}
		}

		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 ) {
		// 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() {
		$images = wp_cache_get( 'images_with_backups', 'wp-smush' );

		if ( ! $images ) {
			global $wpdb;
			$images = $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%')"
			); // Db call ok.

			if ( $images ) {
				wp_cache_set( 'images_with_backups', $images, 'wp-smush' );
			}
		}

		return $images;
	}

	/**
	 * 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 );

		$status = $id && $this->restore_image( $id, false );

		$original_meta = wp_get_attachment_metadata( $id, true );

		// Try to get the file name from path.
		$file_name = explode( '/', $original_meta['file'] );
		$file_name = is_array( $file_name ) ? array_pop( $file_name ) : $original_meta['file'];

		wp_send_json_success(
			array(
				'success' => $status,
				'src'     => $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 );
	}

}