Completed
Push — update/connect-podcast-player-... ( eb8dbb...ca5bd7 )
by
unknown
36:16 queued 25:36
created

Jetpack_Podcast_Helper::get_player_data()   C

Complexity

Conditions 11
Paths 48

Size

Total Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 48
nop 1
dl 0
loc 56
rs 6.8133
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
	 * }
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
44
		// Try loading data from the cache.
45
		$transient_key = 'jetpack_podcast_' . md5( $this->feed . implode( ',', $guids ) );
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 a list of episodes by guid or all tracks in feed.
58
			if ( count( $guids ) ) {
59
				$tracks = array_map( array( $this, 'get_track_data' ), $guids );
60
				$tracks = array_filter(
61
					$tracks,
62
					function ( $track ) {
63
						return ! is_wp_error( $track );
64
					}
65
				);
66
			} else {
67
				$tracks = $this->get_track_list();
68
			}
69
70
			if ( empty( $tracks ) ) {
71
				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...
72
			}
73
74
			// Get podcast meta.
75
			$title = $rss->get_title();
76
			$title = $this->get_plain_text( $title );
77
78
			$cover = $rss->get_image_url();
79
			$cover = ! empty( $cover ) ? esc_url( $cover ) : null;
80
81
			$link = $rss->get_link();
82
			$link = ! empty( $link ) ? esc_url( $link ) : null;
83
84
			$player_data = array(
85
				'title'  => $title,
86
				'link'   => $link,
87
				'cover'  => $cover,
88
				'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
	 * Gets a specific track from the supplied feed URL.
100
	 *
101
	 * @param string $guid     The GUID of the track.
102
	 * @return array|WP_Error  The track object or an error object.
103
	 */
104
	public function get_track_data( $guid ) {
105
		// Try loading track data from the cache.
106
		$transient_key = 'jetpack_podcast_' . md5( "$this->feed::$guid" );
107
		$track_data    = get_transient( $transient_key );
108
109
		// Fetch data if we don't have any cached.
110
		if ( false === $track_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
111
			// Load feed.
112
			$rss = $this->load_feed();
113
114
			if ( is_wp_error( $rss ) ) {
115
				return $rss;
116
			}
117
118
			// Loop over all tracks to find the one.
119
			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...
120
				if ( $guid === $track->get_id() ) {
121
					$track_data = $this->setup_tracks_callback( $track );
122
					break;
123
				}
124
			}
125
126
			if ( false === $track_data ) {
127
				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...
128
			}
129
130
			// Cache for 1 hour.
131
			set_transient( $transient_key, $track_data, HOUR_IN_SECONDS );
132
		}
133
134
		return $track_data;
135
	}
136
137
	/**
138
	 * Gets a list of tracks for the supplied RSS feed.
139
	 *
140
	 * @return array|WP_Error The feed's tracks or a error object.
141
	 */
142
	public function get_track_list() {
143
		$rss = $this->load_feed();
144
145
		if ( is_wp_error( $rss ) ) {
146
			return $rss;
147
		}
148
149
		// Get first ten items and format them.
150
		$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...
151
152
		// Filter out any tracks that are empty.
153
		// Reset the array indicies.
154
		return array_values( array_filter( $track_list ) );
155
	}
156
157
	/**
158
	 * Formats string as pure plaintext, with no HTML tags or entities present.
159
	 * This is ready to be used in React, innerText but needs to be escaped
160
	 * using standard `esc_html` when generating markup on server.
161
	 *
162
	 * @param string $str Input string.
163
	 * @return string Plain text string.
164
	 */
165
	protected function get_plain_text( $str ) {
166
		// Trim string and return if empty.
167
		$str = trim( (string) $str );
168
		if ( empty( $str ) ) {
169
			return '';
170
		}
171
172
		// Make sure there are no tags.
173
		$str = wp_strip_all_tags( $str );
174
175
		// Replace all entities with their characters, including all types of quotes.
176
		$str = html_entity_decode( $str, ENT_QUOTES );
177
178
		return $str;
179
	}
180
181
	/**
182
	 * Loads an RSS feed using `fetch_feed`.
183
	 *
184
	 * @return SimplePie|WP_Error The RSS object or error.
185
	 */
186
	public function load_feed() {
187
		add_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) );
188
		$rss = fetch_feed( $this->feed );
189
		remove_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) );
190
		if ( is_wp_error( $rss ) ) {
191
			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...
192
		}
193
194
		if ( ! $rss->get_item_quantity() ) {
195
			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...
196
		}
197
198
		return $rss;
199
	}
200
201
	/**
202
	 * Action handler to set our podcast specific feed locator class on the SimplePie object.
203
	 *
204
	 * @param SimplePie $feed The SimplePie object, passed by reference.
205
	 */
206
	public static function set_podcast_locator( &$feed ) {
207
		if ( ! class_exists( 'Jetpack_Podcast_Feed_Locator' ) ) {
208
			jetpack_require_lib( 'class-jetpack-podcast-feed-locator' );
209
		}
210
211
		$feed->set_locator_class( 'Jetpack_Podcast_Feed_Locator' );
0 ignored issues
show
Bug introduced by
The method set_locator_class() does not seem to exist on object<SimplePie>.

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...
212
	}
213
214
	/**
215
	 * Prepares Episode data to be used by the Podcast Player block.
216
	 *
217
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
218
	 * @return array
219
	 */
