Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
6.47% covered (danger)
6.47%
15 / 232
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Podcast_Helper
6.47% covered (danger)
6.47%
15 / 232
0.00% covered (danger)
0.00%
0 / 19
3415.78
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_tracks_quantity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_player_data
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
272
 get_track_data
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
9.02
 get_track_list
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_plain_text
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_html_text
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sanitize_and_decode_text
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 load_feed
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 filter_podcast_cache_timeout
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 set_podcast_locator
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 reset_simplepie_cache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setup_tracks_callback
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 get_episode_image_url
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_audio_enclosure
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 format_track_duration
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 get_player_data_schema
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 get_tracks_schema
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 get_options_schema
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
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 */
11class Jetpack_Podcast_Helper {
12    /**
13     * The RSS feed of the podcast.
14     *
15     * @var string
16     */
17    protected $feed = null;
18
19    /**
20     * The number of seconds to cache the podcast feed data.
21     * This value defaults to 1 hour specifically for podcast feeds.
22     * The value can be overridden specifically for podcasts using the
23     * `jetpack_podcast_feed_cache_timeout` filter. Note that the cache timeout value
24     * for all RSS feeds can be modified using the `wp_feed_cache_transient_lifetime`
25     * filter from WordPress core.
26     *
27     * @see https://developer.wordpress.org/reference/hooks/wp_feed_cache_transient_lifetime/
28     * @see WP_Feed_Cache_Transient
29     *
30     * @var int|null
31     */
32    protected $cache_timeout = HOUR_IN_SECONDS;
33
34    /**
35     * Initialize class.
36     *
37     * @param string $feed The RSS feed of the podcast.
38     */
39    public function __construct( $feed ) {
40        $this->feed = esc_url_raw( $feed );
41
42        /**
43         * 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.
44         * Note that this timeout will only work if the site is using the default `WP_Feed_Cache_Transient` cache implementation for RSS feeds,
45         * or their cache implementation relies on the `wp_feed_cache_transient_lifetime` filter.
46         *
47         * @since 11.3
48         * @see https://developer.wordpress.org/reference/hooks/wp_feed_cache_transient_lifetime/
49         *
50         * @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.
51         * @param string   $podcast_url   The URL of the podcast feed.
52         */
53        $podcast_cache_timeout = apply_filters( 'jetpack_podcast_feed_cache_timeout', $this->cache_timeout, $this->feed );
54
55        // Make sure we force new values for $this->cache_timeout to be integers.
56        if ( is_numeric( $podcast_cache_timeout ) ) {
57            $this->cache_timeout = (int) $podcast_cache_timeout;
58        }
59    }
60
61    /**
62     * Retrieves tracks quantity.
63     *
64     * @return int number of tracks
65     */
66    public static function get_tracks_quantity() {
67        /**
68         * Allow requesting a specific number of tracks from SimplePie's `get_items` call.
69         * The default number of tracks is ten.
70         *
71         * @since 10.4.0
72         *
73         * @param int $number Number of tracks fetched. Default is 10.
74         */
75        return (int) apply_filters( 'jetpack_podcast_helper_tracks_quantity', 10 );
76    }
77
78    /**
79     * Gets podcast data formatted to be used by the Podcast Player block in both server-side
80     * block rendering and in API `WPCOM_REST_API_V2_Endpoint_Podcast_Player`.
81     *
82     * The result is cached for one hour.
83     *
84     * @param array $args {
85     *    Optional array of arguments.
86     *    @type string|int $guid  The ID of a specific episode to return rather than a list.
87     * }
88     *
89     * @return array|WP_Error  The player data or a error object.
90     */
91    public function get_player_data( $args = array() ) {
92        $guids           = isset( $args['guids'] ) && $args['guids'] ? $args['guids'] : array();
93        $episode_options = isset( $args['episode-options'] ) && $args['episode-options'];
94
95        // Try loading data from the cache.
96        $transient_key = 'jetpack_podcast_' . md5( $this->feed . implode( ',', $guids ) . "-$episode_options" );
97        $player_data   = get_transient( $transient_key );
98
99        // Fetch data if we don't have any cached.
100        if ( false === $player_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
101            // Load feed.
102            $rss = $this->load_feed();
103
104            if ( is_wp_error( $rss ) ) {
105                return $rss;
106            }
107
108            // Get a list of episodes by guid or all tracks in feed.
109            if ( count( $guids ) ) {
110                $tracks = array_map( array( $this, 'get_track_data' ), $guids );
111                $tracks = array_filter(
112                    $tracks,
113                    function ( $track ) {
114                        return ! is_wp_error( $track );
115                    }
116                );
117            } else {
118                $tracks = $this->get_track_list();
119            }
120
121            if ( is_wp_error( $tracks ) ) {
122                return $tracks;
123            }
124
125            if ( empty( $tracks ) ) {
126                return new WP_Error( 'no_tracks', __( 'Your Podcast couldn\'t be embedded as it doesn\'t contain any tracks. Please double check your URL.', 'jetpack' ) );
127            }
128
129            // Get podcast meta.
130            $title = $rss->get_title();
131            $title = $this->get_plain_text( $title );
132
133            $description = $rss->get_description();
134            $description = $this->get_plain_text( $description );
135
136            $cover = $rss->get_image_url();
137            $cover = ! empty( $cover ) ? esc_url( $cover ) : null;
138
139            $link = $rss->get_link();
140            $link = ! empty( $link ) ? esc_url( $link ) : null;
141
142            $player_data = array(
143                'title'       => $title,
144                'description' => $description,
145                'link'        => $link,
146                'cover'       => $cover,
147                'tracks'      => $tracks,
148            );
149
150            if ( $episode_options ) {
151                $player_data['options'] = array();
152                foreach ( $rss->get_items() as $episode ) {
153                    $enclosure = $this->get_audio_enclosure( $episode );
154                    // If the episode doesn't have playable audio, then don't include it.
155                    if ( is_wp_error( $enclosure ) ) {
156                        continue;
157                    }
158                    $player_data['options'][] = array(
159                        'label' => $this->get_plain_text( $episode->get_title() ),
160                        'value' => $episode->get_id(),
161                    );
162                }
163            }
164
165            // Cache for 1 hour.
166            set_transient( $transient_key, $player_data, HOUR_IN_SECONDS );
167        }
168
169        return $player_data;
170    }
171
172    /**
173     * Gets a specific track from the supplied feed URL.
174     *
175     * @param string  $guid          The GUID of the track.
176     * @param boolean $force_refresh Clear the feed cache.
177     * @return array|WP_Error The track object or an error object.
178     */
179    public function get_track_data( $guid, $force_refresh = false ) {
180        // Get the cache key.
181        $transient_key = 'jetpack_podcast_' . md5( "$this->feed::$guid" );
182
183        // Clear the cache if force_refresh param is true.
184        if ( true === $force_refresh ) {
185            delete_transient( $transient_key );
186        }
187
188        // Try loading track data from the cache.
189        $track_data = get_transient( $transient_key );
190
191        // Fetch data if we don't have any cached.
192        if ( false === $track_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
193            // Load feed.
194            $rss = $this->load_feed( $force_refresh );
195
196            if ( is_wp_error( $rss ) ) {
197                return $rss;
198            }
199
200            // Loop over all tracks to find the one.
201            foreach ( $rss->get_items() as $track ) {
202                if ( $guid === $track->get_id() ) {
203                    $track_data = $this->setup_tracks_callback( $track );
204                    break;
205                }
206            }
207
208            if ( false === $track_data ) {
209                return new WP_Error( 'no_track', __( 'The track was not found.', 'jetpack' ) );
210            }
211
212            // Cache for 1 hour.
213            set_transient( $transient_key, $track_data, HOUR_IN_SECONDS );
214        }
215
216        return $track_data;
217    }
218
219    /**
220     * Gets a list of tracks for the supplied RSS feed.
221     *
222     * @return array|WP_Error The feed's tracks or a error object.
223     */
224    public function get_track_list() {
225        $rss = $this->load_feed();
226
227        if ( is_wp_error( $rss ) ) {
228            return $rss;
229        }
230
231        $tracks_quantity = static::get_tracks_quantity();
232
233        /**
234         * Allow requesting a specific number of tracks from SimplePie's `get_items` call.
235         * The default number of tracks is ten.
236         * Deprecated. Use jetpack_podcast_helper_tracks_quantity filter instead, which takes one less parameter.
237         *
238         * @since 9.5.0
239         * @deprecated 10.4.0
240         *
241         * @param int    $tracks_quantity Number of tracks fetched. Default is 10.
242         * @param object $rss             The SimplePie object built from core's `fetch_feed` call.
243         */
244        $tracks_quantity = apply_filters_deprecated( 'jetpack_podcast_helper_list_quantity', array( $tracks_quantity, $rss ), '10.4.0', 'jetpack_podcast_helper_tracks_quantity' );
245
246        // Process the requested number of items from our feed.
247        $track_list = array_map( array( __CLASS__, 'setup_tracks_callback' ), $rss->get_items( 0, $tracks_quantity ) );
248
249        // Filter out any tracks that are empty.
250        // Reset the array indices.
251        return array_values( array_filter( $track_list ) );
252    }
253
254    /**
255     * Formats string as pure plaintext, with no HTML tags or entities present.
256     * This is ready to be used in React, innerText but needs to be escaped
257     * using standard `esc_html` when generating markup on server.
258     *
259     * @param string $str Input string.
260     * @return string Plain text string.
261     */
262    protected function get_plain_text( $str ) {
263        return $this->sanitize_and_decode_text( $str, true );
264    }
265
266    /**
267     * Formats strings as safe HTML.
268     *
269     * @param string $str Input string.
270     * @return string HTML text string safe for post_content.
271     */
272    protected function get_html_text( $str ) {
273        return $this->sanitize_and_decode_text( $str, false );
274    }
275
276    /**
277     * Strip unallowed html tags and decode entities.
278     *
279     * @param string  $str Input string.
280     * @param boolean $strip_all_tags Strip all tags, otherwise allow post_content safe tags.
281     * @return string Sanitized and decoded text.
282     */
283    protected function sanitize_and_decode_text( $str, $strip_all_tags = true ) {
284        // Trim string and return if empty.
285        $str = trim( (string) $str );
286        if ( empty( $str ) ) {
287            return '';
288        }
289
290        if ( $strip_all_tags ) {
291            // Make sure there are no tags.
292            $str = wp_strip_all_tags( $str );
293        } else {
294            $str = wp_kses_post( $str );
295        }
296
297        // Replace all entities with their characters, including all types of quotes.
298        $str = html_entity_decode( $str, ENT_QUOTES );
299
300        return $str;
301    }
302
303    /**
304     * Loads an RSS feed using `fetch_feed`.
305     *
306     * @param boolean $force_refresh Clear the feed cache.
307     * @return SimplePie\SimplePie|WP_Error The RSS object or error.
308     */
309    public function load_feed( $force_refresh = false ) {
310        // Add action: clear the SimplePie Cache if $force_refresh param is true.
311        if ( true === $force_refresh ) {
312            add_action( 'wp_feed_options', array( __CLASS__, 'reset_simplepie_cache' ) );
313        }
314        // Add action: detect the podcast feed from the provided feed URL.
315        add_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) );
316
317        $cache_timeout_filter_added = false;
318        if ( $this->cache_timeout !== null ) {
319            // If we have a custom cache timeout, apply the custom timeout value.
320            add_filter( 'wp_feed_cache_transient_lifetime', array( $this, 'filter_podcast_cache_timeout' ), 20 );
321            $cache_timeout_filter_added = true;
322        }
323
324        /**
325         * Allow callers to set up any desired hooks when we fetch the content for a podcast.
326         * The `jetpack_podcast_post_fetch` action can be used to perform cleanup.
327         *
328         * @param string $podcast_url URL for the podcast's RSS feed.
329         *
330         * @since 11.2
331         */
332        do_action( 'jetpack_podcast_pre_fetch', $this->feed );
333
334        // Fetch the feed.
335        $rss = fetch_feed( $this->feed );
336
337        // Remove added actions from wp_feed_options hook.
338        remove_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) );
339        if ( true === $force_refresh ) {
340            remove_action( 'wp_feed_options', array( __CLASS__, 'reset_simplepie_cache' ) );
341        }
342
343        if ( $cache_timeout_filter_added ) {
344            // Remove the cache timeout filter we added.
345            remove_filter( 'wp_feed_cache_transient_lifetime', array( $this, 'filter_podcast_cache_timeout' ), 20 );
346        }
347
348        /**
349         * Allow callers to identify when we have completed fetching a specified podcast feed.
350         * This makes it possible to clean up any actions or filters that were set up using the
351         * `jetpack_podcast_pre_fetch` action.
352         *
353         * Note that this action runs after other hooks added by Jetpack have been removed.
354         *
355         * @param string             $podcast_url URL for the podcast's RSS feed.
356         * @param SimplePie\SimplePie|SimplePie|WP_Error $rss Either the SimplePie RSS object or an error.
357         *
358         * @since 11.2
359         */
360        do_action( 'jetpack_podcast_post_fetch', $this->feed, $rss );
361
362        if ( is_wp_error( $rss ) ) {
363            return new WP_Error( 'invalid_url', __( 'Your podcast couldn\'t be embedded. Please double check your URL.', 'jetpack' ) );
364        }
365
366        if ( ! $rss->get_item_quantity() ) {
367            return new WP_Error( 'no_tracks', __( 'Podcast audio RSS feed has no tracks.', 'jetpack' ) );
368        }
369
370        return $rss;
371    }
372
373    /**
374     * Filter to override the default number of seconds to cache RSS feed data for the current feed.
375     * Note that we don't use the feed's URL because some of the SimplePie feed caches trigger this
376     * filter with a feed identifier and not a URL.
377     *
378     * @param int $cache_timeout_in_seconds Number of seconds to cache the podcast feed.
379     *
380     * @return int The number of seconds to cache the podcast feed.
381     */
382    public function filter_podcast_cache_timeout( $cache_timeout_in_seconds ) {
383        if ( $this->cache_timeout !== null ) {
384            return $this->cache_timeout;
385        }
386
387        return $cache_timeout_in_seconds;
388    }
389
390    /**
391     * Action handler to set our podcast specific feed locator class on the SimplePie object.
392     *
393     * @param SimplePie\SimplePie $feed The SimplePie object, passed by reference.
394     */
395    public static function set_podcast_locator( &$feed ) {
396        if ( ! class_exists( 'Jetpack_Podcast_Feed_Locator' ) ) {
397            require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-podcast-feed-locator.php';
398        }
399
400        $feed->get_registry()->register( SimplePie\Locator::class, 'Jetpack_Podcast_Feed_Locator' );
401    }
402
403    /**
404     * Action handler to reset the SimplePie cache for the podcast feed.
405     *
406     * Note this only resets the cache for the specified url. If the feed locator finds the podcast feed
407     * within the markup of the that url, that feed itself may still be cached.
408     *
409     * @param SimplePie\SimplePie $feed The SimplePie object, passed by reference.
410     * @return void
411     */
412    public static function reset_simplepie_cache( &$feed ) {
413        // Retrieve the cache object for a feed url. Based on:
414        // https://github.com/WordPress/WordPress/blob/fd1c2cb4011845ceb7244a062b09b2506082b1c9/wp-includes/class-simplepie.php#L1412.
415        // @todo This method of getting the cache is deprecated, and there doesn't seem to be a real replacement. `$feed->get_cache()` is private.
416        // @phan-suppress-next-line PhanUndeclaredClassReference
417        $cache = $feed->registry->call( 'Cache', 'get_handler', array( $feed->cache_location, call_user_func( $feed->cache_name_function, $feed->feed_url ), 'spc' ) );
418
419        if ( method_exists( $cache, 'unlink' ) ) {
420            $cache->unlink();
421        }
422    }
423
424    /**
425     * Prepares Episode data to be used by the Podcast Player block.
426     *
427     * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
428     * @return array
429     */
430    protected function setup_tracks_callback( SimplePie\Item $episode ) {
431        $enclosure = $this->get_audio_enclosure( $episode );
432
433        // If the audio enclosure is empty then it is not playable.
434        // We therefore return an empty array for this track.
435        // It will be filtered out later.
436        if ( is_wp_error( $enclosure ) ) {
437            return array();
438        }
439
440        // If there is no link return an empty array. We will filter out later.
441        if ( empty( $enclosure->link ) ) {
442            return array();
443        }
444
445        $publish_date = $episode->get_gmdate( DATE_ATOM );
446        // Build track data.
447        $track = array(
448            'id'               => wp_unique_id( 'podcast-track-' ),
449            'link'             => esc_url( $episode->get_link() ),
450            'src'              => esc_url( $enclosure->link ),
451            'type'             => esc_attr( $enclosure->type ),
452            'description'      => $this->get_plain_text( $episode->get_description() ),
453            'description_html' => $this->get_html_text( $episode->get_description() ),
454            'title'            => $this->get_plain_text( $episode->get_title() ),
455            'image'            => esc_url( $this->get_episode_image_url( $episode ) ),
456            'guid'             => $this->get_plain_text( $episode->get_id() ),
457            'publish_date'     => $publish_date ? $publish_date : null,
458        );
459
460        if ( empty( $track['title'] ) ) {
461            $track['title'] = esc_html__( '(no title)', 'jetpack' );
462        }
463
464        if ( ! empty( $enclosure->duration ) ) {
465            $track['duration'] = esc_html( $this->format_track_duration( $enclosure->duration ) );
466        }
467
468        return $track;
469    }
470
471    /**
472     * Retrieves an episode's image URL, if it's available.
473     *
474     * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
475     * @param string         $itunes_ns The itunes namespace, defaulted to the standard 1.0 version.
476     * @return string|null The image URL or null if not found.
477     */
478    protected function get_episode_image_url( SimplePie\Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) {
479        $image = $episode->get_item_tags( $itunes_ns, 'image' );
480        if ( isset( $image[0]['attribs']['']['href'] ) ) {
481            return $image[0]['attribs']['']['href'];
482        }
483        return null;
484    }
485
486    /**
487     * Retrieves an audio enclosure.
488     *
489     * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
490     * @return SimplePie\Enclosure|null
491     */
492    protected function get_audio_enclosure( SimplePie\Item $episode ) {
493        foreach ( (array) $episode->get_enclosures() as $enclosure ) {
494            if ( str_starts_with( $enclosure->type ?? '', 'audio/' ) ) {
495                return $enclosure;
496            }
497        }
498
499        return new WP_Error( 'invalid_audio', __( 'Podcast audio is an invalid type.', 'jetpack' ) );
500    }
501
502    /**
503     * Returns the track duration as a formatted string.
504     *
505     * @param int|float $duration of the track in seconds.
506     * @return string
507     */
508    protected function format_track_duration( $duration ) {
509        $format = $duration > HOUR_IN_SECONDS ? 'H:i:s' : 'i:s';
510
511        return date_i18n( $format, $duration );
512    }
513
514    /**
515     * Gets podcast player data schema.
516     *
517     * Useful for json schema in REST API endpoints.
518     *
519     * @return array Player data json schema.
520     */
521    public static function get_player_data_schema() {
522        return array(
523            '$schema'    => 'http://json-schema.org/draft-04/schema#',
524            'title'      => 'jetpack-podcast-player-data',
525            'type'       => 'object',
526            'properties' => array(
527                'title'   => array(
528                    'description' => __( 'The title of the podcast.', 'jetpack' ),
529                    'type'        => 'string',
530                ),
531                'link'    => array(
532                    'description' => __( 'The URL of the podcast website.', 'jetpack' ),
533                    'type'        => 'string',
534                    'format'      => 'uri',
535                ),
536                'cover'   => array(
537                    'description' => __( 'The URL of the podcast cover image.', 'jetpack' ),
538                    'type'        => 'string',
539                    'format'      => 'uri',
540                ),
541                'tracks'  => self::get_tracks_schema(),
542                'options' => self::get_options_schema(),
543            ),
544        );
545    }
546
547    /**
548     * Gets tracks data schema.
549     *
550     * Useful for json schema in REST API endpoints.
551     *
552     * @return array Tracks json schema.
553     */
554    public static function get_tracks_schema() {
555        return array(
556            'description' => __( 'Latest episodes of the podcast.', 'jetpack' ),
557            'type'        => 'array',
558            'items'       => array(
559                'type'       => 'object',
560                'properties' => array(
561                    'id'               => array(
562                        'description' => __( 'The episode id. Generated per request, not globally unique.', 'jetpack' ),
563                        'type'        => 'string',
564                    ),
565                    'link'             => array(
566                        'description' => __( 'The external link for the episode.', 'jetpack' ),
567                        'type'        => 'string',
568                        'format'      => 'uri',
569                    ),
570                    'src'              => array(
571                        'description' => __( 'The audio file URL of the episode.', 'jetpack' ),
572                        'type'        => 'string',
573                        'format'      => 'uri',
574                    ),
575                    'type'             => array(
576                        'description' => __( 'The mime type of the episode.', 'jetpack' ),
577                        'type'        => 'string',
578                    ),
579                    'description'      => array(
580                        'description' => __( 'The episode description, in plaintext.', 'jetpack' ),
581                        'type'        => 'string',
582                    ),
583                    'description_html' => array(
584                        'description' => __( 'The episode description with allowed html tags.', 'jetpack' ),
585                        'type'        => 'string',
586                    ),
587                    'title'            => array(
588                        'description' => __( 'The episode title.', 'jetpack' ),
589                        'type'        => 'string',
590                    ),
591                    'publish_date'     => array(
592                        'description' => __( 'The UTC publish date and time of the episode', 'jetpack' ),
593                        'type'        => 'string',
594                        'format'      => 'date-time',
595                    ),
596                ),
597            ),
598        );
599    }
600
601    /**
602     * Gets the episode options schema.
603     *
604     * Useful for json schema in REST API endpoints.
605     *
606     * @return array Tracks json schema.
607     */
608    public static function get_options_schema() {
609        return array(
610            'description' => __( 'The options that will be displayed in the episode selection UI', 'jetpack' ),
611            'type'        => 'array',
612            'items'       => array(
613                'type'       => 'object',
614                'properties' => array(
615                    'label' => array(
616                        'description' => __( 'The display label of the option, the episode title.', 'jetpack' ),
617                        'type'        => 'string',
618                    ),
619                    'value' => array(
620                        'description' => __( 'The value used for that option, the episode GUID', 'jetpack' ),
621                        'type'        => 'string',
622                    ),
623                ),
624            ),
625        );
626    }
627}