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

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