Completed
Push — update/podcast-player-single-e... ( 29bbed...c67876 )
by
unknown
558:12 queued 548:14
created

Jetpack_Podcast_Helper::get_player_data()   C

Complexity

Conditions 15
Paths 86

Size

Total Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
nc 86
nop 1
dl 0
loc 56
rs 5.9166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Helper to massage Podcast data to be used in the Podcast block.
4
 *
5
 * @package 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
	 *    @type string     $query A search query to find podcast episodes.
38
	 * }
39
	 * @return array|WP_Error  The player data or a error object.
40
	 */
41
	public function get_player_data( $args = array() ) {
42
		$guid = ! empty( $args['guid'] ) ? $args['guid'] : false;
43
44
		// Try loading data from the cache.
45
		$transient_key = 'jetpack_podcast_' . md5( $this->feed . $guid ? "-$guid" : '' );
46
		$player_data   = get_transient( $transient_key );
47
48
		// Fetch data if we don't have any cached.
49
		if ( false === $player_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
50
			// Load feed.
51
			$rss = $this->load_feed();
52
53
			if ( is_wp_error( $rss ) ) {
54
				return $rss;
55
			}
56
57
			// Get tracks or a single episode.
58
			if ( false !== $guid ) {
59
				$track  = $this->get_track_data( $guid );
60
				$tracks = is_wp_error( $track ) ? null : array( $track );
61
			} elseif ( ! empty( $args['query'] ) ) {
62
				$tracks = $this->search_tracks( $args['query'] );
63
				$tracks = is_wp_error( $tracks ) ? null : $tracks;
64
			} else {
65
				$tracks = $this->get_track_list();
66
				if ( empty( $tracks ) ) {
67
					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' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_tracks'.

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.

Loading history...
68
				}
69
			}
70
71
			// Get podcast meta.
72
			$title = $rss->get_title();
73
			$title = $this->get_plain_text( $title );
74
75
			$cover = $rss->get_image_url();
76
			$cover = ! empty( $cover ) ? esc_url( $cover ) : null;
77
78
			$link = $rss->get_link();
79
			$link = ! empty( $link ) ? esc_url( $link ) : null;
80
81
			$player_data = array(
82
				'title' => $title,
83
				'link'  => $link,
84
				'cover' => $cover,
85
			);
86
87
			if ( $tracks ) {
88
				$player_data['tracks'] = $tracks;
89
			}
90
91
			// Cache for 1 hour.
92
			set_transient( $transient_key, $player_data, HOUR_IN_SECONDS );
93
		}
94
95
		return $player_data;
96
	}
97
98
	/**
99
	 * Does a simplistic fuzzy search of the podcast episode titles using the given search term.
100
	 *
101
	 * @param  string $query  The search term to find.
102
	 * @return array|WP_Error An array of up to 10 matching episode details, or a `WP_Error` if there's an error
103
	 */
