PluginProbe
Friends / 4.1.0
Friends v4.1.0
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.1.0, at includes/class-frontend.php

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