220
	protected function setup_tracks_callback( SimplePie_Item $episode ) {
221
		$enclosure = $this->get_audio_enclosure( $episode );
222
223
		// If the audio enclosure is empty then it is not playable.
224
		// We therefore return an empty array for this track.
225
		// It will be filtered out later.
226
		if ( is_wp_error( $enclosure ) ) {
227
			return array();
228
		}
229
230
		// If there is no link return an empty array. We will filter out later.
231
		if ( empty( $enclosure->link ) ) {
232
			return array();
233
		}
234
235
		// Build track data.
236
		$track = array(
237
			'id'          => wp_unique_id( 'podcast-track-' ),
238
			'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...
239
			'src'         => esc_url( $enclosure->link ),
240
			'type'        => esc_attr( $enclosure->type ),
241
			'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...
242
			'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...
243
			'image'       => esc_url( $this->get_episode_image_url( $episode ) ),
244
			'guid'        => $this->get_plain_text( $episode->get_id() ),
245
		);
246
247
		if ( empty( $track['title'] ) ) {
248
			$track['title'] = esc_html__( '(no title)', 'jetpack' );
249
		}
250
251
		if ( ! empty( $enclosure->duration ) ) {
252
			$track['duration'] = esc_html( $this->format_track_duration( $enclosure->duration ) );
253
		}
254
255
		return $track;
256
	}
257
258
	/**
259
	 * Retrieves an episode's image URL, if it's available.
260
	 *
261
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
262
	 * @param string         $itunes_ns The itunes namespace, defaulted to the standard 1.0 version.
263
	 * @return string|null The image URL or null if not found.
264
	 */
265
	protected function get_episode_image_url( SimplePie_Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) {
266
		$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...
267
		if ( isset( $image[0]['attribs']['']['href'] ) ) {
268
			return $image[0]['attribs']['']['href'];
269
		}
270
		return null;
271
	}
272
273
	/**
274
	 * Retrieves an audio enclosure.
275
	 *
276
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
277
	 * @return SimplePie_Enclosure|null
278
	 */
279
	protected function get_audio_enclosure( SimplePie_Item $episode ) {
280
		foreach ( (array) $episode->get_enclosures() as $enclosure ) {
281
			if ( 0 === strpos( $enclosure->type, 'audio/' ) ) {
282
				return $enclosure;
283
			}
284
		}
285
286
		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...
287
	}
288
289
	/**
290
	 * Returns the track duration as a formatted string.
291
	 *
292
	 * @param number $duration of the track in seconds.
293
	 * @return string
294
	 */
295
	protected function format_track_duration( $duration ) {
296
		$format = $duration > HOUR_IN_SECONDS ? 'H:i:s' : 'i:s';
297
298
		return date_i18n( $format, $duration );
299
	}
300
301
	/**
302
	 * Gets podcast player data schema.
303
	 *
304
	 * Useful for json schema in REST API endpoints.
305
	 *
306
	 * @return array Player data json schema.
307
	 */
308
	public static function get_player_data_schema() {
309
		return array(
310
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
311
			'title'      => 'jetpack-podcast-player-data',
312
			'type'       => 'object',
313
			'properties' => array(
314
				'title'  => array(
315
					'description' => __( 'The title of the podcast.', 'jetpack' ),
316
					'type'        => 'string',
317
				),
318
				'link'   => array(
319
					'description' => __( 'The URL of the podcast website.', 'jetpack' ),
320
					'type'        => 'string',
321
					'format'      => 'uri',
322
				),
323
				'cover'  => array(
324
					'description' => __( 'The URL of the podcast cover image.', 'jetpack' ),
325
					'type'        => 'string',
326
					'format'      => 'uri',
327
				),
328
				'tracks' => self::get_tracks_schema(),
329
			),
330
		);
331
	}
332
333
	/**
334
	 * Gets tracks data schema.
335
	 *
336
	 * Useful for json schema in REST API endpoints.
337
	 *
338
	 * @return array Tracks json schema.
339
	 */
340
	public static function get_tracks_schema() {
341
		return array(
342
			'description' => __( 'Latest episodes of the podcast.', 'jetpack' ),
343
			'type'        => 'array',
344
			'items'       => array(
345
				'type'       => 'object',
346
				'properties' => array(
347
					'id'          => array(
348
						'description' => __( 'The episode id. Generated per request, not globally unique.', 'jetpack' ),
349
						'type'        => 'string',
350
					),
351
					'link'        => array(
352
						'description' => __( 'The external link for the episode.', 'jetpack' ),
353
						'type'        => 'string',
354
						'format'      => 'uri',
355
					),
356
					'src'         => array(
357
						'description' => __( 'The audio file URL of the episode.', 'jetpack' ),
358
						'type'        => 'string',
359
						'format'      => 'uri',
360
					),
361
					'type'        => array(
362
						'description' => __( 'The mime type of the episode.', 'jetpack' ),
363
						'type'        => 'string',
364
					),
365
					'description' => array(
366
						'description' => __( 'The episode description, in plaintext.', 'jetpack' ),
367
						'type'        => 'string',
368
					),
369
					'title'       => array(
370
						'description' => __( 'The episode title.', 'jetpack' ),
371
						'type'        => 'string',
372
					),
373
				),
374
			),
375
		);
376
	}
377
}
378