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

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

1,317 lines 39.5 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 = wp_strip_all_tags( $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 * Gets the notification keywords.
485 *
486 * @return array The notification keywords.
487 */
488 public static function get_active_notification_keywords() {
489 $active_keywords = array();
490 foreach ( self::get_all_notification_keywords() as $entry ) {
491 if ( $entry['enabled'] ) {
492 $active_keywords[] = $entry['keyword'];
493 }
494 }
495
496 return $active_keywords;
497 }
498
499 /**
500 * Gets all notification keywords.
501 *
502 * @return array All notification keywords.
503 */
504 public static function get_all_notification_keywords() {
505 $keywords = get_option( 'friends_notification_keywords', false );
506 if ( false === $keywords ) {
507 // Default keywords.
508 $keywords = array();
509 $keywords[] = array(
510 'enabled' => false,
511 'keyword' => trim( preg_replace( '#^https?:#', '', home_url() ), '/' ),
512 );
513 }
514 return $keywords;
515 }
516
517 /**
518 * Discover the post format for the item.
519 *
520 * @param object $item The feed item.
521 *
522 * @return string The discovered post format.
523 */
524 public function post_format_discovery( $item ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
525 // Not implemented yet.
526 return 'standard';
527 }
528
529 /**
530 * Return the number of revisions to keep.
531 *
532 * @return int The number of revisions to keep.
533 */
534 public function revisions_to_keep() {
535 return 10;
536 }
537
538 public function dissallowed_html( $allowedposttags, $context ) {
539 if ( 'post' === $context ) {
540 unset( $allowedposttags['article'] );
541 }
542 return $allowedposttags;
543 }
544
545 /**
546 * Process incoming feed items
547 *
548 * @param array $items The incoming items.
549 * @param User_Feed $user_feed The feed to which these items belong.
550 * @return array The post ids of the new posts.
551 */
552 public function process_incoming_feed_items( array $items, User_Feed $user_feed ) {
553 $friend_user = $user_feed->get_friend_user();
554 if ( ! $friend_user ) {
555 if ( apply_filters( 'friends_debug', false ) ) {
556 error_log( var_export( $user_feed, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
557 }
558 return;
559 }
560 $remote_post_ids = $friend_user->get_remote_post_ids();
561 $post_formats = get_post_format_strings();
562 $feed_post_format = $user_feed->get_post_format();
563
564 // Sort items by date asc so that older posts will have lower ids than newer posts.
565 usort(
566 $items,
567 function ( $a, $b ) {
568 if ( ! isset( $a->date ) || ! isset( $b->date ) ) {
569 return 0;
570 }
571 return strtotime( $a->date ) - strtotime( $b->date );
572 }
573 );
574
575 // Limit this as a safety measure.
576 add_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
577 $new_post_ids = array();
578 $modified_posts = array();
579 $manual_refresh = isset( $_GET['page'] ) && 'friends-refresh' === $_GET['page'] && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'friends-refresh' );
580 foreach ( $items as $item_key => $item ) {
581 if ( ! isset( $item->permalink ) || ! $item->permalink ) {
582 continue;
583 }
584 $item->permalink = str_replace( array( '&#38;', '&#038;' ), '&', ent2ncr( wp_kses_normalize_entities( $item->permalink ) ) );
585
586 if ( empty( $item->post_content ) ) {
587 $item->post_content = '';
588 }
589
590 add_filter( 'wp_kses_allowed_html', array( $this, 'dissallowed_html' ), 10, 2 );
591 $item->post_content = wp_kses_post( trim( $item->post_content ) );
592 remove_filter( 'wp_kses_allowed_html', array( $this, 'dissallowed_html' ), 10 );
593
594 $item = apply_filters( 'friends_early_modify_feed_item', $item, $user_feed, $friend_user );
595 if ( ! $item || $item->_feed_rule_delete ) {
596 continue;
597 }
598
599 // Fallback, when no friends plugin is installed.
600 $item->post_id = $item->permalink;
601 $item->post_status = 'publish';
602 if ( ( ! $item->post_content && ! $item->title ) || ! $item->permalink ) {
603 continue;
604 }
605
606 $post_id = null;
607 if ( $item->post_id && isset( $remote_post_ids[ $item->post_id ] ) ) {
608 $post_id = $remote_post_ids[ $item->post_id ];
609 }
610 if ( is_null( $post_id ) && $item->permalink && isset( $remote_post_ids[ $item->permalink ] ) ) {
611 $post_id = $remote_post_ids[ $item->permalink ];
612 }
613
614 if ( is_null( $post_id ) ) {
615 $post_id = self::url_to_postid( $item->permalink );
616 }
617 $item->_is_new = is_null( $post_id );
618 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, $post_id );
619
620 $updated_date = $item->date;
621 if ( ! empty( $item->updated_date ) ) {
622 $updated_date = $item->updated_date;
623 }
624
625 if ( empty( $item->title ) ) {
626 $item->title = '';
627 }
628
629 $post_data = array(
630 'post_title' => $item->title,
631 'post_content' => force_balance_tags( $item->post_content ),
632 'post_modified_gmt' => $updated_date,
633 'post_status' => $item->post_status,
634 'guid' => $item->permalink,
635 'comment_count' => $item->comment_count,
636 );
637
638 // Modified via feed rules.
639 if ( ! empty( $item->_feed_rule_transform ) && is_array( $item->_feed_rule_transform ) ) {
640 $post_data = array_merge( $post_data, $item->_feed_rule_transform );
641 }
642
643 $old_post = null;
644 if ( ! is_null( $post_id ) ) {
645 $old_post = get_post( $post_id );
646 $modified_post_data = array();
647 foreach ( array( 'post_title', 'post_content', 'post_status' ) as $field ) {
648 if ( empty( $old_post->$field ) || empty( $post_data[ $field ] ) ) {
649 continue;
650 }
651 if ( 'post_content' === $field && $old_post->$field !== $post_data[ $field ] ) {
652 $modified_post_data[ $field ] = $post_data[ $field ];
653 break;
654 }
655
656 if ( wp_strip_all_tags( $old_post->$field ) !== wp_strip_all_tags( $post_data[ $field ] ) ) {
657 $modified_post_data[ $field ] = $post_data[ $field ];
658 break;
659 }
660 }
661
662 if ( ! empty( $modified_post_data ) && apply_filters( 'friends_can_update_modified_feed_posts', true, $item, $user_feed, $friend_user, $post_id ) ) {
663 $was_modified_by_user = false;
664 foreach ( wp_get_post_revisions( $post_id ) as $revision ) {
665 if ( intval( $revision->post_author ) && intval( $revision->post_author ) !== $friend_user->ID ) {
666 $was_modified_by_user = true;
667 break;
668 }
669 }
670
671 if ( ! $was_modified_by_user || $manual_refresh ) {
672 $modified_post_data['ID'] = $post_id;
673 if ( isset( $modified_post_data['post_content'] ) ) {
674 $modified_post_data['post_content'] = str_replace( '\\', '\\\\', $modified_post_data['post_content'] );
675 }
676 if ( intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
677 $modified_post_data['comment_count'] = $item->comment_count;
678 }
679
680 wp_update_post( $modified_post_data );
681 $modified_posts[ $item_key ] = $post_id;
682 }
683 }
684 } else {
685 $post_data['post_type'] = Friends::CPT;
686 $post_data['post_date_gmt'] = $item->date;
687 $post_data['post_content'] = str_replace( '\\', '\\\\', $post_data['post_content'] );
688 $post_data['meta_input'] = $item->meta;
689
690 $post_id = $friend_user->insert_post( $post_data, true );
691 if ( is_wp_error( $post_id ) ) {
692 continue;
693 }
694
695 $new_post_ids[ $item_key ] = $post_id;
696
697 $remote_post_ids[ $item->permalink ] = $post_id;
698 }
699
700 if ( is_null( $old_post ) || intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
701 // The comment_count needs to be updated manually since it doesn't represent real comments in the database.
702 global $wpdb;
703 $wpdb->update( $wpdb->posts, array( 'comment_count' => $item->comment_count ), array( 'ID' => $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
704 wp_cache_delete( "comments-{$post_id}", 'counts' );
705 clean_post_cache( $post_id );
706 do_action( 'wp_update_comment_count', $post_id, $item->comment_count, $old_post ? $old_post->comment_count : 0 );
707 }
708
709 if ( $item->comments_feed && ( is_null( $old_post ) || $old_post->comments_feed !== $item->comments_feed ) ) {
710 update_post_meta( $post_id, self::COMMENTS_FEED_META, $item->comments_feed, $old_post ? $old_post->comments_feed : '' );
711 }
712
713 $post_format = $feed_post_format;
714 if ( 'autodetect' === $post_format ) {
715 if ( isset( $item->post_format ) && isset( $post_formats[ $item->post_format ] ) ) {
716 $post_format = $item->post_format;
717 } else {
718 $post_format = $this->post_format_discovery( $item );
719 }
720 }
721
722 if ( $post_format ) {
723 set_post_format( $post_id, $post_format );
724 }
725
726 if ( $item->author ) {
727 update_post_meta( $post_id, 'author', $item->author );
728 }
729
730 if ( is_numeric( $item->post_id ) ) {
731 update_post_meta( $post_id, 'remote_post_id', $item->{'post-id'} );
732 }
733
734 if ( ! get_option( 'friends_disable_auto_tagging' ) && isset( $item->friend_tags ) && ! empty( $item->friend_tags ) && is_array( $item->friend_tags ) ) {
735 Friend_Tag::add_tags( $post_id, $item->friend_tags );
736 }
737
738 if ( isset( $item->friend_mention_tags ) && ! empty( $item->friend_mention_tags ) && is_array( $item->friend_mention_tags ) ) {
739 Friend_Tag::add_tags( $post_id, $item->friend_mention_tags );
740 }
741
742 update_post_meta( $post_id, 'parser', $user_feed->get_parser() );
743 update_post_meta( $post_id, 'feed_url', $user_feed->get_url() );
744
745 global $wpdb;
746 $wpdb->update( $wpdb->posts, array( 'comment_count' => $item->comment_count ), array( 'ID' => $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
747 }
748 remove_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
749
750 $deleted_posts = $friend_user->delete_outdated_posts();
751 $new_post_ids = array_diff( $new_post_ids, $deleted_posts );
752
753 $this->notify_about_new_posts( $friend_user, $new_post_ids, $user_feed );
754
755 do_action( 'friends_retrieved_new_posts', $user_feed, $new_post_ids, $modified_posts, $friend_user );
756
757 $new_posts = array();
758 foreach ( $new_post_ids as $key => $post_id ) {
759 if ( isset( $items[ $key ] ) ) {
760 $new_posts[ $post_id ] = $items[ $key ];
761 }
762 }
763
764 return $new_posts;
765 }
766
767 /**
768 * Output an additional XMLNS for the feed.
769 */
770 public function additional_feed_namespaces() {
771 echo ' xmlns:friends="' . esc_attr( self::XMLNS ) . '" ';
772 }
773
774 /**
775 * Additional fields for the friends feed.
776 */
777 public function feed_additional_fields() {
778 global $post;
779 $post_format = get_post_format( $post );
780 if ( empty( $post_format ) ) {
781 $post_format = 'standard';
782 }
783 echo '<friends:post-format>' . esc_html( $post_format ) . '</friends:post-format>' . PHP_EOL;
784 }
785
786 /**
787 * Redirect
788 */
789 public function friends_add_friend_redirect() {
790 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
791 if ( ! isset( $_GET['add-friend'] ) || isset( $_GET['page'] ) ) {
792 return;
793 }
794 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
795 wp_safe_redirect( add_query_arg( 'url', sanitize_text_field( wp_unslash( $_GET['add-friend'] ) ), self_admin_url( 'admin.php?page=add-friend' ) ) );
796 exit;
797 }
798
799 /**
800 * Configure feed downloading options
801 *
802 * @param SimplePie $feed The SimplePie object.
803 */
804 public function wp_feed_options( $feed ) {
805 $feed->useragent .= ' Friends/' . FRIENDS_VERSION;
806 if (
807 isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'friends-refresh' ) &&
808 isset( $_GET['page'] ) && 'page=friends-refresh' === $_GET['page']
809 ) {
810 $feed->enable_cache( false );
811 } else {
812 $feed->set_cache_duration( 3590 );
813 }
814 }
815
816 /**
817 * Invalidatee Post Count Cache
818 *
819 * @param int $post_ID The post id.
820 * @param \WP_Post $post The post.
821 */
822 public function invalidate_post_count_cache( $post_ID, \WP_Post $post ) {
823 $cache_key = 'friends_post_count_by_post_format';
824 delete_transient( $cache_key );
825 if ( is_numeric( $post->post_author ) && $post->post_author > 0 ) {
826 $author_id = intval( $post->post_author );
827 delete_transient( $cache_key . '_author_' . $author_id );
828 }
829 }
830
831 /**
832 * Discover available feeds.
833 *
834 * @param string $url The feed URL.
835 * @return array The available feeds.
836 */
837 public function discover_available_feeds( $url ) {
838 if ( ! Friends::check_url( $url ) ) {
839 return array();
840 }
841 $available_feeds = array();
842 $content = null;
843 $content_type = 'text/html';
844
845 $response = wp_safe_remote_get(
846 $url,
847 array(
848 'timeout' => apply_filters( 'friends_http_timeout', 20 ),
849 'redirection' => 2,
850 )
851 );
852
853 if ( is_wp_error( $response ) ) {
854 $response->add_data( $url );
855 return $response;
856 }
857
858 $response_code = wp_remote_retrieve_response_code( $response );
859 if ( $response_code >= 300 && $response_code < 400 ) {
860 $location = wp_remote_retrieve_header( $response, 'location' );
861 if ( $location ) {
862 return new \WP_Error(
863 'redirect',
864 sprintf(
865 // translators: %s is a URL.
866 __( 'This URL redirects to %s — please use that URL instead.', 'friends' ),
867 '<a href="' . esc_url( admin_url( 'admin.php?page=add-friend&url=' . rawurlencode( $location ) ) ) . '">' . esc_html( $location ) . '</a>'
868 )
869 );
870 }
871 }
872
873 if ( 200 === $response_code ) {
874 $content = wp_remote_retrieve_body( $response );
875 $headers = wp_remote_retrieve_headers( $response );
876
877 // We'll determine the obvious feeds ourself.
878 $available_feeds = $this->discover_link_rel_feeds( $content, $url, $headers );
879 $content_type = strtok( $headers['content-type'], ';' );
880 }
881
882 foreach ( $this->parsers as $slug => $parser ) {
883 foreach ( $parser->discover_available_feeds( $content, $url ) as $link_url => $feed ) {
884 if ( isset( $available_feeds[ $link_url ] ) ) {
885 // If this parser tells us it can parse it right away, allow it to override.
886 if ( isset( $available_feeds[ $link_url ]['parser'] ) || ! isset( $feed['parser'] ) ) {
887 continue;
888 }
889 } else {
890 $available_feeds[ $link_url ] = array();
891 }
892 $available_feeds[ $link_url ] = array_merge( $available_feeds[ $link_url ], $feed );
893 $available_feeds[ $link_url ]['url'] = $link_url;
894 }
895 }
896
897 $has_friends_plugin = false;
898 foreach ( $available_feeds as $link_url => $feed ) {
899 $feed = array_merge(
900 array(
901 'url' => $link_url,
902 'rel' => 'unknown',
903 'type' => 'unknown',
904 'title' => '',
905 'parser' => 'unsupported',
906 'parser_confidence' => 0,
907 ),
908 $feed
909 );
910 $available_feeds[ $link_url ] = $feed;
911
912 if ( 'friends-base-url' === $feed['rel'] ) {
913 $available_feeds[ $link_url ]['parser'] = 'friends';
914 $available_feeds[ $link_url ]['parser_confidence'] = 1000;
915 $has_friends_plugin = $link_url;
916 continue;
917 }
918
919 foreach ( $this->parsers as $slug => $parser ) {
920 $confidence = intval( $parser->feed_support_confidence( $link_url, $feed['type'], $feed['rel'] ) );
921 $confidence = intval( apply_filters( 'friends_parser_confidence', $confidence, $slug, $link_url, $feed ) );
922 if ( $available_feeds[ $link_url ]['parser_confidence'] < $confidence ) {
923 $available_feeds[ $link_url ]['parser_confidence'] = $confidence;
924 $available_feeds[ $link_url ]['parser'] = $slug;
925 }
926 }
927 }
928
929 if ( ! isset( $available_feeds[ $url ] ) ) {
930 $feed = array(
931 'url' => $url,
932 'type' => $content_type,
933 'rel' => 'self',
934 'title' => '',
935 'parser' => 'unsupported',
936 'parser_confidence' => 0,
937 );
938
939 foreach ( $this->parsers as $slug => $parser ) {
940 $confidence = $parser->feed_support_confidence( $url, $feed['type'], $feed['rel'], $content );
941 $confidence = apply_filters( 'friends_parser_confidence', $confidence, $slug, $url, $feed );
942 if ( $feed['parser_confidence'] < $confidence ) {
943 $feed['parser_confidence'] = $confidence;
944 $feed['parser'] = $slug;
945 }
946 }
947
948 if ( 'unknown' !== $feed['parser'] ) {
949 $available_feeds[ $feed['url'] ] = $feed;
950 }
951 }
952
953 foreach ( array_keys( $available_feeds ) as $link_url ) {
954 if ( ! isset( $this->parsers[ $available_feeds[ $link_url ]['parser'] ] ) ) {
955 continue;
956 }
957 $parser = $this->parsers[ $available_feeds[ $link_url ]['parser'] ];
958 $updated_feed_details = $parser->update_feed_details( $available_feeds[ $link_url ] );
959 if ( is_array( $updated_feed_details ) && ! is_wp_error( $updated_feed_details ) ) {
960 $available_feeds[ $link_url ] = array_merge( $available_feeds[ $link_url ], $updated_feed_details );
961 }
962 $available_feeds[ $link_url ] = apply_filters( 'friends_update_feed_details', $available_feeds[ $link_url ], $slug );
963 if ( $available_feeds[ $link_url ]['url'] !== $link_url ) {
964 $new_url = $available_feeds[ $link_url ]['url'];
965 $available_feeds[ $new_url ] = $available_feeds[ $link_url ];
966 unset( $available_feeds[ $link_url ] );
967 }
968 }
969
970 $autoselected = false;
971 // Check if a parser already autoselected a feed.
972 foreach ( $available_feeds as $link_url => $feed ) {
973 if ( isset( $feed['autoselect'] ) && $feed['autoselect'] ) {
974 $autoselected = true;
975 break;
976 }
977 }
978
979 foreach ( $available_feeds as $link_url => $feed ) {
980 $path = wp_parse_url( $link_url, PHP_URL_PATH );
981
982 // Backfill titles.
983 if ( empty( $feed['title'] ) ) {
984 $host = wp_parse_url( $link_url, PHP_URL_HOST );
985 if ( is_string( $path ) && trim( $path, '/' ) ) {
986 $feed['title'] = trim( preg_replace( '#^www\.#i', '', preg_replace( '#[^a-z0-9.:-]#i', ' ', ucwords( $host . ': ' . $path ) ) ), ': ' );
987 } else {
988 $feed['title'] = strtolower( $host );
989 }
990 $available_feeds[ $link_url ]['title'] = $feed['title'];
991 }
992
993 // Autoselect heuristic.
994 if ( $autoselected ) {
995 continue;
996 }
997
998 if ( ! $feed['parser_confidence'] ) {
999 continue;
1000 }
1001
1002 // Prefer the main RSS feed.
1003 if (
1004 '/feed' === substr( '/' . trim( $path, '/' ), -5 )
1005 || '.xml' === substr( $path, -4 )
1006 || 'rss' === substr( $path, -3 )
1007 ) {
1008 if ( $has_friends_plugin ) {
1009 $available_feeds[ $link_url ]['post-format'] = 'autodetect';
1010 }
1011 $autoselected = true;
1012 } elseif ( isset( $feed['rel'] ) ) {
1013 if ( 'alternate' === $feed['rel'] && 'application/rss+xml' === $feed['type'] && 'feed' === trim( $path, '/' ) ) {
1014 $autoselected = true;
1015 }
1016 }
1017
1018 if ( $autoselected ) {
1019 $available_feeds[ $link_url ]['autoselect'] = true;
1020 }
1021 }
1022
1023 $feed_sort_order = array( 'self', 'alternate', 'me' );
1024 if ( $has_friends_plugin ) {
1025 // If we have the Friends plugin, we prefer an (augmented) RSS feed over, for example, microformats.
1026 $feed_sort_order = array( 'alternate', 'self', 'me' );
1027 }
1028
1029 uasort(
1030 $available_feeds,
1031 function ( $a, $b ) use ( $feed_sort_order ) {
1032 if ( isset( $a['autoselect'] ) && $a['autoselect'] ) {
1033 if ( ! isset( $b['autoselect'] ) || ! $b['autoselect'] ) {
1034 return -1;
1035 }
1036 } elseif ( isset( $b['autoselect'] ) && $b['autoselect'] ) {
1037 return 1;
1038 }
1039
1040 foreach ( $feed_sort_order as $rel_sort ) {
1041 if ( $rel_sort === $a['rel'] ) {
1042 if ( $rel_sort !== $b['rel'] ) {
1043 return -1;
1044 }
1045 break;
1046 } elseif ( $rel_sort === $b['rel'] ) {
1047 return 1;
1048 }
1049 }
1050
1051 return strcmp( $a['title'], $b['title'] );
1052 }
1053 );
1054
1055 return apply_filters( 'friends_available_feeds', $available_feeds, $url );
1056 }
1057
1058 /**
1059 * Discover feeds specified in the <link> section.
1060 *
1061 * @param string $content The content to search.
1062 * @param string $url The url.
1063 * @param object $headers The headers from the request.
1064 *
1065 * @return array ( description_of_the_return_value )
1066 */
1067 private function discover_link_rel_feeds( $content, $url, $headers ) {
1068 $discovered_feeds = array();
1069 $has_self = false;
1070 $links = array();
1071 $mf = Mf2\parse( $content, $url );
1072
1073 if ( isset( $mf['rel-urls'] ) ) {
1074 $links = array_merge( $links, $mf['rel-urls'] );
1075 }
1076 foreach ( $headers as $header => $value ) {
1077 if ( 'link' === strtolower( $header ) ) {
1078 $values = wp_parse_args( $value );
1079 if ( isset( $values['href'] ) ) {
1080 if ( ! isset( $values['rels'] ) && $values['rel'] ) {
1081 $values['rels'] = array( $values['rel'] );
1082 unset( $values['rel'] );
1083 }
1084 if ( isset( $values['rels'] ) ) {
1085 if ( isset( $links[ $values['href'] ] ) ) {
1086 foreach ( $values['rels'] as $rel ) {
1087 $links[ $values['href'] ]['rels'][] = $rel;
1088 }
1089 } else {
1090 $links[ $values['href'] ] = $values;
1091 }
1092 }
1093 }
1094 }
1095 }
1096
1097 foreach ( $links as $feed_url => $link ) {
1098 foreach ( array( 'friends-base-url', 'me', 'alternate', 'self' ) as $rel ) {
1099 if ( in_array( $rel, $link['rels'] ) ) {
1100 $discovered_feeds[ $feed_url ] = array(
1101 'rel' => $rel,
1102 );
1103 }
1104 }
1105
1106 if ( ! isset( $discovered_feeds[ $feed_url ] ) ) {
1107 continue;
1108 }
1109
1110 if ( 'self' === $discovered_feeds[ $feed_url ]['rel'] ) {
1111 $has_self = true;
1112 }
1113
1114 if ( isset( $link['type'] ) ) {
1115 $discovered_feeds[ $feed_url ]['type'] = $link['type'];
1116 }
1117
1118 if ( isset( $link['title'] ) ) {
1119 $discovered_feeds[ $feed_url ]['title'] = $link['title'];
1120 } elseif ( isset( $link['text'] ) ) {
1121 $discovered_feeds[ $feed_url ]['title'] = $link['text'];
1122 }
1123 }
1124
1125 if ( ! $has_self ) {
1126 $discovered_feeds[ $url ] = array(
1127 'rel' => 'self',
1128 );
1129 }
1130 if ( empty( $discovered_feeds[ $url ]['title'] ) && ! empty( $mf['metas']['application-name'][0] ) ) {
1131 $discovered_feeds[ $url ]['title'] = $mf['metas']['application-name'][0];
1132 }
1133 if ( empty( $discovered_feeds[ $url ]['title'] ) && ! empty( $mf['metas']['og:title'][0] ) ) {
1134 $discovered_feeds[ $url ]['title'] = $mf['metas']['og:title'][0];
1135 }
1136 if ( empty( $discovered_feeds[ $url ]['title'] ) && ! empty( $mf['title'] ) ) {
1137 $discovered_feeds[ $url ]['title'] = $mf['title'];
1138 }
1139 if ( empty( $discovered_feeds[ $url ]['avatar'] ) && ! empty( $mf['rels']['icon'] ) ) {
1140 foreach ( $mf['rels']['icon'] as $icon_url ) {
1141 if ( ! filter_var( $icon_url, FILTER_VALIDATE_URL ) ) {
1142 continue;
1143 }
1144
1145 if ( ! isset( $mf['rel-urls'][ $icon_url ]['type'] ) || 0 !== strpos( $mf['rel-urls'][ $icon_url ]['type'], 'image/' ) ) {
1146 continue;
1147 }
1148
1149 $discovered_feeds[ $url ]['avatar'] = $icon_url;
1150 }
1151 }
1152
1153 return $discovered_feeds;
1154 }
1155
1156 /**
1157 * More generic version of the native url_to_postid()
1158 *
1159 * @param string $url Permalink to check.
1160 * @param int $author_id The id of the author.
1161 * @return int Post ID, or 0 on failure.
1162 */
1163 public static function url_to_postid( $url, $author_id = false ) {
1164 $cache_key = 'friends_url_to_postid_' . md5( $url ) . ( $author_id ? '_' . $author_id : '' );
1165 $post_id = wp_cache_get( $cache_key, 'friends' );
1166 if ( false !== $post_id ) {
1167 return $post_id;
1168 }
1169 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1170 $args = $post_types;
1171
1172 global $wpdb;
1173 $sql = sprintf(
1174 'SELECT ID from ' . $wpdb->posts . ' WHERE post_type IN ( %s )',
1175 implode( ',', array_fill( 0, count( $post_types ), '%s' ) )
1176 );
1177
1178 if ( $author_id ) {
1179 $args[] = $author_id;
1180 $sql .= ' AND post_author = %d';
1181 }
1182
1183 $sql .= ' AND guid IN (%s, %s) LIMIT 1';
1184 $args[] = $url;
1185 $args[] = esc_attr( $url );
1186
1187 $post_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1188 $wpdb->prepare(
1189 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1190 $args
1191 )
1192 );
1193
1194 wp_cache_set( $cache_key, $post_id, 'friends' );
1195 return $post_id;
1196 }
1197
1198 public function oembed_request_post_id( $post_id, $url ) {
1199 if ( ! $post_id ) {
1200 $post_id = self::url_to_postid( $url );
1201 global $wp_post_types;
1202 $wp_post_types[ Friends::CPT ]->publicly_queryable = true;
1203 }
1204 return $post_id;
1205 }
1206
1207 public function post_embed_url( $embed_url, $post ) {
1208 if ( ! in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ) ) ) {
1209 return $embed_url;
1210 }
1211
1212 return add_query_arg( 'url', $post->guid, rest_url( Rest::PREFIX . '/embed' ) );
1213 }
1214
1215 public function oembed_response_data( $data, $post, $width, $height ) {
1216 $data['width'] = absint( $width );
1217 $data['height'] = absint( $height );
1218 $data['type'] = 'rich';
1219
1220 ob_start();
1221 Friends::template_loader()->get_template_part( 'oembed/status', null, array( 'post' => $post ) );
1222 $data['html'] = ob_get_contents();
1223 ob_end_clean();
1224
1225 $data['html'] = '<p>' . wp_kses_post( $post->post_content ) . '</p>';
1226 return $data;
1227 }
1228
1229 public function get_user_feed_by_url( $url ) {
1230 return User_Feed::get_by_url( $url );
1231 }
1232
1233 /**
1234 * Gets the actual feed parser parser.
1235 *
1236 * @param string $parser The parser slug.
1237 *
1238 * @return Feed_Parser The actual parser.
1239 */
1240 public function get_feed_parser( $parser ) {
1241 if ( ! isset( $this->parsers[ $parser ] ) ) {
1242 return null;
1243 }
1244 return $this->parsers[ $parser ];
1245 }
1246
1247 /**
1248 * Gets the registered parser.
1249 *
1250 * @param string $parser The parser slug.
1251 *
1252 * @return string The parser name.
1253 */
1254 public function get_registered_parser( $parser ) {
1255 if ( ! isset( $this->parsers[ $parser ] ) ) {
1256 return null;
1257 }
1258 $parsers = $this->get_registered_parsers();
1259 return $parsers[ $parser ];
1260 }
1261
1262 /**
1263 * The list of currently supported parsers.
1264 *
1265 * @return array A list of parsers. Key is the slug, value is the parser name.
1266 */
1267 public function get_registered_parsers() {
1268 $parsers = array();
1269 foreach ( $this->parsers as $slug => $parser ) {
1270 $name = $slug;
1271 if ( defined( get_class( $parser ) . '::NAME' ) ) {
1272 $name = esc_html( $parser::NAME );
1273 }
1274 if ( defined( get_class( $parser ) . '::URL' ) ) {
1275 $name = '<a href="' . esc_url( $parser::URL ) . '" target="_blank" rel="noopener noreferrer">' . $name . '</a>';
1276 }
1277 $parsers[ $slug ] = $name;
1278 }
1279 return $parsers;
1280 }
1281
1282 /**
1283 * Get a parser instance by slug.
1284 *
1285 * @param string $slug The parser slug.
1286 * @return Feed_Parser|null The parser instance or null.
1287 */
1288 public function get_parser_by_slug( $slug ) {
1289 return isset( $this->parsers[ $slug ] ) ? $this->parsers[ $slug ] : null;
1290 }
1291
1292 /**
1293 * Get the badge for a feed based on its parser.
1294 *
1295 * @param User_Feed $user_feed The user feed.
1296 * @return array|null Badge info array or null.
1297 */
1298 public function get_feed_badge( $user_feed ) {
1299 $parser_slug = $user_feed->get_parser();
1300 $parser = $this->get_parser_by_slug( $parser_slug );
1301
1302 $badge = null;
1303 if ( $parser && method_exists( $parser, 'get_badge' ) ) {
1304 $badge = $parser->get_badge();
1305 }
1306
1307 /**
1308 * Filter the badge displayed for a feed.
1309 *
1310 * @param array|null $badge The badge array with 'label', 'color', 'title' keys, or null.
1311 * @param User_Feed $user_feed The user feed.
1312 * @param string $parser The parser slug.
1313 */
1314 return apply_filters( 'friends_feed_badge', $badge, $user_feed, $parser_slug );
1315 }
1316 }
1317