wp_back/wp-content/plugins/ewww-image-optimizer/classes/class-backup.php
2024-05-20 15:37:46 +03:00

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