Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.73% covered (danger)
9.73%
11 / 113
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
9.73% covered (danger)
9.73%
11 / 113
16.67% covered (danger)
16.67%
2 / 12
2532.12
0.00% covered (danger)
0.00%
0 / 1
 current
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_uri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_parameters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_fatal_error
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 is_url_excluded
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 is_cacheable
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
462
 is_bypassed_extension
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 is_backend
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
132
 is_404
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 is_feed
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 is_module_disabled
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/*
3 * This file may be called before WordPress is fully initialized. See the README file for info.
4 */
5
6namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
7
8class Request {
9    /**
10     * @var Request - The request instance for current request.
11     */
12    private static $current_request = null;
13
14    /**
15     * @var string - The normalized path for the current request. This is not sanitized. Only to be used for comparison purposes.
16     */
17    private $request_uri = false;
18
19    /**
20     * @var array - The GET parameters and cookies for the current request. Everything considered in the cache key.
21     */
22    private $request_parameters;
23
24    /**
25     * Gets the singleton request instance.
26     *
27     * @return Request The instance of the class.
28     */
29    public static function current() {
30        if ( self::$current_request === null ) {
31            self::$current_request = new self(
32                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
33                isset( $_SERVER['REQUEST_URI'] ) ? Boost_Cache_Utils::normalize_request_uri( $_SERVER['REQUEST_URI'] ) : false,
34                // Set the cookies and get parameters for the current request. Sometimes these arrays are modified by WordPress or other plugins.
35                // We need to cache them here so they can be used for the cache key later. We don't need to sanitize them, as they are only used for comparison.
36                array(
37                    'cookies' => $_COOKIE,
38                    'get'     => $_GET,    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
39                )
40            );
41        }
42
43        return self::$current_request;
44    }
45
46    public function __construct( $uri, $parameters ) {
47        $this->request_uri        = $uri;
48        $this->request_parameters = $parameters;
49    }
50
51    public function get_uri() {
52        return $this->request_uri;
53    }
54
55    /**
56     * Returns the parameters for the current request.
57     *
58     * @return array The parameters for the current request, made up of cookies and get parameters.
59     */
60    public function get_parameters() {
61        /**
62         * Filters the parameters for the current request to identify the cache key.
63         *
64         * @since 3.8.0
65         *
66         * @param array $parameters The parameters for the current request, made up of cookies and get parameters.
67         */
68        return apply_filters( 'jetpack_boost_cache_parameters', $this->request_parameters );
69    }
70
71    /**
72     * Returns true if the current request has a fatal error.
73     *
74     * @return bool
75     */
76    private function is_fatal_error() {
77        $error = error_get_last();
78        if ( $error === null ) {
79            return false;
80        }
81
82        $fatal_errors = array(
83            E_ERROR,
84            E_PARSE,
85            E_CORE_ERROR,
86            E_COMPILE_ERROR,
87            E_USER_ERROR,
88        );
89
90        return in_array( $error['type'], $fatal_errors, true );
91    }
92
93    public function is_url_excluded( $request_uri = '' ) {
94        if ( $request_uri === '' ) {
95            $request_uri = $this->request_uri;
96        }
97
98        // Check if the query parameters `jb-disable-modules` or `jb-generate-critical-css` exist.
99        $request_parameters = $this->get_parameters();
100        $query_params       = $request_parameters['get'] ?? array();
101        if ( isset( $query_params['jb-disable-modules'] ) || isset( $query_params['jb-generate-critical-css'] ) ) {
102            return true;
103        }
104
105        $bypass_patterns = Boost_Cache_Settings::get_instance()->get_bypass_patterns();
106
107        /**
108         * Filters the bypass patterns for the page cache.
109         * If you need to sanitize them, do it before passing them to this filter,
110         * as there's no sanitization done after this filter.
111         *
112         * @since 3.2.0
113         *
114         * @param array $bypass_patterns An array of regex patterns that define URLs that bypass caching.
115         */
116        $bypass_patterns = apply_filters( 'jetpack_boost_cache_bypass_patterns', $bypass_patterns );
117
118        $bypass_patterns[] = 'wp-.*\.php';
119        foreach ( $bypass_patterns as $expr ) {
120            if ( ! empty( $expr ) && preg_match( "~^$expr/?$~", $request_uri ) ) {
121                return true;
122            }
123        }
124
125        return false;
126    }
127
128    /**
129     * Returns true if the request is cacheable.
130     *
131     * If a request is in the backend, or is a POST request, or is not an
132     * html request, it is not cacheable.
133     * The filter boost_cache_cacheable can be used to override this.
134     *
135     * @return bool
136     */
137    public function is_cacheable() {
138        /**
139         * Determines if the request is considered cacheable.
140         *
141         * Can be used to prevent a request from being cached.
142         *
143         * @since 3.2.0
144         *
145         * @param bool $default_status The default cacheability status (true for cacheable).
146         * @param string $request_uri  The request URI to be evaluated for cacheability.
147         */
148        if ( ! apply_filters( 'jetpack_boost_cache_request_cacheable', true, $this->request_uri ) ) {
149            return false;
150        }
151
152        if ( defined( 'DONOTCACHEPAGE' ) ) {
153            return false;
154        }
155
156        // do not cache post previews or customizer previews
157        if ( ! empty( $_GET ) && ( isset( $_GET['preview'] ) || isset( $_GET['customize_changeset_uuid'] ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
158            return false;
159        }
160
161        if ( $this->is_fatal_error() ) {
162            return false;
163        }
164
165        if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
166            return false;
167        }
168
169        if ( $this->is_404() ) {
170            return false;
171        }
172
173        if ( $this->is_feed() ) {
174            return false;
175        }
176
177        if ( $this->is_backend() ) {
178            return false;
179        }
180
181        if ( $this->is_bypassed_extension() ) {
182            return false;
183        }
184
185        if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] !== 'GET' ) {
186            return false;
187        }
188
189        if ( $this->is_url_excluded() ) {
190            Logger::debug( 'Url excluded, not cached!' );
191            return false;
192        }
193
194        if ( $this->is_module_disabled() ) {
195            return false;
196        }
197
198        /**
199         * Filters the accept headers to determine if the request should be cached.
200         *
201         * This filter allows modification of the content types that browsers send
202         * to the server during a request. If the acceptable browser content type header (HTTP_ACCEPT)
203         * matches one of these content types the request will not be cached,
204         * or a cached file served to this visitor.
205         *
206         * @since 3.2.0
207         *
208         * @param array $accept_headers An array of header values that should prevent a request from being cached.
209         */
210        $accept_headers = apply_filters( 'jetpack_boost_cache_accept_headers', array( 'application/json', 'application/activity+json', 'application/ld+json' ) );
211        $accept_headers = array_map( 'strtolower', $accept_headers );
212        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- $accept is checked and set below.
213        $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? strtolower( filter_var( $_SERVER['HTTP_ACCEPT'] ) ) : '';
214
215        if ( $accept !== '' ) {
216            foreach ( $accept_headers as $header ) {
217                if ( str_contains( $accept, $header ) ) {
218                    return false;
219                }
220            }
221        }
222
223        return true;
224    }
225
226    /**
227     * Returns true if the request appears to be for something with a known file extension that is not
228     * usually HTML. e.g.:
229     * - *.txt (including robots.txt, license.txt)
230     * - *.ico (favicon.ico)
231     * - *.jpg, *.png, *.webm (image files).
232     */
233    public function is_bypassed_extension() {
234        $file_extension = pathinfo( $this->request_uri, PATHINFO_EXTENSION );
235
236        return in_array(
237            $file_extension,
238            array(
239                'txt',
240                'ico',
241                'jpg',
242                'jpeg',
243                'png',
244                'webp',
245                'gif',
246            ),
247            true
248        );
249    }
250
251    /**
252     * Returns true if the current request is one of the following:
253     * 1. wp-admin
254     * 2. wp-login.php, xmlrpc.php or wp-cron.php/cron request
255     * 3. WP_CLI
256     * 4. REST request.
257     *
258     * @return bool
259     */
260    public function is_backend() {
261
262        $is_backend = is_admin();
263        if ( $is_backend ) {
264            return $is_backend;
265        }
266
267        $script = isset( $_SERVER['PHP_SELF'] ) ? basename( $_SERVER['PHP_SELF'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
268        if ( $script !== 'index.php' ) {
269            if ( in_array( $script, array( 'wp-login.php', 'xmlrpc.php', 'wp-cron.php' ), true ) ) {
270                $is_backend = true;
271            }
272        }
273
274        if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
275            $is_backend = true;
276        }
277
278        if ( PHP_SAPI === 'cli' || ( defined( 'WP_CLI' ) && constant( 'WP_CLI' ) ) ) {
279            $is_backend = true;
280        }
281
282        if ( defined( 'REST_REQUEST' ) ) {
283            $is_backend = true;
284        }
285
286        return $is_backend;
287    }
288
289    /**
290     * "Safe" version of WordPress' is_404 method. When called before WordPress' query is run, returns
291     * `null` (a falsey value) instead of outputting a _doing_it_wrong warning.
292     */
293    public function is_404() {
294        global $wp_query;
295
296        if ( ! isset( $wp_query ) || ! function_exists( '\is_404' ) ) {
297            return null;
298        }
299
300        return \is_404();
301    }
302
303    /**
304     * "Safe" version of WordPress' is_feed method. When called before WordPress' query is run, returns
305     * `null` (a falsey value) instead of outputting a _doing_it_wrong warning.
306     */
307    public function is_feed() {
308        global $wp_query;
309
310        if ( ! isset( $wp_query ) || ! function_exists( '\is_feed' ) ) {
311            return null;
312        }
313
314        return \is_feed();
315    }
316
317    /**
318     * Return true if the Page Cache module is disabled, or null if we don't know yet.
319     *
320     * If Status and Page_Cache are not available, it means the plugin is not loaded.
321     * This function will be called later when writing a cache file to disk.
322     * It's then that we can check if the module is active.
323     *
324     * @return null|bool
325     */
326    public function is_module_disabled() {
327
328        // A simple check to make sure we're in the output buffer callback.
329        if ( ! function_exists( '\is_feed' ) ) {
330            return null;
331        }
332
333        if (
334            class_exists( '\Automattic\Jetpack_Boost\Lib\Status' ) &&
335            class_exists( '\Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache' )
336        ) {
337            $page_cache_status = new \Automattic\Jetpack_Boost\Lib\Status(
338                \Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache::get_slug()
339            );
340            return ! $page_cache_status->get();
341        } else {
342            return true; // if the classes aren't available, the plugin isn't loaded.
343        }
344    }
345}