Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
7.50% covered (danger)
7.50%
3 / 40
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Update_LCP
7.50% covered (danger)
7.50%
3 / 40
25.00% covered (danger)
25.00%
1 / 4
274.43
0.00% covered (danger)
0.00%
0 / 1
 name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 request_methods
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 response
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
240
 permissions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Save generated LCP data.
4 *
5 * This endpoint is used by WP.com to push the generated LCP data to the boost plugin.
6 */
7
8namespace Automattic\Jetpack_Boost\REST_API\Endpoints;
9
10use Automattic\Jetpack_Boost\Modules\Optimizations\Lcp\LCP_State;
11use Automattic\Jetpack_Boost\Modules\Optimizations\Lcp\LCP_Storage;
12use Automattic\Jetpack_Boost\REST_API\Contracts\Endpoint;
13use Automattic\Jetpack_Boost\REST_API\Permissions\Signed_With_Blog_Token;
14use WP_REST_Server;
15
16/**
17 * Handler for POST lcp/update. Expects the following body params:
18 * - success: boolean - False if the whole LCP job failed.
19 * - message: string - Error message if success is false.
20 * - data: object - All results from the LCP job:
21 *
22 * Each data key contains:
23 * - key: string - The key of the page.
24 * - url: string - The URL of the page.
25 * - devices: object - The LCP data for both mobile and desktop.
26 *
27 * Each device key contains:
28 * - success: boolean - False if this device key failed.
29 * - element: string - The selector of the LCP element.
30 * - type: string - The type of the LCP element. Either 'img' or 'background-image'.
31 * - url: string - Only for 'img' elements. The URL of LCP element.
32 * - html: string - The HTML of the LCP element.
33 * - report: object - The full report of the LCP element.
34 */
35class Update_LCP implements Endpoint {
36
37    public function name() {
38        return 'lcp/update';
39    }
40
41    public function request_methods() {
42        return WP_REST_Server::EDITABLE;
43    }
44
45    public function response( $request ) {
46        $state          = new LCP_State();
47        $storage        = new LCP_Storage();
48        $params         = $request->get_params();
49        $pages          = empty( $params['data'] ) || ! is_array( $params['data'] ) ? array() : $params['data'];
50        $api_successful = array( 'success' => true );
51
52        // If success is false, the whole LCP generation process failed.
53        if ( empty( $params['success'] ) ) {
54            if ( empty( $params['message'] ) || ! is_string( $params['message'] ) ) {
55                $error = __( 'An unknown error occurred', 'jetpack-boost' );
56            } else {
57                $error = $params['message'];
58            }
59
60            $state->set_error( $error );
61            $state->save();
62
63            return $api_successful;
64        }
65
66        $update_errors = array();
67        $applied       = 0;
68        foreach ( $pages as $entry ) {
69            if ( $entry['success'] ) {
70                $result = $state->set_page_success( $entry['key'] );
71            } else {
72                $errors = array();
73                foreach ( $entry['reports'] as $report ) {
74                    if ( isset( $report['success'] ) && false === $report['success'] && ! empty( $report['data'] ) ) {
75                        $errors[] = $report['data'];
76                    }
77                }
78
79                $result = $state->set_page_errors( $entry['key'], $errors );
80            }
81
82            if ( is_wp_error( $result ) ) {
83                // Strip CR/LF from the cloud-controlled key so a malicious signed payload can't
84                // forge extra log lines (CWE-117).
85                $safe_key        = str_replace( array( "\r", "\n" ), ' ', (string) $entry['key'] );
86                $update_errors[] = $safe_key . ': ' . $result->get_error_message();
87                continue;
88            }
89
90            ++$applied;
91
92            // Persist the raw LCP reports only for a key that actually applied to the state. Storing
93            // a rejected key (a page removed from the cornerstone list, or a late/duplicate callback
94            // after a reset) would orphan reports in jb_store_lcp that the render path later serves
95            // with no matching lcp_state entry to gate them.
96            $storage->store_lcp( $entry['key'], $entry['reports'] );
97
98            // Failures must have an array of urls.
99            // @TODO: figure out what to do with failures.
100        }
101
102        if ( ! empty( $update_errors ) ) {
103            error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
104                'Jetpack Boost: LCP update could not apply results to the stored state: ' . implode( '; ', $update_errors )
105            );
106        }
107
108        // Only persist the aggregate state when at least one result actually applied. If every
109        // update failed (e.g. the stored state was reset to the not_analyzed fallback and has no
110        // matching pages), saving would just re-persist that empty state over the good data.
111        if ( $applied > 0 ) {
112            $state->save();
113        }
114
115        return $api_successful;
116    }
117
118    public function permissions() {
119        return array(
120            new Signed_With_Blog_Token(),
121        );
122    }
123}