Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.17% covered (warning)
52.17%
60 / 115
47.37% covered (danger)
47.37%
9 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Critical_CSS_State
52.17% covered (warning)
52.17%
60 / 115
47.37% covered (danger)
47.37%
9 / 19
234.97
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
 clear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 set_error
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
2.50
 set_provider_error_dismissed
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 update_provider_state
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
3.05
 set_provider_errors
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 set_provider_payload_too_large
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 set_provider_success
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 maybe_set_generated
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 has_errors
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 get_error_message
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_generated
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_requesting
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 prepare_request
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 set_pending_providers
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 prepare_for_generation
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 has_pending_provider
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
4
5use WP_Error;
6
7class Critical_CSS_State {
8
9    const GENERATION_STATES = array(
10        'not_generated' => 'not_generated',
11        'pending'       => 'pending',
12        'generated'     => 'generated',
13        'error'         => 'error',
14    );
15
16    const PROVIDER_STATES = array(
17        'pending' => 'pending',
18        'success' => 'success',
19        'error'   => 'error',
20    );
21    public $state;
22
23    public function __construct() {
24        $this->state = jetpack_boost_ds_get( 'critical_css_state' );
25    }
26
27    public function clear() {
28        jetpack_boost_ds_delete( 'critical_css_state' );
29    }
30
31    public function save() {
32        $this->state['updated'] = microtime( true );
33        jetpack_boost_ds_set( 'critical_css_state', $this->state );
34
35        if ( $this->is_generated() ) {
36            /**
37             * Fires when critical CSS has successfully been generated.
38             */
39            do_action( 'jetpack_boost_critical_css_generated' );
40        }
41    }
42
43    public function set_error( $message ) {
44        if ( empty( $message ) ) {
45            error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
46                'Critical CSS: set_error() called with empty message'
47            );
48            return $this;
49        }
50
51        $this->state['status_error'] = $message;
52        $this->state['status']       = self::GENERATION_STATES['error'];
53
54        return $this;
55    }
56
57    public function set_provider_error_dismissed( $provider_key, $error_type, $dismissed ) {
58        if ( empty( $this->state['providers'] ) ) {
59            return new WP_Error( 'invalid_provider_key', 'No providers exist' );
60        }
61
62        $provider_index = array_search( $provider_key, array_column( $this->state['providers'], 'key' ), true );
63        if ( $provider_index === false ) {
64            return new WP_Error( 'invalid_provider_key', 'Invalid provider key' );
65        }
66
67        if ( ! isset( $this->state['providers'][ $provider_index ]['dismissed_errors'] ) ) {
68            $this->state['providers'][ $provider_index ]['dismissed_errors'] = array();
69        }
70
71        if ( $dismissed ) {
72            if ( ! in_array( $error_type, $this->state['providers'][ $provider_index ]['dismissed_errors'], true ) ) {
73                $this->state['providers'][ $provider_index ]['dismissed_errors'][] = $error_type;
74            }
75        } else {
76            $key = array_search( $error_type, $this->state['providers'][ $provider_index ]['dismissed_errors'], true );
77            if ( $key !== false ) {
78                unset( $this->state['providers'][ $provider_index ]['dismissed_errors'][ $key ] );
79                $this->state['providers'][ $provider_index ]['dismissed_errors'] = array_values( $this->state['providers'][ $provider_index ]['dismissed_errors'] );
80            }
81        }
82
83        return true;
84    }
85
86    /**
87     * Update a provider's state. The provider must already exist in the state to be updated.
88     *
89     * @param string $provider_key The provider key.
90     * @param array  $state        An array to overlay over the current state.
91     * @return bool|WP_Error True on success, WP_Error on failure.
92     */
93    private function update_provider_state( $provider_key, $state ) {
94        if ( empty( $this->state['providers'] ) ) {
95            return new WP_Error( 'invalid_provider_key', 'No providers exist' );
96        }
97
98        $provider_index = array_search( $provider_key, array_column( $this->state['providers'], 'key' ), true );
99        if ( $provider_index === false ) {
100            return new WP_Error( 'invalid_provider_key', 'Invalid provider key' );
101        }
102
103        $this->state['providers'][ $provider_index ] = array_merge(
104            $this->state['providers'][ $provider_index ],
105            $state
106        );
107
108        $this->maybe_set_generated();
109
110        return true;
111    }
112
113    /**
114     * Set a provider's state to error.
115     *
116     * @param string $provider_key The provider key.
117     * @param array  $errors        A list of errors to store with this provider.
118     * @return bool|WP_Error True on success, WP_Error on failure.
119     */
120    public function set_provider_errors( $provider_key, $errors ) {
121        return $this->update_provider_state(
122            $provider_key,
123            array(
124                'status' => self::PROVIDER_STATES['error'],
125                'errors' => $errors,
126            )
127        );
128    }
129
130    /**
131     * Report an oversized result for each URL in its provider.
132     *
133     * @since $$next-version$$
134     * @param string $provider_key The provider key.
135     * @return bool|WP_Error True on success, WP_Error on failure.
136     */
137    public function set_provider_payload_too_large( $provider_key ) {
138        if ( empty( $this->state['providers'] ) ) {
139            return new WP_Error( 'invalid_provider_key', 'No providers exist' );
140        }
141        $provider_index = array_search( $provider_key, array_column( $this->state['providers'], 'key' ), true );
142        if ( false === $provider_index ) {
143            return new WP_Error( 'invalid_provider_key', 'Invalid provider key' );
144        }
145        $errors = array();
146        foreach ( $this->state['providers'][ $provider_index ]['urls'] as $url ) {
147            $errors[] = array(
148                'url'     => $url,
149                'type'    => 'PayloadTooLargeError',
150                'message' => __( 'The generated Critical CSS exceeds the 512 KiB inline limit and was not saved.', 'jetpack-boost' ),
151            );
152        }
153        $this->state['providers'][ $provider_index ]['status'] = self::PROVIDER_STATES['error'];
154        $this->state['providers'][ $provider_index ]['errors'] = $errors;
155        $this->maybe_set_generated();
156        return true;
157    }
158
159    /**
160     * Set a provider's state to success.
161     *
162     * @param string $provider_key The provider key.
163     * @return bool|WP_Error True on success, WP_Error on failure.
164     */
165    public function set_provider_success( $provider_key ) {
166        return $this->update_provider_state(
167            $provider_key,
168            array(
169                'status' => self::PROVIDER_STATES['success'],
170            )
171        );
172    }
173
174    /**
175     * Set the state to generated if all providers are done. Should be called wherever
176     * a provider's state is updated.
177     */
178    private function maybe_set_generated() {
179        if ( empty( $this->state['providers'] ) ) {
180            return;
181        }
182
183        $provider_states = array_column( $this->state['providers'], 'status' );
184        $is_done         = ! in_array( self::GENERATION_STATES['pending'], $provider_states, true );
185
186        if ( $is_done ) {
187            $this->state['status'] = self::GENERATION_STATES['generated'];
188        }
189    }
190
191    public function has_errors() {
192        // Check if any of the providers have errors as well.
193        $any_provider_has_error = in_array(
194            'error',
195            array_unique(
196                wp_list_pluck(
197                    $this->state['providers'],
198                    'status'
199                )
200            ),
201            true
202        );
203
204        return self::GENERATION_STATES['error'] === $this->state['status'] || $any_provider_has_error;
205    }
206
207    public function get_error_message() {
208        return $this->state['status_error'] ?? null;
209    }
210
211    public function is_generated() {
212        return self::GENERATION_STATES['generated'] === $this->state['status'];
213    }
214
215    public function is_requesting() {
216        return self::GENERATION_STATES['pending'] === $this->state['status'];
217    }
218
219    public function prepare_request() {
220        $this->state = array(
221            'status'    => self::GENERATION_STATES['pending'],
222            'providers' => array(),
223            'created'   => microtime( true ),
224            'updated'   => microtime( true ),
225        );
226
227        return $this;
228    }
229
230    public function set_pending_providers( $providers ) {
231        foreach ( $providers as $key => $provider ) {
232            $providers[ $key ]['status'] = self::PROVIDER_STATES['pending'];
233        }
234        $this->state['providers'] = $providers;
235        return $this;
236    }
237
238    /**
239     * Add providers to the state, sets their status to pending
240     * and sets the generation status to pending.
241     *
242     * @param array $providers The providers to include in the state and set as pending.
243     * @return $this
244     */
245    public function prepare_for_generation( $providers ) {
246        $this->set_pending_providers( $providers );
247        $this->state['status'] = self::GENERATION_STATES['pending'];
248        return $this;
249    }
250
251    /**
252     * Get fresh state
253     */
254    public function get() {
255        $this->state = jetpack_boost_ds_get( 'critical_css_state' );
256        return $this->state;
257    }
258
259    public function has_pending_provider( $needles = array() ) {
260        if ( empty( $this->state['providers'] ) ) {
261            return false;
262        }
263
264        $providers = $this->state['providers'];
265        foreach ( $providers as $provider ) {
266            if (
267                ! empty( $provider['key'] )
268                && ! empty( $provider['status'] )
269                && self::PROVIDER_STATES['pending'] === $provider['status']
270                && in_array( $provider['key'], $needles, true )
271            ) {
272                return true;
273            }
274        }
275        return false;
276    }
277}