PluginProbe
Friends / 2.8.7
Friends v2.8.7
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.7, at includes/class-frontend.php

1,307 lines 38.7 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 = array();
602
603 if ( false ) {
604 $result = '<a href="' . esc_url( add_query_arg( 'name', $q, admin_url( 'admin.php?page=add-friend' ) ) ) . '" class="has-icon-left">';
605 $result .= '<span class="ab-icon dashicons dashicons-businessperson"><span class="ab-icon dashicons dashicons-plus"></span></span>';
606 $result .= 'Add friend';
607 $result .= ' <small>';
608 $result .= esc_html( $q );
609 $result .= '</small></a>';
610 $results[] = $result;
611 }
612 $results = apply_filters( 'friends_search_autocomplete', $results, $q );
613
614 $result = '<a href="' . esc_url( add_query_arg( 's', $q, home_url( '/friends/' ) ) ) . '" class="has-icon-left">';
615 $result .= '<span class="ab-icon dashicons dashicons-search"></span>';
616 $result .= 'Search for ';
617 $result .= ' <small>';
618 $result .= esc_html( $q );
619 $result .= '</small></a>';
620 $results[] = $result;
621
622 wp_send_json_success( '<li class="menu-item">' . implode( '</li><li class="menu-item">', $results ) . '</li>' );
623 }
624
625 /**
626 * The Add user search entries to the autocomplete results.
627 *
628 * @param array $results The autocomplete results.
629 * @param string $q The query.
630 *
631 * @return array The autocomplete results.
632 */
633 public function autocomplete_user_search( $results, $q ) {
634 $users = User_Query::search( '*' . $q . '*' );
635
636 foreach ( $users->get_results() as $friend ) {
637 $result = '<a href="' . esc_url( $friend->get_local_friends_page_url() ) . '" class="has-icon-left">';
638 $result .= '<span class="ab-icon dashicons dashicons-businessperson"></span>';
639 $result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->display_name );
640 $result .= ' <small>';
641 $result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->user_login );
642 $result .= '</small></a>';
643 $results[] = $result;
644 }
645
646 return $results;
647 }
648
649 /**
650 * The Ajax function to load comments.
651 */
652 public function ajax_load_comments() {
653 if ( ! isset( $_POST['post_id'] ) || ! intval( $_POST['post_id'] ) ) {
654 wp_send_json_error();
655 exit;
656 }
657
658 $post_id = intval( $_POST['post_id'] );
659 check_ajax_referer( "comments-$post_id" );
660
661 $comments = get_comments(
662 array(
663 'post_id' => $post_id,
664 )
665 );
666
667 $author_id = get_post_field( 'post_author', $post_id );
668 $friend_user = new User( $author_id );
669
670 $comments_url = get_post_meta( $post_id, Feed::COMMENTS_FEED_META, true );
671 if ( ! $comments_url && empty( $comments ) ) {
672 wp_send_json_error( __( 'No comments feed available.', 'friends' ) );
673 exit;
674 }
675
676 if ( $friend_user->is_friend_url( $comments_url ) && friends::has_required_privileges() || wp_doing_cron() ) {
677 $comments_url = apply_filters( 'friends_friend_private_feed_url', $comments_url, $friend_user );
678 $comments_url = $this->friends->access_control->append_auth( $comments_url, $friend_user, 300 );
679 }
680
681 $feed_comments = $this->friends->feed->preview( 'simplepie', $comments_url );
682 if ( empty( $comments ) && ( is_wp_error( $feed_comments ) || ! is_array( $feed_comments ) ) ) {
683 wp_send_json_error( '<small>' . __( 'Unfortunately, comments were not available via RSS.', 'friends' ) . '</small>' );
684 exit;
685 } elseif ( is_wp_error( $feed_comments ) ) {
686 $feed_comments = array();
687 }
688
689 $template_loader = Friends::template_loader();
690 ob_start();
691 ?>
692 <h5><?php esc_html_e( 'Comments' ); /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain */ ?></h5>
693 <?php
694 foreach ( $comments as $comment ) {
695 $template_loader->get_template_part(
696 'frontend/parts/comment',
697 null,
698 array(
699 'author' => $comment->comment_author,
700 'date' => $comment->comment_date,
701 'permalink' => $comment->guid . '#comment-' . $comment->comment_ID,
702 'post_content' => $comment->comment_content,
703 )
704 );
705 }
706 foreach ( $feed_comments as $comment ) {
707 $template_loader->get_template_part(
708 'frontend/parts/comment',
709 null,
710 array(
711 'author' => $comment->author,
712 'date' => $comment->date,
713 'permalink' => $comment->permalink,
714 'post_content' => $comment->post_content,
715 )
716 );
717
718 }
719 ?>
720 <p>
721 <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>
722 </p>
723 <?php
724 $content = ob_get_contents();
725 ob_end_clean();
726
727 wp_send_json_success( $content );
728 }
729
730 public function ajax_star_friend_user() {
731 if ( ! isset( $_POST['friend_id'] ) || ! $_POST['friend_id'] ) {
732 wp_send_json_error();
733 exit;
734 }
735
736 $friend_id = wp_unslash( $_POST['friend_id'] );
737 check_ajax_referer( "star-$friend_id" );
738
739 $friend_user = User::get_by_username( $friend_id );
740
741 $starred = boolval( $_POST['starred'] );
742 $friend_user->set_starred( $starred );
743
744 wp_send_json_success(
745 array(
746 'starred' => $friend_user->get_starred(),
747 )
748 );
749 }
750
751 /**
752 * Ensure that untrashed friends posts go back to published.
753 *
754 * @param string $new_status The new status of the post being restored.
755 * @param int $post_id The ID of the post being restored.
756 * @param string $previous_status The status of the post at the point where it was trashed.
757 */
758 public function untrash_post_status( $new_status, $post_id, $previous_status ) {
759 if ( ! in_array( get_post_type( $post_id ), apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
760 return $new_status;
761 }
762 return 'publish';
763 }
764
765 /**
766 * Load the template for /friends
767 *
768 * @param string $template The original template intended to load.
769 * @return string The new template to be loaded.
770 */
771 public function template_override( $template ) {
772 if ( ! Friends::on_frontend() ) {
773 return $template;
774 }
775
776 if ( isset( $_GET['refresh'] ) ) {
777 add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
778 add_filter(
779 'wp_feed_options',
780 function ( $feed ) {
781 $feed->enable_cache( false );
782 }
783 );
784 $this->friends->feed->retrieve_friend_posts( true );
785 }
786
787 global $args;
788 $args = array(
789 'friends' => $this->friends,
790 'friend_user' => $this->author,
791 'frontend_default_view' => get_option( 'friends_frontend_default_view', 'expanded' ),
792 'blocks-everywhere' => false,
793 'post_format' => $this->post_format,
794 );
795
796 return Friends::template_loader()->get_template_part( 'frontend/index', $this->post_format, $args, false );
797 }
798
799 /**
800 * Modify the Friends page title depending on context, for example add an author name or post format.
801 *
802 * @param string $title The original title.
803 *
804 * @return string The modified title.
805 */
806 public function header_widget_title( $title ) {
807 $title = '<a href="' . esc_url( home_url( '/friends/' ) ) . '">' . esc_html( $title ) . '</a>';
808 if ( $this->author ) {
809 $title .= ' &raquo; ' . '<a href="' . esc_url( $this->author->get_local_friends_page_url() ) . '">' . esc_html( $this->author->display_name ) . '</a>';
810 }
811 if ( $this->post_format ) {
812 $post_formats = get_post_format_strings();
813 $title .= ' &raquo; ' . $post_formats[ $this->post_format ];
814 }
815 return $title;
816 }
817
818 /**
819 * Output a link, potentially augmented with authication information.
820 *
821 * @param string $url The url.
822 * @param string $text The link text.
823 * @param array $html_attributes HTML attributes.
824 * @param User $friend_user The friend user.
825 */
826 public function link( $url, $text, array $html_attributes = array(), User $friend_user = null ) {
827 echo wp_kses(
828 self::get_link( $url, $text, $html_attributes, $friend_user ),
829 array(
830 'a' => array(
831 'href' => array(),
832 'title' => array(),
833 'target' => array(),
834 'rel' => array(),
835 'class' => array(),
836 'style' => array(),
837 'data-nonce' => array(),
838 'data-cnonce' => array(),
839 'data-token' => array(),
840 'data-friend' => array(),
841 'data-id' => array(),
842 'data-url' => array(),
843 ),
844 'span' => array( 'class' => array() ),
845 )
846 );
847 }
848
849 /**
850 * Get a link, potentially augmented with authication information.
851 *
852 * @param string $url The url.
853 * @param string $text The link text.
854 * @param array $html_attributes HTML attributes.
855 * @param User $friend_user The friend user.
856 *
857 * @return string The link.
858 */
859 public static function get_link( $url, $text, array $html_attributes = array(), User $friend_user = null ) {
860 if ( is_null( $friend_user ) ) {
861 $friend_user = new User( get_the_author_meta( 'ID' ) );
862 }
863
864 if ( $friend_user->is_friend_url( $url ) && $friend_user->is_valid_friend() ) {
865 $html_attributes['target'] = '_blank';
866 $html_attributes['rel'] = 'noopener noreferrer';
867 if ( ! isset( $html_attributes['class'] ) ) {
868 $html_attributes['class'] = '';
869 }
870 $html_attributes['class'] = trim( $html_attributes['class'] . ' friends-auth-link' );
871 if ( ! isset( $html_attributes['dashicon_back'] ) ) {
872 $html_attributes['dashicon_back'] = 'admin-users';
873 }
874 $html_attributes['data-token'] = $friend_user->get_friend_auth();
875 $html_attributes['data-nonce'] = wp_create_nonce( 'auth-link-' . $url );
876 $html_attributes['data-friend'] = $friend_user->user_login;
877 }
878
879 $link = '<a href="' . esc_url( $url ) . '"';
880 foreach ( $html_attributes as $name => $value ) {
881 if ( ! in_array( $name, array( 'title', 'target', 'rel', 'class', 'style', 'data-nonce', 'data-cnonce', 'data-token', 'data-friend', 'data-id', 'data-url' ) ) ) {
882 continue;
883 }
884 $link .= ' ' . $name . '="' . esc_attr( $value ) . '"';
885 }
886 $link .= '>';
887 if ( isset( $html_attributes['dashicon_front'] ) ) {
888 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_front'] ) . '"></span>';
889 }
890 $link .= esc_html( $text );
891 if ( isset( $html_attributes['dashicon_back'] ) ) {
892 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_back'] ) . '"></span>';
893 }
894 $link .= '</a>';
895
896 return $link;
897 }
898
899 /**
900 * Don't show the edit link for friend posts.
901 *
902 * @param string $link The edit link.
903 * @return string|bool The edit link or false.
904 */
905 public function friend_post_edit_link( $link ) {
906 global $post;
907
908 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
909 if ( Friends::on_frontend() ) {
910 $new_link = false;
911 } else {
912 $new_link = get_the_guid( $post );
913 }
914 /**
915 * Allow overriding the link for editing friend posts.
916 *
917 * By default on the Frontend, a post cannot be edited because $new_link is false.
918 *
919 * Example:
920 *
921 * ```php
922 * add_filter( 'friend_post_edit_link', function( $link, $original_link ) {
923 * if ( ! $link ) {
924 * $link = $original_link; // always allow editing.
925 * }
926 * return $link;
927 * }, 10, 2 );
928 * ```
929 */
930 return apply_filters( 'friend_post_edit_link', $new_link, $link );
931 }
932 return $link;
933 }
934
935 /**
936 * Link friend posts to the remote site.
937 *
938 * @param string $post_link The post's permalink.
939 * @param \WP_Post $post The post in question.
940 * @reeturn string The overriden post link.
941 */
942 public function friend_post_link( $post_link, \WP_Post $post ) {
943 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
944 return get_the_guid( $post );
945 }
946 return $post_link;
947 }
948
949 /**
950 * Potentially override the post author name with metadata.
951 *
952 * @param string $overridden_author_name The already overridden author name.
953 * @param string $author_name The author name.
954 * @param int $post_id The post id.
955 *
956 * @return string The modified author name.
957 */
958 public function override_author_name( $overridden_author_name, $author_name, $post_id ) {
959 if ( $overridden_author_name && $overridden_author_name !== $author_name ) {
960 return $overridden_author_name;
961 }
962 $override_author_name = get_post_meta( $post_id, 'author', true );
963 if ( $override_author_name ) {
964 return $override_author_name;
965 }
966 return $author_name;
967 }
968
969 /**
970 * Render the Friends OPML
971 *
972 * @param bool $only_public Only public feed URLs.
973 */
974 protected function render_opml( $only_public = false ) {
975 $user = wp_get_current_user();
976
977 // translators: %s is a name.
978 $title = sprintf( __( "%s' Subscriptions", 'friends' ), $user->display_name );
979 $filename = 'friends-';
980 if ( ! $only_public ) {
981 $title = __( 'My Friends', 'friends' );
982 $filename .= 'private-';
983 }
984 $filename .= $user->user_login . '.opml';
985
986 $feeds = array();
987 $users = array();
988
989 $friend_users = new User_Query( array( 'role__in' => array( 'friend', 'acquaintance', 'friend_request', 'subscription' ) ) );
990 foreach ( $friend_users->get_results() as $friend_user ) {
991 $role = $friend_user->get_role_name( true, 9 );
992 if ( $only_public ) {
993 $role = 'Feeds';
994 }
995 if ( ! isset( $users[ $role ] ) ) {
996 $users[ $role ] = array();
997 }
998
999 $users[ $role ][] = $friend_user;
1000 }
1001 ksort( $users );
1002 foreach ( $users as $role => $friend_users ) {
1003 foreach ( $friend_users as $friend_user ) {
1004 $user_feeds = $friend_user->get_active_feeds();
1005
1006 $need_local_feed = false;
1007
1008 foreach ( $user_feeds as $feed ) {
1009 switch ( $feed->get_mime_type() ) {
1010 case 'application/atom+xml':
1011 case 'application/atomxml':
1012 case 'application/rss+xml':
1013 case 'application/rssxml':
1014 break;
1015 default:
1016 $need_local_feed = true;
1017 break 2;
1018 }
1019 }
1020
1021 if ( $need_local_feed && ! $only_public ) {
1022 $user_feeds = array_slice( $user_feeds, 0, 1 );
1023 }
1024
1025 $user = array(
1026 'friend_user' => $friend_user,
1027 'feeds' => array(),
1028 );
1029 $html_url = $friend_user->user_url;
1030
1031 foreach ( $user_feeds as $feed ) {
1032 $type = 'rss';
1033 $feed_title = $friend_user->display_name;
1034
1035 if ( ! $only_public ) {
1036 $html_url = $feed->get_local_html_url();
1037 }
1038
1039 if ( $need_local_feed ) {
1040 if ( $only_public ) {
1041 // Cannot create a public URL.
1042 continue;
1043 }
1044 $xml_url = $feed->get_local_url() . '?auth=' . $_GET['auth'];
1045 } else {
1046 if ( $only_public ) {
1047 $xml_url = $feed->get_url();
1048 } else {
1049 $xml_url = $feed->get_private_url( YEAR_IN_SECONDS );
1050 }
1051 if ( 'application/atom+xml' === $feed->get_mime_type() ) {
1052 $type = 'atom';
1053 }
1054 }
1055 $user['feeds'][] = array(
1056 'xml_url' => $xml_url,
1057 'html_url' => $html_url,
1058 'title' => $feed_title,
1059 'type' => $type,
1060 );
1061 }
1062
1063 if ( ! empty( $user['feeds'] ) ) {
1064 if ( ! isset( $feeds[ $role ] ) ) {
1065 $feeds[ $role ] = array();
1066 }
1067 $feeds[ $role ][] = $user;
1068 }
1069 }
1070 }
1071 Friends::template_loader()->get_template_part(
1072 'admin/opml',
1073 null,
1074 array(
1075 'title' => $title,
1076 'feeds' => $feeds,
1077 'filename' => $filename,
1078 )
1079 );
1080 exit;
1081 }
1082
1083 private function has_required_priviledges() {
1084 if ( Friends::is_main_user() ) {
1085 return true;
1086 }
1087
1088 if ( is_multisite() ) {
1089 if ( is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
1090 return true;
1091 }
1092
1093 if ( is_super_admin( get_current_user_id() ) ) {
1094 // Super admins would count as administrators.
1095 return false;
1096 }
1097 }
1098
1099 if ( current_user_can( 'manage_options' ) ) {
1100 return true;
1101 }
1102
1103 return false;
1104 }
1105
1106 /**
1107 * Modify the main query for the /friends page
1108 *
1109 * @param \WP_Query $query The main query.
1110 * @return \WP_Query The modified main query.
1111 */
1112 public function friend_posts_query( $query ) {
1113 global $wp_query, $wp, $authordata;
1114 if ( $wp_query !== $query || $query->is_admin() || $query->is_home() ) {
1115 return $query;
1116 }
1117
1118 $pagename = '';
1119 if ( isset( $wp_query->query['pagename'] ) ) {
1120 $pagename = $wp_query->query['pagename'];
1121 } elseif ( isset( $wp_query->query['name'] ) ) {
1122 $pagename = $wp_query->query['name'];
1123 }
1124
1125 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
1126 $is_friends = array_shift( $pagename_parts );
1127 if ( 'friends' !== $is_friends ) {
1128 return $query;
1129 }
1130
1131 // Not available for the general public or friends.
1132 $viewable = $this->has_required_priviledges() && Friends::on_frontend();
1133 if ( $query->is_feed() ) {
1134 // Feeds can be viewed through extra authentication.
1135 if ( $this->friends->access_control->private_rss_is_authenticated() ) {
1136 $viewable = true;
1137 } elseif ( isset( $wp_query->query['pagename'] ) ) {
1138 if ( apply_filters( 'friends_friend_feed_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
1139 $viewable = true;
1140 }
1141 }
1142 }
1143
1144 if ( ! $viewable && isset( $wp_query->query['pagename'] ) ) {
1145 if ( apply_filters( 'friends_friend_posts_query_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
1146 $viewable = true;
1147 }
1148 }
1149
1150 $page_id = get_query_var( 'page' );
1151
1152 if ( isset( $_GET['share'] ) ) {
1153 $share_hash = hash( 'crc32b', apply_filters( 'friends_share_salt', wp_salt( 'nonce' ), $page_id ) . $page_id );
1154 if ( $_GET['share'] === $share_hash ) {
1155 $viewable = true;
1156 }
1157 }
1158
1159 if ( ! $viewable ) {
1160 if ( $query->is_feed() ) {
1161 status_header( 404 );
1162 $query->set_404();
1163 } elseif ( ! Friends::on_frontend() ) {
1164 if ( count( $pagename_parts ) > 0 ) {
1165 wp_safe_redirect( home_url( '/friends/' ) );
1166 exit;
1167 }
1168 } elseif ( ! Friends::is_main_user() ) {
1169 wp_die( __( 'You are not allowed to view this page.', 'friends' ) );
1170 }
1171
1172 return $query;
1173 }
1174
1175 // Don't let the Post Kinds plugin interfere with this query.
1176 remove_action( 'pre_get_posts', array( 'Kind_Taxonomy', 'kind_firehose_query' ), 99 );
1177
1178 switch_to_locale( get_user_locale() );
1179
1180 $tax_query = array();
1181 $post_format = null;
1182
1183 while ( $pagename_parts ) {
1184 $pagename_part = $pagename_parts[0];
1185 $current_part = array_shift( $pagename_parts );
1186 if ( 'reaction' === substr( $current_part, 0, 8 ) && strlen( $current_part ) > 8 ) {
1187 $reaction = Reactions::validate_emoji( substr( $current_part, 8 ) );
1188 if ( ! $reaction ) {
1189 continue;
1190 }
1191 $this->reaction = $reaction;
1192 if ( ! empty( $tax_query ) ) {
1193 $tax_query['relation'] = 'AND';
1194 }
1195
1196 $tax_query[] = array(
1197 'taxonomy' => 'friend-reaction-' . get_current_user_id(),
1198 'field' => 'slug',
1199 'terms' => array( substr( $current_part, 8 ) ),
1200 );
1201 continue;
1202 }
1203
1204 switch ( $current_part ) {
1205 case 'opml':
1206 return $this->render_opml( isset( $_REQUEST['public'] ) );
1207
1208 case 'type':
1209 $post_format = array_shift( $pagename_parts );
1210 $tax_query = $this->friends->wp_query_get_post_format_tax_query( $tax_query, explode( ',', $post_format ) );
1211 if ( $tax_query ) {
1212 $this->post_format = $post_format;
1213 }
1214 break;
1215
1216 default: // Maybe an author.
1217 $author = User::get_by_username( $current_part );
1218 if ( false === $author || is_wp_error( $author ) ) {
1219 if ( $query->is_feed() ) {
1220 status_header( 404 );
1221 $query->set_404();
1222 return $query;
1223 }
1224 wp_safe_redirect( home_url( '/friends/' ) );
1225 exit;
1226 }
1227
1228 $this->author = $author;
1229 break;
1230 }
1231 }
1232
1233 $this->is_friends_page = true;
1234 $query->is_friends_page = true;
1235 $query->is_singular = false;
1236 $query->is_single = false;
1237 $query->queried_object = null;
1238 $query->queried_object_id = null;
1239 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1240
1241 if ( 'status' === $post_format ) {
1242 // Show your own posts on the status feed.
1243 $post_types[] = 'post';
1244 }
1245
1246 $query->set( 'post_type', $post_types );
1247 $query->set( 'tax_query', $tax_query );
1248
1249 if ( ( isset( $_GET['share'] ) && $_GET['share'] === $share_hash ) || $viewable ) {
1250 $post_status = array( 'publish', 'private', 'future' );
1251 if ( isset( $_GET['show-hidden'] ) ) {
1252 $post_status[] = 'trash';
1253 }
1254 $query->set( 'post_status', $post_status );
1255 }
1256 $query->is_page = false;
1257 $query->is_comments_feed = false;
1258 $query->set( 'pagename', null );
1259 if ( 'collapsed' === get_option( 'friends_frontend_default_view', 'expanded' ) && get_option( 'posts_per_page' ) < 20 ) {
1260 if ( 'status' === $post_format ) {
1261 $query->set( 'posts_per_page', 30 );
1262 } else {
1263 $query->set( 'posts_per_page', 20 );
1264 }
1265 }
1266
1267 if ( $page_id ) {
1268 $query->set( 'page_id', $page_id );
1269 if ( ! $this->author ) {
1270 $post = get_post( $page_id );
1271 $author = User::get_post_author( $post );
1272 if ( false !== $author ) {
1273 $this->author = $author;
1274 }
1275 }
1276 $query->is_single = true;
1277 $query->is_singular = true;
1278 $wp->set_query_var( 'page', null );
1279 $wp->set_query_var( 'page_id', $page_id );
1280 }
1281
1282 if ( $this->author ) {
1283 if ( $this->author instanceof User ) {
1284 $authordata = $this->author;
1285 $this->author->modify_query_by_author( $query );
1286 if ( ! $page_id ) {
1287 $query->is_author = true;
1288 }
1289 } else {
1290 $this->author = null;
1291 }
1292 }
1293
1294 if ( ! $query->is_singular && ! $query->is_author ) {
1295 // This is the main friends page.
1296 $hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' );
1297 $hide_from_friends_page = array_diff( array_map( 'intval', (array) $hide_from_friends_page ), array( 0 ) );
1298
1299 if ( $hide_from_friends_page ) {
1300 $query->set( 'author__not_in', $hide_from_friends_page );
1301 }
1302 }
1303
1304 return $query;
1305 }
1306 }
1307