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