PluginProbe
Friends / 2.8.6
Friends v2.8.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-frontend.php

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

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