PluginProbe
Friends / 2.8.4
Friends v2.8.4
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-frontend.php

class-frontend.php in Friends 2.8.4, at includes/class-frontend.php

1,309 lines 38.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Friends Page
4 *
5 * This contains the functions for /friends/
6 *
7 * @package Friends
8 */
9
10 namespace Friends;
11
12 /**
13 * This is the class for the /friends/ part of the Friends Plugin.
14 *
15 * @since 0.6
16 *
17 * @package Friends
18 * @author Alex Kirk
19 */
20 class Frontend {
21 /**
22 * Contains a reference to the Friends class.
23 *
24 * @var Friends
25 */
26 private $friends;
27
28 /**
29 * Whether we are on the /friends page.
30 *
31 * @var boolean
32 */
33 private $is_friends_page = false;
34
35 /**
36 * Whether an author is being displayed
37 *
38 * @var object|false
39 */
40 public $author = false;
41
42 /**
43 * Whether a post-format is being displayed
44 *
45 * @var string|false
46 */
47 public $post_format = false;
48
49 /**
50 * Whether a reaciton is being displayed
51 *
52 * @var string|false
53 */
54 public $reaction = false;
55
56 /**
57 * Constructor
58 *
59 * @param Friends $friends A reference to the Friends object.
60 */
61 public function __construct( Friends $friends ) {
62 $this->friends = $friends;
63 $this->register_hooks();
64 }
65
66 /**
67 * Register the WordPress hooks
68 */
69 private function register_hooks() {
70 add_filter( 'pre_get_posts', array( $this, 'friend_posts_query' ), 2 );
71 add_filter( 'post_type_link', array( $this, 'friend_post_link' ), 10, 2 );
72 add_filter( 'friends_header_widget_title', array( $this, 'header_widget_title' ) );
73 add_filter( 'get_edit_post_link', array( $this, 'friend_post_edit_link' ) );
74 add_filter( 'template_include', array( $this, 'template_override' ) );
75 add_filter( 'wp_loaded', array( $this, 'add_rewrite_rule' ) );
76 add_filter( 'init', array( $this, 'register_friends_sidebar' ) );
77 add_action( 'init', array( $this, 'add_theme_supports' ) );
78 add_action( 'wp_ajax_friends_publish', array( $this, 'ajax_frontend_publish_post' ) );
79 add_action( 'wp_ajax_friends-change-post-format', array( $this, 'ajax_change_post_format' ) );
80 add_action( 'wp_ajax_friends-load-next-page', array( $this, 'ajax_load_next_page' ) );
81 add_action( 'wp_ajax_friends-autocomplete', array( $this, 'ajax_autocomplete' ) );
82 add_action( 'wp_ajax_friends-in-reply-to-preview', array( $this, 'ajax_in_reply_to_preview' ) );
83 add_action( 'wp_ajax_friends-star', array( $this, 'ajax_star_friend_user' ) );
84 add_action( 'wp_ajax_friends-load-comments', array( $this, 'ajax_load_comments' ) );
85 add_action( 'wp_ajax_friends-reblog', array( $this, 'wp_ajax_reblog' ) );
86 add_action( 'friends_post_footer_first', array( $this, 'reblog_button' ) );
87 add_filter( 'friends_reblog', array( get_called_class(), 'reblog' ), 10, 2 );
88 add_filter( 'friends_unreblog', array( get_called_class(), 'unreblog' ), 10, 2 );
89 add_action( 'wp_untrash_post_status', array( $this, 'untrash_post_status' ), 10, 3 );
90 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
91 add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_scripts' ), 99999 );
92 add_action( 'wp_footer', array( $this, 'dequeue_scripts' ) );
93 add_action( 'the_post', array( $this, 'the_post' ), 10, 2 );
94 add_action( 'parse_query', array( $this, 'parse_query' ) );
95 add_filter( 'body_class', array( $this, 'add_body_class' ) );
96
97 add_filter( 'friends_override_author_name', array( $this, 'override_author_name' ), 10, 3 );
98 }
99
100 /**
101 * We're asking WordPress to handle the title for us.
102 */
103 public function add_rewrite_rule() {
104 add_rewrite_rule(
105 'friends/(.*)/(?:feed/)?(feed|rdf|rss|rss2|atom)/?$',
106 'index.php?pagename=friends/$matches[1]&feed=$matches[2]',
107 'top'
108 );
109 add_rewrite_rule(
110 'friends/(.*)/(\d+)/?$',
111 'index.php?pagename=friends/$matches[1]&page=$matches[2]',
112 'top'
113 );
114 add_rewrite_rule(
115 'friends/(.*)',
116 'index.php?pagename=friends/$matches[1]',
117 'top'
118 );
119 }
120
121 /**
122 * Run our add_theme_supports if on the frontend.
123 */
124 public function add_theme_supports() {
125 if ( ! Friends::on_frontend() ) {
126 return;
127 }
128
129 $this->add_theme_support_title_tag();
130 $this->add_theme_support_admin_bar();
131 }
132
133 /**
134 * We're asking WordPress to handle the title for us.
135 */
136 private function add_theme_support_title_tag() {
137 add_theme_support( 'title-tag' );
138 add_filter( 'document_title_parts', array( $this, 'modify_page_title' ) );
139 }
140
141 /**
142 * Remove the margin-top on the friends page.
143 */
144 private function add_theme_support_admin_bar() {
145 add_theme_support(
146 'admin-bar',
147 array(
148 'callback' => '__return_false',
149 )
150 );
151 }
152
153 /**
154 * Modify the page title to be output. This function is only invoked on the friends page.
155 *
156 * @param array $title The title.
157 *
158 * @return array The modified title.
159 */
160 public function modify_page_title( $title ) {
161 if ( is_single() ) {
162 $title['page'] = __( 'Friends', 'friends' );
163 $title['site'] = $this->author->display_name;
164 } elseif ( $this->author instanceof User ) {
165 $title['title'] = __( 'Friends', 'friends' );
166 $title['site'] = $this->author->display_name;
167 } else {
168 $title = array(
169 'site' => __( 'Friends', 'friends' ),
170 );
171 }
172 return $title;
173 }
174
175 /**
176 * Registers the sidebar for the /friends page.
177 */
178 public function register_friends_sidebar() {
179 register_sidebar(
180 array(
181 'name' => 'Friends Topbar',
182 'id' => 'friends-topbar',
183 'before_widget' => '<div class="friends-main-widget">',
184 'after_widget' => '</div>',
185 'before_title' => '<h1>',
186 'after_title' => '</h1>',
187 )
188 );
189 register_sidebar(
190 array(
191 'name' => 'Friends Sidebar',
192 'id' => 'friends-sidebar',
193 'before_widget' => '<div class="friends-widget">',
194 'after_widget' => '</div>',
195 'before_title' => '<h5>',
196 'after_title' => '</h5>',
197 )
198 );
199
200 if ( Friends::on_frontend() ) {
201 add_action( 'customize_register', '__return_true' );
202 }
203 }
204
205 /**
206 * Reference our script for the /friends page
207 */
208 public function enqueue_scripts() {
209 global $wp_query;
210
211 if ( is_user_logged_in() && Friends::on_frontend() ) {
212 $handle = 'friends';
213 $file = 'friends.js';
214 $version = Friends::VERSION;
215 wp_enqueue_script( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'common', 'jquery', 'wp-util' ), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
216
217 $query_vars = serialize( $this->get_minimal_query_vars( $wp_query->query_vars ) );
218
219 $variables = array(
220 'emojis_json' => plugins_url( 'emojis.json', FRIENDS_PLUGIN_FILE ),
221 'ajax_url' => admin_url( 'admin-ajax.php' ),
222 'text_link_expired' => __( 'The link has expired. A new link has been generated, please click it again.', 'friends' ),
223 'text_undo' => __( 'Undo' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
224 'text_trash_post' => __( 'Trash this post', 'friends' ),
225 'text_del_convers' => __( 'Do you really want to delete this conversation?', 'friends' ),
226 'text_no_more_posts' => __( 'No more posts available.', 'friends' ),
227 'query_vars' => $query_vars,
228 'qv_sign' => sha1( wp_salt( 'nonce' ) . $query_vars ),
229 'current_page' => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1,
230 'max_page' => $wp_query->max_num_pages,
231 );
232 wp_localize_script( 'friends', 'friends', $variables );
233
234 $handle = 'friends';
235 $file = 'friends.css';
236 $version = Friends::VERSION;
237 wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
238 }
239 }
240
241 public function dequeue_scripts() {
242 if ( is_user_logged_in() && Friends::on_frontend() ) {
243 // Dequeue theme styles so taht they don't interact with the Friends frontend.
244 $wp_styles = wp_styles();
245 foreach ( $wp_styles->queue as $style ) {
246 $src = $wp_styles->registered[ $style ]->src;
247 if ( 'global-styles' === $style || false !== strpos( $src, '/themes/' ) ) {
248 wp_dequeue_style( $style );
249 }
250 }
251 }
252 }
253
254 public function the_post( $post, $query ) {
255 Subscription::set_authordata_by_query( $query );
256 }
257
258 public function parse_query( $query ) {
259 Subscription::set_authordata_by_query( $query );
260 }
261
262 /**
263 * Add a CSS class to the body
264 *
265 * @param array $classes The existing CSS classes.
266 * @return array The modified CSS classes.
267 */
268 public function add_body_class( $classes ) {
269 if ( $this->friends->on_frontend() ) {
270 $classes[] = 'friends-page';
271 }
272
273 return $classes;
274 }
275
276 /**
277 * Gets the minimal query variables.
278 *
279 * @param array $query_vars The query variables.
280 *
281 * @return array The minimal query variables.
282 */
283 private function get_minimal_query_vars( $query_vars ) {
284 return array_filter( array_intersect_key( $query_vars, array_flip( array( 'p', 'page_id', 'pagename', 'author', 'author__not_in', 'post_type', 'post_status', 'posts_per_page', 'order', 'tax_query' ) ) ) );
285 }
286
287
288 public function wp_ajax_reblog() {
289 if ( ! current_user_can( Friends::REQUIRED_ROLE ) ) {
290 wp_send_json_error( 'error' );
291 }
292
293 $post = get_post( $_POST['post_id'] );
294 if ( ! $post || ! Friends::check_url( $post->guid ) ) {
295 wp_send_json_error( 'unknown-post', array( 'guid' => $post->guid ) );
296 }
297
298 /**
299 * Reblogs a post
300 *
301 * @param int|null $reblog_post_id The post ID of the reblogged post. Default null.
302 * @param WP_Post $post The post object.
303 */
304 $reblog_post_id = apply_filters( 'friends_reblog', null, $post );
305 if ( ! $reblog_post_id || is_wp_error( $reblog_post_id ) ) {
306 wp_send_json_error( 'error' );
307 }
308
309 wp_send_json_success(
310 array(
311 'post_id' => $reblog_post_id,
312 )
313 );
314 }
315
316 public function reblog_button() {
317 $button_label = apply_filters( 'friends_reblog_button_label', _x( 'Reblog', 'button', 'friends' ) );
318
319 Friends::template_loader()->get_template_part(
320 'frontend/parts/reblog-button',
321 null,
322 array(
323 'button-label' => $button_label,
324 )
325 );
326 }
327
328 public static function reblog( $ret, $post ) {
329 if ( ! $post ) {
330 return $ret;
331 }
332 $post = get_post( $post );
333 if ( ! $post ) {
334 return $ret;
335 }
336
337 $author = get_post_meta( $post->ID, 'author', true );
338 if ( ! $author ) {
339 $friend = User::get_post_author( $post );
340 $author = $friend->display_name;
341 }
342
343 $reblog = '<!-- wp:paragraph -->' . PHP_EOL . '<p>';
344 $reblog .= sprintf(
345 // translators: %s is a link.
346 __( 'Reblog via %s', 'friends' ),
347 '<a href="' . esc_url( $post->guid ) . '">' . esc_html( $author ) . '</a>'
348 );
349
350 $reblog .= PHP_EOL . '</p>' . PHP_EOL . '<!-- /wp:paragraph -->' . PHP_EOL;
351 $new_post = array(
352 'post_title' => $post->title,
353 'post_author' => get_current_user_id(),
354 'post_status' => 'publish',
355 'post_type' => 'post',
356 'post_format' => get_post_format( $post ),
357 'post_content' => $reblog . $post->post_content,
358 );
359 /**
360 * Allows changing a reblog post before it gets posted.
361 *
362 * @param array $new_post A post array to be handed to wp_insert_post.
363 *
364 * Additionally, the array contains the post_format which can also be changed.
365 *
366 * Example:
367 * ```php
368 * add_filter( 'friends_reblog_pre_insert_post', function( $new_post ) {
369 * $new_post['post_type'] = 'custom_post_type';
370 * $new_post['post_format'] = 'aside'; // always set the post_format to aside, regardless of the original post format.
371 * return $new_post;
372 * } );
373 * ```
374 */
375 $new_post = apply_filters( 'friends_reblog_pre_insert_post', $new_post );
376 $new_post_id = wp_insert_post( $new_post );
377
378 set_post_format( $new_post_id, $new_post['post_format'] );
379 update_post_meta( $new_post_id, 'reblog', $post->guid );
380 update_post_meta( $new_post_id, 'reblog_of', $post->ID );
381 update_post_meta( $post->ID, 'reblogged', $new_post_id );
382 update_post_meta( $post->ID, 'reblogged_by', $new_post['post_author'] );
383
384 return $new_post_id;
385 }
386
387 public static function unreblog( $ret, $post ) {
388 if ( ! $post ) {
389 return $ret;
390 }
391 $post = get_post( $post );
392 if ( ! $post ) {
393 return $ret;
394 }
395
396 $reblogged = get_post_meta( $post->ID, 'reblogged', true );
397 if ( ! $reblogged ) {
398 return $ret;
399 }
400
401 wp_trash_post( $reblogged );
402
403 return $reblogged;
404 }
405
406 /**
407 * The Ajax function to be called upon posting from /friends
408 */
409 public function ajax_frontend_publish_post() {
410 if ( ! wp_verify_nonce( $_POST['_wpnonce'], 'friends_publish' ) ) {
411 return false;
412 }
413 $p = array(
414 'post_type' => 'post',
415 'post_title' => isset( $_POST['title'] ) ? $_POST['title'] : '',
416 'post_content' => isset( $_POST['content'] ) ? $_POST['content'] : '',
417 'post_status' => isset( $_POST['status'] ) ? $_POST['status'] : '',
418 'post_format' => isset( $_POST['format'] ) ? $_POST['format'] : '',
419 );
420
421 if ( empty( $p['post_status'] ) ) {
422 $p['post_status'] = 'publish';
423 }
424 $result = 'empty';
425
426 if ( ! empty( $_POST['in_reply_to'] ) ) {
427 $p['meta_input'] = array(
428 'activitypub_in_reply_to' => $_POST['in_reply_to'],
429 );
430 }
431
432 if ( ! empty( $p['post_content'] ) || ! empty( $p['post_title'] ) ) {
433 $post_id = wp_insert_post( $p );
434 if ( ! empty( $p['post_format'] ) ) {
435 set_post_format( $post_id, $p['post_format'] );
436 }
437 $result = is_wp_error( $post_id ) ? 'error' : 'success';
438 }
439 if ( ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) === 'xmlhttprequest' ) {
440 echo esc_html( $result );
441 exit;
442 } else {
443 wp_safe_redirect( remove_query_arg( 'in_reply_to', add_query_arg( 'result', $result, $_SERVER['HTTP_REFERER'] ) ) );
444 exit;
445 }
446 }
447
448 /**
449 * The Ajax function to change the post format from the Frontend.
450 */
451 public function ajax_change_post_format() {
452 $post_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
453 $post_format = isset( $_POST['format'] ) ? $_POST['format'] : 'standard';
454
455 check_ajax_referer( "friends-change-post-format_$post_id" );
456
457 if ( ! current_user_can( Friends::REQUIRED_ROLE, $post_id ) ) {
458 wp_send_json_error();
459 exit;
460 }
461
462 $post_formats = get_post_format_strings();
463 if ( ! isset( $post_formats[ $post_format ] ) ) {
464 wp_send_json_error();
465 exit;
466 }
467
468 if ( ! set_post_format( $post_id, $post_format ) ) {
469 wp_send_json_error();
470 exit;
471 }
472
473 wp_send_json_success();
474 }
475
476 /**
477 * Calculates an estimating read time.
478 *
479 * @param string $original_text The original text.
480 *
481 * @return float The read time in seconds.
482 */
483 private static function calculate_read_time( $original_text ) {
484 // from wp_trim_words().
485 $text = wp_strip_all_tags( $original_text );
486
487 /*
488 * translators: If your word count is based on single characters (e.g. East Asian characters),
489 * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
490 * Do not translate into your own language.
491 */
492 if ( strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) { // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
493 $text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' );
494 preg_match_all( '/./u', $text, $words_array );
495 $words_array = array_shift( $words_array );
496 $words_per_minute = 500;
497 } else {
498 $words_array = preg_split( "/[\n\r\t ]+/", $text, -1, PREG_SPLIT_NO_EMPTY );
499 $words_per_minute = 200;
500 }
501
502 $additional_time = 0;
503 $figures = substr_count( strtolower( $original_text ), '<figure' );
504 for ( $i = 0; $i < $figures; $i++ ) {
505 if ( $i < 10 ) {
506 $additional_time += 12 - $i;
507 } else {
508 $additional_time += 3;
509 }
510 }
511
512 return count( $words_array ) / $words_per_minute * 60 + $additional_time;
513 }
514
515 /**
516 * Handles the post loop on the Friends page.
517 */
518 public static function have_posts() {
519 $friends = Friends::get_instance();
520 while ( have_posts() ) {
521 global $post;
522 the_post();
523 $args = array(
524 'friends' => $friends,
525 'avatar' => get_post_meta( get_the_ID(), 'gravatar', true ),
526 );
527
528 $args['friend_user'] = User::get_post_author( $post );
529
530 $read_time = self::calculate_read_time( get_the_content() );
531 if ( $read_time >= 60 ) {
532 $mins = ceil( $read_time / MINUTE_IN_SECONDS );
533 /* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
534 $args['read_time'] = sprintf( _n( '%s min', '%s mins', $mins ), $mins ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
535 } elseif ( $read_time > 20 ) {
536 /* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
537 $args['read_time'] = _x( '< 1 min', 'reading time', 'friends' );
538 }
539
540 Friends::template_loader()->get_template_part(
541 'frontend/parts/content',
542 get_post_format(),
543 $args
544 );
545 }
546 }
547
548 /**
549 * The Ajax function to load more posts for infinite scrolling.
550 */
551 public function ajax_load_next_page() {
552 $query_vars = wp_unslash( $_POST['query_vars'] );
553 if ( sha1( wp_salt( 'nonce' ) . $query_vars ) !== $_POST['qv_sign'] ) {
554 wp_send_json_error();
555 exit;
556 }
557 $query_vars = unserialize( $query_vars );
558 $query_vars['paged'] = intval( $_POST['page'] ) + 1;
559
560 query_posts( $query_vars );
561 ob_start();
562 if ( have_posts() ) {
563 self::have_posts();
564 } else {
565 esc_html_e( 'No further posts of your friends could were found.', 'friends' );
566 }
567
568 $posts = ob_get_contents();
569 ob_end_clean();
570
571 wp_send_json_success( $posts );
572 }
573
574 /**
575 * Get metadata for in_reply_to_preview.
576 *
577 * @param string $url The url.
578 *
579 * @return array|WP_Error The in reply to metadata.
580 */
581 public function get_in_reply_to_metadata( $url ) {
582 $meta = apply_filters( 'friends_get_activitypub_metadata', array(), $url );
583 if ( is_wp_error( $meta ) ) {
584 return $meta;
585 }
586
587 if ( ! $meta || ! isset( $meta['attributedTo'] ) ) {
588 return new \WP_Error( 'no-activitypub', 'No ActivityPub metadata found.' );
589 }
590
591 $html = 'URL: ' . make_clickable( $meta['id'] );
592 $html .= '<blockquote>' . force_balance_tags( wp_kses_post( $meta['content'] ) ) . '</blockquote>';
593
594 $webfinger = apply_filters( 'friends_get_activitypub_metadata', array(), $meta['attributedTo'] );
595 $mention = '';
596 if ( $webfinger && ! is_wp_error( $webfinger ) ) {
597 $mention = '@' . $webfinger['preferredUsername'] . '@' . parse_url( $url, PHP_URL_HOST );
598 }
599
600 return array(
601 'url' => $url,
602 'html' => $html,
603 'author' => $meta['attributedTo'],
604 'mention' => $mention,
605 );
606 }
607
608 /**
609 * The Ajax function to fill the in-reply-to-preview.
610 */
611 public function ajax_in_reply_to_preview() {
612 $url = wp_unslash( $_POST['url'] );
613
614 if ( ! wp_parse_url( $url ) ) {
615 wp_send_json_error();
616 exit;
617 }
618
619 $meta = $this->get_in_reply_to_metadata( $_POST['url'] );
620
621 if ( is_wp_error( $meta ) ) {
622 wp_send_json_error( $meta->get_error_message() );
623 exit;
624 }
625 wp_send_json_success( $meta );
626 }
627 /**
628 * The Ajax function to autocomplete search.
629 */
630 public function ajax_autocomplete() {
631 $q = wp_unslash( $_POST['q'] );
632 $users = User_Query::search( '*' . $q . '*' );
633 $results = array();
634 foreach ( $users->get_results() as $friend ) {
635 $result = '<li class="menu-item">';
636 $result .= '<a href="' . esc_url( $friend->get_local_friends_page_url() ) . '" class="has-icon-left">';
637 $result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->display_name );
638 $result .= ' <small>';
639 $result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->user_login );
640 $result .= '</small></a></li>';
641 $results[] = $result;
642 }
643
644 wp_send_json_success( implode( PHP_EOL, $results ) );
645 }
646
647 /**
648 * The Ajax function to load comments.
649 */
650 public function ajax_load_comments() {
651 if ( ! isset( $_POST['post_id'] ) || ! intval( $_POST['post_id'] ) ) {
652 wp_send_json_error();
653 exit;
654 }
655
656 $post_id = intval( $_POST['post_id'] );
657 check_ajax_referer( "comments-$post_id" );
658
659 $comments = get_comments(
660 array(
661 'post_id' => $post_id,
662 )
663 );
664
665 $author_id = get_post_field( 'post_author', $post_id );
666 $friend_user = new User( $author_id );
667
668 $comments_url = get_post_meta( $post_id, Feed::COMMENTS_FEED_META, true );
669 if ( ! $comments_url && empty( $comments ) ) {
670 wp_send_json_error( __( 'No comments feed available.', 'friends' ) );
671 exit;
672 }
673
674 if ( $friend_user->is_friend_url( $comments_url ) && friends::has_required_privileges() || wp_doing_cron() ) {
675 $comments_url = apply_filters( 'friends_friend_private_feed_url', $comments_url, $friend_user );
676 $comments_url = $this->friends->access_control->append_auth( $comments_url, $friend_user, 300 );
677 }
678
679 $feed_comments = $this->friends->feed->preview( 'simplepie', $comments_url );
680 if ( empty( $comments ) && ( is_wp_error( $feed_comments ) || ! is_array( $feed_comments ) ) ) {
681 wp_send_json_error( '<small>' . __( 'Unfortunately, comments were not available via RSS.', 'friends' ) . '</small>' );
682 exit;
683 } elseif ( is_wp_error( $feed_comments ) ) {
684 $feed_comments = array();
685 }
686
687 $template_loader = Friends::template_loader();
688 ob_start();
689 ?>
690 <h5><?php esc_html_e( 'Comments' ); /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain */ ?></h5>
691 <?php
692 foreach ( $comments as $comment ) {
693 $template_loader->get_template_part(
694 'frontend/parts/comment',
695 null,
696 array(
697 'author' => $comment->comment_author,
698 'date' => $comment->comment_date,
699 'permalink' => $comment->guid . '#comment-' . $comment->comment_ID,
700 'post_content' => $comment->comment_content,
701 )
702 );
703 }
704 foreach ( $feed_comments as $comment ) {
705 $template_loader->get_template_part(
706 'frontend/parts/comment',
707 null,
708 array(
709 'author' => $comment->author,
710 'date' => $comment->date,
711 'permalink' => $comment->permalink,
712 'post_content' => $comment->post_content,
713 )
714 );
715
716 }
717 ?>
718 <p>
719 <a href="<?php echo esc_url( get_comments_link( $post_id ) ); ?>"><?php esc_html_e( 'Leave a Comment' ); /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain */ ?></a>
720 </p>
721 <?php
722 $content = ob_get_contents();
723 ob_end_clean();
724
725 wp_send_json_success( $content );
726 }
727
728 public function ajax_star_friend_user() {
729 if ( ! isset( $_POST['friend_id'] ) || ! $_POST['friend_id'] ) {
730 wp_send_json_error();
731 exit;
732 }
733
734 $friend_id = wp_unslash( $_POST['friend_id'] );
735 check_ajax_referer( "star-$friend_id" );
736
737 $friend_user = User::get_by_username( $friend_id );
738
739 $starred = boolval( $_POST['starred'] );
740 $friend_user->set_starred( $starred );
741
742 wp_send_json_success(
743 array(
744 'starred' => $friend_user->get_starred(),
745 )
746 );
747 }
748
749 /**
750 * Ensure that untrashed friends posts go back to published.
751 *
752 * @param string $new_status The new status of the post being restored.
753 * @param int $post_id The ID of the post being restored.
754 * @param string $previous_status The status of the post at the point where it was trashed.
755 */
756 public function untrash_post_status( $new_status, $post_id, $previous_status ) {
757 if ( ! in_array( get_post_type( $post_id ), apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
758 return $new_status;
759 }
760 return 'publish';
761 }
762
763 /**
764 * Load the template for /friends
765 *
766 * @param string $template The original template intended to load.
767 * @return string The new template to be loaded.
768 */
769 public function template_override( $template ) {
770 if ( ! Friends::on_frontend() ) {
771 return $template;
772 }
773
774 if ( isset( $_GET['refresh'] ) ) {
775 add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
776 add_filter(
777 'wp_feed_options',
778 function ( $feed ) {
779 $feed->enable_cache( false );
780 }
781 );
782 $this->friends->feed->retrieve_friend_posts( true );
783 }
784
785 global $args;
786 $args = array(
787 'friends' => $this->friends,
788 'friend_user' => $this->author,
789 'frontend_default_view' => get_option( 'friends_frontend_default_view', 'expanded' ),
790 'blocks-everywhere' => false,
791 'post_format' => $this->post_format,
792 );
793
794 if ( isset( $_GET['in_reply_to'] ) && wp_parse_url( $_GET['in_reply_to'] ) ) {
795 $args['in_reply_to'] = $args['friends']->frontend->get_in_reply_to_metadata( $_GET['in_reply_to'] );
796 }
797
798 return Friends::template_loader()->get_template_part( 'frontend/index', $this->post_format, $args, false );
799 }
800
801 /**
802 * Modify the Friends page title depending on context, for example add an author name or post format.
803 *
804 * @param string $title The original title.
805 *
806 * @return string The modified title.
807 */
808 public function header_widget_title( $title ) {
809 $title = '<a href="' . esc_url( home_url( '/friends/' ) ) . '">' . esc_html( $title ) . '</a>';
810 if ( $this->author ) {
811 $title .= ' &raquo; ' . '<a href="' . esc_url( $this->author->get_local_friends_page_url() ) . '">' . esc_html( $this->author->display_name ) . '</a>';
812 }
813 if ( $this->post_format ) {
814 $post_formats = get_post_format_strings();
815 $title .= ' &raquo; ' . $post_formats[ $this->post_format ];
816 }
817 return $title;
818 }
819
820 /**
821 * Output a link, potentially augmented with authication information.
822 *
823 * @param string $url The url.
824 * @param string $text The link text.
825 * @param array $html_attributes HTML attributes.
826 * @param User $friend_user The friend user.
827 */
828 public function link( $url, $text, array $html_attributes = array(), User $friend_user = null ) {
829 echo wp_kses(
830 self::get_link( $url, $text, $html_attributes, $friend_user ),
831 array(
832 'a' => array(
833 'href' => array(),
834 'title' => array(),
835 'target' => array(),
836 'rel' => array(),
837 'class' => array(),
838 'style' => array(),
839 'data-nonce' => array(),
840 'data-cnonce' => array(),
841 'data-token' => array(),
842 'data-friend' => array(),
843 'data-id' => array(),
844 'data-url' => array(),
845 ),
846 'span' => array( 'class' => array() ),
847 )
848 );
849 }
850
851 /**
852 * Get a link, potentially augmented with authication information.
853 *
854 * @param string $url The url.
855 * @param string $text The link text.
856 * @param array $html_attributes HTML attributes.
857 * @param User $friend_user The friend user.
858 *
859 * @return string The link.
860 */
861 public static function get_link( $url, $text, array $html_attributes = array(), User $friend_user = null ) {
862 if ( is_null( $friend_user ) ) {
863 $friend_user = new User( get_the_author_meta( 'ID' ) );
864 }
865
866 if ( $friend_user->is_friend_url( $url ) && $friend_user->is_valid_friend() ) {
867 $html_attributes['target'] = '_blank';
868 $html_attributes['rel'] = 'noopener noreferrer';
869 if ( ! isset( $html_attributes['class'] ) ) {
870 $html_attributes['class'] = '';
871 }
872 $html_attributes['class'] = trim( $html_attributes['class'] . ' friends-auth-link' );
873 if ( ! isset( $html_attributes['dashicon_back'] ) ) {
874 $html_attributes['dashicon_back'] = 'admin-users';
875 }
876 $html_attributes['data-token'] = $friend_user->get_friend_auth();
877 $html_attributes['data-nonce'] = wp_create_nonce( 'auth-link-' . $url );
878 $html_attributes['data-friend'] = $friend_user->user_login;
879 }
880
881 $link = '<a href="' . esc_url( $url ) . '"';
882 foreach ( $html_attributes as $name => $value ) {
883 if ( ! in_array( $name, array( 'title', 'target', 'rel', 'class', 'style', 'data-nonce', 'data-cnonce', 'data-token', 'data-friend', 'data-id', 'data-url' ) ) ) {
884 continue;
885 }
886 $link .= ' ' . $name . '="' . esc_attr( $value ) . '"';
887 }
888 $link .= '>';
889 if ( isset( $html_attributes['dashicon_front'] ) ) {
890 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_front'] ) . '"></span>';
891 }
892 $link .= esc_html( $text );
893 if ( isset( $html_attributes['dashicon_back'] ) ) {
894 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_back'] ) . '"></span>';
895 }
896 $link .= '</a>';
897
898 return $link;
899 }
900
901 /**
902 * Don't show the edit link for friend posts.
903 *
904 * @param string $link The edit link.
905 * @return string|bool The edit link or false.
906 */
907 public function friend_post_edit_link( $link ) {
908 global $post;
909
910 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
911 if ( Friends::on_frontend() ) {
912 $new_link = false;
913 } else {
914 $new_link = get_the_guid( $post );
915 }
916 /**
917 * Allow overriding the link for editing friend posts.
918 *
919 * By default on the Frontend, a post cannot be edited because $new_link is false.
920 *
921 * Example:
922 *
923 * ```php
924 * add_filter( 'friend_post_edit_link', function( $link, $original_link ) {
925 * if ( ! $link ) {
926 * $link = $original_link; // always allow editing.
927 * }
928 * return $link;
929 * }, 10, 2 );
930 * ```
931 */
932 return apply_filters( 'friend_post_edit_link', $new_link, $link );
933 }
934 return $link;
935 }
936
937 /**
938 * Link friend posts to the remote site.
939 *
940 * @param string $post_link The post's permalink.
941 * @param \WP_Post $post The post in question.
942 * @reeturn string The overriden post link.
943 */
944 public function friend_post_link( $post_link, \WP_Post $post ) {
945 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
946 return get_the_guid( $post );
947 }
948 return $post_link;
949 }
950
951 /**
952 * Potentially override the post author name with metadata.
953 *
954 * @param string $overridden_author_name The already overridden author name.
955 * @param string $author_name The author name.
956 * @param int $post_id The post id.
957 *
958 * @return string The modified author name.
959 */
960 public function override_author_name( $overridden_author_name, $author_name, $post_id ) {
961 if ( $overridden_author_name && $overridden_author_name !== $author_name ) {
962 return $overridden_author_name;
963 }
964 $override_author_name = get_post_meta( $post_id, 'author', true );
965 if ( $override_author_name ) {
966 return $override_author_name;
967 }
968 return $author_name;
969 }
970
971 /**
972 * Render the Friends OPML
973 *
974 * @param bool $only_public Only public feed URLs.
975 */
976 protected function render_opml( $only_public = false ) {
977 $user = wp_get_current_user();
978
979 // translators: %s is a name.
980 $title = sprintf( __( "%s' Subscriptions", 'friends' ), $user->display_name );
981 $filename = 'friends-';
982 if ( ! $only_public ) {
983 $title = __( 'My Friends', 'friends' );
984 $filename .= 'private-';
985 }
986 $filename .= $user->user_login . '.opml';
987
988 $feeds = array();
989 $users = array();
990
991 $friend_users = new User_Query( array( 'role__in' => array( 'friend', 'acquaintance', 'friend_request', 'subscription' ) ) );
992 foreach ( $friend_users->get_results() as $friend_user ) {
993 $role = $friend_user->get_role_name( true, 9 );
994 if ( $only_public ) {
995 $role = 'Feeds';
996 }
997 if ( ! isset( $users[ $role ] ) ) {
998 $users[ $role ] = array();
999 }
1000
1001 $users[ $role ][] = $friend_user;
1002 }
1003 ksort( $users );
1004 foreach ( $users as $role => $friend_users ) {
1005 foreach ( $friend_users as $friend_user ) {
1006 $user_feeds = $friend_user->get_active_feeds();
1007
1008 $need_local_feed = false;
1009
1010 foreach ( $user_feeds as $feed ) {
1011 switch ( $feed->get_mime_type() ) {
1012 case 'application/atom+xml':
1013 case 'application/atomxml':
1014 case 'application/rss+xml':
1015 case 'application/rssxml':
1016 break;
1017 default:
1018 $need_local_feed = true;
1019 break 2;
1020 }
1021 }
1022
1023 if ( $need_local_feed && ! $only_public ) {
1024 $user_feeds = array_slice( $user_feeds, 0, 1 );
1025 }
1026
1027 $user = array(
1028 'friend_user' => $friend_user,
1029 'feeds' => array(),
1030 );
1031 $html_url = $friend_user->user_url;
1032
1033 foreach ( $user_feeds as $feed ) {
1034 $type = 'rss';
1035 $feed_title = $friend_user->display_name;
1036
1037 if ( ! $only_public ) {
1038 $html_url = $feed->get_local_html_url();
1039 }
1040
1041 if ( $need_local_feed ) {
1042 if ( $only_public ) {
1043 // Cannot create a public URL.
1044 continue;
1045 }
1046 $xml_url = $feed->get_local_url() . '?auth=' . $_GET['auth'];
1047 } else {
1048 if ( $only_public ) {
1049 $xml_url = $feed->get_url();
1050 } else {
1051 $xml_url = $feed->get_private_url( YEAR_IN_SECONDS );
1052 }
1053 if ( 'application/atom+xml' === $feed->get_mime_type() ) {
1054 $type = 'atom';
1055 }
1056 }
1057 $user['feeds'][] = array(
1058 'xml_url' => $xml_url,
1059 'html_url' => $html_url,
1060 'title' => $feed_title,
1061 'type' => $type,
1062 );
1063 }
1064
1065 if ( ! empty( $user['feeds'] ) ) {
1066 if ( ! isset( $feeds[ $role ] ) ) {
1067 $feeds[ $role ] = array();
1068 }
1069 $feeds[ $role ][] = $user;
1070 }
1071 }
1072 }
1073 Friends::template_loader()->get_template_part(
1074 'admin/opml',
1075 null,
1076 array(
1077 'title' => $title,
1078 'feeds' => $feeds,
1079 'filename' => $filename,
1080 )
1081 );
1082 exit;
1083 }
1084
1085 private function has_required_priviledges() {
1086 if ( Friends::is_main_user() ) {
1087 return true;
1088 }
1089
1090 if ( is_multisite() ) {
1091 if ( is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
1092 return true;
1093 }
1094
1095 if ( is_super_admin( get_current_user_id() ) ) {
1096 // Super admins would count as administrators.
1097 return false;
1098 }
1099 }
1100
1101 if ( current_user_can( 'manage_options' ) ) {
1102 return true;
1103 }
1104
1105 return false;
1106 }
1107
1108 /**
1109 * Modify the main query for the /friends page
1110 *
1111 * @param \WP_Query $query The main query.
1112 * @return \WP_Query The modified main query.
1113 */
1114 public function friend_posts_query( $query ) {
1115 global $wp_query, $wp, $authordata;
1116 if ( $wp_query !== $query || $query->is_admin() || $query->is_home() ) {
1117 return $query;
1118 }
1119
1120 $pagename = '';
1121 if ( isset( $wp_query->query['pagename'] ) ) {
1122 $pagename = $wp_query->query['pagename'];
1123 } elseif ( isset( $wp_query->query['name'] ) ) {
1124 $pagename = $wp_query->query['name'];
1125 }
1126
1127 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
1128 $is_friends = array_shift( $pagename_parts );
1129 if ( 'friends' !== $is_friends ) {
1130 return $query;
1131 }
1132
1133 // Not available for the general public or friends.
1134 $viewable = $this->has_required_priviledges() && Friends::on_frontend();
1135 if ( $query->is_feed() ) {
1136 // Feeds can be viewed through extra authentication.
1137 if ( $this->friends->access_control->private_rss_is_authenticated() ) {
1138 $viewable = true;
1139 } elseif ( isset( $wp_query->query['pagename'] ) ) {
1140 if ( apply_filters( 'friends_friend_feed_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
1141 $viewable = true;
1142 }
1143 }
1144 }
1145
1146 if ( ! $viewable && isset( $wp_query->query['pagename'] ) ) {
1147 if ( apply_filters( 'friends_friend_posts_query_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
1148 $viewable = true;
1149 }
1150 }
1151
1152 $page_id = get_query_var( 'page' );
1153
1154 if ( isset( $_GET['share'] ) ) {
1155 $share_hash = hash( 'crc32b', apply_filters( 'friends_share_salt', wp_salt( 'nonce' ), $page_id ) . $page_id );
1156 if ( $_GET['share'] === $share_hash ) {
1157 $viewable = true;
1158 }
1159 }
1160
1161 if ( ! $viewable ) {
1162 if ( $query->is_feed() ) {
1163 status_header( 404 );
1164 $query->set_404();
1165 } elseif ( ! Friends::on_frontend() ) {
1166 if ( count( $pagename_parts ) > 0 ) {
1167 wp_safe_redirect( home_url( '/friends/' ) );
1168 exit;
1169 }
1170 } elseif ( ! Friends::is_main_user() ) {
1171 wp_die( __( 'You are not allowed to view this page.', 'friends' ) );
1172 }
1173
1174 return $query;
1175 }
1176
1177 // Don't let the Post Kinds plugin interfere with this query.
1178 remove_action( 'pre_get_posts', array( 'Kind_Taxonomy', 'kind_firehose_query' ), 99 );
1179
1180 switch_to_locale( get_user_locale() );
1181
1182 $tax_query = array();
1183 $post_format = null;
1184
1185 while ( $pagename_parts ) {
1186 $pagename_part = $pagename_parts[0];
1187 $current_part = array_shift( $pagename_parts );
1188 if ( 'reaction' === substr( $current_part, 0, 8 ) && strlen( $current_part ) > 8 ) {
1189 $reaction = Reactions::validate_emoji( substr( $current_part, 8 ) );
1190 if ( ! $reaction ) {
1191 continue;
1192 }
1193 $this->reaction = $reaction;
1194 if ( ! empty( $tax_query ) ) {
1195 $tax_query['relation'] = 'AND';
1196 }
1197
1198 $tax_query[] = array(
1199 'taxonomy' => 'friend-reaction-' . get_current_user_id(),
1200 'field' => 'slug',
1201 'terms' => array( substr( $current_part, 8 ) ),
1202 );
1203 continue;
1204 }
1205
1206 switch ( $current_part ) {
1207 case 'opml':
1208 return $this->render_opml( isset( $_REQUEST['public'] ) );
1209
1210 case 'type':
1211 $post_format = array_shift( $pagename_parts );
1212 $tax_query = $this->friends->wp_query_get_post_format_tax_query( $tax_query, explode( ',', $post_format ) );
1213 if ( $tax_query ) {
1214 $this->post_format = $post_format;
1215 }
1216 break;
1217
1218 default: // Maybe an author.
1219 $author = User::get_by_username( $current_part );
1220 if ( false === $author || is_wp_error( $author ) ) {
1221 if ( $query->is_feed() ) {
1222 status_header( 404 );
1223 $query->set_404();
1224 return $query;
1225 }
1226 wp_safe_redirect( home_url( '/friends/' ) );
1227 exit;
1228 }
1229
1230 $this->author = $author;
1231 break;
1232 }
1233 }
1234
1235 $this->is_friends_page = true;
1236 $query->is_friends_page = true;
1237 $query->is_singular = false;
1238 $query->is_single = false;
1239 $query->queried_object = null;
1240 $query->queried_object_id = null;
1241 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1242
1243 if ( 'status' === $post_format ) {
1244 // Show your own posts on the status feed.
1245 $post_types[] = 'post';
1246 }
1247
1248 $query->set( 'post_type', $post_types );
1249 $query->set( 'tax_query', $tax_query );
1250
1251 if ( ( isset( $_GET['share'] ) && $_GET['share'] === $share_hash ) || $viewable ) {
1252 $post_status = array( 'publish', 'private', 'future' );
1253 if ( isset( $_GET['show-hidden'] ) ) {
1254 $post_status[] = 'trash';
1255 }
1256 $query->set( 'post_status', $post_status );
1257 }
1258 $query->is_page = false;
1259 $query->is_comments_feed = false;
1260 $query->set( 'pagename', null );
1261 if ( 'collapsed' === get_option( 'friends_frontend_default_view', 'expanded' ) && get_option( 'posts_per_page' ) < 20 ) {
1262 if ( 'status' === $post_format ) {
1263 $query->set( 'posts_per_page', 30 );
1264 } else {
1265 $query->set( 'posts_per_page', 20 );
1266 }
1267 }
1268
1269 if ( $page_id ) {
1270 $query->set( 'page_id', $page_id );
1271 if ( ! $this->author ) {
1272 $post = get_post( $page_id );
1273 $author = User::get_post_author( $post );
1274 if ( false !== $author ) {
1275 $this->author = $author;
1276 }
1277 }
1278 $query->is_single = true;
1279 $query->is_singular = true;
1280 $wp->set_query_var( 'page', null );
1281 $wp->set_query_var( 'page_id', $page_id );
1282 }
1283
1284 if ( $this->author ) {
1285 if ( $this->author instanceof User ) {
1286 $authordata = $this->author;
1287 $this->author->modify_query_by_author( $query );
1288 if ( ! $page_id ) {
1289 $query->is_author = true;
1290 }
1291 } else {
1292 $this->author = null;
1293 }
1294 }
1295
1296 if ( ! $query->is_singular && ! $query->is_author ) {
1297 // This is the main friends page.
1298 $hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' );
1299 $hide_from_friends_page = array_diff( array_map( 'intval', (array) $hide_from_friends_page ), array( 0 ) );
1300
1301 if ( $hide_from_friends_page ) {
1302 $query->set( 'author__not_in', $hide_from_friends_page );
1303 }
1304 }
1305
1306 return $query;
1307 }
1308 }
1309