filesystem = \Imagify_Filesystem::get_instance(); } /** * Launch the hooks. * * @since 1.6.6 * @author Grégory Viguier */ public function init() { static $done = false; if ( $done ) { return; } $done = true; /** * WebP images to display with a tag. */ add_action( 'as3cf_init', [ $this, 'store_s3_settings' ] ); add_filter( 'imagify_webp_picture_process_image', [ $this, 'picture_tag_webp_image' ] ); /** * Register CDN. */ add_filter( 'imagify_cdn', [ $this, 'register_cdn' ], 8, 3 ); /** * Optimization process. */ add_filter( 'imagify_before_optimize_size', [ $this, 'maybe_copy_file_from_cdn_before_optimization' ], 8, 6 ); add_action( 'imagify_after_optimize', [ $this, 'maybe_send_media_to_cdn_after_optimization' ], 8, 2 ); /** * Restoration process. */ add_action( 'imagify_after_restore_media', [ $this, 'maybe_send_media_to_cdn_after_restore' ], 8, 4 ); /** * WebP support. */ add_filter( 'as3cf_attachment_file_paths', [ $this, 'add_webp_images_to_attachment' ], 8, 3 ); add_filter( 'mime_types', [ $this, 'add_webp_support' ] ); /** * Redirections. */ add_filter( 'imagify_redirect_to', [ $this, 'redirect_referrer' ] ); /** * Stats. */ add_filter( 'imagify_total_attachment_filesize', [ $this, 'add_stats_for_s3_files' ], 8, 4 ); } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION PROCESS ==================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * On AS3CF init, store its settings. * * @since 1.9 * @access public * @author Grégory Viguier * * @param \Amazon_S3_And_CloudFront $as3cf AS3CF’s main instance. */ public function store_s3_settings( $as3cf ) { if ( method_exists( $as3cf, 'get_settings' ) ) { $this->store_s3_settings = (array) $as3cf->get_settings(); } } /** * WebP images to display with a tag. * * @since 1.9 * @see \Imagify\Picture\Display->process_image() * @author Grégory Viguier * * @param array $data An array of data for this image. * @return array */ public function picture_tag_webp_image( $data ) { global $wpdb; if ( ! empty( $data['src']['webp_path'] ) ) { // The file is local. return $data; } $match = $this->is_s3_url( $data['src']['url'] ); if ( ! $match ) { // The file is not on S3. return $data; } // Get the image ID. $post_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value = %s", // We use only year/month + filename, we should not have any subdir between them for the main file. $match['year_month'] . $match['filename'] ) ); if ( $post_id <= 0 ) { // Not in the database. return $data; } $s3_info = get_post_meta( $post_id, 'amazonS3_info', true ); $imagify_data = get_post_meta( $post_id, '_imagify_data', true ); if ( ! $s3_info || ! $imagify_data ) { return $data; } $webp_size_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::WEBP_SUFFIX' ); $webp_size_name = 'full' . $webp_size_suffix; if ( ! empty( $imagify_data['sizes'][ $webp_size_name ]['success'] ) ) { // We have a WebP image. $data['src']['webp_exists'] = true; } if ( empty( $data['srcset'] ) ) { return $data; } $meta_data = get_post_meta( $post_id, '_wp_attachment_metadata', true ); if ( empty( $meta_data['sizes'] ) ) { return $data; } // Ease the search for corresponding file name. $size_files = []; foreach ( $meta_data['sizes'] as $size_name => $size_data ) { $size_files[ $size_data['file'] ] = $size_name; } // Look for a corresponding size name. foreach ( $data['srcset'] as $i => $srcset_data ) { if ( empty( $srcset_data['webp_url'] ) ) { // Not a supported image format. continue; } if ( ! empty( $srcset_data['webp_path'] ) ) { // The file is local. continue; } $match = $this->is_s3_url( $srcset_data['url'] ); if ( ! $match ) { // Not on S3. continue; } // Try with no subdirs. $filename = $match['filename']; if ( isset( $size_files[ $filename ] ) ) { $size_name = $size_files[ $filename ]; } else { // Try with subdirs. $filename = $match['subdirs'] . $match['filename']; if ( isset( $size_files[ $filename ] ) ) { $size_name = $size_files[ $filename ]; } elseif ( preg_match( '@/\d+/$@', $match['subdirs'] ) ) { // Last try: the subdirs may contain the S3 versioning. If not the case, we can still build a pyramid with this code. $filename = preg_replace( '@/\d+/$@', '/', $match['subdirs'] ) . $match['filename']; if ( isset( $size_files[ $filename ] ) ) { $size_name = $size_files[ $filename ]; } else { continue; } } } $webp_size_name = $size_name . $webp_size_suffix; if ( ! empty( $imagify_data['sizes'][ $webp_size_name ]['success'] ) ) { // We have a WebP image. $data['srcset'][ $i ]['webp_exists'] = true; } } return $data; } /** * The CDN to use for this media. * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool|PushCDNInterface $cdn A PushCDNInterface instance. False if no CDN is used. * @param int $media_id The media ID. * @param ContextInterface $context The context object. */ public function register_cdn( $cdn, $media_id, $context ) { if ( 'wp' !== $context->get_name() ) { return $cdn; } if ( $cdn instanceof PushCDNInterface ) { return $cdn; } return new CDN( $media_id ); } /** * Before performing a file optimization, download the file from the CDN if it is missing. * * @since 1.9 * @access public * @author Grégory Viguier * * @param null|\WP_Error $response Null by default. * @param ProcessInterface $process The optimization process instance. * @param File $file The file instance. If $webp is true, $file references the non-webp file. * @param string $thumb_size The media size. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * @param bool $webp The image will be converted to WebP. * @return null|\WP_Error Null. A \WP_Error object on error. */ public function maybe_copy_file_from_cdn_before_optimization( $response, $process, $file, $thumb_size, $optimization_level, $webp ) { if ( is_wp_error( $response ) || 'wp' !== $process->get_media()->get_context() ) { return $response; } $media = $process->get_media(); $cdn = $media->get_cdn(); if ( ! $cdn instanceof CDN ) { return $response; } if ( $this->filesystem->exists( $file->get_path() ) ) { return $response; } // Get files from the CDN. $result = $cdn->get_files_from_cdn( [ $file->get_path() ] ); if ( is_wp_error( $result ) ) { return $result; } return $response; } /** * After performing a media optimization: * - Save some data, * - Upload the files to the CDN, * - Maybe delete them from the server. * * @since 1.9 * @access public * @author Grégory Viguier * * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. */ public function maybe_send_media_to_cdn_after_optimization( $process, $item ) { if ( 'wp' !== $process->get_media()->get_context() ) { return; } $media = $process->get_media(); $cdn = $media->get_cdn(); if ( ! $cdn instanceof CDN ) { return; } $cdn->send_to_cdn( ! empty( $item['data']['is_new_upload'] ) ); } /** * After restoring a media: * - Save some data, * - Upload the files to the CDN, * - Maybe delete WebP files from the CDN. * * @since 1.9 * @access public * @author Grégory Viguier * * @param ProcessInterface $process The optimization process. * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure. * @param array $files The list of files, before restoring them. * @param array $data The optimization data, before deleting it. */ public function maybe_send_media_to_cdn_after_restore( $process, $response, $files, $data ) { if ( 'wp' !== $process->get_media()->get_context() ) { return; } $media = $process->get_media(); $cdn = $media->get_cdn(); if ( ! $cdn instanceof CDN ) { return; } if ( is_wp_error( $response ) ) { $error_code = $response->get_error_code(); if ( 'copy_failed' === $error_code ) { // No files have been restored. return; } // No thumbnails left? } $cdn->send_to_cdn( false ); // Remove WebP files from CDN. $webp_files = []; if ( $files ) { // Get the paths to the WebP files. foreach ( $files as $size_name => $file ) { $webp_size_name = $size_name . $process::WEBP_SUFFIX; if ( empty( $data['sizes'][ $webp_size_name ]['success'] ) ) { // This size has no WebP version. continue; } if ( 0 === strpos( $file['mime-type'], 'image/' ) ) { $webp_file = new File( $file['path'] ); if ( ! $webp_file->is_webp() ) { $webp_files[] = $webp_file->get_path_to_webp(); } } } } if ( $webp_files ) { $cdn->remove_files_from_cdn( $webp_files ); } } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION PROCESS ==================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Add the WebP files to the list of files that the CDN must handle. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $paths A list of file paths, keyed by size name. 'file' for the full size. Includes a 'backup' size and a 'thumb' size. * @param int $attachment_id The media ID. * @param array $metadata The attachment meta data. * @return array */ public function add_webp_images_to_attachment( $paths, $attachment_id, $metadata ) { if ( ! $paths ) { // ¯\(°_o)/¯. return $paths; } $process = imagify_get_optimization_process( $attachment_id, 'wp' ); if ( ! $process->is_valid() ) { return $paths; } $media = $process->get_media(); if ( ! $media->is_image() ) { return $paths; } // Use the optimization data (the files may not be on the server). $data = $process->get_data()->get_optimization_data(); if ( empty( $data['sizes'] ) ) { return $paths; } foreach ( $paths as $size_name => $file_path ) { if ( 'thumb' === $size_name || 'backup' === $size_name || $process->is_size_next_gen( $size_name ) ) { continue; } if ( 'file' === $size_name ) { $size_name = 'full'; } $webp_size_name = $size_name . $process::WEBP_SUFFIX; if ( empty( $data['sizes'][ $webp_size_name ]['success'] ) ) { // This size has no WebP version. continue; } $file = new File( $file_path ); if ( ! $file->is_webp() ) { $paths[ $webp_size_name ] = $file->get_path_to_webp(); } } return $paths; } /** * Add WebP format to the list of allowed mime types. * * @since 1.9 * @access public * @see get_allowed_mime_types() * @author Grégory Viguier * * @param array $mime_types A list of mime types. * @return array */ public function add_webp_support( $mime_types ) { $mime_types['webp'] = 'image/webp'; return $mime_types; } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS HOOKS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * After a non-ajax optimization, remove some unnecessary arguments from the referrer used for the redirection. * Those arguments don't break anything, they're just not relevant and display obsolete admin notices. * * @since 1.6.10 * @author Grégory Viguier * * @param string $redirect The URL to redirect to. * @return string */ public function redirect_referrer( $redirect ) { return remove_query_arg( [ 'as3cfpro-action', 'as3cf_id', 'errors', 'count' ], $redirect ); } /** * Provide the file sizes and the number of thumbnails for files that are only on S3. * * @since 1.6.7 * @author Grégory Viguier * * @param bool $size_and_count False by default. * @param int $image_id The attachment ID. * @param array $files An array of file paths with thumbnail sizes as keys. * @param array $image_ids An array of all attachment IDs. * @return bool|array False by default. Provide an array with the keys 'filesize' (containing the total filesize) and 'thumbnails' (containing the number of thumbnails). */ public function add_stats_for_s3_files( $size_and_count, $image_id, $files, $image_ids ) { static $data; if ( is_array( $size_and_count ) ) { return $size_and_count; } if ( $this->filesystem->exists( $files['full'] ) ) { // If the full size is on the server, that probably means all files are on the server too. return $size_and_count; } if ( ! isset( $data ) ) { $data = \Imagify_DB::get_metas( [ // Get the filesizes. 's3_filesize' => 'wpos3_filesize_total', ], $image_ids ); $data = array_map( 'absint', $data['s3_filesize'] ); } if ( empty( $data[ $image_id ] ) ) { // The file is not on S3. return $size_and_count; } // We can't take the disallowed sizes into account here. return [ 'filesize' => (int) $data[ $image_id ], 'thumbnails' => count( $files ) - 1, ]; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if an URL is a S3 one. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $url The URL to test. * @return array|bool { * An array if an S3 URL. False otherwise. * * @type string $key Bucket key. Ex: subdir/wp-content/uploads/2019/02/13142432/foobar-480x510.jpg. * @type string $year_month The uploads year/month folders. Ex: 2019/02/. * @type string $subdirs Sub-directories between year/month folders and the filename. * It can be the S3 versioning folder, any folder added by a plugin, or both. * There is no way to know which one it is. Ex: foo/13142432/. * @type string $filename The file name. Ex: foobar-480x510.jpg. * } */ public function is_s3_url( $url ) { static $uploads_dir; static $domain; /** * Tell if an URL is a S3 one. * * @since 1.9 * @author Grégory Viguier * * @param null|array|bool $is Null by default. Must return an array if an S3 URL, or false if not. * @param string $url The URL to test. */ $is = apply_filters( 'imagify_as3cf_is_s3_url', null, $url ); if ( false === $is ) { return false; } if ( is_array( $is ) ) { return imagify_merge_intersect( $is, [ 'key' => '', 'year_month' => '', 'subdirs' => '', 'filename' => '', ] ); } if ( ! isset( $uploads_dir ) ) { $uploads_dir = wp_parse_url( $this->filesystem->get_upload_baseurl() ); $uploads_dir = trim( $uploads_dir['path'], '/' ) . '/'; } if ( ! isset( $domain ) ) { if ( ! empty( $this->store_s3_settings['cloudfront'] ) ) { $domain = sanitize_text_field( $this->store_s3_settings['cloudfront'] ); $domain = preg_replace( '@^(?:https?:)?//@', '//', $domain ); $domain = preg_quote( $domain, '@' ); } else { $domain = 's3-.+\.amazonaws\.com/[^/]+/'; } } $pattern = '@^(?:https?:)?//' . $domain . '/(?' . $uploads_dir . '(?\d{4}/\d{2}/)?(?.+/)?(?[^/]+))$@i'; if ( ! preg_match( $pattern, $url, $match ) ) { return false; } unset( $match[0] ); return array_merge( [ 'year_month' => '', 'subdirs' => '', ], $match ); } }