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
← All changes | includes/class-feed.php +392 -154 2.7.84.3.2 View file →
@@ -55,11 +55,8 @@
55 55 /**
56 56 * Register the WordPress hooks
57 57 */
58 58 private function register_hooks() {
59 - add_filter( 'pre_get_posts', array( $this, 'private_feed_query' ), 1 );
60 - add_filter( 'private_title_format', array( $this, 'private_title_format' ) );
61 - add_filter( 'pre_option_rss_use_excerpt', array( $this, 'feed_use_excerpt' ), 90 );
62 59 add_filter( 'friends_early_modify_feed_item', array( $this, 'apply_early_feed_rules' ), 10, 3 );
63 60 add_filter( 'friends_modify_feed_item', array( $this, 'apply_feed_rules' ), 10, 3 );
64 61
65 62 add_action( 'rss_item', array( $this, 'feed_additional_fields' ) );
@@ -112,9 +109,16 @@
112 109 /**
113 110 * Cron function to refresh the feeds of the friends' blogs
114 111 */
115 112 public function cron_friends_refresh_feeds() {
116 - $this->retrieve_friend_posts();
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 + }
117 121 }
118 122
119 123 /**
120 124 * Function to fetch a users feeds.
@@ -128,12 +132,12 @@
128 132 }
129 133
130 134 foreach ( $friend_user->get_active_feeds() as $feed ) {
131 135 $friend_user = $feed->get_friend_user();
132 - if ( $friend_user ) {
136 + if ( $friend_user && $feed->can_be_polled_now() ) {
137 + $feed->set_polling_now();
133 138 $this->retrieve_feed( $feed );
134 139 $feed->was_polled();
135 - $friend_user->delete_outdated_posts();
136 140 }
137 141 }
138 142 }
139 143
@@ -139,17 +143,29 @@
139 143
140 144 /**
141 145 * Preview a URL using a parser.
142 146 *
143 - * @param string $parser The parser slug.
144 - * @param string $url The url.
145 - * @param int $feed_id The feed id.
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.
146 150 *
147 - * @return array|\WP_error The feed items.
151 + * @return array|\WP_Error The feed items.
148 152 */
149 153 public function preview( $parser, $url, $feed_id = null ) {
150 - if ( ! isset( $this->parsers[ $parser ] ) ) {
151 - return new \WP_Error( 'unknown-parser', __( 'An unknown parser name was supplied.', 'friends' ) );
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' ) );
152 168 }
153 169
154 170 $user_feed = null;
155 171 $friend_user = null;
@@ -159,9 +175,9 @@
159 175 $friend_user = $user_feed->get_friend_user();
160 176 }
161 177 }
162 178
163 - $items = $this->parsers[ $parser ]->fetch_feed( $url, $user_feed );
179 + $items = $parser->fetch_feed( $url, $user_feed );
164 180
165 181 if ( ! is_wp_error( $items ) ) {
166 182 if ( empty( $items ) ) {
167 183 $items = new \WP_Error( 'empty-feed', __( "This feed doesn't contain any entries. There might be a problem parsing the feed.", 'friends' ) );
@@ -204,9 +220,9 @@
204 220 do_action( 'friends_retrieve_friends_error', $user_feed, $error, $friend_user );
205 221 return $error;
206 222 }
207 223 try {
208 - $items = $this->parsers[ $parser ]->fetch_feed( $user_feed->get_private_url(), $user_feed );
224 + $items = $this->parsers[ $parser ]->fetch_feed( $user_feed->get_url(), $user_feed );
209 225 } catch ( \Exception $e ) {
210 226 $items = new \WP_Error( $parser . '-failed', $e->getMessage() );
211 227 }
212 228
@@ -238,9 +254,9 @@
238 254 $fulltext = '';
239 255 foreach ( apply_filters( 'friends_keyword_search_fields', array( 'post_title', 'post_content' ) ) as $field ) {
240 256 $fulltext .= PHP_EOL . $post->$field;
241 257 }
242 - $fulltext = strip_tags( $fulltext );
258 + $fulltext = self::prepare_keyword_search_text( $fulltext );
243 259 foreach ( $keywords as $keyword ) {
244 260 if ( preg_match( '/' . str_replace( '/', '\\/', $keyword ) . '/ius', $fulltext ) ) {
245 261 $keyword_match = $keyword;
246 262 break;
@@ -247,9 +263,9 @@
247 263 }
248 264 }
249 265
250 266 if ( $keyword_match ) {
251 - $notified = apply_filters( 'notify_keyword_match_post', false, $post, $keyword_match );
267 + $notified = apply_filters( 'friends_notify_keyword_match_post', false, $post, $keyword_match );
252 268 if ( $notified ) {
253 269 continue;
254 270 }
255 271 }
@@ -254,14 +270,14 @@
254 270 }
255 271 }
256 272 }
257 273
258 - $notify_users = apply_filters( 'notify_about_new_friend_post', true, $friend_user, $post_id, $user_feed );
274 + $notify_users = apply_filters( 'notify_about_new_friend_post', true, $friend_user, $post_id, $user_feed, $keyword_match );
259 275 if ( $notify_users ) {
260 276 if ( ! $post ) {
261 277 $post = get_post( intval( $post_id ) );
262 278 }
263 - do_action( 'notify_new_friend_post', $post, $user_feed );
279 + do_action( 'notify_new_friend_post', $post, $user_feed, $keyword_match );
264 280 }
265 281 }
266 282 }
267 283
@@ -267,18 +283,15 @@
267 283
268 284 /**
269 285 * Retrieve posts from all friends.
270 286 *
271 - * @param bool $force Whether to force retrieval.
287 + * @param bool $ignore_due_date Whether to get also undue feeds.
272 288 */
273 - public function retrieve_friend_posts( $force = false ) {
274 - foreach ( User_Feed::get_all_due() as $feed ) {
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();
275 292 $this->retrieve_feed( $feed );
276 293 $feed->was_polled();
277 - $friend_user = $feed->get_friend_user();
278 - if ( $friend_user ) {
279 - $friend_user->delete_outdated_posts();
280 - }
281 294 }
282 295 }
283 296
284 297 /**
@@ -288,9 +301,9 @@
288 301 * @param User_Feed $feed The feed.
289 302 * @param User $friend_user The friend user.
290 303 * @return Feed_Item The modified feed item.
291 304 */
292 - public function apply_early_feed_rules( $item, User_Feed $feed = null, User $friend_user = null ) {
305 + public function apply_early_feed_rules( $item, ?User_Feed $feed = null, ?User $friend_user = null ) {
293 306 $updated_item = $this->apply_feed_rules( $item, $feed, $friend_user );
294 307 if ( $updated_item->_feed_rule_delete ) {
295 308 return $updated_item;
296 309 }
@@ -304,9 +317,9 @@
304 317 * @param User_Feed $feed The feed object.
305 318 * @param User $friend_user The friend user.
306 319 * @return Feed_Item The modified feed item.
307 320 */
308 - public function apply_feed_rules( $item, User_Feed $feed = null, User $friend_user = null ) {
321 + public function apply_feed_rules( $item, ?User_Feed $feed = null, ?User $friend_user = null ) {
309 322 if ( is_null( $friend_user ) ) {
310 323 return $item;
311 324 }
312 325
@@ -311,8 +324,9 @@
311 324 }
312 325
313 326 $rules = $friend_user->get_feed_rules();
314 327 $action = $friend_user->get_feed_catch_all();
328 + $decided = false;
315 329
316 330 foreach ( $rules as $rule ) {
317 331 if ( $item instanceof \WP_Post ) {
318 332 $field = $this->get_feed_rule_field( $rule['field'] );
@@ -322,20 +336,31 @@
322 336 }
323 337 } else {
324 338 $field = $rule['field'];
325 339 }
326 -
327 - if ( $item->$field && preg_match( '/' . str_replace( '/', '\\/', $rule['regex'] ) . '/ius', $item->$field ) ) {
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 ) ) {
328 349 if ( 'replace' === $rule['action'] ) {
329 350 $item->_feed_rule_transform = array(
330 - $this->get_feed_rule_field( $rule['field'], $item ) => preg_replace( '/' . $rule['regex'] . '/iu', $rule['replace'], $item->$field ),
351 + $field_rule_field => preg_replace( '/' . str_replace( '/', '\\/', $rule['regex'] ) . '/ius', $rule['replace'], $subject ),
331 352 );
332 353 continue;
333 354 }
355 + if ( $decided ) {
356 + continue;
357 + }
334 358 $action = $rule['action'];
335 - break;
359 + $decided = true;
336 360 }
337 361 }
362 +
338 363 switch ( $action ) {
339 364 case 'delete':
340 365 $item->_feed_rule_delete = true;
341 366 return $item;
@@ -340,10 +365,16 @@
340 365 $item->_feed_rule_delete = true;
341 366 return $item;
342 367
343 368 case 'trash':
344 - $item->_feed_rule_transform = array(
345 - 'post_status' => '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 + )
346 377 );
347 378 return $item;
348 379
349 380 case 'accept':
@@ -348,9 +379,8 @@
348 379
349 380 case 'accept':
350 381 return $item;
351 382 }
352 -
353 383 return $item;
354 384 }
355 385
356 386 /**
@@ -377,11 +407,16 @@
377 407 *
378 408 * @param array $rules The rules to validate.
379 409 * @return array The valid rules.
380 410 */
381 - public function validate_feed_rules( $rules ) {
411 + public static function validate_feed_rules( $rules ) {
382 412 if ( ! is_array( $rules ) ) {
383 - return array();
413 + $json_rules = json_decode( $rules, true );
414 + if ( is_array( $json_rules ) ) {
415 + $rules = $json_rules;
416 + } else {
417 + return array();
418 + }
384 419 }
385 420
386 421 if ( isset( $rules['field'] ) && is_array( $rules['field'] ) ) {
387 422 // Transform POST values.
@@ -436,9 +471,9 @@
436 471 *
437 472 * @param array $catch_all The catch_all value to.
438 473 * @return array A valid catch_all
439 474 */
440 - public function validate_feed_catch_all( $catch_all ) {
475 + public static function validate_feed_catch_all( $catch_all ) {
441 476 if ( ! in_array( $catch_all, array( 'accept', 'trash', 'delete' ), true ) ) {
442 477 return 'accept';
443 478 }
444 479
@@ -445,8 +480,113 @@
445 480 return $catch_all;
446 481 }
447 482
448 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 + /**
449 589 * Gets the notification keywords.
450 590 *
451 591 * @return array The notification keywords.
452 592 */
@@ -485,9 +625,9 @@
485 625 * @param object $item The feed item.
486 626 *
487 627 * @return string The discovered post format.
488 628 */
489 - public function post_format_discovery( $item ) {
629 + public function post_format_discovery( $item ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
490 630 // Not implemented yet.
491 631 return 'standard';
492 632 }
493 633
@@ -516,9 +656,11 @@
516 656 */
517 657 public function process_incoming_feed_items( array $items, User_Feed $user_feed ) {
518 658 $friend_user = $user_feed->get_friend_user();
519 659 if ( ! $friend_user ) {
520 - error_log( var_export( $user_feed, true ) );
660 + if ( apply_filters( 'friends_debug', false ) ) {
661 + error_log( var_export( $user_feed, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
662 + }
521 663 return;
522 664 }
523 665 $remote_post_ids = $friend_user->get_remote_post_ids();
524 666 $post_formats = get_post_format_strings();
@@ -523,14 +665,26 @@
523 665 $remote_post_ids = $friend_user->get_remote_post_ids();
524 666 $post_formats = get_post_format_strings();
525 667 $feed_post_format = $user_feed->get_post_format();
526 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 +
527 680 // Limit this as a safety measure.
528 681 add_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
529 - $new_posts = array();
682 + $new_post_ids = array();
530 683 $modified_posts = array();
531 - foreach ( $items as $item ) {
532 - if ( ! $item->permalink ) {
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 ) {
533 687 continue;
534 688 }
535 689 $item->permalink = str_replace( array( '&#38;', '&#038;' ), '&', ent2ncr( wp_kses_normalize_entities( $item->permalink ) ) );
536 690
@@ -554,17 +708,17 @@
554 708 continue;
555 709 }
556 710
557 711 $post_id = null;
558 - if ( isset( $remote_post_ids[ $item->post_id ] ) ) {
712 + if ( $item->post_id && isset( $remote_post_ids[ $item->post_id ] ) ) {
559 713 $post_id = $remote_post_ids[ $item->post_id ];
560 714 }
561 - if ( is_null( $post_id ) && isset( $remote_post_ids[ $item->permalink ] ) ) {
715 + if ( is_null( $post_id ) && $item->permalink && isset( $remote_post_ids[ $item->permalink ] ) ) {
562 716 $post_id = $remote_post_ids[ $item->permalink ];
563 717 }
564 718
565 719 if ( is_null( $post_id ) ) {
566 - $post_id = self::url_to_postid( $item->permalink, $friend_user->ID );
720 + $post_id = self::url_to_postid( $item->permalink );
567 721 }
568 722 $item->_is_new = is_null( $post_id );
569 723 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, $post_id );
570 724
@@ -598,12 +752,17 @@
598 752 foreach ( array( 'post_title', 'post_content', 'post_status' ) as $field ) {
599 753 if ( empty( $old_post->$field ) || empty( $post_data[ $field ] ) ) {
600 754 continue;
601 755 }
602 - if ( strip_tags( $old_post->$field ) !== strip_tags( $post_data[ $field ] ) ) {
756 + if ( 'post_content' === $field && $old_post->$field !== $post_data[ $field ] ) {
603 757 $modified_post_data[ $field ] = $post_data[ $field ];
604 758 break;
605 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 + }
606 765 }
607 766
608 767 if ( ! empty( $modified_post_data ) && apply_filters( 'friends_can_update_modified_feed_posts', true, $item, $user_feed, $friend_user, $post_id ) ) {
609 768 $was_modified_by_user = false;
@@ -613,9 +772,9 @@
613 772 break;
614 773 }
615 774 }
616 775
617 - if ( ! $was_modified_by_user ) {
776 + if ( ! $was_modified_by_user || $manual_refresh ) {
618 777 $modified_post_data['ID'] = $post_id;
619 778 if ( isset( $modified_post_data['post_content'] ) ) {
620 779 $modified_post_data['post_content'] = str_replace( '\\', '\\\\', $modified_post_data['post_content'] );
621 780 }
@@ -621,10 +780,11 @@
621 780 }
622 781 if ( intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
623 782 $modified_post_data['comment_count'] = $item->comment_count;
624 783 }
784 +
625 785 wp_update_post( $modified_post_data );
626 - $modified_posts[] = $post_id;
786 + $modified_posts[ $item_key ] = $post_id;
627 787 }
628 788 }
629 789 } else {
630 790 $post_data['post_type'] = Friends::CPT;
@@ -636,9 +796,9 @@
636 796 if ( is_wp_error( $post_id ) ) {
637 797 continue;
638 798 }
639 799
640 - $new_posts[] = $post_id;
800 + $new_post_ids[ $item_key ] = $post_id;
641 801
642 802 $remote_post_ids[ $item->permalink ] = $post_id;
643 803 }
644 804
@@ -644,9 +804,9 @@
644 804
645 805 if ( is_null( $old_post ) || intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
646 806 // The comment_count needs to be updated manually since it doesn't represent real comments in the database.
647 807 global $wpdb;
648 - $wpdb->update( $wpdb->posts, array( 'comment_count' => $item->comment_count ), array( 'ID' => $post_id ) );
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
649 809 wp_cache_delete( "comments-{$post_id}", 'counts' );
650 810 clean_post_cache( $post_id );
651 811 do_action( 'wp_update_comment_count', $post_id, $item->comment_count, $old_post ? $old_post->comment_count : 0 );
652 812 }
@@ -675,50 +835,39 @@
675 835 if ( is_numeric( $item->post_id ) ) {
676 836 update_post_meta( $post_id, 'remote_post_id', $item->{'post-id'} );
677 837 }
678 838
679 - wp_set_object_terms( $post_id, $user_feed->get_id(), User_Feed::POST_TAXONOMY );
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 + }
680 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 +
681 847 update_post_meta( $post_id, 'parser', $user_feed->get_parser() );
682 848 update_post_meta( $post_id, 'feed_url', $user_feed->get_url() );
683 849
684 850 global $wpdb;
685 - $wpdb->update( $wpdb->posts, array( 'comment_count' => $item->comment_count ), array( 'ID' => $post_id ) );
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
686 852 }
687 853 remove_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
688 854
689 - $this->notify_about_new_posts( $friend_user, $new_posts, $user_feed );
855 + $deleted_posts = $friend_user->delete_outdated_posts();
856 + $new_post_ids = array_diff( $new_post_ids, $deleted_posts );
690 857
691 - do_action( 'friends_retrieved_new_posts', $user_feed, $new_posts, $modified_posts, $friend_user );
858 + $this->notify_about_new_posts( $friend_user, $new_post_ids, $user_feed );
692 859
693 - return $new_posts;
694 - }
860 + do_action( 'friends_retrieved_new_posts', $user_feed, $new_post_ids, $modified_posts, $friend_user );
695 861
696 - /**
697 - * Remove the Private: when sending a private feed.
698 - *
699 - * @param string $title_format The title format for a private post title.
700 - * @return string The modified title format for a private post title.
701 - */
702 - public function private_title_format( $title_format ) {
703 - if ( $this->friends->access_control->feed_is_authenticated() ) {
704 - return '%s';
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 + }
705 867 }
706 - return $title_format;
707 - }
708 868
709 - /**
710 - * Disable excerpted feeds for friend feeds
711 - *
712 - * @param boolean $feed_use_excerpt Whether to only have excerpts in feeds.
713 - * @return boolean The modified flag whether to have excerpts in feeds.
714 - */
715 - public function feed_use_excerpt( $feed_use_excerpt ) {
716 - if ( $this->friends->access_control->feed_is_authenticated() ) {
717 - return 0;
718 - }
719 -
720 - return $feed_use_excerpt;
869 + return $new_posts;
721 870 }
722 871
723 872 /**
724 873 * Output an additional XMLNS for the feed.
@@ -736,17 +885,8 @@
736 885 if ( empty( $post_format ) ) {
737 886 $post_format = 'standard';
738 887 }
739 888 echo '<friends:post-format>' . esc_html( $post_format ) . '</friends:post-format>' . PHP_EOL;
740 -
741 - $authenticated_user_id = $this->friends->access_control->feed_is_authenticated();
742 - if ( ! $authenticated_user_id ) {
743 - return;
744 - }
745 -
746 - echo '<friends:gravatar>' . esc_html( get_avatar_url( $post->post_author ) ) . '</friends:gravatar>' . PHP_EOL;
747 - echo '<friends:post-status>' . esc_html( $post->post_status ) . '</friends:post-status>' . PHP_EOL;
748 - echo '<friends:post-id>' . esc_html( $post->ID ) . '</friends:post-id>' . PHP_EOL;
749 889 }
750 890
751 891 /**
752 892 * Redirect
@@ -751,13 +891,14 @@
751 891 /**
752 892 * Redirect
753 893 */
754 894 public function friends_add_friend_redirect() {
895 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
755 896 if ( ! isset( $_GET['add-friend'] ) || isset( $_GET['page'] ) ) {
756 897 return;
757 898 }
758 -
759 - wp_safe_redirect( add_query_arg( 'url', $_GET['add-friend'], self_admin_url( 'admin.php?page=add-friend' ) ) );
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' ) ) );
760 901 exit;
761 902 }
762 903
763 904 /**
@@ -766,9 +907,12 @@
766 907 * @param SimplePie $feed The SimplePie object.
767 908 */
768 909 public function wp_feed_options( $feed ) {
769 910 $feed->useragent .= ' Friends/' . FRIENDS_VERSION;
770 - if ( isset( $_GET['page'] ) && 'page=friends-refresh' === $_GET['page'] ) {
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 + ) {
771 915 $feed->enable_cache( false );
772 916 } else {
773 917 $feed->set_cache_duration( 3590 );
774 918 }
@@ -795,8 +939,11 @@
795 939 * @param string $url The feed URL.
796 940 * @return array The available feeds.
797 941 */
798 942 public function discover_available_feeds( $url ) {
943 + if ( ! Friends::check_url( $url ) ) {
944 + return array();
945 + }
799 946 $available_feeds = array();
800 947 $content = null;
801 948 $content_type = 'text/html';
802 949
@@ -803,9 +950,9 @@
803 950 $response = wp_safe_remote_get(
804 951 $url,
805 952 array(
806 953 'timeout' => apply_filters( 'friends_http_timeout', 20 ),
807 - 'redirection' => 1,
954 + 'redirection' => 2,
808 955 )
809 956 );
810 957
811 958 if ( is_wp_error( $response ) ) {
@@ -812,9 +959,24 @@
812 959 $response->add_data( $url );
813 960 return $response;
814 961 }
815 962
816 - if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
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 ) {
817 979 $content = wp_remote_retrieve_body( $response );
818 980 $headers = wp_remote_retrieve_headers( $response );
819 981
820 982 // We'll determine the obvious feeds ourself.
@@ -821,20 +983,20 @@
821 983 $available_feeds = $this->discover_link_rel_feeds( $content, $url, $headers );
822 984 $content_type = strtok( $headers['content-type'], ';' );
823 985 }
824 986
825 - if ( $content ) {
826 - foreach ( $this->parsers as $slug => $parser ) {
827 - foreach ( $parser->discover_available_feeds( $content, $url ) as $link_url => $feed ) {
828 - if ( isset( $available_feeds[ $link_url ] ) ) {
829 - // If this parser tells us it can parse it right away, allow it to override.
830 - if ( isset( $available_feeds[ $link_url ]['parser'] ) || ! isset( $feed['parser'] ) ) {
831 - continue;
832 - }
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;
833 993 }
834 - $available_feeds[ $link_url ] = $feed;
835 - $available_feeds[ $link_url ]['url'] = $link_url;
994 + } else {
995 + $available_feeds[ $link_url ] = array();
836 996 }
997 + $available_feeds[ $link_url ] = array_merge( $available_feeds[ $link_url ], $feed );
998 + $available_feeds[ $link_url ]['url'] = $link_url;
837 999 }
838 1000 }
839 1001
840 1002 $has_friends_plugin = false;
@@ -876,9 +1038,8 @@
876 1038 'rel' => 'self',
877 1039 'title' => '',
878 1040 'parser' => 'unsupported',
879 1041 'parser_confidence' => 0,
880 -
881 1042 );
882 1043
883 1044 foreach ( $this->parsers as $slug => $parser ) {
884 1045 $confidence = $parser->feed_support_confidence( $url, $feed['type'], $feed['rel'], $content );
@@ -963,8 +1124,22 @@
963 1124 $available_feeds[ $link_url ]['autoselect'] = true;
964 1125 }
965 1126 }
966 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 +
967 1142 $feed_sort_order = array( 'self', 'alternate', 'me' );
968 1143 if ( $has_friends_plugin ) {
969 1144 // If we have the Friends plugin, we prefer an (augmented) RSS feed over, for example, microformats.
970 1145 $feed_sort_order = array( 'alternate', 'self', 'me' );
@@ -1010,75 +1185,92 @@
1010 1185 */
1011 1186 private function discover_link_rel_feeds( $content, $url, $headers ) {
1012 1187 $discovered_feeds = array();
1013 1188 $has_self = false;
1189 + $links = array();
1014 1190 $mf = Mf2\parse( $content, $url );
1191 +
1015 1192 if ( isset( $mf['rel-urls'] ) ) {
1016 - foreach ( $mf['rel-urls'] as $feed_url => $link ) {
1017 - foreach ( array( 'friends-base-url', 'me', 'alternate', 'self' ) as $rel ) {
1018 - if ( in_array( $rel, $link['rels'] ) ) {
1019 - $discovered_feeds[ $feed_url ] = array(
1020 - 'rel' => $rel,
1021 - );
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'] );
1022 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 + }
1023 1212 }
1213 + }
1214 + }
1024 1215
1025 - if ( ! isset( $discovered_feeds[ $feed_url ] ) ) {
1026 - continue;
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 + );
1027 1222 }
1223 + }
1028 1224
1029 - if ( 'self' === $rel ) {
1030 - $has_self = true;
1031 - }
1225 + if ( ! isset( $discovered_feeds[ $feed_url ] ) ) {
1226 + continue;
1227 + }
1032 1228
1033 - if ( isset( $link['type'] ) ) {
1034 - $discovered_feeds[ $feed_url ]['type'] = $link['type'];
1035 - }
1229 + if ( 'self' === $discovered_feeds[ $feed_url ]['rel'] ) {
1230 + $has_self = true;
1231 + }
1036 1232
1037 - if ( isset( $link['title'] ) ) {
1038 - $discovered_feeds[ $feed_url ]['title'] = $link['title'];
1039 - } elseif ( isset( $link['text'] ) ) {
1040 - $discovered_feeds[ $feed_url ]['title'] = $link['text'];
1041 - }
1233 + if ( isset( $link['type'] ) ) {
1234 + $discovered_feeds[ $feed_url ]['type'] = $link['type'];
1042 1235 }
1043 - }
1044 1236
1045 - if ( ! $has_self && class_exists( '\DOMXpath' ) ) {
1046 - // Convert to a DomDocument and silence the errors while doing so.
1047 - $doc = new \DomDocument();
1048 - set_error_handler( '__return_null' );
1049 - $doc->loadHTML( $content );
1050 - restore_error_handler();
1051 -
1052 - $xpath = new \DOMXpath( $doc );
1053 - if ( $xpath ) {
1054 - $discovered_feeds[ $url ] = array(
1055 - 'rel' => 'self',
1056 - 'title' => $xpath->query( '//title' )->item( 0 )->textContent,
1057 - );
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'];
1058 1241 }
1059 1242 }
1060 1243
1061 - return $discovered_feeds;
1062 - }
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 + }
1063 1263
1064 - /**
1065 - * Modify the main query for the friends feed
1066 - *
1067 - * @param \WP_Query $query The main query.
1068 - * @return \WP_Query The modified main query.
1069 - */
1070 - public function private_feed_query( \WP_Query $query ) {
1071 - if ( ! $this->friends->access_control->feed_is_authenticated() ) {
1072 - return $query;
1073 - }
1264 + if ( ! isset( $mf['rel-urls'][ $icon_url ]['type'] ) || 0 !== strpos( $mf['rel-urls'][ $icon_url ]['type'], 'image/' ) ) {
1265 + continue;
1266 + }
1074 1267
1075 - $friend_user = $this->friends->access_control->get_authenticated_feed_user();
1076 - if ( ! $query->is_admin && $query->is_feed && $friend_user->has_cap( 'friend' ) && ! $friend_user->has_cap( 'acquaintance' ) ) {
1077 - $query->set( 'post_status', array( 'publish', 'private' ) );
1268 + $discovered_feeds[ $url ]['avatar'] = $icon_url;
1269 + }
1078 1270 }
1079 1271
1080 - return $query;
1272 + return $discovered_feeds;
1081 1273 }
1082 1274
1083 1275 /**
1084 1276 * More generic version of the native url_to_postid()
@@ -1088,8 +1280,15 @@
1088 1280 * @return int Post ID, or 0 on failure.
1089 1281 */
1090 1282 public static function url_to_postid( $url, $author_id = false ) {
1091 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 + }
1092 1291 $args = $post_types;
1093 1292
1094 1293 global $wpdb;
1095 1294 $sql = sprintf(
@@ -1105,14 +1304,18 @@
1105 1304 $sql .= ' AND guid IN (%s, %s) LIMIT 1';
1106 1305 $args[] = $url;
1107 1306 $args[] = esc_attr( $url );
1108 1307
1109 - $post_id = $wpdb->get_var(
1308 + $post_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1110 1309 $wpdb->prepare(
1111 1310 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1112 1311 $args
1113 1312 )
1114 1313 );
1314 +
1315 + if ( $post_id ) {
1316 + wp_cache_set( $cache_key, $post_id, 'friends' );
1317 + }
1115 1318 return $post_id;
1116 1319 }
1117 1320
1118 1321 public function oembed_request_post_id( $post_id, $url ) {
@@ -1196,6 +1399,41 @@
1196 1399 }
1197 1400 $parsers[ $slug ] = $name;
1198 1401 }
1199 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 );
1200 1438 }
1201 1439 }