PK     KQ0][F×’Ô  Ô    class-backups-controller.phpnu „[µü¤        <?php

namespace Smush\Core\Backups;

use Smush\Core\Controller;
use Smush\Core\Core;
use Smush\Core\File_System;
use Smush\Core\Helper;
use Smush\Core\Media\Media_Item_Cache;
use Smush\Core\Media\Media_Item_Optimizer;
use Smush\Core\Product_Analytics\Product_Analytics;
use Smush\Core\Settings;
use WP_Smush;

class Backups_Controller extends Controller {
	/**
	 * @var Media_Item_Cache
	 */
	private $media_item_cache;
	/**
	 * @var \WDEV_Logger|null
	 */
	private $logger;
	/**
	 * @var File_System
	 */
	private $fs;
	/**
	 * @var Backups
	 */
	private $backups;

	public function __construct() {
		$this->media_item_cache = Media_Item_Cache::get_instance();
		$this->logger           = Helper::logger();
		$this->fs               = new File_System();
		$this->backups          = new Backups();

		$this->register_action( 'wp_ajax_smush_restore_image', array( $this, 'handle_restore_ajax' ) );
		$this->register_action( 'delete_attachment', array( $this, 'delete_backup_file' ) );
		$this->register_action( 'wp_ajax_restore_step', array( $this, 'restore_step' ) );
		$this->register_action( 'wp_ajax_get_image_count', array( $this, 'get_image_count' ) );
	}

	public function handle_restore_ajax() {
		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.
		// TODO: change Helper::is_user_allowed to a non static method
		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;
		$media_item    = Media_Item_Cache::get_instance()->get( $attachment_id );
		if ( ! $media_item->is_mime_type_supported() ) {
			wp_send_json_error( array(
				'error_msg' => $media_item->get_errors()->get_error_message(),
			) );
		}

		$optimizer = new Media_Item_Optimizer( $media_item );
		$restored  = $optimizer->restore();

		$this->track_single_image_restore( $restored, $optimizer );

		if ( ! $restored ) {
			wp_send_json_error( array(
				'error_msg' => esc_html__( 'Unable to restore image', 'wp-smushit' ),
			) );
		}

		$button_html = WP_Smush::get_instance()->library()->generate_markup( $attachment_id );
		$file_path   = $media_item->get_main_size()->get_file_path();
		$size        = $this->fs->file_exists( $file_path )
			? $this->fs->filesize( $file_path )
			: 0;
		if ( $size > 0 ) {
			$update_size = size_format( $size );
		}

		wp_send_json_success( array(
			'stats'    => $button_html,
			'new_size' => isset( $update_size ) ? $update_size : 0,
		) );
	}

	private function track_single_image_restore( $restored, $optimizer ) {
		$restoration_errors      = $optimizer->get_restoration_errors();
		$missing_backup_detected = ! $restored && ! empty( $restoration_errors->get_error_message( 'missing_backup' ) );

		$limit_per_day = 10;
		Product_Analytics::get_instance()->maybe_track(
			'Single Image Restore',
			array(
				'Total images restored' => $restored ? 1 : 0,
				'Total images'          => 1,
				'Backup not found'      => $missing_backup_detected ? 1 : 0,
				'Backup Status'         => Settings::get_instance()->is_backup_active() ? 'Enabled' : 'Disabled',
			),
			$limit_per_day
		);
	}

	public function delete_backup_file( $attachment_id ) {
		$media_item = $this->media_item_cache->get( $attachment_id );
		if ( $media_item->is_valid() && $media_item->get_default_backup_size() ) {
			// Delete the file
			$default_backup_path = $media_item->get_default_backup_size()->get_file_path();
			if ( $this->fs->file_exists( $default_backup_path ) ) {
				$this->fs->unlink( $default_backup_path );
			}

			// Delete the meta
			$media_item->remove_default_backup_size();
			$media_item->save();
		} else {
			$this->logger->error( sprintf( 'Count not delete webp versions of the media item [%d]', $attachment_id ) );
		}
	}

	/**
	 * 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() ) {
			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'    => $media_item->get_edit_link(),
				'error_code' => $status ? '' : $optimizer->get_restoration_errors()->get_error_code(),
			)
		);
	}

	/**
	 * 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() ) {
			wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 );
		}
		wp_send_json_success(
			array(
				'items' => $this->backups->count_attachments_with_backups(),
			)
		);
	}

}
PK     KQ0]Ìm[þ  þ    class-backups.phpnu „[µü¤        <?php

namespace Smush\Core\Backups;

use Smush\Core\File_System;
use Smush\Core\Helper;
use Smush\Core\Media\Media_Item;
use Smush\Core\Media\Media_Item_Optimizer;
use Smush\Core\Settings;
use WDEV_Logger;
use WP_Error;

class Backups {
	/**
	 * @var WDEV_Logger
	 */
	private $logger;
	/**
	 * @var Settings|null
	 */
	private $settings;
	/**
	 * @var File_System
	 */
	private $fs;

	/**
	 * @var WP_Error
	 */
	private $errors;

	public function __construct() {
		$this->logger   = Helper::logger()->backup();
		$this->settings = Settings::get_instance();
		$this->fs       = new File_System();
		$this->errors   = new WP_Error();
	}

	public function create_backup_file( $source_image_path ) {
		$bak_file_path = $this->generate_unique_bak_file_path( $source_image_path );
		$copied        = $this->fs->copy( $source_image_path, $bak_file_path );
		if ( $copied ) {
			return basename( $bak_file_path );
		}

		return false;
	}

	private function generate_unique_bak_file_path( $source_image_path ) {
		// TODO: why not use the wp_unique_filename method for this?
		$path_info        = pathinfo( $source_image_path );
		$ext              = $path_info['extension'];
		$bak_ext          = ".bak.$ext";
		$file_without_ext = trailingslashit( $path_info['dirname'] ) . $path_info['filename'];
		$bak_file_path    = $file_without_ext . $bak_ext;
		if ( ! $this->fs->file_exists( $bak_file_path ) ) {
			return $bak_file_path;
		}

		$count         = 1;
		$bak_file_path = $file_without_ext . '-' . $count . $bak_ext;
		while ( $this->fs->file_exists( $bak_file_path ) ) {
			$count ++;
			$bak_file_path = $file_without_ext . '-' . $count . $bak_ext;
		}

		return $bak_file_path;
	}

	/**
	 * @param $media_item Media_Item
	 * @param $optimizer Media_Item_Optimizer
	 *
	 * @return bool
	 */
	public function maybe_create_backup( $media_item, $optimizer ) {
		if ( ! $this->settings->is_backup_active() ) {
			return false;
		}

		// Maybe it already exists?
		$backup_size = $media_item->get_default_backup_size();
		if ( $backup_size && $backup_size->get_file_path() && $this->fs->file_exists( $backup_size->get_file_path() ) ) {
			// We have a perfectly viable backup available already
			$this->logger->info( sprintf( 'Found an existing backed up file [%s] for attachment [%d].', $backup_size->get_file(), $media_item->get_id() ) );

			return false;
		}

		$size_to_backup = $media_item->get_full_or_scaled_size();
		if ( ! $size_to_backup || ! $size_to_backup->file_exists() ) {
			$this->logger->warning( sprintf( 'File not found, could not backup up for attachment [%d].', $media_item->get_id() ) );

			return false;
		}

		$create_copy = $optimizer->should_optimize_size( $size_to_backup );

		// Create the backup
		$file_name = $size_to_backup->get_file_name();
		$width     = $size_to_backup->get_width();
		$height    = $size_to_backup->get_height();
		if ( $create_copy ) {
			$file_name = $this->create_backup_file( $size_to_backup->get_file_path() );
			if ( ! $file_name ) {
				$this->logger->info( sprintf( 'File operation failed when trying to create backup file [%s] for attachment [%d].', $size_to_backup->get_file_path(), $media_item->get_id() ) );

				return false;
			}
		}
		$media_item->add_backup_size( $file_name, $width, $height );
		$media_item->save();

		return true;
	}

	/**
	 * @param $media_item Media_Item
	 *
	 * @return bool
	 */
	public function restore_backup( $media_item ) {
		do_action( 'wp_smush_before_restore_backup_attempt', $media_item->get_id() );

		return $this->restore_backup_to_file_path(
			$media_item,
			$media_item->get_original_image_path() // Directly using the path here because the size object is not available when the file doesn't exist on the disk
		);
	}

	/**
	 * @param $media_item Media_Item
	 * @param $file_path
	 *
	 * @return bool
	 */
	public function restore_backup_to_file_path( $media_item, $file_path ) {
		$backup_file_path = '';
		$attachment_id    = $media_item->get_id();
		$this->reset_errors(); // Reset errors.

		do {
			$backup_size = $media_item->get_default_backup_size();
			if ( ! $backup_size ) {
				$this->logger->warning( sprintf( 'A restore was attempted for attachment [%d] but we did not find a backup file.', $media_item->get_id() ) );
				$this->errors->add( 'missing_backup', 'Missing backup file.' );
				break;
			}

			$backup_file_path = $backup_size->get_file_path();

			do_action( 'wp_smush_before_restore_backup', $backup_file_path, $attachment_id, $file_path );

			if ( ! $this->fs->file_exists( $backup_file_path ) ) {
				// Clean up
				$media_item->remove_default_backup_size();
				$media_item->save();

				$this->logger->warning( sprintf( 'A restore was attempted for attachment [%d] but the backup file does not exist.', $media_item->get_id() ) );
				$this->errors->add( 'missing_backup', 'Missing backup file.' );
				break;
			}

			$is_separate_backup_file = $backup_file_path !== $file_path;
			if ( $is_separate_backup_file ) {
				$copied = $this->fs->copy( $backup_file_path, $file_path );
				if ( $copied ) {
					$this->fs->unlink( $backup_file_path );
				} else {
					$this->logger->warning( sprintf( 'Error copying from [%s] to [%s].', $backup_file_path, $file_path ) );
					$this->errors->add( 'copy_failed', 'Error copying backup file.' );
					break;
				}
			}

			$metadata = wp_generate_attachment_metadata( $attachment_id, $file_path );
			$this->maybe_update_attached_file( $media_item, $metadata, $file_path );
			wp_update_attachment_metadata( $attachment_id, $metadata );
			/*
			 * TODO: we might want to follow media_handle_upload which makes an extra update attachment call like this:
			 * wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
			 */

			// The metadata has changed because we called wp_generate_attachment_metadata. We need to reset before saving.
			$media_item->reset();

			$media_item->remove_default_backup_size();
			$media_item->save();
		} while ( 0 );

		$restored = ! $this->errors->has_errors();

		do_action( 'wp_smush_after_restore_backup', $restored, $backup_file_path, $attachment_id, $file_path );

		return $restored;
	}

	/**
	 * Update the attached file for a media item if the file path has changed.
	 *
	 * @param Media_Item $media_item Media Item.
	 * @param array $metadata Image metadata.
	 * @param mixed $file_path Image file path.
	 */
	private function maybe_update_attached_file( $media_item, $metadata, $file_path ) {
		if ( empty( $metadata['file'] ) ) {
			return;
		}

		$current_attached_file     = $media_item->get_attached_file();
		$attached_file_changed     = ! str_contains( $current_attached_file, $metadata['file'] );
		$attached_file_is_original = str_contains( $file_path, $metadata['file'] );
		if ( $attached_file_changed && $attached_file_is_original ) {
			update_attached_file( $media_item->get_id(), $file_path );
		}
	}

	public function count_attachments_with_backups() {
		return count( $this->get_attachments_with_backups() );
	}

	public function get_attachments_with_backups() {
		global $wpdb;
		$wild                     = '%';
		$backup_key_like          = $wild . $wpdb->esc_like( Media_Item::get_default_backup_key() ) . $wild;
		$png_key_like             = $wild . 'smush_png_path' . $wild;
		$attachments_with_backups = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT post_id
				FROM {$wpdb->postmeta}
				WHERE meta_key=%s AND (`meta_value` LIKE %s OR `meta_value` LIKE %s)",
				Media_Item::get_backup_sizes_meta_key(),
				$backup_key_like,
				$png_key_like
			)
		);

		return $attachments_with_backups;
	}

	public function items_with_backup_exist() {
		return $this->count_attachments_with_backups() > 0;
	}

	/**
	 * Reset the error state.
	 */
	private function reset_errors() {
		$this->errors->errors = array();
	}

	/**
	 * Get the errors.
	 *
	 * @return WP_Error
	 */
	public function get_errors() {
		return $this->errors;
	}
}
PK     KQ0]ÛB3®C	  C	    class-bulk-restore.phpnu „[µü¤        <?php
namespace Smush\Core\Backups;

use Smush\Core\Media\Media_Item_Cache;
use Smush\Core\Media\Media_Item_Optimizer;

/**
 * Class Bulk_Restore
 *
 * Handles the bulk restoration of media items.
 *
 * @package Smush\Core\Backups
 */

class Bulk_Restore {
	/**
	 * @var array
	 */
	private $attachment_ids;

	/**
	 * Restored attachments count.
	 *
	 * @var int $restored_count
	 */
	private $restored_count = 0;

	/**
	 * Error counts by error code.
	 *
	 * @var array
	 */
	private $error_counts = array();

	/**
	 * Bulk_Restore constructor.
	 *
	 * @param array $attachment_ids Array of attachment IDs to restore.
	 */
	public function __construct( $attachment_ids ) {
		$this->attachment_ids = array_filter( $attachment_ids, 'is_numeric' );
	}

	/**
	 * Restore media items in bulk.
	 */
	public function bulk_restore() {
		foreach ( $this->attachment_ids as $index => $id ) {
			$media_item = Media_Item_Cache::get_instance()->get( $id );
			$optimizer  = new Media_Item_Optimizer( $media_item );
			$restored   = $optimizer->restore();
			if ( ! $restored ) {
				$error_code = $optimizer->get_restoration_errors()->get_error_code();
				if ( $error_code ) {
					$this->error_counts[ $error_code ] = ( $this->error_counts[ $error_code ] ?? 0 ) + 1;
				} else {
					// The media item could not be restored because the image is in a processing state.
					// We skip restoration for items in this state.
					unset( $this->attachment_ids[ $index ] );
				}
			} else {
				++$this->restored_count;
			}
		}

		do_action(
			'wp_smush_bulk_restore_completed',
			array(
				'type'                 => 'Bulk',
				'restored_count'       => $this->get_restored_count(),
				'total_count'          => $this->get_total_count(),
				'missing_backup_count' => $this->get_error_count( 'missing_backup' ),
			)
		);
	}

	/**
	 * Get the total number of attachments to restore.
	 *
	 * @return int
	 */
	public function get_total_count() {
		return count( $this->attachment_ids );
	}

	/**
	 * Get the total number of restored attachments.
	 *
	 * @return int
	 */
	public function get_restored_count() {
		return $this->restored_count;
	}

	/**
	 * Get the error count for a specific error code.
	 *
	 * @param string $error_code Error code.
	 *
	 * @return int
	 */
	public function get_error_count( $error_code ) {
		return $this->error_counts[ $error_code ] ?? 0;
	}
}
PK       KQ0][F×’Ô  Ô                  class-backups-controller.phpnu „[µü¤        PK       KQ0]Ìm[þ  þ                 class-backups.phpnu „[µü¤        PK       KQ0]ÛB3®C	  C	              _7  class-bulk-restore.phpnu „[µü¤        PK        è@    