This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,303 @@
<?php
/**
* WP Asynchronous Tasks
* Version: 1.0
* Description: Creates an abstract class to execute asynchronous tasks
* Author: 10up, Eric Mann, Luke Gedeon, John P. Bloch
* License: MIT
* Note: Modified to return metadata at the end of the launch function
*
* @package Smush\Core\Modules\Async
*/
namespace Smush\Core\Modules\Async;
use Exception;
use Smush\Core\Helper;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Abstract_Async
*/
abstract class Abstract_Async {
/**
* Constant identifier for a task that should be available to logged-in users
*
* See constructor documentation for more details.
*/
const LOGGED_IN = 1;
/**
* Constant identifier for a task that should be available to logged-out users
*
* See constructor documentation for more details.
*/
const LOGGED_OUT = 2;
/**
* Constant identifier for a task that should be available to all users regardless of auth status
*
* See constructor documentation for more details.
*/
const BOTH = 3;
/**
* This is the argument count for the main action set in the constructor. It
* is set to an arbitrarily high value of twenty, but can be overridden if
* necessary
*
* @var int
*/
protected $argument_count = 20;
/**
* Priority to fire intermediate action.
*
* @var int
*/
protected $priority = 10;
/**
* Action name.
*
* @var string
*/
protected $action;
/**
* Request body data.
*
* @var array
*/
protected $body_data;
/**
* Constructor to wire up the necessary actions
*
* Which hooks the asynchronous postback happens on can be set by the
* $auth_level parameter. There are essentially three options: logged-in users
* only, logged-out users only, or both. Set this when you instantiate an
* object by using one of the three class constants to do so:
* - LOGGED_IN
* - LOGGED_OUT
* - BOTH
* $auth_level defaults to BOTH
*
* @throws Exception If the class' $action value hasn't been set.
*
* @param int $auth_level The authentication level to use (see above).
*/
public function __construct( $auth_level = self::BOTH ) {
if ( empty( $this->action ) ) {
throw new Exception( 'Action not defined for class ' . __CLASS__ );
}
// Handle the actual action.
add_action( $this->action, array( $this, 'launch' ), $this->priority, $this->argument_count );
if ( $auth_level & self::LOGGED_IN ) {
add_action( "admin_post_wp_async_$this->action", array( $this, 'handle_postback' ) );
}
if ( $auth_level & self::LOGGED_OUT ) {
add_action( "admin_post_nopriv_wp_async_$this->action", array( $this, 'handle_postback' ) );
}
}
/**
* Add the shutdown action for launching the real postback if we don't
* get an exception thrown by prepare_data().
*
* @uses func_get_args() To grab any arguments passed by the action
*
* @return mixed|void
*/
public function launch() {
$data = func_get_args();
try {
$data = $this->prepare_data( $data );
} catch ( Exception $e ) {
Helper::logger()->error( sprintf( 'Async Smush: Error in prepare_data: %s', $e->getMessage() ) );
return;
}
$data['action'] = "wp_async_$this->action";
$data['_nonce'] = $this->create_async_nonce();
$this->body_data = $data;
$has_shutdown_action = has_action( 'shutdown', array( $this, 'process_request' ) );
$is_upload_attachment_action = ! empty( $_POST['action'] ) && 'upload-attachment' === $_POST['action'];
$is_post_id_non_empty = ! empty( $_POST ) && isset( $_POST['post_id'] ) && $_POST['post_id'];
$is_async_upload = isset( $_POST['post_id'] ) && empty( $_POST['post_id'] ) && isset( $_FILES['async-upload'] );
$should_hook_to_shutdown = $is_upload_attachment_action || $is_post_id_non_empty || $is_async_upload;
// Do not use this, as in case of importing, only the last image gets processed
// It's very important that all the Media uploads, are handled via shutdown action, else, sometimes the image meta updated
// by smush is earlier, and then original meta update causes discrepancy.
if ( $should_hook_to_shutdown && ! $has_shutdown_action ) {
add_action( 'shutdown', array( $this, 'process_request' ) );
} else {
// Send a ajax request to process image and return image metadata, added for compatibility with plugins like
// WP All Import, and RSS aggregator, which upload multiple images at once.
$this->process_request();
}
// If we have image metadata return it.
if ( ! empty( $data['metadata'] ) ) {
return $data['metadata'];
}
}
/**
* Launch the request on the WordPress shutdown hook
*
* On VIP we got into data races due to the postback sometimes completing
* faster than the data could propogate to the database server cluster.
* This made WordPress get empty data sets from the database without
* failing. On their advice, we're moving the actual firing of the async
* postback to the shutdown hook. Supposedly that will ensure that the
* data at least has time to get into the object cache.
*
* @uses $_COOKIE To send a cookie header for async postback
* @uses apply_filters()
* @uses admin_url()
* @uses wp_remote_post()
*/
public function process_request() {
if ( ! empty( $this->body_data ) ) {
$request_args = array(
'timeout' => apply_filters( 'smush_async_time_out', 0 ),
'blocking' => false,
'sslverify' => false,
'body' => $this->body_data,
'cookies' => wp_unslash( $_COOKIE ),
);
$url = admin_url( 'admin-post.php' );
wp_remote_post( $url, $request_args );
}
}
/**
* Verify the postback is valid, then fire any scheduled events.
*
* @uses $_POST['_nonce']
* @uses is_user_logged_in()
* @uses add_filter()
* @uses wp_die()
*/
public function handle_postback() {
if ( isset( $_POST['_nonce'] ) && $this->verify_async_nonce( $_POST['_nonce'] ) ) {
$this->run_action();
}
add_filter( 'wp_die_handler', array( $this, 'handle_die' ) );
wp_die();
}
/**
* Handle Die
*/
public function handle_die() {
die();
}
/**
* Create a random, one time use token.
*
* Based entirely on wp_create_nonce() but does not tie the nonce to the
* current logged-in user.
*
* @uses wp_nonce_tick()
* @uses wp_hash()
*
* @return string The one-time use token
*/
protected function create_async_nonce() {
$action = $this->get_nonce_action();
$i = wp_nonce_tick();
return substr( wp_hash( $i . $action . get_class( $this ), 'nonce' ), - 12, 10 );
}
/**
* Verify that the correct nonce was used within the time limit.
*
* @uses wp_nonce_tick()
* @uses wp_hash()
*
* @param string $nonce Nonce to be verified.
*
* @return bool Whether the nonce check passed or failed
*/
protected function verify_async_nonce( $nonce ) {
$action = $this->get_nonce_action();
$i = wp_nonce_tick();
// Nonce generated 0-12 hours ago.
if ( substr( wp_hash( $i . $action . get_class( $this ), 'nonce' ), - 12, 10 ) === $nonce ) {
return 1;
}
// Nonce generated 12-24 hours ago.
if ( substr( wp_hash( ( $i - 1 ) . $action . get_class( $this ), 'nonce' ), - 12, 10 ) === $nonce ) {
return 2;
}
// Invalid nonce.
return false;
}
/**
* Get a nonce action based on the $action property of the class
*
* @return string The nonce action for the current instance
*/
protected function get_nonce_action() {
$action = $this->action;
if ( substr( $action, 0, 7 ) === 'nopriv_' ) {
$action = substr( $action, 7 );
}
return "wp_async_$action";
}
/**
* Prepare any data to be passed to the asynchronous postback
*
* The array this function receives will be a numerically keyed array from
* func_get_args(). It is expected that you will return an associative array
* so that the $_POST values used in the asynchronous call will make sense.
*
* The array you send back may or may not have anything to do with the data
* passed into this method. It all depends on the implementation details and
* what data is needed in the asynchronous postback.
*
* Do not set values for 'action' or '_nonce', as those will get overwritten
* later in launch().
*
* @throws Exception If the postback should not occur for any reason.
*
* @param array $data The raw data received by the launch method.
*
* @return array The prepared data
*/
abstract protected function prepare_data( $data );
/**
* Run the do_action function for the asynchronous postback.
*
* This method needs to fetch and sanitize any and all data from the $_POST
* superglobal and provide them to the do_action call.
*
* The action should be constructed as "wp_async_task_$this->action"
*/
abstract protected function run_action();
}

View File

@ -0,0 +1,97 @@
<?php
/**
* Class Async
*
* @package Smush\Core\Modules\Async
* @since 2.5
*
* @author Umesh Kumar <umesh@incsub.com>
*
* @copyright (c) 2016, Incsub (http://incsub.com)
*/
namespace Smush\Core\Modules\Async;
use Exception;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Async
*/
class Async extends Abstract_Async {
/**
* Argument count.
*
* @var int $argument_count
*/
protected $argument_count = 2;
/**
* Priority.
*
* @var int $priority
*/
protected $priority = 12;
/**
* Whenever a attachment metadata is generated
* Had to be hooked on generate and not update, else it goes in infinite loop
*
* @var string
*/
protected $action = 'wp_generate_attachment_metadata';
/**
* Prepare data for the asynchronous request
*
* @throws Exception If for any reason the request should not happen.
*
* @param array $data An array of data sent to the hook.
*
* @return array
*/
protected function prepare_data( $data ) {
// We don't have the data, bail out.
if ( empty( $data ) ) {
return $data;
}
// Return a associative array.
$image_meta = array();
$image_meta['metadata'] = ! empty( $data[0] ) ? $data[0] : '';
$image_meta['id'] = ! empty( $data[1] ) ? $data[1] : '';
/**
* AJAX Thumbnail Rebuild integration.
*
* @see https://app.asana.com/0/14491813218786/730814863045197/f
*/
if ( ! empty( $_POST['action'] ) && 'ajax_thumbnail_rebuild' === $_POST['action'] && ! empty( $_POST['thumbnails'] ) ) { // Input var ok.
$image_meta['regen'] = wp_unslash( $_POST['thumbnails'] ); // Input var ok.
}
return $image_meta;
}
/**
* Run the async task action
*
* TODO: See if auto smush is enabled or not.
* TODO: Check if async is enabled or not.
*/
protected function run_action() {
// Nonce validated in parent method.
$id = ! empty( $_POST['id'] ) ? (int) $_POST['id'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
// Get metadata from $_POST.
if ( ! empty( $_POST['metadata'] ) && wp_attachment_is_image( $id ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
// Allow the Asynchronous task to run.
do_action( "wp_async_$this->action", $id );
}
}
}

View File

@ -0,0 +1,90 @@
<?php
/**
* Class Editor
*
* @package Smush\Core\Modules\Async
* @since 2.5
*
* @author Umesh Kumar <umesh@incsub.com>
*
* @copyright (c) 2016, Incsub (http://incsub.com)
*/
namespace Smush\Core\Modules\Async;
use Exception;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Editor
*/
class Editor extends Abstract_Async {
/**
* Argument count.
*
* @var int $argument_count
*/
protected $argument_count = 2;
/**
* Priority.
*
* @var int $priority
*/
protected $priority = 12;
/**
* Whenever a attachment metadata is generated
* Had to be hooked on generate and not update, else it goes in infinite loop
*
* @var string
*/
protected $action = 'wp_save_image_editor_file';
/**
* Prepare data for the asynchronous request
*
* @throws Exception If for any reason the request should not happen.
*
* @param array $data An array of data sent to the hook.
*
* @return array
*/
protected function prepare_data( $data ) {
// Store the post data in $data variable.
if ( ! empty( $data ) ) {
$data = array_merge( $data, $_POST );
}
// Store the image path.
$data['filepath'] = ! empty( $data[1] ) ? $data[1] : '';
$data['wp-action'] = ! empty( $data['action'] ) ? $data['action'] : '';
unset( $data['action'], $data[1] );
return $data;
}
/**
* Run the async task action
*
* TODO: Add a check for image
* TODO: See if auto smush is enabled or not
* TODO: Check if async is enabled or not
*/
protected function run_action() {
if ( isset( $_POST['wp-action'], $_POST['do'], $_POST['postid'] )
&& 'image-editor' === $_POST['wp-action']
&& check_ajax_referer( 'image_editor-' . (int) $_POST['postid'] )
&& 'open' !== $_POST['do']
) {
$postid = ! empty( $_POST['postid'] ) ? (int) $_POST['postid'] : '';
// Allow the Asynchronous task to run.
do_action( "wp_async_$this->action", $postid, $_POST );
}
}
}

View File

@ -0,0 +1,164 @@
<?php
namespace Smush\Core\Modules\Background;
/**
* Abstract WP_Async_Request class.
*
* @abstract
*/
abstract class Async_Request {
/**
* Identifier
*
* @var mixed
* @access protected
*/
protected $identifier;
/**
* Data
*
* (default value: array())
*
* @var array
* @access protected
*/
protected $data = array();
/**
* Initiate new async request
*/
public function __construct( $identifier ) {
$this->identifier = $identifier;
add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) );
add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) );
}
/**
* Set data used during the request
*
* @param array $data Data.
*
* @return $this
*/
public function data( $data ) {
$this->data = $data;
return $this;
}
/**
* Dispatch the async request
*
* @param int $instance_id
*
* @return array|\WP_Error
*/
public function dispatch( $instance_id ) {
$query_args = $this->get_query_args( $instance_id );
$url = add_query_arg( $query_args, $this->get_query_url() );
$args = $this->get_post_args();
return wp_remote_post( esc_url_raw( $url ), $args );
}
/**
* Get query args
*
* @return array
*/
protected function get_query_args( $instance_id ) {
if ( property_exists( $this, 'query_args' ) ) {
return $this->query_args;
}
return array(
'action' => $this->identifier,
'nonce' => wp_create_nonce( $this->identifier ),
'instance_id' => $instance_id,
);
}
/**
* Get query URL
*
* @return string
*/
protected function get_query_url() {
if ( property_exists( $this, 'query_url' ) ) {
return $this->query_url;
}
return admin_url( 'admin-ajax.php' );
}
/**
* Get process headers.
*
* @return array
*/
protected function get_process_headers() {
$headers = array();
if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
$headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
}
return apply_filters( $this->identifier . '_process_headers', $headers );
}
/**
* Get post args
*
* @return array
*/
protected function get_post_args() {
if ( property_exists( $this, 'post_args' ) ) {
return $this->post_args;
}
$post_args = array(
'timeout' => 0.01,
'blocking' => false,
'body' => $this->data,
'cookies' => $_COOKIE,
'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
);
$headers = $this->get_process_headers();
if ( ! empty( $headers ) ) {
$post_args['headers'] = $headers;
}
return $post_args;
}
/**
* Maybe handle
*
* Check for correct nonce and pass to handler.
*/
public function maybe_handle() {
// Don't lock up other requests while processing
session_write_close();
check_ajax_referer( $this->identifier, 'nonce' );
$instance_id = empty( $_GET['instance_id'] )
? false
: sanitize_key( $_GET['instance_id'] );
$this->handle( $instance_id );
wp_die();
}
/**
* Handle
*
* Override this method to perform any actions required
* during the async request.
*/
abstract protected function handle( $instance_id );
}

View File

@ -0,0 +1,46 @@
<?php
namespace Smush\Core\Modules\Background;
class Background_Logger_Container {
private $logger;
private $identifier;
public function __construct( $identifier ) {
$this->identifier = $identifier;
}
public function set_logger( $logger ) {
$this->logger = $logger;
}
public function error( $message ) {
$this->log( $message, 'error' );
}
public function notice( $message ) {
$this->log( $message, 'notice' );
}
public function warning( $message ) {
$this->log( $message, 'warning' );
}
public function info( $message ) {
$this->log( $message, 'info' );
}
private function log( $message, $type ) {
if ( $this->logger && method_exists( $this->logger, $type ) ) {
$this->logger->$type(
$this->prepare_message( $message )
);
}
}
private function prepare_message( $message ) {
$identifier = $this->identifier;
return "Background $identifier: $message";
}
}

View File

@ -0,0 +1,196 @@
<?php
namespace Smush\Core\Modules\Background;
class Background_Process_Status {
const PROCESSING = 'in_processing';
const CANCELLED = 'is_cancelled';
const COMPLETED = 'is_completed';
const DEAD = 'is_dead';
const TOTAL_ITEMS = 'total_items';
const PROCESSED_ITEMS = 'processed_items';
const FAILED_ITEMS = 'failed_items';
private $identifier;
/**
* @var Background_Utils
*/
private $utils;
public function __construct( $identifier ) {
$this->identifier = $identifier;
$this->utils = new Background_Utils();
}
public function get_data() {
$option_value = $this->utils->get_site_option(
$this->get_option_id(),
array()
);
return wp_parse_args(
$option_value,
array(
self::PROCESSING => false,
self::CANCELLED => false,
self::COMPLETED => false,
self::TOTAL_ITEMS => 0,
self::PROCESSED_ITEMS => 0,
self::FAILED_ITEMS => 0,
)
);
}
public function to_array() {
return $this->get_data();
}
private function set_data( $updated ) {
$data = $this->get_data();
update_site_option( $this->get_option_id(), array_merge( $data, $updated ) );
}
private function get_value( $key ) {
$data = $this->get_data();
return isset( $data[ $key ] )
? $data[ $key ]
: false;
}
private function set_value( $key, $value ) {
$this->mutex( function () use ( $key, $value ) {
$updated_data = array_merge(
$this->get_data(),
array( $key => $value )
);
update_site_option( $this->get_option_id(), $updated_data );
} );
}
private function get_option_id() {
return $this->identifier . '_status';
}
public function is_in_processing() {
return $this->get_value( self::PROCESSING );
}
public function set_in_processing( $in_processing ) {
$this->set_value( self::PROCESSING, $in_processing );
}
public function get_total_items() {
return $this->get_value( self::TOTAL_ITEMS );
}
public function set_total_items( $total_items ) {
$this->set_value( self::TOTAL_ITEMS, $total_items );
}
public function get_processed_items() {
return $this->get_value( self::PROCESSED_ITEMS );
}
public function set_processed_items( $processed_items ) {
$this->set_value( self::PROCESSED_ITEMS, $processed_items );
}
public function get_failed_items() {
return $this->get_value( self::FAILED_ITEMS );
}
public function set_failed_items( $failed_items ) {
$this->set_value( self::PROCESSED_ITEMS, $failed_items );
}
public function is_cancelled() {
return $this->get_value( self::CANCELLED );
}
public function set_is_cancelled( $is_cancelled ) {
$this->set_value( self::CANCELLED, $is_cancelled );
}
public function is_dead() {
return $this->get_value( self::DEAD );
}
public function is_completed() {
return $this->get_value( self::COMPLETED );
}
public function set_is_completed( $is_completed ) {
$this->set_value( self::COMPLETED, $is_completed );
}
private function mutex( $operation ) {
$mutex = new Mutex( $this->get_option_id() );
$mutex->execute( $operation );
}
public function start( $total_items ) {
$this->mutex( function () use ( $total_items ) {
$this->set_data( array(
self::PROCESSING => true,
self::CANCELLED => false,
self::DEAD => false,
self::COMPLETED => false,
self::TOTAL_ITEMS => $total_items,
self::PROCESSED_ITEMS => 0,
self::FAILED_ITEMS => 0,
) );
} );
}
public function complete() {
$this->mutex( function () {
$this->set_data( array(
self::PROCESSING => false,
self::CANCELLED => false,
self::DEAD => false,
self::COMPLETED => true,
) );
} );
}
public function cancel() {
$this->mutex( function () {
$this->set_data( array(
self::PROCESSING => false,
self::CANCELLED => true,
self::DEAD => false,
self::COMPLETED => false,
) );
} );
}
public function mark_as_dead() {
$this->mutex( function () {
$this->set_data( array(
self::PROCESSING => false,
self::CANCELLED => false,
self::DEAD => true,
self::COMPLETED => false,
) );
} );
}
public function task_successful() {
$this->mutex( function () {
$this->set_data( array(
self::PROCESSED_ITEMS => $this->get_processed_items() + 1,
) );
} );
}
public function task_failed() {
$this->mutex( function () {
$this->set_data( array(
self::PROCESSED_ITEMS => $this->get_processed_items() + 1,
self::FAILED_ITEMS => $this->get_failed_items() + 1,
) );
} );
}
}

View File

