// 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; }