add wp-rocket

This commit is contained in:
nguyen dung
2022-02-18 19:09:35 +07:00
parent 39b8cb3612
commit 3110d00ee7
927 changed files with 271703 additions and 2 deletions

View File

@@ -0,0 +1,160 @@
<?php
namespace WP_Rocket\Buffer;
use WP_Rocket\Logger\Logger;
/**
* Handle page cache and optimizations.
*
* @since 3.3
* @author Grégory Viguier
*/
abstract class Abstract_Buffer {
/**
* Process identifier used by the logger.
*
* @var string
* @since 3.3
* @access protected
* @author Grégory Viguier
*/
protected $process_id;
/**
* Instance of the Tests class.
*
* @var Tests
* @since 3.3
* @access protected
* @author Grégory Viguier
*/
protected $tests;
/**
* Constructor.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param Tests $tests Tests instance.
*/
public function __construct( Tests $tests ) {
$this->tests = $tests;
}
/** ----------------------------------------------------------------------------------------- */
/** PROCESS ================================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Launch the process if the tests succeed.
* This should be the first thing to use after initializing the class.
*
* @since 3.3
* @access public
* @see $this->tests->can_init_process()
* @author Grégory Viguier
*/
abstract public function maybe_init_process();
/**
* Process the page buffer if the 2nd set of tests succeed.
* It should be used like this:
* ob_start( [ $this, 'maybe_process_buffer' ] );
*
* @since 3.3
* @access public
* @see $this->tests->can_process_buffer()
* @author Grégory Viguier
*
* @param string $buffer The buffer content.
* @return string The buffered content
*/
abstract public function maybe_process_buffer( $buffer );
/** ----------------------------------------------------------------------------------------- */
/** LOG ===================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Log the last test "error".
*
* @since 3.3
* @access protected
* @author Grégory Viguier
*/
protected function log_last_test_error() {
$error = $this->tests->get_last_error();
$this->log( $error['message'], $error['data'] );
}
/**
* Log events.
*
* @since 3.3
* @access protected
* @author Grégory Viguier
*
* @param string $message A message to log.
* @param array $data Related data.
* @param string $type Event type to log. Possible values are 'info', 'error', and 'debug' (default).
*/
protected function log( $message, $data = [], $type = 'debug' ) {
$data = array_merge(
[
$this->get_process_id(),
'request_uri' => $this->tests->get_raw_request_uri(),
],
$data
);
if ( isset( $data['cookies'] ) ) {
$data['cookies'] = Logger::remove_auth_cookies( $data['cookies'] );
}
switch ( $type ) {
case 'info':
Logger::info( $message, $data );
break;
case 'error':
Logger::error( $message, $data );
break;
default:
Logger::debug( $message, $data );
}
}
/**
* Get the process identifier.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_process_id() {
return $this->process_id . ' - Thread #' . Logger::get_thread_id();
}
/** ----------------------------------------------------------------------------------------- */
/** VARIOUS TOOLS =========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if the page content is HTML.
*
* @since 3.3
* @access protected
* @author Grégory Viguier
*
* @param string $buffer The buffer content.
* @return bool
*/
protected function is_html( $buffer ) {
return preg_match( '/<\/html>/i', $buffer );
}
}

View File

