PluginProbe
Friends / 2.8.3
Friends v2.8.3
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.3, at includes/class-frontend.php

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