_path, $convert_to_webp ); } ); return $this->handle_response( $response, $file_path, $convert_to_webp ); } public function should_retry_smush( $response ) { return WP_SMUSH_RETRY_ATTEMPTS > 0 && ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ); } /** * Takes the raw response from the API and performs all the necessary file operations etc. * * @param $response array|WP_Error * @param $file_path string * @param $convert_to_webp boolean * * @return array|WP_Error */ private function handle_response( $response, $file_path, $convert_to_webp ) { $data = $this->parse_response( $response ); if ( is_wp_error( $data ) ) { if ( $data->get_error_code() === self::ERROR_SSL_CERT ) { // Switch to http protocol. $this->settings->set_setting( 'wp-smush-use_http', 1 ); } $error_format = $convert_to_webp ? 'Cannot convert to webp for image [%s].' : 'Cannot smush image [%s].'; Helper::logger()->error( array( sprintf( $error_format, Helper::clean_file_path( $file_path ) ), $data->get_error_messages(), ) ); return $data; } $bytes_saved = empty( $data->bytes_saved ) ? 0 : $data->bytes_saved; if ( $bytes_saved > 0 ) { $this->save_smushed_image_file( $file_path, $convert_to_webp, $data->image ); } else { // No savings, just add an entry to the log Helper::logger()->notice( sprintf( 'The smushed image is larger than the original image [%s] (bytes saved %d), keep original image.', Helper::clean_file_path( $file_path ), $bytes_saved ) ); } // No need to pass image data any further $data->image = null; $data->image_md5 = null; // Check for API message and store in db. if ( ! empty( $data->api_message ) ) { $this->add_api_message( (array) $data->api_message ); } // If is_premium is set in response, send it over to check for member validity. if ( ! empty( $data->is_premium ) ) { $this->api_headers['is_premium'] = $data->is_premium; } return array( 'success' => true, 'data' => $data, ); } /** * Posts an image to Smush. * * @since 3.8.0 Added new param $convert_to_webp. * * @param string $file_path Path of file to send to Smush. * @param bool $convert_to_webp Convert the image to webp. * * @return bool|array array containing success status, and stats */ private function _post( $file_path, $convert_to_webp = false ) { // Temporary increase the limit. wp_raise_memory_limit( 'image' ); return wp_remote_post( $this->get_api_url(), $this->get_api_request_args( $file_path, $convert_to_webp ) ); } /** * @param $response array|WP_Error * * @return object|WP_Error */ private function parse_response( $response ) { if ( is_wp_error( $response ) ) { $error = $response->get_error_message(); if ( strpos( $error, 'SSL CA cert' ) !== false ) { return new WP_Error( self::ERROR_SSL_CERT, $error ); } else if ( strpos( $error, 'timed out' ) !== false ) { return new WP_Error( 'time_out', esc_html__( "Skipped due to a timeout error. You can increase the request timeout to make sure Smush has enough time to process larger files. define('WP_SMUSH_TIMEOUT', 150);", 'wp-smushit' ) ); } else { return new WP_Error( 'error_posting_to_api', /* translators: %s: Error message. */ sprintf( __( 'Error posting to API: %s', 'wp-smushit' ), $error ) ); } } if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $error = sprintf( /* translators: 1: Error code, 2: Error message. */ __( 'Error posting to API: %1$s %2$s', 'wp-smushit' ), wp_remote_retrieve_response_code( $response ), wp_remote_retrieve_response_message( $response ) ); return new WP_Error( 'non_200_response', $error ); } $json = json_decode( wp_remote_retrieve_body( $response ) ); if ( empty( $json->success ) ) { $error = ! empty( $json->data ) ? $json->data : __( "Image couldn't be smushed", 'wp-smushit' ); return new WP_Error( 'unsuccessful_smush', $error ); } if ( empty( $json->data ) ) { return new WP_Error( 'no_data', __( 'Unknown API error', 'wp-smushit' ) ); } $data = $json->data; $bytes_saved = empty( $data->bytes_saved ) ? 0 : $data->bytes_saved; $image = empty( $data->image ) ? '' : $data->image; if ( $bytes_saved > 0 && $data->image_md5 !== md5( $image ) ) { $error = __( 'Smush data corrupted, try again.', 'wp-smushit' ); return new WP_Error( 'data_corrupted', $error ); } if ( $bytes_saved > 0 && ! empty( $image ) ) { $data->image = base64_decode( $data->image ); } return $data; } /** * Replace the old API message with the latest one if it doesn't exists already * * @param array $api_message API message. */ private function add_api_message( $api_message = array() ) { if ( empty( $api_message ) || ! count( $api_message ) || empty( $api_message['timestamp'] ) || empty( $api_message['message'] ) ) { return; } $o_api_message = get_site_option( 'wp-smush-api_message', array() ); if ( array_key_exists( $api_message['timestamp'], $o_api_message ) ) { return; } $message = array(); $message[ $api_message['timestamp'] ] = array( 'message' => sanitize_text_field( $api_message['message'] ), 'type' => sanitize_text_field( $api_message['type'] ), 'status' => 'show', ); update_site_option( 'wp-smush-api_message', $message ); } /** * Fills $placeholder array with values from $data array * * @param array $placeholders Placeholders array. * @param array $data Data to fill with. * * @return array */ public function array_fill_placeholders( array $placeholders, array $data ) { $placeholders['percent'] = $data['compression']; $placeholders['bytes'] = $data['bytes_saved']; $placeholders['size_before'] = $data['before_size']; $placeholders['size_after'] = $data['after_size']; $placeholders['time'] = $data['time']; return $placeholders; } /** * Returns signature for single size of the smush api message to be saved to db; * * @return array */ public function get_size_signature() { return array( 'percent' => 0, 'bytes' => 0, 'size_before' => 0, 'size_after' => 0, 'time' => 0, ); } /** * Calculate saving percentage from existing and current stats * * @param object|string $stats Stats object. * @param object|string $existing_stats Existing stats object. * * @return float */ public function calculate_percentage( $stats = '', $existing_stats = '' ) { if ( empty( $stats ) || empty( $existing_stats ) ) { return 0; } $size_before = ! empty( $stats->size_before ) ? $stats->size_before : $existing_stats->size_before; $size_after = ! empty( $stats->size_after ) ? $stats->size_after : $existing_stats->size_after; $savings = $size_before - $size_after; if ( $savings > 0 ) { $percentage = ( $savings / $size_before ) * 100; return $percentage > 0 ? round( $percentage, 2 ) : $percentage; } return 0; } public function parallel_available() { if ( ! WP_SMUSH_PARALLEL ) { return false; } return $this->curl_multi_exec_available(); } public function curl_multi_exec_available() { if ( ! function_exists( 'curl_multi_exec' ) ) { return false; } $disabled_functions = explode( ',', ini_get( 'disable_functions' ) ); if ( in_array( 'curl_multi_exec', $disabled_functions ) ) { return false; } return true; } /** * Optimises the image sizes * * Note: Function name is a bit confusing, it is for optimisation, and calls the resizing function as well * * Read the image paths from an attachment's metadata and process each image * with wp_smushit(). * * @param int $attachment_id Image ID. * @param array $meta Image metadata. * * @return WP_Error|array */ public function resize_from_meta_data( $attachment_id, $meta ) { // Check if it's real image, and is supported. if ( ! Helper::is_smushable( $attachment_id ) ) { return $meta; } // Maybe add scaled file to the meta sizes. $meta = apply_filters( 'wp_smush_add_scaled_images_to_meta', $meta, $attachment_id ); // Flag to check, if uploaded size image should be smushed or not. $smush_uploaded = true === $this->settings->get( 'original' ); $stats = array( 'stats' => array_merge( $this->get_size_signature(), array( 'api_version' => - 1, 'lossy' => - 1, 'keep_exif' => false, ) ), 'sizes' => array(), ); // File path and URL for original image. $file_path = Helper::get_attached_file( $attachment_id );// S3+. $file_paths = array(); // If images has other registered size, smush them first. if ( ! empty( $meta['sizes'] ) && ! has_filter( 'wp_image_editors', 'photon_subsizes_override_image_editors' ) ) { $optimized_thumbs = array(); foreach ( $meta['sizes'] as $size_key => $size_data ) { // Check if registered size is supposed to be Smushed or not. if ( 'full' !== $size_key ) { if ( $this->skip_image_size( $size_key ) || isset( $optimized_thumbs[ $size_data['file'] ] ) ) { // If a thumbnail file is optimized we don't need to optimize it again. continue; } /** * Save optimized thumbnail file. * We save all cases included failure case which user can re-check images later. */ $optimized_thumbs[ $size_data['file'] ] = $size_key; } // We take the original image. The 'sizes' will all match the same URL and // path. So just get the dirname and replace the filename. $file_path_size = path_join( dirname( $file_path ), $size_data['file'] ); $ext = Helper::get_mime_type( $file_path_size ); if ( $ext && ! in_array( $ext, Core::$mime_types, true ) ) { continue; } /** * Allows to skip an image from optimization. * * @param bool $compress Optimize image or not. * @param string $size_key Size of image being smushed. * @param string $file_path_size Full thumbnail path of current size. * @param int $attachment_id Attachment ID. * * @since 3.9.6 Add two parameters for the filter. */ if ( ! apply_filters( 'wp_smush_media_image', true, $size_key, $file_path_size, $attachment_id ) ) { continue; } /** * Check if the file exists on the server, * if not, might try to download it from the cloud (s3). * * @since 3.9.6 */ if ( ! Helper::exists_or_downloaded( $file_path_size, $attachment_id ) ) { continue; } $file_paths[ $size_key ] = $file_path_size; } } elseif ( ! has_filter( 'wp_image_editors', 'photon_subsizes_override_image_editors' ) ) { $smush_uploaded = true; } /** * Allows to skip an image from optimization. * * @param bool $compress Optimize image or not. * @param string $size_key Size of image being smushed. */ $smush_full_image = apply_filters( 'wp_smush_media_image', true, 'full', $file_path, $attachment_id ); // If original size is supposed to be smushed. if ( $smush_uploaded && $smush_full_image ) { $file_paths['full'] = $file_path; } if ( $this->parallel_available() ) { $responses = $this->smush_parallel( $file_paths ); } else { $responses = $this->smush_sequential( $file_paths ); } foreach ( $responses as $size_key => $response ) { if ( is_wp_error( $response ) ) { // Logged the error inside do_smushit. return $response; } // If there are no stats or resulting image is larger than original. if ( empty( $response['data'] ) || $response['data']->after_size > $response['data']->before_size ) { continue; } // All clear, store the stat. $stats['sizes'][ $size_key ] = (object) $this->array_fill_placeholders( $this->get_size_signature(), (array) $response['data'] ); } // Make sure we have the correct API details. if ( isset( $response ) && isset( $response['data'] ) && ( empty( $stats['stats']['api_version'] ) || - 1 === $stats['stats']['api_version'] ) ) { $stats['stats']['api_version'] = $response['data']->api_version; $stats['stats']['lossy'] = $response['data']->lossy; $stats['stats']['keep_exif'] = ! empty( $response['data']->keep_exif ) ? $response['data']->keep_exif : 0; } // Set smush status for all the images, store it in wp-smpro-smush-data. $existing_stats = get_post_meta( $attachment_id, self::$smushed_meta_key, true ); if ( ! empty( $existing_stats ) ) { // Update stats for each size. if ( isset( $existing_stats['sizes'] ) && ! empty( $stats['sizes'] ) ) { foreach ( $existing_stats['sizes'] as $size_name => $size_stats ) { // If stats for a particular size doesn't exists. if ( empty( $stats['sizes'][ $size_name ] ) ) { $stats['sizes'][ $size_name ] = $existing_stats['sizes'][ $size_name ]; } else { $existing_stats_size = (object) $existing_stats['sizes'][ $size_name ]; // Store the original image size. $stats['sizes'][ $size_name ]->size_before = ( ! empty( $existing_stats_size->size_before ) && $existing_stats_size->size_before > $stats['sizes'][ $size_name ]->size_before ) ? $existing_stats_size->size_before : $stats['sizes'][ $size_name ]->size_before; // Update compression percent and bytes saved for each size. $stats['sizes'][ $size_name ]->bytes = $stats['sizes'][ $size_name ]->bytes + $existing_stats_size->bytes; $stats['sizes'][ $size_name ]->percent = $this->calculate_percentage( $stats['sizes'][ $size_name ], $existing_stats_size ); } } } // Keep WebP flag. if ( isset( $existing_stats['webp_flag'] ) ) { $stats['webp_flag'] = $existing_stats['webp_flag']; } } // Sum Up all the stats. $stats = WP_Smush::get_instance()->core()->total_compression( $stats ); // If there was any compression and there was no error during optimization. if ( isset( $stats['stats']['bytes'] ) && $stats['stats']['bytes'] >= 0 ) { /** * Runs if the image optimization was successful. * * @param int $attachment_id Image ID. * @param array $stats Smush stats for the image. * @param array $meta Attachment meta. */ do_action( 'wp_smush_image_optimised', $attachment_id, $stats, $meta ); } update_post_meta( $attachment_id, self::$smushed_meta_key, $stats ); return $meta; } /** * Fix SSL CA Certificate issue. * * @since 3.9.6 * * Check for use of http url (Hostgator mostly) - got it from smush_image. */ public function fix_ssl_ca_certificate_error() { // Return if the member defined it. if ( defined( 'WP_SMUSH_API_HTTP' ) ) { return; } static $use_http; /** * Fix for Hostgator. * Check for use of http url (Hostgator mostly). */ if ( is_null( $use_http ) ) { $use_http = $this->settings->get_setting( 'wp-smush-use_http' ); } if ( $use_http ) { define( 'WP_SMUSH_API_HTTP', 'http://smushpro.wpmudev.com/1.0/' ); } } /** * Add action when we don't smush an image, * or get any errors while smushing. * * @param int $attachment_id Attachment ID. * @param WP_Error $errors an instance of WP_Error. * @param mixed $result The data to return. * @return mixed */ private function no_smushit( $attachment_id, $errors, $result = null ) { do_action( 'wp_smush_no_smushit', $attachment_id, $errors ); return isset( $result ) ? $result : false; } /** * Smush image * * We need to detect the response status by using $ref_errors->has_errors(). * * @since 3.9.6 * * @param int $attachment_id Attachment ID. * @param array $ref_meta Original metadata (passed by reference). * @param WP_Error $ref_errors WP_Error (passed by reference). * * @return mixed Returns response data, TRUE if smushed the file, or FALSE with error(s). */ public function smushit( $attachment_id, &$ref_meta, &$ref_errors ) { // TODO: (stats refactor) handle properly $meta = $this->run_optimizer( $attachment_id ); if ( is_wp_error( $meta ) ) { $ref_errors = $meta; return false; } else { $ref_meta = $meta; return true; } /** * Prevent infinite loop when someone calls `wp_generate_attachment_metadata` inside smushit. * * By default, we already avoid it via set in-progress, * but it's better to prevent it from another attachment file from third party and issue from object cache too. */ if ( $this->prevent_infinite_loop ) { return $this->no_smushit( $attachment_id, $ref_errors ); } // Handle errors. if ( ! $ref_errors || ! is_wp_error( $ref_errors ) ) { $ref_errors = new WP_Error(); } $attachment_id = (int) $attachment_id; if ( $attachment_id < 1 ) { $ref_errors->add( 'missing_id', Error_Handler::get_error_message( 'missing_id' ), array( 'file_name' => 'undefined' ) ); return $this->no_smushit( $attachment_id, $ref_errors ); } $file_name = sprintf( /* translators: %d - attachment ID */ esc_html__( 'attachment ID: %d', 'wp-smushit' ), $attachment_id ); // Check if the file is ignored or animated. $is_ignored = (int) get_post_meta( $attachment_id, 'wp-smush-ignore-bulk', true ); if ( $is_ignored > 0 ) { $type = Core::STATUS_ANIMATED === $is_ignored ? 'animated' : 'ignored'; $ref_errors->add( $type, Error_Handler::get_error_message( $type ), array( 'file_name' => $file_name ) ); return $this->no_smushit( $attachment_id, $ref_errors ); } // Return the status if the file is in progress. if ( get_transient( 'wp-smush-restore-' . $attachment_id ) || get_transient( 'smush-in-progress-' . $attachment_id ) ) { $ref_errors->add( 'in_progress', Error_Handler::get_error_message( 'in_progress' ), array( 'file_name' => $file_name ) ); return $this->no_smushit( $attachment_id, $ref_errors ); } // Image metadata. if ( empty( $ref_meta ) ) { // We use unfiltered metadata. $ref_meta = wp_get_attachment_metadata( $attachment_id, true ); } /** * This is often not set when images are imported to the database, without properly adding the meta values. * Causes PHP Warning: Illegal string offset 'file' message. */ if ( ! is_array( $ref_meta ) || ! isset( $ref_meta['file'] ) ) { $ref_errors->add( 'no_file_meta', Error_Handler::get_error_message( 'no_file_meta' ), array( 'file_name' => $file_name ) ); return $this->no_smushit( $attachment_id, $ref_errors ); } // Try to get the file name from path. $file_name = explode( '/', $ref_meta['file'] ); $file_name = is_array( $file_name ) ? array_pop( $file_name ) : $ref_meta['file']; $file_name = Helper::get_image_media_link( $attachment_id, $file_name ); /** * Filter: wp_smush_image * * Whether to smush the given attachment id or not * * @param bool $skip Bool, whether to Smush image or not. * @param int $ID Attachment Id, Attachment id of the image being processed. */ if ( ! apply_filters( 'wp_smush_image', true, $attachment_id ) ) { $ref_errors->add( 'skipped_filter', Error_Handler::get_error_message( 'skipped_filter' ), array( 'file_name' => $file_name ) ); return $this->no_smushit( $attachment_id, $ref_errors ); } // Get the file path for backup. $file_path = Helper::get_attached_file( $attachment_id ); // S3+. // If the file doesn't exist, return. if ( ! file_exists( $file_path ) ) { $ref_errors->add( 'file_not_found', sprintf( Error_Handler::get_error_message( 'file_not_found' ), basename( $file_path ) ), array( 'file_name' => $file_name ) ); return $this->no_smushit( $attachment_id, $ref_errors ); } // Check if file is animated, return. if ( Helper::check_animated_status( $file_path, $attachment_id ) ) { $ref_errors->add( 'animated', Error_Handler::get_error_message( 'animated' ), array( 'file_name' => $file_name ) ); return $this->no_smushit( $attachment_id, $ref_errors ); } // Check file size limit. $size_exceeded = Helper::size_limit_exceeded( $attachment_id ); if ( $size_exceeded ) { $error_code = WP_Smush::is_pro() ? 'size_pro_limit' : 'size_limit'; $ref_errors->add( $error_code, sprintf( Error_Handler::get_error_message( $error_code ), size_format( $size_exceeded ) ), array( 'file_name' => $file_name ) ); return $this->no_smushit( $attachment_id, $ref_errors ); } // Set prevent_infinite_loop before adding some actions, only return after resetting this value. $this->prevent_infinite_loop = true; /** * Fires before Smushing a file. * * @param int $attachment_id Attachment ID. * @param array $ref_meta Metadata. * @param WP_Error The WP_Error object (passed by reference). * * @hooked self::fix_ssl_ca_certificate_error() * @hooked Smush\Core\Integrations\S3::activate_smush_mode() */ do_action_ref_array( 'wp_smush_before_smush_file', array( $attachment_id, $ref_meta, &$ref_errors ) ); // Only Smush if there is no error from third party. $has_error = $ref_errors->has_errors(); if ( ! $has_error ) { // Set a transient to avoid multiple request. set_transient( 'smush-in-progress-' . $attachment_id, 1, HOUR_IN_SECONDS ); // Is doing wp_generate_attachment_metadata. $generating_metadata = doing_filter( 'wp_generate_attachment_metadata' ); // Nothing to smush if that is generating metadata while disabling auto-smush. if ( $generating_metadata && ! $this->should_auto_smush( $attachment_id ) ) { // Remove stats and update cache. WP_Smush::get_instance()->core()->remove_stats( $attachment_id ); } else { // We only take backup before smushing image. WP_Smush::get_instance()->core()->mod->backup->create_backup( $file_path, $attachment_id ); // While uploading from Mobile App or other sources, admin_init action may not fire. // So we need to manually initialize those. if ( $generating_metadata ) { WP_Smush::get_instance()->core()->mod->resize->initialize( true ); } // Send image for resizing, if enabled resize first before any other operation. $updated_meta = $this->resize_image( $attachment_id, $ref_meta ); /** * Convert PNGs to JPG, it should be run with resize_image in order to retrieve the transparent status from the cache. * * @see SMUSH-1027 */ $updated_meta = WP_Smush::get_instance()->core()->mod->png2jpg->png_to_jpg( $attachment_id, $updated_meta ); $ref_meta = ! empty( $updated_meta ) ? $updated_meta : $ref_meta; // Convert to webp. $webp_files = WP_Smush::get_instance()->core()->mod->webp->convert_to_webp( $attachment_id, $ref_meta ); // Handle webp errors. if ( is_wp_error( $webp_files ) ) { $ref_errors = $webp_files; } // Smush the image. $smush = $this->resize_from_meta_data( $attachment_id, $ref_meta ); // Handle compress errors. if ( is_wp_error( $smush ) ) { // Handle WP_Error. $ref_errors->merge_from( $smush ); } } } /** * Fires after optimizing a file. * * @param int $attachment_id Attachment ID. * @param array $ref_meta Metadata. * @param WP_Error The WP_Error object (passed by reference). * * @hooked Smush\Core\Integrations\S3::release_smush_mode() * @hooked Smush\Core\Integrations\S3::maybe_remove_sizes_from_s3_upload() */ do_action_ref_array( 'wp_smush_after_smush_file', array( $attachment_id, $ref_meta, &$ref_errors ) ); // Maybe update metadata after smushing image. $has_error = $has_error || $ref_errors && is_wp_error( $ref_errors ) && $ref_errors->has_errors(); // Update the metadata if there are no errors or converted PNG2JPG or resized image. if ( isset( $generating_metadata ) && ! $generating_metadata && ( ! $has_error || did_action( 'wp_smush_png_jpg_converted' ) || did_action( 'wp_smush_image_resized' ) ) ) { Helper::wp_update_attachment_metadata( $attachment_id, $ref_meta ); } // Log all errors. if ( $has_error ) { Helper::logger()->error( $ref_errors->errors ); } // Delete the transient after attachment meta is updated. delete_transient( 'smush-in-progress-' . $attachment_id ); // Reset prevent infinite loop. $this->prevent_infinite_loop = null; return $has_error ? $this->no_smushit( $attachment_id, $ref_errors ) : true; } /** * Read the image paths from an attachment's metadata and process each image with wp_smushit(). * * @param array $meta Attachment metadata. * @param int $id Attachment ID. * * @return mixed */ public function smush_image( $meta, $id ) { // We need to check if this call originated from Gutenberg and allow only media. if ( Helper::is_non_rest_media() ) { // If not - return image metadata. return $this->no_smushit( $id, null, $meta ); } $upload_attachment = filter_input( INPUT_POST, 'action', FILTER_SANITIZE_SPECIAL_CHARS ); $is_upload_attachment = 'upload-attachment' === $upload_attachment || isset( $_POST['post_id'] ); // Our async task runs when action is upload-attachment and post_id found. So do not run on these conditions. if ( $is_upload_attachment && defined( 'WP_SMUSH_ASYNC' ) && WP_SMUSH_ASYNC ) { return $meta; } // Is doing wp_generate_attachment_metadata. $generating_metadata = doing_filter( 'wp_generate_attachment_metadata' ); if ( $generating_metadata && ! $this->should_auto_smush( $id ) ) { // TODO: the following commented out line is here for historic reference only in case we need it again but hopefully we won't. Remove it once the version 3.13 has been out for a while. The reason why we are removing it is that it causes the test test__global_stats_updated_on_restore to fail. // Remove stats and update cache. //WP_Smush::get_instance()->core()->remove_stats( $id ); return $meta; } /** * Smush image. * * @since 3.9.6 * * @param int $id Attachment ID. * @param array $meta Image metadata (passed by reference). * @param WP_Error $errors WP_Error (passed by reference). */ $this->smushit( $id, $meta, $errors ); return $meta; } /** * Smush single images * * @param int $attachment_id Attachment ID. * @param bool $return Return/echo the stats. * * @return array|string|void */ public function smush_single( $attachment_id, $return = false ) { /** * If the smushing option is already set, return the status. * * @since 3.9.6 * If it's not in ajax we are already handled it inside self::smushit(). */ if ( ! $return && $attachment_id > 0 && ( get_transient( 'smush-in-progress-' . $attachment_id ) || get_transient( 'wp-smush-restore-' . $attachment_id ) ) ) { // Get the button status. $status = WP_Smush::get_instance()->library()->generate_markup( $attachment_id ); wp_send_json_success( $status ); } // Get the image metadata from $_POST. $original_meta = ! empty( $_POST['metadata'] ) ? Helper::format_meta_from_post( $_POST['metadata'] ) : ''; /** * Smush image. * * @since 3.9.6 * * @param int $attachment_id Attachment ID. * @param array $original_meta Image metadata (passed by reference). * @param WP_Error $errors WP_Error (passed by reference). */ $this->smushit( $attachment_id, $original_meta, $errors ); // Send JSON response if we are not supposed to return the results. if ( $errors && is_wp_error( $errors ) && $errors->has_errors() ) { if ( $return ) { return array( 'error' => $errors->get_error_message() ); } // Prepare data for ajax. $error_code = $errors->get_error_code(); $error_data = $errors->get_error_data(); $status = array( 'error' => $error_code, 'error_msg' => Helper::filter_error( $errors->get_error_message( $error_code ), $attachment_id ), 'html_stats' => WP_Smush::get_instance()->library()->generate_markup( $attachment_id ), 'show_warning' => (int) $this->show_warning(), ); // Add error data (file_name) to status. if ( $error_data && is_array( $error_data ) ) { $status = array_merge( $error_data, $status ); } // Send data. wp_send_json_error( $status ); } $this->update_resmush_list( $attachment_id ); Core::add_to_smushed_list( $attachment_id ); // Get the button status later after update resmushed list. $status = WP_Smush::get_instance()->library()->generate_markup( $attachment_id ); if ( $return ) { return $status; } wp_send_json_success( $status ); } /** * If auto smush is set to true or not, default is true * * @return int|bool */ public function is_auto_smush_enabled() { $auto_smush = $this->settings->get( 'auto' ); // Keep the auto smush on by default. if ( ! isset( $auto_smush ) ) { $auto_smush = 1; } return $auto_smush; } /** * Deletes all the backup files when an attachment is deleted * Update resmush List * Update Super Smush image count * * @param int $image_id Attachment ID. * * @return bool|void */ public function delete_images( $image_id ) { // Update the savings cache. WP_Smush::get_instance()->core()->get_savings( 'resize' ); // Update the savings cache. WP_Smush::get_instance()->core()->get_savings( 'pngjpg' ); // If no image id provided. if ( empty( $image_id ) ) { return false; } // Check and Update resmush list. $resmush_list = get_option( 'wp-smush-resmush-list' ); if ( $resmush_list ) { $this->update_resmush_list( $image_id ); } /** Delete Backups */ // Check if we have any smush data for image. WP_Smush::get_instance()->core()->mod->backup->delete_backup_files( $image_id ); /** * Delete webp. * * Run WebP::delete_images always even when the module is deactivated. * * @since 3.8.0 */ //WP_Smush::get_instance()->core()->mod->webp->delete_images( $image_id, false ); } /** * Calculate saving percentage for a given size stats * * @param object $stats Stats object. * * @return float|int */ private function calculate_percentage_from_stats( $stats ) { if ( empty( $stats ) || ! isset( $stats->size_before, $stats->size_after ) ) { return 0; } $savings = $stats->size_before - $stats->size_after; if ( $savings > 0 ) { $percentage = ( $savings / $stats->size_before ) * 100; return $percentage > 0 ? round( $percentage, 2 ) : $percentage; } return 0; } /** * Perform the resize operation for the image * * @param int $attachment_id Attachment ID. * @param array $meta Attachment meta. * * @return mixed */ public function resize_image( $attachment_id, $meta ) { if ( empty( $attachment_id ) || empty( $meta ) ) { return $meta; } return WP_Smush::get_instance()->core()->mod->resize->auto_resize( $attachment_id, $meta ); } /** * Send a smush request for the attachment * * @param int $id Attachment ID. */ public function wp_smush_handle_async( $id ) { // If we don't have image id or auto Smush is disabled, return. if ( empty( $id ) || ! $this->should_auto_smush( $id ) ) { return; } // Try to use smushit. $this->smush_single( $id, true ); } /** * Send a smush request for the attachment * * @param int $id Attachment ID. * @param array $post_data Post data. */ public function wp_smush_handle_editor_async( $id, $post_data ) { // If we don't have image id, or the smush is already in progress for the image, return. if ( empty( $id ) || get_transient( 'smush-in-progress-' . $id ) || get_transient( 'wp-smush-restore-' . $id ) ) { return; } // If auto Smush is disabled. if ( ! $this->should_auto_smush( $id ) ) { return; } /** * Filter: wp_smush_image * * Whether to smush the given attachment id or not * * @param bool $skip Whether to Smush image or not. * @param int $id Attachment ID of the image being processed. */ if ( ! apply_filters( 'wp_smush_image', true, $id ) ) { return; } // If filepath is not set or file doesn't exist. if ( ! isset( $post_data['filepath'] ) || ! file_exists( $post_data['filepath'] ) ) { return; } $res = $this->do_smushit( $post_data['filepath'] ); if ( is_wp_error( $res ) || empty( $res['success'] ) || ! $res['success'] ) { // Logged the error inside do_smushit. return; } // Update stats if it's the full size image. Return if it's not the full image size. if ( get_attached_file( $post_data['postid'] ) !== $post_data['filepath'] ) { return; } // Get the existing Stats. $smush_stats = get_post_meta( $post_data['postid'], self::$smushed_meta_key, true ); $stats_full = ! empty( $smush_stats['sizes'] ) && ! empty( $smush_stats['sizes']['full'] ) ? $smush_stats['sizes']['full'] : ''; if ( empty( $stats_full ) ) { return; } // store the original image size. $stats_full->size_before = ( ! empty( $stats_full->size_before ) && $stats_full->size_before > $res['data']->before_size ) ? $stats_full->size_before : $res['data']->before_size; $stats_full->size_after = $res['data']->after_size; // Update compression percent and bytes saved for each size. $stats_full->bytes = $stats_full->size_before - $stats_full->size_after; $stats_full->percent = $this->calculate_percentage_from_stats( $stats_full ); $smush_stats['sizes']['full'] = $stats_full; // Update stats. update_post_meta( $post_data['postid'], self::$smushed_meta_key, $smush_stats ); } /** * Make sure we treat the scaled image as an attachment size, rather than the original uploaded image. * * @since 3.9.1 * * @param array $meta Attachment meta data. * @param int $attachment_id Attachment ID. * * @return array */ public function add_scaled_to_meta( $meta, $attachment_id ) { // If the image is not a scaled version - do nothing. if ( false === strpos( $meta['file'], '-scaled.' ) || ! isset( $meta['original_image'] ) || isset( $meta['sizes']['wp_scaled'] ) ) { return $meta; } $meta['sizes']['wp_scaled'] = array( 'file' => basename( $meta['file'] ), 'width' => $meta['width'], 'height' => $meta['height'], 'mime-type' => get_post_mime_type( $attachment_id ), ); return $meta; } /** * @param $file_path * @param $image * * @return string */ public function put_webp_image_file( $file_path, $image ) { $file_path = WP_Smush::get_instance()->core()->mod->webp->get_webp_file_path( $file_path, true ); file_put_contents( $file_path, $image ); return $file_path; } /** * @param $file_path * @param $image * * @return void */ public function put_smushed_image_file( $file_path, $image ) { $temp_file = $file_path . '.tmp'; // Add the file as tmp. file_put_contents( $temp_file, $image ); // Replace the file. $success = rename( $temp_file, $file_path ); // If temp file still exists, unlink it. if ( file_exists( $temp_file ) ) { unlink( $temp_file ); } // If file renaming failed. if ( ! $success ) { copy( $temp_file, $file_path ); unlink( $temp_file ); } } /** * @param $file_path * * @return int */ public function get_file_permissions( $file_path ) { clearstatcache(); $perms = fileperms( $file_path ) & 0777; // Some servers are having issue with file permission, this should fix it. if ( empty( $perms ) ) { // Source: WordPress Core. $stat = stat( dirname( $file_path ) ); $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits. } return $perms; } private function save_smushed_image_file( $file_path, $convert_to_webp, $image ) { $pre = apply_filters( 'wp_smush_pre_image_write', false, $file_path, $convert_to_webp, $image ); if ( $pre !== false ) { Helper::logger()->notice( 'Another plugin/theme short circuited the image write operation using the wp_smush_pre_image_write filter.' ); return; } // Backup the old permissions $permissions = $this->get_file_permissions( $file_path ); // Save the new file if ( $convert_to_webp ) { $file_path = $this->put_webp_image_file( $file_path, $image ); } else { $this->put_smushed_image_file( $file_path, $image ); } // Restore the old permissions chmod( $file_path, $permissions ); } /** * @param $convert_to_webp * * @return string[] */ private function get_api_request_headers( $convert_to_webp ) { $headers = array( 'accept' => 'application/json', // The API returns JSON. 'content-type' => 'application/binary', // Set content type to binary. 'lossy' => $this->settings->get( 'lossy' ) ? 'true' : 'false', 'exif' => $this->settings->get( 'strip_exif' ) ? 'false' : 'true', ); if ( $convert_to_webp ) { $headers['webp'] = 'true'; } // Check if premium member, add API key. $api_key = Helper::get_wpmudev_apikey(); if ( ! empty( $api_key ) && WP_Smush::is_pro() ) { $headers['apikey'] = $api_key; } return $headers; } /** * @return string */ private function get_api_url() { return defined( 'WP_SMUSH_API_HTTP' ) ? WP_SMUSH_API_HTTP : WP_SMUSH_API; } /** * @param $file_path * @param $convert_to_webp * * @return array */ private function get_api_request_args( $file_path, $convert_to_webp ) { return array( 'headers' => $this->get_api_request_headers( $convert_to_webp ), 'body' => file_get_contents( $file_path ), 'timeout' => WP_SMUSH_TIMEOUT, 'user-agent' => WP_SMUSH_UA, ); } /** * @return Request_Multiple */ public function get_request_multiple() { return $this->request_multiple; } /** * @param Request_Multiple $request_multiple */ public function set_request_multiple( $request_multiple ) { $this->request_multiple = $request_multiple; } private function do_smushit_optimization( $file_path, $convert_to_webp, $retries ) { $errors = $this->validate_file( $file_path ); if ( count( $errors->get_error_messages() ) ) { return $errors; } $smusher = $convert_to_webp ? new Webp_Converter() : new Smusher(); $smusher->set_retry_attempts( $retries ); $data = $smusher->smush_file( $file_path ); if ( $data ) { return array( 'success' => true, 'data' => $data, ); } else { return $smusher->get_errors(); } } private function run_optimizer( $attachment_id ) { $in_progress_error = new WP_Error( 'in_progress', 'Smush already in progress' ); if ( $this->prevent_infinite_loop ) { return $this->no_smushit( $attachment_id, $in_progress_error, $in_progress_error ); } $this->prevent_infinite_loop = true; $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); $optimizer = new Media_Item_Optimizer( $media_item ); $optimized = $optimizer->optimize(); $this->prevent_infinite_loop = false; if ( $optimized ) { return $media_item->get_wp_metadata(); } else { if ( $media_item->has_errors() ) { return $media_item->get_errors(); } return $optimizer->get_errors(); } } public function should_auto_smush( $attachment_id ) { // TODO: We already verified in restoring status but better to disable auto smush while restoring. if ( ! $this->settings->is_automatic_compression_active() ) { return false; } $media_item = Media_Item_Cache::get_instance()->get( $attachment_id ); if ( ! $media_item->is_valid() ) { return false; } /** * Skip auto smush filter. * * @param bool $skip_auto_smush Whether to skip auto smush or not. */ $skip_auto_smush = apply_filters( 'wp_smush_should_skip_auto_smush', false, $attachment_id ); // We don't want very large files to be auto smushed. $skip_auto_smush = $skip_auto_smush || $media_item->is_large(); if ( $skip_auto_smush ) { return false; } return true; } }