Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.64% covered (danger)
43.64%
24 / 55
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LCP_Optimization_Util
43.64% covered (danger)
43.64%
24 / 55
33.33% covered (danger)
33.33%
2 / 6
357.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 should_skip_optimization
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
462
 should_apply_optimization
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 get_lcp_image_url
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
7.33
 can_optimize
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
5.20
 find_element
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
7.18
1<?php
2
3namespace Automattic\Jetpack_Boost\Modules\Optimizations\Lcp;
4
5use WP_HTML_Tag_Processor;
6
7class LCP_Optimization_Util {
8
9    /**
10     * Each LCP data is an array that includes the LCP for a certain viewport.
11     *
12     * @var array
13     */
14    private $lcp_data;
15
16    public function __construct( $lcp_data ) {
17        $this->lcp_data = $lcp_data;
18    }
19
20    /**
21     * Check if LCP optimization should be skipped for the current request.
22     *
23     * @since 4.0.0
24     * @return bool True if optimization should be skipped, false otherwise.
25     */
26    public static function should_skip_optimization() {
27        /**
28         * Filters whether to short-circuit LCP optimization.
29         *
30         * Returning a value other than null from the filter will short-circuit
31         * the optimization check, returning that value instead.
32         *
33         * @since 4.0.0
34         *
35         * @param null|bool $skip Whether to skip optimization. Default null.
36         */
37        $pre = apply_filters( 'jetpack_boost_pre_should_skip_lcp_optimization', null );
38        if ( null !== $pre ) {
39            return $pre;
40        }
41
42        // Disable in robots.txt.
43        if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( home_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'robots.txt' ) !== false ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This is validating.
44            return true;
45        }
46
47        // Disable in other possible AJAX requests setting cors related header.
48        if ( isset( $_SERVER['HTTP_SEC_FETCH_MODE'] ) && 'cors' === strtolower( $_SERVER['HTTP_SEC_FETCH_MODE'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
49            return true;
50        }
51
52        // Disable in other possible AJAX requests setting XHR related header.
53        if ( isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && 'xmlhttprequest' === strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
54            return true;
55        }
56
57        // Disable in all XLS (see the WP_Sitemaps_Renderer class).
58        if ( isset( $_SERVER['REQUEST_URI'] ) &&
59        (
60            // phpcs:disable WordPress.Security.ValidatedSanitizedInput -- This is validating.
61            str_contains( $_SERVER['REQUEST_URI'], '.xsl' ) ||
62            str_contains( $_SERVER['REQUEST_URI'], 'sitemap-stylesheet=index' ) ||
63            str_contains( $_SERVER['REQUEST_URI'], 'sitemap-stylesheet=sitemap' )
64            // phpcs:enable WordPress.Security.ValidatedSanitizedInput
65        ) ) {
66            return true;
67        }
68
69        // Disable in all POST Requests.
70        // phpcs:disable WordPress.Security.NonceVerification.Missing
71        if ( ! empty( $_POST ) ) {
72            return true;
73        }
74
75        // Disable in customizer previews
76        if ( is_customize_preview() ) {
77            return true;
78        }
79
80        // Disable in feeds, AJAX, Cron, XML.
81        if ( is_feed() || wp_doing_ajax() || wp_doing_cron() || wp_is_xml_request() ) {
82            return true;
83        }
84
85        // Disable in sitemaps.
86        if ( ! empty( get_query_var( 'sitemap' ) ) ) {
87            return true;
88        }
89
90        // Disable in AMP pages.
91        if ( function_exists( 'amp_is_request' ) && amp_is_request() ) {
92            return true;
93        }
94
95        return false;
96    }
97
98    /**
99     * Check if a specific optimization should be applied based on cloud response.
100     * Returns true if no optimizations object exists (backward compatibility) or if the flag is truthy.
101     *
102     * @since 4.5.6
103     *
104     * @param array  $lcp_data The LCP data array.
105     * @param string $key      The optimization key to check.
106     * @return bool True if the optimization should be applied.
107     */
108    public static function should_apply_optimization( $lcp_data, $key ) {
109        return ! isset( $lcp_data['optimizations'] )
110            || ! empty( $lcp_data['optimizations'][ $key ] );
111    }
112
113    public function get_lcp_image_url() {
114        if ( ! $this->can_optimize() ) {
115            return null;
116        }
117
118        if ( LCP::TYPE_BACKGROUND_IMAGE !== $this->lcp_data['type'] && LCP::TYPE_IMAGE !== $this->lcp_data['type'] ) {
119            return null;
120        }
121
122        if ( empty( $this->lcp_data['url'] ) ) {
123            return null;
124        }
125
126        if ( ! wp_http_validate_url( $this->lcp_data['url'] ) ) {
127            return null;
128        }
129
130        return $this->lcp_data['url'];
131    }
132
133    /**
134     * Check if the LCP data is valid and can be optimized.
135     *
136     * @return bool True if the LCP data is valid and can be optimized, false otherwise.
137     *
138     * @since 4.1.0
139     */
140    public function can_optimize() {
141        if ( empty( $this->lcp_data ) || ! is_array( $this->lcp_data ) ) {
142            return false;
143        }
144
145        if ( ! isset( $this->lcp_data['success'] ) || ! $this->lcp_data['success'] ) {
146            return false;
147        }
148
149        return true;
150    }
151
152    /**
153     * Check if the element is present in the LCP data.
154     *
155     * @param string $buffer The HTML to check.
156     * @param string $tag The tag to check. Default is 'img'.
157     * @return WP_HTML_Tag_Processor|false The HTML tag processor if the element is present, false otherwise.
158     *
159     * @since 4.1.0
160     */
161    public function find_element( $buffer, $tag = 'img' ) {
162        $html_processor = new WP_HTML_Tag_Processor( $buffer );
163        $element        = new WP_HTML_Tag_Processor( $this->lcp_data['html'] );
164
165        // Ensure the LCP HTML is a valid tag before proceeding.
166        if ( ! $element->next_tag( $tag ) ) {
167            return false;
168        }
169
170        // Extract attributes from the LCP tag for matching
171        $lcp_id    = $element->get_attribute( 'id' );
172        $lcp_class = $element->get_attribute( 'class' );
173
174        // Perform a quick check to see if the class is present in the HTML.
175        if ( ! empty( $lcp_class ) && ! str_contains( $buffer, $lcp_class ) ) {
176            return false;
177        }
178
179        // Loop through all img tags in the buffer with the same class until we find a match.
180        // We do this because next_tag does not support matching on IDs and sources.
181        while ( $html_processor->next_tag( $tag ) ) {
182            // Tag is considered a match if the class and id match.
183            if ( $lcp_id === $html_processor->get_attribute( 'id' ) &&
184                $lcp_class === $html_processor->get_attribute( 'class' ) ) {
185                return $html_processor;
186            }
187        }
188
189        return false;
190    }
191}