Completed
Push — add/sync-partial-sync-checksum... ( 661477...737c11 )
by
unknown
50:27 queued 41:58
created

Jetpack_Podcast_Helper::get_episode_image_url()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
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
	 * @return array|WP_Error  The player data or a error object.
35
	 */
36
	public function get_player_data() {
37
		// Try loading data from the cache.
38
		$transient_key = 'jetpack_podcast_' . md5( $this->feed );
39
		$player_data   = get_transient( $transient_key );
40
41
		// Fetch data if we don't have any cached.
42
		if ( false === $player_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
43
			// Load feed.
44
			$rss = $this->load_feed();
45
46
			if ( is_wp_error( $rss ) ) {
47
				return $rss;
48
			}
49
50
			// Get tracks.
51
			$tracks = $this->get_track_list();
52
53
			if ( empty( $tracks ) ) {
54
				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...
55
			}
56
57
			// Get podcast meta.
58
			$title = $rss->get_title();
0 ignored issues
show
Bug introduced by
The method get_title() does not seem to exist on object<WP_Error>.

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...
59
			$title = $this->get_plain_text( $title );
60
61
			$cover = $rss->get_image_url();
0 ignored issues
show
Bug introduced by
The method get_image_url() does not seem to exist on object<WP_Error>.

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...
62
			$cover = ! empty( $cover ) ? esc_url( $cover ) : null;
63
64
			$link = $rss->get_link();
0 ignored issues
show
Bug introduced by
The method get_link() does not seem to exist on object<WP_Error>.

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...
65
			$link = ! empty( $link ) ? esc_url( $link ) : null;
66
67
			$player_data = array(
68
				'title'  => $title,
69
				'link'   => $link,
70
				'cover'  => $cover,
71
				'tracks' => $tracks,
72
			);
73
74
			// Cache for 1 hour.
75
			set_transient( $transient_key, $player_data, HOUR_IN_SECONDS );
76
		}
77
78
		return $player_data;
79
	}
80
81
	/**
82
	 * Gets a specific track from the supplied feed URL.
83
	 *
84
	 * @param string $guid     The GUID of the track.
85
	 * @return array|WP_Error  The track object or an error object.
86
	 */
87
	public function get_track_data( $guid ) {
88
		// Try loading track data from the cache.
89
		$transient_key = 'jetpack_podcast_' . md5( "$this->feed::$guid" );
90
		$track_data    = get_transient( $transient_key );
91
92
		// Fetch data if we don't have any cached.
93
		if ( false === $track_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
94
			// Load feed.
95
			$rss = $this->load_feed();
96
97
			if ( is_wp_error( $rss ) ) {
98
				return $rss;
99
			}
100
101
			// Loop over all tracks to find the one.
102
			foreach ( $rss->get_items() as $track ) {
0 ignored issues
show
Bug introduced by
The method get_items() does not seem to exist on object<WP_Error>.

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...
103
				if ( $guid === $track->get_id() ) {
104
					$track_data = $this->setup_tracks_callback( $track );
105
					break;
106
				}
107
			}
108
109
			if ( false === $track_data ) {
110
				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...
111
			}
112
113
			// Cache for 1 hour.
114
			set_transient( $transient_key, $track_data, HOUR_IN_SECONDS );
115
		}
116
117
		return $track_data;
118
	}
119
120
	/**
121
	 * Gets a list of tracks for the supplied RSS feed.
122
	 *
123
	 * @return array|WP_Error The feed's tracks or a error object.
124
	 */
125
	public function get_track_list() {
126
		$rss = $this->load_feed();
127
128
		if ( is_wp_error( $rss ) ) {
129
			return $rss;
130
		}
131
132
		// Get first ten items and format them.
133
		$track_list = array_map( array( __CLASS__, 'setup_tracks_callback' ), $rss->get_items( 0, 10 ) );
0 ignored issues
show
Bug introduced by
The method get_items() does not seem to exist on object<WP_Error>.

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...
134
135
		// Filter out any tracks that are empty.
136
		// Reset the array indicies.
137
		return array_values( array_filter( $track_list ) );
138
	}
139
140
	/**
141
	 * Formats string as pure plaintext, with no HTML tags or entities present.
142
	 * This is ready to be used in React, innerText but needs to be escaped
143
	 * using standard `esc_html` when generating markup on server.
144
	 *
145
	 * @param string $str Input string.
146
	 * @return string Plain text string.
147
	 */
