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

class-admin.php in Friends 4.3.2, at includes/class-admin.php

3,432 lines 117.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Friends Admin
4 *
5 * This contains the functions for the admin section.
6 *
7 * @package Friends
8 */
9
10 namespace Friends;
11
12 /**
13 * This is the class for the Friends Plugin Admin section.
14 *
15 * @since 0.6
16 *
17 * @package Friends
18 * @author Alex Kirk
19 */
20 class Admin {
21 /**
22 * Contains a reference to the Friends class.
23 *
24 * @var Friends
25 */
26 private $friends;
27
28 /**
29 * Constructor
30 *
31 * @param Friends $friends A reference to the Friends object.
32 */
33 public function __construct( Friends $friends ) {
34 $this->friends = $friends;
35 $this->register_hooks();
36 }
37
38 /**
39 * Register the WordPress hooks
40 */
41 private function register_hooks() {
42 add_action( 'admin_menu', array( $this, 'admin_menu' ) );
43 add_filter( 'users_list_table_query_args', array( $this, 'allow_role_multi_select' ) );
44 add_filter( 'the_title', array( $this, 'override_post_format_title' ), 10, 2 );
45 add_filter( 'get_edit_user_link', array( $this, 'admin_edit_user_link' ), 10, 2 );
46 add_action( 'admin_bar_menu', array( $this, 'admin_bar_friends_menu' ), 39 );
47 add_action( 'admin_bar_menu', array( $this, 'admin_bar_new_content' ), 71 );
48 add_action( 'wp_head', array( $this, 'admin_bar_mobile' ) );
49 add_action( 'admin_head', array( $this, 'admin_bar_mobile' ) );
50 add_action( 'current_screen', array( $this, 'register_help' ) );
51 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ), 39 );
52 add_action( 'gettext_with_context', array( $this->friends, 'translate_user_role' ), 10, 4 );
53 add_action( 'wp_ajax_friends_preview_rules', array( $this, 'ajax_preview_friend_rules' ) );
54 add_action( 'wp_ajax_friends_fetch_feeds', array( $this, 'ajax_fetch_feeds' ) );
55 add_action( 'wp_ajax_friends_set_avatar', array( $this, 'ajax_set_avatar' ) );
56 add_action( 'wp_ajax_friends-refresh-feeds', array( $this, 'ajax_refresh_feeds' ) );
57 add_action( 'wp_ajax_friends-preview-subscription', array( $this, 'ajax_preview_subscription' ) );
58 add_action( 'wp_ajax_friends-preview-subscription-feed', array( $this, 'ajax_preview_subscription_feed' ) );
59 add_action( 'wp_ajax_friends-subscribe-frontend', array( $this, 'ajax_subscribe_frontend' ) );
60 add_action( 'delete_user_form', array( $this, 'delete_user_form' ), 10, 2 );
61 add_action( 'delete_user', array( $this, 'delete_user' ) );
62 add_action( 'remove_user_from_blog', array( $this, 'delete_user' ) );
63 add_action( 'tool_box', array( $this, 'toolbox_bookmarklets' ) );
64 add_action( 'dashboard_glance_items', array( $this, 'dashboard_glance_items' ) );
65 add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widgets' ), 8 );
66 add_action( 'wp_ajax_friends_dashboard', array( $this, 'ajax_friends_dashboard' ) );
67 add_filter( 'site_status_test_php_modules', array( $this, 'site_status_test_php_modules' ) );
68 add_filter( 'friends_create_and_follow', array( $this, 'create_and_follow' ), 10, 4 );
69 add_action( 'friends_edit_feed_content_top', array( $this, 'maybe_render_activitypub_inactive_notice' ), 10, 3 );
70
71 if ( ! get_option( 'permalink_structure' ) ) {
72 add_action( 'admin_notices', array( $this, 'admin_notice_unsupported_permalink_structure' ) );
73 }
74 if ( get_option( 'friends_welcome_version' ) ) {
75 add_action( 'admin_notices', array( $this, 'admin_notice_welcome' ) );
76 }
77 add_filter( 'pre_get_posts', array( $this, 'admin_friend_posts_query' ) );
78 }
79
80 /**
81 * Display admin notice about an unsupported permalink structure
82 */
83 public function admin_notice_unsupported_permalink_structure() {
84 $screen = get_current_screen();
85
86 if ( 'plugins' !== $screen->id ) {
87 return;
88 }
89
90 ?>
91 <div class="friends-notice notice notice-error">
92 <p style="max-width:800px;"><b><?php esc_html_e( 'Friends', 'friends' ); ?></b><?php esc_html_e( '&#151; You are running an unsupported permalink structure.', 'friends' ); ?></p>
93 <p style="max-width:800px;">
94 <?php
95 echo wp_kses_post(
96 sprintf(
97 // translators: 1: URL to permalink settings, 2: the name of the Permalink Settings page.
98 __( 'In order to be able to view the Friends page, you need to enable a custom permalink structure. Please go to <a href="%1$s">%2$s</a> and enable an option other than Plain.', 'friends' ),
99 admin_url( 'options-permalink.php' ),
100 __( 'Permalink Settings' ) // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
101 )
102 );
103 ?>
104 </p>
105 </div>
106 <?php
107 }
108
109 /**
110 * Registers the admin menus
111 */
112 public function admin_menu() {
113 if ( isset( $_REQUEST['rerun-activate'] ) && isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'friends-settings' ) ) {
114 Friends::activate_plugin();
115 wp_safe_redirect( add_query_arg( array( 'reran-activation' => 'friends' ), wp_get_referer() ) );
116 exit;
117 }
118 $required_role = Friends::required_menu_role();
119 $unread_badge = $this->get_unread_badge();
120
121 $menu_title = __( 'Friends', 'friends' ) . $unread_badge;
122 $page_type = sanitize_title( $menu_title );
123 $current_page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
124 add_menu_page( __( 'Friends', 'friends' ), $menu_title, $required_role, 'friends', null, 'dashicons-groups', 3 );
125 add_submenu_page( 'friends', __( 'Friends', 'friends' ), __( 'Home', 'friends' ), $required_role, 'friends', array( $this, 'render_admin_home' ) );
126 add_action( 'load-' . $page_type . '_page_friends-page', array( $this, 'redirect_to_friends_page' ) );
127 add_submenu_page( 'friends', __( 'Add Friend', 'friends' ), __( 'Add Friend', 'friends' ), $required_role, 'add-friend', array( $this, 'render_admin_add_friend' ) );
128 // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
129 add_submenu_page( 'friends', __( 'Settings' ), __( 'Settings' ), $required_role, 'friends-settings', array( $this, 'render_admin_settings' ) );
130 if (
131 in_array(
132 $current_page,
133 apply_filters( 'friends_admin_settings_slugs', array( 'friends-settings', 'friends-notification-manager', 'friends-wp-friendships', 'friends-import-export', 'friends-migrations' ) )
134 )
135 ) {
136 add_submenu_page( 'friends', __( 'Notifications', 'friends' ), '- ' . __( 'Notifications', 'friends' ), $required_role, 'friends-notification-manager', array( $this, 'render_admin_notification_manager' ) );
137 add_submenu_page( 'friends', __( 'Import/Export', 'friends' ), '- ' . __( 'Import/Export', 'friends' ), $required_role, 'friends-import-export', array( $this, 'render_admin_import_export' ) );
138 do_action( 'friends_admin_menu_settings', $page_type );
139 }
140
141 if ( 'friends-migrations' === $current_page && current_user_can( 'manage_options' ) ) {
142 add_submenu_page( 'friends', __( 'Migrations', 'friends' ), __( 'Migrations', 'friends' ), 'manage_options', 'friends-migrations', array( Migration::class, 'render_admin_page' ) );
143 }
144 add_action( 'load-' . $page_type . '_page_friends-notification-manager', array( $this, 'process_admin_notification_manager' ) );
145 add_action( 'load-' . $page_type . '_page_friends-import-export', array( $this, 'process_admin_import_export' ) );
146 add_action( 'load-' . $page_type . '_page_friends-settings', array( $this, 'process_admin_settings' ) );
147
148 if (
149 isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'friends-refresh' ) && 'friends-refresh' === $current_page
150 ) {
151 add_submenu_page( 'friends', __( 'Refresh', 'friends' ), __( 'Refresh', 'friends' ), $required_role, 'friends-refresh', array( $this, 'admin_refresh_friend_posts' ) );
152 }
153
154 $friend_submenu_items = array(
155 'edit-friend' => __( 'Edit User', 'friends' ),
156 'edit-friend-feeds' => __( 'Edit Feeds', 'friends' ),
157 'edit-friend-notifications' => __( 'Edit Notifications', 'friends' ),
158 'edit-friend-rules' => __( 'Edit Rules', 'friends' ),
159 'duplicate-remover' => __( 'Duplicates', 'friends' ),
160 );
161 if ( isset( $friend_submenu_items[ $current_page ] ) ) {
162 foreach ( $friend_submenu_items as $slug => $title ) {
163 $user_param = '';
164 if ( isset( $_GET['user'] ) ) {
165 $username = sanitize_user( wp_unslash( $_GET['user'] ) );
166 $user_param = '&user=' . $username . '&_wpnonce=' . wp_create_nonce( $slug . '-' . $username );
167 }
168 $slug_ = strtr( $slug, '-', '_' );
169
170 add_submenu_page(
171 'friends',
172 $title,
173 $title,
174 $required_role,
175 $slug . ( $slug === $current_page ? '' : $user_param ),
176 array( $this, 'render_admin_' . $slug_ )
177 );
178
179 add_action(
180 'load-' . $page_type . '_page_' . $slug,
181 array( $this, 'process_admin_' . $slug_ )
182 );
183 }
184 }
185
186 if ( isset( $_GET['page'] ) && 'friends-logs' === $_GET['page'] ) {
187 // translators: as in log file.
188 $title = __( 'Log', 'friends' );
189 add_submenu_page( 'friends', $title, $title, $required_role, 'friends-logs', array( $this, 'render_friends_logs' ) );
190 }
191
192 $title = __( 'Browser Extension', 'friends' );
193 add_submenu_page( 'friends', $title, $title, $required_role, 'friends-browser-extension', array( $this, 'render_browser_extension' ) );
194
195 if ( isset( $_GET['page'] ) && 'unfriend' === $_GET['page'] ) {
196 $user = new User( intval( $_GET['user'] ) );
197 if ( $user ) {
198 $title = /* translators: %s is a username. */ sprintf( __( 'Unfriend %s', 'friends' ), $user->user_login );
199 add_submenu_page( 'friends', $title, $title, $required_role, 'unfriend', array( $this, 'render_admin_unfriend' ) );
200 add_action( 'load-' . $page_type . '_page_unfriend', array( $this, 'process_admin_unfriend' ) );
201 }
202 }
203 }
204
205 /**
206 * Allow making use of the role__in query.
207 *
208 * @param array $args The arguments.
209 *
210 * @return array The modified array.
211 */
212 public function allow_role_multi_select( $args ) {
213 if ( isset( $args['role'] ) && ! isset( $args['role__in'] ) ) {
214 if ( false !== strpos( $args['role'], ',' ) ) {
215 $args['role__in'] = explode( ',', $args['role'] );
216 unset( $args['role'] );
217 }
218
219 $roles = self::get_associated_roles();
220 if (
221 ( isset( $args['role__in'] ) && array_intersect( $args['role__in'], array_keys( $roles ) ) )
222 || ( isset( $args['role'] ) && isset( $roles[ $args['role'] ] ) )
223 ) {
224 add_action( 'admin_head-users.php', array( $this, 'keep_friends_open_on_users_screen' ) );
225 }
226 }
227 return $args;
228 }
229
230 /**
231 * Use JavaScript to keep the Friends menu open when responding to a Friend Request.
232 */
233 public function keep_friends_open_on_users_screen() {
234 ?>
235 <script type="text/javascript">
236 jQuery( document ).ready( function ( $ ) {
237 $( '#toplevel_page_friends-settings, #toplevel_page_friends-settings > a' ).addClass( 'wp-has-current-submenu wp-menu-open' ).removeClass( 'wp-not-current-submenu' );
238 $( '#menu-users > a' ).removeClass( 'wp-has-current-submenu wp-menu-open' );
239 $( "#toplevel_page_friends-settings ul li a[href='<?php echo esc_html( self::get_users_url() ); ?>']" ).closest( 'li' ).addClass( 'current' );
240 } );
241 </script>
242 <?php
243 }
244
245 /**
246 * Add our help information
247 *
248 * @param \WP_Screen $screen The current wp-admin screen.
249 */
250 public function register_help( $screen ) {
251 if ( ! ( $screen instanceof \WP_Screen ) ) {
252 return;
253 }
254
255 switch ( $screen->id ) {
256 case 'toplevel_page_friends-settings':
257 $screen->add_help_tab(
258 array(
259 'id' => 'overview',
260 'title' => __( 'Overview', 'friends' ),
261 'content' =>
262 '<p>' .
263 __( 'Welcome to the Friends Settings! You can configure the Friends plugin here to your liking.', 'friends' ) .
264 '</p>' .
265 '<p>' .
266 sprintf(
267 // translators: %1$s is a URL, %2$s is the name of a page.
268 __( 'There are more settings available for each friend or subscription individually. To get there, click on the user on the <a href=%1$s>%2$s</a> page.', 'friends' ),
269 '"' . esc_attr( self::get_users_url() ) . '"',
270 __( 'Following', 'friends' )
271 ) .
272 '</p>',
273 )
274 );
275 break;
276 case 'users':
277 $screen->add_help_tab(
278 array(
279 'id' => 'friends',
280 'title' => __( 'Friends', 'friends' ),
281 'content' => '<p>' . __( 'Here you can find your friends and subscriptions.', 'friends' ) . '</p><p>' . __( 'If you no longer want to be friends with someone or stop a subscription, you can simply delete that user.', 'friends' ) . '</p>',
282 )
283 );
284 break;
285 }
286 }
287
288 /**
289 * Reference our script for the /friends page
290 */
291 public function admin_enqueue_scripts() {
292 $handle = 'friends-admin';
293 $file = 'friends-admin.js';
294 $version = Friends::VERSION;
295 wp_enqueue_script( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery' ), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ), true );
296
297 $variables = array(
298 'ajax_url' => admin_url( 'admin-ajax.php' ),
299 'add_friend_url' => self_admin_url( 'admin.php?page=add-friend' ),
300 'add_friend_text' => __( 'Add a Friend', 'friends' ),
301 'copy_text' => __( 'Copy', 'friends' ),
302 'copied_text' => __( 'Copied!', 'friends' ),
303 'delete_feed_question' => __( 'Delete the feed? You need to click "Save Changes" to really delete it.', 'friends' ),
304 'role_subscription' => __( 'Following', 'friends' ),
305 'role_connection' => __( 'Connection', 'friends' ),
306 'role_contact' => __( 'Contact', 'friends' ),
307 'role_connection_request' => __( 'Connection Request', 'friends' ),
308 'role_pending_connection_request' => __( 'Pending Connection Request', 'friends' ),
309 'role_following' => __( 'Following', 'friends' ),
310 );
311 wp_localize_script( 'friends-admin', 'friends', $variables );
312
313 $handle = 'friends-admin';
314 $file = 'friends-admin.css';
315 $version = Friends::VERSION;
316 wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) );
317 }
318
319 /**
320 * Admin menu to refresh the friend posts.
321 */
322 public function admin_refresh_friend_posts() {
323 ?>
324 <h1><?php esc_html_e( "Refreshing Your Friends' Posts", 'friends' ); ?></h1>
325 <?php
326
327 add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
328
329 add_filter(
330 'friends_friend_private_feed_url',
331 function ( $feed_url, $friend_user ) {
332 echo wp_kses(
333 // translators: %1s is the name of the friend, %2$s is the feed URL.
334 sprintf( __( 'Refreshing %1$s at %2$s', 'friends' ) . '<br/>', '<a href="' . esc_url( $friend_user->get_local_friends_page_url() ) . '">' . esc_html( $friend_user->user_login ) . '</a>', '<a href="' . esc_url( $feed_url ) . '">' . esc_html( $feed_url ) . '</a>' ),
335 array(
336 'a' => array(
337 'href' => array(),
338 ),
339 )
340 );
341 return $feed_url;
342 },
343 10,
344 2
345 );
346
347 add_action(
348 'friends_retrieved_new_posts',
349 function ( $user_feed, $new_posts, $modified_posts ) {
350 // translators: %s is the number of new posts found.
351 echo esc_html( sprintf( _n( 'Found %d new post.', 'Found %d new posts.', count( $new_posts ), 'friends' ), count( $new_posts ) ) );
352 ?>
353 <br />
354 <?php
355 // translators: %s is the number of modified posts.
356 echo esc_html( sprintf( _n( '%d post was modified.', '%d posts were modified.', count( $modified_posts ), 'friends' ), count( $modified_posts ) ) );
357 ?>
358 <br />
359 <?php
360 },
361 10,
362 3
363 );
364
365 add_action(
366 'friends_incoming_feed_items',
367 function ( $items ) {
368 // translators: %s is the number of posts found.
369 echo esc_html( sprintf( _n( 'Found %d item in the feed.', 'Found %d items in the feed.', count( $items ), 'friends' ) . ' ', count( $items ) ) );
370 }
371 );
372
373 add_action(
374 'friends_retrieve_friends_error',
375 function ( $user_feed, $error ) {
376 esc_html_e( 'An error occurred while retrieving the posts.', 'friends' );
377 echo esc_html( $error->get_error_message() ), '<br/>';
378 },
379 10,
380 2
381 );
382
383 if ( isset( $_GET['user'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
384 $friend_user = User::get_by_username( sanitize_user( wp_unslash( $_GET['user'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
385 if ( ! $friend_user || is_wp_error( $friend_user ) || ! $friend_user->can_refresh_feeds() ) {
386 wp_die( esc_html__( 'Invalid user ID.' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
387 }
388 $friend_user->retrieve_posts_from_active_feeds();
389 } else {
390 $this->friends->feed->retrieve_friend_posts();
391 }
392 }
393
394 /**
395 * Don't show the edit link for friend posts
396 *
397 * @param string $link The edit link.
398 * @param int|User $user The user.
399 * @return string|bool The edit link or false.
400 */
401 public static function admin_edit_user_link( $link, $user ) {
402 static $cache = array();
403 if ( $user instanceof \WP_User ) {
404 $cache_key = $user->ID;
405 } else {
406 $cache_key = $user;
407 }
408
409 if ( isset( $cache[ $cache_key ] ) ) {
410 if ( false === $cache[ $cache_key ] ) {
411 return $link;
412 }
413 return $cache[ $cache_key ];
414 }
415 if ( ! $user instanceof \WP_User ) {
416 if ( is_string( $user ) ) {
417 $user = User::get_by_username( $user );
418 } else {
419 $user = new \WP_User( $user );
420 }
421 }
422
423 if ( ! $user || is_wp_error( $user ) ) {
424 $cache[ $cache_key ] = false;
425 return $link;
426 }
427
428 if ( is_multisite() && is_super_admin( $user->ID ) ) {
429 $cache[ $cache_key ] = false;
430 return $link;
431 }
432 if ( ! $user->has_cap( 'friends_plugin' ) ) {
433 $cache[ $cache_key ] = false;
434 return $link;
435 }
436
437 $cache[ $cache_key ] = self_admin_url( 'admin.php?page=edit-friend&user=' . $user->user_login );
438 return $cache[ $cache_key ];
439 }
440
441 public static function get_edit_friend_link( $user ) {
442 if ( is_string( $user ) ) {
443 $user = User::get_by_username( $user );
444 } elseif ( ! $user instanceof User && ! $user instanceof Subscription ) {
445 $user = new User( $user );
446 }
447
448 if ( ! $user || is_wp_error( $user ) ) {
449 return '';
450 }
451
452 return apply_filters( 'get_edit_user_link', $user->user_url, $user->user_login );
453 }
454
455 public static function get_unfriend_link( $user ) {
456 if ( ! $user->has_cap( 'friends_plugin' ) ) {
457 return '';
458 }
459
460 return wp_nonce_url( self_admin_url( 'admin.php?page=unfriend&user=' . $user->user_login ), 'unfriend_' . $user->user_login );
461 }
462
463 /**
464 * Redirect to the Friends page
465 */
466 public function redirect_to_friends_page() {
467 wp_safe_redirect( home_url( '/friends/' ) );
468 exit;
469 }
470
471 /**
472 * Check access for the Friends Admin settings page
473 */
474 public function check_admin_settings() {
475 if ( ! Friends::has_required_privileges() ) {
476 wp_die( esc_html__( 'Sorry, you are not allowed to change the settings.', 'friends' ) );
477 }
478 }
479
480 /**
481 * Process the Friends Admin settings page
482 */
483 public function process_admin_settings() {
484 if ( empty( $_REQUEST ) || ! isset( $_REQUEST['_wpnonce'] ) ) {
485 return;
486 }
487
488 if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'friends-settings' ) ) {
489 return;
490 }
491
492 $this->check_admin_settings();
493 foreach ( array( 'disable_auto_tagging', 'disable_link_previews' ) as $checkbox ) {
494 if ( isset( $_POST[ $checkbox ] ) && boolval( $_POST[ $checkbox ] ) ) {
495 update_option( 'friends_' . $checkbox, true );
496 } else {
497 delete_option( 'friends_' . $checkbox );
498 }
499 }
500
501 if ( current_user_can( 'manage_options' ) ) {
502 if ( isset( $_POST['main_user_id'] ) ) {
503 $main_user_id = absint( $_POST['main_user_id'] );
504 if ( $main_user_id && user_can( $main_user_id, Friends::REQUIRED_ROLE ) ) {
505 update_option( 'friends_main_user_id', $main_user_id );
506 }
507 }
508
509 foreach ( array( 'force_enable_post_formats', 'expose_post_format_feeds', 'exclude_compose_format_from_feed' ) as $checkbox ) {
510 if ( isset( $_POST[ $checkbox ] ) && boolval( $_POST[ $checkbox ] ) ) {
511 update_option( 'friends_' . $checkbox, true );
512 } else {
513 delete_option( 'friends_' . $checkbox );
514 }
515 }
516
517 $post_format_slugs = get_post_format_slugs();
518 if ( isset( $_POST['friends_compose_post_format'] ) && in_array( sanitize_key( $_POST['friends_compose_post_format'] ), array_merge( array( 'standard' ), $post_format_slugs ), true ) ) {
519 update_option( 'friends_compose_post_format', sanitize_key( $_POST['friends_compose_post_format'] ) );
520 } else {
521 delete_option( 'friends_compose_post_format' );
522 }
523 }
524
525 if ( isset( $_POST['available_emojis'] ) && is_array( $_POST['available_emojis'] ) ) {
526 $available_emojis = array();
527 foreach ( wp_unslash( $_POST['available_emojis'] ) as $id ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
528 $id = sanitize_key( $id );
529 $data = Reactions::get_emoji_data( $id );
530 if ( $data ) {
531 $available_emojis[ $id ] = $data;
532 }
533 }
534 update_option( 'friends_selected_emojis', $available_emojis );
535 } else {
536 delete_option( 'friends_selected_emojis' );
537 }
538
539 // Global retention.
540 $retention_number_enabled = boolval( isset( $_POST['friends_enable_retention_number'] ) && $_POST['friends_enable_retention_number'] );
541 update_option( 'friends_enable_retention_number', $retention_number_enabled );
542 if ( $retention_number_enabled && isset( $_POST['friends_retention_number'] ) ) {
543 update_option( 'friends_retention_number', max( 1, intval( $_POST['friends_retention_number'] ) ) );
544 }
545 $retention_days_enabled = boolval( isset( $_POST['friends_enable_retention_days'] ) && $_POST['friends_enable_retention_days'] );
546 update_option( 'friends_enable_retention_days', $retention_days_enabled );
547 if ( $retention_days_enabled && isset( $_POST['friends_retention_days'] ) ) {
548 update_option( 'friends_retention_days', max( 1, intval( $_POST['friends_retention_days'] ) ) );
549 }
550
551 if ( isset( $_POST['retention_delete_reacted'] ) && 1 === intval( $_POST['retention_delete_reacted'] ) ) {
552 delete_option( 'friends_retention_delete_reacted' );
553 } else {
554 update_option( 'friends_retention_delete_reacted', true );
555 }
556
557 if ( isset( $_POST['frontend_default_view'] ) && in_array(
558 wp_unslash( $_POST['frontend_default_view'] ),
559 array(
560 'collapsed',
561 )
562 ) ) {
563 update_user_option( get_current_user_id(), 'friends_frontend_default_view', wp_unslash( $_POST['frontend_default_view'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
564 } else {
565 delete_user_option( get_current_user_id(), 'friends_frontend_default_view' );
566 }
567
568 foreach ( array_merge( array( '' ), get_post_format_slugs() ) as $post_type ) {
569 $name = 'friends_frontend_theme';
570 if ( $post_type ) {
571 $name = 'friends_frontend_theme_' . $post_type;
572 }
573 $theme = 'default';
574 if ( isset( $_POST[ $name ] ) && in_array( $theme, array_keys( Frontend::get_themes() ) ) ) {
575 $theme = wp_unslash( $_POST[ $name ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
576 }
577 if ( 'default' === $theme ) {
578 delete_user_option( get_current_user_id(), $name );
579 } else {
580 update_user_option( get_current_user_id(), $name, $theme );
581 }
582 }
583
584 $redirect_args = array( 'updated' => '1' );
585
586 if ( isset( $_GET['_wp_http_referer'] ) ) {
587 wp_safe_redirect( wp_get_referer() );
588 } else {
589 wp_safe_redirect( add_query_arg( $redirect_args, remove_query_arg( array( '_wp_http_referer', '_wpnonce' ) ) ) );
590 }
591 exit;
592 }
593
594 /**
595 * Gets the frontend locale.
596 *
597 * @return string The frontend locale.
598 */
599 public function get_frontend_locale() {
600 $locale = get_option( 'WPLANG' );
601 return empty( $locale ) ? 'en_US' : $locale;
602 }
603
604 /**
605 * Get the registry of news entries, newest first.
606 *
607 * Each entry has: version, title, template, and optionally migration_version
608 * for entries that should show migration status.
609 *
610 * @return array
611 */
612 public static function get_news_entries() {
613 return apply_filters(
614 'friends_news_entries',
615 array(
616 array(
617 'version' => '4.3',
618 'title' => __( '4.3: Link Previews', 'friends' ),
619 'template' => 'admin/news-4-3',
620 ),
621 array(
622 'version' => '4.2',
623 'title' => __( '4.2: Direct Messages', 'friends' ),
624 'template' => 'admin/news-4-2',
625 ),
626 array(
627 'version' => '4.1',
628 'title' => __( '4.1: Add Friend Frontend, Twitter Theme & Browser Extension', 'friends' ),
629 'template' => 'admin/news-4-1',
630 ),
631 array(
632 'version' => '4.0',
633 'title' => __( '4.0: A Major Update', 'friends' ),
634 'template' => 'admin/welcome-4-0',
635 'migration_version' => '4.0.0',
636 ),
637 array(
638 'version' => '3.3',
639 'title' => __( '3.3: Styling Overhaul', 'friends' ),
640 'template' => 'admin/news-3-3',
641 ),
642 array(
643 'version' => '3.0',
644 'title' => __( '3.0: Followers & Notifications', 'friends' ),
645 'template' => 'admin/news-3-0',
646 ),
647 array(
648 'version' => '2.4',
649 'title' => __( '2.4: Mastodon Compatibility', 'friends' ),
650 'template' => 'admin/news-2-4',
651 ),
652 array(
653 'version' => '2.1',
654 'title' => __( '2.1: Frontend & Plugins', 'friends' ),
655 'template' => 'admin/news-2-1',
656 ),
657 array(
658 'version' => '2.0',
659 'title' => __( '2.0: Revisions & Site Health', 'friends' ),
660 'template' => 'admin/news-2-0',
661 ),
662 array(
663 'version' => '0',
664 'title' => __( 'Welcome to the Friends Plugin!', 'friends' ),
665 'template' => 'admin/welcome',
666 ),
667 )
668 );
669 }
670
671 /**
672 * Get migration statuses for a specific version.
673 *
674 * @param string $migration_version The version to filter migrations for.
675 * @return array With keys: statuses, all_complete, has_in_progress.
676 */
677 public static function get_migration_data( $migration_version ) {
678 $all_statuses = Migration::get_all_statuses();
679 $statuses = array();
680 $all_complete = true;
681 $has_in_progress = false;
682
683 foreach ( $all_statuses as $id => $status ) {
684 if ( $status['version'] !== $migration_version ) {
685 continue;
686 }
687 $statuses[ $id ] = $status;
688 if ( empty( $status['completed'] ) ) {
689 $all_complete = false;
690 }
691 if ( ! empty( $status['in_progress'] ) ) {
692 $has_in_progress = true;
693 }
694 }
695
696 return array(
697 'statuses' => $statuses,
698 'all_complete' => $all_complete,
699 'has_in_progress' => $has_in_progress,
700 );
701 }
702
703 /**
704 * Render the Friends Admin home page.
705 *
706 * Shows the welcome page for new users (no subscriptions),
707 * or a news/changelog view for existing users.
708 */
709 public function render_admin_home() {
710 // Dismiss the update notice permanently when visiting this page.
711 if ( get_option( 'friends_welcome_version' ) ) {
712 delete_option( 'friends_welcome_version' );
713 }
714
715 $friends_subscriptions = User_Query::all_associated_users();
716 $is_new_user = 0 === $friends_subscriptions->get_total();
717
718 wp_enqueue_script( 'plugin-install' );
719 add_thickbox();
720 wp_enqueue_script( 'updates' );
721
722 Friends::template_loader()->get_template_part(
723 'admin/settings-header',
724 null,
725 array(
726 'active' => 'friends',
727 )
728 );
729
730 $news_entries = self::get_news_entries();
731
732 if ( $is_new_user ) {
733 // New users: welcome entry first, rest after.
734 $news_entries = array_reverse( $news_entries );
735 }
736
737 Friends::template_loader()->get_template_part(
738 'admin/news',
739 null,
740 array(
741 'entries' => $news_entries,
742 )
743 );
744
745 Friends::template_loader()->get_template_part( 'admin/settings-footer' );
746 }
747
748 /**
749 * Process the response after adding a friend/subscription.
750 *
751 * @param User|\WP_Error $friend_user The friend user object.
752 * @param array $vars The form variables.
753 *
754 * @return bool Whether the operation was successful.
755 */
756 private function process_admin_add_friend_response( $friend_user, $vars ) {
757 if ( is_wp_error( $friend_user ) ) {
758 $this->display_errors( $friend_user );
759 return false;
760 }
761
762 if ( ! $friend_user instanceof User ) {
763 ?>
764 <div id="message" class="updated notice is-dismissible"><p>
765 <?php esc_html_e( 'Unknown error', 'friends' ); ?>
766 </p></div>
767 <?php
768 return false;
769 }
770
771 $feed_options = array();
772 if ( ! isset( $vars['feeds'] ) ) {
773 $vars['feeds'] = array();
774 }
775 foreach ( $vars['feeds'] as $feed ) {
776 if ( isset( $feed['type'] ) ) {
777 $feed['mime-type'] = $feed['type'];
778 unset( $feed['type'] );
779 }
780 $feed_options[ $feed['url'] ] = $feed;
781 }
782
783 $friend_user->save_feeds( $feed_options );
784
785 if ( ! isset( $vars['subscribe'] ) ) {
786 $vars['subscribe'] = array();
787 }
788
789 $count = 0;
790 foreach ( $vars['subscribe'] as $feed_url ) {
791 if ( ! isset( $feed_options[ $feed_url ] ) ) {
792 continue;
793 }
794 $new_feed = $friend_user->subscribe( $feed_url, $feed_options[ $feed_url ] );
795 if ( ! is_wp_error( $new_feed ) ) {
796 do_action( 'friends_user_feed_activated', $new_feed );
797 ++$count;
798 }
799 }
800
801 add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
802 wp_schedule_single_event( time(), 'friends_retrieve_user_feeds', array( $friend_user->ID ) );
803
804 $friend_link = '<a href="' . esc_url( $this->admin_edit_user_link( $friend_user->get_local_friends_page_url(), $friend_user ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $friend_user->display_name ) . '</a>';
805
806 // translators: %s is a Site URL.
807 $message = sprintf( __( "You're now subscribed to %s.", 'friends' ), $friend_link );
808
809 ?>
810 <div id="message" class="updated notice is-dismissible"><p>
811 <?php
812 echo wp_kses( $message, array( 'a' => array( 'href' => array() ) ) );
813 // translators: %s is the friends page URL.
814 echo ' ', wp_kses( sprintf( __( 'Go to your <a href=%s>friends page</a> to view their posts.', 'friends' ), '"' . esc_url( $friend_user->get_local_friends_page_url() ) . '"' ), array( 'a' => array( 'href' => array() ) ) );
815 echo ' <span id="fetch-feeds" data-nonce="', esc_attr( wp_create_nonce( 'fetch-feeds-' . sanitize_user( $friend_user->user_login ) ) ), '" data-friend=', esc_attr( $friend_user->user_login ), '>', esc_html__( 'Fetching feeds...', 'friends' ), '</span>';
816 ?>
817 </p></div>
818 <?php
819 return true;
820 }
821
822 /**
823 * Process the Add Friend form.
824 *
825 * @param array $vars The POST or GET variables.
826 *
827 * @return \WP_Error|null|bool A \WP_Error, null, or true on success.
828 */
829 public function process_admin_add_friend( $vars ) {
830 $errors = new \WP_Error();
831
832 $friend_url = isset( $vars['friend_url'] ) ? trim( $vars['friend_url'] ) : '';
833
834 $friend_user = false;
835
836 $protocol = wp_parse_url( $friend_url, PHP_URL_SCHEME );
837 if ( ! $protocol ) {
838 if ( is_multisite() ) {
839 $friend_user = get_user_by( 'login', $friend_url );
840 if ( $friend_user ) {
841 $site = get_active_blog_for_user( $friend_user->ID );
842 $friend_url = set_url_scheme( $site->siteurl );
843 }
844 }
845
846 if ( ! $friend_user ) {
847 $friend_url = apply_filters( 'friends_rewrite_incoming_url', 'https://' . $friend_url, $friend_url );
848 }
849 }
850 $friend_user_login = apply_filters( 'friends_suggest_user_login', User::get_user_login_for_url( $friend_url ), $friend_url );
851 $friend_display_name = apply_filters( 'friends_suggest_display_name', User::get_display_name_for_url( $friend_url ), $friend_url );
852
853 $friend_user = get_user_by( 'login', $friend_user_login );
854
855 $args = array();
856 if ( $friend_user ) {
857 $args['friends_multisite_user_login'] = $friend_user_login;
858 $args['friends_multisite_display_name'] = $friend_display_name;
859 }
860
861 if ( ( isset( $vars['step2'] ) && isset( $vars['feeds'] ) && is_array( $vars['feeds'] ) ) || isset( $vars['step3'] ) ) {
862 $friend_user_login = trim( str_replace( ' ', '-', sanitize_user( $vars['user_login'] ) ), '-' );
863 $friend_display_name = sanitize_text_field( $vars['display_name'] );
864 if ( ! $friend_user_login ) {
865 // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
866 $errors->add( 'user_login', __( '<strong>Error</strong>: This username is invalid because it uses illegal characters. Please enter a valid username.' ) );
867 } elseif ( ! is_multisite() && username_exists( $friend_user_login ) ) {
868 // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
869 $errors->add( 'user_login', __( '<strong>Error</strong>: This username is already registered. Please choose another one.' ) );
870 }
871
872 $feeds = $vars['feeds'];
873 if ( ! $errors->has_errors() ) {
874 $avatar = null;
875 $description = null;
876 foreach ( $feeds as $feed_details ) {
877 if ( ! $avatar && ! empty( $feed_details['avatar'] ) ) {
878 $avatar = $feed_details['avatar'];
879 }
880 if ( ! $description && ! empty( $feed_details['description'] ) ) {
881 $description = wp_encode_emoji( $feed_details['description'] );
882 }
883 }
884
885 $friend_user = User::create( $friend_user_login, 'subscription', $friend_url, $friend_display_name, $avatar, $description );
886
887 return $this->process_admin_add_friend_response( $friend_user, $vars );
888 }
889 } else {
890 if ( str_starts_with( $friend_url, home_url() ) ) {
891 return new \WP_Error( 'friend-yourself', __( 'It seems like you sent a friend request to yourself.', 'friends' ) );
892 }
893
894 if ( preg_match( '#https://.*?@threads.net#', $friend_url ) ) {
895 return new \WP_Error(
896 'threads-net',
897 sprintf(
898 // translators: %s is a URL.
899 __( '⚠️ This user has <a href="%s">not enabled Fediverse sharing on their Threads.net account</a>.', 'friends' ),
900 'https://about.fb.com/news/2023/07/introducing-threads-new-app-text-sharing/'
901 )
902 );
903 }
904
905 if ( ! Friends::check_url( $friend_url ) ) {
906 return new \WP_Error( 'invalid-url', __( 'You entered an invalid URL.', 'friends' ) );
907 }
908
909 $friend_user = User::get_user( $friend_user_login );
910 if ( $friend_user && ! is_wp_error( $friend_user ) ) {
911 // translators: %s is the name of a friend / site.
912 return new \WP_Error( 'already-subscribed', sprintf( __( 'You are already subscribed to this site: %s', 'friends' ), '<a href="' . esc_url( $this->admin_edit_user_link( $friend_user->get_local_friends_page_url(), $friend_user ) ) . '">' . esc_html( $friend_user->display_name ) . '</a>' ) );
913 }
914
915 $feeds = $this->friends->feed->discover_available_feeds( $friend_url );
916 if ( is_wp_error( $feeds ) ) {
917 return $feeds;
918 }
919 if ( ! $feeds ) {
920 return new \WP_Error( 'no-feed-found', __( 'No suitable feed was found at the provided address.', 'friends' ) );
921 }
922 $has_subscribable_feeds = false;
923 $has_threads_net = false;
924 foreach ( $feeds as $url => $feed ) {
925 if ( 0 === strpos( $url, 'https://threads.net/' ) ) {
926 $has_threads_net = true;
927 }
928 if ( isset( $feed['autoselect'] ) && $feed['autoselect'] ) {
929 $has_subscribable_feeds = true;
930 break;
931 }
932 if ( 'unsupported' !== $feed['parser'] ) {
933 $has_subscribable_feeds = true;
934 break;
935 }
936 }
937
938 if ( ! $has_subscribable_feeds && $has_threads_net ) {
939 $args['feeds_notice'] = sprintf(
940 // translators: %s is a URL.
941 __( '⚠️ This user has <a href="%s">not enabled Fediverse sharing on their Threads.net account</a>.', 'friends' ),
942 'https://about.fb.com/news/2023/07/introducing-threads-new-app-text-sharing/'
943 );
944 }
945
946 $better_user_login = User::get_user_login_from_feeds( $feeds );
947 if ( $better_user_login ) {
948 $friend_user_login = trim( $better_user_login, '-' );
949 }
950
951 $better_display_name = User::get_display_name_from_feeds( $feeds );
952 if ( $better_display_name ) {
953 $friend_display_name = $better_display_name;
954 if ( ! $better_user_login ) {
955 $friend_user_login = trim( strtolower( str_replace( ' ', '-', sanitize_user( $better_display_name ) ) ), '-' );
956 }
957 }
958 }
959
960 if ( isset( $vars['quick-subscribe'] ) ) {
961 $vars['feeds'] = $feeds;
962 $vars['subscribe'] = array();
963 foreach ( $feeds as $feed_url => $details ) {
964 if ( isset( $details['autoselect'] ) && $details['autoselect'] ) {
965 $vars['subscribe'][] = $feed_url;
966 }
967 }
968
969 $avatar = null;
970 $description = null;
971 foreach ( $feeds as $feed_details ) {
972 if ( ! $avatar && ! empty( $feed_details['avatar'] ) ) {
973 $avatar = $feed_details['avatar'];
974 }
975 if ( ! $description && ! empty( $feed_details['description'] ) ) {
976 $description = $feed_details['description'];
977 }
978 }
979
980 $friend_user = User::create( $friend_user_login, 'subscription', $friend_url, $friend_display_name, $avatar, $description );
981
982 return $this->process_admin_add_friend_response( $friend_user, $vars );
983 }
984
985 Friends::template_loader()->get_template_part(
986 'admin/settings-header',
987 null,
988 array(
989 'active' => 'add-friend-confirm',
990 'title' => __( 'Add Friend', 'friends' ),
991 'menu' => array(
992 '1. ' . __( 'Enter Details', 'friends' ) => array(
993 'page' => 'add-friend',
994 'url' => ! empty( $friend_url ) ? $friend_url : false,
995 ),
996 '2. ' . __( 'Confirm', 'friends' ) => 'add-friend-confirm',
997 ),
998 )
999 );
1000
1001 if ( $errors->has_errors() ) {
1002 ?>
1003 <div id="message" class="updated notice is-dismissible"><p><?php echo wp_kses( $errors->get_error_message(), array( 'strong' => array() ) ); ?></p>
1004 </div>
1005 <?php
1006 }
1007
1008 Friends::template_loader()->get_template_part(
1009 'admin/select-feeds',
1010 null,
1011 array_merge(
1012 $args,
1013 array(
1014 'friend_url' => $friend_url,
1015 'friend_user_login' => $friend_user_login,
1016 'friend_display_name' => $friend_display_name,
1017 'post_formats' => array_merge( array( 'autodetect' => __( 'Autodetect Post Format', 'friends' ) ), get_post_format_strings() ),
1018 'registered_parsers' => $this->friends->feed->get_registered_parsers(),
1019 'feeds' => $feeds,
1020 )
1021 )
1022 );
1023 }
1024
1025 /**
1026 * Render the admin form for following someone.
1027 */
1028 public function render_admin_add_friend() {
1029 if ( ! Friends::has_required_privileges() ) {
1030 wp_die( esc_html__( 'Sorry, you are not allowed to do this.', 'friends' ) );
1031 }
1032
1033 if ( ! empty( $_GET['preview'] ) ) {
1034 $url = sanitize_text_field( wp_unslash( $_GET['preview'] ) );
1035
1036 ?>
1037 <h1>
1038 <?php
1039 // translators: %s is a URL.
1040 echo esc_html( sprintf( __( 'Preview for %s', 'friends' ), $url ) );
1041 ?>
1042 </h1>
1043 <?php
1044
1045 if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'preview-feed' ) ) {
1046 ?>
1047 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'For security reasons, this preview is not available.', 'friends' ); ?></p>
1048 </div>
1049 <?php
1050 return;
1051 }
1052 $parser = false;
1053 if ( isset( $_GET['parser'] ) ) {
1054 $parser_name = $this->friends->feed->get_registered_parser( sanitize_text_field( wp_unslash( $_GET['parser'] ) ) );
1055 $parser = $this->friends->feed->get_feed_parser( sanitize_text_field( wp_unslash( $_GET['parser'] ) ) );
1056 }
1057 if ( ! $parser ) {
1058 ?>
1059 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'An unknown parser name was supplied.', 'friends' ); ?></p>
1060 </div>
1061 <?php
1062 return;
1063 }
1064 ?>
1065 <h3><?php esc_html_e( 'Parser Details', 'friends' ); ?></h3>
1066 <ul id="parser">
1067 <li>
1068 <?php
1069 echo wp_kses(
1070 // translators: %s is the name of a parser, e.g. simplepie.
1071 sprintf( __( 'Parser: %s', 'friends' ), $parser_name ),
1072 array(
1073 'a' => array(
1074 'href' => array(),
1075 'rel' => array(),
1076 'target' => array(),
1077 ),
1078 )
1079 );
1080 ?>
1081 </li>
1082 </ul>
1083 <h3><?php esc_html_e( 'Items in the Feed', 'friends' ); ?></h3>
1084
1085 <?php
1086 $feed_id = null;
1087 if ( isset( $_GET['feed'] ) ) {
1088 $feed_id = intval( $_GET['feed'] );
1089 }
1090 $items = $this->friends->feed->preview( $parser, $url, $feed_id );
1091 if ( is_wp_error( $items ) ) {
1092 ?>
1093 <div id="message" class="updated notice is-dismissible"><p><?php echo esc_html( $items->get_error_message() ); ?></p>
1094 </div>
1095 <?php
1096 return;
1097 }
1098 ?>
1099
1100 <ul>
1101 <?php
1102 foreach ( $items as $item ) {
1103 $title = $item->title;
1104 if ( 'status' === $item->post_format ) {
1105 $title = wp_strip_all_tags( $item->content );
1106 }
1107 ?>
1108 <li>
1109 <?php if ( $title ) : ?>
1110 <details><summary>
1111 <?php endif; ?>
1112 <a href="<?php echo esc_url( $item->permalink ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $item->date ); ?></a> (author: <?php echo esc_html( $item->author ); ?>, type: <?php echo esc_html( $item->post_format ); ?>):
1113 <?php if ( $title ) : ?>
1114 <a href="<?php echo esc_url( $item->permalink ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $title ); ?></a> <?php echo esc_html( str_word_count( wp_strip_all_tags( $item->content ) ) ); ?> words</summary>
1115 <?php else : ?>
1116 <p>
1117 <?php endif; ?>
1118 <?php echo esc_textarea( $item->content ); ?>
1119 <?php if ( $title ) : ?>
1120 </details>
1121 <?php else : ?>
1122 </p>
1123 <?php endif; ?>
1124 </li>
1125 <?php
1126 }
1127 ?>
1128 </ul>
1129 <?php
1130 return;
1131 }
1132
1133 if ( apply_filters( 'friends_debug', false ) && isset( $_GET['next'] ) ) {
1134 $_POST = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1135 $_POST['_wpnonce'] = wp_create_nonce( 'add-friend' );
1136 if ( ! empty( $_POST['url'] ) && ! isset( $_POST['friend_url'] ) ) {
1137 $friend_url = sanitize_text_field( wp_unslash( $_POST['url'] ) );
1138 $parsed_url = wp_parse_url( $friend_url );
1139 if ( isset( $parsed_url['host'] ) ) {
1140 if ( ! isset( $parsed_url['scheme'] ) ) {
1141 $friend_url = 'https://' . ltrim( $friend_url, '/' );
1142 }
1143 }
1144 $_POST['friend_url'] = $friend_url;
1145 }
1146 }
1147
1148 $response = null;
1149 $postdata = apply_filters( 'friends_add_friend_postdata', $_POST );
1150 if ( ! empty( $postdata ) ) {
1151 if ( ! wp_verify_nonce( sanitize_key( $postdata['_wpnonce'] ), 'add-friend' ) ) {
1152 $response = new \WP_Error( 'invalid-nonce', __( 'For security reasons, please verify the URL and click next if you want to proceed.', 'friends' ) );
1153 } else {
1154 $response = $this->process_admin_add_friend( $postdata );
1155 }
1156 if ( is_wp_error( $response ) ) {
1157 ?>
1158 <div id="message" class="updated notice is-dismissible"><p>
1159 <?php
1160 $message = $response->get_error_message();
1161 if ( $response->get_error_data() ) {
1162 $message .= ' (' . $response->get_error_data() . ')';
1163 }
1164 echo wp_kses(
1165 $message,
1166 array(
1167 'strong' => array(),
1168 'a' => array(
1169 'href' => array(),
1170 'rel' => array(),
1171 'target' => array(),
1172 ),
1173 )
1174 );
1175 ?>
1176 </p>
1177 </div>
1178 <?php
1179 }
1180 if ( is_null( $response ) ) {
1181 return;
1182 }
1183 }
1184
1185 $args = array(
1186 'friend_url' => '',
1187 'add-friends-placeholder' => apply_filters( 'friends_add_friends_input_placeholder', __( 'Enter URL', 'friends' ) ),
1188 );
1189
1190 if ( ! empty( $_REQUEST['url'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1191 $friend_url = sanitize_text_field( wp_unslash( $_REQUEST['url'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1192 $parsed_url = wp_parse_url( $friend_url );
1193 if ( isset( $parsed_url['host'] ) ) {
1194 if ( ! isset( $parsed_url['scheme'] ) ) {
1195 $args['friend_url'] = apply_filters( 'friends_rewrite_incoming_url', 'https://' . ltrim( $friend_url, '/' ), $friend_url, $parsed_url );
1196 } else {
1197 $args['friend_url'] = $friend_url;
1198 }
1199 } elseif ( class_exists( 'Friends\Feed_Parser_ActivityPub' ) && preg_match( '/^@?' . Feed_Parser_ActivityPub::ACTIVITYPUB_USERNAME_REGEXP . '$/i', $friend_url ) ) {
1200 $args['friend_url'] = $friend_url;
1201 }
1202 }
1203
1204 Friends::template_loader()->get_template_part(
1205 'admin/settings-header',
1206 null,
1207 array(
1208 'active' => 'add-friend',
1209 'title' => __( 'Add Friend', 'friends' ),
1210 'menu' => array(
1211 '1. ' . __( 'Enter Details', 'friends' ) => array(
1212 'page' => 'add-friend',
1213 'url' => ! empty( $friend_url ) ? $friend_url : false,
1214 ),
1215 '2. ' . __( 'Confirm', 'friends' ) => false,
1216 ),
1217 )
1218 );
1219
1220 Friends::template_loader()->get_template_part( 'admin/add-friend', null, $args );
1221
1222 Friends::template_loader()->get_template_part(
1223 'admin/latest-friends',
1224 null,
1225 array(
1226 'friend_requests' => User_Query::recent_friends_subscriptions( 25 )->get_results(),
1227 )
1228 );
1229 Friends::template_loader()->get_template_part( 'admin/settings-footer', null, $args );
1230 }
1231
1232 /**
1233 * Display admin notice about a new version.
1234 */
1235 public function admin_notice_welcome() {
1236 if ( ! current_user_can( 'manage_options' ) ) {
1237 return;
1238 }
1239
1240 if ( isset( $_GET['page'] ) && 'friends' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1241 return;
1242 }
1243
1244 $version = get_option( 'friends_welcome_version' );
1245 $url = admin_url( 'admin.php?page=friends' );
1246 ?>
1247 <div class="friends-notice notice notice-info">
1248 <p>
1249 <b><?php esc_html_e( 'Friends', 'friends' ); ?></b>
1250 <?php
1251 echo wp_kses(
1252 sprintf(
1253 // translators: %1$s is the version number, %2$s is a URL to the What's New page.
1254 __( '&#151; You have been updated to version %1$s! <a href="%2$s">See what\'s new and check the migration status</a>.', 'friends' ),
1255 esc_html( $version ),
1256 esc_url( $url )
1257 ),
1258 array( 'a' => array( 'href' => array() ) )
1259 );
1260 ?>
1261 </p>
1262 </div>
1263 <?php
1264 }
1265
1266 /**
1267 * Render the Friends Admin settings page
1268 */
1269 public function render_admin_settings() {
1270 Friends::template_loader()->get_template_part(
1271 'admin/settings-header',
1272 null,
1273 array(
1274 'active' => 'friends-settings',
1275 )
1276 );
1277 $this->check_admin_settings();
1278
1279 if ( isset( $_GET['updated'] ) && boolval( $_GET['updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1280 ?>
1281 <div id="message" class="updated notice is-dismissible"><p>
1282 <?php
1283 esc_html_e( 'Your settings were updated.', 'friends' );
1284 ?>
1285 </p></div>
1286 <?php
1287 }
1288
1289 $post_stats = Friends::get_post_stats();
1290 $post_type_themes = array();
1291 foreach ( get_post_format_slugs() as $slug ) {
1292 $post_type_themes[ 'frontend_theme_' . $slug ] = get_user_option( 'friends_frontend_theme_' . $slug );
1293 }
1294
1295 Friends::template_loader()->get_template_part(
1296 'admin/settings',
1297 null,
1298 array_merge(
1299 Friends::get_post_stats(),
1300 $post_type_themes,
1301 array(
1302 'force_enable_post_formats' => get_option( 'friends_force_enable_post_formats' ),
1303 'post_format_strings' => get_post_format_strings(),
1304 'limit_homepage_post_format' => get_option( 'friends_limit_homepage_post_format', false ),
1305 'expose_post_format_feeds' => get_option( 'friends_expose_post_format_feeds' ),
1306 'compose_post_format' => get_option( 'friends_compose_post_format', 'status' ),
1307 'exclude_compose_format_from_feed' => get_option( 'friends_exclude_compose_format_from_feed' ),
1308 'main_user_id' => Friends::get_main_friend_user_id(),
1309 'potential_main_users' => User_Query::all_admin_users(),
1310 'disable_auto_tagging' => get_option( 'friends_disable_auto_tagging' ),
1311 'disable_link_previews' => get_option( 'friends_disable_link_previews' ),
1312 'retention_days' => Friends::get_retention_days(),
1313 'retention_number' => Friends::get_retention_number(),
1314 'retention_days_enabled' => get_option( 'friends_enable_retention_days' ),
1315 'retention_number_enabled' => get_option( 'friends_enable_retention_number' ),
1316 'retention_delete_reacted' => get_option( 'friends_retention_delete_reacted' ),
1317 'frontend_default_view' => get_user_option( 'friends_frontend_default_view', get_current_user_id() ),
1318 'frontend_theme' => get_user_option( 'friends_frontend_theme' ),
1319 )
1320 )
1321 );
1322
1323 Friends::template_loader()->get_template_part( 'admin/settings-footer' );
1324 }
1325
1326 /**
1327 * Process access for the Friends Edit Rules page
1328 */
1329 private function check_admin_edit_friend_rules() {
1330 if ( ! Friends::is_main_user() ) {
1331 wp_die( esc_html__( 'Sorry, you are not allowed to edit the rules.', 'friends' ) );
1332 }
1333
1334 if ( ! isset( $_GET['user'] ) ) {
1335 wp_die( esc_html__( 'Invalid user.', 'friends' ) );
1336 }
1337
1338 if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'edit-friend-rules-' . sanitize_user( wp_unslash( $_GET['user'] ) ) ) ) {
1339 wp_die( esc_html__( 'Invalid nonce.', 'friends' ) );
1340 }
1341
1342 $friend = User::get_by_username( sanitize_user( wp_unslash( $_GET['user'] ) ) );
1343 if ( ! $friend || is_wp_error( $friend ) ) {
1344 wp_die( esc_html__( 'Invalid username.', 'friends' ) );
1345 }
1346
1347 if ( ! $friend->has_cap( 'subscription' ) ) {
1348 wp_die( esc_html__( 'This is not a user related to this plugin.', 'friends' ) );
1349 }
1350
1351 return $friend;
1352 }
1353
1354 /**
1355 * Process the Friends Edit Rules page
1356 */
1357 public function process_admin_edit_friend_rules() {
1358 $friend = $this->check_admin_edit_friend_rules();
1359 $arg = 'updated';
1360 $arg_value = 1;
1361 if ( isset( $_POST['_wpnonce'] ) && ! empty( $_POST['friend-rules-raw'] ) && wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'friend-rules-raw-' . $friend->user_login ) ) {
1362 $rules = validate_feed_rules( wp_unslash( $_POST['friend-rules-raw'] ) );
1363 if ( false === $rules ) {
1364 $arg = 'error';
1365 } else {
1366 $friend->update_user_option( 'friends_feed_rules', $rules );
1367 }
1368 } elseif ( isset( $_POST['_wpnonce'] ) && ! empty( $_POST['rules'] ) && ! empty( $_POST['catch_all'] ) && wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'edit-friend-rules-' . sanitize_user( $friend->user_login ) ) ) {
1369 $friend->update_user_option(
1370 'friends_feed_catch_all',
1371 validate_feed_catch_all( wp_unslash( $_POST['catch_all'] ) )
1372 );
1373 $friend->update_user_option(
1374 'friends_feed_rules',
1375 validate_feed_rules( wp_unslash( $_POST['rules'] ) )
1376 );
1377 } else {
1378 return;
1379 }
1380
1381 if ( isset( $_GET['_wp_http_referer'] ) ) {
1382 wp_safe_redirect( wp_get_referer() );
1383 } else {
1384 wp_safe_redirect( add_query_arg( $arg, $arg_value, remove_query_arg( '_wp_http_referer' ) ) );
1385 }
1386 exit;
1387 }
1388
1389 /**
1390 * Render the Friends Edit Rules page
1391 */
1392 public function render_admin_edit_friend_rules() {
1393 $friend = $this->check_admin_edit_friend_rules();
1394 $catch_all = $friend->get_feed_catch_all();
1395 $rules = $friend->get_feed_rules();
1396
1397 $this->header_edit_friend( $friend, 'edit-friend-rules' );
1398
1399 if ( isset( $_GET['updated'] ) ) {
1400 ?>
1401 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'Rules were updated.', 'friends' ); ?></p></div>
1402 <?php
1403 } elseif ( isset( $_GET['error'] ) ) {
1404 ?>
1405 <div id="message" class="updated error is-dismissible"><p><?php esc_html_e( 'An error occurred.', 'friends' ); ?></p></div>
1406 <?php
1407 }
1408
1409 $rules = array_values( $rules );
1410 $rules[] = array(
1411 'field' => 'title',
1412 'regex' => '',
1413 'action' => in_array( $catch_all, array( 'trash', 'delete' ), true ) ? 'accept' : 'trash',
1414 'replace' => '',
1415 );
1416
1417 if ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'edit-friend-rules-' . sanitize_user( $friend->user_login ) ) ) {
1418 if ( isset( $_GET['post'] ) && intval( $_GET['post'] ) ) {
1419 $post = get_post( intval( $_GET['post'] ) );
1420 } else {
1421 $post = null;
1422 }
1423 }
1424
1425 $args = array(
1426 'rules' => $rules,
1427 'friend' => $friend,
1428 'catch_all' => $catch_all,
1429 'post' => $post,
1430 );
1431 Friends::template_loader()->get_template_part( 'admin/edit-rules', null, $args );
1432
1433 echo '<div id="preview-rules">';
1434 $this->render_preview_friend_rules( $rules, $catch_all, $post );
1435 echo '</div>';
1436
1437 array_pop( $args['rules'] );
1438 Friends::template_loader()->get_template_part( 'admin/edit-raw-rules', null, $args );
1439 }
1440
1441 /**
1442 * Respond to the Ajax request to the Friend rules preview
1443 */
1444 public function ajax_preview_friend_rules() {
1445 if ( ! Friends::has_required_privileges() ) {
1446 wp_die( -1 );
1447 }
1448 if ( ! isset( $_GET['user'] ) ) {
1449 wp_die( esc_html__( 'Invalid user.', 'friends' ) );
1450 }
1451
1452 check_ajax_referer( 'edit-friend-rules-' . sanitize_user( wp_unslash( $_GET['user'] ) ) );
1453
1454 if ( isset( $_GET['post'] ) && intval( $_GET['post'] ) ) {
1455 $post = get_post( intval( $_GET['post'] ) );
1456 } else {
1457 $post = null;
1458 }
1459 $rules = array();
1460 if ( isset( $_POST['rules'] ) ) {
1461 $rules = validate_feed_rules( wp_unslash( $_POST['rules'] ) );
1462 }
1463 $catch_all = array();
1464 if ( isset( $_POST['catch_all'] ) ) {
1465 $catch_all = validate_feed_rules( wp_unslash( $_POST['catch_all'] ) );
1466 }
1467 $this->render_preview_friend_rules( $rules, $catch_all, $post );
1468 wp_die( 1 );
1469 }
1470
1471 /**
1472 * Respond to the Ajax request to fetch feeds
1473 */
1474 public function ajax_fetch_feeds() {
1475 if ( ! isset( $_POST['friend'] ) ) {
1476 wp_send_json_error( 'missing-parameters' );
1477 }
1478
1479 check_ajax_referer( 'fetch-feeds-' . sanitize_user( wp_unslash( $_POST['friend'] ) ) );
1480
1481 $friend_user = User::get_by_username( sanitize_user( wp_unslash( $_POST['friend'] ) ) );
1482 if ( ! $friend_user ) {
1483 wp_send_json_error( 'unknown-user' );
1484 }
1485
1486 add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
1487
1488 $friend_user->retrieve_posts_from_active_feeds();
1489
1490 wp_send_json_success();
1491 }
1492
1493 /**
1494 * Render the Friend rules preview
1495 *
1496 * @param array $rules The rules to apply.
1497 * @param string $catch_all The catch all behavior.
1498 * @param \WP_Post $post The post.
1499 */
1500 public function render_preview_friend_rules( $rules, $catch_all, ?\WP_Post $post = null ) {
1501 $friend = $this->check_admin_edit_friend_rules();
1502 $friend_posts = new \WP_Query();
1503
1504 $friend_posts->set( 'post_type', Friends::CPT );
1505 $friend_posts->set( 'post_status', array( 'publish', 'private', 'trash' ) );
1506 $friend_posts->set( 'posts_per_page', 25 );
1507 $friend_posts = $friend->modify_query_by_author( $friend_posts );
1508
1509 $args = array(
1510 'friend' => $friend,
1511 'friend_posts' => $friend_posts,
1512 'feed' => $this->friends->feed,
1513 'post' => $post,
1514 );
1515
1516 $friend->set_feed_rules( $rules );
1517 $friend->set_feed_catch_all( $catch_all );
1518
1519 Friends::template_loader()->get_template_part( 'admin/preview-rules', null, $args );
1520 }
1521
1522 /**
1523 * Process access for the Friends Edit User page
1524 */
1525 private function check_admin_edit_friend() {
1526 if ( ! friends::has_required_privileges() ) {
1527 wp_die( esc_html__( 'Sorry, you are not allowed to edit this user.' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1528 }
1529
1530 if ( ! isset( $_GET['user'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1531 wp_die( esc_html__( 'Invalid user.', 'friends' ) );
1532 }
1533
1534 $friend = User::get_by_username( sanitize_user( wp_unslash( $_GET['user'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
1535 if ( ! $friend || is_wp_error( $friend ) ) {
1536 wp_die( esc_html__( 'Invalid username.', 'friends' ) );
1537 }
1538
1539 if ( ! $friend->has_cap( 'friends_plugin' ) ) {
1540 wp_die( esc_html__( 'This is not a user related to this plugin.', 'friends' ) );
1541 }
1542
1543 return $friend;
1544 }
1545
1546 /**
1547 * Process the Friends Edit User page
1548 */
1549 public function process_admin_edit_friend() {
1550 $friend = $this->check_admin_edit_friend();
1551 $arg = 'updated';
1552 $arg_value = 1;
1553
1554 if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'edit-friend-' . $friend->user_login ) ) {
1555 if ( isset( $_POST['friends_display_name'] ) ) {
1556 $friends_display_name = trim( sanitize_text_field( wp_unslash( $_POST['friends_display_name'] ) ) );
1557 if ( $friends_display_name ) {
1558 $friend->first_name = $friends_display_name;
1559 $friend->display_name = $friends_display_name;
1560 }
1561 }
1562 if ( isset( $_POST['friends_description'] ) ) {
1563 $friend->description = trim( sanitize_text_field( wp_unslash( $_POST['friends_description'] ) ) );
1564 }
1565 if ( isset( $_POST['user_url'] ) ) {
1566 $user_url = sanitize_text_field( wp_unslash( $_POST['user_url'] ) );
1567 if ( filter_var( $user_url, FILTER_VALIDATE_URL ) ) {
1568 $friend->user_url = $user_url;
1569 }
1570 }
1571 if ( isset( $_POST['friends_user_login'] ) ) {
1572 $new_user_login = User::sanitize_username( sanitize_text_field( wp_unslash( $_POST['friends_user_login'] ) ) );
1573 if ( $new_user_login && $new_user_login !== $friend->user_login ) {
1574 $friend->update_user_login( $new_user_login );
1575 }
1576 }
1577 $friend->save();
1578 } else {
1579 return;
1580 }
1581
1582 do_action( 'friends_edit_friend_after_form_submit', $friend );
1583
1584 $redirect_url = self_admin_url( 'admin.php?page=edit-friend&user=' . $friend->user_login );
1585 wp_safe_redirect( add_query_arg( $arg, rawurlencode( $arg_value ), $redirect_url ) );
1586 exit;
1587 }
1588
1589 /**
1590 * The Friends Edit User header
1591 *
1592 * @param User $friend The friend.
1593 * @param string $active The active menu entry.
1594 */
1595 public function header_edit_friend( User $friend, $active ) {
1596 $append = '&user=' . sanitize_user( $friend->user_login );
1597 Friends::template_loader()->get_template_part(
1598 'admin/settings-header',
1599 null,
1600 array(
1601 'active' => $active . $append,
1602 'title' => $friend->user_login,
1603 'menu' => array(
1604 __( 'Posts' ) => $friend->get_local_friends_page_url(), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1605 __( 'Settings' ) => 'edit-friend' . $append, // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1606 __( 'Feeds', 'friends' ) => 'edit-friend-feeds' . $append,
1607 __( 'Notifications', 'friends' ) => 'edit-friend-notifications' . $append,
1608 __( 'Rules', 'friends' ) => 'edit-friend-rules' . $append . '&_wpnonce=' . wp_create_nonce( 'edit-friend-rules-' . $friend->user_login ),
1609 ),
1610 )
1611 );
1612 }
1613
1614 /**
1615 * Render the Friends Edit User page
1616 */
1617 public function render_admin_edit_friend() {
1618 $friend = $this->check_admin_edit_friend();
1619
1620 $args = array_merge(
1621 $friend->get_post_stats(),
1622 array(
1623 'friend' => $friend,
1624 'friends_settings_url' => add_query_arg( '_wp_http_referer', remove_query_arg( '_wp_http_referer' ), self_admin_url( 'admin.php?page=friends-settings' ) ),
1625 'registered_parsers' => $this->friends->feed->get_registered_parsers(),
1626 )
1627 );
1628
1629 $this->header_edit_friend( $friend, 'edit-friend' );
1630 // phpcs:disable WordPress.Security.NonceVerification
1631 if ( isset( $_GET['updated'] ) ) {
1632 ?>
1633 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'User was updated.', 'friends' ); ?></p></div>
1634 <?php
1635 } elseif ( isset( $_GET['friend'] ) ) {
1636 ?>
1637 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'You are now friends.', 'friends' ); ?></p></div>
1638 <?php
1639 } elseif ( isset( $_GET['error'] ) ) {
1640 ?>
1641 <div id="message" class="updated error is-dismissible"><p>
1642 <?php
1643 if ( 1 === intval( $_GET['error'] ) ) {
1644 esc_html_e( 'An error occurred.', 'friends' );
1645 } else {
1646 echo esc_html( Rest::translate_error_message( sanitize_text_field( wp_unslash( $_GET['error'] ) ) ) );
1647 }
1648 ?>
1649 </p></div>
1650 <?php
1651 } elseif ( isset( $_GET['sent-request'] ) ) {
1652 ?>
1653 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'Your request was sent.', 'friends' ); ?></p></div>
1654 <?php
1655 } elseif ( isset( $_GET['subscribed'] ) ) {
1656 ?>
1657 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'Subscription activated.', 'friends' ); ?></p></div>
1658 <?php
1659 }
1660 // phpcs:enable WordPress.Security.NonceVerification
1661
1662 Friends::template_loader()->get_template_part( 'admin/edit-friend', null, $args );
1663 }
1664
1665 public function ajax_refresh_feeds() {
1666 check_ajax_referer( 'friends-refresh' );
1667
1668 if ( ! Friends::has_required_privileges() ) {
1669 wp_send_json_error( __( 'You do not have permission to do this.', 'friends' ) );
1670 }
1671
1672 add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
1673
1674 if ( ! empty( $_POST['user'] ) ) {
1675 $friend_user = User::get_by_username( sanitize_user( wp_unslash( $_POST['user'] ) ) );
1676 if ( ! $friend_user || is_wp_error( $friend_user ) || ! $friend_user->can_refresh_feeds() ) {
1677 wp_send_json_error( __( 'Invalid user ID.' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1678 }
1679 $friend_user->retrieve_posts_from_active_feeds();
1680 } else {
1681 $this->friends->feed->retrieve_friend_posts();
1682 }
1683
1684 wp_send_json_success();
1685 }
1686
1687 private function normalize_frontend_subscription_url( $url ) {
1688 if ( ! is_string( $url ) ) {
1689 return '';
1690 }
1691
1692 $url = trim( $url );
1693 if ( '' === $url ) {
1694 return '';
1695 }
1696
1697 $protocol = wp_parse_url( $url, PHP_URL_SCHEME );
1698 if ( ! $protocol ) {
1699 return apply_filters( 'friends_rewrite_incoming_url', 'https://' . $url, $url );
1700 }
1701
1702 return apply_filters( 'friends_rewrite_incoming_url', $url, $url );
1703 }
1704
1705 public function ajax_preview_subscription() {
1706 if ( ! isset( $_POST['url'] ) || is_array( $_POST['url'] ) ) {
1707 wp_send_json_error( __( 'No URL provided.', 'friends' ) );
1708 }
1709
1710 check_ajax_referer( 'friends_add_subscription' );
1711
1712 if ( ! Friends::has_required_privileges() ) {
1713 wp_send_json_error( __( 'You do not have permission to do this.', 'friends' ) );
1714 }
1715
1716 $incoming_url = trim( wp_unslash( $_POST['url'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1717 if ( ! class_exists( '\Activitypub\Activitypub' ) && preg_match( '/^@?[A-Za-z0-9_.-]+@(?:[A-Za-z0-9_-]+\.)+[A-Za-z]+$/i', $incoming_url ) ) {
1718 wp_send_json_error( __( 'The ActivityPub plugin is required to follow Mastodon handles. Please install and activate it first.', 'friends' ) );
1719 }
1720
1721 $url = $this->normalize_frontend_subscription_url( $incoming_url );
1722
1723 if ( '' === $url ) {
1724 wp_send_json_error( __( 'No URL provided.', 'friends' ) );
1725 }
1726
1727 if ( str_starts_with( $url, home_url() ) ) {
1728 wp_send_json_error( __( 'It seems like you sent a friend request to yourself.', 'friends' ) );
1729 }
1730
1731 if ( ! Friends::check_url( $url ) ) {
1732 wp_send_json_error( __( 'You entered an invalid URL.', 'friends' ) );
1733 }
1734
1735 $user_login = apply_filters( 'friends_suggest_user_login', User::get_user_login_for_url( $url ), $url );
1736 $display_name = apply_filters( 'friends_suggest_display_name', User::get_display_name_for_url( $url ), $url );
1737
1738 $feeds = $this->friends->feed->discover_available_feeds( $url );
1739
1740 if ( is_wp_error( $feeds ) ) {
1741 wp_send_json_error( $feeds->get_error_message() );
1742 }
1743
1744 if ( empty( $feeds ) ) {
1745 wp_send_json_error( __( 'No suitable feed was found at the provided address.', 'friends' ) );
1746 }
1747
1748 $better_user_login = User::get_user_login_from_feeds( $feeds );
1749 if ( $better_user_login ) {
1750 $user_login = trim( $better_user_login, '-' );
1751 }
1752
1753 $better_display_name = User::get_display_name_from_feeds( $feeds );
1754 if ( $better_display_name ) {
1755 $display_name = $better_display_name;
1756 if ( ! $better_user_login ) {
1757 $user_login = trim( User::sanitize_username( $better_display_name ), '-' );
1758 }
1759 }
1760
1761 $friend_user = User::get_user( $user_login );
1762 if ( ! $friend_user || is_wp_error( $friend_user ) ) {
1763 $friend_user = Subscription::get_by_username( $user_login );
1764 }
1765
1766 if ( $friend_user && ! is_wp_error( $friend_user ) ) {
1767 // translators: %s is the name of a friend / site.
1768 wp_send_json_error( sprintf( __( 'You are already subscribed to this site: %s', 'friends' ), $friend_user->display_name ) );
1769 }
1770
1771 $avatar = null;
1772 $description = null;
1773 foreach ( $feeds as $feed_details ) {
1774 if ( ! $avatar && ! empty( $feed_details['avatar'] ) ) {
1775 $avatar = $feed_details['avatar'];
1776 }
1777 if ( ! $description && ! empty( $feed_details['description'] ) ) {
1778 $description = $feed_details['description'];
1779 }
1780 }
1781
1782 wp_send_json_success(
1783 array(
1784 'feeds' => $feeds,
1785 'display_name' => $display_name ? $display_name : '',
1786 'user_login' => $user_login ? $user_login : '',
1787 'avatar' => $avatar,
1788 'description' => $description,
1789 'url' => $url,
1790 )
1791 );
1792 }
1793
1794 public function ajax_preview_subscription_feed() {
1795 check_ajax_referer( 'friends_add_subscription' );
1796
1797 if ( ! Friends::has_required_privileges() ) {
1798 wp_send_json_error( __( 'You do not have permission to do this.', 'friends' ) );
1799 }
1800
1801 $url = isset( $_POST['url'] ) && ! is_array( $_POST['url'] ) ? $this->normalize_frontend_subscription_url( wp_unslash( $_POST['url'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1802 if ( '' === $url || ! Friends::check_url( $url ) ) {
1803 wp_send_json_error( __( 'You entered an invalid URL.', 'friends' ) );
1804 }
1805
1806 $parser = isset( $_POST['parser'] ) && ! is_array( $_POST['parser'] ) ? sanitize_key( wp_unslash( $_POST['parser'] ) ) : '';
1807 if ( ! $parser ) {
1808 wp_send_json_error( __( 'An invalid parser was supplied.', 'friends' ) );
1809 }
1810
1811 $items = $this->friends->feed->preview( $parser, $url );
1812 if ( is_wp_error( $items ) ) {
1813 wp_send_json_error( $items->get_error_message() );
1814 }
1815
1816 $preview_items = array();
1817 foreach ( array_slice( $items, 0, 5 ) as $item ) {
1818 $title = $item->title;
1819 if ( 'status' === $item->post_format || ! $title ) {
1820 $title = wp_strip_all_tags( $item->content );
1821 }
1822
1823 $preview_items[] = array(
1824 'title' => wp_trim_words( wp_strip_all_tags( $title ), 16 ),
1825 'excerpt' => wp_trim_words( wp_strip_all_tags( $item->content ), 40 ),
1826 'permalink' => $item->permalink,
1827 'date' => $item->date,
1828 'author' => $item->author,
1829 'post_format' => $item->post_format,
1830 );
1831 }
1832
1833 wp_send_json_success(
1834 array(
1835 'items' => $preview_items,
1836 )
1837 );
1838 }
1839
1840 public function ajax_subscribe_frontend() {
1841 check_ajax_referer( 'friends_add_subscription' );
1842
1843 if ( ! Friends::has_required_privileges() ) {
1844 wp_send_json_error( __( 'You do not have permission to do this.', 'friends' ) );
1845 }
1846
1847 $url = isset( $_POST['url'] ) && ! is_array( $_POST['url'] ) ? $this->normalize_frontend_subscription_url( wp_unslash( $_POST['url'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1848 $display_name = isset( $_POST['display_name'] ) && ! is_array( $_POST['display_name'] ) ? sanitize_text_field( wp_unslash( $_POST['display_name'] ) ) : '';
1849 $user_login = isset( $_POST['user_login'] ) && ! is_array( $_POST['user_login'] ) ? User::sanitize_username( wp_unslash( $_POST['user_login'] ) ) : User::get_user_login_for_url( $url ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1850 $feeds = isset( $_POST['feeds'] ) && is_array( $_POST['feeds'] ) ? wp_unslash( $_POST['feeds'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1851
1852 if ( empty( $url ) || empty( $feeds ) ) {
1853 wp_send_json_error( __( 'Missing required data.', 'friends' ) );
1854 }
1855
1856 if ( ! Friends::check_url( $url ) ) {
1857 wp_send_json_error( __( 'You entered an invalid URL.', 'friends' ) );
1858 }
1859
1860 $user_login = trim( $user_login, '-' );
1861 if ( ! $user_login ) {
1862 wp_send_json_error( __( 'Please enter a valid username.', 'friends' ) );
1863 }
1864
1865 if ( ! $display_name ) {
1866 $display_name = User::get_display_name_for_url( $url );
1867 }
1868
1869 if ( ! is_multisite() && username_exists( $user_login ) ) {
1870 wp_send_json_error( __( 'This username is already registered. Please choose another one.' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1871 }
1872
1873 $avatar = null;
1874 $description = null;
1875 $feed_options = array();
1876 $subscribe = array();
1877 $post_formats = array_merge( array( 'autodetect' => true ), array_fill_keys( array_keys( get_post_format_strings() ), true ) );
1878
1879 foreach ( $feeds as $feed ) {
1880 if ( ! is_array( $feed ) ) {
1881 continue;
1882 }
1883
1884 $feed_url = '';
1885 if ( ! empty( $feed['url'] ) && is_scalar( $feed['url'] ) ) {
1886 $feed_url = esc_url_raw( trim( $feed['url'] ) );
1887 }
1888
1889 if ( ! $feed_url || ! Friends::check_url( $feed_url ) ) {
1890 continue;
1891 }
1892
1893 $parser = isset( $feed['parser'] ) && is_scalar( $feed['parser'] ) ? sanitize_key( $feed['parser'] ) : 'simplepie';
1894 if ( ! $parser || 'unsupported' === $parser ) {
1895 continue;
1896 }
1897
1898 $post_format = isset( $feed['post-format'] ) && is_scalar( $feed['post-format'] ) ? sanitize_key( $feed['post-format'] ) : 'standard';
1899 if ( ! isset( $post_formats[ $post_format ] ) ) {
1900 $post_format = 'standard';
1901 }
1902
1903 $mime_type = isset( $feed['mime-type'] ) && is_scalar( $feed['mime-type'] ) ? sanitize_text_field( $feed['mime-type'] ) : '';
1904 if ( ! $mime_type && ! empty( $feed['type'] ) && is_scalar( $feed['type'] ) ) {
1905 $mime_type = sanitize_text_field( $feed['type'] );
1906 }
1907
1908 $feed_options[ $feed_url ] = array(
1909 'url' => $feed_url,
1910 'parser' => $parser,
1911 'post-format' => $post_format,
1912 'title' => isset( $feed['title'] ) && is_scalar( $feed['title'] ) ? sanitize_text_field( $feed['title'] ) : $feed_url,
1913 );
1914
1915 if ( $mime_type ) {
1916 $feed_options[ $feed_url ]['mime-type'] = $mime_type;
1917 }
1918
1919 $is_selected = isset( $feed['selected'] ) && in_array( $feed['selected'], array( true, 'true', '1', 1, 'on' ), true );
1920 if ( $is_selected ) {
1921 $subscribe[] = $feed_url;
1922 }
1923
1924 if ( ! $avatar && ! empty( $feed['avatar'] ) && is_scalar( $feed['avatar'] ) ) {
1925 $avatar = esc_url_raw( $feed['avatar'] );
1926 }
1927 if ( ! $description && ! empty( $feed['description'] ) && is_scalar( $feed['description'] ) ) {
1928 $description = wp_encode_emoji( sanitize_textarea_field( $feed['description'] ) );
1929 }
1930 }
1931
1932 if ( empty( $feed_options ) ) {
1933 wp_send_json_error( __( 'No suitable feed was found at the provided address.', 'friends' ) );
1934 }
1935
1936 if ( empty( $subscribe ) ) {
1937 wp_send_json_error( __( 'Please select at least one feed.', 'friends' ) );
1938 }
1939
1940 $friend_user = User::get_user( $user_login );
1941 if ( ! $friend_user || is_wp_error( $friend_user ) ) {
1942 $friend_user = Subscription::get_by_username( $user_login );
1943 }
1944
1945 if ( $friend_user && ! is_wp_error( $friend_user ) ) {
1946 // translators: %s is the name of a friend / site.
1947 wp_send_json_error( sprintf( __( 'You are already subscribed to this site: %s', 'friends' ), $friend_user->display_name ) );
1948 }
1949
1950 $friend_user = User::create( $user_login, 'subscription', $url, $display_name, $avatar, $description );
1951
1952 if ( is_wp_error( $friend_user ) ) {
1953 wp_send_json_error( $friend_user->get_error_message() );
1954 }
1955
1956 $saved_feeds = $friend_user->save_feeds( $feed_options );
1957 if ( is_wp_error( $saved_feeds ) ) {
1958 wp_send_json_error( $saved_feeds->get_error_message() );
1959 }
1960
1961 foreach ( $subscribe as $feed_url ) {
1962 if ( ! isset( $feed_options[ $feed_url ] ) ) {
1963 continue;
1964 }
1965 $new_feed = $friend_user->subscribe( $feed_url, $feed_options[ $feed_url ] );
1966 if ( ! is_wp_error( $new_feed ) ) {
1967 do_action( 'friends_user_feed_activated', $new_feed );
1968 }
1969 }
1970
1971 add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
1972 wp_schedule_single_event( time(), 'friends_retrieve_user_feeds', array( $friend_user->ID ) );
1973
1974 wp_send_json_success(
1975 array(
1976 'message' => sprintf(
1977 // translators: %s is the name of a friend.
1978 __( 'You are now following %s.', 'friends' ),
1979 $display_name
1980 ),
1981 'url' => $friend_user->get_local_friends_page_url(),
1982 )
1983 );
1984 }
1985
1986 public function ajax_set_avatar() {
1987 if ( ! isset( $_POST['user'] ) ) {
1988 wp_send_json_error( __( 'No user specified.', 'friends' ) );
1989 }
1990
1991 check_ajax_referer( 'set-avatar-' . sanitize_user( wp_unslash( $_POST['user'] ) ) );
1992
1993 if ( ! current_user_can( Friends::REQUIRED_ROLE ) ) {
1994 wp_send_json_error();
1995 exit;
1996 }
1997 if ( empty( $_POST['avatar'] ) ) {
1998 wp_send_json_error();
1999 exit;
2000 }
2001 $avatar = check_url( wp_unslash( $_POST['avatar'] ) );
2002 if ( empty( $avatar ) ) {
2003 wp_send_json_error();
2004 exit;
2005 }
2006
2007 $friend = User::get_by_username( sanitize_user( wp_unslash( $_POST['user'] ) ) );
2008 if ( ! $friend || is_wp_error( $friend ) ) {
2009 wp_send_json_error( __( 'Invalid user.', 'friends' ) );
2010 exit;
2011 }
2012
2013 // Use WordPress functions to check the image dimensions.
2014 $size = \wp_getimagesize( $avatar );
2015 if ( ! $size ) {
2016 wp_send_json_error( __( 'Image is in an unknown format.', 'friends' ) );
2017 exit;
2018 }
2019 // Needs to be square and not larger than 512x512.
2020 if ( $size[0] !== $size[1] || $size[0] > 512 ) {
2021 wp_send_json_error( __( 'Image must be square and not larger than 512x512.', 'friends' ) );
2022 exit;
2023 }
2024
2025 $url = $friend->update_user_icon_url( $avatar );
2026
2027 if ( ! $url || is_wp_error( $url ) ) {
2028 wp_send_json_error( $url );
2029 exit;
2030 }
2031
2032 wp_send_json_success(
2033 array(
2034 'url' => $url,
2035 )
2036 );
2037 }
2038
2039 /**
2040 * Process the Friends Edit Notifications page
2041 */
2042 public function process_admin_edit_friend_notifications() {
2043 $friend = $this->check_admin_edit_friend();
2044 $arg = 'updated';
2045 $arg_value = 1;
2046
2047 if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'edit-friend-notifications-' . $friend->user_login ) ) {
2048 if ( ! get_user_option( 'friends_no_new_post_notification' ) ) {
2049 if ( isset( $_POST['friends_new_post_notification'] ) && boolval( $_POST['friends_new_post_notification'] ) ) {
2050 delete_user_option( get_current_user_id(), 'friends_no_new_post_notification_' . $friend->user_login );
2051 } else {
2052 update_user_option( get_current_user_id(), 'friends_no_new_post_notification_' . $friend->user_login, 1 );
2053 }
2054 }
2055
2056 if ( ! get_user_option( 'friends_no_keyword_notification' ) ) {
2057 if ( isset( $_POST['friends_keyword_notification'] ) && boolval( $_POST['friends_keyword_notification'] ) ) {
2058 delete_user_option( get_current_user_id(), 'friends_no_keyword_notification_' . $friend->user_login );
2059 } else {
2060 update_user_option( get_current_user_id(), 'friends_no_keyword_notification_' . $friend->user_login, 1 );
2061 }
2062 }
2063
2064 do_action( 'friends_edit_friend_notifications_after_form_submit', $friend );
2065 } else {
2066 return;
2067 }
2068
2069 if ( isset( $_GET['_wp_http_referer'] ) ) {
2070 wp_safe_redirect( wp_get_referer() );
2071 } else {
2072 wp_safe_redirect( add_query_arg( $arg, $arg_value, remove_query_arg( array( '_wp_http_referer', '_wpnonce' ) ) ) );
2073 }
2074 exit;
2075 }
2076
2077 /**
2078 * Render the Friends Edit Notifications page
2079 */
2080 public function render_admin_edit_friend_notifications() {
2081 $friend = $this->check_admin_edit_friend();
2082 $post_stats = $friend->get_post_stats();
2083
2084 $this->header_edit_friend( $friend, 'edit-friend-notifications' );
2085
2086 // phpcs:disable WordPress.Security.NonceVerification
2087 if ( isset( $_GET['updated'] ) ) {
2088 ?>
2089 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'Notification Settings were updated.', 'friends' ); ?></p></div>
2090 <?php
2091 } elseif ( isset( $_GET['error'] ) ) {
2092 ?>
2093 <div id="message" class="updated error is-dismissible"><p><?php esc_html_e( 'An error occurred.', 'friends' ); ?></p></div>
2094 <?php
2095 }
2096 // phpcs:enable WordPress.Security.NonceVerification
2097
2098 Friends::template_loader()->get_template_part(
2099 'admin/edit-notifications',
2100 null,
2101 array(
2102 'friend' => $friend,
2103 )
2104 );
2105 }
2106
2107 /**
2108 * Process the Friends Edit Feeds page
2109 */
2110 public function process_admin_edit_friend_feeds() {
2111 $friend = $this->check_admin_edit_friend();
2112 $arg = 'updated';
2113 $arg_value = 1;
2114
2115 if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'edit-friend-feeds-' . $friend->user_login ) ) {
2116 $hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' );
2117 if ( ! $hide_from_friends_page ) {
2118 $hide_from_friends_page = array();
2119 }
2120 if ( ! isset( $_POST['show_on_friends_page'] ) || ! boolval( $_POST['show_on_friends_page'] ) ) {
2121 if ( ! in_array( $friend->user_login, $hide_from_friends_page ) ) {
2122 $hide_from_friends_page[] = $friend->user_login;
2123 update_user_option( get_current_user_id(), 'friends_hide_from_friends_page', $hide_from_friends_page );
2124 }
2125 } elseif ( in_array( $friend->user_login, $hide_from_friends_page ) ) {
2126 $hide_from_friends_page = array_values( array_diff( $hide_from_friends_page, array( $friend->user_login ) ) );
2127 update_user_option( get_current_user_id(), 'friends_hide_from_friends_page', $hide_from_friends_page );
2128 }
2129
2130 if ( $friend->set_retention_number_enabled( boolval( filter_input( INPUT_POST, 'friends_enable_retention_number', FILTER_SANITIZE_NUMBER_INT ) ) ) && isset( $_POST['friends_retention_number'] ) ) {
2131 $friend->set_retention_number( filter_input( INPUT_POST, 'friends_retention_number', FILTER_SANITIZE_NUMBER_INT ) );
2132 }
2133 if ( $friend->set_retention_days_enabled( boolval( filter_input( INPUT_POST, 'friends_enable_retention_days', FILTER_SANITIZE_NUMBER_INT ) ) ) && isset( $_POST['friends_retention_days'] ) ) {
2134 $friend->set_retention_days( filter_input( INPUT_POST, 'friends_retention_days', FILTER_SANITIZE_NUMBER_INT ) );
2135 }
2136
2137 $hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' );
2138 if ( ! $hide_from_friends_page ) {
2139 $hide_from_friends_page = array();
2140 }
2141
2142 $show_on_dashboard = filter_input( INPUT_POST, 'show_on_dashboard', FILTER_VALIDATE_BOOLEAN );
2143 $already_on_dashboard = false;
2144 $widgets = get_user_option( 'friends_dashboard_widgets', get_current_user_id() );
2145 if ( ! $widgets ) {
2146 $widgets = array();
2147 }
2148 foreach ( $widgets as $k => $widget ) {
2149 if ( ! empty( $widget['friend'] ) && $widget['friend'] === $friend->user_login ) {
2150 $already_on_dashboard = true;
2151 if ( ! $show_on_dashboard ) {
2152 unset( $widgets[ $k ] );
2153 update_user_option( get_current_user_id(), 'friends_dashboard_widgets', $widgets );
2154 }
2155 break;
2156 }
2157 }
2158 if ( $show_on_dashboard && ! $already_on_dashboard ) {
2159 $widgets[] = array( 'friend' => $friend->user_login );
2160 update_user_option( get_current_user_id(), 'friends_dashboard_widgets', $widgets );
2161 }
2162
2163 if ( isset( $_POST['feeds'] ) ) {
2164 // Sanitized below.
2165 $feeds = wp_unslash( $_POST['feeds'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2166 $existing_feeds = $friend->get_feeds();
2167 if ( isset( $feeds['new'] ) ) {
2168 if ( ! isset( $feeds['new']['url'] ) || '' === trim( $feeds['new']['url'] ) ) {
2169 unset( $feeds['new'] );
2170 } else {
2171 foreach ( $existing_feeds as $term_id => $user_feed ) {
2172 if ( $user_feed->get_url() === trim( $feeds['new']['url'] ) ) {
2173 if ( isset( $feeds[ $term_id ] ) ) {
2174 // Let a newly entered feed overrule an existing one.
2175 $feeds[ $term_id ] = array_merge( $feeds[ $term_id ], $feeds['new'] );
2176 $feeds[ $term_id ]['active'] = 1;
2177 }
2178 unset( $feeds['new'] );
2179 break;
2180 }
2181 }
2182 }
2183 }
2184 foreach ( $feeds as $term_id => $feed ) {
2185 if ( 'new' === $term_id ) {
2186 if ( ! isset( $feed['url'] ) || '' === trim( $feed['url'] ) ) {
2187 continue;
2188 }
2189
2190 $feed['active'] = true;
2191 $protocol = wp_parse_url( $feed['url'], PHP_URL_SCHEME );
2192 if ( ! $protocol ) {
2193 $feed['url'] = apply_filters( 'friends_rewrite_incoming_url', 'https://' . $feed['url'], $feed['url'] );
2194 }
2195 $new_feed = $friend->subscribe( $feed['url'], $feed );
2196 if ( is_wp_error( $new_feed ) ) {
2197 do_action( 'friends_process_feed_item_submit_error', $new_feed, $feed );
2198 continue;
2199 }
2200
2201 do_action( 'friends_user_feed_activated', $new_feed );
2202 do_action( 'friends_process_feed_item_submit', $new_feed, $feed );
2203 continue;
2204 }
2205
2206 if ( ! isset( $existing_feeds[ $term_id ] ) ) {
2207 continue;
2208 }
2209 $user_feed = $existing_feeds[ $term_id ];
2210 unset( $existing_feeds[ $term_id ] );
2211
2212 $protocol = wp_parse_url( $feed['url'], PHP_URL_SCHEME );
2213 if ( ! $protocol ) {
2214 $feed['url'] = apply_filters( 'friends_rewrite_incoming_url', 'https://' . $feed['url'], $feed['url'] );
2215 }
2216
2217 if ( $user_feed->get_url() !== $feed['url'] ) {
2218 do_action( 'friends_user_feed_deactivated', $user_feed );
2219
2220 if ( ! isset( $feed['mime-type'] ) ) {
2221 $feed['mime-type'] = $user_feed->get_mime_type();
2222 }
2223
2224 if ( $feed['active'] ) {
2225 $new_feed = $friend->subscribe( $feed['url'], $feed );
2226 if ( ! is_wp_error( $new_feed ) ) {
2227 do_action( 'friends_user_feed_activated', $new_feed );
2228 }
2229 } else {
2230 $new_feed = $friend->save_feed( $feed['url'], $feed );
2231 }
2232
2233 // Since the URL has changed, the above will create a new feed, therefore we need to delete the old one.
2234 $user_feed->delete();
2235
2236 if ( is_wp_error( $new_feed ) ) {
2237 do_action( 'friends_process_feed_item_submit_error', $new_feed, $feed );
2238 continue;
2239 }
2240
2241 do_action( 'friends_process_feed_item_submit', $new_feed, $feed );
2242 continue;
2243 }
2244
2245 if ( $user_feed->get_title() !== $feed['title'] ) {
2246 $user_feed->update_metadata( 'title', $feed['title'] );
2247 }
2248
2249 if ( $user_feed->get_parser() !== $feed['parser'] ) {
2250 $user_feed->update_metadata( 'parser', $feed['parser'] );
2251 }
2252
2253 if ( $user_feed->get_post_format() !== $feed['post-format'] ) {
2254 $user_feed->update_metadata( 'post-format', $feed['post-format'] );
2255 }
2256
2257 if ( isset( $feed['mime-type'] ) && $user_feed->get_mime_type() !== $feed['mime-type'] ) {
2258 $user_feed->update_metadata( 'mime-type', $feed['mime-type'] );
2259 }
2260
2261 $was_active = $user_feed->is_active();
2262 $is_active = isset( $feed['active'] ) && $feed['active'];
2263 $user_feed->update_metadata( 'active', $is_active );
2264 if ( $was_active !== $is_active ) {
2265 if ( $is_active ) {
2266 do_action( 'friends_user_feed_activated', $user_feed );
2267 } else {
2268 do_action( 'friends_user_feed_deactivated', $user_feed );
2269 }
2270 }
2271
2272 do_action( 'friends_process_feed_item_submit', $user_feed, $feed );
2273 }
2274
2275 // Delete remaining existing feeds since they were not submitted.
2276 foreach ( $existing_feeds as $term_id => $user_feed ) {
2277 do_action( 'friends_user_feed_deactivated', $user_feed );
2278 $user_feed->delete();
2279 }
2280 }
2281 do_action( 'friends_edit_feeds_after_form_submit', $friend );
2282 } else {
2283 return;
2284 }
2285
2286 if ( isset( $_GET['_wp_http_referer'] ) ) {
2287 wp_safe_redirect( wp_get_referer() );
2288 } else {
2289 wp_safe_redirect( add_query_arg( $arg, $arg_value, remove_query_arg( array( '_wp_http_referer', '_wpnonce' ) ) ) );
2290 }
2291 exit;
2292 }
2293
2294 /**
2295 * Render the Friends Edit Feeds page
2296 */
2297 public function render_admin_edit_friend_feeds() {
2298 $friend = $this->check_admin_edit_friend();
2299
2300 $already_on_dashboard = false;
2301 $widgets = get_user_option( 'friends_dashboard_widgets', get_current_user_id() );
2302
2303 if ( ! $widgets ) {
2304 $widgets = array();
2305 }
2306 foreach ( $widgets as $widget ) {
2307 if ( ! empty( $widget['friend'] ) && $widget['friend'] === $friend->user_login ) {
2308 $already_on_dashboard = true;
2309 break;
2310 }
2311 }
2312
2313 $args = array_merge(
2314 $friend->get_post_stats(),
2315 array(
2316 'friend' => $friend,
2317 'rules' => $friend->get_feed_rules(),
2318 'hide_from_friends_page' => get_user_option( 'friends_hide_from_friends_page' ),
2319 'post_formats' => array_merge( array( 'autodetect' => __( 'Autodetect Post Format', 'friends' ) ), get_post_format_strings() ),
2320 'friends_settings_url' => add_query_arg( '_wp_http_referer', remove_query_arg( '_wp_http_referer' ), self_admin_url( 'admin.php?page=friends-settings' ) ),
2321 'registered_parsers' => $this->friends->feed->get_registered_parsers(),
2322 'global_retention_days' => Friends::get_retention_days(),
2323 'global_retention_number' => Friends::get_retention_number(),
2324 'global_retention_days_enabled' => get_option( 'friends_enable_retention_days' ),
2325 'global_retention_number_enabled' => get_option( 'friends_enable_retention_number' ),
2326 'show_on_dashboard' => $already_on_dashboard,
2327 )
2328 );
2329 if ( ! $args['hide_from_friends_page'] ) {
2330 $args['hide_from_friends_page'] = array();
2331 }
2332 $this->header_edit_friend( $friend, 'edit-friend-feeds' );
2333
2334 // phpcs:disable WordPress.Security.NonceVerification
2335 if ( isset( $_GET['updated'] ) ) {
2336 ?>
2337 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'Feeds were updated.', 'friends' ); ?></p></div>
2338 <?php
2339 } elseif ( isset( $_GET['error'] ) ) {
2340 ?>
2341 <div id="message" class="updated error is-dismissible"><p><?php esc_html_e( 'An error occurred.', 'friends' ); ?></p></div>
2342 <?php
2343 }
2344 // phpcs:enable WordPress.Security.NonceVerification
2345
2346 Friends::template_loader()->get_template_part( 'admin/edit-feeds', null, $args );
2347 }
2348
2349 /**
2350 * Process the Unfriend page
2351 */
2352 public function process_admin_unfriend() {
2353 $friend = $this->check_admin_edit_friend();
2354 $arg = 'deleted';
2355 $arg_value = $friend->user_login;
2356
2357 if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'unfriend-' . $friend->user_login ) ) {
2358 $friend->delete();
2359 } else {
2360 return;
2361 }
2362
2363 if ( isset( $_GET['_wp_http_referer'] ) ) {
2364 wp_safe_redirect( wp_get_referer() );
2365 } else {
2366 wp_safe_redirect( add_query_arg( $arg, $arg_value, home_url( '/friends/following/' ) ) );
2367 }
2368 exit;
2369 }
2370
2371 /**
2372 * Render the Unfriend page
2373 */
2374 public function render_admin_unfriend() {
2375 $friend = $this->check_admin_edit_friend();
2376 $post_stats = $friend->get_post_stats();
2377
2378 $args = array(
2379 'friend' => $friend,
2380 'friend_posts' => $post_stats['post_count'],
2381 'total_size' => $post_stats['total_size'],
2382 );
2383
2384 Friends::template_loader()->get_template_part( 'admin/unfriend', null, $args );
2385 }
2386
2387 /**
2388 * Display error messages.
2389 *
2390 * @param object $errors The errors.
2391 */
2392 private function display_errors( $errors ) {
2393 if ( ! is_wp_error( $errors ) ) {
2394 return;
2395 }
2396
2397 ?>
2398 <div id="message" class="updated error is-dismissible"><p><?php echo esc_html( $errors->get_error_message() ); ?></p>
2399 <?php
2400 $error_data = $errors->get_error_data();
2401 if ( isset( $error_data->error ) ) {
2402 $error = unserialize( $error_data->error ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
2403 if ( is_wp_error( $error ) ) {
2404 ?>
2405 <pre>
2406 <?php
2407 print_r( $error ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
2408 ?>
2409 </pre>
2410 <?php
2411 } elseif ( is_array( $error ) && isset( $error['body'] ) ) {
2412 ?>
2413 <textarea>
2414 <?php
2415 echo esc_html( $error['body'] );
2416 ?>
2417 </textarea>
2418 <?php
2419 }
2420 }
2421 ?>
2422 </div>
2423 <?php
2424 }
2425
2426 public function create_and_follow( $user_id, $url ) {
2427 // TODO: replace with frontend functionality.
2428 }
2429
2430 /**
2431 * Process the admin notification manager form submission.
2432 */
2433 public function process_admin_notification_manager() {
2434 if ( empty( $_POST ) ) {
2435 return;
2436 }
2437
2438 if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'notification-manager' ) ) {
2439 return;
2440 }
2441
2442 $this->check_admin_settings();
2443
2444 if ( ! empty( $_POST['notification_keywords'] ) && is_array( $_POST['notification_keywords'] ) ) {
2445 $keywords = array();
2446 foreach ( wp_unslash( $_POST['notification_keywords'] ) as $i => $keyword ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2447 if ( trim( $keyword ) ) {
2448 $keywords[] = array(
2449 'enabled' => isset( $_POST['notification_keywords_enabled'][ $i ] ) && boolval( $_POST['notification_keywords_enabled'][ $i ] ),
2450 'keyword' => sanitize_text_field( $keyword ),
2451 );
2452 }
2453 }
2454 update_option( 'friends_notification_keywords', $keywords );
2455 }
2456
2457 if ( isset( $_POST['keyword_notification_override'] ) && boolval( $_POST['keyword_notification_override'] ) ) {
2458 delete_user_option( get_current_user_id(), 'friends_keyword_notification_override_disabled' );
2459 } else {
2460 update_user_option( get_current_user_id(), 'friends_keyword_notification_override_disabled', 1 );
2461 }
2462
2463 if ( isset( $_POST['new_post_notification'] ) && boolval( $_POST['new_post_notification'] ) ) {
2464 delete_user_option( get_current_user_id(), 'friends_no_new_post_notification' );
2465 } else {
2466 update_user_option( get_current_user_id(), 'friends_no_new_post_notification', 1 );
2467 }
2468
2469 if ( isset( $_POST['friend_follower_notification'] ) && boolval( $_POST['friend_follower_notification'] ) ) {
2470 delete_user_option( get_current_user_id(), 'friends_no_friend_follower_notification' );
2471 } else {
2472 update_user_option( get_current_user_id(), 'friends_no_friend_follower_notification', 1 );
2473 }
2474
2475 foreach ( get_post_format_slugs() as $post_format ) {
2476 if ( isset( $_POST[ 'new_post_format_notification_' . $post_format ] ) && boolval( $_POST[ 'new_post_format_notification_' . $post_format ] ) ) {
2477 delete_user_option( get_current_user_id(), 'friends_no_new_post_format_notification_' . $post_format );
2478 } else {
2479 update_user_option( get_current_user_id(), 'friends_no_new_post_format_notification_' . $post_format, 1 );
2480 }
2481 }
2482
2483 foreach ( array_keys( $this->friends->feed->get_registered_parsers() ) as $parser ) {
2484 if ( isset( $_POST[ 'new_post_by_parser_notification_' . $parser ] ) && boolval( $_POST[ 'new_post_by_parser_notification_' . $parser ] ) ) {
2485 delete_user_option( get_current_user_id(), 'friends_no_new_post_by_parser_notification_' . $parser );
2486 } else {
2487 update_user_option( get_current_user_id(), 'friends_no_new_post_by_parser_notification_' . $parser, 1 );
2488 }
2489 }
2490
2491 if ( empty( $_POST['friend_listed'] ) ) {
2492 return;
2493 }
2494 // This is an array, it is checked before use below.
2495 $friend_usernames = wp_unslash( $_POST['friend_listed'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2496 $current_user_id = get_current_user_id();
2497 $hide_from_friends_page = array();
2498
2499 foreach ( $friend_usernames as $friend_username ) {
2500 $friend_user = User::get_by_username( $friend_username );
2501 if ( ! $friend_user ) {
2502 continue;
2503 }
2504 $friend_username = $friend_user->user_login;
2505 if ( ! isset( $_POST['show_on_friends_page'][ $friend_username ] ) ) {
2506 $hide_from_friends_page[] = $friend_username;
2507 }
2508
2509 $no_new_post_notification = ! isset( $_POST['new_friend_post_notification'][ $friend_username ] ) || '0' === $_POST['new_friend_post_notification'][ $friend_username ];
2510 if ( get_user_option( 'friends_no_new_post_notification_' . $friend_username ) !== $no_new_post_notification ) {
2511 update_user_option( $current_user_id, 'friends_no_new_post_notification_' . $friend_username, $no_new_post_notification );
2512 }
2513
2514 $no_keyword_notification = ! isset( $_POST['keyword_notification'][ $friend_username ] );
2515 if ( get_user_option( 'friends_no_keyword_notification_' . $friend_username ) !== $no_keyword_notification ) {
2516 update_user_option( $current_user_id, 'friends_no_keyword_notification_' . $friend_username, $no_keyword_notification );
2517 }
2518 }
2519
2520 update_user_option( $current_user_id, 'friends_hide_from_friends_page', $hide_from_friends_page );
2521
2522 do_action( 'friends_notification_manager_after_form_submit', $friend_usernames );
2523
2524 if ( isset( $_GET['_wp_http_referer'] ) ) {
2525 wp_safe_redirect( wp_get_referer() );
2526 } else {
2527 wp_safe_redirect( add_query_arg( 'updated', '1', remove_query_arg( array( '_wp_http_referer', '_wpnonce' ) ) ) );
2528 }
2529 exit;
2530 }
2531
2532 /**
2533 * Render the admin notification manager.
2534 */
2535 public function render_admin_notification_manager() {
2536 Friends::template_loader()->get_template_part(
2537 'admin/settings-header',
2538 null,
2539 array(
2540 'active' => 'friends-notification-manager',
2541 'title' => __( 'Friends', 'friends' ),
2542 )
2543 );
2544 $this->check_admin_settings();
2545
2546 $friend_users = User_Query::all_subscriptions();
2547
2548 $hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' );
2549 if ( ! $hide_from_friends_page ) {
2550 $hide_from_friends_page = array();
2551 }
2552
2553 $args = array(
2554 'friend_users' => $friend_users->get_results(),
2555 'friends_settings_url' => add_query_arg( '_wp_http_referer', remove_query_arg( '_wp_http_referer' ), self_admin_url( 'admin.php?page=friends-settings' ) ),
2556 'hide_from_friends_page' => $hide_from_friends_page,
2557 'keyword_override_disabled' => get_user_option( 'friends_keyword_notification_override_disabled' ),
2558 'no_new_post_notification' => get_user_option( 'friends_no_new_post_notification' ),
2559 'no_keyword_notification' => get_user_option( 'friends_no_keyword_notification' ),
2560 'notification_keywords' => Feed::get_all_notification_keywords(),
2561 'active_keywords' => Feed::get_active_notification_keywords(),
2562 'feed_parsers' => $this->friends->feed->get_registered_parsers(),
2563 );
2564
2565 if ( class_exists( '\Activitypub\Notification' ) ) {
2566 $args['no_friend_follower_notification'] = get_user_option( 'friends_no_friend_follower_notification' );
2567 }
2568
2569 Friends::template_loader()->get_template_part(
2570 'admin/notification-manager',
2571 null,
2572 $args
2573 );
2574
2575 Friends::template_loader()->get_template_part( 'admin/settings-footer' );
2576 }
2577
2578 public function render_admin_import_export() {
2579 Friends::template_loader()->get_template_part(
2580 'admin/settings-header',
2581 null,
2582 array(
2583 'active' => 'friends-import-export',
2584 'title' => __( 'Friends', 'friends' ),
2585 )
2586 );
2587 $this->check_admin_settings();
2588
2589 ?>
2590 <h1><?php esc_html_e( 'Import/Export', 'friends' ); ?></h1>
2591 <?php
2592
2593 Friends::template_loader()->get_template_part(
2594 'admin/import-export',
2595 null,
2596 array(
2597 'private_rss_key' => get_option( 'friends_private_rss_key' ),
2598 )
2599 );
2600
2601 Friends::template_loader()->get_template_part( 'admin/settings-footer' );
2602 }
2603
2604 public function process_admin_import_export() {
2605 if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'friends-settings' ) ) {
2606 return;
2607 }
2608
2609 if ( ! Friends::has_required_privileges() ) {
2610 return;
2611 }
2612
2613 if ( isset( $_FILES['opml']['tmp_name'] ) ) {
2614 $opml = file_get_contents( $_FILES['opml']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
2615 $feeds = Import::opml( $opml );
2616 $users_created = count( $feeds );
2617 $feeds_imported = 0;
2618 foreach ( $feeds as $user => $user_feeds ) {
2619 $feeds_imported += count( $user_feeds );
2620 }
2621 ?>
2622 <div class="friends-notice notice notice-success is-dismissible">
2623 <p>
2624 <?php
2625 echo esc_html(
2626 sprintf(
2627 // translators: %d is the number of users imported.
2628 _n( 'Imported %d user.', 'Imported %d users.', $users_created, 'friends' ),
2629 $users_created
2630 )
2631 );
2632 ?>
2633 <?php
2634 echo esc_html(
2635 sprintf(
2636 // translators: %d is the number of feeds imported.
2637 _n( 'They had %d feed.', 'They had %d feeds.', $feeds_imported, 'friends' ),
2638 $feeds_imported
2639 )
2640 );
2641 ?>
2642 </p>
2643 </div>
2644 <?php
2645 }
2646 }
2647
2648 public function process_admin_duplicate_remover() {
2649 $friend = $this->check_admin_duplicate_remover();
2650
2651 // Nonce verification done in check_admin_duplicate_remover.
2652 // phpcs:disable WordPress.Security.NonceVerification.Missing
2653
2654 // We iterate over this array and then we sanitize _id.
2655 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2656 if ( empty( $_POST['deleteduplicate'] ) || ! is_array( $_POST['deleteduplicate'] ) ) {
2657 return;
2658 }
2659
2660 $deleted = 0;
2661 foreach ( array_keys( wp_unslash( $_POST['deleteduplicate'] ) ) as $_id ) {
2662 if ( ! is_numeric( $_id ) ) {
2663 continue;
2664 }
2665
2666 if ( wp_delete_post( intval( $_id ) ) ) {
2667 ++$deleted;
2668 }
2669 }
2670 // phpcs:enable WordPress.Security.NonceVerification.Missing
2671 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2672
2673 if ( $deleted ) {
2674 wp_safe_redirect( add_query_arg( 'deleted', $deleted ) );
2675 exit;
2676 }
2677 }
2678 public function check_admin_duplicate_remover() {
2679 if ( ! Friends::is_main_user() ) {
2680 wp_die( esc_html__( 'Sorry, you are not allowed to edit the rules.', 'friends' ) );
2681 }
2682
2683 if ( ! isset( $_GET['user'] ) ) {
2684 wp_die( esc_html__( 'Invalid user.', 'friends' ) );
2685 }
2686
2687 if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'duplicate-remover-' . sanitize_user( wp_unslash( $_GET['user'] ) ) ) ) {
2688 wp_die( esc_html__( 'Invalid nonce.', 'friends' ) );
2689 }
2690
2691 $friend = User::get_by_username( sanitize_user( wp_unslash( $_GET['user'] ) ) );
2692 if ( ! $friend || is_wp_error( $friend ) ) {
2693 wp_die( esc_html__( 'Invalid username.', 'friends' ) );
2694 }
2695
2696 if ( ! $friend->has_cap( 'subscription' ) ) {
2697 wp_die( esc_html__( 'This is not a user related to this plugin.', 'friends' ) );
2698 }
2699
2700 return $friend;
2701 }
2702 /**
2703 * Render the duplicates remover
2704 */
2705 public function render_admin_duplicate_remover() {
2706 $friend = $this->check_admin_duplicate_remover();
2707
2708 $this->header_edit_friend( $friend, 'duplicate-remover' );
2709 // phpcs:disable WordPress.Security.NonceVerification
2710 if ( isset( $_GET['deleted'] ) ) {
2711 ?>
2712 <div id="message" class="updated notice is-dismissible"><p>
2713 <?php
2714 $deleted = intval( $_GET['deleted'] );
2715 echo esc_html(
2716 sprintf(
2717 // translators: %d is the number of duplicates deleted.
2718 _n( 'Deleted %d selected duplicate.', 'Deleted %d selected duplicates.', $deleted, 'friends' ),
2719 $deleted
2720 )
2721 );
2722 ?>
2723 </p></div>
2724 <?php
2725 }
2726 // phpcs:enable WordPress.Security.NonceVerification
2727
2728 $friend_posts = new \WP_Query();
2729
2730 $friend_posts->set( 'post_type', Friends::CPT );
2731 $friend_posts->set( 'post_status', array( 'publish', 'private', 'trash' ) );
2732 $friend_posts->set( 'posts_per_page', 100 );
2733 $friend_posts = $friend->modify_query_by_author( $friend_posts );
2734
2735 $uniques = array();
2736 foreach ( $friend_posts->get_posts() as $_post ) {
2737 $permalink = get_permalink( $_post );
2738 if ( ! isset( $uniques[ $permalink ] ) ) {
2739 $uniques[ $permalink ] = $_post->ID;
2740 }
2741 }
2742
2743 $args = array(
2744 'friend' => $friend,
2745 'friend_posts' => $friend_posts,
2746 'uniques' => array_flip( $uniques ),
2747 'feed' => $this->friends->feed,
2748 );
2749
2750 Friends::template_loader()->get_template_part( 'admin/duplicates', null, $args );
2751 }
2752
2753
2754 public static function get_browser_api_key_user( $key ) {
2755 $key = (string) $key;
2756 if ( ! $key ) {
2757 return false;
2758 }
2759
2760 $parts = explode( '-', $key, 3 );
2761 if ( 3 !== count( $parts ) ) {
2762 return false;
2763 }
2764
2765 $user_id = (int) $parts[1];
2766 if ( ! $user_id ) {
2767 return false;
2768 }
2769
2770 $desired_key = get_user_option( 'friends_browser_api_key', $user_id );
2771 if ( ! $desired_key || ! hash_equals( (string) $desired_key, (string) $key ) ) {
2772 return false;
2773 }
2774
2775 $user = get_user_by( 'ID', $user_id );
2776 if ( ! $user ) {
2777 return false;
2778 }
2779
2780 return $user;
2781 }
2782
2783 public static function check_browser_api_key( $key ) {
2784 return false !== self::get_browser_api_key_user( $key );
2785 }
2786
2787 public static function revoke_browser_api_key( $user_id = false ) {
2788 if ( ! $user_id ) {
2789 $user_id = get_current_user_id();
2790 }
2791
2792 delete_user_option( $user_id, 'friends_browser_api_key' );
2793 }
2794
2795 public static function get_browser_api_key( $user_id = false ) {
2796 if ( ! $user_id ) {
2797 $user_id = get_current_user_id();
2798 }
2799
2800 $key = get_user_option( 'friends_browser_api_key', $user_id );
2801 if ( ! $key ) {
2802 $key = 'friends-' . $user_id . '-' . wp_generate_password( 32, false );
2803 update_user_option( $user_id, 'friends_browser_api_key', $key );
2804 }
2805
2806 return $key;
2807 }
2808
2809 public function render_browser_extension() {
2810 add_filter(
2811 'friends_admin_tabs',
2812 function ( $menu ) {
2813 $menu[ __( 'Browser Extension', 'friends' ) ] = 'friends-browser-extension';
2814 return $menu;
2815 }
2816 );
2817 Friends::template_loader()->get_template_part(
2818 'admin/settings-header',
2819 null,
2820 array(
2821 'active' => 'friends-browser-extension',
2822 )
2823 );
2824 $this->check_admin_settings();
2825 $browser_api_key = self::get_browser_api_key();
2826
2827 if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'friends-browser-extension' ) ) {
2828 if ( isset( $_POST['revoke-api-key'] ) ) {
2829 self::revoke_browser_api_key();
2830 $browser_api_key = self::get_browser_api_key();
2831 }
2832 }
2833
2834 Friends::template_loader()->get_template_part(
2835 'admin/browser-extension',
2836 null,
2837 array(
2838 'browser-api-key' => $browser_api_key,
2839 )
2840 );
2841
2842 Friends::template_loader()->get_template_part( 'admin/settings-footer' );
2843 }
2844
2845 public function render_friends_logs() {
2846 add_filter(
2847 'friends_admin_tabs',
2848 function ( $menu ) {
2849 $menu[ __( 'Logs', 'friends' ) ] = 'friends-logs';
2850 return $menu;
2851 }
2852 );
2853
2854 Friends::template_loader()->get_template_part(
2855 'admin/settings-header',
2856 null,
2857 array(
2858 'active' => 'friends-logs',
2859 )
2860 );
2861 $this->check_admin_settings();
2862
2863 Friends::template_loader()->get_template_part(
2864 'admin/logs',
2865 null,
2866 array(
2867 'logs' => Logging::get_logs(),
2868 )
2869 );
2870
2871 Friends::template_loader()->get_template_part( 'admin/settings-footer' );
2872 }
2873
2874 /**
2875 * Gets the roles associated with the Friends plugin.
2876 *
2877 * @return array The associated roles.
2878 */
2879 public static function get_associated_roles() {
2880 $roles = new \WP_Roles();
2881 $friend_roles = array();
2882 foreach ( $roles->roles as $role => $data ) {
2883 if ( isset( $data['capabilities']['friends_plugin'] ) ) {
2884 $friend_roles[ $role ] = $data['name'];
2885 }
2886 }
2887 return $friend_roles;
2888 }
2889
2890 public static function get_users_url() {
2891 return home_url( '/friends/following/' );
2892 }
2893
2894 /**
2895 * Override the post title for specific post formats.
2896 *
2897 * @param string $title The title.
2898 * @param int $post_id The post id.
2899 *
2900 * @return string The potentially overriden title.
2901 */
2902 public function override_post_format_title( $title, $post_id = null ) {
2903 if ( $post_id && empty( $title ) && is_admin() && function_exists( 'get_current_screen' ) ) {
2904 $screen = get_current_screen();
2905 if ( $screen && 'edit-post' === $screen->id ) {
2906 if ( 'status' === get_post_format() ) {
2907 $post = get_post( $post_id );
2908 return wp_trim_words( wp_strip_all_tags( $post->post_content ) );
2909 }
2910 }
2911 }
2912 return $title;
2913 }
2914
2915 /**
2916 * Get the unread badge HTML
2917 *
2918 * @return string The unread badge HTML.
2919 */
2920 public function get_unread_badge() {
2921 $unread_count = apply_filters( 'friends_unread_count', 0 );
2922 if ( 0 === intval( $unread_count ) ) {
2923 return '';
2924 }
2925
2926 if ( get_user_option( 'friends_unobtrusive_badge' ) ) {
2927 return ' (' . $unread_count . ')';
2928 }
2929 $unread_badge = ' <div class="wp-core-ui wp-ui-notification friends-open-requests" style="display: inline; font-size: 11px; padding: .1em .5em .1em .4em; border-radius: 9px; background-color: #d63638; color: #fff; text-align: center; height: 18px"><span aria-hidden="true">' . $unread_count . '</span><span class="screen-reader-text">';
2930 // translators: %s is the number of unread items.
2931 $unread_badge .= sprintf( _n( '%s unread item', '%s unread items', $unread_count, 'friends' ), $unread_count );
2932 $unread_badge .= '</span></div>';
2933 return $unread_badge;
2934 }
2935
2936 /**
2937 * Add a Friends menu to the admin bar
2938 *
2939 * @param \WP_Admin_Bar $wp_menu The admin bar to modify.
2940 */
2941 public function admin_bar_friends_menu( \WP_Admin_Bar $wp_menu ) {
2942 if ( ! Friends::has_required_privileges() ) {
2943 return;
2944 }
2945
2946 $my_url = home_url();
2947 $my_admin_url = site_url();
2948
2949 $unread = $this->get_unread_badge();
2950
2951 $wp_menu->add_node(
2952 array(
2953 'id' => 'friends-menu',
2954 'parent' => '',
2955 'title' => '<span class="ab-icon"></span> <span class="ab-label">' . esc_html( __( 'Friends', 'friends' ) ) . $unread . '</span>',
2956 'href' => $my_url . '/friends/',
2957 )
2958 );
2959
2960 do_action( 'friends_own_site_menu_top', $wp_menu, $my_url, $my_admin_url );
2961 do_action( 'friends_current_site_menu_top', $wp_menu, $my_url, $my_admin_url );
2962
2963 $wp_menu->add_menu(
2964 array(
2965 'id' => 'your-feed',
2966 'parent' => 'friends-menu',
2967 'title' => esc_html__( 'Main Feed', 'friends' ),
2968 'href' => home_url( '/friends/' ),
2969 )
2970 );
2971
2972 $wp_menu->add_menu(
2973 array(
2974 'id' => 'add-friend',
2975 'parent' => 'friends-menu',
2976 'title' => esc_html__( 'Add a friend', 'friends' ),
2977 'href' => home_url( '/friends/add-friend' ),
2978 )
2979 );
2980 $wp_menu->add_menu(
2981 array(
2982 'id' => 'friends',
2983 'parent' => 'friends-menu',
2984 'title' => esc_html__( 'Settings' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
2985 'href' => home_url( '/friends/settings/' ),
2986 )
2987 );
2988 }
2989
2990 /**
2991 * Add Friend entries to the New Content admin section
2992 *
2993 * @param \WP_Admin_Bar $wp_menu The admin bar to modify.
2994 */
2995 public function admin_bar_new_content( \WP_Admin_Bar $wp_menu ) {
2996 if ( Friends::has_required_privileges() ) {
2997 $wp_menu->add_menu(
2998 array(
2999 'id' => 'new-friend-request',
3000 'parent' => 'new-content',
3001 'title' => esc_html__( 'Friend', 'friends' ),
3002 'href' => self_admin_url( 'admin.php?page=add-friend' ),
3003 )
3004 );
3005 $wp_menu->add_menu(
3006 array(
3007 'id' => 'new-subscription',
3008 'parent' => 'new-content',
3009 'title' => esc_html__( 'Subscription', 'friends' ),
3010 'href' => self_admin_url( 'admin.php?page=add-friend' ),
3011 )
3012 );
3013 }
3014 }
3015
3016 /**
3017 * Show friends admin bar item on mobile.
3018 */
3019 public function admin_bar_mobile() {
3020 if ( ! is_user_logged_in() ) {
3021 return;
3022 }
3023 $logo_mask = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-10 53 154 187'%3E%3Cpath d='M 132.29 90.93 C 119.28 54.95 70.12 63.99 38.89 88.85 -7.9 126.11 11.81 177.74 25.75 200.93 40.32 225.15 60.67 237.5 74.87 225.14 83.57 217.57 86.99 209.19 77.64 194.01 74.25 188.51 76.44 170.04 85.94 165.64 94.55 161.65 94.95 149.38 83.17 149.73 75.25 149.97 53.78 148.25 61.03 144.89 67.56 141.86 143.08 120.75 132.29 90.93 Z'/%3E%3C/svg%3E\") center/contain no-repeat";
3024 ?>
3025 <style type="text/css" media="screen">
3026 #wpadminbar #wp-admin-bar-friends-menu .ab-icon:before {
3027 content: "";
3028 float: left;
3029 width: 20px;
3030 height: 20px;
3031 margin-top: 2px;
3032 background-color: currentColor;
3033 -webkit-mask: <?php echo $logo_mask; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>;
3034 mask: <?php echo $logo_mask; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>;
3035 }
3036 @media screen and (max-width: 782px) {
3037 #wpadminbar #wp-admin-bar-friends-menu, #wpadminbar #wp-admin-bar-friends-menu .ab-icon {
3038 display: block !important;
3039 }
3040 #wpadminbar #wp-admin-bar-friends-menu .ab-label {
3041 display: none !important;
3042 }
3043 #wpadminbar #wp-admin-bar-friends-menu .ab-icon:before {
3044 width: 32px;
3045 height: 32px;
3046 margin-top: 6px;
3047 margin-left: 6px;
3048 }
3049 body.friends-page #wpadminbar li#wp-admin-bar-comments {
3050 display: none;
3051 }
3052 }
3053 </style>
3054 <?php
3055 }
3056
3057
3058 /**
3059 * Fires at the end of the delete users form prior to the confirm button.
3060 *
3061 * @param \WP_User $current_user \WP_User object for the current user.
3062 * @param array $userids Array of IDs for users being deleted.
3063 */
3064 public function delete_user_form( $current_user, $userids ) {
3065 $only_friends_affiliated = true;
3066 foreach ( $userids as $user_id ) {
3067 $user = new \WP_User( $user_id );
3068 if ( ! $user->has_cap( 'subscription' ) ) {
3069 $only_friends_affiliated = false;
3070 break;
3071 }
3072 }
3073
3074 if ( $only_friends_affiliated ) {
3075 ?>
3076 <script type="text/javascript">
3077 jQuery( function () {
3078 jQuery( '#delete_option1' ).closest( 'li' ).hide();
3079 } );
3080 </script>
3081 <?php
3082 }
3083 }
3084
3085 /**
3086 * Actions when a (friend) user is deleted.
3087 *
3088 * @param integer $user_id The user identifier.
3089 */
3090 public function delete_user( $user_id ) {
3091 $friend_user = User::get_user_by_id( $user_id );
3092 if ( ! $friend_user ) {
3093 return; // user was already deleted?
3094 }
3095 // Allow unsubscribing to all these feeds.
3096 foreach ( $friend_user->get_active_feeds() as $feed ) {
3097 do_action( 'friends_user_feed_deactivated', $feed );
3098 $feed->delete();
3099 }
3100
3101 // Delete the rest.
3102 foreach ( $friend_user->get_feeds() as $feed ) {
3103 $feed->delete();
3104 }
3105
3106 foreach ( $friend_user->get_all_post_ids() as $post_id ) {
3107 wp_delete_post( $post_id );
3108 }
3109 }
3110
3111 /**
3112 * Display the Bookmarklets at the Tools section of wp-admin
3113 */
3114 public function toolbox_bookmarklets() {
3115 ?>
3116 <div class="card">
3117 <h2 class="title"><?php esc_html_e( 'Friends', 'friends' ); ?></h2>
3118 <h3><?php esc_html_e( 'Bookmarklets', 'friends' ); ?></h3>
3119
3120 <p><?php esc_html_e( "Drag one of these bookmarklets to your bookmarks bar and click it when you're on a site around the web for the appropriate action.", 'friends' ); ?></p>
3121 <p>
3122 <a href="javascript:void(location.href='<?php echo esc_attr( self_admin_url( 'admin.php?page=add-friend&url=' ) ); ?>'+encodeURIComponent(location.href))" style="display: inline-block; padding: .5em; border: 1px solid #999; border-radius: 4px; background-color: #ddd;text-decoration: none; margin-right: 3em"><?php esc_html_e( 'Add friend', 'friends' ); ?></a>
3123 <a href="javascript:void(location.href='<?php echo esc_attr( self_admin_url( 'admin.php?page=add-friend&url=' ) ); ?>'+encodeURIComponent(location.href))" style="display: inline-block; padding: .5em; border: 1px solid #999; border-radius: 4px; background-color: #ddd; text-decoration: none; margin-right: 3em"><?php esc_html_e( 'Subscribe', 'friends' ); ?></a>
3124 </p>
3125 <h3><?php esc_html_e( 'Browser Extension', 'friends' ); ?></h3>
3126
3127 <p><?php esc_html_e( 'For a smoother experience, install the Friends browser extension. It adds a toolbar button to subscribe to the current site with one click, plus quick actions provided by other Friends-aware plugins.', 'friends' ); ?></p>
3128 <p>
3129 <a href="https://chromewebstore.google.com/detail/friends/ledbghpaplkpclndlommpbokndieflhl"><?php esc_html_e( 'Chrome Extension', 'friends' ); ?></a>
3130 &nbsp;·&nbsp;
3131 <a href="https://addons.mozilla.org/en-US/firefox/addon/wpfriends/"><?php esc_html_e( 'Firefox Extension', 'friends' ); ?></a>
3132 </p>
3133 </div>
3134 <?php
3135 }
3136
3137 /**
3138 * Add more "at a glance" items
3139
3140 * @param array $items Items inserted by another plugin.
3141 * @return array Items + our items.
3142 */
3143 public function dashboard_glance_items( $items ) {
3144 $subscription_count = User_Query::all_subscriptions()->get_total();
3145 $friend_post_count = wp_count_posts( Friends::CPT );
3146 $friend_post_count = $friend_post_count->publish + $friend_post_count->private;
3147
3148 if ( $subscription_count ) {
3149 // translators: %s is the number of subscriptions.
3150 $items[] = '<a class="subscriptions" href="' . self_admin_url( 'users.php?role=subscription' ) . '">' . sprintf( _n( '%s Subscription', '%s Subscriptions', $subscription_count, 'friends' ), $subscription_count ) . '</a>';
3151 }
3152
3153 if ( $friend_post_count ) {
3154 // translators: %s is the number of friend posts.
3155 $items[] = '<a class="friend-posts" href="' . home_url( '/friends/' ) . '">' . sprintf( _n( '%s Post by Friends', '%s Posts by Friends', $friend_post_count, 'friends' ), number_format_i18n( $friend_post_count ) ) . '</a>';
3156 }
3157 return $items;
3158 }
3159
3160 public function add_dashboard_widgets() {
3161 if ( ! Friends::has_required_privileges() ) {
3162 return;
3163 }
3164 $user_id = get_current_user_id();
3165 $widgets = get_user_option( 'friends_dashboard_widgets', $user_id );
3166 if ( ! $widgets ) {
3167 $widgets = array( array() );
3168 update_user_option( $user_id, 'friends_dashboard_widgets', $widgets );
3169 }
3170 foreach ( $widgets as $i => $widget ) {
3171 if ( ! is_array( $widget ) ) {
3172 continue;
3173 }
3174 $title = __( 'Latest Posts', 'friends' );
3175 if ( isset( $widget['format'] ) ) {
3176 $title = get_post_format_string( sanitize_key( $widget['format'] ) );
3177 }
3178
3179 if ( ! empty( $widget['friend'] ) ) {
3180 $user = User::get_by_username( $widget['friend'] );
3181 $title = ' by ' . $user->display_name;
3182 }
3183 $title = sprintf(
3184 // translators: %s is an author name or "Latest Posts".
3185 __( 'Friends: %s', 'friends' ),
3186 $title
3187 );
3188 wp_add_dashboard_widget( 'friends_dashboard_widget' . $i, $title, array( $this, 'render_dashboard_widget' ), array( $this, 'render_dashboard_widget_controls' ), $widget, 'side', 'high' );
3189 }
3190 }
3191
3192 public function add_new_dashboard_widget( $friend = null, $format = null ) {
3193 $user_id = get_current_user_id();
3194 $widgets = get_user_option( 'friends_dashboard_widgets', $user_id );
3195 if ( ! $widgets ) {
3196 $widgets = array();
3197 }
3198 $widget = array();
3199 if ( $friend ) {
3200 $widget['friend'] = $friend;
3201 }
3202 if ( $format ) {
3203 $widget['format'] = $format;
3204 }
3205 $widgets[] = $widget;
3206 update_user_option( $user_id, 'friends_dashboard_widgets', $widgets );
3207 }
3208
3209 public function render_dashboard_widget_controls( $id, $widget = false ) {
3210 if ( empty( $id ) && $widget ) {
3211 $id = intval( str_replace( 'friends_dashboard_widget', '', $widget['id'] ) );
3212 }
3213 $user_id = get_current_user_id();
3214 $widgets = get_user_option( 'friends_dashboard_widgets', $user_id );
3215 if ( ! $widgets ) {
3216 $widgets = array( array() );
3217 }
3218
3219 // phpcs:disable WordPress.Security.NonceVerification
3220 if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['widget_id'] ) ) {
3221
3222 $id = intval( str_replace( 'friends_dashboard_widget', '', sanitize_text_field( wp_unslash( $_POST['widget_id'] ) ) ) );
3223 if ( isset( $_POST['add-new'] ) ) {
3224 $id = count( $widgets );
3225 $widgets[ $id ] = array();
3226 }
3227 if ( ! empty( $_POST['friend'] ) ) {
3228 $widgets[ $id ]['friend'] = sanitize_text_field( wp_unslash( $_POST['friend'] ) );
3229 } else {
3230 unset( $widgets[ $id ]['friend'] );
3231 }
3232 if ( ! empty( $_POST['format'] ) ) {
3233 $widgets[ $id ]['format'] = sanitize_text_field( wp_unslash( $_POST['format'] ) );
3234 } else {
3235 unset( $widgets[ $id ]['format'] );
3236 }
3237 if ( isset( $_POST['delete'] ) ) {
3238 unset( $widgets[ $id ] );
3239 }
3240
3241 update_user_option( $user_id, 'friends_dashboard_widgets', $widgets );
3242 }
3243 // phpcs:enable WordPress.Security.NonceVerification
3244 $args = array();
3245 if ( isset( $widgets[ $id ] ) ) {
3246 $args = $widgets[ $id ];
3247 }
3248 echo '<p>';
3249 echo '<label>';
3250 esc_html_e( 'Friend:', 'friends' );
3251 echo '<select name="friend">';
3252 echo '<option value="">' . esc_html__( 'Any Friend', 'friends' ) . '</option>';
3253 $users = User_Query::all_associated_users();
3254 foreach ( $users->get_results() as $user ) {
3255 echo '<option value="' . esc_attr( $user->user_login ) . '"';
3256 if ( isset( $args['friend'] ) && $args['friend'] === $user->user_login ) {
3257 echo ' selected="selected"';
3258 }
3259 echo '>' . esc_html( $user->display_name ) . ' (' . esc_html( $user->user_login ) . ')</option>';
3260 }
3261 echo '</select>';
3262 echo '</label>';
3263 echo '</p>';
3264 echo '<p>';
3265 echo '<label>';
3266 esc_html_e( 'Post Format:', 'friends' );
3267 echo '<select name="format">';
3268 echo '<option value="">' . esc_html__( 'Any Post Format', 'friends' ) . '</option>';
3269 foreach ( get_post_format_strings() as $format => $label ) {
3270 echo '<option value="' . esc_attr( $format ) . '"';
3271 if ( isset( $args['format'] ) && $args['format'] === $format ) {
3272 echo ' selected="selected"';
3273 }
3274 echo '>' . esc_html( $label ) . '</option>';
3275 }
3276 echo '</select>';
3277 echo '</label>';
3278 echo '</p>';
3279 echo '<p>';
3280 echo ' <button name="add-new" class="button button-secondary">' . esc_html__( 'Save as a new widget', 'friends' ) . '</button>';
3281 echo ' <button name="delete" class="button">' . esc_html__( 'Delete this widget', 'friends' ) . '</button>';
3282 echo '</p>';
3283 }
3284
3285 public function render_dashboard_widget( $args, $widget ) {
3286 $args = $widget['args'];
3287 echo '<div class="friends-dashboard-widget" data-nonce="';
3288 echo esc_attr( wp_create_nonce( 'friends-dashboard' ) );
3289 echo '"';
3290 if ( ! empty( $args['friend'] ) ) {
3291 echo ' data-friend="' . esc_attr( $args['friend'] ) . '"';
3292 }
3293 if ( ! empty( $args['format'] ) ) {
3294 echo ' data-format="' . esc_attr( $args['format'] ) . '"';
3295 }
3296 echo '></div>';
3297 }
3298
3299 public function ajax_friends_dashboard() {
3300 check_ajax_referer( 'friends-dashboard' );
3301
3302 $query_args = array();
3303 $args = array();
3304
3305 if ( isset( $_POST['friend'] ) ) {
3306 $friend = User::get_by_username( sanitize_text_field( wp_unslash( $_POST['friend'] ) ) );
3307 if ( $friend ) {
3308 $args['friend_user'] = $friend;
3309 $query_args = $friend->modify_get_posts_args_by_author( $query_args );
3310 }
3311 }
3312
3313 if ( isset( $_POST['format'] ) ) {
3314 $post_formats = get_post_format_slugs();
3315 $format = sanitize_text_field( wp_unslash( $_POST['format'] ) );
3316
3317 if ( isset( $post_formats[ $format ] ) ) {
3318 $args['post_format'] = $format;
3319 if ( 'standard' !== $format ) {
3320 $query_args['tax_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
3321 array(
3322 'taxonomy' => 'post_format',
3323 'field' => 'slug',
3324 'terms' => array( 'post-format-' . $format ),
3325 ),
3326 );
3327 } else {
3328 $query_args['tax_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
3329 array(
3330 'taxonomy' => 'post_format',
3331 'operator' => 'NOT EXISTS',
3332 ),
3333 );
3334 }
3335 }
3336 }
3337
3338 $any_friends = User_Query::all_associated_users();
3339
3340 ob_start();
3341 if ( 0 === $any_friends->get_total() && empty( $query_args ) ) {
3342 Friends::template_loader()->get_template_part(
3343 'admin/dashboard-widget-welcome',
3344 null,
3345 array()
3346 );
3347
3348 } else {
3349 $query_args['post_type'] = apply_filters( 'friends_frontend_post_types', array( 'post' ) );
3350 $args['posts'] = get_posts( $query_args );
3351 Friends::template_loader()->get_template_part( 'admin/dashboard-widget', null, $args );
3352 }
3353 $data = ob_get_contents();
3354 ob_end_clean();
3355
3356 wp_send_json_success(
3357 $data
3358 );
3359 }
3360
3361 public function site_status_test_php_modules( $modules ) {
3362 $modules['mbstring']['required'] = true;
3363 return $modules;
3364 }
3365
3366 public function admin_friend_posts_query( $query ) {
3367 global $wp_query, $wp, $authordata;
3368 if ( $wp_query !== $query || ! is_admin() ) {
3369 return $query;
3370 }
3371 if ( ! isset( $query->query['post_type'] ) || ! in_array( $query->query['post_type'], apply_filters( 'friends_frontend_post_types', array( 'post' ) ), true ) ) {
3372 return $query;
3373 }
3374
3375 if ( empty( $query->query['author'] ) ) {
3376 return $query;
3377 }
3378
3379 $author = User::get_user_by_id( $query->query['author'] );
3380 if ( ! $author ) {
3381 return $query;
3382 }
3383 $query->query_vars['author'] = '';
3384 $query = $author->modify_query_by_author( $query );
3385
3386 return $query;
3387 }
3388
3389 /**
3390 * Render an "ActivityPub plugin not active" notice for activitypub-parser feeds
3391 * when the ActivityPub plugin is not loaded (so Feed_Parser_ActivityPub never fires).
3392 *
3393 * @param User_Feed $feed The feed.
3394 * @param int $term_id The term ID.
3395 * @param string $parser The parser slug.
3396 */
3397 public function maybe_render_activitypub_inactive_notice( $feed, $term_id, $parser ) {
3398 if ( 'activitypub' !== $parser ) {
3399 return;
3400 }
3401
3402 if ( class_exists( '\Activitypub\Activitypub' ) ) {
3403 return;
3404 }
3405 ?>
3406 <div class="activitypub-subscription-check">
3407 <div class="ap-section-header"><?php esc_html_e( 'ActivityPub Plugin', 'friends' ); ?></div>
3408 <div class="ap-data-grid">
3409 <span class="ap-data-label"><?php esc_html_e( 'Status', 'friends' ); ?></span>
3410 <span class="ap-data-value"><em style="color: orange;"><?php esc_html_e( 'not active', 'friends' ); ?></em></span>
3411 </div>
3412 <div class="ap-section-footer">
3413 <?php
3414 if ( current_user_can( 'activate_plugins' ) ) {
3415 echo wp_kses(
3416 sprintf(
3417 /* translators: %s is a link to the plugin search page */
3418 __( 'The <a href="%s">ActivityPub plugin</a> is required to receive posts from this feed.', 'friends' ),
3419 esc_url( admin_url( 'plugin-install.php?s=activitypub&tab=search&type=term' ) )
3420 ),
3421 array( 'a' => array( 'href' => array() ) )
3422 );
3423 } else {
3424 esc_html_e( 'The ActivityPub plugin is required to receive posts from this feed.', 'friends' );
3425 }
3426 ?>
3427 </div>
3428 </div>
3429 <?php
3430 }
3431 }
3432