459 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			459 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * Implements backup/restore functions.
 | |
|  *
 | |
|  * @link https://ewww.io
 | |
|  * @package EIO
 | |
|  */
 | |
| 
 | |
| namespace EWWW;
 | |
| 
 | |
| if ( ! defined( 'ABSPATH' ) ) {
 | |
| 	exit;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Backup & Restore images from both local and cloud locations.
 | |
|  */
 | |
| class Backup extends Base {
 | |
| 
 | |
| 	/**
 | |
| 	 * An error from a restore operation.
 | |
| 	 *
 | |
| 	 * @access protected
 | |
| 	 * @var string $error_message
 | |
| 	 */
 | |
| 	protected $error_message = '';
 | |
| 
 | |
| 	/**
 | |
| 	 * A list of exclusions.
 | |
| 	 *
 | |
| 	 * @access protected
 | |
| 	 * @var array $exclusions
 | |
| 	 */
 | |
| 	protected $exclusions = array();
 | |
| 
 | |
| 	/**
 | |
| 	 * Backup mode (local/cloud).
 | |
| 	 *
 | |
| 	 * @var string $backup_mode
 | |
| 	 */
 | |
| 	protected $backup_mode = '';
 | |
| 
 | |
| 	/**
 | |
| 	 * Backup location.
 | |
| 	 *
 | |
| 	 * @var string $backup_dir
 | |
| 	 */
 | |
| 	protected $backup_dir = '';
 | |
| 
 | |
| 	/**
 | |
| 	 * Backup location for media uploads.
 | |
| 	 *
 | |
| 	 * @var string $backup_uploads_dir
 | |
| 	 */
 | |
| 	protected $backup_uploads_dir = '';
 | |
| 
 | |
| 	/**
 | |
| 	 * Backup location for images outside the wp-content directory.
 | |
| 	 *
 | |
| 	 * @var string $backup_root_dir
 | |
| 	 */
 | |
| 	protected $backup_root_dir = '';
 | |
| 
 | |
| 	/**
 | |
| 	 * Register (once) actions and filters for Backup and Restore.
 | |
| 	 */
 | |
| 	public function __construct() {
 | |
| 		global $eio_backup;
 | |
| 		if ( \is_object( $eio_backup ) ) {
 | |
| 			return $eio_backup;
 | |
| 		}
 | |
| 		parent::__construct();
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		if ( 'local' === $this->get_option( 'ewww_image_optimizer_backup_files' ) ) {
 | |
| 			$this->backup_mode = 'local';
 | |
| 			// Sub-folders of the content directory will be stored directly in the image-backup/ folder.
 | |
| 			$this->backup_dir = \trailingslashit( $this->content_dir ) . \trailingslashit( 'image-backup' );
 | |
| 			// Sub-folders of the content directory will be stored directly in the image-backup/ folder.
 | |
| 			$this->backup_uploads_dir = \trailingslashit( $this->backup_dir ) . \trailingslashit( 'uploads' );
 | |
| 			// Folders outside the content dir, and relative to ABSPATH will be stored in the root/ directory.
 | |
| 			$this->backup_root_dir = \trailingslashit( $this->backup_dir ) . \trailingslashit( 'root' );
 | |
| 		} elseif ( $this->get_option( 'ewww_image_optimizer_cloud_key' ) && $this->get_option( 'ewww_image_optimizer_backup_files' ) ) {
 | |
| 			$this->backup_mode = 'cloud';
 | |
| 		}
 | |
| 
 | |
| 		// AJAX action hook for manually restoring a single image from cloud/local backups.
 | |
| 		\add_action( 'wp_ajax_ewww_manual_image_restore_single', array( $this, 'restore_single_image_handler' ) );
 | |
| 		\add_action( 'ewww_image_optimizer_pre_optimization', array( $this, 'store_local_backup' ) );
 | |
| 
 | |
| 		$this->exclusions = array(
 | |
| 			$this->content_dir,
 | |
| 			'/wp-admin/',
 | |
| 			'/wp-includes/',
 | |
| 			'/cache/',
 | |
| 			'/dynamic/', // Nextgen dynamic images.
 | |
| 		);
 | |
| 		$this->exclusions = \apply_filters( 'ewww_image_optimizer_backup_exclusions', $this->exclusions );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Gets the error message from the most recent restore operation, if any.
 | |
| 	 *
 | |
| 	 * @return string An error message.
 | |
| 	 */
 | |
| 	public function get_error() {
 | |
| 		return (string) $this->error_message;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Sets the error message for restore operations.
 | |
| 	 *
 | |
| 	 * @param string $error An error message.
 | |
| 	 */
 | |
| 	public function throw_error( $error ) {
 | |
| 		if ( \is_string( $error ) ) {
 | |
| 			$this->error_message = \sanitize_text_field( $error );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Checks whether a file is in the uploads dir, content dir, or within the ABSPATH/root.
 | |
| 	 *
 | |
| 	 * This helps to deal with cases where folks have upload and/or content dirs outside ABSPATH.
 | |
| 	 *
 | |
| 	 * @param string $file The filename to backup.
 | |
| 	 * @return string The backup location for the file.
 | |
| 	 */
 | |
| 	public function get_backup_location( $file ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		if ( \ewww_image_optimizer_stream_wrapped( $file ) || 'local' !== $this->backup_mode ) {
 | |
| 			return '';
 | |
| 		}
 | |
| 		$upload_dir = \wp_get_upload_dir();
 | |
| 		$upload_dir = \trailingslashit( \realpath( $upload_dir['basedir'] ) );
 | |
| 		if ( $upload_dir && \strpos( $file, $upload_dir ) === 0 ) {
 | |
| 			$this->debug_message( 'using ' . $this->backup_uploads_dir );
 | |
| 			return \str_replace( $upload_dir, $this->backup_uploads_dir, $file );
 | |
| 		}
 | |
| 		$content_dir = \trailingslashit( \realpath( WP_CONTENT_DIR ) );
 | |
| 		if ( $content_dir && \strpos( $file, $content_dir ) === 0 ) {
 | |
| 			$this->debug_message( 'using ' . $this->backup_dir );
 | |
| 			return \str_replace( $content_dir, $this->backup_dir, $file );
 | |
| 		}
 | |
| 		$wp_dir = \trailingslashit( \realpath( ABSPATH ) );
 | |
| 		if ( $wp_dir && \strpos( $file, $wp_dir ) === 0 ) {
 | |
| 			$this->debug_message( 'using ' . $this->backup_root_dir );
 | |
| 			return \str_replace( $wp_dir, $this->backup_root_dir, $file );
 | |
| 		}
 | |
| 		return '';
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Checks to see if a backup is available for a given file.
 | |
| 	 *
 | |
| 	 * @param string $file The image file to search for a backup.
 | |
| 	 * @param array  $record The database record for the file. Optional.
 | |
| 	 * @return bool True if a backup is available, false otherwise.
 | |
| 	 */
 | |
| 	public function is_backup_available( $file, $record = false ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		$file = \ewww_image_optimizer_absolutize_path( $file );
 | |
| 		if ( 'local' === $this->backup_mode ) {
 | |
| 			\clearstatcache();
 | |
| 			$backup_file = $this->get_backup_location( $file );
 | |
| 			return $this->is_file( $backup_file );
 | |
| 		} elseif ( 'cloud' === $this->backup_mode ) {
 | |
| 			if ( ! $record || ! isset( $record['backup'] ) || ! isset( $record['updated'] ) ) {
 | |
| 				$record = \ewww_image_optimizer_find_already_optimized( $file );
 | |
| 			}
 | |
| 			if ( $record && $this->is_iterable( $record ) && ! empty( $record['backup'] ) && ! empty( $record['updated'] ) ) {
 | |
| 				$updated_time = \strtotime( $record['updated'] );
 | |
| 				if ( DAY_IN_SECONDS * 30 + $updated_time > \time() ) {
 | |
| 					return true;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Backup a file to local or cloud storage.
 | |
| 	 *
 | |
| 	 * @param string $file Name of the file to backup.
 | |
| 	 */
 | |
| 	public function backup_file( $file ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		$this->debug_message( "file: $file " );
 | |
| 		foreach ( $this->exclusions as $exclusion ) {
 | |
| 			if ( false !== \strpos( $file, $exclusion ) ) {
 | |
| 				return;
 | |
| 			}
 | |
| 		}
 | |
| 		if ( 'local' === $this->backup_mode ) {
 | |
| 			$this->store_local_backup( $file );
 | |
| 		} elseif ( 'cloud' === $this->backup_mode ) {
 | |
| 			$this->store_cloud_backup( $file );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Copy a file to the backup location.
 | |
| 	 *
 | |
| 	 * @param string $file Name of the file to backup.
 | |
| 	 */
 | |
| 	public function store_local_backup( $file ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		if ( 'local' !== $this->backup_mode ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		if ( ! $this->is_file( $file ) || ! $this->is_readable( $file ) ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		if ( apply_filters( 'ewww_image_optimizer_skip_local_backup', false, $file ) ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		$backup_file = $this->get_backup_location( $file );
 | |
| 		if ( ! $backup_file || $backup_file === $file ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		\clearstatcache();
 | |
| 		if ( $this->is_file( $backup_file ) ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		\wp_mkdir_p( \dirname( $backup_file ) );
 | |
| 		\clearstatcache();
 | |
| 		if ( ! \is_writable( \dirname( $backup_file ) ) ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		$this->debug_message( "backing up $file to $backup_file" );
 | |
| 		\copy( $file, $backup_file );
 | |
| 		if ( $this->filesize( $file ) !== $this->filesize( $backup_file ) ) {
 | |
| 			// In order to not store bogus files.
 | |
| 			$this->delete_file( $backup_file );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Send a file to the API for backup.
 | |
| 	 *
 | |
| 	 * @param string $file Name of the file to backup.
 | |
| 	 */
 | |
| 	protected function store_cloud_backup( $file ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		\ewww_image_optimizer_cloud_backup( $file );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Restore an image from local or cloud storage.
 | |
| 	 *
 | |
| 	 * @global object $wpdb
 | |
| 	 * @global object $ewwwdb A clone of $wpdb unless it is lacking utf8 connectivity.
 | |
| 	 *
 | |
| 	 * @param int|array $image The db record/ID of the image to restore.
 | |
| 	 * @return bool True if the image was restored successfully.
 | |
| 	 */
 | |
| 	public function restore_file( $image ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		global $wpdb;
 | |
| 		if ( \strpos( $wpdb->charset, 'utf8' ) === false ) {
 | |
| 			\ewww_image_optimizer_db_init();
 | |
| 			global $ewwwdb;
 | |
| 		} else {
 | |
| 			$ewwwdb = $wpdb;
 | |
| 		}
 | |
| 		$this->error_message = '';
 | |
| 		if ( ! \is_array( $image ) && ! empty( $image ) && \is_numeric( $image ) ) {
 | |
| 			$image = $ewwwdb->get_row( "SELECT id,path,backup FROM $ewwwdb->ewwwio_images WHERE id = $image", ARRAY_A );
 | |
| 		}
 | |
| 		if ( ! empty( $image['path'] ) ) {
 | |
| 			$image['path'] = \ewww_image_optimizer_absolutize_path( $image['path'] );
 | |
| 		}
 | |
| 		if ( empty( $image['path'] ) ) {
 | |
| 			return false;
 | |
| 		}
 | |
| 		if ( 'local' === $this->backup_mode ) {
 | |
| 			return $this->restore_from_local( $image );
 | |
| 		} elseif ( 'cloud' === $this->backup_mode ) {
 | |
| 			return $this->restore_from_cloud( $image );
 | |
| 		}
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Restore a file from a local backup location.
 | |
| 	 *
 | |
| 	 * @param array $image The db record of the image to restore.
 | |
| 	 */
 | |
| 	protected function restore_from_local( $image ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		if ( 'local' !== $this->backup_mode ) {
 | |
| 			return false;
 | |
| 		}
 | |
| 		$file = $image['path'];
 | |
| 		if ( ! \is_writable( \dirname( $file ) ) ) {
 | |
| 			$this->debug_message( "$file (or the parent dir) is not writable" );
 | |
| 			/* translators: %s: An image filename */
 | |
| 			$this->error_message = \sprintf( \__( '%s is not writable.', 'ewww-image-optimizer' ), $file );
 | |
| 			return false;
 | |
| 		}
 | |
| 		$backup_file = $this->get_backup_location( $file );
 | |
| 		if ( ! $backup_file || $backup_file === $file ) {
 | |
| 			$this->debug_message( "$backup_file is not a valid backup location for $file" );
 | |
| 			/* translators: %s: An image filename */
 | |
| 			$this->error_message = \sprintf( \__( 'Could not determine backup location for %s.', 'ewww-image-optimizer' ), $file );
 | |
| 			return false;
 | |
| 		}
 | |
| 		\clearstatcache();
 | |
| 		if ( ! $this->is_file( $backup_file ) ) {
 | |
| 			$this->debug_message( "$backup_file does not exist" );
 | |
| 			/* translators: %s: An image filename */
 | |
| 			$this->error_message = \sprintf( \__( 'No backup available for %s.', 'ewww-image-optimizer' ), $file );
 | |
| 			return false;
 | |
| 		}
 | |
| 		if ( \ewww_image_optimizer_mimetype( $file, 'i' ) !== \ewww_image_optimizer_mimetype( $backup_file, 'i' ) ) {
 | |
| 			$this->debug_message( "$backup_file is different type than $file " . \ewww_image_optimizer_mimetype( $backup_file, 'i' ) . ' vs. ' . \ewww_image_optimizer_mimetype( $file, 'i' ) );
 | |
| 			/* translators: %s: An image filename */
 | |
| 			$this->error_message = \sprintf( \__( 'Backup file for %s has the wrong mime type.', 'ewww-image-optimizer' ), $file );
 | |
| 			return false;
 | |
| 		}
 | |
| 		$filesize = $this->filesize( $file );
 | |
| 		$backsize = $this->filesize( $backup_file );
 | |
| 		if ( $filesize && $filesize === $backsize ) {
 | |
| 			// $this->delete_file( $backup_file );
 | |
| 			// return true; // Because restore not needed, already done!
 | |
| 		}
 | |
| 		$this->debug_message( "restoring $file from $backup_file" );
 | |
| 		copy( $backup_file, $file );
 | |
| 		if ( $this->filesize( $file ) === $this->filesize( $backup_file ) ) {
 | |
| 			if ( $this->is_file( $file . '.webp' ) && \is_writable( $file . '.webp' ) ) {
 | |
| 				$this->delete_file( $file . '.webp' );
 | |
| 			}
 | |
| 			/* $this->delete_file( $backup_file ); */
 | |
| 			global $wpdb;
 | |
| 			// Reset the image record.
 | |
| 			$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->ewwwio_images SET results = '', image_size = 0, updates = 0, updated=updated, level = 0 WHERE id = %d", $image['id'] ) );
 | |
| 			return true;
 | |
| 		}
 | |
| 		/* translators: %s: An image filename */
 | |
| 		$this->error_message = \sprintf( \__( 'Restore attempted for %s, but could not be confirmed.', 'ewww-image-optimizer' ), $file );
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Send a file to the API for backup.
 | |
| 	 *
 | |
| 	 * @param int|array $image The db record/ID of the image to restore.
 | |
| 	 * @return bool True if the image was restored successfully.
 | |
| 	 */
 | |
| 	protected function restore_from_cloud( $image ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		return \ewww_image_optimizer_cloud_restore_single_image( $image );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Delete the local backup file. Used when deleting an attachment.
 | |
| 	 *
 | |
| 	 * @param array $file The filename of the image for which we should remove the backup.
 | |
| 	 */
 | |
| 	public function delete_local_backup( $file ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		if ( ! $file ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		if ( 'local' !== $this->backup_mode ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		$backup_file = $this->get_backup_location( $file );
 | |
| 		if ( ! $backup_file || $backup_file === $file ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		\clearstatcache();
 | |
| 		if ( ! $this->is_file( $backup_file ) || ! \is_writable( $backup_file ) ) {
 | |
| 			return;
 | |
| 		}
 | |
| 		$this->delete_file( $backup_file );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Restore an attachment from the API or local backups.
 | |
| 	 *
 | |
| 	 * @global object $wpdb
 | |
| 	 * @global object $ewwwdb A clone of $wpdb unless it is lacking utf8 connectivity.
 | |
| 	 *
 | |
| 	 * @param int    $id The attachment id number.
 | |
| 	 * @param string $gallery Optional. The gallery from whence we came. Default 'media'.
 | |
| 	 * @param array  $meta Optional. The image metadata from the postmeta table.
 | |
| 	 * @return array The altered meta (if size differs), or the original value passed along.
 | |
| 	 */
 | |
| 	public function restore_backup_from_meta_data( $id, $gallery = 'media', $meta = array() ) {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		global $wpdb;
 | |
| 		if ( \strpos( $wpdb->charset, 'utf8' ) === false ) {
 | |
| 			\ewww_image_optimizer_db_init();
 | |
| 			global $ewwwdb;
 | |
| 		} else {
 | |
| 			$ewwwdb = $wpdb;
 | |
| 		}
 | |
| 		$images = $ewwwdb->get_results( "SELECT id,path,resize,backup FROM $ewwwdb->ewwwio_images WHERE attachment_id = $id AND gallery = '$gallery'", ARRAY_A );
 | |
| 		foreach ( $images as $image ) {
 | |
| 			if ( ! empty( $image['path'] ) ) {
 | |
| 				$image['path'] = \ewww_image_optimizer_absolutize_path( $image['path'] );
 | |
| 			}
 | |
| 			$this->restore_file( $image );
 | |
| 			if ( 'media' === $gallery && 'full' === $image['resize'] && ! empty( $meta['width'] ) && ! empty( $meta['height'] ) ) {
 | |
| 				list( $width, $height ) = \wp_getimagesize( $image['path'] );
 | |
| 				if ( (int) $width !== (int) $meta['width'] || (int) $height !== (int) $meta['height'] ) {
 | |
| 					$meta['height'] = $height;
 | |
| 					$meta['width']  = $width;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		if ( 'media' === $gallery ) {
 | |
| 			\remove_filter( 'wp_update_attachment_metadata', 'ewww_image_optimizer_update_filesize_metadata', 9 );
 | |
| 			$meta = \ewww_image_optimizer_update_filesize_metadata( $meta, $id );
 | |
| 		}
 | |
| 		if ( $this->s3_uploads_enabled() ) {
 | |
| 			\ewww_image_optimizer_remote_push( $meta, $id );
 | |
| 			$this->debug_message( 're-uploading to S3(_Uploads)' );
 | |
| 		}
 | |
| 		return $meta;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Handle the AJAX call for a single image restore.
 | |
| 	 */
 | |
| 	public function restore_single_image_handler() {
 | |
| 		$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
 | |
| 		// Check permissions of current user.
 | |
| 		$permissions = \apply_filters( 'ewww_image_optimizer_manual_permissions', '' );
 | |
| 		if ( ! \current_user_can( $permissions ) ) {
 | |
| 			// Display error message if insufficient permissions.
 | |
| 			$this->ob_clean();
 | |
| 			\wp_die( \wp_json_encode( array( 'error' => \esc_html__( 'You do not have permission to optimize images.', 'ewww-image-optimizer' ) ) ) );
 | |
| 		}
 | |
| 		// Make sure we didn't accidentally get to this page without an attachment to work on.
 | |
| 		if ( empty( $_REQUEST['ewww_image_id'] ) ) {
 | |
| 			// Display an error message since we don't have anything to work on.
 | |
| 			$this->ob_clean();
 | |
| 			\wp_die( \wp_json_encode( array( 'error' => \esc_html__( 'No image ID was provided.', 'ewww-image-optimizer' ) ) ) );
 | |
| 		}
 | |
| 		if ( empty( $_REQUEST['ewww_wpnonce'] ) || ! \wp_verify_nonce( \sanitize_key( $_REQUEST['ewww_wpnonce'] ), 'ewww-image-optimizer-tools' ) ) {
 | |
| 			$this->ob_clean();
 | |
| 			\wp_die( \wp_json_encode( array( 'error' => \esc_html__( 'Access token has expired, please reload the page.', 'ewww-image-optimizer' ) ) ) );
 | |
| 		}
 | |
| 		\session_write_close();
 | |
| 		$image = (int) $_REQUEST['ewww_image_id'];
 | |
| 		$this->debug_message( "attempting restore for $image" );
 | |
| 		if ( $this->restore_file( $image ) ) {
 | |
| 			$this->ob_clean();
 | |
| 			\wp_die( \wp_json_encode( array( 'success' => 1 ) ) );
 | |
| 		}
 | |
| 		$this->ob_clean();
 | |
| 		\wp_die( \wp_json_encode( array( 'error' => \esc_html__( 'Unable to restore image.', 'ewww-image-optimizer' ) ) ) );
 | |
| 	}
 | |
| }
 | |
| 
 | |
| global $eio_backup;
 | |
| $eio_backup = new Backup();
 |