PluginProbe
Friends / 2.9.1
Friends v2.9.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 2.9.1, at includes/class-feed.php

1,209 lines 35.4 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( '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 add_filter( 'friends_early_modify_feed_item', array( $this, 'apply_early_feed_rules' ), 10, 3 );
63 add_filter( 'friends_modify_feed_item', array( $this, 'apply_feed_rules' ), 10, 3 );
64
65 add_action( 'rss_item', array( $this, 'feed_additional_fields' ) );
66 add_action( 'rss2_item', array( $this, 'feed_additional_fields' ) );
67 add_action( 'rss_ns', array( $this, 'additional_feed_namespaces' ) );
68 add_action( 'rss2_ns', array( $this, 'additional_feed_namespaces' ) );
69
70 add_action( 'cron_friends_refresh_feeds', array( $this, 'cron_friends_refresh_feeds' ) );
71 add_action( 'friends_retrieve_user_feeds', array( $this, 'friends_retrieve_user_feeds' ) );
72
73 add_action( 'wp_loaded', array( $this, 'friends_add_friend_redirect' ), 100 );
74 add_action( 'wp_feed_options', array( $this, 'wp_feed_options' ), 90 );
75
76 add_action( 'wp_insert_post', array( $this, 'invalidate_post_count_cache' ), 10, 2 );
77 add_action( 'oembed_request_post_id', array( $this, 'oembed_request_post_id' ), 10, 2 );
78 add_action( 'post_embed_url', array( $this, 'post_embed_url' ), 10, 2 );
79 }
80
81 /**
82 * Allow registering a parser
83 *
84 * @param string $slug The slug.
85 * @param Feed_Parser $parser The parser that extends the Feed_Parser class.
86 */
87 public function register_parser( $slug, Feed_Parser $parser ) {
88 if ( in_array( $slug, $this->reserved_parser_slugs, true ) ) {
89 // translators: %s is the slug of a parser.
90 return new \WP_Error( 'resevered-slug', sprintf( __( 'The slug "%s" cannot be used.', 'friends' ), $slug ) );
91 }
92 if ( isset( $this->parsers[ $slug ] ) ) {
93 // translators: %s is the slug of a parser.
94 return new \WP_Error( 'parser-already-registered', sprintf( __( 'There is already a parser registered with the slug "%s".', 'friends' ), $slug ) );
95 }
96 $this->parsers[ $slug ] = $parser;
97
98 return true;
99 }
100
101 /**
102 * Allow unregistering a parser
103 *
104 * @param string $slug The slug.
105 */
106 public function unregister_parser( $slug ) {
107 if ( ! in_array( $slug, $this->reserved_parser_slugs, true ) ) {
108 unset( $this->parsers[ $slug ] );
109 }
110 }
111
112 /**
113 * Cron function to refresh the feeds of the friends' blogs
114 */
115 public function cron_friends_refresh_feeds() {
116 $this->retrieve_friend_posts();
117 }
118
119 /**
120 * Function to fetch a users feeds.
121 *
122 * @param int $user_id The user id.
123 */
124 public function friends_retrieve_user_feeds( $user_id ) {
125 $friend_user = User::get_user_by_id( $user_id );
126 if ( ! $friend_user ) {
127 return;
128 }
129
130 foreach ( $friend_user->get_active_feeds() as $feed ) {
131 $friend_user = $feed->get_friend_user();
132 if ( $friend_user ) {
133 $this->retrieve_feed( $feed );
134 $feed->was_polled();
135 $friend_user->delete_outdated_posts();
136 }
137 }
138 }
139
140 /**
141 * Preview a URL using a parser.
142 *
143 * @param string $parser The parser slug.
144 * @param string $url The url.
145 * @param int $feed_id The feed id.
146 *
147 * @return array|\WP_error The feed items.
148 */
149 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' ) );
152 }
153
154 $user_feed = null;
155 $friend_user = null;
156 if ( ! is_null( $feed_id ) ) {
157 $user_feed = User_Feed::get_by_id( $feed_id );
158 if ( ! is_wp_error( $user_feed ) ) {
159 $friend_user = $user_feed->get_friend_user();
160 }
161 }
162
163 $items = $this->parsers[ $parser ]->fetch_feed( $url, $user_feed );
164
165 if ( ! is_wp_error( $items ) ) {
166 if ( empty( $items ) ) {
167 $items = new \WP_Error( 'empty-feed', __( "This feed doesn't contain any entries. There might be a problem parsing the feed.", 'friends' ) );
168 } else {
169 foreach ( $items as $key => $item ) {
170 if ( is_wp_error( $item ) ) {
171 unset( $items[ $key ] );
172 continue;
173 }
174 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, null );
175
176 if ( ! $item || $item->_feed_rule_delete ) {
177 unset( $items[ $key ] );
178 continue;
179 }
180 if ( ! empty( $item->_feed_rule_transform ) && is_array( $item->_feed_rule_transform ) ) {
181 if ( isset( $item->_feed_rule_transform['post_content'] ) ) {
182 $items[ $key ]->content = $item->_feed_rule_transform['post_content'];
183 }
184 }
185 }
186 }
187 }
188
189 return $items;
190 }
191
192 /**
193 * Retrieves a user feed.
194 *
195 * @param User_Feed $user_feed The user feed.
196 *
197 * @return array|\WP_Error The retrieved items.
198 */
199 public function retrieve_feed( User_Feed $user_feed ) {
200 $friend_user = $user_feed->get_friend_user();
201 $parser = $user_feed->get_parser();
202 if ( ! isset( $this->parsers[ $parser ] ) ) {
203 $error = new \WP_Error( 'unknown-parser', __( 'An unknown parser name was supplied.', 'friends' ) );
204 do_action( 'friends_retrieve_friends_error', $user_feed, $error, $friend_user );
205 return $error;
206 }
207 try {
208 $items = $this->parsers[ $parser ]->fetch_feed( $user_feed->get_private_url(), $user_feed );
209 } catch ( \Exception $e ) {
210 $items = new \WP_Error( $parser . '-failed', $e->getMessage() );
211 }
212
213 if ( is_wp_error( $items ) ) {
214 do_action( 'friends_retrieve_friends_error', $user_feed, $items, $friend_user );
215 return $items;
216 }
217
218 $new_posts = $this->process_incoming_feed_items( $items, $user_feed );
219
220 return $new_posts;
221 }
222
223 /**
224 * Notify users about new posts of this friend
225 *
226 * @param User $friend_user The friend.
227 * @param array $new_posts The new posts of this friend.
228 * @param User_Feed $user_feed The user feed.
229 */
230 public function notify_about_new_posts( User $friend_user, $new_posts, User_Feed $user_feed ) {
231 $keywords = self::get_active_notification_keywords();
232 foreach ( $new_posts as $post_id ) {
233 $post = false;
234
235 $keyword_match = false;
236 if ( $keywords ) {
237 $post = get_post( intval( $post_id ) );
238 $fulltext = '';
239 foreach ( apply_filters( 'friends_keyword_search_fields', array( 'post_title', 'post_content' ) ) as $field ) {
240 $fulltext .= PHP_EOL . $post->$field;
241 }
242 $fulltext = wp_strip_all_tags( $fulltext );
243 foreach ( $keywords as $keyword ) {
244 if ( preg_match( '/' . str_replace( '/', '\\/', $keyword ) . '/ius', $fulltext ) ) {
245 $keyword_match = $keyword;
246 break;
247 }
248 }
249
250 if ( $keyword_match ) {
251 $notified = apply_filters( 'notify_keyword_match_post', false, $post, $keyword_match );
252 if ( $notified ) {
253 continue;
254 }
255 }
256 }
257
258 $notify_users = apply_filters( 'notify_about_new_friend_post', true, $friend_user, $post_id, $user_feed );
259 if ( $notify_users ) {
260 if ( ! $post ) {
261 $post = get_post( intval( $post_id ) );
262 }
263 do_action( 'notify_new_friend_post', $post, $user_feed );
264 }
265 }
266 }
267
268 /**
269 * Retrieve posts from all friends.
270 *
271 * @param bool $force Whether to force retrieval.
272 */
273 public function retrieve_friend_posts( $force = false ) {
274 foreach ( User_Feed::get_all_due() as $feed ) {
275 $this->retrieve_feed( $feed );
276 $feed->was_polled();
277 $friend_user = $feed->get_friend_user();
278 if ( $friend_user ) {
279 $friend_user->delete_outdated_posts();
280 }
281 }
282 }
283
284 /**
285 * Apply the feed rules that need to be applied early.
286 *
287 * @param Feed_Item $item The feed item.
288 * @param User_Feed $feed The feed.
289 * @param User $friend_user The friend user.
290 * @return Feed_Item The modified feed item.
291 */
292 public function apply_early_feed_rules( $item, User_Feed $feed = null, User $friend_user = null ) {
293 $updated_item = $this->apply_feed_rules( $item, $feed, $friend_user );
294 if ( $updated_item->_feed_rule_delete ) {
295 return $updated_item;
296 }
297 return $item;
298 }
299
300 /**
301 * Apply the feed rules
302 *
303 * @param Feed_Item $item The feed item.
304 * @param User_Feed $feed The feed object.
305 * @param User $friend_user The friend user.
306 * @return Feed_Item The modified feed item.
307 */
308 public function apply_feed_rules( $item, User_Feed $feed = null, User $friend_user = null ) {
309 if ( is_null( $friend_user ) ) {
310 return $item;
311 }
312
313 $rules = $friend_user->get_feed_rules();
314 $action = $friend_user->get_feed_catch_all();
315
316 foreach ( $rules as $rule ) {
317 if ( $item instanceof \WP_Post ) {
318 $field = $this->get_feed_rule_field( $rule['field'] );
319
320 if ( 'author' === $rule['field'] ) {
321 $item->$field = get_post_meta( $item->ID, 'author', true );
322 }
323 } else {
324 $field = $rule['field'];
325 }
326
327 if ( $item->$field && preg_match( '/' . str_replace( '/', '\\/', $rule['regex'] ) . '/ius', $item->$field ) ) {
328 if ( 'replace' === $rule['action'] ) {
329 $item->_feed_rule_transform = array(
330 $this->get_feed_rule_field( $rule['field'], $item ) => preg_replace( '/' . $rule['regex'] . '/iu', $rule['replace'], $item->$field ),
331 );
332 continue;
333 }
334 $action = $rule['action'];
335 break;
336 }
337 }
338 switch ( $action ) {
339 case 'delete':
340 $item->_feed_rule_delete = true;
341 return $item;
342
343 case 'trash':
344 $item->_feed_rule_transform = array(
345 'post_status' => 'trash',
346 );
347 return $item;
348
349 case 'accept':
350 return $item;
351 }
352
353 return $item;
354 }
355
356 /**
357 * Get the field name for the feed item.
358 *
359 * @param string $field The field name.
360 * @return string The adapted field name.
361 */
362 private function get_feed_rule_field( $field ) {
363 switch ( $field ) {
364 case 'title':
365 return 'post_title';
366 case 'permalink':
367 return 'guid';
368 case 'content':
369 return 'post_content';
370 }
371
372 return $field;
373 }
374
375 /**
376 * Validate feed item rules
377 *
378 * @param array $rules The rules to validate.
379 * @return array The valid rules.
380 */
381 public function validate_feed_rules( $rules ) {
382 if ( ! is_array( $rules ) ) {
383 return array();
384 }
385
386 if ( isset( $rules['field'] ) && is_array( $rules['field'] ) ) {
387 // Transform POST values.
388 $transformed_rules = array();
389 foreach ( array_keys( $rules['field'] ) as $key ) {
390 $rule = array();
391 foreach ( $rules as $part => $keys ) {
392 if ( isset( $keys[ $key ] ) ) {
393 $rule[ $part ] = stripslashes( $keys[ $key ] );
394 }
395 }
396 $transformed_rules[] = $rule;
397 }
398 $rules = $transformed_rules;
399 }
400
401 foreach ( $rules as $k => $rule ) {
402 if ( ! isset( $rule['field'] ) || ! in_array( $rule['field'], array( 'title', 'content', 'permalink', 'author' ), true ) ) {
403 unset( $rules[ $k ] );
404 continue;
405 }
406
407 if ( ! isset( $rule['regex'] ) || ! is_string( $rule['regex'] ) || '' === trim( $rule['regex'] ) ) {
408 unset( $rules[ $k ] );
409 continue;
410 }
411
412 $rules[ $k ]['regex'] = substr( $rule['regex'], 0, 10240 );
413
414 if ( ! isset( $rule['action'] ) || ! in_array( $rule['action'], array( 'accept', 'trash', 'delete', 'replace' ), true ) ) {
415 unset( $rules[ $k ] );
416 continue;
417 }
418
419 if ( 'replace' === $rule['action'] ) {
420 if ( ! isset( $rule['replace'] ) || ! is_string( $rule['replace'] ) ) {
421 unset( $rules[ $k ] );
422 continue;
423 }
424
425 $rules[ $k ]['replace'] = substr( $rule['replace'], 0, 10240 );
426 } else {
427 unset( $rules[ $k ]['replace'] );
428 }
429 }
430
431 return $rules;
432 }
433
434 /**
435 * Validate feed catch_all
436 *
437 * @param array $catch_all The catch_all value to.
438 * @return array A valid catch_all
439 */
440 public function validate_feed_catch_all( $catch_all ) {
441 if ( ! in_array( $catch_all, array( 'accept', 'trash', 'delete' ), true ) ) {
442 return 'accept';
443 }
444
445 return $catch_all;
446 }
447
448 /**
449 * Gets the notification keywords.
450 *
451 * @return array The notification keywords.
452 */
453 public static function get_active_notification_keywords() {
454 $active_keywords = array();
455 foreach ( self::get_all_notification_keywords() as $entry ) {
456 if ( $entry['enabled'] ) {
457 $active_keywords[] = $entry['keyword'];
458 }
459 }
460
461 return $active_keywords;
462 }
463
464 /**
465 * Gets all notification keywords.
466 *
467 * @return array All notification keywords.
468 */
469 public static function get_all_notification_keywords() {
470 $keywords = get_option( 'friends_notification_keywords', false );
471 if ( false === $keywords ) {
472 // Default keywords.
473 $keywords = array();
474 $keywords[] = array(
475 'enabled' => false,
476 'keyword' => trim( preg_replace( '#^https?:#', '', home_url() ), '/' ),
477 );
478 }
479 return $keywords;
480 }
481
482 /**
483 * Discover the post format for the item.
484 *
485 * @param object $item The feed item.
486 *
487 * @return string The discovered post format.
488 */
489 public function post_format_discovery( $item ) {
490 // Not implemented yet.
491 return 'standard';
492 }
493
494 /**
495 * Return the number of revisions to keep.
496 *
497 * @return int The number of revisions to keep.
498 */
499 public function revisions_to_keep() {
500 return 10;
501 }
502
503 public function dissallowed_html( $allowedposttags, $context ) {
504 if ( 'post' === $context ) {
505 unset( $allowedposttags['article'] );
506 }
507 return $allowedposttags;
508 }
509
510 /**
511 * Process incoming feed items
512 *
513 * @param array $items The incoming items.
514 * @param User_Feed $user_feed The feed to which these items belong.
515 * @return array The post ids of the new posts.
516 */
517 public function process_incoming_feed_items( array $items, User_Feed $user_feed ) {
518 $friend_user = $user_feed->get_friend_user();
519 if ( ! $friend_user ) {
520 error_log( var_export( $user_feed, true ) );
521 return;
522 }
523 $remote_post_ids = $friend_user->get_remote_post_ids();
524 $post_formats = get_post_format_strings();
525 $feed_post_format = $user_feed->get_post_format();
526
527 // Limit this as a safety measure.
528 add_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
529 $new_posts = array();
530 $modified_posts = array();
531 foreach ( $items as $item ) {
532 if ( ! $item->permalink ) {
533 continue;
534 }
535 $item->permalink = str_replace( array( '&#38;', '&#038;' ), '&', ent2ncr( wp_kses_normalize_entities( $item->permalink ) ) );
536
537 if ( empty( $item->post_content ) ) {
538 $item->post_content = '';
539 }
540
541 add_filter( 'wp_kses_allowed_html', array( $this, 'dissallowed_html' ), 10, 2 );
542 $item->post_content = wp_kses_post( trim( $item->post_content ) );
543 remove_filter( 'wp_kses_allowed_html', array( $this, 'dissallowed_html' ), 10 );
544
545 $item = apply_filters( 'friends_early_modify_feed_item', $item, $user_feed, $friend_user );
546 if ( ! $item || $item->_feed_rule_delete ) {
547 continue;
548 }
549
550 // Fallback, when no friends plugin is installed.
551 $item->post_id = $item->permalink;
552 $item->post_status = 'publish';
553 if ( ( ! $item->post_content && ! $item->title ) || ! $item->permalink ) {
554 continue;
555 }
556
557 $post_id = null;
558 if ( isset( $remote_post_ids[ $item->post_id ] ) ) {
559 $post_id = $remote_post_ids[ $item->post_id ];
560 }
561 if ( is_null( $post_id ) && isset( $remote_post_ids[ $item->permalink ] ) ) {
562 $post_id = $remote_post_ids[ $item->permalink ];
563 }
564
565 if ( is_null( $post_id ) ) {
566 $post_id = self::url_to_postid( $item->permalink, $friend_user->ID );
567 }
568 $item->_is_new = is_null( $post_id );
569 $item = apply_filters( 'friends_modify_feed_item', $item, $user_feed, $friend_user, $post_id );
570
571 $updated_date = $item->date;
572 if ( ! empty( $item->updated_date ) ) {
573 $updated_date = $item->updated_date;
574 }
575
576 if ( empty( $item->title ) ) {
577 $item->title = '';
578 }
579
580 $post_data = array(
581 'post_title' => $item->title,
582 'post_content' => force_balance_tags( $item->post_content ),
583 'post_modified_gmt' => $updated_date,
584 'post_status' => $item->post_status,
585 'guid' => $item->permalink,
586 'comment_count' => $item->comment_count,
587 );
588
589 // Modified via feed rules.
590 if ( ! empty( $item->_feed_rule_transform ) && is_array( $item->_feed_rule_transform ) ) {
591 $post_data = array_merge( $post_data, $item->_feed_rule_transform );
592 }
593
594 $old_post = null;
595 if ( ! is_null( $post_id ) ) {
596 $old_post = get_post( $post_id );
597 $modified_post_data = array();
598 foreach ( array( 'post_title', 'post_content', 'post_status' ) as $field ) {
599 if ( empty( $old_post->$field ) || empty( $post_data[ $field ] ) ) {
600 continue;
601 }
602 if ( wp_strip_all_tags( $old_post->$field ) !== wp_strip_all_tags( $post_data[ $field ] ) ) {
603 $modified_post_data[ $field ] = $post_data[ $field ];
604 break;
605 }
606 }
607
608 if ( ! empty( $modified_post_data ) && apply_filters( 'friends_can_update_modified_feed_posts', true, $item, $user_feed, $friend_user, $post_id ) ) {
609 $was_modified_by_user = false;
610 foreach ( wp_get_post_revisions( $post_id ) as $revision ) {
611 if ( intval( $revision->post_author ) && intval( $revision->post_author ) !== $friend_user->ID ) {
612 $was_modified_by_user = true;
613 break;
614 }
615 }
616
617 if ( ! $was_modified_by_user ) {
618 $modified_post_data['ID'] = $post_id;
619 if ( isset( $modified_post_data['post_content'] ) ) {
620 $modified_post_data['post_content'] = str_replace( '\\', '\\\\', $modified_post_data['post_content'] );
621 }
622 if ( intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
623 $modified_post_data['comment_count'] = $item->comment_count;
624 }
625 wp_update_post( $modified_post_data );
626 $modified_posts[] = $post_id;
627 }
628 }
629 } else {
630 $post_data['post_type'] = Friends::CPT;
631 $post_data['post_date_gmt'] = $item->date;
632 $post_data['post_content'] = str_replace( '\\', '\\\\', $post_data['post_content'] );
633 $post_data['meta_input'] = $item->meta;
634
635 $post_id = $friend_user->insert_post( $post_data, true );
636 if ( is_wp_error( $post_id ) ) {
637 continue;
638 }
639
640 $new_posts[] = $post_id;
641
642 $remote_post_ids[ $item->permalink ] = $post_id;
643 }
644
645 if ( is_null( $old_post ) || intval( $old_post->comment_count ) !== intval( $item->comment_count ) ) {
646 // The comment_count needs to be updated manually since it doesn't represent real comments in the database.
647 global $wpdb;
648 $wpdb->update( $wpdb->posts, array( 'comment_count' => $item->comment_count ), array( 'ID' => $post_id ) );
649 wp_cache_delete( "comments-{$post_id}", 'counts' );
650 clean_post_cache( $post_id );
651 do_action( 'wp_update_comment_count', $post_id, $item->comment_count, $old_post ? $old_post->comment_count : 0 );
652 }
653
654 if ( $item->comments_feed && ( is_null( $old_post ) || $old_post->comments_feed !== $item->comments_feed ) ) {
655 update_post_meta( $post_id, self::COMMENTS_FEED_META, $item->comments_feed, $old_post ? $old_post->comments_feed : '' );
656 }
657
658 $post_format = $feed_post_format;
659 if ( 'autodetect' === $post_format ) {
660 if ( isset( $item->post_format ) && isset( $post_formats[ $item->post_format ] ) ) {
661 $post_format = $item->post_format;
662 } else {
663 $post_format = $this->post_format_discovery( $item );
664 }
665 }
666
667 if ( $post_format ) {
668 set_post_format( $post_id, $post_format );
669 }
670
671 if ( $item->author ) {
672 update_post_meta( $post_id, 'author', $item->author );
673 }
674
675 if ( is_numeric( $item->post_id ) ) {
676 update_post_meta( $post_id, 'remote_post_id', $item->{'post-id'} );
677 }
678
679 wp_set_object_terms( $post_id, $user_feed->get_id(), User_Feed::POST_TAXONOMY );
680
681 update_post_meta( $post_id, 'parser', $user_feed->get_parser() );
682 update_post_meta( $post_id, 'feed_url', $user_feed->get_url() );
683
684 global $wpdb;
685 $wpdb->update( $wpdb->posts, array( 'comment_count' => $item->comment_count ), array( 'ID' => $post_id ) );
686 }
687 remove_filter( 'wp_revisions_to_keep', array( $this, 'revisions_to_keep' ) );
688
689 $this->notify_about_new_posts( $friend_user, $new_posts, $user_feed );
690
691 do_action( 'friends_retrieved_new_posts', $user_feed, $new_posts, $modified_posts, $friend_user );
692
693 return $new_posts;
694 }
695
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';
705 }
706 return $title_format;
707 }
708
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;
721 }
722
723 /**
724 * Output an additional XMLNS for the feed.
725 */
726 public function additional_feed_namespaces() {
727 echo ' xmlns:friends="' . esc_attr( self::XMLNS ) . '" ';
728 }
729
730 /**
731 * Additional fields for the friends feed.
732 */
733 public function feed_additional_fields() {
734 global $post;
735 $post_format = get_post_format( $post );
736 if ( empty( $post_format ) ) {
737 $post_format = 'standard';
738 }
739 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 }
750
751 /**
752 * Redirect
753 */
754 public function friends_add_friend_redirect() {
755 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
756 if ( ! isset( $_GET['add-friend'] ) || isset( $_GET['page'] ) ) {
757 return;
758 }
759 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
760 wp_safe_redirect( add_query_arg( 'url', $_GET['add-friend'], self_admin_url( 'admin.php?page=add-friend' ) ) );
761 exit;
762 }
763
764 /**
765 * Configure feed downloading options
766 *
767 * @param SimplePie $feed The SimplePie object.
768 */
769 public function wp_feed_options( $feed ) {
770 $feed->useragent .= ' Friends/' . FRIENDS_VERSION;
771 if (
772 isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'friends-refresh' ) &&
773 isset( $_GET['page'] ) && 'page=friends-refresh' === $_GET['page']
774 ) {
775 $feed->enable_cache( false );
776 } else {
777 $feed->set_cache_duration( 3590 );
778 }
779 }
780
781 /**
782 * Invalidatee Post Count Cache
783 *
784 * @param int $post_ID The post id.
785 * @param \WP_Post $post The post.
786 */
787 public function invalidate_post_count_cache( $post_ID, \WP_Post $post ) {
788 $cache_key = 'friends_post_count_by_post_format';
789 delete_transient( $cache_key );
790 if ( is_numeric( $post->post_author ) && $post->post_author > 0 ) {
791 $author_id = intval( $post->post_author );
792 delete_transient( $cache_key . '_author_' . $author_id );
793 }
794 }
795
796 /**
797 * Discover available feeds.
798 *
799 * @param string $url The feed URL.
800 * @return array The available feeds.
801 */
802 public function discover_available_feeds( $url ) {
803 if ( ! Friends::check_url( $url ) ) {
804 return array();
805 }
806 $available_feeds = array();
807 $content = null;
808 $content_type = 'text/html';
809
810 $response = wp_safe_remote_get(
811 $url,
812 array(
813 'timeout' => apply_filters( 'friends_http_timeout', 20 ),
814 'redirection' => 1,
815 )
816 );
817
818 if ( is_wp_error( $response ) ) {
819 $response->add_data( $url );
820 return $response;
821 }
822
823 if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
824 $content = wp_remote_retrieve_body( $response );
825 $headers = wp_remote_retrieve_headers( $response );
826
827 // We'll determine the obvious feeds ourself.
828 $available_feeds = $this->discover_link_rel_feeds( $content, $url, $headers );
829 $content_type = strtok( $headers['content-type'], ';' );
830 }
831
832 if ( $content ) {
833 foreach ( $this->parsers as $slug => $parser ) {
834 foreach ( $parser->discover_available_feeds( $content, $url ) as $link_url => $feed ) {
835 if ( isset( $available_feeds[ $link_url ] ) ) {
836 // If this parser tells us it can parse it right away, allow it to override.
837 if ( isset( $available_feeds[ $link_url ]['parser'] ) || ! isset( $feed['parser'] ) ) {
838 continue;
839 }
840 }
841 $available_feeds[ $link_url ] = $feed;
842 $available_feeds[ $link_url ]['url'] = $link_url;
843 }
844 }
845 }
846
847 $has_friends_plugin = false;
848 foreach ( $available_feeds as $link_url => $feed ) {
849 $feed = array_merge(
850 array(
851 'url' => $link_url,
852 'rel' => 'unknown',
853 'type' => 'unknown',
854 'title' => '',
855 'parser' => 'unsupported',
856 'parser_confidence' => 0,
857 ),
858 $feed
859 );
860 $available_feeds[ $link_url ] = $feed;
861
862 if ( 'friends-base-url' === $feed['rel'] ) {
863 $available_feeds[ $link_url ]['parser'] = 'friends';
864 $available_feeds[ $link_url ]['parser_confidence'] = 1000;
865 $has_friends_plugin = $link_url;
866 continue;
867 }
868
869 foreach ( $this->parsers as $slug => $parser ) {
870 $confidence = intval( $parser->feed_support_confidence( $link_url, $feed['type'], $feed['rel'] ) );
871 $confidence = intval( apply_filters( 'friends_parser_confidence', $confidence, $slug, $link_url, $feed ) );
872 if ( $available_feeds[ $link_url ]['parser_confidence'] < $confidence ) {
873 $available_feeds[ $link_url ]['parser_confidence'] = $confidence;
874 $available_feeds[ $link_url ]['parser'] = $slug;
875 }
876 }
877 }
878
879 if ( ! isset( $available_feeds[ $url ] ) ) {
880 $feed = array(
881 'url' => $url,
882 'type' => $content_type,
883 'rel' => 'self',
884 'title' => '',
885 'parser' => 'unsupported',
886 'parser_confidence' => 0,
887
888 );
889
890 foreach ( $this->parsers as $slug => $parser ) {
891 $confidence = $parser->feed_support_confidence( $url, $feed['type'], $feed['rel'], $content );
892 $confidence = apply_filters( 'friends_parser_confidence', $confidence, $slug, $url, $feed );
893 if ( $feed['parser_confidence'] < $confidence ) {
894 $feed['parser_confidence'] = $confidence;
895 $feed['parser'] = $slug;
896 }
897 }
898
899 if ( 'unknown' !== $feed['parser'] ) {
900 $available_feeds[ $feed['url'] ] = $feed;
901 }
902 }
903
904 foreach ( array_keys( $available_feeds ) as $link_url ) {
905 if ( ! isset( $this->parsers[ $available_feeds[ $link_url ]['parser'] ] ) ) {
906 continue;
907 }
908 $parser = $this->parsers[ $available_feeds[ $link_url ]['parser'] ];
909 $updated_feed_details = $parser->update_feed_details( $available_feeds[ $link_url ] );
910 if ( is_array( $updated_feed_details ) && ! is_wp_error( $updated_feed_details ) ) {
911 $available_feeds[ $link_url ] = array_merge( $available_feeds[ $link_url ], $updated_feed_details );
912 }
913 $available_feeds[ $link_url ] = apply_filters( 'friends_update_feed_details', $available_feeds[ $link_url ], $slug );
914 if ( $available_feeds[ $link_url ]['url'] !== $link_url ) {
915 $new_url = $available_feeds[ $link_url ]['url'];
916 $available_feeds[ $new_url ] = $available_feeds[ $link_url ];
917 unset( $available_feeds[ $link_url ] );
918 }
919 }
920
921 $autoselected = false;
922 // Check if a parser already autoselected a feed.
923 foreach ( $available_feeds as $link_url => $feed ) {
924 if ( isset( $feed['autoselect'] ) && $feed['autoselect'] ) {
925 $autoselected = true;
926 break;
927 }
928 }
929
930 foreach ( $available_feeds as $link_url => $feed ) {
931 $path = wp_parse_url( $link_url, PHP_URL_PATH );
932
933 // Backfill titles.
934 if ( empty( $feed['title'] ) ) {
935 $host = wp_parse_url( $link_url, PHP_URL_HOST );
936 if ( is_string( $path ) && trim( $path, '/' ) ) {
937 $feed['title'] = trim( preg_replace( '#^www\.#i', '', preg_replace( '#[^a-z0-9.:-]#i', ' ', ucwords( $host . ': ' . $path ) ) ), ': ' );
938 } else {
939 $feed['title'] = strtolower( $host );
940 }
941 $available_feeds[ $link_url ]['title'] = $feed['title'];
942 }
943
944 // Autoselect heuristic.
945 if ( $autoselected ) {
946 continue;
947 }
948
949 if ( ! $feed['parser_confidence'] ) {
950 continue;
951 }
952
953 // Prefer the main RSS feed.
954 if (
955 '/feed' === substr( '/' . trim( $path, '/' ), -5 )
956 || '.xml' === substr( $path, -4 )
957 || 'rss' === substr( $path, -3 )
958 ) {
959 if ( $has_friends_plugin ) {
960 $available_feeds[ $link_url ]['post-format'] = 'autodetect';
961 }
962 $autoselected = true;
963 } elseif ( isset( $feed['rel'] ) ) {
964 if ( 'alternate' === $feed['rel'] && 'application/rss+xml' === $feed['type'] && 'feed' === trim( $path, '/' ) ) {
965 $autoselected = true;
966 }
967 }
968
969 if ( $autoselected ) {
970 $available_feeds[ $link_url ]['autoselect'] = true;
971 }
972 }
973
974 $feed_sort_order = array( 'self', 'alternate', 'me' );
975 if ( $has_friends_plugin ) {
976 // If we have the Friends plugin, we prefer an (augmented) RSS feed over, for example, microformats.
977 $feed_sort_order = array( 'alternate', 'self', 'me' );
978 }
979
980 uasort(
981 $available_feeds,
982 function ( $a, $b ) use ( $feed_sort_order ) {
983 if ( isset( $a['autoselect'] ) && $a['autoselect'] ) {
984 if ( ! isset( $b['autoselect'] ) || ! $b['autoselect'] ) {
985 return -1;
986 }
987 } elseif ( isset( $b['autoselect'] ) && $b['autoselect'] ) {
988 return 1;
989 }
990
991 foreach ( $feed_sort_order as $rel_sort ) {
992 if ( $rel_sort === $a['rel'] ) {
993 if ( $rel_sort !== $b['rel'] ) {
994 return -1;
995 }
996 break;
997 } elseif ( $rel_sort === $b['rel'] ) {
998 return 1;
999 }
1000 }
1001
1002 return strcmp( $a['title'], $b['title'] );
1003 }
1004 );
1005
1006 return apply_filters( 'friends_available_feeds', $available_feeds, $url );
1007 }
1008
1009 /**
1010 * Discover feeds specified in the <link> section.
1011 *
1012 * @param string $content The content to search.
1013 * @param string $url The url.
1014 * @param object $headers The headers from the request.
1015 *
1016 * @return array ( description_of_the_return_value )
1017 */
1018 private function discover_link_rel_feeds( $content, $url, $headers ) {
1019 $discovered_feeds = array();
1020 $has_self = false;
1021 $mf = Mf2\parse( $content, $url );
1022 if ( isset( $mf['rel-urls'] ) ) {
1023 foreach ( $mf['rel-urls'] as $feed_url => $link ) {
1024 foreach ( array( 'friends-base-url', 'me', 'alternate', 'self' ) as $rel ) {
1025 if ( in_array( $rel, $link['rels'] ) ) {
1026 $discovered_feeds[ $feed_url ] = array(
1027 'rel' => $rel,
1028 );
1029 }
1030 }
1031
1032 if ( ! isset( $discovered_feeds[ $feed_url ] ) ) {
1033 continue;
1034 }
1035
1036 if ( 'self' === $rel ) {
1037 $has_self = true;
1038 }
1039
1040 if ( isset( $link['type'] ) ) {
1041 $discovered_feeds[ $feed_url ]['type'] = $link['type'];
1042 }
1043
1044 if ( isset( $link['title'] ) ) {
1045 $discovered_feeds[ $feed_url ]['title'] = $link['title'];
1046 } elseif ( isset( $link['text'] ) ) {
1047 $discovered_feeds[ $feed_url ]['title'] = $link['text'];
1048 }
1049 }
1050 }
1051
1052 if ( ! $has_self && class_exists( '\DOMXpath' ) ) {
1053 // Convert to a DomDocument and silence the errors while doing so.
1054 $doc = new \DomDocument();
1055 set_error_handler( '__return_null' );
1056 $doc->loadHTML( $content );
1057 restore_error_handler();
1058
1059 $xpath = new \DOMXpath( $doc );
1060 if ( $xpath ) {
1061 $discovered_feeds[ $url ] = array(
1062 'rel' => 'self',
1063 'title' => $xpath->query( '//title' )->item( 0 )->textContent,
1064 );
1065 }
1066 }
1067
1068 return $discovered_feeds;
1069 }
1070
1071 /**
1072 * Modify the main query for the friends feed
1073 *
1074 * @param \WP_Query $query The main query.
1075 * @return \WP_Query The modified main query.
1076 */
1077 public function private_feed_query( \WP_Query $query ) {
1078 if ( ! $this->friends->access_control->feed_is_authenticated() ) {
1079 return $query;
1080 }
1081
1082 $friend_user = $this->friends->access_control->get_authenticated_feed_user();
1083 if ( ! $query->is_admin && $query->is_feed && $friend_user->has_cap( 'friend' ) && ! $friend_user->has_cap( 'acquaintance' ) ) {
1084 $query->set( 'post_status', array( 'publish', 'private' ) );
1085 }
1086
1087 return $query;
1088 }
1089
1090 /**
1091 * More generic version of the native url_to_postid()
1092 *
1093 * @param string $url Permalink to check.
1094 * @param int $author_id The id of the author.
1095 * @return int Post ID, or 0 on failure.
1096 */
1097 public static function url_to_postid( $url, $author_id = false ) {
1098 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1099 $args = $post_types;
1100
1101 global $wpdb;
1102 $sql = sprintf(
1103 'SELECT ID from ' . $wpdb->posts . ' WHERE post_type IN ( %s )',
1104 implode( ',', array_fill( 0, count( $post_types ), '%s' ) )
1105 );
1106
1107 if ( $author_id ) {
1108 $args[] = $author_id;
1109 $sql .= ' AND post_author = %d';
1110 }
1111
1112 $sql .= ' AND guid IN (%s, %s) LIMIT 1';
1113 $args[] = $url;
1114 $args[] = esc_attr( $url );
1115
1116 $post_id = $wpdb->get_var(
1117 $wpdb->prepare(
1118 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1119 $args
1120 )
1121 );
1122 return $post_id;
1123 }
1124
1125 public function oembed_request_post_id( $post_id, $url ) {
1126 if ( ! $post_id ) {
1127 $post_id = self::url_to_postid( $url );
1128 global $wp_post_types;
1129 $wp_post_types[ Friends::CPT ]->publicly_queryable = true;
1130 }
1131 return $post_id;
1132 }
1133
1134 public function post_embed_url( $embed_url, $post ) {
1135 if ( ! in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ) ) ) {
1136 return $embed_url;
1137 }
1138
1139 return add_query_arg( 'url', $post->guid, rest_url( Rest::PREFIX . '/embed' ) );
1140 }
1141
1142 public function oembed_response_data( $data, $post, $width, $height ) {
1143 $data['width'] = absint( $width );
1144 $data['height'] = absint( $height );
1145 $data['type'] = 'rich';
1146
1147 ob_start();
1148 Friends::template_loader()->get_template_part( 'oembed/status', null, array( 'post' => $post ) );
1149 $data['html'] = ob_get_contents();
1150 ob_end_clean();
1151
1152 $data['html'] = '<p>' . wp_kses_post( $post->post_content ) . '</p>';
1153 return $data;
1154 }
1155
1156 public function get_user_feed_by_url( $url ) {
1157 return User_Feed::get_by_url( $url );
1158 }
1159
1160 /**
1161 * Gets the actual feed parser parser.
1162 *
1163 * @param string $parser The parser slug.
1164 *
1165 * @return Feed_Parser The actual parser.
1166 */
1167 public function get_feed_parser( $parser ) {
1168 if ( ! isset( $this->parsers[ $parser ] ) ) {
1169 return null;
1170 }
1171 return $this->parsers[ $parser ];
1172 }
1173
1174 /**
1175 * Gets the registered parser.
1176 *
1177 * @param string $parser The parser slug.
1178 *
1179 * @return string The parser name.
1180 */
1181 public function get_registered_parser( $parser ) {
1182 if ( ! isset( $this->parsers[ $parser ] ) ) {
1183 return null;
1184 }
1185 $parsers = $this->get_registered_parsers();
1186 return $parsers[ $parser ];
1187 }
1188
1189 /**
1190 * The list of currently supported parsers.
1191 *
1192 * @return array A list of parsers. Key is the slug, value is the parser name.
1193 */
1194 public function get_registered_parsers() {
1195 $parsers = array();
1196 foreach ( $this->parsers as $slug => $parser ) {
1197 $name = $slug;
1198 if ( defined( get_class( $parser ) . '::NAME' ) ) {
1199 $name = esc_html( $parser::NAME );
1200 }
1201 if ( defined( get_class( $parser ) . '::URL' ) ) {
1202 $name = '<a href="' . esc_url( $parser::URL ) . '" target="_blank" rel="noopener noreferrer">' . $name . '</a>';
1203 }
1204 $parsers[ $slug ] = $name;
1205 }
1206 return $parsers;
1207 }
1208 }
1209