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
← All changes | includes/class-frontend.php +1414 -259 2.7.54.3.2 View file →
@@ -39,8 +39,15 @@
39 39 */
40 40 public $author = false;
41 41
42 42 /**
43 + * Whether an tag is being displayed
44 + *
45 + * @var object|false
46 + */
47 + public $tag = false;
48 +
49 + /**
43 50 * Whether a post-format is being displayed
44 51 *
45 52 * @var string|false
46 53 */
@@ -46,15 +53,35 @@
46 53 */
47 54 public $post_format = false;
48 55
49 56 /**
50 - * Whether a reaciton is being displayed
57 + * Whether a specific template was selected to load.
51 58 *
52 59 * @var string|false
53 60 */
61 + public $template = false;
62 +
63 + /**
64 + * Whether a reaction is being displayed.
65 + *
66 + * @var string|false
67 + */
54 68 public $reaction = false;
55 69
56 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 + /**
57 84 * Constructor
58 85 *
59 86 * @param Friends $friends A reference to the Friends object.
60 87 */
@@ -67,8 +94,9 @@
67 94 * Register the WordPress hooks
68 95 */
69 96 private function register_hooks() {
70 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 );
71 99 add_filter( 'post_type_link', array( $this, 'friend_post_link' ), 10, 2 );
72 100 add_filter( 'friends_header_widget_title', array( $this, 'header_widget_title' ) );
73 101 add_filter( 'get_edit_post_link', array( $this, 'friend_post_edit_link' ) );
74 102 add_filter( 'template_include', array( $this, 'template_override' ) );
@@ -75,27 +103,56 @@
75 103 add_filter( 'wp_loaded', array( $this, 'add_rewrite_rule' ) );
76 104 add_filter( 'init', array( $this, 'register_friends_sidebar' ) );
77 105 add_action( 'init', array( $this, 'add_theme_supports' ) );
78 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' ) );
79 108 add_action( 'wp_ajax_friends-change-post-format', array( $this, 'ajax_change_post_format' ) );
80 109 add_action( 'wp_ajax_friends-load-next-page', array( $this, 'ajax_load_next_page' ) );
81 110 add_action( 'wp_ajax_friends-autocomplete', array( $this, 'ajax_autocomplete' ) );
82 - add_action( 'wp_ajax_friends-in-reply-to-preview', array( $this, 'ajax_in_reply_to_preview' ) );
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 );
83 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' ) );
84 117 add_action( 'wp_ajax_friends-load-comments', array( $this, 'ajax_load_comments' ) );
85 118 add_action( 'wp_ajax_friends-reblog', array( $this, 'wp_ajax_reblog' ) );
86 119 add_action( 'friends_post_footer_first', array( $this, 'reblog_button' ) );
87 - add_filter( 'friends_reblog', array( get_called_class(), 'reblog' ), 10, 2 );
120 + add_filter( 'friends_reblog', array( get_called_class(), 'reblog' ), 10, 3 );
88 121 add_filter( 'friends_unreblog', array( get_called_class(), 'unreblog' ), 10, 2 );
89 - add_action( 'wp_untrash_post_status', array( $this, 'untrash_post_status' ), 10, 3 );
122 + add_action( 'wp_untrash_post_status', array( $this, 'untrash_post_status' ), 10, 2 );
90 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' ) );
91 138 add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_scripts' ), 99999 );
92 139 add_action( 'wp_footer', array( $this, 'dequeue_scripts' ) );
93 140 add_action( 'the_post', array( $this, 'the_post' ), 10, 2 );
94 141 add_action( 'parse_query', array( $this, 'parse_query' ) );
95 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 );
96 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 +
97 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 );
98 155 }
99 156
100 157 /**
101 158 * We're asking WordPress to handle the title for us.
@@ -100,23 +157,28 @@
100 157 /**
101 158 * We're asking WordPress to handle the title for us.
102 159 */
103 160 public function add_rewrite_rule() {
104 - add_rewrite_rule(
105 - 'friends/(.*)/(?:feed/)?(feed|rdf|rss|rss2|atom)/?$',
106 - 'index.php?pagename=friends/$matches[1]&feed=$matches[2]',
107 - 'top'
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]',
108 169 );
109 - add_rewrite_rule(
110 - 'friends/(.*)/(\d+)/?$',
111 - 'index.php?pagename=friends/$matches[1]&page=$matches[2]',
112 - 'top'
113 - );
114 - add_rewrite_rule(
115 - 'friends/(.*)',
116 - 'index.php?pagename=friends/$matches[1]',
117 - 'top'
118 - );
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 + }
119 181 }
120 182
121 183 /**
122 184 * Run our add_theme_supports if on the frontend.
@@ -195,8 +257,19 @@
195 257 'before_title' => '<h5>',
196 258 'after_title' => '</h5>',
197 259 )
198 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 + );
199 272
200 273 if ( Friends::on_frontend() ) {
201 274 add_action( 'customize_register', '__return_true' );
202 275 }
@@ -201,51 +274,176 @@
201 274 add_action( 'customize_register', '__return_true' );
202 275 }
203 276 }
204 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 +
205 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 + /**
206 355 * Reference our script for the /friends page
207 356 */
208 357 public function enqueue_scripts() {
358 + if ( ! is_user_logged_in() || ! Friends::on_frontend() ) {
359 + return;
360 + }
209 361 global $wp_query;
210 362
211 - if ( is_user_logged_in() && Friends::on_frontend() ) {
212 - $handle = 'friends';
213 - $file = 'friends.js';
214 - $version = Friends::VERSION;
215 - wp_enqueue_script( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'common', 'jquery', 'wp-util' ), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
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 );
216 367
217 - $query_vars = serialize( $this->get_minimal_query_vars( $wp_query->query_vars ) );
368 + $query_vars = wp_json_encode( $this->get_minimal_query_vars( $wp_query->query_vars ) );
218 369
219 - $variables = array(
220 - 'emojis_json' => plugins_url( 'emojis.json', FRIENDS_PLUGIN_FILE ),
221 - 'ajax_url' => admin_url( 'admin-ajax.php' ),
222 - 'text_link_expired' => __( 'The link has expired. A new link has been generated, please click it again.', 'friends' ),
223 - 'text_undo' => __( 'Undo' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
224 - 'text_trash_post' => __( 'Trash this post', 'friends' ),
225 - 'text_del_convers' => __( 'Do you really want to delete this conversation?', 'friends' ),
226 - 'text_no_more_posts' => __( 'No more posts available.', 'friends' ),
227 - 'query_vars' => $query_vars,
228 - 'qv_sign' => sha1( wp_salt( 'nonce' ) . $query_vars ),
229 - 'current_page' => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1,
230 - 'max_page' => $wp_query->max_num_pages,
231 - );
232 - wp_localize_script( 'friends', 'friends', $variables );
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 + );
233 394
234 - $handle = 'friends';
235 - $file = 'friends.css';
236 - $version = Friends::VERSION;
237 - wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
238 - }
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 );
239 433 }
240 434
241 435 public function dequeue_scripts() {
242 - if ( is_user_logged_in() && Friends::on_frontend() ) {
243 - // Dequeue theme styles so taht they don't interact with the Friends frontend.
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.
244 438 $wp_styles = wp_styles();
439 + $theme_root_path = wp_parse_url( get_theme_root_uri(), PHP_URL_PATH );
245 440 foreach ( $wp_styles->queue as $style ) {
246 441 $src = $wp_styles->registered[ $style ]->src;
247 - if ( 'global-styles' === $style || false !== strpos( $src, '/themes/' ) ) {
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 ) {
248 446 wp_dequeue_style( $style );
249 447 }
250 448 }
251 449 }
@@ -250,8 +448,49 @@
250 448 }
251 449 }
252 450 }
253 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 +
254 493 public function the_post( $post, $query ) {
255 494 Subscription::set_authordata_by_query( $query );
256 495 }
257 496
@@ -267,14 +506,449 @@
267 506 */
268 507 public function add_body_class( $classes ) {
269 508 if ( $this->friends->on_frontend() ) {
270 509 $classes[] = 'friends-page';
510 + $classes[] = 'off-canvas';
511 + $classes[] = 'off-canvas-sidebar-show';
271 512 }
272 513
273 514 return $classes;
274 515 }
275 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 +
276 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 + /**
277 951 * Gets the minimal query variables.
278 952 *
279 953 * @param array $query_vars The query variables.
280 954 *
@@ -280,22 +954,35 @@
280 954 *
281 955 * @return array The minimal query variables.
282 956 */
283 957 private function get_minimal_query_vars( $query_vars ) {
284 - return array_filter( array_intersect_key( $query_vars, array_flip( array( 'p', 'page_id', 'pagename', 'author', 'author__not_in', 'post_type', 'post_status', 'posts_per_page', 'order', 'tax_query' ) ) ) );
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' ) ) ) );
285 959 }
286 960
287 -
288 961 public function wp_ajax_reblog() {
289 - if ( ! current_user_can( Friends::REQUIRED_ROLE ) ) {
962 + if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['post_id'] ) ) {
290 963 wp_send_json_error( 'error' );
291 964 }
292 965
293 - $post = get_post( $_POST['post_id'] );
966 + check_ajax_referer( 'friends-reblog' );
967 +
968 + $post = get_post( intval( $_POST['post_id'] ) );
294 969 if ( ! $post || ! Friends::check_url( $post->guid ) ) {
295 970 wp_send_json_error( 'unknown-post', array( 'guid' => $post->guid ) );
296 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 + );
297 980
981 + wp_send_json_error( 'already-reblogged', array( 'guid' => $post->guid ) );
982 + return;
983 + }
984 +
298 985 /**
299 986 * Reblogs a post
300 987 *
301 988 * @param int|null $reblog_post_id The post ID of the reblogged post. Default null.
@@ -300,9 +987,9 @@
300 987 *
301 988 * @param int|null $reblog_post_id The post ID of the reblogged post. Default null.
302 989 * @param WP_Post $post The post object.
303 990 */
304 - $reblog_post_id = apply_filters( 'friends_reblog', null, $post );
991 + $reblog_post_id = apply_filters( 'friends_reblog', null, $post, 'draft' );
305 992 if ( ! $reblog_post_id || is_wp_error( $reblog_post_id ) ) {
306 993 wp_send_json_error( 'error' );
307 994 }
308 995
@@ -307,26 +994,23 @@
307 994 }
308 995
309 996 wp_send_json_success(
310 997 array(
311 - 'post_id' => $reblog_post_id,
998 + 'post_id' => $reblog_post_id,
999 + 'redirect' => get_edit_post_link( $reblog_post_id, 'js' ),
312 1000 )
313 1001 );
314 1002 }
315 1003
316 1004 public function reblog_button() {
317 - $button_label = apply_filters( 'friends_reblog_button_label', _x( 'Reblog', 'button', 'friends' ) );
318 -
319 1005 Friends::template_loader()->get_template_part(
320 1006 'frontend/parts/reblog-button',
321 1007 null,
322 - array(
323 - 'button-label' => $button_label,
324 - )
1008 + array()
325 1009 );
326 1010 }
327 1011
328 - public static function reblog( $ret, $post ) {
1012 + public static function reblog( $ret, $post, $post_status = 'publish' ) {
329 1013 if ( ! $post ) {
330 1014 return $ret;
331 1015 }
332 1016 $post = get_post( $post );
@@ -339,9 +1023,9 @@
339 1023 $friend = User::get_post_author( $post );
340 1024 $author = $friend->display_name;
341 1025 }
342 1026
343 - $reblog = '<!-- wp:paragraph -->' . PHP_EOL . '<p>';
1027 + $reblog = '<!-- wp:paragraph {"className":"friends-reblog"} -->' . PHP_EOL . '<p class="friends-reblog">';
344 1028 $reblog .= sprintf(
345 1029 // translators: %s is a link.
346 1030 __( 'Reblog via %s', 'friends' ),
347 1031 '<a href="' . esc_url( $post->guid ) . '">' . esc_html( $author ) . '</a>'
@@ -346,23 +1030,54 @@
346 1030 __( 'Reblog via %s', 'friends' ),
347 1031 '<a href="' . esc_url( $post->guid ) . '">' . esc_html( $author ) . '</a>'
348 1032 );
349 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 +
350 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 +
351 1048 $new_post = array(
352 - 'post_title' => $post->title,
1049 + 'post_title' => $reblog_title,
353 1050 'post_author' => get_current_user_id(),
354 - 'post_status' => 'publish',
1051 + 'post_status' => $post_status,
355 1052 'post_type' => 'post',
356 - 'post_content' => $reblog . $post->post_content,
1053 + 'post_format' => get_post_format( $post ),
1054 + 'post_content' => $reblog,
357 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 );
358 1073 $new_post_id = wp_insert_post( $new_post );
359 1074
360 - set_post_format( $new_post_id, get_post_format( $post ) );
1075 + set_post_format( $new_post_id, $new_post['post_format'] );
361 1076 update_post_meta( $new_post_id, 'reblog', $post->guid );
362 1077 update_post_meta( $new_post_id, 'reblog_of', $post->ID );
363 1078 update_post_meta( $post->ID, 'reblogged', $new_post_id );
364 - update_post_meta( $post->ID, 'reblogged_by', get_current_user_id() );
1079 + update_post_meta( $post->ID, 'reblogged_by', $new_post['post_author'] );
365 1080
366 1081 return $new_post_id;
367 1082 }
368 1083
@@ -385,20 +1100,74 @@
385 1100 return $reblogged;
386 1101 }
387 1102
388 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 + /**
389 1158 * The Ajax function to be called upon posting from /friends
390 1159 */
391 1160 public function ajax_frontend_publish_post() {
392 - if ( ! wp_verify_nonce( $_POST['_wpnonce'], 'friends_publish' ) ) {
1161 + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'friends_publish' ) ) {
393 1162 return false;
394 1163 }
395 1164 $p = array(
396 1165 'post_type' => 'post',
397 - 'post_title' => isset( $_POST['title'] ) ? $_POST['title'] : '',
398 - 'post_content' => isset( $_POST['content'] ) ? $_POST['content'] : '',
399 - 'post_status' => isset( $_POST['status'] ) ? $_POST['status'] : '',
400 - 'post_format' => isset( $_POST['format'] ) ? $_POST['format'] : '',
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'] ) ) : '',
401 1170 );
402 1171
403 1172 if ( empty( $p['post_status'] ) ) {
404 1173 $p['post_status'] = 'publish';
@@ -405,10 +1174,10 @@
405 1174 }
406 1175 $result = 'empty';
407 1176
408 1177 if ( ! empty( $_POST['in_reply_to'] ) ) {
409 - $p['post_meta_input'] = array(
410 - 'activitypub_in_reply_to' => $_POST['in_reply_to'],
1178 + $p['meta_input'] = array(
1179 + 'activitypub_in_reply_to' => sanitize_text_field( wp_unslash( $_POST['in_reply_to'] ) ),
411 1180 );
412 1181 }
413 1182
414 1183 if ( ! empty( $p['post_content'] ) || ! empty( $p['post_title'] ) ) {
@@ -417,15 +1186,23 @@
417 1186 set_post_format( $post_id, $p['post_format'] );
418 1187 }
419 1188 $result = is_wp_error( $post_id ) ? 'error' : 'success';
420 1189 }
1190 + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1191 + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
421 1192 if ( ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) === 'xmlhttprequest' ) {
422 1193 echo esc_html( $result );
423 1194 exit;
424 - } else {
1195 + }
1196 +
1197 + if ( ! empty( $_SERVER['HTTP_REFERER'] ) ) {
425 1198 wp_safe_redirect( remove_query_arg( 'in_reply_to', add_query_arg( 'result', $result, $_SERVER['HTTP_REFERER'] ) ) );
426 1199 exit;
427 1200 }
1201 + wp_safe_redirect( home_url( '/friends/' ) );
1202 + exit;
1203 + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1204 + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
428 1205 }
429 1206
430 1207 /**
431 1208 * The Ajax function to change the post format from the Frontend.
@@ -430,10 +1207,10 @@
430 1207 /**
431 1208 * The Ajax function to change the post format from the Frontend.
432 1209 */
433 1210 public function ajax_change_post_format() {
434 - $post_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
435 - $post_format = isset( $_POST['format'] ) ? $_POST['format'] : 'standard';
1211 + $post_id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
1212 + $post_format = isset( $_POST['format'] ) ? sanitize_text_field( wp_unslash( $_POST['format'] ) ) : 'standard';
436 1213
437 1214 check_ajax_referer( "friends-change-post-format_$post_id" );
438 1215
439 1216 if ( ! current_user_can( Friends::REQUIRED_ROLE, $post_id ) ) {
@@ -461,9 +1238,9 @@
461 1238 * @param string $original_text The original text.
462 1239 *
463 1240 * @return float The read time in seconds.
464 1241 */
465 - private static function calculate_read_time( $original_text ) {
1242 + public static function calculate_read_time( $original_text ) {
466 1243 // from wp_trim_words().
467 1244 $text = wp_strip_all_tags( $original_text );
468 1245
469 1246 /*
@@ -493,22 +1270,51 @@
493 1270
494 1271 return count( $words_array ) / $words_per_minute * 60 + $additional_time;
495 1272 }
496 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 + }
497 1292 /**
498 1293 * Handles the post loop on the Friends page.
499 1294 */
500 1295 public static function have_posts() {
501 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 + }
502 1302 while ( have_posts() ) {
503 1303 global $post;
504 1304 the_post();
505 1305 $args = array(
506 1306 'friends' => $friends,
507 - 'avatar' => get_post_meta( get_the_ID(), 'gravatar', true ),
508 1307 );
1308 + $args['item_friend_user'] = User::get_post_author( $post );
509 1309
510 - $args['friend_user'] = User::get_post_author( $post );
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 + }
511 1317
512 1318 $read_time = self::calculate_read_time( get_the_content() );
513 1319 if ( $read_time >= 60 ) {
514 1320 $mins = ceil( $read_time / MINUTE_IN_SECONDS );
@@ -530,17 +1336,28 @@
530 1336 /**
531 1337 * The Ajax function to load more posts for infinite scrolling.
532 1338 */
533 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 + }
534 1346 $query_vars = wp_unslash( $_POST['query_vars'] );
1347 + // We do have a nonce verification here.
535 1348 if ( sha1( wp_salt( 'nonce' ) . $query_vars ) !== $_POST['qv_sign'] ) {
536 1349 wp_send_json_error();
537 1350 exit;
538 1351 }
539 - $query_vars = unserialize( $query_vars );
1352 +
1353 + $query_vars = json_decode( $query_vars, true );
540 1354 $query_vars['paged'] = intval( $_POST['page'] ) + 1;
1355 + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1356 + // phpcs:enable WordPress.Security.NonceVerification.Missing
541 1357
542 - query_posts( $query_vars );
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
543 1360 ob_start();
544 1361 if ( have_posts() ) {
545 1362 self::have_posts();
546 1363 } else {
@@ -553,87 +1370,94 @@
553 1370 wp_send_json_success( $posts );
554 1371 }
555 1372
556 1373 /**
557 - * Get metadata for in_reply_to_preview.
558 - *
559 - * @param string $url The url.
560 - *
561 - * @return array|WP_Error The in reply to metadata.
1374 + * The Ajax function to autocomplete search.
562 1375 */
563 - function get_in_reply_to_metadata( $url ) {
564 - $meta = apply_filters( 'friends_get_activitypub_metadata', array(), $url );
565 - if ( is_wp_error( $meta ) ) {
566 - return $meta;
1376 + public function ajax_autocomplete() {
1377 + check_ajax_referer( 'friends-autocomplete' );
1378 + if ( ! isset( $_POST['q'] ) ) {
1379 + wp_send_json_error();
1380 + exit;
567 1381 }
568 1382
569 - if ( ! $meta || ! isset( $meta['attributedTo'] ) ) {
570 - return new \WP_Error( 'no-activitypub', 'No ActivityPub metadata found.' );
571 - }
1383 + $q = sanitize_text_field( wp_unslash( $_POST['q'] ) );
1384 + $results = array();
572 1385
573 - $html = 'URL: ' . make_clickable( $meta['id'] );
574 - $html .= '<blockquote>' . force_balance_tags( wp_kses_post( $meta['content'] ) ) . '</blockquote>';
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 );
575 1396
576 - $webfinger = apply_filters( 'friends_get_activitypub_metadata', array(), $meta['attributedTo'] );
577 - $mention = '';
578 - if ( $webfinger && ! is_wp_error( $webfinger ) ) {
579 - $mention = '@' . $webfinger['preferredUsername'] . '@' . parse_url( $url, PHP_URL_HOST );
580 - }
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;
581 1404
582 - return array(
583 - 'url' => $url,
584 - 'html' => $html,
585 - 'author' => $meta['attributedTo'],
586 - 'mention' => $mention,
587 - );
588 -
1405 + wp_send_json_success( '<li class="menu-item">' . implode( '</li><li class="menu-item">', $results ) . '</li>' );
589 1406 }
590 1407
591 1408 /**
592 - * The Ajax function to fill the in-reply-to-preview.
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.
593 1415 */
594 - function ajax_in_reply_to_preview() {
595 - $url = wp_unslash( $_POST['url'] );
1416 + public function autocomplete_user_search( $results, $q ) {
1417 + $users = User_Query::search( '*' . $q . '*' );
596 1418
597 - if ( ! wp_parse_url( $url ) ) {
598 - wp_send_json_error();
599 - exit;
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;
600 1427 }
601 1428
602 - $meta = $this->get_in_reply_to_metadata( $_POST['url'] );
1429 + return $results;
1430 + }
603 1431
604 - if ( is_wp_error( $meta ) ) {
605 - wp_send_json_error( $meta->get_error_message() );
606 - exit;
607 - }
608 - wp_send_json_success( $meta );
609 - }
610 1432 /**
611 - * The Ajax function to autocomplete search.
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.
612 1441 */
613 - function ajax_autocomplete() {
614 - $q = wp_unslash( $_POST['q'] );
615 - $users = User_Query::search( '*' . $q . '*' );
616 - $results = array();
617 - foreach ( $users->get_results() as $friend ) {
618 - $result = '<li class="menu-item">';
619 - $result .= '<a href="' . esc_url( $friend->get_local_friends_page_url() ) . '" class="has-icon-left">';
620 - $result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->display_name );
621 - $result .= ' <small>';
622 - $result .= str_ireplace( $q, '<mark>' . $q . '</mark>', $friend->user_login );
623 - $result .= '</small></a></li>';
624 - $results[] = $result;
1442 + public function activitypub_comment_link( $comment_link, $comment ) {
1443 + if ( ! $comment || 'comment' !== $comment->comment_type ) {
1444 + return $comment_link;
625 1445 }
626 1446
627 - wp_send_json_success( implode( PHP_EOL, $results ) );
1447 + $source_url = get_comment_meta( $comment->comment_ID, 'source_url', true );
1448 + if ( $source_url ) {
1449 + return $source_url;
1450 + }
628 1451
1452 + return $comment_link;
629 1453 }
630 1454
631 1455 /**
632 1456 * The Ajax function to load comments.
633 1457 */
634 - function ajax_load_comments() {
635 - if ( ! isset( $_POST['post_id'] ) || ! intval( $_POST['post_id'] ) ) {
1458 + public function ajax_load_comments() {
1459 + if ( ! isset( $_POST['post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
636 1460 wp_send_json_error();
637 1461 exit;
638 1462 }
639 1463
@@ -639,84 +1463,68 @@
639 1463
640 1464 $post_id = intval( $_POST['post_id'] );
641 1465 check_ajax_referer( "comments-$post_id" );
642 1466
643 - $comments = get_comments(
644 - array(
645 - 'post_id' => $post_id,
646 - )
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
647 1477 );
648 1478
649 - $author_id = get_post_field( 'post_author', $post_id );
650 - $friend_user = new User( $author_id );
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 );
651 1482
652 - $comments_url = get_post_meta( $post_id, Feed::COMMENTS_FEED_META, true );
653 - if ( ! $comments_url && empty( $comments ) ) {
654 - wp_send_json_error( __( 'No comments feed available.', 'friends' ) );
655 - exit;
656 - }
1483 + remove_all_filters( 'comment_form_before' );
1484 + remove_all_filters( 'comment_form_after' );
657 1485
658 - if ( $friend_user->is_friend_url( $comments_url ) && friends::has_required_privileges() || wp_doing_cron() ) {
659 - $comments_url = apply_filters( 'friends_friend_private_feed_url', $comments_url, $friend_user );
660 - $comments_url = $this->friends->access_control->append_auth( $comments_url, $friend_user, 300 );
661 - }
1486 + // Add filter to fix ActivityPub comment links in AJAX context.
1487 + add_filter( 'get_comment_link', array( $this, 'activitypub_comment_link' ), 20, 2 );
662 1488
663 - $feed_comments = $this->friends->feed->preview( 'simplepie', $comments_url );
664 - if ( empty( $comments ) && ( is_wp_error( $feed_comments ) || ! is_array( $feed_comments ) ) ) {
665 - wp_send_json_error( '<small>' . __( 'Unfortunately, comments were not available via RSS.', 'friends' ) . '</small>' );
666 - exit;
667 - } elseif ( is_wp_error( $feed_comments ) ) {
668 - $feed_comments = array();
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();
669 1511 }
670 1512
671 - $template_loader = Friends::template_loader();
672 - ob_start();
673 - ?>
674 - <h5><?php esc_html_e( 'Comments' ); /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain */ ?></h5>
675 - <?php
676 - foreach ( $comments as $comment ) {
677 - $template_loader->get_template_part(
678 - 'frontend/parts/comment',
679 - null,
680 - array(
681 - 'author' => $comment->comment_author,
682 - 'date' => $comment->comment_date,
683 - 'permalink' => $comment->guid . '#comment-' . $comment->comment_ID,
684 - 'post_content' => $comment->comment_content,
685 - )
686 - );
687 - }
688 - foreach ( $feed_comments as $comment ) {
689 - $template_loader->get_template_part(
690 - 'frontend/parts/comment',
691 - null,
692 - array(
693 - 'author' => $comment->author,
694 - 'date' => $comment->date,
695 - 'permalink' => $comment->permalink,
696 - 'post_content' => $comment->post_content,
697 - )
698 - );
1513 + remove_filter( 'get_comment_link', array( $this, 'activitypub_comment_link' ), 20 );
699 1514
700 - }
701 - ?>
702 - <p>
703 - <a href="<?php echo esc_url( get_comments_link( $post_id ) ); ?>"><?php esc_html_e( 'Leave a Comment' ); /* phpcs:ignore WordPress.WP.I18n.MissingArgDomain */ ?></a>
704 - </p>
705 - <?php
706 - $content = ob_get_contents();
707 - ob_end_clean();
1515 + $content = apply_filters( 'friends_comments_content', $content, $post_id, $friend_user, $user_feed );
708 1516
709 1517 wp_send_json_success( $content );
710 1518 }
711 1519
712 1520 public function ajax_star_friend_user() {
713 - if ( ! isset( $_POST['friend_id'] ) || ! $_POST['friend_id'] ) {
1521 + if ( ! isset( $_POST['friend_id'] ) || ! isset( $_POST['starred'] ) ) {
714 1522 wp_send_json_error();
715 1523 exit;
716 1524 }
717 1525
718 - $friend_id = wp_unslash( $_POST['friend_id'] );
1526 + $friend_id = sanitize_text_field( wp_unslash( $_POST['friend_id'] ) );
719 1527 check_ajax_referer( "star-$friend_id" );
720 1528
721 1529 $friend_user = User::get_by_username( $friend_id );
722 1530
@@ -730,15 +1538,66 @@
730 1538 );
731 1539 }
732 1540
733 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 + /**
734 1594 * Ensure that untrashed friends posts go back to published.
735 1595 *
736 1596 * @param string $new_status The new status of the post being restored.
737 1597 * @param int $post_id The ID of the post being restored.
738 - * @param string $previous_status The status of the post at the point where it was trashed.
739 1598 */
740 - public function untrash_post_status( $new_status, $post_id, $previous_status ) {
1599 + public function untrash_post_status( $new_status, $post_id ) {
741 1600 if ( ! in_array( get_post_type( $post_id ), apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
742 1601 return $new_status;
743 1602 }
744 1603 return 'publish';
@@ -754,31 +1613,169 @@
754 1613 if ( ! Friends::on_frontend() ) {
755 1614 return $template;
756 1615 }
757 1616
758 - if ( isset( $_GET['refresh'] ) ) {
759 - add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
760 - add_filter(
761 - 'wp_feed_options',
762 - function( $feed ) {
763 - $feed->enable_cache( false );
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';
764 1637 }
765 - );
766 - $this->friends->feed->retrieve_friend_posts( true );
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 );
767 1645 }
768 1646
769 - global $args;
770 - $args = array(
771 - 'friends' => $this->friends,
772 - 'friend_user' => $this->author,
773 - 'frontend_default_view' => get_option( 'friends_frontend_default_view', 'expanded' ),
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',
774 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 + }
775 1700
776 - if ( isset( $_GET['in_reply_to'] ) && wp_parse_url( $_GET['in_reply_to'] ) ) {
777 - $args['in_reply_to'] = $args['friends']->frontend->get_in_reply_to_metadata( $_GET['in_reply_to'] );
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';
778 1774 }
779 1775
780 - return Friends::template_loader()->get_template_part( 'frontend/index', null, $args, false );
1776 + $wp_query->is_404 = false;
1777 + return $path;
781 1778 }
782 1779
783 1780 /**
784 1781 * Modify the Friends page title depending on context, for example add an author name or post format.
@@ -786,12 +1783,12 @@
786 1783 * @param string $title The original title.
787 1784 *
788 1785 * @return string The modified title.
789 1786 */
790 - function header_widget_title( $title ) {
1787 + public function header_widget_title( $title ) {
791 1788 $title = '<a href="' . esc_url( home_url( '/friends/' ) ) . '">' . esc_html( $title ) . '</a>';
792 1789 if ( $this->author ) {
793 - $title .= ' &raquo; ' . '<a href="' . esc_url( $this->author->get_local_friends_page_url() ) . '">' . esc_html( $this->author->display_name ) . '</a>';
1790 + $title .= ' &raquo; <a href="' . esc_url( $this->author->get_local_friends_page_url() ) . '">' . esc_html( $this->author->display_name ) . '</a>';
794 1791 }
795 1792 if ( $this->post_format ) {
796 1793 $post_formats = get_post_format_strings();
797 1794 $title .= ' &raquo; ' . $post_formats[ $this->post_format ];
@@ -806,9 +1803,9 @@
806 1803 * @param string $text The link text.
807 1804 * @param array $html_attributes HTML attributes.
808 1805 * @param User $friend_user The friend user.
809 1806 */
810 - function link( $url, $text, array $html_attributes = array(), User $friend_user = null ) {
1807 + public function link( $url, $text, array $html_attributes = array(), ?User $friend_user = null ) {
811 1808 echo wp_kses(
812 1809 self::get_link( $url, $text, $html_attributes, $friend_user ),
813 1810 array(
814 1811 'a' => array(
@@ -839,28 +1836,13 @@
839 1836 * @param User $friend_user The friend user.
840 1837 *
841 1838 * @return string The link.
842 1839 */
843 - public static function get_link( $url, $text, array $html_attributes = array(), User $friend_user = null ) {
1840 + public static function get_link( $url, $text, array $html_attributes = array(), ?User $friend_user = null ) {
844 1841 if ( is_null( $friend_user ) ) {
845 1842 $friend_user = new User( get_the_author_meta( 'ID' ) );
846 1843 }
847 1844
848 - if ( $friend_user->is_friend_url( $url ) && $friend_user->is_valid_friend() ) {
849 - $html_attributes['target'] = '_blank';
850 - $html_attributes['rel'] = 'noopener noreferrer';
851 - if ( ! isset( $html_attributes['class'] ) ) {
852 - $html_attributes['class'] = '';
853 - }
854 - $html_attributes['class'] = trim( $html_attributes['class'] . ' friends-auth-link' );
855 - if ( ! isset( $html_attributes['dashicon_back'] ) ) {
856 - $html_attributes['dashicon_back'] = 'admin-users';
857 - }
858 - $html_attributes['data-token'] = $friend_user->get_friend_auth();
859 - $html_attributes['data-nonce'] = wp_create_nonce( 'auth-link-' . $url );
860 - $html_attributes['data-friend'] = $friend_user->user_login;
861 - }
862 -
863 1845 $link = '<a href="' . esc_url( $url ) . '"';
864 1846 foreach ( $html_attributes as $name => $value ) {
865 1847 if ( ! in_array( $name, array( 'title', 'target', 'rel', 'class', 'style', 'data-nonce', 'data-cnonce', 'data-token', 'data-friend', 'data-id', 'data-url' ) ) ) {
866 1848 continue;
@@ -866,13 +1848,13 @@
866 1848 continue;
867 1849 }
868 1850 $link .= ' ' . $name . '="' . esc_attr( $value ) . '"';
869 1851 }
870 - $link .= '>';
1852 + $link .= ' title="' . esc_attr( $text ) . '">';
871 1853 if ( isset( $html_attributes['dashicon_front'] ) ) {
872 1854 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_front'] ) . '"></span>';
873 1855 }
874 - $link .= esc_html( $text );
1856 + $link .= '<span class="text">' . esc_html( $text ) . '</span>';
875 1857 if ( isset( $html_attributes['dashicon_back'] ) ) {
876 1858 $link .= '<span class="dashicons dashicons-' . esc_attr( $html_attributes['dashicon_back'] ) . '"></span>';
877 1859 }
878 1860 $link .= '</a>';
@@ -879,8 +1861,58 @@
879 1861
880 1862 return $link;
881 1863 }
882 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 +
883 1915 /**
884 1916 * Don't show the edit link for friend posts.
885 1917 *
886 1918 * @param string $link The edit link.
@@ -920,9 +1952,9 @@
920 1952 * Link friend posts to the remote site.
921 1953 *
922 1954 * @param string $post_link The post's permalink.
923 1955 * @param \WP_Post $post The post in question.
924 - * @reeturn string The overriden post link.
1956 + * @return string The overriden post link.
925 1957 */
926 1958 public function friend_post_link( $post_link, \WP_Post $post ) {
927 1959 if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
928 1960 return get_the_guid( $post );
@@ -930,8 +1962,29 @@
930 1962 return $post_link;
931 1963 }
932 1964
933 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 + /**
934 1987 * Potentially override the post author name with metadata.
935 1988 *
936 1989 * @param string $overridden_author_name The already overridden author name.
937 1990 * @param string $author_name The author name.
@@ -949,8 +2002,15 @@
949 2002 }
950 2003 return $author_name;
951 2004 }
952 2005
2006 + public function expose_opml( $viewable, $pagename ) {
2007 + if ( 'opml' === $pagename ) {
2008 + return true;
2009 + }
2010 + return $viewable;
2011 + }
2012 +
953 2013 /**
954 2014 * Render the Friends OPML
955 2015 *
956 2016 * @param bool $only_public Only public feed URLs.
@@ -955,12 +2015,18 @@
955 2015 *
956 2016 * @param bool $only_public Only public feed URLs.
957 2017 */
958 2018 protected function render_opml( $only_public = false ) {
959 - $user = wp_get_current_user();
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 + }
960 2026
961 2027 // translators: %s is a name.
962 - $title = sprintf( __( "%s' Subscriptions", 'friends' ), $user->display_name );
2028 + $title = sprintf( __( '%s is following', 'friends' ), $user->display_name );
963 2029 $filename = 'friends-';
964 2030 if ( ! $only_public ) {
965 2031 $title = __( 'My Friends', 'friends' );
966 2032 $filename .= 'private-';
@@ -969,9 +2035,9 @@
969 2035
970 2036 $feeds = array();
971 2037 $users = array();
972 2038
973 - $friend_users = new User_Query( array( 'role__in' => array( 'friend', 'acquaintance', 'friend_request', 'subscription' ) ) );
2039 + $friend_users = User_Query::all_associated_users();
974 2040 foreach ( $friend_users->get_results() as $friend_user ) {
975 2041 $role = $friend_user->get_role_name( true, 9 );
976 2042 if ( $only_public ) {
977 2043 $role = 'Feeds';
@@ -989,8 +2055,11 @@
989 2055
990 2056 $need_local_feed = false;
991 2057
992 2058 foreach ( $user_feeds as $feed ) {
2059 + if ( $feed->get_parser() === Feed_Parser_SimplePie::SLUG ) {
2060 + break;
2061 + }
993 2062 switch ( $feed->get_mime_type() ) {
994 2063 case 'application/atom+xml':
995 2064 case 'application/atomxml':
996 2065 case 'application/rss+xml':
@@ -1024,14 +2093,19 @@
1024 2093 if ( $only_public ) {
1025 2094 // Cannot create a public URL.
1026 2095 continue;
1027 2096 }
1028 - $xml_url = $feed->get_local_url() . '?auth=' . $_GET['auth'];
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 + }
1029 2103 } else {
1030 2104 if ( $only_public ) {
1031 2105 $xml_url = $feed->get_url();
1032 2106 } else {
1033 - $xml_url = $feed->get_private_url( YEAR_IN_SECONDS );
2107 + $xml_url = $feed->get_url();
1034 2108 }
1035 2109 if ( 'application/atom+xml' === $feed->get_mime_type() ) {
1036 2110 $type = 'atom';
1037 2111 }
@@ -1084,9 +2158,40 @@
1084 2158 return true;
1085 2159 }
1086 2160
1087 2161 return false;
2162 + }
1088 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;
1089 2194 }
1090 2195
1091 2196 /**
1092 2197 * Modify the main query for the /friends page
@@ -1102,8 +2207,10 @@
1102 2207
1103 2208 $pagename = '';
1104 2209 if ( isset( $wp_query->query['pagename'] ) ) {
1105 2210 $pagename = $wp_query->query['pagename'];
2211 + } elseif ( isset( $wp_query->query['category_name'] ) ) {
2212 + $pagename = $wp_query->query['category_name'];
1106 2213 } elseif ( isset( $wp_query->query['name'] ) ) {
1107 2214 $pagename = $wp_query->query['name'];
1108 2215 }
1109 2216
@@ -1108,27 +2215,37 @@
1108 2215 }
1109 2216
1110 2217 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
1111 2218 $is_friends = array_shift( $pagename_parts );
2219 +
1112 2220 if ( 'friends' !== $is_friends ) {
1113 2221 return $query;
1114 2222 }
1115 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 +
1116 2229 // Not available for the general public or friends.
1117 - $viewable = $this->has_required_priviledges();
2230 + $viewable = $this->has_required_priviledges() && Friends::on_frontend();
1118 2231 if ( $query->is_feed() ) {
1119 2232 // Feeds can be viewed through extra authentication.
1120 - if ( $this->friends->access_control->private_rss_is_authenticated() ) {
1121 - $viewable = true;
1122 - } elseif ( isset( $wp_query->query['pagename'] ) ) {
1123 - $pagename_parts = explode( '/', trim( $wp_query->query['pagename'], '/' ) );
1124 - if ( apply_filters( 'friends_friend_feed_viewable', false, $pagename_parts[1] ) ) {
2233 + if ( isset( $wp_query->query['pagename'] ) ) {
2234 + if ( apply_filters( 'friends_friend_feed_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) {
1125 2235 $viewable = true;
1126 2236 }
1127 2237 }
1128 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 +
1129 2246 $page_id = get_query_var( 'page' );
1130 -
2247 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
1131 2248 if ( isset( $_GET['share'] ) ) {
1132 2249 $share_hash = hash( 'crc32b', apply_filters( 'friends_share_salt', wp_salt( 'nonce' ), $page_id ) . $page_id );
1133 2250 if ( $_GET['share'] === $share_hash ) {
1134 2251 $viewable = true;
@@ -1134,9 +2251,9 @@
1134 2251 $viewable = true;
1135 2252 }
1136 2253 }
1137 2254
1138 - if ( ! $viewable || ! Friends::on_frontend() ) {
2255 + if ( ! $viewable ) {
1139 2256 if ( $query->is_feed() ) {
1140 2257 status_header( 404 );
1141 2258 $query->set_404();
1142 2259 } elseif ( ! Friends::on_frontend() ) {
@@ -1144,9 +2261,9 @@
1144 2261 wp_safe_redirect( home_url( '/friends/' ) );
1145 2262 exit;
1146 2263 }
1147 2264 } elseif ( ! Friends::is_main_user() ) {
1148 - wp_die( __( 'You are not allowed to view this page.', 'friends' ) );
2265 + wp_die( esc_html__( 'You are not allowed to view this page.', 'friends' ) );
1149 2266 }
1150 2267
1151 2268 return $query;
1152 2269 }
@@ -1181,9 +2298,9 @@
1181 2298 }
1182 2299
1183 2300 switch ( $current_part ) {
1184 2301 case 'opml':
1185 - return $this->render_opml( isset( $_REQUEST['public'] ) );
2302 + return $this->render_opml( isset( $_REQUEST['public'] ) && boolval( $_REQUEST['public'] ) );
1186 2303
1187 2304 case 'type':
1188 2305 $post_format = array_shift( $pagename_parts );
1189 2306 $tax_query = $this->friends->wp_query_get_post_format_tax_query( $tax_query, explode( ',', $post_format ) );
@@ -1191,8 +2308,19 @@
1191 2308 $this->post_format = $post_format;
1192 2309 }
1193 2310 break;
1194 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 +
1195 2323 default: // Maybe an author.
1196 2324 $author = User::get_by_username( $current_part );
1197 2325 if ( false === $author || is_wp_error( $author ) ) {
1198 2326 if ( $query->is_feed() ) {
@@ -1199,8 +2327,18 @@
1199 2327 status_header( 404 );
1200 2328 $query->set_404();
1201 2329 return $query;
1202 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 + }
1203 2341 wp_safe_redirect( home_url( '/friends/' ) );
1204 2342 exit;
1205 2343 }
1206 2344
@@ -1212,8 +2350,10 @@
1212 2350 $this->is_friends_page = true;
1213 2351 $query->is_friends_page = true;
1214 2352 $query->is_singular = false;
1215 2353 $query->is_single = false;
2354 + $query->is_category = false;
2355 + $query->is_archive = false;
1216 2356 $query->queried_object = null;
1217 2357 $query->queried_object_id = null;
1218 2358 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1219 2359
@@ -1224,23 +2364,24 @@
1224 2364
1225 2365 $query->set( 'post_type', $post_types );
1226 2366 $query->set( 'tax_query', $tax_query );
1227 2367
1228 - if ( ( isset( $_GET['share'] ) && $_GET['share'] === $share_hash ) || friends::has_required_privileges() ) {
1229 - $post_status = array( 'publish', 'private' );
2368 + if ( ( isset( $_GET['share'] ) && $_GET['share'] === $share_hash ) || $viewable ) {
2369 + $post_status = array( 'publish', 'private', 'future' );
1230 2370 if ( isset( $_GET['show-hidden'] ) ) {
1231 2371 $post_status[] = 'trash';
1232 2372 }
1233 2373 $query->set( 'post_status', $post_status );
1234 2374 }
2375 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
1235 2376 $query->is_page = false;
1236 - $query->is_comments_feed = false;
2377 + $query->is_comment_feed = false;
1237 2378 $query->set( 'pagename', null );
1238 - if ( 'collapsed' === get_option( 'friends_frontend_default_view', 'expanded' ) && get_option( 'posts_per_page' ) < 20 ) {
1239 - if ( 'status' === $post_format ) {
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 ) {
1240 2383 $query->set( 'posts_per_page', 30 );
1241 - } else {
1242 - $query->set( 'posts_per_page', 20 );
1243 2384 }
1244 2385 }
1245 2386
1246 2387 if ( $page_id ) {
@@ -1246,11 +2387,14 @@
1246 2387 if ( $page_id ) {
1247 2388 $query->set( 'page_id', $page_id );
1248 2389 if ( ! $this->author ) {
1249 2390 $post = get_post( $page_id );
1250 - $author = User::get_post_author( $post );
1251 - if ( false !== $author ) {
1252 - $this->author = $author;
2391 +
2392 + if ( $post ) {
2393 + $author = User::get_post_author( $post );
2394 + if ( false !== $author ) {
2395 + $this->author = $author;
2396 + }
1253 2397 }
1254 2398 }
1255 2399 $query->is_single = true;
1256 2400 $query->is_singular = true;
@@ -1259,9 +2403,9 @@
1259 2403 }
1260 2404
1261 2405 if ( $this->author ) {
1262 2406 if ( $this->author instanceof User ) {
1263 - $authordata = $this->author;
2407 + $authordata = $this->author; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1264 2408 $this->author->modify_query_by_author( $query );
1265 2409 if ( ! $page_id ) {
1266 2410 $query->is_author = true;
1267 2411 }
@@ -1272,12 +2416,23 @@
1272 2416
1273 2417 if ( ! $query->is_singular && ! $query->is_author ) {
1274 2418 // This is the main friends page.
1275 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 +
1276 2422 if ( $hide_from_friends_page ) {
1277 2423 $query->set( 'author__not_in', $hide_from_friends_page );
1278 2424 }
1279 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
1280 2435
1281 2436 return $query;
1282 2437 }
1283 2438 }