PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-videopress / src / class-data.php

class-data.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-videopress/src/class-data.php

612 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
9 namespace Automattic\Jetpack\VideoPress;
10
11 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12 use Automattic\Jetpack\Status\Host;
13 use WP_REST_Request;
14 /**
15 * The Data class.
16 */
17 class 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 }
612