OwlCyberSecurity - MANAGER
Edit File: class-s3.php
<?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 ); } } } }