PluginProbe
Friends / 4.3.1
Friends v4.3.1
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
friends / feed-parsers / class-feed-parser-simplepie.php

class-feed-parser-simplepie.php in Friends 4.3.1, at feed-parsers/class-feed-parser-simplepie.php

576 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Friends SimplePie Parser Wrapper
4 *
5 * With this parser, we can import RSS and Atom Feeds for a friend.
6 *
7 * @package Friends
8 */
9
10 namespace Friends;
11
12 /**
13 * This is the class for the feed part of the Friends Plugin.
14 *
15 * @since 1.0
16 *
17 * @package Friends
18 * @author Alex Kirk
19 */
20 class Feed_Parser_SimplePie extends Feed_Parser_V2 {
21 const NAME = 'SimplePie';
22 const URL = 'http://simplepie.org';
23 const SLUG = 'simplepie';
24
25 private $friends_feed;
26
27 /**
28 * Constructor.
29 *
30 * @param Feed $friends_feed The friends feed.
31 */
32 public function __construct( Feed $friends_feed ) {
33 $this->friends_feed = $friends_feed;
34
35 \add_filter( 'friends_get_comments', array( $this, 'get_comments' ), 10, 4 );
36 \add_filter( 'friends_no_comments_feed_available', array( $this, 'no_comments_feed_available' ), 10, 2 );
37 \add_filter( 'friends_modify_feed_item', array( $this, 'podcast_audio_support' ) );
38 \add_filter( 'friends_modify_feed_item', array( $this, 'youtube_video_support' ) );
39 }
40
41 /**
42 * Get the badge for RSS/Atom feeds.
43 *
44 * @return array Badge info.
45 */
46 public function get_badge() {
47 return array(
48 'label' => 'RSS',
49 'color' => '#5b9a68',
50 'title' => __( 'RSS/Atom Feed', 'friends' ),
51 );
52 }
53
54 /**
55 * Determines if this is a supported feed and to what degree we feel it's supported.
56 *
57 * @param string $url The url.
58 * @param string $mime_type The mime type.
59 * @param string $title The title.
60 * @param string|null $content The content, it can't be assumed that it's always available.
61 *
62 * @return int Return 0 if unsupported, a positive value representing the confidence for the feed, use 10 if you're reasonably confident.
63 */
64 public function feed_support_confidence( $url, $mime_type, $title, $content = null ) {
65 $rewritten = $this->rewrite_known_url( $url );
66 if ( $rewritten ) {
67 $mime_type = $rewritten['type'];
68 }
69
70 switch ( $mime_type ) {
71 case 'application/rss+xml':
72 case 'application/atom+xml':
73 return 10;
74 }
75
76 switch ( $mime_type ) {
77 case 'application/xml':
78 return 5;
79 }
80
81 return 0;
82 }
83
84 /**
85 * Rewrite known URLs to their RSS feeds.
86 *
87 * @param string $url The url.
88 *
89 * @return array An equivalent link array.
90 */
91 public function rewrite_known_url( $url ) {
92 $host = wp_parse_url( strtolower( $url ), PHP_URL_HOST );
93
94 switch ( $host ) {
95 case 'www.youtube.com':
96 case 'youtube.com':
97 if ( preg_match( '#/(?:channel/|@)([^?&$]+)#i', $url, $m ) ) {
98 return array(
99 'title' => 'Youtube',
100 'rel' => 'alternate',
101 'type' => 'application/rss+xml',
102 'url' => 'https://www.youtube.com/feeds/videos.xml?channel_id=' . $m[1],
103 'post-format' => 'video',
104 );
105 }
106 if ( preg_match( '#/user/([^?&$]+)#i', $url, $m ) ) {
107 return array(
108 'title' => 'Youtube',
109 'rel' => 'alternate',
110 'type' => 'application/rss+xml',
111 'url' => 'https://www.youtube.com/feeds/videos.xml?user=' . $m[1],
112 'post-format' => 'video',
113 );
114 }
115 return array();
116 case 'github.com':
117 return array(
118 'rel' => 'alternate',
119 'type' => 'application/atom+xml',
120 'url' => $url,
121 'post-format' => 'aside',
122 );
123 }
124
125 return array();
126 }
127
128 /**
129 * Format the feed title and autoselect the posts feed.
130 *
131 * @param array $feed_details The feed details.
132 *
133 * @return array The (potentially) modified feed details.
134 */
135 public function update_feed_details( $feed_details ) {
136 $rewritten = $this->rewrite_known_url( $feed_details['url'] );
137 if ( $rewritten ) {
138 $feed_details = $rewritten;
139 }
140
141 foreach ( get_post_format_strings() as $format => $title ) {
142 if ( preg_match( '/\b' . preg_quote( $format, '/' ) . '\b/i', $feed_details['url'] ) ) {
143 $feed_details['post-format'] = $format;
144 break;
145 }
146 }
147
148 return $feed_details;
149 }
150
151 /**
152 * Check whether a URL looks like a direct feed file.
153 *
154 * @param string $url The URL to check.
155 * @return bool Whether the URL has a feed file extension.
156 */
157 private function is_direct_feed_url( $url ) {
158 $path = wp_parse_url( $url, PHP_URL_PATH );
159 if ( ! $path ) {
160 return false;
161 }
162
163 return (bool) preg_match( '/\.(?:atom|rdf|rss|xml)$/i', $path );
164 }
165
166 /**
167 * Instanciate SimplePie the same way WordPress does it.
168 *
169 * @return SimplePie A simplepie instance.
170 */
171 private function get_simplepie() {
172 if ( ! class_exists( 'SimplePie', false ) ) {
173 require_once ABSPATH . WPINC . '/class-simplepie.php';
174 }
175
176 require_once ABSPATH . WPINC . '/class-wp-feed-cache-transient.php';
177 require_once ABSPATH . WPINC . '/class-wp-simplepie-file.php';
178 require_once __DIR__ . '/SimplePie/class-simplepie-file-accept-only-rss.php';
179 require_once __DIR__ . '/SimplePie/class-simplepie-misc.php';
180 require_once ABSPATH . WPINC . '/class-wp-simplepie-sanitize-kses.php';
181 require_once __DIR__ . '/SimplePie/class-simplepie-sanitize-kses.php';
182
183 // Workaround for SimplePie assuming that CURL is loaded.
184 if ( ! defined( 'CURLOPT_USERAGENT' ) ) {
185 define( 'CURLOPT_USERAGENT', 10018 );
186 }
187
188 $feed = new \SimplePie();
189
190 $feed->get_registry()->register( \SimplePie\Sanitize::class, __NAMESPACE__ . '\SimplePie_Sanitize_KSES', true );
191
192 // We must manually overwrite $feed->sanitize because SimplePie's
193 // constructor sets it before we have a chance to set the sanitization class.
194 $feed->sanitize = new SimplePie_Sanitize_KSES();
195
196 \SimplePie_Cache::register( 'wp_transient', '\WP_Feed_Cache_Transient' );
197 $feed->set_cache_location( 'wp_transient' );
198
199 $registry = $feed->get_registry();
200 $registry->register( 'Misc', __NAMESPACE__ . '\SimplePie_Misc' );
201
202 return $feed;
203 }
204
205 /**
206 * Discover the feeds available at the URL specified.
207 *
208 * @param string $content The content for the URL is already provided here.
209 * @param string $url The url to search.
210 *
211 * @return array A list of supported feeds at the URL.
212 */
213 public function discover_available_feeds( $content, $url ) {
214 if ( ! is_string( $content ) ) {
215 return array();
216 }
217 $feed = $this->get_simplepie();
218 do_action_ref_array( 'wp_feed_options', array( &$feed, $url ) );
219
220 $feed->set_raw_data( $content );
221
222 $feed->set_output_encoding( get_option( 'blog_charset' ) );
223
224 $feed->init();
225
226 if ( $feed->error() ) {
227 return array();
228 }
229
230 $discovered_feeds = array();
231
232 $items = $feed->get_items();
233 if ( count( $items ) ) {
234 // This is a feed.
235 $mime_type = false;
236 if ( $feed->get_type() & SIMPLEPIE_TYPE_RSS_ALL ) {
237 $mime_type = 'application/rss+xml';
238 } elseif ( $feed->get_type() & SIMPLEPIE_TYPE_ATOM_ALL ) {
239 $mime_type = 'application/atom+xml';
240 }
241
242 if ( $mime_type ) {
243 $feed_url = $feed->subscribe_url();
244 if ( ! $feed_url ) {
245 $feed_url = $url;
246 }
247
248 $discovered_feeds[ $feed_url ] = array(
249 'type' => $mime_type,
250 'title' => $feed->get_title(),
251 'parser' => 'simplepie',
252 'rel' => 'self',
253 'autoselect' => true,
254 );
255 }
256 }
257
258 return $discovered_feeds;
259 }
260
261 /**
262 * Fetches a feed and returns the processed items.
263 *
264 * @param string $url The url.
265 * @param User_Feed $user_feed The user feed.
266 *
267 * @return array An array of feed items.
268 */
269 public function fetch_feed( $url, ?User_Feed $user_feed = null ) {
270 // Use SimplePie which is bundled with WordPress.
271 $feed = $this->get_simplepie();
272
273 $host = wp_parse_url( strtolower( $url ), PHP_URL_HOST );
274
275 switch ( $host ) {
276 case 'github.com':
277 $feed->get_registry()->register( 'File', __NAMESPACE__ . '\SimplePie_File_Accept_Only_RSS' );
278 break;
279 default:
280 $feed->get_registry()->register( 'File', '\WP_SimplePie_File' );
281 }
282 /**
283 * Maybe Rewrite a URL
284 *
285 * Allows modifying the URL before fetching it.
286 *
287 * @param string $url The URL to fetch.
288 * @param Feed_Parser_V2 $parser The parser instance.
289 */
290 $feed->set_feed_url( $url );
291 $feed->set_cache_duration( apply_filters( 'wp_feed_cache_transient_lifetime', HOUR_IN_SECONDS - 600, $url ) );
292 if ( $this->is_direct_feed_url( $url ) ) {
293 $feed->force_feed( true );
294 }
295
296 do_action_ref_array( 'wp_feed_options', array( &$feed, $url ) );
297
298 $feed->init();
299
300 $feed->set_output_encoding( get_option( 'blog_charset' ) );
301
302 if ( $feed->error() ) {
303 return new \WP_Error( 'simplepie-error', $feed->error() );
304 }
305
306 return $this->process_items( $feed->get_items(), $url );
307 }
308
309 /**
310 * Process the feed items.
311 *
312 * @param array $items The items.
313 * @param string $url The url.
314 *
315 * @return array The processed feed items.
316 */
317 public function process_items( $items, $url ) {
318 $feed_items = array();
319 foreach ( $items as $item ) {
320 /* See https://www.rssboard.org/rss-encoding-examples */
321 $title = $item->get_title();
322 if ( $title ) {
323 $title = htmlspecialchars_decode( $title, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
324 if ( $title ) {
325 $title = \html_entity_decode( $title, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8' );
326 }
327 }
328 $feed_item = new Feed_Item(
329 array(
330 'permalink' => $item->get_permalink(),
331 'title' => $title,
332 'content' => $this->convert_relative_urls_to_absolute_urls( $item->get_content(), $item->get_permalink() ),
333 )
334 );
335
336 foreach ( array(
337 'gravatar' => 'gravatar',
338 'comment_count' => 'comments',
339 'status' => 'post-status',
340 'post_format' => 'post-format',
341 'id' => 'post-id',
342 ) as $key => $lookup_key ) {
343 foreach ( array( Feed::XMLNS, 'com-wordpress:feed-additions:1' ) as $xmlns ) {
344 if ( ! isset( $item->data['child'][ $xmlns ][ $lookup_key ][0]['data'] ) ) {
345 continue;
346 }
347
348 $feed_item->{$key} = $item->data['child'][ $xmlns ][ $lookup_key ][0]['data'];
349 break;
350 }
351 }
352
353 foreach ( array(
354 'http://purl.org/rss/1.0/modules/slash/' => array(
355 'comment_count' => 'comments',
356 ),
357 'http://wellformedweb.org/CommentAPI/' => array(
358 'comments_feed' => 'commentRss',
359 ),
360 ) as $xmlns => $keys ) {
361 foreach ( $keys as $key => $lookup_key ) {
362 if ( ! isset( $item->data['child'][ $xmlns ][ $lookup_key ][0]['data'] ) ) {
363 continue;
364 }
365
366 $feed_item->{$key} = $item->data['child'][ $xmlns ][ $lookup_key ][0]['data'];
367 break;
368 }
369 }
370
371 $enclosures = $item->get_enclosures();
372 if ( is_array( $enclosures ) ) {
373 foreach ( $enclosures as $enclosure ) {
374 if ( ! isset( $enclosure->link ) ) {
375 continue;
376 }
377
378 $feed_item->enclosure = array_filter(
379 array(
380 'url' => $enclosure->get_link(),
381 'type' => $enclosure->get_type(),
382 'length' => $enclosure->get_length(),
383 )
384 );
385 }
386 }
387
388 if ( is_object( $item->get_author() ) ) {
389 $feed_item->author = $item->get_author()->name;
390 if ( ! $feed_item->author ) {
391 $feed_item->author = '';
392 }
393 // Strip HTML tags, even if they are escaped.
394 $feed_item->author = htmlspecialchars_decode( $feed_item->author, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
395 $feed_item->author = \wp_strip_all_tags( $feed_item->author );
396 }
397
398 $feed_item->date = $item->get_gmdate( 'U' );
399 $feed_item->updated_date = $item->get_updated_gmdate( 'U' );
400
401 $tags = $this->extract_hashtags( $feed_item );
402 if ( ! empty( $tags ) ) {
403 $feed_item->friend_tags = $tags;
404 }
405
406 $feed_items[] = $feed_item;
407 }
408
409 return $feed_items;
410 }
411
412 /**
413 * Extract hashtags from a feed item's content and title
414 *
415 * @param Feed_Item $feed_item The feed item.
416 * @return array Array of hashtag strings
417 */
418 public function extract_hashtags( $feed_item ) {
419 $tags = array();
420
421 $text = '';
422 if ( ! empty( $feed_item->title ) ) {
423 $text .= $feed_item->title . ' ';
424 }
425 if ( ! empty( $feed_item->content ) ) {
426 $text .= wp_strip_all_tags( $feed_item->content );
427 }
428
429 if ( preg_match_all( '/#([A-Za-z0-9_-]+)/', $text, $matches ) ) {
430 foreach ( $matches[1] as $hashtag ) {
431 $tag_name = sanitize_title( strtolower( $hashtag ) );
432 if ( ! empty( $tag_name ) && strlen( $tag_name ) > 1 ) {
433 $tags[] = $tag_name;
434 }
435 }
436 }
437
438 return array_unique( $tags );
439 }
440
441 public function no_comments_feed_available( $text, $post_id ) {
442 if ( User_Feed::get_parser_for_post_id( $post_id ) !== self::SLUG ) {
443 return $text;
444 }
445
446 $comments_url = get_post_meta( $post_id, Feed::COMMENTS_FEED_META, true );
447 if ( ! $comments_url ) {
448 return __( 'We tried to load comments remotely but there was no comments feed available.', 'friends' );
449 }
450 return $text;
451 }
452
453 /**
454 * Get comments from a feed.
455 *
456 * @param array $comments The comments.
457 * @param int $post_id The post id.
458 * @param User $friend_user The friend user.
459 * @param User_Feed $user_feed The user feed.
460 *
461 * @return array The comments.
462 */
463 public function get_comments( $comments, $post_id, ?User $friend_user = null, ?User_Feed $user_feed = null ) {
464 $comments_url = get_post_meta( $post_id, Feed::COMMENTS_FEED_META, true );
465 if ( ! $comments_url || ! $friend_user || ! $user_feed ) {
466 return $comments;
467 }
468
469 $items = $this->fetch_feed( $comments_url, $user_feed );
470
471 if ( is_wp_error( $items ) ) {
472 return $comments;
473 }
474
475 foreach ( $items as $key => $item ) {
476 if ( is_wp_error( $item ) ) {
477 unset( $items[ $key ] );
478 continue;
479 }
480 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, null );
481
482 if ( ! $item || $item->_feed_rule_delete ) {
483 unset( $items[ $key ] );
484 continue;
485 }
486 if ( ! empty( $item->_feed_rule_transform ) && is_array( $item->_feed_rule_transform ) ) {
487 if ( isset( $item->_feed_rule_transform['post_content'] ) ) {
488 $items[ $key ]->content = $item->_feed_rule_transform['post_content'];
489 }
490 }
491 }
492
493 return array_merge( $comments, $items );
494 }
495
496 public function podcast_audio_support( $item ) {
497 if ( ! isset( $item->enclosure['url'] ) ) {
498 return $item;
499 }
500 if ( false !== stripos( $item->post_content, '<audio' ) || false !== stripos( $item->post_content, $item->enclosure['url'] ) ) {
501 return $item;
502 }
503
504 if ( ! preg_match( '#\.(mp3|m4a|ogg|wav)$#i', $item->enclosure['url'] ) ) {
505 if ( ! isset( $item->enclosure['type'] ) || ! preg_match( '#audio/(mp3|m4a|ogg|wav)$#i', $item->enclosure['type'] ) ) {
506 return $item;
507 }
508 }
509
510 $audio_block = '<!-- wp:audio -->';
511 $audio_block .= PHP_EOL;
512 $audio_block .= '<figure class="wp-block-audio"><audio controls src="';
513 $audio_block .= esc_url( $item->enclosure['url'] );
514 if ( isset( $item->enclosure['type'] ) ) {
515 $audio_block .= '" type="';
516 $audio_block .= esc_attr( $item->enclosure['type'] );
517 }
518 if ( isset( $item->enclosure['length'] ) ) {
519 $audio_block .= '" length="';
520 $audio_block .= esc_attr( $item->enclosure['length'] );
521 }
522 $audio_block .= '"></audio></figure>';
523 $audio_block .= PHP_EOL;
524 $audio_block .= '<!-- /wp:audio -->';
525 $audio_block .= PHP_EOL;
526 $audio_block .= PHP_EOL;
527
528 $item->post_content = $audio_block . $item->post_content;
529 return $item;
530 }
531 public function youtube_video_support( $item ) {
532 if ( ! isset( $item->enclosure['url'] ) ) {
533 return $item;
534 }
535 if ( false !== stripos( $item->post_content, '<iframe' ) || false !== stripos( $item->post_content, $item->enclosure['url'] ) ) {
536 return $item;
537 }
538 $p = wp_parse_url( $item->enclosure['url'] );
539 if ( ! isset( $p['host'] ) || ! isset( $p['path'] ) ) {
540 return $item;
541 }
542 if ( ! in_array( $p['host'], array( 'www.youtube.com', 'youtube.com', 'youtu.be' ) ) ) {
543 return $item;
544 }
545
546 if ( ! preg_match( '#/embed/([^?&$]+)#i', $item->enclosure['url'], $m ) ) {
547 if ( ! preg_match( '#/v/([^?&$]+)#i', $item->enclosure['url'], $m ) ) {
548 return $item;
549 }
550 }
551
552 if ( ! preg_match( '#^[a-zA-Z0-9_-]+$#', $m[1] ) ) {
553 return $item;
554 }
555
556 $video_url = 'https://youtube.com/v/' . esc_html( $m[1] );
557
558 $youtube_block = '<!-- wp:embed {"url":"';
559 $youtube_block .= esc_url( $video_url );
560 $youtube_block .= '","type":"wp:youtube"} -->';
561 $youtube_block .= PHP_EOL;
562 $youtube_block .= '<figure class="wp-block-embed is-type-rich is-provider-embed-handler wp-block-embed-embed-handler wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">';
563 $youtube_block .= PHP_EOL;
564 $youtube_block .= esc_url( $video_url );
565 $youtube_block .= PHP_EOL;
566 $youtube_block .= '</div></figure>';
567 $youtube_block .= PHP_EOL;
568 $youtube_block .= '<!-- /wp:embed -->';
569 $youtube_block .= PHP_EOL;
570 $youtube_block .= PHP_EOL;
571
572 $item->post_content = $youtube_block . $item->post_content;
573 return $item;
574 }
575 }
576