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

1,205 lines 35.2 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 = strip_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 ( strip_tags( $old_post->$field ) !== strip_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 if ( ! isset( $_GET['add-friend'] ) || isset( $_GET['page'] ) ) {
756 return;
757 }
758
759 wp_safe_redirect( add_query_arg( 'url', $_GET['add-friend'], self_admin_url( 'admin.php?page=add-friend' ) ) );
760 exit;
761 }
762
763 /**
764 * Configure feed downloading options
765 *
766 * @param SimplePie $feed The SimplePie object.
767 */
768 public function wp_feed_options( $feed ) {
769 $feed->useragent .= ' Friends/' . FRIENDS_VERSION;
770 if ( isset( $_GET['page'] ) && 'page=friends-refresh' === $_GET['page'] ) {
771 $feed->enable_cache( false );
772 } else {
773 $feed->set_cache_duration( 3590 );
774 }
775 }
776
777 /**
778 * Invalidatee Post Count Cache
779 *
780 * @param int $post_ID The post id.
781 * @param \WP_Post $post The post.
782 */
783 public function invalidate_post_count_cache( $post_ID, \WP_Post $post ) {
784 $cache_key = 'friends_post_count_by_post_format';
785 delete_transient( $cache_key );
786 if ( is_numeric( $post->post_author ) && $post->post_author > 0 ) {
787 $author_id = intval( $post->post_author );
788 delete_transient( $cache_key . '_author_' . $author_id );
789 }
790 }
791
792 /**
793 * Discover available feeds.
794 *
795 * @param string $url The feed URL.
796 * @return array The available feeds.
797 */
798 public function discover_available_feeds( $url ) {
799 if ( ! Friends::check_url( $url ) ) {
800 return array();
801 }
802 $available_feeds = array();
803 $content = null;
804 $content_type = 'text/html';
805
806 $response = wp_safe_remote_get(
807 $url,
808 array(
809 'timeout' => apply_filters( 'friends_http_timeout', 20 ),
810 'redirection' => 1,
811 )
812 );
813
814 if ( is_wp_error( $response ) ) {
815 $response->add_data( $url );
816 return $response;
817 }
818
819 if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
820 $content = wp_remote_retrieve_body( $response );
821 $headers = wp_remote_retrieve_headers( $response );
822
823 // We'll determine the obvious feeds ourself.
824 $available_feeds = $this->discover_link_rel_feeds( $content, $url, $headers );
825 $content_type = strtok( $headers['content-type'], ';' );
826 }
827
828 if ( $content ) {
829 foreach ( $this->parsers as $slug => $parser ) {
830 foreach ( $parser->discover_available_feeds( $content, $url ) as $link_url => $feed ) {
831 if ( isset( $available_feeds[ $link_url ] ) ) {
832 // If this parser tells us it can parse it right away, allow it to override.
833 if ( isset( $available_feeds[ $link_url ]['parser'] ) || ! isset( $feed['parser'] ) ) {
834 continue;
835 }
836 }
837 $available_feeds[ $link_url ] = $feed;
838 $available_feeds[ $link_url ]['url'] = $link_url;
839 }
840 }
841 }
842
843 $has_friends_plugin = false;
844 foreach ( $available_feeds as $link_url => $feed ) {
845 $feed = array_merge(
846 array(
847 'url' => $link_url,
848 'rel' => 'unknown',
849 'type' => 'unknown',
850 'title' => '',
851 'parser' => 'unsupported',
852 'parser_confidence' => 0,
853 ),
854 $feed
855 );
856 $available_feeds[ $link_url ] = $feed;
857
858 if ( 'friends-base-url' === $feed['rel'] ) {
859 $available_feeds[ $link_url ]['parser'] = 'friends';
860 $available_feeds[ $link_url ]['parser_confidence'] = 1000;
861 $has_friends_plugin = $link_url;
862 continue;
863 }
864
865 foreach ( $this->parsers as $slug => $parser ) {
866 $confidence = intval( $parser->feed_support_confidence( $link_url, $feed['type'], $feed['rel'] ) );
867 $confidence = intval( apply_filters( 'friends_parser_confidence', $confidence, $slug, $link_url, $feed ) );
868 if ( $available_feeds[ $link_url ]['parser_confidence'] < $confidence ) {
869 $available_feeds[ $link_url ]['parser_confidence'] = $confidence;
870 $available_feeds[ $link_url ]['parser'] = $slug;
871 }
872 }
873 }
874
875 if ( ! isset( $available_feeds[ $url ] ) ) {
876 $feed = array(
877 'url' => $url,
878 'type' => $content_type,
879 'rel' => 'self',
880 'title' => '',
881 'parser' => 'unsupported',
882 'parser_confidence' => 0,
883
884 );
885
886 foreach ( $this->parsers as $slug => $parser ) {
887 $confidence = $parser->feed_support_confidence( $url, $feed['type'], $feed['rel'], $content );
888 $confidence = apply_filters( 'friends_parser_confidence', $confidence, $slug, $url, $feed );
889 if ( $feed['parser_confidence'] < $confidence ) {
890 $feed['parser_confidence'] = $confidence;
891 $feed['parser'] = $slug;
892 }
893 }
894
895 if ( 'unknown' !== $feed['parser'] ) {
896 $available_feeds[ $feed['url'] ] = $feed;
897 }
898 }
899
900 foreach ( array_keys( $available_feeds ) as $link_url ) {
901 if ( ! isset( $this->parsers[ $available_feeds[ $link_url ]['parser'] ] ) ) {
902 continue;
903 }
904 $parser = $this->parsers[ $available_feeds[ $link_url ]['parser'] ];
905 $updated_feed_details = $parser->update_feed_details( $available_feeds[ $link_url ] );
906 if ( is_array( $updated_feed_details ) && ! is_wp_error( $updated_feed_details ) ) {
907 $available_feeds[ $link_url ] = array_merge( $available_feeds[ $link_url ], $updated_feed_details );
908 }
909 $available_feeds[ $link_url ] = apply_filters( 'friends_update_feed_details', $available_feeds[ $link_url ], $slug );
910 if ( $available_feeds[ $link_url ]['url'] !== $link_url ) {
911 $new_url = $available_feeds[ $link_url ]['url'];
912 $available_feeds[ $new_url ] = $available_feeds[ $link_url ];
913 unset( $available_feeds[ $link_url ] );
914 }
915 }
916
917 $autoselected = false;
918 // Check if a parser already autoselected a feed.
919 foreach ( $available_feeds as $link_url => $feed ) {
920 if ( isset( $feed['autoselect'] ) && $feed['autoselect'] ) {
921 $autoselected = true;
922 break;
923 }
924 }
925
926 foreach ( $available_feeds as $link_url => $feed ) {
927 $path = wp_parse_url( $link_url, PHP_URL_PATH );
928
929 // Backfill titles.
930 if ( empty( $feed['title'] ) ) {
931 $host = wp_parse_url( $link_url, PHP_URL_HOST );
932 if ( is_string( $path ) && trim( $path, '/' ) ) {
933 $feed['title'] = trim( preg_replace( '#^www\.#i', '', preg_replace( '#[^a-z0-9.:-]#i', ' ', ucwords( $host . ': ' . $path ) ) ), ': ' );
934 } else {
935 $feed['title'] = strtolower( $host );
936 }
937 $available_feeds[ $link_url ]['title'] = $feed['title'];
938 }
939
940 // Autoselect heuristic.
941 if ( $autoselected ) {
942 continue;
943 }
944
945 if ( ! $feed['parser_confidence'] ) {
946 continue;
947 }
948
949 // Prefer the main RSS feed.
950 if (
951 '/feed' === substr( '/' . trim( $path, '/' ), -5 )
952 || '.xml' === substr( $path, -4 )
953 || 'rss' === substr( $path, -3 )
954 ) {
955 if ( $has_friends_plugin ) {
956 $available_feeds[ $link_url ]['post-format'] = 'autodetect';
957 }
958 $autoselected = true;
959 } elseif ( isset( $feed['rel'] ) ) {
960 if ( 'alternate' === $feed['rel'] && 'application/rss+xml' === $feed['type'] && 'feed' === trim( $path, '/' ) ) {
961 $autoselected = true;
962 }
963 }
964
965 if ( $autoselected ) {
966 $available_feeds[ $link_url ]['autoselect'] = true;
967 }
968 }
969
970 $feed_sort_order = array( 'self', 'alternate', 'me' );
971 if ( $has_friends_plugin ) {
972 // If we have the Friends plugin, we prefer an (augmented) RSS feed over, for example, microformats.
973 $feed_sort_order = array( 'alternate', 'self', 'me' );
974 }
975
976 uasort(
977 $available_feeds,
978 function ( $a, $b ) use ( $feed_sort_order ) {
979 if ( isset( $a['autoselect'] ) && $a['autoselect'] ) {
980 if ( ! isset( $b['autoselect'] ) || ! $b['autoselect'] ) {
981 return -1;
982 }
983 } elseif ( isset( $b['autoselect'] ) && $b['autoselect'] ) {
984 return 1;
985 }
986
987 foreach ( $feed_sort_order as $rel_sort ) {
988 if ( $rel_sort === $a['rel'] ) {
989 if ( $rel_sort !== $b['rel'] ) {
990 return -1;
991 }
992 break;
993 } elseif ( $rel_sort === $b['rel'] ) {
994 return 1;
995 }
996 }
997
998 return strcmp( $a['title'], $b['title'] );
999 }
1000 );
1001
1002 return apply_filters( 'friends_available_feeds', $available_feeds, $url );
1003 }
1004
1005 /**
1006 * Discover feeds specified in the <link> section.
1007 *
1008 * @param string $content The content to search.
1009 * @param string $url The url.
1010 * @param object $headers The headers from the request.
1011 *
1012 * @return array ( description_of_the_return_value )
1013 */
1014 private function discover_link_rel_feeds( $content, $url, $headers ) {
1015 $discovered_feeds = array();
1016 $has_self = false;
1017 $mf = Mf2\parse( $content, $url );
1018 if ( isset( $mf['rel-urls'] ) ) {
1019 foreach ( $mf['rel-urls'] as $feed_url => $link ) {
1020 foreach ( array( 'friends-base-url', 'me', 'alternate', 'self' ) as $rel ) {
1021 if ( in_array( $rel, $link['rels'] ) ) {
1022 $discovered_feeds[ $feed_url ] = array(
1023 'rel' => $rel,
1024 );
1025 }
1026 }
1027
1028 if ( ! isset( $discovered_feeds[ $feed_url ] ) ) {
1029 continue;
1030 }
1031
1032 if ( 'self' === $rel ) {
1033 $has_self = true;
1034 }
1035
1036 if ( isset( $link['type'] ) ) {
1037 $discovered_feeds[ $feed_url ]['type'] = $link['type'];
1038 }
1039
1040 if ( isset( $link['title'] ) ) {
1041 $discovered_feeds[ $feed_url ]['title'] = $link['title'];
1042 } elseif ( isset( $link['text'] ) ) {
1043 $discovered_feeds[ $feed_url ]['title'] = $link['text'];
1044 }
1045 }
1046 }
1047
1048 if ( ! $has_self && class_exists( '\DOMXpath' ) ) {
1049 // Convert to a DomDocument and silence the errors while doing so.
1050 $doc = new \DomDocument();
1051 set_error_handler( '__return_null' );
1052 $doc->loadHTML( $content );
1053 restore_error_handler();
1054
1055 $xpath = new \DOMXpath( $doc );
1056 if ( $xpath ) {
1057 $discovered_feeds[ $url ] = array(
1058 'rel' => 'self',
1059 'title' => $xpath->query( '//title' )->item( 0 )->textContent,
1060 );
1061 }
1062 }
1063
1064 return $discovered_feeds;
1065 }
1066
1067 /**
1068 * Modify the main query for the friends feed
1069 *
1070 * @param \WP_Query $query The main query.
1071 * @return \WP_Query The modified main query.
1072 */
1073 public function private_feed_query( \WP_Query $query ) {
1074 if ( ! $this->friends->access_control->feed_is_authenticated() ) {
1075 return $query;
1076 }
1077
1078 $friend_user = $this->friends->access_control->get_authenticated_feed_user();
1079 if ( ! $query->is_admin && $query->is_feed && $friend_user->has_cap( 'friend' ) && ! $friend_user->has_cap( 'acquaintance' ) ) {
1080 $query->set( 'post_status', array( 'publish', 'private' ) );
1081 }
1082
1083 return $query;
1084 }
1085
1086 /**
1087 * More generic version of the native url_to_postid()
1088 *
1089 * @param string $url Permalink to check.
1090 * @param int $author_id The id of the author.
1091 * @return int Post ID, or 0 on failure.
1092 */
1093 public static function url_to_postid( $url, $author_id = false ) {
1094 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1095 $args = $post_types;
1096
1097 global $wpdb;
1098 $sql = sprintf(
1099 'SELECT ID from ' . $wpdb->posts . ' WHERE post_type IN ( %s )',
1100 implode( ',', array_fill( 0, count( $post_types ), '%s' ) )
1101 );
1102
1103 if ( $author_id ) {
1104 $args[] = $author_id;
1105 $sql .= ' AND post_author = %d';
1106 }
1107
1108 $sql .= ' AND guid IN (%s, %s) LIMIT 1';
1109 $args[] = $url;
1110 $args[] = esc_attr( $url );
1111
1112 $post_id = $wpdb->get_var(
1113 $wpdb->prepare(
1114 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1115 $args
1116 )
1117 );
1118 return $post_id;
1119 }
1120
1121 public function oembed_request_post_id( $post_id, $url ) {
1122 if ( ! $post_id ) {
1123 $post_id = self::url_to_postid( $url );
1124 global $wp_post_types;
1125 $wp_post_types[ Friends::CPT ]->publicly_queryable = true;
1126 }
1127 return $post_id;
1128 }
1129
1130 public function post_embed_url( $embed_url, $post ) {
1131 if ( ! in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ) ) ) {
1132 return $embed_url;
1133 }
1134
1135 return add_query_arg( 'url', $post->guid, rest_url( Rest::PREFIX . '/embed' ) );
1136 }
1137
1138 public function oembed_response_data( $data, $post, $width, $height ) {
1139 $data['width'] = absint( $width );
1140 $data['height'] = absint( $height );
1141 $data['type'] = 'rich';
1142
1143 ob_start();
1144 Friends::template_loader()->get_template_part( 'oembed/status', null, array( 'post' => $post ) );
1145 $data['html'] = ob_get_contents();
1146 ob_end_clean();
1147
1148 $data['html'] = '<p>' . wp_kses_post( $post->post_content ) . '</p>';
1149 return $data;
1150 }
1151
1152 public function get_user_feed_by_url( $url ) {
1153 return User_Feed::get_by_url( $url );
1154 }
1155
1156 /**
1157 * Gets the actual feed parser parser.
1158 *
1159 * @param string $parser The parser slug.
1160 *
1161 * @return Feed_Parser The actual parser.
1162 */
1163 public function get_feed_parser( $parser ) {
1164 if ( ! isset( $this->parsers[ $parser ] ) ) {
1165 return null;
1166 }
1167 return $this->parsers[ $parser ];
1168 }
1169
1170 /**
1171 * Gets the registered parser.
1172 *
1173 * @param string $parser The parser slug.
1174 *
1175 * @return string The parser name.
1176 */
1177 public function get_registered_parser( $parser ) {
1178 if ( ! isset( $this->parsers[ $parser ] ) ) {
1179 return null;
1180 }
1181 $parsers = $this->get_registered_parsers();
1182 return $parsers[ $parser ];
1183 }
1184
1185 /**
1186 * The list of currently supported parsers.
1187 *
1188 * @return array A list of parsers. Key is the slug, value is the parser name.
1189 */
1190 public function get_registered_parsers() {
1191 $parsers = array();
1192 foreach ( $this->parsers as $slug => $parser ) {
1193 $name = $slug;
1194 if ( defined( get_class( $parser ) . '::NAME' ) ) {
1195 $name = esc_html( $parser::NAME );
1196 }
1197 if ( defined( get_class( $parser ) . '::URL' ) ) {
1198 $name = '<a href="' . esc_url( $parser::URL ) . '" target="_blank" rel="noopener noreferrer">' . $name . '</a>';
1199 }
1200 $parsers[ $slug ] = $name;
1201 }
1202 return $parsers;
1203 }
1204 }
1205