Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
37.78% covered (danger)
37.78%
150 / 397
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_REST_API_V2_Endpoint_VideoPress
37.66% covered (danger)
37.66%
148 / 393
11.11% covered (danger)
11.11%
1 / 9
787.90
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 register_routes
96.67% covered (success)
96.67%
145 / 150
0.00% covered (danger)
0.00%
0 / 1
5
 videopress_video_belong_to_site
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 wpcom_poster_request
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 videopress_block_update_poster
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 videopress_block_get_poster
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 videopress_upload_jwt
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 videopress_playback_jwt
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 videopress_block_update_meta
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 1
930
1<?php
2/**
3 * REST API endpoint for managing VideoPress metadata.
4 *
5 * @package automattic/jetpack
6 * @since-jetpack 9.3.0
7 * @since 0.1.3
8 */
9
10namespace Automattic\Jetpack\VideoPress;
11
12use Automattic\Jetpack\Connection\Client;
13use Automattic\Jetpack\Constants;
14use WP_Error;
15use WP_REST_Controller;
16use WP_REST_Request;
17use WP_REST_Response;
18use WP_REST_Server;
19
20if ( ! defined( 'ABSPATH' ) ) {
21    exit( 0 );
22}
23
24/**
25 * VideoPress wpcom api v2 endpoint
26 *
27 * @phan-constructor-used-for-side-effects
28 */
29class WPCOM_REST_API_V2_Endpoint_VideoPress extends WP_REST_Controller {
30    /**
31     * Constructor.
32     */
33    public function __construct() {
34        $this->namespace = 'wpcom/v2';
35        $this->rest_base = 'videopress';
36
37        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
38    }
39
40    /**
41     * Register the route.
42     */
43    public function register_routes() {
44        // Meta Route.
45        register_rest_route(
46            $this->namespace,
47            $this->rest_base . '/meta',
48            array(
49                'args'                => array(
50                    'id'              => array(
51                        'description' => __( 'The post id for the attachment.', 'jetpack-videopress-pkg' ),
52                        'type'        => 'integer',
53                        'required'    => true,
54                    ),
55                    'title'           => array(
56                        'description'       => __( 'The title of the video.', 'jetpack-videopress-pkg' ),
57                        'type'              => 'string',
58                        'sanitize_callback' => 'sanitize_text_field',
59                    ),
60                    'description'     => array(
61                        'description'       => __( 'The description of the video.', 'jetpack-videopress-pkg' ),
62                        'type'              => 'string',
63                        'sanitize_callback' => 'sanitize_textarea_field',
64                    ),
65                    'caption'         => array(
66                        'description'       => __( 'The caption of the video.', 'jetpack-videopress-pkg' ),
67                        'type'              => 'string',
68                        'sanitize_callback' => 'sanitize_textarea_field',
69                    ),
70                    'rating'          => array(
71                        'description'       => __( 'The video content rating. One of G, PG-13 or R-17', 'jetpack-videopress-pkg' ),
72                        'type'              => 'string',
73                        'sanitize_callback' => 'sanitize_text_field',
74                    ),
75                    'display_embed'   => array(
76                        'description' => __( 'Display the share menu in the player.', 'jetpack-videopress-pkg' ),
77                        'type'        => 'boolean',
78                    ),
79                    'allow_download'  => array(
80                        'description' => __( 'Display download option and allow viewers to download this video', 'jetpack-videopress-pkg' ),
81                        'type'        => 'boolean',
82                    ),
83                    'privacy_setting' => array(
84                        'description' => __( 'How to determine if the video should be public or private', 'jetpack-videopress-pkg' ),
85                        'type'        => 'integer',
86                        'enum'        => array(
87                            \VIDEOPRESS_PRIVACY::IS_PUBLIC,
88                            \VIDEOPRESS_PRIVACY::IS_PRIVATE,
89                            \VIDEOPRESS_PRIVACY::SITE_DEFAULT,
90                        ),
91                    ),
92                ),
93                'methods'             => WP_REST_Server::EDITABLE,
94                'callback'            => array( $this, 'videopress_block_update_meta' ),
95                'permission_callback' => function () {
96                    return Data::can_perform_action() && current_user_can( 'edit_posts' );
97                },
98            )
99        );
100
101        // Poster Route.
102        register_rest_route(
103            $this->namespace,
104            $this->rest_base . '/(?P<video_guid>[A-Za-z0-9]{8})/poster',
105            array(
106                'args' => array(
107                    'video_guid' => array(
108                        'description' => __( 'The VideoPress GUID.', 'jetpack-videopress-pkg' ), // @phan-suppress-current-line PhanPluginMixedKeyNoKey
109                        'type'        => 'string',
110                        'required'    => true,
111                    ),
112                ),
113                array(
114                    'methods'             => WP_REST_Server::READABLE,
115                    'callback'            => array( $this, 'videopress_block_get_poster' ),
116                    'permission_callback' => function () {
117                        return current_user_can( 'read' );
118                    },
119                ),
120                array(
121                    'args'                => array(
122                        'at_time'              => array(
123                            'description' => __( 'The time in the video to use as the poster frame.', 'jetpack-videopress-pkg' ),
124                            'type'        => 'integer',
125                        ),
126                        'is_millisec'          => array(
127                            'description' => __( 'Whether the time is in milliseconds or seconds.', 'jetpack-videopress-pkg' ),
128                            'type'        => 'boolean',
129                        ),
130                        'poster_attachment_id' => array(
131                            'description' => __( 'The attachment id of the poster image.', 'jetpack-videopress-pkg' ),
132                            'type'        => 'integer',
133                        ),
134                    ),
135                    'methods'             => WP_REST_Server::EDITABLE,
136                    'callback'            => array( $this, 'videopress_block_update_poster' ),
137                    'permission_callback' => function () {
138                        return Data::can_perform_action() && current_user_can( 'upload_files' );
139                    },
140                ),
141            )
142        );
143
144        // Endpoint to know if the video metadata is editable.
145        register_rest_route(
146            $this->namespace,
147            $this->rest_base . '/(?P<video_guid>[A-Za-z0-9]{8})/check-ownership/(?P<post_id>\d+)/',
148            array(
149                'args' => array(
150                    'video_guid' => array(
151                        'description' => __( 'The VideoPress GUID.', 'jetpack-videopress-pkg' ), // @phan-suppress-current-line PhanPluginMixedKeyNoKey
152                        'type'        => 'string',
153                        'required'    => true,
154                    ),
155                    'post_id'    => array(
156                        'description' => __( 'The post id for the attachment.', 'jetpack-videopress-pkg' ),
157                        'type'        => 'integer',
158                        'required'    => true,
159                    ),
160                ),
161                array(
162                    'methods'             => WP_REST_Server::READABLE,
163                    'callback'            => array( $this, 'videopress_video_belong_to_site' ),
164                    'permission_callback' => function () {
165                        return Data::can_perform_action() && current_user_can( 'upload_files' );
166                    },
167                ),
168            )
169        );
170
171        // Token Route.
172        register_rest_route(
173            $this->namespace,
174            $this->rest_base . '/upload-jwt',
175            array(
176                'methods'             => \WP_REST_Server::EDITABLE,
177                'callback'            => array( $this, 'videopress_upload_jwt' ),
178                'permission_callback' => function () {
179                    return Data::can_perform_action() && current_user_can( 'upload_files' );
180                },
181            )
182        );
183
184        // Playback Token Route.
185        register_rest_route(
186            $this->namespace,
187            $this->rest_base . '/playback-jwt/(?P<video_guid>[A-Za-z0-9]{8})',
188            array(
189                'args'                => array(
190                    'video_guid' => array(
191                        'description' => __( 'The VideoPress GUID.', 'jetpack-videopress-pkg' ),
192                        'type'        => 'string',
193                        'required'    => true,
194                    ),
195                ),
196                'methods'             => \WP_REST_Server::EDITABLE,
197                'callback'            => array( $this, 'videopress_playback_jwt' ),
198                'permission_callback' => function () {
199                    return current_user_can( 'read' );
200                },
201            )
202        );
203    }
204
205    /**
206     * Check whether the video belongs to the current site,
207     * considering the given post_id and the video_guid.
208     *
209     * @param WP_REST_Request $request The request object.
210     * @return WP_REST_Response True if the video belongs to the current site, false otherwise.
211     */
212    public function videopress_video_belong_to_site( $request ) {
213        $post_id    = $request->get_param( 'post_id' );
214        $video_guid = $request->get_param( 'video_guid' );
215
216        if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
217            $found_guid = get_post_meta( $post_id, 'videopress_guid', true );
218        } else {
219            $blog_id    = get_current_blog_id();
220            $info       = video_get_info_by_blogpostid( $blog_id, $post_id );
221            $found_guid = $info ? $info->guid : '';
222        }
223
224        if ( ! $found_guid ) {
225            return rest_ensure_response( array( 'video-belong-to-site' => false ) );
226        }
227
228        return rest_ensure_response( array( 'video-belong-to-site' => $found_guid === $video_guid ) );
229    }
230
231    /**
232     * Hit WPCOM poster endpoint.
233     *
234     * @param string $video_guid  The VideoPress GUID.
235     * @param array  $args        Request args.
236     * @param array  $body        Request body.
237     * @param string $query       Request query.
238     * @return WP_REST_Response|WP_Error
239     */
240    public function wpcom_poster_request( $video_guid, $args, $body = null, $query = '' ) {
241        $query    = $query !== '' ? '?' . $query : '';
242        $endpoint = 'videos/' . $video_guid . '/poster' . $query;
243
244        $url = sprintf(
245            '%s/%s/v%s/%s',
246            Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
247            'rest',
248            '1.1',
249            $endpoint
250        );
251
252        $request_args = array_merge( $args, array( 'body' => $body ) );
253
254        // @phan-suppress-next-line PhanAccessMethodInternal -- Phan is correct, but the usage is intentional.
255        $result = Client::_wp_remote_request( $url, $request_args );
256
257        if ( is_wp_error( $result ) ) {
258            return rest_ensure_response( $result );
259        }
260
261        $response = $result['http_response'];
262
263        $status = $response->get_status();
264
265        $data = array(
266            'code' => $status,
267            'data' => json_decode( $response->get_data(), true ),
268        );
269
270        return rest_ensure_response(
271            new WP_REST_Response( $data, $status )
272        );
273    }
274
275    /**
276     * Update the a poster image via the WPCOM REST API.
277     *
278     * @param WP_REST_Request $request The request object.
279     * @return WP_REST_Response|WP_Error
280     */
281    public function videopress_block_update_poster( $request ) {
282        try {
283            $blog_id     = VideoPressToken::blog_id();
284            $token       = VideoPressToken::videopress_onetime_upload_token();
285            $video_guid  = $request->get_param( 'video_guid' );
286            $json_params = $request->get_json_params();
287
288            $args = array(
289                'method'  => 'POST',
290                'headers' => array(
291                    'content-type'  => 'application/json',
292                    'Authorization' => 'X_UPLOAD_TOKEN token="' . $token . '" blog_id="' . $blog_id . '"',
293                ),
294            );
295
296            return $this->wpcom_poster_request(
297                $video_guid,
298                $args,
299                wp_json_encode( $json_params, JSON_UNESCAPED_SLASHES )
300            );
301        } catch ( \Exception $e ) {
302            return rest_ensure_response( new WP_Error( 'videopress_block_update_poster_error', $e->getMessage() ) );
303        }
304    }
305
306    /**
307     * Retrieves a poster image via the WPCOM REST API.
308     *
309     * @param WP_REST_Request $request the request object.
310     * @return object|WP_Error Success object or WP_Error with error details.
311     */
312    public function videopress_block_get_poster( $request ) {
313        $video_guid = $request->get_param( 'video_guid' );
314        $jwt        = VideoPressToken::videopress_playback_jwt( $video_guid );
315
316        $args = array(
317            'method' => 'GET',
318        );
319
320        return $this->wpcom_poster_request(
321            $video_guid,
322            $args,
323            null,
324            'metadata_token=' . $jwt
325        );
326    }
327
328    /**
329     * Endpoint for getting the VideoPress Upload JWT
330     *
331     * @return WP_Rest_Response - The response object.
332     */
333    public static function videopress_upload_jwt() {
334        $has_connected_owner = Data::has_connected_owner();
335        if ( ! $has_connected_owner ) {
336            return rest_ensure_response(
337                new WP_Error(
338                    'owner_not_connected',
339                    'User not connected.',
340                    array(
341                        'code'        => 503,
342                        'connect_url' => Admin_UI::get_admin_page_url(),
343                    )
344                )
345            );
346        }
347
348        $blog_id = Data::get_blog_id();
349        if ( ! $blog_id ) {
350            return rest_ensure_response(
351                new WP_Error( 'site_not_registered', 'Site not registered.', 503 )
352            );
353        }
354
355        try {
356            $token  = VideoPressToken::videopress_upload_jwt();
357            $status = 200;
358            $data   = array(
359                'upload_token'   => $token,
360                'upload_url'     => videopress_make_resumable_upload_path( $blog_id ),
361                'upload_blog_id' => $blog_id,
362            );
363        } catch ( \Exception $e ) {
364            $status = 500;
365            $data   = array(
366                'error' => $e->getMessage(),
367            );
368
369        }
370
371        return rest_ensure_response(
372            new WP_REST_Response( $data, $status )
373        );
374    }
375
376    /**
377     * Endpoint for generating a VideoPress Playback JWT
378     *
379     * @param WP_REST_Request $request the request object.
380     * @return WP_Rest_Response - The response object.
381     */
382    public static function videopress_playback_jwt( $request ) {
383        $has_connected_owner = Data::has_connected_owner();
384        if ( ! $has_connected_owner ) {
385            return rest_ensure_response(
386                new WP_Error(
387                    'owner_not_connected',
388                    'User not connected.',
389                    array(
390                        'code'        => 503,
391                        'connect_url' => Admin_UI::get_admin_page_url(),
392                    )
393                )
394            );
395        }
396
397        $blog_id = Data::get_blog_id();
398        if ( ! $blog_id ) {
399            return rest_ensure_response(
400                new WP_Error( 'site_not_registered', 'Site not registered.', 503 )
401            );
402        }
403
404        try {
405            $video_guid = $request->get_param( 'video_guid' );
406            $token      = VideoPressToken::videopress_playback_jwt( $video_guid );
407            $status     = 200;
408            $data       = array(
409                'playback_token' => $token,
410            );
411        } catch ( \Exception $e ) {
412            $status = 500;
413            $data   = array(
414                'error' => $e->getMessage(),
415            );
416
417        }
418
419        return rest_ensure_response(
420            new WP_REST_Response( $data, $status )
421        );
422    }
423
424    /**
425     * Updates attachment meta and video metadata via the WPCOM REST API.
426     *
427     * @param WP_REST_Request $request the request object.
428     * @return object|WP_Error Success object or WP_Error with error details.
429     */
430    public function videopress_block_update_meta( $request ) {
431        $json_params = $request->get_json_params();
432        $post_id     = $json_params['id'];
433
434        if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
435            $guid = get_post_meta( $post_id, 'videopress_guid', true );
436        } else {
437            $blog_id = get_current_blog_id();
438            $info    = video_get_info_by_blogpostid( $blog_id, $post_id );
439            $guid    = $info ? $info->guid : '';
440        }
441
442        if ( ! $guid ) {
443            return rest_ensure_response(
444                new WP_Error(
445                    'error',
446                    __( 'This attachment cannot be updated yet.', 'jetpack-videopress-pkg' )
447                )
448            );
449        }
450
451        $video_request_params = $json_params;
452        unset( $video_request_params['id'] );
453        $video_request_params['guid'] = $guid;
454
455        $endpoint = 'videos';
456        $args     = array(
457            'method'  => 'POST',
458            'headers' => array( 'content-type' => 'application/json' ),
459        );
460
461        $result = Client::wpcom_json_api_request_as_blog(
462            $endpoint,
463            '2',
464            $args,
465            wp_json_encode( $video_request_params, JSON_UNESCAPED_SLASHES ),
466            'wpcom'
467        );
468
469        if ( is_wp_error( $result ) ) {
470            return rest_ensure_response( $result );
471        }
472
473        $response_body = json_decode( wp_remote_retrieve_body( $result ) );
474        if ( is_bool( $response_body ) && $response_body ) {
475            /*
476             * Title, description and caption of the video are not stored as metadata on the attachment,
477             * but as post_content, post_title and post_excerpt on the attachment's post object.
478             * We need to update those fields here, too.
479             */
480            $post_title = null;
481            if ( isset( $json_params['title'] ) ) {
482                $post_title = sanitize_text_field( $json_params['title'] );
483                wp_update_post(
484                    array(
485                        'ID'         => $post_id,
486                        'post_title' => $post_title,
487                    )
488                );
489            }
490
491            $post_content = null;
492            if ( isset( $json_params['description'] ) ) {
493                $post_content = sanitize_textarea_field( $json_params['description'] );
494                wp_update_post(
495                    array(
496                        'ID'           => $post_id,
497                        'post_content' => $post_content,
498                    )
499                );
500            }
501
502            $post_excerpt = null;
503            if ( isset( $json_params['caption'] ) ) {
504                $post_excerpt = sanitize_textarea_field( $json_params['caption'] );
505                wp_update_post(
506                    array(
507                        'ID'           => $post_id,
508                        'post_excerpt' => $post_excerpt,
509                    )
510                );
511            }
512
513            // VideoPress data is stored in attachment meta for Jetpack sites, but not on wpcom.
514            if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
515                $meta               = wp_get_attachment_metadata( $post_id );
516                $should_update_meta = false;
517
518                if ( ! $meta ) {
519                    return rest_ensure_response(
520                        new WP_Error(
521                            'error',
522                            __( 'Attachment meta was not found.', 'jetpack-videopress-pkg' )
523                        )
524                    );
525                }
526
527                if ( isset( $json_params['display_embed'] ) && isset( $meta['videopress']['display_embed'] ) ) {
528                    $meta['videopress']['display_embed'] = $json_params['display_embed'];
529                    $should_update_meta                  = true;
530                }
531
532                if ( isset( $json_params['rating'] ) && isset( $meta['videopress']['rating'] ) && videopress_is_valid_video_rating( $json_params['rating'] ) ) {
533                    $meta['videopress']['rating'] = $json_params['rating'];
534                    $should_update_meta           = true;
535
536                    /** Set a new meta field so we can filter using it directly */
537                    update_post_meta( $post_id, 'videopress_rating', $json_params['rating'] );
538                }
539
540                if ( isset( $json_params['title'] ) ) {
541                    $meta['videopress']['title'] = $post_title;
542                    $should_update_meta          = true;
543                }
544
545                if ( isset( $json_params['description'] ) ) {
546                    $meta['videopress']['description'] = $post_content;
547                    $should_update_meta                = true;
548                }
549
550                if ( isset( $json_params['caption'] ) ) {
551                    $meta['videopress']['caption'] = $post_excerpt;
552                    $should_update_meta            = true;
553                }
554
555                if ( isset( $json_params['poster'] ) ) {
556                    $meta['videopress']['poster'] = $json_params['poster'];
557                    $should_update_meta           = true;
558                }
559
560                if ( isset( $json_params['allow_download'] ) ) {
561                    $allow_download = (bool) $json_params['allow_download'];
562                    if ( ! isset( $meta['videopress']['allow_download'] ) || $meta['videopress']['allow_download'] !== $allow_download ) {
563                        $meta['videopress']['allow_download'] = $allow_download;
564                        $should_update_meta                   = true;
565                    }
566                }
567
568                if ( isset( $json_params['privacy_setting'] ) ) {
569                    $privacy_setting = $json_params['privacy_setting'];
570                    if ( ! isset( $meta['videopress']['privacy_setting'] ) || $meta['videopress']['privacy_setting'] !== $privacy_setting ) {
571                        $meta['videopress']['privacy_setting'] = $privacy_setting;
572                        $should_update_meta                    = true;
573
574                        /** Set a new meta field so we can filter using it directly */
575                        update_post_meta( $post_id, 'videopress_privacy_setting', $privacy_setting );
576                    }
577                }
578
579                if ( $should_update_meta ) {
580                    wp_update_attachment_metadata( $post_id, $meta );
581                }
582            }
583
584            return rest_ensure_response(
585                array(
586                    'code'    => 'success',
587                    'message' => __( 'Video meta updated successfully.', 'jetpack-videopress-pkg' ),
588                    'data'    => 200,
589                )
590            );
591        } else {
592            return rest_ensure_response(
593                new WP_Error(
594                    $response_body->code,
595                    $response_body->message,
596                    $response_body->data
597                )
598            );
599        }
600    }
601}
602
603if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
604    wpcom_rest_api_v2_load_plugin( 'Automattic\Jetpack\VideoPress\WPCOM_REST_API_V2_Endpoint_VideoPress' );
605}