From 6587bb31af27f80ba618c8448a1fd342914b5de8 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Wed, 26 Jan 2022 13:06:59 -0500 Subject: [PATCH 01/13] Add code for WPR2x that was removed from 68c489b4 This is just grabbing the WPR2X code that was deleted. It's no longer wired into our stuff, so it won't do anything yet. --- .../class-imagify-wp-retina-2x-core.php | 1694 +++++++++++++++++ .../classes/class-imagify-wp-retina-2x.php | 681 +++++++ inc/3rd-party/perfect-images/wp-retina-2x.php | 8 + 3 files changed, 2383 insertions(+) create mode 100644 inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x-core.php create mode 100644 inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x.php create mode 100755 inc/3rd-party/perfect-images/wp-retina-2x.php diff --git a/inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x-core.php b/inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x-core.php new file mode 100644 index 000000000..1916d69ca --- /dev/null +++ b/inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x-core.php @@ -0,0 +1,1694 @@ +filesystem = Imagify_Filesystem::get_instance(); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** GENERATE RETINA IMAGES ================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Generate retina images (except full size), and optimize them if the non-retina images are. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return bool|object True on success, false if prevented, a WP_Error object on failure. + */ + public function generate_retina_images( $attachment ) { + $tests = $this->validate( __FUNCTION__, $attachment ); + + if ( true !== $tests ) { + return $tests; + } + + // Backup the optimized full-sized image and replace it by the original backup file, so it can be used to create new retina images. + $this->backup_optimized_file( $attachment ); + + if ( ! $this->filesystem->exists( $attachment->get_original_path() ) ) { + return new WP_Error( 'file_missing', 'The main file does not exist.' ); + } + + // Create retina images. + wr2x_generate_images( wp_get_attachment_metadata( $attachment->get_id() ) ); + + // Put the optimized full-sized file back. + $this->put_optimized_file_back( $attachment ); + + /** + * If the non-retina images are optimized by Imagify (or at least the user wanted it to be optimized at some point, and now has a "already optimized" or "error" status), optimize newly created retina files. + * If the retina version of the full size exists and is not optimized yet, it will be processed as well. + */ + if ( $attachment->is_optimized() && $this->can_auto_optimize() ) { + $this->optimize_retina_images( $attachment ); + } + + return true; + } + + /** + * Delete previous retina images and recreate them (except full size), and optimize them if they previously were. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return bool|object True on success, false if prevented, a WP_Error object on failure. + */ + public function regenerate_retina_images( $attachment ) { + $tests = $this->validate( __FUNCTION__, $attachment ); + + if ( true !== $tests ) { + return $tests; + } + + // Delete the retina files and remove retina sizes from Imagify data. + $result = $this->delete_retina_images( $attachment ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + // Create new retina files (and optimize them if they previously were). + return $this->generate_retina_images( $attachment ); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** DELETE RETINA IMAGES ==================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Delete the retina images. Also removes the related Imagify data. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param bool $delete_full_image True to also delete the retina version of the full size. + * @return bool|object True on success, false if prevented, a WP_Error object on failure. + */ + public function delete_retina_images( $attachment, $delete_full_image = false ) { + $tests = $this->validate( __FUNCTION__, $attachment, array( + 'metadata_dimensions' => 'error', + ) ); + + if ( true !== $tests ) { + return $tests; + } + + /** + * To be a bit faster we update the data at once at the end. + * + * @see Imagify_WP_Retina_2x::remove_retina_thumbnail_data_hook(). + */ + $this->prevent( 'remove_retina_image_data_by_filename' ); + + // Delete the retina thumbnails. + wr2x_delete_attachment( $attachment->get_id(), $delete_full_image ); + + $this->allow( 'remove_retina_image_data_by_filename' ); + + // Remove retina sizes from Imagify data. + $this->remove_retina_images_data( $attachment, $delete_full_image ); + + return true; + } + + /** + * Delete the retina version of the full size. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return bool|object True on success, false if prevented, a WP_Error object on failure. + */ + public function delete_full_retina_image( $attachment ) { + $tests = $this->validate( __FUNCTION__, $attachment, array( + 'metadata_file' => false, + 'metadata_sizes' => false, + ) ); + + if ( true !== $tests ) { + return $tests; + } + + $retina_path = wr2x_get_retina( $attachment->get_original_path() ); + + if ( $retina_path ) { + // The file exists. + $this->filesystem->delete( $retina_path ); + } + + // Delete related Imagify data. + return $this->remove_size_from_imagify_data( $attachment, 'full@2x' ); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** REPLACE IMAGES ========================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Replace an attachment (except the retina version of the full size). + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param string $file_path Path to the new file. + * @return bool|object True on success, false if prevented, a WP_Error object on failure. + */ + public function replace_attachment( $attachment, $file_path ) { + $tests = $this->validate( __FUNCTION__, $attachment, array( + 'metadata_sizes' => false, + ) ); + + if ( true !== $tests ) { + return $tests; + } + + $attachment_id = $attachment->get_id(); + $sizes = $this->get_attachment_sizes( $attachment ); + $original_path = $attachment->get_original_path(); + $dir_path = $this->filesystem->path_info( $original_path, 'dir_path' ); + + // Insert the new file (and overwrite the full size). + $moved = $this->filesystem->move( $file_path, $original_path, true ); + + if ( ! $moved ) { + return new WP_Error( 'not_writable', __( 'Replacement failed.', 'imagify' ) ); + } + + // Delete retina images. + $this->delete_retina_images( $attachment ); + + // Delete the non-retina images. + if ( $sizes ) { + foreach ( $sizes as $name => $attr ) { + $size_path = $dir_path . $attr['file']; + + if ( $this->filesystem->exists( $size_path ) && $this->filesystem->is_file( $size_path ) ) { + // If the deletion fails, we're screwed anyway since the main file has been deleted, so no need to return an error here. + $this->filesystem->delete( $size_path ); + } + } + } + + // Get some Imagify data before deleting everything. + $optimization_level = $this->get_optimization_level( $attachment ); + $full_retina_data = $attachment->get_data(); + $full_retina_data = ! empty( $full_retina_data['sizes']['full@2x'] ) ? $full_retina_data['sizes']['full@2x'] : false; + $full_retina_optimized = $full_retina_data && ! empty( $full_retina_data['success'] ); + + // Delete the Imagify data. + $attachment->delete_imagify_data(); + + // Delete the backup file. + $attachment->delete_backup(); + + // Prevent auto-optimization. + Imagify_Auto_Optimization::prevent_optimization( $attachment_id ); + + // Generate the non-retina images and the related WP metadata. + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $original_path ) ); + + // Allow auto-optimization back. + Imagify_Auto_Optimization::allow_optimization( $attachment_id ); + + // Generate retina images (since the Imagify data has been deleted, the images won't be optimized here). + $result = $this->generate_retina_images( $attachment ); + + if ( is_wp_error( $result ) ) { + if ( $full_retina_optimized ) { + // The retina version of the full size is optimized: restore it overwise the user may optimize it again some day. + $this->restore_full_retina_file( $attachment ); + } + + return $result; + } + + if ( $this->can_auto_optimize() ) { + if ( $full_retina_optimized ) { + // Don't optimize the retina full size, it already is. + remove_filter( 'imagify_fill_full_size_data', array( Imagify_WP_Retina_2x::get_instance(), 'optimize_full_retina_version_hook' ) ); + } + + /** + * Optimize everyone. + * + * @see Imagify_WP_Retina_2x::optimize_full_retina_version_hook() + * @see Imagify_WP_Retina_2x::optimize_retina_version_hook() + * @see Imagify_WP_Retina_2x::maybe_optimize_unauthorized_retina_version_hook(). + */ + $attachment->optimize( $optimization_level ); + + if ( $full_retina_optimized ) { + add_filter( 'imagify_fill_full_size_data', array( Imagify_WP_Retina_2x::get_instance(), 'optimize_full_retina_version_hook' ), 10, 8 ); + + if ( $attachment->is_optimized() ) { + // Put data back. + $data = $attachment->get_data(); + $data['sizes']['full@2x'] = $full_retina_data; + update_post_meta( $attachment_id, '_imagify_data', $data ); + } else { + $this->restore_full_retina_file( $attachment ); + } + } + } elseif ( $full_retina_optimized ) { + // The retina version of the full size is optimized: restore it overwise the user may optimize it again some day. + $this->restore_full_retina_file( $attachment ); + } + + return true; + } + + /** + * Replace an attachment (except the retina version of the full size). + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param string $file_path Path to the new file. + * @return bool|object True on success, false if prevented, a WP_Error object on failure. + */ + public function replace_full_retina_image( $attachment, $file_path ) { + $tests = $this->validate( __FUNCTION__, $attachment, array( + 'metadata_file' => false, + 'metadata_sizes' => false, + ) ); + + if ( true !== $tests ) { + return $tests; + } + + // Replace the file. + $retina_path = $this->get_retina_path( $attachment->get_original_path() ); + $moved = $this->filesystem->move( $file_path, $retina_path, true ); + + if ( ! $moved ) { + return new WP_Error( 'not_writable', __( 'Replacement failed.', 'imagify' ) ); + } + + // Delete related Imagify data. + $this->remove_size_from_imagify_data( $attachment, 'full@2x' ); + + // Delete previous backup file. + $result = $this->delete_file_backup( $retina_path ); + + if ( is_wp_error( $result ) ) { + $this->filesystem->delete( $file_path ); + return $result; + } + + // Optimize. + if ( $attachment->is_optimized() && $this->can_auto_optimize() ) { + return $this->optimize_full_retina_image( $attachment ); + } + } + + + /** ----------------------------------------------------------------------------------------- */ + /** OPTIMIZE RETINA IMAGES ================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Optimize retina images. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param bool $optimize_full_size False to not optimize the retina version of the full size. + * @return bool|object True on success, false if prevented or not supported or no sizes, a WP_Error object on failure. + */ + public function optimize_retina_images( $attachment, $optimize_full_size = true ) { + $tests = $this->validate( __FUNCTION__, $attachment, array( + 'supported' => true, + 'can_optimize' => 'error', + 'metadata_dimensions' => 'error', + ) ); + + if ( true !== $tests ) { + return $tests; + } + + $metadata = wp_get_attachment_metadata( $attachment->get_id() ); + + if ( $optimize_full_size ) { + $metadata['sizes']['full'] = array( + 'file' => $this->filesystem->file_name( $metadata['file'] ), + 'width' => (int) $metadata['width'], + 'height' => (int) $metadata['height'], + 'mime-type' => get_post_mime_type( $attachment->get_id() ), + ); + } + + return $this->optimize_retina_sizes( $attachment, $metadata['sizes'] ); + } + + /** + * Optimize the full size retina image. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return bool|object True on success, false if prevented or not supported or no sizes, a WP_Error object on failure. + */ + public function optimize_full_retina_image( $attachment ) { + $tests = $this->validate( __FUNCTION__, $attachment, array( + 'supported' => true, + 'can_optimize' => 'error', + 'metadata_dimensions' => 'error', + ) ); + + if ( true !== $tests ) { + return $tests; + } + + $metadata = wp_get_attachment_metadata( $attachment->get_id() ); + + $sizes = array( + 'full' => array( + 'file' => $this->filesystem->file_name( $metadata['file'] ), + 'width' => (int) $metadata['width'], + 'height' => (int) $metadata['height'], + 'mime-type' => get_post_mime_type( $attachment->get_id() ), + ), + ); + + return $this->optimize_retina_sizes( $attachment, $sizes ); + } + + /** + * Optimize the given retina images. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param array $sizes A list of non-retina sizes, formatted like in wp_get_attachment_metadata(). + * @return bool|object True on success, false if prevented or not supported or no sizes, a WP_Error object on failure. + */ + public function optimize_retina_sizes( $attachment, $sizes ) { + $tests = $this->validate( __FUNCTION__, $attachment, array( + 'supported' => true, + 'can_optimize' => 'error', + 'metadata_dimensions' => 'error', + ) ); + + if ( true !== $tests ) { + return $tests; + } + + $attachment_id = $attachment->get_id(); + $optimization_level = $this->get_optimization_level( $attachment ); + + /** + * Filter the retina thumbnail sizes to optimize for a given attachment. This includes the sizes disabled in Imagify’ settings. + * + * @since 1.8 + * @author Grégory Viguier + * + * @param array $sizes An array of non-retina thumbnail sizes. + * @param int $attachment_id The attachment ID. + * @param int $optimization_level The optimization level. + */ + $sizes = apply_filters( 'imagify_attachment_retina_sizes', $sizes, $attachment_id, $optimization_level ); + + if ( ! $sizes || ! is_array( $sizes ) ) { + return false; + } + + $original_dirpath = $this->filesystem->dir_path( $attachment->get_original_path() ); + + foreach ( $sizes as $size_key => $image_data ) { + $retina_path = wr2x_get_retina( $original_dirpath . $image_data['file'] ); + + if ( ! $retina_path ) { + unset( $sizes[ $size_key ] ); + continue; + } + + // The file exists. + $sizes[ $size_key ]['retina-path'] = $retina_path; + } + + if ( ! $sizes ) { + return false; + } + + $attachment->set_running_status(); + + /** + * Fires before optimizing the retina images. + * + * @since 1.8 + * @author Grégory Viguier + * + * @param int $attachment_id The attachment ID. + * @param array $sizes An array of non-retina thumbnail sizes. + * @param int $optimization_level The optimization level. + */ + do_action( 'before_imagify_optimize_retina_images', $attachment_id, $sizes, $optimization_level ); + + $imagify_data = $attachment->get_data(); + + foreach ( $sizes as $size_key => $image_data ) { + $imagify_data = $this->optimize_retina_image( array( + 'data' => $imagify_data, + 'attachment' => $attachment, + 'retina_path' => $image_data['retina-path'], + 'size_key' => $size_key, + 'optimization_level' => $optimization_level, + ) ); + } + + $this->update_imagify_data( $attachment, $imagify_data ); + + /** + * Fires after optimizing the retina images. + * + * @since 1.8 + * @author Grégory Viguier + * + * @param int $attachment_id The attachment ID. + * @param array $sizes An array of non-retina thumbnail sizes. + * @param int $optimization_level The optimization level. + */ + do_action( 'after_imagify_optimize_retina_images', $attachment_id, $sizes, $optimization_level ); + + $attachment->delete_running_status(); + + return true; + } + + /** + * Optimize the retina version of an image. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param array $args { + * An array of required arguments. + * + * @type array $data The statistics data. + * @type object $attachment An Imagify attachment. + * @type string $retina_path The path to the retina file. + * @type string $size_key The attachment size key (without '@2x'). + * @type int $optimization_level The optimization level. Optionnal. + * @type array $metadata WP metadata. If omitted, wp_get_attachment_metadata() will be used. + * } + * @return array The new optimization data. + */ + public function optimize_retina_image( $args ) { + static $backup; + + $args = array_merge( array( + 'data' => array(), + 'attachment' => false, + 'retina_path' => '', + 'size_key' => '', + 'optimization_level' => false, + 'metadata' => array(), + ), $args ); + + if ( $this->is_prevented( __FUNCTION__ ) || ! $args['retina_path'] || $this->has_filesystem_error() ) { + return $args['data']; + } + + $retina_key = $args['size_key'] . '@2x'; + + if ( isset( $args['data'][ $retina_key ] ) ) { + // Don't optimize something that already is. + return $args['data']; + } + + $disallowed = $this->size_is_disallowed( $args['size_key'] ); + $do_retina = ! $disallowed; + /** + * Allow to optimize the retina version generated by WP Retina x2. + * + * @since 1.0 + * @since 1.8 Added $args parameter. + * + * @param bool $do_retina True will allow the optimization. False to prevent it. + * @param string $args The arguments passed to the method. + */ + $do_retina = apply_filters( 'do_imagify_optimize_retina', $do_retina, $args ); + + if ( ! $do_retina ) { + if ( $disallowed ) { + $message = __( 'This size is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ); + } else { + $message = __( 'This size optimization has been prevented by a filter.', 'imagify' ); + } + + $args['data']['sizes'][ $retina_key ] = array( + 'success' => false, + 'error' => $message, + ); + return $args['data']; + } + + if ( ! $args['metadata'] || ! is_array( $args['metadata'] ) ) { + $args['metadata'] = wp_get_attachment_metadata( $args['attachment']->get_id() ); + } + + $is_a_copy = $this->size_is_a_full_copy( array( + 'size_name' => $args['size_key'], + 'metadata' => $args['metadata'], + 'imagify_data' => $args['data'], + 'retina_path' => $args['retina_path'], + ) ); + + if ( $is_a_copy ) { + // This thumbnail is a copy of the full size image, which is already optimized. + $args['data']['sizes'][ $retina_key ] = $args['data']['sizes']['full']; + + if ( isset( $args['data']['sizes']['full']['original_size'], $args['data']['sizes']['full']['optimized_size'] ) ) { + // Concistancy only. + $args['data']['stats']['original_size'] += $args['data']['sizes']['full']['original_size']; + $args['data']['stats']['optimized_size'] += $args['data']['sizes']['full']['optimized_size']; + } + + return $args['data']; + } + + if ( ! is_int( $args['optimization_level'] ) ) { + $args['optimization_level'] = get_imagify_option( 'optimization_level' ); + } + + // Hammer time. + $response = do_imagify( $args['retina_path'], array( + // Backup only if it's the full size. + 'backup' => 'full' === $args['size_key'], + 'optimization_level' => $args['optimization_level'], + 'context' => 'wp-retina', + ) ); + + return $args['attachment']->fill_data( $args['data'], $response, $retina_key ); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** HANDLE BACKUPS ========================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Backup a retina file. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $file_path Path to the file. + * @return bool|object True on success, false if prevented or no need for backup, a WP_Error object on failure. + */ + public function backup_file( $file_path ) { + static $backup; + + if ( $this->is_prevented( __FUNCTION__ ) ) { + return false; + } + + if ( ! isset( $backup ) ) { + $backup = get_imagify_option( 'backup' ); + } + + if ( ! $backup ) { + return false; + } + + if ( $this->has_filesystem_error() ) { + return new WP_Error( 'filesystem', __( 'Filesystem error.', 'imagify' ) ); + } + + $upload_basedir = get_imagify_upload_basedir(); + + if ( ! $upload_basedir ) { + $file_path = make_path_relative( $file_path ); + + /* translators: %s is a file path. */ + return new WP_Error( 'upload_basedir', sprintf( __( 'The file %s could not be backed up. Image optimization aborted.', 'imagify' ), '' . esc_html( $file_path ) . '' ) ); + } + + $file_path = wp_normalize_path( $file_path ); + $backup_dir = get_imagify_backup_dir_path(); + $backup_path = str_replace( $upload_basedir, $backup_dir, $file_path ); + + if ( $this->filesystem->exists( $backup_path ) ) { + $this->filesystem->delete( $backup_path ); + } + + $backup_result = imagify_backup_file( $file_path, $backup_path ); + + if ( is_wp_error( $backup_result ) ) { + return $backup_result; + } + + return true; + } + + /** + * Delete a retina file backup. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $file_path Path to the file. + * @return bool|object True on success, false if the file doesn't exist, a WP_Error object on failure. + */ + public function delete_file_backup( $file_path ) { + $tests = $this->validate( __FUNCTION__ ); + + if ( true !== $tests ) { + return $tests; + } + + $upload_basedir = get_imagify_upload_basedir(); + + if ( ! $upload_basedir ) { + $file_path = make_path_relative( $file_path ); + + /* translators: %s is a file path. */ + return new WP_Error( 'upload_basedir', sprintf( __( 'Previous backup file for %s could not be deleted.', 'imagify' ), '' . esc_html( $file_path ) . '' ) ); + } + + $file_path = wp_normalize_path( $file_path ); + $backup_dir = get_imagify_backup_dir_path(); + $backup_path = str_replace( $upload_basedir, $backup_dir, $file_path ); + + if ( ! $this->filesystem->exists( $backup_path ) ) { + return false; + } + + $result = $this->filesystem->delete( $backup_path ); + + if ( ! $result ) { + $file_path = make_path_relative( $file_path ); + + /* translators: %s is a file path. */ + return new WP_Error( 'not_deleted', sprintf( __( 'Previous backup file for %s could not be deleted.', 'imagify' ), '' . esc_html( $file_path ) . '' ) ); + } + + return true; + } + + /** + * Restore the retina version of the full size. + * This doesn't remove the Imagify data. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return bool|object True on success, false if prevented or backup doesn't exist, a WP_Error object on failure. + */ + public function restore_full_retina_file( $attachment ) { + $tests = $this->validate( __FUNCTION__, $attachment, array( + 'metadata_file' => false, + 'metadata_sizes' => false, + ) ); + + if ( true !== $tests ) { + return $tests; + } + + $has_backup = $this->full_retina_has_backup( $attachment ); + + if ( is_wp_error( $has_backup ) ) { + return $has_backup; + } + + if ( ! $has_backup ) { + return new WP_Error( 'no_backup', __( 'The retina version of the full size of this image does not have backup.', 'imagify' ) ); + } + + $file_path = $this->get_retina_path( $attachment->get_original_path() ); + $backup_path = $this->get_full_retina_backup_path( $attachment ); + + /** + * Fires before restoring the retina version of the full size. + * + * @since 1.8 + * @author Grégory Viguier + * + * @param string $backup_path Path to the backup file. + * @param string $file_path Path to the source file. + */ + do_action( 'before_imagify_restore_full_retina_file', $backup_path, $file_path ); + + // Save disc space by moving it instead of copying it. + $moved = $this->filesystem->move( $backup_path, $file_path, true ); + + /** + * Fires after restoring the retina version of the full size. + * + * @since 1.8 + * @author Grégory Viguier + * + * @param string $backup_path Path to the backup file. + * @param string $file_path Path to the source file. + * @param bool $moved Restore success. + */ + do_action( 'after_imagify_restore_full_retina_file', $backup_path, $file_path, $moved ); + + if ( ! $moved ) { + return new WP_Error( 'upload_basedir', __( 'Backup of the retina version of the full size image could not be restored.', 'imagify' ) ); + } + + return true; + } + + /** + * Get the path to the retina version of the full size. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return string|object The path on success, a WP_Error object on failure. + */ + public function get_full_retina_backup_path( $attachment ) { + $file_path = $this->get_retina_path( $attachment->get_original_path() ); + $backup_path = get_imagify_attachment_backup_path( $file_path ); + + if ( ! $backup_path ) { + return new WP_Error( 'upload_basedir', __( 'Could not retrieve the path to the backup of the retina version of the full size image.', 'imagify' ) ); + } + + return $backup_path; + } + + /** + * Tell if the retina version of the full size has a backup. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return bool|object A WP_Error object on failure. + */ + public function full_retina_has_backup( $attachment ) { + $backup_path = $this->get_full_retina_backup_path( $attachment ); + + if ( is_wp_error( $backup_path ) ) { + return $backup_path; + } + + return $this->filesystem->exists( $backup_path ); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** HANDLE IMAGIFY DATA ===================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Remove retina versions from Imagify data. + * It also rebuilds the attachment stats. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param bool $remove_full_size True to also remove the full size data. + */ + public function remove_retina_images_data( $attachment, $remove_full_size = false ) { + if ( $this->is_prevented( __FUNCTION__ ) ) { + return; + } + + $imagify_data = $attachment->get_data(); + + if ( empty( $imagify_data['sizes'] ) || ! is_array( $imagify_data['sizes'] ) ) { + return; + } + + $sizes = $this->get_attachment_sizes( $attachment ); + + if ( ! $sizes ) { + return; + } + + $update = false; + + if ( $remove_full_size && isset( $imagify_data['sizes']['full@2x'] ) ) { + unset( $imagify_data['sizes']['full@2x'] ); + $update = true; + } + + foreach ( $sizes as $size => $attr ) { + $size .= '@2x'; + + if ( isset( $imagify_data['sizes'][ $size ] ) ) { + unset( $imagify_data['sizes'][ $size ] ); + $update = true; + } + } + + if ( ! $update ) { + return; + } + + $this->update_imagify_data( $attachment, $imagify_data ); + } + + /** + * Remove a retina thumbnail from attachment's Imagify data, given the retina file name. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param string $retina_filename Retina thumbnail file name. + */ + public function remove_retina_image_data_by_filename( $attachment, $retina_filename ) { + if ( $this->is_prevented( __FUNCTION__ ) ) { + return; + } + + $imagify_data = $attachment->get_data(); + + if ( empty( $imagify_data['sizes'] ) || ! is_array( $imagify_data['sizes'] ) ) { + return; + } + + $sizes = $this->get_attachment_sizes( $attachment ); + + if ( ! $sizes ) { + return; + } + + $image_filename = str_replace( $this->get_suffix(), '.', $retina_filename ); + $size = false; + + foreach ( $sizes as $name => $attr ) { + if ( $image_filename === $attr['file'] ) { + $size = $name; + break; + } + } + + if ( ! $size || ! isset( $imagify_data['sizes'][ $size ] ) ) { + return; + } + + unset( $imagify_data['sizes'][ $size ] ); + + $this->update_imagify_data( $attachment, $imagify_data ); + } + + /** + * Rebuild the attachment stats and store the data. + * Delete all Imagify data if the sizes are empty. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param array $imagify_data Imagify data. + * @return bool True on update, false on delete or prevented. + */ + public function update_imagify_data( $attachment, $imagify_data ) { + if ( $this->is_prevented( __FUNCTION__ ) ) { + return false; + } + + if ( empty( $imagify_data['sizes'] ) || ! is_array( $imagify_data['sizes'] ) ) { + // No new sizes. + $attachment->delete_imagify_data(); + return false; + } + + $imagify_data['stats'] = array( + 'original_size' => 0, + 'optimized_size' => 0, + 'percent' => 0, + ); + + foreach ( $imagify_data['sizes'] as $size_data ) { + $imagify_data['stats']['original_size'] += ! empty( $size_data['original_size'] ) ? $size_data['original_size'] : 0; + $imagify_data['stats']['optimized_size'] += ! empty( $size_data['optimized_size'] ) ? $size_data['optimized_size'] : 0; + } + + if ( $imagify_data['stats']['original_size'] && $imagify_data['stats']['optimized_size'] ) { + $imagify_data['stats']['percent'] = round( ( ( $imagify_data['stats']['original_size'] - $imagify_data['stats']['optimized_size'] ) / $imagify_data['stats']['original_size'] ) * 100, 2 ); + } + + update_post_meta( $attachment->get_id(), '_imagify_data', $imagify_data ); + + return true; + } + + /** + * Remove a size from Imagify data. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param string $size_name Name of the size. + */ + public function remove_size_from_imagify_data( $attachment, $size_name ) { + if ( $this->is_prevented( __FUNCTION__ ) ) { + return; + } + + $imagify_data = $attachment->get_data(); + + if ( ! isset( $imagify_data['sizes'][ $size_name ] ) ) { + return; + } + + unset( $imagify_data['sizes'][ $size_name ] ); + + $this->update_imagify_data( $attachment, $imagify_data ); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** INTERNAL TOOLS ========================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Tell if a file extension is supported by WP Retina 2x. + * It uses $wr2x_core->is_supported_image() if available. + * + * @since 1.8 + * @access public + * @see $wr2x_core->is_supported_image() + * @author Grégory Viguier + * + * @param string|int $file_path Path to the file or attachment ID. + * @return bool + */ + public function is_supported_format( $file_path ) { + global $wr2x_core; + static $method; + static $results = array(); + + if ( ! $file_path ) { + return false; + } + + if ( isset( $results[ $file_path ] ) ) { + // $file_path can be a path or an attachment ID. + return $results[ $file_path ]; + } + + if ( is_int( $file_path ) ) { + $attachment_id = $file_path; + $file_path = get_attached_file( $attachment_id ); + + if ( ! $file_path ) { + $results[ $attachment_id ] = false; + return false; + } + + if ( isset( $results[ $file_path ] ) ) { + // $file_path is now a path for sure. + $results[ $attachment_id ] = $results[ $file_path ]; + return $results[ $file_path ]; + } + } + + if ( ! isset( $method ) ) { + if ( $wr2x_core && is_object( $wr2x_core ) && method_exists( $wr2x_core, 'is_supported_image' ) ) { + $method = array( $wr2x_core, 'is_supported_image' ); + } else { + $method = array( $this, 'is_supported_extension' ); + } + } + + // $file_path is now a path for sure. + $results[ $file_path ] = call_user_func( $method, $file_path ); + + if ( ! empty( $attachment_id ) ) { + $results[ $attachment_id ] = $results[ $file_path ]; + } + + return $results[ $file_path ]; + } + + /** + * Tell if a file extension is supported by WP Retina 2x. + * Internal version of $wr2x_core->is_supported_image(). + * + * @since 1.8 + * @access public + * @see $this->is_supported_format() + * @author Grégory Viguier + * + * @param string $file_path Path to a file. + * @return bool + */ + protected function is_supported_extension( $file_path ) { + $extension = strtolower( $this->filesystem->path_info( $file_path, 'extension' ) ); + $extensions = array( + 'jpg' => 1, + 'jpeg' => 1, + 'png' => 1, + 'gif' => 1, + ); + + return isset( $extensions[ $extension ] ); + } + + /** + * Get the path to the retina version of an image. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $file_path Path to the non-retina image. + * @return string + */ + public function get_retina_path( $file_path ) { + $path_info = $this->filesystem->path_info( $file_path ); + $suffix = rtrim( $this->get_suffix(), '.' ); + $extension = isset( $path_info['extension'] ) ? '.' . $path_info['extension'] : ''; + + return $path_info['dir_path'] . $path_info['file_base'] . $suffix . $extension; + } + + /** + * Tell if the attchment has at least one retina image. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return bool + */ + public function has_retina_images( $attachment ) { + $dir_path = $this->filesystem->path_info( $attachment->get_original_path(), 'dir_path' ); + $metadata = wp_get_attachment_metadata( $attachment->get_id() ); + $sizes = ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ? $metadata['sizes'] : array(); + $sizes['full'] = array( + 'file' => $this->filesystem->file_name( $metadata['file'] ), + ); + + foreach ( $sizes as $name => $attr ) { + $size_path = $this->get_retina_path( $dir_path . $attr['file'] ); + + if ( $this->filesystem->exists( $size_path ) ) { + return true; + } + } + + return false; + } + + /** + * Prevent a method to do its job. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $method_name Name of the method to prevent. + */ + public function prevent( $method_name ) { + if ( empty( self::$prevented[ $method_name ] ) || self::$prevented[ $method_name ] < 1 ) { + self::$prevented[ $method_name ] = 1; + } else { + ++self::$prevented[ $method_name ]; + } + } + + /** + * Allow a method to do its job. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $method_name Name of the method to allow. + */ + public function allow( $method_name ) { + if ( empty( self::$prevented[ $method_name ] ) || self::$prevented[ $method_name ] <= 1 ) { + unset( self::$prevented[ $method_name ] ); + } else { + --self::$prevented[ $method_name ]; + } + } + + /** + * Tell if a method is prevented to do its job. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $method_name Name of the method. + * @return bool + */ + public function is_prevented( $method_name ) { + return ! empty( self::$prevented[ $method_name ] ); + } + + /** + * Tell if a thumbnail size is disallowed for optimization.. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $size_name The size name. + * @return bool + */ + public function size_is_disallowed( $size_name ) { + static $disallowed_sizes; + + if ( imagify_is_active_for_network() ) { + return false; + } + + if ( ! isset( $disallowed_sizes ) ) { + $disallowed_sizes = get_imagify_option( 'disallowed-sizes' ); + } + + return isset( $disallowed_sizes[ $size_name ] ); + } + + /** + * Tell if a thumbnail file is a copy of the full size image. Will return false if the full size is not optimized. + * Make sure both files exist before using this. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param array $args { + * An array of arguments. + * + * @type string $size_name The size name. Required. + * @type array $metadata WP metadata. Required. + * @type array $imagify_data Imagify data. Required. + * @type string $retina_path Path to the image we're testing. Required. + * @type string $full_path Path to the full size image. Optional but should be provided. + * } + * @return bool + */ + public function size_is_a_full_copy( $args ) { + $size_name = $args['size_name']; + $metadata = $args['metadata']; + $imagify_data = $args['imagify_data']; + + if ( empty( $imagify_data['sizes']['full'] ) ) { + // The full size is not optimized, so there is no point in checking if the given file is a copy. + return false; + } + + if ( ! isset( $metadata['width'], $metadata['height'], $metadata['file'] ) ) { + return false; + } + + if ( ! isset( $metadata['sizes'][ $size_name ]['width'], $metadata['sizes'][ $size_name ]['height'] ) ) { + return false; + } + + $size = $metadata['sizes'][ $size_name ]; + + if ( $size['width'] * 2 !== $metadata['width'] || $size['height'] * 2 !== $metadata['height'] ) { + // The full size image doesn't have the right dimensions. + return false; + } + + if ( empty( $args['full_path'] ) ) { + $dir_path = $this->filesystem->path_info( $args['retina_path'], 'dir_path' ); + $args['full_path'] = $dir_path . $metadata['file']; + } + + $full_hash = md5_file( $args['full_path'] ); + $retina_hash = md5_file( $args['retina_path'] ); + + return hash_equals( $full_hash, $retina_hash ); + } + + /** + * Tell if there is a filesystem error. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @return bool + */ + public function has_filesystem_error() { + return ! empty( $this->filesystem->errors->errors ); + } + + /** + * Do few tests: method is not prevented, attachment is valid, filesystem has no error. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $method The name of the method using this. + * @param object $attachment An Imagify attachment. + * @param array $args A list of additional tests. + * @return bool|object True if ok, false if prevented, a WP_Error object on failure. + */ + public function validate( $method, $attachment = false, $args = array() ) { + $args = array_merge( array( + 'supported' => false, + 'can_optimize' => false, + 'metadata_dimensions' => false, + 'metadata_file' => $attachment ? 'error' : false, + 'metadata_sizes' => $attachment ? 'error' : false, + ), $args ); + + if ( $this->is_prevented( $method ) ) { + return false; + } + + if ( $attachment && ! $attachment->is_valid() ) { + return new WP_Error( 'invalid_attachment', __( 'Invalid attachment.', 'imagify' ) ); + } + + if ( $args['supported'] && ! $attachment->is_extension_supported() ) { + if ( 'error' !== $args['supported'] ) { + return false; + } + + return new WP_Error( 'mime_type_not_supported', __( 'This type of file is not supported.', 'imagify' ) ); + } + + if ( $args['can_optimize'] ) { + if ( 'error' !== $args['can_optimize'] ) { + if ( ! Imagify_Requirements::is_api_key_valid() || Imagify_Requirements::is_over_quota() ) { + return false; + } + } else { + if ( ! Imagify_Requirements::is_api_key_valid() ) { + return new WP_Error( 'invalid_api_key', __( 'Your API key is not valid!', 'imagify' ) ); + } + if ( Imagify_Requirements::is_over_quota() ) { + return new WP_Error( 'over_quota', __( 'You have used all your credits!', 'imagify' ) ); + } + } + } + + if ( $this->has_filesystem_error() ) { + return new WP_Error( 'filesystem', __( 'Filesystem error.', 'imagify' ) ); + } + + if ( $args['metadata_dimensions'] ) { + $metadata = wp_get_attachment_metadata( $attachment->get_id() ); + + if ( empty( $metadata['width'] ) || empty( $metadata['height'] ) ) { + if ( 'error' !== $args['metadata_sizes'] ) { + return false; + } + + return new WP_Error( 'metadata_dimensions', __( 'This attachment lacks the required metadata.', 'imagify' ) ); + } + } + + if ( $args['metadata_file'] ) { + $metadata = isset( $metadata ) ? $metadata : wp_get_attachment_metadata( $attachment->get_id() ); + + if ( empty( $metadata['file'] ) ) { + if ( 'error' !== $args['metadata_file'] ) { + return false; + } + + return new WP_Error( 'metadata_file', __( 'This attachment lacks the required metadata.', 'imagify' ) ); + } + } + + if ( $args['metadata_sizes'] ) { + $metadata = isset( $metadata ) ? $metadata : wp_get_attachment_metadata( $attachment->get_id() ); + + if ( empty( $metadata['sizes'] ) || ! is_array( $metadata['sizes'] ) ) { + if ( 'error' !== $args['metadata_sizes'] ) { + return false; + } + + return new WP_Error( 'metadata_sizes', __( 'This attachment has no registered thumbnail sizes.', 'imagify' ) ); + } + } + + return true; + } + + /** + * Tell if Imagify can optimize the files. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @return bool + */ + public function can_optimize() { + return ! $this->has_filesystem_error() && Imagify_Requirements::is_api_key_valid() && ! Imagify_Requirements::is_over_quota(); + } + + /** + * Tell if Imagify can auto-optimize the files. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @return bool + */ + public function can_auto_optimize() { + return $this->can_optimize() && get_imagify_option( 'auto_optimize' ); + } + + /** + * Get thumbnail sizes from an attachment. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return array + */ + public function get_attachment_sizes( $attachment ) { + $metadata = wp_get_attachment_metadata( $attachment->get_id() ); + return ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ? $metadata['sizes'] : array(); + } + + /** + * Get the optimization level used to optimize the given attachment. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return int The attachment optimization level. The default level if not optimized. + */ + public function get_optimization_level( $attachment ) { + static $default; + + if ( $attachment->get_status() ) { + $level = $attachment->get_optimization_level(); + + if ( is_int( $level ) ) { + return $level; + } + } + + if ( ! isset( $default ) ) { + $default = get_imagify_option( 'optimization_level' ); + } + + return $default; + } + + /** + * Get the path to the temporary file. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $file_path The optimized full-sized file path. + * @return string + */ + public function get_temporary_file_path( $file_path ) { + return $file_path . '_backup'; + } + + /** + * Backup the optimized full-sized file and replace it by the original backup file. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + */ + public function backup_optimized_file( $attachment ) { + if ( $this->is_prevented( __FUNCTION__ ) ) { + return; + } + + $backup_path = $attachment->get_backup_path(); + + if ( ! $backup_path || ! $attachment->is_optimized() ) { + return; + } + + /** + * Replace the optimized full-sized file by the backup, so any optimization will not use an optimized file, but the original one. + * The optimized full-sized file is kept and renamed, and will be put back in place at the end of the optimization process. + */ + $file_path = $attachment->get_original_path(); + $tmp_file_path = $this->get_temporary_file_path( $file_path ); + + if ( $this->filesystem->exists( $file_path ) ) { + $this->filesystem->move( $file_path, $tmp_file_path, true ); + } + + $copied = $this->filesystem->copy( $backup_path, $file_path ); + + if ( ! $copied ) { + // Uh ho... + $this->filesystem->move( $tmp_file_path, $file_path, true ); + return; + } + + // Make sure the dimensions are in sync in post meta. + $this->maybe_update_image_dimensions( $attachment, $file_path ); + } + + /** + * Put the optimized full-sized file back. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + */ + public function put_optimized_file_back( $attachment ) { + if ( $this->is_prevented( __FUNCTION__ ) ) { + return; + } + + $file_path = $attachment->get_original_path(); + $tmp_file_path = $this->get_temporary_file_path( $file_path ); + + if ( ! $this->filesystem->exists( $tmp_file_path ) ) { + return; + } + + $moved = $this->filesystem->move( $tmp_file_path, $file_path, true ); + + if ( ! $moved ) { + // Uh ho... + return; + } + + // Make sure the dimensions are in sync in post meta. + $this->maybe_update_image_dimensions( $attachment, $file_path ); + } + + /** + * Make sure the dimensions are in sync in post meta. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param string $file_path Path to the file. + * @return bool True when updated. + */ + public function maybe_update_image_dimensions( $attachment, $file_path ) { + if ( $this->is_prevented( __FUNCTION__ ) ) { + return false; + } + + $metadata = wp_get_attachment_metadata( $attachment->get_id() ); + $width = ! empty( $metadata['width'] ) ? (int) $metadata['width'] : 0; + $height = ! empty( $metadata['height'] ) ? (int) $metadata['height'] : 0; + $dimensions = $this->filesystem->get_image_size( $file_path ); + + if ( ! $dimensions ) { + return false; + } + + if ( $width === $dimensions['width'] && $height === $dimensions['height'] ) { + return false; + } + + $metadata['width'] = $dimensions['width']; + $metadata['height'] = $dimensions['height']; + + // Prevent auto-optimization. + Imagify_Auto_Optimization::prevent_optimization( $attachment_id ); + + wp_update_attachment_metadata( $attachment->get_id(), $metadata ); + + // Allow auto-optimization back. + Imagify_Auto_Optimization::allow_optimization( $attachment_id ); + return true; + } + + + /** ----------------------------------------------------------------------------------------- */ + /** WR2X COMPAT' TOOLS ====================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Get the suffix added to the file name, with a trailing dot. + * Don't use it for the size name. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @return string + */ + public function get_suffix() { + global $wr2x_core; + static $suffix; + + if ( ! isset( $suffix ) ) { + $suffix = $wr2x_core && is_object( $wr2x_core ) && method_exists( $wr2x_core, 'retina_extension' ) ? $wr2x_core->retina_extension() : '@2x.'; + } + + return $suffix; + } + + /** + * Get info about retina version. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @param string $type The type of info. Possible values are 'basic' and 'full' (for the full size). + * @return array An array containing some HTML, indexed by the attachment ID. + */ + public function get_retina_info( $attachment, $type = 'basic' ) { + global $wr2x_core; + static $can_get_info; + + if ( ! isset( $can_get_info ) ) { + $can_get_info = $wr2x_core && is_object( $wr2x_core ) && method_exists( $wr2x_core, 'retina_info' ) && method_exists( $wr2x_core, 'html_get_basic_retina_info_full' ) && method_exists( $wr2x_core, 'html_get_basic_retina_info' ); + } + + if ( ! $can_get_info ) { + return ''; + } + + $attachment_id = $attachment->get_id(); + $info = $wr2x_core->retina_info( $attachment_id ); + + if ( 'full' === $type ) { + return array( + $attachment_id => $wr2x_core->html_get_basic_retina_info_full( $attachment_id, $info ), + ); + } + + return array( + $attachment_id => $wr2x_core->html_get_basic_retina_info( $attachment_id, $info ), + ); + } + + /** + * Log. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $text Text to log. + */ + public function log( $text ) { + global $wr2x_core; + static $can_log; + + if ( ! isset( $can_log ) ) { + $can_log = $wr2x_core && is_object( $wr2x_core ) && method_exists( $wr2x_core, 'log' ); + } + + if ( $can_log ) { + $wr2x_core->log( $text ); + } + } +} diff --git a/inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x.php b/inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x.php new file mode 100644 index 000000000..9c332449a --- /dev/null +++ b/inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x.php @@ -0,0 +1,681 @@ +core ) ) { + $this->core = new Imagify_WP_Retina_2x_Core(); + } + + return $this->core; + } + + + /** ----------------------------------------------------------------------------------------- */ + /** INIT ==================================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Launch the hooks. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function init() { + // Deal with Imagify when WPR2X is working. + add_action( 'wp_ajax_wr2x_generate', array( $this, 'wr2x_generate_ajax_cb' ), 5 ); + add_action( 'wp_ajax_wr2x_delete', array( $this, 'wr2x_delete_all_retina_ajax_cb' ), 5 ); + add_action( 'wp_ajax_wr2x_delete_full', array( $this, 'wr2x_delete_full_retina_ajax_cb' ), 5 ); + add_action( 'wp_ajax_wr2x_replace', array( $this, 'wr2x_replace_all_ajax_cb' ), 5 ); + add_action( 'wp_ajax_wr2x_upload', array( $this, 'wr2x_replace_full_retina_ajax_cb' ), 5 ); + add_action( 'imagify_assets_enqueued', array( $this, 'enqueue_scripts' ) ); + add_action( 'wr2x_retina_file_removed', array( $this, 'remove_retina_thumbnail_data_hook' ), 10, 2 ); + // Deal with Imagify when WP is working. + add_action( 'delete_attachment', array( $this, 'delete_full_retina_backup_file_hook' ) ); + // Deal with retina thumbnails when Imagify processes the "normal" images. + add_filter( 'imagify_fill_full_size_data', array( $this, 'optimize_full_retina_version_hook' ), 10, 8 ); + add_filter( 'imagify_fill_thumbnail_data', array( $this, 'optimize_retina_version_hook' ), 10, 8 ); + add_filter( 'imagify_fill_unauthorized_thumbnail_data', array( $this, 'maybe_optimize_unauthorized_retina_version_hook' ), 10, 7 ); + add_action( 'after_imagify_restore_attachment', array( $this, 'restore_retina_images_hook' ) ); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** AJAX CALLBACKS ========================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * (Re)generate the retina thumbnails (except the full size). + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function wr2x_generate_ajax_cb() { + $this->check_nonce( 'imagify_wr2x_generate' ); + $this->check_user_capacity(); + + $attachment = $this->get_requested_attachment( 'wr2x_generate' ); + + // Delete previous retina images and recreate them. + $result = $this->get_core()->regenerate_retina_images( $attachment ); + + // Send results. + $this->maybe_send_json_error( $result ); + + $this->send_json( array( + 'results' => $this->get_core()->get_retina_info( $attachment ), + 'message' => __( 'Retina files generated.', 'imagify' ), + 'imagify_info' => $this->get_imagify_info( $attachment ), + ) ); + } + + /** + * Delete all retina images, including the one for the full size. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function wr2x_delete_all_retina_ajax_cb() { + $this->check_nonce( 'imagify_wr2x_delete' ); + $this->check_user_capacity(); + + $attachment = $this->get_requested_attachment( 'wr2x_delete_all' ); + + // Delete the retina versions, including the full size. + $result = $this->get_core()->delete_retina_images( $attachment, true ); + + // Send results. + $this->maybe_send_json_error( $result ); + + $this->send_json( array( + 'results' => $this->get_core()->get_retina_info( $attachment ), + 'results_full' => $this->get_core()->get_retina_info( $attachment, 'full' ), + 'message' => __( 'Retina files deleted.', 'imagify' ), + ) ); + } + + /** + * Delete the retina version of the full size. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function wr2x_delete_full_retina_ajax_cb() { + $this->check_nonce( 'imagify_wr2x_delete_full' ); + $this->check_user_capacity(); + + $attachment = $this->get_requested_attachment( 'wr2x_delete_full' ); + + $result = $this->get_core()->delete_full_retina_image( $attachment ); + + // Send results. + $this->maybe_send_json_error( $result ); + + $this->send_json( array( + 'results' => $this->get_core()->get_retina_info( $attachment, 'full' ), + 'message' => __( 'Full retina file deleted.', 'imagify' ), + ) ); + } + + /** + * Replace an attachment (except the retina version of the full size). + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function wr2x_replace_all_ajax_cb() { + $this->check_nonce( 'imagify_wr2x_replace' ); + $this->check_user_capacity(); + + $attachment = $this->get_requested_attachment( 'wr2x_replace_all' ); + $tmp_file_path = $this->get_uploaded_file_path(); + + $result = $this->get_core()->replace_attachment( $attachment, $tmp_file_path ); + + // Send results. + $this->maybe_send_json_error( $result ); + + $this->send_json( array( + 'results' => $this->get_core()->get_retina_info( $attachment ), + 'message' => __( 'Images replaced successfully.', 'imagify' ), + ) ); + } + + /** + * Upload a new retina version for the full size. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function wr2x_replace_full_retina_ajax_cb() { + $this->check_nonce( 'imagify_wr2x_upload' ); + $this->check_user_capacity(); + + $attachment = $this->get_requested_attachment( 'wr2x_replace_full' ); + $tmp_file_path = $this->get_uploaded_file_path(); + + $result = $this->get_core()->replace_full_retina_image( $attachment, $tmp_file_path ); + + // Send results. + $this->maybe_send_json_error( $result ); + + $this->send_json( array( + 'results' => $this->get_core()->get_retina_info( $attachment ), + 'message' => __( 'Image replaced successfully.', 'imagify' ), + ) ); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** OTHER HOOKS ============================================================================= */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Queue some JS to add our nonce parameter to all WR2X jQuery ajax requests. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function enqueue_scripts() { + if ( ! $this->user_can() ) { + return; + } + + $assets = Imagify_Assets::get_instance(); + + $assets->register_script( 'weakmap-polyfill', 'weakmap-polyfill', array(), '2.0.0' ); + $assets->register_script( 'formdata-polyfill', 'formdata-polyfill', array( 'weakmap-polyfill' ), '3.0.10-beta' ); + $assets->register_script( 'wp-retina-2x', 'imagify-wp-retina-2x', array( 'formdata-polyfill', 'jquery' ) ); + + if ( imagify_is_screen( 'library' ) || imagify_is_screen( 'media_page_wp-retina-2x' ) ) { + $assets->localize_script( 'wp-retina-2x', 'imagifyRetina2x', array( + 'wr2x_generate' => wp_create_nonce( 'imagify_wr2x_generate' ), + 'wr2x_delete' => wp_create_nonce( 'imagify_wr2x_delete' ), + 'wr2x_delete_full' => wp_create_nonce( 'imagify_wr2x_delete_full' ), + 'wr2x_replace' => wp_create_nonce( 'imagify_wr2x_replace' ), + 'wr2x_upload' => wp_create_nonce( 'imagify_wr2x_upload' ), + ) ); + $assets->enqueue( 'wp-retina-2x' ); + } + } + + /** + * After a retina thumbnail is deleted, remove its Imagify data. + * This should be useless since we replaced every AJAX callbacks. + * + * @since 1.8 + * @access public + * @see wr2x_delete_attachment() + * @author Grégory Viguier + * + * @param int $attachment_id An attachment ID. + * @param string $retina_filename The retina thumbnail file name. + */ + public function remove_retina_thumbnail_data_hook( $attachment_id, $retina_filename ) { + $attachment = get_imagify_attachment( 'wp', $attachment_id, 'wr2x_delete' ); + + $this->get_core()->remove_retina_image_data_by_filename( $attachment, $retina_filename ); + } + + /** + * Delete the backup of the retina version of the full size file when an attachement is deleted. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param int $attachment_id An attachment ID. + */ + public function delete_full_retina_backup_file_hook( $attachment_id ) { + if ( ! $this->get_core()->is_supported_format( $attachment_id ) ) { + return; + } + + $attachment = get_imagify_attachment( 'wp', $attachment_id, 'delete_attachment' ); + $retina_path = $this->get_core()->get_retina_path( $attachment->get_original_path() ); + + if ( $retina_path ) { + $this->get_core()->delete_file_backup( $retina_path ); + } + } + + /** + * Filter the optimization data of the full size. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param array $data The statistics data. + * @param object $response The API response. + * @param int $attachment_id The attachment ID. + * @param string $path The attachment path. + * @param string $url The attachment URL. + * @param string $size_key The attachment size key. The value is obviously 'full' but it's kept for oncistancy with other filters. + * @param int $optimization_level The optimization level. + * @param array $metadata WP metadata. + * @return array $data The new optimization data. + */ + public function optimize_full_retina_version_hook( $data, $response, $attachment_id, $path, $url, $size_key, $optimization_level, $metadata ) { + if ( ! $this->get_core()->is_supported_format( $attachment_id ) ) { + return $data; + } + + $attachment = get_imagify_attachment( 'wp', $attachment_id, 'optimize_full_retina_version_hook' ); + + return $this->get_core()->optimize_retina_image( array( + 'data' => $data, + 'attachment' => get_imagify_attachment( 'wp', $attachment_id, 'optimize_full_retina_version_hook' ), + 'retina_path' => wr2x_get_retina( $path ), + 'size_key' => $size_key, + 'optimization_level' => $optimization_level, + 'metadata' => $metadata, + ) ); + } + + /** + * Filter the optimization data of each thumbnail. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param array $data The statistics data. + * @param object $response The API response. + * @param int $attachment_id The attachment ID. + * @param string $path The thumbnail path. + * @param string $url The thumbnail URL. + * @param string $size_key The thumbnail size key. + * @param int $optimization_level The optimization level. + * @param array $metadata WP metadata. + * @return array $data The new optimization data. + */ + public function optimize_retina_version_hook( $data, $response, $attachment_id, $path, $url, $size_key, $optimization_level, $metadata ) { + if ( ! $this->get_core()->is_supported_format( $attachment_id ) ) { + return $data; + } + + return $this->get_core()->optimize_retina_image( array( + 'data' => $data, + 'attachment' => get_imagify_attachment( 'wp', $attachment_id, 'optimize_retina_version_hook' ), + 'retina_path' => wr2x_get_retina( $path ), + 'size_key' => $size_key, + 'optimization_level' => $optimization_level, + 'metadata' => $metadata, + ) ); + } + + /** + * If a thumbnail size is disallowed in Imagify' settings, we can still try to optimize its "@2x" version. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param array $data The statistics data. + * @param int $attachment_id The attachment ID. + * @param string $path The thumbnail path. + * @param string $url The thumbnail URL. + * @param string $size_key The thumbnail size key. + * @param int $optimization_level The optimization level. + * @param array $metadata WP metadata. + * @return array $data The new optimization data. + */ + public function maybe_optimize_unauthorized_retina_version_hook( $data, $attachment_id, $path, $url, $size_key, $optimization_level, $metadata ) { + if ( ! $this->get_core()->is_supported_format( $attachment_id ) ) { + return $data; + } + + return $this->get_core()->optimize_retina_image( array( + 'data' => $data, + 'attachment' => get_imagify_attachment( 'wp', $attachment_id, 'maybe_optimize_unauthorized_retina_version_hook' ), + 'retina_path' => wr2x_get_retina( $path ), + 'size_key' => $size_key, + 'optimization_level' => $optimization_level, + 'metadata' => $metadata, + ) ); + } + + /** + * Delete previous retina images and recreate them. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param int $attachment_id An attachment ID. + */ + public function restore_retina_images_hook( $attachment_id ) { + if ( ! $this->get_core()->is_supported_format( $attachment_id ) ) { + return; + } + + $attachment = get_imagify_attachment( 'wp', $attachment_id, 'restore_retina_images_hook' ); + + if ( ! $this->get_core()->has_retina_images( $attachment ) ) { + return; + } + + // At this point, previous Imagify data has been removed. + $this->get_core()->regenerate_retina_images( $attachment ); + $this->get_core()->restore_full_retina_file( $attachment ); + } + + + /** ----------------------------------------------------------------------------------------- */ + /** INTERNAL TOOLS ========================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Check for nonce. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $action Action nonce. + * @param string|bool $query_arg Optional. Key to check for the nonce in `$_REQUEST`. If false, `$_REQUEST` values will be evaluated for '_ajax_nonce', and '_wpnonce' (in that order). Default false. + */ + public function check_nonce( $action, $query_arg = 'imagify_nonce' ) { + if ( ! check_ajax_referer( $action, $query_arg, false ) ) { + $this->send_json( array( + 'success' => false, + 'message' => __( 'Sorry, you are not allowed to do that.', 'imagify' ), + ) ); + } + } + + /** + * Check for user capacity. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function check_user_capacity() { + if ( ! $this->user_can() ) { + $this->send_json( array( + 'success' => false, + 'message' => __( 'Sorry, you are not allowed to do that.', 'imagify' ), + ) ); + } + } + + /** + * Tell if the current user can re-optimize files. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + */ + public function user_can() { + return imagify_current_user_can( 'auto-optimize' ); + } + + /** + * Shorthand to get the attachment ID sent via $_POST. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param string $context The context to use in get_imagify_attachment(). + * @param string $key The $_POST key. + * @return int $attachment_id + */ + public function get_requested_attachment( $context, $key = 'attachmentId' ) { + $attachment_id = filter_input( INPUT_POST, $key, FILTER_VALIDATE_INT ); + + if ( $attachment_id <= 0 ) { + $this->send_json( array( + 'success' => false, + 'message' => __( 'The attachment ID is missing.', 'imagify' ), + ) ); + } + + if ( ! $this->get_core()->is_supported_format( $attachment_id ) ) { + $this->send_json( array( + 'success' => false, + 'message' => __( 'This format is not supported.', 'imagify' ), + ) ); + } + + $attachment = get_imagify_attachment( 'wp', $attachment_id, $context ); + + if ( ! $this->has_required_metadata( $attachment ) ) { + $this->send_json( array( + 'success' => false, + 'message' => __( 'This attachment lacks the required metadata.', 'imagify' ), + ) ); + } + + return $attachment; + } + + /** + * Shorthand to get the path to the uploaded file. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @return string Path to the temporary file. + */ + public function get_uploaded_file_path() { + $tmp_file_path = ! empty( $_FILES['file']['tmp_name'] ) ? wp_unslash( $_FILES['file']['tmp_name'] ) : ''; + $tmp_file_path = $tmp_file_path && is_uploaded_file( $tmp_file_path ) ? $tmp_file_path : ''; + $filesystem = Imagify_Filesystem::get_instance(); + + if ( ! $tmp_file_path || ! $filesystem->is_image( $tmp_file_path ) ) { + $this->get_core()->log( 'The file is not an image or the upload went wrong.' ); + $filesystem->delete( $tmp_file_path ); + + $this->send_json_string( array( + 'success' => false, + 'message' => __( 'The file is not an image or the upload went wrong.', 'imagify' ), + ) ); + } + + $file_name = filter_input( INPUT_POST, 'filename', FILTER_SANITIZE_STRING ); + $file_data = wp_check_filetype_and_ext( $tmp_file_path, $file_name ); + + if ( empty( $file_data['ext'] ) ) { + $this->get_core()->log( 'You cannot use this file (wrong extension? wrong type?).' ); + $filesystem->delete( $tmp_file_path ); + + $this->send_json_string( array( + 'success' => false, + 'message' => __( 'You cannot use this file (wrong extension? wrong type?).', 'imagify' ), + ) ); + } + + $this->get_core()->log( 'The temporary file was written successfully.' ); + + return $tmp_file_path; + } + + /** + * Tell if Imagify's column content has been requested. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @return bool + */ + public function needs_info() { + return filter_input( INPUT_POST, 'imagify_info', FILTER_VALIDATE_INT ) === 1; + } + + /** + * Tell if the attachment has the required WP metadata. + * + * @since 1.8 + * @access public + * @see $wr2x_core->is_image_meta() + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return bool + */ + public function has_required_metadata( $attachment ) { + if ( ! $attachment->has_required_metadata() ) { + return false; + } + + $metadata = wp_get_attachment_metadata( $attachment->get_id() ); + + if ( ! isset( $metadata['sizes'], $metadata['width'], $metadata['height'] ) ) { + return false; + } + + return is_array( $metadata['sizes'] ); + } + + /** + * Get info about Imagify. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param object $attachment An Imagify attachment. + * @return array An array containing some HTML, indexed by the attachment ID. + */ + public function get_imagify_info( $attachment ) { + if ( ! $this->needs_info() ) { + return array(); + } + + return array( + $attachment->get_id() => get_imagify_media_column_content( $attachment ), + ); + } + + /** + * Send a JSON response back to an Ajax request. + * It sends a "success" by default. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param array $data An array of data to print and die. + */ + public function send_json( $data ) { + // Use the same JSON format than WPR2X. + $data = array_merge( array( + 'success' => true, + 'message' => '', + 'source' => 'imagify', + 'context' => 'wr2x', + ), $data ); + + echo wp_json_encode( $data ); + die; + } + + /** + * Send a JSON error response if the given argument is a WP_Error object. + * + * @since 1.8 + * @access public + * @author Grégory Viguier + * + * @param mixed $result Result of an operation. + */ + public function maybe_send_json_error( $result ) { + if ( ! is_wp_error( $result ) ) { + return; + } + + // Oh no. + $this->send_json( array( + 'success' => false, + 'message' => $result->get_error_message(), + ) ); + } +} diff --git a/inc/3rd-party/perfect-images/wp-retina-2x.php b/inc/3rd-party/perfect-images/wp-retina-2x.php new file mode 100755 index 000000000..067d2f956 --- /dev/null +++ b/inc/3rd-party/perfect-images/wp-retina-2x.php @@ -0,0 +1,8 @@ +init(); From 47b90fd00c0f548c23b64f611ecc4f514321be65 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Wed, 26 Jan 2022 13:54:37 -0500 Subject: [PATCH 02/13] Rename, namespace and add to PSR4 - Rename the filenames and classnames to comply with PSR4 - Add PSR4 namespacing to classes - Add revised namespace to composer for autoloading --- composer.json | 1 + .../PerfectImages.php} | 5 +++-- .../PerfectImagesCore.php} | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) rename inc/3rd-party/perfect-images/{inc/classes/class-imagify-wp-retina-2x.php => classes/PerfectImages.php} (99%) rename inc/3rd-party/perfect-images/{inc/classes/class-imagify-wp-retina-2x-core.php => classes/PerfectImagesCore.php} (99%) diff --git a/composer.json b/composer.json index 2a017ddb7..85bf59d74 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,7 @@ "Imagify\\ThirdParty\\EnableMediaReplace\\": "inc/3rd-party/enable-media-replace/classes/", "Imagify\\ThirdParty\\FormidablePro\\": "inc/3rd-party/formidable-pro/classes/", "Imagify\\ThirdParty\\NGG\\": "inc/3rd-party/nextgen-gallery/classes/", + "Imagify\\ThirdParty\\PerfectImages\\": "inc/3rd-party/perfect-images/classes/", "Imagify\\ThirdParty\\RegenerateThumbnails\\": "inc/3rd-party/regenerate-thumbnails/classes/", "Imagify\\ThirdParty\\WPRocket\\": "inc/3rd-party/wp-rocket/classes/" }, diff --git a/inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x.php b/inc/3rd-party/perfect-images/classes/PerfectImages.php similarity index 99% rename from inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x.php rename to inc/3rd-party/perfect-images/classes/PerfectImages.php index 9c332449a..6ee4687d3 100644 --- a/inc/3rd-party/perfect-images/inc/classes/class-imagify-wp-retina-2x.php +++ b/inc/3rd-party/perfect-images/classes/PerfectImages.php @@ -1,5 +1,6 @@ Date: Wed, 26 Jan 2022 14:31:58 -0500 Subject: [PATCH 03/13] Clean some comments --- .../perfect-images/classes/PerfectImages.php | 52 ++----------------- .../classes/PerfectImagesCore.php | 26 +--------- 2 files changed, 4 insertions(+), 74 deletions(-) diff --git a/inc/3rd-party/perfect-images/classes/PerfectImages.php b/inc/3rd-party/perfect-images/classes/PerfectImages.php index 6ee4687d3..b8fde8fd0 100644 --- a/inc/3rd-party/perfect-images/classes/PerfectImages.php +++ b/inc/3rd-party/perfect-images/classes/PerfectImages.php @@ -10,22 +10,10 @@ */ class PerfectImages { - /** - * Class version. - * - * @var string - * @since 1.8 - * @author Grégory Viguier - */ - const VERSION = '1.0.1'; - /** * Core instance. * * @var object Imagify_WP_Retina_2x_Core - * @since 1.8 - * @access protected - * @author Grégory Viguier */ protected $core; @@ -33,24 +21,12 @@ class PerfectImages { * The single instance of the class. * * @var object Imagify_WP_Retina_2x - * @since 1.8 - * @access protected - * @author Grégory Viguier */ protected static $_instance; - - /** ----------------------------------------------------------------------------------------- */ - /** INSTANCE ================================================================================ */ - /** ----------------------------------------------------------------------------------------- */ - /** * Get the main Instance. * - * @since 1.8 - * @access public - * @author Grégory Viguier - * * @return object Main instance. */ public static function get_instance() { @@ -62,22 +38,14 @@ public static function get_instance() { } /** - * The constructor. - * - * @since 1.8 - * @access protected - * @author Grégory Viguier + * Constructor. */ protected function __construct() {} /** - * Get the core Instance. + * Get an instance of Imagify's PerfectImagesCore. * - * @since 1.8 - * @access public - * @author Grégory Viguier - * - * @return object Imagify_WP_Retina_2x_Core instance. + * @return object PerfectImagesCore instance. */ public function get_core() { if ( ! isset( $this->core ) ) { @@ -87,17 +55,8 @@ public function get_core() { return $this->core; } - - /** ----------------------------------------------------------------------------------------- */ - /** INIT ==================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Launch the hooks. - * - * @since 1.8 - * @access public - * @author Grégory Viguier */ public function init() { // Deal with Imagify when WPR2X is working. @@ -117,11 +76,6 @@ public function init() { add_action( 'after_imagify_restore_attachment', array( $this, 'restore_retina_images_hook' ) ); } - - /** ----------------------------------------------------------------------------------------- */ - /** AJAX CALLBACKS ========================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * (Re)generate the retina thumbnails (except the full size). * diff --git a/inc/3rd-party/perfect-images/classes/PerfectImagesCore.php b/inc/3rd-party/perfect-images/classes/PerfectImagesCore.php index 0f8b11a2e..6a652f4ae 100644 --- a/inc/3rd-party/perfect-images/classes/PerfectImagesCore.php +++ b/inc/3rd-party/perfect-images/classes/PerfectImagesCore.php @@ -10,22 +10,10 @@ */ class PerfectImagesCore { - /** - * Class version. - * - * @var string - * @since 1.8 - * @author Grégory Viguier - */ - const VERSION = '1.1'; - /** * Filesystem object. * * @var object Imagify_Filesystem - * @since 1.8 - * @access protected - * @author Grégory Viguier */ protected $filesystem; @@ -33,28 +21,16 @@ class PerfectImagesCore { * Used to store methods that should not run for a time being. * * @var array - * @since 1.8 - * @access protected - * @author Grégory Viguier */ protected static $prevented = array(); /** - * The constructor. - * - * @since 1.8 - * @access public - * @author Grégory Viguier + * Constructor. */ public function __construct() { $this->filesystem = Imagify_Filesystem::get_instance(); } - - /** ----------------------------------------------------------------------------------------- */ - /** GENERATE RETINA IMAGES ================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Generate retina images (except full size), and optimize them if the non-retina images are. * From 0775fa7684a7812ad952d12871409c0c065f01db Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Wed, 26 Jan 2022 15:13:35 -0500 Subject: [PATCH 04/13] Allow PerfectImages classes to be called Add the check for Perfect Images plugin install present, and load compatibility classes when it's there. Clean more comments and rename static $_instance property -> $instance --- inc/3rd-party/3rd-party.php | 1 + .../perfect-images/classes/PerfectImages.php | 21 +++++++++---------- .../{wp-retina-2x.php => perfect-images.php} | 5 ++++- 3 files changed, 15 insertions(+), 12 deletions(-) rename inc/3rd-party/perfect-images/{wp-retina-2x.php => perfect-images.php} (54%) diff --git a/inc/3rd-party/3rd-party.php b/inc/3rd-party/3rd-party.php index 35190d7bb..10f4e667f 100755 --- a/inc/3rd-party/3rd-party.php +++ b/inc/3rd-party/3rd-party.php @@ -9,6 +9,7 @@ require IMAGIFY_PATH . 'inc/3rd-party/enable-media-replace/enable-media-replace.php'; require IMAGIFY_PATH . 'inc/3rd-party/formidable-pro/formidable-pro.php'; require IMAGIFY_PATH . 'inc/3rd-party/nextgen-gallery/nextgen-gallery.php'; +require IMAGIFY_PATH . 'inc/3rd-party/perfect-images/perfect-images.php'; require IMAGIFY_PATH . 'inc/3rd-party/regenerate-thumbnails/regenerate-thumbnails.php'; require IMAGIFY_PATH . 'inc/3rd-party/screets-lc.php'; require IMAGIFY_PATH . 'inc/3rd-party/wp-real-media-library.php'; diff --git a/inc/3rd-party/perfect-images/classes/PerfectImages.php b/inc/3rd-party/perfect-images/classes/PerfectImages.php index b8fde8fd0..eb942d54f 100644 --- a/inc/3rd-party/perfect-images/classes/PerfectImages.php +++ b/inc/3rd-party/perfect-images/classes/PerfectImages.php @@ -2,27 +2,26 @@ namespace Imagify\ThirdParty\PerfectImages; +use Imagify_Assets; + /** - * Class that handles compatibility with WP Retina 2x plugin. - * - * @since 1.8 - * @author Grégory Viguier + * Perfect Images plugin compatibility. */ class PerfectImages { /** - * Core instance. + * PerfectImagesCore instance. * - * @var object Imagify_WP_Retina_2x_Core + * @var PerfectImagesCore */ protected $core; /** * The single instance of the class. * - * @var object Imagify_WP_Retina_2x + * @var PerfectImages */ - protected static $_instance; + protected static $instance; /** * Get the main Instance. @@ -30,11 +29,11 @@ class PerfectImages { * @return object Main instance. */ public static function get_instance() { - if ( ! isset( self::$_instance ) ) { - self::$_instance = new self(); + if ( ! isset( self::$instance ) ) { + self::$instance = new self(); } - return self::$_instance; + return self::$instance; } /** diff --git a/inc/3rd-party/perfect-images/wp-retina-2x.php b/inc/3rd-party/perfect-images/perfect-images.php similarity index 54% rename from inc/3rd-party/perfect-images/wp-retina-2x.php rename to inc/3rd-party/perfect-images/perfect-images.php index 067d2f956..ce28a7d6f 100755 --- a/inc/3rd-party/perfect-images/wp-retina-2x.php +++ b/inc/3rd-party/perfect-images/perfect-images.php @@ -1,8 +1,11 @@ init(); +PerfectImages::get_instance()->init(); From 1734e05a3d028ed2b0ad8f8f31ed12e3ada18702 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Wed, 26 Jan 2022 15:25:47 -0500 Subject: [PATCH 05/13] Stop using deprecated imagify_user_can() --- .../perfect-images/classes/PerfectImages.php | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/inc/3rd-party/perfect-images/classes/PerfectImages.php b/inc/3rd-party/perfect-images/classes/PerfectImages.php index eb942d54f..ec81ff804 100644 --- a/inc/3rd-party/perfect-images/classes/PerfectImages.php +++ b/inc/3rd-party/perfect-images/classes/PerfectImages.php @@ -214,7 +214,7 @@ public function wr2x_replace_full_retina_ajax_cb() { * @author Grégory Viguier */ public function enqueue_scripts() { - if ( ! $this->user_can() ) { + if ( ! imagify_get_context( 'wp' )->current_user_can( 'auto-optimize' ) ) { return; } @@ -430,7 +430,7 @@ public function check_nonce( $action, $query_arg = 'imagify_nonce' ) { * @author Grégory Viguier */ public function check_user_capacity() { - if ( ! $this->user_can() ) { + if ( ! imagify_get_context( 'wp' )->current_user_can( 'auto-optimize' ) ) { $this->send_json( array( 'success' => false, 'message' => __( 'Sorry, you are not allowed to do that.', 'imagify' ), @@ -438,17 +438,6 @@ public function check_user_capacity() { } } - /** - * Tell if the current user can re-optimize files. - * - * @since 1.8 - * @access public - * @author Grégory Viguier - */ - public function user_can() { - return imagify_current_user_can( 'auto-optimize' ); - } - /** * Shorthand to get the attachment ID sent via $_POST. * From 232c86e7e37ea30763ad6910cf983ac9ea6ec39b Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Wed, 26 Jan 2022 15:34:35 -0500 Subject: [PATCH 06/13] Clarify available context values There is no default context; it must be provided. The intention is that we have 2 contexts to choose from. (should be replaced with an enum when we get to use php8.1) --- inc/functions/common.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/inc/functions/common.php b/inc/functions/common.php index 680c27d65..e478e56e3 100755 --- a/inc/functions/common.php +++ b/inc/functions/common.php @@ -52,10 +52,7 @@ function imagify_sanitize_context( $context ) { /** * Get the Imagify context instance. * - * @since 1.9 - * @author Grégory Viguier - * - * @param string $context The context name. Default values are 'wp' and 'custom-folders'. + * @param string $context The context name. Available values are 'wp' and 'custom-folders'. * @return ContextInterface The context instance. */ function imagify_get_context( $context ) { From 307f91ee468ab4cb18b1762f3dc78dc7f70acb57 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Wed, 26 Jan 2022 15:36:42 -0500 Subject: [PATCH 07/13] Raise duplicated can_backup() to parent method The can_backup() method is identical in both contexts, so we can move the logic to the parent method here, and only overload it in the Noop context where it returns differently. --- classes/Context/AbstractContext.php | 7 ++++++- classes/Context/CustomFolders.php | 19 ------------------- classes/Context/WP.php | 21 +-------------------- 3 files changed, 7 insertions(+), 40 deletions(-) diff --git a/classes/Context/AbstractContext.php b/classes/Context/AbstractContext.php index ca3d475aa..a4f2a967f 100644 --- a/classes/Context/AbstractContext.php +++ b/classes/Context/AbstractContext.php @@ -170,7 +170,12 @@ public function can_resize() { * @return bool */ public function can_backup() { - return $this->can_backup; + if ( isset( $this->can_backup ) ) { + return $this->can_backup; + } + + $this->can_backup = get_imagify_option( 'backup' ); + } /** diff --git a/classes/Context/CustomFolders.php b/classes/Context/CustomFolders.php index a8ebb404c..1f04fe4dd 100644 --- a/classes/Context/CustomFolders.php +++ b/classes/Context/CustomFolders.php @@ -54,25 +54,6 @@ public function get_resizing_threshold() { return 0; } - /** - * Tell if the optimization process is allowed to backup in this context. - * - * @since 1.9 - * @access public - * @author Grégory Viguier - * - * @return bool - */ - public function can_backup() { - if ( isset( $this->can_backup ) ) { - return $this->can_backup; - } - - $this->can_backup = get_imagify_option( 'backup' ); - - return $this->can_backup; - } - /** * Tell if the optimization process is allowed to keep exif in this context. * diff --git a/classes/Context/WP.php b/classes/Context/WP.php index bd10e3fd8..9f7eba211 100644 --- a/classes/Context/WP.php +++ b/classes/Context/WP.php @@ -82,26 +82,7 @@ public function get_resizing_threshold() { return $this->resizing_threshold; } - - /** - * Tell if the optimization process is allowed to backup in this context. - * - * @since 1.9 - * @access public - * @author Grégory Viguier - * - * @return bool - */ - public function can_backup() { - if ( isset( $this->can_backup ) ) { - return $this->can_backup; - } - - $this->can_backup = get_imagify_option( 'backup' ); - - return $this->can_backup; - } - + /** * Tell if the optimization process is allowed to keep exif in this context. * From 5a4734a2f744fc99fbb43ae40c4dd034dbc21e75 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Thu, 27 Jan 2022 09:23:19 -0500 Subject: [PATCH 08/13] Update phpcs minimum version checks --- phpcs.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phpcs.xml b/phpcs.xml index aee22fdfc..d4a72a172 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -27,9 +27,9 @@ - - - + + + From 1bb523f835b6ca44fe58444042a185e41fd98be8 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Thu, 27 Jan 2022 09:23:58 -0500 Subject: [PATCH 09/13] Remove extra blank line space --- classes/Context/WP.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/Context/WP.php b/classes/Context/WP.php index 9f7eba211..c5e6e2e0b 100644 --- a/classes/Context/WP.php +++ b/classes/Context/WP.php @@ -82,7 +82,7 @@ public function get_resizing_threshold() { return $this->resizing_threshold; } - + /** * Tell if the optimization process is allowed to keep exif in this context. * From 4a1d3ed8d765b836c5912a55ae679eb99f5d3a73 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Thu, 27 Jan 2022 09:37:55 -0500 Subject: [PATCH 10/13] Re-namespace and start add_2x_sizes filter method Re-namespace and change filenames to bring the compatibility classes more in line with PS4 in preparation for adding unit tests. Adjust composer to point namespace to proper directory. Create a method to filter media sizes, where we can add @2x sizes into into the files array of a media we are about to optimize. (Starts by just returning the given files array.) --- composer.json | 2 +- .../Classes}/PerfectImages.php | 16 ++++++++++++++++ .../Classes}/PerfectImagesCore.php | 0 .../perfect-images.php | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) rename inc/3rd-party/{perfect-images/classes => PerfectImages/Classes}/PerfectImages.php (97%) rename inc/3rd-party/{perfect-images/classes => PerfectImages/Classes}/PerfectImagesCore.php (100%) rename inc/3rd-party/{perfect-images => PerfectImages}/perfect-images.php (74%) diff --git a/composer.json b/composer.json index 85bf59d74..4981c4930 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "Imagify\\ThirdParty\\EnableMediaReplace\\": "inc/3rd-party/enable-media-replace/classes/", "Imagify\\ThirdParty\\FormidablePro\\": "inc/3rd-party/formidable-pro/classes/", "Imagify\\ThirdParty\\NGG\\": "inc/3rd-party/nextgen-gallery/classes/", - "Imagify\\ThirdParty\\PerfectImages\\": "inc/3rd-party/perfect-images/classes/", + "Imagify\\ThirdParty\\PerfectImages\\": "inc/3rd-party/PerfectImages/Classes/", "Imagify\\ThirdParty\\RegenerateThumbnails\\": "inc/3rd-party/regenerate-thumbnails/classes/", "Imagify\\ThirdParty\\WPRocket\\": "inc/3rd-party/wp-rocket/classes/" }, diff --git a/inc/3rd-party/perfect-images/classes/PerfectImages.php b/inc/3rd-party/PerfectImages/Classes/PerfectImages.php similarity index 97% rename from inc/3rd-party/perfect-images/classes/PerfectImages.php rename to inc/3rd-party/PerfectImages/Classes/PerfectImages.php index ec81ff804..10314cd7e 100644 --- a/inc/3rd-party/perfect-images/classes/PerfectImages.php +++ b/inc/3rd-party/PerfectImages/Classes/PerfectImages.php @@ -2,6 +2,7 @@ namespace Imagify\ThirdParty\PerfectImages; +use Imagify\Media\MediaInterface; use Imagify_Assets; /** @@ -73,6 +74,21 @@ public function init() { add_filter( 'imagify_fill_thumbnail_data', array( $this, 'optimize_retina_version_hook' ), 10, 8 ); add_filter( 'imagify_fill_unauthorized_thumbnail_data', array( $this, 'maybe_optimize_unauthorized_retina_version_hook' ), 10, 7 ); add_action( 'after_imagify_restore_attachment', array( $this, 'restore_retina_images_hook' ) ); + + add_filter( 'imagify_media_files', [ $this, 'add_2x_images_to_sizes_for_optimization', 10, 2 ] ); + } + + /** + * Add @2x image sizes to the sizes of a given media optimization. + * + * @param array $files File sizes to optimize. @see Media\WP::get_media_files(). + * @param MediaInterface $media The Media for optimization. + * + * @return array Modified file sizes. + */ + public function add_2x_images_to_sizes_for_optimization( array $files, MediaInterface $media ): array { + + return $files; } /** diff --git a/inc/3rd-party/perfect-images/classes/PerfectImagesCore.php b/inc/3rd-party/PerfectImages/Classes/PerfectImagesCore.php similarity index 100% rename from inc/3rd-party/perfect-images/classes/PerfectImagesCore.php rename to inc/3rd-party/PerfectImages/Classes/PerfectImagesCore.php diff --git a/inc/3rd-party/perfect-images/perfect-images.php b/inc/3rd-party/PerfectImages/perfect-images.php similarity index 74% rename from inc/3rd-party/perfect-images/perfect-images.php rename to inc/3rd-party/PerfectImages/perfect-images.php index ce28a7d6f..a816ae804 100755 --- a/inc/3rd-party/perfect-images/perfect-images.php +++ b/inc/3rd-party/PerfectImages/perfect-images.php @@ -1,6 +1,6 @@ Date: Thu, 27 Jan 2022 10:13:23 -0500 Subject: [PATCH 11/13] Add test for empty files array case. --- .../Classes/PerfectImagesTest.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Tests/Unit/inc/3rd-party/PerfectImages/Classes/PerfectImagesTest.php diff --git a/Tests/Unit/inc/3rd-party/PerfectImages/Classes/PerfectImagesTest.php b/Tests/Unit/inc/3rd-party/PerfectImages/Classes/PerfectImagesTest.php new file mode 100644 index 000000000..d8c6e034c --- /dev/null +++ b/Tests/Unit/inc/3rd-party/PerfectImages/Classes/PerfectImagesTest.php @@ -0,0 +1,26 @@ +assertEquals([], $perfectImages->add_2x_images_to_sizes_for_optimization( [], $media ) ); + } +} From 7d4a8385fdd9fe38ccebd4bb8266f44c493c51dd Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Thu, 27 Jan 2022 10:14:47 -0500 Subject: [PATCH 12/13] Add 3rd-party test path to find 3rd-party --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4981c4930..c768b2edf 100644 --- a/composer.json +++ b/composer.json @@ -64,7 +64,10 @@ ] }, "autoload-dev": { - "psr-4": { "Imagify\\Tests\\": "Tests/" } + "psr-4": { + "Imagify\\Tests\\": "Tests/", + "Imagify\\Tests\\Unit\\Inc\\ThirdParty\\": "Tests/Unit/inc/3rd-party/" + } }, "extra": { "mozart": { From e8e12c512156ca0255fd097fbd0476747209bfc6 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Thu, 27 Jan 2022 10:16:13 -0500 Subject: [PATCH 13/13] Remove unnecessary WP cheatin' death We don't need these. Classes will not directly output anything, and they're killing the unit tests before they can run when we need to mock them. --- classes/Media/AbstractMedia.php | 2 -- classes/Media/MediaInterface.php | 2 -- classes/Media/WP.php | 2 -- inc/deprecated/Traits/Media/WPDeprecatedTrait.php | 2 -- 4 files changed, 8 deletions(-) diff --git a/classes/Media/AbstractMedia.php b/classes/Media/AbstractMedia.php index 87647597f..9826863af 100644 --- a/classes/Media/AbstractMedia.php +++ b/classes/Media/AbstractMedia.php @@ -4,8 +4,6 @@ use Imagify\CDN\PushCDNInterface; use Imagify\Context\ContextInterface; -defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); - /** * Abstract used for "media groups" (aka attachments). * diff --git a/classes/Media/MediaInterface.php b/classes/Media/MediaInterface.php index 79be27807..c809a5994 100644 --- a/classes/Media/MediaInterface.php +++ b/classes/Media/MediaInterface.php @@ -4,8 +4,6 @@ use Imagify\CDN\PushCDNInterface; use Imagify\Context\ContextInterface; -defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); - /** * Interface to use for "media groups" (aka attachments). * diff --git a/classes/Media/WP.php b/classes/Media/WP.php index 1bc3d7671..e5f9b804a 100644 --- a/classes/Media/WP.php +++ b/classes/Media/WP.php @@ -1,8 +1,6 @@