<?php
/**
 * S3 integration: S3 class
 *
 * Minimum supported version - Offload Media 2.4
 *
 * @package Smush\Core\Modules\Integrations
 * @subpackage S3
 * @since 2.7
 *
 * @author Umesh Kumar <umesh@incsub.com>
 *
 * @copyright (c) 2017, Incsub (http://incsub.com)
 */

namespace Smush\Core\Integrations;

use Amazon_S3_And_CloudFront;
use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item;
use Smush\App\Admin;
use Smush\Core\Helper;
use Smush\Core\Settings;
use WP_Smush;

if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class S3
 */
class S3 extends Abstract_Integration {

	/**
	 * Save list of files need to remove
	 * when user enabling "Remove Files From Server".
	 *
	 * @var array
	 */
	private $files_to_remove = array();

	/**
	 * Cache list of files download failed.
	 *
	 * @var array
	 */
	private $files_download_failed = array();

	/**
	 * On Smush mode, we will disable auto download and auto-upload
	 * while working with Smush.
	 *
	 * @var boolean
	 */
	private $on_smush_mode;

	/**
	 * Cache list of files to delete via remove_file method.
	 *
	 * @var array
	 */
	private $doing_files;

	/**
	 * Cache list of filters for get_attached_file.
	 *
	 * @var null|array
	 */
	private $list_file_filters;

	/**
	 * S3 constructor.
	 */
	public function __construct() {
		$this->module  = 's3';
		$this->class   = 'pro';
		$this->enabled = function_exists( 'as3cf_init' ) || function_exists( 'as3cf_pro_init' );

		parent::__construct();

		// Hook at the end of setting row to output a error div.
		add_action( 'smush_setting_column_right_inside', array( $this, 's3_setup_message' ), 15 );

		// Show S3 integration message, if user hasn't enabled it.
		add_action( 'wp_smush_header_notices', array( $this, 'show_s3_support_required_notice' ) );

		// Add Pro tag.
		if ( ! WP_Smush::is_pro() || ! $this->enabled ) {
			add_action( 'smush_setting_column_tag', array( $this, 'add_pro_tag' ) );
		}

		// Do not continue if not enabled or S3 Offload plugin is not installed.
		if ( ! $this->is_active() ) {
			return;
		}

		// Load all our custom actions/filters after loading as3cf.
		if ( did_action( 'as3cf_init' ) ) {
			$this->init();
		} else {
			add_action( 'as3cf_init', array( $this, 'init' ) );
		}
	}

	/**
	 * Register actions, filters for S3.
	 *
	 * @since 3.9.6
	 */
	public function init() {
		// TODO: (stats refactor) we still need some of the stuff from this controller e.g. notices
		return;

		global $as3cf;

		// Check file exists.
		add_filter( 'wp_smush_file_exists', array( $this, 'file_exists_on_s3' ), 10, 4 );

		// Get attached file.
		add_filter( 'wp_smush_get_attached_file', array( $this, 'get_attached_file' ), 10, 5 );

		// Check unique file with the backup file.
		add_filter( 'wp_unique_filename', array( $this, 'filter_unique_filename' ), 99, 3 );// S3 is using priority 10.

		// Check if the file exists for the given path and download.
		add_action( 'wp_smush_before_restore_backup', array( $this, 'maybe_download_file' ), 10, 2 );
		// Update original source path of as3cf_items after converting PNG to JPG.
		add_action( 'wp_smush_image_url_changed', array( $this, 'update_original_source_path_after_png2jpg' ), 10, 4 );
		// Update original source path of as3cf_items after restore the converted PNG2JPG file.
		add_action( 'wp_smush_image_url_updated', array( $this, 'update_original_source_path_after_restore_png' ), 10, 3 );

		// If user is enabling copy to S3.
		if ( $as3cf->get_setting( 'copy-to-s3' ) ) {
			/**
			 * Activate Smush mode (Disable auto download and upload attachments).
			 *
			 * Note, we managed to release this mode by using Helper::wp_update_attachment_metadata() or
			 * via action "wp_smush_after_smush_file"
			 */
			// Activate smush mode before smushing/restoring.
			add_action( 'wp_smush_before_smush_file', array( $this, 'activate_smush_mode' ), 1 );
			add_action( 'wp_smush_before_restore_backup', array( $this, 'activate_smush_mode' ), 1 );

			/**
			 * When WP create a new attachment, S3 will try to upload it to the server,
			 * and then it will upload the thumbnails later after regenerating the thumbnails.
			 * So we use this filter to temporary disable upload if smush can do with this file.
			 *
			 * Note, if we don't work with the current image, e.g animated file,
			 * we managed to release this mode by using maybe_release_smush_mode.
			 */
			add_filter( 'wp_update_attachment_metadata', array( $this, 'maybe_active_smush_mode' ), 1, 2 );

			// Reset smush mode after smushing/restoring.
			add_action( 'wp_smush_after_smush_file', array( $this, 'release_smush_mode' ) );
			add_action( 'wp_smush_after_restore_backup', array( $this, 'release_smush_mode' ) );
			/**
			 * Make sure we exit Smush mode before updating the metadata,
			 * this will help we to upload the attachments, and remove them if it's required.
			 */
			add_action( 'wp_smush_before_update_attachment_metadata', array( $this, 'release_smush_mode' ) );
			// Release mode when we don't smush the image.
			add_action( 'wp_smush_no_smushit', array( $this, 'maybe_release_smush_mode' ) );

			// Remove .bak file after restoring, and JPG files after restoring PNG file.
			add_action( 'wp_smush_after_remove_file', array( $this, 'remove_file' ), 10, 3 );
			// Make sure we removed all downloaded files.
			add_filter( 'shutdown', array( $this, 'maybe_remove_downloaded_files' ), 10, 2 );
		}
	}

	/**
	 * Disable module functionality if not PRO.
	 *
	 * @return bool
	 */
	public function setting_status() {
		return ! WP_Smush::is_pro() ? true : ! $this->enabled;
	}

	/**************************************
	 *
	 * OVERWRITE PARENT CLASS FUNCTIONALITY
	 */

	/**
	 * Filters the setting variable to add S3 setting title and description.
	 *
	 * @param array $settings  Settings array.
	 *
	 * @return array
	 */
	public function register( $settings ) {
		$plugin_url                = esc_url( 'https://wordpress.org/plugins/amazon-s3-and-cloudfront/' );
		$settings[ $this->module ] = array(
			'label'       => __( 'Enable Amazon S3 support', 'wp-smushit' ),
			'short_label' => __( 'Amazon S3', 'wp-smushit' ),
			'desc'        => sprintf( /* translators: %1$s - <a>, %2$s - </a> */
				esc_html__(
					"Storing your image on S3 buckets using %1\$sWP Offload Media%2\$s? Smush can detect and smush those assets for you, including when you're removing files from your host server.",
					'wp-smushit'
				),
				"<a href='$plugin_url' target = '_blank'>",
				'</a>'
			),
		);

		return $settings;
	}