@@ -0,0 +1,704 @@
<?php
namespace WP_Rocket\Buffer;
defined( 'ABSPATH' ) || exit;
/**
* Handle page cache.
*
* @since 3.3
* @author Grégory Viguier
*/
class Cache extends Abstract_Buffer {
/**
* Process identifier used by the logger.
*
* @var string
* @since 3.3
* @access protected
* @author Grégory Viguier
*/
protected $process_id = 'caching process';
/**
* Tests instance
*
* @var Tests
*/
protected $tests;
/**
* Config instance
*
* @var Config
*/
private $config;
/**
* Path to the directory containing the cache files.
*
* @var string
* @since 3.3
* @access private
* @author Grégory Viguier
*/
private $cache_dir_path;
/**
* Constructor.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param Tests $tests Tests instance.
* @param Config $config Config instance.
* @param array $args {
* An array of arguments.
*
* @type string $cache_dir_path Path to the directory containing the cache files.
* }
*/
public function __construct( Tests $tests, Config $config, array $args ) {
$this->config = $config;
$this->cache_dir_path = rtrim( $args['cache_dir_path'], '/' ) . '/';
parent::__construct( $tests );
$this->log( 'CACHING PROCESS STARTED.', [], 'info' );
}
/** ----------------------------------------------------------------------------------------- */
/** CACHE =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Serve the cache file if it exists. If not, init the buffer.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*/
public function maybe_init_process() {
if ( ! $this->tests->can_init_process() ) {
$this->define_donotoptimize_true();
$this->log_last_test_error();
return;
}
/**
* Serve the cache file if it exists.
*/
$cache_filepath = $this->get_cache_path();
$this->log(
'Looking for cache file.',
[
'path' => $cache_filepath,
]
);
$cache_filepath_gzip = $cache_filepath . '_gzip';
$accept_encoding = $this->config->get_server_input( 'HTTP_ACCEPT_ENCODING' );
$accept_gzip = $accept_encoding && false !== strpos( $accept_encoding, 'gzip' );
// Check if cache file exist.
if ( $accept_gzip && is_readable( $cache_filepath_gzip ) ) {
$this->serve_gzip_cache_file( $cache_filepath_gzip );
}
if ( is_readable( $cache_filepath ) ) {
$this->serve_cache_file( $cache_filepath );
}
// Maybe we're looking for a webp file.
$cache_filename = basename( $cache_filepath );
if ( strpos( $cache_filename, '-webp' ) !== false ) {
// We're looking for a webp file that doesn't exist: try to locate any `.no-webp` file.
$cache_dir_path = rtrim( dirname( $cache_filepath ), '/\\' ) . DIRECTORY_SEPARATOR;
if ( file_exists( $cache_dir_path . '.no-webp' ) ) {
// We have a `.no-webp` file: try to deliver a non-webp cache file.
$cache_filepath = $cache_dir_path . str_replace( '-webp', '', $cache_filename );
$cache_filepath_gzip = $cache_filepath . '_gzip';
$this->log(
'Looking for non-webp cache file.',
[
'path' => $cache_filepath,
]
);
// Try to deliver the non-webp version instead.
if ( $accept_gzip && is_readable( $cache_filepath_gzip ) ) {
$this->serve_gzip_cache_file( $cache_filepath_gzip );
}
if ( is_readable( $cache_filepath ) ) {
$this->serve_cache_file( $cache_filepath );
}
}
}
/**
* No cache file yet: launch caching process.
*/
$this->log(
'Start buffer.',
[
'path' => $cache_filepath,
]
);
ob_start( [ $this, 'maybe_process_buffer' ] );
}
/**
* Serve a cache file.
*
* @since 3.3
* @access private
* @author Grégory Viguier
*
* @param string $cache_filepath Path to the cache file.
*/
private function serve_cache_file( $cache_filepath ) {
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' );
$if_modified_since = $this->get_if_modified_since();
// Checking if the client is validating his cache and if it is current.
if ( $if_modified_since && ( strtotime( $if_modified_since ) === @filemtime( $cache_filepath ) ) ) {
// Client's cache is current, so we just respond '304 Not Modified'.
header( $this->config->get_server_input( 'SERVER_PROTOCOL', '' ) . ' 304 Not Modified', true, 304 );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
header( 'Cache-Control: no-cache, must-revalidate' );
$this->log(
'Serving `304` cache file.',
[
'path' => $cache_filepath,
'modified' => $if_modified_since,
],
'info'
);
exit;
}
// Serve the cache if file isn't store in the client browser cache.
readfile( $cache_filepath );
$this->log(
'Serving cache file.',
[
'path' => $cache_filepath,
'modified' => $if_modified_since,
],
'info'
);
exit;
}
/**
* Serve a gzipped cache file.
*
* @since 3.3
* @access private
* @author Grégory Viguier
*
* @param string $cache_filepath Path to the gzip cache file.
*/
private function serve_gzip_cache_file( $cache_filepath ) {
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' );
$if_modified_since = $this->get_if_modified_since();
// Checking if the client is validating his cache and if it is current.
if ( $if_modified_since && ( strtotime( $if_modified_since ) === @filemtime( $cache_filepath ) ) ) {
// Client's cache is current, so we just respond '304 Not Modified'.
header( $this->config->get_server_input( 'SERVER_PROTOCOL', '' ) . ' 304 Not Modified', true, 304 );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
header( 'Cache-Control: no-cache, must-revalidate' );
$this->log(
'Serving `304` gzip cache file.',
[
'path' => $cache_filepath,
'modified' => $if_modified_since,
],
'info'
);
exit;
}
// Serve the cache if file isn't store in the client browser cache.
readgzfile( $cache_filepath );
$this->log(
'Serving gzip cache file.',
[
'path' => $cache_filepath,
'modified' => $if_modified_since,
],
'info'
);
exit;
}
/**
* Maybe cache the page content.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param string $buffer The buffer content.
* @return string The buffered content.
*/
public function maybe_process_buffer( $buffer ) {
if ( ! $this->tests->can_process_buffer( $buffer ) ) {
$this->log_last_test_error();
return $buffer;
}
$footprint = '';
$is_html = $this->is_html( $buffer );
if ( ! static::can_generate_caching_files() ) {
// Not allowed to generate cache files.
if ( $is_html ) {
$footprint = $this->get_rocket_footprint();
}
$this->log(
'Page not cached by filter.',
[
'filter' => 'do_rocket_generate_caching_files',
]
);
return $buffer . $footprint;
}
$webp_enabled = preg_match( '@<!-- Rocket (has|no) webp -->@', $buffer, $webp_tag );
$has_webp = ! empty( $webp_tag ) ? 'has' === $webp_tag[1] : false;
$cache_filepath = $this->get_cache_path( [ 'webp' => $has_webp ] );
$cache_dir_path = dirname( $cache_filepath );
// Create cache folders.
rocket_mkdir_p( $cache_dir_path );
if ( $is_html ) {
$footprint = $this->get_rocket_footprint( time() );
}
// Webp request.
if ( $webp_enabled ) {
$buffer = str_replace( $webp_tag[0], '', $buffer );
if ( ! $has_webp ) {
// The buffer doesnt contain webp files.
$cache_dir_path = rtrim( dirname( $cache_filepath ), '/\\' );
$this->maybe_create_nowebp_file( $cache_dir_path );
}
}
$this->write_cache_file( $cache_filepath, $buffer . $footprint );
$this->maybe_create_nginx_mobile_file( $cache_dir_path );
// Send headers with the last modified time of the cache file.
if ( file_exists( $cache_filepath ) ) {
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' );
}
if ( $is_html ) {
$footprint = $this->get_rocket_footprint();
}
$this->log(
'Page cached.',
[
'path' => $cache_filepath,
],
'info'
);
return $buffer . $footprint;
}
/**
* Writes the cache file(s)
*
* @since 3.5
* @author Remy Perona
*
* @param string $cache_filepath Absolute path to the cache file.
* @param string $content Content to write in the cache file.
* @return void
*/
private function write_cache_file( $cache_filepath, $content ) {
$gzip_filepath = $cache_filepath . '_gzip';
$temp_filepath = $cache_filepath . '_temp';
$temp_gzip_filepath = $gzip_filepath . '_temp';
if ( rocket_direct_filesystem()->exists( $temp_filepath ) ) {
return;
}
// Save the cache file.
rocket_put_content( $temp_filepath, $content );
rocket_direct_filesystem()->move( $temp_filepath, $cache_filepath, true );
if ( function_exists( 'gzencode' ) ) {
/**
* Filters the Gzip compression level to use for the cache file
*
* @param int $compression_level Compression level between 0 and 9.
*/
$compression_level = apply_filters( 'rocket_gzencode_level_compression', 6 );
rocket_put_content( $temp_gzip_filepath, gzencode( $content, $compression_level ) );
rocket_direct_filesystem()->move( $temp_gzip_filepath, $gzip_filepath, true );
}
}
/**
* Get the path to the cache file.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param array $args {
* A list of arguments.
*
* @type bool $webp Set to false to prevent adding the part related to webp.
* }
* @return string
*/
public function get_cache_path( $args = [] ) {
$args = array_merge(
[
'webp' => true,
],
$args
);
$cookies = $this->tests->get_cookies();
$request_uri_path = $this->get_request_cache_path( $cookies );
$filename = 'index';
$filename = $this->maybe_mobile_filename( $filename );
// Rename the caching filename for SSL URLs.
if ( is_ssl() && $this->config->get_config( 'cache_ssl' ) ) {
$filename .= '-https';
}
if ( $args['webp'] ) {
$filename = $this->maybe_webp_filename( $filename );
}
$filename = $this->maybe_dynamic_cookies_filename( $filename, $cookies );
// Ensure proper formatting of the path.
$request_uri_path = preg_replace_callback( '/%[0-9A-F]{2}/', [ $this, 'reset_lowercase' ], $request_uri_path );
// Directories in Windows can't contain question marks.
$request_uri_path = str_replace( '?', '#', $request_uri_path );
// Limit filename max length to 255 characters.
$request_uri_path .= '/' . substr( $filename, 0, 250 ) . '.html';
return $request_uri_path;
}
/** ----------------------------------------------------------------------------------------- */
/** VARIOUS TOOLS =========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Declares and sets value of constant preventing Optimizations.
*
* @since 3.3
* @access private
* @author Grégory Viguier
*/
final private function define_donotoptimize_true() {
if ( ! defined( 'DONOTROCKETOPTIMIZE' ) ) {
define( 'DONOTROCKETOPTIMIZE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
}
}
/**
* Gets If-modified-since header value
*
* @since 3.3
* @access private
* @author Remy Perona
* @return string
*/
private function get_if_modified_since() {
if ( function_exists( 'apache_request_headers' ) ) {
$headers = apache_request_headers();
return isset( $headers['If-Modified-Since'] ) ? $headers['If-Modified-Since'] : '';
}
return $this->config->get_server_input( 'HTTP_IF_MODIFIED_SINCE', '' );
}
/**
* Get WP Rocket footprint
*
* @since 3.0.5 White label footprint if WP_ROCKET_WHITE_LABEL_FOOTPRINT is defined.
* @since 2.0
*
* @param int $time UNIX timestamp when the cache file was saved.
* @return string The footprint that will be printed
*/
private function get_rocket_footprint( $time = '' ) {
$footprint = defined( 'WP_ROCKET_WHITE_LABEL_FOOTPRINT' ) ?
"\n" . '<!-- Cached for great performance' :
"\n" . '<!-- This website is like a Rocket, isn\'t it? Performance optimized by ' . WP_ROCKET_PLUGIN_NAME . '. Learn more: https://wp-rocket.me';
if ( ! empty( $time ) ) {
$footprint .= ' - Debug: cached@' . $time;
}
$footprint .= ' -->';
return $footprint;
}
/**
* Create a hidden empty file for mobile detection on NGINX with the Rocket NGINX configuration.
*
* @param string $cache_dir_path Path to the current cache directory.
* @return void
*/
private function maybe_create_nginx_mobile_file( $cache_dir_path ) {
global $is_nginx;
if ( ! $this->config->get_config( 'do_caching_mobile_files' ) ) {
return;
}
if ( ! $is_nginx ) {
return;
}
$nginx_mobile_detect = $cache_dir_path . '/.mobile-active';
if ( rocket_direct_filesystem()->exists( $nginx_mobile_detect ) ) {
return;
}
rocket_direct_filesystem()->touch( $nginx_mobile_detect );
}
/**
* Create a hidden empty file when webp is enabled but the buffer doesnt contain webp files.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @param string $cache_dir_path Path to the current cache directory (without trailing slah).
*/
private function maybe_create_nowebp_file( $cache_dir_path ) {
$nowebp_filepath = $cache_dir_path . DIRECTORY_SEPARATOR . '.no-webp';
if ( rocket_direct_filesystem()->exists( $nowebp_filepath ) ) {
return;
}
rocket_direct_filesystem()->touch( $nowebp_filepath );
}
/**
* Tell if generating cache files is allowed.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public static function can_generate_caching_files() {
/**
* Allow to the generate the caching file.
*
* @since 2.5
*
* @param bool True will force the cache file generation.
*/
return (bool) apply_filters( 'do_rocket_generate_caching_files', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
}
/**
* Gets the base cache path for the current request
*
* @since 3.3
* @author Remy Perona
*
* @param array $cookies Cookies for the current request.
* @return string
*/
private function get_request_cache_path( $cookies ) {
$host = $this->config->get_host();
if ( $this->config->get_config( 'url_no_dots' ) ) {
$host = str_replace( '.', '_', $host );
}
$request_uri = $this->tests->get_clean_request_uri();
$cookie_hash = $this->config->get_config( 'cookie_hash' );
$logged_in_cookie = $this->config->get_config( 'logged_in_cookie' );
$logged_in_cookie_no_hash = str_replace( $cookie_hash, '', $logged_in_cookie );
// Get cache folder of host name.
if ( $logged_in_cookie && isset( $cookies[ $logged_in_cookie ] ) && ! $this->tests->has_rejected_cookie( $logged_in_cookie_no_hash ) ) {
if ( $this->config->get_config( 'common_cache_logged_users' ) ) {
return $this->cache_dir_path . $host . '-loggedin' . rtrim( $request_uri, '/' );
}
$user_key = explode( '|', $cookies[ $logged_in_cookie ] );
$user_key = reset( $user_key );
$user_key = $user_key . '-' . $this->config->get_config( 'secret_cache_key' );
// Get cache folder of host name.
return $this->cache_dir_path . $host . '-' . $user_key . rtrim( $request_uri, '/' );
}
return $this->cache_dir_path . $host . rtrim( $request_uri, '/' );
}
/**
* Modifies the filename if the request is from a mobile device.
*
* @since 3.3
* @author Remy Perona
*
* @param string $filename Cache filename.
* @return string
*/
private function maybe_mobile_filename( $filename ) {
$cache_mobile_files_tablet = $this->config->get_config( 'cache_mobile_files_tablet' );
if ( ! ( $this->config->get_config( 'cache_mobile' ) && $this->config->get_config( 'do_caching_mobile_files' ) ) ) {
return $filename;
}
if ( ! $cache_mobile_files_tablet ) {
return $filename;
}
if ( ! class_exists( 'WP_Rocket_Mobile_Detect' ) ) {
return $filename;
}
$detect = new \WP_Rocket_Mobile_Detect();
if ( $detect->isMobile() && ! $detect->isTablet() && 'desktop' === $cache_mobile_files_tablet || ( $detect->isMobile() || $detect->isTablet() ) && 'mobile' === $cache_mobile_files_tablet ) {
return $filename .= '-mobile';
}
return $filename;
}
/**
* Modifies the filename if the request is WebP compatible
*
* @since 3.4
* @author Remy Perona
*
* @param string $filename Cache filename.
* @return string
*/
private function maybe_webp_filename( $filename ) {
if ( ! $this->config->get_config( 'cache_webp' ) ) {
return $filename;
}
/**
* Force WP Rocket to disable its webp cache.
*
* @since 3.4
* @author Grégory Viguier
*
* @param bool $disable_webp_cache Set to true to disable the webp cache.
*/
$disable_webp_cache = apply_filters( 'rocket_disable_webp_cache', false );
if ( $disable_webp_cache ) {
return $filename;
}
$http_accept = $this->config->get_server_input( 'HTTP_ACCEPT', '' );
if ( ! $http_accept && function_exists( 'apache_request_headers' ) ) {
$headers = apache_request_headers();
$http_accept = isset( $headers['Accept'] ) ? $headers['Accept'] : '';
}
if ( ! $http_accept || false === strpos( $http_accept, 'webp' ) ) {
if ( preg_match( '#Firefox/(?<version>[0-9]{2})#i', $this->config->get_server_input( 'HTTP_USER_AGENT' ), $matches ) ) {
if ( 66 <= (int) $matches['version'] ) {
return $filename . '-webp';
}
}
return $filename;
}
return $filename . '-webp';
}
/**
* Modifies the filename if dynamic cookies are set
*
* @param string $filename Cache filename.
* @param array $cookies Cookies for the request.
* @return string
*/
private function maybe_dynamic_cookies_filename( $filename, $cookies ) {
$cache_dynamic_cookies = $this->config->get_config( 'cache_dynamic_cookies' );
if ( ! $cache_dynamic_cookies ) {
return $filename;
}
foreach ( $cache_dynamic_cookies as $key => $cookie_name ) {
if ( is_array( $cookie_name ) ) {
if ( isset( $_COOKIE[ $key ] ) ) {
foreach ( $cookie_name as $cookie_key ) {
if ( '' !== $cookies[ $key ][ $cookie_key ] ) {
$cache_key = $cookies[ $key ][ $cookie_key ];
$cache_key = preg_replace( '/[^a-z0-9_\-]/i', '-', $cache_key );
$filename .= '-' . $cache_key;
}
}
}
continue;
}
if ( isset( $cookies[ $cookie_name ] ) && '' !== $cookies[ $cookie_name ] ) {
$cache_key = $cookies[ $cookie_name ];
$cache_key = preg_replace( '/[^a-z0-9_\-]/i', '-', $cache_key );
$filename .= '-' . $cache_key;
}
}
return $filename;
}
/**
* Force lowercase on encoded url strings from different alphabets to prevent issues on some hostings.
*
* @since 3.3
* @access protected
* @author Grégory Viguier
*
* @param array $matches Cache path.
* @return string Cache path in lowercase.
*/
protected function reset_lowercase( $matches ) {
return strtolower( $matches[0] );
}
}

View File

@@ -0,0 +1,296 @@
<?php
namespace WP_Rocket\Buffer;
/**
* Configuration class for WP Rocket cache
*
* @since 3.3
* @author Remy Perona
*/
class Config {
use \WP_Rocket\Traits\Memoize;
/**
* Path to the directory containing the config files.
*
* @var string
* @since 3.3
* @access private
* @author Grégory Viguier
*/
private static $config_dir_path;
/**
* Values of $_SERVER to use for some tests.
*
* @var array
* @since 3.3
* @access private
* @author Grégory Viguier
*/
private static $server;
/**
* Constructor
*
* @param array $args {
* An array of arguments.
*
* @type string $config_dir_path WP Rocket config directory path.
* @type array $server Values of $_SERVER to use for the tests. Default is $_SERVER.
* }
*/
public function __construct( $args ) {
if ( isset( self::$config_dir_path ) ) {
// Make sure to keep the same values all along.
return;
}
if ( ! isset( $args['server'] ) && ! empty( $_SERVER ) && is_array( $_SERVER ) ) {
$args['server'] = $_SERVER;
}
self::$config_dir_path = rtrim( $args['config_dir_path'], '/' ) . '/';
self::$server = ! empty( $args['server'] ) && is_array( $args['server'] ) ? $args['server'] : [];
}
/**
* Get a $_SERVER entry.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param string $entry_name Name of the entry.
* @param mixed $default Value to return if the entry is not set.
* @return mixed
*/
public function get_server_input( $entry_name, $default = null ) {
if ( ! isset( self::$server[ $entry_name ] ) ) {
return $default;
}
return self::$server[ $entry_name ];
}
/**
* Get the `server` property.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @return array
*/
public function get_server() {
return self::$server;
}
/**
* Get a specific config/option value.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param string $config_name Name of a specific config/option.
* @return mixed
*/
public function get_config( $config_name ) {
$config = $this->get_configs();
return isset( $config[ $config_name ] ) ? $config[ $config_name ] : null;
}
/**
* Get the whole current configuration.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @return array|bool An array containing the configuration. False on failure.
*/
public function get_configs() {
if ( self::is_memoized( __FUNCTION__ ) ) {
return self::get_memoized( __FUNCTION__ );
}
$config_file_path = $this->get_config_file_path();
if ( ! $config_file_path['success'] ) {
return self::memoize( __FUNCTION__, [], false );
}
include $config_file_path['path'];
$config = [
'cookie_hash' => '',
'logged_in_cookie' => '',
'common_cache_logged_users' => 0,
'cache_mobile_files_tablet' => 'desktop',
'cache_ssl' => 0,
'cache_webp' => 0,
'cache_mobile' => 0,
'do_caching_mobile_files' => 0,
'secret_cache_key' => '',
'cache_reject_uri' => '',
'cache_query_strings' => [],
'cache_ignored_parameters' => [],
'cache_reject_cookies' => '',
'cache_reject_ua' => '',
'cache_mandatory_cookies' => '',
'cache_dynamic_cookies' => [],
'url_no_dots' => 0,
];
foreach ( $config as $entry_name => $entry_value ) {
$var_name = 'rocket_' . $entry_name;
if ( isset( $$var_name ) ) {
$config[ $entry_name ] = $$var_name;
}
}
return self::memoize( __FUNCTION__, [], $config );
}
/**
* Get the host, to use for config and cache file path.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_host() {
if ( self::is_memoized( __FUNCTION__ ) ) {
return self::get_memoized( __FUNCTION__ );
}
$host = $this->get_server_input( 'HTTP_HOST', (string) time() );
$host = preg_replace( '/:\d+$/', '', $host );
$host = trim( strtolower( $host ), '.' );
return self::memoize( __FUNCTION__, [], rawurlencode( $host ) );
}
/**
* Get the path to an existing config file.
*
* @since 3.3
* @access protected
* @author Grégory Viguier
*
* @return string|bool The path to the file. False if no file is found.
*/
public function get_config_file_path() {
if ( self::is_memoized( __FUNCTION__ ) ) {
return self::get_memoized( __FUNCTION__ );
}
$config_dir_real_path = realpath( self::$config_dir_path ) . DIRECTORY_SEPARATOR;
$host = $this->get_host();
if ( realpath( self::$config_dir_path . $host . '.php' ) && 0 === stripos( realpath( self::$config_dir_path . $host . '.php' ), $config_dir_real_path ) ) {
$config_file_path = self::$config_dir_path . $host . '.php';
return self::memoize(
__FUNCTION__,
[],
[
'success' => true,
'path' => $config_file_path,
]
);
}
$path = str_replace( '\\', '/', strtok( $this->get_server_input( 'REQUEST_URI', '' ), '?' ) );
$path = preg_replace( '|(?<=.)/+|', '/', $path );
$path = explode( '%2F', preg_replace( '/^(?:%2F)*(.*?)(?:%2F)*$/', '$1', rawurlencode( $path ) ) );
foreach ( $path as $p ) {
static $dir;
if ( realpath( self::$config_dir_path . $host . '.' . $p . '.php' ) && 0 === stripos( realpath( self::$config_dir_path . $host . '.' . $p . '.php' ), $config_dir_real_path ) ) {
$config_file_path = self::$config_dir_path . $host . '.' . $p . '.php';
return self::memoize(
__FUNCTION__,
[],
[
'success' => true,
'path' => $config_file_path,
]
);
}
if ( realpath( self::$config_dir_path . $host . '.' . $dir . $p . '.php' ) && 0 === stripos( realpath( self::$config_dir_path . $host . '.' . $dir . $p . '.php' ), $config_dir_real_path ) ) {
$config_file_path = self::$config_dir_path . $host . '.' . $dir . $p . '.php';
return self::memoize(
__FUNCTION__,
[],
[
'success' => true,
'path' => $config_file_path,
]
);
}
$dir .= $p . '.';
}
return self::memoize(
__FUNCTION__,
[],
[
'success' => false,
'path' => self::$config_dir_path . $host . implode( '/', $path ) . '.php',
]
);
}
/** ----------------------------------------------------------------------------------------- */
/** SPECIFIC CONFIG GETTERS ================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Get rejected cookies as a regex pattern.
* `#` is used as pattern delimiter.
*
* @since 3.3
* @access protected
* @author Grégory Viguier
*
* @return string
*/
public function get_rejected_cookies() {
$rejected_cookies = $this->get_config( 'cache_reject_cookies' );
if ( '' === $rejected_cookies ) {
return $rejected_cookies;
}
return '#' . $rejected_cookies . '#';
}
/**
* Get mandatory cookies as a regex pattern.
* `#` is used as pattern delimiter.
*
* @since 3.3
* @access protected
* @author Grégory Viguier
*
* @return string
*/
public function get_mandatory_cookies() {
$mandatory_cookies = $this->get_config( 'cache_mandatory_cookies' );
if ( '' === $mandatory_cookies ) {
return $mandatory_cookies;
}
return '#' . $mandatory_cookies . '#';
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace WP_Rocket\Buffer;
/**
* Handle page optimizations.
*
* @since 3.3
* @author Grégory Viguier
*/
class Optimization extends Abstract_Buffer {
/**
* Process identifier used by the logger.
*
* @var string
* @since 3.3
* @access protected
* @author Grégory Viguier
*/
protected $process_id = 'optimization process';
/**
* Tests instance
*
* @var Tests
*/
protected $tests;
/**
* Constructor.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param Tests $tests Tests instance.
*/
public function __construct( Tests $tests ) {
parent::__construct( $tests );
$this->log( 'OPTIMIZATION PROCESS STARTED.', [], 'info' );
}
/** ----------------------------------------------------------------------------------------- */
/** CACHE =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Do preliminary tests and maybe launch the buffer process.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*/
public function maybe_init_process() {
if ( ! $this->tests->can_init_process() ) {
$this->log_last_test_error();
return;
}
ob_start( [ $this, 'maybe_process_buffer' ] );
}
/**
* Maybe optimize the page content.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param string $buffer The buffer content.
* @return string The buffered content.
*/
public function maybe_process_buffer( $buffer ) {
/**
* Triggered before WP Rocket starts the optimization process.
*
* @since 3.4.2
* @author Soponar Cristina
*
* @param string $buffer HTML content.
*/
do_action( 'rocket_before_maybe_process_buffer', $buffer );
if ( ! $this->is_html( $buffer ) ) {
return $buffer;
}
if ( ! $this->tests->can_process_buffer( $buffer ) ) {
$this->log_last_test_error();
return $buffer;
}
/**
* This hook is used for:
* - Async CSS files
* - Defer JavaScript files
* - Minify/Combine HTML/CSS/JavaScript
* - CDN
* - LazyLoad
*
* @param string $buffer The page content.
*/
$buffer = (string) apply_filters( 'rocket_buffer', $buffer );
$this->log( 'Page optimized.', [], 'info' );
return $buffer;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,393 @@
<?php
namespace WP_Rocket\Cache;
use WP_Rocket\Buffer\Cache;
/**
* Purge expired cache files based on the defined lifespan
*
* @since 3.4
* @author Grégory Viguier
*/
class Expired_Cache_Purge {
/**
* Path to the global cache folder.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @var string
*/
private $cache_path;
/**
* Filesystem object.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @var \WP_Filesystem_Direct
*/
private $filesystem;
/**
* Constructor
*
* @param string $cache_path Path to the global cache folder.
*/
public function __construct( $cache_path ) {
$this->cache_path = $cache_path;
}
/**
* Perform the event action.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param int $lifespan The cache lifespan in seconds.
*/
public function purge_expired_files( $lifespan ) {
if ( ! $lifespan ) {
// Uh?
return;
}
$urls = get_rocket_i18n_uri();
$file_age_limit = time() - $lifespan;
/**
* Filter home URLs that will be searched for old cache files.
*
* @since 3.4
* @author Grégory Viguier
*
* @param array $urls URLs that will be searched for old cache files.
* @param int $file_age_limit Timestamp of the maximum age files must have.
*/
$urls = apply_filters( 'rocket_automatic_cache_purge_urls', $urls, $file_age_limit );
if ( ! is_array( $urls ) ) {
// I saw what you did ಠ_ಠ.
$urls = get_rocket_i18n_uri();
}
$urls = array_filter( $urls, 'is_string' );
$urls = array_filter( $urls );
if ( ! $urls ) {
return;
}
$urls = array_unique( $urls );
if ( empty( $this->filesystem ) ) {
$this->filesystem = rocket_direct_filesystem();
}
$deleted = [];
$cache_enabled = Cache::can_generate_caching_files();
foreach ( $urls as $url ) {
/**
* Fires before purging a cache directory.
*
* @since 3.4
* @author Grégory Viguier
*
* @param string $url The home url.
* @param int $file_age_limit Timestamp of the maximum age files must have.
*/
do_action( 'rocket_before_automatic_cache_purge_dir', $url, $file_age_limit );
$url_deleted = [];
if ( $cache_enabled ) {
// Get the directory names.
$file = get_rocket_parse_url( $url );
/** This filter is documented in inc/front/htaccess.php */
if ( apply_filters( 'rocket_url_no_dots', false ) ) {
$file['host'] = str_replace( '.', '_', $file['host'] );
}
$sub_dir = rtrim( $file['path'], '/' );
$files = $this->get_cache_files_in_dir( $file );
foreach ( $files as $item ) {
$dir_path = $item->getPathname();
$sub_dir_path = $dir_path . $sub_dir;
// Time to cut old leaves.
$item_paths = $this->purge_dir( $sub_dir_path, $file_age_limit );
if ( $item_paths ) {
$url_deleted[] = [
'home_url' => $url,
'home_path' => $sub_dir_path,
'logged_in' => $dir_path !== $this->cache_path . $file['host'],
'files' => $item_paths,
];
}
if ( $this->is_dir_empty( $dir_path ) ) {
// If the folder is empty, remove it.
$this->filesystem->delete( $dir_path );
}
}
if ( $url_deleted ) {
$deleted = array_merge( $deleted, $url_deleted );
}
}
$args = [
'url' => $url,
'lifespan' => $lifespan,
'file_age_limit' => $file_age_limit,
];
/**
* Fires after a cache directory is purged.
*
* @since 3.4
* @author Grégory Viguier
*
* @param array $deleted {
* An array of arrays sharing the same home URL, described like: {
* @type string $home_url The home URL. This is the same as $args['url'].
* @type string $home_path Path to home.
* @type bool $logged_in True if the home path corresponds to a logged in users folder.
* @type array $files A list of paths of files that have been deleted.
* }
* Ex:
* [
* [
* 'home_url' => 'http://example.com/home1',
* 'home_path' => '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1',
* 'logged_in' => false,
* 'files' => [
* '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1/deleted-page',
* '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1/very-dead-page',
* ],
* ],
* [
* 'home_url' => 'http://example.com/home1',
* 'home_path' => '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1',
* 'logged_in' => true,
* 'files' => [
* '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1/how-to-prank-your-coworkers',
* '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1/best-source-of-gifs',
* ],
* ],
* ]
* @param array $args {
* @type string $url The home url.
* @type int $lifespan Files lifespan in seconds.
* @type int $file_age_limit Timestamp of the maximum age files must have. This is basically `time() - $lifespan`.
* }
*/
do_action( 'rocket_after_automatic_cache_purge_dir', $url_deleted, $args );
}
$args = [
'lifespan' => $lifespan,
'file_age_limit' => $file_age_limit,
];
/**
* Fires after cache directories are purged.
*
* @since 3.4
* @author Grégory Viguier
*
* @param array $deleted {
* An array of arrays, described like: {
* @type string $home_url The home URL.
* @type string $home_path Path to home.
* @type bool $logged_in True if the home path corresponds to a logged in users folder.
* @type array $files A list of paths of files that have been deleted.
* }
* Ex:
* [
* [
* 'home_url' => 'http://example.com/home1',
* 'home_path' => '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1',
* 'logged_in' => false,
* 'files' => [
* '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1/deleted-page',
* '/path-to/home1/wp-content/cache/wp-rocket/example.com/home1/very-dead-page',
* ],
* ],
* [
* 'home_url' => 'http://example.com/home1',
* 'home_path' => '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1',
* 'logged_in' => true,
* 'files' => [
* '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1/how-to-prank-your-coworkers',
* '/path-to/home1/wp-content/cache/wp-rocket/example.com-Greg-594d03f6ae698691165999/home1/best-source-of-gifs',
* ],
* ],
* [
* 'home_url' => 'http://example.com/home4',
* 'home_path' => '/path-to/home4/wp-content/cache/wp-rocket/example.com-Greg-71edg8d6af865569979569/home4',
* 'logged_in' => true,
* 'files' => [
* '/path-to/home4/wp-content/cache/wp-rocket/example.com-Greg-71edg8d6af865569979569/home4/easter-eggs-in-code-your-best-opportunities',
* ],
* ],
* ]
* }
* @param array $args {
* @type int $lifespan Files lifespan in seconds.
* @type int $file_age_limit Timestamp of the maximum age files must have. This is basically `time() - $lifespan`.
* }
*/
do_action( 'rocket_after_automatic_cache_purge', $deleted, $args );
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get all cache files for the provided URL
*
* @since 3.4
* @author Gregory Viguier
*
* @param array $file An array of the parsed URL parts.
* @return array|DirectoryIterator
*/
private function get_cache_files_in_dir( $file ) {
// Grab cache folders.
$host_pattern = '@^' . preg_quote( $file['host'], '@' ) . '@';
$sub_dir = rtrim( $file['path'], '/' );
try {
$iterator = new \DirectoryIterator( $this->cache_path );
}
catch ( \Exception $e ) {
return [];
}
return new \CallbackFilterIterator(
$iterator,
function ( $current ) use ( $host_pattern, $sub_dir ) {
if ( ! $current->isDir() || $current->isDot() ) {
// We look for folders only, and don't want '.' nor '..'.
return false;
}
if ( ! preg_match( $host_pattern, $current->getFilename() ) ) {
// Not the right host.
return false;
}
if ( '' !== $sub_dir && ! $this->filesystem->exists( $current->getPathname() . $sub_dir ) ) {
// Not the right path.
return false;
}
return true;
}
);
}
/**
* Purge a folder from old files.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @param string $dir_path Path to the folder to purge.
* @param int $file_age_limit Timestamp of the maximum age files must have.
* @return array A list of files that have been deleted.
*/
private function purge_dir( $dir_path, $file_age_limit ) {
$deleted = [];
try {
$iterator = new \DirectoryIterator( $dir_path );
}
catch ( \Exception $e ) {
return [];
}
foreach ( $iterator as $item ) {
if ( $item->isDot() ) {
continue;
}
if ( $item->isDir() ) {
/**
* A folder, lets see whats in there.
* Maybe theres a dinosaur fossil or a hidden treasure.
*/
$dir_deleted = $this->purge_dir( $item->getPathname(), $file_age_limit );
$deleted = array_merge( $deleted, $dir_deleted );
} elseif ( $item->isFile() && $item->getCTime() < $file_age_limit ) {
$file_path = $item->getPathname();
/**
* The file is older than our limit.
* This will also delete the file if `$item->getCTime()` fails.
*/
if ( ! $this->filesystem->delete( $file_path ) ) {
continue;
}
/**
* A page can have mutiple cache files:
* index(-mobile)(-https)(-dynamic-cookie-key){0,*}.html(_gzip).
*/
$dir_path = dirname( $file_path );
if ( ! in_array( $dir_path, $deleted, true ) ) {
$deleted[] = $dir_path;
}
}
}
if ( $this->is_dir_empty( $dir_path ) ) {
// If the folder is empty, remove it.
$this->filesystem->delete( $dir_path );
}
return $deleted;
}
/**
* Tell if a folder is empty.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @param string $dir_path Path to the folder to purge.
* @return bool True if empty. False if it contains files.
*/
private function is_dir_empty( $dir_path ) {
try {
$iterator = new \DirectoryIterator( $dir_path );
}
catch ( \Exception $e ) {
return [];
}
foreach ( $iterator as $item ) {
if ( $item->isDot() ) {
continue;
}
return false;
}
return true;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace WP_Rocket\ServiceProvider;
use WP_Rocket\Engine\Container\ServiceProvider\AbstractServiceProvider;
/**
* Service provider for WP Rocket features common for admin and front
*
* @since 3.3
* @author Remy Perona
*/
class Common_Subscribers extends AbstractServiceProvider {
/**
* The provides array is a way to let the container
* know that a service is provided by this service
* provider. Every service that is registered via
* this service provider must have an alias added
* to this array or it will be ignored.
*
* @var array
*/
protected $provides = [
'db_optimization_subscriber',
'webp_subscriber',
'expired_cache_purge',
'expired_cache_purge_subscriber',
'detect_missing_tags',
];
/**
* Registers the subscribers in the container
*
* @since 3.3
* @author Remy Perona
*
* @return void
*/
public function register() {
$options = $this->getContainer()->get( 'options' );
$this->getContainer()->share( 'db_optimization_subscriber', 'WP_Rocket\Subscriber\Admin\Database\Optimization_Subscriber' )
->withArgument( $this->getContainer()->get( 'db_optimization' ) )
->withArgument( $options );
$this->getContainer()->add( 'expired_cache_purge', 'WP_Rocket\Cache\Expired_Cache_Purge' )
->withArgument( rocket_get_constant( 'WP_ROCKET_CACHE_PATH' ) );
$this->getContainer()->share( 'expired_cache_purge_subscriber', 'WP_Rocket\Subscriber\Cache\Expired_Cache_Purge_Subscriber' )
->withArgument( $options )
->withArgument( $this->getContainer()->get( 'expired_cache_purge' ) );
$this->getContainer()->share( 'webp_subscriber', 'WP_Rocket\Subscriber\Media\Webp_Subscriber' )
->withArgument( $options )
->withArgument( $this->getContainer()->get( 'options_api' ) )
->withArgument( $this->getContainer()->get( 'cdn_subscriber' ) )
->withArgument( $this->getContainer()->get( 'beacon' ) );
$this->getContainer()->share( 'detect_missing_tags_subscriber', 'WP_Rocket\Subscriber\Tools\Detect_Missing_Tags_Subscriber' );
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WP_Rocket\ServiceProvider;
use WP_Rocket\Engine\Container\ServiceProvider\AbstractServiceProvider;
/**
* Service Provider for database optimization
*
* @since 3.3
* @author Remy Perona
*/
class Database extends AbstractServiceProvider {
/**
* The provides array is a way to let the container
* know that a service is provided by this service
* provider. Every service that is registered via
* this service provider must have an alias added
* to this array or it will be ignored.
*
* @var array
*/
protected $provides = [
'db_optimization_process',
'db_optimization',
];
/**
* Registers the option array in the container
*
* @since 3.3
* @author Remy Perona
*
* @return void
*/
public function register() {
$this->getContainer()->add( 'db_optimization_process', 'WP_Rocket\Admin\Database\Optimization_Process' );
$this->getContainer()->add( 'db_optimization', 'WP_Rocket\Admin\Database\Optimization' )
->withArgument( $this->getContainer()->get( 'db_optimization_process' ) );
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace WP_Rocket\ServiceProvider;
use WP_Rocket\Engine\Container\ServiceProvider\AbstractServiceProvider;
/**
* Service provider for the WP Rocket options
*
* @since 3.3
* @author Remy Perona
*/
class Options extends AbstractServiceProvider {
/**
* The provides array is a way to let the container
* know that a service is provided by this service
* provider. Every service that is registered via
* this service provider must have an alias added
* to this array or it will be ignored.
*
* @var array
*/
protected $provides = [
'options',
];
/**
* Registers the option array in the container
*
* @since 3.3
* @author Remy Perona
*
* @return void
*/
public function register() {
$this->getContainer()->add( 'options', 'WP_Rocket\Admin\Options_Data' )
->withArgument( $this->getContainer()->get( 'options_api' )->get( 'settings', [] ) );
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace WP_Rocket\ServiceProvider;
use WP_Rocket\Engine\Container\ServiceProvider\AbstractServiceProvider;
/**
* Service provider for the WP Rocket updates.
*
* @since 3.3.6
* @author Grégory Viguier
*/
class Updater_Subscribers extends AbstractServiceProvider {
/**
* The provided array is a way to let the container
* know that a service is provided by this service
* provider. Every service that is registered via
* this service provider must have an alias added
* to this array or it will be ignored.
*
* @var array
* @since 3.3.6
* @access protected
* @author Grégory Viguier
*/
protected $provides = [
'plugin_updater_common_subscriber',
'plugin_information_subscriber',
'plugin_updater_subscriber',
];
/**
* Registers the option array in the container.
*
* @since 3.3.6
* @access public
* @author Grégory Viguier
*/
public function register() {
$api_url = wp_parse_url( WP_ROCKET_WEB_INFO );
$this->getContainer()->add( 'plugin_updater_common_subscriber', 'WP_Rocket\Subscriber\Plugin\Updater_Api_Common_Subscriber' )
->withArgument(
[
'api_host' => $api_url['host'],
'site_url' => home_url(),
'plugin_version' => WP_ROCKET_VERSION,
'settings_slug' => WP_ROCKET_SLUG,
'settings_nonce_key' => WP_ROCKET_PLUGIN_SLUG,
'plugin_options' => $this->getContainer()->get( 'options' ),
]
);
$this->getContainer()->add( 'plugin_information_subscriber', 'WP_Rocket\Subscriber\Plugin\Information_Subscriber' )
->withArgument(
[
'plugin_file' => WP_ROCKET_FILE,
'api_url' => WP_ROCKET_WEB_INFO,
]
);
$this->getContainer()->add( 'plugin_updater_subscriber', 'WP_Rocket\Subscriber\Plugin\Updater_Subscriber' )
->withArgument(
[
'plugin_file' => WP_ROCKET_FILE,
'plugin_version' => WP_ROCKET_VERSION,
'vendor_url' => WP_ROCKET_WEB_MAIN,
'api_url' => WP_ROCKET_WEB_CHECK,
'icons' => [
'2x' => WP_ROCKET_ASSETS_IMG_URL . 'icon-256x256.png',
'1x' => WP_ROCKET_ASSETS_IMG_URL . 'icon-128x128.png',
],
]
);
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace WP_Rocket\Admin\Database;
use WP_Rocket_WP_Background_Process;
/**
* Extends the background process class for the database optimization background process.
*
* @since 2.11
*
* @see WP_Background_Process
*/
class Optimization_Process extends WP_Rocket_WP_Background_Process {
/**
* Prefix
*
* @var string
* @access protected
*/
protected $prefix = 'rocket';
/**
* Specific action identifier for sitemap preload.
*
* @access protected
* @var string Action identifier
*/
protected $action = 'database_optimization';
/**
* Count the number of optimized items.
*
* @access protected
* @var array $count An array of indexed number of optimized items.
*/
protected $count = [];
/**
* Dispatch
*
* @access public
* @return array|WP_Error
*/
public function dispatch() {
set_transient( 'rocket_database_optimization_process', 'running', HOUR_IN_SECONDS );
// Perform remote post.
return parent::dispatch();
}
/**
* Perform the optimization corresponding to $item
*
* @param mixed $item Queue item to iterate over.
*
* @return bool false
*/
protected function task( $item ) {
global $wpdb;
switch ( $item ) {
case 'database_revisions':
$query = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_type = 'revision'" );
if ( $query ) {
$number = 0;
foreach ( $query as $id ) {
$number += wp_delete_post_revision( intval( $id ) ) instanceof \WP_Post ? 1 : 0;
}
$this->count[ $item ] = $number;
}
break;
case 'database_auto_drafts':
$query = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'auto-draft'" );
if ( $query ) {
$number = 0;
foreach ( $query as $id ) {
$number += wp_delete_post( intval( $id ), true ) instanceof \WP_Post ? 1 : 0;
}
$this->count[ $item ] = $number;
}
break;
case 'database_trashed_posts':
$query = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'trash'" );
if ( $query ) {
$number = 0;
foreach ( $query as $id ) {
$number += wp_delete_post( $id, true ) instanceof \WP_Post ? 1 : 0;
}
$this->count[ $item ] = $number;
}
break;
case 'database_spam_comments':
$query = $wpdb->get_col( "SELECT comment_ID FROM $wpdb->comments WHERE comment_approved = 'spam'" );
if ( $query ) {
$number = 0;
foreach ( $query as $id ) {
$number += (int) wp_delete_comment( intval( $id ), true );
}
$this->count[ $item ] = $number;
}
break;
case 'database_trashed_comments':
$query = $wpdb->get_col( "SELECT comment_ID FROM $wpdb->comments WHERE (comment_approved = 'trash' OR comment_approved = 'post-trashed')" );
if ( $query ) {
$number = 0;
foreach ( $query as $id ) {
$number += (int) wp_delete_comment( intval( $id ), true );
}
$this->count[ $item ] = $number;
}
break;
case 'database_expired_transients':
$time = isset( $_SERVER['REQUEST_TIME'] ) ? (int) $_SERVER['REQUEST_TIME'] : time();
$query = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s AND option_value < %d", $wpdb->esc_like( '_transient_timeout' ) . '%', $time ) );
if ( $query ) {
$number = 0;
foreach ( $query as $transient ) {
$key = str_replace( '_transient_timeout_', '', $transient );
$number += (int) delete_transient( $key );
}
$this->count[ $item ] = $number;
}
break;
case 'database_all_transients':
$query = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_site_transient_' ) . '%' ) );
if ( $query ) {
$number = 0;
foreach ( $query as $transient ) {
if ( strpos( $transient, '_site_transient_' ) !== false ) {
$number += (int) delete_site_transient( str_replace( '_site_transient_', '', $transient ) );
} else {
$number += (int) delete_transient( str_replace( '_transient_', '', $transient ) );
}
}
$this->count[ $item ] = $number;
}
break;
case 'database_optimize_tables':
$query = $wpdb->get_results( "SELECT table_name, data_free FROM information_schema.tables WHERE table_schema = '" . DB_NAME . "' and Engine <> 'InnoDB' and data_free > 0" );
if ( $query ) {
$number = 0;
foreach ( $query as $table ) {
$number += (int) $wpdb->query( "OPTIMIZE TABLE $table->table_name" );
}
$this->count[ $item ] = $number;
}
break;
}
return false;
}
/**
* Complete
*/
protected function complete() {
delete_transient( 'rocket_database_optimization_process' );
set_transient( 'rocket_database_optimization_process_complete', $this->count );
parent::complete();
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace WP_Rocket\Admin\Database;
defined( 'ABSPATH' ) || exit;
/**
* Handles the database optimization process.
*
* @since 2.11
* @author Remy Perona
*/
class Optimization {
/**
* Background process instance
*
* @since 2.11
* @var Optimization_Process $process Background Process instance.
* @access protected
*/
protected $process;
/**
* Array of option name/label pairs.
*
* @var array
* @access private
*/
private $options;
/**
* Class constructor.
*
* @since 2.11
* @author Remy Perona
*
* @param Optimization_Process $process Background process instance.
*/
public function __construct( Optimization_Process $process ) {
$this->process = $process;
$this->options = [
'database_revisions' => __( 'Revisions', 'rocket' ),
'database_auto_drafts' => __( 'Auto Drafts', 'rocket' ),
'database_trashed_posts' => __( 'Trashed Posts', 'rocket' ),
'database_spam_comments' => __( 'Spam Comments', 'rocket' ),
'database_trashed_comments' => __( 'Trashed Comments', 'rocket' ),
'database_expired_transients' => __( 'Expired transients', 'rocket' ),
'database_all_transients' => __( 'Transients', 'rocket' ),
'database_optimize_tables' => __( 'Tables', 'rocket' ),
];
}
/**
* Get Database options
*
* @since 3.0.4
* @author Remy Perona
*
* @return array
*/
public function get_options() {
return $this->options;
}
/**
* Performs the database optimization
*
* @since 2.11
* @author Remy Perona
*
* @param array $options WP Rocket Database options.
*/
public function process_handler( $options ) {
if ( method_exists( $this->process, 'cancel_process' ) ) {
$this->process->cancel_process();
}
array_map( [ $this->process, 'push_to_queue' ], $options );
$this->process->save()->dispatch();
}
/**
* Count the number of items concerned by the database cleanup
*
* @since 2.8
* @author Remy Perona
*
* @param string $type Item type to count.
* @return int Number of items for this type
*/
public function count_cleanup_items( $type ) {
global $wpdb;
$count = 0;
switch ( $type ) {
case 'database_revisions':
$count = $wpdb->get_var( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_type = 'revision'" );
break;
case 'database_auto_drafts':
$count = $wpdb->get_var( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_status = 'auto-draft'" );
break;
case 'database_trashed_posts':
$count = $wpdb->get_var( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_status = 'trash'" );
break;
case 'database_spam_comments':
$count = $wpdb->get_var( "SELECT COUNT(comment_ID) FROM $wpdb->comments WHERE comment_approved = 'spam'" );
break;
case 'database_trashed_comments':
$count = $wpdb->get_var( "SELECT COUNT(comment_ID) FROM $wpdb->comments WHERE (comment_approved = 'trash' OR comment_approved = 'post-trashed')" );
break;
case 'database_expired_transients':
$time = isset( $_SERVER['REQUEST_TIME'] ) ? (int) $_SERVER['REQUEST_TIME'] : time();
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(option_name) FROM $wpdb->options WHERE option_name LIKE %s AND option_value < %d", $wpdb->esc_like( '_transient_timeout' ) . '%', $time ) );
break;
case 'database_all_transients':
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(option_id) FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_site_transient_' ) . '%' ) );
break;
case 'database_optimize_tables':
$count = $wpdb->get_var( "SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = '" . DB_NAME . "' and Engine <> 'InnoDB' and data_free > 0" );
break;
}
return $count;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace WP_Rocket\Admin;
defined( 'ABSPATH' ) || exit;
/**
* Manages options using the WordPress options API.
*
* @since 3.0
* @author Remy Perona
*/
abstract class Abstract_Options {
/**
* Gets the option for the given name. Returns the default value if the value does not exist.
*
* @since 3.0
* @author Remy Perona
*
* @param string $name Name of the option to get.
* @param mixed $default Default value to return if the value does not exist.
*
* @return mixed
*/
abstract public function get( $name, $default = null );
/**
* Sets the value of an option. Update the value if the option for the given name already exists.
*
* @since 3.0
* @author Remy Perona
* @param string $name Name of the option to set.
* @param mixed $value Value to set for the option.
*
* @return void
*/
abstract public function set( $name, $value );
/**
* Deletes the option with the given name.
*
* @since 3.0
* @author Remy Perona
*
* @param string $name Name of the option to delete.
*
* @return void
*/
abstract public function delete( $name );
/**
* Checks if the option with the given name exists.
*
* @since 3.0
* @author Remy Perona
*
* @param string $name Name of the option to check.
*
* @return boolean True if the option exists, false otherwise
*/
public function has( $name ) {
return null !== $this->get( $name );
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace WP_Rocket\Admin;
use WP_Rocket\Logger\Logger;
use WP_Rocket\Event_Management\Subscriber_Interface;
defined( 'ABSPATH' ) || exit;
/**
* Class that handles few things about the logs.
*
* @since 3.1.4
* @author Grégory Viguier
*/
class Logs implements Subscriber_Interface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return array
*/
public static function get_subscribed_events() {
return [
'pre_update_option_' . WP_ROCKET_SLUG => [ 'enable_debug', 10, 2 ],
'admin_post_rocket_download_debug_file' => 'download_debug_file',
'admin_post_rocket_delete_debug_file' => 'delete_debug_file',
];
}
/** ----------------------------------------------------------------------------------------- */
/** DEBUG ACTIVATION ======================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Enable or disable the debug mode when settings are saved.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param array $newvalue An array of submitted options values.
* @param array $oldvalue An array of previous options values.
* @return array Updated submitted options values.
*/
public function enable_debug( $newvalue, $oldvalue ) {
if ( empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
return $newvalue;
}
if ( ! empty( $newvalue['debug_enabled'] ) ) {
Logger::enable_debug();
} else {
Logger::disable_debug();
}
unset( $newvalue['debug_enabled'] );
return $newvalue;
}
/** ----------------------------------------------------------------------------------------- */
/** ADMIN POST CALLBACKS ==================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Download the log file.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*/
public function download_debug_file() {
if ( ! $this->verify_nonce( 'download_debug_file' ) ) {
wp_nonce_ays( '' );
}
if ( ! $this->current_user_can() ) {
$this->redirect();
}
$contents = Logger::get_log_file_contents();
if ( is_wp_error( $contents ) ) {
add_settings_error( 'general', $contents->get_error_code(), $contents->get_error_message(), 'error' );
set_transient( 'settings_errors', get_settings_errors(), 30 );
$this->redirect( add_query_arg( 'settings-updated', 1, wp_get_referer() ) );
}
$file_name = Logger::get_log_file_path();
$file_name = basename( $file_name, '.log' ) . Logger::get_log_file_extension();
nocache_headers();
@header( 'Content-Type: text/x-log' );
@header( 'Content-Disposition: attachment; filename="' . $file_name . '"' );
@header( 'Content-Transfer-Encoding: binary' );
@header( 'Content-Length: ' . strlen( $contents ) );
@header( 'Connection: close' );
echo $contents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit();
}
/**
* Delete the log file.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*/
public function delete_debug_file() {
if ( ! $this->verify_nonce( 'delete_debug_file' ) ) {
wp_nonce_ays( '' );
}
if ( ! $this->current_user_can() ) {
$this->redirect();
}
if ( ! Logger::delete_log_file() ) {
add_settings_error( 'general', 'debug_file_not_deleted', __( 'The debug file could not be deleted.', 'rocket' ), 'error' );
set_transient( 'settings_errors', get_settings_errors(), 30 );
$this->redirect( add_query_arg( 'settings-updated', 1, wp_get_referer() ) );
}
// Done.
$this->redirect();
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Verify the nonce sent in $_GET['_wpnonce'].
*
* @since 3.1.4
* @access protected
* @author Grégory Viguier
*
* @param string $nonce_name The nonce name.
* @return bool
*/
protected function verify_nonce( $nonce_name ) {
return isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], $nonce_name ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
}
/**
* Tell if the current user can operate.
*
* @since 3.1.4
* @access protected
* @author Grégory Viguier
*
* @return bool
*/
protected function current_user_can() {
return current_user_can( 'rocket_manage_options' );
}
/**
* Redirect the user.
*
* @since 3.1.4
* @access protected
* @author Grégory Viguier
*
* @param string $redirect URL to redirect the user to.
*/
protected function redirect( $redirect = null ) {
if ( empty( $redirect ) ) {
$redirect = wp_get_referer();
}
wp_safe_redirect( esc_url_raw( $redirect ) );
die();
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace WP_Rocket\Admin;
/**
* Manages the data inside an option.
*
* @since 3.0
* @author Remy Perona
*/
class Options_Data {
/**
* Option data
*
* @var Array Array of data inside the option
*/
private $options;
/**
* Constructor
*
* @param Array $options Array of data coming from an option.
*/
public function __construct( $options ) {
$this->options = $options;
}
/**
* Checks if the provided key exists in the option data array.
*
* @since 3.0
* @author Remy Perona
*
* @param string $key key name.
* @return boolean true if it exists, false otherwise
*/
public function has( $key ) {
return isset( $this->options[ $key ] );
}
/**
* Gets the value associated with a specific key.
*
* @since 3.0
* @author Remy Perona
*
* @param string $key key name.
* @param mixed $default default value to return if key doesn't exist.
* @return mixed
*/
public function get( $key, $default = '' ) {
/**
* Pre-filter any WP Rocket option before read
*
* @since 2.5
*
* @param mixed $default The default value.
*/
$value = apply_filters( 'pre_get_rocket_option_' . $key, null, $default ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
if ( null !== $value ) {
return $value;
}
if ( 'consumer_key' === $key && rocket_has_constant( 'WP_ROCKET_KEY' ) ) {
return WP_ROCKET_KEY;
} elseif ( 'consumer_email' === $key && rocket_has_constant( 'WP_ROCKET_EMAIL' ) ) {
return WP_ROCKET_EMAIL;
}
if ( ! $this->has( $key ) ) {
return $default;
}
/**
* Filter any WP Rocket option after read
*
* @since 2.5
*
* @param mixed $default The default value.
*/
return apply_filters( 'get_rocket_option_' . $key, $this->options[ $key ], $default ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
}
/**
* Sets the value associated with a specific key.
*
* @since 3.0
* @author Remy Perona
*
* @param string $key key name.
* @param mixed $value to set.
* @return void
*/
public function set( $key, $value ) {
$this->options[ $key ] = $value;
}
/**
* Sets multiple values.
*
* @since 3.0
* @author Remy Perona
*
* @param array $options An array of key/value pairs to set.
* @return void
*/
public function set_values( $options ) {
foreach ( $options as $key => $value ) {
$this->set( $key, $value );
}
}
/**
* Gets the option array.
*
* @since 3.0
* @author Remy Perona
*
* @return array
*/
public function get_options() {
return $this->options;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace WP_Rocket\Admin;
/**
* Manages options using the WordPress options API.
*
* @since 3.0
* @author Remy Perona
*/
class Options extends Abstract_Options {
/**
* The prefix used by WP Rocket options.
*
* @since 3.0
* @author Remy Perona
*
* @var string
*/
private $prefix;
/**
* Constructor
*
* @since 3.0
* @author Remy Perona
*
* @param string $prefix WP Rocket options prefix.
*/
public function __construct( $prefix = '' ) {
$this->prefix = $prefix;
}
/**
* Gets the option name used to store the option in the WordPress database.
*
* @since 3.0
* @author Remy Perona
*
* @param string $name Unprefixed name of the option.
*
* @return string Option name used to store it
*/
public function get_option_name( $name ) {
return $this->prefix . $name;
}
/**
* Gets the option for the given name. Returns the default value if the value does not exist.
*
* @since 3.0
* @author Remy Perona
*
* @param string $name Name of the option to get.
* @param mixed $default Default value to return if the value does not exist.
*
* @return mixed
*/
public function get( $name, $default = null ) {
$option = get_option( $this->get_option_name( $name ), $default );
if ( is_array( $default ) && ! is_array( $option ) ) {
$option = (array) $option;
}
return $option;
}
/**
* Sets the value of an option. Update the value if the option for the given name already exists.
*
* @since 3.0
* @author Remy Perona
* @param string $name Name of the option to set.
* @param mixed $value Value to set for the option.
*
* @return void
*/
public function set( $name, $value ) {
update_option( $this->get_option_name( $name ), $value );
}
/**
* Deletes the option with the given name.
*
* @since 3.0
* @author Remy Perona
*
* @param string $name Name of the option to delete.
*
* @return void
*/
public function delete( $name ) {
delete_option( $this->get_option_name( $name ) );
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace WP_Rocket\Admin\Deactivation;
use WP_Rocket\Abstract_Render;
defined( 'ABSPATH' ) || exit;
/**
* Handles rendering of deactivation intent form on plugins page
*
* @since 3.0
* @author Remy Perona
*/
class Render extends Abstract_Render {
/**
* Renders Deactivation intent form
*
* @since 3.0
* @author Remy Perona
*
* @return void
*/
public function render_form() {
$args = [
'deactivation_url' => wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . rawurlencode( 'wp-rocket/wp-rocket.php' ), 'deactivate-plugin_wp-rocket/wp-rocket.php' ),
];
echo $this->generate( 'form', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view.
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace WP_Rocket\Busting;
/**
* Abstract class for assets busting
*
* @since 3.1
* @author Remy Perona
*/
abstract class Abstract_Busting {
/**
* Cache busting files base path
*
* @var string
*/
protected $busting_path;
/**
* Cache busting base URL
*
* @var string
*/
protected $busting_url;
/**
* Filename for the cache busting file.
*
* @var string
*/
protected $filename;
/**
* Gets the content of an URL
*
* @since 3.1
* @author Remy Perona
*
* @param string $url The URL to request.
* @return string|bool
*/
protected function get_file_content( $url ) {
$content = wp_remote_retrieve_body( wp_remote_get( $url ) );
if ( ! $content ) {
return false;
}
return $content;
}
/**
* Saves the content of the URL to bust to the busting file if it doesn't exist yet.
*
* @since 3.1
* @author Remy Perona
*
* @param string $url URL to get the content from.
* @return bool
*/
public function save( $url ) {
$path = $this->busting_path . $this->filename;
if ( \rocket_direct_filesystem()->exists( $path ) ) {
return true;
}
return $this->refresh_save( $url );
}
/**
* Saves the content of the URL to bust to the busting file.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $url URL to get the content from.
* @return bool
*/
public function refresh_save( $url ) {
$path = $this->busting_path . $this->filename;
$content = $this->get_file_content( $url );
if ( ! $content ) {
// If a previous version is present, it is kept in place.
return false;
}
if ( ! \rocket_direct_filesystem()->exists( $this->busting_path ) ) {
\rocket_mkdir_p( $this->busting_path );
}
if ( ! \rocket_put_content( $path, $content ) ) {
return false;
}
return true;
}
/**
* Gets the final URL for the cache busting file.
*
* @since 3.1
* @author Remy Perona
*
* @return string
*/
protected function get_busting_url() {
// This filter is documented in inc/functions/minify.php.
return apply_filters( 'rocket_js_url', $this->busting_url . $this->filename );
}
/**
* Performs the replacement process.
*
* @since 3.1
* @author Remy Perona
*
* @param string $html HTML content.
* @return string
*/
abstract public function replace_url( $html );
/**
* Searches for element(s) in the DOM
*
* @since 3.1
* @author Remy Perona
*
* @param string $pattern Pattern to match.
* @param string $html HTML content.
* @return string
*/
abstract protected function find( $pattern, $html );
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,673 @@
<?php
namespace WP_Rocket\Busting;
use WP_Rocket\Logger\Logger;
/**
* Manages the cache busting of the Facebook SDK file.
*
* @since 3.2
* @author Grégory Viguier
*/
class Facebook_SDK extends Abstract_Busting {
/**
* Facebook SDK URL.
* %s is a locale like "en_US".
*
* @var string
* @since 3.2
* @access protected
* @author Grégory Viguier
*/
protected $url = 'https://connect.facebook.net/%s/sdk.js';
/**
* Filename for the cache busting file.
* %s is a locale like "en_US".
*
* @var string
* @since 3.2
* @access protected
* @author Grégory Viguier
*/
protected $filename = 'fbsdk-%s.js';
/**
* Flag to track the replacement.
*
* @var bool
* @since 3.2
* @access private
* @author Grégory Viguier
*/
protected $is_replaced = false;
/**
* Constructor.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $busting_path Path to the busting directory.
* @param string $busting_url URL of the busting directory.
*/
public function __construct( $busting_path, $busting_url ) {
/** Warning: the file name and script URL are dynamic, and must be run through sprintf(). */
$this->busting_path = $busting_path . 'facebook-tracking/';
$this->busting_url = $busting_url . 'facebook-tracking/';
}
/**
* Perform the URL replacement process.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $html HTML contents.
* @return string HTML contents.
*/
public function replace_url( $html ) {
$this->is_replaced = false;
$tag = $this->find( '<script[^>]*?>(.*)<\/script>', $html );
if ( ! $tag ) {
return $html;
}
Logger::info(
'FACEBOOK SDK CACHING PROCESS STARTED.',
[
'fb sdk',
'tag' => $tag,
]
);
$locale = $this->get_locale_from_url( $tag );
$remote_url = $this->get_url( $locale );
if ( ! $this->save( $remote_url ) ) {
return $html;
}
$file_url = $this->get_busting_file_url( $locale );
$replace_tag = preg_replace( '@(?:https?:)?//connect\.facebook\.net/[a-zA-Z_-]+/sdk\.js@i', $file_url, $tag, -1, $count );
if ( ! $count || false === strpos( $html, $tag ) ) {
Logger::error( 'The local file URL could not be replaced in the page contents.', [ 'fb sdk' ] );
return $html;
}
$html = str_replace( $tag, $replace_tag, $html );
$file_path = $this->get_busting_file_path( $locale );
$xfbml = $this->get_xfbml_from_url( $tag ); // Default value should be set to false.
$app_id = $this->get_appId_from_url( $tag ); // APP_ID is the only required value.
$url_version = $this->get_version_from_url( $tag );
$version = false === $url_version ? 'v5.0' : $url_version; // If version is not available set it to the latest: v.5.0.
if ( false !== $app_id ) {
// Add FB async init.
$fb_async_script = '<script>window.fbAsyncInit = function fbAsyncInit () {FB.init({appId: \'' . $app_id . '\',xfbml: ' . $xfbml . ',version: \'' . $version . '\'})}</script>';
$html = str_replace( '</body>', $fb_async_script . '</body>', $html );
}
$this->is_replaced = true;
/**
* Triggered once the Facebook SDK URL has been replaced in the page contents.
*
* @since 3.2
* @author Grégory Viguier
*
* @param string $file_url URL of the local main file.
* @param string $file_path Path to the local file.
*/
do_action( 'rocket_after_facebook_sdk_url_replaced', $file_url, $file_path );
Logger::info(
'Facebook SDK caching process succeeded.',
[
'fb sdk',
'file' => $file_path,
]
);
return $html;
}
/**
* Tell if the replacement was sucessful or not.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_replaced() {
return $this->is_replaced;
}
/** ----------------------------------------------------------------------------------------- */
/** GRAB/MANIPULATE DATA IN CONTENTS ======================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Search for an element in the DOM.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $pattern Pattern to match.
* @param string $html HTML contents.
* @return string|bool The matched HTML on success. False if nothing is found.
*/
protected function find( $pattern, $html ) {
preg_match_all( '/' . $pattern . '/Umsi', $html, $matches, PREG_SET_ORDER );
if ( empty( $matches ) ) {
return false;
}
foreach ( $matches as $match ) {
if ( trim( $match[1] ) && preg_match( '@//connect\.facebook\.net/[a-zA-Z_-]+/sdk\.js@i', $match[1] ) ) {
return $match[0];
}
}
return false;
}
/** ----------------------------------------------------------------------------------------- */
/** UPDATE/SAVE A LOCAL FILE ================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Save the contents of a URL into a local file if it doesn't exist yet.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $url URL to get the contents from.
* @return bool True on success. False on failure.
*/
public function save( $url ) {
$locale = $this->get_locale_from_url( $url );
$path = $this->get_busting_file_path( $locale );
if ( \rocket_direct_filesystem()->exists( $path ) ) {
// If a previous version is present, keep it.
return true;
}
return $this->refresh_save( $url );
}
/**
* Save the contents of a URL into a local file.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $url URL to get the contents from.
* @return bool True on success. False on failure.
*/
public function refresh_save( $url ) {
$content = $this->get_file_content( $url );
if ( ! $content ) {
// Error, we couldn't fetch the file contents.
return false;
}
$locale = $this->get_locale_from_url( $url );
$path = $this->get_busting_file_path( $locale );
return (bool) $this->update_file_contents( $path, $content );
}
/**
* Add new contents to a file. If the file doesn't exist, it is created.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $file_path Path to the file to update.
* @param string $file_contents New contents.
* @return string|bool The file contents on success. False on failure.
*/
private function update_file_contents( $file_path, $file_contents ) {
if ( ! \rocket_direct_filesystem()->exists( $this->busting_path ) ) {
\rocket_mkdir_p( $this->busting_path );
}
if ( ! \rocket_put_content( $file_path, $file_contents ) ) {
Logger::error(
'Contents could not be written into file.',
[
'fb sdk',
'path' => $file_path,
]
);
return false;
}
/**
* Triggered once a file contents have been updated.
*
* @since 3.2
* @author Grégory Viguier
*
* @param string $file_path Path to the file to update.
* @param string $file_contents The file contents.
*/
do_action( 'rocket_after_facebook_sdk_file_updated', $file_path, $file_contents );
return $file_contents;
}
/** ----------------------------------------------------------------------------------------- */
/** PUBLIC BULK ACTIONS ON LOCAL FILES ====================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Look for existing local files and update their contents if there's a new version available.
* Actually, if a more recent version exists on the FB side, it will delete all local files and hit the home page to recreate them.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return bool True on success. False on failure.
*/
public function refresh() {
$files = $this->get_files();
if ( ! $files ) {
// No files (or there's an error).
return false !== $files;
}
$error_paths = [];
$pattern = $this->escape_file_name( $this->filename );
$pattern = sprintf( $pattern, '(?<locale>[a-zA-Z_-]+)' );
foreach ( $files as $file ) {
preg_match( '/^' . $pattern . '$/', $file, $matches );
$remote_url = $this->get_url( $matches['locale'] );
if ( ! $this->refresh_save( $remote_url ) ) {
$error_paths[] = $this->get_busting_file_path( $matches['locale'] );
}
}
if ( $error_paths ) {
Logger::error(
'Local file(s) could not be updated.',
[
'fb sdk',
'paths' => $error_paths,
]
);
}
/**
* Triggered once all local files have been updated (or not).
*
* @since 3.2
* @author Grégory Viguier
*
* @param array $files An array of file names.
* @param array $error_paths Paths to the files that couldn't be updated. An empty array if everything is fine.
*/
do_action( 'rocket_after_facebook_sdk_files_refresh', $files, $error_paths );
return ! $error_paths;
}
/**
* Delete all Facebook SDK busting files.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return bool True on success. False on failure.
*/
public function delete() {
$filesystem = \rocket_direct_filesystem();
$files = $this->get_files();
if ( ! $files ) {
// No files (or there's an error).
return false !== $files;
}
$error_paths = [];
foreach ( $files as $file_name ) {
if ( ! $filesystem->delete( $this->busting_path . $file_name, false, 'f' ) ) {
$error_paths[] = $this->busting_path . $file_name;
}
}
if ( $error_paths ) {
Logger::error(
'Local file(s) could not be deleted.',
[
'fb sdk',
'paths' => $error_paths,
]
);
}
/**
* Triggered once all local files have been deleted (or not).
*
* @since 3.2
* @author Grégory Viguier
*
* @param array $files An array of file names.
* @param array $error_paths Paths to the files that couldn't be deleted. An empty array if everything is fine.
*/
do_action( 'rocket_after_facebook_sdk_files_deleted', $files, $error_paths );
return ! $error_paths;
}
/** ----------------------------------------------------------------------------------------- */
/** SCAN FOR LOCAL FILES ==================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get all cached files in the directory.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @return array|bool A list of file names. False on failure.
*/
private function get_files() {
$filesystem = \rocket_direct_filesystem();
$dir_path = rtrim( $this->busting_path, '\\/' );
if ( ! $filesystem->exists( $dir_path ) ) {
return [];
}
if ( ! $filesystem->is_writable( $dir_path ) ) {
Logger::error(
'Directory is not writable.',
[
'fb sdk',
'path' => $dir_path,
]
);
return false;
}
$dir = $filesystem->dirlist( $dir_path );
if ( false === $dir ) {
Logger::error(
'Could not get the directory contents.',
[
'fb sdk',
'path' => $dir_path,
]
);
return false;
}
if ( ! $dir ) {
return [];
}
$list = [];
$pattern = $this->escape_file_name( $this->filename );
$pattern = sprintf( $pattern, '[a-zA-Z_-]+' );
foreach ( $dir as $entry ) {
if ( 'f' !== $entry['type'] ) {
continue;
}
if ( preg_match( '/^' . $pattern . '$/', $entry['name'], $matches ) ) {
$list[ $entry['name'] ] = $entry['name'];
}
}
return $list;
}
/** ----------------------------------------------------------------------------------------- */
/** REMOTE SDK FILE ========================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the remote Facebook SDK URL.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $locale A locale string, like 'en_US'.
* @return string
*/
public function get_url( $locale ) {
return sprintf( $this->url, $locale );
}
/**
* Extract the locale from a URL to bust.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $url Any string containing the URL to bust.
* @return string|bool The locale on success. False on failure.
*/
private function get_locale_from_url( $url ) {
$pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js@i';
if ( ! preg_match( $pattern, $url, $matches ) ) {
return false;
}
return $matches['locale'];
}
/**
* Extract XFBML from a URL to bust.
*
* @since 3.4.3
* @access private
* @author Soponar Cristina
*
* @param string $url Any string containing the URL to bust.
* @return string|bool The XFBML on success. False on failure.
*/
private function get_xfbml_from_url( $url ) {
$pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?xfbml=(?<xfbml>[0-9]+)@i';
if ( ! preg_match( $pattern, $url, $matches ) ) {
return false;
}
return $matches['xfbml'];
}
/**
* Extract appId from a URL to bust.
*
* @since 3.4.3
* @access private
* @author Soponar Cristina
*
* @param string $url Any string containing the URL to bust.
* @return string|bool The appId on success. False on failure.
*/
private function get_appId_from_url( $url ) {
$pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?appId=(?<appId>[0-9]+)@i';
if ( ! preg_match( $pattern, $url, $matches ) ) {
return false;
}
return $matches['appId'];
}
/**
* Extract version from a URL to bust.
*
* @since 3.4.3
* @access private
* @author Soponar Cristina
*
* @param string $url Any string containing the URL to bust.
* @return string|bool The version on success. False on failure.
*/
private function get_version_from_url( $url ) {
$pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?version=(?<version>[a-zA-Z0-9.]+)@i';
if ( ! preg_match( $pattern, $url, $matches ) ) {
return false;
}
return $matches['version'];
}
/** ----------------------------------------------------------------------------------------- */
/** BUSTING FILE ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the local Facebook SDK URL.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $locale A locale string, like 'en_US'.
* @return string
*/
private function get_busting_file_url( $locale ) {
$filename = $this->get_busting_file_name( $locale );
// This filter is documented in inc/functions/minify.php.
return apply_filters( 'rocket_js_url', apply_filters( 'rocket_facebook_sdk_url', $this->busting_url . $filename ) );
}
/**
* Get the local Facebook SDK file name.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $locale A locale string, like 'en_US'.
* @return string
*/
private function get_busting_file_name( $locale ) {
return sprintf( $this->filename, $locale );
}
/**
* Get the local Facebook SDK file path.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $locale A locale string, like 'en_US'.
* @return string
*/
private function get_busting_file_path( $locale ) {
return $this->busting_path . $this->get_busting_file_name( $locale );
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the contents of a URL.
*
* @since 3.2
* @access protected
* @author Grégory Viguier
*
* @param string $url The URL to request.
* @return string|bool The contents on success. False on failure.
*/
protected function get_file_content( $url ) {
try {
$response = wp_remote_get( $url );
} catch ( \Exception $e ) {
Logger::error(
'Remote file could not be fetched.',
[
'fb sdk',
'url' => $url,
'response' => $e->getMessage(),
]
);
return false;
}
if ( is_wp_error( $response ) ) {
Logger::error(
'Remote file could not be fetched.',
[
'fb sdk',
'url' => $url,
'response' => $response->get_error_message(),
]
);
return false;
}
$contents = wp_remote_retrieve_body( $response );
if ( ! $contents ) {
Logger::error(
'Remote file could not be fetched.',
[
'fb sdk',
'url' => $url,
'response' => $response,
]
);
return false;
}
return $contents;
}
/**
* Escape a file name, to be used in a regex pattern (delimiter is `/`).
* `%s` conversion specifications are protected.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $file_name The file name.
* @return string
*/
private function escape_file_name( $file_name ) {
$file_name = explode( '%s', $file_name );
$file_name = array_map( 'preg_quote', $file_name );
return implode( '%s', $file_name );
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace WP_Rocket;
use WP_Rocket\Interfaces\Render_Interface;
/**
* Handle rendering of HTML content created by WP Rocket.
*
* @since 3.0
* @author Remy Perona
*/
abstract class Abstract_Render implements Render_Interface {
/**
* Path to the templates
*
* @since 3.0
* @author Remy Perona
*
* @var string
*/
private $template_path;
/**
* Constructor
*
* @since 3.0
* @author Remy Perona
*
* @param string $template_path Path to the templates.
*/
public function __construct( $template_path ) {
$this->template_path = $template_path;
}
/**
* Renders the given template if it's readable.
*
* @since 3.0
* @author Remy Perona
*
* @param string $template Template slug.
* @param array $data Data to pass to the template.
*/
public function generate( $template, $data = [] ) {
$template_path = $this->get_template_path( $template );
if ( ! rocket_direct_filesystem()->is_readable( $template_path ) ) {
return;
}
ob_start();
include $template_path;
return trim( ob_get_clean() );
}
/**
* Returns the path a specific template.
*
* @since 3.0
* @author Remy Perona
*
* @param string $path Relative path to the template.
* @return string
*/
private function get_template_path( $path ) {
return $this->template_path . '/' . $path . '.php';
}
/**
* Displays the button template.
*
* @since 3.0
* @author Remy Perona
*
* @param string $type Type of button (can be button or link).
* @param string $action Action to be performed.
* @param array $args Optional array of arguments to populate the button attributes.
* @return void
*/
public function render_action_button( $type, $action, $args = [] ) {
$default = [
'label' => '',
'action' => '',
'url' => '',
'parameter' => '',
'attributes' => '',
];
$args = wp_parse_args( $args, $default );
if ( ! empty( $args['attributes'] ) ) {
$attributes = '';
foreach ( $args['attributes'] as $key => $value ) {
$attributes .= ' ' . sanitize_key( $key ) . '="' . esc_attr( $value ) . '"';
}
$args['attributes'] = $attributes;
}
if ( 'link' !== $type ) {
$args['action'] = $action;
echo $this->generate( 'buttons/button', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view.
return;
}
switch ( $action ) {
case 'ask_support':
case 'view_account':
$args['url'] = rocket_get_external_url(
'ask_support' === $action ? 'support' : 'account',
[
'utm_source' => 'wp_plugin',
'utm_medium' => 'wp_rocket',
]
);
break;
case 'purge_cache':
case 'preload':
case 'rocket_purge_opcache':
case 'rocket_purge_cloudflare':
case 'rocket_purge_sucuri':
case 'rocket_rollback':
case 'rocket_export':
case 'rocket_generate_critical_css':
case 'rocket_purge_rocketcdn':
$url = admin_url( 'admin-post.php?action=' . $action );
if ( ! empty( $args['parameters'] ) ) {
$url = add_query_arg( $args['parameters'], $url );
}
if ( 'purge_cache' === $action ) {
$action .= '_all';
}
$args['url'] = wp_nonce_url( $url, $action );
break;
case 'documentation':
$args['url'] = get_rocket_documentation_url();
break;
}
echo $this->generate( 'buttons/link', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dynamic content is properly escaped in the view.
}
}

View File

@@ -0,0 +1,251 @@
<?php
defined( 'ABSPATH' ) || exit;
/**
* Class to check if the current WordPress and PHP versions meet our requirements
*
* @since 3.0
* @author Remy Perona
*/
class WP_Rocket_Requirements_Check {
/**
* Plugin Name
*
* @var string
*/
private $plugin_name;
/**
* Plugin filepath
*
* @var string
*/
private $plugin_file;
/**
* Plugin version
*
* @var string
*/
private $plugin_version;
/**
* Plugin previous version
*
* @var string
*/
private $plugin_last_version;
/**
* Required WordPress version
*
* @var string
*/
private $wp_version;
/**
* Required PHP version
*
* @var string
*/
private $php_version;
/**
* WP Rocket options
*
* @var array
*/
private $options;
/**
* Constructor
*
* @since 3.0
* @author Remy Perona
*
* @param array $args {
* Arguments to populate the class properties.
*
* @type string $plugin_name Plugin name.
* @type string $wp_version Required WordPress version.
* @type string $php_version Required PHP version.
* @type string $plugin_file Plugin filepath.
* }
*/
public function __construct( $args ) {
foreach ( [ 'plugin_name', 'plugin_file', 'plugin_version', 'plugin_last_version', 'wp_version', 'php_version' ] as $setting ) {
if ( isset( $args[ $setting ] ) ) {
$this->$setting = $args[ $setting ];
}
}
$this->plugin_last_version = version_compare( PHP_VERSION, '5.3' ) >= 0 ? $this->plugin_last_version : '2.10.12';
$this->options = get_option( 'wp_rocket_settings' );
}
/**
* Checks if all requirements are ok, if not, display a notice and the rollback
*
* @since 3.0
* @author Remy Perona
*
* @return bool
*/
public function check() {
if ( ! $this->php_passes() || ! $this->wp_passes() ) {
add_action( 'admin_notices', [ $this, 'notice' ] );
add_action( 'admin_post_rocket_rollback', [ $this, 'rollback' ] );
add_filter( 'http_request_args', [ $this, 'add_own_ua' ], 10, 2 );
return false;
}
return true;
}
/**
* Checks if the current PHP version is equal or superior to the required PHP version
*
* @since 3.0
* @author Remy Perona
*
* @return bool
*/
private function php_passes() {
return version_compare( PHP_VERSION, $this->php_version ) >= 0;
}
/**
* Checks if the current WordPress version is equal or superior to the required PHP version
*
* @since 3.0
* @author Remy Perona
*
* @return bool
*/
private function wp_passes() {
global $wp_version;
return version_compare( $wp_version, $this->wp_version ) >= 0;
}
/**
* Warns if PHP or WP version are less than the defined values and offer rollback.
*
* @since 3.0 Updated minimum PHP version to 5.4 and minimum WordPress version to 4.2
* @since 3.0 Moved to class
* @since 2.11
* @author Remy Perona
*/
public function notice() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Translators: %1$s = Plugin name, %2$s = Plugin version.
$message = '<p>' . sprintf( __( 'To function properly, %1$s %2$s requires at least:', 'rocket' ), $this->plugin_name, $this->plugin_version ) . '</p><ul>';
if ( ! $this->php_passes() ) {
// Translators: %1$s = PHP version required.
$message .= '<li>' . sprintf( __( 'PHP %1$s. To use this WP Rocket version, please ask your web host how to upgrade your server to PHP %1$s or higher.', 'rocket' ), $this->php_version ) . '</li>';
}
if ( ! $this->wp_passes() ) {
// Translators: %1$s = WordPress version required.
$message .= '<li>' . sprintf( __( 'WordPress %1$s. To use this WP Rocket version, please upgrade WordPress to version %1$s or higher.', 'rocket' ), $this->wp_version ) . '</li>';
}
$message .= '</ul><p>' . __( 'If you are not able to upgrade, you can rollback to the previous version by using the button below.', 'rocket' ) . '</p><p><a href="' . wp_nonce_url( admin_url( 'admin-post.php?action=rocket_rollback' ), 'rocket_rollback' ) . '" class="button">' .
// Translators: %s = Previous plugin version.
sprintf( __( 'Re-install version %s', 'rocket' ), $this->plugin_last_version )
. '</a></p>';
echo '<div class="notice notice-error">' . wp_kses_post( $message ) . '</div>';
}
/**
* Do the rollback
*
* @since 3.0
* @author Remy Perona
*/
public function rollback() {
check_ajax_referer( 'rocket_rollback' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die();
}
$consumer_key = isset( $this->options['consumer_key'] ) ? $this->options['consumer_key'] : false;
if ( ! $consumer_key && defined( 'WP_ROCKET_KEY' ) ) {
$consumer_key = WP_ROCKET_KEY;
}
$plugin_transient = get_site_transient( 'update_plugins' );
$plugin_folder = plugin_basename( dirname( $this->plugin_file ) );
$plugin_file = basename( $this->plugin_file );
$url = sprintf( 'https://wp-rocket.me/%s/wp-rocket_%s.zip', $consumer_key, $this->plugin_last_version );
$temp_array = [
'slug' => $plugin_folder,
'new_version' => $this->plugin_last_version,
'url' => 'https://wp-rocket.me',
'package' => $url,
];
$temp_object = (object) $temp_array;
$plugin_transient->response[ $plugin_folder . '/' . $plugin_file ] = $temp_object;
set_site_transient( 'update_plugins', $plugin_transient );
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
// translators: %s is the plugin name.
$title = sprintf( __( '%s Update Rollback', 'rocket' ), $this->plugin_name );
$plugin = 'wp-rocket/wp-rocket.php';
$nonce = 'upgrade-plugin_' . $plugin;
$url = 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $plugin );
$upgrader_skin = new Plugin_Upgrader_Skin( compact( 'title', 'nonce', 'url', 'plugin' ) );
$upgrader = new Plugin_Upgrader( $upgrader_skin );
remove_filter( 'site_transient_update_plugins', 'rocket_check_update', 1 );
$upgrader->upgrade( $plugin );
wp_die(
'',
// translators: %s is the plugin name.
sprintf( esc_html__( '%s Update Rollback', 'rocket' ), esc_html( $this->plugin_name ) ),
[
'response' => 200,
]
);
}
/**
* Filters the User Agent when doing a request to WP Rocket server
*
* @since 3.0
* @author Remy Perona
*
* @param array $request Array of arguments associated with the request.
* @param string $url URL requested.
*/
public function add_own_ua( $request, $url ) {
if ( strpos( $url, 'wp-rocket.me' ) === false ) {
return $request;
}
$consumer_key = isset( $this->options['consumer_key'] ) ? $this->options['consumer_key'] : false;
if ( ! $consumer_key && defined( 'WP_ROCKET_KEY' ) ) {
$consumer_key = WP_ROCKET_KEY;
}
$consumer_email = isset( $this->options['consumer_email'] ) ? $this->options['consumer_email'] : false;
if ( ! $consumer_email && defined( 'WP_ROCKET_EMAIL' ) ) {
$consumer_email = WP_ROCKET_EMAIL;
}
$request['user-agent'] = sprintf( '%s;WP-Rocket|%s%s|%s|%s|%s|;', $request['user-agent'], $this->plugin_version, '', $consumer_key, $consumer_email, esc_url( home_url() ) );
return $request;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,46 @@
{
"name": "wp-media/background-processing",
"description": "Async & Background Tasks Processing",
"homepage": "https://github.com/wp-media/background-processing",
"license": "GPL-2.0+",
"authors": [
{
"name": "WP Media",
"email": "contact@wp-media.me",
"homepage": "https://wp-media.me"
}
],
"type": "library",
"config": {
"sort-packages": true
},
"support": {
"issues": "https://github.com/wp-media/background-processing/issues",
"source": "https://github.com/wp-media/background-processing"
},
"require-dev": {
"php": "^5.6 || ^7",
"brain/monkey": "^2.0",
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
"phpcompatibility/phpcompatibility-wp": "^2.0",
"phpunit/phpunit": "^5.7 || ^7",
"wp-coding-standards/wpcs": "^2",
"wp-media/phpunit": "^1.0"
},
"autoload": {
"classmap": [ "" ]
},
"autoload-dev": {},
"scripts": {
"test-unit": "\"vendor/bin/wpmedia-phpunit\" unit path=Tests/Unit",
"test-integration": "\"vendor/bin/wpmedia-phpunit\" integration path=Tests/Integration/",
"run-tests": [
"@test-unit",
"@test-integration"
],
"install-codestandards": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run",
"phpcs": "phpcs --basepath=.",
"phpcs-changed": "./bin/phpcs-changed.sh",
"phpcs:fix": "phpcbf"
}
}

View File

@@ -0,0 +1,181 @@
<?php
/**
* WP Async Request
*
* @package WP-Background-Processing
*/
/**
* Abstract WP_Rocket_WP_Async_Request class.
*
* @abstract
*/
abstract class WP_Rocket_WP_Async_Request {
/**
* Prefix
*
* (default value: 'wp')
*
* @var string
* @access protected
*/
protected $prefix = 'wp';
/**
* Action
*
* (default value: 'async_request')
*
* @var string
* @access protected
*/
protected $action = '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() {
$this->identifier = $this->prefix . '_' . $this->action;
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
*
* @return array|WP_Error
*/
public function dispatch() {
$url = add_query_arg( $this->get_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() {
if ( property_exists( $this, 'query_args' ) ) {
return $this->query_args;
}
$args = array(
'action' => $this->identifier,
'nonce' => wp_create_nonce( $this->identifier ),
);
/**
* Filters the post arguments used during an async request.
*
* @param array $url
*/
return apply_filters( $this->identifier . '_query_args', $args );
}
/**
* Get query URL
*
* @return string
*/
protected function get_query_url() {
if ( property_exists( $this, 'query_url' ) ) {
return $this->query_url;
}
$url = admin_url( 'admin-ajax.php' );
/**
* Filters the post arguments used during an async request.
*
* @param string $url
*/
return apply_filters( $this->identifier . '_query_url', $url );
}
/**
* Get post args
*
* @return array
*/
protected function get_post_args() {
if ( property_exists( $this, 'post_args' ) ) {
return $this->post_args;
}
$args = array(
'timeout' => 0.01,
'blocking' => false,
'body' => $this->data,
'cookies' => $_COOKIE,
'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
);
/**
* Filters the post arguments used during an async request.
*
* @param array $args
*/
return apply_filters( $this->identifier . '_post_args', $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' );
$this->handle();
wp_die();
}
/**
* Handle
*
* Override this method to perform any actions required
* during the async request.
*/
abstract protected function handle();
}

View File

@@ -0,0 +1,520 @@
<?php
/**
* WP Background Process
*
* @package WP-Background-Processing
*/
/**
* Abstract WP_Rocket_WP_Background_Process class.
*
* @abstract
* @extends WP_Rocket_WP_Async_Request
*/
abstract class WP_Rocket_WP_Background_Process extends WP_Rocket_WP_Async_Request {
/**
* Action
*
* (default value: 'background_process')
*
* @var string
* @access protected
*/
protected $action = 'background_process';
/**
* Start time of current process.
*
* (default value: 0)
*
* @var int
* @access protected
*/
protected $start_time = 0;
/**
* Cron_hook_identifier
*
* @var mixed
* @access protected
*/
protected $cron_hook_identifier;
/**
* Cron_interval_identifier
*
* @var mixed
* @access protected
*/
protected $cron_interval_identifier;
/**
* Initiate new background process
*/
public function __construct() {
parent::__construct();
$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' ) );
}
/**
* Dispatch
*
* @access public
* @return void
*/
public function dispatch() {
// Schedule the cron healthcheck.
$this->schedule_event();
// Perform remote post.
return parent::dispatch();
}
/**
* Push to queue
*
* @param mixed $data Data.
*
* @return $this
*/
public function push_to_queue( $data ) {
$this->data[] = $data;
return $this;
}
/**
* Save queue
*
* @return $this
*/
public function save() {
$key = $this->generate_key();
if ( ! empty( $this->data ) ) {
update_site_option( $key, $this->data );
}
return $this;
}
/**
* Update queue
*
* @param string $key Key.
* @param array $data Data.
*
* @return $this
*/
public function update( $key, $data ) {
if ( ! empty( $data ) ) {
update_site_option( $key, $data );
}
return $this;
}
/**
* Delete queue
*
* @param string $key Key.
*
* @return $this
*/
public function delete( $key ) {
delete_site_option( $key );
return $this;
}
/**
* Generate key
*
* Generates a unique key based on microtime. Queue items are
* given a unique key so that they can be merged upon save.
*
* @param int $length Length.
*
* @return string
*/
protected function generate_key( $length = 64 ) {
$unique = md5( microtime() . rand() );
$prepend = $this->identifier . '_batch_';
return substr( $prepend . $unique, 0, $length );
}
/**
* 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();
if ( $this->is_process_running() ) {
// Background process already running.
wp_die();
}
if ( $this->is_queue_empty() ) {
// No data to process.
wp_die();
}
check_ajax_referer( $this->identifier, 'nonce' );
$this->handle();
wp_die();
}
/**
* Is queue empty
*
* @return bool
*/
protected function is_queue_empty() {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
if ( is_multisite() ) {
$table = $wpdb->sitemeta;
$column = 'meta_key';
}
$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
$count = $wpdb->get_var( $wpdb->prepare( "
SELECT COUNT(*)
FROM {$table}
WHERE {$column} LIKE %s
", $key ) );
return ( $count > 0 ) ? false : true;
}
/**
* 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->identifier . '_process_lock' ) ) {
// Process already running.
return true;
}
return false;
}
/**
* Is process cancelled
*
* Check whether the current process is cancelled
* in a background process.
*/
protected function is_process_cancelled() {
if ( ! \rocket_direct_filesystem()->exists( WP_ROCKET_CACHE_ROOT_PATH . '.' . $this->identifier . '_process_cancelled' ) ) {
return false;
}
return true;
}
/**
* Lock process
*
* Lock the process so that multiple instances can't run simultaneously.
* Override if applicable, but the duration should be greater than that
* defined in the time_exceeded() method.
*/
protected function lock_process() {
$this->start_time = time(); // Set start time of current process.
$lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
$lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
}
/**
* Unlock process
*
* Unlock the process so that other instances can spawn.
*
* @return $this
*/
protected function unlock_process() {
delete_site_transient( $this->identifier . '_process_lock' );
return $this;
}
/**
* Get batch
*
* @return stdClass Return the first batch from the queue
*/
protected function get_batch() {
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';
}
$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
$query = $wpdb->get_row( $wpdb->prepare( "
SELECT *
FROM {$table}
WHERE {$column} LIKE %s
ORDER BY {$key_column} ASC
LIMIT 1
", $key ) );
$batch = new stdClass();
$batch->key = $query->$column;
$batch->data = maybe_unserialize( $query->$value_column );
return $batch;
}
/**
* Handle
*
* Pass each queue item to the task handler, while remaining
* within server memory and time limit constraints.
*/
protected function handle() {
$this->lock_process();
do {
$batch = $this->get_batch();
foreach ( $batch->data as $key => $value ) {
$task = $this->task( $value );
if ( false !== $task ) {
$batch->data[ $key ] = $task;
} else {
unset( $batch->data[ $key ] );
}
if ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_process_cancelled() ) {
// Batch limits reached.
break;
}
}
// Update or delete current batch.
if ( ! empty( $batch->data ) && ! $this->is_process_cancelled() ) {
$this->update( $batch->key, $batch->data );
} else {
$this->delete( $batch->key );
}
} while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() && ! $this->is_process_cancelled() );
$this->unlock_process();
// Start next batch or complete process.
if ( ! $this->is_queue_empty() ) {
$this->dispatch();
} else {
$this->complete();
}
wp_die();
}
/**
* Memory exceeded
*
* Ensures the batch process never exceeds 90%
* of the maximum WordPress memory.
*
* @return bool
*/
protected function memory_exceeded() {
$memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
$current_memory = memory_get_usage( true );
$return = false;
if ( $current_memory >= $memory_limit ) {
$return = true;
}
return apply_filters( $this->identifier . '_memory_exceeded', $return );
}
/**
* Get memory limit
*
* @return int
*/
protected function get_memory_limit() {
if ( function_exists( 'ini_get' ) ) {
$memory_limit = ini_get( 'memory_limit' );
} else {
// Sensible default.
$memory_limit = '128M';
}
if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
// Unlimited, set to 32GB.
$memory_limit = '32000M';
}
return wp_convert_hr_to_bytes( $memory_limit );
}
/**
* Time exceeded.
*
* Ensures the batch 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 + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
$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() {
// Unschedule the cron healthcheck.
$this->clear_scheduled_event();
\rocket_direct_filesystem()->delete( WP_ROCKET_CACHE_ROOT_PATH . '.' . $this->identifier . '_process_cancelled' );
}
/**
* Schedule cron healthcheck
*
* @param mixed $schedules Schedules.
*
* @return mixed
*/
public function schedule_cron_healthcheck( $schedules ) {
$interval = apply_filters( $this->identifier . '_cron_interval', 5 );
if ( property_exists( $this, 'cron_interval' ) ) {
$interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval );
}
// Adds every 5 minutes to the existing schedules.
$schedules[ $this->identifier . '_cron_interval' ] = array(
'interval' => MINUTE_IN_SECONDS * $interval,
'display' => sprintf( __( 'Every %d Minutes' ), $interval ),
);
return $schedules;
}
/**
* Handle cron healthcheck
*
* Restart the background process if not already running
* and data exists in the queue.
*/
public function handle_cron_healthcheck() {
if ( $this->is_process_running() ) {
// Background process already running.
exit;
}
if ( $this->is_queue_empty() ) {
// No data to process.
$this->clear_scheduled_event();
exit;
}
$this->handle();
exit;
}
/**
* Schedule event
*/
protected function schedule_event() {
if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier );
}
}
/**
* Clear scheduled event
*/
protected function clear_scheduled_event() {
$timestamp = wp_next_scheduled( $this->cron_hook_identifier );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
}
}
/**
* Cancel Process
*
* Stop processing queue items, clear cronjob and delete batch.
*
*/
public function cancel_process() {
if ( ! $this->is_queue_empty() ) {
$batch = $this->get_batch();
$this->delete( $batch->key );
$this->unlock_process();
wp_clear_scheduled_hook( $this->cron_hook_identifier );
\rocket_direct_filesystem()->touch( WP_ROCKET_CACHE_ROOT_PATH . '.' . $this->identifier . '_process_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 $item Queue item to iterate over.
*
* @return mixed
*/
abstract protected function task( $item );
}

View File

@@ -0,0 +1,135 @@
<?php
namespace WP_Rocket\Event_Management;
/**
* The event manager manages events using the WordPress plugin API.
*
* @since 3.1
* @author Carl Alexander <contact@carlalexander.ca>
*/
class Event_Manager {
/**
* Adds a callback to a specific hook of the WordPress plugin API.
*
* @uses add_filter()
*
* @param string $hook_name Name of the hook.
* @param callable $callback Callback function.
* @param int $priority Priority.
* @param int $accepted_args Number of arguments.
*/
public function add_callback( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
add_filter( $hook_name, $callback, $priority, $accepted_args );
}
/**
* Add an event subscriber.
*
* The event manager registers all the hooks that the given subscriber
* wants to register with the WordPress Plugin API.
*
* @param Subscriber_Interface $subscriber Subscriber_Interface implementation.
*/
public function add_subscriber( Subscriber_Interface $subscriber ) {
if ( $subscriber instanceof Event_Manager_Aware_Subscriber_Interface ) {
$subscriber->set_event_manager( $this );
}
$events = $subscriber->get_subscribed_events();
if ( empty( $events ) ) {
return;
}
foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) {
$this->add_subscriber_callback( $subscriber, $hook_name, $parameters );
}
}
/**
* Checks the WordPress plugin API to see if the given hook has
* the given callback. The priority of the callback will be returned
* or false. If no callback is given will return true or false if
* there's any callbacks registered to the hook.
*
* @uses has_filter()
*
* @param string $hook_name Hook name.
* @param mixed $callback Callback.
*
* @return bool|int
*/
public function has_callback( $hook_name, $callback = false ) {
return has_filter( $hook_name, $callback );
}
/**
* Removes the given callback from the given hook. The WordPress plugin API only
* removes the hook if the callback and priority match a registered hook.
*
* @uses remove_filter()
*
* @param string $hook_name Hook name.
* @param callable $callback Callback.
* @param int $priority Priority.
*
* @return bool
*/
public function remove_callback( $hook_name, $callback, $priority = 10 ) {
return remove_filter( $hook_name, $callback, $priority );
}
/**
* Remove an event subscriber.
*
* The event manager removes all the hooks that the given subscriber
* wants to register with the WordPress Plugin API.
*
* @param Subscriber_Interface $subscriber Subscriber_Interface implementation.
*/
public function remove_subscriber( Subscriber_Interface $subscriber ) {
foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) {
$this->remove_subscriber_callback( $subscriber, $hook_name, $parameters );
}
}
/**
* Adds the given subscriber's callback to a specific hook
* of the WordPress plugin API.
*
* @param Subscriber_Interface $subscriber Subscriber_Interface implementation.
* @param string $hook_name Hook name.
* @param mixed $parameters Parameters, can be a string, an array or a multidimensional array.
*/
private function add_subscriber_callback( Subscriber_Interface $subscriber, $hook_name, $parameters ) {
if ( is_string( $parameters ) ) {
$this->add_callback( $hook_name, [ $subscriber, $parameters ] );
} elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) {
foreach ( $parameters as $parameter ) {
$this->add_subscriber_callback( $subscriber, $hook_name, $parameter );
}
} elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) {
$this->add_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10, isset( $parameters[2] ) ? $parameters[2] : 1 );
}
}
/**
* Removes the given subscriber's callback to a specific hook
* of the WordPress plugin API.
*
* @param Subscriber_Interface $subscriber Subscriber_Interface implementation.
* @param string $hook_name Hook name.
* @param mixed $parameters Parameters, can be a string, an array or a multidimensional array.
*/
private function remove_subscriber_callback( Subscriber_Interface $subscriber, $hook_name, $parameters ) {
if ( is_string( $parameters ) ) {
$this->remove_callback( $hook_name, [ $subscriber, $parameters ] );
} elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) {
foreach ( $parameters as $parameter ) {
$this->remove_subscriber_callback( $subscriber, $hook_name, $parameter );
}
} elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) {
$this->remove_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10 );
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace WP_Rocket\Event_Management;
interface Event_Manager_Aware_Subscriber_Interface extends Subscriber_Interface {
/**
* Set the WordPress event manager for the subscriber.
*
* @since 3.1
* @author Remy Perona
*
* @param Event_Manager $event_manager Event_Manager instance.
*/
public function set_event_manager( Event_Manager $event_manager );
}

View File

@@ -0,0 +1,32 @@
<?php
namespace WP_Rocket\Event_Management;
/**
* A Subscriber knows what specific WordPress events it wants to listen to.
*
* When an EventManager adds a Subscriber, it gets all the WordPress events that
* it wants to listen to. It then adds the subscriber as a listener for each of them.
*
* @author Carl Alexander <contact@carlalexander.ca>
*/
interface Subscriber_Interface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* The array key is the event name. The value can be:
*
* * The method name
* * An array with the method name and priority
* * An array with the method name, priority and number of accepted arguments
*
* For instance:
*
* * array('hook_name' => 'method_name')
* * array('hook_name' => array('method_name', $priority))
* * array('hook_name' => array('method_name', $priority, $accepted_args))
* * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2)))
*
* @return array
*/
public static function get_subscribed_events();
}

View File

@@ -0,0 +1,21 @@
<?php
namespace WP_Rocket\Interfaces;
/**
* Render interface
*
* @since 3.0
* @author Remy Perona
*/
interface Render_Interface {
/**
* Renders the given template if it's readable.
*
* @since 3.0
* @author Remy Perona
*
* @param string $template Template slug.
* @param array $data Data to pass to the template.
*/
public function generate( $template, $data );
}

View File

@@ -0,0 +1,58 @@
<?php
namespace WP_Rocket\Logger;
use Monolog\Formatter\HtmlFormatter;
defined( 'ABSPATH' ) || exit;
/**
* Class used to format log records as HTML.
*
* @since 3.2
* @author Grégory Viguier
*/
class HTML_Formatter extends HtmlFormatter {
/**
* Formats a log record.
* Compared to the parent method, it removes the "channel" row.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param array $record A record to format.
* @return mixed The formatted record.
*/
public function format( array $record ) {
$output = $this->addTitle( $record['level_name'], $record['level'] );
$output .= '<table cellspacing="1" width="100%" class="monolog-output">';
$output .= $this->addRow( 'Message', (string) $record['message'] );
$output .= $this->addRow( 'Time', $record['datetime']->format( $this->dateFormat ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( $record['context'] ) {
$embedded_table = '<table cellspacing="1" width="100%">';
foreach ( $record['context'] as $key => $value ) {
$embedded_table .= $this->addRow( $key, $this->convertToString( $value ) );
}
$embedded_table .= '</table>';
$output .= $this->addRow( 'Context', $embedded_table, false );
}
if ( $record['extra'] ) {
$embedded_table = '<table cellspacing="1" width="100%">';
foreach ( $record['extra'] as $key => $value ) {
$embedded_table .= $this->addRow( $key, $this->convertToString( $value ) );
}
$embedded_table .= '</table>';
$output .= $this->addRow( 'Extra', $embedded_table, false );
}
return $output . '</table>';
}
}

View File

@@ -0,0 +1,557 @@
<?php
namespace WP_Rocket\Logger;
use Monolog\Logger as Monologger;
use Monolog\Registry;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Handler\StreamHandler as MonoStreamHandler;
use Monolog\Formatter\LineFormatter;
use WP_Rocket\Logger\HTML_Formatter as HtmlFormatter;
use WP_Rocket\Logger\Stream_Handler as StreamHandler;
defined( 'ABSPATH' ) || exit;
/**
* Class used to log events.
*
* @since 3.1.4
* @since 3.2 Changed namespace from \WP_Rocket to \WP_Rocket\Logger.
* @author Grégory Viguier
*/
class Logger {
/**
* Logger name.
*
* @var string
* @since 3.1.4
* @author Grégory Viguier
*/
const LOGGER_NAME = 'wp_rocket';
/**
* Name of the logs file.
*
* @var string
* @since 3.1.4
* @author Grégory Viguier
*/
const LOG_FILE_NAME = 'wp-rocket-debug.log.html';
/**
* A unique ID given to the current thread.
*
* @var string
* @since 3.3
* @access private
* @author Grégory Viguier
*/
private static $thread_id;
/** ----------------------------------------------------------------------------------------- */
/** LOG ===================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Adds a log record at the DEBUG level.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param string $message The log message.
* @param array $context The log context.
* @return bool|null Whether the record has been processed.
*/
public static function debug( $message, array $context = [] ) {
return static::debug_enabled() ? static::get_logger()->debug( $message, $context ) : null;
}
/**
* Adds a log record at the INFO level.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param string $message The log message.
* @param array $context The log context.
* @return bool|null Whether the record has been processed.
*/
public static function info( $message, array $context = [] ) {
return static::debug_enabled() ? static::get_logger()->info( $message, $context ) : null;
}
/**
* Adds a log record at the NOTICE level.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param string $message The log message.
* @param array $context The log context.
* @return bool|null Whether the record has been processed.
*/
public static function notice( $message, array $context = [] ) {
return static::debug_enabled() ? static::get_logger()->notice( $message, $context ) : null;
}
/**
* Adds a log record at the WARNING level.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param string $message The log message.
* @param array $context The log context.
* @return bool|null Whether the record has been processed.
*/
public static function warning( $message, array $context = [] ) {
return static::debug_enabled() ? static::get_logger()->warning( $message, $context ) : null;
}
/**
* Adds a log record at the ERROR level.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param string $message The log message.
* @param array $context The log context.
* @return bool|null Whether the record has been processed.
*/
public static function error( $message, array $context = [] ) {
return static::debug_enabled() ? static::get_logger()->error( $message, $context ) : null;
}
/**
* Adds a log record at the CRITICAL level.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param string $message The log message.
* @param array $context The log context.
* @return bool|null Whether the record has been processed.
*/
public static function critical( $message, array $context = [] ) {
return static::debug_enabled() ? static::get_logger()->critical( $message, $context ) : null;
}
/**
* Adds a log record at the ALERT level.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param string $message The log message.
* @param array $context The log context.
* @return bool|null Whether the record has been processed.
*/
public static function alert( $message, array $context = [] ) {
return static::debug_enabled() ? static::get_logger()->alert( $message, $context ) : null;
}
/**
* Adds a log record at the EMERGENCY level.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param string $message The log message.
* @param array $context The log context.
* @return bool|null Whether the record has been processed.
*/
public static function emergency( $message, array $context = [] ) {
return static::debug_enabled() ? static::get_logger()->emergency( $message, $context ) : null;
}
/**
* Get the logger instance.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return Logger A Logger instance.
*/
public static function get_logger() {
$logger_name = static::LOGGER_NAME;
$log_level = Monologger::DEBUG;
if ( Registry::hasLogger( $logger_name ) ) {
return Registry::$logger_name();
}
/**
* File handler.
* HTML formatter is used.
*/
$handler = new StreamHandler( static::get_log_file_path(), $log_level );
$formatter = new HtmlFormatter();
$handler->setFormatter( $formatter );
/**
* Thanks to the processors, add data to each log:
* - `debug_backtrace()` (exclude this class and Abstract_Buffer).
*/
$trace_processor = new IntrospectionProcessor( $log_level, [ get_called_class(), 'Abstract_Buffer' ] );
// Create the logger.
$logger = new Monologger( $logger_name, [ $handler ], [ $trace_processor ] );
// Store the logger.
Registry::addLogger( $logger );
return $logger;
}
/** ----------------------------------------------------------------------------------------- */
/** LOG FILE ================================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the path to the log file.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public static function get_log_file_path() {
if ( defined( 'WP_ROCKET_DEBUG_LOG_FILE' ) && WP_ROCKET_DEBUG_LOG_FILE && is_string( WP_ROCKET_DEBUG_LOG_FILE ) ) {
// Make sure the file uses a ".log" extension.
return preg_replace( '/\.[^.]*$/', '', WP_ROCKET_DEBUG_LOG_FILE ) . '.log';
}
return WP_CONTENT_DIR . '/wp-rocket-config/' . static::LOG_FILE_NAME;
}
/**
* Get the log file contents.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return string|object The file contents on success. A WP_Error object on failure.
*/
public static function get_log_file_contents() {
$filesystem = \rocket_direct_filesystem();
$file_path = static::get_log_file_path();
if ( ! $filesystem->exists( $file_path ) ) {
return new \WP_Error( 'no_file', __( 'The log file does not exist.', 'rocket' ) );
}
$contents = $filesystem->get_contents( $file_path );
if ( false === $contents ) {
return new \WP_Error( 'file_not_read', __( 'The log file could not be read.', 'rocket' ) );
}
return $contents;
}
/**
* Get the log file size and number of entries.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return array|object An array of statistics on success. A WP_Error object on failure.
*/
public static function get_log_file_stats() {
$formatter = static::get_stream_formatter();
if ( ! $formatter ) {
return new \WP_Error( 'no_stream_formatter', __( 'The logs are not saved into a file.', 'rocket' ) );
}
$filesystem = \rocket_direct_filesystem();
$file_path = static::get_log_file_path();
if ( ! $filesystem->exists( $file_path ) ) {
return new \WP_Error( 'no_file', __( 'The log file does not exist.', 'rocket' ) );
}
$contents = $filesystem->get_contents( $file_path );
if ( false === $contents ) {
return new \WP_Error( 'file_not_read', __( 'The log file could not be read.', 'rocket' ) );
}
if ( $formatter instanceof HtmlFormatter ) {
$entries = preg_split( '@<h1 @', $contents );
} elseif ( $formatter instanceof LineFormatter ) {
$entries = preg_split( '@^\[\d{4,}-\d{2,}-\d{2,} \d{2,}:\d{2,}:\d{2,}] @m', $contents );
} else {
$entries = 0;
}
$entries = $entries ? number_format_i18n( count( $entries ) ) : '0';
$bytes = $filesystem->size( $file_path );
$decimals = $bytes > pow( 1024, 3 ) ? 1 : 0;
$bytes = @size_format( $bytes, $decimals );
$bytes = str_replace( ' ', ' ', $bytes ); // Non-breaking space character.
return compact( 'entries', 'bytes' );
}
/**
* Get the log file extension related to the formatter in use. This can be used when the file is downloaded.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return string The corresponding file extension with the heading dot.
*/
public static function get_log_file_extension() {
$formatter = static::get_stream_formatter();
if ( ! $formatter ) {
return '.log';
}
if ( $formatter instanceof HtmlFormatter ) {
return '.html';
}
if ( $formatter instanceof LineFormatter ) {
return '.txt';
}
return '.log';
}
/**
* Delete the log file.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return bool True on success. False on failure.
*/
public static function delete_log_file() {
$filesystem = \rocket_direct_filesystem();
$file_path = static::get_log_file_path();
if ( ! $filesystem->exists( $file_path ) ) {
return true;
}
$filesystem->put_contents( $file_path, '' );
$filesystem->delete( $file_path, false, 'f' );
return ! $filesystem->exists( $file_path );
}
/**
* Get the handler used for the log file.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return object|bool The formatter object on success. False on failure.
*/
public static function get_stream_handler() {
$handlers = static::get_logger()->getHandlers();
if ( ! $handlers ) {
return false;
}
foreach ( $handlers as $_handler ) {
if ( $_handler instanceof MonoStreamHandler ) {
$handler = $_handler;
break;
}
}
if ( empty( $handler ) ) {
return false;
}
return $handler;
}
/**
* Get the formatter used for the log file.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return object|bool The formatter object on success. False on failure.
*/
public static function get_stream_formatter() {
$handler = static::get_stream_handler();
if ( empty( $handler ) ) {
return false;
}
return $handler->getFormatter();
}
/** ----------------------------------------------------------------------------------------- */
/** CONSTANT ================================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if debug is enabled.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public static function debug_enabled() {
return defined( 'WP_ROCKET_DEBUG' ) && WP_ROCKET_DEBUG;
}
/**
* Enable debug mode by adding a constant in the `wp-config.php` file.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*/
public static function enable_debug() {
static::define_debug( true );
}
/**
* Disable debug mode by removing the constant in the `wp-config.php` file.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*/
public static function disable_debug() {
static::define_debug( false );
}
/**
* Enable or disable debug mode by adding or removing a constant in the `wp-config.php` file.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param bool $enable True to enable debug, false to disable.
*/
public static function define_debug( $enable ) {
if ( $enable && static::debug_enabled() ) {
// Debug is already enabled.
return;
}
if ( ! $enable && ! static::debug_enabled() ) {
// Debug is already disabled.
return;
}
// Get the path to the file.
$file_path = \rocket_find_wpconfig_path();
if ( ! $file_path ) {
// Couldn't get the path to the file.
return;
}
// Get the content of the file.
$filesystem = \rocket_direct_filesystem();
$content = $filesystem->get_contents( $file_path );
if ( false === $content ) {
// Cound't get the content of the file.
return;
}
// Remove previous value.
$placeholder = '## WP_ROCKET_DEBUG placeholder ##';
$content = preg_replace( '@^[\t ]*define\s*\(\s*["\']WP_ROCKET_DEBUG["\'].*$@miU', $placeholder, $content );
$content = preg_replace( "@\n$placeholder@", '', $content );
if ( $enable ) {
// Add the constant.
$define = "define( 'WP_ROCKET_DEBUG', true ); // Added by WP Rocket.\r\n";
$content = preg_replace( '@<\?php\s*@i', "<?php\n$define", $content, 1 );
}
// Save the file.
$chmod = rocket_get_filesystem_perms( 'file' );
$filesystem->put_contents( $file_path, $content, $chmod );
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the thread identifier.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @return string
*/
public static function get_thread_id() {
if ( ! isset( self::$thread_id ) ) {
self::$thread_id = uniqid( '', true );
}
return self::$thread_id;
}
/**
* Remove cookies related to WP auth.
*
* @since 3.1.4
* @access public
* @author Grégory Viguier
*
* @param array $cookies An array of cookies.
* @return array
*/
public static function remove_auth_cookies( $cookies = [] ) {
if ( ! $cookies || ! is_array( $cookies ) ) {
$cookies = $_COOKIE;
}
unset( $cookies['wordpress_test_cookie'] );
if ( ! $cookies ) {
return [];
}
$pattern = strtolower( '@^WordPress(?:user|pass|_sec|_logged_in)?_@' ); // Trolling PHPCS.
foreach ( $cookies as $cookie_name => $value ) {
if ( preg_match( $pattern, $cookie_name ) ) {
$cookies[ $cookie_name ] = 'Value removed by WP Rocket.';
}
}
return $cookies;
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace WP_Rocket\Logger;
use Monolog\Handler\StreamHandler;
defined( 'ABSPATH' ) || exit;
/**
* Class used to log records into a local file.
*
* @since 3.2
* @author Grégory Viguier
*/
class Stream_Handler extends StreamHandler {
/**
* Tell if the .htaccess file exists.
*
* @var bool
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $htaccess_exists;
/**
* Tell if there is an error.
*
* @var bool
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $has_error;
/**
* Contains an error message.
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $error_message;
/**
* Writes the record down to the log of the implementing handler.
*
* @since 3.2
* @access protected
* @author Grégory Viguier
*
* @param array $record Log contents.
*/
protected function write( array $record ) {
parent::write( $record );
$this->create_htaccess_file();
}
/**
* Create a .htaccess file in the log folder, to prevent direct access and directory listing.
*
* @since 3.2
* @access protected
* @throws \UnexpectedValueException When the .htaccess file could not be created.
* @author Grégory Viguier
*
* @return bool True if the file exists or has been created. False on failure.
*/
public function create_htaccess_file() {
if ( $this->htaccess_exists ) {
return true;
}
if ( $this->has_error ) {
return false;
}
$dir = $this->get_dir_from_stream( $this->url );
if ( ! $dir || ! is_dir( $dir ) ) {
$this->has_error = true;
return false;
}
$file_path = $dir . '/.htaccess';
if ( file_exists( $file_path ) ) {
$this->htaccess_exists = true;
return true;
}
$this->error_message = null;
set_error_handler( [ $this, 'custom_error_handler' ] ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
$file_resource = fopen( $file_path, 'a' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
restore_error_handler();
if ( ! is_resource( $file_resource ) ) {
$this->has_error = true;
throw new \UnexpectedValueException( sprintf( 'The file "%s" could not be opened: ' . $this->error_message, $file_path ) );
}
$new_content = "<Files ~ \"\.log$\">\nOrder allow,deny\nDeny from all\n</Files>\nOptions -Indexes";
fwrite( $file_resource, $new_content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
fclose( $file_resource ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
@chmod( $file_path, 0644 );
$this->htaccess_exists = true;
return true;
}
/**
* Temporary error handler that "cleans" the error messages.
*
* @since 3.2
* @access private
* @see parent::customErrorHandler()
* @author Grégory Viguier
*
* @param int $code Error code.
* @param string $msg Error message.
*/
private function custom_error_handler( $code, $msg ) {
$this->error_message = preg_replace( '{^(fopen|mkdir)\(.*?\): }', '', $msg );
}
/**
* A dirname() that also works for streams, by removing the protocol.
*
* @since 3.2
* @access private
* @see parent::getDirFromStream()
* @author Grégory Viguier
*
* @param string $stream Path to a file.
* @return null|string
*/
private function get_dir_from_stream( $stream ) {
$pos = strpos( $stream, '://' );
if ( false === $pos ) {
return dirname( $stream );
}
if ( 'file://' === substr( $stream, 0, 7 ) ) {
return dirname( substr( $stream, 7 ) );
}
}
}

View File

@@ -0,0 +1,226 @@
<?php
namespace WP_Rocket\Subscriber\Cache;
use WP_Rocket\Admin\Options_Data;
use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Cache\Expired_Cache_Purge;
/**
* Event subscriber to clear cached files after lifespan.
*
* @since 3.4
* @author Grégory Viguier
*/
class Expired_Cache_Purge_Subscriber implements Subscriber_Interface {
/**
* Cron name.
*
* @since 3.4
* @author Grégory Viguier
*
* @var string
*/
const EVENT_NAME = 'rocket_purge_time_event';
/**
* WP Rocket Options instance.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @var Options_Data
*/
private $options;
/**
* Expired Cache Purge instance.
*
* @since 3.4
* @access private
* @author Remy Perona
*
* @var Expired_Cache_Purge
*/
private $purge;
/**
* Constructor.
*
* @param Options_Data $options Options instance.
* @param Expired_Cache_Purge $purge Purge instance.
*/
public function __construct( Options_Data $options, Expired_Cache_Purge $purge ) {
$this->options = $options;
$this->purge = $purge;
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'init' => 'schedule_event',
'rocket_deactivation' => 'unschedule_event',
static::EVENT_NAME => 'purge_expired_files',
'cron_schedules' => 'custom_cron_schedule',
'update_option_' . WP_ROCKET_SLUG => [ 'clean_expired_cache_scheduled_event', 10, 2 ],
];
}
/**
* Clean expired cache scheduled event when Lifespan is changed to minutes.
*
* @since 3.4.3
* @author Soponar Cristina
*
* @param array $old_value An array of previous values for the settings.
* @param array $value An array of submitted values for the settings.
*/
public function clean_expired_cache_scheduled_event( $old_value, $value ) {
if ( empty( $value['purge_cron_unit'] ) ) {
return;
}
$old_value['purge_cron_unit'] = isset( $old_value['purge_cron_unit'] ) ? $old_value['purge_cron_unit'] : '';
$unit_list = [ 'HOUR_IN_SECONDS', 'DAY_IN_SECONDS' ];
// Bail out if the cron unit is changed from hours to days.
// Allow clean scheduled event when is changed from Minutes to Hours or Days, or the other way around.
$allow_clear_event = false;
if ( in_array( $old_value['purge_cron_unit'], $unit_list, true ) && 'MINUTE_IN_SECONDS' === $value['purge_cron_unit'] ) {
$allow_clear_event = true;
}
if ( in_array( $value['purge_cron_unit'], $unit_list, true ) && 'MINUTE_IN_SECONDS' === $old_value['purge_cron_unit'] ) {
$allow_clear_event = true;
}
// Allow if interval is changed when unit is set to minutes.
if (
'MINUTE_IN_SECONDS' === $old_value['purge_cron_unit']
&&
'MINUTE_IN_SECONDS' === $value['purge_cron_unit']
&&
$old_value['purge_cron_interval'] !== $value['purge_cron_interval']
) {
$allow_clear_event = true;
}
// Bail out if the cron unit is not changed from minutes to hours / days or other way around.
if ( ! $allow_clear_event ) {
return;
}
$this->unschedule_event();
}
/**
* Adds a custom cron schedule based on purge lifespan interval.
*
* @since 3.4.3
* @access public
* @author Soponar Cristina
*
* @param array $schedules An array of non-default cron schedules.
*/
public function custom_cron_schedule( $schedules ) {
$schedules['rocket_expired_cache_cron_interval'] = [
'interval' => $this->get_interval(),
'display' => __( 'WP Rocket Expired Cache Interval', 'rocket' ),
];
return $schedules;
}
/** ----------------------------------------------------------------------------------------- */
/** HOOK CALLBACKS ========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Scheduling the cron event.
* If the task is not programmed, it is automatically added.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function schedule_event() {
if ( $this->get_cache_lifespan() && ! wp_next_scheduled( static::EVENT_NAME ) ) {
$interval = $this->get_interval();
wp_schedule_event( time() + $interval, 'rocket_expired_cache_cron_interval', static::EVENT_NAME );
}
}
/**
* Gets the interval when the scheduled clean cache purge needs to run.
* If Minutes option is selected, then the interval will be set to minutes.
* If Hours / Days options are selected, then it will be set to 1 hour.
*
* @since 3.4.3
* @access private
* @author Soponar Cristina
*
* @return int $interval Interval time in seconds.
*/
private function get_interval() {
$unit = $this->options->get( 'purge_cron_unit' );
$lifespan = $this->options->get( 'purge_cron_interval', 10 );
$interval = HOUR_IN_SECONDS;
if ( ! $unit || ! defined( $unit ) ) {
$unit = 'HOUR_IN_SECONDS';
}
if ( 'MINUTE_IN_SECONDS' === $unit ) {
$interval = $lifespan * MINUTE_IN_SECONDS;
}
return $interval;
}
/**
* Unschedule the event.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function unschedule_event() {
wp_clear_scheduled_hook( static::EVENT_NAME );
}
/**
* Perform the event action.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function purge_expired_files() {
$this->purge->purge_expired_files( $this->get_cache_lifespan() );
}
/**
* Get the cache lifespan in seconds.
* If no value is filled in the settings, return 0. It means the purge is disabled.
* If the value from the settings is filled but invalid, fallback to the initial value (10 hours).
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return int The cache lifespan in seconds.
*/
public function get_cache_lifespan() {
$lifespan = $this->options->get( 'purge_cron_interval' );
if ( ! $lifespan ) {
return 0;
}
$unit = $this->options->get( 'purge_cron_unit' );
if ( $lifespan < 0 || ! $unit || ! defined( $unit ) ) {
return 10 * HOUR_IN_SECONDS;
}
return $lifespan * constant( $unit );
}
}

View File

@@ -0,0 +1,847 @@
<?php
namespace WP_Rocket\Subscriber\Media;
use WP_Rocket\Admin\Options;
use WP_Rocket\Admin\Options_Data;
use WP_Rocket\Engine\Admin\Beacon\Beacon;
use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Engine\CDN\Subscriber as CDNSubscriber;
/**
* Subscriber for the WebP support.
*
* @since 3.4
* @author Remy Perona
* @author Grégory Viguier
*/
class Webp_Subscriber implements Subscriber_Interface {
/**
* Options_Data instance.
*
* @var Options_Data
* @access private
* @author Remy Perona
*/
private $options_data;
/**
* Options instance.
*
* @var Options
* @access private
* @author Grégory Viguier
*/
private $options_api;
/**
* CDNSubscriber instance.
*
* @var CDNSubscriber
* @access private
* @author Grégory Viguier
*/
private $cdn_subscriber;
/**
* Beacon instance
*
* @var Beacon
* @access private
* @author Grégory Viguier
*/
private $beacon;
/**
* Values of $_SERVER to use for some tests.
*
* @var array
* @access private
* @author Grégory Viguier
*/
private $server;
/**
* \WP_Filesystem_Direct instance.
*
* @var \WP_Filesystem_Direct
* @access private
* @author Grégory Viguier
*/
private $filesystem;
/**
* Constructor.
*
* @since 3.4
* @access public
* @author Remy Perona
*
* @param Options_Data $options_data Options_Data instance.
* @param Options $options_api Options instance.
* @param CDNSubscriber $cdn_subscriber CDNSubscriber instance.
* @param Beacon $beacon Beacon instance.
* @param array $server Values of $_SERVER to use for the tests. Default is $_SERVER.
*/
public function __construct( Options_Data $options_data, Options $options_api, CDNSubscriber $cdn_subscriber, Beacon $beacon, $server = null ) {
$this->options_data = $options_data;
$this->options_api = $options_api;
$this->cdn_subscriber = $cdn_subscriber;
$this->beacon = $beacon;
if ( ! isset( $server ) && ! empty( $_SERVER ) && is_array( $_SERVER ) ) {
$server = $_SERVER;
}
$this->server = $server && is_array( $server ) ? $server : [];
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'rocket_buffer' => [ 'convert_to_webp', 16 ],
'rocket_cache_webp_setting_field' => [
[ 'maybe_disable_setting_field' ],
[ 'webp_section_description' ],
],
'rocket_disable_webp_cache' => 'maybe_disable_webp_cache',
'rocket_third_party_webp_change' => 'sync_webp_cache_with_third_party_plugins',
'rocket_homepage_preload_url_request_args' => 'add_accept_header',
'rocket_preload_after_purge_cache_request_args' => 'add_accept_header',
'rocket_preload_url_request_args' => 'add_accept_header',
'rocket_partial_preload_url_request_args' => 'add_accept_header',
];
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Converts images extension to WebP if the file exists.
*
* @since 3.4
* @access public
* @author Remy Perona
* @author Grégory Viguier
*
* @param string $html HTML content.
* @return string
*/
public function convert_to_webp( $html ) {
if ( ! $this->options_data->get( 'cache_webp' ) ) {
return $html;
}
/** This filter is documented in inc/classes/buffer/class-cache.php */
if ( apply_filters( 'rocket_disable_webp_cache', false ) ) {
return $html;
}
// Only to supporting browsers.
$http_accept = isset( $this->server['HTTP_ACCEPT'] ) ? $this->server['HTTP_ACCEPT'] : '';
if ( ! $http_accept && function_exists( 'apache_request_headers' ) ) {
$headers = apache_request_headers();
$http_accept = isset( $headers['Accept'] ) ? $headers['Accept'] : '';
}
if ( ! $http_accept || false === strpos( $http_accept, 'webp' ) ) {
$user_agent = isset( $this->server['HTTP_USER_AGENT'] ) ? $this->server['HTTP_USER_AGENT'] : '';
if ( $user_agent && preg_match( '#Firefox/(?<version>[0-9]{2,})#i', $this->server['HTTP_USER_AGENT'], $matches ) ) {
if ( 66 >= (int) $matches['version'] ) {
return $html;
}
} else {
return $html;
}
}
$extensions = $this->get_extensions();
$attribute_names = $this->get_attribute_names();
if ( ! $extensions || ! $attribute_names ) {
return $html . '<!-- Rocket no webp -->';
}
$extensions = implode( '|', $extensions );
$attribute_names = implode( '|', $attribute_names );
if ( ! preg_match_all( '@["\'\s](?<name>(?:data-(?:[a-z0-9_-]+-)?)?(?:' . $attribute_names . '))\s*=\s*["\']\s*(?<value>(?:https?:/)?/[^"\']+\.(?:' . $extensions . ')[^"\']*?)\s*["\']@is', $html, $attributes, PREG_SET_ORDER ) ) {
return $html . '<!-- Rocket no webp -->';
}
if ( ! isset( $this->filesystem ) ) {
$this->filesystem = \rocket_direct_filesystem();
}
$has_hebp = false;
foreach ( $attributes as $attribute ) {
if ( preg_match( '@srcset$@i', strtolower( $attribute['name'] ) ) ) {
/**
* This is a srcset attribute, with probably multiple URLs.
*/
$new_value = $this->srcset_to_webp( $attribute['value'], $extensions );
} else {
/**
* A single URL attibute.
*/
$new_value = $this->url_to_webp( $attribute['value'], $extensions );
}
if ( ! $new_value ) {
// No webp here.
continue;
}
// Replace in content.
$has_hebp = true;
$new_attr = preg_replace( '@' . $attribute['name'] . '\s*=\s*["\'][^"\']+["\']@s', $attribute['name'] . '="' . $new_value . '"', $attribute[0] );
$html = str_replace( $attribute[0], $new_attr, $html );
}
/**
* Tell if the page contains webp files.
*
* @since 3.4
* @author Grégory Viguier
*
* @param bool $has_hebp True if the page contains webp files. False otherwise.
* @param string $html The pages html contents.
*/
$has_hebp = apply_filters( 'rocket_page_has_hebp_files', $has_hebp, $html );
// Tell the cache process if some URLs have been replaced.
if ( $has_hebp ) {
$html .= '<!-- Rocket has webp -->';
} else {
$html .= '<!-- Rocket no webp -->';
}
return $html;
}
/**
* Modifies the WebP section description of WP Rocket settings.
*
* @since 3.4
* @access public
* @author Remy Perona
* @author Grégory Viguier
*
* @param array $cache_webp_field Section description.
* @return string
*/
public function webp_section_description( $cache_webp_field ) {
$webp_beacon = $this->beacon->get_suggest( 'webp' );
$webp_plugins = $this->get_webp_plugins();
$serving = [];
$serving_not_compatible = [];
$creating = [];
if ( $webp_plugins ) {
$is_using_cdn = $this->is_using_cdn();
foreach ( $webp_plugins as $plugin ) {
if ( $plugin->is_serving_webp() ) {
if ( $is_using_cdn && ! $plugin->is_serving_webp_compatible_with_cdn() ) {
// Serving WebP using a method not compatible with CDN.
$serving_not_compatible[ $plugin->get_id() ] = $plugin->get_name();
} else {
// Serving WebP when no CDN or with a method compatible with CDN.
$serving[ $plugin->get_id() ] = $plugin->get_name();
}
}
if ( $plugin->is_converting_to_webp() ) {
// Generating WebP.
$creating[ $plugin->get_id() ] = $plugin->get_name();
}
}
}
if ( $serving ) {
// 5, 8.
$cache_webp_field['helper'] = sprintf(
// Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag.
esc_html( _n( 'You are using %1$s to serve WebP images so you do not need to enable this option. If you prefer to have WP Rocket serve WebP for you instead, please disable them from serving in %1$s. %2$sMore info%3$s', 'You are using %1$s to serve WebP images so you do not need to enable this option. If you prefer to have WP Rocket serve WebP for you instead, please disable them from serving in %1$s. %2$sMore info%3$s', count( $serving ), 'rocket' ) ),
esc_html( wp_sprintf_l( '%l', $serving ) ),
'<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
);
return $cache_webp_field;
}
/** This filter is documented in inc/classes/buffer/class-cache.php */
if ( apply_filters( 'rocket_disable_webp_cache', false ) ) {
$cache_webp_field['helper'] = esc_html__( 'WebP cache is disabled by filter.', 'rocket' );
return $cache_webp_field;
}
if ( $serving_not_compatible ) {
if ( ! $this->options_data->get( 'cache_webp' ) ) {
// 6.
$cache_webp_field['helper'] = sprintf(
// Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag.
esc_html( _n( 'You are using %1$s to convert images to WebP. If you want WP Rocket to serve them for you, activate this option. %2$sMore info%3$s', 'You are using %1$s to convert images to WebP. If you want WP Rocket to serve them for you, activate this option. %2$sMore info%3$s', count( $serving_not_compatible ), 'rocket' ) ),
esc_html( wp_sprintf_l( '%l', $serving_not_compatible ) ),
'<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
);
return $cache_webp_field;
}
// 7.
$cache_webp_field['helper'] = sprintf(
// Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag.
esc_html( _n( 'You are using %1$s to convert images to WebP. WP Rocket will create separate cache files to serve your WebP images. %2$sMore info%3$s', 'You are using %1$s to convert images to WebP. WP Rocket will create separate cache files to serve your WebP images. %2$sMore info%3$s', count( $serving_not_compatible ), 'rocket' ) ),
esc_html( wp_sprintf_l( '%l', $serving_not_compatible ) ),
'<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
);
return $cache_webp_field;
}
if ( $creating ) {
if ( ! $this->options_data->get( 'cache_webp' ) ) {
// 3.
$cache_webp_field['helper'] = sprintf(
// Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag.
esc_html( _n( 'You are using %1$s to convert images to WebP. If you want WP Rocket to serve them for you, activate this option. %2$sMore info%3$s', 'You are using %1$s to convert images to WebP. If you want WP Rocket to serve them for you, activate this option. %2$sMore info%3$s', count( $creating ), 'rocket' ) ),
esc_html( wp_sprintf_l( '%l', $creating ) ),
'<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
);
return $cache_webp_field;
}
// 4.
$cache_webp_field['helper'] = sprintf(
// Translators: %1$s = plugin name(s), %2$s = opening <a> tag, %3$s = closing </a> tag.
esc_html( _n( 'You are using %1$s to convert images to WebP. WP Rocket will create separate cache files to serve your WebP images. %2$sMore info%3$s', 'You are using %1$s to convert images to WebP. WP Rocket will create separate cache files to serve your WebP images. %2$sMore info%3$s', count( $creating ), 'rocket' ) ),
esc_html( wp_sprintf_l( '%l', $creating ) ),
'<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
);
return $cache_webp_field;
}
if ( ! $this->options_data->get( 'cache_webp' ) ) {
// 1.
if ( rocket_valid_key() && ! \Imagify_Partner::has_imagify_api_key() ) {
$imagify_link = '<a href="#imagify">';
} else {
// The Imagify page is not displayed.
$imagify_link = '<a href="https://wordpress.org/plugins/imagify/" target="_blank" rel="noopener noreferrer">';
}
$cache_webp_field['container_class'][] = 'wpr-field--parent';
$cache_webp_field['helper'] = sprintf(
// Translators: %1$s = opening <a> tag, %2$s = closing </a> tag.
esc_html__( 'You dont seem to be using a method to create and serve WebP that we are auto-compatible with. If you are not using WebP do not enable this option. %1$sMore info%2$s', 'rocket' ),
'<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
);
$cache_webp_field['warning'] = [
'title' => __( 'We have not detected any compatible WebP plugin!', 'rocket' ),
'description' => sprintf(
// Translators: %1$s and %2$s = opening <a> tags, %3$s = closing </a> tag.
esc_html__( 'If you activate this option WP Rocket will create separate cache files to serve WebP images. Any WebP images you have on your site will be served from these files to compatible browsers. If you dont already have WebP images on your site consider using %1$sImagify%3$s or another supported plugin. %2$sMore info%3$s', 'rocket' ),
$imagify_link,
'<a href="' . esc_url( $webp_beacon['url'] ) . '" data-beacon-article="' . esc_attr( $webp_beacon['id'] ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
),
'button_label' => esc_html__( 'Enable WebP caching', 'rocket' ),
];
return $cache_webp_field;
}
// 2.
$cache_webp_field['helper'] = esc_html__( 'WP Rocket will create separate cache files to serve your WebP images.', 'rocket' );
return $cache_webp_field;
}
/**
* Disable 'cache_webp' setting field if another plugin serves WebP.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param array $cache_webp_field Data to be added to the setting field.
* @return array
*/
public function maybe_disable_setting_field( $cache_webp_field ) {
/** This filter is documented in inc/classes/buffer/class-cache.php */
if ( ! apply_filters( 'rocket_disable_webp_cache', false ) ) {
return $cache_webp_field;
}
foreach ( [ 'input_attr', 'container_class' ] as $attr ) {
if ( ! isset( $cache_webp_field[ $attr ] ) || ! is_array( $cache_webp_field[ $attr ] ) ) {
$cache_webp_field[ $attr ] = [];
}
}
$cache_webp_field['input_attr']['disabled'] = 1;
$cache_webp_field['container_class'][] = 'wpr-isDisabled';
$cache_webp_field['container_class'][] = 'wpr-isParent';
return $cache_webp_field;
}
/**
* Disable the WebP cache if a WebP plugin is in use.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param bool $disable_webp_cache True to allow WebP cache (default). False otherwise.
* @return bool
*/
public function maybe_disable_webp_cache( $disable_webp_cache ) {
return ! $disable_webp_cache && $this->get_plugins_serving_webp() ? true : (bool) $disable_webp_cache;
}
/**
* When a 3rd party plugin enables or disables its webp feature, disable or enable WPR feature accordingly.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function sync_webp_cache_with_third_party_plugins() {
if ( $this->options_data->get( 'cache_webp' ) && $this->get_plugins_serving_webp() ) {
// Disable the cache webp option.
$this->options_data->set( 'cache_webp', 0 );
$this->options_api->set( 'settings', $this->options_data->get_options() );
}
rocket_generate_config_file();
}
/**
* Add WebP to the HTTP_ACCEPT headers on preload request when the WebP option is active
*
* @since 3.4
* @author Remy Perona
*
* @param array $args Arguments for the request.
* @return array
*/
public function add_accept_header( $args ) {
if ( ! $this->options_data->get( 'cache_webp' ) ) {
return $args;
}
$args['headers']['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8';
$args['headers']['HTTP_ACCEPT'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8';
return $args;
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the list of file extensions that may have a webp version.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return array
*/
private function get_extensions() {
$extensions = [ 'jpg', 'jpeg', 'jpe', 'png', 'gif' ];
/**
* Filter the list of file extensions that may have a webp version.
*
* @since 3.4
* @author Grégory Viguier
*
* @param array $extensions An array of file extensions.
*/
$extensions = apply_filters( 'rocket_file_extensions_for_webp', $extensions );
$extensions = array_filter(
(array) $extensions,
function( $extension ) {
return $extension && is_string( $extension );
}
);
return array_unique( $extensions );
}
/**
* Get the names of the HTML attributes where WP Rocket must search for image files.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return array
*/
private function get_attribute_names() {
$attributes = [ 'href', 'src', 'srcset', 'content' ];
/**
* Filter the names of the HTML attributes where WP Rocket must search for image files.
* Don't prepend new names with `data-`, WPR will do it. For example if you want to add `data-foo-bar`, you only need to add `foo-bar` or `bar` to the list.
*
* @since 3.4
* @author Grégory Viguier
*
* @param array $attributes An array of HTML attribute names.
*/
$attributes = apply_filters( 'rocket_attributes_for_webp', $attributes );
$attributes = array_filter(
(array) $attributes,
function( $attributes ) {
return $attributes && is_string( $attributes );
}
);
return array_unique( $attributes );
}
/**
* Convert a URL to an absolute path.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @param string $url URL to convert.
* @return string|bool
*/
private function url_to_path( $url ) {
static $hosts, $site_host, $subdir_levels;
$url_host = wp_parse_url( $url, PHP_URL_HOST );
// Relative path.
if ( null === $url_host ) {
if ( ! isset( $subdir_levels ) ) {
$subdir_levels = substr_count( preg_replace( '@^https?://@', '', site_url() ), '/' );
}
if ( $subdir_levels ) {
$url = ltrim( $url, '/' );
$url = explode( '/', $url );
array_splice( $url, 0, $subdir_levels );
$url = implode( '/', $url );
}
$url = site_url( $url );
}
// CDN.
if ( ! isset( $hosts ) ) {
$hosts = $this->cdn_subscriber->get_cdn_hosts( [], [ 'all', 'images' ] );
$hosts = array_flip( $hosts );
}
if ( isset( $hosts[ $url_host ] ) ) {
if ( ! isset( $site_host ) ) {
$site_host = wp_parse_url( site_url( '/' ), PHP_URL_HOST );
}
if ( $site_host ) {
$url = preg_replace( '@^(https?://)' . $url_host . '/@', '$1' . $site_host . '/', $url );
}
}
// URL to path.
$url = preg_replace( '@^https?:@', '', $url );
$paths = $this->get_url_to_path_associations();
if ( ! $paths ) {
// Uh?
return false;
}
foreach ( $paths as $asso_url => $asso_path ) {
if ( 0 === strpos( $url, $asso_url ) ) {
$file = str_replace( $asso_url, $asso_path, $url );
break;
}
}
if ( empty( $file ) ) {
return false;
}
/** This filter is documented in inc/functions/formatting.php. */
return (string) apply_filters( 'rocket_url_to_path', $file, $url );
}
/**
* Add a webp extension to a URL.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @param string $url A URL (I see you're very surprised).
* @param string $extensions Allowed image extensions.
* @return string|bool The same URL with a webp extension if the file exists. False if the webp image doesn't exist.
*/
private function url_to_webp( $url, $extensions ) {
if ( ! preg_match( '@^(?<src>.+\.(?<extension>' . $extensions . '))(?<query>(?:\?.*)?)$@i', $url, $src_url ) ) {
// Probably something like "image.jpg.webp".
return false;
}
$src_path = $this->url_to_path( $src_url['src'] );
if ( ! $src_path ) {
return false;
}
$src_path_webp = preg_replace( '@\.' . $src_url['extension'] . '$@', '.webp', $src_path );
if ( $this->filesystem->exists( $src_path_webp ) ) {
// File name: image.jpg => image.webp.
return preg_replace( '@\.' . $src_url['extension'] . '$@', '.webp', $src_url['src'] ) . $src_url['query'];
}
if ( $this->filesystem->exists( $src_path . '.webp' ) ) {
// File name: image.jpg => image.jpg.webp.
return $src_url['src'] . '.webp' . $src_url['query'];
}
return false;
}
/**
* Add webp extension to URLs in a srcset attribute.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @param array|string $srcset_values Value of a srcset attribute.
* @param string $extensions Allowed image extensions.
* @return string|bool An array similar to $srcset_values, with webp extensions when the files exist. False if no images have webp versions.
*/
private function srcset_to_webp( $srcset_values, $extensions ) {
if ( ! $srcset_values ) {
return false;
}
if ( ! is_array( $srcset_values ) ) {
$srcset_values = explode( ',', $srcset_values );
}
$has_webp = false;
foreach ( $srcset_values as $i => $srcset_value ) {
$srcset_value = preg_split( '/\s+/', trim( $srcset_value ) );
if ( count( $srcset_value ) > 2 ) {
// Not a good idea to have space characters in file name.
$descriptor = array_pop( $srcset_value );
$srcset_value = [
'url' => implode( ' ', $srcset_value ),
'descriptor' => $descriptor,
];
} else {
$srcset_value = [
'url' => $srcset_value[0],
'descriptor' => ! empty( $srcset_value[1] ) ? $srcset_value[1] : '1x',
];
}
$url_webp = $this->url_to_webp( $srcset_value['url'], $extensions );
if ( ! $url_webp ) {
$srcset_values[ $i ] = implode( ' ', $srcset_value );
continue;
}
$srcset_values[ $i ] = $url_webp . ' ' . $srcset_value['descriptor'];
$has_webp = true;
}
if ( ! $has_webp ) {
return false;
}
return implode( ',', $srcset_values );
}
/**
* Get a list of URL/path associations.
* URLs are schema-less, starting by a double slash.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return array A list of URLs as keys and paths as values.
*/
private function get_url_to_path_associations() {
static $list;
if ( isset( $list ) ) {
return $list;
}
$content_url = preg_replace( '@^https?:@', '', content_url( '/' ) );
$content_dir = trailingslashit( rocket_get_constant( 'WP_CONTENT_DIR' ) );
$list = [ $content_url => $content_dir ];
/**
* Filter the list of URL/path associations.
* The URLs with the most levels must come first.
*
* @since 3.4
* @author Grégory Viguier
*
* @param array $list The list of URL/path associations. URLs are schema-less, starting by a double slash.
*/
$list = apply_filters( 'rocket_url_to_path_associations', $list );
$list = array_filter(
$list,
function( $path, $url ) {
return $path && $url && is_string( $path ) && is_string( $url );
},
ARRAY_FILTER_USE_BOTH
);
if ( $list ) {
$list = array_unique( $list );
}
return $list;
}
/**
* Get a list of plugins that serve webp images on frontend.
* If the CDN is used, this won't list plugins that use a technique not compatible with CDN.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return array The WebP plugin names.
*/
private function get_plugins_serving_webp() {
$webp_plugins = $this->get_webp_plugins();
if ( ! $webp_plugins ) {
// Somebody probably messed up.
return [];
}
$checks = [];
$is_using_cdn = $this->is_using_cdn();
foreach ( $webp_plugins as $plugin ) {
if ( $is_using_cdn && $plugin->is_serving_webp_compatible_with_cdn() ) {
$checks[ $plugin->get_id() ] = $plugin->get_name();
} elseif ( ! $is_using_cdn && $plugin->is_serving_webp() ) {
$checks[ $plugin->get_id() ] = $plugin->get_name();
}
}
return $checks;
}
/**
* Get a list of active plugins that convert and/or serve webp images.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return array An array of Webp_Interface objects.
*/
private function get_webp_plugins() {
/**
* Add Webp plugins.
*
* @since 3.4
* @author Grégory Viguier
*
* @param array $webp_plugins An array of Webp_Interface objects.
*/
$webp_plugins = (array) apply_filters( 'rocket_webp_plugins', [] );
if ( ! $webp_plugins ) {
// Somebody probably messed up.
return [];
}
foreach ( $webp_plugins as $i => $plugin ) {
if ( ! is_a( $plugin, '\WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp\Webp_Interface' ) ) {
unset( $webp_plugins[ $i ] );
continue;
}
if ( ! $this->is_plugin_active( $plugin->get_basename() ) ) {
unset( $webp_plugins[ $i ] );
continue;
}
}
return $webp_plugins;
}
/**
* Tell if a plugin is active.
*
* @since 3.4
* @access public
* @see \plugin_basename()
* @author Grégory Viguier
*
* @param string $plugin_basename A plugin basename.
* @return bool
*/
private function is_plugin_active( $plugin_basename ) {
if ( \doing_action( 'deactivate_' . $plugin_basename ) ) {
return false;
}
if ( \doing_action( 'activate_' . $plugin_basename ) ) {
return true;
}
return \rocket_is_plugin_active( $plugin_basename );
}
/**
* Tell if WP Rocket uses a CDN for images.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return bool
*/
private function is_using_cdn() {
// Don't use `$this->options_data->get( 'cdn' )` here, we need an up-to-date value when the CDN option changes.
$use = get_rocket_option( 'cdn' ) && $this->cdn_subscriber->get_cdn_hosts( [], [ 'all', 'images' ] );
/**
* Filter whether WP Rocket is using a CDN for webp images.
*
* @since 3.4
* @author Grégory Viguier
*
* @param bool $use True if WP Rocket is using a CDN for webp images. False otherwise.
*/
return (bool) apply_filters( 'rocket_webp_is_using_cdn', $use );
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WP_Rocket\Subscriber\Optimization;
use WP_Rocket\Buffer\Optimization;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Event subscriber to buffer and process a page content.
*
* @since 3.3
* @author Grégory Viguier
*/
class Buffer_Subscriber implements Subscriber_Interface {
/**
* Optimization instance
*
* @var Optimization
*/
private $optimizer;
/**
* Constructor
*
* @param Optimization $optimizer Optimization instance.
*/
public function __construct( Optimization $optimizer ) {
$this->optimizer = $optimizer;
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'template_redirect' => [ 'start_content_process', 2 ],
];
}
/**
* Start buffering the page content and apply optimizations if we can.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*/
public function start_content_process() {
return $this->optimizer->maybe_init_process();
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace WP_Rocket\Subscriber\Optimization;
use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Admin\Options_Data as Options;
use WP_Scripts;
/**
* Dequeue jQuery Migrate
*
* @since 3.5
* @author Soponar Cristina
*/
class Dequeue_JQuery_Migrate_Subscriber implements Subscriber_Interface {
/**
* Plugin options
*
* @since 3.5
* @author Soponar Cristina
*
* @var Options
*/
private $options;
/**
* Constructor
*
* @since 3.5
* @author Soponar Cristina
*
* @param Options $options Plugin options.
*/
public function __construct( Options $options ) {
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'wp_default_scripts' => [ 'dequeue_jquery_migrate' ],
];
}
/**
* Dequeue jquery migrate
*
* @since 3.5
* @author Soponar Cristina
*
* @param WP_Scripts $scripts WP_Scripts instance.
* @return bool|void
*/
public function dequeue_jquery_migrate( $scripts ) {
if ( ! $this->is_allowed() ) {
return false;
}
if ( ! empty( $scripts->registered['jquery'] ) ) {
$jquery_dependencies = $scripts->registered['jquery']->deps;
$scripts->registered['jquery']->deps = array_diff( $jquery_dependencies, [ 'jquery-migrate' ] );
}
}
/**
* Check if dequeue jquery migrate option is enabled
*
* @since 3.5
* @author Soponar Cristina
*
* @return boolean
*/
protected function is_allowed() {
if ( rocket_get_constant( 'DONOTROCKETOPTIMIZE', false ) ) {
return false;
}
if ( ! $this->options->get( 'dequeue_jquery_migrate' ) ) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace WP_Rocket\Subscriber\Plugin;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Manages the plugin information.
*
* @since 3.3.6
* @author Grégory Viguier
*/
class Information_Subscriber implements Subscriber_Interface {
use \WP_Rocket\Traits\Updater_Api_Tools;
/**
* Plugin slug.
*
* @var string
* @since 3.3.6
* @access private
* @author Grégory Viguier
*/
private $plugin_slug;
/**
* URL to contact to get plugin info.
*
* @var string
* @since 3.3.6
* @access private
* @author Grégory Viguier
*/
private $api_url;
/**
* An ID to use when a API request fails.
*
* @var string
* @since 3.3.6
* @access protected
* @author Grégory Viguier
*/
protected $request_error_id = 'plugins_api_failed';
/**
* Constructor
*
* @since 3.3.6
* @access public
* @author Grégory Viguier
*
* @param array $args {
* Required arguments to populate the class properties.
*
* @type string $plugin_file Full path to the plugin.
* @type string $api_url URL to contact to get update info.
* }
*/
public function __construct( $args ) {
if ( isset( $args['plugin_file'] ) ) {
$this->plugin_slug = $this->get_plugin_slug( $args['plugin_file'] );
}
if ( isset( $args['api_url'] ) ) {
$this->api_url = $args['api_url'];
}
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'plugins_api' => [ 'exclude_rocket_from_wp_info', 10, 3 ],
'plugins_api_result' => [ 'add_rocket_info', 10, 3 ],
];
}
/** ----------------------------------------------------------------------------------------- */
/** PLUGIN INFO ============================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Dont ask for plugin info to the repository.
*
* @since 3.3.6
* @access public
* @see plugins_api()
* @author Grégory Viguier
*
* @param false|object|array $bool The result object or array. Default false.
* @param string $action The type of information being requested from the Plugin Install API.
* @param object $args Plugin API arguments.
* @return false|object|array Empty object if slug is WP Rocket, default value otherwise.
*/
public function exclude_rocket_from_wp_info( $bool, $action, $args ) {
if ( ! $this->is_requesting_rocket_info( $action, $args ) ) {
return $bool;
}
return new \stdClass();
}
/**
* Insert WP Rocket plugin info.
*
* @since 3.3.6
* @access public
* @see plugins_api()
* @author Grégory Viguier
*
* @param object|\WP_Error $res Response object or WP_Error.
* @param string $action The type of information being requested from the Plugin Install API.
* @param object $args Plugin API arguments.
* @return object|\WP_Error Updated response object or WP_Error.
*/
public function add_rocket_info( $res, $action, $args ) {
if ( ! $this->is_requesting_rocket_info( $action, $args ) || empty( $res->external ) ) {
return $res;
}
$request = wp_remote_post(
$this->api_url,
[
'timeout' => 30,
'action' => 'plugin_information',
'request' => maybe_serialize( $args ),
]
);
if ( is_wp_error( $request ) ) {
return $this->get_request_error( $request->get_error_message() );
}
$res = maybe_unserialize( wp_remote_retrieve_body( $request ) );
$code = wp_remote_retrieve_response_code( $request );
if ( 200 !== $code || ! ( is_object( $res ) || is_array( $res ) ) ) {
return $this->get_request_error( wp_remote_retrieve_body( $request ) );
}
return $res;
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if requesting WP Rocket plugin info.
*
* @since 3.3.6
* @access private
* @author Grégory Viguier
*
* @param string $action The type of information being requested from the Plugin Install API.
* @param object $args Plugin API arguments.
* @return bool
*/
private function is_requesting_rocket_info( $action, $args ) {
return ( 'query_plugins' === $action || 'plugin_information' === $action ) && isset( $args->slug ) && $args->slug === $this->plugin_slug;
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace WP_Rocket\Subscriber\Plugin;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Manages common hooks for the plugin updater.
*
* @since 3.3.6
* @author Grégory Viguier
*/
class Updater_Api_Common_Subscriber implements Subscriber_Interface {
/**
* APIs URL domain.
*
* @var string
* @since 3.3.6
* @access private
* @author Grégory Viguier
*/
private $api_host;
/**
* URL to the sites home.
*
* @var string
* @since 3.3.6
* @access private
* @author Grégory Viguier
*/
private $site_url;
/**
* Current version of the plugin.
*
* @var string
* @since 3.3.6
* @access private
* @author Grégory Viguier
*/
private $plugin_version;
/**
* Key slug used when submitting new settings (POST).
*
* @var string
* @since 3.3.6
* @access private
* @author Grégory Viguier
*/
private $settings_slug;
/**
* The key (1st part of the action) used for the nonce field used on the settings page. It is also used in the page URL.
*
* @var string
* @since 3.3.6
* @access private
* @author Grégory Viguier
*/
private $settings_nonce_key;
/**
* Options instance.
*
* @var \WP_Rocket\Admin\Options
* @since 3.3.6
* @access private
* @author Grégory Viguier
*/
private $plugin_options;
/**
* Constructor
*
* @since 3.3.6
* @access public
* @author Grégory Viguier
*
* @param array $args {
* Required arguments to populate the class properties.
*
* @type string $api_host APIs URL domain.
* @type string $site_url URL to the sites home.
* @type string $plugin_version Current version of the plugin.
* @type string $settings_slug Key slug used when submitting new settings (POST).
* @type string $settings_nonce_key The key (1st part of the action) used for the nonce field used on the settings page. It is also used in the page URL.
* @type Options $plugin_options Options instance.
* }
*/
public function __construct( $args ) {
foreach ( [ 'api_host', 'site_url', 'plugin_version', 'settings_slug', 'settings_nonce_key', 'plugin_options' ] as $setting ) {
if ( isset( $args[ $setting ] ) ) {
$this->$setting = $args[ $setting ];
}
}
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'http_request_args' => [ 'maybe_set_rocket_user_agent', 10, 2 ],
];
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Force our user agent header when we hit our URLs.
*
* @since 3.3.6
* @access public
*
* @param array $request An array of request arguments.
* @param string $url Requested URL.
* @return array An array of requested arguments
*/
public function maybe_set_rocket_user_agent( $request, $url ) {
if ( ! is_string( $url ) ) {
return $request;
}
if ( $this->api_host && strpos( $url, $this->api_host ) !== false ) {
$request['user-agent'] = sprintf( '%s;%s', $request['user-agent'], $this->get_rocket_user_agent() );
}
return $request;
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the user agent to use when requesting the API.
*
* @since 3.3.6
* @access protected
* @author Grégory Viguier
*
* @return string WP Rocket user agent
*/
public function get_rocket_user_agent() {
$consumer_key = $this->get_current_option( 'consumer_key' );
$consumer_email = $this->get_current_option( 'consumer_email' );
$bonus = $this->plugin_options && $this->plugin_options->get( 'do_beta' ) ? '+' : '';
$php_version = preg_replace( '@^(\d+\.\d+).*@', '\1', phpversion() );
return sprintf( 'WP-Rocket|%s%s|%s|%s|%s|%s;', $this->plugin_version, $bonus, $consumer_key, $consumer_email, esc_url( $this->site_url ), $php_version );
}
/**
* Get a plugin option. If the value is currently being posted through the settings page, it is returned instead of the one stored in the database.
*
* @since 3.3.6
* @access protected
* @author Grégory Viguier
*
* @param string $field_name Name of a plugin option.
* @return string
*/
protected function get_current_option( $field_name ) {
if ( current_user_can( 'rocket_manage_options' ) && wp_verify_nonce( filter_input( INPUT_POST, '_wpnonce' ), $this->settings_nonce_key . '-options' ) ) {
$posted = filter_input( INPUT_POST, $this->settings_slug, FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
if ( ! empty( $posted[ $field_name ] ) && is_string( $posted[ $field_name ] ) ) {
// The value has been posted through the settings page.
return sanitize_text_field( $posted[ $field_name ] );
}
}
if ( ! $this->plugin_options ) {
return '';
}
$option_value = $this->plugin_options->get( $field_name );
if ( $option_value && is_string( $option_value ) ) {
return $option_value;
}
return '';
}
}

View File

@@ -0,0 +1,435 @@
<?php
namespace WP_Rocket\Subscriber\Plugin;
use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Traits\Updater_Api_Tools;
/**
* Manages the plugin updates.
*
* @since 3.3.6
*/
class Updater_Subscriber implements Subscriber_Interface {
use Updater_Api_Tools;
/**
* Full path to the plugin.
*
* @var string
* @since 3.3.6
*/
private $plugin_file;
/**
* Current version of the plugin.
*
* @var string
* @since 3.3.6
*/
private $plugin_version;
/**
* URL to the plugin provider.
*
* @var string
* @since 3.3.6
*/
private $vendor_url;
/**
* URL to contact to get update info.
*
* @var string
* @since 3.3.6
*/
private $api_url;
/**
* A list of plugins icon URLs.
*
* @var array {
* @type string $2x URL to the High-DPI size (png or jpg). Optional.
* @type string $1x URL to the normal icon size (png or jpg). Mandatory.
* @type string $svg URL to the svg version of the icon. Optional.
* }
* @since 3.3.6
* @see https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons
*/
private $icons;
/**
* An ID to use when a API request fails.
*
* @var string
* @since 3.3.6
*/
protected $request_error_id = 'rocket_update_failed';
/**
* Name of the transient that caches the update data.
*
* @var string
* @since 3.3.6
*/
protected $cache_transient_name = 'wp_rocket_update_data';
/**
* Constructor
*
* @since 3.3.6
*
* @param array $args {
* Required arguments to populate the class properties.
*
* @type string $plugin_file Full path to the plugin.
* @type string $plugin_version Current version of the plugin.
* @type string $vendor_url URL to the plugin provider.
* @type string $api_url URL to contact to get update info.
* }
*/
public function __construct( $args ) {
foreach ( [ 'plugin_file', 'plugin_version', 'vendor_url', 'api_url', 'icons' ] as $setting ) {
if ( isset( $args[ $setting ] ) ) {
$this->$setting = $args[ $setting ];
}
}
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'http_request_args' => [ 'exclude_rocket_from_wp_updates', 5, 2 ],
'pre_set_site_transient_update_plugins' => 'maybe_add_rocket_update_data',
'deleted_site_transient' => 'maybe_delete_rocket_update_data_cache',
'wp_rocket_loaded' => 'maybe_force_check',
'auto_update_plugin' => [ 'disable_auto_updates', 10, 2 ],
];
}
/** ----------------------------------------------------------------------------------------- */
/** PLUGIN UPDATE DATA ====================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* When WP checks plugin versions against the latest versions hosted on WordPress.org, remove WPR from the list.
*
* @since 3.3.6
* @see wp_update_plugins()
*
* @param array $request An array of HTTP request arguments.
* @param string $url The request URL.
* @return array Updated array of HTTP request arguments.
*/
public function exclude_rocket_from_wp_updates( $request, $url ) {
if ( ! is_string( $url ) ) {
return $request;
}
if ( ! preg_match( '@^https?://api.wordpress.org/plugins/update-check(/|\?|$)@', $url ) || empty( $request['body']['plugins'] ) ) {
// Not a plugin update request. Stop immediately.
return $request;
}
/**
* Depending on the API version, the data can have several forms:
* - Can be serialized or JSON encoded,
* - Can be an object of arrays or an object of objects.
*/
$is_serialized = is_serialized( $request['body']['plugins'] );
$basename = plugin_basename( $this->plugin_file );
$edited = false;
if ( $is_serialized ) {
$plugins = maybe_unserialize( $request['body']['plugins'] );
} else {
$plugins = json_decode( $request['body']['plugins'] );
}
if ( ! empty( $plugins->plugins ) ) {
if ( is_object( $plugins->plugins ) ) {
if ( isset( $plugins->plugins->$basename ) ) {
unset( $plugins->plugins->$basename );
$edited = true;
}
} elseif ( is_array( $plugins->plugins ) ) {
if ( isset( $plugins->plugins[ $basename ] ) ) {
unset( $plugins->plugins[ $basename ] );
$edited = true;
}
}
}
if ( ! empty( $plugins->active ) ) {
$active_is_object = is_object( $plugins->active );
if ( $active_is_object || is_array( $plugins->active ) ) {
foreach ( $plugins->active as $key => $plugin_basename ) {
if ( $plugin_basename !== $basename ) {
continue;
}
if ( $active_is_object ) {
unset( $plugins->active->$key );
} else {
unset( $plugins->active[ $key ] );
}
$edited = true;
break;
}
}
}
if ( $edited ) {
if ( $is_serialized ) {
$request['body']['plugins'] = maybe_serialize( $plugins );
} else {
$request['body']['plugins'] = wp_json_encode( $plugins );
}
}
return $request;
}
/**
* Add WPR update data to the "WP update" transient.
*
* @since 3.3.6
*
* @param \stdClass $transient_value New value of site transient.
* @return \stdClass
*/
public function maybe_add_rocket_update_data( $transient_value ) {
if ( defined( 'WP_INSTALLING' ) ) {
return $transient_value;
}
// Get the remote version data.
$remote_data = $this->get_cached_latest_version_data();
if ( is_wp_error( $remote_data ) ) {
return $transient_value;
}
// Make sure the transient value is well formed.
if ( ! is_object( $transient_value ) ) {
$transient_value = new \stdClass();
}
if ( empty( $transient_value->response ) ) {
$transient_value->response = [];
}
if ( empty( $transient_value->checked ) ) {
$transient_value->checked = [];
}
// If a newer version is available, add the update.
if ( version_compare( $this->plugin_version, $remote_data->new_version, '<' ) ) {
$transient_value->response[ $remote_data->plugin ] = $remote_data;
}
$transient_value->checked[ $remote_data->plugin ] = $this->plugin_version;
return $transient_value;
}
/**
* Delete WPR update data cache when the "WP update" transient is deleted.
*
* @since 3.3.6
*
* @param string $transient_name Deleted transient name.
*/
public function maybe_delete_rocket_update_data_cache( $transient_name ) {
if ( 'update_plugins' === $transient_name ) {
$this->delete_rocket_update_data_cache();
}
}
/**
* If the `rocket_force_update` query arg is set, force WP to refresh the list of plugins to update.
*
* @since 3.3.6
*/
public function maybe_force_check() {
if ( is_string( filter_input( INPUT_GET, 'rocket_force_update' ) ) ) {
delete_site_transient( 'update_plugins' );
}
}
/**
* Disable auto-updates for WP Rocket
*
* @since 3.7.5
*
* @param bool|null $update Whether to update. The value of null is internally used to detect whether nothing has hooked into this filter.
* @param object $item The update offer.
* @return bool|null
*/
public function disable_auto_updates( $update, $item ) {
if ( 'wp-rocket/wp-rocket.php' === $item->plugin ) {
return false;
}
return $update;
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the latest WPR update data from our server.
*
* @since 3.3.6
*
* @return \stdClass|\WP_Error {
* A \WP_Error object on failure. An object on success:
*
* @type string $slug The plugin slug.
* @type string $plugin The plugin base name.
* @type string $new_version The plugin new version.
* @type string $url URL to the plugin provider.
* @type string $package URL to the zip file of the new version.
* @type array $icons {
* A list of plugins icon URLs.
*
* @type string $2x URL to the High-DPI size (png or jpg). Optional.
* @type string $1x URL to the normal icon size (png or jpg). Mandatory.
* @type string $svg URL to the svg version of the icon. Optional.
* }
* }
*/
public function get_latest_version_data() {
$request = wp_remote_get(
$this->api_url,
[
'timeout' => 30,
]
);
if ( is_wp_error( $request ) ) {
return $this->get_request_error(
[
'error_code' => $request->get_error_code(),
'response' => $request->get_error_message(),
]
);
}
$res = trim( wp_remote_retrieve_body( $request ) );
$code = wp_remote_retrieve_response_code( $request );
if ( 200 !== $code ) {
/**
* If the response doesnt have a status 200: it is an error, or there is no new update.
*/
return $this->get_request_error(
[
'http_code' => $code,
'response' => $res,
]
);
}
/**
* This will match:
* - `2.3.4.5-beta1||1.2.3.4-beta2||||||||||||||||||||||||||||||||`: expired license.
* - `2.3.4.5-beta1|https://wp-rocket.me/i-should-write-a-funny-thing-here/wp-rocket_1.2.3.4-beta2.zip|1.2.3.4-beta2`: valid license.
*/
if ( ! preg_match( '@^(?<stable_version>\d+(?:\.\d+){1,3}[^|]*)\|(?<package>(?:http.+\.zip)?)\|(?<user_version>\d+(?:\.\d+){1,3}[^|]*)(?:\|+)?$@', $res, $match ) ) {
/**
* If the response doesnt have the right format, it is an error.
*/
return $this->get_request_error( $res );
}
$obj = new \stdClass();
$obj->slug = $this->get_plugin_slug( $this->plugin_file );
$obj->plugin = plugin_basename( $this->plugin_file );
$obj->new_version = $match['user_version'];
$obj->url = $this->vendor_url;
$obj->package = $match['package'];
$obj->tested = WP_ROCKET_WP_VERSION_TESTED;
if ( $this->icons && ! empty( $this->icons['1x'] ) ) {
$obj->icons = $this->icons;
}
return $obj;
}
/**
* Get the cached version of the latest WPR update data.
*
* @since 3.3.6
*
* @return \stdClass|\WP_Error {
* A \WP_Error object on failure. An object on success:
*
* @type string $slug The plugin slug.
* @type string $plugin The plugin base name.
* @type string $new_version The plugin new version.
* @type string $url URL to the plugin provider.
* @type string $package URL to the zip file of the new version.
* @type array $icons {
* A list of plugins icon URLs.
*
* @type string $2x URL to the High-DPI size (png or jpg). Optional.
* @type string $1x URL to the normal icon size (png or jpg). Mandatory.
* @type string $svg URL to the svg version of the icon. Optional.
* }
* }
*/
public function get_cached_latest_version_data() {
static $response;
if ( isset( $response ) ) {
// "force update" wont bypass the static cache: only one http request by page load.
return $response;
}
$force_update = is_string( filter_input( INPUT_GET, 'rocket_force_update' ) );
if ( ! $force_update ) {
// No "force update": try to get the result from a transient.
$response = get_site_transient( $this->cache_transient_name );
if ( $response && is_object( $response ) ) {
// Got something in cache.
return $response;
}
}
// Get fresh data.
$response = $this->get_latest_version_data();
$cache_duration = 12 * HOUR_IN_SECONDS;
if ( is_wp_error( $response ) ) {
$error_data = $response->get_error_data();
if ( ! empty( $error_data['error_code'] ) ) {
// `wp_remote_get()` returned an internal error ('error_code' contains a WP_Error code ).
$cache_duration = HOUR_IN_SECONDS;
} elseif ( ! empty( $error_data['http_code'] ) && $error_data['http_code'] >= 400 ) {
// We got a 4xx or 5xx HTTP error.
$cache_duration = 2 * HOUR_IN_SECONDS;
}
}
set_site_transient( $this->cache_transient_name, $response, $cache_duration );
return $response;
}
/**
* Delete WP Rocket update data cache.
*
* @since 3.3.6
*/
public function delete_rocket_update_data_cache() {
delete_site_transient( $this->cache_transient_name );
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace WP_Rocket\Subscriber\Tools;
use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Logger\Logger;
/**
* Detect and report when <html>, wp_footer() and <body> tags are missing.
*
* @since 3.4.2
* @author Soponar Cristina
*/
class Detect_Missing_Tags_Subscriber implements Subscriber_Interface {
/**
* Return an array of events that this subscriber wants to listen to.
*
* @since 3.4.2
* @author Soponar Cristina
*
* @return array
*/
public static function get_subscribed_events() {
return [
'admin_notices' => 'rocket_notice_missing_tags',
'rocket_before_maybe_process_buffer' => 'maybe_missing_tags',
'wp_rocket_upgrade' => 'delete_transient_after_upgrade',
];
}
/**
* Check if there is a missing </html> or </body> tag
*
* @since 3.4.2
* @author Soponar Cristina
*
* @param string $html HTML content.
*/
public function maybe_missing_tags( $html ) {
// If there is a redirect the content is empty and can display a false positive notice.
if ( strlen( $html ) <= 255 ) {
return;
}
// If the http response is not 200 do not report missing tags.
if ( http_response_code() !== 200 ) {
return;
}
// If content type is not HTML do not report missing tags.
if ( empty( $_SERVER['content_type'] ) || false === strpos( wp_unslash( $_SERVER['content_type'] ), 'text/html' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return;
}
// If the content does not contain HTML Doctype, do not report missing tags.
if ( false === stripos( $html, '<!DOCTYPE html' ) ) {
return;
}
Logger::info(
'START Detect Missing closing tags ( <html>, </body> or wp_footer() )',
[
'maybe_missing_tags',
'URI' => $this->get_raw_request_uri(),
]
);
// Remove all comments before testing tags. If </html> or </body> tags are commented this will identify it as a missing tag.
$html = preg_replace( '/<!--([\\s\\S]*?)-->/', '', $html );
$missing_tags = [];
if ( false === strpos( $html, '</html>' ) ) {
$missing_tags[] = '</html>';
Logger::debug(
'Not found closing </html> tag.',
[
'maybe_missing_tags',
'URI' => $this->get_raw_request_uri(),
]
);
}
if ( false === strpos( $html, '</body>' ) ) {
$missing_tags[] = '</body>';
Logger::debug(
'Not found closing </body> tag.',
[
'maybe_missing_tags',
'URI' => $this->get_raw_request_uri(),
]
);
}
if ( did_action( 'wp_footer' ) === 0 ) {
$missing_tags[] = 'wp_footer()';
Logger::debug(
'wp_footer() function did not run.',
[
'maybe_missing_tags',
'URI' => $this->get_raw_request_uri(),
]
);
}
if ( ! $missing_tags ) {
return;
}
$transient = get_transient( 'rocket_notice_missing_tags' );
$transient = is_array( $transient ) ? $transient : [];
$missing_tags = array_unique( array_merge( $transient, $missing_tags ) );
if ( count( $transient ) === count( $missing_tags ) ) {
return;
}
// Prevent saving the transient if the notice is dismissed.
$boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true );
if ( in_array( 'rocket_notice_missing_tags', (array) $boxes, true ) ) {
return;
}
set_transient( 'rocket_notice_missing_tags', $missing_tags );
}
/**
* This notice is displayed if there is a missing required tag or function: </html>, </body> or wp_footer()
*
* @since 3.4.2
* @author Soponar Cristina
*/
public function rocket_notice_missing_tags() {
$screen = get_current_screen();
if ( ! current_user_can( 'rocket_manage_options' ) || 'settings_page_wprocket' !== $screen->id ) {
return;
}
$boxes = get_user_meta( get_current_user_id(), 'rocket_boxes', true );
if ( in_array( __FUNCTION__, (array) $boxes, true ) ) {
return;
}
$notice = get_transient( 'rocket_notice_missing_tags' );
if ( empty( $notice ) || ! is_array( $notice ) ) {
return;
}
foreach ( $notice as $i => $tag ) {
$notice[ $i ] = '<code>' . esc_html( $tag ) . '</code>';
}
$msg = '<b>' . __( 'WP Rocket: ', 'rocket' ) . '</b>';
$msg .= sprintf(
/* translators: %1$s = missing tags; */
esc_html( _n( 'Failed to detect the following requirement in your theme: closing %1$s.', 'Failed to detect the following requirements in your theme: closing %1$s.', count( $notice ), 'rocket' ) ),
// translators: Documentation exists in EN, FR.
wp_sprintf_l( '%l', $notice )
);
$msg .= ' ' . sprintf(
/* translators: %1$s = opening link; %2$s = closing link */
__( 'Read the %1$sdocumentation%2$s for further guidance.', 'rocket' ),
// translators: Documentation exists in EN, FR; use localized URL if applicable.
'<a href="' . esc_url( __( 'https://docs.wp-rocket.me/article/99-pages-not-cached-or-minify-cssjs-not-working/?utm_source=wp_plugin&utm_medium=wp_rocket#theme', 'rocket' ) ) . '" rel="noopener noreferrer" target="_blank">',
'</a>'
);
\rocket_notice_html(
[
'status' => 'info',
'dismissible' => '',
'message' => $msg,
'dismiss_button' => __FUNCTION__,
]
);
}
/**
* Get the request URI.
*
* @since 3.4.2
* @author Soponar Cristina
*
* @return string
*/
public function get_raw_request_uri() {
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return '';
}
if ( '' === $_SERVER['REQUEST_URI'] ) {
return '';
}
return '/' . esc_html( ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
/**
* Deletes the transient storing the missing tags when updating the plugin
*
* @since 3.4.2.2
* @author Soponar Cristina
*/
public function delete_transient_after_upgrade() {
delete_transient( 'rocket_notice_missing_tags' );
}
}

View File

@@ -0,0 +1,265 @@
<?php
namespace WP_Rocket\Subscriber\Admin\Database;
use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Admin\Database\Optimization;
use WP_Rocket\Admin\Options_Data;
defined( 'ABSPATH' ) || exit;
/**
* Subscriber for the database optimization
*
* @since 3.3
* @author Remy Perona
*/
class Optimization_Subscriber implements Subscriber_Interface {
/**
* Optimization process instance.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @var Optimization
*/
private $optimize;
/**
* WP Rocket Options instance.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @var Options_Data
*/
private $options;
/**
* Constructor
*
* @param Optimization $optimize Optimize instance.
* @param Options_Data $options WP Rocket options.
*/
public function __construct( Optimization $optimize, Options_Data $options ) {
$this->optimize = $optimize;
$this->options = $options;
}
/**
* Return an array of events that this subscriber wants to listen to.
*
* @since 3.3
* @author Remy Perona
*
* @return array
*/
public static function get_subscribed_events() {
return [
'cron_schedules' => 'add_cron_schedule',
'init' => 'database_optimization_scheduled',
'rocket_database_optimization_time_event' => 'cron_optimize',
'pre_update_option_' . WP_ROCKET_SLUG => 'save_optimize',
'admin_notices' => [
[ 'notice_process_running' ],
[ 'notice_process_complete' ],
],
];
}
/**
* Add a new interval for the cron job.
* This adds a weekly/monthly interval for database optimization.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param array $schedules An array of intervals used by cron jobs.
* @return array Updated array of intervals.
*/
public function add_cron_schedule( $schedules ) {
if ( ! $this->options->get( 'schedule_automatic_cleanup', false ) ) {
return $schedules;
}
switch ( $this->options->get( 'automatic_cleanup_frequency', 'weekly' ) ) {
case 'weekly':
$schedules['weekly'] = [
'interval' => 604800,
'display' => __( 'weekly', 'rocket' ),
];
break;
case 'monthly':
$schedules['monthly'] = [
'interval' => 2592000,
'display' => __( 'monthly', 'rocket' ),
];
break;
}
return $schedules;
}
/**
* Plans database optimization cron
* If the task is not programmed, it is automatically triggered
*
* @since 2.8
* @author Remy Perona
*
* @see process_handler()
*/
public function database_optimization_scheduled() {
if ( ! $this->options->get( 'schedule_automatic_cleanup', false ) ) {
return;
}
if ( ! wp_next_scheduled( 'rocket_database_optimization_time_event' ) ) {
wp_schedule_event( time(), $this->options->get( 'automatic_cleanup_frequency', 'weekly' ), 'rocket_database_optimization_time_event' );
}
}
/**
* Database Optimization cron callback
*
* @since 3.0.4
* @author Remy Perona
*/
public function cron_optimize() {
$items = array_filter( array_keys( $this->optimize->get_options() ), [ $this->options, 'get' ] );
if ( empty( $items ) ) {
return;
}
$this->optimize->process_handler( $items );
}
/**
* Launches the database optimization when the settings are saved with optimize button
*
* @since 2.8
* @author Remy Perona
*
* @see process_handler()
*
* @param array $value The new, unserialized option value.
* @return array
*/
public function save_optimize( $value ) {
if ( empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
return $value;
}
if ( empty( $value ) || ! isset( $value['submit_optimize'] ) ) {
return $value;
}
unset( $value['submit_optimize'] );
if ( ! current_user_can( 'rocket_manage_options' ) ) {
return $value;
}
$items = [];
$db_options = $this->optimize->get_options();
foreach ( $value as $key => $option_value ) {
if ( isset( $db_options[ $key ] ) && 1 === $option_value ) {
$items[] = $key;
}
}
if ( empty( $items ) ) {
return $value;
}
$this->optimize->process_handler( $items );
return $value;
}
/**
* This notice is displayed after launching the database optimization process
*
* @since 2.11
* @author Remy Perona
*/
public function notice_process_running() {
$screen = get_current_screen();
if ( ! current_user_can( 'rocket_manage_options' ) ) {
return;
}
if ( 'settings_page_wprocket' !== $screen->id ) {
return;
}
$notice = get_transient( 'rocket_database_optimization_process' );
if ( ! $notice ) {
return;
}
\rocket_notice_html(
[
'status' => 'info',
'message' => esc_html__( 'Database optimization process is running', 'rocket' ),
]
);
}
/**
* This notice is displayed when the database optimization process is complete
*
* @since 2.11
* @author Remy Perona
*/
public function notice_process_complete() {
$screen = get_current_screen();
if ( ! current_user_can( 'rocket_manage_options' ) ) {
return;
}
if ( 'settings_page_wprocket' !== $screen->id ) {
return;
}
$optimized = get_transient( 'rocket_database_optimization_process_complete' );
if ( false === $optimized ) {
return;
}
$db_options = $this->optimize->get_options();
delete_transient( 'rocket_database_optimization_process_complete' );
$message = esc_html__( 'Database optimization process is complete. Everything was already optimized!', 'rocket' );
if ( ! empty( $optimized ) ) {
$message = esc_html__( 'Database optimization process is complete. List of optimized items below:', 'rocket' );
}
if ( ! empty( $optimized ) ) {
$message .= '<ul>';
foreach ( $optimized as $key => $number ) {
$message .= '<li>' .
/* translators: %1$d = number of items optimized, %2$s = type of optimization */
sprintf( esc_html__( '%1$d %2$s optimized.', 'rocket' ), $number, $db_options[ $key ] )
. '</li>';
}
$message .= '</ul>';
}
\rocket_notice_html(
[
'message' => $message,
]
);
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Hostings;
use WP_Rocket\Logger\Logger;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Subscriber for compatibility with Litespeed
*
* @since 3.4.1
* @author Soponar Cristina
*/
class Litespeed_Subscriber implements Subscriber_Interface {
/**
* Subscribed events for Litespeed.
*
* @since 3.4.1
* @author Soponar Cristina
* @inheritDoc
*/
public static function get_subscribed_events() {
if ( ! isset( $_SERVER['HTTP_X_LSCACHE'] ) ) {
return [];
}
return [
'before_rocket_clean_domain' => 'litespeed_clean_domain',
'before_rocket_clean_file' => 'litespeed_clean_file',
'before_rocket_clean_home' => [ 'litespeed_clean_home', 10, 2 ],
];
}
/**
* Purge Litespeed all domain.
*
* @since 3.4.1
* @author Soponar Cristina
*/
public function litespeed_clean_domain() {
$this->litespeed_header_purge_all();
}
/**
* Purge a specific page
*
* @since 3.4.1
* @author Soponar Cristina
*
* @param string $url The url to purge.
*/
public function litespeed_clean_file( $url ) {
$this->litespeed_header_purge_url( trailingslashit( $url ) );
}
/**
* Purge the homepage and its pagination
*
* @since 3.4.1
* @author Soponar Cristina
*
* @param string $root The path of home cache file.
* @param string $lang The current lang to purge.
*/
public function litespeed_clean_home( $root, $lang ) {
$home_url = trailingslashit( get_rocket_i18n_home_url( $lang ) );
$home_pagination_url = $home_url . trailingslashit( $GLOBALS['wp_rewrite']->pagination_base );
$this->litespeed_header_purge_url( $home_url );
$this->litespeed_header_purge_url( $home_pagination_url );
}
/**
* Purge Litespeed URL
*
* @since 3.4.1
* @author Soponar Cristina
*
* @param string $url The URL to purge.
* @return void
*/
public function litespeed_header_purge_url( $url ) {
if ( headers_sent() ) {
Logger::debug(
'X-LiteSpeed Headers already sent',
[ 'headers_sent' ]
);
return;
}
$parse_url = get_rocket_parse_url( $url );
$path = rtrim( $parse_url['path'], '/' );
$private_prefix = 'X-LiteSpeed-Purge: ' . $path;
Logger::debug(
'X-LiteSpeed',
[
'litespeed_header_purge_url',
'path' => $private_prefix,
]
);
@header( $private_prefix );
}
/**
* Purge Litespeed Cache
*
* @since 3.4.1
* @author Soponar Cristina
*
* @return void
*/
public function litespeed_header_purge_all() {
if ( headers_sent() ) {
return;
}
$private_prefix = 'X-LiteSpeed-Purge: *';
@header( $private_prefix );
}
}

View File

@@ -0,0 +1,390 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp;
use WP_Rocket\Admin\Options_Data;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Subscriber for the WebP support with EWWW.
*
* @since 3.4
* @author Grégory Viguier
*/
class EWWW_Subscriber implements Webp_Interface, Subscriber_Interface {
use Webp_Common;
/**
* Options_Data instance.
*
* @var Options_Data
* @access private
* @author Remy Perona
*/
private $options;
/**
* EWWW basename.
*
* @var string
* @access private
* @author Grégory Viguier
*/
private $plugin_basename;
/**
* Constructor.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param Options_Data $options Options instance.
*/
public function __construct( Options_Data $options ) {
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'rocket_webp_plugins' => 'register',
'wp_rocket_loaded' => 'load_hooks',
];
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Launch filters.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function load_hooks() {
if ( ! $this->options->get( 'cache_webp' ) ) {
return;
}
/**
* Every time EWWW is (de)activated, we must "sync" our webp cache option.
*/
if ( did_action( 'activate_' . $this->get_basename() ) ) {
$this->plugin_activation();
}
if ( did_action( 'deactivate_' . $this->get_basename() ) ) {
$this->plugin_deactivation();
}
add_action( 'activate_' . $this->get_basename(), [ $this, 'plugin_activation' ], 20 );
add_action( 'deactivate_' . $this->get_basename(), [ $this, 'plugin_deactivation' ], 20 );
if ( ! function_exists( 'ewww_image_optimizer_get_option' ) ) {
return;
}
/**
* Since Rocket already updates the config file after updating its options, there is no need to do it again if the CDN or zone options change.
* Sadly, we cant monitor EWWW options accurately to update our config file.
*/
add_filter( 'rocket_cdn_cnames', [ $this, 'maybe_remove_images_cnames' ], 1000, 2 );
add_filter( 'rocket_allow_cdn_images', [ $this, 'maybe_remove_images_from_cdn_dropdown' ] );
$option_names = [
'ewww_image_optimizer_exactdn',
'ewww_image_optimizer_webp_for_cdn',
];
foreach ( $option_names as $option_name ) {
if ( $this->is_active_for_network() ) {
add_filter( 'add_site_option_' . $option_name, [ $this, 'trigger_webp_change' ] );
add_filter( 'update_site_option_' . $option_name, [ $this, 'trigger_webp_change' ] );
add_filter( 'delete_site_option_' . $option_name, [ $this, 'trigger_webp_change' ] );
} else {
add_filter( 'add_option_' . $option_name, [ $this, 'trigger_webp_change' ] );
add_filter( 'update_option_' . $option_name, [ $this, 'trigger_webp_change' ] );
add_filter( 'delete_option_' . $option_name, [ $this, 'trigger_webp_change' ] );
}
}
}
/**
* Remove CDN hosts for images if EWWW uses ExactDN.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param array $hosts List of CDN URLs.
* @param array $zones List of zones. Default is [ 'all' ].
* @return array
*/
public function maybe_remove_images_cnames( $hosts, $zones ) {
if ( ! $hosts ) {
return $hosts;
}
if ( ! ewww_image_optimizer_get_option( 'ewww_image_optimizer_exactdn' ) ) {
return $hosts;
}
// EWWW uses ExactDN: WPR CDN should be disabled for images.
if ( ! in_array( 'images', $zones, true ) ) {
// Not asking for images.
return $hosts;
}
if ( ! array_diff( $zones, [ 'all', 'images' ] ) ) {
// This is clearly for images: return an empty list of hosts.
return [];
}
// We also want other things, like js and css: let's only remove the hosts for 'images'.
$cdn_urls = $this->options->get( 'cdn_cnames', [] );
if ( ! $cdn_urls ) {
return $hosts;
}
// Separate image hosts from the other ones.
$image_hosts = [];
$other_hosts = [];
$cdn_zones = $this->options->get( 'cdn_zone', [] );
foreach ( $cdn_urls as $k => $urls ) {
if ( ! in_array( $cdn_zones[ $k ], $zones, true ) ) {
continue;
}
$urls = explode( ',', $urls );
$urls = array_map( 'trim', $urls );
if ( 'images' === $cdn_zones[ $k ] ) {
foreach ( $urls as $url ) {
$image_hosts[] = $url;
}
} else {
foreach ( $urls as $url ) {
$other_hosts[] = $url;
}
}
}
// Make sure the image hosts are not also used for other things (duplicate).
$image_hosts = array_diff( $image_hosts, $other_hosts );
// Then remove the remaining from the final list.
return array_diff( $hosts, $image_hosts );
}
/**
* Maybe remove the images option from the CDN dropdown.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param bool $allow true to add the option, false otherwise.
* @return bool
*/
public function maybe_remove_images_from_cdn_dropdown( $allow ) {
if ( ! $allow ) {
return $allow;
}
if ( ! ewww_image_optimizer_get_option( 'ewww_image_optimizer_exactdn' ) ) {
return $allow;
}
// EWWW uses ExactDN: WPR CDN should be disabled for images.
return false;
}
/** ----------------------------------------------------------------------------------------- */
/** PUBLIC TOOLS ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the plugin name.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_name() {
return 'EWWW';
}
/**
* Get the plugin identifier.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_id() {
return 'ewww';
}
/**
* Tell if the plugin converts images to webp.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_converting_to_webp() {
if ( ! function_exists( 'ewww_image_optimizer_get_option' ) ) {
// No EWWW, no webp.
return false;
}
return (bool) ewww_image_optimizer_get_option( 'ewww_image_optimizer_webp' );
}
/**
* Tell if the plugin serves webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp() {
if ( ! function_exists( 'ewww_image_optimizer_get_option' ) ) {
// No EWWW, no webp.
return false;
}
if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_exactdn' ) ) {
// EWWW uses ExactDN (WPR CDN should be disabled for images).
return true;
}
if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_webp_for_cdn' ) ) {
// EWWW uses JS to rewrite file extensions.
return true;
}
// Decide if rewrite rules are used.
if ( ! function_exists( 'ewww_image_optimizer_webp_rewrite_verify' ) ) {
// Uh?
return false;
}
if ( ! function_exists( 'get_home_path' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
if ( ! function_exists( 'extract_from_markers' ) ) {
require_once ABSPATH . 'wp-admin/includes/misc.php';
}
/**
* This function returns null if rules are present and valid. Otherwise it returns rules to be inserted.
* Note: this also returns null if WP Fastest Cache rules for webp are found in the file.
*
* @see ewww_image_optimizer_wpfc_webp_enabled()
*/
$use_rewrite_rules = ! ewww_image_optimizer_webp_rewrite_verify();
/**
* Filter wether EWW is using rewrite rules for webp.
*
* @since 3.4
* @author Grégory Viguier
*
* @param bool $use_rewrite_rules True when EWWW uses rewrite rules. False otherwise.
*/
return (bool) apply_filters( 'rocket_webp_ewww_use_rewrite_rules', $use_rewrite_rules );
}
/**
* Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp_compatible_with_cdn() {
if ( ! function_exists( 'ewww_image_optimizer_get_option' ) ) {
// No EWWW, no webp.
return false;
}
if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_exactdn' ) ) {
// EWWW uses ExactDN.
return true;
}
if ( ewww_image_optimizer_get_option( 'ewww_image_optimizer_webp_for_cdn' ) ) {
// EWWW uses JS to rewrite file extensions.
return true;
}
// At this point, the plugin is using rewrite rules or nothing.
return false;
}
/**
* Get the plugin basename.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function get_basename() {
if ( empty( $this->plugin_basename ) ) {
$this->plugin_basename = rocket_has_constant( 'EWWW_IMAGE_OPTIMIZER_PLUGIN_FILE' )
? plugin_basename( rocket_get_constant( 'EWWW_IMAGE_OPTIMIZER_PLUGIN_FILE' ) )
: 'ewww-image-optimizer/ewww-image-optimizer.php';
}
return $this->plugin_basename;
}
/** ----------------------------------------------------------------------------------------- */
/** PRIVATE TOOLS =========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if EWWW is active for network.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return bool
*/
private function is_active_for_network() {
static $is;
if ( isset( $is ) ) {
return $is;
}
if ( ! is_multisite() ) {
$is = false;
return $is;
}
if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$is = is_plugin_active_for_network( $this->get_basename() ) && ! get_site_option( 'ewww_image_optimizer_allow_multisite_override' );
return $is;
}
}

View File

@@ -0,0 +1,435 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp;
use WP_Rocket\Admin\Options_Data;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Subscriber for the WebP support with Imagify.
*
* @since 3.4
* @author Grégory Viguier
*/
class Imagify_Subscriber implements Webp_Interface, Subscriber_Interface {
use Webp_Common;
/**
* Options_Data instance.
*
* @var Options_Data
* @access private
* @author Remy Perona
*/
private $options;
/**
* Imagify basename.
*
* @var string
* @access private
* @author Grégory Viguier
*/
private $plugin_basename;
/**
* Imagifys "serve webp" option name.
*
* @var string
* @access private
* @author Grégory Viguier
*/
private $plugin_option_name_to_serve_webp;
/**
* Temporarily store the result of $this->is_serving_webp().
*
* @var bool
* @access private
* @author Grégory Viguier
*/
private $tmp_is_serving_webp;
/**
* Constructor.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param Options_Data $options Options instance.
*/
public function __construct( Options_Data $options ) {
$this->options = $options;
}
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return array
*/
public static function get_subscribed_events() {
return [
'rocket_webp_plugins' => 'register',
'wp_rocket_loaded' => 'load_hooks',
];
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Launch filters.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function load_hooks() {
if ( ! $this->options->get( 'cache_webp' ) ) {
return;
}
/**
* Every time Imagify is (de)activated, we must "sync" our webp cache option.
*/
add_action( 'imagify_activation', [ $this, 'plugin_activation' ], 20 );
add_action( 'imagify_deactivation', [ $this, 'plugin_deactivation' ], 20 );
if ( ! rocket_has_constant( 'IMAGIFY_VERSION' ) ) {
return;
}
/**
* Since Rocket already updates the config file after updating its options, there is no need to do it again if the CDN or zone options change.
*/
/**
* Every time Imagifys option changes, we must "sync" our webp cache option.
*/
$option_name = $this->get_option_name_to_serve_webp();
if ( $this->is_active_for_network() ) {
add_filter( 'add_site_option_' . $option_name, [ $this, 'sync_on_network_option_add' ], 10, 3 );
add_filter( 'update_site_option_' . $option_name, [ $this, 'sync_on_network_option_update' ], 10, 4 );
add_filter( 'pre_delete_site_option_' . $option_name, [ $this, 'store_option_value_before_network_delete' ], 10, 2 );
add_filter( 'delete_site_option_' . $option_name, [ $this, 'sync_on_network_option_delete' ], 10, 2 );
return;
}
add_filter( 'add_option_' . $option_name, [ $this, 'sync_on_option_add' ], 10, 2 );
add_filter( 'update_option_' . $option_name, [ $this, 'sync_on_option_update' ], 10, 2 );
add_filter( 'delete_option', [ $this, 'store_option_value_before_delete' ] );
add_filter( 'delete_option_' . $option_name, [ $this, 'sync_on_option_delete' ] );
}
/**
* Maybe deactivate webp cache after Imagify network option has been successfully added.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the network option.
* @param mixed $value Value of the network option.
* @param int $network_id ID of the network.
*/
public function sync_on_network_option_add( $option, $value, $network_id ) {
if ( get_current_network_id() === $network_id && ! empty( $value['display_webp'] ) ) {
$this->trigger_webp_change();
}
}
/**
* Maybe activate or deactivate webp cache after Imagify network option has been modified.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the network option.
* @param mixed $value Current value of the network option.
* @param mixed $old_value Old value of the network option.
* @param int $network_id ID of the network.
*/
public function sync_on_network_option_update( $option, $value, $old_value, $network_id ) {
if ( get_current_network_id() === $network_id ) {
$this->sync_on_option_update( $old_value, $value );
}
}
/**
* Store the Imagify network option value before it is deleted.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Option name.
* @param int $network_id ID of the network.
*/
public function store_option_value_before_network_delete( $option, $network_id ) {
if ( get_current_network_id() === $network_id ) {
$this->tmp_is_serving_webp = $this->is_serving_webp();
}
}
/**
* Maybe activate webp cache after Imagify network option has been deleted.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the network option.
* @param int $network_id ID of the network.
*/
public function sync_on_network_option_delete( $option, $network_id ) {
if ( get_current_network_id() === $network_id && false !== $this->tmp_is_serving_webp ) {
$this->trigger_webp_change();
}
}
/**
* Maybe deactivate webp cache after Imagify option has been successfully added.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the option to add.
* @param mixed $value Value of the option.
*/
public function sync_on_option_add( $option, $value ) {
if ( ! empty( $value['display_webp'] ) ) {
$this->trigger_webp_change();
}
}
/**
* Maybe activate or deactivate webp cache after Imagify option has been modified.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param mixed $old_value The old option value.
* @param mixed $value The new option value.
*/
public function sync_on_option_update( $old_value, $value ) {
$old_display = ! empty( $old_value['display_webp'] );
$display = ! empty( $value['display_webp'] );
if ( $old_display !== $display || $old_value['display_webp_method'] !== $value['display_webp_method'] ) {
$this->trigger_webp_change();
}
}
/**
* Store the Imagify option value before it is deleted.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the option to delete.
*/
public function store_option_value_before_delete( $option ) {
if ( $this->get_option_name_to_serve_webp() === $option ) {
$this->tmp_is_serving_webp = $this->is_serving_webp();
}
}
/**
* Maybe activate webp cache after Imagify option has been deleted.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the deleted option.
*/
public function sync_on_option_delete( $option ) {
if ( false !== $this->tmp_is_serving_webp ) {
$this->trigger_webp_change();
}
}
/** ----------------------------------------------------------------------------------------- */
/** PUBLIC TOOLS ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the plugin name.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_name() {
return 'Imagify';
}
/**
* Get the plugin identifier.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_id() {
return 'imagify';
}
/**
* Tell if the plugin converts images to webp.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_converting_to_webp() {
if ( ! function_exists( 'get_imagify_option' ) ) {
// No Imagify, no webp.
return false;
}
return (bool) get_imagify_option( 'convert_to_webp' );
}
/**
* Tell if the plugin serves webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp() {
if ( ! function_exists( 'get_imagify_option' ) ) {
// No Imagify, no webp.
return false;
}
return (bool) get_imagify_option( 'display_webp' );
}
/**
* Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp_compatible_with_cdn() {
if ( ! $this->is_serving_webp() ) {
return false;
}
return 'rewrite' !== get_imagify_option( 'display_webp_method' );
}
/**
* Get the plugin basename.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function get_basename() {
if ( empty( $this->plugin_basename ) ) {
$this->plugin_basename = rocket_has_constant( 'IMAGIFY_FILE' )
? plugin_basename( rocket_get_constant( 'IMAGIFY_FILE' ) )
: 'imagify/imagify.php';
}
return $this->plugin_basename;
}
/** ----------------------------------------------------------------------------------------- */
/** PRIVATE TOOLS =========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the name of the Imagifys "serve webp" option.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return string
*/
private function get_option_name_to_serve_webp() {
if ( ! empty( $this->plugin_option_name_to_serve_webp ) ) {
return $this->plugin_option_name_to_serve_webp;
}
$default = 'imagify_settings';
if ( ! class_exists( '\Imagify_Options' ) || ! method_exists( '\Imagify_Options', 'get_instance' ) ) {
$this->plugin_option_name_to_serve_webp = $default;
return $this->plugin_option_name_to_serve_webp;
}
$instance = \Imagify_Options::get_instance();
if ( ! method_exists( $instance, 'get_option_name' ) ) {
$this->plugin_option_name_to_serve_webp = $default;
return $this->plugin_option_name_to_serve_webp;
}
$this->plugin_option_name_to_serve_webp = $instance->get_option_name();
return $this->plugin_option_name_to_serve_webp;
}
/**
* Tell if Imagify is active for network.
*
* @since 3.4
* @access private
* @author Grégory Viguier
*
* @return bool
*/
private function is_active_for_network() {
static $is;
if ( isset( $is ) ) {
return $is;
}
if ( function_exists( 'imagify_is_active_for_network' ) ) {
$is = imagify_is_active_for_network();
return $is;
}
if ( ! is_multisite() ) {
$is = false;
return $is;
}
if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$is = is_plugin_active_for_network( $this->get_basename() );
return $is;
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Subscriber for the WebP support with Optimus.
*
* @since 3.4
* @author Grégory Viguier
*/
class Optimus_Subscriber implements Webp_Interface, Subscriber_Interface {
/**
* Optimus basename.
*
* @var string
* @access private
* @author Grégory Viguier
*/
private $plugin_basename;
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return array
*/
public static function get_subscribed_events() {
if ( ! defined( 'OPTIMUS_FILE' ) ) {
return [];
}
return [
'rocket_webp_plugins' => 'register',
];
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Register the plugin.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param array $webp_plugins An array of Webp_Interface objects.
* @return array
*/
public function register( $webp_plugins ) {
$webp_plugins[] = $this;
return $webp_plugins;
}
/** ----------------------------------------------------------------------------------------- */
/** PUBLIC TOOLS ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the plugin name.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_name() {
return 'Optimus';
}
/**
* Get the plugin identifier.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_id() {
return 'optimus';
}
/**
* Tell if the plugin converts images to webp.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_converting_to_webp() {
if ( class_exists( '\Optimus' ) && method_exists( '\Optimus', 'get_options' ) ) {
$options = \Optimus::get_options();
} else {
$options = get_option( 'optimus' );
}
return ! empty( $options['webp_convert'] );
}
/**
* Tell if the plugin serves webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp() {
return false;
}
/**
* Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp_compatible_with_cdn() {
return false;
}
/**
* Get the plugin basename.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function get_basename() {
if ( empty( $this->plugin_basename ) ) {
$this->plugin_basename = defined( 'OPTIMUS_FILE' ) ? plugin_basename( OPTIMUS_FILE ) : 'optimus/optimus.php';
}
return $this->plugin_basename;
}
}

View File

@@ -0,0 +1,294 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp;
use WP_Rocket\Admin\Options_Data;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Subscriber for the WebP support with ShortPixel.
*
* @since 3.4
* @author Grégory Viguier
*/
class ShortPixel_Subscriber implements Webp_Interface, Subscriber_Interface {
use Webp_Common;
/**
* Options_Data instance.
*
* @var Options_Data
* @access private
* @author Remy Perona
*/
private $options;
/**
* ShortPixel basename.
*
* @var string
* @access private
* @author Grégory Viguier
*/
private $plugin_basename;
/**
* ShortPixels "serve webp" option name.
*
* @var string
* @access private
* @author Grégory Viguier
*/
private $plugin_option_name_to_serve_webp = 'wp-short-pixel-create-webp-markup';
/**
* Temporarily store the result of $this->is_serving_webp().
*
* @var bool
* @access private
* @author Grégory Viguier
*/
private $tmp_is_serving_webp;
/**
* Constructor.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param Options_Data $options Options instance.
*/
public function __construct( Options_Data $options ) {
$this->options = $options;
}
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return array
*/
public static function get_subscribed_events() {
return [
'rocket_webp_plugins' => 'register',
'wp_rocket_loaded' => 'load_hooks',
];
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Launch filters.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function load_hooks() {
if ( ! $this->options->get( 'cache_webp' ) ) {
return;
}
/**
* Every time ShortPixel is (de)activated, we must "sync" our webp cache option.
*/
if ( did_action( 'activate_' . $this->get_basename() ) ) {
$this->plugin_activation();
}
if ( did_action( 'deactivate_' . $this->get_basename() ) ) {
$this->plugin_deactivation();
}
add_action( 'activate_' . $this->get_basename(), [ $this, 'plugin_activation' ], 20 );
add_action( 'deactivate_' . $this->get_basename(), [ $this, 'plugin_deactivation' ], 20 );
if ( ! defined( 'SHORTPIXEL_IMAGE_OPTIMISER_VERSION' ) ) {
return;
}
/**
* Since Rocket already updates the config file after updating its options, there is no need to do it again if the CDN or zone options change.
*/
/**
* Every time ShortPixels option changes, we must "sync" our webp cache option.
*/
$option_name = $this->plugin_option_name_to_serve_webp;
add_filter( 'add_option_' . $option_name, [ $this, 'sync_on_option_add' ], 10, 2 );
add_filter( 'update_option_' . $option_name, [ $this, 'sync_on_option_update' ], 10, 2 );
add_filter( 'delete_option', [ $this, 'store_option_value_before_delete' ] );
add_filter( 'delete_option_' . $option_name, [ $this, 'sync_on_option_delete' ] );
}
/**
* Maybe deactivate webp cache after ShortPixel option has been successfully added.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the option to add.
* @param mixed $value Value of the option.
*/
public function sync_on_option_add( $option, $value ) {
if ( $value ) {
$this->trigger_webp_change();
}
}
/**
* Maybe activate or deactivate webp cache after ShortPixel option has been modified.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param mixed $old_value The old option value.
* @param mixed $value The new option value.
*/
public function sync_on_option_update( $old_value, $value ) {
/**
* 0 = Dont serve webp.
* 1 = <picture> + buffer
* 2 = <picture> + hooks
* 3 = .htaccess
*/
$old_value = $old_value > 0;
$value = $value > 0;
if ( $old_value !== $value ) {
$this->trigger_webp_change();
}
}
/**
* Store the ShortPixel option value before it is deleted.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the option to delete.
*/
public function store_option_value_before_delete( $option ) {
if ( $this->plugin_option_name_to_serve_webp === $option ) {
$this->tmp_is_serving_webp = $this->is_serving_webp();
}
}
/**
* Maybe activate webp cache after ShortPixel option has been deleted.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function sync_on_option_delete() {
if ( false !== $this->tmp_is_serving_webp ) {
$this->trigger_webp_change();
}
}
/** ----------------------------------------------------------------------------------------- */
/** PUBLIC TOOLS ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the plugin name.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_name() {
return 'ShortPixel';
}
/**
* Get the plugin identifier.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_id() {
return 'shortpixel';
}
/**
* Tell if the plugin converts images to webp.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_converting_to_webp() {
return (bool) get_option( 'wp-short-create-webp' );
}
/**
* Tell if the plugin serves webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp() {
return (bool) get_option( $this->plugin_option_name_to_serve_webp );
}
/**
* Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp_compatible_with_cdn() {
$display = (int) get_option( $this->plugin_option_name_to_serve_webp );
if ( ! $display ) {
// The option is not enabled, no webp.
return false;
}
if ( 3 === $display ) {
// The option is set to "rewrite rules".
return false;
}
return true;
}
/**
* Get the plugin basename.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function get_basename() {
if ( empty( $this->plugin_basename ) ) {
$this->plugin_basename = defined( 'SHORTPIXEL_PLUGIN_FILE' ) ? plugin_basename( SHORTPIXEL_PLUGIN_FILE ) : 'shortpixel-image-optimiser/wp-shortpixel.php';
}
return $this->plugin_basename;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp;
/**
* Trait for webp subscribers, focussed on plugins that serve webp images on frontend.
*
* @since 3.4
* @author Grégory Viguier
*/
trait Webp_Common {
/**
* Register the plugin.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @param array $webp_plugins An array of Webp_Interface objects.
* @return array
*/
public function register( $webp_plugins ) {
$webp_plugins[] = $this;
return $webp_plugins;
}
/**
* On plugin activation, deactivate Rocket webp cache if the plugin is serving webp.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function plugin_activation() {
if ( $this->is_serving_webp() ) {
$this->trigger_webp_change();
}
}
/**
* On plugin deactivation, activate Rocket webp cache if the plugin is serving webp.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function plugin_deactivation() {
if ( $this->is_serving_webp() ) {
$this->trigger_webp_change();
}
}
/**
* Trigger an action when the webp feature is enabled/disabled in a third party plugin.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*/
public function trigger_webp_change() {
/**
* Trigger an action when the webp feature is enabled/disabled in a third party plugin.
*
* @since 3.4
* @author Grégory Viguier
*/
do_action( 'rocket_third_party_webp_change' );
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins\Images\Webp;
/**
* Interface to use for webp subscribers.
*
* @since 3.4
* @author Grégory Viguier
*/
interface Webp_Interface {
/**
* Get the plugin name.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_name();
/**
* Get the plugin identifier.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return string
*/
public function get_id();
/**
* Tell if the plugin converts images to webp.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_converting_to_webp();
/**
* Tell if the plugin serves webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp();
/**
* Tell if the plugin uses a CDN-compatible technique to serve webp images on frontend.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_serving_webp_compatible_with_cdn();
/**
* Get the plugin basename.
*
* @since 3.4
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function get_basename();
}

View File

@@ -0,0 +1,396 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins;
use WP_Rocket\Event_Management\Subscriber_Interface;
defined( 'ABSPATH' ) || exit;
/**
* Class that handles events related to plugins that add mobile themes.
*
* @since 3.2
* @author Grégory Viguier
*/
class Mobile_Subscriber implements Subscriber_Interface {
/**
* Options to activate when a mobile plugin is active.
*
* @since 3.2
* @access protected
* @author Grégory Viguier
*
* @var array
*/
protected static $options = [
'cache_mobile' => 1,
'do_caching_mobile_files' => 1,
];
/**
* Cache the value of self::is_mobile_plugin_active().
*
* @since 3.2
* @access protected
* @author Grégory Viguier
*
* @var array An array of arrays of booleans.
* First level of keys corresponds to the network ID. Second level of keys corresponds to the blog ID.
*/
protected static $is_mobile_active = [];
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return array
*/
public static function get_subscribed_events() {
// In case a mobile plugin has already been activated.
$do = [];
$undo = [];
$plugin_events = [];
if ( ! function_exists( '\is_plugin_active' ) ) {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
foreach ( static::get_mobile_plugins() as $plugin => $plugin_data ) {
if ( \did_action( 'activate_' . $plugin ) &&
! isset( $plugin_data['is_active_callback'] ) ) {
$do[] = $plugin;
}
if ( \did_action( 'activate_' . $plugin ) &&
isset( $plugin_data['is_active_callback'] ) &&
call_user_func( $plugin_data['is_active_callback'] ) ) {
$do[] = $plugin;
}
if ( \did_action( 'deactivate_' . $plugin ) ) {
$undo[] = $plugin;
}
if ( \is_plugin_active( $plugin ) ) {
if ( isset( $plugin_data['activation_hook'] ) ) {
$plugin_events[ $plugin_data['activation_hook'] ] = 'maybe_update_mobile_cache_activation_plugin_hook';
}
if ( isset( $plugin_data['deactivation_hook'] ) ) {
$plugin_events[ $plugin_data['deactivation_hook'] ] = 'maybe_update_mobile_cache_activation_plugin_hook';
}
}
}
if ( array_diff( $do, $undo ) || array_diff( $undo, $do ) ) {
static::update_mobile_cache_activation();
}
// Register events.
$events = [
// Plugin activation/deactivation.
'add_option_active_plugins' => [ 'add_option_callback', 10, 2 ],
'update_option_active_plugins' => [ 'update_option_callback', 10, 2 ],
'delete_option_active_plugins' => 'delete_option_callback',
'add_site_option_active_sitewide_plugins' => [ 'add_site_option_callback', 10, 3 ],
'update_site_option_active_sitewide_plugins' => [ 'update_site_option_callback', 10, 4 ],
'delete_site_option_active_sitewide_plugins' => [ 'delete_site_option_callback', 10, 2 ],
// WPR settings (`get_option()`).
'option_' . WP_ROCKET_SLUG => 'mobile_options_filter',
];
foreach ( static::$options as $option => $value ) {
// WPR settings (`get_rocket_option()`).
$events[ 'pre_get_rocket_option_' . $option ] = 'is_mobile_plugin_active_callback';
}
$events = array_merge( $events, $plugin_events );
return $events;
}
/** ----------------------------------------------------------------------------------------- */
/** HOOK CALLBACKS ========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Callback triggered after the option `active_plugins` is created.
* This should normally never be triggered.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the option to add.
* @param mixed $value Value of the option.
*/
public function add_option_callback( $option, $value ) {
$this->maybe_update_mobile_cache_activation( $value, [] );
}
/**
* Callback triggered after the option `active_plugins` is updated.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param mixed $old_value The old option value.
* @param mixed $value Value of the option.
*/
public function update_option_callback( $old_value, $value ) {
$this->maybe_update_mobile_cache_activation( $value, $old_value );
}
/**
* Callback triggered after the option `active_plugins` is deleted.
* Very low probability to be triggered.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*/
public function delete_option_callback() {
static::update_mobile_cache_activation();
}
/**
* Callback triggered after the option `active_sitewide_plugins` is created.
* This should normally never be triggered.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the option to add.
* @param mixed $value Value of the option.
* @param int $network_id ID of the network.
*/
public function add_site_option_callback( $option, $value, $network_id ) {
if ( get_current_network_id() === $network_id ) {
$this->maybe_update_mobile_cache_activation( $value, [] );
}
}
/**
* Callback triggered after the option `active_sitewide_plugins` is updated.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the option to add.
* @param mixed $value Value of the option.
* @param mixed $old_value The old option value.
* @param int $network_id ID of the network.
*/
public function update_site_option_callback( $option, $value, $old_value, $network_id ) {
if ( get_current_network_id() === $network_id ) {
$this->maybe_update_mobile_cache_activation( $value, $old_value );
}
}
/**
* Callback triggered after the option `active_sitewide_plugins` is deleted.
* Very low probability to be triggered.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $option Name of the option to add.
* @param int $network_id ID of the network.
*/
public function delete_site_option_callback( $option, $network_id ) {
if ( get_current_network_id() === $network_id ) {
static::update_mobile_cache_activation();
}
}
/**
* Enable mobile caching when a mobile plugin is activated, or revert it back to its previous state when a mobile plugin is deactivated.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param mixed $value The new option value.
* @param mixed $old_value The old option value.
*/
public function maybe_update_mobile_cache_activation( $value, $old_value ) {
$plugins = static::get_mobile_plugins();
$plugins = array_keys( $plugins );
$value = array_intersect( $plugins, (array) $value );
$old_value = array_intersect( $plugins, (array) $old_value );
if ( $value !== $old_value ) {
static::update_mobile_cache_activation();
}
}
/**
* Enables mobile caching when a mobile plugin option is activated, or reverts it back to its previous state when a mobile plugin option is deactivated.
*
* @since 3.4.2
* @access public
* @author Soponar Cristina
*
* @return void
*/
public function maybe_update_mobile_cache_activation_plugin_hook() {
$is_mobile_plugin_active = static::is_mobile_plugin_active();
static::reset_class_cache();
$is_new_mobile_plugin_active = static::is_mobile_plugin_active();
if ( $is_mobile_plugin_active !== $is_new_mobile_plugin_active ) {
static::update_mobile_cache_activation();
}
}
/**
* Forces the values for the mobile options if a mobile plugin is active.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param array $values Option values.
* @return array
*/
public function mobile_options_filter( $values ) {
if ( static::is_mobile_plugin_active() ) {
return array_merge( (array) $values, static::$options );
}
return $values;
}
/**
* Forces the value for a mobile option if a mobile plugin is active.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param int|null $value Option value.
* @return int|null
*/
public function is_mobile_plugin_active_callback( $value ) {
if ( static::is_mobile_plugin_active() ) {
return 1;
}
return $value;
}
/** ----------------------------------------------------------------------------------------- */
/** MAIN HELPERS ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Update the config file and the advanced cache file.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*/
public static function update_mobile_cache_activation() {
// Reset class cache.
static::reset_class_cache();
// Update the config file.
rocket_generate_config_file();
// Update the advanced cache file.
rocket_generate_advanced_cache_file();
// Flush htaccess file.
flush_rocket_htaccess();
}
/**
* Reset `is_mobile_plugin_active()` cache.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*/
public static function reset_class_cache() {
// Reset class cache.
unset( static::$is_mobile_active[ get_current_network_id() ][ get_current_blog_id() ] );
}
/**
* Get the concerned plugins.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return array
*/
public static function get_mobile_plugins() {
return [
'jetpack/jetpack.php' => [
'is_active_callback' => function() {
if ( ! class_exists( 'Jetpack' ) ) {
return false;
}
return \Jetpack::is_active() && \Jetpack::is_module_active( 'minileven' );
},
'activation_hook' => 'jetpack_activate_module_minileven',
'deactivation_hook' => 'jetpack_deactivate_module_minileven',
],
'wptouch/wptouch.php' => [],
'wiziapp-create-your-own-native-iphone-app/wiziapp.php' => [],
'wordpress-mobile-pack/wordpress-mobile-pack.php' => [],
'wp-mobilizer/wp-mobilizer.php' => [],
'wp-mobile-edition/wp-mobile-edition.php' => [],
'device-theme-switcher/dts_controller.php' => [],
'wp-mobile-detect/wp-mobile-detect.php' => [],
'easy-social-share-buttons3/easy-social-share-buttons3.php' => [],
];
}
/**
* Tell if a mobile plugin is active.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return bool True if a mobile plugin in the list is active, false otherwise.
*/
public static function is_mobile_plugin_active() {
$network_id = get_current_network_id();
$blog_id = get_current_blog_id();
if ( isset( static::$is_mobile_active[ $network_id ][ $blog_id ] ) ) {
return static::$is_mobile_active[ $network_id ][ $blog_id ];
}
if ( ! function_exists( '\is_plugin_active' ) ) {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! isset( static::$is_mobile_active[ $network_id ] ) ) {
static::$is_mobile_active[ $network_id ] = [];
}
foreach ( static::get_mobile_plugins() as $mobile_plugin => $mobile_plugin_data ) {
if ( \is_plugin_active( $mobile_plugin ) &&
isset( $mobile_plugin_data['is_active_callback'] ) &&
call_user_func( $mobile_plugin_data['is_active_callback'] ) ) {
static::$is_mobile_active[ $network_id ][ $blog_id ] = true;
return true;
}
if ( \is_plugin_active( $mobile_plugin ) &&
! isset( $mobile_plugin_data['is_active_callback'] ) ) {
static::$is_mobile_active[ $network_id ][ $blog_id ] = true;
return true;
}
}
static::$is_mobile_active[ $network_id ][ $blog_id ] = false;
return false;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins;
use WP_Rocket\Event_Management\Subscriber_Interface;
defined( 'ABSPATH' ) || exit;
/**
* Class that handles events related to Next Gen Gallery.
*
* @since 3.3.1
* @author Remy Perona
*/
class NGG_Subscriber implements Subscriber_Interface {
/**
* Return an array of events that this subscriber wants to listen to.
*
* @since 3.3.1
* @author Remy Perona
*
* @return array
*/
public static function get_subscribed_events() {
if ( ! class_exists( 'C_NextGEN_Bootstrap' ) ) {
return;
}
return [
'run_ngg_resource_manager' => 'deactivate_resource_manager',
];
}
/**
* Deactivate NGG Resource Manager to prevent conflict with WP Rocket output buffering
*
* @since 3.3.1
* @author Remy Perona
*
* @return bool
*/
public function deactivate_resource_manager() {
return false;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins;
use WP_Rocket\Event_Management\Subscriber_Interface;
/**
* Compatibility class for SyntaxHighlighter plugin
*
* @since 3.3.1
* @author Remy Perona
*/
class SyntaxHighlighter_Subscriber implements Subscriber_Interface {
/**
* Return an array of events that this subscriber wants to listen to.
*
* @since 3.3.1
* @author Remy Perona
*
* @return array
*/
public static function get_subscribed_events() {
if ( ! class_exists( 'SyntaxHighlighter' ) ) {
return [];
}
return [
'rocket_exclude_defer_js' => 'exclude_defer_js_syntaxhighlighter_scripts',
'rocket_exclude_js' => 'exclude_minify_js_syntaxhighlighter_scripts',
];
}
/**
* Adds SyntaxHighlighter scripts to defer JS exclusion
*
* @since 3.3.1
* @author Remy Perona
*
* @param array $excluded_scripts Array of scripts to exclude.
* @return array
*/
public function exclude_defer_js_syntaxhighlighter_scripts( $excluded_scripts ) {
return array_merge(
$excluded_scripts,
[
'syntaxhighlighter/syntaxhighlighter3/scripts/(.*).js',
'syntaxhighlighter/syntaxhighlighter2/scripts/(.*).js',
]
);
}
/**
* Adds SyntaxHighlighter scripts to minify/combine JS exclusion
*
* @since 3.3.1
* @author Remy Perona
*
* @param array $excluded_scripts Array of scripts to exclude.
* @return array
*/
public function exclude_minify_js_syntaxhighlighter_scripts( $excluded_scripts ) {
return array_merge(
$excluded_scripts,
[
rocket_clean_exclude_file( plugins_url( 'syntaxhighlighter/syntaxhighlighter3/scripts/(.*).js' ) ),
rocket_clean_exclude_file( plugins_url( 'syntaxhighlighter/syntaxhighlighter2/scripts/(.*).js' ) ),
]
);
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins\Ecommerce;
use WP_Rocket\Event_Management\Event_Manager;
use WP_Rocket\Event_Management\Event_Manager_Aware_Subscriber_Interface;
/**
* BigCommerce compatibility subscriber
*
* @since 3.3.7
* @author Remy Perona
*/
class BigCommerce_Subscriber implements Event_Manager_Aware_Subscriber_Interface {
use \WP_Rocket\Traits\Config_Updater;
/**
* The WordPress Event Manager
*
* @var Event_Manager;
*/
protected $event_manager;
/**
* {@inheritdoc}
*
* @param Event_Manager $event_manager The WordPress Event Manager.
*/
public function set_event_manager( Event_Manager $event_manager ) {
$this->event_manager = $event_manager;
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
$events = [
'activate_bigcommerce/bigcommerce.php' => [ 'activate_bigcommerce', 11 ],
'deactivate_bigcommerce/bigcommerce.php' => [ 'deactivate_bigcommerce', 11 ],
];
if ( function_exists( 'bigcommerce_init' ) ) {
$events['update_option_bigcommerce_login_page_id'] = [ 'after_update_single_option', 10, 2 ];
$events['update_option_bigcommerce_account_page_id'] = [ 'after_update_single_option', 10, 2 ];
$events['update_option_bigcommerce_address_page_id'] = [ 'after_update_single_option', 10, 2 ];
$events['update_option_bigcommerce_orders_page_id'] = [ 'after_update_single_option', 10, 2 ];
$events['update_option_bigcommerce_cart_page_id'] = [ 'after_update_single_option', 10, 2 ];
$events['update_option_bigcommerce_checkout_page_id'] = [ 'after_update_single_option', 10, 2 ];
$events['shutdown'] = 'maybe_update_config';
$events['transition_post_status'] = [ 'maybe_exclude_page', 10, 3 ];
$events['rocket_cache_reject_uri'] = [
[ 'exclude_pages' ],
];
}
return $events;
}
/**
* Add exclusions when activating the BigCommerce plugin
*
* @since 3.3.7
* @author Rémy Perona
*/
public function activate_bigcommerce() {
$this->event_manager->add_callback( 'rocket_cache_reject_uri', [ $this, 'exclude_pages' ] );
// Update .htaccess file rules.
flush_rocket_htaccess();
// Regenerate the config file.
rocket_generate_config_file();
}
/**
* Remove exclusions when deactivating the BigCommerce plugin
*
* @since 3.3.7
* @author Rémy Perona
*/
public function deactivate_woocommerce() {
$this->event_manager->remove_callback( 'rocket_cache_reject_uri', [ $this, 'exclude_pages' ] );
// Update .htaccess file rules.
flush_rocket_htaccess();
// Regenerate the config file.
rocket_generate_config_file();
}
/**
* Maybe regenerate the htaccess & config file if a BigCommerce page is published
*
* @since 3.3.7
* @author Remy Perona
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param WP_Post $post Post object.
* @return bool
*/
public function maybe_exclude_page( $new_status, $old_status, $post ) {
if ( 'publish' === $old_status || 'publish' !== $new_status ) {
return false;
}
if ( get_option( 'bigcommerce_login_page_id' ) !== $post->ID && get_option( 'bigcommerce_account_page_id' ) !== $post->ID && get_option( 'bigcommerce_address_page_id' ) !== $post->ID && get_option( 'bigcommerce_orders_page_id' ) !== $post->ID && get_option( 'bigcommerce_cart_page_id' ) !== $post->ID && get_option( 'bigcommerce_checkout_page_id' ) !== $post->ID ) {
return false;
}
// Update .htaccess file rules.
flush_rocket_htaccess();
// Regenerate the config file.
rocket_generate_config_file();
return true;
}
/**
* Exclude BigCommerce login, cart, checkout, account, address and orders pages from caching
*
* @since 3.3.7
*
* @param array $urls An array of excluded pages.
* @return array
*/
public function exclude_pages( $urls ) {
$checkout_urls = $this->exclude_page( get_option( 'bigcommerce_checkout_page_id' ) );
$cart_urls = $this->exclude_page( get_option( 'bigcommerce_cart_page_id' ) );
$account_urls = $this->exclude_page( get_option( 'bigcommerce_account_page_id' ) );
$login_urls = $this->exclude_page( get_option( 'bigcommerce_login_page_id' ) );
$address_urls = $this->exclude_page( get_option( 'bigcommerce_address_page_id' ) );
$orders_urls = $this->exclude_page( get_option( 'bigcommerce_orders_page_id' ) );
return array_merge( $urls, $checkout_urls, $cart_urls, $account_urls, $login_urls, $address_urls, $orders_urls );
}
/**
* Excludes BigCommerce checkout page from cache
*
* @since 3.3.7
* @author Remy Perona
*
* @param int $page_id ID of page to exclude.
* @param string $post_type Post type of the page.
* @param string $pattern Pattern to use for the exclusion.
* @return array
*/
private function exclude_page( $page_id, $post_type = 'page', $pattern = '' ) {
$urls = [];
if ( ! $page_id ) {
return $urls;
}
if ( $page_id <= 0 || (int) get_option( 'page_on_front' ) === $page_id ) {
return $urls;
}
if ( 'publish' !== get_post_status( $page_id ) ) {
return $urls;
}
$urls = get_rocket_i18n_translated_post_urls( $page_id, $post_type, $pattern );
return $urls;
}
}

View File

@@ -0,0 +1,395 @@
<?php
namespace WP_Rocket\Subscriber\Third_Party\Plugins\Security;
use WP_Rocket\Admin\Options_Data as Options;
use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Logger\Logger;
defined( 'ABSPATH' ) || exit;
/**
* Sucuri Security compatibility.
* %s is here for the other query args.
*
* @since 3.2
* @author Grégory Viguier
*/
class Sucuri_Subscriber implements Subscriber_Interface {
/**
* URL of the API.
*
* @var string
* @since 3.2
* @author Grégory Viguier
*/
const API_URL = 'https://waf.sucuri.net/api?v2&%s';
/**
* Instance of the Option_Data class.
*
* @var Options
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $options;
/**
* Constructor.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param Options $options Instance of the Option_Data class.
*/
public function __construct( Options $options ) {
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public static function get_subscribed_events() {
return [
'after_rocket_clean_domain' => 'maybe_clean_firewall_cache',
'after_rocket_clean_post' => 'maybe_clean_firewall_cache',
'after_rocket_clean_term' => 'maybe_clean_firewall_cache',
'after_rocket_clean_user' => 'maybe_clean_firewall_cache',
'after_rocket_clean_home' => 'maybe_clean_firewall_cache',
'after_rocket_clean_files' => 'maybe_clean_firewall_cache',
'admin_post_rocket_purge_sucuri' => 'do_admin_post_rocket_purge_sucuri',
'admin_notices' => 'maybe_print_notice',
];
}
/** ----------------------------------------------------------------------------------------- */
/** HOOK CALLBACKS ========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Clear Sucuri firewall cache.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*/
public function maybe_clean_firewall_cache() {
static $done = false;
if ( $done ) {
return;
}
$done = true;
if ( ! $this->options->get( 'sucury_waf_cache_sync', 0 ) ) {
return;
}
$this->clean_firewall_cache();
}
/**
* Ajax callback to empty Sucury cache.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*/
public function do_admin_post_rocket_purge_sucuri() {
if ( empty( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'rocket_purge_sucuri' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
wp_nonce_ays( '' );
}
if ( ! current_user_can( 'rocket_purge_sucuri_cache' ) ) {
wp_nonce_ays( '' );
}
$purged = $this->clean_firewall_cache();
if ( is_wp_error( $purged ) ) {
$purged_result = [
'result' => 'error',
/* translators: %s is the error message returned by the API. */
'message' => sprintf( __( 'Sucuri cache purge error: %s', 'rocket' ), $purged->get_error_message() ),
];
} else {
$purged_result = [
'result' => 'success',
'message' => __( 'The Sucuri cache is being cleared. Note that it may take up to two minutes for it to be fully flushed.', 'rocket' ),
];
}
set_transient( get_current_user_id() . '_sucuri_purge_result', $purged_result );
wp_safe_redirect( esc_url_raw( wp_get_referer() ) );
die();
}
/**
* Print an admin notice if the cache failed to be cleared.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*/
public function maybe_print_notice() {
if ( ! current_user_can( 'rocket_purge_sucuri_cache' ) ) {
return;
}
if ( ! is_admin() ) {
return;
}
$user_id = get_current_user_id();
$notice = get_transient( $user_id . '_sucuri_purge_result' );
if ( ! $notice ) {
return;
}
delete_transient( $user_id . '_sucuri_purge_result' );
rocket_notice_html(
[
'status' => $notice['result'],
'message' => $notice['message'],
]
);
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if a API key is well formatted.
*
* @since 3.2.3
* @access public
* @author Grégory Viguier
*
* @param string $api_key An API kay.
* @return array|bool An array with the keys 'k' and 's' (required by the API) if valid. False otherwise.
*/
public static function is_api_key_valid( $api_key ) {
if ( '' !== $api_key && preg_match( '@^(?<k>[a-z0-9]{32})/(?<s>[a-z0-9]{32})$@', $api_key, $matches ) ) {
return $matches;
}
return false;
}
/**
* Clear Sucuri firewall cache.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @return bool|object True on success. A WP_Error object on failure.
*/
private function clean_firewall_cache() {
$api_key = $this->get_api_key();
if ( is_wp_error( $api_key ) ) {
return $api_key;
}
$response = $this->request_api(
[
'a' => 'clear_cache',
'k' => $api_key['k'],
's' => $api_key['s'],
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
Logger::info(
'Sucuri firewall cache cleared.',
[
'sucuri firewall cache',
]
);
return true;
}
/**
* Get the API key.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @return array|object An array with the keys 'k' and 's', required by the API. A WP_Error object if no key or invalid key.
*/
private function get_api_key() {
$api_key = trim( $this->options->get( 'sucury_waf_api_key', '' ) );
if ( ! $api_key ) {
Logger::error(
'API key was not found.',
[
'sucuri firewall cache',
]
);
return new \WP_Error( 'no_sucuri_api_key', __( 'Sucuri firewall API key was not found.', 'rocket' ) );
}
$matches = self::is_api_key_valid( $api_key );
if ( ! $matches ) {
Logger::error(
'API key is invalid.',
[
'sucuri firewall cache',
]
);
return new \WP_Error( 'invalid_sucuri_api_key', __( 'Sucuri firewall API key is invalid.', 'rocket' ) );
}
return [
'k' => $matches['k'],
's' => $matches['s'],
];
}
/**
* Request against the API.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param array $params Parameters to send.
* @return array|object The response data on success. A WP_Error object on failure.
*/
private function request_api( $params = [] ) {
$params['time'] = time();
$params = $this->build_query( $params );
$url = sprintf( static::API_URL, $params );
try {
/**
* Filters the arguments for the Sucuri API request
*
* @since 3.3.4
* @author Soponar Cristina
*
* @param array $args Arguments for the request.
*/
$args = apply_filters(
'rocket_sucuri_api_request_args',
[
'timeout' => 5,
'redirection' => 5,
'httpversion' => '1.1',
'blocking' => true,
/** This filter is documented in wp-includes/class-wp-http-streams.php */
'sslverify' => apply_filters( 'https_ssl_verify', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
]
);
$response = wp_remote_get( $url, $args );
} catch ( \Exception $e ) {
Logger::error(
'Error when contacting the API.',
[
'sucuri firewall cache',
'url' => $url,
'response' => $e->getMessage(),
]
);
return new \WP_Error( 'error_sucuri_api', __( 'Error when contacting Sucuri firewall API.', 'rocket' ) );
}
if ( is_wp_error( $response ) ) {
Logger::error(
'Error when contacting the API.',
[
'sucuri firewall cache',
'url' => $url,
'response' => $response->get_error_message(),
]
);
/* translators: %s is an error message. */
return new \WP_Error( 'wp_error_sucuri_api', sprintf( __( 'Error when contacting Sucuri firewall API. Error message was: %s', 'rocket' ), $response->get_error_message() ) );
}
$contents = wp_remote_retrieve_body( $response );
if ( ! $contents ) {
Logger::error(
'Could not get a response from the API.',
[
'sucuri firewall cache',
'url' => $url,
'response' => $response,
]
);
return new \WP_Error( 'sucuri_api_no_response', __( 'Could not get a response from the Sucuri firewall API.', 'rocket' ) );
}
$data = @json_decode( $contents, true );
if ( ! $data || ! is_array( $data ) ) {
Logger::error(
'Invalid response from the API.',
[
'sucuri firewall cache',
'url' => $url,
'response_body' => $contents,
]
);
return new \WP_Error( 'sucuri_api_invalid_response', __( 'Got an invalid response from the Sucuri firewall API.', 'rocket' ) );
}
if ( empty( $data['status'] ) ) {
Logger::error(
'The action failed.',
[
'sucuri firewall cache',
'url' => $url,
'response_data' => $data,
]
);
if ( empty( $data['messages'] ) || ! is_array( $data['messages'] ) ) {
return new \WP_Error( 'sucuri_api_error_status', __( 'The Sucuri firewall API returned an unknown error.', 'rocket' ) );
}
/* translators: %s is an error message. */
$message = _n( 'The Sucuri firewall API returned the following error: %s', 'The Sucuri firewall API returned the following errors: %s', count( $data['messages'] ), 'rocket' );
$message = sprintf( $message, '<br/>' . implode( '<br/>', $data['messages'] ) );
return new \WP_Error( 'sucuri_api_error_status', $message );
}
return $data;
}
/**
* An i18n-firendly alternative to the built-in PHP method `http_build_query()`.
*
* @param array|object $params An array or object containing properties.
* @return string A URL-encoded string.
*/
private function build_query( $params ) {
if ( ! $params ) {
return '';
}
$params = (array) $params;
foreach ( $params as $param => $value ) {
$params[ $param ] = $param . '=' . rawurlencode( (string) $value );
}
return implode( '&', $params );
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace WP_Rocket\Traits;
trait Config_Updater {
/**
* Update htaccess and WP Rocket config file if the option was modified.
*
* @since 3.1
* @author Remy Perona
*
* @param string $old_value Option's previous value.
* @param string $value Option's new value.
* @return void
*/
public function after_update_single_option( $old_value, $value ) {
if ( $old_value !== $value ) {
$this->flush_htaccess();
$this->generate_config_file();
}
}
/**
* Sets the htaccess update request
*
* @since 3.1
* @author Remy Perona
*
* @return void
*/
protected function flush_htaccess() {
wp_cache_set( 'rocket_flush_htaccess', 1 );
}
/**
* Sets WP Rocket config file update request
*
* @since 3.1
* @author Remy Perona
*
* @return void
*/
protected function generate_config_file() {
wp_cache_set( 'rocket_generate_config_file', 1 );
}
/**
* Performs the files update if requested
*
* @since 3.1
* @author Remy Perona
*
* @return void
*/
public function maybe_update_config() {
if ( wp_cache_get( 'rocket_flush_htaccess' ) ) {
flush_rocket_htaccess();
wp_cache_delete( 'rocket_flush_htaccess' );
}
if ( wp_cache_get( 'rocket_generate_config_file' ) ) {
\rocket_generate_config_file();
wp_cache_delete( 'rocket_generate_config_file' );
}
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace WP_Rocket\Traits;
/**
* Statically store values.
*
* @since 3.3
* @author Grégory Viguier
*/
trait Memoize {
/**
* Store the values.
*
* @var array
* @since 3.3
* @access private
* @author Grégory Viguier
*/
private static $memoized = [];
/**
* Tell if a value is memoized.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param string $method Name of the method.
* @param array $args Arguments passed to the parent method. It is used to build a hash.
* @return bool
*/
final public static function is_memoized( $method, $args = [] ) {
$hash = self::get_memoize_args_hash( $args );
return isset( self::$memoized[ $method ][ $hash ] );
}
/**
* Get a stored value.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param string $method Name of the method.
* @param array $args Arguments passed to the parent method. It is used to build a hash.
* @return mixed
*/
final public static function get_memoized( $method, $args = [] ) {
$hash = self::get_memoize_args_hash( $args );
return isset( self::$memoized[ $method ][ $hash ] ) ? self::$memoized[ $method ][ $hash ] : null;
}
/**
* Cache a value.
*
* @since 3.3
* @access public
* @author Grégory Viguier
*
* @param string $method Name of the method.
* @param array $args Arguments passed to the parent method. It is used to build a hash.
* @param mixed $value Value to store.
* @return mixed The stored value.
*/
final public static function memoize( $method, $args = [], $value = null ) {
$hash = self::get_memoize_args_hash( $args );
if ( ! isset( self::$memoized[ $method ] ) ) {
self::$memoized[ $method ] = [];
}
self::$memoized[ $method ][ $hash ] = $value;
return self::$memoized[ $method ][ $hash ];
}
/**
* Create a hash based on an array of arguments.
*
* @since 3.3
* @access private
* @author Grégory Viguier
*
* @param array $args An array of arguments.
* @return string
*/
final private static function get_memoize_args_hash( $args ) {
if ( [] === $args ) {
return 'd751713988987e9331980363e24189ce'; // `md5( json_encode( [] ) )`
}
return md5( call_user_func( 'json_encode', $args ) );
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace WP_Rocket\Traits;
use WP_Rocket\Logger\Logger;
/**
* Trait for the plugin updater.
*
* @since 3.3.6
* @author Grégory Viguier
*/
trait Updater_Api_Tools {
/**
* An ID to use when a API request fails.
*
* @var string
* @since 3.3.6
* @access protected
* @author Grégory Viguier
*/
/*protected $request_error_id;*/
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get a \WP_Error object to use when the request to WP Rockets server fails.
*
* @since 3.3.6
* @access protected
* @author Grégory Viguier
*
* @param mixed $data Error data to pass along the \WP_Error object.
* @return \WP_Error
*/
protected function get_request_error( $data = [] ) {
if ( ! is_array( $data ) ) {
$data = [
'response' => $data,
];
}
Logger::debug(
'Error when contacting the API.',
array_merge( [ 'Plugin Information' ], $data )
);
return new \WP_Error(
$this->request_error_id,
sprintf(
// translators: %s is an URL.
__( 'An unexpected error occurred. Something may be wrong with WP-Rocket.me or this server&#8217;s configuration. If you continue to have problems, <a href="%s">contact support</a>.', 'rocket' ),
$this->get_support_url()
),
$data
);
}
/**
* Get support URL.
*
* @since 3.3.6
* @access protected
* @author Grégory Viguier
*
* @return string
*/
protected function get_support_url() {
return rocket_get_external_url(
'support',
[
'utm_source' => 'wp_plugin',
'utm_medium' => 'wp_rocket',
]
);
}
/**
* Get a plugin slug, given its full path.
*
* @since 3.3.6
* @access protected
* @author Grégory Viguier
*
* @param string $plugin_file Full path to the plugin.
* @return string
*/
protected function get_plugin_slug( $plugin_file ) {
$plugin_file = trim( $plugin_file, '/' );
$plugin_slug = explode( '/', $plugin_file );
$plugin_slug = end( $plugin_slug );
$plugin_slug = str_replace( '.php', '', $plugin_slug );
return $plugin_slug;
}
}