PluginProbe
Friends / 4.3.2
Friends v4.3.2
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 / includes / class-feed.php

class-feed.php in Friends 4.3.2, at includes/class-feed.php

1,440 lines 42.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Friends Feed
4 *
5 * This contains the feed functions.
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 0.6
16 *
17 * @package Friends
18 * @author Alex Kirk
19 */
20 class Feed {
21 const XMLNS = 'wordpress-plugin-friends:feed-additions:1';
22 const COMMENTS_FEED_META = 'comments-feed';
23
24 /**
25 * Contains a reference to the Friends class.
26 *
27 * @var Friends
28 */
29 private $friends;
30
31 /**
32 * Contains the registered parsers.
33 *
34 * @var array
35 */
36 private $parsers = array();
37
38 /**
39 * These parsers cannot be registered.
40 *
41 * @var array
42 */
43 private $reserved_parser_slugs = array( 'friends', 'unsupported' );
44
45 /**
46 * Constructor
47 *
48 * @param Friends $friends A reference to the Friends object.
49 */
50 public function __construct( Friends $friends ) {
51 $this->friends = $friends;
52 $this->register_hooks();
53 }
54
55 /**
56 * Register the WordPress hooks
57 */
58 private function register_hooks() {
59 add_filter( 'friends_early_modify_feed_item', array( $this, 'apply_early_feed_rules' ), 10, 3 );
60 add_filter( 'friends_modify_feed_item', array( $this, 'apply_feed_rules' ), 10, 3 );
61
62 add_action( 'rss_item', array( $this, 'feed_additional_fields' ) );
63 add_action( 'rss2_item', array( $this, 'feed_additional_fields' ) );
64 add_action( 'rss_ns', array( $this, 'additional_feed_namespaces' ) );
65 add_action( 'rss2_ns', array( $this, 'additional_feed_namespaces' ) );
66
67 add_action( 'cron_friends_refresh_feeds', array( $this, 'cron_friends_refresh_feeds' ) );
68 add_action( 'friends_retrieve_user_feeds', array( $this, 'friends_retrieve_user_feeds' ) );
69
70 add_action( 'wp_loaded', array( $this, 'friends_add_friend_redirect' ), 100 );
71 add_action( 'wp_feed_options', array( $this, 'wp_feed_options' ), 90 );
72
73 add_action( 'wp_insert_post', array( $this, 'invalidate_post_count_cache' ), 10, 2 );
74 add_action( 'oembed_request_post_id', array( $this, 'oembed_request_post_id' ), 10, 2 );
75 add_action( 'post_embed_url', array( $this, 'post_embed_url' ), 10, 2 );
76 }
77
78 /**
79 * Allow registering a parser
80 *
81 * @param string $slug The slug.
82 * @param Feed_Parser $parser The parser that extends the Feed_Parser class.
83 */
84 public function register_parser( $slug, Feed_Parser $parser ) {
85 if ( in_array( $slug, $this->reserved_parser_slugs, true ) ) {
86 // translators: %s is the slug of a parser.
87 return new \WP_Error( 'resevered-slug', sprintf( __( 'The slug "%s" cannot be used.', 'friends' ), $slug ) );
88 }
89 if ( isset( $this->parsers[ $slug ] ) ) {
90 // translators: %s is the slug of a parser.
91 return new \WP_Error( 'parser-already-registered', sprintf( __( 'There is already a parser registered with the slug "%s".', 'friends' ), $slug ) );
92 }
93 $this->parsers[ $slug ] = $parser;
94
95 return true;
96 }
97
98 /**
99 * Allow unregistering a parser
100 *
101 * @param string $slug The slug.
102 */
103 public function unregister_parser( $slug ) {
104 if ( ! in_array( $slug, $this->reserved_parser_slugs, true ) ) {
105 unset( $this->parsers[ $slug ] );
106 }
107 }
108
109 /**
110 * Cron function to refresh the feeds of the friends' blogs
111 */
112 public function cron_friends_refresh_feeds() {
113 foreach ( User_Feed::get_all_due() as $feed ) {
114 if ( ! $feed->can_be_polled_now() ) {
115 continue;
116 }
117 $feed->set_polling_now();
118 $this->retrieve_feed( $feed );
119 $feed->was_polled();
120 }
121 }
122
123 /**
124 * Function to fetch a users feeds.
125 *
126 * @param int $user_id The user id.
127 */
128 public function friends_retrieve_user_feeds( $user_id ) {
129 $friend_user = User::get_user_by_id( $user_id );
130 if ( ! $friend_user ) {
131 return;
132 }
133
134 foreach ( $friend_user->get_active_feeds() as $feed ) {
135 $friend_user = $feed->get_friend_user();
136 if ( $friend_user && $feed->can_be_polled_now() ) {
137 $feed->set_polling_now();
138 $this->retrieve_feed( $feed );
139 $feed->was_polled();
140 }
141 }
142 }
143
144 /**
145 * Preview a URL using a parser.
146 *
147 * @param string|Feed_Parser $parser The parser or its slug slug.
148 * @param string $url The url.
149 * @param int $feed_id The feed id.
150 *
151 * @return array|\WP_Error The feed items.
152 */
153 public function preview( $parser, $url, $feed_id = null ) {
154 if ( is_string( $parser ) ) {
155 if ( ! isset( $this->parsers[ $parser ] ) ) {
156 return new \WP_Error(
157 'unknown-parser',
158 sprintf(
159 // translators: %s is a parser name.
160 __( 'An unknown parser name was supplied: %s', 'friends' ),
161 $parser
162 )
163 );
164 }
165 $parser = $this->parsers[ $parser ];
166 } elseif ( ! $parser instanceof Feed_Parser_V2 ) {
167 return new \WP_Error( 'invalid-parser', __( 'An invalid parser was supplied.', 'friends' ) );
168 }
169
170 $user_feed = null;
171 $friend_user = null;
172 if ( ! is_null( $feed_id ) ) {
173 $user_feed = User_Feed::get_by_id( $feed_id );
174 if ( ! is_wp_error( $user_feed ) ) {
175 $friend_user = $user_feed->get_friend_user();
176 }
177 }
178
179 $items = $parser->fetch_feed( $url, $user_feed );
180
181 if ( ! is_wp_error( $items ) ) {
182 if ( empty( $items ) ) {
183 $items = new \WP_Error( 'empty-feed', __( "This feed doesn't contain any entries. There might be a problem parsing the feed.", 'friends' ) );
184 } else {
185 foreach ( $items as $key => $item ) {
186 if ( is_wp_error( $item ) ) {
187 unset( $items[ $key ] );
188 continue;
189 }
190 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, null );
191
192 if ( ! $item || $item->_feed_rule_delete ) {
193 unset( $items[ $key ] );
194 continue;
195 }
196 if ( ! empty( $item->_feed_rule_transform ) && is_array( $item->_feed_rule_transform ) ) {
197 if ( isset( $item->_feed_rule_transform['post_content'] ) ) {
198 $items[ $key ]->content = $item->_feed_rule_transform['post_content'];
199 }
200 }
201 }
202 }
203 }
204
205 return $items;
206 }
207
208 /**
209 * Retrieves a user feed.
210 *
211 * @param User_Feed $user_feed The user feed.
212 *
213 * @return array|\WP_Error The retrieved items.
214 */
215 public function retrieve_feed( User_Feed $user_feed ) {
216 $friend_user = $user_feed->get_friend_user();
217 $parser = $user_feed->get_parser();
218 if ( ! isset( $this->parsers[ $parser ] ) ) {
219 $error = new \WP_Error( 'unknown-parser', __( 'An unknown parser name was supplied.', 'friends' ) );
220 do_action( 'friends_retrieve_friends_error', $user_feed, $error, $friend_user );
221 return $error;
222 }
223 try {
224 $items = $this->parsers[ $parser ]->fetch_feed( $user_feed->get_url(), $user_feed );
225 } catch ( \Exception $e ) {
226 $items = new \WP_Error( $parser . '-failed', $e->getMessage() );
227 }
228
229 if ( is_wp_error( $items ) ) {
230 do_action( 'friends_retrieve_friends_error', $user_feed, $items, $friend_user );
231 return $items;
232 }
233
234 $new_posts = $this->process_incoming_feed_items( $items, $user_feed );
235
236 return $new_posts;
237 }
238
239 /**
240 * Notify users about new posts of this friend
241 *
242 * @param User $friend_user The friend.
243 * @param array $new_posts The new posts of this friend.
244 * @param User_Feed $user_feed The user feed.
245 */
246 public function notify_about_new_posts( User $friend_user, $new_posts, User_Feed $user_feed ) {
247 $keywords = self::get_active_notification_keywords();
248 foreach ( $new_posts as $post_id ) {
249 $post = false;
250
251 $keyword_match = false;
252 if ( $keywords ) {
253 $post = get_post( intval( $post_id ) );
254 $fulltext = '';
255 foreach ( apply_filters( 'friends_keyword_search_fields', array( 'post_title', 'post_content' ) ) as $field ) {
256 $fulltext .= PHP_EOL . $post->$field;
257 }
258 $fulltext = self::prepare_keyword_search_text( $fulltext );
259 foreach ( $keywords as $keyword ) {
260 if ( preg_match( '/' . str_replace( '/', '\\/', $keyword ) . '/ius', $fulltext ) ) {
261 $keyword_match = $keyword;
262 break;
263 }
264 }
265
266 if ( $keyword_match ) {
267 $notified = apply_filters( 'friends_notify_keyword_match_post', false, $post, $keyword_match );
268 if ( $notified ) {
269 continue;
270 }
271 }
272 }
273
274 $notify_users = apply_filters( 'notify_about_new_friend_post', true, $friend_user, $post_id, $user_feed, $keyword_match );
275 if ( $notify_users ) {
276 if ( ! $post ) {
277 $post = get_post( intval( $post_id ) );
278 }
279 do_action( 'notify_new_friend_post', $post, $user_feed, $keyword_match );
280 }
281 }
282 }
283
284 /**
285 * Retrieve posts from all friends.
286 *
287 * @param bool $ignore_due_date Whether to get also undue feeds.
288 */
289 public function retrieve_friend_posts( $ignore_due_date = false ) {
290 foreach ( User_Feed::get_all_due( $ignore_due_date ) as $feed ) {
291 $feed->set_polling_now();
292 $this->retrieve_feed( $feed );
293 $feed->was_polled();
294 }
295 }
296
297 /**
298 * Apply the feed rules that need to be applied early.
299 *
300 * @param Feed_Item $item The feed item.
301 * @param User_Feed $feed The feed.
302 * @param User $friend_user The friend user.
303 * @return Feed_Item The modified feed item.
304 */
305 public function apply_early_feed_rules( $item, ?User_Feed $feed = null, ?User $friend_user = null ) {
306 $updated_item = $this->apply_feed_rules( $item, $feed, $friend_user );
307 if ( $updated_item->_feed_rule_delete ) {
308 return $updated_item;
309 }
310 return $item;
311 }
312
313 /**
314 * Apply the feed rules
315 *
316 * @param Feed_Item $item The feed item.
317 * @param User_Feed $feed The feed object.
318 * @param User $friend_user The friend user.
319 * @return Feed_Item The modified feed item.
320 */
321 public function apply_feed_rules( $item, ?User_Feed $feed = null, ?User $friend_user = null ) {
322 if ( is_null( $friend_user ) ) {
323 return $item;
324 }
325
326 $rules = $friend_user->get_feed_rules();
327 $action = $friend_user->get_feed_catch_all();
328 $decided = false;
329
330 foreach ( $rules as $rule ) {
331 if ( $item instanceof \WP_Post ) {
332 $field = $this->get_feed_rule_field( $rule['field'] );
333
334 if ( 'author' === $rule['field'] ) {
335 $item->$field = get_post_meta( $item->ID, 'author', true );
336 }
337 } else {
338 $field = $rule['field'];
339 }
340 $subject = false;
341 if ( $item->$field ) {
342 $subject = $item->$field;
343 }
344 $field_rule_field = $this->get_feed_rule_field( $rule['field'], $item );
345 if ( isset( $item->_feed_rule_transform[ $field_rule_field ] ) ) {
346 $subject = $item->_feed_rule_transform[ $field_rule_field ];
347 }
348 if ( $subject && preg_match( '/' . str_replace( '/', '\\/', $rule['regex'] ) . '/ius', $subject ) ) {
349 if ( 'replace' === $rule['action'] ) {
350 $item->_feed_rule_transform = array(
351 $field_rule_field => preg_replace( '/' . str_replace( '/', '\\/', $rule['regex'] ) . '/ius', $rule['replace'], $subject ),
352 );
353 continue;
354 }
355 if ( $decided ) {
356 continue;
357 }
358 $action = $rule['action'];
359 $decided = true;
360 }
361 }
362
363 switch ( $action ) {
364 case 'delete':
365 $item->_feed_rule_delete = true;
366 return $item;
367
368 case 'trash':
369 if ( empty( $item->_feed_rule_transform ) ) {
370 $item->_feed_rule_transform = array();
371 }
372 $item->_feed_rule_transform = array_merge(
373 $item->_feed_rule_transform,
374 array(
375 'post_status' => 'trash',
376 )
377 );
378 return $item;
379
380 case 'accept':
381 return $item;
382 }
383 return $item;
384 }
385
386 /**
387 * Get the field name for the feed item.
388 *
389 * @param string $field The field name.
390 * @return string The adapted field name.
391 */
392 private function get_feed_rule_field( $field ) {
393 switch ( $field ) {
394 case 'title':
395 return 'post_title';
396 case 'permalink':
397 return 'guid';
398 case 'content':
399 return 'post_content';
400 }
401
402 return $field;
403 }
404
405 /**
406 * Validate feed item rules
407 *
408 * @param array $rules The rules to validate.
409 * @return array The valid rules.
410 */
411 public static function validate_feed_rules( $rules ) {
412 if ( ! is_array( $rules ) ) {
413 $json_rules = json_decode( $rules, true );
414 if ( is_array( $json_rules ) ) {
415 $rules = $json_rules;
416 } else {
417 return array();
418 }
419 }
420
421 if ( isset( $rules['field'] ) && is_array( $rules['field'] ) ) {
422 // Transform POST values.
423 $transformed_rules = array();
424 foreach ( array_keys( $rules['field'] ) as $key ) {
425 $rule = array();
426 foreach ( $rules as $part => $keys ) {
427 if ( isset( $keys[ $key ] ) ) {
428 $rule[ $part ] = stripslashes( $keys[ $key ] );
429 }
430 }
431 $transformed_rules[] = $rule;
432 }
433 $rules = $transformed_rules;
434 }
435
436 foreach ( $rules as $k => $rule ) {
437 if ( ! isset( $rule['field'] ) || ! in_array( $rule['field'], array( 'title', 'content', 'permalink', 'author' ), true ) ) {
438 unset( $rules[ $k ] );
439 continue;
440 }
441
442 if ( ! isset( $rule['regex'] ) || ! is_string( $rule['regex'] ) || '' === trim( $rule['regex'] ) ) {
443 unset( $rules[ $k ] );
444 continue;
445 }
446
447 $rules[ $k ]['regex'] = substr( $rule['regex'], 0, 10240 );
448
449 if ( ! isset( $rule['action'] ) || ! in_array( $rule['action'], array( 'accept', 'trash', 'delete', 'replace' ), true ) ) {
450 unset( $rules[ $k ] );
451 continue;
452 }
453
454 if ( 'replace' === $rule['action'] ) {
455 if ( ! isset( $rule['replace'] ) || ! is_string( $rule['replace'] ) ) {
456 unset( $rules[ $k ] );
457 continue;
458 }
459
460 $rules[ $k ]['replace'] = substr( $rule['replace'], 0, 10240 );
461 } else {
462 unset( $rules[ $k ]['replace'] );
463 }
464 }
465
466 return $rules;
467 }
468
469 /**
470 * Validate feed catch_all
471 *
472 * @param array $catch_all The catch_all value to.
473 * @return array A valid catch_all
474 */
475 public static function validate_feed_catch_all( $catch_all ) {
476 if ( ! in_array( $catch_all, array( 'accept', 'trash', 'delete' ), true ) ) {
477 return 'accept';
478 }
479
480 return $catch_all;
481 }
482
483 /**
484 * Prepares post fields for keyword notification matching.
485 *
486 * @param string $text The text to search.
487 * @return string The prepared text.
488 */
489 public static function prepare_keyword_search_text( $text ) {
490 $text = wp_strip_all_tags( $text );
491
492 return preg_replace_callback(
493 '#https?://[^\s<>"\']+#i',
494 function ( $matches ) {
495 return self::remove_tracking_url_query_args_from_keyword_search_text( $matches[0] );
496 },
497 $text
498 );
499 }
500
501 /**
502 * Removes tracking query args from URLs before keyword notification matching.
503 *
504 * @param string $url The URL to normalize.
505 * @return string The URL without known tracking query args.
506 */
507 private static function remove_tracking_url_query_args_from_keyword_search_text( $url ) {
508 $parsed_url = wp_parse_url( $url );
509 if ( ! is_array( $parsed_url ) || empty( $parsed_url['host'] ) ) {
510 return $url;
511 }
512
513 $query_args = array();
514 if ( isset( $parsed_url['query'] ) ) {
515 wp_parse_str( html_entity_decode( $parsed_url['query'], ENT_QUOTES ), $query_args );
516 }
517
518 foreach ( array_keys( $query_args ) as $query_arg ) {
519 if ( self::is_tracking_query_arg( $query_arg ) ) {
520 unset( $query_args[ $query_arg ] );
521 }
522 }
523
524 $normalized_url = $parsed_url['scheme'] . '://';
525 if ( isset( $parsed_url['user'] ) ) {
526 $normalized_url .= $parsed_url['user'];
527 if ( isset( $parsed_url['pass'] ) ) {
528 $normalized_url .= ':' . $parsed_url['pass'];
529 }
530 $normalized_url .= '@';
531 }
532 $normalized_url .= $parsed_url['host'];
533 if ( isset( $parsed_url['port'] ) ) {
534 $normalized_url .= ':' . $parsed_url['port'];
535 }
536 if ( isset( $parsed_url['path'] ) ) {
537 $normalized_url .= $parsed_url['path'];
538 }
539 if ( $query_args ) {
540 $normalized_url .= '?' . http_build_query( $query_args, '', '&' );
541 }
542 if ( isset( $parsed_url['fragment'] ) ) {
543 $normalized_url .= '#' . $parsed_url['fragment'];
544 }
545
546 return $normalized_url;
547 }
548
549 /**
550 * Checks whether a query arg is used for tracking.
551 *
552 * @param string $query_arg The query arg.
553 * @return bool Whether the query arg is used for tracking.
554 */
555 private static function is_tracking_query_arg( $query_arg ) {
556 if ( preg_match( '/^(?:utm|mtm|hsa)_/i', $query_arg ) ) {
557 return true;
558 }
559
560 return in_array(
561 strtolower( $query_arg ),
562 array(
563 '_hsenc',
564 '_hsmi',
565 'dclid',
566 'fbclid',
567 'gclid',
568 'gbraid',
569 'igshid',
570 'li_fat_id',
571 'mc_cid',
572 'mc_eid',
573 'mkt_tok',
574 'msclkid',
575 'oly_anon_id',
576 'oly_enc_id',
577 'pk_campaign',
578 'pk_kwd',
579 'twclid',
580 'vero_id',
581 'wbraid',
582 'yclid',
583 ),
584 true
585 );
586 }
587
588 /**
589 * Gets the notification keywords.
590 *
591 * @return array The notification keywords.
592 */
593 public static function get_active_notification_keywords() {
594 $active_keywords = array();
595 foreach ( self::get_all_notification_keywords() as $entry ) {
596 if ( $entry['enabled'] ) {
597 $active_keywords[] = $entry['keyword'];
598 }
599 }
600
601 return $active_keywords;
602 }
603
604 /**
605 * Gets all notification keywords.
606 *
607 * @return array All notification keywords.
608 */
609 public static function get_all_notification_keywords() {
610 $keywords = get_option( 'friends_notification_keywords', false );
611 if ( false === $keywords ) {
612 // Default keywords.
613 $keywords = array();
614 $keywords[] = array(
615 'enabled' => false,
616 'keyword' => trim( preg_replace( '#^https?:#', '', home_url() ), '/' ),
617 );
618 }
619 return $keywords;
620 }
621
622 /**
623 * Discover the post format for the item.
624 *
625 * @param object $item The feed item.
626 *
627 * @return string The discovered post format.
628 */
629 public function post_format_discovery( $item ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
630 // Not implemented yet.
631 return 'standard';
632 }
633
634 /**
635 * Return the number of revisions to keep.
636 *
637 * @return int The number of revisions to keep.
638 */
639 public function revisions_to_keep() {
640 return 10;
641 }
642
643 public function dissallowed_html( $allowedposttags, $context ) {
644 if ( 'post' === $context ) {
645 unset( $allowedposttags['article'] );
646 }
647 return $allowedposttags;
648 }
649
650 /**
651 * Process incoming feed items
652 *
653 * @param array $items The incoming items.
654 * @param User_Feed $user_feed The feed to which these items belong.
655 * @return array The post ids of the new posts.
656 */
657 public function process_incoming_feed_items( array $items, User_Feed $user_feed ) {
658 $friend_user = $user_feed->get_friend_user();
659 if ( ! $friend_user ) {
660 if ( apply_filters( 'friends_debug', false ) ) {
661 error_log( var_export( $user_feed, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
662 }
663 return;
664 }
665 $remote_post_ids = $friend_user->get_remote_post_ids();
666 $post_formats = get_post_format_strings();
667 $feed_post_format = $user_feed->get_post_format();
668
669 // Sort items by date asc so that older posts will have lower ids than newer posts.
670 usort(
671 $items,
672 function ( $a, $b ) {
673 if ( ! isset( $a->date ) || ! isset( $b->date ) ) {
674 return 0;
675 }
676 return strtotime( $a->date ) - strtotime( $b->date );
677 }
678 );
679
680 // Limit this as a safety measure.
681 add_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
682 $new_post_ids = array();
683 $modified_posts = array();
684 $manual_refresh = isset( $_GET['page'] ) && 'friends-refresh' === $_GET['page'] && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'friends-refresh' );
685 foreach ( $items as $item_key => $item ) {
686 if ( ! isset( $item->permalink ) || ! $item->permalink ) {
687 continue;
688 }
689 $item->permalink = str_replace( array( '&#38;', '&#038;' ), '&', ent2ncr( wp_kses_normalize_entities( $item->permalink ) ) );
690
691 if ( empty( $item->post_content ) ) {
692 $item->post_content = '';
693 }
694
695 add_filter( 'wp_kses_allowed_html', array( $this, 'dissallowed_html' ), 10, 2 );
696 $item->post_content = wp_kses_post( trim( $item->post_content ) );
697 remove_filter( 'wp_kses_allowed_html', array( $this, 'dissallowed_html' ), 10 );
698
699 $item = apply_filters( 'friends_early_modify_feed_item', $item, $user_feed, $friend_user );
700 if ( ! $item || $item->_feed_rule_delete ) {
701 continue;
702 }
703
704 // Fallback, when no friends plugin is installed.
705 $item->post_id = $item->permalink;
706 $item->post_status = 'publish';
707 if ( ( ! $item->post_content && ! $item->title ) || ! $item->permalink ) {
708 continue;
709 }
710
711 $post_id = null;
712 if ( $item->post_id && isset( $remote_post_ids[ $item->post_id ] ) ) {
713 $post_id = $remote_post_ids[ $item->post_id ];
714 }
715 if ( is_null( $post_id ) && $item->permalink && isset( $remote_post_ids[ $item->permalink ] ) ) {
716 $post_id = $remote_post_ids[ $item->permalink ];
717 }
718
719 if ( is_null( $post_id ) ) {
720 $post_id = self::url_to_postid( $item->permalink );
721 }
722 $item->_is_new = is_null( $post_id );
723 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, $post_id );
724
725 $updated_date = $item->date;
726 if ( ! empty( $item->updated_date ) ) {
727 $updated_date = $item->updated_date;
728 }
729
730 if ( empty( $item->title ) ) {
731 $item->title = '';
732 }
733
734 $post_data = array(
735 'post_title' => $item->title,
736 'post_content' => force_balance_tags( $item->post_content ),
737 'post_modified_gmt' => $updated_date,
738 'post_status' => $item->post_status,
739 'guid' => $item->permalink,
740 'comment_count' => $item->comment_count,
741 );
742
743 // Modified via feed rules.
744 if ( ! empty( $item->_feed_rule_transform ) && is_array( $item->_feed_rule_transform ) ) {
745 $post_data = array_merge( $post_data, $item->_feed_rule_transform );
746 }
747
748 $old_post = null;
749 if ( ! is_null( $post_id ) ) {
750 $old_post = get_post( $post_id );
751 $modified_post_data = array();
752 foreach ( array( 'post_title', 'post_content', 'post_status' ) as $field ) {
753 if ( empty( $old_post->$field ) || empty( $post_data[ $field ] ) ) {
754 continue;
755 }
756 if ( 'post_content' === $field && $old_post->$field !== $post_data[ $field ] ) {
757 $modified_post_data[ $field ] = $post_data[ $field ];
758 break;
759 }
760
761 if ( wp_strip_all_tags( $old_post->$field ) !== wp_strip_all_tags( $post_data[ $field ] ) ) {
762 $modified_post_data[ $field ] = $post_data[ $field ];
763 break;
764 }
765 }
766
767 if ( ! empty( $modified_post_data ) && apply_filters( 'friends_can_update_modified_feed_posts', true, $item, $user_feed, $friend_user, $post_id ) ) {
768 $was_modified_by_user = false;
769 foreach ( wp_get_post_revisions( $post_id ) as $revision ) {
770 if ( intval( $revision->post_author ) && intval( $revision->post_author ) !== $friend_user->ID ) {
771 $was_modified_by_user = true;
772 break;
773 }
774 }
775
776 if ( ! $was_modified_by_user || $manual_refresh ) {
777 $modified_post_data['ID'] = $post_id;
778 if ( isset( $modified_post_data['post_content'] ) ) {
779 $modified_post_data['post_content'] = str_replace( '\\', '\\\\', $modified_post_data['post_content'] );
780 }
781 if ( intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
782 $modified_post_data['comment_count'] = $item->comment_count;
783 }
784
785 wp_update_post( $modified_post_data );
786 $modified_posts[ $item_key ] = $post_id;
787 }
788 }
789 } else {
790 $post_data['post_type'] = Friends::CPT;
791 $post_data['post_date_gmt'] = $item->date;
792 $post_data['post_content'] = str_replace( '\\', '\\\\', $post_data['post_content'] );
793 $post_data['meta_input'] = $item->meta;
794
795 $post_id = $friend_user->insert_post( $post_data, true );
796 if ( is_wp_error( $post_id ) ) {
797 continue;
798 }
799
800 $new_post_ids[ $item_key ] = $post_id;
801
802 $remote_post_ids[ $item->permalink ] = $post_id;
803 }
804
805 if ( is_null( $old_post ) || intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
806 // The comment_count needs to be updated manually since it doesn't represent real comments in the database.
807 global $wpdb;
808 $wpdb->update( $wpdb->posts, array( 'comment_count' => $item->comment_count ), array( 'ID' => $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
809 wp_cache_delete( "comments-{$post_id}", 'counts' );
810 clean_post_cache( $post_id );
811 do_action( 'wp_update_comment_count', $post_id, $item->comment_count, $old_post ? $old_post->comment_count : 0 );
812 }
813
814 if ( $item->comments_feed && ( is_null( $old_post ) || $old_post->comments_feed !== $item->comments_feed ) ) {
815 update_post_meta( $post_id, self::COMMENTS_FEED_META, $item->comments_feed, $old_post ? $old_post->comments_feed : '' );
816 }
817
818 $post_format = $feed_post_format;
819 if ( 'autodetect' === $post_format ) {
820 if ( isset( $item->post_format ) && isset( $post_formats[ $item->post_format ] ) ) {
821 $post_format = $item->post_format;
822 } else {
823 $post_format = $this->post_format_discovery( $item );
824 }
825 }
826
827 if ( $post_format ) {
828 set_post_format( $post_id, $post_format );
829 }
830
831 if ( $item->author ) {
832 update_post_meta( $post_id, 'author', $item->author );
833 }
834
835 if ( is_numeric( $item->post_id ) ) {
836 update_post_meta( $post_id, 'remote_post_id', $item->{'post-id'} );
837 }
838
839 if ( ! get_option( 'friends_disable_auto_tagging' ) && isset( $item->friend_tags ) && ! empty( $item->friend_tags ) && is_array( $item->friend_tags ) ) {
840 Friend_Tag::add_tags( $post_id, $item->friend_tags );
841 }
842
843 if ( isset( $item->friend_mention_tags ) && ! empty( $item->friend_mention_tags ) && is_array( $item->friend_mention_tags ) ) {
844 Friend_Tag::add_tags( $post_id, $item->friend_mention_tags );
845 }
846
847 update_post_meta( $post_id, 'parser', $user_feed->get_parser() );
848 update_post_meta( $post_id, 'feed_url', $user_feed->get_url() );
849
850 global $wpdb;
851 $wpdb->update( $wpdb->posts, array( 'comment_count' => $item->comment_count ), array( 'ID' => $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
852 }
853 remove_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
854
855 $deleted_posts = $friend_user->delete_outdated_posts();
856 $new_post_ids = array_diff( $new_post_ids, $deleted_posts );
857
858 $this->notify_about_new_posts( $friend_user, $new_post_ids, $user_feed );
859
860 do_action( 'friends_retrieved_new_posts', $user_feed, $new_post_ids, $modified_posts, $friend_user );
861
862 $new_posts = array();
863 foreach ( $new_post_ids as $key => $post_id ) {
864 if ( isset( $items[ $key ] ) ) {
865 $new_posts[ $post_id ] = $items[ $key ];
866 }
867 }
868
869 return $new_posts;
870 }
871
872 /**
873 * Output an additional XMLNS for the feed.
874 */
875 public function additional_feed_namespaces() {
876 echo ' xmlns:friends="' . esc_attr( self::XMLNS ) . '" ';
877 }
878
879 /**
880 * Additional fields for the friends feed.
881 */
882 public function feed_additional_fields() {
883 global $post;
884 $post_format = get_post_format( $post );
885 if ( empty( $post_format ) ) {
886 $post_format = 'standard';
887 }
888 echo '<friends:post-format>' . esc_html( $post_format ) . '</friends:post-format>' . PHP_EOL;
889 }
890
891 /**
892 * Redirect
893 */
894 public function friends_add_friend_redirect() {
895 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
896 if ( ! isset( $_GET['add-friend'] ) || isset( $_GET['page'] ) ) {
897 return;
898 }
899 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
900 wp_safe_redirect( add_query_arg( 'url', sanitize_text_field( wp_unslash( $_GET['add-friend'] ) ), self_admin_url( 'admin.php?page=add-friend' ) ) );
901 exit;
902 }
903
904 /**
905 * Configure feed downloading options
906 *
907 * @param SimplePie $feed The SimplePie object.
908 */
909 public function wp_feed_options( $feed ) {
910 $feed->useragent .= ' Friends/' . FRIENDS_VERSION;
911 if (
912 isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'friends-refresh' ) &&
913 isset( $_GET['page'] ) && 'page=friends-refresh' === $_GET['page']
914 ) {
915 $feed->enable_cache( false );
916 } else {
917 $feed->set_cache_duration( 3590 );
918 }
919 }
920
921 /**
922 * Invalidatee Post Count Cache
923 *
924 * @param int $post_ID The post id.
925 * @param \WP_Post $post The post.
926 */
927 public function invalidate_post_count_cache( $post_ID, \WP_Post $post ) {
928 $cache_key = 'friends_post_count_by_post_format';
929 delete_transient( $cache_key );
930 if ( is_numeric( $post->post_author ) && $post->post_author > 0 ) {
931 $author_id = intval( $post->post_author );
932 delete_transient( $cache_key . '_author_' . $author_id );
933 }
934 }
935
936 /**
937 * Discover available feeds.
938 *
939 * @param string $url The feed URL.
940 * @return array The available feeds.
941 */
942 public function discover_available_feeds( $url ) {
943 if ( ! Friends::check_url( $url ) ) {
944 return array();
945 }
946 $available_feeds = array();
947 $content = null;
948 $content_type = 'text/html';
949
950 $response = wp_safe_remote_get(
951 $url,
952 array(
953 'timeout' => apply_filters( 'friends_http_timeout', 20 ),
954 'redirection' => 2,
955 )
956 );
957
958 if ( is_wp_error( $response ) ) {
959 $response->add_data( $url );
960 return $response;
961 }
962
963 $response_code = wp_remote_retrieve_response_code( $response );
964 if ( $response_code >= 300 && $response_code < 400 ) {
965 $location = wp_remote_retrieve_header( $response, 'location' );
966 if ( $location ) {
967 return new \WP_Error(
968 'redirect',
969 sprintf(
970 // translators: %s is a URL.
971 __( 'This URL redirects to %s — please use that URL instead.', 'friends' ),
972 '<a href="' . esc_url( admin_url( 'admin.php?page=add-friend&url=' . rawurlencode( $location ) ) ) . '">' . esc_html( $location ) . '</a>'
973 )
974 );
975 }
976 }
977
978 if ( 200 === $response_code ) {
979 $content = wp_remote_retrieve_body( $response );
980 $headers = wp_remote_retrieve_headers( $response );
981
982 // We'll determine the obvious feeds ourself.
983 $available_feeds = $this->discover_link_rel_feeds( $content, $url, $headers );
984 $content_type = strtok( $headers['content-type'], ';' );
985 }
986
987 foreach ( $this->parsers as $slug => $parser ) {
988 foreach ( $parser->discover_available_feeds( $content, $url ) as $link_url => $feed ) {
989 if ( isset( $available_feeds[ $link_url ] ) ) {
990 // If this parser tells us it can parse it right away, allow it to override.
991 if ( isset( $available_feeds[ $link_url ]['parser'] ) || ! isset( $feed['parser'] ) ) {
992 continue;
993 }
994 } else {
995 $available_feeds[ $link_url ] = array();
996 }
997 $available_feeds[ $link_url ] = array_merge( $available_feeds[ $link_url ], $feed );
998 $available_feeds[ $link_url ]['url'] = $link_url;
999 }
1000 }
1001
1002 $has_friends_plugin = false;
1003 foreach ( $available_feeds as $link_url => $feed ) {
1004 $feed = array_merge(
1005 array(
1006 'url' => $link_url,
1007 'rel' => 'unknown',
1008 'type' => 'unknown',
1009 'title' => '',
1010 'parser' => 'unsupported',
1011 'parser_confidence' => 0,
1012 ),
1013 $feed
1014 );
1015 $available_feeds[ $link_url ] = $feed;
1016
1017 if ( 'friends-base-url' === $feed['rel'] ) {
1018 $available_feeds[ $link_url ]['parser'] = 'friends';
1019 $available_feeds[ $link_url ]['parser_confidence'] = 1000;
1020 $has_friends_plugin = $link_url;
1021 continue;
1022 }
1023
1024 foreach ( $this->parsers as $slug => $parser ) {
1025 $confidence = intval( $parser->feed_support_confidence( $link_url, $feed['type'], $feed['rel'] ) );
1026 $confidence = intval( apply_filters( 'friends_parser_confidence', $confidence, $slug, $link_url, $feed ) );
1027 if ( $available_feeds[ $link_url ]['parser_confidence'] < $confidence ) {
1028 $available_feeds[ $link_url ]['parser_confidence'] = $confidence;
1029 $available_feeds[ $link_url ]['parser'] = $slug;
1030 }
1031 }
1032 }
1033
1034 if ( ! isset( $available_feeds[ $url ] ) ) {
1035 $feed = array(
1036 'url' => $url,
1037 'type' => $content_type,
1038 'rel' => 'self',
1039 'title' => '',
1040 'parser' => 'unsupported',
1041 'parser_confidence' => 0,
1042 );
1043
1044 foreach ( $this->parsers as $slug => $parser ) {
1045 $confidence = $parser->feed_support_confidence( $url, $feed['type'], $feed['rel'], $content );
1046 $confidence = apply_filters( 'friends_parser_confidence', $confidence, $slug, $url, $feed );
1047 if ( $feed['parser_confidence'] < $confidence ) {
1048 $feed['parser_confidence'] = $confidence;
1049 $feed['parser'] = $slug;
1050 }
1051 }
1052
1053 if ( 'unknown' !== $feed['parser'] ) {
1054 $available_feeds[ $feed['url'] ] = $feed;
1055 }
1056 }
1057
1058 foreach ( array_keys( $available_feeds ) as $link_url ) {
1059 if ( ! isset( $this->parsers[ $available_feeds[ $link_url ]['parser'] ] ) ) {
1060 continue;
1061 }
1062 $parser = $this->parsers[ $available_feeds[ $link_url ]['parser'] ];
1063 $updated_feed_details = $parser->update_feed_details( $available_feeds[ $link_url ] );
1064 if ( is_array( $updated_feed_details ) && ! is_wp_error( $updated_feed_details ) ) {
1065 $available_feeds[ $link_url ] = array_merge( $available_feeds[ $link_url ], $updated_feed_details );
1066 }
1067 $available_feeds[ $link_url ] = apply_filters( 'friends_update_feed_details', $available_feeds[ $link_url ], $slug );
1068 if ( $available_feeds[ $link_url ]['url'] !== $link_url ) {
1069 $new_url = $available_feeds[ $link_url ]['url'];
1070 $available_feeds[ $new_url ] = $available_feeds[ $link_url ];
1071 unset( $available_feeds[ $link_url ] );
1072 }
1073 }
1074
1075 $autoselected = false;
1076 // Check if a parser already autoselected a feed.
1077 foreach ( $available_feeds as $link_url => $feed ) {
1078 if ( isset( $feed['autoselect'] ) && $feed['autoselect'] ) {
1079 $autoselected = true;
1080 break;
1081 }
1082 }
1083
1084 foreach ( $available_feeds as $link_url => $feed ) {
1085 $path = wp_parse_url( $link_url, PHP_URL_PATH );
1086
1087 // Backfill titles.
1088 if ( empty( $feed['title'] ) ) {
1089 $host = wp_parse_url( $link_url, PHP_URL_HOST );
1090 if ( is_string( $path ) && trim( $path, '/' ) ) {
1091 $feed['title'] = trim( preg_replace( '#^www\.#i', '', preg_replace( '#[^a-z0-9.:-]#i', ' ', ucwords( $host . ': ' . $path ) ) ), ': ' );
1092 } else {
1093 $feed['title'] = strtolower( $host );
1094 }
1095 $available_feeds[ $link_url ]['title'] = $feed['title'];
1096 }
1097
1098 // Autoselect heuristic.
1099 if ( $autoselected ) {
1100 continue;
1101 }
1102
1103 if ( ! $feed['parser_confidence'] ) {
1104 continue;
1105 }
1106
1107 // Prefer the main RSS feed.
1108 if (
1109 '/feed' === substr( '/' . trim( $path, '/' ), -5 )
1110 || '.xml' === substr( $path, -4 )
1111 || 'rss' === substr( $path, -3 )
1112 ) {
1113 if ( $has_friends_plugin ) {
1114 $available_feeds[ $link_url ]['post-format'] = 'autodetect';
1115 }
1116 $autoselected = true;
1117 } elseif ( isset( $feed['rel'] ) ) {
1118 if ( 'alternate' === $feed['rel'] && 'application/rss+xml' === $feed['type'] && 'feed' === trim( $path, '/' ) ) {
1119 $autoselected = true;
1120 }
1121 }
1122
1123 if ( $autoselected ) {
1124 $available_feeds[ $link_url ]['autoselect'] = true;
1125 }
1126 }
1127
1128 // Prefer the ActivityPub actor feed over RSS when both are available.
1129 $activitypub_feed = null;
1130 foreach ( $available_feeds as $link_url => $feed ) {
1131 if ( 'activitypub' === $feed['parser'] ) {
1132 $activitypub_feed = $link_url;
1133 break;
1134 }
1135 }
1136 if ( $activitypub_feed ) {
1137 foreach ( $available_feeds as $link_url => $feed ) {
1138 $available_feeds[ $link_url ]['autoselect'] = $link_url === $activitypub_feed;
1139 }
1140 }
1141
1142 $feed_sort_order = array( 'self', 'alternate', 'me' );
1143 if ( $has_friends_plugin ) {
1144 // If we have the Friends plugin, we prefer an (augmented) RSS feed over, for example, microformats.
1145 $feed_sort_order = array( 'alternate', 'self', 'me' );
1146 }
1147
1148 uasort(
1149 $available_feeds,
1150 function ( $a, $b ) use ( $feed_sort_order ) {
1151 if ( isset( $a['autoselect'] ) && $a['autoselect'] ) {
1152 if ( ! isset( $b['autoselect'] ) || ! $b['autoselect'] ) {
1153 return -1;
1154 }
1155 } elseif ( isset( $b['autoselect'] ) && $b['autoselect'] ) {
1156 return 1;
1157 }
1158
1159 foreach ( $feed_sort_order as $rel_sort ) {
1160 if ( $rel_sort === $a['rel'] ) {
1161 if ( $rel_sort !== $b['rel'] ) {
1162 return -1;
1163 }
1164 break;
1165 } elseif ( $rel_sort === $b['rel'] ) {
1166 return 1;
1167 }
1168 }
1169
1170 return strcmp( $a['title'], $b['title'] );
1171 }
1172 );
1173
1174 return apply_filters( 'friends_available_feeds', $available_feeds, $url );
1175 }
1176
1177 /**
1178 * Discover feeds specified in the <link> section.
1179 *
1180 * @param string $content The content to search.
1181 * @param string $url The url.
1182 * @param object $headers The headers from the request.
1183 *
1184 * @return array ( description_of_the_return_value )
1185 */
1186 private function discover_link_rel_feeds( $content, $url, $headers ) {
1187 $discovered_feeds = array();
1188 $has_self = false;
1189 $links = array();
1190 $mf = Mf2\parse( $content, $url );
1191
1192 if ( isset( $mf['rel-urls'] ) ) {
1193 $links = array_merge( $links, $mf['rel-urls'] );
1194 }
1195 foreach ( $headers as $header => $value ) {
1196 if ( 'link' === strtolower( $header ) ) {
1197 $values = wp_parse_args( $value );
1198 if ( isset( $values['href'] ) ) {
1199 if ( ! isset( $values['rels'] ) && $values['rel'] ) {
1200 $values['rels'] = array( $values['rel'] );
1201 unset( $values['rel'] );
1202 }
1203 if ( isset( $values['rels'] ) ) {
1204 if ( isset( $links[ $values['href'] ] ) ) {
1205 foreach ( $values['rels'] as $rel ) {
1206 $links[ $values['href'] ]['rels'][] = $rel;
1207 }
1208 } else {
1209 $links[ $values['href'] ] = $values;
1210 }
1211 }
1212 }
1213 }
1214 }
1215
1216 foreach ( $links as $feed_url => $link ) {
1217 foreach ( array( 'friends-base-url', 'me', 'alternate', 'self' ) as $rel ) {
1218 if ( in_array( $rel, $link['rels'] ) ) {
1219 $discovered_feeds[ $feed_url ] = array(
1220 'rel' => $rel,
1221 );
1222 }
1223 }
1224
1225 if ( ! isset( $discovered_feeds[ $feed_url ] ) ) {
1226 continue;
1227 }
1228
1229 if ( 'self' === $discovered_feeds[ $feed_url ]['rel'] ) {
1230 $has_self = true;
1231 }
1232
1233 if ( isset( $link['type'] ) ) {
1234 $discovered_feeds[ $feed_url ]['type'] = $link['type'];
1235 }
1236
1237 if ( isset( $link['title'] ) ) {
1238 $discovered_feeds[ $feed_url ]['title'] = $link['title'];
1239 } elseif ( isset( $link['text'] ) ) {
1240 $discovered_feeds[ $feed_url ]['title'] = $link['text'];
1241 }
1242 }
1243
1244 if ( ! $has_self ) {
1245 $discovered_feeds[ $url ] = array(
1246 'rel' => 'self',
1247 );
1248 }
1249 if ( empty( $discovered_feeds[ $url ]['title'] ) && ! empty( $mf['metas']['application-name'][0] ) ) {
1250 $discovered_feeds[ $url ]['title'] = $mf['metas']['application-name'][0];
1251 }
1252 if ( empty( $discovered_feeds[ $url ]['title'] ) && ! empty( $mf['metas']['og:title'][0] ) ) {
1253 $discovered_feeds[ $url ]['title'] = $mf['metas']['og:title'][0];
1254 }
1255 if ( empty( $discovered_feeds[ $url ]['title'] ) && ! empty( $mf['title'] ) ) {
1256 $discovered_feeds[ $url ]['title'] = $mf['title'];
1257 }
1258 if ( empty( $discovered_feeds[ $url ]['avatar'] ) && ! empty( $mf['rels']['icon'] ) ) {
1259 foreach ( $mf['rels']['icon'] as $icon_url ) {
1260 if ( ! filter_var( $icon_url, FILTER_VALIDATE_URL ) ) {
1261 continue;
1262 }
1263
1264 if ( ! isset( $mf['rel-urls'][ $icon_url ]['type'] ) || 0 !== strpos( $mf['rel-urls'][ $icon_url ]['type'], 'image/' ) ) {
1265 continue;
1266 }
1267
1268 $discovered_feeds[ $url ]['avatar'] = $icon_url;
1269 }
1270 }
1271
1272 return $discovered_feeds;
1273 }
1274
1275 /**
1276 * More generic version of the native url_to_postid()
1277 *
1278 * @param string $url Permalink to check.
1279 * @param int $author_id The id of the author.
1280 * @return int Post ID, or 0 on failure.
1281 */
1282 public static function url_to_postid( $url, $author_id = false ) {
1283 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1284
1285 // The post types are part of the cache key since they can be filtered per lookup.
1286 $cache_key = 'friends_url_to_postid_' . md5( $url . '|' . implode( ',', $post_types ) ) . ( $author_id ? '_' . $author_id : '' );
1287 $post_id = wp_cache_get( $cache_key, 'friends' );
1288 if ( false !== $post_id ) {
1289 return $post_id;
1290 }
1291 $args = $post_types;
1292
1293 global $wpdb;
1294 $sql = sprintf(
1295 'SELECT ID from ' . $wpdb->posts . ' WHERE post_type IN ( %s )',
1296 implode( ',', array_fill( 0, count( $post_types ), '%s' ) )
1297 );
1298
1299 if ( $author_id ) {
1300 $args[] = $author_id;
1301 $sql .= ' AND post_author = %d';
1302 }
1303
1304 $sql .= ' AND guid IN (%s, %s) LIMIT 1';
1305 $args[] = $url;
1306 $args[] = esc_attr( $url );
1307
1308 $post_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1309 $wpdb->prepare(
1310 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1311 $args
1312 )
1313 );
1314
1315 if ( $post_id ) {
1316 wp_cache_set( $cache_key, $post_id, 'friends' );
1317 }
1318 return $post_id;
1319 }
1320
1321 public function oembed_request_post_id( $post_id, $url ) {
1322 if ( ! $post_id ) {
1323 $post_id = self::url_to_postid( $url );
1324 global $wp_post_types;
1325 $wp_post_types[ Friends::CPT ]->publicly_queryable = true;
1326 }
1327 return $post_id;
1328 }
1329
1330 public function post_embed_url( $embed_url, $post ) {
1331 if ( ! in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ) ) ) {
1332 return $embed_url;
1333 }
1334
1335 return add_query_arg( 'url', $post->guid, rest_url( Rest::PREFIX . '/embed' ) );
1336 }
1337
1338 public function oembed_response_data( $data, $post, $width, $height ) {
1339 $data['width'] = absint( $width );
1340 $data['height'] = absint( $height );
1341 $data['type'] = 'rich';
1342
1343 ob_start();
1344 Friends::template_loader()->get_template_part( 'oembed/status', null, array( 'post' => $post ) );
1345 $data['html'] = ob_get_contents();
1346 ob_end_clean();
1347
1348 $data['html'] = '<p>' . wp_kses_post( $post->post_content ) . '</p>';
1349 return $data;
1350 }
1351
1352 public function get_user_feed_by_url( $url ) {
1353 return User_Feed::get_by_url( $url );
1354 }
1355
1356 /**
1357 * Gets the actual feed parser parser.
1358 *
1359 * @param string $parser The parser slug.
1360 *
1361 * @return Feed_Parser The actual parser.
1362 */
1363 public function get_feed_parser( $parser ) {
1364 if ( ! isset( $this->parsers[ $parser ] ) ) {
1365 return null;
1366 }
1367 return $this->parsers[ $parser ];
1368 }
1369
1370 /**
1371 * Gets the registered parser.
1372 *
1373 * @param string $parser The parser slug.
1374 *
1375 * @return string The parser name.
1376 */
1377 public function get_registered_parser( $parser ) {
1378 if ( ! isset( $this->parsers[ $parser ] ) ) {
1379 return null;
1380 }
1381 $parsers = $this->get_registered_parsers();
1382 return $parsers[ $parser ];
1383 }
1384
1385 /**
1386 * The list of currently supported parsers.
1387 *
1388 * @return array A list of parsers. Key is the slug, value is the parser name.
1389 */
1390 public function get_registered_parsers() {
1391 $parsers = array();
1392 foreach ( $this->parsers as $slug => $parser ) {
1393 $name = $slug;
1394 if ( defined( get_class( $parser ) . '::NAME' ) ) {
1395 $name = esc_html( $parser::NAME );
1396 }
1397 if ( defined( get_class( $parser ) . '::URL' ) ) {
1398 $name = '<a href="' . esc_url( $parser::URL ) . '" target="_blank" rel="noopener noreferrer">' . $name . '</a>';
1399 }
1400 $parsers[ $slug ] = $name;
1401 }
1402 return $parsers;
1403 }
1404
1405 /**
1406 * Get a parser instance by slug.
1407 *
1408 * @param string $slug The parser slug.
1409 * @return Feed_Parser|null The parser instance or null.
1410 */
1411 public function get_parser_by_slug( $slug ) {
1412 return isset( $this->parsers[ $slug ] ) ? $this->parsers[ $slug ] : null;
1413 }
1414
1415 /**
1416 * Get the badge for a feed based on its parser.
1417 *
1418 * @param User_Feed $user_feed The user feed.
1419 * @return array|null Badge info array or null.
1420 */
1421 public function get_feed_badge( $user_feed ) {
1422 $parser_slug = $user_feed->get_parser();
1423 $parser = $this->get_parser_by_slug( $parser_slug );
1424
1425 $badge = null;
1426 if ( $parser && method_exists( $parser, 'get_badge' ) ) {
1427 $badge = $parser->get_badge();
1428 }
1429
1430 /**
1431 * Filter the badge displayed for a feed.
1432 *
1433 * @param array|null $badge The badge array with 'label', 'color', 'title' keys, or null.
1434 * @param User_Feed $user_feed The user feed.
1435 * @param string $parser The parser slug.
1436 */
1437 return apply_filters( 'friends_feed_badge', $badge, $user_feed, $parser_slug );
1438 }
1439 }
1440