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

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

2,414 lines 77.9 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 an tag is being displayed
44 *
45 * @var object|false
46 */
47 public $tag = false;
48
49 /**
50 * Whether a post-format is being displayed
51 *
52 * @var string|false
53 */
54 public $post_format = false;
55
56 /**
57 * Whether a specific template was selected to load.
58 *
59 * @var string|false
60 */
61 public $template = false;
62
63 /**
64 * Whether a reaction is being displayed.
65 *
66 * @var string|false
67 */
68 public $reaction = false;
69
70 /**
71 * The current theme.
72 *
73 * @var string
74 */
75 private $theme = 'default';
76 /**
77 * Available themes.
78 *
79 * @var array
80 */
81 private static $themes = array();
82
83 /**
84 * Constructor
85 *
86 * @param Friends $friends A reference to the Friends object.
87 */
88 public function __construct( Friends $friends ) {
89 $this->friends = $friends;
90 $this->register_hooks();
91 }
92
93 /**
94 * Register the WordPress hooks
95 */
96 private function register_hooks() {
97 add_filter( 'pre_get_posts', array( $this, 'friend_posts_query' ), 2 );
98 add_filter( 'pre_get_posts', array( $this, 'exclude_compose_format_from_feed' ), 10 );
99 add_filter( 'post_type_link', array( $this, 'friend_post_link' ), 10, 2 );
100 add_filter( 'friends_header_widget_title', array( $this, 'header_widget_title' ) );
101 add_filter( 'get_edit_post_link', array( $this, 'friend_post_edit_link' ) );
102 add_filter( 'template_include', array( $this, 'template_override' ) );
103 add_filter( 'wp_loaded', array( $this, 'add_rewrite_rule' ) );
104 add_filter( 'init', array( $this, 'register_friends_sidebar' ) );
105 add_action( 'init', array( $this, 'add_theme_supports' ) );
106 add_action( 'wp_ajax_friends_publish', array( $this, 'ajax_frontend_publish_post' ) );
107 add_action( 'wp_ajax_friends-mention-autocomplete', array( $this, 'ajax_mention_autocomplete' ) );
108 add_action( 'wp_ajax_friends-change-post-format', array( $this, 'ajax_change_post_format' ) );
109 add_action( 'wp_ajax_friends-load-next-page', array( $this, 'ajax_load_next_page' ) );
110 add_action( 'wp_ajax_friends-autocomplete', array( $this, 'ajax_autocomplete' ) );
111 add_action( 'wp_ajax_friends-set-widget-open-state', array( $this, 'ajax_set_widget_open_state' ) );
112 add_action( 'wp_ajax_friends-get-post-counts', array( $this, 'ajax_get_post_counts' ) );
113 add_action( 'friends_search_autocomplete', array( $this, 'autocomplete_user_search' ), 10, 2 );
114 add_action( 'wp_ajax_friends-star', array( $this, 'ajax_star_friend_user' ) );
115 add_action( 'wp_ajax_friends-move-to-folder', array( $this, 'ajax_move_to_folder' ) );
116 add_action( 'wp_ajax_friends-create-folder', array( $this, 'ajax_create_folder' ) );
117 add_action( 'wp_ajax_friends-load-comments', array( $this, 'ajax_load_comments' ) );
118 add_action( 'wp_ajax_friends-reblog', array( $this, 'wp_ajax_reblog' ) );
119 add_action( 'friends_post_footer_first', array( $this, 'reblog_button' ) );
120 add_filter( 'friends_reblog', array( get_called_class(), 'reblog' ), 10, 3 );
121 add_filter( 'friends_unreblog', array( get_called_class(), 'unreblog' ), 10, 2 );
122 add_action( 'wp_untrash_post_status', array( $this, 'untrash_post_status' ), 10, 2 );
123 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
124 add_action( 'template_redirect', array( $this, 'load_theme' ) );
125 add_action( 'admin_bar_menu', array( $this, 'admin_bar_theme_switcher' ), 100 );
126 add_action( 'customize_loaded_components', array( $this, 'ensure_widget_editing' ) );
127 add_action( 'friends_load_theme_default', array( $this, 'default_theme' ) );
128 add_action( 'friends_load_theme_block', array( $this, 'block_theme' ) );
129 add_action( 'friends_load_theme_google-reader', array( $this, 'google_reader_theme' ) );
130 add_action( 'friends_load_theme_mastodon', array( $this, 'mastodon_theme' ) );
131 add_action( 'friends_load_theme_twitter', array( $this, 'twitter_theme' ) );
132 add_action( 'friends_load_themes', array( $this, 'register_block_theme' ) );
133 add_action( 'friends_load_themes', array( $this, 'register_google_reader_theme' ) );
134 add_action( 'friends_load_themes', array( $this, 'register_mastodon_theme' ) );
135 add_action( 'friends_load_themes', array( $this, 'register_twitter_theme' ) );
136 add_action( 'init', array( $this, 'register_block_templates' ) );
137 add_action( 'friends_template_paths', array( $this, 'friends_template_paths' ) );
138 add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_scripts' ), 99999 );
139 add_action( 'wp_footer', array( $this, 'dequeue_scripts' ) );
140 add_action( 'the_post', array( $this, 'the_post' ), 10, 2 );
141 add_action( 'parse_query', array( $this, 'parse_query' ) );
142 add_filter( 'body_class', array( $this, 'add_body_class' ) );
143 add_filter( 'block_type_metadata_settings', array( $this, 'block_type_metadata_settings' ), 15 );
144 add_filter( 'pre_render_block', array( $this, 'render_friends_template_part' ), 10, 2 );
145 add_filter( 'pre_get_block_file_template', array( $this, 'get_friends_block_file_template' ), 10, 3 );
146 add_filter( 'get_block_templates', array( $this, 'add_friends_template_parts' ), 10, 3 );
147 add_filter( 'tag_row_actions', array( $this, 'tag_row_actions' ), 10, 2 );
148
149 add_action( 'friends_after_header', array( $this, 'migration_notification' ) );
150 add_action( 'wp_ajax_friends_dismiss_migration_notification', array( $this, 'ajax_dismiss_migration_notification' ) );
151
152 add_filter( 'friends_override_author_name', array( $this, 'override_author_name' ), 10, 3 );
153 add_filter( 'friends_friend_posts_query_viewable', array( $this, 'expose_opml' ), 10, 2 );
154 add_filter( 'get_comment_link', array( $this, 'friend_post_comment_link' ), 10, 2 );
155 }
156
157 /**
158 * We're asking WordPress to handle the title for us.
159 */
160 public function add_rewrite_rule() {
161 $existing_rules = get_option( 'rewrite_rules' );
162 $needs_flush = false;
163
164 $rules = array(
165 '^friends/(.*)/(?:feed/)?(feed|rdf|rss|rss2|atom)/?$' => 'index.php?pagename=friends/$matches[1]&feed=$matches[2]',
166 '^friends/feed/?$' => 'index.php?pagename=friends&feed=feed',
167 '^friends/(.*)/(\d+)/?$' => 'index.php?pagename=friends/$matches[1]&page=$matches[2]',
168 '^friends/(.*)' => 'index.php?pagename=friends/$matches[1]',
169 );
170
171 foreach ( $rules as $rule => $rewrite ) {
172 if ( empty( $existing_rules[ $rule ] ) ) {
173 $needs_flush = true;
174 }
175 add_rewrite_rule( $rule, $rewrite, 'top' );
176 }
177
178 if ( $needs_flush ) {
179 flush_rewrite_rules();
180 }
181 }
182
183 /**
184 * Run our add_theme_supports if on the frontend.
185 */
186 public function add_theme_supports() {
187 if ( ! Friends::on_frontend() ) {
188 return;
189 }
190
191 $this->add_theme_support_title_tag();
192 $this->add_theme_support_admin_bar();
193 }
194
195 /**
196 * We're asking WordPress to handle the title for us.
197 */
198 private function add_theme_support_title_tag() {
199 add_theme_support( 'title-tag' );
200 add_filter( 'document_title_parts', array( $this, 'modify_page_title' ) );
201 }
202
203 /**
204 * Remove the margin-top on the friends page.
205 */
206 private function add_theme_support_admin_bar() {
207 add_theme_support(
208 'admin-bar',
209 array(
210 'callback' => '__return_false',
211 )
212 );
213 }
214
215 /**
216 * Modify the page title to be output. This function is only invoked on the friends page.
217 *
218 * @param array $title The title.
219 *
220 * @return array The modified title.
221 */
222 public function modify_page_title( $title ) {
223 if ( is_single() ) {
224 $title['page'] = __( 'Friends', 'friends' );
225 $title['site'] = $this->author->display_name;
226 } elseif ( $this->author instanceof User ) {
227 $title['title'] = __( 'Friends', 'friends' );
228 $title['site'] = $this->author->display_name;
229 } else {
230 $title = array(
231 'site' => __( 'Friends', 'friends' ),
232 );
233 }
234 return $title;
235 }
236
237 /**
238 * Registers the sidebar for the /friends page.
239 */
240 public function register_friends_sidebar() {
241 register_sidebar(
242 array(
243 'name' => 'Friends Topbar',
244 'id' => 'friends-topbar',
245 'before_widget' => '<div class="friends-main-widget">',
246 'after_widget' => '</div>',
247 'before_title' => '<h1>',
248 'after_title' => '</h1>',
249 )
250 );
251 register_sidebar(
252 array(
253 'name' => 'Friends Sidebar',
254 'id' => 'friends-sidebar',
255 'before_widget' => '<div class="friends-widget">',
256 'after_widget' => '</div>',
257 'before_title' => '<h5>',
258 'after_title' => '</h5>',
259 )
260 );
261 register_sidebar(
262 array(
263 'name' => __( 'Friends Sidebar 2 (Twitter theme)', 'friends' ),
264 'id' => 'friends-sidebar-2',
265 'description' => __( 'Shown in the right column of the Twitter theme.', 'friends' ),
266 'before_widget' => '<div class="friends-widget">',
267 'after_widget' => '</div>',
268 'before_title' => '<h5>',
269 'after_title' => '</h5>',
270 )
271 );
272
273 if ( Friends::on_frontend() ) {
274 add_action( 'customize_register', '__return_true' );
275 }
276 }
277
278 public static function ensure_widget_editing( $components ) {
279 if ( ! is_array( $components ) ) {
280 $components = array();
281 }
282 $components[] = 'widgets';
283 return $components;
284 }
285
286 public function load_theme() {
287 if ( ! is_user_logged_in() || ! Friends::on_frontend() ) {
288 return;
289 }
290 // Handle theme switching via GET parameter.
291 if ( isset( $_GET['friends_theme'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
292 $new_theme = sanitize_text_field( wp_unslash( $_GET['friends_theme'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
293 $themes = self::get_themes();
294 if ( isset( $themes[ $new_theme ] ) ) {
295 update_user_option( get_current_user_id(), 'friends_frontend_theme', $new_theme );
296 }
297 }
298
299 $theme = 'default';
300 $default_theme = get_user_option( 'friends_frontend_theme', get_current_user_id() );
301 if ( $default_theme ) {
302 $theme = $default_theme;
303 }
304 if ( $this->post_format ) {
305 $post_type_theme = get_user_option( 'friends_frontend_theme_' . $this->post_format, get_current_user_id() );
306 if ( $post_type_theme ) {
307 $theme = $post_type_theme;
308 }
309 }
310 if ( ! has_action( 'friends_load_theme_' . $theme ) ) {
311 $theme = 'default';
312 }
313 $this->theme = $theme;
314 do_action( 'friends_load_theme_' . $theme );
315 }
316
317 public function friends_template_paths( $file_paths ) {
318 $backup_file_paths = $file_paths;
319 if ( has_filter( 'friends_template_paths_theme_' . $this->theme ) ) {
320 $file_paths = apply_filters( 'friends_template_paths_theme_' . $this->theme, $file_paths );
321 }
322 if ( empty( $file_paths ) ) {
323 return $backup_file_paths;
324 }
325
326 return $file_paths;
327 }
328
329 public function default_theme() {
330 $handle = 'friends';
331 $file = 'friends.css';
332 $version = Friends::VERSION;
333 wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
334
335 wp_enqueue_script( 'friends-default-theme', plugins_url( 'friends-default-theme.js', FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $version, true );
336 }
337
338 /**
339 * Reference our script for the /friends page
340 */
341 public function enqueue_scripts() {
342 if ( ! is_user_logged_in() || ! Friends::on_frontend() ) {
343 return;
344 }
345 global $wp_query;
346
347 $handle = 'friends';
348 $file = 'friends.js';
349 $version = Friends::VERSION;
350 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 ), true );
351
352 $query_vars = wp_json_encode( $this->get_minimal_query_vars( $wp_query->query_vars ) );
353
354 $variables = array(
355 'emojis_json' => plugins_url( 'emojis.json', FRIENDS_PLUGIN_FILE ),
356 'ajax_url' => admin_url( 'admin-ajax.php' ),
357 'rest_base' => rest_url( 'friends/v1/' ),
358 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
359 'text_link_expired' => __( 'The link has expired. A new link has been generated, please click it again.', 'friends' ),
360 'text_undo' => __( 'Undo' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
361 'text_trash_post' => __( 'Trash this post', 'friends' ),
362 'text_del_convers' => __( 'Do you really want to delete this conversation?', 'friends' ),
363 'text_no_more_posts' => __( 'No more posts available.', 'friends' ),
364 'text_checking_url' => __( 'Checking URL.', 'friends' ),
365 'text_refreshed' => __( 'Refreshed', 'friends' ),
366 'text_refreshing' => __( 'Refreshing', 'friends' ),
367 'text_compact_mode' => __( 'Compact mode', 'friends' ),
368 'text_expanded_mode' => __( 'Expanded mode', 'friends' ),
369 'text_loading_comments' => __( 'Loading comments...', 'friends' ),
370 'text_still_loading' => __( 'Still loading...', 'friends' ),
371 'refresh_now' => isset( $_GET['refresh'] ) ? 'true' : 'false', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
372 'query_vars' => $query_vars,
373 'qv_sign' => sha1( wp_salt( 'nonce' ) . $query_vars ),
374 'current_page' => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1,
375 'max_page' => $wp_query->max_num_pages,
376 );
377
378 // translators: %s is a user handle.
379 $variables['text_confirm_delete_follower'] = __( 'Do you really want to delete the follower %s?', 'friends' );
380 $variables['text_new_folder'] = __( 'Folder name:', 'friends' );
381 $variables['text_loading'] = __( 'Loading...', 'friends' );
382 $variables['text_discovering'] = __( 'Discovering feeds...', 'friends' );
383 $variables['text_ready_to_follow'] = __( 'Ready', 'friends' );
384 $variables['text_follow'] = __( 'Follow', 'friends' );
385 $variables['text_follow_selected'] = __( 'Follow selected', 'friends' );
386 $variables['text_review_selected'] = __( 'Review selected', 'friends' );
387 $variables['text_skip'] = __( 'Skip', 'friends' );
388 $variables['text_error'] = __( 'An error occurred.', 'friends' );
389 $variables['text_display_name'] = __( 'Display Name', 'friends' );
390 $variables['text_username'] = __( 'Username', 'friends' );
391 $variables['text_post_format'] = __( 'Post Format', 'friends' );
392 $variables['text_feed_parser'] = __( 'Parser', 'friends' );
393 $variables['text_feed_metadata'] = __( 'Display feed metadata', 'friends' );
394 $variables['text_hide_feed_metadata'] = __( 'Hide feed metadata', 'friends' );
395 $variables['text_preview_feed'] = __( 'Preview feed', 'friends' );
396 $variables['text_hide_preview'] = __( 'Hide preview', 'friends' );
397 $variables['text_no_preview_items'] = __( 'No preview items were found.', 'friends' );
398 $variables['text_add_feed_url'] = __( 'Add feed URL', 'friends' );
399 $variables['text_feed_url'] = __( 'Feed URL', 'friends' );
400 $variables['text_custom_feed'] = __( 'Custom feed', 'friends' );
401 $variables['text_feed_url_required'] = __( 'Please enter a feed URL.', 'friends' );
402 $variables['text_show_more_feeds'] = __( 'Show more feeds', 'friends' );
403 $variables['text_show_unsupported_feeds'] = __( 'Show unsupported feeds', 'friends' );
404 $variables['text_no_supported_feeds'] = __( 'No supported feeds discovered.', 'friends' );
405 $variables['text_no_feed_selected'] = __( 'Please select at least one feed.', 'friends' );
406 $variables['text_opml_no_feeds'] = __( 'No feeds were found in the OPML file.', 'friends' );
407 $variables['text_opml_invalid'] = __( 'Could not parse the OPML file.', 'friends' );
408 $variables['text_opml_select_all'] = __( 'Select all', 'friends' );
409 $variables['text_opml_deselect_all'] = __( 'Deselect all', 'friends' );
410 $variables['text_opml_import_selected'] = __( 'Review selected', 'friends' );
411 $variables['text_opml_no_selected'] = __( 'Please select at least one feed.', 'friends' );
412 $variables['text_opml_no_feeds_for_url'] = __( 'No feeds discovered.', 'friends' );
413 $variables['post_formats'] = array_merge( array( 'autodetect' => __( 'Autodetect Post Format', 'friends' ) ), get_post_format_strings() );
414 $variables['registered_parsers'] = array_map( 'wp_strip_all_tags', $this->friends->feed->get_registered_parsers() );
415 wp_localize_script( 'friends', 'friends', $variables );
416 }
417
418 public function dequeue_scripts() {
419 if ( is_user_logged_in() && Friends::on_frontend() && 'block' !== $this->theme ) {
420 // Dequeue theme styles so that they don't interact with the Friends frontend.
421 $wp_styles = wp_styles();
422 $theme_root_path = wp_parse_url( get_theme_root_uri(), PHP_URL_PATH );
423 foreach ( $wp_styles->queue as $style ) {
424 $src = $wp_styles->registered[ $style ]->src;
425 $src_path = wp_parse_url( $src, PHP_URL_PATH );
426 $is_theme_style = $theme_root_path && $src_path
427 && 0 === strpos( $src_path, trailingslashit( $theme_root_path ) );
428 if ( 'global-styles' === $style || $is_theme_style ) {
429 wp_dequeue_style( $style );
430 }
431 }
432 }
433 }
434
435 public function migration_notification() {
436 if ( ! Friends::on_frontend() ) {
437 return;
438 }
439 if ( ! Friends::has_pending_migrations() ) {
440 return;
441 }
442 ?>
443 <div class="friends-migration-notification">
444 <?php esc_html_e( 'Welcome to Friends 4.0!', 'friends' ); ?>
445 <?php esc_html_e( 'Some data migrations are still pending.', 'friends' ); ?>
446 <a href="<?php echo esc_url( admin_url( 'admin.php?page=friends' ) ); ?>"><?php esc_html_e( "Learn what's new \u{2192}", 'friends' ); ?></a>
447 <span class="friends-migration-notification-meta">
448 <small class="friends-migration-reappear" hidden><?php esc_html_e( 'Will reappear in 2 days.', 'friends' ); ?></small>
449 <button id="friends-dismiss-migration-notification" data-nonce="<?php echo esc_attr( wp_create_nonce( 'friends-dismiss-migration-notification' ) ); ?>"><?php esc_html_e( 'Dismiss', 'friends' ); ?></button>
450 </span>
451 </div>
452 <script>
453 document.getElementById( 'friends-dismiss-migration-notification' ).addEventListener( 'click', function() {
454 var btn = this;
455 var notification = btn.closest( '.friends-migration-notification' );
456 btn.hidden = true;
457 notification.querySelector( '.friends-migration-reappear' ).hidden = false;
458 fetch( <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>, {
459 method: 'POST',
460 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
461 body: 'action=friends_dismiss_migration_notification&_ajax_nonce=' + encodeURIComponent( btn.dataset.nonce ),
462 } ).then( function() {
463 notification.remove();
464 } );
465 } );
466 </script>
467 <?php
468 }
469
470 public function ajax_dismiss_migration_notification() {
471 check_ajax_referer( 'friends-dismiss-migration-notification' );
472 update_option( 'friends_migration_status', time(), false );
473 wp_send_json_success();
474 }
475
476 public function the_post( $post, $query ) {
477 Subscription::set_authordata_by_query( $query );
478 }
479
480 public function parse_query( $query ) {
481 Subscription::set_authordata_by_query( $query );
482 }
483
484 /**
485 * Add a CSS class to the body
486 *
487 * @param array $classes The existing CSS classes.
488 * @return array The modified CSS classes.
489 */
490 public function add_body_class( $classes ) {
491 if ( $this->friends->on_frontend() ) {
492 $classes[] = 'friends-page';
493 $classes[] = 'off-canvas';
494 $classes[] = 'off-canvas-sidebar-show';
495 }
496
497 return $classes;
498 }
499
500 public function register_google_reader_theme( Frontend $friends_frontend ) {
501 $friends_frontend->register_theme( 'Google Reader', 'google-reader' );
502 add_filter(
503 'friends_theme_name',
504 function ( $name, $slug ) {
505 return 'google-reader' === $slug ? __( 'Google Reader', 'friends' ) : $name;
506 },
507 10,
508 2
509 );
510 }
511
512 public function register_mastodon_theme( Frontend $friends_frontend ) {
513 $friends_frontend->register_theme( 'Mastodon', 'mastodon' );
514 add_filter(
515 'friends_theme_name',
516 function ( $name, $slug ) {
517 return 'mastodon' === $slug ? __( 'Mastodon', 'friends' ) : $name;
518 },
519 10,
520 2
521 );
522 }
523
524 public function mastodon_theme() {
525 $handle = 'friends-mastodon';
526 $file = 'templates/mastodon/mastodon.css';
527 $version = Friends::VERSION;
528 wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
529
530 wp_enqueue_script( 'friends-mastodon', plugins_url( 'templates/mastodon/mastodon.js', FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $version, true );
531 wp_localize_script(
532 'friends-mastodon',
533 'friendsMastodon',
534 array(
535 'mentionNonce' => wp_create_nonce( 'friends-mention-autocomplete' ),
536 )
537 );
538
539 add_filter(
540 'friends_template_paths_theme_mastodon',
541 function ( $file_paths ) {
542 $file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/mastodon/';
543 return $file_paths;
544 }
545 );
546 }
547
548 public function register_twitter_theme( Frontend $friends_frontend ) {
549 $friends_frontend->register_theme( 'Twitter', 'twitter' );
550 add_filter(
551 'friends_theme_name',
552 function ( $name, $slug ) {
553 return 'twitter' === $slug ? __( 'Twitter', 'friends' ) : $name;
554 },
555 10,
556 2
557 );
558 }
559
560 public function twitter_theme() {
561 $handle = 'friends-twitter';
562 $file = 'templates/twitter/twitter.css';
563 $version = Friends::VERSION;
564 wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
565
566 wp_enqueue_script( 'friends-twitter', plugins_url( 'templates/twitter/twitter.js', FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $version, true );
567 wp_localize_script(
568 'friends-twitter',
569 'friendsTwitter',
570 array(
571 'mentionNonce' => wp_create_nonce( 'friends-mention-autocomplete' ),
572 )
573 );
574
575 add_filter(
576 'friends_template_paths_theme_twitter',
577 function ( $file_paths ) {
578 $file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/twitter/';
579 return $file_paths;
580 }
581 );
582 }
583
584 public function google_reader_theme() {
585 $handle = 'friends-google-reader';
586 $file = 'templates/google-reader/google-reader.css';
587 $version = Friends::VERSION;
588 wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
589
590 wp_enqueue_script( 'friends-google-reader', plugins_url( 'templates/google-reader/google-reader.js', FRIENDS_PLUGIN_FILE ), array( 'jquery' ), $version, true );
591
592 add_filter(
593 'friends_template_paths_theme_google-reader',
594 function ( $file_paths ) {
595 // Add Google Reader templates at highest priority (lowest number), fall back to defaults.
596 $file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/google-reader/';
597 return $file_paths;
598 }
599 );
600 }
601
602 /**
603 * Add a theme switcher submenu to the Friends admin bar menu.
604 *
605 * @param \WP_Admin_Bar $wp_admin_bar The admin bar instance.
606 */
607 public function admin_bar_theme_switcher( $wp_admin_bar ) {
608 if ( ! is_user_logged_in() || ! Friends::on_frontend() ) {
609 return;
610 }
611
612 $current_theme = get_user_option( 'friends_frontend_theme', get_current_user_id() );
613 if ( ! $current_theme ) {
614 $current_theme = 'default';
615 }
616
617 $themes = self::get_themes();
618
619 $wp_admin_bar->add_node(
620 array(
621 'parent' => 'friends-menu',
622 'id' => 'friends-theme',
623 'title' => esc_html__( 'Theme', 'friends' ),
624 'href' => admin_url( 'admin.php?page=friends-settings' ),
625 )
626 );
627
628 $wp_admin_bar->add_group(
629 array(
630 'parent' => 'friends-theme',
631 'id' => 'friends-theme-list',
632 )
633 );
634
635 foreach ( $themes as $slug => $name ) {
636 $wp_admin_bar->add_node(
637 array(
638 'parent' => 'friends-theme-list',
639 'id' => 'friends-theme-' . $slug,
640 'title' => ( $slug === $current_theme ? '✓ ' : '' ) . esc_html( $name ),
641 'href' => add_query_arg( 'friends_theme', $slug ),
642 )
643 );
644 }
645 }
646
647 public function register_block_theme( Frontend $friends_frontend ) {
648 $friends_frontend->register_theme( 'Block Theme', 'block' );
649 add_filter(
650 'friends_theme_name',
651 function ( $name, $slug ) {
652 return 'block' === $slug ? __( 'Block Theme', 'friends' ) : $name;
653 },
654 10,
655 2
656 );
657 }
658
659 public function block_theme() {
660 // No theme swap needed; templates are registered as plugin templates
661 // and injected via template_override.
662 $handle = 'friends-blocks';
663 $file = 'friends-blocks.css';
664 $version = Friends::VERSION;
665 wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
666 }
667
668 /**
669 * Register Friends block templates as plugin templates.
670 */
671 public function register_block_templates() {
672 if ( ! wp_is_block_theme() || ! class_exists( 'WP_Block_Templates_Registry' ) ) {
673 return;
674 }
675
676 $registry = \WP_Block_Templates_Registry::get_instance();
677 $templates = array(
678 'friends//friends-index' => array(
679 'title' => __( 'Friends Feed', 'friends' ),
680 'content' => 'index',
681 ),
682 'friends//friends-author-index' => array(
683 'title' => __( 'Friends Author Feed', 'friends' ),
684 'content' => 'friends-author-index',
685 ),
686 'friends//friends-followers' => array(
687 'title' => __( 'Friends Followers', 'friends' ),
688 'content' => 'friends-followers',
689 ),
690 'friends//friends-add-friend' => array(
691 'title' => __( 'Friends Add Friend', 'friends' ),
692 'content' => 'friends-add-friend',
693 ),
694 'friends//friends-subscriptions' => array(
695 'title' => __( 'Friends Following', 'friends' ),
696 'content' => 'friends-subscriptions',
697 ),
698 'friends//friends-single' => array(
699 'title' => __( 'Friends Single Post', 'friends' ),
700 'content' => 'single-friend_post_cache',
701 ),
702 );
703
704 foreach ( $templates as $name => $args ) {
705 $file = FRIENDS_PLUGIN_DIR . 'themes/friends/templates/' . $args['content'] . '.html';
706 if ( file_exists( $file ) ) {
707 $registry->register(
708 $name,
709 array(
710 'title' => $args['title'],
711 'content' => file_get_contents( $file ), // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
712 )
713 );
714 }
715 }
716 }
717
718 /**
719 * Render template parts for the friends plugin.
720 *
721 * @param string|null $pre_render The pre-rendered content.
722 * @param array $parsed_block The parsed block.
723 * @return string|null The rendered content or null.
724 */
725 public function render_friends_template_part( $pre_render, $parsed_block ) {
726 if ( 'core/template-part' !== $parsed_block['blockName'] ) {
727 return $pre_render;
728 }
729
730 $attrs = $parsed_block['attrs'];
731 if ( ! isset( $attrs['theme'] ) || 'friends' !== $attrs['theme'] || ! isset( $attrs['slug'] ) ) {
732 return $pre_render;
733 }
734
735 // Check for user-customized version in the database first.
736 $custom_query = new \WP_Query(
737 array(
738 'post_type' => 'wp_template_part',
739 'post_status' => 'publish',
740 'post_name__in' => array( $attrs['slug'] ),
741 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
742 array(
743 'taxonomy' => 'wp_theme',
744 'field' => 'name',
745 'terms' => 'friends',
746 ),
747 ),
748 'posts_per_page' => 1,
749 'no_found_rows' => true,
750 )
751 );
752
753 if ( $custom_query->have_posts() ) {
754 $content = $custom_query->posts[0]->post_content;
755 } else {
756 $file = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/' . $attrs['slug'] . '.html';
757 if ( ! file_exists( $file ) ) {
758 return $pre_render;
759 }
760 $content = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
761 }
762
763 $tag = isset( $attrs['tagName'] ) ? $attrs['tagName'] : 'div';
764
765 return '<' . $tag . ' class="wp-block-template-part">' . do_blocks( $content ) . '</' . $tag . '>';
766 }
767
768 /**
769 * Build a WP_Block_Template for a friends template part.
770 *
771 * @param string $slug The template part slug.
772 * @param string $file The file path.
773 * @return WP_Block_Template
774 */
775 private function build_friends_template_part( $slug, $file ) {
776 $template = new \WP_Block_Template();
777 $template->id = 'friends//' . $slug;
778 $template->theme = 'friends';
779 $template->plugin = 'friends';
780 $template->slug = $slug;
781 $template->source = 'plugin';
782 $template->origin = 'plugin';
783 $template->type = 'wp_template_part';
784 $template->title = ucwords( str_replace( '-', ' ', $slug ) );
785 $template->status = 'publish';
786 $template->has_theme_file = true;
787 $template->is_custom = false;
788 $template->content = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
789 $template->area = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
790
791 return $template;
792 }
793
794 /**
795 * Resolve a friends template part by ID.
796 *
797 * @param WP_Block_Template|null $block_template The found block template.
798 * @param string $id Template unique identifier.
799 * @param string $template_type Template type.
800 * @return WP_Block_Template|null
801 */
802 public function get_friends_block_file_template( $block_template, $id, $template_type ) {
803 if ( 'wp_template_part' !== $template_type ) {
804 return $block_template;
805 }
806
807 $parts = explode( '//', $id, 2 );
808 if ( count( $parts ) < 2 || 'friends' !== $parts[0] ) {
809 return $block_template;
810 }
811
812 $slug = $parts[1];
813 $file = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/' . $slug . '.html';
814 if ( ! file_exists( $file ) ) {
815 return $block_template;
816 }
817
818 return $this->build_friends_template_part( $slug, $file );
819 }
820
821 /**
822 * Add friends template parts to block template queries.
823 *
824 * @param WP_Block_Template[] $query_result Array of found block templates.
825 * @param array $query Arguments to retrieve templates.
826 * @param string $template_type Template type.
827 * @return WP_Block_Template[]
828 */
829 public function add_friends_template_parts( $query_result, $query, $template_type ) {
830 if ( 'wp_template_part' !== $template_type ) {
831 return $query_result;
832 }
833
834 $parts_dir = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/';
835 if ( ! is_dir( $parts_dir ) ) {
836 return $query_result;
837 }
838
839 $slugs_to_include = isset( $query['slug__in'] ) ? $query['slug__in'] : array();
840
841 foreach ( glob( $parts_dir . '*.html' ) as $file ) {
842 $slug = basename( $file, '.html' );
843
844 if ( $slugs_to_include && ! in_array( $slug, $slugs_to_include, true ) ) {
845 continue;
846 }
847
848 // Don't add if already in results.
849 $exists = false;
850 foreach ( $query_result as $existing ) {
851 if ( $existing->slug === $slug && 'friends' === $existing->theme ) {
852 $exists = true;
853 break;
854 }
855 }
856 if ( ! $exists ) {
857 $query_result[] = $this->build_friends_template_part( $slug, $file );
858 }
859 }
860
861 return $query_result;
862 }
863
864 public function block_type_metadata_settings( $settings ) {
865 if ( ! Friends::on_frontend() || ! isset( $settings['name'] ) ) {
866 return $settings;
867 }
868 if ( 'core/post-author-name' === $settings['name'] ) {
869 $settings['render_callback'] = function ( $attributes, $content, $block ) {
870 if ( isset( $block->context['postId'] ) ) {
871 $author = User::get_post_author( get_post( $block->context['postId'] ) );
872 } else {
873 return '';
874 }
875 if ( empty( $author ) || is_wp_error( $author ) ) {
876 return '';
877 }
878
879 $author_name = $author->display_name;
880 $author_name_html = Feed_Parser_ActivityPub::replace_custom_emojis_for_user( $author_name, $author );
881 $override_author_name = apply_filters( 'friends_override_author_name', '', $author_name, $block->context['postId'] );
882 if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) {
883 $author_name_html = sprintf( '<a href="%1$s" target="%2$s" class="wp-block-post-author-name__link">%3$s</a>', esc_url( $author->get_local_friends_page_url() ), esc_attr( $attributes['linkTarget'] ), $author_name_html );
884 }
885
886 if ( $override_author_name && trim( str_replace( $override_author_name, '', $author_name ) ) === $author_name ) {
887 $author_name_html .= ' – ' . esc_html( $override_author_name );
888 }
889
890 $classes = array();
891 if ( isset( $attributes['textAlign'] ) ) {
892 $classes[] = 'has-text-align-' . $attributes['textAlign'];
893 }
894 if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
895 $classes[] = 'has-link-color';
896 }
897 $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) );
898
899 return sprintf(
900 '<div %1$s>%2$s</div>',
901 $wrapper_attributes,
902 wp_kses(
903 $author_name_html,
904 array_merge(
905 Feed_Parser_ActivityPub::get_custom_emoji_allowed_html(),
906 array(
907 'a' => array(
908 'class' => true,
909 'href' => true,
910 'target' => true,
911 ),
912 )
913 )
914 )
915 );
916 };
917 }
918 return $settings;
919 }
920 public function tag_row_actions( $actions, $tag ) {
921 $actions['view-friends'] = sprintf(
922 '<a href="%s">%s</a>',
923 esc_url( home_url( '/friends/tag/' . $tag->name ) ),
924 __( 'View on your Friends page', 'friends' )
925 );
926 return $actions;
927 }
928
929
930 /**
931 * Gets the minimal query variables.
932 *
933 * @param array $query_vars The query variables.
934 *
935 * @return array The minimal query variables.
936 */
937 private function get_minimal_query_vars( $query_vars ) {
938 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', 's' ) ) ) );
939 }
940
941 public function wp_ajax_reblog() {
942 if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['post_id'] ) ) {
943 wp_send_json_error( 'error' );
944 }
945
946 check_ajax_referer( 'friends-reblog' );
947
948 $post = get_post( intval( $_POST['post_id'] ) );
949 if ( ! $post || ! Friends::check_url( $post->guid ) ) {
950 wp_send_json_error( 'unknown-post', array( 'guid' => $post->guid ) );
951 }
952 $reblog_post_id = get_post_meta( get_the_ID(), 'reblogged', true );
953 if ( $reblog_post_id ) {
954 wp_send_json_success(
955 array(
956 'post_id' => $reblog_post_id,
957 'redirect' => get_edit_post_link( $reblog_post_id, 'js' ),
958 )
959 );
960
961 wp_send_json_error( 'already-reblogged', array( 'guid' => $post->guid ) );
962 return;
963 }
964
965 /**
966 * Reblogs a post
967 *
968 * @param int|null $reblog_post_id The post ID of the reblogged post. Default null.
969 * @param WP_Post $post The post object.
970 */
971 $reblog_post_id = apply_filters( 'friends_reblog', null, $post, 'draft' );
972 if ( ! $reblog_post_id || is_wp_error( $reblog_post_id ) ) {
973 wp_send_json_error( 'error' );
974 }
975
976 wp_send_json_success(
977 array(
978 'post_id' => $reblog_post_id,
979 'redirect' => get_edit_post_link( $reblog_post_id, 'js' ),
980 )
981 );
982 }
983
984 public function reblog_button() {
985 Friends::template_loader()->get_template_part(
986 'frontend/parts/reblog-button',
987 null,
988 array()
989 );
990 }
991
992 public static function reblog( $ret, $post, $post_status = 'publish' ) {
993 if ( ! $post ) {
994 return $ret;
995 }
996 $post = get_post( $post );
997 if ( ! $post ) {
998 return $ret;
999 }
1000
1001 $author = get_post_meta( $post->ID, 'author', true );
1002 if ( ! $author ) {
1003 $friend = User::get_post_author( $post );
1004 $author = $friend->display_name;
1005 }
1006
1007 $reblog = '<!-- wp:paragraph {"className":"friends-reblog"} -->' . PHP_EOL . '<p class="friends-reblog">';
1008 $reblog .= sprintf(
1009 // translators: %s is a link.
1010 __( 'Reblog via %s', 'friends' ),
1011 '<a href="' . esc_url( $post->guid ) . '">' . esc_html( $author ) . '</a>'
1012 );
1013
1014 $reblog_title = sprintf(
1015 // translators: %1$s is the author, %2$s is the post title.
1016 __( 'Reblog of %1$s: %2$s', 'friends' ),
1017 $author,
1018 $post->post_title
1019 );
1020
1021 $reblog .= PHP_EOL . '</p>' . PHP_EOL . '<!-- /wp:paragraph -->' . PHP_EOL;
1022
1023 // Wrap the post content in a quote block.
1024 $reblog .= '<!-- wp:quote {"className":"friends-reblog"} -->' . PHP_EOL . '<blockquote class="wp-block-quote friends-reblog">';
1025 $reblog .= $post->post_content;
1026 $reblog .= '</blockquote>' . PHP_EOL . '<!-- /wp:quote -->';
1027
1028 $new_post = array(
1029 'post_title' => $reblog_title,
1030 'post_author' => get_current_user_id(),
1031 'post_status' => $post_status,
1032 'post_type' => 'post',
1033 'post_format' => get_post_format( $post ),
1034 'post_content' => $reblog,
1035 );
1036 /**
1037 * Allows changing a reblog post before it gets posted.
1038 *
1039 * @param array $new_post A post array to be handed to wp_insert_post.
1040 *
1041 * Additionally, the array contains the post_format which can also be changed.
1042 *
1043 * Example:
1044 * ```php
1045 * add_filter( 'friends_reblog_pre_insert_post', function( $new_post ) {
1046 * $new_post['post_type'] = 'custom_post_type';
1047 * $new_post['post_format'] = 'aside'; // always set the post_format to aside, regardless of the original post format.
1048 * return $new_post;
1049 * } );
1050 * ```
1051 */
1052 $new_post = apply_filters( 'friends_reblog_pre_insert_post', $new_post );
1053 $new_post_id = wp_insert_post( $new_post );
1054
1055 set_post_format( $new_post_id, $new_post['post_format'] );
1056 update_post_meta( $new_post_id, 'reblog', $post->guid );
1057 update_post_meta( $new_post_id, 'reblog_of', $post->ID );
1058 update_post_meta( $post->ID, 'reblogged', $new_post_id );
1059 update_post_meta( $post->ID, 'reblogged_by', $new_post['post_author'] );
1060
1061 return $new_post_id;
1062 }
1063
1064 public static function unreblog( $ret, $post ) {
1065 if ( ! $post ) {
1066 return $ret;
1067 }
1068 $post = get_post( $post );
1069 if ( ! $post ) {
1070 return $ret;
1071 }
1072
1073 $reblogged = get_post_meta( $post->ID, 'reblogged', true );
1074 if ( ! $reblogged ) {
1075 return $ret;
1076 }
1077
1078 wp_trash_post( $reblogged );
1079
1080 return $reblogged;
1081 }
1082
1083 /**
1084 * The Ajax function to autocomplete @mentions in the compose box.
1085 */
1086 public function ajax_mention_autocomplete() {
1087 check_ajax_referer( 'friends-mention-autocomplete' );
1088
1089 if ( ! current_user_can( Friends::REQUIRED_ROLE ) ) {
1090 wp_send_json_error();
1091 exit;
1092 }
1093
1094 if ( ! isset( $_POST['q'] ) ) {
1095 wp_send_json_success( array() );
1096 exit;
1097 }
1098
1099 $q = sanitize_text_field( wp_unslash( $_POST['q'] ) );
1100 $users = User_Query::search( '*' . $q . '*' );
1101 $results = array();
1102
1103 foreach ( $users->get_results() as $friend ) {
1104 $handle = null;
1105
1106 foreach ( $friend->get_feeds() as $feed ) {
1107 $ap_actor_id = $feed->get_ap_actor_id();
1108 if ( $ap_actor_id && class_exists( '\Activitypub\Collection\Remote_Actors' ) ) {
1109 $acct = \Activitypub\Collection\Remote_Actors::get_acct( $ap_actor_id );
1110 if ( $acct ) {
1111 $handle = $acct;
1112 break;
1113 }
1114 }
1115 }
1116
1117 if ( ! $handle ) {
1118 $handle = $friend->user_login;
1119 if ( $friend->user_url ) {
1120 $host = wp_parse_url( $friend->user_url, PHP_URL_HOST );
1121 if ( $host ) {
1122 $handle = $friend->user_login . '@' . $host;
1123 }
1124 }
1125 }
1126
1127 $results[] = array(
1128 'handle' => $handle,
1129 'display_name' => $friend->display_name,
1130 'avatar' => $friend->get_avatar_url(),
1131 );
1132 }
1133
1134 wp_send_json_success( $results );
1135 }
1136
1137 /**
1138 * The Ajax function to be called upon posting from /friends
1139 */
1140 public function ajax_frontend_publish_post() {
1141 if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'friends_publish' ) ) {
1142 return false;
1143 }
1144 $p = array(
1145 'post_type' => 'post',
1146 'post_title' => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '',
1147 'post_content' => isset( $_POST['content'] ) ? sanitize_textarea_field( wp_unslash( $_POST['content'] ) ) : '',
1148 'post_status' => isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '',
1149 'post_format' => isset( $_POST['format'] ) ? sanitize_text_field( wp_unslash( $_POST['format'] ) ) : '',
1150 );
1151
1152 if ( empty( $p['post_status'] ) ) {
1153 $p['post_status'] = 'publish';
1154 }
1155 $result = 'empty';
1156
1157 if ( ! empty( $_POST['in_reply_to'] ) ) {
1158 $p['meta_input'] = array(
1159 'activitypub_in_reply_to' => sanitize_text_field( wp_unslash( $_POST['in_reply_to'] ) ),
1160 );
1161 }
1162
1163 if ( ! empty( $p['post_content'] ) || ! empty( $p['post_title'] ) ) {
1164 $post_id = wp_insert_post( $p );
1165 if ( ! empty( $p['post_format'] ) ) {
1166 set_post_format( $post_id, $p['post_format'] );
1167 }
1168 $result = is_wp_error( $post_id ) ? 'error' : 'success';
1169 }
1170 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1171 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1172 if ( ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) === 'xmlhttprequest' ) {
1173 echo esc_html( $result );
1174 exit;
1175 }
1176
1177 if ( ! empty( $_SERVER['HTTP_REFERER'] ) ) {
1178 wp_safe_redirect( remove_query_arg( 'in_reply_to', add_query_arg( 'result', $result, $_SERVER['HTTP_REFERER'] ) ) );
1179 exit;
1180 }
1181 wp_safe_redirect( home_url( '/friends/' ) );
1182 exit;
1183 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1184 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1185 }
1186
1187 /**
1188 * The Ajax function to change the post format from the Frontend.
1189 */
1190 public function ajax_change_post_format() {
1191 $post_id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
1192 $post_format = isset( $_POST['format'] ) ? sanitize_text_field( wp_unslash( $_POST['format'] ) ) : 'standard';
1193
1194 check_ajax_referer( "friends-change-post-format_$post_id" );
1195
1196 if ( ! current_user_can( Friends::REQUIRED_ROLE, $post_id ) ) {
1197 wp_send_json_error();
1198 exit;
1199 }
1200
1201 $post_formats = get_post_format_strings();
1202 if ( ! isset( $post_formats[ $post_format ] ) ) {
1203 wp_send_json_error();
1204 exit;
1205 }
1206
1207 if ( ! set_post_format( $post_id, $post_format ) ) {
1208 wp_send_json_error();
1209 exit;
1210 }
1211
1212 wp_send_json_success();
1213 }
1214
1215 /**
1216 * Calculates an estimating read time.
1217 *
1218 * @param string $original_text The original text.
1219 *
1220 * @return float The read time in seconds.
1221 */
1222 public static function calculate_read_time( $original_text ) {
1223 // from wp_trim_words().
1224 $text = wp_strip_all_tags( $original_text );
1225
1226 /*
1227 * translators: If your word count is based on single characters (e.g. East Asian characters),
1228 * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
1229 * Do not translate into your own language.
1230 */
1231 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
1232 $text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' );
1233 preg_match_all( '/./u', $text, $words_array );
1234 $words_array = array_shift( $words_array );
1235 $words_per_minute = 500;
1236 } else {
1237 $words_array = preg_split( "/[\n\r\t ]+/", $text, -1, PREG_SPLIT_NO_EMPTY );
1238 $words_per_minute = 200;
1239 }
1240
1241 $additional_time = 0;
1242 $figures = substr_count( strtolower( $original_text ), '<figure' );
1243 for ( $i = 0; $i < $figures; $i++ ) {
1244 if ( $i < 10 ) {
1245 $additional_time += 12 - $i;
1246 } else {
1247 $additional_time += 3;
1248 }
1249 }
1250
1251 return count( $words_array ) / $words_per_minute * 60 + $additional_time;
1252 }
1253
1254 public function register_theme( $name, $slug ) {
1255 self::$themes[ $slug ] = $name;
1256 }
1257
1258 public static function get_themes() {
1259 $themes = array_merge(
1260 array(
1261 'default' => __( 'Friends Default Theme', 'friends' ),
1262 ),
1263 self::$themes
1264 );
1265
1266 foreach ( $themes as $slug => $name ) {
1267 $themes[ $slug ] = apply_filters( 'friends_theme_name', $name, $slug );
1268 }
1269
1270 return $themes;
1271 }
1272 /**
1273 * Handles the post loop on the Friends page.
1274 */
1275 public static function have_posts() {
1276 $friends = Friends::get_instance();
1277 $known_author = $friends->frontend->author;
1278 $known_avatar = null;
1279 if ( $known_author instanceof User ) {
1280 $known_avatar = $known_author->get_avatar_url();
1281 }
1282 while ( have_posts() ) {
1283 global $post;
1284 the_post();
1285 $args = array(
1286 'friends' => $friends,
1287 );
1288 $args['item_friend_user'] = User::get_post_author( $post );
1289
1290 if ( $known_author ) {
1291 $args['friend_user'] = $known_author;
1292 $args['avatar'] = $known_avatar;
1293 } else {
1294 $args['friend_user'] = $args['item_friend_user'];
1295 $args['avatar'] = $args['friend_user']->get_avatar_url();
1296 }
1297
1298 $read_time = self::calculate_read_time( get_the_content() );
1299 if ( $read_time >= 60 ) {
1300 $mins = ceil( $read_time / MINUTE_IN_SECONDS );
1301 /* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
1302 $args['read_time'] = sprintf( _n( '%s min', '%s mins', $mins ), $mins ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1303 } elseif ( $read_time > 20 ) {
1304 /* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
1305 $args['read_time'] = _x( '< 1 min', 'reading time', 'friends' );
1306 }
1307
1308 Friends::template_loader()->get_template_part(
1309 'frontend/parts/content',
1310 get_post_format(),
1311 $args
1312 );
1313 }
1314 }
1315
1316 /**
1317 * The Ajax function to load more posts for infinite scrolling.
1318 */
1319 public function ajax_load_next_page() {
1320 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1321 // phpcs:disable WordPress.Security.NonceVerification.Missing
1322 if ( ! isset( $_POST['query_vars'] ) || ! isset( $_POST['page'] ) || ! isset( $_POST['qv_sign'] ) ) {
1323 wp_send_json_error();
1324 exit;
1325 }
1326 $query_vars = wp_unslash( $_POST['query_vars'] );
1327 // We do have a nonce verification here.
1328 if ( sha1( wp_salt( 'nonce' ) . $query_vars ) !== $_POST['qv_sign'] ) {
1329 wp_send_json_error();
1330 exit;
1331 }
1332
1333 $query_vars = json_decode( $query_vars, true );
1334 $query_vars['paged'] = intval( $_POST['page'] ) + 1;
1335 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1336 // phpcs:enable WordPress.Security.NonceVerification.Missing
1337
1338 // We actually want to render the query loop, so we use query_posts.
1339 query_posts( $query_vars ); // phpcs:ignore WordPress.WP.DiscouragedFunctions.query_posts_query_posts
1340 ob_start();
1341 if ( have_posts() ) {
1342 self::have_posts();
1343 } else {
1344 esc_html_e( 'No further posts of your friends could were found.', 'friends' );
1345 }
1346
1347 $posts = ob_get_contents();
1348 ob_end_clean();
1349
1350 wp_send_json_success( $posts );
1351 }
1352
1353 /**
1354 * The Ajax function to autocomplete search.
1355 */
1356 public function ajax_autocomplete() {
1357 check_ajax_referer( 'friends-autocomplete' );
1358 if ( ! isset( $_POST['q'] ) ) {
1359 wp_send_json_error();
1360 exit;
1361 }
1362
1363 $q = sanitize_text_field( wp_unslash( $_POST['q'] ) );
1364 $results = array();
1365
1366 /**
1367 $result = '<a href="' . esc_url( add_query_arg( 'name', $q, admin_url( 'admin.php?page=add-friend' ) ) ) . '" class="has-icon-left">';
1368 $result .= '<span class="ab-icon dashicons dashicons-businessperson"><span class="ab-icon dashicons dashicons-plus"></span></span>';
1369 $result .= 'Add friend';
1370 $result .= ' <small>';
1371 $result .= esc_html( $q );
1372 $result .= '</small></a>';
1373 $results[] = $result;
1374 */
1375 $results = apply_filters( 'friends_search_autocomplete', $results, $q );
1376
1377 $result = '<a href="' . esc_url( add_query_arg( 's', $q, home_url( '/friends/' ) ) ) . '" class="has-icon-left">';
1378 $result .= '<span class="ab-icon dashicons dashicons-search"></span>';
1379 $result .= 'Search for ';
1380 $result .= ' <small>';
1381 $result .= esc_html( $q );
1382 $result .= '</small></a>';
1383 $results[] = $result;
1384
1385 wp_send_json_success( '<li class="menu-item">' . implode( '</li><li class="menu-item">', $results ) . '</li>' );
1386 }
1387
1388 /**
1389 * The Add user search entries to the autocomplete results.
1390 *
1391 * @param array $results The autocomplete results.
1392 * @param string $q The query.
1393 *
1394 * @return array The autocomplete results.
1395 */
1396 public function autocomplete_user_search( $results, $q ) {
1397 $users = User_Query::search( '*' . $q . '*' );
1398
1399 foreach ( $users->get_results() as $friend ) {
1400 $result = '<a href="' . esc_url( $friend->get_local_friends_page_url() ) . '" class="has-icon-left">';
1401 $result .= '<span class="ab-icon dashicons dashicons-businessperson"></span>';
1402 $result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->display_name );
1403 $result .= ' <small>';
1404 $result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->user_login );
1405 $result .= '</small></a>';
1406 $results[] = $result;
1407 }
1408
1409 return $results;
1410 }
1411
1412 /**
1413 * Filter comment links to use ActivityPub source URL in AJAX context.
1414 *
1415 * The ActivityPub plugin skips its filter when is_admin() is true,
1416 * but AJAX requests go through admin-ajax.php where is_admin() returns true.
1417 *
1418 * @param string $comment_link The comment permalink.
1419 * @param WP_Comment $comment The comment object.
1420 * @return string The filtered comment link.
1421 */
1422 public function activitypub_comment_link( $comment_link, $comment ) {
1423 if ( ! $comment || 'comment' !== $comment->comment_type ) {
1424 return $comment_link;
1425 }
1426
1427 $source_url = get_comment_meta( $comment->comment_ID, 'source_url', true );
1428 if ( $source_url ) {
1429 return $source_url;
1430 }
1431
1432 return $comment_link;
1433 }
1434
1435 /**
1436 * The Ajax function to load comments.
1437 */
1438 public function ajax_load_comments() {
1439 if ( ! isset( $_POST['post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1440 wp_send_json_error();
1441 exit;
1442 }
1443
1444 $post_id = intval( $_POST['post_id'] );
1445 check_ajax_referer( "comments-$post_id" );
1446
1447 $comments = apply_filters(
1448 'friends_get_comments',
1449 get_comments(
1450 array(
1451 'post_id' => $post_id,
1452 'status' => 'approve',
1453 'order' => 'ASC',
1454 )
1455 ),
1456 $post_id
1457 );
1458
1459 $friend_user = User::get_post_author( get_post( $post_id ) );
1460 $user_feed_url = get_post_meta( $post_id, 'feed_url', true );
1461 $user_feed = $this->friends->feed->get_user_feed_by_url( $user_feed_url );
1462
1463 remove_all_filters( 'comment_form_before' );
1464 remove_all_filters( 'comment_form_after' );
1465
1466 // Add filter to fix ActivityPub comment links in AJAX context.
1467 add_filter( 'get_comment_link', array( $this, 'activitypub_comment_link' ), 20, 2 );
1468
1469 if ( empty( $comments ) ) {
1470 $content = apply_filters( 'friends_no_comments_feed_available', __( 'No comments yet.', 'friends' ), $post_id, $friend_user, $user_feed );
1471 } else {
1472 $template_loader = Friends::template_loader();
1473 ob_start();
1474 ?>
1475 <h5><?php esc_html_e( 'Comments' ); /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain */ ?></h5>
1476 <ol class="comment-list">
1477 <?php
1478 wp_list_comments(
1479 array(
1480 'style' => 'ol',
1481 'short_ping' => true,
1482 'avatar_size' => 24,
1483 ),
1484 $comments
1485 );
1486 ?>
1487 </ol><!-- .comment-list -->
1488 <?php
1489 $content = ob_get_contents();
1490 ob_end_clean();
1491 }
1492
1493 remove_filter( 'get_comment_link', array( $this, 'activitypub_comment_link' ), 20 );
1494
1495 $content = apply_filters( 'friends_comments_content', $content, $post_id, $friend_user, $user_feed );
1496
1497 wp_send_json_success( $content );
1498 }
1499
1500 public function ajax_star_friend_user() {
1501 if ( ! isset( $_POST['friend_id'] ) || ! isset( $_POST['starred'] ) ) {
1502 wp_send_json_error();
1503 exit;
1504 }
1505
1506 $friend_id = sanitize_text_field( wp_unslash( $_POST['friend_id'] ) );
1507 check_ajax_referer( "star-$friend_id" );
1508
1509 $friend_user = User::get_by_username( $friend_id );
1510
1511 $starred = boolval( $_POST['starred'] );
1512 $friend_user->set_starred( $starred );
1513
1514 wp_send_json_success(
1515 array(
1516 'starred' => $friend_user->get_starred(),
1517 )
1518 );
1519 }
1520
1521 /**
1522 * Ajax handler to move a subscription to a folder.
1523 */
1524 public function ajax_move_to_folder() {
1525 if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['friend_id'] ) ) {
1526 wp_send_json_error();
1527 exit;
1528 }
1529
1530 check_ajax_referer( 'friends-move-to-folder' );
1531
1532 $friend_id = sanitize_text_field( wp_unslash( $_POST['friend_id'] ) );
1533 $folder_id = isset( $_POST['folder_id'] ) ? intval( $_POST['folder_id'] ) : 0;
1534
1535 $friend_user = User::get_by_username( $friend_id );
1536 if ( ! $friend_user || is_wp_error( $friend_user ) || ! ( $friend_user instanceof Subscription ) ) {
1537 wp_send_json_error( 'invalid-user' );
1538 exit;
1539 }
1540
1541 $result = $friend_user->move_to_folder( $folder_id );
1542 wp_send_json_success( array( 'moved' => $result ) );
1543 }
1544
1545 /**
1546 * Ajax handler to create a new folder.
1547 */
1548 public function ajax_create_folder() {
1549 if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['name'] ) ) {
1550 wp_send_json_error();
1551 exit;
1552 }
1553
1554 check_ajax_referer( 'friends-move-to-folder' );
1555
1556 $name = sanitize_text_field( wp_unslash( $_POST['name'] ) );
1557 $parent_id = isset( $_POST['parent_id'] ) ? intval( $_POST['parent_id'] ) : 0;
1558
1559 $folder = Subscription::create_folder( $name, $parent_id );
1560 if ( is_wp_error( $folder ) ) {
1561 wp_send_json_error( $folder->get_error_message() );
1562 exit;
1563 }
1564
1565 wp_send_json_success(
1566 array(
1567 'term_id' => $folder->term_id,
1568 'name' => $folder->name,
1569 )
1570 );
1571 }
1572
1573 /**
1574 * Ensure that untrashed friends posts go back to published.
1575 *
1576 * @param string $new_status The new status of the post being restored.
1577 * @param int $post_id The ID of the post being restored.
1578 */
1579 public function untrash_post_status( $new_status, $post_id ) {
1580 if ( ! in_array( get_post_type( $post_id ), apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
1581 return $new_status;
1582 }
1583 return 'publish';
1584 }
1585
1586 /**
1587 * Load the template for /friends
1588 *
1589 * @param string $template The original template intended to load.
1590 * @return string The new template to be loaded.
1591 */
1592 public function template_override( $template ) {
1593 if ( ! Friends::on_frontend() ) {
1594 return $template;
1595 }
1596
1597 global $args;
1598 $args = array(
1599 'friends' => $this->friends,
1600 'friend_user' => $this->author,
1601 'post_format' => $this->post_format,
1602 );
1603
1604 if ( $this->template ) {
1605 global $wp_query;
1606 $wp_query->is_404 = false;
1607
1608 status_header( 200 );
1609
1610 if ( 'block' === $this->theme ) {
1611 $block_template_content = $this->get_block_template_content_for( $this->template );
1612 if ( false !== $block_template_content ) {
1613 global $_wp_current_template_content, $_wp_current_template_id;
1614 $_wp_current_template_content = $block_template_content;
1615 $_wp_current_template_id = get_stylesheet() . '//friends-' . basename( $this->template );
1616 return ABSPATH . WPINC . '/template-canvas.php';
1617 }
1618 }
1619
1620 if ( 'frontend/index' === $this->template ) {
1621 $args['frontend_default_view'] = get_user_option( 'friends_frontend_default_view', get_current_user_id() );
1622
1623 }
1624 return Friends::template_loader()->get_template_part( $this->template, null, $args, false );
1625 }
1626
1627 if ( 'block' === $this->theme ) {
1628 global $wp_query;
1629 if ( $wp_query->is_single ) {
1630 $template_key = 'frontend/single';
1631 $template_id = 'friends-single';
1632 } elseif ( $this->author ) {
1633 $template_key = 'frontend/author-index';
1634 $template_id = 'friends-author-index';
1635 } else {
1636 $template_key = 'frontend/index';
1637 $template_id = 'friends-index';
1638 }
1639 $block_template_content = $this->get_block_template_content_for( $template_key );
1640 if ( false !== $block_template_content ) {
1641 global $_wp_current_template_content, $_wp_current_template_id;
1642 $_wp_current_template_content = $block_template_content;
1643 $_wp_current_template_id = get_stylesheet() . '//' . $template_id;
1644 return ABSPATH . WPINC . '/template-canvas.php';
1645 }
1646 }
1647
1648 $args['frontend_default_view'] = get_user_option( 'friends_frontend_default_view', get_current_user_id() );
1649 $args['blocks-everywhere'] = false;
1650
1651 if ( isset( $_GET['welcome'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1652 $args['show_welcome'] = true;
1653 }
1654
1655 return Friends::template_loader()->get_template_part( 'frontend/index', $this->post_format, $args, false );
1656 }
1657
1658 public function get_block_template_content_for( $template_path ) {
1659 $map = array(
1660 'frontend/index' => 'index',
1661 'frontend/author-index' => 'friends-author-index',
1662 'frontend/single' => 'single-friend_post_cache',
1663 'frontend/followers' => 'friends-followers',
1664 'frontend/add-friend' => 'friends-add-friend',
1665 'frontend/subscriptions' => 'friends-subscriptions',
1666 );
1667 if ( ! isset( $map[ $template_path ] ) ) {
1668 return false;
1669 }
1670 $template = $map[ $template_path ];
1671 if ( 'frontend/index' === $template_path && isset( $_GET['welcome'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1672 $template = 'welcome';
1673 }
1674 $file = FRIENDS_PLUGIN_DIR . 'themes/friends/templates/' . $template . '.html';
1675 if ( ! file_exists( $file ) ) {
1676 return false;
1677 }
1678 return file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
1679 }
1680
1681 public function get_static_frontend_template( $path ) {
1682 global $friends_args;
1683 global $wp_query;
1684 $wp_query->is_singular = false;
1685
1686 switch ( $path ) {
1687 case 'add-friend':
1688 $path = 'frontend/add-friend';
1689 break;
1690
1691 case 'subscriptions':
1692 wp_safe_redirect( home_url( '/friends/following/' ) );
1693 exit;
1694
1695 case 'following':
1696 $friends_args = array();
1697 $path = 'frontend/subscriptions';
1698 break;
1699
1700 case 'messages':
1701 $friends_args = array(
1702 'title' => __( 'Direct Messages', 'friends' ),
1703 'no-bottom-margin' => true,
1704 );
1705 $path = 'frontend/messages';
1706 break;
1707
1708 case 'followers':
1709 if ( ! class_exists( '\Activitypub\Collection\Followers' ) ) {
1710 return 'frontend/index';
1711 }
1712
1713 $friends_args = array();
1714 if ( isset( $_GET['mutual'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1715 $friends_args['only_mutual'] = true;
1716 $friends_args['title'] = __( 'Your Mutual Followers', 'friends' );
1717 } else {
1718 $friends_args['only_mutual'] = false;
1719 $friends_args['title'] = __( 'Your Followers', 'friends' );
1720 }
1721 $friends_args['user_id'] = get_current_user_id();
1722 $path = 'frontend/followers';
1723 break;
1724
1725 case 'blog-followers':
1726 $friends_args = array();
1727 if ( ! class_exists( '\Activitypub\Collection\Actors' ) ) {
1728 return 'frontend/index';
1729 }
1730 if ( isset( $_GET['mutual'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1731 $friends_args['only_mutual'] = true;
1732 $friends_args['title'] = __( 'Your Mutual Blog Followers', 'friends' );
1733 } else {
1734 $friends_args['only_mutual'] = false;
1735 $friends_args['title'] = __( 'Your Blog Followers', 'friends' );
1736 }
1737 $friends_args['user_id'] = \Activitypub\Collection\Actors::BLOG_USER_ID;
1738 $path = 'frontend/followers';
1739 break;
1740 case 'mutual':
1741 if ( ! class_exists( '\Activitypub\Collection\Followers' ) ) {
1742 return 'frontend/index';
1743 }
1744
1745 $friends_args = array();
1746 $friends_args['title'] = __( 'Friends', 'friends' );
1747 $friends_args['filter'] = 'following';
1748 $friends_args['user_id'] = get_current_user_id();
1749 $path = 'frontend/followers';
1750 break;
1751
1752 default:
1753 return 'frontend/index';
1754 }
1755
1756 $wp_query->is_404 = false;
1757 return $path;
1758 }
1759
1760 /**
1761 * Modify the Friends page title depending on context, for example add an author name or post format.
1762 *
1763 * @param string $title The original title.
1764 *
1765 * @return string The modified title.
1766 */
1767 public function header_widget_title( $title ) {
1768 $title = '<a href="' . esc_url( home_url( '/friends/' ) ) . '">' . esc_html( $title ) . '</a>';
1769 if ( $this->author ) {
1770 $title .= ' &raquo; <a href="' . esc_url( $this->author->get_local_friends_page_url() ) . '">' . esc_html( $this->author->display_name ) . '</a>';
1771 }
1772 if ( $this->post_format ) {
1773 $post_formats = get_post_format_strings();
1774 $title .= ' &raquo; ' . $post_formats[ $this->post_format ];
1775 }
1776 return $title;
1777 }
1778
1779 /**
1780 * Output a link, potentially augmented with authication information.
1781 *
1782 * @param string $url The url.
1783 * @param string $text The link text.
1784 * @param array $html_attributes HTML attributes.
1785 * @param User $friend_user The friend user.
1786 */
1787 public function link( $url, $text, array $html_attributes = array(), ?User $friend_user = null ) {
1788 echo wp_kses(
1789 self::get_link( $url, $text, $html_attributes, $friend_user ),
1790 array(
1791 'a' => array(
1792 'href' => array(),
1793 'title' => array(),
1794 'target' => array(),
1795 'rel' => array(),
1796 'class' => array(),
1797 'style' => array(),
1798 'data-nonce' => array(),
1799 'data-cnonce' => array(),
1800 'data-token' => array(),
1801 'data-friend' => array(),
1802 'data-id' => array(),
1803 'data-url' => array(),
1804 ),
1805 'span' => array( 'class' => array() ),
1806 )
1807 );
1808 }
1809
1810 /**
1811 * Get a link, potentially augmented with authication information.
1812 *
1813 * @param string $url The url.
1814 * @param string $text The link text.
1815 * @param array $html_attributes HTML attributes.
1816 * @param User $friend_user The friend user.
1817 *
1818 * @return string The link.
1819 */
1820 public static function get_link( $url, $text, array $html_attributes = array(), ?User $friend_user = null ) {
1821 if ( is_null( $friend_user ) ) {
1822 $friend_user = new User( get_the_author_meta( 'ID' ) );
1823 }
1824
1825 $link = '<a href="' . esc_url( $url ) . '"';
1826 foreach ( $html_attributes as $name => $value ) {
1827 if ( ! in_array( $name, array( 'title', 'target', 'rel', 'class', 'style', 'data-nonce', 'data-cnonce', 'data-token', 'data-friend', 'data-id', 'data-url' ) ) ) {
1828 continue;
1829 }
1830 $link .= ' ' . $name . '="' . esc_attr( $value ) . '"';
1831 }
1832 $link .= ' title="' . esc_attr( $text ) . '">';
1833 if ( isset( $html_attributes['dashicon_front'] ) ) {
1834 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_front'] ) . '"></span>';
1835 }
1836 $link .= '<span class="text">' . esc_html( $text ) . '</span>';
1837 if ( isset( $html_attributes['dashicon_back'] ) ) {
1838 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_back'] ) . '"></span>';
1839 }
1840 $link .= '</a>';
1841
1842 return $link;
1843 }
1844
1845 public static function get_widget_open_state( $widget ) {
1846 static $state = null;
1847 if ( is_null( $state ) ) {
1848 $state = get_user_meta( get_current_user_id(), 'friends_widget_state', true );
1849 if ( ! is_array( $state ) ) {
1850 $state = array();
1851 }
1852 }
1853 return isset( $state[ $widget ] ) && is_string( $state[ $widget ] ) ? $state[ $widget ] : 'open';
1854 }
1855
1856 public function ajax_set_widget_open_state() {
1857 if ( ! isset( $_POST['widget'] ) || ! isset( $_POST['state'] ) ) {
1858 wp_send_json_error();
1859 exit;
1860 }
1861
1862 check_ajax_referer( 'friends_widget_state' );
1863
1864 $widget = sanitize_text_field( wp_unslash( $_POST['widget'] ) );
1865 $open_state = sanitize_text_field( wp_unslash( $_POST['state'] ) );
1866
1867 $state = get_user_meta( get_current_user_id(), 'friends_widget_state', true );
1868 if ( ! is_array( $state ) ) {
1869 $state = array();
1870 }
1871 $state[ $widget ] = $open_state;
1872 update_user_meta( get_current_user_id(), 'friends_widget_state', $state );
1873
1874 wp_send_json_success();
1875 }
1876
1877 public function ajax_get_post_counts() {
1878 check_ajax_referer( 'friends_post_counts' );
1879 $counts = array_fill_keys( get_post_format_slugs(), 0 );
1880
1881 foreach ( $this->friends->get_post_count_by_post_format( true ) as $post_format => $count ) {
1882 $counts[ $post_format ] = $this->friends->get_post_format_plural_string( $post_format, $count );
1883 }
1884
1885 $post_counts = $this->friends->get_post_count_by_post_status( true );
1886 if ( isset( $post_counts->trash ) && $post_counts->trash ) {
1887 $counts['trash'] = sprintf( /* translators: %s is the number of hidden posts */_n( '%s hidden items', '%s hidden items', $post_counts->trash, 'friends' ), number_format_i18n( $post_counts->trash ) );
1888 } else {
1889 $counts['trash'] = 0;
1890 }
1891
1892 wp_send_json_success( $counts );
1893 }
1894
1895 /**
1896 * Don't show the edit link for friend posts.
1897 *
1898 * @param string $link The edit link.
1899 * @return string|bool The edit link or false.
1900 */
1901 public function friend_post_edit_link( $link ) {
1902 global $post;
1903
1904 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
1905 if ( Friends::on_frontend() ) {
1906 $new_link = false;
1907 } else {
1908 $new_link = get_the_guid( $post );
1909 }
1910 /**
1911 * Allow overriding the link for editing friend posts.
1912 *
1913 * By default on the Frontend, a post cannot be edited because $new_link is false.
1914 *
1915 * Example:
1916 *
1917 * ```php
1918 * add_filter( 'friend_post_edit_link', function( $link, $original_link ) {
1919 * if ( ! $link ) {
1920 * $link = $original_link; // always allow editing.
1921 * }
1922 * return $link;
1923 * }, 10, 2 );
1924 * ```
1925 */
1926 return apply_filters( 'friend_post_edit_link', $new_link, $link );
1927 }
1928 return $link;
1929 }
1930
1931 /**
1932 * Link friend posts to the remote site.
1933 *
1934 * @param string $post_link The post's permalink.
1935 * @param \WP_Post $post The post in question.
1936 * @return string The overriden post link.
1937 */
1938 public function friend_post_link( $post_link, \WP_Post $post ) {
1939 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
1940 return get_the_guid( $post );
1941 }
1942 return $post_link;
1943 }
1944
1945 /**
1946 * Link friend post comments to the local friends page.
1947 *
1948 * @param string $link The comment permalink.
1949 * @param \WP_Comment $comment The comment object.
1950 * @return string The overriden comment link.
1951 */
1952 public function friend_post_comment_link( $link, \WP_Comment $comment ) {
1953 // Don't override external ActivityPub comments - they should link to their source.
1954 if ( 'activitypub' === $comment->comment_type || 'activitypub' === get_comment_meta( $comment->comment_ID, 'protocol', true ) ) {
1955 return $link;
1956 }
1957
1958 $post = get_post( $comment->comment_post_ID );
1959 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
1960 $friend_user = User::get_post_author( $post );
1961 return $friend_user->get_local_friends_page_url( $post->ID ) . '#comment-' . $comment->comment_ID;
1962 }
1963 return $link;
1964 }
1965
1966 /**
1967 * Potentially override the post author name with metadata.
1968 *
1969 * @param string $overridden_author_name The already overridden author name.
1970 * @param string $author_name The author name.
1971 * @param int $post_id The post id.
1972 *
1973 * @return string The modified author name.
1974 */
1975 public function override_author_name( $overridden_author_name, $author_name, $post_id ) {
1976 if ( $overridden_author_name && $overridden_author_name !== $author_name ) {
1977 return $overridden_author_name;
1978 }
1979 $override_author_name = get_post_meta( $post_id, 'author', true );
1980 if ( $override_author_name ) {
1981 return $override_author_name;
1982 }
1983 return $author_name;
1984 }
1985
1986 public function expose_opml( $viewable, $pagename ) {
1987 if ( 'opml' === $pagename ) {
1988 return true;
1989 }
1990 return $viewable;
1991 }
1992
1993 /**
1994 * Render the Friends OPML
1995 *
1996 * @param bool $only_public Only public feed URLs.
1997 */
1998 protected function render_opml( $only_public = false ) {
1999 if ( ! \is_user_logged_in() ) {
2000 $only_public = true;
2001 $user_id = Friends::get_main_friend_user_id();
2002 $user = new User( $user_id );
2003 } else {
2004 $user = wp_get_current_user();
2005 }
2006
2007 // translators: %s is a name.
2008 $title = sprintf( __( '%s is following', 'friends' ), $user->display_name );
2009 $filename = 'friends-';
2010 if ( ! $only_public ) {
2011 $title = __( 'My Friends', 'friends' );
2012 $filename .= 'private-';
2013 }
2014 $filename .= $user->user_login . '.opml';
2015
2016 $feeds = array();
2017 $users = array();
2018
2019 $friend_users = User_Query::all_associated_users();
2020 foreach ( $friend_users->get_results() as $friend_user ) {
2021 $role = $friend_user->get_role_name( true, 9 );
2022 if ( $only_public ) {
2023 $role = 'Feeds';
2024 }
2025 if ( ! isset( $users[ $role ] ) ) {
2026 $users[ $role ] = array();
2027 }
2028
2029 $users[ $role ][] = $friend_user;
2030 }
2031 ksort( $users );
2032 foreach ( $users as $role => $friend_users ) {
2033 foreach ( $friend_users as $friend_user ) {
2034 $user_feeds = $friend_user->get_active_feeds();
2035
2036 $need_local_feed = false;
2037
2038 foreach ( $user_feeds as $feed ) {
2039 if ( $feed->get_parser() === Feed_Parser_SimplePie::SLUG ) {
2040 break;
2041 }
2042 switch ( $feed->get_mime_type() ) {
2043 case 'application/atom+xml':
2044 case 'application/atomxml':
2045 case 'application/rss+xml':
2046 case 'application/rssxml':
2047 break;
2048 default:
2049 $need_local_feed = true;
2050 break 2;
2051 }
2052 }
2053
2054 if ( $need_local_feed && ! $only_public ) {
2055 $user_feeds = array_slice( $user_feeds, 0, 1 );
2056 }
2057
2058 $user = array(
2059 'friend_user' => $friend_user,
2060 'feeds' => array(),
2061 );
2062 $html_url = $friend_user->user_url;
2063
2064 foreach ( $user_feeds as $feed ) {
2065 $type = 'rss';
2066 $feed_title = $friend_user->display_name;
2067
2068 if ( ! $only_public ) {
2069 $html_url = $feed->get_local_html_url();
2070 }
2071
2072 if ( $need_local_feed ) {
2073 if ( $only_public ) {
2074 // Cannot create a public URL.
2075 continue;
2076 }
2077 $xml_url = $feed->get_local_url();
2078 // phpcs:disable WordPress.Security.NonceVerification.Recommended
2079 if ( isset( $_GET['auth'] ) ) {
2080 $xml_url = add_query_arg( 'auth', sanitize_text_field( wp_unslash( $_GET['auth'] ) ), $xml_url );
2081 // phpcs:enable WordPress.Security.NonceVerification.Recommended
2082 }
2083 } else {
2084 if ( $only_public ) {
2085 $xml_url = $feed->get_url();
2086 } else {
2087 $xml_url = $feed->get_url();
2088 }
2089 if ( 'application/atom+xml' === $feed->get_mime_type() ) {
2090 $type = 'atom';
2091 }
2092 }
2093 $user['feeds'][] = array(
2094 'xml_url' => $xml_url,
2095 'html_url' => $html_url,
2096 'title' => $feed_title,
2097 'type' => $type,
2098 );
2099 }
2100
2101 if ( ! empty( $user['feeds'] ) ) {
2102 if ( ! isset( $feeds[ $role ] ) ) {
2103 $feeds[ $role ] = array();
2104 }
2105 $feeds[ $role ][] = $user;
2106 }
2107 }
2108 }
2109 Friends::template_loader()->get_template_part(
2110 'admin/opml',
2111 null,
2112 array(
2113 'title' => $title,
2114 'feeds' => $feeds,
2115 'filename' => $filename,
2116 )
2117 );
2118 exit;
2119 }
2120
2121 private function has_required_priviledges() {
2122 if ( Friends::is_main_user() ) {
2123 return true;
2124 }
2125
2126 if ( is_multisite() ) {
2127 if ( is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) {
2128 return true;
2129 }
2130
2131 if ( is_super_admin( get_current_user_id() ) ) {
2132 // Super admins would count as administrators.
2133 return false;
2134 }
2135 }
2136
2137 if ( current_user_can( 'manage_options' ) ) {
2138 return true;
2139 }
2140
2141 return false;
2142 }
2143
2144 /**
2145 * Exclude the compose post format from the main RSS feed.
2146 *
2147 * @param \WP_Query $query The main query.
2148 * @return \WP_Query The modified main query.
2149 */
2150 public function exclude_compose_format_from_feed( $query ) {
2151 if ( ! $query->is_main_query() || ! $query->is_feed() ) {
2152 return $query;
2153 }
2154
2155 if ( ! get_option( 'friends_exclude_compose_format_from_feed' ) ) {
2156 return $query;
2157 }
2158
2159 $format = get_option( 'friends_compose_post_format', 'status' );
2160 if ( ! $format || 'standard' === $format ) {
2161 return $query;
2162 }
2163
2164 $tax_query = (array) $query->get( 'tax_query' );
2165 $tax_query[] = array(
2166 'taxonomy' => 'post_format',
2167 'field' => 'slug',
2168 'terms' => array( 'post-format-' . $format ),
2169 'operator' => 'NOT IN',
2170 );
2171 $query->set( 'tax_query', $tax_query );
2172
2173 return $query;
2174 }
2175
2176 /**
2177 * Modify the main query for the /friends page
2178 *
2179 * @param \WP_Query $query The main query.
2180 * @return \WP_Query The modified main query.
2181 */
2182 public function friend_posts_query( $query ) {
2183 global $wp_query, $wp, $authordata;
2184 if ( $wp_query !== $query || $query->is_admin() || $query->is_home() ) {
2185 return $query;
2186 }
2187
2188 $pagename = '';
2189 if ( isset( $wp_query->query['pagename'] ) ) {
2190 $pagename = $wp_query->query['pagename'];
2191 } elseif ( isset( $wp_query->query['category_name'] ) ) {
2192 $pagename = $wp_query->query['category_name'];
2193 } elseif ( isset( $wp_query->query['name'] ) ) {
2194 $pagename = $wp_query->query['name'];
2195 }
2196
2197 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
2198 $is_friends = array_shift( $pagename_parts );
2199
2200 if ( 'friends' !== $is_friends ) {
2201 return $query;
2202 }
2203
2204 // Not available for the general public or friends.
2205 $viewable = $this->has_required_priviledges() && Friends::on_frontend();
2206 if ( $query->is_feed() ) {
2207 // Feeds can be viewed through extra authentication.
2208 if ( isset( $wp_query->query['pagename'] ) ) {
2209 if ( apply_filters( 'friends_friend_feed_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
2210 $viewable = true;
2211 }
2212 }
2213 }
2214
2215 if ( ! $viewable && isset( $wp_query->query['pagename'] ) ) {
2216 if ( apply_filters( 'friends_friend_posts_query_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
2217 $viewable = true;
2218 }
2219 }
2220
2221 $page_id = get_query_var( 'page' );
2222 // phpcs:disable WordPress.Security.NonceVerification.Recommended
2223 if ( isset( $_GET['share'] ) ) {
2224 $share_hash = hash( 'crc32b', apply_filters( 'friends_share_salt', wp_salt( 'nonce' ), $page_id ) . $page_id );
2225 if ( $_GET['share'] === $share_hash ) {
2226 $viewable = true;
2227 }
2228 }
2229
2230 if ( ! $viewable ) {
2231 if ( $query->is_feed() ) {
2232 status_header( 404 );
2233 $query->set_404();
2234 } elseif ( ! Friends::on_frontend() ) {
2235 if ( count( $pagename_parts ) > 0 ) {
2236 wp_safe_redirect( home_url( '/friends/' ) );
2237 exit;
2238 }
2239 } elseif ( ! Friends::is_main_user() ) {
2240 wp_die( esc_html__( 'You are not allowed to view this page.', 'friends' ) );
2241 }
2242
2243 return $query;
2244 }
2245
2246 // Don't let the Post Kinds plugin interfere with this query.
2247 remove_action( 'pre_get_posts', array( 'Kind_Taxonomy', 'kind_firehose_query' ), 99 );
2248
2249 switch_to_locale( get_user_locale() );
2250
2251 $tax_query = array();
2252 $post_format = null;
2253
2254 while ( $pagename_parts ) {
2255 $pagename_part = $pagename_parts[0];
2256 $current_part = array_shift( $pagename_parts );
2257 if ( 'reaction' === substr( $current_part, 0, 8 ) && strlen( $current_part ) > 8 ) {
2258 $reaction = Reactions::validate_emoji( substr( $current_part, 8 ) );
2259 if ( ! $reaction ) {
2260 continue;
2261 }
2262 $this->reaction = $reaction;
2263 if ( ! empty( $tax_query ) ) {
2264 $tax_query['relation'] = 'AND';
2265 }
2266
2267 $tax_query[] = array(
2268 'taxonomy' => 'friend-reaction-' . get_current_user_id(),
2269 'field' => 'slug',
2270 'terms' => array( substr( $current_part, 8 ) ),
2271 );
2272 continue;
2273 }
2274
2275 switch ( $current_part ) {
2276 case 'opml':
2277 return $this->render_opml( isset( $_REQUEST['public'] ) && boolval( $_REQUEST['public'] ) );
2278
2279 case 'type':
2280 $post_format = array_shift( $pagename_parts );
2281 $tax_query = $this->friends->wp_query_get_post_format_tax_query( $tax_query, explode( ',', $post_format ) );
2282 if ( $tax_query ) {
2283 $this->post_format = $post_format;
2284 }
2285 break;
2286
2287 case 'tag':
2288 if ( empty( $pagename_parts ) && $page_id ) {
2289 // Support numeric tags.
2290 $this->tag = strval( $page_id );
2291 $page_id = false;
2292 } else {
2293 $this->tag = array_shift( $pagename_parts );
2294 }
2295 $tax_query = $this->friends->wp_query_get_post_tag_tax_query( $tax_query, $this->tag );
2296 break;
2297
2298 default: // Maybe an author.
2299 $author = User::get_by_username( $current_part );
2300 if ( false === $author || is_wp_error( $author ) ) {
2301 if ( $query->is_feed() ) {
2302 status_header( 404 );
2303 $query->set_404();
2304 return $query;
2305 }
2306
2307 $template = $this->get_static_frontend_template( $current_part );
2308 if ( 'frontend/index' !== $template ) {
2309 $wp_query->is_404 = false;
2310 $query->is_404 = false;
2311 status_header( 200 );
2312 add_filter( 'pre_handle_404', '__return_true' );
2313 $this->template = $template;
2314 return $query;
2315 }
2316 wp_safe_redirect( home_url( '/friends/' ) );
2317 exit;
2318 }
2319
2320 $this->author = $author;
2321 break;
2322 }
2323 }
2324
2325 $this->is_friends_page = true;
2326 $query->is_friends_page = true;
2327 $query->is_singular = false;
2328 $query->is_single = false;
2329 $query->is_category = false;
2330 $query->is_archive = false;
2331 $query->queried_object = null;
2332 $query->queried_object_id = null;
2333 $post_types = apply_filters( 'friends_frontend_post_types', array() );
2334
2335 if ( 'status' === $post_format ) {
2336 // Show your own posts on the status feed.
2337 $post_types[] = 'post';
2338 }
2339
2340 $query->set( 'post_type', $post_types );
2341 $query->set( 'tax_query', $tax_query );
2342
2343 if ( ( isset( $_GET['share'] ) && $_GET['share'] === $share_hash ) || $viewable ) {
2344 $post_status = array( 'publish', 'private', 'future' );
2345 if ( isset( $_GET['show-hidden'] ) ) {
2346 $post_status[] = 'trash';
2347 }
2348 $query->set( 'post_status', $post_status );
2349 }
2350 // phpcs:enable WordPress.Security.NonceVerification.Recommended
2351 $query->is_page = false;
2352 $query->is_comment_feed = false;
2353 $query->set( 'pagename', null );
2354 $query->set( 'category_name', null );
2355 if ( get_option( 'posts_per_page' ) < 20 ) {
2356 $query->set( 'posts_per_page', 20 );
2357 if ( 'collapsed' === get_user_option( 'friends_frontend_default_view', get_current_user_id() ) && 'status' === $post_format ) {
2358 $query->set( 'posts_per_page', 30 );
2359 }
2360 }
2361
2362 if ( $page_id ) {
2363 $query->set( 'page_id', $page_id );
2364 if ( ! $this->author ) {
2365 $post = get_post( $page_id );
2366
2367 if ( $post ) {
2368 $author = User::get_post_author( $post );
2369 if ( false !== $author ) {
2370 $this->author = $author;
2371 }
2372 }
2373 }
2374 $query->is_single = true;
2375 $query->is_singular = true;
2376 $wp->set_query_var( 'page', null );
2377 $wp->set_query_var( 'page_id', $page_id );
2378 }
2379
2380 if ( $this->author ) {
2381 if ( $this->author instanceof User ) {
2382 $authordata = $this->author; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2383 $this->author->modify_query_by_author( $query );
2384 if ( ! $page_id ) {
2385 $query->is_author = true;
2386 }
2387 } else {
2388 $this->author = null;
2389 }
2390 }
2391
2392 if ( ! $query->is_singular && ! $query->is_author ) {
2393 // This is the main friends page.
2394 $hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' );
2395 $hide_from_friends_page = array_diff( array_map( 'intval', (array) $hide_from_friends_page ), array( 0 ) );
2396
2397 if ( $hide_from_friends_page ) {
2398 $query->set( 'author__not_in', $hide_from_friends_page );
2399 }
2400 }
2401
2402 // phpcs:disable WordPress.Security.NonceVerification.Recommended
2403 if ( isset( $_GET['order'] ) ) {
2404 $order = strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) );
2405 if ( in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
2406 $query->set( 'order', $order );
2407 }
2408 }
2409 // phpcs:enable WordPress.Security.NonceVerification.Recommended
2410
2411 return $query;
2412 }
2413 }
2414