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

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