Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
11.30% covered (danger)
11.30%
13 / 115
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
11.30% covered (danger)
11.30%
13 / 115
16.67% covered (danger)
16.67%
2 / 12
2487.91
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
5.71% covered (danger)
5.71%
2 / 35
0.00% covered (danger)
0.00%
0 / 1
427.68
 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|false - 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        if ( false === $this->request_uri ) {
139            return false;
140        }
141
142        /**
143         * Determines if the request is considered cacheable.
144         *
145         * Can be used to prevent a request from being cached.
146         *
147         * @since 3.2.0
148         *
149         * @param bool $default_status The default cacheability status (true for cacheable).
150         * @param string $request_uri  The request URI to be evaluated for cacheability.
151         */
152        if ( ! apply_filters( 'jetpack_boost_cache_request_cacheable', true, $this->request_uri ) ) {
153            return false;
154        }
155
156        if ( defined( 'DONOTCACHEPAGE' ) ) {
157            return false;
158        }
159
160        // do not cache post previews or customizer previews
161        if ( ! empty( $_GET ) && ( isset( $_GET['preview'] ) || isset( $_GET['customize_changeset_uuid'] ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
162            return false;
163        }
164
165        if ( $this->is_fatal_error() ) {
166            return false;
167        }
168
169        if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
170            return false;
171        }
172
173        if ( $this->is_404() ) {
174            return false;
175        }
176
177        if ( $this->is_feed() ) {
178            return false;
179        }
180
181        if ( $this->is_backend() ) {
182            return false;
183        }
184
185        if ( $this->is_bypassed_extension() ) {
186            return false;
187        }
188
189        if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] !== 'GET' ) {
190            return false;
191        }
192
193        if ( $this->is_url_excluded() ) {
194            Logger::debug( 'Url excluded, not cached!' );
195            return false;
196        }
197
198        if ( $this->is_module_disabled() ) {
199            return false;
200        }
201
202        /**
203         * Filters the accept headers to determine if the request should be cached.
204         *
205         * This filter allows modification of the content types that browsers send
206         * to the server during a request. If the acceptable browser content type header (HTTP_ACCEPT)
207         * matches one of these content types the request will not be cached,
208         * or a cached file served to this visitor.
209         *
210         * @since 3.2.0
211         *
212         * @param array $accept_headers An array of header values that should prevent a request from being cached.
213         */
214        $accept_headers = apply_filters( 'jetpack_boost_cache_accept_headers', array( 'application/json', 'application/activity+json', 'application/ld+json' ) );
215        $accept_headers = array_map( 'strtolower', $accept_headers );
216        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- $accept is checked and set below.
217        $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? strtolower( filter_var( $_SERVER['HTTP_ACCEPT'] ) ) : '';
218
219        if ( $accept !== '' ) {
220            foreach ( $accept_headers as $header ) {
221                if ( str_contains( $accept, $header ) ) {
222                    return false;
223                }
224            }
225        }
226
227        return true;
228    }
229
230    /**
231     * Returns true if the request appears to be for something with a known file extension that is not
232     * usually HTML. e.g.:
233     * - *.txt (including robots.txt, license.txt)
234     * - *.ico (favicon.ico)
235     * - *.jpg, *.png, *.webm (image files).
236     */
237    public function is_bypassed_extension() {
238        $file_extension = pathinfo( $this->request_uri, PATHINFO_EXTENSION );
239
240        return in_array(
241            $file_extension,
242            array(
243                'txt',
244                'ico',
245                'jpg',
246                'jpeg',
247                'png',
248                'webp',
249                'gif',
250            ),
251            true
252        );
253    }
254
255    /**
256     * Returns true if the current request is one of the following:
257     * 1. wp-admin
258     * 2. wp-login.php, xmlrpc.php or wp-cron.php/cron request
259     * 3. WP_CLI
260     * 4. REST request.
261     *
262     * @return bool
263     */
264    public function is_backend() {
265
266        $is_backend = is_admin();
267        if ( $is_backend ) {
268            return $is_backend;
269        }
270
271        $script = isset( $_SERVER['PHP_SELF'] ) ? basename( $_SERVER['PHP_SELF'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
272        if ( $script !== 'index.php' ) {
273            if ( in_array( $script, array( 'wp-login.php', 'xmlrpc.php', 'wp-cron.php' ), true ) ) {
274                $is_backend = true;
275            }
276        }
277
278        if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
279            $is_backend = true;
280        }
281
282        if ( PHP_SAPI === 'cli' || ( defined( 'WP_CLI' ) && constant( 'WP_CLI' ) ) ) {
283            $is_backend = true;
284        }
285
286        if ( defined( 'REST_REQUEST' ) ) {
287            $is_backend = true;
288        }
289
290        return $is_backend;
291    }
292
293    /**
294     * "Safe" version of WordPress' is_404 method. When called before WordPress' query is run, returns
295     * `null` (a falsey value) instead of outputting a _doing_it_wrong warning.
296     */
297    public function is_404() {
298        global $wp_query;
299
300        if ( ! isset( $wp_query ) || ! function_exists( '\is_404' ) ) {
301            return null;
302        }
303
304        return \is_404();
305    }
306
307    /**
308     * "Safe" version of WordPress' is_feed method. When called before WordPress' query is run, returns
309     * `null` (a falsey value) instead of outputting a _doing_it_wrong warning.
310     */
311    public function is_feed() {
312        global $wp_query;
313
314        if ( ! isset( $wp_query ) || ! function_exists( '\is_feed' ) ) {
315            return null;
316        }
317
318        return \is_feed();
319    }
320
321    /**
322     * Return true if the Page Cache module is disabled, or null if we don't know yet.
323     *
324     * If Status and Page_Cache are not available, it means the plugin is not loaded.
325     * This function will be called later when writing a cache file to disk.
326     * It's then that we can check if the module is active.
327     *
328     * @return null|bool
329     */
330    public function is_module_disabled() {
331
332        // A simple check to make sure we're in the output buffer callback.
333        if ( ! function_exists( '\is_feed' ) ) {
334            return null;
335        }
336
337        if (
338            class_exists( '\Automattic\Jetpack_Boost\Lib\Status' ) &&
339            class_exists( '\Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache' )
340        ) {
341            $page_cache_status = new \Automattic\Jetpack_Boost\Lib\Status(
342                \Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache::get_slug()
343            );
344            return ! $page_cache_status->get();
345        } else {
346            return true; // if the classes aren't available, the plugin isn't loaded.
347        }
348    }
349}