104
	public function search_tracks( $query ) {
105
		// We're going to sanitize strings by removing accents, and we need to set a locale for that.
106
		$current_locale = setlocale( LC_ALL, 0 );
107
		$transient_key  = 'jetpack_podcast_search_' . md5( $this->feed );
108
		$search_data    = get_transient( $transient_key );
109
		$needle         = $this->sanitize_for_search( $query );
110
111
		// Check we have a valid search term.
112
		if ( empty( $needle ) ) {
113
			return new WP_Error( 'no_query', __( 'The search query is invalid as it contains no alphanumeric characters.', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_query'.

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.

Loading history...
114
		}
115
116
		// Fetch data if we don't have any cached.
117
		if ( false === $search_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
118
			$rss = $this->load_feed();
119
120
			if ( is_wp_error( $rss ) ) {
121
				return $rss;
122
			}
123
124
			setlocale( LC_ALL, 'en_US.utf8' );
125
			$track_list  = array_values( array_filter( array_map( array( __CLASS__, 'setup_tracks_callback' ), $rss->get_items() ) ) );
0 ignored issues
show
Bug introduced by
The method get_items does only exist in SimplePie, but not in WP_Error.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
126
			$search_data = array_map(
127
				function ( $track ) {
128
					// We don't want the search string to be too long or it could cause problems with levenshtein.
129
					$track['search_cache'] = $this->sanitize_for_search( substr( $track['title'], 0, 255 ) );
130
					return $track;
131
				},
132
				$track_list
133
			);
134
			set_transient( $transient_key, $search_data, HOUR_IN_SECONDS );
135
		}
136
137
		// Calculate the levenshtein scores for each track.
138
		$search_data = array_map(
139
			function ( $track ) use ( $needle ) {
140
				$track['score'] = levenshtein( $track['search_cache'], $needle );
141
				return $track;
142
			},
143
			$search_data
144
		);
145
146
		// Filter out any values that are too far away.
147
		$needle_length = strlen( $needle );
148
		$search_data   = array_filter(
149
			$search_data,
150
			function ( $track ) use ( $needle_length ) {
151
				return $track['score'] <= ( strlen( $track['search_cache'] ) - $needle_length );
152
			}
153
		);
154
155
		// Sort the data by doing a fuzzy search for the query string.
156
		usort(
157
			$search_data,
158
			function ( $a, $b ) use ( $needle ) {
159
				$in_a = strpos( $a['search_cache'], $needle ) !== false;
160
				$in_b = strpos( $b['search_cache'], $needle ) !== false;
161
				if ( $in_a && ! $in_b ) {
162
					return -1;
163
				}
164
				if ( ! $in_a && $in_b ) {
165
					return 1;
166
				}
167
168
				if ( $a['score'] === $b['score'] ) {
169
					return 0;
170
				}
171
				return $a['score'] < $b['score'] ? -1 : 1;
172
			}
173
		);
174
175
		// Make sure we restore the locale.
176
		setlocale( LC_ALL, $current_locale );
177
		return array_slice( $search_data, 0, 10 );
178
	}
179
180
	/**
181
	 * Converts a string to alphanumeric values and spaces, in order to help with matching
182
	 *
183
	 * @param  string $string The string to sanitize.
184
	 * @return string         The string converted to just alphanumeric characters, removing accents etc.
185
	 */
186
	private function sanitize_for_search( $string ) {
187
		$unaccented = iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $string );
188
		return trim(
189
			preg_replace(
190
				'/\s+/',
191
				' ',
192
				preg_replace(
193
					'/[^a-z0-9]+/',
194
					' ',
195
					strtolower( $unaccented )
196
				)
197
			)
198
		);
199
	}
200
201
	/**
202
	 * Gets a specific track from the supplied feed URL.
203
	 *
204
	 * @param string $guid     The GUID of the track.
205
	 * @return array|WP_Error  The track object or an error object.
206
	 */
207
	public function get_track_data( $guid ) {
208
		// Try loading track data from the cache.
209
		$transient_key = 'jetpack_podcast_' . md5( "$this->feed::$guid" );
210
		$track_data    = get_transient( $transient_key );
211
212
		// Fetch data if we don't have any cached.
213
		if ( false === $track_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
214
			// Load feed.
215
			$rss = $this->load_feed();
216
217
			if ( is_wp_error( $rss ) ) {
218
				return $rss;
219
			}
220
221
			// Loop over all tracks to find the one.
222
			foreach ( $rss->get_items() as $track ) {
0 ignored issues
show
Bug introduced by
The method get_items does only exist in SimplePie, but not in WP_Error.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
223
				if ( $guid === $track->get_id() ) {
224
					$track_data = $this->setup_tracks_callback( $track );
225
					break;
226
				}
227
			}
228
229
			if ( false === $track_data ) {
230
				return new WP_Error( 'no_track', __( 'The track was not found.', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_track'.

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.

Loading history...
231
			}
232
233
			// Cache for 1 hour.
234
			set_transient( $transient_key, $track_data, HOUR_IN_SECONDS );
235
		}
236
237
		return $track_data;
238
	}
239
240
	/**
241
	 * Gets a list of tracks for the supplied RSS feed.
242
	 *
243
	 * @return array|WP_Error The feed's tracks or a error object.
244
	 */
245
	public function get_track_list() {
246
		$rss = $this->load_feed();
247
248
		if ( is_wp_error( $rss ) ) {
249
			return $rss;
250
		}
251
252
		// Get first ten items and format them.
253
		$track_list = array_map( array( __CLASS__, 'setup_tracks_callback' ), $rss->get_items( 0, 10 ) );
0 ignored issues
show
Unused Code introduced by
The call to SimplePie::get_items() has too many arguments starting with 0.

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.

Loading history...
Bug introduced by
The method get_items does only exist in SimplePie, but not in WP_Error.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
254
255
		// Filter out any tracks that are empty.
256
		// Reset the array indicies.
257
		return array_values( array_filter( $track_list ) );
258
	}
259
260
	/**
261
	 * Formats string as pure plaintext, with no HTML tags or entities present.
262
	 * This is ready to be used in React, innerText but needs to be escaped
263
	 * using standard `esc_html` when generating markup on server.
264
	 *
265
	 * @param string $str Input string.
266
	 * @return string Plain text string.
267
	 */
268
	protected function get_plain_text( $str ) {
269
		// Trim string and return if empty.
270
		$str = trim( (string) $str );
271
		if ( empty( $str ) ) {
272
			return '';
273
		}
274
275
		// Make sure there are no tags.
276
		$str = wp_strip_all_tags( $str );
277
278
		// Replace all entities with their characters, including all types of quotes.
279
		$str = html_entity_decode( $str, ENT_QUOTES );
280
281
		return $str;
282
	}
283
284
	/**
285
	 * Loads an RSS feed using `fetch_feed`.
286
	 *
287
	 * @return SimplePie|WP_Error The RSS object or error.
288
	 */
289 View Code Duplication
	public function load_feed() {
290
		$rss = fetch_feed( $this->feed );
291
		if ( is_wp_error( $rss ) ) {
292
			return new WP_Error( 'invalid_url', __( 'Your podcast couldn\'t be embedded. Please double check your URL.', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_url'.

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.

Loading history...
293
		}
294
295
		if ( ! $rss->get_item_quantity() ) {
296
			return new WP_Error( 'no_tracks', __( 'Podcast audio RSS feed has no tracks.', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_tracks'.

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.

Loading history...
297
		}
298
299
		return $rss;
300
	}
301
302
	/**
303
	 * Prepares Episode data to be used by the Podcast Player block.
304
	 *
305
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
306
	 * @return array
307
	 */
308
	protected function setup_tracks_callback( SimplePie_Item $episode ) {
309
		$enclosure = $this->get_audio_enclosure( $episode );
310
311
		// If the audio enclosure is empty then it is not playable.
312
		// We therefore return an empty array for this track.
313
		// It will be filtered out later.
314
		if ( is_wp_error( $enclosure ) ) {
315
			return array();
316
		}
317
318
		// If there is no link return an empty array. We will filter out later.
319
		if ( empty( $enclosure->link ) ) {
320
			return array();
321
		}
322
323
		// Build track data.
324
		$track = array(
325
			'id'          => wp_unique_id( 'podcast-track-' ),
326
			'link'        => esc_url( $episode->get_link() ),
0 ignored issues
show
Bug introduced by
The method get_link() does not seem to exist on object<SimplePie_Item>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
327
			'src'         => esc_url( $enclosure->link ),
328
			'type'        => esc_attr( $enclosure->type ),
329
			'description' => $this->get_plain_text( $episode->get_description() ),
0 ignored issues
show
Bug introduced by
The method get_description() does not seem to exist on object<SimplePie_Item>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
330
			'title'       => $this->get_plain_text( $episode->get_title() ),
0 ignored issues
show
Bug introduced by
The method get_title() does not seem to exist on object<SimplePie_Item>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
331
			'image'       => esc_url( $this->get_episode_image_url( $episode ) ),
332
			'guid'        => $this->get_plain_text( $episode->get_id() ),
333
		);
334
335
		if ( empty( $track['title'] ) ) {
336
			$track['title'] = esc_html__( '(no title)', 'jetpack' );
337
		}
338
339
		if ( ! empty( $enclosure->duration ) ) {
340
			$track['duration'] = esc_html( $this->format_track_duration( $enclosure->duration ) );
341
		}
342
343
		return $track;
344
	}
345
346
	/**
347
	 * Retrieves an episode's image URL, if it's available.
348
	 *
349
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
350
	 * @param string         $itunes_ns The itunes namespace, defaulted to the standard 1.0 version.
351
	 * @return string|null The image URL or null if not found.
352
	 */
353
	protected function get_episode_image_url( SimplePie_Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) {
354
		$image = $episode->get_item_tags( $itunes_ns, 'image' );
0 ignored issues
show
Bug introduced by
The method get_item_tags() does not seem to exist on object<SimplePie_Item>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
355
		if ( isset( $image[0]['attribs']['']['href'] ) ) {
356
			return $image[0]['attribs']['']['href'];
357
		}
358
		return null;
359
	}
360
361
	/**
362
	 * Retrieves an audio enclosure.
363
	 *
364
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
365
	 * @return SimplePie_Enclosure|null
366
	 */
367
	protected function get_audio_enclosure( SimplePie_Item $episode ) {
368
		foreach ( (array) $episode->get_enclosures() as $enclosure ) {
369
			if ( 0 === strpos( $enclosure->type, 'audio/' ) ) {
370
				return $enclosure;
371
			}
372
		}
373
374
		return new WP_Error( 'invalid_audio', __( 'Podcast audio is an invalid type.', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_audio'.

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.

Loading history...
375
	}
376
377
	/**
378
	 * Returns the track duration as a formatted string.
379
	 *
380
	 * @param number $duration of the track in seconds.
381
	 * @return string
382
	 */
383
	protected function format_track_duration( $duration ) {
384
		$format = $duration > HOUR_IN_SECONDS ? 'H:i:s' : 'i:s';
385
386
		return date_i18n( $format, $duration );
387
	}
388
389
	/**
390
	 * Gets podcast player data schema.
391
	 *
392
	 * Useful for json schema in REST API endpoints.
393
	 *
394
	 * @return array Player data json schema.
395
	 */
396
	public static function get_player_data_schema() {
397
		return array(
398
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
399
			'title'      => 'jetpack-podcast-player-data',
400
			'type'       => 'object',
401
			'properties' => array(
402
				'title'  => array(
403
					'description' => __( 'The title of the podcast.', 'jetpack' ),
404
					'type'        => 'string',
405
				),
406
				'link'   => array(
407
					'description' => __( 'The URL of the podcast website.', 'jetpack' ),
408
					'type'        => 'string',
409
					'format'      => 'uri',
410
				),
411
				'cover'  => array(
412
					'description' => __( 'The URL of the podcast cover image.', 'jetpack' ),
413
					'type'        => 'string',
414
					'format'      => 'uri',
415
				),
416
				'tracks' => self::get_tracks_schema(),
417
			),
418
		);
419
	}
420
421
	/**
422
	 * Gets tracks data schema.
423
	 *
424
	 * Useful for json schema in REST API endpoints.
425
	 *
426
	 * @return array Tracks json schema.
427
	 */
428
	public static function get_tracks_schema() {
429
		return array(
430
			'description' => __( 'Latest episodes of the podcast.', 'jetpack' ),
431
			'type'        => 'array',
432
			'items'       => array(
433
				'type'       => 'object',
434
				'properties' => array(
435
					'id'          => array(
436
						'description' => __( 'The episode id. Generated per request, not globally unique.', 'jetpack' ),
437
						'type'        => 'string',
438
					),
439
					'link'        => array(
440
						'description' => __( 'The external link for the episode.', 'jetpack' ),
441
						'type'        => 'string',
442
						'format'      => 'uri',
443
					),
444
					'src'         => array(
445
						'description' => __( 'The audio file URL of the episode.', 'jetpack' ),
446
						'type'        => 'string',
447
						'format'      => 'uri',
448
					),
449
					'type'        => array(
450
						'description' => __( 'The mime type of the episode.', 'jetpack' ),
451
						'type'        => 'string',
452
					),
453
					'description' => array(
454
						'description' => __( 'The episode description, in plaintext.', 'jetpack' ),
455
						'type'        => 'string',
456
					),
457
					'title'       => array(
458
						'description' => __( 'The episode title.', 'jetpack' ),
459
						'type'        => 'string',
460
					),
461
				),
462
			),
463
		);
464
	}
465
}
466