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 ) );
+ }
+}
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..c5e6e2e0b 100644
--- a/classes/Context/WP.php
+++ b/classes/Context/WP.php
@@ -83,25 +83,6 @@ 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.
*
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 @@
core ) ) {
+ $this->core = new Imagify_WP_Retina_2x_Core();
+ }
+
+ return $this->core;
+ }
+
+ /**
+ * Launch the hooks.
+ */
+ 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' ) );
+
+ 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;
+ }
+
+ /**
+ * (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 ( ! imagify_get_context( 'wp' )->current_user_can( 'auto-optimize' ) ) {
+ 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 ( ! 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' ),
+ ) );
+ }
+ }
+
+ /**
+ * 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/PerfectImages/Classes/PerfectImagesCore.php b/inc/3rd-party/PerfectImages/Classes/PerfectImagesCore.php
new file mode 100644
index 000000000..6a652f4ae
--- /dev/null
+++ b/inc/3rd-party/PerfectImages/Classes/PerfectImagesCore.php
@@ -0,0 +1,1671 @@
+filesystem = Imagify_Filesystem::get_instance();
+ }
+
+ /**
+ * 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/PerfectImages/perfect-images.php b/inc/3rd-party/PerfectImages/perfect-images.php
new file mode 100755
index 000000000..a816ae804
--- /dev/null
+++ b/inc/3rd-party/PerfectImages/perfect-images.php
@@ -0,0 +1,11 @@
+init();
diff --git a/inc/deprecated/Traits/Media/WPDeprecatedTrait.php b/inc/deprecated/Traits/Media/WPDeprecatedTrait.php
index 0c89bc5d4..2a5e58140 100644
--- a/inc/deprecated/Traits/Media/WPDeprecatedTrait.php
+++ b/inc/deprecated/Traits/Media/WPDeprecatedTrait.php
@@ -1,8 +1,6 @@
-
-
-
+
+
+