debug_message( '' . __METHOD__ . '()' ); 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( '' . __METHOD__ . '()' ); 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( '' . __METHOD__ . '()' ); $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( '' . __METHOD__ . '()' ); $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( '' . __METHOD__ . '()' ); 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( '' . __METHOD__ . '()' ); \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( '' . __METHOD__ . '()' ); 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( '' . __METHOD__ . '()' ); 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( '' . __METHOD__ . '()' ); 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( '' . __METHOD__ . '()' ); 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( '' . __METHOD__ . '()' ); 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( '' . __METHOD__ . '()' ); // 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();