	/**************************************
	 *
	 * PUBLIC CLASSES
	 */

	/**
	 * Check if the file is served by S3 and download the file for given path
	 *
	 * @param string $file_path      Full file path.
	 * @param string $attachment_id  Attachment ID.
	 *
	 * @return bool|string False/ File Path
	 */
	public function maybe_download_file( $file_path, $attachment_id ) {
		// Download the backup file if it doesn't exist on the server.
		return $this->download_file( $file_path, $attachment_id );
	}

	/**
	 * Checks if the given attachment is on S3 or not, Returns S3 URL or WP Error
	 *
	 * @param string $attachment_id  Attachment ID.
	 *
	 * @return bool
	 */
	public function is_image_on_s3( $attachment_id = '' ) {
		if ( empty( $attachment_id ) ) {
			return false;
		}

		// If the file path contains S3, get the s3 URL for the file.
		if ( function_exists( 'as3cf_get_attachment_url' ) ) {
			return as3cf_get_attachment_url( $attachment_id );
		}

		Helper::logger()->integrations()->error( 'S3 - Function as3cf_get_attachment_url does not exists.' );
		return false;
	}

	/**
	 * Checks if file exits on S3.
	 *
	 * @param bool   $exists           If file exists on S3.
	 * @param mixed  $file_path        File path or empty value.
	 * @param string $attachment_id    Attachment ID.
	 * @param bool   $should_download  Should download attachment.
	 *
	 * @return bool|string Returns TRUE OR File path if it exists, FALSE otherwise.
	 */
	public function file_exists_on_s3( $exists, $file_path, $attachment_id, $should_download ) {
		if ( ! $exists ) {
			if ( empty( $file_path ) ) {
				// Maybe file is not uploaded to provider, try to get the raw file path.
				$file_path = $this->get_raw_attached_file( $attachment_id );
			}
			$exists = file_exists( $file_path );
		}

		if ( ! $exists ) {
			if ( $should_download ) {
				$exists = $this->download_file( $file_path, $attachment_id );
			} else {
				$exists = $this->does_image_exists( $attachment_id, $file_path );
			}
		}

		return $exists;
	}

	/**
	 * Error message to show when S3 support is required.
	 *
	 * Show a error message to admins, if they need to enable S3 support. If "remove files from
	 * server" option is enabled in WP Offload Media plugin, we need WP Smush Pro to enable S3 support.
	 */
	public function show_s3_support_required_notice() {
		// Do not display it for other users. Do not display on network screens, if network-wide option is disabled.
		if ( ! current_user_can( 'manage_options' ) || ! Settings::can_access( 'integrations' ) ) {
			return;
		}

		// If already dismissed, do not show.
		if ( '1' === get_site_option( 'wp-smush-hide_s3support_alert' ) ) {
			return;
		}

		// Return early, if support is not required.
		if ( ! $this->s3_support_required() ) {
			return;
		}

		// Settings link.
		$settings_link = is_multisite() && is_network_admin()
			? network_admin_url( 'admin.php?page=smush-integrations' )
			: menu_page_url( 'smush-integrations', false );

		if ( WP_Smush::is_pro() ) {
			/**
			 * If premium user, but S3 support is not enabled.
			 */
			$message = sprintf(
				/* Translators: %1$s: opening strong tag, %2$s: closing strong tag, %s: settings link, %3$s: opening a and strong tags, %4$s: closing a and strong tags */
				__(
					'We can see you have WP Offload Media installed. If you want to optimize your S3 images, you’ll need to enable the %3$sAmazon S3 Support%4$s feature in Smush’s Integrations.',
					'wp-smushit'
				),
				'<strong>',
				'</strong>',
				"<a href='$settings_link'><strong>",
				'</strong></a>'
			);
		} else {
			/**
			 * If not a premium user.
			 */
			$message = sprintf(
				/* Translators: %1$s: opening strong tag, %2$s: closing strong tag, %s: settings link, %3$s: opening a and strong tags, %4$s: closing a and strong tags */
				__(
					"We can see you have WP Offload Media installed. If you want to optimize your S3 images you'll need to %3\$supgrade to Smush Pro%4\$s",
					'wp-smushit'
				),
				'<strong>',
				'</strong>',
				'<a href=' . esc_url( 'https://wpmudev.com/project/wp-smush-pro' ) . '><strong>',
				'</strong></a>'
			);
		}
		$message = '<p>' . $message . '</p>';
		echo '<div role="alert" id="wp-smush-s3support-alert" class="sui-notice" data-message="' . esc_attr( $message ) . '" aria-live="assertive"></div>';
	}

