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 +396 -154 2.7.54.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' ) );
@@ -166,8 +182,12 @@
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' ) );
168 184 } else {
169 185 foreach ( $items as $key => $item ) {
186 + if ( is_wp_error( $item ) ) {
187 + unset( $items[ $key ] );
188 + continue;
189 + }
170 190 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, null );
171 191
172 192 if ( ! $item || $item->_feed_rule_delete ) {
173 193 unset( $items[ $key ] );
@@ -200,9 +220,9 @@
200 220 do_action( 'friends_retrieve_friends_error', $user_feed, $error, $friend_user );
201 221 return $error;
202 222 }
203 223 try {
204 - $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 );
205 225 } catch ( \Exception $e ) {
206 226 $items = new \WP_Error( $parser . '-failed', $e->getMessage() );
207 227 }
208 228
@@ -234,9 +254,9 @@
234 254 $fulltext = '';
235 255 foreach ( apply_filters( 'friends_keyword_search_fields', array( 'post_title', 'post_content' ) ) as $field ) {
236 256 $fulltext .= PHP_EOL . $post->$field;
237 257 }
238 - $fulltext = strip_tags( $fulltext );
258 + $fulltext = self::prepare_keyword_search_text( $fulltext );
239 259 foreach ( $keywords as $keyword ) {
240 260 if ( preg_match( '/' . str_replace( '/', '\\/', $keyword ) . '/ius', $fulltext ) ) {
241 261 $keyword_match = $keyword;
242 262 break;
@@ -243,9 +263,9 @@
243 263 }
244 264 }
245 265
246 266 if ( $keyword_match ) {
247 - $notified = apply_filters( 'notify_keyword_match_post', false, $post, $keyword_match );
267 + $notified = apply_filters( 'friends_notify_keyword_match_post', false, $post, $keyword_match );
248 268 if ( $notified ) {
249 269 continue;
250 270 }
251 271 }
@@ -250,14 +270,14 @@
250 270 }
251 271 }
252 272 }
253 273
254 - $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 );
255 275 if ( $notify_users ) {
256 276 if ( ! $post ) {
257 277 $post = get_post( intval( $post_id ) );
258 278 }
259 - do_action( 'notify_new_friend_post', $post, $user_feed );
279 + do_action( 'notify_new_friend_post', $post, $user_feed, $keyword_match );
260 280 }
261 281 }
262 282 }
263 283
@@ -263,18 +283,15 @@
263 283
264 284 /**
265 285 * Retrieve posts from all friends.
266 286 *
267 - * @param bool $force Whether to force retrieval.
287 + * @param bool $ignore_due_date Whether to get also undue feeds.
268 288 */
269 - public function retrieve_friend_posts( $force = false ) {
270 - 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();
271 292 $this->retrieve_feed( $feed );
272 293 $feed->was_polled();
273 - $friend_user = $feed->get_friend_user();
274 - if ( $friend_user ) {
275 - $friend_user->delete_outdated_posts();
276 - }
277 294 }
278 295 }
279 296
280 297 /**
@@ -284,9 +301,9 @@
284 301 * @param User_Feed $feed The feed.
285 302 * @param User $friend_user The friend user.
286 303 * @return Feed_Item The modified feed item.
287 304 */
288 - 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 ) {
289 306 $updated_item = $this->apply_feed_rules( $item, $feed, $friend_user );
290 307 if ( $updated_item->_feed_rule_delete ) {
291 308 return $updated_item;
292 309 }
@@ -300,9 +317,9 @@
300 317 * @param User_Feed $feed The feed object.
301 318 * @param User $friend_user The friend user.
302 319 * @return Feed_Item The modified feed item.
303 320 */
304 - 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 ) {
305 322 if ( is_null( $friend_user ) ) {
306 323 return $item;
307 324 }
308 325
@@ -307,8 +324,9 @@
307 324 }
308 325
309 326 $rules = $friend_user->get_feed_rules();
310 327 $action = $friend_user->get_feed_catch_all();
328 + $decided = false;
311 329
312 330 foreach ( $rules as $rule ) {
313 331 if ( $item instanceof \WP_Post ) {
314 332 $field = $this->get_feed_rule_field( $rule['field'] );
@@ -318,20 +336,31 @@
318 336 }
319 337 } else {
320 338 $field = $rule['field'];
321 339 }
322 -
323 - 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 ) ) {
324 349 if ( 'replace' === $rule['action'] ) {
325 350 $item->_feed_rule_transform = array(
326 - $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 ),
327 352 );
328 353 continue;
329 354 }
355 + if ( $decided ) {
356 + continue;
357 + }
330 358 $action = $rule['action'];
331 - break;
359 + $decided = true;
332 360 }
333 361 }
362 +
334 363 switch ( $action ) {
335 364 case 'delete':
336 365 $item->_feed_rule_delete = true;
337 366 return $item;
@@ -336,10 +365,16 @@
336 365 $item->_feed_rule_delete = true;
337 366 return $item;
338 367
339 368 case 'trash':
340 - $item->_feed_rule_transform = array(
341 - '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 + )
342 377 );
343 378 return $item;
344 379
345 380 case 'accept':
@@ -344,9 +379,8 @@
344 379
345 380 case 'accept':
346 381 return $item;
347 382 }
348 -
349 383 return $item;
350 384 }
351 385
352 386 /**
@@ -373,11 +407,16 @@
373 407 *
374 408 * @param array $rules The rules to validate.
375 409 * @return array The valid rules.
376 410 */
377 - public function validate_feed_rules( $rules ) {
411 + public static function validate_feed_rules( $rules ) {
378 412 if ( ! is_array( $rules ) ) {
379 - 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 + }
380 419 }
381 420
382 421 if ( isset( $rules['field'] ) && is_array( $rules['field'] ) ) {
383 422 // Transform POST values.
@@ -432,9 +471,9 @@
432 471 *
433 472 * @param array $catch_all The catch_all value to.
434 473 * @return array A valid catch_all
435 474 */
436 - public function validate_feed_catch_all( $catch_all ) {
475 + public static function validate_feed_catch_all( $catch_all ) {
437 476 if ( ! in_array( $catch_all, array( 'accept', 'trash', 'delete' ), true ) ) {
438 477 return 'accept';
439 478 }
440 479
@@ -441,8 +480,113 @@
441 480 return $catch_all;
442 481 }
443 482
444 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 + /**
445 589 * Gets the notification keywords.
446 590 *
447 591 * @return array The notification keywords.
448 592 */
@@ -481,9 +625,9 @@
481 625 * @param object $item The feed item.
482 626 *
483 627 * @return string The discovered post format.
484 628 */
485 - public function post_format_discovery( $item ) {
629 + public function post_format_discovery( $item ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
486 630 // Not implemented yet.
487 631 return 'standard';
488 632 }
489 633
@@ -512,9 +656,11 @@
512 656 */
513 657 public function process_incoming_feed_items( array $items, User_Feed $user_feed ) {
514 658 $friend_user = $user_feed->get_friend_user();
515 659 if ( ! $friend_user ) {
516 - 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 + }
517 663 return;
518 664 }
519 665 $remote_post_ids = $friend_user->get_remote_post_ids();
520 666 $post_formats = get_post_format_strings();
@@ -519,14 +665,26 @@
519 665 $remote_post_ids = $friend_user->get_remote_post_ids();
520 666 $post_formats = get_post_format_strings();
521 667 $feed_post_format = $user_feed->get_post_format();
522 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 +
523 680 // Limit this as a safety measure.
524 681 add_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
525 - $new_posts = array();
682 + $new_post_ids = array();
526 683 $modified_posts = array();
527 - foreach ( $items as $item ) {
528 - 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 ) {
529 687 continue;
530 688 }
531 689 $item->permalink = str_replace( array( '&#38;', '&#038;' ), '&', ent2ncr( wp_kses_normalize_entities( $item->permalink ) ) );
532 690
@@ -550,17 +708,17 @@
550 708 continue;
551 709 }
552 710
553 711 $post_id = null;
554 - if ( isset( $remote_post_ids[ $item->post_id ] ) ) {
712 + if ( $item->post_id && isset( $remote_post_ids[ $item->post_id ] ) ) {
555 713 $post_id = $remote_post_ids[ $item->post_id ];
556 714 }
557 - 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 ] ) ) {
558 716 $post_id = $remote_post_ids[ $item->permalink ];
559 717 }
560 718
561 719 if ( is_null( $post_id ) ) {
562 - $post_id = self::url_to_postid( $item->permalink, $friend_user->ID );
720 + $post_id = self::url_to_postid( $item->permalink );
563 721 }
564 722 $item->_is_new = is_null( $post_id );
565 723 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, $post_id );
566 724
@@ -594,12 +752,17 @@
594 752 foreach ( array( 'post_title', 'post_content', 'post_status' ) as $field ) {
595 753 if ( empty( $old_post->$field ) || empty( $post_data[ $field ] ) ) {
596 754 continue;
597 755 }
598 - if ( strip_tags( $old_post->$field ) !== strip_tags( $post_data[ $field ] ) ) {
756 + if ( 'post_content' === $field && $old_post->$field !== $post_data[ $field ] ) {
599 757 $modified_post_data[ $field ] = $post_data[ $field ];
600 758 break;
601 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 + }
602 765 }
603 766
604 767 if ( ! empty( $modified_post_data ) && apply_filters( 'friends_can_update_modified_feed_posts', true, $item, $user_feed, $friend_user, $post_id ) ) {
605 768 $was_modified_by_user = false;
@@ -609,9 +772,9 @@
609 772 break;
610 773 }
611 774 }
612 775
613 - if ( ! $was_modified_by_user ) {
776 + if ( ! $was_modified_by_user || $manual_refresh ) {
614 777 $modified_post_data['ID'] = $post_id;
615 778 if ( isset( $modified_post_data['post_content'] ) ) {
616 779 $modified_post_data['post_content'] = str_replace( '\\', '\\\\', $modified_post_data['post_content'] );
617 780 }
@@ -617,10 +780,11 @@
617 780 }
618 781 if ( intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
619 782 $modified_post_data['comment_count'] = $item->comment_count;
620 783 }
784 +
621 785 wp_update_post( $modified_post_data );
622 - $modified_posts[] = $post_id;
786 + $modified_posts[ $item_key ] = $post_id;
623 787 }
624 788 }
625 789 } else {
626 790 $post_data['post_type'] = Friends::CPT;
@@ -632,9 +796,9 @@
632 796 if ( is_wp_error( $post_id ) ) {
633 797 continue;
634 798 }
635 799
636 - $new_posts[] = $post_id;
800 + $new_post_ids[ $item_key ] = $post_id;
637 801
638 802 $remote_post_ids[ $item->permalink ] = $post_id;
639 803 }
640 804
@@ -640,9 +804,9 @@
640 804
641 805 if ( is_null( $old_post ) || intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
642 806 // The comment_count needs to be updated manually since it doesn't represent real comments in the database.
643 807 global $wpdb;
644 - $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
645 809 wp_cache_delete( "comments-{$post_id}", 'counts' );
646 810 clean_post_cache( $post_id );
647 811 do_action( 'wp_update_comment_count', $post_id, $item->comment_count, $old_post ? $old_post->comment_count : 0 );
648 812 }
@@ -671,50 +835,39 @@
671 835 if ( is_numeric( $item->post_id ) ) {
672 836 update_post_meta( $post_id, 'remote_post_id', $item->{'post-id'} );
673 837 }
674 838
675 - 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 + }
676 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 +
677 847 update_post_meta( $post_id, 'parser', $user_feed->get_parser() );
678 848 update_post_meta( $post_id, 'feed_url', $user_feed->get_url() );
679 849
680 850 global $wpdb;
681 - $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
682 852 }
683 853 remove_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
684 854
685 - $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 );
686 857
687 - 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 );
688 859
689 - return $new_posts;
690 - }
860 + do_action( 'friends_retrieved_new_posts', $user_feed, $new_post_ids, $modified_posts, $friend_user );
691 861
692 - /**
693 - * Remove the Private: when sending a private feed.
694 - *
695 - * @param string $title_format The title format for a private post title.
696 - * @return string The modified title format for a private post title.
697 - */
698 - public function private_title_format( $title_format ) {
699 - if ( $this->friends->access_control->feed_is_authenticated() ) {
700 - 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 + }
701 867 }
702 - return $title_format;
703 - }
704 868
705 - /**
706 - * Disable excerpted feeds for friend feeds
707 - *
708 - * @param boolean $feed_use_excerpt Whether to only have excerpts in feeds.
709 - * @return boolean The modified flag whether to have excerpts in feeds.
710 - */
711 - public function feed_use_excerpt( $feed_use_excerpt ) {
712 - if ( $this->friends->access_control->feed_is_authenticated() ) {
713 - return 0;
714 - }
715 -
716 - return $feed_use_excerpt;
869 + return $new_posts;
717 870 }
718 871
719 872 /**
720 873 * Output an additional XMLNS for the feed.
@@ -732,17 +885,8 @@
732 885 if ( empty( $post_format ) ) {
733 886 $post_format = 'standard';
734 887 }
735 888 echo '<friends:post-format>' . esc_html( $post_format ) . '</friends:post-format>' . PHP_EOL;
736 -
737 - $authenticated_user_id = $this->friends->access_control->feed_is_authenticated();
738 - if ( ! $authenticated_user_id ) {
739 - return;
740 - }
741 -
742 - echo '<friends:gravatar>' . esc_html( get_avatar_url( $post->post_author ) ) . '</friends:gravatar>' . PHP_EOL;
743 - echo '<friends:post-status>' . esc_html( $post->post_status ) . '</friends:post-status>' . PHP_EOL;
744 - echo '<friends:post-id>' . esc_html( $post->ID ) . '</friends:post-id>' . PHP_EOL;
745 889 }
746 890
747 891 /**
748 892 * Redirect
@@ -747,13 +891,14 @@
747 891 /**
748 892 * Redirect
749 893 */
750 894 public function friends_add_friend_redirect() {
895 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
751 896 if ( ! isset( $_GET['add-friend'] ) || isset( $_GET['page'] ) ) {
752 897 return;
753 898 }
754 -
755 - 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' ) ) );
756 901 exit;
757 902 }
758 903
759 904 /**
@@ -762,9 +907,12 @@
762 907 * @param SimplePie $feed The SimplePie object.
763 908 */
764 909 public function wp_feed_options( $feed ) {
765 910 $feed->useragent .= ' Friends/' . FRIENDS_VERSION;
766 - 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 + ) {
767 915 $feed->enable_cache( false );
768 916 } else {
769 917 $feed->set_cache_duration( 3590 );
770 918 }
@@ -791,8 +939,11 @@
791 939 * @param string $url The feed URL.
792 940 * @return array The available feeds.
793 941 */
794 942 public function discover_available_feeds( $url ) {
943 + if ( ! Friends::check_url( $url ) ) {
944 + return array();
945 + }
795 946 $available_feeds = array();
796 947 $content = null;
797 948 $content_type = 'text/html';
798 949
@@ -799,9 +950,9 @@
799 950 $response = wp_safe_remote_get(
800 951 $url,
801 952 array(
802 953 'timeout' => apply_filters( 'friends_http_timeout', 20 ),
803 - 'redirection' => 1,
954 + 'redirection' => 2,
804 955 )
805 956 );
806 957
807 958 if ( is_wp_error( $response ) ) {
@@ -808,9 +959,24 @@
808 959 $response->add_data( $url );
809 960 return $response;
810 961 }
811 962
812 - 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 ) {
813 979 $content = wp_remote_retrieve_body( $response );
814 980 $headers = wp_remote_retrieve_headers( $response );
815 981
816 982 // We'll determine the obvious feeds ourself.
@@ -817,20 +983,20 @@
817 983 $available_feeds = $this->discover_link_rel_feeds( $content, $url, $headers );
818 984 $content_type = strtok( $headers['content-type'], ';' );
819 985 }
820 986
821 - if ( $content ) {
822 - foreach ( $this->parsers as $slug => $parser ) {
823 - foreach ( $parser->discover_available_feeds( $content, $url ) as $link_url => $feed ) {
824 - if ( isset( $available_feeds[ $link_url ] ) ) {
825 - // If this parser tells us it can parse it right away, allow it to override.
826 - if ( isset( $available_feeds[ $link_url ]['parser'] ) || ! isset( $feed['parser'] ) ) {
827 - continue;
828 - }
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;
829 993 }
830 - $available_feeds[ $link_url ] = $feed;
831 - $available_feeds[ $link_url ]['url'] = $link_url;
994 + } else {
995 + $available_feeds[ $link_url ] = array();
832 996 }
997 + $available_feeds[ $link_url ] = array_merge( $available_feeds[ $link_url ], $feed );
998 + $available_feeds[ $link_url ]['url'] = $link_url;
833 999 }
834 1000 }
835 1001
836 1002 $has_friends_plugin = false;
@@ -872,9 +1038,8 @@
872 1038 'rel' => 'self',
873 1039 'title' => '',
874 1040 'parser' => 'unsupported',
875 1041 'parser_confidence' => 0,
876 -
877 1042 );
878 1043
879 1044 foreach ( $this->parsers as $slug => $parser ) {
880 1045 $confidence = $parser->feed_support_confidence( $url, $feed['type'], $feed['rel'], $content );
@@ -959,8 +1124,22 @@
959 1124 $available_feeds[ $link_url ]['autoselect'] = true;
960 1125 }
961 1126 }
962 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 +
963 1142 $feed_sort_order = array( 'self', 'alternate', 'me' );
964 1143 if ( $has_friends_plugin ) {
965 1144 // If we have the Friends plugin, we prefer an (augmented) RSS feed over, for example, microformats.
966 1145 $feed_sort_order = array( 'alternate', 'self', 'me' );
@@ -1006,75 +1185,92 @@
1006 1185 */
1007 1186 private function discover_link_rel_feeds( $content, $url, $headers ) {
1008 1187 $discovered_feeds = array();
1009 1188 $has_self = false;
1189 + $links = array();
1010 1190 $mf = Mf2\parse( $content, $url );
1191 +
1011 1192 if ( isset( $mf['rel-urls'] ) ) {
1012 - foreach ( $mf['rel-urls'] as $feed_url => $link ) {
1013 - foreach ( array( 'friends-base-url', 'me', 'alternate', 'self' ) as $rel ) {
1014 - if ( in_array( $rel, $link['rels'] ) ) {
1015 - $discovered_feeds[ $feed_url ] = array(
1016 - 'rel' => $rel,
1017 - );
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'] );
1018 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 + }
1019 1212 }
1213 + }
1214 + }
1020 1215
1021 - if ( ! isset( $discovered_feeds[ $feed_url ] ) ) {
1022 - 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 + );
1023 1222 }
1223 + }
1024 1224
1025 - if ( 'self' === $rel ) {
1026 - $has_self = true;
1027 - }
1225 + if ( ! isset( $discovered_feeds[ $feed_url ] ) ) {
1226 + continue;
1227 + }
1028 1228
1029 - if ( isset( $link['type'] ) ) {
1030 - $discovered_feeds[ $feed_url ]['type'] = $link['type'];
1031 - }
1229 + if ( 'self' === $discovered_feeds[ $feed_url ]['rel'] ) {
1230 + $has_self = true;
1231 + }
1032 1232
1033 - if ( isset( $link['title'] ) ) {
1034 - $discovered_feeds[ $feed_url ]['title'] = $link['title'];
1035 - } elseif ( isset( $link['text'] ) ) {
1036 - $discovered_feeds[ $feed_url ]['title'] = $link['text'];
1037 - }
1233 + if ( isset( $link['type'] ) ) {
1234 + $discovered_feeds[ $feed_url ]['type'] = $link['type'];
1038 1235 }
1039 - }
1040 1236
1041 - if ( ! $has_self && class_exists( '\DOMXpath' ) ) {
1042 - // Convert to a DomDocument and silence the errors while doing so.
1043 - $doc = new \DomDocument;
1044 - set_error_handler( '__return_null' );
1045 - $doc->loadHTML( $content );
1046 - restore_error_handler();
1047 -
1048 - $xpath = new \DOMXpath( $doc );
1049 - if ( $xpath ) {
1050 - $discovered_feeds[ $url ] = array(
1051 - 'rel' => 'self',
1052 - 'title' => $xpath->query( '//title' )->item( 0 )->textContent,
1053 - );
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'];
1054 1241 }
1055 1242 }
1056 1243
1057 - return $discovered_feeds;
1058 - }
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 + }
1059 1263
1060 - /**
1061 - * Modify the main query for the friends feed
1062 - *
1063 - * @param \WP_Query $query The main query.
1064 - * @return \WP_Query The modified main query.
1065 - */
1066 - public function private_feed_query( \WP_Query $query ) {
1067 - if ( ! $this->friends->access_control->feed_is_authenticated() ) {
1068 - return $query;
1069 - }
1264 + if ( ! isset( $mf['rel-urls'][ $icon_url ]['type'] ) || 0 !== strpos( $mf['rel-urls'][ $icon_url ]['type'], 'image/' ) ) {
1265 + continue;
1266 + }
1070 1267
1071 - $friend_user = $this->friends->access_control->get_authenticated_feed_user();
1072 - if ( ! $query->is_admin && $query->is_feed && $friend_user->has_cap( 'friend' ) && ! $friend_user->has_cap( 'acquaintance' ) ) {
1073 - $query->set( 'post_status', array( 'publish', 'private' ) );
1268 + $discovered_feeds[ $url ]['avatar'] = $icon_url;
1269 + }
1074 1270 }
1075 1271
1076 - return $query;
1272 + return $discovered_feeds;
1077 1273 }
1078 1274
1079 1275 /**
1080 1276 * More generic version of the native url_to_postid()
@@ -1084,8 +1280,15 @@
1084 1280 * @return int Post ID, or 0 on failure.
1085 1281 */
1086 1282 public static function url_to_postid( $url, $author_id = false ) {
1087 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 + }
1088 1291 $args = $post_types;
1089 1292
1090 1293 global $wpdb;
1091 1294 $sql = sprintf(
@@ -1101,14 +1304,18 @@
1101 1304 $sql .= ' AND guid IN (%s, %s) LIMIT 1';
1102 1305 $args[] = $url;
1103 1306 $args[] = esc_attr( $url );
1104 1307
1105 - $post_id = $wpdb->get_var(
1308 + $post_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1106 1309 $wpdb->prepare(
1107 1310 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1108 1311 $args
1109 1312 )
1110 1313 );
1314 +
1315 + if ( $post_id ) {
1316 + wp_cache_set( $cache_key, $post_id, 'friends' );
1317 + }
1111 1318 return $post_id;
1112 1319 }
1113 1320
1114 1321 public function oembed_request_post_id( $post_id, $url ) {
@@ -1192,6 +1399,41 @@
1192 1399 }
1193 1400 $parsers[ $slug ] = $name;
1194 1401 }
1195 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 );
1196 1438 }
1197 1439 }