config = array_merge( array( 'enabled' => false, 'cache_dir' => '', 'lifespan' => 36000, 'mobile_separate' => false, 'exclude_urls' => array(), 'exclude_cookies' => array(), 'exclude_agents' => array(), 'ignore_params' => array(), ), $config ); if ( '' === (string) $this->config['cache_dir'] && defined( 'WP_CONTENT_DIR' ) ) { $this->config['cache_dir'] = WP_CONTENT_DIR . '/cache/gsso/pages'; } } /** Load the config written by GSSO_Config_File. Returns null if missing/invalid. */ public static function load_config( $file ) { if ( ! is_readable( $file ) ) { return null; } $config = json_decode( (string) file_get_contents( $file ), true ); return is_array( $config ) ? $config : null; } /** Should this request skip the cache entirely (no read, no write)? */ public function should_bypass() { if ( empty( $this->config['enabled'] ) || empty( $this->config['cache_dir'] ) ) { return $this->bypass( 'disabled' ); } $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) : 'GET'; if ( 'GET' !== $method && 'HEAD' !== $method ) { return $this->bypass( 'method' ); } // Command-line scripts that load WordPress must never receive a cached page. if ( 'cli' === PHP_SAPI || 'phpdbg' === PHP_SAPI ) { return $this->bypass( 'cli' ); } if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE ) ) { return $this->bypass( 'constant' ); } $uri = $this->uri(); $path = (string) parse_url( $uri, PHP_URL_PATH ); // Anything that is a PHP endpoint, admin, login, REST, feed or file-like. if ( preg_match( '#(/wp-admin|/wp-login\.php|/wp-json|/xmlrpc\.php|/wp-cron\.php|/wp-content/|/wp-includes/|\.php$|/feed/?$|/robots\.txt$|\.xml$|\.xsl$|\.txt$)#i', $path ) ) { return $this->bypass( 'path' ); } if ( preg_match( '#/(index\.php/)?(wp-)?sitemap#i', $path ) ) { return $this->bypass( 'sitemap' ); } // Query strings: only cache if every parameter is on the ignore list. $query = (string) parse_url( $uri, PHP_URL_QUERY ); if ( '' !== $query ) { parse_str( $query, $params ); $ignored = array_map( 'strtolower', (array) $this->config['ignore_params'] ); $ignored[] = self::PRELOAD_PARAM; foreach ( array_keys( $params ) as $param ) { if ( ! in_array( strtolower( (string) $param ), $ignored, true ) ) { return $this->bypass( 'query' ); } } } // Cookies. $cookie_rules = array_merge( array( 'wordpress_logged_in', 'wp-postpass', 'comment_author', 'wordpressuser_', 'wordpresspass_', 'wordpress_no_cache' ), (array) $this->config['exclude_cookies'] ); foreach ( array_keys( $_COOKIE ) as $cookie ) { foreach ( $cookie_rules as $rule ) { if ( '' !== $rule && 0 === stripos( (string) $cookie, $rule ) ) { return $this->bypass( 'cookie' ); } } } // User agents. $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : ''; if ( false !== stripos( $agent, 'GSSO Health' ) ) { return $this->bypass( 'health' ); } foreach ( (array) $this->config['exclude_agents'] as $rule ) { if ( '' !== $rule && false !== stripos( $agent, $rule ) ) { return $this->bypass( 'agent' ); } } // URL exclusions (supports * wildcard). foreach ( (array) $this->config['exclude_urls'] as $rule ) { if ( self::url_matches( $path, $rule ) ) { return $this->bypass( 'excluded-url' ); } } return false; } private function bypass( $reason ) { $this->bypass_reason = $reason; return true; } /** Match a path against a rule like "/cart/*" or "/about-us/". */ public static function url_matches( $path, $rule ) { $rule = trim( (string) $rule ); if ( '' === $rule ) { return false; } // Allow full URLs in rules. if ( preg_match( '#^https?://#i', $rule ) ) { $rule = (string) parse_url( $rule, PHP_URL_PATH ); } $pattern = '#^' . str_replace( '\*', '.*', preg_quote( '/' . ltrim( $rule, '/' ), '#' ) ) . '$#i'; $path = '/' . ltrim( (string) $path, '/' ); // Treat "/page" and "/page/" as the same. return (bool) ( preg_match( $pattern, $path ) || preg_match( $pattern, rtrim( $path, '/' ) . '/' ) || preg_match( $pattern, rtrim( $path, '/' ) ) ); } public function uri() { return isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : '/'; } public function is_https() { if ( isset( $_SERVER['HTTPS'] ) && ( 'on' === strtolower( (string) $_SERVER['HTTPS'] ) || '1' === (string) $_SERVER['HTTPS'] ) ) { return true; } if ( isset( $_SERVER['SERVER_PORT'] ) && '443' === (string) $_SERVER['SERVER_PORT'] ) { return true; } if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === strtolower( (string) $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) { return true; } return false; } public function is_mobile() { $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : ''; return (bool) preg_match( '/Mobile|Android|Silk\/|Kindle|BlackBerry|Opera Mini|Opera Mobi|iPhone|iPod/i', $agent ); } /** Sanitized host, safe to use as a directory name. */ public static function host( $host ) { $host = str_replace( ':', '_', strtolower( (string) $host ) ); $host = preg_replace( '/[^a-z0-9.\-_]/', '', $host ); return '' === $host ? 'default' : $host; } /** * Convert a URL path into a safe, traversal-proof relative directory. */ public static function path_dir( $path ) { $path = rawurldecode( (string) $path ); $segments = array(); foreach ( explode( '/', $path ) as $segment ) { $segment = preg_replace( '/[^\p{L}\p{N}_\-\.~]/u', '_', $segment ); if ( null === $segment || '' === $segment || '.' === $segment || '..' === $segment ) { continue; } $segments[] = substr( $segment, 0, 200 ); } return implode( '/', $segments ); } /** Absolute path of the cache file for the current request. */ public function cache_file() { $host = self::host( isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' ); $dir = self::path_dir( (string) parse_url( $this->uri(), PHP_URL_PATH ) ); $name = 'index' . ( $this->is_https() ? '-https' : '' ) . ( $this->config['mobile_separate'] && $this->is_mobile() ? '-mobile' : '' ) . '.html'; return rtrim( $this->config['cache_dir'], '/' ) . '/' . $host . ( '' === $dir ? '' : '/' . $dir ) . '/' . $name; } public function is_fresh( $file ) { if ( ! is_file( $file ) ) { return false; } $lifespan = (int) $this->config['lifespan']; return 0 === $lifespan || ( time() - (int) filemtime( $file ) ) < $lifespan; } /** Serve a cached file and stop. Returns false if there was nothing to serve. */ public function serve() { $file = $this->cache_file(); if ( ! $this->is_fresh( $file ) || headers_sent() ) { return false; } $mtime = (int) filemtime( $file ); header( 'Content-Type: text/html; charset=UTF-8' ); header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $mtime ) . ' GMT' ); header( 'X-GSSO-Cache: HIT' ); header( 'Cache-Control: no-cache, must-revalidate, max-age=0' ); if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) && strtotime( (string) $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) >= $mtime ) { http_response_code( 304 ); exit; } if ( 'HEAD' !== ( isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) : 'GET' ) ) { readfile( $file ); } exit; } } // phpcs:enable endif;