Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.57% covered (warning)
54.57%
173 / 317
44.44% covered (danger)
44.44%
8 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Data
54.57% covered (warning)
54.57%
173 / 317
44.44% covered (danger)
44.44%
8 / 18
664.01
0.00% covered (danger)
0.00%
0 / 1
 get_blog_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_videopress_videos_private_for_site
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
8.12
 get_videopress_auto_subtitles_disabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_videopress_player_preload_disabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_videopress_inline_player_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_videopress_share_menu_disabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_videopress_settings
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
4.21
 get_video_data
96.97% covered (success)
96.97%
32 / 33
0.00% covered (danger)
0.00%
0 / 1
5
 get_latest_videopress_playlist_entries
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
11
 query_latest_videopress_playlist_entries
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
15.12
 playlist_entry
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 get_user_data
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 get_storage_used
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 get_connected_initial_state
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 has_connected_owner
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 can_perform_action
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 get_initial_state
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 1
30
 prepare_videopress_video_data
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2/**
3 * The Data class.
4 * This class provides methods for data VideoPress access and manipulation.
5 *
6 * @package automattic/jetpack-videopress
7 */
8
9namespace Automattic\Jetpack\VideoPress;
10
11use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12use Automattic\Jetpack\Status\Host;
13use WP_REST_Request;
14/**
15 * The Data class.
16 */
17class Data {
18
19    /**
20     * Gets the Jetpack blog ID
21     *
22     * @return int The blog ID
23     */
24    public static function get_blog_id() {
25        return VideoPressToken::blog_id();
26    }
27
28    /**
29     * Gets the VideoPress site privacy configuration.
30     *
31     * @return boolean If all the videos are private on the site
32     */
33    public static function get_videopress_videos_private_for_site() {
34        /**
35         * If it's a Simple site, returns the site privacy setting.
36         */
37        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
38            return video_is_private_wpcom_blog( get_current_blog_id() );
39        }
40        /**
41         * If it's a private Atomic site, the default setting is private as well.
42         */
43        if ( ( new Host() )->is_woa_site() ) {
44            if ( ( intval( get_option( 'blog_public', '' ) ) === -1 ) ) {
45                return true;
46            }
47        }
48
49        /* If it's a Jetpack site or a public Atomic site, check the settings */
50        return boolval( get_option( 'videopress_private_enabled_for_site', false ) );
51    }
52
53    /**
54     * Gets whether auto-generated subtitles are disabled for the site.
55     *
56     * Subtitles are generated by default, so this opt-out option defaults to false.
57     *
58     * @return boolean If auto-generated subtitles should be skipped for new videos.
59     */
60    public static function get_videopress_auto_subtitles_disabled() {
61        return boolval( get_option( 'videopress_auto_subtitles_disabled', false ) );
62    }
63
64    /**
65     * Gets whether player preloading is turned off for every embed on the site.
66     *
67     * Preloading is on by default, so this opt-out option defaults to false.
68     *
69     * @return boolean If embeds should wait for playback before fetching video data and player assets.
70     */
71    public static function get_videopress_player_preload_disabled() {
72        return boolval( get_option( 'videopress_player_preload_disabled', false ) );
73    }
74
75    /**
76     * Gets whether embeds render an inline player instead of one iframe per video.
77     *
78     * Iframes are the default, so this opt-in option defaults to false.
79     *
80     * @return boolean If embeds should mount players in the page from one shared player script.
81     */
82    public static function get_videopress_inline_player_enabled() {
83        return boolval( get_option( 'videopress_inline_player_enabled', false ) );
84    }
85
86    /**
87     * Gets whether the share menu is turned off for every video on the site.
88     *
89     * Sharing follows each video's own setting by default, so this opt-out option defaults to false.
90     *
91     * @return boolean If no video may display the share menu, whatever its own setting.
92     */
93    public static function get_videopress_share_menu_disabled() {
94        return boolval( get_option( 'videopress_share_menu_disabled', false ) );
95    }
96
97    /**
98     * Gets the VideoPress Settings.
99     *
100     * @return array The settings as an associative array.
101     */
102    public static function get_videopress_settings() {
103        $site_type       = 'jetpack';
104        $site_is_private = false;
105
106        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
107            $site_type       = 'simple';
108            $site_is_private = video_is_private_wpcom_blog( get_current_blog_id() );
109        } elseif ( ( new Host() )->is_woa_site() ) {
110            $site_type       = 'atomic';
111            $site_is_private = intval( get_option( 'blog_public', '' ) ) === -1;
112        }
113
114        return array(
115            'videopress_videos_private_for_site' => self::get_videopress_videos_private_for_site(),
116            'videopress_auto_subtitles_disabled' => self::get_videopress_auto_subtitles_disabled(),
117            'videopress_player_preload_disabled' => self::get_videopress_player_preload_disabled(),
118            'videopress_inline_player_enabled'   => self::get_videopress_inline_player_enabled(),
119            'videopress_share_menu_disabled'     => self::get_videopress_share_menu_disabled(),
120            'site_is_private'                    => $site_is_private,
121            'site_type'                          => $site_type,
122        );
123    }
124
125    /**
126     * Gets the video data
127     *
128     * @param boolean $is_videopress - True when getting VideoPress data.
129     * @return array
130     */
131    public static function get_video_data( $is_videopress = true ) {
132        $video_data = array(
133            'videos'     => array(),
134            'total'      => 0,
135            'totalPages' => 0,
136            'query'      => array(
137                'order'        => 'desc',
138                'orderBy'      => 'date',
139                'itemsPerPage' => 6,
140                'page'         => 1,
141            ),
142        );
143
144        $args = array(
145            'order'    => $video_data['query']['order'],
146            'orderby'  => $video_data['query']['orderBy'],
147            'per_page' => $video_data['query']['itemsPerPage'],
148            'page'     => $video_data['query']['page'],
149        );
150
151        if ( $is_videopress ) {
152            $args['mime_type'] = 'video/videopress';
153        } else {
154            $args['media_type']    = 'video';
155            $args['no_videopress'] = true;
156        }
157
158        // Do an internal request for the media list
159        $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
160        $request->set_query_params( $args );
161        $response = rest_do_request( $request );
162
163        if ( $response->is_error() ) {
164            // @todo: error handling
165            return $video_data;
166        }
167
168        // load the real values
169        $video_data['videos'] = $response->get_data();
170        $headers              = $response->get_headers();
171
172        if ( isset( $headers['X-WP-Total'] ) ) {
173            $video_data['total'] = $headers['X-WP-Total'];
174        }
175
176        if ( isset( $headers['X-WP-TotalPages'] ) ) {
177            $video_data['totalPages'] = $headers['X-WP-TotalPages'];
178        }
179
180        return $video_data;
181    }
182
183    /**
184     * Gets the site's newest VideoPress videos as playlist entries, newest first.
185     *
186     * Goes through the media REST endpoint rather than a direct WP_Query so the
187     * package's rest_attachment_query filter resolves VideoPress membership on
188     * every host: by mime off WordPress.com Simple, from wpcom's videos table on it.
189     * The editor's block preview sends the same request.
190     *
191     * @param int $count How many videos to fetch.
192     *
193     * @return array Entries with guid, durationMs and height keys.
194     */
195    public static function get_latest_videopress_playlist_entries( $count ) {
196        $count = max( 1, (int) $count );
197        $args  = array(
198            'per_page'            => $count,
199            'orderby'             => 'date',
200            'order'               => 'desc',
201            'videopress_has_guid' => 1,
202        );
203
204        // Attachments keep their original mime there, so narrow to videos before the ID-set constraint.
205        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
206            $args['videopress_only_videos'] = 1;
207        }
208
209        $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
210        $request->set_query_params( $args );
211        $response = rest_do_request( $request );
212
213        // WordPress.com Simple registers the wp/v2 routes only for REST requests, so on a
214        // page render the media endpoint does not exist: query the attachments directly.
215        if ( $response->is_error() ) {
216            return self::query_latest_videopress_playlist_entries( $count );
217        }
218
219        $entries = array();
220        foreach ( (array) $response->get_data() as $item ) {
221            $item = (array) $item;
222            $guid = $item['jetpack_videopress_guid'] ?? '';
223
224            // The REST field is empty on a site without a connection; the attachment meta still knows.
225            if ( ( ! is_string( $guid ) || '' === $guid ) && ! empty( $item['id'] ) ) {
226                $guid = get_post_meta( (int) $item['id'], 'videopress_guid', true );
227            }
228
229            $details    = isset( $item['media_details'] ) ? (array) $item['media_details'] : array();
230            $videopress = isset( $details['videopress'] ) ? (array) $details['videopress'] : array();
231            $entry      = self::playlist_entry( $guid, $videopress['duration'] ?? 0, $details['height'] ?? $videopress['height'] ?? 0 );
232            if ( $entry ) {
233                $entries[] = $entry;
234            }
235        }
236
237        return $entries;
238    }
239
240    /**
241     * The newest VideoPress videos straight from the posts table, for hosts where the
242     * media endpoint is unavailable. Mirrors what the endpoint does with the package's
243     * query filter: by mime off WordPress.com Simple, from wpcom's videos table on it.
244     *
245     * @param int $count How many videos to fetch.
246     *
247     * @return array Entries with guid, durationMs and height keys.
248     */
249    private static function query_latest_videopress_playlist_entries( $count ) {
250        $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
251        $posts    = get_posts(
252            array(
253                'post_type'      => 'attachment',
254                'post_status'    => 'inherit',
255                'post_mime_type' => $is_wpcom ? 'video' : 'video/videopress',
256                'orderby'        => 'date',
257                'order'          => 'DESC',
258                'posts_per_page' => $count,
259            )
260        );
261
262        $entries = array();
263        foreach ( $posts as $post ) {
264            $guid = get_post_meta( $post->ID, 'videopress_guid', true );
265            if ( ( ! is_string( $guid ) || '' === $guid ) && $is_wpcom && function_exists( 'video_get_info_by_blogpostid' ) ) {
266                $info = video_get_info_by_blogpostid( get_current_blog_id(), $post->ID );
267                $guid = is_object( $info ) && ! empty( $info->guid ) ? (string) $info->guid : '';
268            }
269
270            $meta       = wp_get_attachment_metadata( $post->ID );
271            $meta       = is_array( $meta ) ? $meta : array();
272            $videopress = isset( $meta['videopress'] ) ? (array) $meta['videopress'] : array();
273            $duration   = $videopress['duration'] ?? ( isset( $meta['length'] ) && is_numeric( $meta['length'] ) ? (int) $meta['length'] * 1000 : 0 );
274            $entry      = self::playlist_entry( $guid, $duration, $meta['height'] ?? $videopress['height'] ?? 0 );
275            if ( $entry ) {
276                $entries[] = $entry;
277            }
278        }
279
280        return $entries;
281    }
282
283    /**
284     * One playlist entry, or null when the GUID is unusable.
285     *
286     * @param mixed $guid     VideoPress GUID.
287     * @param mixed $duration Duration in ms.
288     * @param mixed $height   Height in px.
289     *
290     * @return array|null
291     */
292    private static function playlist_entry( $guid, $duration, $height ) {
293        if ( ! is_string( $guid ) || ! preg_match( '/^[a-zA-Z0-9]{8}$/', $guid ) ) {
294            return null;
295        }
296        return array(
297            'guid'       => $guid,
298            'durationMs' => is_numeric( $duration ) ? max( 0, (int) $duration ) : 0,
299            'height'     => is_numeric( $height ) ? max( 0, (int) $height ) : 0,
300        );
301    }
302
303    /**
304     * Gets the user data
305     *
306     * @return array
307     */
308    public static function get_user_data() {
309        $user_data = array(
310            'items'      => array(),
311            'pagination' => array(
312                'total'      => 0,
313                'totalPages' => 1,
314            ),
315            'query'      => array(
316                'order'   => 'asc',
317                'orderBy' => 'name',
318            ),
319            '_meta'      => array(
320                'relyOnInitialState' => true,
321            ),
322        );
323
324        $args = array(
325            'order'   => $user_data['query']['order'],
326            'orderby' => $user_data['query']['orderBy'],
327        );
328
329        // Do an internal request for the user list
330        $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
331        $request->set_query_params( $args );
332        $response = rest_do_request( $request );
333
334        if ( $response->is_error() ) {
335            // @todo: error handling
336            return $user_data;
337        }
338
339        // load the real values
340        $user_data['items'] = $response->get_data();
341        $headers            = $response->get_headers();
342
343        if ( isset( $headers['X-WP-Total'] ) ) {
344            $user_data['pagination']['total'] = $headers['X-WP-Total'];
345        }
346
347        if ( isset( $headers['X-WP-TotalPages'] ) ) {
348            $user_data['pagination']['totalPages'] = $headers['X-WP-TotalPages'];
349        }
350
351        return $user_data;
352    }
353
354    /**
355     * Gets the VideoPress used storage space in bytes
356     *
357     * @return int the used storage space
358     */
359    public static function get_storage_used() {
360        $site_data = Site::get_site_info();
361        if ( is_wp_error( $site_data ) ) {
362            return 0;
363        }
364
365        if ( isset( $site_data['options'] ) && isset( $site_data['options']['videopress_storage_used'] ) ) {
366            return intval( round( $site_data['options']['videopress_storage_used'] * 1024 * 1024 ) );
367        } else {
368            return 0;
369        }
370    }
371
372    /**
373     * Return all the initial state that depends on a valid site connection
374     *
375     * @return array
376     */
377    public static function get_connected_initial_state() {
378        return array(
379            'videos'    => array(
380                'storageUsed' => self::get_storage_used(),
381            ),
382            'purchases' => array(
383                'items'      => Site::get_purchases(),
384                'isFetching' => false,
385            ),
386        );
387    }
388
389    /**
390     * Checks if the site has as connected owner
391     */
392    public static function has_connected_owner() {
393        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
394            return true;
395        }
396
397        if ( ( new Connection_Manager() )->has_connected_owner() ) {
398            return true;
399        }
400
401        return false;
402    }
403
404    /**
405     * Checks if the user is able to perform actions that modify data
406     */
407    public static function can_perform_action() {
408        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
409            return true;
410        }
411
412        $connection = new Connection_Manager();
413
414        return (
415            $connection->is_connected() &&
416            self::has_connected_owner() &&
417            $connection->is_user_connected()
418        );
419    }
420
421    /**
422     * Return the initial state of the VideoPress app,
423     * used to render initially the app in the frontend.
424     *
425     * @return array
426     */
427    public static function get_initial_state() {
428        $videopress_data   = self::get_video_data();
429        $local_videos_data = self::get_video_data( false );
430
431        // Tweak local videos data.
432        $local_videos = array_map(
433            function ( $video ) {
434                $video              = (array) $video;
435                $id                 = $video['id'];
436                $media_details      = $video['media_details'];
437                $jetpack_videopress = $video['jetpack_videopress'];
438                $read_error         = null;
439
440                // In malformed files, the media_details or jetpack_videopress properties are not arrays.
441                if ( ! is_array( $media_details ) || ! is_array( $jetpack_videopress ) ) {
442                    $media_details      = (array) $media_details;
443                    $jetpack_videopress = (array) $jetpack_videopress;
444                    $read_error         = Upload_Exception::ERROR_MALFORMED_FILE;
445                }
446
447                // Check if video is already uploaded to VideoPress or has some error.
448                try {
449                    $uploader                  = new Uploader( $id );
450                    $is_uploaded_to_videopress = $uploader->is_uploaded();
451                } catch ( Upload_Exception $e ) {
452                    $is_uploaded_to_videopress = false;
453                    $read_error                = $e->getCode();
454                }
455
456                $upload_date = $video['date'];
457                $url         = $video['source_url'];
458
459                $title       = $jetpack_videopress['title'];
460                $description = $jetpack_videopress['description'];
461                $caption     = $jetpack_videopress['caption'];
462
463                $width    = $media_details['width'] ?? null;
464                $height   = $media_details['height'] ?? null;
465                $duration = $media_details['length'] ?? null;
466
467                return array(
468                    'id'                     => $id,
469                    'title'                  => $title,
470                    'description'            => $description,
471                    'caption'                => $caption,
472                    'width'                  => $width,
473                    'height'                 => $height,
474                    'url'                    => $url,
475                    'uploadDate'             => $upload_date,
476                    'duration'               => $duration,
477                    'isUploadedToVideoPress' => $is_uploaded_to_videopress,
478                    'readError'              => $read_error,
479                );
480            },
481            $local_videos_data['videos']
482        );
483
484        // Tweak VideoPress videos data.
485        $videos = array_map(
486            array( __CLASS__, 'prepare_videopress_video_data' ),
487            $videopress_data['videos']
488        );
489
490        $site_settings = self::get_videopress_settings();
491
492        $initial_state = array(
493            'users'        => self::get_user_data(),
494            'siteSettings' => array(
495                'videoPressVideosPrivateForSite' => $site_settings['videopress_videos_private_for_site'],
496                'siteIsPrivate'                  => $site_settings['site_is_private'],
497                'siteType'                       => $site_settings['site_type'],
498            ),
499            'videos'       => array(
500                'uploadedVideoCount'           => $videopress_data['total'],
501                'items'                        => $videos,
502                'isFetching'                   => false,
503                'isFetchingUploadedVideoCount' => false,
504                'pagination'                   => array(
505                    'totalPages' => $videopress_data['totalPages'],
506                    'total'      => $videopress_data['total'],
507                ),
508                'query'                        => $videopress_data['query'],
509                '_meta'                        => array(
510                    'processedAllVideosBeingRemoved' => true,
511                    'relyOnInitialState'             => true,
512                ),
513            ),
514            'localVideos'  => array(
515                'uploadedVideoCount'           => $local_videos_data['total'],
516                'items'                        => $local_videos,
517                'isFetching'                   => false,
518                'isFetchingUploadedVideoCount' => false,
519                'pagination'                   => array(
520                    'totalPages' => $local_videos_data['totalPages'],
521                    'total'      => $local_videos_data['total'],
522                ),
523                'query'                        => $local_videos_data['query'],
524                '_meta'                        => array(
525                    'relyOnInitialState' => true,
526                ),
527            ),
528        );
529
530        if ( self::has_connected_owner() ) {
531            return array_merge_recursive( $initial_state, self::get_connected_initial_state() );
532        }
533
534        return $initial_state;
535    }
536
537    /**
538     * Maps a single VideoPress video, as returned by the media REST endpoint, to
539     * the shape consumed by the VideoPress dashboard app.
540     *
541     * Videos that are still processing (or otherwise have partial metadata) come
542     * back without `videopress`, `width` or `height` in their media_details, so
543     * every nested access is coalesced to avoid undefined-key and offset-on-null
544     * warnings.
545     *
546     * @param array|object $video A single video entry from the media REST endpoint.
547     * @return array The video data formatted for the dashboard app.
548     */
549    private static function prepare_videopress_video_data( $video ) {
550        $video              = (array) $video;
551        $id                 = $video['id'];
552        $guid               = $video['jetpack_videopress_guid'];
553        $media_details      = (array) $video['media_details'];
554        $jetpack_videopress = (array) $video['jetpack_videopress'];
555
556        $videopress_media_details = $media_details['videopress'] ?? array();
557        $width                    = $media_details['width'] ?? null;
558        $height                   = $media_details['height'] ?? null;
559
560        $title                = $jetpack_videopress['title'];
561        $description          = $jetpack_videopress['description'];
562        $caption              = $jetpack_videopress['caption'];
563        $rating               = $jetpack_videopress['rating'];
564        $allow_download       = $jetpack_videopress['allow_download'];
565        $display_embed        = $jetpack_videopress['display_embed'];
566        $privacy_setting      = $jetpack_videopress['privacy_setting'];
567        $needs_playback_token = $jetpack_videopress['needs_playback_token'];
568        $is_private           = $jetpack_videopress['is_private'];
569
570        $original      = $videopress_media_details['original'] ?? null;
571        $poster        = ( ! $needs_playback_token ) ? ( $videopress_media_details['poster'] ?? null ) : null;
572        $upload_date   = $videopress_media_details['upload_date'] ?? null;
573        $duration      = $videopress_media_details['duration'] ?? null;
574        $file_url_base = $videopress_media_details['file_url_base'] ?? null;
575        $finished      = $videopress_media_details['finished'] ?? null;
576        $files         = $videopress_media_details['files'] ?? null;
577        $filename      = $original !== null ? basename( $original ) : null;
578
579        if ( isset( $files['dvd']['original_img'] ) && isset( $file_url_base['https'] ) && $privacy_setting !== 1 ) {
580            $thumbnail = $file_url_base['https'] . $files['dvd']['original_img'];
581        } else {
582            $thumbnail = null;
583        }
584
585        return array(
586            'id'                 => $id,
587            'guid'               => $guid,
588            'title'              => $title,
589            'description'        => $description,
590            'caption'            => $caption,
591            'url'                => $original,
592            'uploadDate'         => $upload_date,
593            'duration'           => $duration,
594            'isPrivate'          => $is_private,
595            'posterImage'        => $poster,
596            'allowDownload'      => $allow_download,
597            'displayEmbed'       => $display_embed,
598            'rating'             => $rating,
599            'privacySetting'     => $privacy_setting,
600            'needsPlaybackToken' => $needs_playback_token,
601            'width'              => $width,
602            'height'             => $height,
603            'poster'             => array(
604                'src' => $poster,
605            ),
606            'thumbnail'          => $thumbnail,
607            'finished'           => $finished,
608            'filename'           => $filename,
609        );
610    }
611}