148
	protected function get_plain_text( $str ) {
149
		// Trim string and return if empty.
150
		$str = trim( (string) $str );
151
		if ( empty( $str ) ) {
152
			return '';
153
		}
154
155
		// Make sure there are no tags.
156
		$str = wp_strip_all_tags( $str );
157
158
		// Replace all entities with their characters, including all types of quotes.
159
		$str = html_entity_decode( $str, ENT_QUOTES );
160
161
		return $str;
162
	}
163
164
	/**
165
	 * Loads an RSS feed using `fetch_feed`.
166
	 *
167
	 * @return SimplePie|WP_Error The RSS object or error.
168
	 */
169
	public function load_feed() {
170
		$rss = fetch_feed( $this->feed );
171
		if ( is_wp_error( $rss ) ) {
172
			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...
173
		}
174
175
		if ( ! $rss->get_item_quantity() ) {
176
			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...
177
		}
178
179
		return $rss;
180
	}
181
182
	/**
183
	 * Prepares Episode data to be used by the Podcast Player block.
184
	 *
185
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
186
	 * @return array
187
	 */
188
	protected function setup_tracks_callback( SimplePie_Item $episode ) {
189
		$enclosure = $this->get_audio_enclosure( $episode );
190
191
		// If the audio enclosure is empty then it is not playable.
192
		// We therefore return an empty array for this track.
193
		// It will be filtered out later.
194
		if ( is_wp_error( $enclosure ) ) {
195
			return array();
196
		}
197
198
		// If there is no link return an empty array. We will filter out later.
199
		if ( empty( $enclosure->link ) ) {
200
			return array();
201
		}
202
203
		// Build track data.
204
		$track = array(
205
			'id'          => wp_unique_id( 'podcast-track-' ),
206
			'link'        => esc_url( $episode->get_link() ),
207
			'src'         => esc_url( $enclosure->link ),
208
			'type'        => esc_attr( $enclosure->type ),
209
			'description' => $this->get_plain_text( $episode->get_description() ),
210
			'title'       => $this->get_plain_text( $episode->get_title() ),
211
			'image'       => esc_url( $this->get_episode_image_url( $episode ) ),
212
			'guid'        => $this->get_plain_text( $episode->get_id() ),
213
		);
214
215
		if ( empty( $track['title'] ) ) {
216
			$track['title'] = esc_html__( '(no title)', 'jetpack' );
217
		}
218
219
		if ( ! empty( $enclosure->duration ) ) {
220
			$track['duration'] = esc_html( $this->format_track_duration( $enclosure->duration ) );
221
		}
222
223
		return $track;
224
	}
225
226
	/**
227
	 * Retrieves an episode's image URL, if it's available.
228
	 *
229
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
230
	 * @param string         $itunes_ns The itunes namespace, defaulted to the standard 1.0 version.
231
	 * @return string|null The image URL or null if not found.
232
	 */
233
	protected function get_episode_image_url( SimplePie_Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) {
234
		$image = $episode->get_item_tags( $itunes_ns, 'image' );
235
		if ( isset( $image[0]['attribs']['']['href'] ) ) {
236
			return $image[0]['attribs']['']['href'];
237
		}
238
		return null;
239
	}
240
241
	/**
242
	 * Retrieves an audio enclosure.
243
	 *
244
	 * @param SimplePie_Item $episode SimplePie_Item object, representing a podcast episode.
245
	 * @return SimplePie_Enclosure|null
246
	 */
247
	protected function get_audio_enclosure( SimplePie_Item $episode ) {
248
		foreach ( (array) $episode->get_enclosures() as $enclosure ) {
249
			if ( 0 === strpos( $enclosure->type, 'audio/' ) ) {
250
				return $enclosure;
251
			}
252
		}
253
254
		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...
255
	}
256
257
	/**
258
	 * Returns the track duration as a formatted string.
259
	 *
260
	 * @param number $duration of the track in seconds.
261
	 * @return string
262
	 */
263
	protected function format_track_duration( $duration ) {
264
		$format = $duration > HOUR_IN_SECONDS ? 'H:i:s' : 'i:s';
265
266
		return date_i18n( $format, $duration );
267
	}
268
}
269