OwlCyberSecurity - MANAGER
Edit File: class-smusher.php
<?php namespace Smush\Core\Smush; use Smush\Core\Api\Backoff; use Smush\Core\Api\Request_Multiple; use Smush\Core\File_System; use Smush\Core\Helper; use Smush\Core\Server_Utils; use Smush\Core\Settings; use Smush\Core\Upload_Dir; use WP_Error; use WP_Smush; /** * Takes raw image file paths and processes them through the Smush API. */ class Smusher { const ERROR_SSL_CERT = 'ssl_cert_error'; /** * @var Settings */ private $settings; /** * @var Request_Multiple */ private $request_multiple; /** * @var Backoff */ private $backoff; /** * @var \WDEV_Logger|null */ private $logger; /** * @var int */ private $retry_attempts; /** * @var int */ private $retry_wait; /** * @var int */ private $timeout; /** * @var string */ private $user_agent; /** * @var int */ private $connect_timeout; /** * @var boolean */ private $smush_parallel; /** * @var WP_Error */ private $errors; /** * @var File_System */ private $fs; /** * @var Upload_Dir */ private $upload_dir; /** * @var Server_Utils */ private $server_utils; public function __construct() { $this->retry_attempts = WP_SMUSH_RETRY_ATTEMPTS; $this->retry_wait = WP_SMUSH_RETRY_WAIT; $this->user_agent = WP_SMUSH_UA; $this->smush_parallel = WP_SMUSH_PARALLEL; $this->timeout = WP_SMUSH_TIMEOUT; $this->connect_timeout = 5; $this->settings = Settings::get_instance(); $this->logger = Helper::logger(); $this->request_multiple = new Request_Multiple(); $this->backoff = new Backoff(); $this->errors = new WP_Error(); $this->fs = new File_System(); $this->upload_dir = new Upload_Dir(); $this->server_utils = new Server_Utils(); } /** * @param $file_paths string[] * * @return boolean[]|object[] */ public function smush( $file_paths, $try_parallel = true ) { $this->set_errors( new WP_Error() ); if ( $try_parallel && $this->smush_parallel && $this->parallel_available_on_server() && $this->memory_available_for_parallel() ) { return $this->smush_parallel( $file_paths ); } else { return $this->smush_sequential( $file_paths ); } } private function memory_available_for_parallel() { $memory_limit = $this->server_utils->get_memory_limit() * 0.75; // 75% of max memory $memory_limit = apply_filters( 'wp_smush_parallel_memory_cutoff', $memory_limit ); $current_memory = $this->server_utils->get_memory_usage(); return $current_memory < $memory_limit; } /** * @param $file_paths string[] * * @return boolean[]|object[] */ private function smush_parallel( $file_paths ) { $retry = array(); $requests = array(); foreach ( $file_paths as $size_key => $size_file_path ) { $requests[ $size_key ] = $this->get_parallel_request_args( $size_file_path ); } // Send off the valid paths to the API $responses = array(); $this->request_multiple->do_requests( $requests, array( 'timeout' => $this->timeout, 'connect_timeout' => $this->connect_timeout, 'user-agent' => $this->user_agent, 'complete' => function ( $response, $response_size_key ) use ( &$requests, &$responses, &$retry, $file_paths ) { // Free up memory $requests[ $response_size_key ] = null; $size_file_path = $file_paths[ $response_size_key ]; if ( $this->should_retry_smush( $response ) ) { $retry[ $response_size_key ] = $size_file_path; } else { $responses[ $response_size_key ] = $this->handle_response( $response, $response_size_key, $size_file_path ); } }, ) ); // Retry failures with exponential backoff foreach ( $retry as $retry_size_key => $retry_size_file ) { $responses[ $retry_size_key ] = $this->smush_file( $retry_size_file, $retry_size_key ); } return $responses; } /** * @param $file_paths string[] * * @return boolean[]|object[] */ private function smush_sequential( $file_paths ) { $responses = array(); foreach ( $file_paths as $size_key => $size_file_path ) { $responses[ $size_key ] = $this->smush_file( $size_file_path, $size_key ); } return $responses; } /** * @param $file_path string * @param $size_key string * * @return bool|object */ public function smush_file( $file_path, $size_key = '' ) { $response = $this->backoff->set_wait( $this->retry_wait ) ->set_max_attempts( $this->retry_attempts ) ->enable_jitter() ->set_decider( array( $this, 'should_retry_smush' ) ) ->run( function () use ( $file_path ) { return $this->make_post_request( $file_path ); } ); return $this->handle_response( $response, $size_key, $file_path ); } private function make_post_request( $file_path ) { // Temporary increase the limit. wp_raise_memory_limit( 'image' ); return wp_remote_post( $this->get_api_url(), $this->get_api_request_args( $file_path ) ); } private function get_api_request_args( $file_path ) { return array( 'headers' => $this->get_api_request_headers( $file_path ), 'body' => $this->fs->file_get_contents( $file_path ), 'timeout' => $this->timeout, 'user-agent' => $this->user_agent, ); } /** * @param $response * @param $size_key string * @param $file_path string * * @return bool|object */ private function handle_response( $response, $size_key, $file_path ) { $data = $this->parse_response( $response, $size_key, $file_path ); if ( ! $data ) { if ( $this->has_error( self::ERROR_SSL_CERT ) ) { // Switch to http protocol. $this->settings->set_setting( 'wp-smush-use_http', 1 ); } return false; } if ( $data->bytes_saved > 0 ) { $optimized_image_saved = $this->save_smushed_image_file( $file_path, $data->image ); if ( ! $optimized_image_saved ) { $this->add_error( $size_key, 'image_not_saved', /* translators: %s: File path. */ sprintf( __( 'Smush was successful but we were unable to save the file due to a file system error: [%s].', 'wp-smushit' ), $this->upload_dir->get_human_readable_path( $file_path ) ) ); return false; } } // 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 ); } return $data; } protected function save_smushed_image_file( $file_path, $image ) { $pre = apply_filters( 'wp_smush_pre_image_write', false, $file_path, $image ); if ( $pre !== false ) { $this->logger->notice( 'Another plugin/theme short circuited the image write operation using the wp_smush_pre_image_write filter.' ); // Assume that the plugin/theme responsible took care of it return true; } // Backup the old permissions $permissions = $this->get_file_permissions( $file_path ); // Save the new file $success = $this->put_smushed_image_file( $file_path, $image ); // Restore the old permissions // TODO: this is the only chmod but restoring in the comment suggests that we changed the permissions before, what are we doing? chmod( $file_path, $permissions ); return $success; } private function put_smushed_image_file( $file_path, $image ) { $temp_file = $file_path . '.tmp'; $success = $this->put_image_using_temp_file( $file_path, $image, $temp_file ); // Clean up if ( $this->fs->file_exists( $temp_file ) ) { $this->fs->unlink( $temp_file ); } return $success; } private function put_image_using_temp_file( $file_path, $image, $temp_file ) { $file_written = file_put_contents( $temp_file, $image ); if ( ! $file_written ) { return false; } $renamed = rename( $temp_file, $file_path ); if ( $renamed ) { return true; } $copied = $this->fs->copy( $temp_file, $file_path ); if ( $copied ) { return true; } return false; } private 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 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 ); } /** * @param $response * @param $size_key string * @param $file_path string * * @return object|false */ private function parse_response( $response, $size_key, $file_path ) { if ( is_wp_error( $response ) ) { $error = $response->get_error_message(); if ( strpos( $error, 'SSL CA cert' ) !== false ) { $this->add_error( $size_key, self::ERROR_SSL_CERT, $error ); return false; } else if ( strpos( $error, 'timed out' ) !== false ) { $this->add_error( $size_key, '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' ) ); return false; } else { $this->add_error( $size_key, 'error_posting_to_api', /* translators: %s: Error message. */ sprintf( __( 'Error posting to API: %s', 'wp-smushit' ), $error ) ); return false; } } 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 ) ); $this->add_error( $size_key, 'non_200_response', $error ); return false; } $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' ); $this->add_error( $size_key, 'unsuccessful_smush', $error ); return false; } if ( empty( $json->data ) || empty( $json->data->before_size ) || empty( $json->data->after_size ) ) { $this->add_error( $size_key, 'no_data', __( 'Unknown API error', 'wp-smushit' ) ); return false; } $data = $json->data; $data->bytes_saved = isset( $data->bytes_saved ) ? (int) $data->bytes_saved : 0; $optimized_image_larger = $data->after_size > $data->before_size; if ( $optimized_image_larger ) { $this->add_error( $size_key, 'optimized_image_larger', /* translators: 1: File path, 2: Savings bytes. */ sprintf( 'The smushed image is larger than the original image [%s] (bytes saved %d), keep original image.', $this->upload_dir->get_human_readable_path( $file_path ), $data->bytes_saved ) ); return false; } $image = empty( $data->image ) ? '' : $data->image; if ( $data->bytes_saved > 0 ) { // Because of the API response structure, the following should only be done when there are some bytes_saved. if ( $data->image_md5 !== md5( $image ) ) { $error = __( 'Smush data corrupted, try again.', 'wp-smushit' ); $this->add_error( $size_key, 'data_corrupted', $error ); return false; } if ( ! empty( $image ) ) { $data->image = base64_decode( $data->image ); } } return $data; } public function should_retry_smush( $response ) { return $this->retry_attempts > 0 && ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ); } private function get_parallel_request_args( $file_path ) { return array( 'url' => $this->get_api_url(), 'headers' => $this->get_api_request_headers( $file_path ), 'data' => $this->fs->file_get_contents( $file_path ), 'type' => 'POST', ); } /** * @return string */ private function get_api_url() { return defined( 'WP_SMUSH_API_HTTP' ) ? WP_SMUSH_API_HTTP : WP_SMUSH_API; } /** * @return string[] */ protected function get_api_request_headers( $file_path ) { $headers = array( 'accept' => 'application/json', // The API returns JSON. 'content-type' => 'application/binary', // Set content type to binary. 'exif' => $this->settings->get( 'strip_exif' ) ? 'false' : 'true', ); $headers['lossy'] = $this->settings->get_lossy_level_setting(); // 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; $is_large_file = $this->is_large_file( $file_path ); if ( $is_large_file ) { $headers['islarge'] = 1; } } return $headers; } private function is_large_file( $file_path ) { $file_size = file_exists( $file_path ) ? filesize( $file_path ) : 0; $cut_off = $this->settings->get_large_file_cutoff(); return $file_size > $cut_off; } /** * @return bool */ public function parallel_available_on_server() { return $this->curl_multi_exec_available(); } /** * @return bool */ 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; } /** * @param int $retry_attempts * * @return Smusher */ public function set_retry_attempts( $retry_attempts ) { $this->retry_attempts = $retry_attempts; return $this; } /** * @param int $timeout */ public function set_timeout( $timeout ) { $this->timeout = $timeout; } /** * @param bool $smush_parallel * * @return Smusher */ public function set_smush_parallel( $smush_parallel ) { $this->smush_parallel = $smush_parallel; return $this; } /** * @param Request_Multiple $request_multiple * * @return Smusher */ public function set_request_multiple( $request_multiple ) { $this->request_multiple = $request_multiple; return $this; } public function get_errors() { return $this->errors; } /** * @param $errors WP_Error * * @return void */ private function set_errors( $errors ) { $this->errors = $errors; } /** * @param $size_key string * @param $code string * @param $message string * * @return void */ private function add_error( $size_key, $code, $message ) { // Log the error $this->logger->error( "[$size_key] $message" ); // Add the error $this->errors->add( $code, "[$size_key] $message" ); } /** * @param $code string * * @return bool */ private function has_error( $code ) { return ! empty( $this->errors->get_error_message( $code ) ); } }