ynamic-cookie segments sanitized to [A-Za-z0-9_-] (see * maybe_dynamic_cookies_filename()), so it can never contain "~". An overlong * (hashed) output always contains exactly one "~", so no short verbatim value * can ever equal a hashed value — distinct full $filename values can never map * to the same bounded basename, including across the short/long branches. * * @param string $filename Cache basename before the ".html" suffix. * @return string Bounded basename, <= 245 bytes. */ private function bound_cache_basename( $filename ) { if ( strlen( $filename ) <= 245 ) { return $filename; } return substr( $filename, 0, 212 ) . '~' . md5( $filename ); } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS TOOLS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Declares and sets value of constant preventing Optimizations. * * @since 3.3 */ 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 * @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" . ''; 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 doesn’t contain webp files. * * @since 3.4 * * @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 * * @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 * * @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-' . $this->config->get_config( 'secret_cache_key' ) . rtrim( $request_uri, '/' ); } // CL: begin — bind the per-user logged-in cache bucket to the full (unforgeable) auth-cookie value. // The username portion alone is client-controlled, so an unauthenticated attacker forging // "|..." would otherwise be routed into the victim's bucket. Hashing the full cookie // (which carries the WP session token + hmac) means a forged cookie yields a different bucket // → cache miss → fall through to WordPress, which rejects the forged cookie. A genuine user's // cookie is stable within a session, so legitimate cache hits are preserved. $cookie_value = (string) $cookies[ $logged_in_cookie ]; $parts = explode( '|', $cookie_value ); $user_key = $this->sanitize_user( reset( $parts ) ); $user_key .= '-' . hash( 'sha256', $cookie_value ); $user_key .= '-' . $this->config->get_config( 'secret_cache_key' ); // CL: end. // 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 * * @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'; // CL. } return $filename; } /** * Modifies the filename if the request is WebP compatible * * @since 3.4 * * @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 * * @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; } if ( ! $this->is_browser_webp_compatible() ) { return $filename; } return $filename . '-webp'; } /** * Checks if the browser is WebP compatible * * @since 3.12.6 * * @return bool */ private function is_browser_webp_compatible(): bool { // Only to supporting browsers. $http_accept = $this->config->get_server_input( 'HTTP_ACCEPT', '' ); if ( empty( $http_accept ) && function_exists( 'apache_request_headers' ) ) { $headers = apache_request_headers(); $http_accept = isset( $headers['Accept'] ) ? $headers['Accept'] : ''; } if ( ! empty( $http_accept ) && false !== strpos( $http_accept, 'webp' ) ) { return true; } return $this->is_user_agent_compatible(); } /** * Check the User Agent if the Accept headers is missing the WebP info * * @since 3.12.6 * * @return bool */ private function is_user_agent_compatible(): bool { $user_agent = $this->config->get_server_input( 'HTTP_USER_AGENT' ); if ( empty( $user_agent ) ) { return false; } if ( preg_match( '#Firefox/(?[0-9]{2,})#i', $user_agent, $matches ) ) { if ( 66 >= (int) $matches['version'] ) { return false; } } if ( preg_match( '#(?:iPad|iPhone)(.*)Version/(?[0-9]{2,})#i', $user_agent, $matches ) ) { if ( 14 > (int) $matches['version'] ) { return false; } return true; } if ( preg_match( '#Version/(?[0-9]{2,})(?:.*)Safari#i', $user_agent, $matches ) ) { if ( 16 > (int) $matches['version'] ) { return false; } } return true; } /** * Modifies the filename if dynamic cookies are set * * @note CL. * @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 ) ) { // Nested form: parent => [sub1, sub2, ...]. if ( isset( $_COOKIE[ $key ] ) ) { $parent = $cookies[ $key ] ?? null; foreach ( $cookie_name as $cookie_key ) { // CL: If subkey missing - append placeholder "-" (quirk kept for rocket parity). if ( ! is_array( $parent ) || ! array_key_exists( $cookie_key, $parent ) ) { $filename .= '-'; continue; } $val = $parent[ $cookie_key ]; if ( '' !== $val ) { $cache_key = preg_replace( '/[^a-z0-9_\-]/i', '-', (string) $val ); $filename .= '-' . $cache_key; } } } continue; } // Scalar form: "name". if ( isset( $cookies[ $cookie_name ] ) && '' !== $cookies[ $cookie_name ] ) { $cache_key = preg_replace( '/[^a-z0-9_\-]/i', '-', (string) $cookies[ $cookie_name ] ); $filename .= '-' . $cache_key; } } return $filename; } /** * Force lowercase on encoded url strings from different alphabets to prevent issues on some hostings. * * @since 3.3 * * @param array $matches Cache path. * @return string Cache path in lowercase. */ protected function reset_lowercase( $matches ) { return strtolower( $matches[0] ); } /** * Sanitizes a string username. * * @param string $user String username. * * @return string */ private function sanitize_user( string $user = '' ): string { return strtolower( rawurlencode( $user ) ); } /** * Check if permalink structure and url match. * * @return bool */ private function maybe_allow_wp_redirect(): bool { $exclusions = implode( '|', $this->wp_redirect_exclusions ); // Return early for excluded urls. if ( preg_match( '#' . $exclusions . '#', $this->tests->get_request_uri_base() ) ) { return false; } $permalink_structure = $this->config->get_config( 'permalink_structure' ); // Last character of permalink. $permalink_last_char = '/' !== substr( $permalink_structure, -1 ) ? '' : '/'; // Request uri without protocol & domain name. $request_uri = $this->tests->get_request_uri_base(); // Last character of request uri. $request_uri_last_char = '/' !== substr( $request_uri, -1 ) ? '' : '/'; // In cases where we have the home with a trailng slash (visible or invisible) // and permalink is without trailing slash. if ( '' === $permalink_last_char ) { // Check for root installation. $request_uri_last_char = '/' === $request_uri ? '' : $request_uri_last_char; /** * Check for subdir installation. * Use config file name to get home request_uri. */ $home = str_replace( $this->config->get_host(), '', basename( $this->config->get_config_file_path()['path'] ) ); $home = str_replace( '.', '/', str_replace( '.php', '', $home ) ); if ( '/' !== $request_uri && rtrim( $request_uri, '/' ) === $home ) { $request_uri_last_char = ''; } } return $permalink_last_char !== $request_uri_last_char; } /** * Generates the referrer string to be used in URLs. * * @return string Referrer string to be used in URLs. * @since 3.15.4-1.1-13 * @note CL. */ public static function get_referrer(): string { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return ''; } $uri = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ); $uri = esc_url_raw( $uri ); return '&_wp_http_referer=' . rawurlencode( remove_query_arg( 'fl_builder', $uri ) ); } }