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

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