PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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
← All changes | _inc/lib/class-jetpack-podcast-helper.php +169 -24 12.7.3 → 16.3-a.7 View file →
@@ -9,8 +9,31 @@
9 9 * Class Jetpack_Podcast_Helper
10 10 */
11 11 class Jetpack_Podcast_Helper {
12 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 + /**
13 36 * The RSS feed of the podcast.
14 37 *
15 38 * @var string
16 39 */
@@ -60,9 +83,9 @@
60 83
61 84 /**
62 85 * Retrieves tracks quantity.
63 86 *
64 - * @returns int number of tracks
87 + * @return int number of tracks
65 88 */
66 89 public static function get_tracks_quantity() {
67 90 /**
68 91 * Allow requesting a specific number of tracks from SimplePie's `get_items` call.
@@ -78,13 +101,18 @@
78 101 /**
79 102 * Gets podcast data formatted to be used by the Podcast Player block in both server-side
80 103 * block rendering and in API `WPCOM_REST_API_V2_Endpoint_Podcast_Player`.
81 104 *
82 - * The result is cached for one hour.
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.
83 108 *
84 109 * @param array $args {
85 110 * Optional array of arguments.
86 - * @type string|int $guid The ID of a specific episode to return rather than a list.
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.
87 115 * }
88 116 *
89 117 * @return array|WP_Error The player data or a error object.
90 118 */
@@ -90,13 +118,19 @@
90 118 */
91 119 public function get_player_data( $args = array() ) {
92 120 $guids = isset( $args['guids'] ) && $args['guids'] ? $args['guids'] : array();
93 121 $episode_options = isset( $args['episode-options'] ) && $args['episode-options'];
122 + $report_errors = isset( $args['report_errors'] ) && $args['report_errors'];
94 123
95 124 // Try loading data from the cache.
96 125 $transient_key = 'jetpack_podcast_' . md5( $this->feed . implode( ',', $guids ) . "-$episode_options" );
97 126 $player_data = get_transient( $transient_key );
98 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 +
99 133 // Fetch data if we don't have any cached.
100 134 if ( false === $player_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
101 135 // Load feed.
102 136 $rss = $this->load_feed();
@@ -101,9 +135,9 @@
101 135 // Load feed.
102 136 $rss = $this->load_feed();
103 137
104 138 if ( is_wp_error( $rss ) ) {
105 - return $rss;
139 + return $this->handle_failure( $transient_key, $rss, $report_errors );
106 140 }
107 141
108 142 // Get a list of episodes by guid or all tracks in feed.
109 143 if ( count( $guids ) ) {
@@ -117,10 +151,18 @@
117 151 } else {
118 152 $tracks = $this->get_track_list();
119 153 }
120 154
155 + if ( is_wp_error( $tracks ) ) {
156 + return $this->handle_failure( $transient_key, $tracks, $report_errors );
157 + }
158 +
121 159 if ( empty( $tracks ) ) {
122 - 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' ) );
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 + );
123 165 }
124 166
125 167 // Get podcast meta.
126 168 $title = $rss->get_title();
@@ -157,16 +199,117 @@
157 199 );
158 200 }
159 201 }
160 202
161 - // Cache for 1 hour.
162 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;
163 211 }
164 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 +
165 224 return $player_data;
166 225 }
167 226
168 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 + /**
169 312 * Gets a specific track from the supplied feed URL.
170 313 *
171 314 * @param string $guid The GUID of the track.
172 315 * @param boolean $force_refresh Clear the feed cache.
@@ -242,9 +385,9 @@
242 385 // Process the requested number of items from our feed.
243 386 $track_list = array_map( array( __CLASS__, 'setup_tracks_callback' ), $rss->get_items( 0, $tracks_quantity ) );
244 387
245 388 // Filter out any tracks that are empty.
246 - // Reset the array indicies.
389 + // Reset the array indices.
247 390 return array_values( array_filter( $track_list ) );
248 391 }
249 392
250 393 /**
@@ -299,9 +442,9 @@
299 442 /**
300 443 * Loads an RSS feed using `fetch_feed`.
301 444 *
302 445 * @param boolean $force_refresh Clear the feed cache.
303 - * @return SimplePie|WP_Error The RSS object or error.
446 + * @return SimplePie\SimplePie|WP_Error The RSS object or error.
304 447 */
305 448 public function load_feed( $force_refresh = false ) {
306 449 // Add action: clear the SimplePie Cache if $force_refresh param is true.
307 450 if ( true === $force_refresh ) {
@@ -348,9 +491,9 @@
348 491 *
349 492 * Note that this action runs after other hooks added by Jetpack have been removed.
350 493 *
351 494 * @param string $podcast_url URL for the podcast's RSS feed.
352 - * @param SimplePie|WP_Error $rss Either the SimplePie RSS object or an error.
495 + * @param SimplePie\SimplePie|SimplePie|WP_Error $rss Either the SimplePie RSS object or an error.
353 496 *
354 497 * @since 11.2
355 498 */
356 499 do_action( 'jetpack_podcast_post_fetch', $this->feed, $rss );
@@ -385,9 +528,9 @@
385 528
386 529 /**
387 530 * Action handler to set our podcast specific feed locator class on the SimplePie object.
388 531 *
389 - * @param SimplePie $feed The SimplePie object, passed by reference.
532 + * @param SimplePie\SimplePie $feed The SimplePie object, passed by reference.
390 533 */
391 534 public static function set_podcast_locator( &$feed ) {
392 535 if ( ! class_exists( 'Jetpack_Podcast_Feed_Locator' ) ) {
393 536 require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-podcast-feed-locator.php';
@@ -392,9 +535,9 @@
392 535 if ( ! class_exists( 'Jetpack_Podcast_Feed_Locator' ) ) {
393 536 require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-podcast-feed-locator.php';
394 537 }
395 538
396 - $feed->set_locator_class( 'Jetpack_Podcast_Feed_Locator' );
539 + $feed->get_registry()->register( SimplePie\Locator::class, 'Jetpack_Podcast_Feed_Locator' );
397 540 }
398 541
399 542 /**
400 543 * Action handler to reset the SimplePie cache for the podcast feed.
@@ -401,14 +544,16 @@
401 544 *
402 545 * Note this only resets the cache for the specified url. If the feed locator finds the podcast feed
403 546 * within the markup of the that url, that feed itself may still be cached.
404 547 *
405 - * @param SimplePie $feed The SimplePie object, passed by reference.
548 + * @param SimplePie\SimplePie $feed The SimplePie object, passed by reference.
406 549 * @return void
407 550 */
408 551 public static function reset_simplepie_cache( &$feed ) {
409 552 // Retrieve the cache object for a feed url. Based on:
410 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
411 556 $cache = $feed->registry->call( 'Cache', 'get_handler', array( $feed->cache_location, call_user_func( $feed->cache_name_function, $feed->feed_url ), 'spc' ) );
412 557
413 558 if ( method_exists( $cache, 'unlink' ) ) {
414 559 $cache->unlink();
@@ -417,12 +562,12 @@
417 562
418 563 /**
419 564 * Prepares Episode data to be used by the Podcast Player block.
420 565 *
421 - * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
566 + * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
422 567 * @return array
423 568 */
424 - protected function setup_tracks_callback( SimplePie_Item $episode ) {
569 + protected function setup_tracks_callback( SimplePie\Item $episode ) {
425 570 $enclosure = $this->get_audio_enclosure( $episode );
426 571
427 572 // If the audio enclosure is empty then it is not playable.
428 573 // We therefore return an empty array for this track.
@@ -440,10 +585,10 @@
440 585 // Build track data.
441 586 $track = array(
442 587 'id' => wp_unique_id( 'podcast-track-' ),
443 588 'link' => esc_url( $episode->get_link() ),
444 - 'src' => esc_url( $enclosure->link ),
445 - 'type' => esc_attr( $enclosure->type ),
589 + 'src' => esc_url( (string) $enclosure->link ),
590 + 'type' => esc_attr( (string) $enclosure->type ),
446 591 'description' => $this->get_plain_text( $episode->get_description() ),
447 592 'description_html' => $this->get_html_text( $episode->get_description() ),
448 593 'title' => $this->get_plain_text( $episode->get_title() ),
449 594 'image' => esc_url( $this->get_episode_image_url( $episode ) ),
@@ -455,9 +600,9 @@
455 600 $track['title'] = esc_html__( '(no title)', 'jetpack' );
456 601 }
457 602
458 603 if ( ! empty( $enclosure->duration ) ) {
459 - $track['duration'] = esc_html( $this->format_track_duration( $enclosure->duration ) );
604 + $track['duration'] = esc_html( $this->format_track_duration( (int) $enclosure->duration ) );
460 605 }
461 606
462 607 return $track;
463 608 }
@@ -464,13 +609,13 @@
464 609
465 610 /**
466 611 * Retrieves an episode's image URL, if it's available.
467 612 *
468 - * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
613 + * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
469 614 * @param string $itunes_ns The itunes namespace, defaulted to the standard 1.0 version.
470 615 * @return string|null The image URL or null if not found.
471 616 */
472 - protected function get_episode_image_url( SimplePie_Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) {
617 + protected function get_episode_image_url( SimplePie\Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) {
473 618 $image = $episode->get_item_tags( $itunes_ns, 'image' );
474 619 if ( isset( $image[0]['attribs']['']['href'] ) ) {
475 620 return $image[0]['attribs']['']['href'];
476 621 }
@@ -479,14 +624,14 @@
479 624
480 625 /**
481 626 * Retrieves an audio enclosure.
482 627 *
483 - * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
484 - * @return SimplePie_Enclosure|null
628 + * @param SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
629 + * @return SimplePie\Enclosure|null
485 630 */
486 - protected function get_audio_enclosure( SimplePie_Item $episode ) {
631 + protected function get_audio_enclosure( SimplePie\Item $episode ) {
487 632 foreach ( (array) $episode->get_enclosures() as $enclosure ) {
488 - if ( 0 === strpos( $enclosure->type, 'audio/' ) ) {
633 + if ( str_starts_with( $enclosure->type ?? '', 'audio/' ) ) {
489 634 return $enclosure;
490 635 }
491 636 }
492 637
@@ -495,9 +640,9 @@
495 640
496 641 /**
497 642 * Returns the track duration as a formatted string.
498 643 *
499 - * @param number $duration of the track in seconds.
644 + * @param int|float $duration of the track in seconds.
500 645 * @return string
501 646 */
502 647 protected function format_track_duration( $duration ) {
503 648 $format = $duration > HOUR_IN_SECONDS ? 'H:i:s' : 'i:s';