Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.42% covered (warning)
84.42%
65 / 77
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Rest_Controller
84.42% covered (warning)
84.42%
65 / 77
80.00% covered (warning)
80.00%
4 / 5
16.97
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register_rest_routes
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 stats_video_plays_args
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 permissions_callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_stats_video_plays
69.23% covered (warning)
69.23%
27 / 39
0.00% covered (danger)
0.00%
0 / 1
12.91
1<?php
2/**
3 * The VideoPress REST Controller.
4 *
5 * Registers the `/jetpack/v4/videopress/*` routes backing the
6 * modernized wp-build dashboard: a proxy to the WPCOM
7 * `sites/{id}/stats/video-plays` endpoint for the Overview screen's
8 * KPI / trends / top-N cards. Also registers the Playlists tab's
9 * `/wpcom/v3/videopress/playlists` routes ( see Playlist_Manager ).
10 *
11 * @package automattic/jetpack-videopress
12 */
13
14namespace Automattic\Jetpack\VideoPress;
15
16use Automattic\Jetpack\Connection\Client;
17use Jetpack_Options;
18use WP_Error;
19use WP_REST_Request;
20use WP_REST_Server;
21
22/**
23 * REST routes for the modernized VideoPress admin UI.
24 */
25class Rest_Controller {
26
27    /**
28     * REST namespace used by this package's modernization routes.
29     *
30     * @var string
31     */
32    const REST_NAMESPACE = 'jetpack/v4/videopress';
33
34    /**
35     * Hook the route registration on `rest_api_init`.
36     *
37     * @return void
38     */
39    public static function init() {
40        add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
41    }
42
43    /**
44     * Register the VideoPress REST routes.
45     *
46     * @return void
47     */
48    public static function register_rest_routes() {
49        register_rest_route(
50            self::REST_NAMESPACE,
51            '/stats/video-plays',
52            array(
53                'methods'             => WP_REST_Server::READABLE,
54                'callback'            => array( __CLASS__, 'get_stats_video_plays' ),
55                'permission_callback' => array( __CLASS__, 'permissions_callback' ),
56                'args'                => self::stats_video_plays_args(),
57            )
58        );
59
60        // WordPress.com registers the playlist routes through its REST plugin
61        // loader instead ( see WPCOM_REST_API_V3_Endpoint_VideoPress_Playlists ).
62        if ( ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
63            Playlist_Manager::register_rest_routes();
64        }
65        Transcript_Index::register_rest_routes();
66    }
67
68    /**
69     * Query params accepted by the video-plays proxy. Forwarded verbatim
70     * to WPCOM after permission and shape validation. `complete_stats` and
71     * `check_stats_module` are always forced by the callback and are
72     * therefore not exposed as incoming params.
73     *
74     * @return array
75     */
76    private static function stats_video_plays_args() {
77        return array(
78            'period'     => array(
79                'description' => __( 'Period unit: day, week, month, or year.', 'jetpack-videopress-pkg' ),
80                'type'        => 'string',
81                'enum'        => array( 'day', 'week', 'month', 'year' ),
82            ),
83            'num'        => array(
84                'description' => __( 'Number of periods to include.', 'jetpack-videopress-pkg' ),
85                'type'        => 'integer',
86                'minimum'     => 1,
87                'maximum'     => 365,
88            ),
89            'date'       => array(
90                'description' => __( 'Most recent day to include in results (YYYY-MM-DD).', 'jetpack-videopress-pkg' ),
91                'type'        => 'string',
92                'format'      => 'date',
93            ),
94            'start_date' => array(
95                'description' => __( 'Starting date for range queries (YYYY-MM-DD).', 'jetpack-videopress-pkg' ),
96                'type'        => 'string',
97                'format'      => 'date',
98            ),
99        );
100    }
101
102    /**
103     * Permission callback. Admin-gated. The upstream call is blog-signed,
104     * matching the existing `Stats::fetch_video_plays` path; no user-level
105     * WPCOM connection is required.
106     *
107     * @return bool
108     */
109    public static function permissions_callback() {
110        return current_user_can( 'manage_options' );
111    }
112
113    /**
114     * Proxy the video-plays stats endpoint.
115     *
116     * Forwards the allowed query params to WPCOM (REST v1.1, blog-signed
117     * — matching the existing `Stats::fetch_video_plays` path) and forces
118     * `complete_stats=true`. `check_stats_module=false` is also forced so the
119     * report loads for standalone VideoPress sites without the Jetpack Stats
120     * module active, matching `Stats::fetch_video_plays`. In complete-stats
121     * mode, each day entry carries
122     * `total.views`, `total.impressions`, and `total.watch_time` (in hours)
123     * plus a per-video `data[]` array whose entries have `post_id`, `title`,
124     * `views`, `impressions`, `watch_time` (hours), and `retention_rate`.
125     * The `plays` field is NOT returned in complete-stats mode.
126     *
127     * @param WP_REST_Request $request Incoming request.
128     * @return mixed Decoded JSON response from WPCOM, or WP_Error on failure.
129     */
130    public static function get_stats_video_plays( WP_REST_Request $request ) {
131        $blog_id = (int) Jetpack_Options::get_option( 'id' );
132        if ( ! $blog_id ) {
133            return new WP_Error(
134                'videopress_stats_not_connected',
135                esc_html__( 'This site is not connected to WordPress.com.', 'jetpack-videopress-pkg' ),
136                array( 'status' => 400 )
137            );
138        }
139
140        $params = array(
141            'complete_stats'     => 'true',
142            'check_stats_module' => 'false',
143        );
144        foreach ( array_keys( self::stats_video_plays_args() ) as $key ) {
145            $value = $request->get_param( $key );
146            if ( $value !== null && $value !== '' ) {
147                $params[ $key ] = $value;
148            }
149        }
150
151        $path     = sprintf(
152            'sites/%d/stats/video-plays?%s',
153            $blog_id,
154            http_build_query( $params )
155        );
156        $response = Client::wpcom_json_api_request_as_blog( $path );
157
158        if ( is_wp_error( $response ) ) {
159            return new WP_Error(
160                'videopress_stats_request_failed',
161                $response->get_error_message(),
162                array( 'status' => 500 )
163            );
164        }
165
166        $status = (int) wp_remote_retrieve_response_code( $response );
167        $body   = json_decode( wp_remote_retrieve_body( $response ), true );
168
169        if ( 200 !== $status ) {
170            $message = is_array( $body ) && isset( $body['message'] )
171                ? (string) $body['message']
172                : esc_html__( 'Unable to fetch VideoPress stats.', 'jetpack-videopress-pkg' );
173            return new WP_Error(
174                'videopress_stats_request_failed',
175                $message,
176                array( 'status' => $status ? $status : 500 )
177            );
178        }
179
180        return rest_ensure_response( $body );
181    }
182}