@ -0,0 +1,604 @@
<?php
namespace Smush\Core\Modules\Background;
use Smush\Core\Server_Utils;
/**
* Abstract WP_Background_Process class.
*
* @abstract
* @extends Async_Request
*/
abstract class Background_Process extends Async_Request {
const TASKS_PER_REQUEST_UNLIMITED = - 1;
/**
* Start time of current process.
*
* (default value: 0)
*
* @var int
* @access protected
*/
private $start_time = 0;
/**
* Cron_hook_identifier
*
* @var mixed
* @access protected
*/
private $cron_hook_identifier;
/**
* Cron_interval_identifier
*
* @var mixed
* @access protected
*/
private $cron_interval_identifier;
/**
* @var Background_Logger_Container
*/
private $logger_container;
/**
* @var Background_Process_Status
*/
private $status;
/**
* @var Background_Utils
*/
private $utils;
private $tasks_per_request = self::TASKS_PER_REQUEST_UNLIMITED;
private $server_utils;
/**
* Initiate new background process
*/
public function __construct( $identifier ) {
parent::__construct( $identifier );
$this->cron_hook_identifier = $this->identifier . '_cron';
$this->cron_interval_identifier = $this->identifier . '_cron_interval';
add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );
add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
$this->logger_container = new Background_Logger_Container( $this->identifier );
$this->status = new Background_Process_Status( $this->identifier );
$this->utils = new Background_Utils();
$this->server_utils = new Server_Utils();
}
private function generate_instance_id() {
return md5( microtime() . rand() );
}
/**
* Dispatch
*
* @access public
* @return array|\WP_Error
*/
public function dispatch( $instance_id ) {
$this->logger()->info( "Dispatching a new request for instance $instance_id." );
// Schedule the cron healthcheck.
$this->schedule_event();
// Perform remote post.
return parent::dispatch( $instance_id );
}
public function spawn() {
$instance_id = $this->generate_instance_id();
$this->logger()->info( "Spawning a brand new instance (ID: $instance_id) for the process." );
$this->set_active_instance_id( $instance_id );
$this->dispatch( $instance_id );
}
/**
* Update queue
*
* @param array $tasks An array of tasks.
*/
private function update_queue( $tasks ) {
if ( ! empty( $tasks ) ) {
update_site_option( $this->get_queue_key(), $tasks );
}
}
/**
* Delete queue
*/
private function delete_queue() {
delete_site_option( $this->get_queue_key() );
}
/**
* Generate key
*
* Generates a unique key based on microtime. Queue items are
* given a unique key so that they can be merged upon save.
*
* @return string
*/
protected function get_queue_key() {
return $this->identifier . '_queue';
}
/**
* Maybe process queue
*
* Checks whether data exists within the queue and that
* the process is not already running.
*/
public function maybe_handle() {
// Don't lock up other requests while processing
session_write_close();
$this->mutex( function () {
$instance_id = empty( $_GET['instance_id'] )
? false
: $_GET['instance_id'];
if ( $this->is_queue_empty() ) {
$this->logger()->warning( "Handler called with instance ID $instance_id but the queue is empty. Killing this instance." );
return;
}
if ( ! $instance_id || ! $this->is_active_instance( $instance_id ) ) {
// We thought the process died, so we spawned a new instance.
// Kill this instance and let the new one continue.
$active_instance_id = $this->get_active_instance_id();
$this->logger()->warning( "Handler called with instance ID $instance_id but the active instance ID is $active_instance_id. Killing $instance_id so $active_instance_id can continue." );
return;
}
if ( ! check_ajax_referer( $this->identifier, 'nonce', false ) ) {
return;
}
$this->handle( $instance_id );
} );
wp_die();
}
/**
* Is queue empty
*
* @return bool
*/
protected function is_queue_empty() {
return empty( $this->get_queue() );
}
/**
* Is process running
*
* Check whether the current process is already running
* in a background process.
*/
protected function is_process_running() {
if ( get_site_transient( $this->get_last_run_transient_key() ) ) {
// Process already running.
return true;
}
return false;
}
protected function update_timestamp( $instance_id ) {
$timestamp = time();
$this->start_time = $timestamp; // Set start time of current process.
set_site_transient(
$this->get_last_run_transient_key(),
$timestamp,
$this->get_instance_expiry_duration()
);
$human_readable_timestamp = wp_date( 'Y-m-d H:i:s', $timestamp );
$this->logger()->info( "Setting last run timestamp for instance ID $instance_id to $human_readable_timestamp" );
}
/**
* Get queue
*
* @return array Return the first queue from the queue
*/
protected function get_queue() {
$queue = $this->utils->get_site_option( $this->get_queue_key(), array() );
return empty( $queue ) || ! is_array( $queue )
? array()
: $queue;
}
/**
* Handle
*
* Pass each queue item to the task handler, while remaining
* within server memory and time limit constraints.
*/
protected function handle( $instance_id ) {
$this->logger()->info( "Handling instance ID $instance_id." );
$this->update_timestamp( $instance_id );
$queue = $this->get_queue();
$processed_tasks_count = 0;
foreach ( $queue as $key => $value ) {
$this->logger()->info( "Executing task $value." );
$task = $this->task( $value );
if ( $task ) {
$this->status->task_successful();
} else {
$this->status->task_failed();
}
if ( $this->status->is_cancelled() ) {
$this->logger()->info( "While we were busy doing the task $value, the process got cancelled. Clean up and stop." );
return;
}
unset( $queue[ $key ] );
$processed_tasks_count ++;
if ( $this->task_limit_reached( $processed_tasks_count ) ) {
$tasks_per_request = $this->get_tasks_per_request();
$this->logger()->info( "Stopping because we are only supposed to perform $tasks_per_request tasks in a single request and we have reached that limit." );
break;
}
if ( $this->time_exceeded() || $this->memory_exceeded() ) {
$this->logger()->warning( "Time/Memory limits reached, save the queue and dispatch a new request." );
break;
}
}
if ( empty( $queue ) ) {
$this->complete();
} else {
$this->update_queue( $queue );
$this->dispatch( $instance_id );
}
}
/**
* Memory exceeded
*
* Ensures the process never exceeds 90%
* of the maximum WordPress memory.
*
* @return bool
*/
protected function memory_exceeded() {
$memory_limit = $this->server_utils->get_memory_limit() * 0.9; // 90% of max memory
$current_memory = $this->server_utils->get_memory_usage();
$return = false;
if ( $current_memory >= $memory_limit ) {
$return = true;
}
return apply_filters( $this->identifier . '_memory_exceeded', $return );
}
/**
* Time exceeded.
*
* Ensures the process never exceeds a sensible time limit.
* A timeout limit of 30s is common on shared hosting.
*
* @return bool
*/
protected function time_exceeded() {
$finish = $this->start_time + $this->get_time_limit();
$return = false;
if ( time() >= $finish ) {
$return = true;
}
return apply_filters( $this->identifier . '_time_exceeded', $return );
}
/**
* Complete.
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
$this->logger()->info( "Process completed." );
$this->cleanup();
$this->status->complete();
$this->do_action( 'completed' );
}
/**
* Schedule cron healthcheck
*
* @access public
*
* @param mixed $schedules Schedules.
*
* @return mixed
*/
public function schedule_cron_healthcheck( $schedules ) {
$interval = $this->get_cron_interval_seconds();
// Adds every 5 minutes to the existing schedules.
$schedules[ $this->identifier . '_cron_interval' ] = array(
'interval' => $interval,
/* translators: %s: Cron interval in minutes */
'display' => sprintf( __( 'Every %d Minutes', 'wp-smushit' ), $interval / MINUTE_IN_SECONDS ),
);
return $schedules;
}
/**
* Handle cron healthcheck
*
* Restart the background process if not already running
* and data exists in the queue.
*/
public function handle_cron_healthcheck() {
$this->logger()->info( "Running scheduled health check." );
if ( $this->is_process_running() ) {
$this->logger()->info( "Health check: Process seems healthy, no action required." );
exit;
}
if ( $this->is_queue_empty() ) {
$this->logger()->info( "Health check: Process not in progress but the queue is empty, no action required." );
$this->clear_scheduled_event();
exit;
}
if ( $this->status->is_cancelled() ) {
$this->logger()->info( "Health check: Process has been cancelled already, no action required." );
$this->clear_scheduled_event();
exit;
}
if ( $this->attempt_restart_during_health_check() ) {
$this->logger()->warning( "Health check: Process instance seems to have died. Spawn a new instance." );
$this->spawn();
} else {
$this->logger()->warning( "Health check: Process instance seems to have died. Restart disabled, marking the process as dead." );
$this->mark_as_dead();
}
exit;
}
private function mark_as_dead() {
$this->status->mark_as_dead();
$this->cleanup();
$this->do_action( 'dead' );
}
/**
* Schedule event
*/
protected function schedule_event() {
$hook = $this->cron_hook_identifier;
if ( ! wp_next_scheduled( $hook ) ) {
$interval = $this->cron_interval_identifier;
$next_run = time() + $this->get_cron_interval_seconds();
wp_schedule_event( $next_run, $interval, $hook );
$this->logger()->info( "Scheduling new event with hook $hook to run $interval." );
}
}
/**
* Clear scheduled event
*/
protected function clear_scheduled_event() {
$hook = $this->cron_hook_identifier;
$this->logger()->info( "Cancelling event with hook $hook." );
wp_clear_scheduled_hook( $hook );
}
/**
* Cancel Process
*
* Stop processing queue items, clear cronjob and delete queue.
*/
private function cancel_process() {
$this->cleanup();
$this->logger()->info( "Process cancelled." );
}
public function cancel() {
// Update the cancel flag first
$active_instance_id = $this->get_active_instance_id();
$this->logger()->info( "Starting cancellation (Instance: $active_instance_id)." );
$this->status->cancel();
// Since actual cancellation involves deletion of the queue and the handler
// might be in the middle of updating the queue, we need to use a mutex
$mutex = new Mutex( $this->get_handler_mutex_id() );
$mutex
->set_break_on_timeout( false ) // Since this is a user operation, we must cancel, even if there is a timeout
->set_timeout( $this->get_time_limit() ) // Shouldn't take more time than the time allocated to the process itself
->execute( function () use ( $active_instance_id ) {
$this->logger()->info( "Cancelling the process (Instance: $active_instance_id)." );
$this->cancel_process();
$this->logger()->info( "Cancellation completed (Instance: $active_instance_id)." );
$this->do_action( 'cancelled' );
} );
}
/**
* Task
*
* Override this method to perform any actions required on each
* queue item. Return the modified item for further processing
* in the next pass through. Or, return false to remove the
* item from the queue.
*
* @param mixed $task Queue item to iterate over.
*
* @return mixed
*/
abstract protected function task( $task );
private function is_active_instance( $instance_id ) {
return $instance_id === $this->get_active_instance_id();
}
/**
* Save the unique ID of the process we are presuming to be dead, so we can prevent it from coming back.
*
* @param $instance_id
*
* @return void
*/
private function set_active_instance_id( $instance_id ) {
update_site_option( $this->get_active_instance_option_id(), $instance_id );
}
private function get_active_instance_id() {
return get_site_option( $this->get_active_instance_option_id(), '' );
}
private function get_active_instance_option_id() {
return $this->identifier . '_active_instance';
}
public function set_logger( $logger ) {
$this->logger_container->set_logger( $logger );
}
/**
* @return Background_Logger_Container
*/
private function logger() {
return $this->logger_container;
}
public function get_status() {
return $this->status;
}
/**
* @param $tasks array
*
* @return void
*/
public function start( $tasks ) {
$this->do_action( 'before_start' );
$total_items = count( $tasks );
$this->status->start( $total_items );
$this->logger()->info( "Starting new process with $total_items tasks" );
$this->update_queue( $tasks );
$this->spawn();
$this->do_action( 'started' );
}
private function mutex( $operation ) {
$mutex = new Mutex( $this->get_handler_mutex_id() );
$mutex->set_break_on_timeout( true ) // Let the previous handler do its thing
->set_timeout( $this->get_lock_duration() )
->execute( $operation );
}
private function get_handler_mutex_id() {
return $this->identifier . '_handler_lock';
}
private function get_time_limit() {
return apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
}
private function get_lock_duration() {
$lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
return apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
}
private function get_instance_expiry_duration() {
return apply_filters( $this->identifier . '_instance_expiry_time', 60 * 2 ); // 2 minutes
}
private function get_last_run_transient_key() {
return $this->identifier . '_last_run';
}
private function clear_last_run_timestamp() {
delete_site_transient( $this->get_last_run_transient_key() );
}
private function cleanup() {
// Delete options and transients
$this->delete_queue();
delete_site_option( $this->get_active_instance_option_id() );
$this->clear_last_run_timestamp();
// Cancel all events
$this->clear_scheduled_event();
}
private function task_limit_reached( $processed_tasks_count ) {
if ( $this->get_tasks_per_request() === self::TASKS_PER_REQUEST_UNLIMITED ) {
return false;
}
return $processed_tasks_count >= $this->get_tasks_per_request();
}
public function get_tasks_per_request() {
return $this->tasks_per_request;
}
/**
* @param int $tasks_per_request
*/
public function set_tasks_per_request( $tasks_per_request ) {
$this->tasks_per_request = $tasks_per_request;
}
private function do_action( $action ) {
do_action( "{$this->identifier}_$action", $this->identifier, $this );
}
private function get_cron_interval_seconds() {
$minutes = property_exists( $this, 'cron_interval' )
? $this->cron_interval
: 5;
$interval = apply_filters( $this->identifier . '_cron_interval', $minutes );
return $interval * MINUTE_IN_SECONDS;
}
protected function attempt_restart_during_health_check() {
return true;
}
public function get_identifier() {
return $this->identifier;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Smush\Core\Modules\Background;
class Background_Utils {
/**
* Thread safe version of get_site_option, queries the database directly to prevent use of cached values
*
* @param $option_id string
* @param $default
*
* @return false|mixed
*/
public function get_site_option( $option_id, $default = false ) {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
$key_column = 'option_id';
$value_column = 'option_value';
if ( is_multisite() ) {
$table = $wpdb->sitemeta;
$column = 'meta_key';
$key_column = 'meta_id';
$value_column = 'meta_value';
}
return $this->get_value_from_db( $table, $column, $key_column, $option_id, $value_column, $default );
}
public function get_option( $option_id, $default = false ) {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
$key_column = 'option_id';
$value_column = 'option_value';
return $this->get_value_from_db( $table, $column, $key_column, $option_id, $value_column, $default );
}
private function get_value_from_db( $table, $column, $key_column, $option_id, $value_column, $default ) {
global $wpdb;
$row = $wpdb->get_row( $wpdb->prepare( "
SELECT *
FROM {$table}
WHERE {$column} = %s
ORDER BY {$key_column} ASC
LIMIT 1
", $option_id ) );
if ( empty( $row->$value_column ) || ! is_object( $row ) ) {
return $default;
}
return maybe_unserialize( $row->$value_column );
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Smush\Core\Modules\Background;
class Mutex {
/**
* @var string
*/
private $key;
/**
* TRUE: Don't perform the operation if lock couldn't be acquired
* FALSE: Even if lock is not acquired, still perform the operation
*
* @var bool
*/
private $break_on_timeout = false;
/**
* @var int
*/
private $timeout = 10;
public function __construct( $key ) {
$this->key = $key;
}
public function execute( $operation ) {
$acquired = $this->acquire_lock();
if ( $acquired || ! $this->break_on_timeout() ) {
call_user_func( $operation );
}
$this->release_lock();
}
private function acquire_lock() {
global $wpdb;
$lock = $wpdb->get_row(
$wpdb->prepare(
'SELECT GET_LOCK(%s,%d) as lock_set',
array(
$this->get_key(),
$this->get_timeout(),
)
)
);
return 1 === intval( $lock->lock_set );
}
private function release_lock() {
global $wpdb;
$wpdb->get_row(
$wpdb->prepare(
'SELECT RELEASE_LOCK(%s) as lock_released',
array( $this->get_key() )
)
);
}
/**
* @return bool
*/
public function break_on_timeout() {
return $this->break_on_timeout;
}
/**
* @param bool $break_on_timeout
*/
public function set_break_on_timeout( $break_on_timeout ) {
$this->break_on_timeout = $break_on_timeout;
return $this;
}
/**
* @return int
*/
public function get_timeout() {
return $this->timeout;
}
/**
* @param int $timeout
*/
public function set_timeout( $timeout ) {
$this->timeout = $timeout;
return $this;
}
/**
* @return string
*/
public function get_key() {
return $this->key;
}
/**
* @param string $key
*/
public function set_key( $key ) {
$this->key = $key;
return $this;
}
}

View File

@ -0,0 +1,327 @@
<?php
namespace Smush\Core\Modules\Bulk;
use Smush\Core\Error_Handler;
use Smush\Core\Helper;
use Smush\Core\Server_Utils;
use Smush\Core\Stats\Global_Stats;
use WP_Smush;
class Background_Bulk_Smush {
const REQUIRED_MYSQL_VERSION = '5.6';
/**
* @var Bulk_Smush_Background_Process
*/
private $background_process;
private $mail;
private $logger;
private $global_stats;
private $server_utils;
public function __construct() {
$process_manager = new Background_Process_Manager(
is_multisite(),
get_current_blog_id()
);
$this->background_process = $process_manager->create_process();
$this->mail = new Mail( 'wp_smush_background' );
$this->logger = Helper::logger();
$this->global_stats = Global_Stats::get();
$this->server_utils = new Server_Utils();
if ( ! $this->should_use_background() ) {
return;
}
$this->register_ajax_handler( 'bulk_smush_start', array( $this, 'bulk_smush_start' ) );
$this->register_ajax_handler( 'bulk_smush_cancel', array( $this, 'bulk_smush_cancel' ) );
$this->register_ajax_handler( 'bulk_smush_get_status', array( $this, 'bulk_smush_get_status' ) );
$this->register_ajax_handler( 'bulk_smush_get_global_stats', array( $this, 'bulk_smush_get_global_stats' ) );
add_filter( 'wp_smush_script_data', array( $this, 'localize_background_stats' ) );
add_action( 'init', array( $this, 'cancel_programmatically' ) );
}
public function cancel_programmatically() {
$background_disabled = ! $this->is_background_enabled();
$constant_value = defined( 'WP_SMUSH_STOP_BACKGROUND_PROCESSING' ) && WP_SMUSH_STOP_BACKGROUND_PROCESSING;
$filter_value = apply_filters( 'wp_smush_stop_background_processing', false );
$capability = is_multisite() ? 'manage_network' : 'manage_options';
$param_value = ! empty( $_GET['wp_smush_stop_background_processing'] ) && current_user_can( $capability );
$should_cancel = $background_disabled || $constant_value || $filter_value || $param_value;
$status = $this->background_process->get_status();
if ( $should_cancel && $status->is_in_processing() && ! $status->is_cancelled() ) {
$this->logger->notice( 'Cancelling background processing because a constant/query param/filter indicated that the process needs to be stopped.' );
$this->background_process->cancel();
}
}
public function bulk_smush_start() {
$this->check_ajax_referrer();
$process = $this->background_process;
$in_processing = $process->get_status()->is_in_processing();
if ( $in_processing ) {
// Already in progress
wp_send_json_error();
}
if ( ! Helper::loopback_supported() ) {
$this->logger->error( 'Loopback check failed. Not starting a new background process.' );
wp_send_json_error( array(
'message' => sprintf(
/* translators: %s: a doc link */
esc_html__( 'Your site seems to have an issue with loopback requests. Please try again and if the problem persists find out more %s.', 'wp-smushit' ),
sprintf( '<a target="_blank" href="https://wpmudev.com/docs/wpmu-dev-plugins/smush/#background-processing">%s</a>', esc_html__( 'here', 'wp-smushit' ) )
),
) );
} else {
$this->logger->notice( 'Loopback check successful.' );
}
$tasks = $this->prepare_background_tasks();
if ( $tasks ) {
do_action( 'wp_smush_bulk_smush_start' );
$process->start( $tasks );
wp_send_json_success( $process->get_status()->to_array() );
}
wp_send_json_error();
}
public function bulk_smush_cancel() {
$this->check_ajax_referrer();
$this->background_process->cancel();
wp_send_json_success();
}
public function bulk_smush_get_status() {
$this->check_ajax_referrer();
wp_send_json_success( array_merge(
$this->background_process->get_status()->to_array(),
array(
'in_process_notice' => $this->get_in_process_notice(),
)
) );
}
public function bulk_smush_get_global_stats() {
$this->check_ajax_referrer();
$stats = WP_Smush::get_instance()->admin()->get_global_stats_with_bulk_smush_content();
wp_send_json_success( $stats );
}
private function check_ajax_referrer() {
check_ajax_referer( 'wp-smush-ajax', '_nonce' );
// Check capability.
if ( ! Helper::is_user_allowed( 'manage_options' ) ) {
wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 );
}
}
private function register_ajax_handler( $action, $handler ) {
add_action( "wp_ajax_$action", $handler );
}
/**
* @return Smush_Background_Task[]
*/
private function prepare_background_tasks() {
$smush_tasks = $this->prepare_smush_tasks();
$resmush_tasks = $this->prepare_resmush_tasks();
return array_merge(
$smush_tasks,
$resmush_tasks
);
}
private function prepare_smush_tasks() {
$to_smush = $this->global_stats->get_optimize_list()->get_ids();
if ( empty( $to_smush ) || ! is_array( $to_smush ) ) {
$to_smush = array();
}
return array_map( function ( $image_id ) {
return new Smush_Background_Task(
Smush_Background_Task::TASK_TYPE_SMUSH,
$image_id
);
}, $to_smush );
}
private function prepare_resmush_tasks() {
$to_resmush = $this->global_stats->get_redo_ids();
return array_map( function ( $image_id ) {
return new Smush_Background_Task(
Smush_Background_Task::TASK_TYPE_RESMUSH,
$image_id
);
}, $to_resmush );
}
private function prepare_error_tasks() {
$error_items_to_retry = $this->global_stats->get_error_list()->get_ids();
return array_map( function ( $image_id ) {
return new Smush_Background_Task(
Smush_Background_Task::TASK_TYPE_ERROR,
$image_id
);
}, $error_items_to_retry );
}
public function localize_background_stats( $script_data ) {
global $current_screen;
$is_bulk_smush_page = isset( $current_screen->id )
&& strpos( $current_screen->id, '_page_smush-bulk' ) !== false;
if ( $is_bulk_smush_page ) {
$script_data['bo_stats'] = $this->background_process->get_status()->to_array();
}
return $script_data;
}
/**
* Whether BO is in processing or not.
*
* @return boolean
*/
public function is_in_processing() {
return $this->background_process->get_status()->is_in_processing();
}
/**
* Whether BO is completed or not.
*
* @return boolean
*/
public function is_completed() {
return $this->background_process->get_status()->is_completed();
}
/**
* Get total items.
*
* @return int
*/
public function get_total_items() {
return $this->background_process->get_status()->get_total_items();
}
/**
* Get failed items.
*
* @return int
*/
public function get_failed_items() {
return $this->background_process->get_status()->get_failed_items();
}
/**
* Get email address of recipient.
*
* @return string
*/
public function get_mail_recipient() {
$emails = $this->mail->get_mail_recipients();
return ! empty( $emails ) ? $emails[0] : get_option( 'admin_email' );
}
public function get_in_process_notice() {
return $this->mail->reporting_email_enabled()
? $this->get_email_enabled_notice()
: $this->get_email_disabled_notice();
}
private function get_email_disabled_notice() {
$email_setting_link = sprintf(
'<a href="#background_email-settings-row">%s</a>',
esc_html__( 'Enable the email notification', 'wp-smushit' )
);
/* translators: %s: a link */
return sprintf( __( 'Feel free to close this page while Smush works its magic in the background. %s to receive an email when the process finishes.', 'wp-smushit' ), $email_setting_link );
}
private function get_email_enabled_notice() {
$mail_recipient = $this->get_mail_recipient();
/* translators: %s: Email address */
return sprintf( __( 'Feel free to close this page while Smush works its magic in the background. Well email you at <strong>%s</strong> when its done.', 'wp-smushit' ), $mail_recipient );
}
public function is_background_enabled() {
if ( ! $this->can_use_background() ) {
return false;
}
return defined( 'WP_SMUSH_BACKGROUND' ) && WP_SMUSH_BACKGROUND;
}
public function should_use_background() {
return $this->is_background_enabled()
&& $this->is_background_supported();
}
public function is_background_supported() {
return $this->is_mysql_requirement_met();
}
public function can_use_background() {
return WP_Smush::is_pro() || ! empty( get_site_option( 'wp_smush_pre_3_12_6_site' ) );
}
/**
* We need the right version of MySQL for locks used by the Mutex class
* @return bool|int
*/
private function is_mysql_requirement_met() {
return version_compare( $this->get_actual_mysql_version(), $this->get_required_mysql_version(), '>=' );
}
public function get_required_mysql_version() {
return self::REQUIRED_MYSQL_VERSION;
}
public function get_actual_mysql_version() {
return $this->server_utils->get_mysql_version();
}
public function start_bulk_smush_direct() {
if ( ! $this->should_use_background() ) {
return false;
}
$process = $this->background_process;
$in_processing = $process->get_status()->is_in_processing();
if ( $in_processing ) {
return $process->get_status()->to_array();
}
if ( ! Helper::loopback_supported() ) {
$this->logger->error( 'Loopback check failed. Not starting a new background process.' );
return false;
}
$tasks = $this->prepare_background_tasks();
if ( $tasks ) {
do_action( 'wp_smush_bulk_smush_start' );
$process->start( $tasks );
}
return $process->get_status()->to_array();
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Smush\Core\Modules\Bulk;
use Smush\Core\Modules\Background\Mutex;
class Background_Process_Manager {
const ACTIVE_PROCESSES_EXPIRATION = DAY_IN_SECONDS;
const ACTIVE_PROCESSES_KEY = 'wp_smush_bulk_smush_active_processes';
const MAX_TASKS_PER_REQUEST = 8;
private $is_multisite;
private $current_site_id;
public function __construct( $is_multisite, $current_site_id ) {
$this->is_multisite = $is_multisite;
$this->current_site_id = $current_site_id;
}
public function create_process() {
$identifier = $this->make_process_identifier();
$background_process = new Bulk_Smush_Background_Process( $identifier );
$tasks_per_request = $this->calculate_tasks_per_request();
if ( $tasks_per_request ) {
$background_process->set_tasks_per_request( $tasks_per_request );
}
$this->register( $identifier );
return $background_process;
}
public function register( $identifier ) {
$register = function ( $identifier ) {
$this->register_active_process( $identifier );
};
$unregister = function ( $identifier ) {
$this->unregister_process( $identifier );
};
add_action( "{$identifier}_started", $register );
add_action( "{$identifier}_completed", $unregister );
add_action( "{$identifier}_cancelled", $unregister );
}
private function make_process_identifier() {
$identifier = 'wp_smush_bulk_smush_background_process';
if ( $this->is_multisite ) {
$post_fix = "_" . $this->current_site_id;
$identifier .= $post_fix;
}
return $identifier;
}
private function get_active_processes() {
$active_processes = get_site_transient( self::ACTIVE_PROCESSES_KEY );
return empty( $active_processes ) || ! is_array( $active_processes )
? array()
: $active_processes;
}
private function mutex( $operation ) {
$mutex = new Mutex( self::ACTIVE_PROCESSES_KEY );
$mutex->execute( $operation );
}
private function register_active_process( $identifier ) {
$this->mutex( function () use ( $identifier ) {
$active_processes = $this->get_active_processes();
$active_processes[ $identifier ] = $identifier;
$this->set_active_processes( $active_processes );
} );
}
private function unregister_process( $identifier ) {
$this->mutex( function () use ( $identifier ) {
$active_processes = $this->get_active_processes();
unset( $active_processes[ $identifier ] );
$this->set_active_processes( $active_processes );
} );
}
private function set_active_processes( $active_processes ) {
set_site_transient(
self::ACTIVE_PROCESSES_KEY,
array_unique( $active_processes ),
self::ACTIVE_PROCESSES_EXPIRATION
);
}
private function calculate_tasks_per_request() {
$active_processes_count = count( $this->get_active_processes() );
$should_limit = $this->is_multisite && $active_processes_count > 1;
if ( ! $should_limit ) {
return false;
}
// Divide the available slots between the active processes
$tasks_per_request = intval( floor( self::MAX_TASKS_PER_REQUEST / $active_processes_count ) );
// At least 1 task per request
return max( $tasks_per_request, 1 );
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace Smush\Core\Modules\Bulk;
use Smush\Core\Core;
use Smush\Core\Helper;
use Smush\Core\Media\Media_Item_Cache;
use Smush\Core\Media\Media_Item_Optimizer;
use Smush\Core\Modules\Background\Background_Process;
use Smush\Core\Modules\Smush;
use Smush\Core\Png2Jpg\Png2Jpg_Optimization;
use Smush\Core\Resize\Resize_Optimization;
use Smush\Core\Smush\Smush_Media_Item_Stats;
use Smush\Core\Smush\Smush_Optimization;
use WP_Smush;
class Bulk_Smush_Background_Process extends Background_Process {
public function __construct( $identifier ) {
parent::__construct( $identifier );
$this->set_logger( Helper::logger() );
}
/**
* @param $task Smush_Background_Task
*
* @return boolean
*/
protected function task( $task ) {
if ( ! is_a( $task, Smush_Background_Task::class ) || ! $task->is_valid() ) {
Helper::logger()->error( 'An invalid background task was encountered.' );
return false;
}
$attachment_id = $task->get_image_id();
$media_item = Media_Item_Cache::get_instance()->get( $attachment_id );
$optimizer = new Media_Item_Optimizer( $media_item );
$optimized = $optimizer->optimize();
if ( ! $optimized ) {
Helper::logger()->error( "Error encountered while smushing attachment ID $attachment_id:" . $optimizer->get_errors()->get_error_message() );
return false;
}
$smush_optimization = $optimizer->get_optimization( Smush_Optimization::KEY );
/**
* @var $smush_stats Smush_Media_Item_Stats
*/
$smush_stats = $smush_optimization->get_stats();
$resize_optimization = $optimizer->get_optimization( Resize_Optimization::KEY );
$png2jpg_optimization = $optimizer->get_optimization( Png2Jpg_Optimization::KEY );
do_action(
'image_smushed',
$attachment_id,
array(
'count' => $smush_optimization->get_optimized_sizes_count(),
'size_before' => $smush_stats->get_size_before(),
'size_after' => $smush_stats->get_size_after(),
'savings_resize' => $resize_optimization ? $resize_optimization->get_stats()->get_bytes() : 0,
'savings_conversion' => $png2jpg_optimization ? $png2jpg_optimization->get_stats()->get_bytes() : 0,
'is_lossy' => $smush_stats->is_lossy(),
)
);
return true;
}
/**
* Email when bulk smush complete.
*/
protected function complete() {
parent::complete();
// Send email.
if ( $this->get_status()->get_total_items() ) {
$mail = new Mail( 'wp_smush_background' );
if ( $mail->reporting_email_enabled() ) {
if ( $mail->send_email() ) {
Helper::logger()->notice(
sprintf(
'Bulk Smush completed for %s, and sent a summary email to %s at %s.',
get_site_url(),
join( ',', $mail->get_mail_recipients() ),
wp_date( 'd/m/y H:i:s' )
)
);
} else {
Helper::logger()->error(
sprintf(
'Bulk Smush completed for %s, but could not send a summary email to %s at %s.',
get_site_url(),
join( ',', $mail->get_mail_recipients() ),
wp_date( 'd/m/y H:i:s' )
)
);
}
} else {
Helper::logger()->info( sprintf( 'Bulk Smush completed for %s, and reporting email is disabled.', get_site_url() ) );
}
}
do_action( 'wp_smush_bulk_smush_completed' );
}
}

View File

@ -0,0 +1,202 @@
<?php
/**
* Handle mail for background process.
*
* @package Smush\Core\Modules\Helpers
*/
namespace Smush\Core\Modules\Bulk;
use Smush\Core\Settings;
use WP_Smush;
use Smush\Core\Modules\Helpers;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Mail
*/
class Mail extends Helpers\Mail {
/**
* Plugin id.
*/
const PLUGIN_ID = 912164;
/**
* View class.
*
* @var Helpers\View
*/
private $view;
/**
* Constructor.
*
* @param string $identifier Identifier.
*/
public function __construct( $identifier ) {
parent::__construct( $identifier );
$this->view = new Helpers\View();
$this->view->set_template_dir( WP_SMUSH_DIR . 'app/' );
}
/**
* Whether to receive email or not.
*
* @return bool
*/
public function reporting_email_enabled() {
return Settings::get_instance()->get( 'background_email' );
}
/**
* Get sender name.
*
* @return string
*/
protected function get_sender_name() {
if ( WP_Smush::is_pro() && $this->whitelabel->enabled() ) {
$plugin_label = $this->whitelabel->get_plugin_name( self::PLUGIN_ID );
if ( empty( $plugin_label ) ) {
$plugin_label = __( 'Bulk Compression', 'wp-smushit' );
}
} else {
$plugin_label = WP_Smush::is_pro() ? __( 'Smush Pro', 'wp-smushit' ) : __( 'Smush', 'wp-smushit' );
}
return $plugin_label;
}
/**
* Get email subject.
*
* @return string
*/
protected function get_mail_subject() {
$site_url = get_site_url();
$site_url = preg_replace( '#http(s)?://(www.)?#', '', $site_url );
if ( $this->whitelabel->enabled() ) {
/* translators: %s - Site Url */
return sprintf( __( 'Bulk compression completed for %s', 'wp-smushit' ), esc_html( $site_url ) );
}
/* translators: %s - Site Url */
return sprintf( __( 'Bulk Smush completed for %s', 'wp-smushit' ), esc_html( $site_url ) );
}
/**
* Get email message.
*
* @return string
*/
protected function get_mail_message() {
if ( $this->whitelabel->enabled() ) {
$title = __( 'Bulk Compression', 'wp-smushit' );
$temp_file_name = 'email/index-whitelabel';
} else {
$title = __( 'Bulk Smush', 'wp-smushit' );
$temp_file_name = 'email/index';
}
return $this->view->get_template_content(
$temp_file_name,
array(
'title' => $title,
'content_body' => $this->get_summary_content(),
'content_upsell' => $this->get_upsell_content(),
)
);
}
/**
* Get the summary content of bulk smush.
*
* @return string
*/
private function get_summary_content() {
$bg_optimization = WP_Smush::get_instance()->core()->mod->bg_optimization;
$site_url = get_site_url();
$total_items = $bg_optimization->get_total_items();
$failed_items = $bg_optimization->get_failed_items();
if ( empty( $failed_items ) ) {
$redirect_url = is_network_admin() ? network_admin_url( 'admin.php?page=smush' ) : admin_url( 'admin.php?page=smush-bulk' );
} else {
$redirect_url = admin_url( 'upload.php?mode=list&attachment-filter=post_mime_type:image&m=0&smush-filter=failed_processing' );
}
return $this->view->get_template_content(
'email/bulk-smush',
array_merge(
array(
'site_url' => $site_url,
'name' => $this->get_recipient_name(),
'total_items' => $total_items,
'failed_items' => $failed_items,
'smushed_items' => $total_items - $failed_items,
'redirect_url' => $redirect_url,
),
$this->get_summary_template_args()
)
);
}
/**
* Get extra template arguments.
*
* @return array
*/
private function get_summary_template_args() {
$bg_optimization = WP_Smush::get_instance()->core()->mod->bg_optimization;
$failed_items = $bg_optimization->get_failed_items();
if ( $failed_items > 0 ) {
$failed_msg = __( 'The number of images unsuccessfully compressed (find out why below).', 'wp-smushit' );
} else {
$failed_msg = __( 'The number of images unsuccessfully compressed.', 'wp-smushit' );
}
if ( $this->whitelabel->enabled() ) {
return array(
/* Translators: %s: Site URL */
'mail_title' => __( 'Bulk compression completed for %s', 'wp-smushit' ),
'mail_desc' => __( 'The bulk compress you actioned has successfully completed. Heres a quick summary of the results:', 'wp-smushit' ),
'total_title' => __( 'Total image attachments', 'wp-smushit' ),
'total_desc' => __( 'The number of images analyzed during the bulk compress.', 'wp-smushit' ),
'smushed_title' => __( 'Images compressed successfully', 'wp-smushit' ),
'smushed_desc' => __( 'The number of images successfully compressed.', 'wp-smushit' ),
'failed_title' => __( 'Images failed to compress', 'wp-smushit' ),
'failed_desc' => $failed_msg,
);
}
return array(
/* Translators: %s: Site URL */
'mail_title' => __( 'Bulk Smush completed for %s', 'wp-smushit' ),
'mail_desc' => __( 'The bulk smush you actioned has successfully completed. Heres a quick summary of the results:', 'wp-smushit' ),
'total_title' => __( 'Total image attachments', 'wp-smushit' ),
'total_desc' => __( 'The number of images analyzed during the bulk smush.', 'wp-smushit' ),
'smushed_title' => __( 'Images smushed successfully', 'wp-smushit' ),
'smushed_desc' => __( 'The number of images successfully compressed.', 'wp-smushit' ),
'failed_title' => __( 'Images failed to smush', 'wp-smushit' ),
'failed_desc' => $failed_msg,
);
}
/**
* Get upsell CDN content.
*/
private function get_upsell_content() {
if ( WP_Smush::is_pro() ) {
return;
}
$upsell_url = add_query_arg(
array(
'utm_source' => 'smush',
'utm_medium' => 'plugin',
'utm_campaign' => 'smush_bulksmush_bo_email',
),
'https://wpmudev.com/project/wp-smush-pro/'
);
return $this->view->get_template_content(
'email/upsell-cdn',
array(
'upsell_url' => $upsell_url,
)
);
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Smush\Core\Modules\Bulk;
class Smush_Background_Task implements \Serializable {
const TASK_TYPE_SMUSH = 'SMUSH';
const TASK_TYPE_RESMUSH = 'RESMUSH';
const TASK_TYPE_ERROR = 'ERROR';
private $type;
private $image_id;
public function __construct( $type, $image_id ) {
$this->type = $type;
$this->image_id = $image_id;
}
public function is_valid() {
return $this->is_type_valid( $this->type )
&& $this->is_image_id_valid( $this->image_id );
}
private function is_type_valid( $type ) {
$valid_types = array( self::TASK_TYPE_SMUSH, self::TASK_TYPE_RESMUSH );
return in_array( $type, $valid_types );
}
private function is_image_id_valid( $image_id ) {
return intval( $image_id ) > 0;
}
/**
* @return mixed
*/
public function get_type() {
return $this->type;
}
/**
* @param mixed $type
*/
public function set_type( $type ) {
$this->type = $type;
}
/**
* @return mixed
*/
public function get_image_id() {
return $this->image_id;
}
/**
* @param mixed $image_id
*/
public function set_image_id( $image_id ) {
$this->image_id = $image_id;
}
private static function get( $array, $key ) {
return empty( $array[ $key ] ) ? null : $array[ $key ];
}
public function serialize() {
return json_encode( $this->__serialize() );
}
public function unserialize( $data ) {
$this->__unserialize( json_decode( $data, true ) );
}
public function __unserialize( $data ) {
$type = self::get( $data, 'type' );
$type = $this->is_type_valid( $type ) ? $type : '';
$this->set_type( $type );
$image_id = self::get( $data, 'image_id' );
$image_id = $this->is_image_id_valid( $image_id ) ? $image_id : 0;
$this->set_image_id( $image_id );
}
public function __serialize() {
return array(
'type' => $this->type,
'image_id' => $this->image_id,
);
}
public function __toString() {
return json_encode( $this->__serialize() );
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* Abstract module class: Abstract_Module
*
* @since 3.0
* @package Smush\Core\Modules
*/
namespace Smush\Core\Modules;
use Smush\Core\Settings;
use WP_Smush;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Abstract_Module
*
* @since 3.0
*/
abstract class Abstract_Module {
/**
* Module slug.
*
* @var string
*/
protected $slug;
/**
* Whether module is pro or not.
*
* @var string
*/
protected $is_pro = false;
/**
* Settings instance.
*
* @since 3.0
* @var Settings
*/
protected $settings;
/**
* Abstract_Module constructor.
*
* @since 3.0
*/
public function __construct() {
$this->settings = Settings::get_instance();
$this->init();
}
/**
* Initialize the module.
*
* Do not use __construct in modules, instead use init().
*
* @since 3.0
*/
protected function init() {}
/**
* Return true if the module is activated.
*
* @return boolean
*/
public function is_active() {
if ( $this->slug ) {
if ( ! $this->is_pro ) {
return (bool) $this->settings->get( $this->slug );
} else {
return WP_Smush::is_pro() && $this->settings->get( $this->slug );
}
}
return true;
}
/**
* Return module slug.
*
* @return string.
*/
public function get_slug() {
return $this->slug;
}
}

View File

@ -0,0 +1,885 @@
<?php
/**
* Smush backup class
*
* @package Smush\Core\Modules
*/
namespace Smush\Core\Modules;
use Smush\Core\Core;
use Smush\Core\Helper;
use Smush\Core\Media\Media_Item_Cache;
use Smush\Core\Media\Media_Item_Optimizer;
use WP_Smush;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Backup
*/
class Backup extends Abstract_Module {
/**
* Module slug.
*
* @var string
*/
protected $slug = 'backup';
/**
* Key for storing file path for image backup
*
* @var string
*/
private $backup_key = 'smush-full';
/**
* Backup constructor.
*/
public function init() {
// Handle Restore operation.
//add_action( 'wp_ajax_smush_restore_image', array( $this, 'restore_image' ) );
// Handle bulk restore from modal.
add_action( 'wp_ajax_get_image_count', array( $this, 'get_image_count' ) );
add_action( 'wp_ajax_restore_step', array( $this, 'restore_step' ) );
}
/**
* Check if the backup file exists.
*
* @param int $attachment_id Attachment ID.
* @param string $file_path Current file path.
* @return bool True if the backup file exists, false otherwise.
*/
public function backup_exists( $attachment_id, $file_path = false ) {
$media_item = Media_Item_Cache::get_instance()->get( $attachment_id );
return $media_item->backup_file_exists();
}
/**
* Generate unique .bak file.
*
* @param string $bak_file The .bak file.
* @param int $attachment_id Attachment ID.
* @return string Returns a unique backup file.
*/
private function generate_unique_bak_file( $bak_file, $attachment_id ) {
if ( strpos( $bak_file, '.bak' ) && Helper::file_exists( $bak_file, $attachment_id ) ) {
$count = 1;
$ext = Helper::get_file_ext( $bak_file );
$ext = ".bak.$ext";
$file_without_ext = rtrim( $bak_file, $ext );
$bak_file = $file_without_ext . '-' . $count . $ext;
while ( Helper::file_exists( $bak_file, $attachment_id ) ) {
$count++;
$bak_file = $file_without_ext . '-' . $count . $ext;
}
return $bak_file;
}
return $bak_file;
}
/**
* Creates a backup of file for the given attachment path.
*
* Checks if there is an existing backup, else create one.
*
* @param string $file_path File path.
* @param int $attachment_id Attachment ID.
*
* @return void
*/
public function create_backup( $file_path, $attachment_id ) {
if ( empty( $file_path ) || empty( $attachment_id ) ) {
return;
}
// If backup not enabled, return.
if ( ! $this->is_active() ) {
return;
}
/**
* If [ not compress original ]:
* if [ is-scaled.file ]:
* Backup original file.
* elseif [ no-resize + no-png2jpg ]:
* We don't need to backup, let user try to use regenerate plugin
* to restore the compressed thumbnails size.
* else: continue as compress_original.
* else:
* We don't need to backup if we had a backup file for PNG2JPG,
* or .bak file. But if the .bak file is from third party, we will generate our new backup file.
* end.
*/
// We might not need to backup the file if we're not compressing original.
if ( ! $this->settings->get( 'original' ) ) {
/**
* Add WordPress 5.3 support for -scaled images size, and those can always be used to restore.
* Maybe user doesn't want to auto-scale JPG from WP for some images,
* so we allow user to restore it even we don't Smush this image.
*/
if ( false !== strpos( $file_path, '-scaled.' ) && function_exists( 'wp_get_original_image_path' ) ) {
// Scaled images already have a backup. Use that and don't create a new one.
$file_path = Helper::get_attached_file( $attachment_id, 'backup' );// Supported S3.
if ( file_exists( $file_path ) ) {
/**
* We do not need an additional backup file if we're not compressing originals.
* But we need to save the original file as a backup file in the metadata to allow restoring this image later.
*/
$this->add_to_image_backup_sizes( $attachment_id, $file_path );
return;
}
}
$mod = WP_Smush::get_instance()->core()->mod;
// If there is not *-scaled.jpg file, we don't need to backup the file if we don't work with original file.
if ( ! $mod->resize->is_active() && ! $mod->png2jpg->is_active() ) {
/**
* In this case, we can add the meta to save the original file as a backup file,
* but if there is a lot of images, might take a lot of row for postmeta table,
* so leave it for user to use a "regenerate thumbnail" plugin instead.
*/
Helper::logger()->backup()->info( sprintf( 'Not modify the original file [%s(%d)], skip the backup.', Helper::clean_file_path( $file_path ), $attachment_id ) );
return;
}
$should_backup = false;
// We should backup this image if we can resize it.
if ( $mod->resize->is_active() && $mod->resize->should_resize( $attachment_id ) ) {
$should_backup = true;
}
// We should backup this image if we can convert it from PNG to JPEG.
if (
! $should_backup && $mod->png2jpg->is_active() && Helper::get_file_ext( $file_path, 'png' )
&& $mod->png2jpg->can_be_converted( $attachment_id, 'full', 'image/png', $file_path )
) {
$should_backup = true;
}
// As we don't work with the original file, so we don't back it up.
if ( ! $should_backup ) {
Helper::logger()->backup()->info( sprintf( 'Not modify the original file [%s(%d)], skip the backup.', Helper::clean_file_path( $file_path ), $attachment_id ) );
return;
}
}
/**
* Check if exists backup file from meta,
* Because we will compress the original file,
* so we only keep the backup file if there is PNG2JPG or .bak file.
*/
$backup_path = $this->get_backup_file( $attachment_id, $file_path );
if ( $backup_path ) {
/**
* We will compress the original file so the backup file have to different from current file.
* And the backup file should be the same folder with the main file.
*/
if ( $backup_path !== $file_path && dirname( $file_path ) === dirname( $backup_path ) ) {
// Check if there is a .bak file or PNG2JPG file.
if ( strpos( $backup_path, '.bak' ) || ( Helper::get_file_ext( $backup_path, 'png' ) && Helper::get_file_ext( $file_path, 'jpg' ) ) ) {
Helper::logger()->backup()->info( sprintf( 'Found backed up file [%s(%d)].', Helper::clean_file_path( $backup_path ), $attachment_id ) );
return;
}
}
}
/**
* To avoid the conflict with 3rd party, we will generate a new backup file.
* Because how about if 3rd party delete the backup file before trying to restore it from Smush?
* We only try to use their bak file while restoring the backup file.
*/
$backup_path = $this->generate_unique_bak_file( $this->get_image_backup_path( $file_path ), $attachment_id );
/**
* We need to save the .bak file to the meta. Because if there is a PNG, when we convert PNG2JPG,
* the converted file is .jpg, so the bak file will be .bak.jpg not .bak.png
*/
// Store the backup path in image backup sizes.
if ( copy( $file_path, $backup_path ) ) {
$this->add_to_image_backup_sizes( $attachment_id, $backup_path );
} else {
Helper::logger()->backup()->error( sprintf( 'Cannot backup file [%s(%d)].', Helper::clean_file_path( $file_path ), $attachment_id ) );
}
}
/**
* Store new backup path for the image.
*
* @param int $attachment_id Attachment ID.
* @param string $backup_path Backup path.
* @param string $backup_key Backup key.
*/
public function add_to_image_backup_sizes( $attachment_id, $backup_path, $backup_key = '' ) {
if ( empty( $attachment_id ) || empty( $backup_path ) ) {
return;
}
// Get the Existing backup sizes.
$backup_sizes = $this->get_backup_sizes( $attachment_id );
if ( empty( $backup_sizes ) ) {
$backup_sizes = array();
}
// Prevent phar deserialization vulnerability.
if ( false !== stripos( $backup_path, 'phar://' ) ) {
Helper::logger()->backup()->info( sprintf( 'Prevent phar deserialization vulnerability [%s(%d)].', Helper::clean_file_path( $backup_path ), $attachment_id ) );
return;
}
// Return if backup file doesn't exist.
if ( ! file_exists( $backup_path ) ) {
Helper::logger()->backup()->notice( sprintf( 'Back file [%s(%d)] does not exist.', Helper::clean_file_path( $backup_path ), $attachment_id ) );
return;
}
list( $width, $height ) = getimagesize( $backup_path );
// Store our backup path.
$backup_key = empty( $backup_key ) ? $this->backup_key : $backup_key;
$backup_sizes[ $backup_key ] = array(
'file' => wp_basename( $backup_path ),
'width' => $width,
'height' => $height,
);
wp_cache_delete( 'images_with_backups', 'wp-smush' );
update_post_meta( $attachment_id, '_wp_attachment_backup_sizes', $backup_sizes );
}
/**
* Get backup sizes.
*
* @param int $attachment_id Attachment ID.
* @return mixed False or an array of backup sizes.
*/
public function get_backup_sizes( $attachment_id ) {
return get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
}
/**
* Back up an image if it hasn't backed up yet.
*
* @since 3.9.6
*
* @param int $attachment_id Image id.
* @param string $backup_file File path to back up.
*
* Note, we used it to manage backup PNG2JPG to keep the backup file is the original file to avoid conflicts with a duplicate PNG file.
* If the backup file exists it will rename the original backup file to
* the new backup file.
*
* @return bool True if added this file to the backup sizes, false if the image was backed up before.
*/
public function maybe_backup_image( $attachment_id, $backup_file ) {
if ( ! file_exists( $backup_file ) ) {
return false;
}
// We don't use .bak file from 3rd party while backing up.
$backed_up_file = $this->get_backup_file( $attachment_id, $backup_file );
$was_backed_up = true;
if ( $backed_up_file && $backed_up_file !== $backup_file && dirname( $backed_up_file ) === dirname( $backup_file ) ) {
$was_backed_up = rename( $backed_up_file, $backup_file );
}
// Backup the image.
if ( $was_backed_up ) {
$this->add_to_image_backup_sizes( $attachment_id, $backup_file );
}
return $was_backed_up;
}
/**
* Get the backup file from the meta.
*
* @since 3.9.6
*
* @param int $id Image ID.
* @param string $file_path Current file path.
*
* @return bool|null Backup file or false|null if the image doesn't exist.
*/
public function get_backup_file( $id, $file_path = false ) {
if ( empty( $id ) ) {
return null;
}
if ( empty( $file_path ) ) {
// Get unfiltered path file.
$file_path = Helper::get_attached_file( $id, 'original' );
// If the file path is still empty, nothing to check here.
if ( empty( $file_path ) ) {
return null;
}
}
// Initial result.
$backup_file = false;
// Try to get the backup file from _wp_attachment_backup_sizes.
$backup_sizes = $this->get_backup_sizes( $id );
// Check if we have backup file from the metadata.
if ( $backup_sizes ) {
// Try to get the original file first.
if ( isset( $backup_sizes[ $this->backup_key ]['file'] ) ) {
$original_file = str_replace( wp_basename( $file_path ), wp_basename( $backup_sizes[ $this->backup_key ]['file'] ), $file_path );
if ( Helper::file_exists( $original_file, $id ) ) {
$backup_file = $original_file;
}
}
// Try to check it from legacy original file or from the resized PNG file.
if ( ! $backup_file ) {
// If we don't have the original backup path in backup sizes, check for legacy original file path. It's for old version < V.2.7.0.
$original_file = get_post_meta( $id, 'wp-smush-original_file', true );
if ( ! empty( $original_file ) ) {
// For old version < v.2.7.0, we are saving meta['file'] or _wp_attached_file.
$original_file = Helper::original_file( $original_file );
if ( Helper::file_exists( $original_file, $id ) ) {
$backup_file = $original_file;
// As we don't use this meta key so save it as a full backup file and delete the old metadata.
WP_Smush::get_instance()->core()->mod->backup->add_to_image_backup_sizes( $id, $backup_file );
delete_post_meta( $id, 'wp-smush-original_file' );
}
}
// Check the backup file from resized PNG file.
if ( ! $backup_file && isset( $backup_sizes['smush_png_path']['file'] ) ) {
$original_file = str_replace( wp_basename( $file_path ), wp_basename( $backup_sizes['smush_png_path']['file'] ), $file_path );
if ( Helper::file_exists( $original_file, $id ) ) {
$backup_file = $original_file;
}
}
}
}
return $backup_file;
}
/**
* Restore the image and its sizes from backup
*
* @param string $attachment_id Attachment ID.
* @param bool $resp Send JSON response or not.
*
* @return bool
*/
public function restore_image( $attachment_id = '', $resp = true ) {
// TODO: (stats refactor) handle properly
// If no attachment id is provided, check $_POST variable for attachment_id.
if ( empty( $attachment_id ) ) {
// Check Empty fields.
if ( empty( $_POST['attachment_id'] ) || empty( $_POST['_nonce'] ) ) {
wp_send_json_error(
array(
'error_msg' => esc_html__( 'Error in processing restore action, fields empty.', 'wp-smushit' ),
)
);
}
$nonce_value = filter_input( INPUT_POST, '_nonce', FILTER_SANITIZE_SPECIAL_CHARS );
$attachment_id = filter_input( INPUT_POST, 'attachment_id', FILTER_SANITIZE_NUMBER_INT );
if ( ! wp_verify_nonce( $nonce_value, "wp-smush-restore-$attachment_id" ) ) {
wp_send_json_error(
array(
'error_msg' => esc_html__( 'Image not restored, nonce verification failed.', 'wp-smushit' ),
)
);
}
// Check capability.
if ( ! Helper::is_user_allowed( 'upload_files' ) ) {
wp_send_json_error(
array(
'error_msg' => esc_html__( "You don't have permission to work with uploaded files.", 'wp-smushit' ),
)
);
}
}
$attachment_id = (int) $attachment_id;
$mod = WP_Smush::get_instance()->core()->mod;
// Set an option to avoid the smush-restore-smush loop.
set_transient( 'wp-smush-restore-' . $attachment_id, 1, HOUR_IN_SECONDS );
/**
* Delete WebP.
*
* Run WebP::delete_images always even when the module is deactivated.
*
* @since 3.8.0
*/
$mod->webp->delete_images( $attachment_id );
// Restore Full size -> get other image sizes -> restore other images.
// Get the Original Path, supported S3.
$file_path = Helper::get_attached_file( $attachment_id, 'original' );
// Store the restore success/failure for full size image.
$restored = false;
// Retrieve backup file.
$backup_full_path = $this->get_backup_file( $attachment_id, $file_path );
// Is restoring the PNG which is converted to JPG or not.
$restore_png = false;
/**
* Fires before restoring a file.
*
* @since 3.9.6
*
* @param string|false $backup_full_path Full backup path.
* @param int $attachment_id Attachment id.
* @param string $file_path Original unfiltered file path.
*
* @hooked Smush\Core\Integrations\s3::maybe_download_file()
*/
do_action( 'wp_smush_before_restore_backup', $backup_full_path, $attachment_id, $file_path );
// Finally, if we have the backup path, perform the restore operation.
if ( ! empty( $backup_full_path ) ) {
// If the backup file is the same as the main file, we only need to re-generate the metadata.
if ( $backup_full_path === $file_path ) {
$restored = true;
} else {
// Is real backup file or .bak file.
$is_real_filename = false === strpos( $backup_full_path, '.bak' );
$restore_png = Helper::get_file_ext( trim( $backup_full_path ), 'png' ) && ! Helper::get_file_ext( $file_path, 'png' );
if ( $restore_png ) {
// Restore PNG full size.
$org_backup_full_path = $backup_full_path;
if ( ! $is_real_filename ) {
// Try to get a unique file name.
$dirname = dirname( $backup_full_path );
$new_file_name = wp_unique_filename( $dirname, wp_basename( str_replace( '.bak', '', $backup_full_path ) ) );
$new_png_file = path_join( $dirname, $new_file_name );
// Restore PNG full size.
$restored = copy( $backup_full_path, $new_png_file );
if ( $restored ) {
// Assign the new PNG file to the backup file.
$backup_full_path = $new_png_file;
}
} else {
$restored = true;
}
// Restore all other image sizes.
if ( $restored ) {
$metadata = $this->restore_png( $attachment_id, $backup_full_path, $file_path );
$restored = ! empty( $metadata );
if ( $restored && ! $is_real_filename ) {
// Reset the backup file to delete it later.
$backup_full_path = $org_backup_full_path;
}
}
} else {
// If file exists, corresponding to our backup path - restore.
if ( ! $is_real_filename ) {
$restored = copy( $backup_full_path, $file_path );
} else {
$restored = true;
}
}
// Remove the backup, if we were able to restore the image.
if ( $restored ) {
// Remove our backup file.
$this->remove_from_backup_sizes( $attachment_id );
/**
* Delete our backup file if it's .bak file, we will try to backup later when running Smush.
*/
if ( ! $is_real_filename ) {
// It will also delete file from the cloud, e.g. S3.
Helper::delete_permanently( array( $this->backup_key => $backup_full_path ), $attachment_id, false );
}
}
}
} else {
Helper::logger()->backup()->warning( sprintf( 'Backup file [%s(%d)] does not exist.', Helper::clean_file_path( $backup_full_path ), $attachment_id ) );
}
/**
* Regenerate thumbnails
*
* All this is handled in self::restore_png().
*/
if ( $restored ) {
if ( ! $restore_png ) {
// Generate all other image size, and update attachment metadata.
$metadata = wp_generate_attachment_metadata( $attachment_id, $file_path );
}
// Update metadata to db if it was successfully generated.
if ( ! empty( $metadata ) && ! is_wp_error( $metadata ) ) {
Helper::wp_update_attachment_metadata( $attachment_id, $metadata );
} else {
Helper::logger()->backup()->warning( sprintf( 'Meta file [%s(%d)] is empty.', Helper::clean_file_path( $file_path ), $attachment_id ) );
}
}
/**
* Fires before restoring a file.
*
* @since 3.9.6
*
* @param bool $restored Restore status.
* @param string|false $backup_full_path Full backup path.
* @param int $attachment_id Attachment id.
* @param string $file_path Original unfiltered file path.
*/
do_action( 'wp_smush_after_restore_backup', $restored, $backup_full_path, $attachment_id, $file_path );
// If any of the image is restored, we count it as success.
if ( $restored ) {
// Remove the Meta, And send json success.
delete_post_meta( $attachment_id, Smush::$smushed_meta_key );
// Remove PNG to JPG conversion savings.
delete_post_meta( $attachment_id, 'wp-smush-pngjpg_savings' );
// Remove Original File.
delete_post_meta( $attachment_id, 'wp-smush-original_file' );
// Delete resize savings.
delete_post_meta( $attachment_id, 'wp-smush-resize_savings' );
// Remove lossy flag.
delete_post_meta( $attachment_id, 'wp-smush-lossy' );
// Clear backups cache.
wp_cache_delete( 'images_with_backups', 'wp-smush' );
Core::remove_from_smushed_list( $attachment_id );
// Get the Button html without wrapper.
$button_html = WP_Smush::get_instance()->library()->generate_markup( $attachment_id );
// Release the attachment after restoring.
delete_transient( 'wp-smush-restore-' . $attachment_id );
if ( ! $resp ) {
return true;
}
$size = file_exists( $file_path ) ? filesize( $file_path ) : 0;
if ( $size > 0 ) {
$update_size = size_format( $size ); // Used in js to update image stat.
}
wp_send_json_success(
array(
'stats' => $button_html,
'new_size' => isset( $update_size ) ? $update_size : 0,
)
);
}
// Release the attachment after restoring.
delete_transient( 'wp-smush-restore-' . $attachment_id );
if ( $resp ) {
wp_send_json_error( array( 'error_msg' => esc_html__( 'Unable to restore image', 'wp-smushit' ) ) );
}
return false;
}
/**
* Restore PNG.
*
* @param int $attachment_id Attachment ID.
* @param string $backup_file_path Full backup file, the result of self::get_backup_file().
* @param string $file_path File path.
*
* @since 3.9.10 Moved wp_update_attachment_metadata into self::restore_image() after deleting the backup file,
* in order to support S3 - @see SMUSH-1141.
*
* @return bool|array
*/
private function restore_png( $attachment_id, $backup_file_path, $file_path ) {
if ( empty( $attachment_id ) || empty( $backup_file_path ) || empty( $file_path ) ) {
return false;
}
$meta = array();
// Else get the Attachment details.
/**
* For Full Size
* 1. Get the original file path
* 2. Update the attachment metadata and all other meta details
* 3. Delete the JPEG
* 4. And we're done
* 5. Add an action after updating the URLs, that'd allow the users to perform an additional search, replace action
*/
if ( file_exists( $backup_file_path ) ) {
$mod = WP_Smush::get_instance()->core()->mod;
// Update the path details in meta and attached file, replace the image.
$meta = $mod->png2jpg->update_image_path( $attachment_id, $file_path, $backup_file_path, $meta, 'full', 'restore' );
$files_to_remove = array();
// Unlink JPG after updating attached file.
if ( ! empty( $meta['file'] ) && wp_basename( $backup_file_path ) === wp_basename( $meta['file'] ) ) {
/**
* Note, we use size key smush-png2jpg-full for PNG2JPG file to support S3 private media,
* to remove converted JPG file after restoring in private folder.
*
* @see Smush\Core\Integrations\S3::get_object_key()
*/
$files_to_remove['smush-png2jpg-full'] = $file_path;
}
$jpg_meta = wp_get_attachment_metadata( $attachment_id );
foreach ( $jpg_meta['sizes'] as $size_key => $size_data ) {
$size_path = str_replace( wp_basename( $backup_file_path ), wp_basename( $size_data['file'] ), $backup_file_path );
// Add to delete the thumbnails jpg.
$files_to_remove[ $size_key ] = $size_path;
}
// Re-generate metadata for PNG file.
$metadata = wp_generate_attachment_metadata( $attachment_id, $backup_file_path );
// Perform an action after the image URL is updated in post content.
do_action( 'wp_smush_image_url_updated', $attachment_id, $file_path, $backup_file_path );
} else {
Helper::logger()->backup()->warning( sprintf( 'Backup file [%s(%d)] does not exist.', Helper::clean_file_path( $backup_file_path ), $attachment_id ) );
}
if ( ! empty( $metadata ) ) {
// Delete jpg files, we also try to delete these files on cloud, e.g S3.
Helper::delete_permanently( $files_to_remove, $attachment_id, false );
return $metadata;
} else {
Helper::logger()->backup()->warning( sprintf( 'Meta file [%s(%d)] is empty.', Helper::clean_file_path( $backup_file_path ), $attachment_id ) );
}
return false;
}
/**
* Remove a specific backup key from the backup size array.
*
* @param int $attachment_id Attachment ID.
*/
private function remove_from_backup_sizes( $attachment_id ) {
// Get backup sizes.
$backup_sizes = $this->get_backup_sizes( $attachment_id );
// If we don't have any backup sizes list or if the particular key is not set, return.
if ( empty( $backup_sizes ) || ! isset( $backup_sizes[ $this->backup_key ] ) ) {
return;
}
unset( $backup_sizes[ $this->backup_key ] );
if ( empty( $backup_sizes ) ) {
delete_post_meta( $attachment_id, '_wp_attachment_backup_sizes' );
} else {
update_post_meta( $attachment_id, '_wp_attachment_backup_sizes', $backup_sizes );
}
}
/**
* Get the attachments that can be restored.
*
* @since 3.6.0 Changed from private to public.
*
* @return array Array of attachments IDs.
*/
public function get_attachments_with_backups() {
global $wpdb;
$images_to_restore = $wpdb->get_col(
"SELECT post_id FROM {$wpdb->postmeta}
WHERE meta_key='_wp_attachment_backup_sizes'
AND (`meta_value` LIKE '%smush-full%'
OR `meta_value` LIKE '%smush_png_path%')"
);
return $images_to_restore;
}
/**
* Get the number of attachments that can be restored.
*
* @since 3.2.2
*/
public function get_image_count() {
check_ajax_referer( 'smush_bulk_restore' );
// Check for permission.
if ( ! Helper::is_user_allowed( 'manage_options' ) ) {
wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 );
}
wp_send_json_success(
array(
'items' => $this->get_attachments_with_backups(),
)
);
}
/**
* Bulk restore images from the modal.
*
* @since 3.2.2
*/
public function restore_step() {
check_ajax_referer( 'smush_bulk_restore' );
// Check for permission.
if ( ! Helper::is_user_allowed( 'manage_options' ) ) {
wp_die( esc_html__( 'Unauthorized', 'wp-smushit' ), 403 );
}
$id = filter_input( INPUT_POST, 'item', FILTER_SANITIZE_NUMBER_INT, FILTER_NULL_ON_FAILURE );
$media_item = Media_Item_Cache::get_instance()->get( $id );
if ( ! $media_item->is_mime_type_supported() ) {
wp_send_json_error(
array(
/* translators: %s: Error message */
'error_msg' => sprintf( esc_html__( 'Image not restored. %s', 'wp-smushit' ), $media_item->get_errors()->get_error_message() ),
)
);
}
$optimizer = new Media_Item_Optimizer( $media_item );
$status = $id && $optimizer->restore();
$file_name = $media_item->get_full_or_scaled_size()->get_file_name();
wp_send_json_success(
array(
'success' => $status,
'src' => ! empty( $file_name ) ? $file_name : __( 'Error getting file name', 'wp-smushit' ),
'thumb' => wp_get_attachment_image( $id ),
'link' => Helper::get_image_media_link( $id, $file_name, true ),
)
);
}
/**
* Returns the backup path for attachment
*
* @param string $attachment_path Attachment path.
*
* @return string
*/
public function get_image_backup_path( $attachment_path ) {
if ( empty( $attachment_path ) ) {
return '';
}
$path = pathinfo( $attachment_path );
if ( empty( $path['extension'] ) ) {
return '';
}
return trailingslashit( $path['dirname'] ) . $path['filename'] . '.bak.' . $path['extension'];
}
/**
* Clear up all the backup files for the image while deleting the image.
*
* @since 3.9.6
* Note, we only call this method while deleting the image, as it will delete
* .bak file and might be the original file too.
*
* Note, for the old version < 3.9.6 we also save all PNG files (original file and thumbnails)
* when the site doesn't compress original file.
* But it's not safe to remove them if the user add another image with the same PNG file name, and didn't convert it.
* So we still leave them there.
*
* @param int $attachment_id Attachment ID.
*/
public function delete_backup_files( $attachment_id ) {
$smush_meta = get_post_meta( $attachment_id, Smush::$smushed_meta_key, true );
if ( empty( $smush_meta ) ) {
return;
}
// Save list files to remove.
$files_to_remove = array();
$unfiltered = false;
$file_path = get_attached_file( $attachment_id, false );
// We only work with the real file path, not cloud URL like S3.
if ( false === strpos( $file_path, ABSPATH ) ) {
$unfiltered = true;
$file_path = get_attached_file( $attachment_id, true );
}
// Remove from the cache.
wp_cache_delete( 'images_with_backups', 'wp-smush' );
/**
* We only remove the backup file from the metadata,
* keep the backup file from 3rd-party.
*/
$backup_path = null;// Reset backup file.
$backup_sizes = $this->get_backup_sizes( $attachment_id );
if ( isset( $backup_sizes[ $this->backup_key ]['file'] ) ) {
$backup_path = str_replace( wp_basename( $file_path ), wp_basename( $backup_sizes[ $this->backup_key ]['file'] ), $file_path );
// Add to remove the backup file.
$files_to_remove[ $this->backup_key ] = $backup_path;
}
// Check the backup file from resized PNG file (< 3.9.6).
if ( isset( $backup_sizes['smush_png_path']['file'] ) ) {
$backup_path = str_replace( wp_basename( $file_path ), wp_basename( $backup_sizes['smush_png_path']['file'] ), $file_path );
// Add to remove the backup file.
$files_to_remove['smush_png_path'] = $backup_path;
}
if ( ! $backup_path ) {
// Check for legacy original file path. It's for old version < V.2.7.0.
$original_file = get_post_meta( $attachment_id, 'wp-smush-original_file', true );
if ( ! empty( $original_file ) ) {
// For old version < v.2.7.0, we are saving meta['file'] or _wp_attached_file.
$backup_path = Helper::original_file( $original_file );
// Add to remove the backup file.
$files_to_remove[] = $backup_path;
}
}
// Check meta for rest of the sizes.
$meta = wp_get_attachment_metadata( $attachment_id, $unfiltered );
if ( empty( $meta ) || empty( $meta['sizes'] ) ) {
Helper::logger()->backup()->info( sprintf( 'Empty meta sizes [%s(%d)]', $file_path, $attachment_id ) );
return;
}
foreach ( $meta['sizes'] as $size ) {
if ( empty( $size['file'] ) ) {
continue;
}
// Image path and backup path.
$image_size_path = path_join( dirname( $file_path ), $size['file'] );
$image_backup_path = $this->get_image_backup_path( $image_size_path );
// Add to remove the backup file.
$files_to_remove[] = $image_backup_path;
}
// We also try to delete this file on cloud, e.g. S3.
Helper::delete_permanently( $files_to_remove, $attachment_id, false );
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,695 @@
<?php
/**
* Lazy load images class: Lazy
*
* @since 3.2.0
* @package Smush\Core\Modules
*/
namespace Smush\Core\Modules;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Lazy
*/
class Lazy extends Abstract_Module {
/**
* Module slug.
*
* @var string
*/
protected $slug = 'lazy_load';
/**
* Lazy loading settings.
*
* @since 3.2.0
* @var array $settings
*/
private $options;
/**
* Page parser.
*
* @since 3.2.2
* @var Helpers\Parser $parser
*/
protected $parser;
/**
* Excluded classes list.
*
* @since 3.6.2
* @var array
*/
private $excluded_classes = array(
'no-lazyload', // Internal class to skip images.
'skip-lazy',
'rev-slidebg', // Skip Revolution slider images.
'soliloquy-preload', // Soliloquy slider.
);
/**
* Lazy constructor.
*
* @since 3.2.2
* @param Helpers\Parser $parser Page parser instance.
*/
public function __construct( Helpers\Parser $parser ) {
$this->parser = $parser;
parent::__construct();
}
/**
* Initialize module actions.
*
* @since 3.2.0
*/
public function init() {
// Only run on front end and if lazy loading is enabled.
if ( is_admin() || ! $this->is_active() ) {
return;
}
$this->options = $this->settings->get_setting( 'wp-smush-lazy_load' );
// Enabled without settings? Don't think so... Exit.
if ( ! $this->options ) {
return;
}
// Disable WordPress native lazy load.
add_filter( 'wp_lazy_loading_enabled', '__return_false' );
// Load js file that is required in public facing pages.
add_action( 'wp_head', array( $this, 'add_inline_styles' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ), 99 );
if ( defined( 'WP_SMUSH_ASYNC_LAZY' ) && WP_SMUSH_ASYNC_LAZY ) {
add_filter( 'script_loader_tag', array( $this, 'async_load' ), 10, 2 );
}
// Allow lazy load attributes in img tag.
add_filter( 'wp_kses_allowed_html', array( $this, 'add_lazy_load_attributes' ) );
$this->parser->enable( $this->slug );
if ( isset( $this->options['format']['iframe'] ) && $this->options['format']['iframe'] ) {
$this->parser->enable( 'iframes' );
}
add_filter( 'wp_smush_should_skip_parse', array( $this, 'maybe_skip_parse' ) );
// Filter images.
if ( ! isset( $this->options['output']['content'] ) || ! $this->options['output']['content'] ) {
add_filter( 'the_content', array( $this, 'exclude_from_lazy_loading' ), 100 );
}
if ( ! isset( $this->options['output']['thumbnails'] ) || ! $this->options['output']['thumbnails'] ) {
add_filter( 'post_thumbnail_html', array( $this, 'exclude_from_lazy_loading' ), 100 );
}
if ( ! isset( $this->options['output']['gravatars'] ) || ! $this->options['output']['gravatars'] ) {
add_filter( 'get_avatar', array( $this, 'exclude_from_lazy_loading' ), 100 );
}
if ( ! isset( $this->options['output']['widgets'] ) || ! $this->options['output']['widgets'] ) {
add_action( 'dynamic_sidebar_before', array( $this, 'filter_sidebar_content_start' ), 0 );
add_action( 'dynamic_sidebar_after', array( $this, 'filter_sidebar_content_end' ), 1000 );
}
}
/**
* Add inline styles at the top of the page for pre-loaders and effects.
*
* @since 3.2.0
*/
public function add_inline_styles() {
if ( $this->is_amp() ) {
return;
}
// Fix for poorly coded themes that do not remove the no-js in the HTML class.
?>
<script>
document.documentElement.className = document.documentElement.className.replace( 'no-js', 'js' );
</script>
<?php
if ( ! $this->options['animation']['selected'] || 'none' === $this->options['animation']['selected'] ) {
return;
}
// Spinner.
if ( 'spinner' === $this->options['animation']['selected'] ) {
$loader = WP_SMUSH_URL . 'app/assets/images/smush-lazyloader-' . $this->options['animation']['spinner']['selected'] . '.gif';
if ( isset( $this->options['animation']['spinner']['selected'] ) && 5 < (int) $this->options['animation']['spinner']['selected'] ) {
$loader = wp_get_attachment_image_src( $this->options['animation']['spinner']['selected'], 'full' );
$loader = $loader[0];
}
$background = 'rgba(255, 255, 255, 0)';
} else {
// Placeholder.
$loader = WP_SMUSH_URL . 'app/assets/images/smush-placeholder.png';
$background = '#FAFAFA';
if ( isset( $this->options['animation']['placeholder']['selected'] ) && 2 === (int) $this->options['animation']['placeholder']['selected'] ) {
$background = '#333333';
}
if ( isset( $this->options['animation']['placeholder']['selected'] ) && 2 < (int) $this->options['animation']['placeholder']['selected'] ) {
$loader = wp_get_attachment_image_src( (int) $this->options['animation']['placeholder']['selected'], 'full' );
// Can't find a loader on multisite? Try main site.
if ( ! $loader && is_multisite() ) {
switch_to_blog( 1 );
$loader = wp_get_attachment_image_src( (int) $this->options['animation']['placeholder']['selected'], 'full' );
restore_current_blog();
}
$loader = $loader[0];
}
if ( isset( $this->options['animation']['placeholder']['color'] ) ) {
$background = $this->options['animation']['placeholder']['color'];
}
}
// Fade in.
$fadein = isset( $this->options['animation']['fadein']['duration'] ) ? $this->options['animation']['fadein']['duration'] : 0;
$delay = isset( $this->options['animation']['fadein']['delay'] ) ? $this->options['animation']['fadein']['delay'] : 0;
?>
<style>
.no-js img.lazyload { display: none; }
figure.wp-block-image img.lazyloading { min-width: 150px; }
<?php if ( 'fadein' === $this->options['animation']['selected'] ) : ?>
.lazyload, .lazyloading { opacity: 0; }
.lazyloaded {
opacity: 1;
transition: opacity <?php echo esc_html( $fadein ); ?>ms;
transition-delay: <?php echo esc_html( $delay ); ?>ms;
}
<?php else : ?>
.lazyload { opacity: 0; }
.lazyloading {
border: 0 !important;
opacity: 1;
background: <?php echo esc_attr( $background ); ?> url('<?php echo esc_url( $loader ); ?>') no-repeat center !important;
background-size: 16px auto !important;
min-width: 16px;
}
<?php endif; ?>
</style>
<?php
}
/**
* Enqueue JS files required in public pages.
*
* @since 3.2.0
*/
public function enqueue_assets() {
if ( $this->is_amp() ) {
return;
}
$script = WP_SMUSH_URL . 'app/assets/js/smush-lazy-load.min.js';
// Native lazy loading support.
if ( isset( $this->options['native'] ) && $this->options['native'] ) {
$script = WP_SMUSH_URL . 'app/assets/js/smush-lazy-load-native.min.js';
}
$in_footer = isset( $this->options['footer'] ) ? $this->options['footer'] : true;
wp_enqueue_script(
'smush-lazy-load',
$script,
array(),
WP_SMUSH_VERSION,
$in_footer
);
$this->add_masonry_support();
if ( defined( 'WP_SMUSH_LAZY_LOAD_AVADA' ) && WP_SMUSH_LAZY_LOAD_AVADA ) {
$this->add_avada_support();
}
$this->add_divi_support();
$this->add_soliloquy_support();
}
/**
* Async load the lazy load scripts.
*
* @since 3.7.0
*
* @param string $tag The <script> tag for the enqueued script.
* @param string $handle The script's registered handle.
*
* @return string
*/
public function async_load( $tag, $handle ) {
if ( 'smush-lazy-load' === $handle ) {
return str_replace( ' src', ' async="async" src', $tag );
}
return $tag;
}
/**
* Add support for plugins that use the masonry grid system (Block Gallery and CoBlocks plugins).
*
* @since 3.5.0
*
* @see https://wordpress.org/plugins/coblocks/
* @see https://github.com/godaddy/block-gallery
* @see https://masonry.desandro.com/methods.html#layout-masonry
*/
private function add_masonry_support() {
if ( ! function_exists( 'has_block' ) ) {
return;
}
// None of the supported blocks are active - exit.
if ( ! has_block( 'blockgallery/masonry' ) && ! has_block( 'coblocks/gallery-masonry' ) ) {
return;
}
$js = "var e = jQuery( '.wp-block-coblocks-gallery-masonry ul' );";
if ( has_block( 'blockgallery/masonry' ) ) {
$js = "var e = jQuery( '.wp-block-blockgallery-masonry ul' );";
}
$block_gallery_compat = "jQuery(document).on('lazyloaded', function(){{$js} if ('function' === typeof e.masonry) e.masonry();});";
wp_add_inline_script( 'smush-lazy-load', $block_gallery_compat );
}
/**
* Add fusion gallery support in Avada theme.
*
* @since 3.7.0
*/
private function add_avada_support() {
if ( ! defined( 'FUSION_BUILDER_VERSION' ) ) {
return;
}
$js = "var e = jQuery( '.fusion-gallery' );";
$block_gallery_compat = "jQuery(document).on('lazyloaded', function(){{$js} if ('function' === typeof e.isotope) e.isotope();});";
wp_add_inline_script( 'smush-lazy-load', $block_gallery_compat );
}
/**
* Adds lazyload support to Divi & it's Waypoint library.
*
* @since 3.9.0
*/
private function add_divi_support() {
if ( ! defined( 'ET_BUILDER_THEME' ) || ! ET_BUILDER_THEME ) {
return;
}
$script = "function rw() { Waypoint.refreshAll(); } window.addEventListener( 'lazybeforeunveil', rw, false); window.addEventListener( 'lazyloaded', rw, false);";
wp_add_inline_script( 'smush-lazy-load', $script );
}
/**
* Prevents the navigation from being missaligned in Soliloquy when lazy loading.
*
* @since 3.7.0
*/
private function add_soliloquy_support() {
if ( ! function_exists( 'soliloquy' ) ) {
return;
}
$js = "var e = jQuery( '.soliloquy-image:not(.lazyloaded)' );";
$soliloquy = "jQuery(document).on('lazybeforeunveil', function(){{$js}e.each(function(){lazySizes.loader.unveil(this);});});";
wp_add_inline_script( 'smush-lazy-load', $soliloquy );
}
/**
* Make sure WordPress does not filter out img elements with lazy load attributes.
*
* @since 3.2.0
*
* @param array $allowedposttags Allowed post tags.
*
* @return mixed
*/
public function add_lazy_load_attributes( $allowedposttags ) {
if ( ! isset( $allowedposttags['img'] ) ) {
return $allowedposttags;
}
$smush_attributes = array(
'data-src' => true,
'data-srcset' => true,
'data-sizes' => true,
);
$img_attributes = array_merge( $allowedposttags['img'], $smush_attributes );
$allowedposttags['img'] = $img_attributes;
return $allowedposttags;
}
/**
* Check if we need to skip parsing of this page.
*
* @since 3.2.2
* @param bool $skip Skip parsing.
*
* @return bool
*/
public function maybe_skip_parse( $skip ) {
// Don't lazy load for feeds, previews, embeds.
if ( is_feed() || is_preview() || is_embed() ) {
$skip = true;
}
if ( $this->skip_post_type() || $this->is_exluded_uri() ) {
$skip = true;
}
return $skip;
}
/**
* Parse image for Lazy load.
*
* @since 3.2.2
*
* @param string $src Image URL.
* @param string $image Image tag (<img>).
* @param string $type Element type. Accepts: 'img', 'source' or 'iframe'. Default: 'img'.
*
* @return string
*/
public function parse_image( $src, $image, $type = 'img' ) {
if ( $this->is_amp() ) {
return $image;
}
/**
* Filter to skip a single image from lazy load.
*
* @since 3.3.0 Added $image param.
*
* @param bool $skip Should skip? Default: false.
* @param string $src Image url.
* @param string $image Image.
*/
if ( apply_filters( 'smush_skip_image_from_lazy_load', false, $src, $image ) ) {
return $image;
}
$is_gravatar = false !== strpos( $src, 'gravatar.com' );
$path = wp_parse_url( $src, PHP_URL_PATH );
// Make sure $path is not null, because passing null to parameter is deprecated in PHP 8.1.
if ( empty( $path ) ) {
return $image;
}
$ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
$ext = 'jpg' === $ext ? 'jpeg' : $ext;
// If not a supported image in src or not an iframe - skip.
$iframe = 'iframe' === substr( $image, 1, 6 );
if ( ! $is_gravatar && ! in_array( $ext, array( 'jpeg', 'gif', 'png', 'svg', 'webp' ), true ) && ! $iframe ) {
return $image;
}
// Check if some image formats are excluded.
if ( in_array( false, $this->options['format'], true ) && isset( $this->options['format'][ $ext ] ) && ! $this->options['format'][ $ext ] ) {
return $image;
}
// Check if iframes are excluded.
if ( $iframe && isset( $this->options['format']['iframe'] ) && ! $this->options['format']['iframe'] ) {
return $image;
}
/**
* Filter to skip a iframe from lazy load.
*
* @since 3.4.2
* @since 3.7.0 Added filtering by empty source. Better approach to make the get_images_from_content() work
* by finding all the images (even escaped). But it does what it does.
*
* @param bool $skip Should skip? Default: false.
* @param string $src Iframe url.
*/
if ( empty( $src ) || ( $iframe && apply_filters( 'smush_skip_iframe_from_lazy_load', false, $src ) ) ) {
return $image;
}
// Check if the iframe URL is valid if not skip it from lazy load.
if ( $iframe && esc_url_raw( $src ) !== $src ) {
return $image;
}
if ( $this->has_excluded_class_or_id( $image ) ) {
return $image;
}
// Check for the data-skip-lazy attribute.
if ( false !== strpos( $image, 'data-skip-lazy' ) ) {
return $image;
}
$new_image = $image;
/**
* The sizes attribute does not have to be replaced to data-sizes, but it fixes the W3C validation.
*
* @since 3.6.2
*/
$attributes = array( 'src', 'sizes' );
foreach ( $attributes as $attribute ) {
$attr = Helpers\Parser::get_attribute( $new_image, $attribute );
if ( $attr ) {
Helpers\Parser::remove_attribute( $new_image, $attribute );
Helpers\Parser::add_attribute( $new_image, "data-{$attribute}", $attr );
}
}
// Change srcset to data-srcset attribute.
$new_image = preg_replace( '/<(.*?)(srcset=)(.*?)>/i', '<$1data-$2$3>', $new_image );
// Exit early if this is a <source> element from <picture>.
if ( 'source' === $type ) {
return $new_image;
}
// Add .lazyload class.
$class = Helpers\Parser::get_attribute( $new_image, 'class' );
if ( $class ) {
$class .= ' lazyload';
} else {
$class = 'lazyload';
}
Helpers\Parser::remove_attribute( $new_image, 'class' );
Helpers\Parser::add_attribute( $new_image, 'class', apply_filters( 'wp_smush_lazy_load_classes', $class ) );
Helpers\Parser::add_attribute( $new_image, 'src', 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' );
// Use noscript element in HTML to load elements normally when JavaScript is disabled in browser.
if ( ! $iframe && ( ! isset( $this->options['noscript'] ) || ! $this->options['noscript'] ) ) {
$new_image .= '<noscript>' . $image . '</noscript>';
}
return $new_image;
}
/**
* Get images from content and add exclusion class.
*
* @since 3.2.2
*
* @param string $content Page/block content.
*
* @return string
*/
public function exclude_from_lazy_loading( $content ) {
$images = $this->parser->get_images_from_content( $content );
if ( empty( $images ) ) {
return $content;
}
foreach ( $images[0] as $key => $image ) {
$new_image = $image;
// Add .no-lazyload class.
$class = Helpers\Parser::get_attribute( $new_image, 'class' );
if ( $class ) {
Helpers\Parser::remove_attribute( $new_image, 'class' );
$class .= ' no-lazyload';
} else {
$class = 'no-lazyload';
}
Helpers\Parser::add_attribute( $new_image, 'class', $class );
/**
* Filters the no-lazyload image.
*
* @since 3.8.5
*
* @param string $text The image that can be filtered.
*/
$new_image = apply_filters( 'wp_smush_filter_no_lazyload_image', $new_image );
$content = str_replace( $image, $new_image, $content );
}
return $content;
}
/**
* Check if this is part of the allowed post type.
*
* @since 3.2.0
*
* @return bool
*/
private function skip_post_type() {
// If not settings are set, probably, all are disabled.
if ( ! is_array( $this->options['include'] ) ) {
return true;
}
$blog_is_frontpage = ( 'posts' === get_option( 'show_on_front' ) && ! is_multisite() ) ? true : false;
if ( is_front_page() && isset( $this->options['include']['frontpage'] ) && ! $this->options['include']['frontpage'] ) {
return true;
} elseif ( is_home() && isset( $this->options['include']['home'] ) && ! $this->options['include']['home'] && ! $blog_is_frontpage ) {
return true;
} elseif ( is_page() && isset( $this->options['include']['page'] ) && ! $this->options['include']['page'] ) {
return true;
} elseif ( is_single() && isset( $this->options['include']['single'] ) && ! $this->options['include']['single'] ) {
return true;
} elseif ( is_archive() && isset( $this->options['include']['archive'] ) && ! $this->options['include']['archive'] ) {
return true;
} elseif ( is_category() && isset( $this->options['include']['category'] ) && ! $this->options['include']['category'] ) {
return true;
} elseif ( is_tag() && isset( $this->options['include']['tag'] ) && ! $this->options['include']['tag'] ) {
return true;
} elseif ( self::skip_custom_post_type( get_post_type() ) ) {
return true;
}
return false;
}
/**
* Skip custom post type added in settings.
*
* @since 3.5.0
*
* @param string $post_type Post type to check in settings.
*
* @return bool
*/
private function skip_custom_post_type( $post_type ) {
if ( isset( $this->options['include'][ $post_type ] ) && ! $this->options['include'][ $post_type ] ) {
return true;
}
return false;
}
/**
* Check if the page has been added to Post, Pages & URLs filter in lazy loading settings.
*
* @since 3.2.0
*
* @return bool
*/
private function is_exluded_uri() {
// No exclusion rules defined.
if ( ! isset( $this->options['exclude-pages'] ) || empty( $this->options['exclude-pages'] ) ) {
return false;
}
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
// Remove empty values.
$uri_pattern = array_filter( $this->options['exclude-pages'] );
$uri_pattern = implode( '|', $uri_pattern );
if ( preg_match( "#{$uri_pattern}#i", $request_uri ) ) {
return true;
}
return false;
}
/**
* Check if the image has a defined class or ID.
*
* @since 3.2.0
*
* @param string $image Image.
*
* @return bool
*/
private function has_excluded_class_or_id( $image ) {
$image_classes = Helpers\Parser::get_attribute( $image, 'class' );
$image_classes = explode( ' ', $image_classes );
$image_id = '#' . Helpers\Parser::get_attribute( $image, 'id' );
if ( in_array( $image_id, $this->options['exclude-classes'], true ) ) {
return true;
}
foreach ( $image_classes as $class ) {
if ( in_array( $class, $this->excluded_classes, true ) ) {
return true;
}
if ( in_array( ".{$class}", $this->options['exclude-classes'], true ) ) {
return true;
}
}
return false;
}
/**
* Buffer sidebar content.
*
* @since 3.2.0
*/
public function filter_sidebar_content_start() {
ob_start();
}
/**
* Process buffered content.
*
* @since 3.2.0
*/
public function filter_sidebar_content_end() {
$content = ob_get_clean();
echo $this->exclude_from_lazy_loading( $content );
unset( $content );
}
/**
* Determine whether it is an AMP page.
*
* @since 3.4.0
*
* @return bool Whether AMP.
*/
private function is_amp() {
return function_exists( 'is_amp_endpoint' ) && is_amp_endpoint();
}
}

View File

@ -0,0 +1,755 @@
<?php
/**
* PNG to JPG conversion: Png2jpg class
*
* @package Smush\Core\Modules
*
* @version 2.4
*
* @author Umesh Kumar <umesh@incsub.com>
*
* @copyright (c) 2016, Incsub (http://incsub.com)
*/
namespace Smush\Core\Modules;
use Exception;
use Imagick;
use Smush\Core\Helper;
use WP_Smush;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Png2jpg
*/
class Png2jpg extends Abstract_Module {
/**
* Module slug.
*
* @var string
*/
protected $slug = 'png_to_jpg';
/**
* Whether module is pro or not.
*
* @var string
*/
protected $is_pro = true;
/**
* Init method.
*
* @since 3.0
*/
public function init() {
}
/**
* Check if Imagick is available or not
*
* @return bool True/False Whether Imagick is available or not
*/
private function supports_imagick() {
if ( ! class_exists( '\Imagick' ) ) {
return false;
}
return true;
}
/**
* Check if GD is loaded
*
* @return bool True/False Whether GD is available or not
*/
private function supports_gd() {
if ( ! function_exists( 'gd_info' ) ) {
return false;
}
return true;
}
/**
* Save can be converted status before resizing the image,
* because the the image might be lost the undefined-transparent behavior after resizing.
*
* @see WP_Image_Editor_Imagick::thumbnail_image()
* WP Resize function will convert Imagick::ALPHACHANNEL_UNDEFINED -> Imagick::ALPHACHANNEL_OPAQUE.
*
* @param array $sizes Array of sizes containing max width and height for all the uploaded images.
* @param string $file_path Image file path.
* @param int $id Image id.
*
* @return array the Original $sizes.
*
* @since 3.9.6
*/
public function cache_can_be_converted_status( $sizes, $file_path, $id ) {
// Call can_be_converted and cache the status.
$this->can_be_converted( $id, 'full', '', $file_path );
// Always return $sizes.
return $sizes;
}
/**
* Checks if the Given PNG file is transparent or not
*
* @param string $id Attachment ID.
* @param string $file File path for the attachment.
*
* @return bool|int
*/
private function is_transparent( $id = '', $file = '' ) {
// No attachment id/ file path, return.
if ( empty( $id ) && empty( $file ) ) {
return false;
}
if ( empty( $file ) ) {
// This downloads the file from S3 when S3 is enabled.
$file = Helper::get_attached_file( $id );
}
// Check if File exists.
if ( empty( $file ) || ! file_exists( $file ) ) {
Helper::logger()->png2jpg()->info( sprintf( 'File [%s(%d)] is empty or does not exist.', Helper::clean_file_path( $file ), $id ) );
return false;
}
$transparent = '';
// Try to get transparency using Imagick.
if ( $this->supports_imagick() ) {
try {
$im = new Imagick( $file );
return $im->getImageAlphaChannel();
} catch ( Exception $e ) {
Helper::logger()->png2jpg()->error( 'Imagick: Error in checking PNG transparency ' . $e->getMessage() );
}
} else {
// Simple check.
// Src: http://camendesign.com/code/uth1_is-png-32bit.
if ( ord( file_get_contents( $file, false, null, 25, 1 ) ) & 4 ) {
Helper::logger()->png2jpg()->info( sprintf( 'File [%s(%d)] is an PNG 32-bit.', Helper::clean_file_path( $file ), $id ) );
return true;
}
// Src: http://www.jonefox.com/blog/2011/04/15/how-to-detect-transparency-in-png-images/.
$contents = file_get_contents( $file );
if ( stripos( $contents, 'PLTE' ) !== false && stripos( $contents, 'tRNS' ) !== false ) {
Helper::logger()->png2jpg()->info( sprintf( 'File [%s(%d)] is an PNG 8-bit.', Helper::clean_file_path( $file ), $id ) );
return true;
}
// If both the conditions failed, that means not transparent.
return false;
}
// If Imagick is installed, and the code exited due to some error.
// Src: StackOverflow.
if ( empty( $transparent ) && $this->supports_gd() ) {
// Check for transparency using GD.
$i = imagecreatefrompng( $file );
$palette = ( imagecolortransparent( $i ) < 0 );
if ( $palette ) {
return true;
}
}
return false;
}
/**
* Check if given attachment id can be converted to JPEG or not
*
* @param string $id Atachment ID.
* @param string $size Image size.
* @param string $mime Mime type.
* @param string $file File.
*
* @since 3.9.6 We removed the private method should_convert
* and we also handled the case which we need to delete the download file inside S3.
*
* Note, we also used this for checking resmush, so we only download the attached file (s3)
* if it's necessary (self::is_transparent()). Check the comment on self::__construct() for detail.
*
* @return bool True/False Can be converted or not
*/
public function can_be_converted( $id = '', $size = 'full', $mime = '', $file = '' ) {
// If PNG2JPG not enabled, or is not smushable, return.
if ( ! $this->is_active() || ! Helper::is_smushable( $id ) ) {
return false;
}
// Check it from the cache for full size.
if ( 'full' === $size && null !== Helper::cache_get( $id, 'png2jpg_can_be_converted' ) ) {
return Helper::cache_get( $id, 'png2jpg_can_be_converted' );
}
// False if not a PNG.
$mime = empty( $mime ) ? get_post_mime_type( $id ) : $mime;
if ( 'image/png' !== $mime && 'image/x-png' !== $mime ) {
return false;
}
// Return if Imagick and GD is not available.
if ( ! $this->supports_imagick() && ! $this->supports_gd() ) {
Helper::logger()->png2jpg()->warning( 'The site does not support Imagick or GD.' );
return false;
}
// If already tried the conversion.
if ( get_post_meta( $id, 'wp-smush-pngjpg_savings', true ) ) {
Helper::logger()->png2jpg()->info( sprintf( 'File [%s(%d)] already tried the conversion.', Helper::clean_file_path( $file ), $id ) );
return false;
}
// Check if registered size is supposed to be converted or not.
if ( 'full' !== $size && WP_Smush::get_instance()->core()->mod->smush->skip_image_size( $size ) ) {
return false;
}
// Make sure $file is not empty.
if ( empty( $file ) ) {
$file = Helper::get_attached_file( $id );// S3+.
}
/**
* Filter whether to convert the PNG to JPG or not
*
* @since 2.4
*
* @param bool $should_convert Current choice for image conversion
*
* @param int $id Attachment id
*
* @param string $file File path for the image
*
* @param string $size Image size being converted
*/
$should_convert = apply_filters( 'wp_smush_convert_to_jpg', ! $this->is_transparent( $id, $file ), $id, $file, $size );
if ( 'full' === $size ) {
/**
* We used this method inside Backup::create_backup(), Smush function, and filter wp_smush_resize_sizes,
* so cache the result to avoid to check it again.
*/
Helper::cache_set( $id, $should_convert, 'png2jpg_can_be_converted' );
}
return $should_convert;
}
/**
* Check whether to resmush image or not.
*
* @since 3.9.6
*
* @usedby Smush\App\Ajax::scan_images()
*
* @param bool $should_resmush Current status.
* @param int $attachment_id Attachment ID.
* @return bool|string png2jpg|TRUE|FALSE
*/
public function should_resmush( $should_resmush, $attachment_id ) {
if ( ! $should_resmush && $this->can_be_converted( $attachment_id ) ) {
$should_resmush = 'png2jpg';
}
return $should_resmush;
}
/**
* Update the image URL, MIME Type, Attached File, file path in Meta, URL in post content
*
* @param string $id Attachment ID.
* @param string $o_file Original File Path that has to be replaced.
* @param string $n_file New File Path which replaces the old file.
* @param string $meta Attachment Meta.
* @param string $size_k Image Size.
* @param string $o_type Operation Type "conversion", "restore".
*
* @return array Attachment Meta with updated file path.
*/
public function update_image_path( $id, $o_file, $n_file, $meta, $size_k, $o_type = 'conversion' ) {
// Upload Directory.
$upload_dir = wp_upload_dir();
// Upload Path.
$upload_path = trailingslashit( $upload_dir['basedir'] );
$dir_name = pathinfo( $o_file, PATHINFO_DIRNAME );
// Full Path to new file.
$n_file_path = path_join( $dir_name, $n_file );
// Current URL for image.
$o_url = wp_get_attachment_url( $id );
// Update URL for image size.
if ( 'full' !== $size_k ) {
$base_url = dirname( $o_url );
$o_url = $base_url . '/' . basename( $o_file );
}
// Update File path, Attached File, GUID.
$meta = empty( $meta ) ? wp_get_attachment_metadata( $id ) : $meta;
$mime = Helper::get_mime_type( $n_file_path );
/**
* If there's no fileinfo extension installed, the mime type will be returned as false.
* As a fallback, we set it manually.
*
* @since 3.8.3
*/
if ( false === $mime ) {
$mime = 'conversion' === $o_type ? 'image/jpeg' : 'image/png';
}
$del_file = true;
// Update File Path, Attached file, Mime Type for Image.
if ( 'full' === $size_k ) {
if ( ! empty( $meta ) ) {
$new_file = str_replace( $upload_path, '', $n_file_path );
$meta['file'] = $new_file;
// Update Attached File.
if ( ! update_attached_file( $id, $meta['file'] ) ) {
$del_file = false;
}
}
// Update Mime type.
if ( ! wp_update_post(
array(
'ID' => $id,
'post_mime_type' => $mime,
)
) ) {
$del_file = false;
}
} else {
$meta['sizes'][ $size_k ]['file'] = basename( $n_file );
$meta['sizes'][ $size_k ]['mime-type'] = $mime;
}
// To be called after the attached file key is updated for the image.
if ( ! $this->update_image_url( $id, $size_k, $n_file, $o_url ) ) {
$del_file = false;
}
/**
* Delete the Original files if backup not enabled
* We only delete the file if we don't have any issues while updating the DB.
* SMUSH-1088?focusedCommentId=92914.
*/
if ( $del_file && 'conversion' === $o_type ) {
// We might need to backup the full size file, will delete it later if we don't need to use it for backup.
if ( 'full' !== $size_k ) {
/**
* We only need to keep the original file as a backup file.
* and try to delete this file on cloud too, e.g S3.
*/
Helper::delete_permanently( $o_file, $id );
}
}
return $meta;
}
/**
* Replace the file if there are savings, and return savings
*
* @param string $file Original File Path.
* @param array $result Array structure.
* @param string $n_file Updated File path.
*
* @return array
*/
private function replace_file( $file = '', $result = array(), $n_file = '' ) {
if ( empty( $file ) || empty( $n_file ) ) {
return $result;
}
// Get the file size of original image.
$o_file_size = filesize( $file );
$n_file = path_join( dirname( $file ), $n_file );
$n_file_size = filesize( $n_file );
// If there aren't any savings return.
if ( $n_file_size >= $o_file_size ) {
// Delete the JPG image and return.
unlink( $n_file );
Helper::logger()->png2jpg()->notice( sprintf( 'The new file [%s](%s) is larger than the original file [%s](%s).', Helper::clean_file_path( $n_file ), size_format( $n_file_size ), Helper::clean_file_path( $file ), size_format( $o_file_size ) ) );
return $result;
}
// Get the savings.
$savings = $o_file_size - $n_file_size;
// Store Stats.
$savings = array(
'bytes' => $savings,
'size_before' => $o_file_size,
'size_after' => $n_file_size,
);
$result['savings'] = $savings;
return $result;
}
/**
* Perform the conversion process, using WordPress Image Editor API
*
* @param string $id Attachment ID.
* @param string $file Attachment File path.
* @param string $meta Attachment meta.
* @param string $size Image size, default empty for full image.
*
* @return array $result array(
* 'meta' => array Update Attachment metadata
* 'savings' => Reduction of Image size in bytes
* )
*/
private function convert_to_jpg( $id = '', $file = '', $meta = '', $size = 'full' ) {
$result = array(
'meta' => $meta,
'savings' => '',
);
// Flag: Whether the image was converted or not.
if ( 'full' === $size ) {
$result['converted'] = false;
}
// If any of the values is not set.
if ( empty( $id ) || empty( $file ) || empty( $meta ) || ! file_exists( $file ) ) {
Helper::logger()->png2jpg()->info( sprintf( 'Meta file [%s(%d)] is empty or file not found.', Helper::clean_file_path( $file ), $id ) );
return $result;
}
$editor = wp_get_image_editor( $file );
if ( is_wp_error( $editor ) ) {
// Use custom method maybe.
Helper::logger()->png2jpg()->error( sprintf( 'Image Editor cannot load file [%s(%d)]: %s.', Helper::clean_file_path( $file ), $id, $editor->get_error_message() ) );
return $result;
}
$n_file = pathinfo( $file );
if ( ! empty( $n_file['filename'] ) && $n_file['dirname'] ) {
// Get a unique File name.
$file_detail = Helper::cache_get( $id, 'convert_to_jpg' );
if ( $file_detail ) {
list( $old_main_filename, $new_main_filename ) = $file_detail;
/**
* Thumbnail name.
* E.g.
* test-150x150.jpg
* test-1-150x150.jpg
*/
if ( $old_main_filename !== $new_main_filename ) {
$n_file['filename'] = str_replace( $old_main_filename, $new_main_filename, $n_file['filename'] );
}
$n_file['filename'] .= '.jpg';
} else {
$org_filename = $n_file['filename'];
/**
* Get unique file name for the main file.
* E.g.
* test.png => test.jpg
* test.png => test-1.jpg
*/
$n_file['filename'] = wp_unique_filename( $n_file['dirname'], $org_filename . '.jpg' );
Helper::cache_set( $id, array( $org_filename, pathinfo( $n_file['filename'], PATHINFO_FILENAME ) ), 'convert_to_jpg' );
}
$n_file = path_join( $n_file['dirname'], $n_file['filename'] );
} else {
Helper::logger()->png2jpg()->error( sprintf( 'Cannot retrieve the path info of file [%s(%d)].', Helper::clean_file_path( $file ), $id ) );
return $result;
}
// Save PNG as JPG.
$new_image_info = $editor->save( $n_file, 'image/jpeg' );
// If image editor was unable to save the image, return.
if ( is_wp_error( $new_image_info ) ) {
return $result;
}
$n_file = ! empty( $new_image_info ) ? $new_image_info['file'] : '';
// Replace file, and get savings.
$result = $this->replace_file( $file, $result, $n_file );
if ( ! empty( $result['savings'] ) ) {
if ( 'full' === $size ) {
$result['converted'] = true;
}
// Update the File Details. and get updated meta.
$result['meta'] = $this->update_image_path( $id, $file, $n_file, $meta, $size );
/**
* Perform a action after the image URL is updated in post content
*/
do_action( 'wp_smush_image_url_changed', $id, $file, $n_file, $size );
}
return $result;
}
/**
* Convert a PNG to JPG, Lossless Conversion, if we have any savings
*
* @param string $id Image ID.
* @param string|array $meta Image meta.
*
* @uses Backup::add_to_image_backup_sizes()
*
* @return mixed|string
*
* TODO: Save cumulative savings
*/
public function png_to_jpg( $id = '', $meta = '' ) {
// If we don't have meta or ID, or if not a premium user.
if ( empty( $id ) || empty( $meta ) || ! $this->is_active() || ! Helper::is_smushable( $id ) ) {
return $meta;
}
$file = Helper::get_attached_file( $id );// S3+.
// Whether to convert to jpg or not.
$should_convert = $this->can_be_converted( $id, 'full', '', $file );
if ( ! $should_convert ) {
return $meta;
}
$result['meta'] = $meta;
/**
* Allow to force convert the PNG to JPG via filter wp_smush_convert_to_jpg.
*
* @since 3.9.6
* @see self::can_be_converted()
*/
// Perform the conversion, and update path.
$result = $this->convert_to_jpg( $id, $file, $result['meta'] );
$savings['full'] = ! empty( $result['savings'] ) ? $result['savings'] : '';
// If original image was converted and other sizes are there for the image, Convert all other image sizes.
if ( $result['converted'] ) {
if ( ! empty( $meta['sizes'] ) ) {
$converted_thumbs = array();
foreach ( $meta['sizes'] as $size_k => $data ) {
// Some thumbnail sizes are using the same image path, so check if the thumbnail file is converted.
if ( isset( $converted_thumbs[ $data['file'] ] ) ) {
// Update converted thumbnail size.
$result['meta']['sizes'][ $size_k ]['file'] = $result['meta']['sizes'][ $converted_thumbs[ $data['file'] ] ]['file'];
$result['meta']['sizes'][ $size_k ]['mime-type'] = $result['meta']['sizes'][ $converted_thumbs[ $data['file'] ] ]['mime-type'];
continue;
}
$s_file = path_join( dirname( $file ), $data['file'] );
/**
* 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( $s_file, $id ) ) {
continue;
}
/**
* Since these sizes are derived from the main png file,
* We can safely perform the conversion.
*/
$result = $this->convert_to_jpg( $id, $s_file, $result['meta'], $size_k );
if ( ! empty( $result['savings'] ) ) {
$savings[ $size_k ] = $result['savings'];
/**
* Save converted thumbnail file, allow to try to convert the thumbnail to PNG again if it was failed.
*/
$converted_thumbs[ $data['file'] ] = $size_k;
}
}
}
$mod = WP_Smush::get_instance()->core()->mod;
// Save the original File URL.
/**
* Filter: wp_smush_png2jpg_enable_backup
*
* Whether to backup the PNG before converting it to JPG or not
*
* It's safe when we try to backup the PNG file before converting it to JPG when disabled backup option.
* But if exists the backup file, we can delete the PNG file to free up space.
* Note, if enabling resize the image, the backup file is a file that has already been resized, not the original file.
* Use this filter to disable this option:
* add_filter('wp_smush_png2jpg_enable_backup', '__return_false' );
*/
if ( $mod->backup->is_active() || apply_filters( 'wp_smush_png2jpg_enable_backup', ! $mod->backup->is_active(), $id, $file ) ) {
if ( ! $mod->backup->maybe_backup_image( $id, $file ) ) {
/**
* Delete the original file if the backup file exists.
*
* Note, we use size key smush-png2jpg-full for PNG2JPG file to support S3 private media,
* to remove converted JPG file after restoring in private folder.
*
* @see Smush\Core\Integrations\S3::get_object_key()
*/
Helper::delete_permanently( array( 'smush-png2jpg-full' => $file ), $id );// S3+.
}
}
// Remove webp images created from the png version, if any.
$mod->webp->delete_images( $id, true, $file );
/**
* Do action, if the PNG to JPG conversion was successful
*/
do_action( 'wp_smush_png_jpg_converted', $id, $meta, $savings );
/**
* The file converted to JPG,
* we can clear the temp cache related to this converting.
*/
Helper::cache_delete( 'png2jpg_can_be_converted' );
Helper::cache_delete( 'convert_to_jpg' );
}
// Update the Final Stats.
update_post_meta( $id, 'wp-smush-pngjpg_savings', $savings );
return $result['meta'];
}
/**
* Get JPG quality from WordPress Image Editor
*
* @param string $file File.
*
* @return int Quality for JPEG images
*/
private function get_quality( $file ) {
if ( empty( $file ) ) {
return 82;
}
$editor = wp_get_image_editor( $file );
if ( ! is_wp_error( $editor ) ) {
$quality = $editor->get_quality();
} else {
Helper::logger()->png2jpg()->error( sprintf( 'Image Editor cannot load image [%s].', Helper::clean_file_path( $file ) ) );
}
// Choose the default quality if we didn't get it.
if ( ! isset( $quality ) || $quality < 1 || $quality > 100 ) {
// The default quality.
$quality = 82;
}
return $quality;
}
/**
* Check whether the given attachment was converted from PNG to JPG.
*
* @param int $id Attachment ID.
*
* @since 3.9.6 Use this function to check if an image is converted from PNG to JPG.
* @see Backup::get_backup_file() To check the backup file.
*
* @return int|false False if the image id is empty.
* 0 Not yet converted, -1 Tried to convert but it failed or not saving, 1 Convert successfully.
*/
public function is_converted( $id ) {
if ( empty( $id ) ) {
return false;
}
$savings = get_post_meta( $id, 'wp-smush-pngjpg_savings', true );
$is_converted = 0;
if ( ! empty( $savings ) ) {
$is_converted = -1;// The image was tried to convert to JPG but it failed or larger than the original file.
if ( ! empty( $savings['full'] ) ) {
$is_converted = 1;// The image was converted to JPG successfully.
}
}
return $is_converted;
}
/**
* Update Image URL in post content
*
* @param string $id Attachment ID.
* @param string $size_k Image Size.
* @param string $n_file New File Path which replaces the old file.
* @param string $o_url URL to search for.
*/
private function update_image_url( $id, $size_k, $n_file, $o_url ) {
if ( 'full' === $size_k ) {
// Get the updated image URL.
$n_url = wp_get_attachment_url( $id );
} else {
$n_url = trailingslashit( dirname( $o_url ) ) . basename( $n_file );
}
// Update In Post Content, Loop Over a set of posts to avoid the query failure for large sites.
global $wpdb;
// Get existing Images with current URL.
$wild = '%';
$o_url_like = $wild . $wpdb->esc_like( $o_url ) . $wild;
$query = $wpdb->prepare(
"SELECT ID, post_content FROM $wpdb->posts WHERE post_content LIKE %s",
$o_url_like
);
$rows = $wpdb->get_results( $query, ARRAY_A );
if ( empty( $rows ) || ! is_array( $rows ) ) {
return true;
}
// Iterate over rows to update post content.
$total = count( $rows );
foreach ( $rows as $row ) {
// replace old URLs with new URLs.
$post_content = $row['post_content'];
$post_content = str_replace( $o_url, $n_url, $post_content );
// Update Post content.
if ( $wpdb->update(
$wpdb->posts,
array(
'post_content' => $post_content,
),
array(
'ID' => $row['ID'],
)
) ) {
$total --;
}
clean_post_cache( $row['ID'] );
}
// SMUSH-1088?focusedCommentId=92914.
return 0 === $total;
}
}

View File

@ -0,0 +1,521 @@
<?php
namespace Smush\Core\Modules;
use Smush\Core\Array_Utils;
use Smush\Core\Integrations\Mixpanel;
use Smush\Core\Media_Library\Background_Media_Library_Scanner;
use Smush\Core\Media_Library\Media_Library_Scan_Background_Process;
use Smush\Core\Media_Library\Media_Library_Scanner;
use Smush\Core\Modules\Background\Background_Process;
use Smush\Core\Server_Utils;
use Smush\Core\Settings;
use Smush\Core\Stats\Global_Stats;
use WP_Smush;
class Product_Analytics {
const PROJECT_TOKEN = '5d545622e3a040aca63f2089b0e6cae7';
/**
* @var Mixpanel
*/
private $mixpanel;
/**
* @var Settings
*/
private $settings;
/**
* @var Server_Utils
*/
private $server_utils;
/**
* @var Media_Library_Scan_Background_Process
*/
private $scan_background_process;
public function __construct() {
$this->settings = Settings::get_instance();
$this->server_utils = new Server_Utils();
$this->scan_background_process = Background_Media_Library_Scanner::get_instance()->get_background_process();
$this->hook_actions();
}
private function hook_actions() {
// Setting events.
add_action( 'wp_smush_settings_updated', array( $this, 'track_opt_toggle' ), 10, 2 );
add_action( 'wp_smush_settings_updated', array( $this, 'intercept_settings_update' ), 10, 2 );
add_action( 'wp_smush_settings_deleted', array( $this, 'intercept_reset' ) );
add_action( 'wp_smush_settings_updated', array( $this, 'track_integrations_saved' ), 10, 2 );
if ( ! $this->settings->get( 'usage' ) ) {
return;
}
// Other events.
add_action( 'wp_smush_directory_smush_start', array( $this, 'track_directory_smush' ) );
add_action( 'wp_smush_bulk_smush_start', array( $this, 'track_bulk_smush_start' ) );
add_action( 'wp_smush_bulk_smush_completed', array( $this, 'track_bulk_smush_completed' ) );
add_action( 'wp_smush_config_applied', array( $this, 'track_config_applied' ) );
$identifier = $this->scan_background_process->get_identifier();
add_action( "{$identifier}_before_start", array( $this, 'track_background_scan_start' ) );
add_action( "{$identifier}_completed", array( $this, 'track_background_scan_end' ) );
add_action(
"{$identifier}_cancelled",
array( $this, 'track_background_scan_process_cancellation' ),
10, 2
);
add_action(
"{$identifier}_dead",
array( $this, 'track_background_scan_process_death' ),
10, 2
);
}
/**
* @return Mixpanel
*/
private function get_mixpanel() {
if ( is_null( $this->mixpanel ) ) {
$this->mixpanel = $this->prepare_mixpanel_instance();
}
return $this->mixpanel;
}
public function intercept_settings_update( $old_settings, $settings ) {
if ( empty( $settings['usage'] ) ) {
// Use the most up-to-data value of 'usage'
return;
}
$settings = $this->remove_unchanged_settings( $old_settings, $settings );
$handled = $this->maybe_track_feature_toggle( $settings );
if ( ! $handled ) {
$handled = $this->maybe_track_cdn_update( $settings );
}
}
private function maybe_track_feature_toggle( array $settings ) {
foreach ( $settings as $setting_key => $setting_value ) {
$handler = "track_{$setting_key}_feature_toggle";
if ( method_exists( $this, $handler ) ) {
call_user_func( array( $this, $handler ), $setting_value );
return true;
}
}
return false;
}
private function remove_unchanged_settings( $old_settings, $settings ) {
$changed = array();
foreach ( $settings as $setting_key => $setting_value ) {
$old_setting_value = isset( $old_settings[ $setting_key ] ) ? $old_settings[ $setting_key ] : '';
if ( $old_setting_value !== $setting_value ) {
$changed[ $setting_key ] = $setting_value;
}
}
return $changed;
}
public function get_bulk_properties() {
$bulk_property_labels = array(
'auto' => 'Automatic Compression',
'strip_exif' => 'Metadata',
'resize' => 'Resize Original Images',
'original' => 'Compress original images',
'backup' => 'Backup original images',
'png_to_jpg' => 'Auto-convert PNGs to JPEGs (lossy)',
'no_scale' => 'Disable scaled images',
);
$image_sizes = Settings::get_instance()->get_setting( 'wp-smush-image_sizes' );
$bulk_properties = array(
'Image Sizes' => empty( $image_sizes ) ? 'All' : 'Custom',
'Mode' => $this->settings->get_current_lossy_level_label(),
'Parallel Processing' => defined( 'WP_SMUSH_PARALLEL' ) && WP_SMUSH_PARALLEL ? 'Enabled' : 'Disabled',
);
foreach ( $bulk_property_labels as $bulk_setting => $bulk_property_label ) {
$property_value = Settings::get_instance()->get( $bulk_setting )
? 'Enabled'
: 'Disabled';
$bulk_properties[ $bulk_property_label ] = $property_value;
}
return $bulk_properties;
}
private function track_detection_feature_toggle( $setting_value ) {
return $this->track_feature_toggle( $setting_value, 'Image Resize Detection' );
}
private function track_webp_mod_feature_toggle( $setting_value ) {
return $this->track_feature_toggle( $setting_value, 'Local WebP' );
}
private function track_cdn_feature_toggle( $setting_value ) {
return $this->track_feature_toggle( $setting_value, 'CDN' );
}
private function track_lazy_load_feature_toggle( $setting_value ) {
return $this->track_feature_toggle( $setting_value, 'Lazy Load' );
}
private function track_feature_toggle( $active, $feature ) {
$event = $active
? 'Feature Activated'
: 'Feature Deactivated';
$this->get_mixpanel()->track( $event, array(
'Feature' => $feature,
'Triggered From' => $this->identify_referrer(),
) );
return true;
}
private function identify_referrer() {
$onboarding_request = ! empty( $_REQUEST['action'] ) && 'smush_setup' === $_REQUEST['action'];
if ( $onboarding_request ) {
return 'Wizard';
}
$path = parse_url( wp_get_referer(), PHP_URL_QUERY );
$query_vars = array();
parse_str( $path, $query_vars );
$page = empty( $query_vars['page'] ) ? '' : $query_vars['page'];
$triggered_from = array(
'smush' => 'Dashboard',
'smush-bulk' => 'Bulk Smush',
'smush-directory' => 'Directory Smush',
'smush-lazy-load' => 'Lazy Load',
'smush-cdn' => 'CDN',
'smush-webp' => 'Local WebP',
'smush-integrations' => 'Integrations',
'smush-settings' => 'Settings',
);
return empty( $triggered_from[ $page ] )
? ''
: $triggered_from[ $page ];
}
private function prepare_mixpanel_instance() {
$mixpanel = new Mixpanel( $this->get_token() );
$mixpanel->identify( $this->get_unique_id() );
$mixpanel->registerAll( $this->get_super_properties() );
return $mixpanel;
}
public function get_super_properties() {
global $wp_version;
return array(
'active_theme' => get_stylesheet(),
'locale' => get_locale(),
'mysql_version' => $this->server_utils->get_mysql_version(),
'php_version' => phpversion(),
'plugin' => 'Smush',
'plugin_type' => WP_Smush::is_pro() ? 'pro' : 'free',
'plugin_version' => WP_SMUSH_VERSION,
'server_type' => $this->server_utils->get_server_type(),
'memory_limit' => $this->convert_to_megabytes( $this->server_utils->get_memory_limit() ),
'max_execution_time' => $this->server_utils->get_max_execution_time(),
'wp_type' => is_multisite() ? 'multisite' : 'single',
'wp_version' => $wp_version,
'device' => $this->get_device(),
'user_agent' => $this->server_utils->get_user_agent(),
);
}
private function get_device() {
if ( ! $this->is_mobile() ) {
return 'desktop';
}
if ( $this->is_tablet() ) {
return 'tablet';
}
return 'mobile';
}
private function is_tablet() {
if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
return false;
}
/**
* It doesn't work with IpadOS due to of this:
* https://stackoverflow.com/questions/62323230/how-can-i-detect-with-php-that-the-user-uses-an-ipad-when-my-user-agent-doesnt-c
*/
$tablet_pattern = '/(tablet|ipad|playbook|kindle|silk)/i';
return preg_match( $tablet_pattern, $_SERVER['HTTP_USER_AGENT'] );
}
private function is_mobile() {
if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
return false;
}
// Do not use wp_is_mobile() since it doesn't detect ipad/tablet.
$mobile_patten = '/Mobile|iP(hone|od|ad)|Android|BlackBerry|tablet|IEMobile|Kindle|NetFront|Silk|(hpw|web)OS|Fennec|Minimo|Opera M(obi|ini)|Blazer|Dolfin|Dolphin|Skyfire|Zune|playbook/i';
return preg_match( $mobile_patten, $_SERVER['HTTP_USER_AGENT'] );
}
private function normalize_url( $url ) {
$url = str_replace( array( 'http://', 'https://', 'www.' ), '', $url );
return untrailingslashit( $url );
}
private function maybe_track_cdn_update( $settings ) {
$cdn_properties = array();
$cdn_property_labels = $this->cdn_property_labels();
foreach ( $settings as $setting_key => $setting_value ) {
if ( array_key_exists( $setting_key, $cdn_property_labels ) ) {
$property_label = $cdn_property_labels[ $setting_key ];
$property_value = $setting_value ? 'Enabled' : 'Disabled';
$cdn_properties[ $property_label ] = $property_value;
}
}
if ( $cdn_properties ) {
$this->get_mixpanel()->track( 'CDN Updated', $cdn_properties );
return true;
}
return false;
}
private function cdn_property_labels() {
return array(
'background_images' => 'Background Images',
'auto_resize' => 'Automatic Resizing',
'webp' => 'WebP Conversions',
'rest_api_support' => 'Rest API',
);
}
public function track_directory_smush() {
$this->get_mixpanel()->track( 'Directory Smushed' );
}
public function track_bulk_smush_start() {
$this->get_mixpanel()->track( 'Bulk Smush Started', $this->get_bulk_properties() );
}
public function track_bulk_smush_completed() {
$this->get_mixpanel()->track( 'Bulk Smush Completed', $this->get_bulk_smush_stats() );
}
private function get_bulk_smush_stats() {
$global_stats = WP_Smush::get_instance()->core()->get_global_stats();
$array_util = new Array_Utils();
return array(
'Total Savings' => $this->convert_to_megabytes( (int) $array_util->get_array_value( $global_stats, 'savings_bytes' ) ),
'Total Images' => (int) $array_util->get_array_value( $global_stats, 'count_images' ),
'Media Optimization Percentage' => (float) $array_util->get_array_value( $global_stats, 'percent_optimized' ),
'Percentage of Savings' => (float) $array_util->get_array_value( $global_stats, 'savings_percent' ),
'Images Resized' => (int) $array_util->get_array_value( $global_stats, 'count_resize' ),
'Resize Savings' => $this->convert_to_megabytes( (int) $array_util->get_array_value( $global_stats, 'savings_resize' ) ),
);
}
public function track_config_applied( $config_name ) {
$properties = $config_name
? array( 'Config Name' => $config_name )
: array();
$properties['Triggered From'] = $this->identify_referrer();
$this->get_mixpanel()->track( 'Config Applied', $properties );
}
public function get_unique_id() {
return $this->normalize_url( home_url() );
}
public function get_token() {
return self::PROJECT_TOKEN;
}
public function track_opt_toggle( $old_settings, $settings ) {
$settings = $this->remove_unchanged_settings( $old_settings, $settings );
if ( isset( $settings['usage'] ) ) {
$this->get_mixpanel()->track( $settings['usage'] ? 'Opt In' : 'Opt Out' );
}
}
public function track_integrations_saved( $old_settings, $settings ) {
if ( empty( $settings['usage'] ) ) {
return;
}
$settings = $this->remove_unchanged_settings( $old_settings, $settings );
if ( empty( $settings ) ) {
return;
}
$this->maybe_track_integrations_toggle( $settings );
}
private function maybe_track_integrations_toggle( $settings ) {
$integrations = array(
'gutenberg' => 'Gutenberg',
'gform' => 'Gravity Forms',
'js_builder' => 'WP Bakery',
's3' => 'Amazon S3',
'nextgen' => 'NextGen Gallery',
);
foreach ( $settings as $integration_slug => $is_activated ) {
if ( ! array_key_exists( $integration_slug, $integrations ) ) {
continue;
}
if ( $is_activated ) {
$this->get_mixpanel()->track(
'Integration Activated',
array(
'Integration' => $integrations[ $integration_slug ],
)
);
} else {
$this->get_mixpanel()->track(
'Integration Deactivated',
array(
'Integration' => $integrations[ $integration_slug ],
)
);
}
}
}
public function intercept_reset() {
if ( $this->settings->get( 'usage' ) ) {
$this->get_mixpanel()->track( 'Opt Out' );
}
}
public function track_background_scan_start() {
$properties = array(
'Scan Type' => $this->scan_background_process->get_status()->is_dead() ? 'Retry' : 'New',
);
$this->get_mixpanel()->track( 'Scan Started', array_merge(
$properties,
$this->get_bulk_properties(),
$this->get_scan_properties()
) );
}
public function track_background_scan_end() {
$this->get_mixpanel()->track( 'Scan Ended', array_merge(
$this->get_bulk_properties(),
$this->get_scan_properties()
) );
}
/**
* @param $identifier string
* @param $background_process Background_Process
*
* @return void
*/
public function track_background_scan_process_cancellation( $identifier, $background_process ) {
$this->get_mixpanel()->track(
'Background Scan Process Cancelled',
$this->get_background_process_status_properties( $background_process )
);
}
/**
* @param $identifier string
* @param $background_process Background_Process
*
* @return void
*/
public function track_background_scan_process_death( $identifier, $background_process ) {
$scanner = new Media_Library_Scanner();
$this->get_mixpanel()->track(
'Background Scan Process Dead',
array_merge(
array( 'Slice Size' => $scanner->get_slice_size() ),
$this->get_background_process_status_properties( $background_process )
)
);
}
private function get_scan_properties() {
$global_stats = Global_Stats::get();
$global_stats_array = $global_stats->to_array();
$labels = array(
'image_attachment_count' => 'Image Attachment Count',
'optimized_images_count' => 'Optimized Images Count',
'optimize_count' => 'Optimize Count',
'reoptimize_count' => 'Reoptimize Count',
'ignore_count' => 'Ignore Count',
'animated_count' => 'Animated Count',
'error_count' => 'Error Count',
'percent_optimized' => 'Percent Optimized',
'size_before' => 'Size Before',
'size_after' => 'Size After',
'savings_percent' => 'Savings Percent',
);
$savings_keys = array(
'size_before',
'size_after',
);
foreach ( $labels as $key => $label ) {
if ( isset( $global_stats_array[ $key ] ) ) {
$properties[ $label ] = $global_stats_array[ $key ];
if ( in_array( $key, $savings_keys, true ) ) {
$properties[ $label ] = $this->convert_to_megabytes( $properties[ $label ] );
}
}
}
return $properties;
}
/**
* @param Background_Process $background_process
*
* @return array
*/
private function get_background_process_status_properties( Background_Process $background_process ) {
$background_process_status = $background_process ? $background_process->get_status() : false;
$properties = array();
if ( $background_process_status ) {
$properties = array(
'Total Items' => $background_process_status->get_total_items(),
'Processed Items' => $background_process_status->get_processed_items(),
'Failed Items' => $background_process_status->get_failed_items(),
);
}
return $properties;
}
private function convert_to_megabytes( $size_in_bytes ) {
if ( empty( $size_in_bytes ) ) {
return 0;
}
$unit_mb = pow( 1024, 2 );
return round( $size_in_bytes / $unit_mb, 2 );
}
}

View File

@ -0,0 +1,170 @@
<?php
/**
* Auto resize functionality: Resize_Detection class
*
* @package Smush\Core\Modules
* @version 2.8.0
*
* @author Joel James <joel@incsub.com>
*
* @copyright (c) 2018, Incsub (http://incsub.com)
*/
namespace Smush\Core\Modules;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Resize_Detection
*/
class Resize_Detection extends Abstract_Module {
/**
* Is auto detection enabled.
*
* @var bool
*/
private $can_auto_detect = false;
/**
* Resize_Detection constructor.
*/
public function init() {
// Set auto resize flag.
add_action( 'wp', array( $this, 'init_flags' ) );
// Load js file that is required in public facing pages.
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_resize_assets' ) );
// Set a flag to media library images.
add_filter( 'smush_cdn_image_tag', array( $this, 'skip_image_resize_detection' ) );
// Generate markup for the template engine.
add_action( 'wp_footer', array( $this, 'generate_markup' ) );
}
/**
* Check if auto resize can be performed.
*
* Allow only if current user is admin and auto resize
* detection is enabled in settings.
*/
public function init_flags() {
// Only required for admin users.
if ( $this->settings->get( 'detection' ) && current_user_can( 'manage_options' ) ) {
$this->can_auto_detect = true;
}
}
/**
* Enqueue JS files required in public pages.
*
* Enqueue resize detection js and css files to public
* facing side of the site. Load only if auto detect
* is enabled.
*
* @return void
*/
public function enqueue_resize_assets() {
// Required only if auto detection is required.
if ( ! $this->can_auto_detect ) {
return;
}
// Required scripts for front end.
wp_enqueue_script(
'smush-resize-detection',
WP_SMUSH_URL . 'app/assets/js/smush-rd.min.js',
array( 'jquery' ),
WP_SMUSH_VERSION,
true
);
// Required styles for front end.
wp_enqueue_style(
'smush-resize-detection',
WP_SMUSH_URL . 'app/assets/css/smush-rd.min.css',
array(),
WP_SMUSH_VERSION
);
// Define ajaxurl var.
wp_localize_script(
'smush-resize-detection',
'wp_smush_resize_vars',
array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'ajax_nonce' => wp_create_nonce( 'smush_resize_nonce' ),
// translators: %s - width, %s - height.
'large_image' => sprintf( __( 'This image is too large for its container. Adjust the image dimensions to %1$s x %2$spx for optimal results.', 'wp-smushit' ), 'width', 'height' ),
// translators: %s - width, %s - height.
'small_image' => sprintf( __( 'This image is too small for its container. Adjust the image dimensions to %1$s x %2$spx for optimal results.', 'wp-smushit' ), 'width', 'height' ),
)
);
}
/**
* Generate markup for the template engine.
*
* @since 2.9
*/
public function generate_markup() {
// Required only if auto detection is required.
if ( ! $this->can_auto_detect ) {
return;
}
?>
<div id="smush-image-bar-toggle" class="closed">
<i class="sui-icon-loader" aria-hidden="true"> </i>
</div>
<div id="smush-image-bar" class="closed">
<h3><?php esc_html_e( 'Image Issues', 'wp-smushit' ); ?></h3>
<p>
<?php esc_html_e( 'The images listed below are being resized to fit a container. To avoid serving oversized or blurry images, try to match the images to their container sizes.', 'wp-smushit' ); ?>
</p>
<div id="smush-image-bar-items-bigger">
<strong><?php esc_html_e( 'Oversized', 'wp-smushit' ); ?></strong>
</div>
<div id="smush-image-bar-items-smaller">
<strong><?php esc_html_e( 'Undersized', 'wp-smushit' ); ?></strong>
</div>
<div id="smush-image-bar-notice">
<p><?php esc_html_e( 'All images are properly sized', 'wp-smushit' ); ?></p>
</div>
<p id="smush-image-bar-notice-desc">
<?php esc_html_e( 'Note: Its not always easy to make this happen, fix up what you can.', 'wp-smushit' ); ?>
</p>
</div>
<?php
}
/**
* Exclude images that are hosted on CDN and have auto resize enabled.
*
* @since 3.2.0
*
* @param string $image Image tag.
*
* @return string
*/
public function skip_image_resize_detection( $image ) {
// No need to add attachment id if auto detection is not enabled.
if ( ! $this->can_auto_detect ) {
return $image;
}
// CDN with auto resize need to be enabled.
if ( ! $this->settings->get( 'cdn' ) || ! $this->settings->get( 'auto_resize' ) ) {
return $image;
}
Helpers\Parser::add_attribute( $image, 'no-resize-detection' );
return $image;
}
}

View File

@ -0,0 +1,485 @@
<?php
/**
* Smush resize functionality: Resize class
*
* @package Smush\Core\Modules
* @version 2.3
*
* @author Umesh Kumar <umesh@incsub.com>
*
* @copyright (c) 2016, Incsub (http://incsub.com)
*/
namespace Smush\Core\Modules;
use Smush\Core\Core;
use Smush\Core\Helper;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Resize
*/
class Resize extends Abstract_Module {
/**
* Module slug.
*
* @var string
*/
protected $slug = 'resize';
/**
* Specified width for resizing images
*
* @var int
*/
public $max_w = 0;
/**
* Specified Height for resizing images
*
* @var int
*/
public $max_h = 0;
/**
* If resizing is enabled or not
*
* @var bool
*/
public $resize_enabled = false;
/**
* Resize constructor.
*
* Initialize class variables, after all stuff has been loaded.
*/
public function init() {
add_action( 'admin_init', array( $this, 'initialize' ) );
add_action( 'admin_init', array( $this, 'maybe_disable_module' ), 15 );
// Apply filter(s) if activated resizing.
if ( $this->is_active() ) {
// Add a filter to check if the image should resmush.
//add_filter( 'wp_smush_should_resmush', array( $this, 'should_resmush' ), 10, 2 );
}
}
/**
* Get the settings for resizing
*
* @param bool $skip_check Added for Mobile APP uploads.
*/
public function initialize( $skip_check = false ) {
// Do not initialize unless in the WP Backend Or On one of the smush pages.
if ( ! is_user_logged_in() || ( ! is_admin() && ! $skip_check ) ) {
return;
}
// Make sure the screen function exists.
$current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : false;
if ( ! empty( $current_screen ) && ! $skip_check ) {
// Do not Proceed if not on one of the required screens.
if ( ! in_array( $current_screen->base, Core::$external_pages, true ) && false === strpos( $current_screen->base, 'page_smush' ) ) {
return;
}
}
// If resizing is enabled.
$this->resize_enabled = $this->is_active();
$resize_sizes = $this->settings->get_setting( 'wp-smush-resize_sizes', array() );
// Resize width and Height.
$this->max_w = ! empty( $resize_sizes['width'] ) ? $resize_sizes['width'] : 0;
$this->max_h = ! empty( $resize_sizes['height'] ) ? $resize_sizes['height'] : 0;
}
/**
* We do not need this module on WordPress 5.3+.
*
* @since 3.3.2
*/
public function maybe_disable_module() {
global $wp_version;
$this->resize_enabled = version_compare( $wp_version, '5.3.0', '<' ) || $this->settings->get( 'no_scale' );
}
/**
* Checks whether the image should be resized.
*
* @uses self::check_should_resize().
*
* @param string $id Attachment ID.
* @param string $meta Attachment Metadata.
*
* @return bool Should resize or not
*/
public function should_resize( $id = '', $meta = '' ) {
/**
* If resizing not enabled, or if both max width and height is set to 0, return.
*
* Do not use $this->resize_enabled here, because the initialize does not always detect the proper screen
* in the media library or via ajax requests.
*/
if ( ! $this->is_active() || ( 0 === $this->max_w && 0 === $this->max_h ) || ! Helper::is_smushable( $id ) ) {
return false;
}
// Check it from the cache.
if ( null !== Helper::cache_get( $id, 'should_resize' ) ) {
return Helper::cache_get( $id, 'should_resize' );
}
/**
* Filter whether the uploaded image should be resized or not
*
* @since 2.3
*
* @param bool $should_resize Whether to resize the image.
* @param array $id Attachment ID.
* @param array $meta Attachment Metadata.
*/
$should_resize = apply_filters( 'wp_smush_resize_uploaded_image', $this->check_should_resize( $id, $meta ), $id, $meta );
/**
* We used this inside Backup::create_backup() and Smush function
* so cache result to avoid to check it again.
*/
Helper::cache_set( $id, $should_resize, 'should_resize' );
return $should_resize;
}
/**
* Checks whether the image should be resized judging by its properties.
*
* @since 3.8.3
*
* @param string $id Attachment ID.
* @param string $meta Attachment Metadata.
*
* @return bool
*/
private function check_should_resize( $id = '', $meta = '' ) {
/**
* Get unfiltered file path if it exists, otherwise we will use filtered attached file ( e.g s3).
* Please check Png2jpg::__construct() for the detail.
*/
$file_path = Helper::get_attached_file( $id, 'check-resize' );
if ( ! empty( $file_path ) ) {
// Skip: if "noresize" is included in the filename, Thanks to Imsanity.
if ( strpos( $file_path, 'noresize' ) !== false ) {
return false;
}
} else {
// Nothing to check.
return false;
}
// Get attachment metadata.
$meta = empty( $meta ) ? wp_get_attachment_metadata( $id ) : $meta;
if ( empty( $meta['width'] ) || empty( $meta['height'] ) ) {
return false;
}
// If GIF is animated, return.
if ( Helper::check_animated_status( $file_path, $id ) ) {
return false;
}
$old_width = $meta['width'];
$old_height = $meta['height'];
$resize_dim = $this->settings->get_setting( 'wp-smush-resize_sizes' );
$max_width = ! empty( $resize_dim['width'] ) ? $resize_dim['width'] : 0;
$max_height = ! empty( $resize_dim['height'] ) ? $resize_dim['height'] : 0;
if ( ( $old_width > $max_width && $max_width > 0 ) || ( $old_height > $max_height && $max_height > 0 ) ) {
return true;
}
return false;
}
/**
* Check whether to resmush image or not.
*
* @since 3.9.6
*
* @usedby Smush\App\Ajax::scan_images()
*
* @param bool $should_resmush Should resmush status.
* @param int $attachment_id Attachment ID.
* @return bool|string resize|TRUE|FALSE
*/
public function should_resmush( $should_resmush, $attachment_id ) {
if ( ! $should_resmush && $this->should_resize( $attachment_id ) ) {
$should_resmush = 'resize';
}
return $should_resmush;
}
/**
* Handles the Auto resizing of new uploaded images
*
* @param int $id Attachment ID.
* @param mixed $meta Attachment Metadata.
*
* @return mixed Updated/Original Metadata if image was resized or not
*/
public function auto_resize( $id, $meta ) {
// Do not perform resize while restoring images/ Editing images.
if ( ! empty( $_REQUEST['do'] ) && ( 'restore' === $_REQUEST['do'] || 'scale' === $_REQUEST['do'] ) ) {
return $meta;
}
// Check if we should resize the image.
if ( ! $this->should_resize( $id, $meta ) ) {
return $meta;
}
$savings = array(
'bytes' => 0,
'size_before' => 0,
'size_after' => 0,
);
// Good to go.
$file_path = Helper::get_attached_file( $id, 'resize' );// S3+.
// Make sure scaled file exits.
if ( ! file_exists( $file_path ) ) {
return;
}
$original_file_size = filesize( $file_path );
$resize = $this->perform_resize( $file_path, $original_file_size, $id, $meta );
// If resize wasn't successful.
if ( ! $resize || $resize['filesize'] >= $original_file_size ) {
update_post_meta( $id, 'wp-smush-resize_savings', $savings );
return $meta;
}
// Else Replace the Original file with resized file.
$replaced = $this->replace_original_image( $file_path, $resize, $meta );
if ( $replaced ) {
// Clear Stat Cache, Else the size obtained is same as the original file size.
clearstatcache();
// Updated File size.
$u_file_size = filesize( $file_path );
$savings['bytes'] = $original_file_size > $u_file_size ? $original_file_size - $u_file_size : 0;
$savings['size_before'] = $original_file_size;
$savings['size_after'] = $u_file_size;
// Store savings in metadata.
update_post_meta( $id, 'wp-smush-resize_savings', $savings );
$meta['width'] = ! empty( $resize['width'] ) ? $resize['width'] : $meta['width'];
$meta['height'] = ! empty( $resize['height'] ) ? $resize['height'] : $meta['height'];
/**
* Called after the image has been successfully resized
* Can be used to update the stored stats
*/
do_action( 'wp_smush_image_resized', $id, $savings );
/**
* The file resized,
* we can clear the temp cache related to this resizing.
*/
Helper::cache_delete( 'should_resize' );
}
return $meta;
}
/**
* Generates the new image for specified width and height,
* Checks if the size of generated image is greater,
*
* @param string $file_path Original File path.
* @param int $original_file_size File size before optimisation.
* @param int $id Attachment ID.
* @param array $meta Attachment Metadata.
* @param bool $unlink Whether to unlink the original image or not.
*
* @return array|bool|false If the image generation was successful
*/
public function perform_resize( $file_path, $original_file_size, $id, $meta = array(), $unlink = true ) {
/**
* Filter the resize image dimensions
*
* @since 2.3
*
* @param array $sizes {
* Array of sizes containing max width and height for all the uploaded images.
*
* @type int $width Maximum Width For resizing
* @type int $height Maximum Height for resizing
* }
*
* @param string $file_path Original Image file path
*
* @param array $upload {
* Array of upload data.
*
* @type string $file Filename of the newly-uploaded file.
* @type string $url URL of the uploaded file.
* @type string $type File type.
* }
*
* @hooked Png2jpg::cache_can_be_converted_status() Save transparent status before resizing the image.
*/
$sizes = apply_filters(
'wp_smush_resize_sizes',
array(
'width' => $this->max_w,
'height' => $this->max_h,
),
$file_path,
$id
);
$data = image_make_intermediate_size( $file_path, $sizes['width'], $sizes['height'] );
// If the image wasn't resized.
if ( empty( $data['file'] ) ) {
if ( $this->try_gd_fallback() ) {
$data = image_make_intermediate_size( $file_path, $sizes['width'], $sizes['height'] );
}
if ( empty( $data['file'] ) ) {
Helper::logger()->resize()->warning( sprintf( 'Cannot resize image [%s(%d)].', Helper::clean_file_path( $file_path ), $id ) );
return false;
}
}
// Check if file size is lesser than original image.
$resize_path = path_join( dirname( $file_path ), $data['file'] );
if ( ! file_exists( $resize_path ) ) {
Helper::logger()->resize()->notice( sprintf( 'The resized image [%s(%d)] does not exist.', Helper::clean_file_path( $resize_path ), Helper::clean_file_path( $file_path ), $id ) );
return false;
}
$data['file_path'] = $resize_path;
$file_size = filesize( $resize_path );
$data['filesize'] = $file_size;
if ( $file_size > $original_file_size ) {
// Don't Unlink for nextgen images.
if ( $unlink ) {
$this->maybe_unlink( $resize_path, $meta );
}
Helper::logger()->resize()->notice( sprintf( 'The resized image [%s](%s) is larger than the original image [%s(%d)](%s).', Helper::clean_file_path( $resize_path ), size_format( $file_size ), Helper::clean_file_path( $file_path ), $id, size_format( $original_file_size ) ) );
}
return $data;
}
/**
* Fix for WP Engine 'width or height exceeds limit' Imagick error.
*
* If unable to resize with Imagick, try to fallback to GD.
*
* @since 3.4.0
*/
private function try_gd_fallback() {
if ( ! function_exists( 'gd_info' ) ) {
return false;
}
return add_filter(
'wp_image_editors',
function( $editors ) {
$editors = array_diff( $editors, array( 'WP_Image_Editor_GD' ) );
array_unshift( $editors, 'WP_Image_Editor_GD' );
return $editors;
}
);
}
/**
* Replace the original file with resized file
*
* @param string $file_path File path.
* @param mixed $resized Resized.
* @param array $meta Meta.
*
* @return bool
*/
private function replace_original_image( $file_path, $resized, $meta = array() ) {
$replaced = copy( $resized['file_path'], $file_path );
$this->maybe_unlink( $resized['file_path'], $meta );
return $replaced;
}
/**
* Return Filename.
*
* @param string $filename Filename.
*
* @return mixed
*/
public function file_name( $filename ) {
if ( empty( $filename ) ) {
return $filename;
}
return $filename . 'tmp';
}
/**
* Do not unlink the resized file if the name is similar to one of the image sizes
*
* @param string $path Image File Path.
* @param array $meta Image Meta.
*
* @return bool
*/
private function maybe_unlink( $path, $meta ) {
if ( empty( $path ) || ! file_exists( $path ) ) {
return true;
}
// Unlink directly if meta value is not specified.
if ( empty( $meta['sizes'] ) ) {
unlink( $path );
}
$unlink = true;
// Check if the file name is similar to one of the image sizes.
$path_parts = pathinfo( $path );
$filename = ! empty( $path_parts['basename'] ) ? $path_parts['basename'] : $path_parts['filename'];
if ( ! empty( $meta['sizes'] ) ) {
foreach ( $meta['sizes'] as $image_size ) {
if ( false === strpos( $image_size['file'], $filename ) ) {
continue;
}
$unlink = false;
break;
}
}
if ( $unlink ) {
unlink( $path );
}
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,961 @@
<?php
/**
* WebP class: WebP
*
* @package Smush\Core\Modules
* @since 3.8.0
*/
namespace Smush\Core\Modules;
use Smush\Core\Core;
use Smush\Core\Helper;
use Smush\Core\Stats\Global_Stats;
use WP_Error;
use WP_Smush;
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class WebP extends Abstract_Module.
*/
class WebP extends Abstract_Module {
/**
* Module slug.
*
* @var string
*/
protected $slug = 'webp_mod';
/**
* Whether module is pro or not.
*
* @var string
*/
protected $is_pro = true;
/**
* If server is configured for webp
*
* @access private
* @var bool $is_configured
*/
private $is_configured;
/**
* Initialize the module.
*/
public function init() {
// Show success message after deleting all webp images.
add_action( 'wp_smush_header_notices', array( $this, 'maybe_show_notices' ) );
// Only apply filters for PRO + activated Webp.
if ( $this->is_active() ) {
// Add a filter to check if the image should resmush.
//add_filter( 'wp_smush_should_resmush', array( $this, 'should_resmush' ), 10, 2 );
}
}
/**
* Enables and disables the WebP module.
*
* @since 3.8.0
*
* @param boolean $enable Whether to enable or disable WebP.
*/
public function toggle_webp( $enable = true ) {
$this->settings->set( $this->slug, $enable );
global $wp_filesystem;
if ( is_null( $wp_filesystem ) ) {
// These aren't included when applying a config from the Hub.
if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();
}
$parsed_udir = $this->get_upload_dir();
$flag_file_path = $parsed_udir['webp_path'] . '/disable_smush_webp';
// Handle the file used as a flag by the server rules.
if ( $enable ) {
$wp_filesystem->delete( $flag_file_path, true );
} else {
$wp_filesystem->put_contents( $flag_file_path, '' );
}
}
/**
* Gets whether WebP is configured, returning a message to display when it's not.
* This is a wrapper for displaying a message on failure which is used in three places.
* Moved here to reduce the redundancy.
*
* @since 3.8.8
*
* @param bool $force Force check.
*
* @return true|string True when it's configured. String when it's not.
*/
public function get_is_configured_with_error_message( $force = false ) {
$is_configured = $this->is_configured( $force );
if ( true === $is_configured ) {
return true;
}
if ( is_wp_error( $is_configured ) ) {
return $is_configured->get_error_message();
}
if ( 'apache' === $this->get_server_type() && $this->is_htaccess_written() ) {
return __( "The server rules have been applied but the server doesn't seem to be serving your images as WebP. We recommend contacting your hosting provider to learn more about the cause of this issue.", 'wp-smushit' );
}
return __( "Server configurations haven't been applied yet. Make configurations to start serving images in WebP format.", 'wp-smushit' );
}
/**
* Get status of server configuration for webp.
*
* @since 3.8.0
*
* @param bool $force force to recheck.
*
* @return bool|WP_Error
*/
public function is_configured( $force = false ) {
if ( ! is_null( $this->is_configured ) && ! $force ) {
return $this->is_configured;
}
$this->is_configured = $this->check_server_config();
return $this->is_configured;
}
/**
* Check if server is configured to serve webp image.
*
* @since 3.8.0
*
* @return bool|WP_Error
*/
private function check_server_config() {
$files_created = $this->create_test_files();
// WebP test images couldn't be created.
if ( true !== $files_created ) {
$message = sprintf(
/* translators: path that couldn't be written */
__( 'We couldn\'t create the WebP test files. This is probably due to your current folder permissions. Please adjust the permissions for "%s" to 755 and try again.', 'wp-smushit' ),
$files_created
);
return new WP_Error( 'test_files_not_created', $message );
}
$udir = $this->get_upload_dir();
$test_image = $udir['upload_url'] . '/smush-webp-test.png';
$args = array(
'timeout' => 10,
'headers' => array(
'Accept' => 'image/webp',
),
);
// Add support for basic auth in WPMU DEV staging.
if ( isset( $_SERVER['WPMUDEV_HOSTING_ENV'] ) && 'staging' === $_SERVER['WPMUDEV_HOSTING_ENV'] && isset( $_SERVER['PHP_AUTH_USER'] ) ) {
$args['headers']['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
}
$response = wp_remote_get( $test_image, $args );
// If there is an error, return.
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
// Check the image's format when the request was successful.
if ( 200 === $code ) {
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
return 'image/webp' === $content_type;
}
// Return the response code and message otherwise.
$error_message = sprintf(
/* translators: 1. error code, 2. error message. */
__( "We couldn't check the WebP server rules status because there was an error with the test request. Please contact support for assistance. Code %1\$s: %2\$s.", 'wp-smushit' ),
$code,
wp_remote_retrieve_response_message( $response )
);
return new WP_Error( $code, $error_message );
}
/**
* Code to use on Nginx servers.
*
* @since 3.8.0
*
* @param bool $marker whether to wrap code with marker comment lines.
* @return string
*/
public function get_nginx_code( $marker = true ) {
$udir = $this->get_upload_dir();
$base = trailingslashit( dirname( $udir['upload_rel_path'] ) );
$directory = trailingslashit( basename( $udir['upload_rel_path'] ) );
$regex_base = $base . '(' . $directory . ')';
/**
* We often need to remove WebP file extension from Nginx cache rule in order to make Smush WebP work,
* so always add expiry header rule for Nginx.
*
* @since 3.9.8
* @see https://incsub.atlassian.net/browse/SMUSH-1072
*/
$code = 'location ~* "' . str_replace( '/', '\/', $regex_base ) . '(.*\.(?:png|jpe?g))" {
add_header Vary Accept;
set $image_path $2;
if (-f "' . $udir['webp_path'] . '/disable_smush_webp") {
break;
}
if ($http_accept !~* "webp") {
break;
}
expires max;
try_files /' . trailingslashit( $udir['webp_rel_path'] ) . '$image_path.webp $uri =404;
}';
if ( true === $marker ) {
$code = $this->marker_line() . "\n" . $code;
$code = $code . "\n" . $this->marker_line( true );
}
return apply_filters( 'smush_nginx_webp_rules', $code );
}
/**
* Code to use on Apache servers.
*
* @since 3.8.0
*
* @todo Find out what's wrong with the rules. We shouldn't need these two different RewriteRule.
*
* @param string $location Where the .htaccess file is.
*
* @return string
*/
private function get_apache_code( $location ) {
$udir = $this->get_upload_dir();
$rewrite_path = '%{DOCUMENT_ROOT}/' . $udir['webp_rel_path'];
$code = '<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond ' . $rewrite_path . '/disable_smush_webp !-f
RewriteCond %{HTTP_ACCEPT} image/webp' . "\n";
if ( 'root' === $location ) {
// This works on single sites at root.
$code .= ' RewriteCond ' . $rewrite_path . '/$1.webp -f
RewriteRule ' . $udir['upload_rel_path'] . '/(.*\.(?:png|jpe?g))$ ' . $udir['webp_rel_path'] . '/$1.webp [NC,T=image/webp]';
} else {
// This works at /uploads/.
$code .= ' RewriteCond ' . $rewrite_path . '/$1.$2.webp -f
RewriteRule ^/?(.+)\.(jpe?g|png)$ /' . $udir['webp_rel_path'] . '/$1.$2.webp [NC,T=image/webp]';
}
$code .= "\n" . '</IfModule>
<IfModule mod_headers.c>
Header append Vary Accept env=WEBP_image
</IfModule>
<IfModule mod_mime.c>
AddType image/webp .webp
</IfModule>';
return apply_filters( 'smush_apache_webp_rules', $code );
}
/**
* Gets the apache rules for printing them in the config tab.
*
* @since 3.8.4
*
* @return string
*/
public function get_apache_code_to_print() {
$location = is_multisite() ? 'uploads' : 'root';
$code = $this->marker_line() . "\n";
$code .= $this->get_apache_code( $location );
$code .= "\n" . $this->marker_line( true );
return $code;
}
/**
* Retrieves uploads directory and WebP directory information.
* All paths and urls do not have trailing slash.
*
* @return array
*/
public function get_upload_dir() {
static $upload_dir_info;
if ( isset( $upload_dir_info ) ) {
return $upload_dir_info;
}
if ( ! is_multisite() || is_main_site() ) {
$upload = wp_upload_dir();
} else {
// Use the main site's upload directory for all subsite's webp converted images.
// This makes it easier to have a single rule on the server configs for serving webp in mu.
$blog_id = get_main_site_id();
switch_to_blog( $blog_id );
$upload = wp_upload_dir();
restore_current_blog();
}
// Is it possible that none of the following conditions are met?
$root_path_base = '';
// Get the Document root path. There must be a better way to do this.
// For example, /srv/www/site/public_html for /srv/www/site/public_html/wp-content/uploads.
if ( 0 === strpos( $upload['basedir'], ABSPATH ) ) {
// Environments like Flywheel have an ABSPATH that's not used in the paths.
$root_path_base = ABSPATH;
} elseif ( ! empty( $_SERVER['DOCUMENT_ROOT'] ) && 0 === strpos( $upload['basedir'], wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) ) {
/**
* This gets called when scanning for uncompressed images.
* When ran from certain contexts, $_SERVER['DOCUMENT_ROOT'] might not be set.
*
* We are removing this part from the path later on.
*/
$root_path_base = realpath( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
} elseif ( 0 === strpos( $upload['basedir'], dirname( WP_CONTENT_DIR ) ) ) {
// We're assuming WP_CONTENT_DIR is only one level deep into the document root.
// This might not be true in customized sites. A bit edgy.
$root_path_base = dirname( WP_CONTENT_DIR );
}
/**
* Filters the Document root path used to get relative paths for webp rules.
* Hopefully of help for debugging and SLS.
*
* @since 3.9.0
*/
$root_path_base = apply_filters( 'smush_webp_rules_root_path_base', $root_path_base );
// Get the upload path relative to the Document root.
// For example, wp-content/uploads for /srv/www/site/public_html/wp-content/uploads.
$upload_root_rel_path = ltrim( str_replace( $root_path_base, '', $upload['basedir'] ), '/' );
// Get the relative path for the directory containing the webp files.
// This directory is a sibling of the 'uploads' directory.
// For example, wp-content/smush-webp for wp-content/uploads.
$webp_root_rel_path = dirname( $upload_root_rel_path ) . '/smush-webp';
/**
* Add a hook for user custom webp address.
*
* @since 3.9.8
*/
return apply_filters(
'wp_smush_webp_dir',
array(
'upload_path' => $upload['basedir'],
'upload_rel_path' => $upload_root_rel_path,
'upload_url' => $upload['baseurl'],
'webp_path' => dirname( $upload['basedir'] ) . '/smush-webp',
'webp_rel_path' => $webp_root_rel_path,
'webp_url' => dirname( $upload['baseurl'] ) . '/smush-webp',
)
);
}
/**
* Create test files and required directory.
*
* @return true|string String with the path that couldn't be written on failure.
*/
public function create_test_files() {
$udir = $this->get_upload_dir();
$test_png_file = $udir['upload_path'] . '/smush-webp-test.png';
$test_webp_file = $udir['webp_path'] . '/smush-webp-test.png.webp';
// Create the png file to be requested if it doesn't exist. Bail out if it fails.
if (
! file_exists( $test_png_file ) &&
! copy( WP_SMUSH_DIR . 'app/assets/images/smush-webp-test.png', $test_png_file )
) {
Helper::logger()->webp()->error( 'Cannot create test PNG file [%s].', $test_png_file );
return $udir['upload_path'];
}
// Create the WebP file that should be sent in the response if the rules work.
if ( ! file_exists( $test_webp_file ) ) {
$directory_created = is_dir( $udir['webp_path'] ) || wp_mkdir_p( $udir['webp_path'] );
// Bail out if it fails.
if (
! $directory_created ||
! copy( WP_SMUSH_DIR . 'app/assets/images/smush-webp-test.png.webp', $test_webp_file )
) {
Helper::logger()->webp()->error( 'Cannot create test Webp file [%s].', $test_webp_file );
return $udir['webp_path'];
}
}
return true;
}
/**
* Retrieves related webp image file path for a given non webp image file path.
* Also create required directories for webp image if not exists.
*
* @param string $file_path Non webp image file path.
* @param bool $make Weather to create required directories.
*
* @return string
*/
public function get_webp_file_path( $file_path, $make = false ) {
$udir = $this->get_upload_dir();
$file_rel_path = substr( $file_path, strlen( $udir['upload_path'] ) );
$webp_file_path = $udir['webp_path'] . $file_rel_path . '.webp';
if ( $make ) {
$webp_file_dir = dirname( $webp_file_path );
if ( ! is_dir( $webp_file_dir ) ) {
wp_mkdir_p( $webp_file_dir );
}
}
return $webp_file_path;
}
/**
* Check whether the given attachment id or mime type can be converted to WebP.
*
* @param string $id Atachment ID.
* @param string $mime Mime type.
*
* @return bool
*/
private function can_be_converted( $id = '', $mime = '' ) {
if ( empty( $id ) && empty( $mime ) ) {
return false;
}
$mime = empty( $mime ) ? get_post_mime_type( $id ) : $mime;
// This image can not be converted to webp.
if ( ! ( 'image/jpeg' === $mime || 'image/png' === $mime ) ) {
return false;
}
return true;
}
/**
* Checks whether an attachment should be converted to WebP.
* Returns false if WebP isn't configured, the attachment was already converted,
* or if the attachment can't be converted ( @see self::can_be_converted() ).
*
* @since 3.8.0
*
* @param string $id Attachment ID.
*
* @return bool
*/
public function should_be_converted( $id ) {
// Avoid conversion when webp disabled, or when Smush is Free.
if ( ! $this->is_active() || ! Helper::is_smushable( $id ) ) {
return false;
}
$meta = get_post_meta( $id, Smush::$smushed_meta_key, true );
$webp_udir = $this->get_upload_dir();
// The image was already converted to WebP.
if ( ! empty( $meta['webp_flag'] ) && file_exists( $webp_udir['webp_path'] . '/' . $meta['webp_flag'] ) ) {
Helper::logger()->webp()->info( sprintf( 'The image [%d] is already converted to Webp: [%s]', $id, $meta['webp_flag'] ) );
return false;
}
return $this->can_be_converted( $id );
}
/**
* Check whether to resmush image or not.
*
* @since 3.9.6
*
* @usedby Smush\App\Ajax::scan_images()
*
* @param bool $should_resmush Current status.
* @param int $attachment_id Attachment ID.
* @return bool webp|TRUE|FALSE.
*/
public function should_resmush( $should_resmush, $attachment_id ) {
if ( ! $should_resmush && $this->should_be_converted( $attachment_id ) ) {
$should_resmush = 'webp';
}
return $should_resmush;
}
/**
* Convert images to WebP.
*
* @since 3.8.0
*
* @param int $attachment_id Attachment ID.
* @param array $meta Attachment meta.
*
* @return WP_Error|array
*/
public function convert_to_webp( $attachment_id, $meta ) {
$webp_files = array();
if ( ! $this->should_be_converted( $attachment_id ) ) {
return $webp_files;
}
// Maybe add scaled image file to the meta sizes.
$meta = apply_filters( 'wp_smush_add_scaled_images_to_meta', $meta, $attachment_id );
// File path and URL for original image.
$file_path = Helper::get_attached_file( $attachment_id );// S3+.
$smush = WP_Smush::get_instance()->core()->mod->smush;
// initial an error.
$errors = null;
// If images has other registered size, smush them first.
if ( ! empty( $meta['sizes'] ) && ! has_filter( 'wp_image_editors', 'photon_subsizes_override_image_editors' ) ) {
/**
* Add the full image as a converted image to avoid doing it duplicate.
* E.g. Exclude the scaled file when disable compress the original image.
*/
$converted_thumbs = array(
basename( $file_path ) => 1,
);
foreach ( $meta['sizes'] as $size_data ) {
// Some thumbnail sizes are using the same image path, so check if the thumbnail file is converted.
if ( isset( $converted_thumbs[ $size_data['file'] ] ) ) {
continue;
}
// 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 && false === array_search( $ext, Core::$mime_types, true ) ) {
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;
}
$response = $smush->do_smushit( $file_path_size, true );
if ( is_wp_error( $response ) || ! $response ) {
// Logged the error inside do_smushit.
if ( ! $errors ) {
$errors = new WP_Error();
}
if ( ! $response ) {
// Handle empty response.
if ( $errors->get_error_data( 'empty_response' ) ) {
$errors->add(
'empty_response',
__( 'Webp no response was received.', 'wp-smushit' ),
array(
'filename' => Helper::clean_file_path( $file_path_size ),
)
);
} else {
$errors->add_data(
array(
'filename' => Helper::clean_file_path( $file_path_size ),
),
'empty_response'
);
}
} else {
// Handle WP_Error.
$errors->merge_from( $response );
}
} else {
$webp_files[] = $this->get_webp_file_path( $file_path_size );
// Cache converted thumbnail file.
$converted_thumbs[ $size_data['file'] ] = 1;
}
}
}
// Return errors.
if ( $errors ) {
return $errors;
}
$response = $smush->do_smushit( $file_path, true );
// Logged the error inside do_smushit.
if ( ! is_wp_error( $response ) ) {
$webp_files[] = $this->get_webp_file_path( $file_path );
// If all images have been converted, set a flag in meta.
$stats = get_post_meta( $attachment_id, Smush::$smushed_meta_key, true );
if ( ! $stats ) {
$stats = array();
}
$upload_dir = $this->get_upload_dir();
// Use the relative path of the first webp image as a flag.
$stats['webp_flag'] = substr( $webp_files[0], strlen( $upload_dir['webp_path'] . '/' ) );
update_post_meta( $attachment_id, Smush::$smushed_meta_key, $stats );
}
return $webp_files;
}
/**
* Deletes all the webp files when an attachment is deleted
* Update Smush::$smushed_meta_key meta ( optional )
* Used in Smush::delete_images() and Backup::restore_image()
*
* @since 3.8.0
*
* @param int $image_id Attachment ID.
* @param bool $update_meta Whether to update meta or not.
* @param string $main_file Main file to replace the one retrieved via the $id.
* Useful for deleting webp images after PNG to JPG conversion.
*/
public function delete_images( $image_id, $update_meta = true, $main_file = '' ) {
$meta = wp_get_attachment_metadata( $image_id );
// File path for original image.
if ( empty( $main_file ) ) {
$main_file = get_attached_file( $image_id );
}
// Not a supported image? Exit.
if ( ! in_array( strtolower( pathinfo( $main_file, PATHINFO_EXTENSION ) ), array( 'gif', 'jpg', 'jpeg', 'png' ), true ) ) {
return;
}
$main_file_webp = $this->get_webp_file_path( $main_file );
$dir_path = dirname( $main_file_webp );
if ( file_exists( $main_file_webp ) ) {
unlink( $main_file_webp );
}
if ( ! empty( $meta['sizes'] ) ) {
foreach ( $meta['sizes'] as $size_data ) {
$size_file = path_join( $dir_path, $size_data['file'] );
if ( file_exists( $size_file . '.webp' ) ) {
unlink( $size_file . '.webp' );
}
}
}
if ( $update_meta ) {
$smushed_meta_key = Smush::$smushed_meta_key;
$stats = get_post_meta( $image_id, $smushed_meta_key, true );
if ( ! empty( $stats ) && is_array( $stats ) ) {
unset( $stats['webp_flag'] );
update_post_meta( $image_id, $smushed_meta_key, $stats );
}
}
}
/**
* Deletes all webp images for the whole network or the current subsite.
* It deletes the whole smush-webp directory when it's a single install
* or a MU called from the network admin (and the current_user_can( manage_network )).
*
* @since 3.8.0
*/
public function delete_all() {
global $wp_filesystem;
if ( is_null( $wp_filesystem ) ) {
WP_Filesystem();
}
$parsed_udir = $this->get_upload_dir();
// Delete the whole webp directory only when on single install or network admin.
$wp_filesystem->delete( $parsed_udir['webp_path'], true );
}
/**
* Renders the notice after deleting all webp images.
*
* @since 3.8.0
*
* @param string $tab Smush tab name.
*/
public function maybe_show_notices( $tab ) {
// Show only on WebP page.
if ( ! isset( $tab ) || 'webp' !== $tab ) {
return;
}
// Show only when there are images in the library, except on mu, where the count is always 0.
$core = WP_Smush::get_instance()->core();
$global_stats = $core->get_global_stats();
if ( ! is_multisite() && empty( $global_stats['count_total'] ) ) {
return;
}
$show_message = filter_input( INPUT_GET, 'notice', FILTER_SANITIZE_SPECIAL_CHARS );
// Success notice after deleting all WebP images.
if ( 'webp-deleted' === $show_message ) {
$message = __( 'WebP files were deleted successfully.', 'wp-smushit' );
echo '<div role="alert" id="wp-smush-webp-delete-all-notice" data-message="' . esc_attr( $message ) . '" class="sui-notice" aria-live="assertive"></div>';
}
}
/*
* Server related methods.
*/
/**
* Return the server type (Apache, NGINX...)
*
* @return string Server type
*/
public function get_server_type() {
global $is_apache, $is_IIS, $is_iis7, $is_nginx;
if ( $is_apache ) {
// It's a common configuration to use nginx in front of Apache.
// Let's make sure that this server is Apache.
$response = wp_remote_get( home_url() );
if ( is_wp_error( $response ) ) {
// Bad luck.
return 'apache';
}
$server = strtolower( wp_remote_retrieve_header( $response, 'server' ) );
// Could be LiteSpeed too.
return ( strpos( $server, 'nginx' ) !== false ? 'nginx' : 'apache' );
}
if ( $is_nginx ) {
return 'nginx';
}
if ( $is_IIS ) {
return 'IIS';
}
if ( $is_iis7 ) {
return 'IIS 7';
}
return 'unknown';
}
/*
* Apache's .htaccess rules handling.
*/
/**
* Gets the path of .htaccess file for the given location.
*
* @param string $location Location of the .htaccess file to retrieve. root|uploads.
*
* @return string
*/
private function get_htaccess_file( $location ) {
if ( 'root' === $location ) {
// Get the .htaccess located at the root.
$base_dir = get_home_path();
} else {
// Get the .htaccess located at the uploads directory.
if ( ! function_exists( 'wp_upload_dir' ) ) {
require_once ABSPATH . 'wp-includes/functions.php';
}
$uploads = wp_upload_dir();
$base_dir = $uploads['basedir'];
}
return rtrim( $base_dir, '/' ) . '/.htaccess';
}
/**
* Get unique string to use at marker comment line in .htaccess or nginx config file.
*
* @since 3.8.0
*
* @return string
*/
private function marker_suffix() {
return 'SMUSH-WEBP';
}
/**
* Get unique string to use as marker comment line in .htaccess or nginx config file.
*
* @param bool $end whether to use marker after end of the config code.
* @return string
*/
private function marker_line( $end = false ) {
if ( true === $end ) {
return '# END ' . $this->marker_suffix();
} else {
return '# BEGIN ' . $this->marker_suffix();
}
}
/**
* Check if .htaccess has rules for this module in place.
*
* @since 3.8.0
*
* @param bool|string $location Location of the .htaccess to check.
*
* @return bool
*/
public function is_htaccess_written( $location = false ) {
if ( ! function_exists( 'extract_from_markers' ) ) {
require_once ABSPATH . 'wp-admin/includes/misc.php';
}
$has_rules = false;
// Remove the rules from all the possible places if not specified.
$locations = ! $location ? $this->get_htaccess_locations() : array( $location );
foreach ( $locations as $name ) {
$htaccess = $this->get_htaccess_file( $name );
$has_rules = ! empty( $has_rules ) || array_filter( extract_from_markers( $htaccess, $this->marker_suffix() ) );
}
return $has_rules;
}
/**
* Tries different rules in different locations of the .htaccess file.
*
* @since 3.8.0
*
* @return bool|string True on success. String with the error message on failure.
*/
public function save_htaccess() {
$cannot_write_message = sprintf(
/* translators: 1. opening 'a' tag to premium support, 2. closing 'a' tag. */
__( 'We tried to apply the .htaccess rules automatically but we were unable to complete this action. Make sure the file permissions on your .htaccess file are set to 644, or switch to manual mode and apply the rules yourself. If you need further assistance, you can %1$scontact support%2$s for help.', 'wp-smushit' ),
'<a href="https://wpmudev.com/hub2/support/#get-support" target="_blank">',
'</a>'
);
$last_error = sprintf(
/* translators: 1. opening 'a' tag to docs, 2. opening 'a' tag to premium support, 3. closing 'a' tag. */
__( 'We tried different rules but your server still isn\'t serving WebP images. Please contact your hosting provider for further assistance. You can also see our %1$stroubleshooting guide%3$s or %2$scontact support%3$s for help.', 'wp-smushit' ),
'<a href="https://wpmudev.com/docs/wpmu-dev-plugins/smush/#wordpress-in-its-own-directory" target="_blank">',
'<a href="https://wpmudev.com/hub2/support/#get-support" target="_blank">',
'</a>'
);
$locations = $this->get_htaccess_locations();
$is_configured = false;
foreach ( $locations as $location ) {
$htaccess = $this->get_htaccess_file( $location );
$code = $this->get_apache_code( $location );
$code = explode( "\n", $code );
$markers_inserted = insert_with_markers( $htaccess, $this->marker_suffix(), $code );
if ( ! $markers_inserted ) {
$last_error = $cannot_write_message;
continue;
}
$is_configured = $this->check_server_config();
if ( true === $is_configured ) {
break;
}
// TODO: if $is_configured is a wp error, display the message.
if ( $is_configured && is_wp_error( $is_configured ) ) {
Helper::logger()->webp()->error( sprintf( 'Server config error: %s.', $is_configured->get_error_message() ) );
}
$this->unsave_htaccess( $location );
}
if ( $is_configured ) {
return true;
}
return $last_error;
}
/**
* Returns the handled locations for the .htaccess.
*
* @since 3.8.3
*
* @return array
*/
private function get_htaccess_locations() {
if ( ! is_multisite() ) {
$locations[] = 'root';
}
$locations[] = 'uploads';
return $locations;
}
/**
* Remove rules from .htaccess file.
*
* @since 3.8.0
*
* @param bool|string $location Location of the htaccess to unsave. uploads|root.
*
* @return bool|string True on success. String with the error message on failure.
*/
public function unsave_htaccess( $location = false ) {
if ( ! $this->is_htaccess_written( $location ) ) {
return esc_html__( "The .htaccess file doesn't contain the WebP rules from Smush.", 'wp-smushit' );
}
$markers_inserted = false;
// Remove the rules from all the possible places if not specified.
$locations = ! $location ? $this->get_htaccess_locations() : array( $location );
foreach ( $locations as $name ) {
$htaccess = $this->get_htaccess_file( $name );
$markers_inserted = insert_with_markers( $htaccess, $this->marker_suffix(), '' ) || ! empty( $markers_inserted );
}
if ( ! $markers_inserted ) {
return esc_html__( 'We were unable to automatically remove the rules. We recommend trying to remove the rules manually. If you dont have access to the .htaccess file to remove it manually, please consult with your hosting provider to change the configuration on the server.', 'wp-smushit' );
}
return true;
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* Smush directory smush scanner: DScanner class
*
* @package Smush\Core\Modules\Helpers
* @since 2.8.1
*
* @author Anton Vanyukov <anton@incsub.com>
*
* @copyright (c) 2018, Incsub (http://incsub.com)
*/
namespace Smush\Core\Modules\Helpers;
use WP_Smush;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class DScanner
*
* @since 2.8.1
*/
class DScanner {
/**
* Indicates if a scan is in process
*
* @var bool
*/
private $is_scanning = false;
/**
* Indicates the current step being scanned
*
* @var int
*/
private $current_step = 0;
/**
* Options names
*/
const IS_SCANNING_SLUG = 'wp-smush-files-scanning';
const CURRENT_STEP = 'wp-smush-scan-step';
/**
* Refresh status variables.
*/
private function refresh_status() {
$this->is_scanning = get_transient( self::IS_SCANNING_SLUG );
$this->current_step = (int) get_option( self::CURRENT_STEP );
}
/**
* Initializes the scan.
*/
public function init_scan() {
set_transient( self::IS_SCANNING_SLUG, true, 60 * 5 ); // 5 minutes max
update_option( self::CURRENT_STEP, 0 );
$this->refresh_status();
}
/**
* Reset the scan as if it weren't being executed (on finish and cancel).
*/
public function reset_scan() {
delete_transient( self::IS_SCANNING_SLUG );
delete_option( self::CURRENT_STEP );
$this->refresh_status();
}
/**
* Update the current step being scanned.
*
* @param int $step Current scan step.
*/
public function update_current_step( $step ) {
update_option( self::CURRENT_STEP, absint( $step ) );
$this->refresh_status();
}
/**
* Get the current scan step being scanned.
*
* @return mixed
*/
public function get_current_scan_step() {
$this->refresh_status();
return $this->current_step;
}
/**
* Return the number of total steps to finish the scan.
*
* @return int
*/
public function get_scan_steps() {
return count( WP_Smush::get_instance()->core()->mod->dir->get_scanned_images() );
}
/**
* Check if a scanning is in process
*
* @return bool
*/
public function is_scanning() {
$this->refresh_status();
return $this->is_scanning;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Filters the list of directories, exclude the media subfolders.
*
* @package Smush\Core\Modules\Helpers
*/
namespace Smush\Core\Modules\Helpers;
use RecursiveFilterIterator;
use WP_Smush;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Iterator extends RecursiveFilterIterator
*/
class Iterator extends RecursiveFilterIterator {
/**
* Accept method.
*
* @return bool
*/
public function accept(): bool {
$path = $this->current()->getPathname();
if ( $this->isDir() && ! WP_Smush::get_instance()->core()->mod->dir->skip_dir( $path ) ) {
return true;
}
if ( ! $this->isDir() ) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,215 @@
<?php
/**
* Mail.
*
* @package Smush\Core
*/
namespace Smush\Core\Modules\Helpers;
defined( 'ABSPATH' ) || exit;
/**
* Class Mail
*/
abstract class Mail {
/**
* Identifier
*
* @var mixed
* @access protected
*/
protected $identifier;
/**
* Whitelabel class.
*
* @var Whitelabel
*/
protected $whitelabel;
/**
* Constructor.
*
* @param string $identifier Identifier.
*/
public function __construct( $identifier ) {
$this->identifier = $identifier;
$this->whitelabel = new WhiteLabel();
}
/**
* Get first blog admin.
*
* @return null|WP_User.
*/
private function get_blog_admin() {
$admins = get_users(
array(
'role' => 'administrator',
'orderby' => 'ID',
'order' => 'ASC',
'limit' => 1,
)
);
if ( ! empty( $admins ) ) {
return $admins[0];
}
}
/**
* Get user meta data of recipient.
*
* @return false|WP_User
*/
protected function get_recipient_meta() {
$admin_id = (int) apply_filters( $this->identifier . '_mail_admin_id', 0 );
if ( $admin_id > 0 ) {
$user_data = get_user_by( 'id', $admin_id );
if ( ! empty( $user_data ) ) {
return $user_data;
}
}
$blog_admin = get_user_by( 'email', get_option( 'admin_email' ) );
if ( empty( $blog_admin ) ) {
$blog_admin = $this->get_blog_admin();
}
return $blog_admin;
}
/**
* Get first name of recipient.
*/
public function get_recipient_name() {
$user_data = $this->get_recipient_meta();
if ( empty( $user_data ) ) {
return 'Sir';
}
return ! empty( $user_data->first_name ) ? $user_data->first_name : $user_data->display_name;
}
/**
* Get mail recipients.
*
* @return array
*/
public function get_mail_recipients() {
$recipients = (array) apply_filters( $this->identifier . '_get_mail_recipients', array() );
if ( ! empty( $recipients ) ) {
return $recipients;
}
$user_data = $this->get_recipient_meta();
if ( ! empty( $user_data->user_email ) ) {
$recipients[] = $user_data->user_email;
}
if ( empty( $recipients ) ) {
$recipients[] = get_option( 'admin_email' );
}
return $recipients;
}
/**
* Send an email.
*/
public function send_email() {
$mail_attributes = array(
'to' => $this->get_mail_recipients(),
'subject' => $this->get_mail_subject(),
'message' => $this->get_mail_message(),
'headers' => $this->get_mail_headers(),
);
$modified_attributes = apply_filters( $this->identifier . '_mail_attributes', $mail_attributes );
if ( is_array( $modified_attributes ) && $modified_attributes !== $mail_attributes ) {
$mail_attributes = wp_parse_args( $modified_attributes, $mail_attributes );
}
$sender_email_callback = array( $this, 'custom_sender_email' );
$sender_name_callback = array( $this, 'custom_sender_name' );
$priority = - 10; // Let other plugins take over
add_filter( 'wp_mail_from', $sender_email_callback, $priority );
add_filter( 'wp_mail_from_name', $sender_name_callback, $priority );
// Send email.
$sent = wp_mail( $mail_attributes['to'], $mail_attributes['subject'], $mail_attributes['message'], $mail_attributes['headers'] );
remove_filter( 'wp_mail_from', $sender_email_callback, $priority );
remove_filter( 'wp_mail_from_name', $sender_name_callback, $priority );
return $sent;
}
/**
* Get email header.
*
* @return array
*/
protected function get_mail_headers() {
return array( 'Content-Type: text/html; charset=UTF-8' );
}
/**
* Retrieve noreply email.
*
* @return string
*/
public function get_noreply_email() {
$noreply_email = apply_filters( $this->identifier . '_noreply_email', null );
if ( $noreply_email && filter_var( $noreply_email, FILTER_VALIDATE_EMAIL ) ) {
return $noreply_email;
}
// Get the site domain and get rid of www.
$sitename = wp_parse_url( network_home_url(), PHP_URL_HOST );
$noreply_email = 'noreply@';
if ( null !== $sitename ) {
if ( 'www.' === substr( $sitename, 0, 4 ) ) {
$sitename = substr( $sitename, 4 );
}
$noreply_email .= $sitename;
}
return $noreply_email;
}
/**
* Custom sender email.
*
* @return string
*/
public function custom_sender_email() {
return $this->get_noreply_email();
}
/**
* Custom sender name.
*
* @return string
*/
public function custom_sender_name() {
return $this->get_sender_name();
}
/**
* Get mail subject.
*
* @return string
*/
abstract protected function get_mail_message();
/**
* Get mail subject.
*
* @return string
*/
abstract protected function get_mail_subject();
/**
* Get sender name.
*
* @return string
*/
abstract protected function get_sender_name();
}

View File

@ -0,0 +1,456 @@
<?php
/**
* Smush page parser that is used by CDN and Lazy load modules.
*
* @since 3.2.2
* @package Smush\Core\Modules\Helpers
*/
namespace Smush\Core\Modules\Helpers;
use WP_Smush;
/**
* Class Parser
*/
class Parser {
/**
* CDN module status.
*
* @var bool $cdn
*/
private $cdn = false;
/**
* Lazy load module status.
*
* @var bool $lazy_load
*/
private $lazy_load = false;
/**
* Process background images.
*
* @since 3.2.2
* @var bool $background_images
*/
private $background_images = false;
/**
* Smush will __construct this class multiple times, but only once does it need to be initialized.
*
* @since 3.5.0 Moved from __construct().
*/
public function init() {
if ( is_admin() || is_customize_preview() || wp_doing_ajax() || wp_doing_cron() ) {
return;
}
if ( $this->is_page_builder() ) {
return;
}
if ( $this->is_smartcrawl_analysis() ) {
return;
}
// Start an output buffer before any output starts.
add_action(
'template_redirect',
function () {
ob_start( array( $this, 'parse_page' ) );
},
1
);
}
/**
* Enable parser for selected module.
*
* @since 3.2.2
* @param string $module Module ID.
*/
public function enable( $module ) {
if ( ! in_array( $module, array( 'cdn', 'lazy_load', 'background_images' ), true ) ) {
return;
}
$this->$module = true;
}
/**
* Disable parser for selected module.
*
* @since 3.2.2
* @param string $module Module ID.
*/
public function disable( $module ) {
if ( ! in_array( $module, array( 'cdn', 'lazy_load' ), true ) ) {
return;
}
$this->$module = false;
}
/**
* Process images from current buffer content.
*
* Use DOMDocument class to find all available images in current HTML content and set attachment ID attribute.
*
* @since 3.0
* @since 3.2.2 Moved from \Smush\Core\Modules\CDN.
*
* @param string $content Current buffer content.
*
* @return string
*/
public function parse_page( $content ) {
// Do not parse page if CDN and Lazy load modules are disabled.
if ( ! $this->cdn && ! $this->lazy_load ) {
return $content;
}
if ( is_customize_preview() ) {
return $content;
}
if ( empty( $content ) ) {
return $content;
}
$content = $this->process_images( $content );
if ( $this->background_images ) {
$content = $this->process_background_images( $content );
}
return $content;
}
/**
* Process all images within <img> tags.
*
* @since 3.2.2
*
* @param string $content Current buffer content.
*
* @return string
*/
private function process_images( $content ) {
$images = $this->get_images_from_content( $content );
if ( empty( $images ) ) {
return $content;
}
foreach ( $images[0] as $key => $image ) {
$img_src = $images['src'][ $key ];
$new_image = $image;
// Update the image with correct CDN links.
if ( $this->cdn ) {
$new_image = WP_Smush::get_instance()->core()->mod->cdn->parse_image( $img_src, $new_image, $images['srcset'][ $key ], $images['type'][ $key ] );
}
/**
* Internal filter to disable page parsing.
*
* Because the page parser module is universal, we need to make sure that all modules have the ability to skip
* parsing of certain pages. For example, lazy loading should skip if_preview() pages. In order to achieve this
* functionality, I've introduced this filter. Filter priority can be used to overwrite the $skip param.
*
* @since 3.2.2
*
* @param bool $skip Skip status.
*/
if ( $this->lazy_load && ! apply_filters( 'wp_smush_should_skip_parse', false ) ) {
$new_image = WP_Smush::get_instance()->core()->mod->lazy->parse_image( $img_src, $new_image, $images['type'][ $key ] );
}
$content = str_replace( $image, $new_image, $content );
}
return $content;
}
/**
* Process all images that are contained as background-images.
*
* @since 3.2.2
*
* @param string $content Current buffer content.
*
* @return string
*/
private function process_background_images( $content ) {
$images = self::get_background_images( $content );
if ( empty( $images ) ) {
return $content;
}
// Try to sort out the duplicate entries.
$elements = array_unique( $images[0] );
$urls = array_unique( $images['img_url'] );
if ( count( $elements ) === count( $urls ) ) {
$images[0] = $elements;
$images['img_url'] = $urls;
}
foreach ( $images[0] as $key => $image ) {
$img_src = $images['img_url'][ $key ];
$new_image = $image;
// Update the image with correct CDN links.
$new_image = WP_Smush::get_instance()->core()->mod->cdn->parse_background_image( $img_src, $new_image );
$content = str_replace( $image, $new_image, $content );
/**
* Filter the current page content after process background images.
*
* @param string $content Current Page content.
* @param string $image Backround Image tag without src.
* @param string $img_src Image src.
*/
$content = apply_filters( 'smush_after_process_background_images', $content, $image, $img_src );
}
return $content;
}
/**
* Compatibility with SmartCrawl readability analysis.
* Do not process page on analysis.
*
* @since 3.3.0
*/
private function is_smartcrawl_analysis() {
$wds_analysis = filter_input( INPUT_POST, 'action', FILTER_SANITIZE_SPECIAL_CHARS );
if ( ! is_null( $wds_analysis ) && 'wds-analysis-recheck' === $wds_analysis ) {
return true;
}
if ( null !== filter_input( INPUT_GET, 'wds-frontend-check', FILTER_SANITIZE_SPECIAL_CHARS ) ) {
return true;
}
return false;
}
/**
* Get image tags from page content.
*
* @since 3.1.0
* @since 3.2.0 Moved to WP_Smush_Content from \Smush\Core\Modules\CDN
* @since 3.2.2 Moved to Parser from WP_Smush_Content
*
* Performance test: auto generated page with ~900 lines of HTML code, 84 images.
* - Smush 2.4.0: 82 matches, 104359 steps (~80 ms) <- does not match <source> images in <picture>.
* - Smush 2.5.0: 84 matches, 63791 steps (~51 ms).
*
* @param string $content Page content.
*
* @return array
*/
public function get_images_from_content( $content ) {
$images = array();
/**
* Filter out only <body> content. As this was causing issues with escaped JS strings in <head>.
*
* @since 3.6.2
*/
if ( preg_match( '/(?=<body).*<\/body>/is', $content, $body ) ) {
$content = $body[0];
}
$pattern = '/<(?P<type>img|source|iframe)\b(?>\s+(?:src=[\'"](?P<src>[^\'"]*)[\'"]|srcset=[\'"](?P<srcset>[^\'"]*)[\'"])|[^\s>]+|\s+)*>/is';
$pattern = apply_filters( 'smush_images_from_content_regex', $pattern );
if ( preg_match_all( $pattern, $content, $images ) ) {
foreach ( $images as $key => $unused ) {
// Simplify the output as much as possible, mostly for confirming test results.
if ( is_numeric( $key ) && $key > 0 ) {
unset( $images[ $key ] );
}
}
}
return $images;
}
/**
* Get background images from content.
*
* @since 3.2.2
*
* Performance test: auto generated page with ~900 lines of HTML code, 84 images (only 1 with background image).
* - Smush 2.4.0: 1 match, 522510 steps (~355 ms)
* - Smush 2.5.0: 1 match, 12611 steps, (~12 ms)
*
* @param string $content Page content.
*
* @return array
*/
private static function get_background_images( $content ) {
$images = array();
$pattern = '/(?:background-image:\s*?url\(\s*[\'"]?(?P<img_url>.*?[^)\'"]+)[\'"]?\s*\))/i';
$pattern = apply_filters( 'smush_background_images_regex', $pattern );
if ( preg_match_all( $pattern, $content, $images ) ) {
foreach ( $images as $key => $unused ) {
// Simplify the output as much as possible, mostly for confirming test results.
if ( is_numeric( $key ) && $key > 0 ) {
unset( $images[ $key ] );
}
}
}
/**
* Make sure that the image doesn't start and end with &quot;.
*
* @since 3.5.0
*/
$images['img_url'] = array_map(
function ( $image ) {
// Quote entities.
$quotes = apply_filters( 'wp_smush_background_image_quotes', array( '&quot;', '&#034;', '&#039;', '&apos;' ) );
$image = trim( $image );
// Remove the starting quotes.
if ( in_array( substr( $image, 0, 6 ), $quotes, true ) ) {
$image = substr( $image, 6 );
}
// Remove the ending quotes.
if ( in_array( substr( $image, -6 ), $quotes, true ) ) {
$image = substr( $image, 0, -6 );
}
return $image;
},
$images['img_url']
);
return $images;
}
/**
* Check if this is one of the known page builders.
*
* @since 3.5.1
*
* @return bool
*/
private function is_page_builder() {
// Oxygen builder.
if ( defined( 'SHOW_CT_BUILDER' ) && SHOW_CT_BUILDER ) {
return true;
}
// Oxygen builder as well.
if ( null !== filter_input( INPUT_GET, 'ct_builder' ) ) {
return true;
}
// Beaver builder.
if ( null !== filter_input( INPUT_GET, 'fl_builder' ) ) {
return true;
}
// Thrive Architect Builder.
if ( null !== filter_input( INPUT_GET, 'tve' ) && null !== filter_input( INPUT_GET, 'tcbf' ) ) {
return true;
}
// Tatsu page builder.
if ( null !== filter_input( INPUT_GET, 'tatsu' ) ) {
return true;
}
// BuddyBoss' AJAX requests. They do something strange and end up defining
// DOING_AJAX on template_redirect after self::parse_page() runs. That makes
// our lazy load page parsing break some of their AJAX requests.
if ( function_exists( 'bbp_is_ajax' ) && bbp_is_ajax() ) {
return true;
}
return false;
}
/**
* Add attribute to selected tag.
*
* @since 3.1.0
* @since 3.2.0 Moved to WP_Smush_Content from \Smush\Core\Modules\CDN
* @since 3.2.2 Moved to Parser from WP_Smush_Content
*
* @param string $element Image element.
* @param string $name Img attribute name (srcset, size, etc).
* @param string $value Attribute value.
*/
public static function add_attribute( &$element, $name, $value = null ) {
$closing = false === strpos( $element, '/>' ) ? '>' : ' />';
$quotes = false === strpos( $element, '"' ) ? '\'' : '"';
if ( ! is_null( $value ) ) {
$element = rtrim( $element, $closing ) . " {$name}={$quotes}{$value}{$quotes}{$closing}";
} else {
$element = rtrim( $element, $closing ) . " {$name}{$closing}";
}
}
/**
* Get attribute from an HTML element.
*
* @since 3.2.0
* @since 3.2.2 Moved to Parser from WP_Smush_Content
*
* @param string $element HTML element.
* @param string $name Attribute name.
*
* @return string
*/
public static function get_attribute( $element, $name ) {
preg_match( "/{$name}=['\"]([^'\"]+)['\"]/is", $element, $value );
return isset( $value['1'] ) ? $value['1'] : '';
}
/**
* Remove attribute from selected tag.
*
* @since 3.2.0
* @since 3.2.2 Moved to Parser from WP_Smush_Content
*
* @param string $element Image element.
* @param string $attribute Img attribute name (srcset, size, etc).
*/
public static function remove_attribute( &$element, $attribute ) {
$element = preg_replace( '/' . $attribute . '=[\'"](.*?)[\'"]/i', '', $element );
}
/**
* Get URLs from a string of content.
*
* This is mostly used to get the URLs from srcset and parse each single URL to use in CDN.
*
* Performance test: auto generated page with ~900 lines of HTML code, 84 images
* - Smush 2.4.0: 11957 matches, 237227 steps (~169 ms) <- many false positive matches.
* - Smush 2.5.0: 278 matches, 14509 steps, (~15 ms).
*
* @since 3.3.0
*
* @param string $content Content.
*
* @return array
*/
public static function get_links_from_content( $content ) {
$images = array();
preg_match_all( '/(?:https?[^\s\'"]*)/is', $content, $images );
return $images;
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* View.
*
* @package Smush\Core
*/
namespace Smush\Core\Modules\Helpers;
defined( 'ABSPATH' ) || exit;
/**
* Class View
*/
class View {
/**
* Templdate directory.
*
* @var string
*/
private $template_dir;
/**
* Get template content.
*
* @param string $fname template name = file name.
* @param array $args Arguments.
* @param string $dir Directory for the views. Default: views.
*/
public function get_template_content( $fname, $args = array(), $dir = 'views' ) {
$file = $fname;
if ( ! empty( $dir ) ) {
$file = "{$dir}/{$file}";
}
$file = trailingslashit( $this->get_template_dir() ) . $file . '.php';
$content = '';
if ( is_file( $file ) ) {
add_filter( 'safe_style_css', array( $this, 'wp_kses_custom_safe_style_css' ) );
extract( $args, EXTR_PREFIX_SAME, 'wpmudev' );
ob_start();
include $file;
$content = ob_get_clean();
remove_filter( 'safe_style_css', array( $this, 'wp_kses_custom_safe_style_css' ) );
}
// Everything escaped in all template files.
return $content;
}
/**
* Allow display/float CSS property.
*
* @param array $styles Current allowed style CSS properties.
* @return array
*/
public function wp_kses_custom_safe_style_css( $styles ) {
$styles[] = 'display';
return $styles;
}
/**
* Get template directory.
*
* @return string
*/
private function get_template_dir() {
return $this->template_dir;
}
/**
* Template directory.
*
* @param string $template_dir Template directory.
*/
public function set_template_dir( $template_dir ) {
$this->template_dir = $template_dir;
return $this;
}
}

View File

@ -0,0 +1,108 @@
<?php
/**
* WhiteLabel.
*
* @package Smush\Core
*/
namespace Smush\Core\Modules\Helpers;
use WPMUDEV_Dashboard;
defined( 'ABSPATH' ) || exit;
/**
* Class WhiteLabel
*/
class WhiteLabel {
/**
* Whether to activate white label.
*
* @return bool
*/
public function enabled() {
return $this->hide_branding();
}
/**
* Whether to hide branding or not.
*
* @return bool
*/
public function hide_branding() {
return apply_filters( 'wpmudev_branding_hide_branding', false );
}
/**
* Whether to hide doc link or not.
*
* @return bool
*/
public function hide_doc_link() {
return apply_filters( 'wpmudev_branding_hide_doc_link', false );
}
/**
* Whether to custom plugin labels or not.
*
* @param int $plugin_id Plugin id.
*
* @return bool
*/
private function plugin_enabled( $plugin_id ) {
if ( ! $this->enabled() ) {
return false;
}
if (
! class_exists( '\WPMUDEV_Dashboard' ) ||
empty( WPMUDEV_Dashboard::$whitelabel ) ||
! method_exists( WPMUDEV_Dashboard::$whitelabel, 'get_settings' )
) {
return false;
}
$whitelabel_settings = WPMUDEV_Dashboard::$whitelabel->get_settings();
return ! empty( $whitelabel_settings['labels_enabled'] ) && ! empty( $whitelabel_settings['labels_config'][ $plugin_id ] );
}
/**
* Get custom plugin label.
*
* @param int $plugin_id Plugin id.
* @return bool|string
*/
public function get_plugin_name( $plugin_id ) {
if ( ! $this->plugin_enabled( $plugin_id ) ) {
return false;
}
$whitelabel_settings = WPMUDEV_Dashboard::$whitelabel->get_settings();
if ( empty( $whitelabel_settings['labels_config'][ $plugin_id ]['name'] ) ) {
return false;
}
return $whitelabel_settings['labels_config'][ $plugin_id ]['name'];
}
/**
* Get custom plugin logo url.
*
* @param int $plugin_id Plugin id.
* @return bool|string
*/
public function get_plugin_logo( $plugin_id ) {
if ( ! $this->plugin_enabled( $plugin_id ) ) {
return false;
}
$whitelabel_settings = WPMUDEV_Dashboard::$whitelabel->get_settings();
$plugin_settings = $whitelabel_settings['labels_config'][ $plugin_id ];
if ( empty( $plugin_settings['icon_type'] ) ) {
return false;
}
if ( 'link' === $plugin_settings['icon_type'] && ! empty( $plugin_settings['icon_url'] ) ) {
return $plugin_settings['icon_url'];
}
if ( 'upload' === $plugin_settings['icon_type'] && ! empty( $plugin_settings['thumb_id'] ) ) {
return wp_get_attachment_image_url( $plugin_settings['thumb_id'], 'full' );
}
return false;
}
}