Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.00% covered (success)
96.00%
72 / 75
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Activity_Log_Bridge
98.63% covered (success)
98.63%
72 / 73
50.00% covered (danger)
50.00%
1 / 2
7
0.00% covered (danger)
0.00%
0 / 1
 register_routes
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
1
 get_activity_log
97.22% covered (success)
97.22%
35 / 36
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Activity log REST bridge — proxies /sites/{id}/activity/rewindable.
4 *
5 * @package automattic/jetpack-backup-plugin
6 */
7
8namespace Automattic\Jetpack\Backup\V0005\REST;
9
10use Automattic\Jetpack\Connection\Client;
11use WP_Error;
12use WP_REST_Request;
13use WP_REST_Server;
14
15if ( ! defined( 'ABSPATH' ) ) {
16    exit( 0 );
17}
18
19/**
20 * Powers the modernized Overview's activity list. Forwards the raw
21 * WPCOM rewindable-activity response; the React layer's
22 * `normalize/activity-log.ts` converts entries to `ActivityItem` shape.
23 */
24class Activity_Log_Bridge {
25
26    /**
27     * Register routes.
28     *
29     * @return void
30     */
31    public static function register_routes() {
32        // NOTE: not `/activity-log` — that namespace is owned by the
33        // `automattic/jetpack-activity-log` package's REST controller,
34        // which is loaded inside the Jetpack plugin. Sharing the route
35        // would let `register_rest_route`'s endpoint-merge pick the
36        // wrong handler at dispatch time (the two bridges proxy
37        // different WPCOM endpoints with different response shapes).
38        register_rest_route(
39            'jetpack/v4',
40            '/site/rewindable-activity',
41            array(
42                'methods'             => WP_REST_Server::READABLE,
43                'callback'            => array( __CLASS__, 'get_activity_log' ),
44                'permission_callback' => array( Rest_Controller::class, 'permission_check' ),
45                // Bounds mirror what WPCOM's own `get_stream_args()`
46                // validate_callback enforces on `/activity/rewindable`, and
47                // the sibling `jetpack-activity-log` package's declaration.
48                // Declaring them here turns an out-of-range value into a 400
49                // with a useful message instead of a round-trip that WPCOM
50                // rejects.
51                'args'                => array(
52                    'number'     => array(
53                        'description' => __( 'Number of items to return per page.', 'jetpack-backup-pkg' ),
54                        'type'        => 'integer',
55                        'minimum'     => 1,
56                        'maximum'     => 1000,
57                    ),
58                    'page'       => array(
59                        'description' => __( '1-indexed page number.', 'jetpack-backup-pkg' ),
60                        'type'        => 'integer',
61                        'minimum'     => 1,
62                    ),
63                    // Direction only: WPCOM's `get_stream_args()` sorts on `ts_utc`
64                    // and takes no field, so there is no `orderby` to declare.
65                    'sort_order' => array(
66                        'description' => __( 'Sort direction.', 'jetpack-backup-pkg' ),
67                        'type'        => 'string',
68                        'enum'        => array( 'asc', 'desc' ),
69                    ),
70                    // `format` makes the REST server refuse anything `rest_parse_date()` can't read.
71                    'after'      => array(
72                        'description' => __( 'Only activity after this ISO 8601 date-time.', 'jetpack-backup-pkg' ),
73                        'type'        => 'string',
74                        'format'      => 'date-time',
75                    ),
76                    'before'     => array(
77                        'description' => __( 'Only activity before this ISO 8601 date-time.', 'jetpack-backup-pkg' ),
78                        'type'        => 'string',
79                        'format'      => 'date-time',
80                    ),
81                ),
82            )
83        );
84    }
85
86    /**
87     * Proxy the WPCOM rewindable activity log.
88     *
89     * Forwards `number` (per page) and `page` (1-indexed) so the dashboard
90     * paginates against the real server response — `totalItems` /
91     * `totalPages` come back in the WPCOM envelope and drive DataViews'
92     * pagination footer. Same param shape the `automattic/jetpack-activity-log`
93     * package uses against the parent `/sites/{id}/activity` endpoint.
94     *
95     * `sort_order` is forwarded too, so the list's Order control reorders
96     * the real result set rather than the ten rows already on screen.
97     * Omitted when absent: WPCOM defaults it to `desc`.
98     *
99     * `after` and `before` narrow it to a time window, so the Overview can
100     * fetch a backup that isn't on any page it has loaded.
101     *
102     * @param WP_REST_Request $request The REST request.
103     * @return \WP_REST_Response|WP_Error
104     */
105    public static function get_activity_log( WP_REST_Request $request ) {
106        $blog_id = Rest_Controller::get_blog_id_or_error();
107        if ( is_wp_error( $blog_id ) ) {
108            return $blog_id;
109        }
110
111        $query = array_filter(
112            array(
113                'number'     => $request->get_param( 'number' ),
114                'page'       => $request->get_param( 'page' ),
115                'sort_order' => $request->get_param( 'sort_order' ),
116                'after'      => $request->get_param( 'after' ),
117                'before'     => $request->get_param( 'before' ),
118                // Activity summaries are rendered by WPCOM. Without this they
119                // come back in English whatever the reader's language is.
120                '_locale'    => get_user_locale(),
121            ),
122            static function ( $value ) {
123                return null !== $value && '' !== $value;
124            }
125        );
126
127        $endpoint = sprintf( '/sites/%d/activity/rewindable', $blog_id );
128        if ( ! empty( $query ) ) {
129            $endpoint .= '?' . http_build_query( $query );
130        }
131
132        $response = Client::wpcom_json_api_request_as_user(
133            $endpoint,
134            'v2',
135            array(),
136            null,
137            'wpcom'
138        );
139
140        if ( is_wp_error( $response ) ) {
141            return Rest_Controller::transport_error( $response, 'activity_log_fetch_failed' );
142        }
143
144        // Cast because `wp_remote_retrieve_response_code()` hands back
145        // whatever the transport put there, and a numeric string fails the
146        // strict comparison below — routing a perfectly good response into
147        // the failure branch, where `upstream_error()`'s clamp then reports
148        // it as a 500. Same reasoning at every bridge; the long version is
149        // on `Rest_Controller::upstream_error()`.
150        $status_code = (int) wp_remote_retrieve_response_code( $response );
151        if ( 200 !== $status_code ) {
152            return Rest_Controller::upstream_error(
153                $response,
154                'activity_log_fetch_failed',
155                __( 'Could not fetch the site activity log.', 'jetpack-backup-pkg' )
156            );
157        }
158
159        return rest_ensure_response( json_decode( wp_remote_retrieve_body( $response ), true ) );
160    }
161}