PluginProbe
Friends / 2.7.6
Friends v2.7.6
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.6, at includes/class-feed.php

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