PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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 13.8.3 All 506 releases
jetpack / _inc / lib / class-jetpack-podcast-helper.php

class-jetpack-podcast-helper.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at _inc/lib/class-jetpack-podcast-helper.php

767 lines 25.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Helper to massage Podcast data to be used in the Podcast block.
4 *
5 * @package automattic/jetpack
6 */
7
8 /**
9 * Class Jetpack_Podcast_Helper
10 */
11 class Jetpack_Podcast_Helper {
12 /**
13 * How long to wait before retrying a feed that failed to load. Kept out of step with
14 * the five minutes WordPress.com caches a feed at the edge, so retries don't keep
15 * landing on a cold entry.
16 *
17 * @var int
18 */
19 const ERROR_CACHE_TIMEOUT = 90;
20
21 /**
22 * How long to keep the last successful response as a fallback.
23 *
24 * @var int
25 */
26 const FALLBACK_CACHE_TIMEOUT = WEEK_IN_SECONDS;
27
28 /**
29 * Feed errors that describe the feed's contents rather than our failure to reach it.
30 *
31 * @var string[]
32 */
33 const AUTHORITATIVE_ERROR_CODES = array( 'no_tracks' );
34
35 /**
36 * The RSS feed of the podcast.
37 *
38 * @var string
39 */
40 protected $feed = null;
41
42 /**
43 * The number of seconds to cache the podcast feed data.
44 * This value defaults to 1 hour specifically for podcast feeds.
45 * The value can be overridden specifically for podcasts using the
46 * `jetpack_podcast_feed_cache_timeout` filter. Note that the cache timeout value
47 * for all RSS feeds can be modified using the `wp_feed_cache_transient_lifetime`
48 * filter from WordPress core.
49 *
50 * @see https://developer.wordpress.org/reference/hooks/wp_feed_cache_transient_lifetime/
51 * @see WP_Feed_Cache_Transient
52 *
53 * @var int|null
54 */
55 protected $cache_timeout = HOUR_IN_SECONDS;
56
57 /**
58 * Initialize class.
59 *
60 * @param string $feed The RSS feed of the podcast.
61 */
62 public function __construct( $feed ) {
63 $this->feed = esc_url_raw( $feed );
64
65 /**
66 * Filter the number of seconds to cache a specific podcast URL for. The returned value will be ignored if it is null or not a valid integer.
67 * Note that this timeout will only work if the site is using the default `WP_Feed_Cache_Transient` cache implementation for RSS feeds,
68 * or their cache implementation relies on the `wp_feed_cache_transient_lifetime` filter.
69 *
70 * @since 11.3
71 * @see https://developer.wordpress.org/reference/hooks/wp_feed_cache_transient_lifetime/
72 *
73 * @param int|null $cache_timeout The number of seconds to cache the podcast data. Default value is null, so we don't override any defaults from existing filters.
74 * @param string $podcast_url The URL of the podcast feed.
75 */
76 $podcast_cache_timeout = apply_filters( 'jetpack_podcast_feed_cache_timeout', $this->cache_timeout, $this->feed );
77
78 // Make sure we force new values for $this->cache_timeout to be integers.
79 if ( is_numeric( $podcast_cache_timeout ) ) {
80 $this->cache_timeout = (int) $podcast_cache_timeout;
81 }
82 }
83
84 /**
85 * Retrieves tracks quantity.
86 *
87 * @return int number of tracks
88 */
89 public static function get_tracks_quantity() {
90 /**
91 * Allow requesting a specific number of tracks from SimplePie's `get_items` call.
92 * The default number of tracks is ten.
93 *
94 * @since 10.4.0
95 *
96 * @param int $number Number of tracks fetched. Default is 10.
97 */
98 return (int) apply_filters( 'jetpack_podcast_helper_tracks_quantity', 10 );
99 }
100
101 /**
102 * Gets podcast data formatted to be used by the Podcast Player block in both server-side
103 * block rendering and in API `WPCOM_REST_API_V2_Endpoint_Podcast_Player`.
104 *
105 * A successful response is cached for one hour, and kept for a week as a fallback to serve
106 * while the feed is unreachable. Callers that need the feed's true state can opt out of
107 * both with the `report_errors` argument.
108 *
109 * @param array $args {
110 * Optional array of arguments.
111 * @type array $guids The IDs of specific episodes to return rather than a list.
112 * @type bool $episode-options Whether to include the episode list for the selection UI.
113 * @type bool $report_errors Whether to return feed errors as-is rather than falling
114 * back to the last successful response. Default false.
115 * }
116 *
117 * @return array|WP_Error The player data or a error object.
118 */
119 public function get_player_data( $args = array() ) {
120 $guids = isset( $args['guids'] ) && $args['guids'] ? $args['guids'] : array();
121 $episode_options = isset( $args['episode-options'] ) && $args['episode-options'];
122 $report_errors = isset( $args['report_errors'] ) && $args['report_errors'];
123
124 // Try loading data from the cache.
125 $transient_key = 'jetpack_podcast_' . md5( $this->feed . implode( ',', $guids ) . "-$episode_options" );
126 $player_data = get_transient( $transient_key );
127
128 // A remembered failure would outlive the fix it is asking the author to make.
129 if ( $report_errors && is_wp_error( $player_data ) ) {
130 $player_data = false;
131 }
132
133 // Fetch data if we don't have any cached.
134 if ( false === $player_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
135 // Load feed.
136 $rss = $this->load_feed();
137
138 if ( is_wp_error( $rss ) ) {
139 return $this->handle_failure( $transient_key, $rss, $report_errors );
140 }
141
142 // Get a list of episodes by guid or all tracks in feed.
143 if ( count( $guids ) ) {
144 $tracks = array_map( array( $this, 'get_track_data' ), $guids );
145 $tracks = array_filter(
146 $tracks,
147 function ( $track ) {
148 return ! is_wp_error( $track );
149 }
150 );
151 } else {
152 $tracks = $this->get_track_list();
153 }
154
155 if ( is_wp_error( $tracks ) ) {
156 return $this->handle_failure( $transient_key, $tracks, $report_errors );
157 }
158
159 if ( empty( $tracks ) ) {
160 return $this->handle_failure(
161 $transient_key,
162 new WP_Error( 'no_tracks', __( 'Your Podcast couldn\'t be embedded as it doesn\'t contain any tracks. Please double check your URL.', 'jetpack' ) ),
163 $report_errors
164 );
165 }
166
167 // Get podcast meta.
168 $title = $rss->get_title();
169 $title = $this->get_plain_text( $title );
170
171 $description = $rss->get_description();
172 $description = $this->get_plain_text( $description );
173
174 $cover = $rss->get_image_url();
175 $cover = ! empty( $cover ) ? esc_url( $cover ) : null;
176
177 $link = $rss->get_link();
178 $link = ! empty( $link ) ? esc_url( $link ) : null;
179
180 $player_data = array(
181 'title' => $title,
182 'description' => $description,
183 'link' => $link,
184 'cover' => $cover,
185 'tracks' => $tracks,
186 );
187
188 if ( $episode_options ) {
189 $player_data['options'] = array();
190 foreach ( $rss->get_items() as $episode ) {
191 $enclosure = $this->get_audio_enclosure( $episode );
192 // If the episode doesn't have playable audio, then don't include it.
193 if ( is_wp_error( $enclosure ) ) {
194 continue;
195 }
196 $player_data['options'][] = array(
197 'label' => $this->get_plain_text( $episode->get_title() ),
198 'value' => $episode->get_id(),
199 );
200 }
201 }
202
203 set_transient( $transient_key, $player_data, HOUR_IN_SECONDS );
204
205 // Callers that asked for errors never read a fallback, so don't pay to write one.
206 if ( ! $report_errors ) {
207 $this->store_fallback( $transient_key, $player_data );
208 }
209
210 return $player_data;
211 }
212
213 // Only a remembered failure reaches here; a fresh one returns via handle_failure().
214 if ( is_wp_error( $player_data ) ) {
215 return $this->fallback_for( $transient_key, $player_data );
216 }
217
218 // A response cached before this feed had a fallback still deserves to cover the next
219 // outage, rather than leaving a gap until the cache next turns over.
220 if ( ! $report_errors ) {
221 $this->store_fallback( $transient_key, $player_data, true );
222 }
223
224 return $player_data;
225 }
226
227 /**
228 * Keeps a successful response around to serve while the feed is unreachable.
229 *
230 * @param string $transient_key Cache key for this feed/args combination.
231 * @param array $player_data The response to keep.
232 * @param bool $only_if_missing Whether to leave an existing fallback in place.
233 */
234 protected function store_fallback( $transient_key, $player_data, $only_if_missing = false ) {
235 $fallback_key = static::fallback_key( $transient_key );
236
237 if ( $only_if_missing && false !== get_transient( $fallback_key ) ) {
238 return;
239 }
240
241 set_transient( $fallback_key, $player_data, static::FALLBACK_CACHE_TIMEOUT );
242 }
243
244 /**
245 * Decides what to serve for a failed fetch, and whether to remember the failure.
246 *
247 * @param string $transient_key Cache key for this feed/args combination.
248 * @param WP_Error $error The error to fall back from.
249 * @param bool $report_errors Whether the caller asked for the error itself.
250 * @return array|WP_Error The last successful response, or the error.
251 */
252 protected function handle_failure( $transient_key, $error, $report_errors = false ) {
253 if ( $report_errors ) {
254 return $error;
255 }
256
257 $fallback = $this->fallback_for( $transient_key, $error );
258
259 // Remembering a failure we can't paper over only delays the retry that would earn us
260 // a fallback. An authoritative one is worth remembering either way.
261 if ( is_array( $fallback ) || static::is_authoritative( $error ) ) {
262 // The error, never the fallback: the editor shares this key and would take stale
263 // episodes as a working feed.
264 set_transient( $transient_key, $error, static::ERROR_CACHE_TIMEOUT );
265 }
266
267 return $fallback;
268 }
269
270 /**
271 * What to serve for a failed fetch: the last successful response, or the error itself.
272 *
273 * @param string $transient_key Cache key for this feed/args combination.
274 * @param WP_Error $error The error to fall back from.
275 * @return array|WP_Error The last successful response, or the error.
276 */
277 protected function fallback_for( $transient_key, $error ) {
278 // The feed's own answer, so stale episodes have no business standing in for it. They
279 // stay stored rather than being deleted: a feed can report itself empty mid-migration,
280 // and one such read shouldn't cost the week of cover only a success can restore.
281 if ( static::is_authoritative( $error ) ) {
282 return $error;
283 }
284
285 $fallback = get_transient( static::fallback_key( $transient_key ) );
286
287 return is_array( $fallback ) ? $fallback : $error;
288 }
289
290 /**
291 * Whether the error reflects the feed's real contents rather than our failure to
292 * reach it.
293 *
294 * @param WP_Error $error The error to classify.
295 * @return bool
296 */
297 protected static function is_authoritative( $error ) {
298 return in_array( $error->get_error_code(), static::AUTHORITATIVE_ERROR_CODES, true );
299 }
300
301 /**
302 * Cache key holding the last successful response for a feed/args combination.
303 *
304 * @param string $transient_key The regular cache key.
305 * @return string
306 */
307 protected static function fallback_key( $transient_key ) {
308 return $transient_key . '_last';
309 }
310
311 /**
312 * Gets a specific track from the supplied feed URL.
313 *
314 * @param string $guid The GUID of the track.
315 * @param boolean $force_refresh Clear the feed cache.
316 * @return array|WP_Error The track object or an error object.
317 */
318 public function get_track_data( $guid, $force_refresh = false ) {
319 // Get the cache key.
320 $transient_key = 'jetpack_podcast_' . md5( "$this->feed::$guid" );
321
322 // Clear the cache if force_refresh param is true.
323 if ( true === $force_refresh ) {
324 delete_transient( $transient_key );
325 }
326
327 // Try loading track data from the cache.
328 $track_data = get_transient( $transient_key );
329
330 // Fetch data if we don't have any cached.
331 if ( false === $track_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
332 // Load feed.
333 $rss = $this->load_feed( $force_refresh );
334
335 if ( is_wp_error( $rss ) ) {
336 return $rss;
337 }
338
339 // Loop over all tracks to find the one.
340 foreach ( $rss->get_items() as $track ) {
341 if ( $guid === $track->get_id() ) {
342 $track_data = $this->setup_tracks_callback( $track );
343 break;
344 }
345 }
346
347 if ( false === $track_data ) {
348 return new WP_Error( 'no_track', __( 'The track was not found.', 'jetpack' ) );
349 }
350
351 // Cache for 1 hour.
352 set_transient( $transient_key, $track_data, HOUR_IN_SECONDS );
353 }
354
355 return $track_data;
356 }
357
358 /**
359 * Gets a list of tracks for the supplied RSS feed.
360 *
361 * @return array|WP_Error The feed's tracks or a error object.
362 */
363 public function get_track_list() {
364 $rss = $this->load_feed();
365
366 if ( is_wp_error( $rss ) ) {
367 return $rss;
368 }
369
370 $tracks_quantity = static::get_tracks_quantity();
371
372 /**
373 * Allow requesting a specific number of tracks from SimplePie's `get_items` call.
374 * The default number of tracks is ten.
375 * Deprecated. Use jetpack_podcast_helper_tracks_quantity filter instead, which takes one less parameter.
376 *
377 * @since 9.5.0
378 * @deprecated 10.4.0
379 *
380 * @param int $tracks_quantity Number of tracks fetched. Default is 10.
381 * @param object $rss The SimplePie object built from core's `fetch_feed` call.
382 */
383 $tracks_quantity = apply_filters_deprecated( 'jetpack_podcast_helper_list_quantity', array( $tracks_quantity, $rss ), '10.4.0', 'jetpack_podcast_helper_tracks_quantity' );
384
385 // Process the requested number of items from our feed.
386 $track_list = array_map( array( __CLASS__, 'setup_tracks_callback' ), $rss->get_items( 0, $tracks_quantity ) );
387
388 // Filter out any tracks that are empty.
389 // Reset the array indices.
390 return array_values( array_filter( $track_list ) );
391 }
392
393 /**
394 * Formats string as pure plaintext, with no HTML tags or entities present.
395 * This is ready to be used in React, innerText but needs to be escaped
396 * using standard `esc_html` when generating markup on server.
397 *
398 * @param string $str Input string.
399 * @return string Plain text string.
400 */
401 protected function get_plain_text( $str ) {
402 return $this->sanitize_and_decode_text( $str, true );
403 }
404
405 /**
406 * Formats strings as safe HTML.
407 *
408 * @param string $str Input string.
409 * @return string HTML text string safe for post_content.
410 */
411 protected function get_html_text( $str ) {
412 return $this->sanitize_and_decode_text( $str, false );
413 }
414
415 /**
416 * Strip unallowed html tags and decode entities.
417 *
418 * @param string $str Input string.
419 * @param boolean $strip_all_tags Strip all tags, otherwise allow post_content safe tags.
420 * @return string Sanitized and decoded text.
421 */
422 protected function sanitize_and_decode_text( $str, $strip_all_tags = true ) {
423 // Trim string and return if empty.
424 $str = trim( (string) $str );
425 if ( empty( $str ) ) {
426 return '';
427 }
428
429 if ( $strip_all_tags ) {
430 // Make sure there are no tags.
431 $str = wp_strip_all_tags( $str );
432 } else {
433 $str = wp_kses_post( $str );
434 }
435
436 // Replace all entities with their characters, including all types of quotes.
437 $str = html_entity_decode( $str, ENT_QUOTES );
438
439 return $str;
440 }
441
442 /**
443 * Loads an RSS feed using `fetch_feed`.
444 *
445 * @param boolean $force_refresh Clear the feed cache.
446 * @return SimplePie\SimplePie|WP_Error The RSS object or error.
447 */
448 public function load_feed( $force_refresh = false ) {
449 // Add action: clear the SimplePie Cache if $force_refresh param is true.
450 if ( true === $force_refresh ) {
451 add_action( 'wp_feed_options', array( __CLASS__, 'reset_simplepie_cache' ) );
452 }
453 // Add action: detect the podcast feed from the provided feed URL.
454 add_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) );
455
456 $cache_timeout_filter_added = false;
457 if ( $this->cache_timeout !== null ) {
458 // If we have a custom cache timeout, apply the custom timeout value.
459 add_filter( 'wp_feed_cache_transient_lifetime', array( $this, 'filter_podcast_cache_timeout' ), 20 );
460 $cache_timeout_filter_added = true;
461 }
462
463 /**
464 * Allow callers to set up any desired hooks when we fetch the content for a podcast.
465 * The `jetpack_podcast_post_fetch` action can be used to perform cleanup.
466 *
467 * @param string $podcast_url URL for the podcast's RSS feed.
468 *
469 * @since 11.2
470 */
471 do_action( 'jetpack_podcast_pre_fetch', $this->feed );
472
473 // Fetch the feed.
474 $rss = fetch_feed( $this->feed );
475
476 // Remove added actions from wp_feed_options hook.
477 remove_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) );
478 if ( true === $force_refresh ) {
479 remove_action( 'wp_feed_options', array( __CLASS__, 'reset_simplepie_cache' ) );
480 }
481
482 if ( $cache_timeout_filter_added ) {
483 // Remove the cache timeout filter we added.
484 remove_filter( 'wp_feed_cache_transient_lifetime', array( $this, 'filter_podcast_cache_timeout' ), 20 );
485 }
486
487 /**
488 * Allow callers to identify when we have completed fetching a specified podcast feed.
489 * This makes it possible to clean up any actions or filters that were set up using the
490 * `jetpack_podcast_pre_fetch` action.
491 *
492 * Note that this action runs after other hooks added by Jetpack have been removed.
493 *
494 * @param string $podcast_url URL for the podcast's RSS feed.
495 * @param SimplePie\SimplePie|SimplePie|WP_Error $rss Either the SimplePie RSS object or an error.
496 *
497 * @since 11.2
498 */
499 do_action( 'jetpack_podcast_post_fetch', $this->feed, $rss );
500
501 if ( is_wp_error( $rss ) ) {
502 return new WP_Error( 'invalid_url', __( 'Your podcast couldn\'t be embedded. Please double check your URL.', 'jetpack' ) );
503 }
504
505 if ( ! $rss->get_item_quantity() ) {
506 return new WP_Error( 'no_tracks', __( 'Podcast audio RSS feed has no tracks.', 'jetpack' ) );
507 }
508
509 return $rss;
510 }
511
512 /**
513 * Filter to override the default number of seconds to cache RSS feed data for the current feed.
514 * Note that we don't use the feed's URL because some of the SimplePie feed caches trigger this
515 * filter with a feed identifier and not a URL.
516 *
517 * @param int $cache_timeout_in_seconds Number of seconds to cache the podcast feed.
518 *
519 * @return int The number of seconds to cache the podcast feed.
520 */
521 public function filter_podcast_cache_timeout( $cache_timeout_in_seconds ) {
522 if ( $this->cache_timeout !== null ) {
523 return $this->cache_timeout;
524 }
525
526 return $cache_timeout_in_seconds;
527 }
528
529 /**
530 * Action handler to set our podcast specific feed locator class on the SimplePie object.
531 *
532 * @param SimplePie\SimplePie $feed The SimplePie object, passed by reference.
533 */
534 public static function set_podcast_locator( &$feed ) {
535 if ( ! class_exists( 'Jetpack_Podcast_Feed_Locator' ) ) {
536 require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-podcast-feed-locator.php';
537 }
538
539 $feed->get_registry()->register( SimplePie\Locator::class, 'Jetpack_Podcast_Feed_Locator' );
540 }
541
542 /**
543 * Action handler to reset the SimplePie cache for the podcast feed.
544 *
545 * Note this only resets the cache for the specified url. If the feed locator finds the podcast feed
546 * within the markup of the that url, that feed itself may still be cached.
547 *
548 * @param SimplePie\SimplePie $feed The SimplePie object, passed by reference.
549 * @return void
550 */
551 public static function reset_simplepie_cache( &$feed ) {
552 // Retrieve the cache object for a feed url. Based on:
553 // https://github.com/WordPress/WordPress/blob/fd1c2cb4011845ceb7244a062b09b2506082b1c9/wp-includes/class-simplepie.php#L1412.
554 // @todo This method of getting the cache is deprecated, and there doesn't seem to be a real replacement. `$feed->get_cache()` is private.
555 // @phan-suppress-next-line PhanUndeclaredClassReference
556 $cache = $feed->registry->call( 'Cache', 'get_handler', array( $feed->cache_location, call_user_func( $feed->cache_name_function, $feed->feed_url ), 'spc' ) );
557
558 if ( method_exists( $cache, 'unlink' ) ) {
559 $cache->unlink();
560 }
561 }
562
563 /**
564 * Prepares Episode data to be used by the Podcast Player block.
565 *
566 * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
567 * @return array
568 */
569 protected function setup_tracks_callback( SimplePie\Item $episode ) {
570 $enclosure = $this->get_audio_enclosure( $episode );
571
572 // If the audio enclosure is empty then it is not playable.
573 // We therefore return an empty array for this track.
574 // It will be filtered out later.
575 if ( is_wp_error( $enclosure ) ) {
576 return array();
577 }
578
579 // If there is no link return an empty array. We will filter out later.
580 if ( empty( $enclosure->link ) ) {
581 return array();
582 }
583
584 $publish_date = $episode->get_gmdate( DATE_ATOM );
585 // Build track data.
586 $track = array(
587 'id' => wp_unique_id( 'podcast-track-' ),
588 'link' => esc_url( $episode->get_link() ),
589 'src' => esc_url( (string) $enclosure->link ),
590 'type' => esc_attr( (string) $enclosure->type ),
591 'description' => $this->get_plain_text( $episode->get_description() ),
592 'description_html' => $this->get_html_text( $episode->get_description() ),
593 'title' => $this->get_plain_text( $episode->get_title() ),
594 'image' => esc_url( $this->get_episode_image_url( $episode ) ),
595 'guid' => $this->get_plain_text( $episode->get_id() ),
596 'publish_date' => $publish_date ? $publish_date : null,
597 );
598
599 if ( empty( $track['title'] ) ) {
600 $track['title'] = esc_html__( '(no title)', 'jetpack' );
601 }
602
603 if ( ! empty( $enclosure->duration ) ) {
604 $track['duration'] = esc_html( $this->format_track_duration( (int) $enclosure->duration ) );
605 }
606
607 return $track;
608 }
609
610 /**
611 * Retrieves an episode's image URL, if it's available.
612 *
613 * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
614 * @param string $itunes_ns The itunes namespace, defaulted to the standard 1.0 version.
615 * @return string|null The image URL or null if not found.
616 */
617 protected function get_episode_image_url( SimplePie\Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) {
618 $image = $episode->get_item_tags( $itunes_ns, 'image' );
619 if ( isset( $image[0]['attribs']['']['href'] ) ) {
620 return $image[0]['attribs']['']['href'];
621 }
622 return null;
623 }
624
625 /**
626 * Retrieves an audio enclosure.
627 *
628 * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
629 * @return SimplePie\Enclosure|null
630 */
631 protected function get_audio_enclosure( SimplePie\Item $episode ) {
632 foreach ( (array) $episode->get_enclosures() as $enclosure ) {
633 if ( str_starts_with( $enclosure->type ?? '', 'audio/' ) ) {
634 return $enclosure;
635 }
636 }
637
638 return new WP_Error( 'invalid_audio', __( 'Podcast audio is an invalid type.', 'jetpack' ) );
639 }
640
641 /**
642 * Returns the track duration as a formatted string.
643 *
644 * @param int|float $duration of the track in seconds.
645 * @return string
646 */
647 protected function format_track_duration( $duration ) {
648 $format = $duration > HOUR_IN_SECONDS ? 'H:i:s' : 'i:s';
649
650 return date_i18n( $format, $duration );
651 }
652
653 /**
654 * Gets podcast player data schema.
655 *
656 * Useful for json schema in REST API endpoints.
657 *
658 * @return array Player data json schema.
659 */
660 public static function get_player_data_schema() {
661 return array(
662 '$schema' => 'http://json-schema.org/draft-04/schema#',
663 'title' => 'jetpack-podcast-player-data',
664 'type' => 'object',
665 'properties' => array(
666 'title' => array(
667 'description' => __( 'The title of the podcast.', 'jetpack' ),
668 'type' => 'string',
669 ),
670 'link' => array(
671 'description' => __( 'The URL of the podcast website.', 'jetpack' ),
672 'type' => 'string',
673 'format' => 'uri',
674 ),
675 'cover' => array(
676 'description' => __( 'The URL of the podcast cover image.', 'jetpack' ),
677 'type' => 'string',
678 'format' => 'uri',
679 ),
680 'tracks' => self::get_tracks_schema(),
681 'options' => self::get_options_schema(),
682 ),
683 );
684 }
685
686 /**
687 * Gets tracks data schema.
688 *
689 * Useful for json schema in REST API endpoints.
690 *
691 * @return array Tracks json schema.
692 */
693 public static function get_tracks_schema() {
694 return array(
695 'description' => __( 'Latest episodes of the podcast.', 'jetpack' ),
696 'type' => 'array',
697 'items' => array(
698 'type' => 'object',
699 'properties' => array(
700 'id' => array(
701 'description' => __( 'The episode id. Generated per request, not globally unique.', 'jetpack' ),
702 'type' => 'string',
703 ),
704 'link' => array(
705 'description' => __( 'The external link for the episode.', 'jetpack' ),
706 'type' => 'string',
707 'format' => 'uri',
708 ),
709 'src' => array(
710 'description' => __( 'The audio file URL of the episode.', 'jetpack' ),
711 'type' => 'string',
712 'format' => 'uri',
713 ),
714 'type' => array(
715 'description' => __( 'The mime type of the episode.', 'jetpack' ),
716 'type' => 'string',
717 ),
718 'description' => array(
719 'description' => __( 'The episode description, in plaintext.', 'jetpack' ),
720 'type' => 'string',
721 ),
722 'description_html' => array(
723 'description' => __( 'The episode description with allowed html tags.', 'jetpack' ),
724 'type' => 'string',
725 ),
726 'title' => array(
727 'description' => __( 'The episode title.', 'jetpack' ),
728 'type' => 'string',
729 ),
730 'publish_date' => array(
731 'description' => __( 'The UTC publish date and time of the episode', 'jetpack' ),
732 'type' => 'string',
733 'format' => 'date-time',
734 ),
735 ),
736 ),
737 );
738 }
739
740 /**
741 * Gets the episode options schema.
742 *
743 * Useful for json schema in REST API endpoints.
744 *
745 * @return array Tracks json schema.
746 */
747 public static function get_options_schema() {
748 return array(
749 'description' => __( 'The options that will be displayed in the episode selection UI', 'jetpack' ),
750 'type' => 'array',
751 'items' => array(
752 'type' => 'object',
753 'properties' => array(
754 'label' => array(
755 'description' => __( 'The display label of the option, the episode title.', 'jetpack' ),
756 'type' => 'string',
757 ),
758 'value' => array(
759 'description' => __( 'The value used for that option, the episode GUID', 'jetpack' ),
760 'type' => 'string',
761 ),
762 ),
763 ),
764 );
765 }
766 }
767