PluginProbe
Friends / 2.7.6
Friends v2.7.6
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
friends / includes / class-frontend.php

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

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