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
|
|
|
* The RSS feed of the podcast. |
14
|
|
|
* |
15
|
|
|
* @var string |
16
|
|
|
*/ |
17
|
|
|
protected $feed = null; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Initialize class. |
21
|
|
|
* |
22
|
|
|
* @param string $feed The RSS feed of the podcast. |
23
|
|
|
*/ |
24
|
|
|
public function __construct( $feed ) { |
25
|
|
|
$this->feed = esc_url_raw( $feed ); |
26
|
|
|
} |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Gets podcast data formatted to be used by the Podcast Player block in both server-side |
30
|
|
|
* block rendering and in API `WPCOM_REST_API_V2_Endpoint_Podcast_Player`. |
31
|
|
|
* |
32
|
|
|
* The result is cached for one hour. |
33
|
|
|
* |
34
|
|
|
* @param array $args { |
35
|
|
|
* Optional array of arguments. |
36
|
|
|
* @type string|int $guid The ID of a specific episode to return rather than a list. |
37
|
|
|
* } |
38
|
|
|
* |
39
|
|
|
* @return array|WP_Error The player data or a error object. |
40
|
|
|
*/ |
41
|
|
|
public function get_player_data( $args = array() ) { |
42
|
|
|
$guids = isset( $args['guids'] ) && $args['guids'] ? $args['guids'] : array(); |
43
|
|
|
$episode_options = isset( $args['episode-options'] ) && $args['episode-options']; |
44
|
|
|
|
45
|
|
|
// Try loading data from the cache. |
46
|
|
|
$transient_key = 'jetpack_podcast_' . md5( $this->feed . implode( ',', $guids ) . "-$episode_options" ); |
47
|
|
|
$player_data = get_transient( $transient_key ); |
48
|
|
|
|
49
|
|
|
// Fetch data if we don't have any cached. |
50
|
|
|
if ( false === $player_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) { |
51
|
|
|
// Load feed. |
52
|
|
|
$rss = $this->load_feed(); |
53
|
|
|
|
54
|
|
|
if ( is_wp_error( $rss ) ) { |
55
|
|
|
return $rss; |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
// Get a list of episodes by guid or all tracks in feed. |
59
|
|
|
if ( count( $guids ) ) { |
60
|
|
|
$tracks = array_map( array( $this, 'get_track_data' ), $guids ); |
61
|
|
|
$tracks = array_filter( |
62
|
|
|
$tracks, |
63
|
|
|
function ( $track ) { |
64
|
|
|
return ! is_wp_error( $track ); |
65
|
|
|
} |
66
|
|
|
); |
67
|
|
|
} else { |
68
|
|
|
$tracks = $this->get_track_list(); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
if ( empty( $tracks ) ) { |
72
|
|
|
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' ) ); |
|
|
|
|
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
// Get podcast meta. |
76
|
|
|
$title = $rss->get_title(); |
77
|
|
|
$title = $this->get_plain_text( $title ); |
78
|
|
|
|
79
|
|
|
$description = $rss->get_description(); |
80
|
|
|
$description = $this->get_plain_text( $description ); |
81
|
|
|
|
82
|
|
|
$cover = $rss->get_image_url(); |
83
|
|
|
$cover = ! empty( $cover ) ? esc_url( $cover ) : null; |
84
|
|
|
|
85
|
|
|
$link = $rss->get_link(); |
86
|
|
|
$link = ! empty( $link ) ? esc_url( $link ) : null; |
87
|
|
|
|
88
|
|
|
$player_data = array( |
89
|
|
|
'title' => $title, |
90
|
|
|
'description' => $description, |
91
|
|
|
'link' => $link, |
92
|
|
|
'cover' => $cover, |
93
|
|
|
'tracks' => $tracks, |
94
|
|
|
); |
95
|
|
|
|
96
|
|
|
if ( $episode_options ) { |
97
|
|
|
$player_data['options'] = array(); |
98
|
|
|
foreach ( $rss->get_items() as $episode ) { |
|
|
|
|
99
|
|
|
$enclosure = $this->get_audio_enclosure( $episode ); |
100
|
|
|
// If the episode doesn't have playable audio, then don't include it. |
101
|
|
|
if ( is_wp_error( $enclosure ) ) { |
102
|
|
|
continue; |
103
|
|
|
} |
104
|
|
|
$player_data['options'][] = array( |
105
|
|
|
'label' => $this->get_plain_text( $episode->get_title() ), |
106
|
|
|
'value' => $episode->get_id(), |
107
|
|
|
); |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
// Cache for 1 hour. |
112
|
|
|
set_transient( $transient_key, $player_data, HOUR_IN_SECONDS ); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
return $player_data; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Gets a specific track from the supplied feed URL. |
120
|
|
|
* |
121
|
|
|
* @param string $guid The GUID of the track. |
122
|
|
|
* @param boolean $force_refresh Clear the feed cache. |
123
|
|
|
* @return array|WP_Error The track object or an error object. |
124
|
|
|
*/ |
125
|
|
|
public function get_track_data( $guid, $force_refresh = false ) { |
126
|
|
|
// Get the cache key. |
127
|
|
|
$transient_key = 'jetpack_podcast_' . md5( "$this->feed::$guid" ); |
128
|
|
|
|
129
|
|
|
// Clear the cache if force_refresh param is true. |
130
|
|
|
if ( true === $force_refresh ) { |
131
|
|
|
delete_transient( $transient_key ); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
// Try loading track data from the cache. |
135
|
|
|
$track_data = get_transient( $transient_key ); |
136
|
|
|
|
137
|
|
|
// Fetch data if we don't have any cached. |
138
|
|
|
if ( false === $track_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) { |
139
|
|
|
// Load feed. |
140
|
|
|
$rss = $this->load_feed( $force_refresh ); |
141
|
|
|
|
142
|
|
|
if ( is_wp_error( $rss ) ) { |
143
|
|
|
return $rss; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
// Loop over all tracks to find the one. |
147
|
|
|
foreach ( $rss->get_items() as $track ) { |
|
|
|
|
148
|
|
|
if ( $guid === $track->get_id() ) { |
149
|
|
|
$track_data = $this->setup_tracks_callback( $track ); |
150
|
|
|
break; |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
if ( false === $track_data ) { |
155
|
|
|
return new WP_Error( 'no_track', __( 'The track was not found.', 'jetpack' ) ); |
|
|
|
|
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
// Cache for 1 hour. |
159
|
|
|
set_transient( $transient_key, $track_data, HOUR_IN_SECONDS ); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return $track_data; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Gets a list of tracks for the supplied RSS feed. |
167
|
|
|
* |
168
|
|
|
* @return array|WP_Error The feed's tracks or a error object. |
169
|
|
|
*/ |
170
|
|
|
public function get_track_list() { |
171
|
|
|
$rss = $this->load_feed(); |
172
|
|
|
|
173
|
|
|
if ( is_wp_error( $rss ) ) { |
174
|
|
|
return $rss; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Allow requesting a specific number of tracks from SimplePie's `get_items` call. |
179
|
|
|
* The default number of tracks is ten. |
180
|
|
|
* |
181
|
|
|
* @since 9.5.0 |
182
|
|
|
* |
183
|
|
|
* @param int $number Number of tracks fetched. Default is 10. |
184
|
|
|
* @param object $rss The SimplePie object built from core's `fetch_feed` call. |
185
|
|
|
*/ |
186
|
|
|
$tracks_quantity = apply_filters( 'jetpack_podcast_helper_list_quantity', 10, $rss ); |
|
|
|
|
187
|
|
|
|
188
|
|
|
// Process the requested number of items from our feed. |
189
|
|
|
$track_list = array_map( array( __CLASS__, 'setup_tracks_callback' ), $rss->get_items( 0, $tracks_quantity ) ); |
|
|
|
|
190
|
|
|
|
191
|
|
|
// Filter out any tracks that are empty. |
192
|
|
|
// Reset the array indicies. |
193
|
|
|
return array_values( array_filter( $track_list ) ); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Formats string as pure plaintext, with no HTML tags or entities present. |
198
|
|
|
* This is ready to be used in React, innerText but needs to be escaped |
199
|
|
|
* using standard `esc_html` when generating markup on server. |
200
|
|
|
* |
201
|
|
|
* @param string $str Input string. |
202
|
|
|
* @return string Plain text string. |
203
|
|
|
*/ |
204
|
|
|
protected function get_plain_text( $str ) { |
205
|
|
|
return $this->sanitize_and_decode_text( $str, true ); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Formats strings as safe HTML. |
210
|
|
|
* |
211
|
|
|
* @param string $str Input string. |
212
|
|
|
* @return string HTML text string safe for post_content. |
213
|
|
|
*/ |
214
|
|
|
protected function get_html_text( $str ) { |
215
|
|
|
return $this->sanitize_and_decode_text( $str, false ); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Strip unallowed html tags and decode entities. |
220
|
|
|
* |
221
|
|
|
* @param string $str Input string. |
222
|
|
|
* @param boolean $strip_all_tags Strip all tags, otherwise allow post_content safe tags. |
223
|
|
|
* @return string Sanitized and decoded text. |
224
|
|
|
*/ |
225
|
|
|
protected function sanitize_and_decode_text( $str, $strip_all_tags = true ) { |
226
|
|
|
// Trim string and return if empty. |
227
|
|
|
$str = trim( (string) $str ); |
228
|
|
|
if ( empty( $str ) ) { |
229
|
|
|
return ''; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
if ( $strip_all_tags ) { |
233
|
|
|
// Make sure there are no tags. |
234
|
|
|
$str = wp_strip_all_tags( $str ); |
235
|
|
|
} else { |
236
|
|
|
$str = wp_kses_post( $str ); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
// Replace all entities with their characters, including all types of quotes. |
240
|
|
|
$str = html_entity_decode( $str, ENT_QUOTES ); |
241
|
|
|
|
242
|
|
|
return $str; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Loads an RSS feed using `fetch_feed`. |
247
|
|
|
* |
248
|
|
|
* @param boolean $force_refresh Clear the feed cache. |
249
|
|
|
* @return SimplePie|WP_Error The RSS object or error. |
250
|
|
|
*/ |
251
|
|
|
public function load_feed( $force_refresh = false ) { |
252
|
|
|
// Add action: clear the SimplePie Cache if $force_refresh param is true. |
253
|
|
|
if ( true === $force_refresh ) { |
254
|
|
|
add_action( 'wp_feed_options', array( __CLASS__, 'reset_simplepie_cache' ) ); |
255
|
|
|
} |
256
|
|
|
// Add action: detect the podcast feed from the provided feed URL. |
257
|
|
|
add_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) ); |
258
|
|
|
|
259
|
|
|
// Fetch the feed. |
260
|
|
|
$rss = fetch_feed( $this->feed ); |
261
|
|
|
|
262
|
|
|
// Remove added actions from wp_feed_options hook. |
263
|
|
|
remove_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) ); |
264
|
|
|
if ( true === $force_refresh ) { |
265
|
|
|
remove_action( 'wp_feed_options', array( __CLASS__, 'reset_simplepie_cache' ) ); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
if ( is_wp_error( $rss ) ) { |
269
|
|
|
return new WP_Error( 'invalid_url', __( 'Your podcast couldn\'t be embedded. Please double check your URL.', 'jetpack' ) ); |
|
|
|
|
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
if ( ! $rss->get_item_quantity() ) { |
273
|
|
|
return new WP_Error( 'no_tracks', __( 'Podcast audio RSS feed has no tracks.', 'jetpack' ) ); |
|
|
|
|
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return $rss; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* Action handler to set our podcast specific feed locator class on the SimplePie object. |
281
|
|
|
* |
282
|
|
|
* @param SimplePie $feed The SimplePie object, passed by reference. |
283
|
|
|
*/ |
284
|
|
|
public static function set_podcast_locator( &$feed ) { |
285
|
|
|
if ( ! class_exists( 'Jetpack_Podcast_Feed_Locator' ) ) { |
286
|
|
|
jetpack_require_lib( 'class-jetpack-podcast-feed-locator' ); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
$feed->set_locator_class( 'Jetpack_Podcast_Feed_Locator' ); |
|
|
|
|
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Action handler to reset the SimplePie cache for the podcast feed. |
294
|
|
|
* |
295
|
|
|
* Note this only resets the cache for the specified url. If the feed locator finds the podcast feed |
296
|
|
|
* within the markup of the that url, that feed itself may still be cached. |
297
|
|
|
* |
298
|
|
|
* @param SimplePie $feed The SimplePie object, passed by reference. |
299
|
|
|
* @return void |
300
|
|
|
*/ |
301
|
|
|
public static function reset_simplepie_cache( &$feed ) { |
302
|
|
|
// Retrieve the cache object for a feed url. Based on: |
303
|
|
|
// https://github.com/WordPress/WordPress/blob/fd1c2cb4011845ceb7244a062b09b2506082b1c9/wp-includes/class-simplepie.php#L1412. |
304
|
|
|
$cache = $feed->registry->call( 'Cache', 'get_handler', array( $feed->cache_location, call_user_func( $feed->cache_name_function, $feed->feed_url ), 'spc' ) ); |
|
|
|
|
305
|
|
|
|
306
|
|
|
if ( method_exists( $cache, 'unlink' ) ) { |
307
|
|
|
$cache->unlink(); |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Prepares Episode data to be used by the Podcast Player block. |
313
|
|
|
* |
314
|
|
|
* @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode. |
315
|
|
|
* @return array |
316
|
|
|
*/ |
317
|
|
|
protected function setup_tracks_callback( SimplePie_Item $episode ) { |
318
|
|
|
$enclosure = $this->get_audio_enclosure( $episode ); |
319
|
|
|
|
320
|
|
|
// If the audio enclosure is empty then it is not playable. |
321
|
|
|
// We therefore return an empty array for this track. |
322
|
|
|
// It will be filtered out later. |
323
|
|
|
if ( is_wp_error( $enclosure ) ) { |
324
|
|
|
return array(); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
// If there is no link return an empty array. We will filter out later. |
328
|
|
|
if ( empty( $enclosure->link ) ) { |
329
|
|
|
return array(); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
$publish_date = $episode->get_gmdate( DATE_ATOM ); |
|
|
|
|
333
|
|
|
// Build track data. |
334
|
|
|
$track = array( |
335
|
|
|
'id' => wp_unique_id( 'podcast-track-' ), |
336
|
|
|
'link' => esc_url( $episode->get_link() ), |
|
|
|
|
337
|
|
|
'src' => esc_url( $enclosure->link ), |
338
|
|
|
'type' => esc_attr( $enclosure->type ), |
339
|
|
|
'description' => $this->get_plain_text( $episode->get_description() ), |
|
|
|
|
340
|
|
|
'description_html' => $this->get_html_text( $episode->get_description() ), |
|
|
|
|
341
|
|
|
'title' => $this->get_plain_text( $episode->get_title() ), |
|
|
|
|
342
|
|
|
'image' => esc_url( $this->get_episode_image_url( $episode ) ), |
343
|
|
|
'guid' => $this->get_plain_text( $episode->get_id() ), |
344
|
|
|
'publish_date' => $publish_date ? $publish_date : null, |
345
|
|
|
); |
346
|
|
|
|
347
|
|
|
if ( empty( $track['title'] ) ) { |
348
|
|
|
$track['title'] = esc_html__( '(no title)', 'jetpack' ); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
if ( ! empty( $enclosure->duration ) ) { |
352
|
|
|
$track['duration'] = esc_html( $this->format_track_duration( $enclosure->duration ) ); |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
return $track; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Retrieves an episode's image URL, if it's available. |
360
|
|
|
* |
361
|
|
|
* @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode. |
362
|
|
|
* @param string $itunes_ns The itunes namespace, defaulted to the standard 1.0 version. |
363
|
|
|
* @return string|null The image URL or null if not found. |
364
|
|
|
*/ |
365
|
|
|
protected function get_episode_image_url( SimplePie_Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) { |
366
|
|
|
$image = $episode->get_item_tags( $itunes_ns, 'image' ); |
|
|
|
|
367
|
|
|
if ( isset( $image[0]['attribs']['']['href'] ) ) { |
368
|
|
|
return $image[0]['attribs']['']['href']; |
369
|
|
|
} |
370
|
|
|
return null; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Retrieves an audio enclosure. |
375
|
|
|
* |
376
|
|
|
* @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode. |
377
|
|
|
* @return SimplePie_Enclosure|null |
378
|
|
|
*/ |
379
|
|
|
protected function get_audio_enclosure( SimplePie_Item $episode ) { |
380
|
|
|
foreach ( (array) $episode->get_enclosures() as $enclosure ) { |
381
|
|
|
if ( 0 === strpos( $enclosure->type, 'audio/' ) ) { |
382
|
|
|
return $enclosure; |
383
|
|
|
} |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
return new WP_Error( 'invalid_audio', __( 'Podcast audio is an invalid type.', 'jetpack' ) ); |
|
|
|
|
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* Returns the track duration as a formatted string. |
391
|
|
|
* |
392
|
|
|
* @param number $duration of the track in seconds. |
393
|
|
|
* @return string |
394
|
|
|
*/ |
395
|
|
|
protected function format_track_duration( $duration ) { |
396
|
|
|
$format = $duration > HOUR_IN_SECONDS ? 'H:i:s' : 'i:s'; |
397
|
|
|
|
398
|
|
|
return date_i18n( $format, $duration ); |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* Gets podcast player data schema. |
403
|
|
|
* |
404
|
|
|
* Useful for json schema in REST API endpoints. |
405
|
|
|
* |
406
|
|
|
* @return array Player data json schema. |
407
|
|
|
*/ |
408
|
|
|
public static function get_player_data_schema() { |
409
|
|
|
return array( |
410
|
|
|
'$schema' => 'http://json-schema.org/draft-04/schema#', |
411
|
|
|
'title' => 'jetpack-podcast-player-data', |
412
|
|
|
'type' => 'object', |
413
|
|
|
'properties' => array( |
414
|
|
|
'title' => array( |
415
|
|
|
'description' => __( 'The title of the podcast.', 'jetpack' ), |
416
|
|
|
'type' => 'string', |
417
|
|
|
), |
418
|
|
|
'link' => array( |
419
|
|
|
'description' => __( 'The URL of the podcast website.', 'jetpack' ), |
420
|
|
|
'type' => 'string', |
421
|
|
|
'format' => 'uri', |
422
|
|
|
), |
423
|
|
|
'cover' => array( |
424
|
|
|
'description' => __( 'The URL of the podcast cover image.', 'jetpack' ), |
425
|
|
|
'type' => 'string', |
426
|
|
|
'format' => 'uri', |
427
|
|
|
), |
428
|
|
|
'tracks' => self::get_tracks_schema(), |
429
|
|
|
'options' => self::get_options_schema(), |
430
|
|
|
), |
431
|
|
|
); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
/** |
435
|
|
|
* Gets tracks data schema. |
436
|
|
|
* |
437
|
|
|
* Useful for json schema in REST API endpoints. |
438
|
|
|
* |
439
|
|
|
* @return array Tracks json schema. |
440
|
|
|
*/ |
441
|
|
|
public static function get_tracks_schema() { |
442
|
|
|
return array( |
443
|
|
|
'description' => __( 'Latest episodes of the podcast.', 'jetpack' ), |
444
|
|
|
'type' => 'array', |
445
|
|
|
'items' => array( |
446
|
|
|
'type' => 'object', |
447
|
|
|
'properties' => array( |
448
|
|
|
'id' => array( |
449
|
|
|
'description' => __( 'The episode id. Generated per request, not globally unique.', 'jetpack' ), |
450
|
|
|
'type' => 'string', |
451
|
|
|
), |
452
|
|
|
'link' => array( |
453
|
|
|
'description' => __( 'The external link for the episode.', 'jetpack' ), |
454
|
|
|
'type' => 'string', |
455
|
|
|
'format' => 'uri', |
456
|
|
|
), |
457
|
|
|
'src' => array( |
458
|
|
|
'description' => __( 'The audio file URL of the episode.', 'jetpack' ), |
459
|
|
|
'type' => 'string', |
460
|
|
|
'format' => 'uri', |
461
|
|
|
), |
462
|
|
|
'type' => array( |
463
|
|
|
'description' => __( 'The mime type of the episode.', 'jetpack' ), |
464
|
|
|
'type' => 'string', |
465
|
|
|
), |
466
|
|
|
'description' => array( |
467
|
|
|
'description' => __( 'The episode description, in plaintext.', 'jetpack' ), |
468
|
|
|
'type' => 'string', |
469
|
|
|
), |
470
|
|
|
'description_html' => array( |
471
|
|
|
'description' => __( 'The episode description with allowed html tags.', 'jetpack' ), |
472
|
|
|
'type' => 'string', |
473
|
|
|
), |
474
|
|
|
'title' => array( |
475
|
|
|
'description' => __( 'The episode title.', 'jetpack' ), |
476
|
|
|
'type' => 'string', |
477
|
|
|
), |
478
|
|
|
'publish_date' => array( |
479
|
|
|
'description' => __( 'The UTC publish date and time of the episode', 'jetpack' ), |
480
|
|
|
'type' => 'string', |
481
|
|
|
'format' => 'date-time', |
482
|
|
|
), |
483
|
|
|
), |
484
|
|
|
), |
485
|
|
|
); |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
/** |
489
|
|
|
* Gets the episode options schema. |
490
|
|
|
* |
491
|
|
|
* Useful for json schema in REST API endpoints. |
492
|
|
|
* |
493
|
|
|
* @return array Tracks json schema. |
494
|
|
|
*/ |
495
|
|
|
public static function get_options_schema() { |
496
|
|
|
return array( |
497
|
|
|
'description' => __( 'The options that will be displayed in the episode selection UI', 'jetpack' ), |
498
|
|
|
'type' => 'array', |
499
|
|
|
'items' => array( |
500
|
|
|
'type' => 'object', |
501
|
|
|
'properties' => array( |
502
|
|
|
'label' => array( |
503
|
|
|
'description' => __( 'The display label of the option, the episode title.', 'jetpack' ), |
504
|
|
|
'type' => 'string', |
505
|
|
|
), |
506
|
|
|
'value' => array( |
507
|
|
|
'description' => __( 'The value used for that option, the episode GUID', 'jetpack' ), |
508
|
|
|
'type' => 'string', |
509
|
|
|
), |
510
|
|
|
), |
511
|
|
|
), |
512
|
|
|
); |
513
|
|
|
} |
514
|
|
|
} |
515
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.