PluginProbe
Friends / 4.2.0
Friends v4.2.0
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 2.9.2 All 87 releases
friends / includes / class-admin.php

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

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