// Sanitize URIs and remove single quote from them to avoid syntax errors in .htaccess and php config file.
return str_replace( "'", '', esc_url_raw( $uri ) );
},
$uris
);
if ( '' !== $home_root ) {
foreach ( $uris as $i => $uri ) {
if ( preg_match( '/' . $home_root_escaped . '\(?\//', $uri ) ) {
// Remove the home directory from the new URIs.
$uris[ $i ] = substr( $uri, $home_root_len );
}
}
}
$uris = implode( '|', $uris );
if ( '' !== $home_root ) {
// Add the home directory back.
$uris = $home_root . '(' . $uris . ')';
}
return $uris;
}
/**
* Get all cookie names we don't cache.
*
* @since 2.0
*
* @return string A pipe separated list of rejected cookies.
*/
function get_rocket_cache_reject_cookies() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
$logged_in_cookie = explode( COOKIEHASH, LOGGED_IN_COOKIE );
$logged_in_cookie = array_map( 'preg_quote', $logged_in_cookie );
$logged_in_cookie = implode( '.+', $logged_in_cookie );
$cookies = get_rocket_option( 'cache_reject_cookies', [] );
// CL: When caching for logged-in users is enabled, ensure WP logged-in cookie is NOT rejected.
if ( get_rocket_option( 'cache_logged_user', false ) ) {
$cookies = array_values(
array_filter(
(array) $cookies,
static function ( $c ) use ( $logged_in_cookie ) {
// Remove exact computed pattern and any direct 'wordpress_logged_in_' entries.
return $c !== $logged_in_cookie && 0 !== strpos( (string) $c, 'wordpress_logged_in_' );
}
)
);
} else {
$cookies[] = $logged_in_cookie;
}
$cookies[] = 'wp-postpass_';
$cookies[] = 'wptouch_switch_toggle';
$cookies[] = 'comment_author_';
$cookies[] = 'comment_author_email_';
/**
* Filter the rejected cookies.
*
* @since 2.1
*
* @param array $cookies List of rejected cookies.
*/
$cookies = (array) apply_filters( 'rocket_cache_reject_cookies', $cookies );
$cookies = array_filter( $cookies );
$cookies = array_flip( array_flip( $cookies ) );
return implode( '|', $cookies );
}
/**
* Get list of mandatory cookies to be able to cache pages.
*
* @since 2.7
*
* @return string A pipe separated list of mandatory cookies.
*/
function get_rocket_cache_mandatory_cookies() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
$cookies = [];
/**
* Filter list of mandatory cookies.
*
* @since 2.7
*
* @param array $cookies List of mandatory cookies.
*/
$cookies = (array) apply_filters( 'rocket_cache_mandatory_cookies', $cookies );
$cookies = array_filter( $cookies );
$cookies = array_flip( array_flip( $cookies ) );
return implode( '|', $cookies );
}
/**
* Get list of dynamic cookies.
*
* @since 2.7
*
* @return array List of dynamic cookies.
*/
function get_rocket_cache_dynamic_cookies() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
/**
* Get list of dynamic cookies.
*
* CL: Base list comes from the saved option `cache_dynamic_cookies`,
* then filters can extend/override it via `rocket_cache_dynamic_cookies`.
* Supports nested definitions like [ 'parent' => ['sub1','sub2'], 0 => 'scalar' ].
* Removes empty items and deduplicates while preserving structure and order.
*
* @since 2.7
*
* @param array $cookies List of dynamic cookies.
*/
$base = (array) get_rocket_option( 'cache_dynamic_cookies', [] );
$raw = (array) apply_filters( 'rocket_cache_dynamic_cookies', $base );
$result = [];
$seen_scalar = [];
$seen_nested_parent = [];
foreach ( $raw as $key => $value ) {
// Nested: parent => [sub1, sub2, ...].
if ( is_array( $value ) ) {
if ( is_string( $key ) && '' !== $key ) {
if ( ! isset( $seen_nested_parent[ $key ] ) ) {
$seen_nested_parent[ $key ] = [];
$result[ $key ] = [];
}
foreach ( $value as $subkey ) {
if ( is_string( $subkey ) && '' !== $subkey && ! in_array( $subkey, $seen_nested_parent[ $key ], true ) ) {
$result[ $key ][] = $subkey;
$seen_nested_parent[ $key ][] = $subkey;
}
}
continue;
}
// Numeric array of scalars inside an array entry.
foreach ( $value as $maybe_token ) {
if ( is_string( $maybe_token ) && '' !== $maybe_token && ! isset( $seen_scalar[ $maybe_token ] ) ) {
$result[] = $maybe_token;
$seen_scalar[ $maybe_token ] = true;
}
}
continue;
}
// Scalar token.
if ( is_string( $value ) && '' !== $value && ! isset( $seen_scalar[ $value ] ) ) {
$result[] = $value;
$seen_scalar[ $value ] = true;
}
}
return $result;
}
/**
* Get all User-Agent we don't allow to get cache files.
*
* @since 2.3.5
*
* @return string A pipe separated list of rejected User-Agent.
*/
function get_rocket_cache_reject_ua() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
$ua = get_rocket_option( 'cache_reject_ua', [] );
$ua[] = 'facebookexternalhit';
$ua[] = 'WhatsApp';
/**
* Filter the rejected User-Agent
*
* @since 2.3.5
*
* @param array $ua List of rejected User-Agent.
*/
$ua = (array) apply_filters( 'rocket_cache_reject_ua', $ua );
$ua = array_filter( $ua );
$ua = array_flip( array_flip( $ua ) );
$ua = implode( '|', $ua );
return str_replace( [ ' ', '\\\\ ' ], '\\ ', $ua );
}
/**
* Get all query strings which can be cached.
*
* @since 2.3
*
* @note CL.
* @return array List of query strings which can be cached.
*/
function get_rocket_cache_query_string() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
// CL.
$query_strings = array_merge(
[
'lang',
's',
'permalink_name',
'lp-variation-id',
],
(array) get_rocket_option( 'cache_query_strings', [] )
);
/**
* Filter query strings which can be cached.
*
* @since 2.3
*
* @param array $query_strings List of query strings which can be cached.
*/
$query_strings = (array) apply_filters( 'rocket_cache_query_strings', $query_strings );
$query_strings = array_filter( $query_strings );
$query_strings = array_flip( array_flip( $query_strings ) );
return $query_strings;
}
/**
* Determine if the key is valid
*
* @since 2.9 use hash_equals() to compare the hash values
* @since 1.0
*
* @note CL.
* @return bool true if everything is ok, false otherwise
*/
function rocket_valid_key() {
if ( false === clsop_is_standalone() ) {
return true;
}
rocket_maybe_check_key_daily();
return rocket_valid_key_local();
}
/**
* Local-only license validity check (no remote calls).
*
* @note CL.
*
* @return bool
*/
function rocket_valid_key_local(): bool {
// CL: In CLOS mode, license is managed by AccelerateWP - always valid.
if ( false === clsop_is_standalone() ) {
return true;
}
$rocket_secret_key = (string) get_rocket_option( 'secret_key', '' );
if ( empty( $rocket_secret_key ) ) {
return false;
}
$consumer_key = (string) get_rocket_option( 'consumer_key', '' );
if ( empty( $consumer_key ) ) {
set_transient(
'rocket_check_key_errors',
[
'The provided license data are not valid.' .
'
' .
// Translators: %1$s = opening link tag, %2$s = closing link tag.
sprintf( 'To resolve, please %1$scontact support%2$s.', '', '' ),
]
);
return false;
}
return true;
}
/**
* Runs license validation at most once per day, using transients for throttling.
*
* @note CL.
*
* @return void
*/
function rocket_maybe_check_key_daily(): void {
static $in_progress = false;
if ( $in_progress ) {
return;
}
if ( false === clsop_is_standalone() ) {
return;
}
$consumer_key = (string) get_rocket_option( 'consumer_key', '' );
if ( '' === $consumer_key ) {
return;
}
$last_check = (int) get_transient( 'rocket_license_last_check' );
if ( $last_check && ( time() - $last_check ) < DAY_IN_SECONDS ) {
return;
}
// Prevent thundering herd (front + admin + cron etc.).
if ( get_transient( 'rocket_license_check_lock' ) ) {
return;
}
set_transient( 'rocket_license_check_lock', 1, 5 * MINUTE_IN_SECONDS );
$in_progress = true;
try {
$checked = rocket_check_key( true );
// Persist new secrets/consumer key if returned (successful validation).
if ( is_array( $checked ) && ! empty( $checked['secret_key'] ) ) {
$options = get_option( rocket_get_constant( 'WP_ROCKET_SLUG' ), [] );
if ( ! is_array( $options ) ) {
$options = [];
}
foreach ( [ 'consumer_key', 'consumer_email', 'secret_key' ] as $k ) {
if ( array_key_exists( $k, $checked ) ) {
$options[ $k ] = $checked[ $k ];
}
}
update_option( rocket_get_constant( 'WP_ROCKET_SLUG' ), $options );
// CL: Only mark successful check time to allow retry on error.
set_transient( 'rocket_license_last_check', time(), DAY_IN_SECONDS );
}
} finally {
$in_progress = false;
delete_transient( 'rocket_license_check_lock' );
}
}
/**
* Determine if the key is valid.
*
* @since 2.9.7 Remove arguments ($type & $data).
* @since 2.9.7 Stop to auto-check the validation each 1 & 30 days.
* @since 2.2 The function do the live check and update the option.
*
* @note CL.
* @param bool $skip_precheck Skip local validity precheck (used to avoid recursion).
* @return bool|array
*/
function rocket_check_key( bool $skip_precheck = false ) {
if ( false === clsop_is_standalone() ) {
return true;
}
// Recheck the license (local-only, no remote calls).
$return = $skip_precheck ? false : rocket_valid_key_local();
if ( $return ) {
rocket_delete_licence_data_file();
return $return;
}
Logger::info( 'LICENSE VALIDATION PROCESS STARTED.', [ 'license validation process' ] );
$consumer_key = (string) get_rocket_option( 'consumer_key', '' );
// Request from UI (settings form submit).
if (
isset( $_POST['_wpnonce'], $_POST['wp_rocket_settings']['api_key'] )
&&
wp_verify_nonce(
sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ),
rocket_get_constant( 'WP_ROCKET_PLUGIN_SLUG', 'clsop' ) . '-options'
)
) {
$consumer_key = sanitize_text_field( wp_unslash( $_POST['wp_rocket_settings']['api_key'] ) );
// CL: Clear previous errors and throttle on new key submission.
delete_transient( 'rocket_check_key_errors' );
delete_transient( 'rocket_license_last_check' );
delete_transient( 'rocket_license_check_lock' );
}
if ( empty( $consumer_key ) ) {
return false;
}
// CL: V2 API endpoint with proper HTTP codes.
$response = wp_remote_post(
clsop_saas_url() . 'v2/key/check',
[
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $consumer_key,
],
'timeout' => 30,
'body' => [
'site_url' => defined( 'WP_HOME' ) ? WP_HOME : (string) get_option( 'home' ),
],
]
);
if ( is_wp_error( $response ) ) {
Logger::error(
'License validation failed.',
[
'license validation process',
'request_error' => $response->get_error_messages(),
]
);
set_transient( 'rocket_check_key_errors', $response->get_error_messages() );
return $return;
}
$http_code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$json = json_decode( $body );
$rocket_options = [];
$rocket_options['consumer_key'] = $consumer_key;
$rocket_options['secret_key'] = '';
$rocket_options['consumer_email'] = get_option( 'admin_email' );
// HTTP 401 - Invalid or missing token.
if ( 401 === $http_code ) {
$error_code = isset( $json->error->code ) ? $json->error->code : 'INVALID_TOKEN';
$api_message = isset( $json->error->message ) ? $json->error->message : null;
Logger::error(
'License validation failed. Invalid API token.',
[
'license validation process',
'http_code' => 401,
'error_code' => $error_code,
]
);
// CL: Store error code for later translation in admin notice (after init hook).
set_transient(
'rocket_check_key_errors',
[
[
'code' => $error_code,
'api_message' => $api_message,
],
]
);
return false;
}
// HTTP 400 - Validation error like BAD_SITE_URL.
if ( 400 === $http_code ) {
$error_code = isset( $json->error->code ) ? $json->error->code : 'UNKNOWN_ERROR';
$api_message = isset( $json->error->message ) ? $json->error->message : null;
Logger::error(
'License validation failed.',
[
'license validation process',
'http_code' => 400,
'error_code' => $error_code,
]
);
// CL: Store error code for later translation in admin notice (after init hook).
set_transient(
'rocket_check_key_errors',
[
[
'code' => $error_code,
'api_message' => $api_message,
],
]
);
return false;
}
// HTTP 5xx - Server error, don't delete key.
if ( $http_code >= 500 ) {
Logger::error(
'License validation failed. Server error.',
[
'license validation process',
'http_code' => $http_code,
]
);
// CL: Store error code for later translation in admin notice (after init hook).
set_transient(
'rocket_check_key_errors',
[
[
'code' => 'SERVER_ERROR',
'http_code' => $http_code,
],
]
);
return $return;
}
// HTTP 200 - Success.
if ( 200 === $http_code && isset( $json->data->secret_key ) ) {
delete_transient( 'wp_rocket_customer_data' );
$rocket_options['secret_key'] = $json->data->secret_key;
if ( ! get_rocket_option( 'license' ) ) {
$rocket_options['license'] = '1';
}
Logger::info( 'License validation successful.', [ 'license validation process' ] );
set_transient( rocket_get_constant( 'WP_ROCKET_SLUG' ), $rocket_options );
delete_transient( 'rocket_check_key_errors' );
rocket_delete_licence_data_file();
update_option( 'wp_rocket_no_licence', 0 );
return $rocket_options;
}
// Unexpected response.
Logger::error(
'License validation failed. Unexpected response.',
[
'license validation process',
'http_code' => $http_code,
]
);
// CL: Store error code for later translation in admin notice (after init hook).
set_transient(
'rocket_check_key_errors',
[
[
'code' => 'UNEXPECTED_RESPONSE',
'http_code' => $http_code,
],
]
);
return $return;
}
/**
* Deletes the licence-data.php file if it exists
*
* @since 3.5
* @author Remy Perona
*
* @return void
*/
function rocket_delete_licence_data_file() {
// CL: Do not delete licence-data.php in non-standalone mode.
if ( false === clsop_is_standalone() ) {
return;
}
if ( is_multisite() ) {
return;
}
$rocket_path = rocket_get_constant( 'WP_ROCKET_PATH' );
if ( ! rocket_direct_filesystem()->exists( $rocket_path . 'licence-data.php' ) ) {
return;
}
rocket_direct_filesystem()->delete( $rocket_path . 'licence-data.php' );
}
/**
* Is WP a MultiSite and a subfolder install?
*
* @since 3.1.1
* @author Grégory Viguier
*
* @return bool
*/
function rocket_is_subfolder_install() {
global $wpdb;
static $subfolder_install;
if ( isset( $subfolder_install ) ) {
return $subfolder_install;
}
if ( is_multisite() ) {
$subfolder_install = ! is_subdomain_install();
} elseif ( ! is_null( $wpdb->sitemeta ) ) {
$subfolder_install = ! $wpdb->get_var( "SELECT meta_value FROM $wpdb->sitemeta WHERE site_id = 1 AND meta_key = 'subdomain_install'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
} else {
$subfolder_install = false;
}
return $subfolder_install;
}
/**
* Get the name of the "home directory", in case the home URL is not at the domain's root.
* It can be seen like the `RewriteBase` from the .htaccess file, but without the trailing slash.
*
* @since 3.1.1
* @author Grégory Viguier
*
* @return string
*/
function rocket_get_home_dirname() {
static $home_root;
if ( isset( $home_root ) ) {
return $home_root;
}
$home_root = wp_parse_url( rocket_get_main_home_url() );
if ( ! empty( $home_root['path'] ) ) {
$home_root = '/' . trim( $home_root['path'], '/' );
$home_root = rtrim( $home_root, '/' );
} else {
$home_root = '';
}
return $home_root;
}
/**
* Get the URL of the site's root. It corresponds to the main site's home page URL.
*
* @since 3.1.1
* @author Grégory Viguier
*
* @return string
*/
function rocket_get_main_home_url() {
static $root_url;
if ( isset( $root_url ) ) {
return $root_url;
}
if ( ! is_multisite() || is_main_site() ) {
$root_url = rocket_get_home_url( '/' );
return $root_url;
}
$current_network = get_network();
if ( $current_network ) {
$root_url = set_url_scheme( 'https://' . $current_network->domain . $current_network->path );
$root_url = trailingslashit( $root_url );
} else {
$root_url = rocket_get_home_url( '/' );
}
return $root_url;
}