	/**
	 * Prints the message for S3 setup
	 *
	 * @param string $setting_key  Settings key.
	 */
	public function s3_setup_message( $setting_key ) {
		// Return if not S3.
		if ( $this->module !== $setting_key ) {
			return;
		}

		/**
		 * Amazon_S3_And_CloudFront global.
		 *
		 * @var Amazon_S3_And_CloudFront $as3cf
		 */
		global $as3cf;

		// If S3 integration is not enabled, return.
		$setting_val = WP_Smush::is_pro() ? $this->settings->get( $this->module ) : 0;

		// If integration is disabled when S3 offload is active, do not continue.
		if ( ! $setting_val && is_object( $as3cf ) ) {
			return;
		}

		// If S3 offload global variable is not available, plugin is not active.
		if ( ! is_object( $as3cf ) ) {
			$class   = '';
			$message = __( 'To use this feature you need to install WP Offload Media and have an Amazon S3 account setup.', 'wp-smushit' );
		} elseif ( ! method_exists( $as3cf, 'is_plugin_setup' ) || ! method_exists( $as3cf, 'get_plugin_page_url' ) ) {
			// Check if in case for some reason, we couldn't find the required function.
			$class   = ' sui-notice-warning';
			$message = sprintf( /* translators: %1$s: opening a tag, %2$s: closing a tag */
				esc_html__(
					'We are having trouble interacting with WP Offload Media, make sure the plugin is activated. Or you can %1$sreport a bug%2$s.',
					'wp-smushit'
				),
				'<a href="' . esc_url( 'https://wpmudev.com/contact' ) . '" target="_blank">',
				'</a>'
			);
		} elseif ( ! $as3cf->is_plugin_setup() ) {
			// Plugin is not setup, or some information is missing.
			$class   = ' sui-notice-warning';
			$message = sprintf( /* translators: %1$s: opening a tag, %2$s: closing a tag */
				esc_html__(
					'It seems you haven’t finished setting up WP Offload Media yet. %1$sConfigure it now%2$s to enable Amazon S3 support.',
					'wp-smushit'
				),
				'<a href="' . $as3cf->get_plugin_page_url() . '" target="_blank">',
				'</a>'
			);
		} else {
			// S3 support is active.
			$class   = ' sui-notice-info';
			$message = __( 'Amazon S3 support is active.', 'wp-smushit' );
		}
		?>
		<div class="sui-toggle-content">
			<div class="sui-notice<?php echo esc_attr( $class ); ?>">
				<div class="sui-notice-content">
					<div class="sui-notice-message">
						<i class="sui-notice-icon sui-icon-info" aria-hidden="true"></i>
						<p><?php echo wp_kses_post( $message ); ?></p>
					</div>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Add a pro tag next to the setting title.
	 *
	 * @param string $setting_key  Setting key name.
	 *
	 * @since 3.4.0
	 */
	public function add_pro_tag( $setting_key ) {
		// Return if not NextGen integration.
		if ( $this->module !== $setting_key || WP_Smush::is_pro() ) {
			return;
		}
		?>
		<span class="sui-tag sui-tag-pro">
			<?php esc_html_e( 'Pro', 'wp-smushit' ); ?>
		</span>
		<?php
	}

	/**
	 * Disable auto downloading the file to local while Smush doing, only allow it with our custom methods:
	 * Helper::get_attached_file( $attachment_id, $type='smush|resize|original')
	 * or Helper::exists_or_downloaded( $file, $attachment_id ),
	 * and set wait_for_generate_attachment_metadata is TRUE to disable auto upload attachments while
	 * we are working with it.
	 *
	 * @since 3.4.0
	 *
	 * @since 3.9.6
	 * Enable copy file back to local and wait_for_generate_attachment_metadata while we working with smush or restore.
	 */
	public function activate_smush_mode() {
		if ( $this->is_active() && ! $this->on_smush_mode ) {
			global $as3cf;
			// Save mode.
			$this->on_smush_mode = true;
			/**
			 * Disable auto download to local, we only enable it via  self::downoad_file method to avoid automatic download link.
			 * e.g. wp_attachment_is_image()
			 */
			add_filter( 'as3cf_get_attached_file_copy_back_to_local', '__return_false', 9998 );
			// Disable auto upload attachments.
			add_filter( 'as3cf_wait_for_generate_attachment_metadata', '__return_true', 9998 );

			// If user enabling remove file on local.
			if ( $as3cf && $as3cf->get_setting( 'remove-local-file' ) ) {
				// We don't remove the filter because it might be called later.
				add_filter( 'as3cf_upload_attachment_local_files_to_remove', array( $this, 'remove_missing_files_to_avoid_error_log_from_s3' ), 99 );
			}
		}
	}

	/**
	 * Set Smush mode while creating a new image.
	 *
	 * @param array $image_meta Image meta.
	 * @param int   $attachment_id Attachment ID.
	 * @return array The provided metadata.
	 */
	public function maybe_active_smush_mode( $image_meta, $attachment_id ) {
		if (
			! $this->on_smush_mode
			// When async uploading or the image is created from Gutenberg, activate smush mode.
			&& empty( $new_meta['sizes'] )
			&& ! empty( $image_meta['file'] )
			&& ( isset( $_POST['post_id'] ) || isset( $_FILES['async-upload'] ) || ! Helper::is_non_rest_media() )
			// If enabling auto-smush.
			&& $this->settings->get( 'auto' )
			// Managed it.
			&& ! did_action( 'wp_smush_before_smush_file' )
			// Managed it.
			&& ! did_action( 'wp_smush_before_restore_backup' )
			// Managed it.
			&& ! did_action( 'wp_smush_before_update_attachment_metadata' )
			// Don't smush it.
			&& ! did_action( 'wp_smush_no_smushit' )
			// Only support Image.
			&& Helper::is_smushable( $attachment_id )
			// Make sure we only disable while async upload new image.
			&& ! $this->does_image_exists( $attachment_id, $this->get_raw_attached_file( $attachment_id, 'original' ) )
		) {
			$this->activate_smush_mode();
		}

		return $image_meta;
	}

	/**
	 * If we don't work with current image,
	 * make sure we released Smush mode which set by maybe_active_smush_mode.
	 *
	 * @param int $attachment_id Attachment ID.
	 */
	public function maybe_release_smush_mode( $attachment_id ) {
		// Release smush mode.
		$this->release_smush_mode();

		if ( ! WP_SMUSH_ASYNC || ! $this->settings->get( 'auto' ) || ! doing_filter( 'wp_async_wp_generate_attachment_metadata' ) || ! did_action( 'wp_smush_no_smushit' ) ) {
			return;
		}

		if ( get_transient( 'smush-in-progress-' . $attachment_id ) || get_transient( 'wp-smush-restore-' . $attachment_id ) ) {
			return;
		}

		// Make sure all images will upload to cloud.
		if ( ! did_action( 'wp_smush_before_update_attachment_metadata' ) ) {
			global $as3cf;
			// If the image is already uploaded, returns.
			if ( ! $as3cf->get_setting( 'copy-to-s3' ) || $this->does_image_exists( $attachment_id, $this->get_raw_attached_file( $attachment_id, 'original' ) ) ) {
				return;
			}
			// Make sure method exits.
			if ( method_exists( $as3cf, 'upload_attachment' ) ) {
				$as3cf->upload_attachment( $attachment_id, wp_get_attachment_metadata( $attachment_id ) );
				return;
			}

			$s3_filter_obj = $this->get_s3_filter_class();
			if ( $s3_filter_obj && method_exists( $s3_filter_obj, 'wp_update_attachment_metadata' ) ) {
				$s3_filter_obj->wp_update_attachment_metadata( wp_get_attachment_metadata( $attachment_id ), $attachment_id );
				return;
			}

			// Log a warning.
			Helper::logger()->integrations()->warning( 'S3 - the upload method does not exists, try to upload files via filter wp_update_attachment_metadata.' );

			// Try to upload attachments via filter.
			// Temporary disable our filters.
			remove_filter( 'wp_update_attachment_metadata', array( $this, 'maybe_active_smush_mode' ), 1 );
			apply_filters( 'wp_update_attachment_metadata', wp_get_attachment_metadata( $attachment_id ), $attachment_id );
			// Restore our filters.
			add_filter( 'wp_update_attachment_metadata', array( $this, 'maybe_active_smush_mode' ), 1, 2 );
		}
	}

	/**
	 * Release all our configs for copy file back to local
	 * and wait_for_generate_attachment_metadata before calling
	 * wp_update_attachment_metadata
	 *
	 * @since 3.9.6
	 */
	public function release_smush_mode() {
		if ( $this->is_active() && $this->on_smush_mode ) {
			remove_filter( 'as3cf_get_attached_file_copy_back_to_local', '__return_false', 9998 );
			remove_filter( 'as3cf_wait_for_generate_attachment_metadata', '__return_true', 9998 );
			// Reset mode.
			$this->on_smush_mode = false;
		}
	}

	/**
	 * Download file back to the server if missing when get_attached_file() is called.
	 *
	 * @since 3.9.6
	 *
	 * @param null|string $file_path        File path or file url(checking resize).
	 * @param int         $attachment_id    Attachment ID.
	 * @param bool        $should_download  Should download the file if it doesn't exist.
	 * @param bool        $should_real_path Expecting a real file path instead an URL.
	 * @param string      $type             original|smush|backup|resize.
	 *
	 * @return string     File path or S3 url.
	 */
	public function get_attached_file( $file_path, $attachment_id, $should_download, $should_real_path, $type ) {
		if ( is_null( $file_path ) ) {
			// Try to get crawl file path.
			$file_path = $this->get_raw_attached_file( $attachment_id, $type );
			if ( file_exists( $file_path ) ) {
				return $file_path;
			} else {
				if ( $should_download ) {
					$downloaded_file_path = $this->download_file( $file_path, $attachment_id );
					if ( $downloaded_file_path ) {
						$file_path = $downloaded_file_path;
					}
				} elseif ( ! $should_real_path ) {
					// Try to get S3 url.
					$file_path = apply_filters( 'get_attached_file', $file_path, $attachment_id );
				}
			}
		}
		return $file_path;
	}

	/**
	 * Delete multiple objects.
	 *
	 * @param array $objects_to_remove Objects to remove.
	 */
	private function delete_objects( $objects_to_remove ) {
		$provider_client = $this->get_provider_client();
		if ( $provider_client && method_exists( $provider_client, 'delete_objects' ) ) {
			global $as3cf;
			return $provider_client->delete_objects(
				array(
					'Bucket' => $as3cf->get_setting( 'bucket' ),
					'Delete' => array(
						'Objects' => $objects_to_remove,
					),
				)
			);
		} else {
			Helper::logger()->integrations()->error( 'S3 - AWS_Provider->delete_objects does not exist.' );
		}
		return false;
	}

	/**
	 * Remove the backup file and JPG files (PNG2JPG) from S3 when image is restored.
	 *
	 * @since 3.8.4
	 *
	 * @since 3.9.6 Remove file(s) from S3.
	 *
	 * @param int    $attachment_id  Attachment ID.
	 * @param string $file_paths      File path(s).
	 * @param bool   $removed        Whether the provided files are removed or not.
	 */
	public function remove_file( $attachment_id, $file_paths, $removed ) {
		// Returns if file path is empty.
		if ( empty( $attachment_id ) || empty( $file_paths ) ) {
			return false;
		}
		/**
		 * Amazon_S3_And_CloudFront global.
		 *
		 * @var Amazon_S3_And_CloudFront $as3cf
		 */
		global $as3cf;

		// Get s3 object for the file.
		if ( ! $s3_object = $this->is_attachment_served_by_provider( $as3cf, $attachment_id ) ) {
			return false;
		}

		$file_paths = (array) $file_paths;

		// Get file key.
		$is_object         = is_object( $s3_object );
		$objects_to_remove = array();
		if ( $is_object && $s3_object instanceof Media_Library_Item && method_exists( $s3_object, 'key' ) ) {
			/**
			 * We use this method to support private mode too.
			 *
			 * @see Media_Library_Item()->key() (>=2.4)
			 */
			foreach ( $file_paths as $size_key => $file_path ) {
				if ( ! $removed && file_exists( $file_path ) ) {
					unlink( $file_path );
				}

				$objects_to_remove[] = array(
					'Key' => $this->get_object_key( $s3_object, $file_path, $size_key ),
				);
			}
		} else {
			// Try with the old version.
			if ( $is_object ) {
				$key = $s3_object->path();
			} else {
				$key = $s3_object['key'];
			}

			$size_prefix      = dirname( $key );
			$size_file_prefix = ( '.' === $size_prefix ) ? '' : $size_prefix . '/';

			foreach ( $file_paths as $file_path ) {
				if ( ! $removed && file_exists( $file_path ) ) {
					unlink( $file_path );
				}
				// Get the File path using basename for given attachment path.
				$objects_to_remove[] = array(
					'Key' => path_join( $size_file_prefix, wp_basename( $file_path ) ),
				);
			}
		}

		return $this->delete_objects( $objects_to_remove );
	}

	/**
	 * Add a filter before wp_update_attachment_metadata
	 * to remove skipped thumbnails from s3 upload.
	 *
	 * @since 3.9.6
	 */
	public function maybe_remove_sizes_from_s3_upload() {
		/**
		 * When S3 integration is enabled, the wp_update_attachment_metadata below will trigger the
		 * wp_update_attachment_metadata filter WP Offload Media, which in turn will try to re-upload all the files
		 * to an S3 bucket. But, if some sizes are skipped during Smushing, WP Offload Media will print error
		 * messages to debug.log. This will help avoid that.
		 *
		 * @since 3.0
		 *
		 * @since 3.9.6 Moved it from Smush\Core\Modules\Smush to S3
		 */
		add_filter( 'as3cf_attachment_file_paths', array( $this, 'remove_sizes_from_s3_upload' ), 10, 3 );
	}

	/**
	 * Remove all downloaded files
	 * if user enabling "Remove Files From Server".
	 *
	 * @since 3.9.6
	 *
	 * Note, we will remove all images that saved in variable $this->files_to_remove
	 * so please check member enabling "Remove Files From Server" before adding the image.
	 */
	public function maybe_remove_downloaded_files() {
		if ( $this->is_active() ) {
			global $as3cf;
			if ( isset( $this->files_to_remove ) && $as3cf && $as3cf->get_setting( 'remove-local-file' ) ) {
				foreach ( $this->files_to_remove  as $file_path ) {
					if ( file_exists( $file_path ) ) {
						unlink( $file_path );
					}
				}
			}
		}
	}

	/**
	 * Verify the unique file name with the backup file of PNG2JPG file.
	 *
	 * E.g
	 * If member enable "remove file from local", the uploads folder will be empty,
	 * so WP core will not handle this case, S3 only handle the main file and the sub-sizes.
	 * => In order to avoid conflicts, when generate a new unique file, we also need to check if it is a backed up file or not.
	 * What we are expecting is:
	 * 1. Image 1 test.png => test.jpg + backup file test.png
	 * 2. Image 2 test.png => test-1.jpg + backup file test-1.png not test.png
	 *
	 * @param string $filename Unique file name.
	 * @param string $ext      File extension, eg. ".png".
	 * @param string $dir      Directory path.
	 *
	 * @return string
	 */
	public function filter_unique_filename( $filename, $ext, $dir ) {
		// Only check for PNG type and activating backup mode.
		if ( '.png' === $ext && WP_Smush::get_instance()->core()->mod->png2jpg->is_active() ) {
			global $as3cf;
			if ( method_exists( $as3cf, 'does_file_exist' ) ) {
				$uploads  = wp_upload_dir();
				$basedir  = trailingslashit( $uploads['basedir'] );
				$count    = 0;
				$name     = pathinfo( $filename, PATHINFO_FILENAME );
				$filename = $name . $ext;
				$time     = current_time( 'mysql' );
				while ( ( $count && $as3cf->does_file_exist( $filename, $time ) ) || $this->is_png2jpg_backup_file( $filename, $dir, $basedir ) ) {
					$count++;
					$filename = $name . '-' . $count . $ext;
				}
				return $filename;
			} else {
				Helper::logger()->integrations()->error( 'S3 - Method $as3cf->does_file_exist() does not exists.' );
			}
		}
		return $filename;
	}

	/**
	 * Get unique PNG file name by verify the backup file.
	 * file name
	 *
	 * @param  string $filename  Unique file name.
	 * @param  string $dir       Directory path.
	 * @param  string $basedir   Base upload directory.
	 * @return string Unique PNG file name.
	 */
	private function is_png2jpg_backup_file( $filename, $dir, $basedir ) {
		$backup    = WP_Smush::get_instance()->core()->mod->backup;
		$file_path = substr( path_join( $dir, pathinfo( $filename, PATHINFO_FILENAME ) . '.jpg' ), strlen( $basedir ) );
		// Get s3_object_item from jpg file.
		$jpg_items = Media_Library_Item::get_by_source_path( $file_path, array(), true, true );
		if ( ! empty( $jpg_items ) ) {
			foreach ( $jpg_items as $jpg_item ) {
				$jpg_id       = $jpg_item->source_id();
				$backup_sizes = $backup->get_backup_sizes( $jpg_id );
				// If the current file is the same as the backup file, try to get a unique file name.
				if ( $backup_sizes && isset( $backup_sizes['smush-full']['file'] ) && $backup_sizes['smush-full']['file'] === $filename ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Remove paths that should not be re-uploaded to an S3 bucket.
	 *
	 * See as3cf_attachment_file_paths filter description for more information.
	 *
	 * @since 3.0
	 *
	 * @param array $paths          Paths to be uploaded to S3 bucket.
	 * @param int   $attachment_id  Attachment ID.
	 * @param array $meta           Image metadata.
	 *
	 * @since 3.9.6 Moved it from Smush\Core\Modules\Smush to S3
	 *
	 * @return mixed
	 */
	public function remove_sizes_from_s3_upload( $paths, $attachment_id, $meta ) {
		// Only run when S3 integration is active (it shouldn't run otherwise, but check just in case),
		// and when the image does have sizes.
		if ( empty( $meta['sizes'] ) ) {
			return $paths;
		}
		$smush = WP_Smush::get_instance()->core()->mod->smush;
		foreach ( $meta['sizes'] as $size_key => $size_data ) {
			// Check if registered size is supposed to be Smushed or not.
			if ( 'full' !== $size_key && $smush->skip_image_size( $size_key ) ) {
				unset( $paths[ $size_key ] );
			}
		}

		return $paths;
	}

	/**
	 * S3 remove the local file without checking the exists,
	 * and they also log the warning into debug.log
	 * E.g When we convert PNG2JPG and delete the original files,
	 * and this will cause the missing files for S3.
	 * We only apply the filter on smush mode.
	 *
	 * @param  array $files_to_remove List file to remove on local.
	 * @return array
	 */
	public function remove_missing_files_to_avoid_error_log_from_s3( $files_to_remove ) {
		global $as3cf;
		if ( $as3cf && $as3cf->get_setting( 'remove-local-file' ) ) {
			foreach ( $files_to_remove as $size => $file_path ) {
				if ( ! file_exists( $file_path ) ) {
					unset( $files_to_remove[ $size ] );
				}
			}
		}
		return $files_to_remove;
	}

	/** Activating Private Media */

	/**
	 * Return true if private media is activated.
	 *
	 * @return boolean
	 */
	public function enable_private_media() {
		global $as3cf;
		return $as3cf && $as3cf->get_setting( 'enable-signed-urls' ) && ! empty( $as3cf->get_setting( 'signed-urls-object-prefix' ) );
	}

	/**
	 * Get object key.
	 *
	 * @see self::maybe_add_missing_files_to_the_list() for the detail.
	 *
	 * @since 3.9.6
	 *
	 * @param Media_Library_Item $s3_object An object item.
	 * @param string             $file_path File path.
	 * @param string             $size Image size.
	 *
	 * @return string
	 */
	private function get_object_key( Media_Library_Item $s3_object, $file_path, $size = 0 ) {
		if ( $this->enable_private_media() ) {
			$this->doing_files = array( $size => $file_path );
			// We use this trick to avoid S3 set the missing files as private files.
			add_filter( 'as3cf_attachment_file_paths', array( $this, 'maybe_add_missing_files_to_the_list' ) );
		}
		/**
		 * We use this method to support private mode too.
		 *
		 * @since 3.9.6
		 *
		 * @see Media_Library_Item()->key() (>=2.4)
		 */
		$key = $s3_object->key( wp_basename( $file_path ) );
		// Remove filter.
		if ( $this->doing_files ) {
			// Reset list files.
			$this->doing_files = null;
			remove_filter( 'as3cf_attachment_file_paths', array( $this, 'maybe_add_missing_files_to_the_list' ) );
		}

		return $key;
	}

	/**
	 * When enable private media,
	 * if the file is not in the list of file paths (AS3CF_Utils:get_attachment_file_paths()),
	 * S3 will set it's private size, but it's not managed fully,
	 * some other place, it will set is not private.
	 * So we use this trick to add the missing size keys.
	 *
	 * Note, we use size key smush-png2jpg-full for PNG2JPG file
	 * to remove the old PNG file after converting or converted JPG file after restoring in private folder.
	 *
	 * @since 3.9.6
	 *
	 * @param array $file_paths     List of the file paths.
	 *
	 * @return array List of file paths.
	 */
	public function maybe_add_missing_files_to_the_list( $file_paths ) {
		/**
		 * Get the full size key.
		 * From S3 2.6, they changed the full size key.
		 *
		 * @since 3.9.10
		 */
		$full_size_key = is_callable( array( '\DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item', 'primary_object_key' ) ) ? Media_Library_Item::primary_object_key() : 'original';
		// Make sure exits the main file, not original file, and activating backup.
		if (
			isset( $file_paths[ $full_size_key ] )
			&& $this->doing_files
			&& $this->enable_private_media()
			&& WP_Smush::get_instance()->core()->mod->backup->is_active()
		) {
			foreach ( $this->doing_files as $size => $file ) {
				if ( 'smush-png2jpg-full' === $size ) {
					if ( isset( $file_paths['file'] ) && $file_paths['file'] === $file ) {
						unset( $file_paths['file'] );
					}
					$file_paths[''] = $file;
				} elseif ( is_string( $size ) ) {
					$file_paths[ $size ] = $file;
				} elseif ( ! in_array( $file, $file_paths, true ) ) {
					$file_paths[ 'smush_missing_key_' . basename( $file ) ] = $file;
				}
			}
		}

		return $file_paths;
	}

	/** End Private Media */

	/**************************************
	 *
	 * PRIVATE CLASSES
	 */

	/**
	 * Return true if S3 is activated.
	 *
	 * @since 3.9.6
	 *
	 * @return bool
	 */
	private function is_active() {
		static $is_active;
		if ( is_null( $is_active ) ) {
			$is_active = $this->enabled && $this->settings->get( $this->module ) && WP_Smush::is_pro();
		}
		return $is_active;
	}

	/**
	 * If enabling "Remove Files From Server",
	 * save all downloaded files to remove them later.
	 *
	 * @since 3.9.6
	 *
	 * @param string $file_path File path.
	 */
	private function add_file_to_remove( $file_path ) {
		global $as3cf;
		if ( $as3cf && $as3cf->get_setting( 'remove-local-file' ) ) {
			$this->files_to_remove[] = $file_path;
		}
	}

	/**
	 * Return unfiltered path.
	 *
	 * @since 3.9.6
	 *
	 * @param int    $attachment_id  Attachment ID.
	 * @param string $type           false|original|smush|backup|resize
	 *
	 * $type = original|backup => Try to get the original image file if it's available.
	 * $type = smush           => Get the file path ( if it exists ), or filtered file path if it doesn't exist.
	 * $type = original        => Only get the file path.
	 * $type = false           => Get the file path base on the setting "compress original".
	 *
	 * @see Helper::get_raw_attached_file()
	 *
	 * @return false|string
	 */
	private function get_raw_attached_file( $attachment_id, $type = false ) {
		/**
		 * S3 works with unfiltered path.
		 *
		 * @see AS3CF_Utils:get_attachment_file_paths() (>=1.2) and $as3cf->get_attachment_file_paths() (<1.2)
		 */
		// Temporary disable filters of get_attached_file.
		$this->temp_disable_s3_file_filter();
		// Get unfiltered file path.
		$file_path = Helper::get_raw_attached_file( $attachment_id, $type, true );
		// Revert S3 filters.
		$this->revert_s3_file_filter();

		return $file_path;
	}

	/**
	 * Get the main file filter class of S3.
	 * Before 2.6.0 it's $as3cf,
	 * From 2.6.0 it's \DeliciousBrains\WP_Offload_Media\Integrations\Media_Library_Integration
	 *
	 * @return false|object False or class instance.
	 */
	private function get_s3_filter_class() {
		static $s3_filter_obj;

		if ( isset( $s3_filter_obj ) ) {
			return $s3_filter_obj;
		}

		global $as3cf;

		if ( ! is_object( $as3cf ) ) {
			return false;
		}

		if ( method_exists( $as3cf, 'get_attached_file' ) ) {
			return $as3cf;
		}

		$s3_filter_obj = false;
		if ( method_exists( $as3cf, 'get_integration_manager' ) ) {
			if ( method_exists( $as3cf->get_integration_manager(), 'get_integration' ) ) {
				$media_library = $as3cf->get_integration_manager()->get_integration( 'mlib' );
				if ( method_exists( $media_library, 'get_attached_file' ) ) {
					$s3_filter_obj = $media_library;
				} else {
					Helper::logger()->integrations()->error( 'S3 - Media_Lib->get_attached_file does not exists.' );
				}
			}
		}

		return $s3_filter_obj;
	}

	/**
	 * Temporary disable S3 get_attached_file filter.
	 *
	 * @since 3.9.8
	 */
	private function temp_disable_s3_file_filter() {
		$s3_filter_obj = $this->get_s3_filter_class();
		if ( $s3_filter_obj ) {
			// Temporary disable filters URL from S3.
			remove_filter( 'get_attached_file', array( $s3_filter_obj, 'get_attached_file' ), 10, 2 );
			remove_filter( 'wp_get_original_image_path', array( $s3_filter_obj, 'get_attached_file' ), 10, 2 );
			return;
		}
		global $wp_filter;
		// Temporary disable all file filters.
		if ( isset( $wp_filter['get_attached_file'] ) ) {
			// Cache file filters.
			$this->list_file_filters['get_attached_file'] = $wp_filter['get_attached_file'];
			unset( $wp_filter['get_attached_file'] );

			if ( isset( $wp_filter['wp_get_original_image_path'] ) ) {
				$this->list_file_filters['get_attached_file'] = $wp_filter['wp_get_original_image_path'];
				unset( $wp_filter['wp_get_original_image_path'] );
			}
		}
	}

	/**
	 * Revert S3 get_attached_file filter.
	 *
	 * @since 3.9.8
	 */
	private function revert_s3_file_filter() {
		$s3_filter_obj = $this->get_s3_filter_class();
		if ( $s3_filter_obj ) {
			// Revert filters URL of S3.
			add_filter( 'get_attached_file', array( $s3_filter_obj, 'get_attached_file' ), 10, 2 );
			add_filter( 'wp_get_original_image_path', array( $s3_filter_obj, 'get_attached_file' ), 10, 2 );
			return;
		}
		// Maybe revert file filters.
		if ( $this->list_file_filters ) {
			global $wp_filter;
			$wp_filter['get_attached_file'] = $this->list_file_filters['get_attached_file'];
			if ( isset( $this->list_file_filters['wp_get_original_image_path'] ) ) {
				$wp_filter['wp_get_original_image_path'] = $this->list_file_filters['wp_get_original_image_path'];
			}
		}
	}

	/**
	 * Download a specified file to local server with respect to provided attachment id
	 * and/or Attachment path.
	 *
	 * @param string $file_path   Full file path.
	 * @param int    $attachment_id  Attachment ID.
	 *
	 * @since 3.9.6
	 * We use AS3CF_Plugin_Compatibility()->legacy_copy_back_to_local
	 * to download the file to avoid the new change and and support private mode too.
	 *
	 * @return bool|string  Returns file path or false
	 */
	private function download_file( $file_path, $attachment_id ) {
		if ( ! $this->is_active() || empty( $file_path ) || isset( $this->files_download_failed[ $file_path ] ) ) {
			return false;
		}

		/**
		 * Amazon_S3_And_CloudFront global.
		 *
		 * @var Amazon_S3_And_CloudFront $as3cf
		 */
		global $as3cf;

		// Check if the file exists on the server.
		if ( file_exists( $file_path ) ) {
			return $file_path;
		}

		if ( ! isset( $as3cf->plugin_compat ) || ! method_exists( $as3cf->plugin_compat, 'legacy_copy_back_to_local' ) ) {
			Helper::logger()->integrations()->error( 'S3 - Method $as3cf->plugin_compat->legacy_copy_back_to_local() does not exists.' );
			return false;
		}

		$as3cf_item = $this->is_attachment_served_by_provider( $as3cf, $attachment_id );
		if ( ! $as3cf_item ) {
			return false;
		}

		$file = false;

		// Enable "copy file back to local", priority is 9999 > Smush mode 9998.
		add_filter( 'as3cf_get_attached_file_copy_back_to_local', '__return_true', 9999 );
		/**
		 * Download file back to local.
		 *
		 * @since 3.9.6
		 *
		 * We use this way to download the file to avoid the new change, and support private mode too.
		 *
		 * @see AS3CF_Plugin_Compatibility()->legacy_copy_back_to_local (>=1.x)
		 * @see Media_Library_Item()->key() (>= 2.4)
		 *
		 * We set the default URL as 0 just for debugging purposes.
		 */
		$file = $as3cf->plugin_compat->legacy_copy_back_to_local( 0, $file_path, $attachment_id, $as3cf_item );
		/**
		 * If there is a not found image, and if we don't check it exists before downloading it,
		 * then S3 will save the current error log as an error image the same as the provided path.
		 * So we need to delete it to avoid the error.
		 *
		 * @since 3.9.6
		 */
		if ( ! $file ) {
			// Cache the result to avoid downloading it again.
			$this->files_download_failed[ $file_path ] = $file;
			if ( file_exists( $file_path ) ) {
				unlink( $file_path );
			}
		}

		// Restore "copy file back to local" status.
		remove_filter( 'as3cf_get_attached_file_copy_back_to_local', '__return_true', 9999 );

		// If we don't have the file, Try it the basic way.
		if ( ! $file ) {
			$s3_url = $this->is_image_on_s3( $attachment_id );

			// If we couldn't get the image URL, return false.
			if ( is_wp_error( $s3_url ) || empty( $s3_url ) ) {
				return false;
			}

			// Make sure function download_url available.
			if ( ! function_exists( 'download_url' ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
			}

			// Get the File path using basename for given attachment path.
			$s3_url = str_replace( wp_basename( $s3_url ), wp_basename( $file_path ), $s3_url );

			// Download the file.
			$temp_file = download_url( $s3_url );
			$renamed   = false;
			if ( ! is_wp_error( $temp_file ) ) {
				$renamed = copy( $temp_file, $file_path );
				unlink( $temp_file );
			} else {
				Helper::logger()->integrations()->error( 'S3 - Cannot download file [%s] due to error: %s', Helper::clean_file_path( $file_path ), $temp_file->get_error_message() );
			}

			// If we were able to successfully rename the file, return file path.
			if ( $renamed ) {
				// The file was downloaded, so remove it from the cached.
				if ( isset( $this->files_download_failed[ $file_path ] ) ) {
					unset( $this->files_download_failed[ $file_path ] );
				}
				$file = $file_path;
			}
		}

		// Save all downloaded files to remove them later.
		$this->add_file_to_remove( $file_path );

		return $file;
	}

	/**
	 * Check if file exists for the given path
	 *
	 * @param string $attachment_id  Attachment ID.
	 * @param string $file_path      File path.
	 *
	 * @return bool
	 */
	private function does_image_exists( $attachment_id, $file_path ) {
		/**
		 * Amazon_S3_And_CloudFront global.
		 *
		 * @var Amazon_S3_And_CloudFront $as3cf
		 */
		global $as3cf;

		if ( empty( $attachment_id ) || empty( $file_path ) ) {
			return false;
		}

		// Get s3 object for the file.
		if ( ! $s3_object = $this->is_attachment_served_by_provider( $as3cf, $attachment_id ) ) {
			return false;
		}

		// Get file key.
		$is_object = is_object( $s3_object );
		if ( $is_object && $s3_object instanceof Media_Library_Item && method_exists( $s3_object, 'key' ) ) {
			$key = $this->get_object_key( $s3_object, $file_path );
		} else {
			// Try with the old version.
			if ( $is_object ) {
				$key = $s3_object->path();
			} else {
				$key = $s3_object['key'];
			}

			$size_prefix      = dirname( $key );
			$size_file_prefix = ( '.' === $size_prefix ) ? '' : $size_prefix . '/';

			// Get the File path using basename for given attachment path.
			$key = path_join( $size_file_prefix, wp_basename( $file_path ) );
		}

		$bucket   = $as3cf->get_setting( 'bucket' );
		$s3client = $this->get_provider_client();
		if ( ! $s3client ) {
			Helper::logger()->integrations()->error( 'S3 - Provider client does not exists.' );
			return false;
		}

		// If we still have the older version of S3 Offload, use old method.
		if ( method_exists( $s3client, 'does_object_exist' ) ) {
			$file_exists = $s3client->does_object_exist( $bucket, $key );
		} elseif ( method_exists( $s3client, 'doesObjectExist' ) ) {
			$file_exists = $s3client->doesObjectExist( $bucket, $key );
		} else {
			Helper::logger()->integrations()->error( 'S3 - Method AWS_Provider->does_object_exist does not exist.' );
			$file_exists = false;
		}

		return $file_exists;
	}

	/**
	 * Check if S3 support is required for Smush.
	 *
	 * @return bool
	 */
	private function s3_support_required() {
		/**
		 * Amazon_S3_And_CloudFront global.
		 *
		 * @var Amazon_S3_And_CloudFront $as3cf
		 */
		global $as3cf;

		// Check if S3 offload plugin is active.
		if ( ! is_object( $as3cf ) || ! method_exists( $as3cf, 'get_setting' ) ) {
			return false;
		}

		// If not Pro user or S3 support is disabled.
		return ( ! WP_Smush::is_pro() || ! $this->settings->get( $this->module ) );
	}

	/**
	 * Wrapper method.
	 *
	 * Check if the attachment is server by S3.
	 *
	 * @since 3.0
	 *
	 * @param Amazon_S3_And_CloudFront $as3cf          Amazon_S3_And_CloudFront global.
	 * @param int                      $attachment_id  Attachment ID.
	 *
	 * @return bool|array|Media_Library_Item Version < 2.3 Returns an array, >= 2.3 Media_Library_Item
	 */
	private function is_attachment_served_by_provider( $as3cf, $attachment_id ) {
		if ( ! $as3cf ) {
			return false;
		}
		// Version >= 2.0.0.
		if ( method_exists( $as3cf, 'is_attachment_served_by_provider' ) ) {
			return $as3cf->is_attachment_served_by_provider( $attachment_id, true );
		} elseif ( method_exists( $as3cf, 'is_attachment_served_by_s3' ) ) {
			// Version < 2.0.0.
			return $as3cf->is_attachment_served_by_s3( $attachment_id, true );
		} else {
			Helper::logger()->integrations()->error( 'S3 - Method $as3cf->is_attachment_served_by_provider() does not exists.' );
		}

		return false;
	}

	/**
	 * Wrapper method.
	 *
	 * Copy file to server.
	 *
	 * @since 3.0
	 *
	 * @param Amazon_S3_And_CloudFront $as3cf         Amazon_S3_And_CloudFront global.
	 * @param array|object             $s3_object     Data array.
	 * @param string                   $uf_file_path  File path.
	 *
	 * @return bool|string
	 */
	private function copy_provider_file_to_server( $as3cf, $s3_object, $uf_file_path ) {
		if ( ! is_object( $as3cf->plugin_compat ) ) {
			return false;
		}

		if ( method_exists( $as3cf->plugin_compat, 'copy_provider_file_to_server' ) ) {
			return $as3cf->plugin_compat->copy_provider_file_to_server( $s3_object, $uf_file_path );
		} elseif ( method_exists( $as3cf->plugin_compat, 'copy_s3_file_to_server' ) ) {
			return $as3cf->plugin_compat->copy_s3_file_to_server( $s3_object, $uf_file_path );
		} else {
			Helper::logger()->integrations()->error( 'S3 - Method $as3cf->plugin_compat->copy_provider_file_to_server() does not exists.' );
		}

		return false;
	}

	/**
	 * Wrapper method.
	 *
	 * Get provider client.
	 *
	 * @since 3.0
	 *
	 * @return \Provider|\Null_Provider|bool
	 * @throws \Exception Exception.
	 */
	private function get_provider_client() {
		global $as3cf;
		// Get bucket details.
		$region = $as3cf->get_setting( 'region' );
		if ( is_wp_error( $region ) ) {
			Helper::logger()->integrations()->error( 'S3 - Cannot retrieve the region: %s', $region->get_error_message() );
			$region = '';
		}
		if ( method_exists( $as3cf, 'get_provider_client' ) ) {
			return $as3cf->get_provider_client( $region );
		} elseif ( method_exists( $as3cf, 'get_s3client' ) ) {
			return $as3cf->get_s3client( $region );
		} else {
			Helper::logger()->integrations()->error( 'S3 - Method $as3cf->get_provider_client() does not exists.' );
		}

		return false;
	}

	/**
	 * Update the original source path after converting PNG to JPG.
	 *
	 * @since 3.9.6
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $old_filepath  Old PNG file path.
	 * @param string $new_filename  New file name in JPG.
	 * @param string $size          Image size name.
	 */
	public function update_original_source_path_after_png2jpg( $attachment_id, $old_filepath, $new_filename, $size ) {
		if ( 'full' === $size ) {
			$this->update_original_source_path( $attachment_id, $new_filename, $old_filepath );
		}
	}

	/**
	 * Update the original source path after restoring JPG to PNG.
	 *
	 * @since 3.9.6
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $old_filepath  Old JPG file path.
	 * @param string $new_filepath  Restored file path - New PNG file path.
	 */
	public function update_original_source_path_after_restore_png( $attachment_id, $old_filepath, $new_filepath ) {
		/**
		 * After restore we also need to update the source path,
		 * to make sure the JPG file does not exists and avoid unique file name issue.
		 * $is_restoring = true.
		 */
		$this->update_original_source_path( $attachment_id, $new_filepath, $old_filepath, true );
	}

	/**
	 * Update the original source path after changing the file format (PNG<->JPG).
	 *
	 * @since 3.9.6
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $new_file      New file path/name.
	 * @param string $old_filepath  Old file path (origin PNG| converted JPG).
	 * @param bool   $is_restoring  Is restoring or converting PNG2JPG.
	 */
	private function update_original_source_path( $attachment_id, $new_file, $old_filepath, $is_restoring = false ) {
		global $as3cf;
		$as3cf_item = $this->is_attachment_served_by_provider( $as3cf, $attachment_id );
		if ( ! ( $as3cf_item && is_object( $as3cf_item ) && $as3cf_item instanceof Media_Library_Item && method_exists( $as3cf_item, 'key' ) && method_exists( $as3cf_item, 'is_private' ) ) ) {
			Helper::logger()->integrations()->warning( 'S3 - Empty $as3cf_item or Media_Library_Item->is_private does not exist.' );
			return false;
		}
		// If user enabling remove file on local, we will remove all our old PNG/JPG files from list of file paths to avoid error log.
		if ( $as3cf && $as3cf->get_setting( 'remove-local-file' ) ) {
			// We don't remove the filter because it might be called later.
			add_filter( 'as3cf_upload_attachment_local_files_to_remove', array( $this, 'remove_missing_files_to_avoid_error_log_from_s3' ), 99 );
		}

		$extra_info = array();
		if ( method_exists( $as3cf_item, 'extra_info' ) ) {
			$extra_info = $as3cf_item->extra_info();
		}
		/**
		 * Remove backup key from extra info.
		 * From S3 2.6, they re-build the upload files base on extra info,
		 * so we also need to remove the backup file from this data.
		 *
		 * @since 3.9.10
		 */
		if ( $is_restoring && isset( $extra_info['objects']['smush-full'] ) ) {
			unset( $extra_info['objects']['smush-full'] );
		}

		$new_filename = basename( $new_file );
		$as3cf_item   = new Media_Library_Item(
			$as3cf_item->provider(),
			$as3cf_item->region(),
			$as3cf_item->bucket(),
			path_join( dirname( $as3cf_item->path() ), $new_filename ),
			$as3cf_item->is_private(),
			$as3cf_item->source_id(),
			path_join( dirname( $as3cf_item->source_path() ), $new_filename ),
			$new_filename,
			$extra_info,
			$as3cf_item->id()
		);

		$as3cf_item->save();

		// If enable private media, try to delete old PNG file of PNG2JPG.
		if ( $this->enable_private_media() ) {
			$backup = WP_Smush::get_instance()->core()->mod->backup;
			if ( $as3cf_item->is_private() ) {
				if ( $is_restoring ) {
					// Remove the original PNG file as a backup file in public folder.
					if ( $as3cf_item->key() !== $as3cf_item->path() && $backup->is_active() ) {
						$object_key = path_join( dirname( $as3cf_item->path() ), basename( $new_file ) );
					}
				} else {
					// Remove the original PNG file in private folder.
					$object_key = path_join( dirname( $as3cf_item->key() ), basename( $old_filepath ) );
				}
			} elseif ( $backup->is_active() && ( method_exists( $as3cf_item, 'is_private_size' ) && $as3cf_item->is_private_size( 'smush-full' ) || $as3cf_item->is_private( 'smush-full' ) ) ) {
				if ( method_exists( $as3cf_item, 'private_prefix' ) && method_exists( $as3cf_item, 'normalized_path_dir' ) ) {
					/**
					 * Remove old PNG file when user enable private media for backup size,
					 * and doesn't activate the image yet.
					 *
					 * E.g:
					 * function smush_as3cf_upload_acl_sizes( $acl, $size, $post_id, $metadata ) {
					 *  // Enable private for smush backup size.
					 *  if ( 'smush-full' === $size ) {
					 *      return 'private';
					 *  }

					*  return $acl;
					* }
					* add_filter( 'as3cf_upload_acl_sizes', 'smush_as3cf_upload_acl_sizes', 10, 4 );
					*/

					if ( $is_restoring ) {
						// Remove the original PNG file as a backup file in private folder.
						$object_key = $as3cf_item->private_prefix() . $as3cf_item->normalized_path_dir() . basename( $new_file );
					} else {
						// Remove the original PNG file in public folder after converting to JPG.
						$object_key = $as3cf_item->key( basename( $old_filepath ) );
					}
				} else {
					Helper::logger()->integrations()->error( 'S3 - Method $as3cf->private_prefix() or $as3cf->normalized_path_dir() does not exists.' );
				}
			}

			// Delete old PNG file.
			if ( isset( $object_key ) ) {
				$objects_to_remove[] = array(
					'Key' => $object_key,
				);
				$this->delete_objects( $objects_to_remove );
			}
		}
	}

}