Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
33.64% covered (danger)
33.64%
37 / 110
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_REST_API_V2_Attachment_VideoPress_Data
33.33% covered (danger)
33.33%
36 / 108
40.00% covered (danger)
40.00%
4 / 10
442.63
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 register_fields
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 add_jetpack_videopress_custom_query_filters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 filter_attachments_by_jetpack_videopress_fields
31.43% covered (danger)
31.43%
11 / 35
0.00% covered (danger)
0.00%
0 / 1
42.24
 get_schema
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 get
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 get_videopress_data
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
 is_video
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 remove_field_for_non_videos
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 video_is_private
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Extend the REST API functionality for VideoPress users.
4 *
5 * @package automattic/jetpack-videopress
6 * @since-jetpack 7.1.0
7 * @since 0.3.1
8 */
9
10namespace Automattic\Jetpack\VideoPress;
11
12use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
13use WP_Post;
14use WP_REST_Request;
15use WP_REST_Response;
16
17/**
18 * Add per-attachment VideoPress data.
19 *
20 * { # Attachment Object
21 *   ...
22 *   jetpack_videopress: (object) VideoPress data
23 *   ...
24 * }
25 *
26 * @since 7.1.0
27 *
28 * @phan-constructor-used-for-side-effects
29 */
30class WPCOM_REST_API_V2_Attachment_VideoPress_Data {
31    /**
32     * The REST Object Type to which the jetpack_videopress field will be added.
33     *
34     * @var string
35     */
36    protected $object_type = 'attachment';
37
38    /**
39     * The name of the REST API field to add.
40     *
41     * @var string $field_name
42     */
43    protected $field_name = 'jetpack_videopress';
44
45    /**
46     * Constructor.
47     */
48    public function __construct() {
49        add_action( 'rest_api_init', array( $this, 'register_fields' ) );
50
51        if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
52            add_action( 'rest_api_init', array( $this, 'add_jetpack_videopress_custom_query_filters' ) );
53        }
54
55        // do this again later to collect any CPTs that get registered later.
56        add_action( 'restapi_theme_init', array( $this, 'register_fields' ), 20 );
57    }
58
59    /**
60     * Registers the jetpack_videopress field and adds a filter to remove it for attachments that are not videos.
61     */
62    public function register_fields() {
63        global $wp_rest_additional_fields;
64
65        if ( ! empty( $wp_rest_additional_fields[ $this->object_type ][ $this->field_name ] ) ) {
66            return;
67        }
68
69        register_rest_field(
70            $this->object_type,
71            $this->field_name,
72            array(
73                'get_callback'    => array( $this, 'get' ),
74                'update_callback' => null,
75                'schema'          => $this->get_schema(),
76            )
77        );
78
79        add_filter( 'rest_prepare_attachment', array( $this, 'remove_field_for_non_videos' ), 10, 2 );
80    }
81
82    /**
83     * Adds the custom query filters
84     */
85    public function add_jetpack_videopress_custom_query_filters() {
86        add_filter( 'rest_attachment_query', array( $this, 'filter_attachments_by_jetpack_videopress_fields' ), 999, 2 );
87    }
88
89    /**
90     * Filter request args to handle the custom VideoPress query filters
91     *
92     * Possible filters:
93     *
94     * `no_videopress`: the returned attachments should not have a videopress_guid
95     *
96     * @param array           $args The original list of args before the filtering.
97     * @param WP_REST_Request $request The original request data.
98     */
99    public function filter_attachments_by_jetpack_videopress_fields( $args, $request ) {
100
101        if ( ! isset( $args['meta_query'] ) || ! is_array( $args['meta_query'] ) ) {
102            $args['meta_query'] = array();
103        }
104
105        /* To ignore all VideoPress videos, select only attachments without videopress_guid meta field */
106        if ( isset( $request['no_videopress'] ) ) {
107            $args['meta_query'][] = array(
108                'key'     => 'videopress_guid',
109                'compare' => 'NOT EXISTS',
110            );
111        }
112
113        /*
114         * Hide local attachments that have already been uploaded to VideoPress.
115         * Such "zombie" locals carry a `_videopress_uploaded_id` meta pointing
116         * at their VideoPress sibling attachment; the sibling is the row the
117         * dashboard should surface.
118         */
119        if ( isset( $request['videopress_hide_already_uploaded'] ) ) {
120            $args['meta_query'][] = array(
121                'key'     => Uploader::UPLOADED_KEY,
122                'compare' => 'NOT EXISTS',
123            );
124        }
125
126        /* Filter using privacy setting meta key */
127        if ( isset( $request['videopress_privacy_setting'] ) ) {
128            $videopress_privacy_setting = sanitize_text_field( $request['videopress_privacy_setting'] );
129
130            /* Allows the filtering to happens using a list of privacy settings separated by comma */
131            $videopress_privacy_setting_list = explode( ',', $videopress_privacy_setting );
132
133            $site_default_is_private = Data::get_videopress_videos_private_for_site();
134
135            if ( $site_default_is_private ) {
136                /**
137                 * If the search is looking for private videos and the site default is private,
138                 * the site default setting should be included on the search.
139                 */
140                if ( in_array( strval( \VIDEOPRESS_PRIVACY::IS_PRIVATE ), $videopress_privacy_setting_list, true ) ) {
141                    $videopress_privacy_setting_list[] = \VIDEOPRESS_PRIVACY::SITE_DEFAULT;
142                }
143            } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
144                /**
145                 * If the search is looking for public videos and the site default is public,
146                 * the site default setting should be included on the search.
147                 */
148                if ( in_array( strval( \VIDEOPRESS_PRIVACY::IS_PUBLIC ), $videopress_privacy_setting_list, true ) ) {
149                    $videopress_privacy_setting_list[] = \VIDEOPRESS_PRIVACY::SITE_DEFAULT;
150                }
151            }
152
153            $args['meta_query'][] = array(
154                'key'     => 'videopress_privacy_setting',
155                'value'   => $videopress_privacy_setting_list,
156                'compare' => 'IN',
157            );
158        }
159
160        /* Filter using rating meta key */
161        if ( isset( $request['videopress_rating'] ) ) {
162            $videopress_rating = sanitize_text_field( $request['videopress_rating'] );
163
164            /* Allows the filtering to happens using a list of ratings separated by comma */
165            $videopress_rating_list = explode( ',', $videopress_rating );
166
167            $args['meta_query'][] = array(
168                'key'     => 'videopress_rating',
169                'value'   => $videopress_rating_list,
170                'compare' => 'IN',
171            );
172        }
173
174        return $args;
175    }
176
177    /**
178     * Defines data structure and what elements are visible in which contexts
179     */
180    public function get_schema() {
181        return array(
182            '$schema'     => 'http://json-schema.org/draft-04/schema#',
183            'title'       => $this->field_name,
184            'type'        => 'object',
185            'context'     => array( 'view', 'edit' ),
186            'readonly'    => true,
187            'description' => __( 'VideoPress Data', 'jetpack-videopress-pkg' ),
188        );
189    }
190
191    /**
192     * Getter: Retrieve current VideoPress data for a given attachment.
193     *
194     * @param array           $attachment Response from the attachment endpoint.
195     * @param WP_REST_Request $request Request to the attachment endpoint.
196     *
197     * @return array
198     */
199    public function get( $attachment, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
200        if ( ! isset( $attachment['id'] ) ) {
201            return array();
202        }
203
204        $blog_id = Jetpack_Connection::get_site_id();
205        if ( ! is_int( $blog_id ) ) {
206            return array();
207        }
208
209        $videopress = $this->get_videopress_data( (int) $attachment['id'], $blog_id );
210
211        if ( ! $videopress ) {
212            return array();
213        }
214
215        return $videopress;
216    }
217
218    /**
219     * Gets the VideoPress GUID for a given attachment.
220     *
221     * This is pulled out into a separate method to support unit test mocking.
222     *
223     * @param int $attachment_id Attachment ID.
224     * @param int $blog_id Blog ID.
225     *
226     * @return array
227     */
228    public function get_videopress_data( $attachment_id, $blog_id ) {
229        $info = video_get_info_by_blogpostid( $blog_id, $attachment_id );
230        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
231            $title       = video_get_title( $blog_id, $attachment_id );
232            $description = video_get_description( $blog_id, $attachment_id );
233
234            $video_attachment = get_blog_post( $blog_id, $attachment_id );
235            if ( null === $video_attachment ) {
236                $caption = '';
237            } else {
238                $caption = $video_attachment->post_excerpt;
239            }
240        } else {
241            $title       = $info->title;
242            $description = $info->description;
243            $caption     = $info->caption;
244        }
245
246        $video_privacy_setting    = ! isset( $info->privacy_setting ) ? \VIDEOPRESS_PRIVACY::SITE_DEFAULT : intval( $info->privacy_setting );
247        $private_enabled_for_site = Data::get_videopress_videos_private_for_site();
248        $is_private               = $this->video_is_private( $video_privacy_setting, $private_enabled_for_site );
249
250        // The video needs a playback token if it's private for any reason (video privacy setting or site default privacy setting)
251        $video_needs_playback_token = $is_private;
252
253        return array(
254            'title'                    => $title,
255            'description'              => $description,
256            'caption'                  => $caption,
257            'guid'                     => $info->guid ?? null,
258            'rating'                   => $info->rating ?? null,
259            'allow_download'           =>
260                isset( $info->allow_download ) && $info->allow_download ? 1 : 0,
261            'display_embed'            =>
262                isset( $info->display_embed ) && $info->display_embed ? 1 : 0,
263            'privacy_setting'          => $video_privacy_setting,
264            'needs_playback_token'     => $video_needs_playback_token,
265            'is_private'               => $is_private,
266            'private_enabled_for_site' => $private_enabled_for_site,
267        );
268    }
269
270    /**
271     * Checks if the given attachment is a video.
272     *
273     * @param object $attachment The attachment object.
274     *
275     * @return false|int
276     */
277    public function is_video( $attachment ) {
278        return isset( $attachment->post_mime_type ) && wp_startswith( $attachment->post_mime_type, 'video/' );
279    }
280
281    /**
282     * Removes the jetpack_videopress field from the response if the
283     * given attachment is not a video.
284     *
285     * @param WP_REST_Response $response Response from the attachment endpoint.
286     * @param WP_Post          $attachment The original attachment object.
287     *
288     * @return mixed
289     */
290    public function remove_field_for_non_videos( $response, $attachment ) {
291        if ( ! $this->is_video( $attachment ) ) {
292            unset( $response->data[ $this->field_name ] );
293        }
294
295        return $response;
296    }
297
298    /**
299     * Determines if a video is private based on the video privacy
300     * setting and the site default privacy setting.
301     *
302     * @param int  $video_privacy_setting The privacy setting for the video.
303     * @param bool $private_enabled_for_site Flag stating if the default video privacy is private.
304     *
305     * @return bool
306     */
307    private function video_is_private( $video_privacy_setting, $private_enabled_for_site ) {
308        if ( $video_privacy_setting === \VIDEOPRESS_PRIVACY::IS_PUBLIC ) {
309            return false;
310        }
311        if ( $video_privacy_setting === \VIDEOPRESS_PRIVACY::IS_PRIVATE ) {
312            return true;
313        }
314
315        return $private_enabled_for_site;
316    }
317}
318
319if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
320    wpcom_rest_api_v2_load_plugin( 'Automattic\Jetpack\VideoPress\WPCOM_REST_API_V2_Attachment_VideoPress_Data' );
321}