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

1,290 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['post_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' => get_user_option( 'friends_blocks_everywhere' ),
773 );
774
775 if ( isset( $_GET['in_reply_to'] ) && wp_parse_url( $_GET['in_reply_to'] ) ) {
776 $args['in_reply_to'] = $args['friends']->frontend->get_in_reply_to_metadata( $_GET['in_reply_to'] );
777 }
778
779 return Friends::template_loader()->get_template_part( 'frontend/index', null, $args, false );
780 }
781
782 /**
783 * Modify the Friends page title depending on context, for example add an author name or post format.
784 *
785 * @param string $title The original title.
786 *
787 * @return string The modified title.
788 */
789 public function header_widget_title( $title ) {
790 $title = '<a href="' . esc_url( home_url( '/friends/' ) ) . '">' . esc_html( $title ) . '</a>';
791 if ( $this->author ) {
792 $title .= ' &raquo; ' . '<a href="' . esc_url( $this->author->get_local_friends_page_url() ) . '">' . esc_html( $this->author->display_name ) . '</a>';
793 }
794 if ( $this->post_format ) {
795 $post_formats = get_post_format_strings();
796 $title .= ' &raquo; ' . $post_formats[ $this->post_format ];
797 }
798 return $title;
799 }
800
801 /**
802 * Output a link, potentially augmented with authication information.
803 *
804 * @param string $url The url.
805 * @param string $text The link text.
806 * @param array $html_attributes HTML attributes.
807 * @param User $friend_user The friend user.
808 */
809 public function link( $url, $text, array $html_attributes = array(), User $friend_user = null ) {
810 echo wp_kses(
811 self::get_link( $url, $text, $html_attributes, $friend_user ),
812 array(
813 'a' => array(
814 'href' => array(),
815 'title' => array(),
816 'target' => array(),
817 'rel' => array(),
818 'class' => array(),
819 'style' => array(),
820 'data-nonce' => array(),
821 'data-cnonce' => array(),
822 'data-token' => array(),
823 'data-friend' => array(),
824 'data-id' => array(),
825 'data-url' => array(),
826 ),
827 'span' => array( 'class' => array() ),
828 )
829 );
830 }
831
832 /**
833 * Get a link, potentially augmented with authication information.
834 *
835 * @param string $url The url.
836 * @param string $text The link text.
837 * @param array $html_attributes HTML attributes.
838 * @param User $friend_user The friend user.
839 *
840 * @return string The link.
841 */
842 public static function get_link( $url, $text, array $html_attributes = array(), User $friend_user = null ) {
843 if ( is_null( $friend_user ) ) {
844 $friend_user = new User( get_the_author_meta( 'ID' ) );
845 }
846
847 if ( $friend_user->is_friend_url( $url ) && $friend_user->is_valid_friend() ) {
848 $html_attributes['target'] = '_blank';
849 $html_attributes['rel'] = 'noopener noreferrer';
850 if ( ! isset( $html_attributes['class'] ) ) {
851 $html_attributes['class'] = '';
852 }
853 $html_attributes['class'] = trim( $html_attributes['class'] . ' friends-auth-link' );
854 if ( ! isset( $html_attributes['dashicon_back'] ) ) {
855 $html_attributes['dashicon_back'] = 'admin-users';
856 }
857 $html_attributes['data-token'] = $friend_user->get_friend_auth();
858 $html_attributes['data-nonce'] = wp_create_nonce( 'auth-link-' . $url );
859 $html_attributes['data-friend'] = $friend_user->user_login;
860 }
861
862 $link = '<a href="' . esc_url( $url ) . '"';
863 foreach ( $html_attributes as $name => $value ) {
864 if ( ! in_array( $name, array( 'title', 'target', 'rel', 'class', 'style', 'data-nonce', 'data-cnonce', 'data-token', 'data-friend', 'data-id', 'data-url' ) ) ) {
865 continue;
866 }
867 $link .= ' ' . $name . '="' . esc_attr( $value ) . '"';
868 }
869 $link .= '>';
870 if ( isset( $html_attributes['dashicon_front'] ) ) {
871 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_front'] ) . '"></span>';
872 }
873 $link .= esc_html( $text );
874 if ( isset( $html_attributes['dashicon_back'] ) ) {
875 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_back'] ) . '"></span>';
876 }
877 $link .= '</a>';
878
879 return $link;
880 }
881
882 /**
883 * Don't show the edit link for friend posts.
884 *
885 * @param string $link The edit link.
886 * @return string|bool The edit link or false.
887 */
888 public function friend_post_edit_link( $link ) {
889 global $post;
890
891 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
892 if ( Friends::on_frontend() ) {
893 $new_link = false;
894 } else {
895 $new_link = get_the_guid( $post );
896 }
897 /**
898 * Allow overriding the link for editing friend posts.
899 *
900 * By default on the Frontend, a post cannot be edited because $new_link is false.
901 *
902 * Example:
903 *
904 * ```php
905 * add_filter( 'friend_post_edit_link', function( $link, $original_link ) {
906 * if ( ! $link ) {
907 * $link = $original_link; // always allow editing.
908 * }
909 * return $link;
910 * }, 10, 2 );
911 * ```
912 */
913 return apply_filters( 'friend_post_edit_link', $new_link, $link );
914 }
915 return $link;
916 }
917
918 /**
919 * Link friend posts to the remote site.
920 *
921 * @param string $post_link The post's permalink.
922 * @param \WP_Post $post The post in question.
923 * @reeturn string The overriden post link.
924 */
925 public function friend_post_link( $post_link, \WP_Post $post ) {
926 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
927 return get_the_guid( $post );
928 }
929 return $post_link;
930 }
931
932 /**
933 * Potentially override the post author name with metadata.
934 *
935 * @param string $overridden_author_name The already overridden author name.
936 * @param string $author_name The author name.
937 * @param int $post_id The post id.
938 *
939 * @return string The modified author name.
940 */
941 public function override_author_name( $overridden_author_name, $author_name, $post_id ) {
942 if ( $overridden_author_name && $overridden_author_name !== $author_name ) {
943 return $overridden_author_name;
944 }
945 $override_author_name = get_post_meta( $post_id, 'author', true );
946 if ( $override_author_name ) {
947 return $override_author_name;
948 }
949 return $author_name;
950 }
951
952 /**
953 * Render the Friends OPML
954 *
955 * @param bool $only_public Only public feed URLs.
956 */
957 protected function render_opml( $only_public = false ) {
958 $user = wp_get_current_user();
959
960 // translators: %s is a name.
961 $title = sprintf( __( "%s' Subscriptions", 'friends' ), $user->display_name );
962 $filename = 'friends-';
963 if ( ! $only_public ) {
964 $title = __( 'My Friends', 'friends' );
965 $filename .= 'private-';
966 }
967 $filename .= $user->user_login . '.opml';
968
969 $feeds = array();
970 $users = array();
971
972 $friend_users = new User_Query( array( 'role__in' => array( 'friend', 'acquaintance', 'friend_request', 'subscription' ) ) );
973 foreach ( $friend_users->get_results() as $friend_user ) {
974 $role = $friend_user->get_role_name( true, 9 );
975 if ( $only_public ) {
976 $role = 'Feeds';
977 }
978 if ( ! isset( $users[ $role ] ) ) {
979 $users[ $role ] = array();
980 }
981
982 $users[ $role ][] = $friend_user;
983 }
984 ksort( $users );
985 foreach ( $users as $role => $friend_users ) {
986 foreach ( $friend_users as $friend_user ) {
987 $user_feeds = $friend_user->get_active_feeds();
988
989 $need_local_feed = false;
990
991 foreach ( $user_feeds as $feed ) {
992 switch ( $feed->get_mime_type() ) {
993 case 'application/atom+xml':
994 case 'application/atomxml':
995 case 'application/rss+xml':
996 case 'application/rssxml':
997 break;
998 default:
999 $need_local_feed = true;
1000 break 2;
1001 }
1002 }
1003
1004 if ( $need_local_feed && ! $only_public ) {
1005 $user_feeds = array_slice( $user_feeds, 0, 1 );
1006 }
1007
1008 $user = array(
1009 'friend_user' => $friend_user,
1010 'feeds' => array(),
1011 );
1012 $html_url = $friend_user->user_url;
1013
1014 foreach ( $user_feeds as $feed ) {
1015 $type = 'rss';
1016 $feed_title = $friend_user->display_name;
1017
1018 if ( ! $only_public ) {
1019 $html_url = $feed->get_local_html_url();
1020 }
1021
1022 if ( $need_local_feed ) {
1023 if ( $only_public ) {
1024 // Cannot create a public URL.
1025 continue;
1026 }
1027 $xml_url = $feed->get_local_url() . '?auth=' . $_GET['auth'];
1028 } else {
1029 if ( $only_public ) {
1030 $xml_url = $feed->get_url();
1031 } else {
1032 $xml_url = $feed->get_private_url( YEAR_IN_SECONDS );
1033 }
1034 if ( 'application/atom+xml' === $feed->get_mime_type() ) {
1035 $type = 'atom';
1036 }
1037 }
1038 $user['feeds'][] = array(
1039 'xml_url' => $xml_url,
1040 'html_url' => $html_url,
1041 'title' => $feed_title,
1042 'type' => $type,
1043 );
1044 }
1045
1046 if ( ! empty( $user['feeds'] ) ) {
1047 if ( ! isset( $feeds[ $role ] ) ) {
1048 $feeds[ $role ] = array();
1049 }
1050 $feeds[ $role ][] = $user;
1051 }
1052 }
1053 }
1054 Friends::template_loader()->get_template_part(
1055 'admin/opml',
1056 null,
1057 array(
1058 'title' => $title,
1059 'feeds' => $feeds,
1060 'filename' => $filename,
1061 )
1062 );
1063 exit;
1064 }
1065
1066 private function has_required_priviledges() {
1067 if ( Friends::is_main_user() ) {
1068 return true;
1069 }
1070
1071 if ( is_multisite() ) {
1072 if ( is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
1073 return true;
1074 }
1075
1076 if ( is_super_admin( get_current_user_id() ) ) {
1077 // Super admins would count as administrators.
1078 return false;
1079 }
1080 }
1081
1082 if ( current_user_can( 'manage_options' ) ) {
1083 return true;
1084 }
1085
1086 return false;
1087 }
1088
1089 /**
1090 * Modify the main query for the /friends page
1091 *
1092 * @param \WP_Query $query The main query.
1093 * @return \WP_Query The modified main query.
1094 */
1095 public function friend_posts_query( $query ) {
1096 global $wp_query, $wp, $authordata;
1097 if ( $wp_query !== $query || $query->is_admin() || $query->is_home() ) {
1098 return $query;
1099 }
1100
1101 $pagename = '';
1102 if ( isset( $wp_query->query['pagename'] ) ) {
1103 $pagename = $wp_query->query['pagename'];
1104 } elseif ( isset( $wp_query->query['name'] ) ) {
1105 $pagename = $wp_query->query['name'];
1106 }
1107
1108 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
1109 $is_friends = array_shift( $pagename_parts );
1110 if ( 'friends' !== $is_friends ) {
1111 return $query;
1112 }
1113
1114 // Not available for the general public or friends.
1115 $viewable = $this->has_required_priviledges() && Friends::on_frontend();
1116 if ( $query->is_feed() ) {
1117 // Feeds can be viewed through extra authentication.
1118 if ( $this->friends->access_control->private_rss_is_authenticated() ) {
1119 $viewable = true;
1120 } elseif ( isset( $wp_query->query['pagename'] ) ) {
1121 if ( apply_filters( 'friends_friend_feed_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
1122 $viewable = true;
1123 }
1124 }
1125 }
1126
1127 if ( ! $viewable && isset( $wp_query->query['pagename'] ) ) {
1128 if ( apply_filters( 'friends_friend_posts_query_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
1129 $viewable = true;
1130 }
1131 }
1132
1133 $page_id = get_query_var( 'page' );
1134
1135 if ( isset( $_GET['share'] ) ) {
1136 $share_hash = hash( 'crc32b', apply_filters( 'friends_share_salt', wp_salt( 'nonce' ), $page_id ) . $page_id );
1137 if ( $_GET['share'] === $share_hash ) {
1138 $viewable = true;
1139 }
1140 }
1141
1142 if ( ! $viewable ) {
1143 if ( $query->is_feed() ) {
1144 status_header( 404 );
1145 $query->set_404();
1146 } elseif ( ! Friends::on_frontend() ) {
1147 if ( count( $pagename_parts ) > 0 ) {
1148 wp_safe_redirect( home_url( '/friends/' ) );
1149 exit;
1150 }
1151 } elseif ( ! Friends::is_main_user() ) {
1152 wp_die( __( 'You are not allowed to view this page.', 'friends' ) );
1153 }
1154
1155 return $query;
1156 }
1157
1158 // Don't let the Post Kinds plugin interfere with this query.
1159 remove_action( 'pre_get_posts', array( 'Kind_Taxonomy', 'kind_firehose_query' ), 99 );
1160
1161 switch_to_locale( get_user_locale() );
1162
1163 $tax_query = array();
1164 $post_format = null;
1165
1166 while ( $pagename_parts ) {
1167 $pagename_part = $pagename_parts[0];
1168 $current_part = array_shift( $pagename_parts );
1169 if ( 'reaction' === substr( $current_part, 0, 8 ) && strlen( $current_part ) > 8 ) {
1170 $reaction = Reactions::validate_emoji( substr( $current_part, 8 ) );
1171 if ( ! $reaction ) {
1172 continue;
1173 }
1174 $this->reaction = $reaction;
1175 if ( ! empty( $tax_query ) ) {
1176 $tax_query['relation'] = 'AND';
1177 }
1178
1179 $tax_query[] = array(
1180 'taxonomy' => 'friend-reaction-' . get_current_user_id(),
1181 'field' => 'slug',
1182 'terms' => array( substr( $current_part, 8 ) ),
1183 );
1184 continue;
1185 }
1186
1187 switch ( $current_part ) {
1188 case 'opml':
1189 return $this->render_opml( isset( $_REQUEST['public'] ) );
1190
1191 case 'type':
1192 $post_format = array_shift( $pagename_parts );
1193 $tax_query = $this->friends->wp_query_get_post_format_tax_query( $tax_query, explode( ',', $post_format ) );
1194 if ( $tax_query ) {
1195 $this->post_format = $post_format;
1196 }
1197 break;
1198
1199 default: // Maybe an author.
1200 $author = User::get_by_username( $current_part );
1201 if ( false === $author || is_wp_error( $author ) ) {
1202 if ( $query->is_feed() ) {
1203 status_header( 404 );
1204 $query->set_404();
1205 return $query;
1206 }
1207 wp_safe_redirect( home_url( '/friends/' ) );
1208 exit;
1209 }
1210
1211 $this->author = $author;
1212 break;
1213 }
1214 }
1215
1216 $this->is_friends_page = true;
1217 $query->is_friends_page = true;
1218 $query->is_singular = false;
1219 $query->is_single = false;
1220 $query->queried_object = null;
1221 $query->queried_object_id = null;
1222 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1223
1224 if ( 'status' === $post_format ) {
1225 // Show your own posts on the status feed.
1226 $post_types[] = 'post';
1227 }
1228
1229 $query->set( 'post_type', $post_types );
1230 $query->set( 'tax_query', $tax_query );
1231
1232 if ( ( isset( $_GET['share'] ) && $_GET['share'] === $share_hash ) || $viewable ) {
1233 $post_status = array( 'publish', 'private' );
1234 if ( isset( $_GET['show-hidden'] ) ) {
1235 $post_status[] = 'trash';
1236 }
1237 $query->set( 'post_status', $post_status );
1238 }
1239 $query->is_page = false;
1240 $query->is_comments_feed = false;
1241 $query->set( 'pagename', null );
1242 if ( 'collapsed' === get_option( 'friends_frontend_default_view', 'expanded' ) && get_option( 'posts_per_page' ) < 20 ) {
1243 if ( 'status' === $post_format ) {
1244 $query->set( 'posts_per_page', 30 );
1245 } else {
1246 $query->set( 'posts_per_page', 20 );
1247 }
1248 }
1249
1250 if ( $page_id ) {
1251 $query->set( 'page_id', $page_id );
1252 if ( ! $this->author ) {
1253 $post = get_post( $page_id );
1254 $author = User::get_post_author( $post );
1255 if ( false !== $author ) {
1256 $this->author = $author;
1257 }
1258 }
1259 $query->is_single = true;
1260 $query->is_singular = true;
1261 $wp->set_query_var( 'page', null );
1262 $wp->set_query_var( 'page_id', $page_id );
1263 }
1264
1265 if ( $this->author ) {
1266 if ( $this->author instanceof User ) {
1267 $authordata = $this->author;
1268 $this->author->modify_query_by_author( $query );
1269 if ( ! $page_id ) {
1270 $query->is_author = true;
1271 }
1272 } else {
1273 $this->author = null;
1274 }
1275 }
1276
1277 if ( ! $query->is_singular && ! $query->is_author ) {
1278 // This is the main friends page.
1279 $hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' );
1280 $hide_from_friends_page = array_diff( array_map( 'intval', (array) $hide_from_friends_page ), array( 0 ) );
1281
1282 if ( $hide_from_friends_page ) {
1283 $query->set( 'author__not_in', $hide_from_friends_page );
1284 }
1285 }
1286
1287 return $query;
1288 }
1289 }
1290