PluginProbe
Friends / 4.3.2
Friends v4.3.2
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
← All changes | includes/class-friends.php +698 -242 2.8.74.3.2 View file →
@@ -8,10 +8,8 @@
8 8 */
9 9
10 10 namespace Friends;
11 11
12 -use stdClass;
13 -
14 12 /**
15 13 * This is the class for the Friends Plugin.
16 14 *
17 15 * @package Friends
@@ -19,8 +17,9 @@
19 17 */
20 18 class Friends {
21 19 const VERSION = FRIENDS_VERSION;
22 20 const CPT = 'friend_post_cache';
21 + const TAG_TAXONOMY = Friend_Tag::TAXONOMY;
23 22 const FEED_URL = 'friends-feed-url';
24 23 const PLUGIN_URL = 'https://wordpress.org/plugins/friends/';
25 24 const REQUIRED_ROLE = 'edit_private_posts';
26 25
@@ -31,20 +30,20 @@
31 30 static::get_instance();
32 31 }
33 32
34 33 /**
35 - * A reference to the Admin object.
34 + * A reference to the Abilities object.
36 35 *
37 - * @var Admin
36 + * @var Abilities
38 37 */
39 - public $admin;
38 + public $abilities;
40 39
41 40 /**
42 - * A reference to the Access_Control object.
41 + * A reference to the Admin object.
43 42 *
44 - * @var Access_Control
43 + * @var Admin
45 44 */
46 - public $access_control;
45 + public $admin;
47 46
48 47 /**
49 48 * A reference to the Feed object.
50 49 *
@@ -73,8 +72,15 @@
73 72 */
74 73 public $frontend;
75 74
76 75 /**
76 + * A reference to the Link_Preview object.
77 + *
78 + * @var Link_Preview
79 + */
80 + public $link_preview;
81 +
82 + /**
77 83 * A reference to the REST object.
78 84 *
79 85 * @var REST
80 86 */
@@ -117,9 +123,8 @@
117 123 /**
118 124 * Constructor
119 125 */
120 126 public function __construct() {
121 - $this->access_control = new Access_Control( $this );
122 127 $this->admin = new Admin( $this );
123 128 $this->feed = new Feed( $this );
124 129 $this->messages = new Messages( $this );
125 130 $this->notifications = new Notifications( $this );
@@ -125,18 +130,68 @@
125 130 $this->notifications = new Notifications( $this );
126 131 $this->frontend = new Frontend( $this );
127 132 $this->reactions = new Reactions( $this );
128 133 $this->rest = new REST( $this );
134 + $this->abilities = new Abilities( $this );
129 135
136 + $this->link_preview = new Link_Preview( $this );
137 +
130 138 new Third_Parties( $this );
131 139 new Blocks( $this );
132 140 new Logging( $this );
133 141 new Shortcodes( $this );
134 - new Automatic_Status( $this );
142 + new Site_Health();
143 + new Migration();
135 144 $this->register_hooks();
136 145 load_plugin_textdomain( 'friends', false, FRIENDS_PLUGIN_FILE . '/languages/' );
146 +
147 + /**
148 + * Friends has loaded.
149 + *
150 + * @param Friends $friends The friends object.
151 + *
152 + * You can now assume that all the Friends hooks and objects are available.
153 + *
154 + * Example:
155 + * ```php
156 + * add_action( 'friends_loaded', function( Friends $friends ) {
157 + * add_action( 'init', 'initialize_my_plugin' );
158 + * } );
159 + * ```
160 + */
137 161 do_action( 'friends_loaded', $this );
162 +
163 + /**
164 + * Time to register your parser.
165 + *
166 + * @param Feed $feed The feed object.
167 + *
168 + * You'll receive the feed object on which you need to call `register_parser()`.
169 + *
170 + * Example:
171 + * ```php
172 + * add_action( 'friends_load_parsers', function( Friends\Feed $friends_feed ) {
173 + * $friends_feed->register_parser( 'simplepie', new Feed_Parser_SimplePie( $friends_feed ) );
174 + * } );
175 + * ```
176 + */
138 177 do_action( 'friends_load_parsers', $this->feed );
178 +
179 + /**
180 + * Time to register your theme.
181 + *
182 + * @param Frontend $frontend The frontend object.
183 + *
184 + * You'll receive the frontend object on which you need to call `register_theme()`.
185 + *
186 + * Example:
187 + * ```php
188 + * add_action( 'friends_load_themes', function( Friends\Frontend $friends_frontend ) {
189 + * $friends_frontend->register_theme( 'Mastodon', 'mastodon' );
190 + * } );
191 + * ```
192 + */
193 + do_action( 'friends_load_themes', $this->frontend );
139 194 }
140 195
141 196 /**
142 197 * Register the WordPress hooks
@@ -142,10 +197,13 @@
142 197 * Register the WordPress hooks
143 198 */
144 199 private function register_hooks() {
145 200 add_action( 'init', array( $this, 'register_custom_post_type' ) );
201 + add_action( 'init', array( $this, 'register_friend_tag_taxonomy' ) );
146 202 add_action( 'init', array( 'Friends\Subscription', 'register_taxonomy' ) );
203 +
147 204 add_action( 'init', array( 'Friends\User_Feed', 'register_taxonomy' ) );
205 + add_action( 'wp', array( $this, 'allow_browser_extension_request' ) );
148 206 add_filter( 'get_avatar_data', array( $this, 'get_avatar_data' ), 10, 2 );
149 207
150 208 add_action( 'template_redirect', array( $this, 'http_header' ), 5 );
151 209 add_filter( 'wp_head', array( $this, 'html_rel_links' ) );
@@ -151,17 +209,33 @@
151 209 add_filter( 'wp_head', array( $this, 'html_rel_links' ) );
152 210 add_filter( 'login_head', array( $this, 'html_rel_links' ) );
153 211
154 212 add_filter( 'after_setup_theme', array( $this, 'enable_post_formats' ) );
155 - add_filter( 'pre_get_posts', array( $this, 'pre_get_posts_filter_by_post_format' ), 20 );
156 - add_filter( 'template_redirect', array( $this, 'disable_friends_author_page' ) );
213 + add_filter( 'cron_schedules', array( $this, 'add_fifteen_minutes_interval' ) ); // phpcs:ignore WordPressVIPMinimum.Performance.IntervalInSeconds.IntervalInSeconds
214 + add_action( 'cron_friends_delete_old_posts', array( $this, 'cron_friends_delete_outdated_posts' ) );
215 + add_action( 'friends_migrate_post_tags_batch', array( $this, 'cron_migrate_post_tags_batch' ) );
216 + add_action( 'friends_migrate_ap_attributed_to_batch', array( $this, 'cron_migrate_ap_attributed_to_batch' ) );
217 + add_action( 'friends_link_ap_feeds_batch', array( $this, 'cron_link_ap_feeds_batch' ) );
218 + add_action( 'friends_backfill_external_attributed_to_batch', array( $this, 'cron_backfill_external_attributed_to_batch' ) );
219 + add_action( 'friends_convert_replies_batch', array( $this, 'cron_convert_replies_batch' ) );
220 + add_action( 'friends_convert_friend_users_batch', array( $this, 'cron_convert_friend_users_batch' ) );
221 + add_action( 'friends_convert_single_reply', array( $this, 'convert_single_reply_to_comment' ) );
222 + add_action( 'template_redirect', array( $this, 'redirect_trashed_reply_to_comment' ), 4 );
223 + add_action( 'template_redirect', array( $this, 'fallback_redirect_via_comment_meta' ), 4 );
157 224
158 225 add_action( 'comment_form_defaults', array( $this, 'comment_form_defaults' ) );
159 226 add_filter( 'friends_frontend_post_types', array( $this, 'add_frontend_post_types' ) );
160 227
161 228 add_filter( 'request', array( $this, 'limit_post_format_request' ), 20 );
229 + add_filter( 'my_apps_plugins', array( $this, 'register_my_apps' ) );
230 + User::register_wrapper_hooks();
231 + }
162 232
163 - User::register_wrapper_hooks();
233 + /**
234 + * Register the friend tag taxonomy
235 + */
236 + public function register_friend_tag_taxonomy() {
237 + Friend_Tag::register();
164 238 }
165 239
166 240 /**
167 241 * Registers the custom post type
@@ -185,9 +259,9 @@
185 259
186 260 $args = array(
187 261 'labels' => $labels,
188 262 'description' => "A cached friend's post",
189 - 'publicly_queryable' => self::authenticated_for_posts(),
263 + 'publicly_queryable' => is_admin() && self::is_main_user() && apply_filters( 'friends_show_cached_posts', false ),
190 264 'show_ui' => true,
191 265 'show_in_menu' => apply_filters( 'friends_show_cached_posts', false ),
192 266 'show_in_nav_menus' => false,
193 267 'show_in_admin_bar' => false,
@@ -197,9 +271,9 @@
197 271 'delete_with_user' => true,
198 272 'menu_position' => 5,
199 273 'menu_icon' => 'dashicons-groups',
200 274 'supports' => array( 'title', 'editor', 'author', 'revisions', 'thumbnail', 'excerpt', 'comments', 'post-formats' ),
201 - 'taxonomies' => array( 'post_tag', 'post_format', 'friend-reaction-' . get_current_user_id() ),
275 + 'taxonomies' => array( self::TAG_TAXONOMY, 'post_format', 'friend-reaction-' . get_current_user_id() ),
202 276 'has_archive' => true,
203 277 'rewrite' => false,
204 278 );
205 279
@@ -265,37 +339,25 @@
265 339 )
266 340 );
267 341 }
268 342
343 + public function allow_browser_extension_request() {
344 + if ( get_http_origin() && is_home() ) {
345 + $scheme = wp_parse_url( get_http_origin(), PHP_URL_SCHEME );
346 + if ( 'moz-extension' === $scheme ) {
347 + header( 'access-control-allow-origin: ' . get_http_origin() );
348 + }
349 + }
350 + }
351 +
269 352 public static function get_role_capabilities( $role ) {
270 353 $capabilities = array();
271 354
272 - $capabilities['friend_request'] = array(
273 - 'friend_request' => true,
274 - );
275 -
276 - $capabilities['pending_friend_request'] = array(
277 - 'pending_friend_request' => true,
278 - );
279 -
280 355 $capabilities['subscription'] = array(
281 - 'subscription' => true,
356 + 'subscription' => true,
357 + 'friends_plugin' => true,
282 358 );
283 359
284 - $capabilities['acquaintance'] = array(
285 - 'read' => true,
286 - 'friend' => true,
287 - );
288 -
289 - // Friend is an Acquaintance who can read private posts.
290 - $capabilities['friend'] = $capabilities['acquaintance'];
291 - $capabilities['friend']['read_private_posts'] = true;
292 -
293 - // All roles belonging to this plugin have the friends_plugin capability.
294 - foreach ( array_keys( $capabilities ) as $type ) {
295 - $capabilities[ $type ]['friends_plugin'] = true;
296 - }
297 -
298 360 if ( ! isset( $capabilities[ $role ] ) ) {
299 361 return array();
300 362 }
301 363
@@ -306,13 +368,9 @@
306 368 * Create the Friend user roles
307 369 */
308 370 private static function setup_roles() {
309 371 $default_roles = array(
310 - 'friend' => _x( 'Friend', 'User role', 'friends' ),
311 - 'acquaintance' => _x( 'Acquaintance', 'User role', 'friends' ),
312 - 'friend_request' => _x( 'Friend Request', 'User role', 'friends' ),
313 - 'pending_friend_request' => _x( 'Pending Friend Request', 'User role', 'friends' ),
314 - 'subscription' => _x( 'Subscription', 'User role', 'friends' ),
372 + 'subscription' => _x( 'Subscription', 'User role', 'friends' ),
315 373 );
316 374
317 375 $roles = new \WP_Roles();
318 376
@@ -336,55 +394,8 @@
336 394 }
337 395 }
338 396
339 397 /**
340 - * Creates a page /friends/ to enable customization via.
341 - */
342 - public static function create_friends_page() {
343 - $query = new \WP_Query(
344 - array(
345 - 'name' => 'friends',
346 - 'post_type' => 'page',
347 - )
348 - );
349 - if ( $query->have_posts() ) {
350 - return;
351 - }
352 - $content = '<!-- wp:paragraph {"className":"only-friends"} -->' . PHP_EOL . '<p class="only-friends">';
353 - $content .= __( 'Hi Friend!', 'friends' );
354 - $content .= '<br/><br/>';
355 - $content .= __( 'Do you know any of my friends? Maybe you want to become friends with them as well?', 'friends' );
356 - $content .= PHP_EOL . '</p>' . PHP_EOL . '<!-- /wp:paragraph -->' . PHP_EOL;
357 -
358 - $content .= '<!-- wp:friends/friends-list {"className":"only-friends","user_types":"friends"} /-->' . PHP_EOL;
359 -
360 - $content .= '<!-- wp:paragraph {"className":"not-friends"} -->' . PHP_EOL . '<p class="not-friends">';
361 - $content .= __( 'I have connected with my friends using <strong>WordPress</strong> and the <strong>Friends plugin</strong>. This means I can share private posts with just my friends while keeping my data under control.', 'friends' );
362 - $content .= PHP_EOL;
363 - // translators: %1$s and %2$s are URLs.
364 - $content .= sprintf( __( 'If you also have a WordPress site with the friends plugin, you can send me a friend request. If not, get your own <a href="%1$s">WordPress</a> now, install the <a href="%2$s">Friends plugin</a>, and follow me!', 'friends' ), 'https://wordpress.org/', self::PLUGIN_URL );
365 - $content .= PHP_EOL . '</p>' . PHP_EOL . '<!-- /wp:paragraph -->' . PHP_EOL;
366 -
367 - $content .= '<!-- wp:friends/follow-me {"className":"not-friends"} -->' . PHP_EOL . '<div class="wp-block-friends-follow-me not-friends">';
368 - $content .= '<form method="post"><!-- wp:paragraph -->' . PHP_EOL . '<p>';
369 - $content .= __( 'Enter your blog URL to join my network. <a href="https://wpfriends.at/follow-me">Learn more</a>', 'friends' );
370 - $content .= '</p>' . PHP_EOL;
371 - $content .= '<!-- /wp:paragraph --><div><input type="text" name="friends_friend_request_url" placeholder="https://example.com/"/> <button>';
372 - $content .= __( 'Follow this site', 'friends' );
373 - $content .= '</button></div></form></div>' . PHP_EOL;
374 - $content .= '</p>' . PHP_EOL . '<!-- /wp:friends/follow-me -->' . PHP_EOL;
375 -
376 - $post_data = array(
377 - 'post_title' => __( 'Friends', 'friends' ),
378 - 'post_content' => $content,
379 - 'post_type' => 'page',
380 - 'post_name' => 'friends',
381 - 'post_status' => 'publish',
382 - );
383 - wp_insert_post( $post_data );
384 - }
385 -
386 - /**
387 398 * Enable translated user roles.
388 399 * props https://wordpress.stackexchange.com/a/141705/74893
389 400 *
390 401 * @param string $translations The potentially translated text.
@@ -394,12 +405,8 @@
394 405 * @return string Translated text on success, original text on failure.
395 406 */
396 407 public static function translate_user_role( $translations, $text, $context, $domain ) {
397 408 $roles = array(
398 - 'Friend',
399 - 'Acquaintance',
400 - 'Friend Request',
401 - 'Pending Friend Request',
402 409 'Subscription',
403 410 );
404 411
405 412 if (
@@ -419,9 +426,9 @@
419 426 *
420 427 * @return array The roles.
421 428 */
422 429 public static function get_friends_plugin_roles() {
423 - return apply_filters( 'friends_plugin_roles', array( 'friend', 'pending_friend_request', 'friend_request', 'subscription' ) );
430 + return apply_filters( 'friends_plugin_roles', array( 'subscription' ) );
424 431 }
425 432
426 433 /**
427 434 * If a plugin version upgrade requires changes, they can be done here
@@ -428,45 +435,47 @@
428 435 *
429 436 * @param \WP_Upgrader $upgrader_object The WP_Upgrader instance.
430 437 * @param array $options Array of bulk item update data.
431 438 */
432 - public static function upgrade_plugin( $upgrader_object, $options ) {
439 + public static function upgrade_plugin_trigger( $upgrader_object, $options ) {
433 440 $upgraded = false;
434 441 if ( 'update' === $options['action'] && 'plugin' === $options['type'] && isset( $options['plugins'] ) ) {
435 442 foreach ( $options['plugins'] as $plugin ) {
436 443 if ( FRIENDS_PLUGIN_BASENAME === $plugin ) {
437 - $upgraded = true;
438 - break;
444 + return self::upgrade_plugin();
439 445 }
440 446 }
441 447 }
442 - if ( ! $upgraded ) {
443 - return;
448 + }
449 +
450 + public static function has_pending_migrations() {
451 + $status = get_option( 'friends_migration_status' );
452 + if ( ! $status || 'completed' === $status ) {
453 + return false;
444 454 }
455 + if ( is_numeric( $status ) && ( time() - intval( $status ) ) < 2 * DAY_IN_SECONDS ) {
456 + return false;
457 + }
458 + require_once __DIR__ . '/class-migration.php';
459 + if ( ! Migration::has_pending_tracked_migrations() ) {
460 + return false;
461 + }
462 + return true;
463 + }
464 +
465 + public static function upgrade_plugin() {
445 466 $previous_version = get_option( 'friends_plugin_version' );
446 467
447 - if ( version_compare( $previous_version, '2.9.0', '<' ) ) {
448 - $users = User_Query::all_associated_users();
449 - foreach ( $users->get_results() as $user ) {
450 - if ( ! ( $user instanceof Subscription ) ) {
451 - // We have a user that is not a virtual user, so the friendship functionality had been used.
452 - update_option( 'friends_enable_wp_friendships', 1 );
453 - break;
454 - }
455 - }
468 + // Bail early if no migration is necessary.
469 + if ( version_compare( $previous_version, Friends::VERSION, '>=' ) ) {
470 + return;
456 471 }
472 +
473 + // Load migration class for any upgrade.
474 + require_once __DIR__ . '/class-migration.php';
475 +
457 476 if ( version_compare( $previous_version, '0.20.1', '<' ) ) {
458 - $users = User_Query::all_associated_users();
459 - foreach ( $users->get_results() as $user ) {
460 - $gravatar = get_user_option( 'friends_gravatar', $user->ID );
461 - $user_icon_url = get_user_option( 'friends_user_icon_url', $user->ID );
462 - if ( $gravatar ) {
463 - if ( ! $user_icon_url ) {
464 - update_user_option( $user->ID, 'friends_user_icon_url', $gravatar );
465 - }
466 - delete_user_option( $user->ID, 'friends_gravatar' );
467 - }
468 - }
477 + Migration::migrate_gravatar_to_user_icon_url();
469 478 }
470 479
471 480 if ( version_compare( $previous_version, '2.1.3', '<' ) ) {
472 481 self::setup_roles();
@@ -472,20 +481,45 @@
472 481 self::setup_roles();
473 482 }
474 483
475 484 if ( version_compare( $previous_version, '2.6.0', '<' ) ) {
476 - $users = User_Query::all_associated_users();
477 - foreach ( $users->get_results() as $user ) {
478 - if ( get_option( 'friends_feed_rules_' . $user->ID ) ) {
479 - $user->update_user_option( 'friends_feed_rules', get_option( 'friends_feed_rules_' . $user->ID ) );
485 + Migration::migrate_feed_options_to_user_options();
486 + }
487 +
488 + if ( version_compare( $previous_version, '2.8.7', '<' ) ) {
489 + Migration::enable_wp_friendships_if_used();
490 + }
491 +
492 + if ( version_compare( $previous_version, '2.9.4', '<' ) ) {
493 + Migration::migrate_external_user_and_cron();
494 + }
495 +
496 + if ( version_compare( $previous_version, '3.1.8', '<' ) ) {
497 + Migration::migrate_frontend_default_view_to_user_option();
498 + }
499 +
500 + if ( version_compare( $previous_version, '4.0.0', '<' ) ) {
501 + Migration::migrate_post_tags_to_friend_tags();
502 + Migration::backfill_mention_tags_from_mastodon_html();
503 + Migration::migrate_activitypub_attributed_to();
504 + Migration::import_activitypub_followings();
505 + Migration::link_activitypub_feeds_to_actors();
506 + Migration::link_feeds_as_term_children();
507 +
508 + // Show the welcome/update screen if this is an upgrade (not a fresh install).
509 + if ( $previous_version ) {
510 + update_option( 'friends_welcome_version', '4.0' );
511 + if ( Migration::has_pending_tracked_migrations() ) {
512 + update_option( 'friends_migration_status', 'pending', false );
513 + } else {
514 + update_option( 'friends_migration_status', 'completed', false );
480 515 }
481 - if ( get_option( 'friends_feed_catch_all_' . $user->ID ) ) {
482 - $user->update_user_option( 'friends_feed_catch_all', get_option( 'friends_feed_catch_all_' . $user->ID ) );
483 - }
484 516 }
485 517 }
486 518
487 - update_option( 'friends_plugin_version', Friends::VERSION );
519 + Migration::backfill_activitypub_attributed_to_acct();
520 +
521 + update_option( 'friends_plugin_version', Friends::VERSION );
488 522 }
489 523
490 524 /**
491 525 * Actions to take upon plugin activation.
@@ -540,9 +574,8 @@
540 574 * Actions to take upon plugin activation.
541 575 */
542 576 private static function setup() {
543 577 self::setup_roles();
544 - self::create_friends_page();
545 578
546 579 self::upgrade_plugin(
547 580 null,
548 581 array(
@@ -559,14 +592,14 @@
559 592 if ( false === get_option( 'friends_private_rss_key' ) ) {
560 593 update_option( 'friends_private_rss_key', wp_generate_password( 128, false ) );
561 594 }
562 595
563 - if ( false === get_option( 'friends_default_friend_role' ) ) {
564 - update_option( 'friends_default_friend_role', 'friend' );
596 + if ( ! wp_next_scheduled( 'cron_friends_refresh_feeds' ) ) {
597 + wp_schedule_event( time(), 'fifteen-minutes', 'cron_friends_refresh_feeds' );
565 598 }
566 599
567 - if ( ! wp_next_scheduled( 'cron_friends_refresh_feeds' ) ) {
568 - wp_schedule_event( time(), 'hourly', 'cron_friends_refresh_feeds' );
600 + if ( ! wp_next_scheduled( 'cron_friends_delete_old_posts' ) ) {
601 + wp_schedule_event( time(), 'hourly', 'cron_friends_delete_old_posts' );
569 602 }
570 603
571 604 self::add_default_sidebars_widgets();
572 605 flush_rewrite_rules();
@@ -571,8 +604,17 @@
571 604 self::add_default_sidebars_widgets();
572 605 flush_rewrite_rules();
573 606 }
574 607
608 + public function add_fifteen_minutes_interval( $schedules ) {
609 + $schedules['fifteen-minutes'] = array(
610 + 'interval' => 900,
611 + 'display' => __( 'Every 15 Minutes', 'friends' ),
612 + );
613 +
614 + return $schedules;
615 + }
616 +
575 617 /**
576 618 * Add default widgets to the sidebars.
577 619 */
578 620 public static function add_default_sidebars_widgets() {
@@ -590,8 +632,9 @@
590 632 ),
591 633 'friends-widget-new-private-post' => array(),
592 634 ),
593 635 'friends-sidebar' => array(
636 + 'friends-widget-stats' => array(),
594 637 'friends-widget-refresh' => array(),
595 638 'friends-widget-post-formats' => array(),
596 639 'friends-widget-friend-list' => array(
597 640 'title' => 'Friends',
@@ -603,9 +646,9 @@
603 646 continue;
604 647 }
605 648 $sidebars_widgets[ $sidebar_id ] = array();
606 649 foreach ( $default_widgets as $widget_id => $options ) {
607 - $id += 1;
650 + ++$id;
608 651 $sidebars_widgets[ $sidebar_id ][] = $widget_id . '-' . $id;
609 652 update_option( 'widget_' . $widget_id, array( $id => $options ) );
610 653 }
611 654 }
@@ -620,16 +663,25 @@
620 663 $timestamp = wp_next_scheduled( 'cron_friends_refresh_feeds' );
621 664 wp_unschedule_event( $timestamp, 'cron_friends_refresh_feeds' );
622 665 }
623 666
667 + /**
668 + * Get the required capability for the menu entries.
669 + */
624 670 public static function required_menu_role() {
625 671 return 'edit_private_posts';
626 672 }
627 673
674 + /**
675 + * Check if the user hast the required priviliges.
676 + */
628 677 public static function has_required_privileges() {
629 - return self::is_main_user() || current_user_can( 'administrator' );
678 + return self::is_main_user() || current_user_can( 'manage_options' );
630 679 }
631 680
681 + /**
682 + * Check if the current user is the main user.
683 + */
632 684 public static function is_main_user() {
633 685 return is_user_logged_in() && get_current_user_id() === self::get_main_friend_user_id();
634 686 }
635 687
@@ -717,10 +769,10 @@
717 769 global $wp_query;
718 770
719 771 if ( ! isset( $wp_query ) || ! isset( $wp_query->query['pagename'] ) ) {
720 772 // The request has not yet been parsed but we need to know, so this snippet is from wp->parse_request().
721 - list( $req_uri ) = explode( '?', $_SERVER['REQUEST_URI'] );
722 - $home_path = parse_url( home_url(), PHP_URL_PATH );
773 + list( $req_uri ) = explode( '?', remove_query_arg( '' ) );
774 + $home_path = wp_parse_url( home_url(), PHP_URL_PATH );
723 775 $home_path_regex = '';
724 776 if ( is_string( $home_path ) && '' !== $home_path ) {
725 777 $home_path = trim( $home_path, '/' );
726 778 $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) );
@@ -735,8 +787,10 @@
735 787 } else {
736 788 $pagename = $wp_query->query['pagename'];
737 789 }
738 790
791 + // A nonce is not needed here since this is to show the public page.
792 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
739 793 if ( isset( $_GET['public'] ) ) {
740 794 return false;
741 795 }
742 796
@@ -744,18 +798,38 @@
744 798 return false;
745 799 }
746 800
747 801 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
748 - return count( $pagename_parts ) > 0 && 'friends' === $pagename_parts[0];
749 - }
802 + if ( count( $pagename_parts ) > 0 && 'friends' === $pagename_parts[0] ) {
803 + return true;
804 + }
805 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
806 + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
807 + if ( implode( '/', array_slice( $pagename_parts, 0, 2 ) ) === 'wp-admin/customize.php' && isset( $_REQUEST['url'] ) ) {
808 + $pagename_parts = explode( '/', trim( str_replace( '://', '', wp_unslash( $_REQUEST['url'] ) ), '/' ) );
809 + if ( count( $pagename_parts ) > 0 && 'friends' === $pagename_parts[1] ) {
810 + return true;
811 + }
812 + }
750 813
751 - /**
752 - * Check whether the request has been authenticated to display (private) posts.
753 - *
754 - * @return bool Whether the posts can be accessed.
755 - */
756 - public static function authenticated_for_posts() {
757 - return Access_Control::private_rss_is_authenticated() || ( is_admin() && self::is_main_user() && apply_filters( 'friends_show_cached_posts', false ) );
814 + if ( implode( '/', array_slice( $pagename_parts, 0, 2 ) ) === 'wp-admin/site-editor.php' && isset( $_REQUEST['postId'] ) ) {
815 + $pagename_parts = explode( '//', wp_unslash( $_REQUEST['postId'] ) );
816 + if ( count( $pagename_parts ) > 0 && 'friends' === $pagename_parts[0] ) {
817 + return true;
818 + }
819 + }
820 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
821 + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
822 +
823 + if ( implode( '/', array_slice( $pagename_parts, 0, 5 ) ) === 'wp-json/wp/v2/templates/friends' ) {
824 + return true;
825 + }
826 +
827 + if ( implode( '/', array_slice( $pagename_parts, 0, 5 ) ) === 'wp-json/wp/v2/template-parts/friends' ) {
828 + return true;
829 + }
830 +
831 + return false;
758 832 }
759 833
760 834 /**
761 835 * Adds the post types to be displayed on the frontend.
@@ -768,65 +842,136 @@
768 842 return array_merge( array( Friends::CPT ), $post_types );
769 843 }
770 844
771 845 /**
846 + * Gets the post count by post status.
847 + *
848 + * @param bool $force_fetching Whether to force fetching.
849 + *
850 + * @return object The post count.
851 + */
852 + public function get_post_count_by_post_status( $force_fetching = false ) {
853 + $cache_key = 'friends_post_count';
854 +
855 + $post_types = apply_filters( 'friends_frontend_post_types', array() );
856 +
857 + $counts = wp_cache_get( $cache_key, 'friends' );
858 + if ( false !== $counts ) {
859 + return $counts;
860 + }
861 +
862 + $counts = get_transient( $cache_key );
863 + if ( false !== $counts ) {
864 + return $counts;
865 + } elseif ( ! $force_fetching ) {
866 + return (object) array(
867 + 'trash' => '...',
868 + );
869 + }
870 +
871 + $counts = new \stdClass();
872 +
873 + foreach ( $post_types as $post_type ) {
874 + $count = wp_count_posts( $post_type );
875 + foreach ( (array) $count as $post_status => $c ) {
876 + if ( ! isset( $counts->$post_status ) ) {
877 + $counts->$post_status = 0;
878 + }
879 + $counts->$post_status += $c;
880 + }
881 + }
882 +
883 + set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
884 + wp_cache_set( $cache_key, $counts, 'friends', HOUR_IN_SECONDS );
885 +
886 + return $counts;
887 + }
888 +
889 + /**
772 890 * Gets the post count by post format.
773 891 *
892 + * @param bool $force_fetching Whether to force fetching.
893 + *
774 894 * @return array The post count by post format.
775 895 */
776 - public function get_post_count_by_post_format() {
777 - $cache_key = 'friends_post_count_by_post_format';
896 + public function get_post_count_by_post_format( $force_fetching = false ) {
897 + $cache_key = 'post_count_by_post_format';
778 898
899 + $post_types = apply_filters( 'friends_frontend_post_types', array() );
900 +
901 + $counts = wp_cache_get( $cache_key, 'friends' );
902 + if ( false !== $counts ) {
903 + return $counts;
904 + }
905 +
779 906 $counts = get_transient( $cache_key );
780 - if ( false === $counts ) {
781 - $counts = array();
782 - $post_types = apply_filters( 'friends_frontend_post_types', array() );
907 + if ( false !== $counts ) {
908 + return $counts;
909 + } elseif ( ! $force_fetching ) {
910 + $post_formats = get_post_format_slugs();
911 + uksort(
912 + $post_formats,
913 + function ( $a, $b ) {
914 + // Sort standard to the top.
915 + if ( 'standard' === $a ) {
916 + return -1;
917 + } elseif ( 'standard' === $b ) {
918 + return 1;
919 + }
920 + return strnatcmp( $a, $b );
921 + }
922 + );
923 + $counts = array_fill_keys( $post_formats, '...' );
924 + return $counts;
925 + }
783 926
784 - global $wpdb;
927 + $counts = array();
785 928
786 - $counts = array();
929 + global $wpdb;
787 930
788 - $post_format_counts = $wpdb->get_results(
789 - $wpdb->prepare(
790 - "SELECT terms.slug AS post_format, COUNT(terms.slug) AS count
791 - FROM {$wpdb->posts} AS posts
792 - JOIN {$wpdb->term_relationships} AS relationships
793 - JOIN {$wpdb->term_taxonomy} AS taxonomy
794 - JOIN {$wpdb->terms} AS terms
931 + $counts = array();
795 932
796 - WHERE posts.post_status IN ( 'publish', 'private' )
797 - AND posts.post_type IN ( " . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . " )
798 - AND relationships.object_id = posts.ID
799 - AND relationships.term_taxonomy_id = taxonomy.term_taxonomy_id
800 - AND taxonomy.taxonomy = 'post_format'
933 + $post_format_counts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
934 + $wpdb->prepare(
935 + "SELECT terms.slug AS post_format, COUNT(terms.slug) AS count
936 + FROM {$wpdb->posts} AS posts
937 + JOIN {$wpdb->term_relationships} AS relationships
938 + JOIN {$wpdb->term_taxonomy} AS taxonomy
939 + JOIN {$wpdb->terms} AS terms
801 940
802 - AND terms.term_id = taxonomy.term_id
803 - GROUP BY terms.slug",
804 - $post_types
805 - )
806 - );
807 - $post_count = null;
808 - foreach ( $post_types as $post_type ) {
809 - $count = wp_count_posts( $post_type );
810 - if ( is_null( $post_count ) ) {
811 - $post_count = $count;
812 - } else {
813 - foreach ( (array) $count as $post_status => $c ) {
814 - $post_count->$post_status = ( isset( $post_count->$post_status ) ? $post_count->$post_status : 0 ) + $c;
815 - }
941 + WHERE posts.post_status IN ( 'publish', 'private' )
942 + AND posts.post_type IN ( " . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . " )
943 + AND relationships.object_id = posts.ID
944 + AND relationships.term_taxonomy_id = taxonomy.term_taxonomy_id
945 + AND taxonomy.taxonomy = 'post_format'
946 +
947 + AND terms.term_id = taxonomy.term_id
948 + GROUP BY terms.slug",
949 + $post_types
950 + )
951 + );
952 + $post_count = null;
953 + foreach ( $post_types as $post_type ) {
954 + $count = wp_count_posts( $post_type );
955 + if ( is_null( $post_count ) ) {
956 + $post_count = $count;
957 + } else {
958 + foreach ( (array) $count as $post_status => $c ) {
959 + $post_count->$post_status = ( isset( $post_count->$post_status ) ? $post_count->$post_status : 0 ) + $c;
816 960 }
817 961 }
818 - $counts['standard'] = $post_count->publish + $post_count->private;
962 + }
963 + $counts['standard'] = $post_count->publish + $post_count->private;
819 964
820 - foreach ( $post_format_counts as $row ) {
821 - $counts[ str_replace( 'post-format-', '', $row->post_format ) ] = $row->count;
822 - $counts['standard'] -= $row->count;
823 - }
965 + foreach ( $post_format_counts as $row ) {
966 + $counts[ str_replace( 'post-format-', '', $row->post_format ) ] = $row->count;
967 + $counts['standard'] -= $row->count;
968 + }
824 969
825 - $counts = array_filter( $counts );
970 + $counts = array_filter( $counts );
826 971
827 - set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
828 - }
972 + set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
973 + wp_cache_set( $cache_key, $counts, 'friends', HOUR_IN_SECONDS );
829 974
830 975 return $counts;
831 976 }
832 977
@@ -864,10 +1009,16 @@
864 1009 * @return object The post stats.
865 1010 */
866 1011 public static function get_post_stats() {
867 1012 global $wpdb;
1013 + $cache_key = 'post_stats';
1014 + $post_stats = wp_cache_get( $cache_key, 'friends' );
1015 + if ( false !== $post_stats ) {
1016 + return $post_stats;
1017 + }
1018 +
868 1019 $post_types = apply_filters( 'friends_frontend_post_types', array() );
869 - $post_stats = $wpdb->get_row(
1020 + $post_stats = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
870 1021 $wpdb->prepare(
871 1022 'SELECT SUM(
872 1023 LENGTH( ID ) +
873 1024 LENGTH( post_author ) +
@@ -900,9 +1051,9 @@
900 1051 ARRAY_A
901 1052 );
902 1053 $post_stats['earliest_post_date'] = mysql2date(
903 1054 'U',
904 - $wpdb->get_var(
1055 + $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
905 1056 $wpdb->prepare(
906 1057 "SELECT MIN(post_date) FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ( " . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )',
907 1058 $post_types
908 1059 )
@@ -908,8 +1059,9 @@
908 1059 )
909 1060 )
910 1061 );
911 1062
1063 + wp_cache_set( $cache_key, $post_stats, 'friends', HOUR_IN_SECONDS );
912 1064 return $post_stats;
913 1065 }
914 1066
915 1067 /**
@@ -920,9 +1072,9 @@
920 1072 public function get_main_header_data() {
921 1073 $data = array(
922 1074 'description' => '',
923 1075 'post_count_by_post_format' => $this->get_post_count_by_post_format(),
924 - 'post_count_by_post_status' => \wp_count_posts( self::CPT ),
1076 + 'post_count_by_post_status' => $this->get_post_count_by_post_status(),
925 1077 );
926 1078
927 1079 return $data;
928 1080 }
@@ -927,41 +1079,8 @@
927 1079 return $data;
928 1080 }
929 1081
930 1082 /**
931 - * Disables the author page for friends users.
932 - */
933 - public function disable_friends_author_page() {
934 - global $wp_query;
935 -
936 - if ( is_author() && ! self::authenticated_for_posts() ) {
937 - $author_obj = $wp_query->get_queried_object();
938 - if ( $author_obj instanceof \WP_User && User::is_friends_plugin_user( $author_obj ) && ! self::on_frontend() ) {
939 - $wp_query->set_404();
940 - status_header( 404 );
941 - }
942 - }
943 - }
944 -
945 - /**
946 - * Modify the main query to allow limiting the post format on the homepage.
947 - *
948 - * @param \WP_Query $query The query.
949 - */
950 - public function pre_get_posts_filter_by_post_format( $query ) {
951 - global $wp_query;
952 -
953 - if ( ! ( $query->is_main_query() || $query->is_feed() ) || ! empty( $wp_query->query['post_format'] ) || $query->is_friends_page ) {
954 - return;
955 - }
956 -
957 - $tax_query = $this->wp_query_get_post_format_tax_query( array(), get_option( 'friends_limit_homepage_post_format', false ) );
958 - if ( $tax_query ) {
959 - $query->set( 'tax_query', $tax_query );
960 - }
961 - }
962 -
963 - /**
964 1083 * Fix a bug in core where it outputs cached friend posts.
965 1084 *
966 1085 * @param array $qvs Query variables.
967 1086 * @return array
@@ -976,8 +1095,18 @@
976 1095 }
977 1096
978 1097 return $qvs;
979 1098 }
1099 +
1100 + public function register_my_apps( $apps ) {
1101 + $apps['friends'] = array(
1102 + 'name' => __( 'Friends', 'friends' ),
1103 + 'icon_url' => 'https://ps.w.org/friends/assets/icon-256x256.png',
1104 + 'url' => '/friends/',
1105 + );
1106 + return $apps;
1107 + }
1108 +
980 1109 /**
981 1110 * Add a post_format filter to a \WP_Query.
982 1111 *
983 1112 * @param array $tax_query The tax query.
@@ -1031,13 +1160,46 @@
1031 1160 },
1032 1161 $filter_by_post_format
1033 1162 );
1034 1163 }
1035 - $tax_query[] = $post_format_query;
1164 + if ( ! empty( $tax_query ) ) {
1165 + $tax_query[] = $post_format_query;
1166 + } else {
1167 + $tax_query = array( $post_format_query );
1168 + }
1036 1169
1037 1170 return $tax_query;
1038 1171 }
1039 1172
1173 + public function wp_query_get_post_tag_tax_query( $tax_query, $filter_by_post_tag ) {
1174 + if ( empty( $filter_by_post_tag ) ) {
1175 + return $tax_query;
1176 + }
1177 +
1178 + if ( ! is_array( $filter_by_post_tag ) ) {
1179 + $filter_by_post_tag = array( $filter_by_post_tag );
1180 + }
1181 +
1182 + if ( ! empty( $tax_query ) ) {
1183 + $tax_query['relation'] = 'AND';
1184 + }
1185 + $post_tag_query = array(
1186 + 'taxonomy' => self::TAG_TAXONOMY,
1187 + 'field' => 'slug',
1188 + 'operator' => 'IN',
1189 + 'terms' => $filter_by_post_tag,
1190 + );
1191 +
1192 + if ( ! empty( $tax_query ) ) {
1193 + $tax_query[] = $post_tag_query;
1194 + } else {
1195 + $tax_query = array( $post_tag_query );
1196 + }
1197 +
1198 + return $tax_query;
1199 + }
1200 +
1201 +
1040 1202 /**
1041 1203 * Plural texts for post formats.
1042 1204 *
1043 1205 * @param string $format The post format.
@@ -1072,8 +1234,12 @@
1072 1234 // translators: %s is a count.
1073 1235 'audio' => _nx_noop( '%s audio', '%s audios', 'Post format', 'friends' ),
1074 1236 );
1075 1237
1238 + if ( ! is_numeric( $count ) ) {
1239 + return sprintf( translate_nooped_plural( $plurals[ $format ], 0, 'friends' ), $count );
1240 + }
1241 +
1076 1242 return sprintf( translate_nooped_plural( $plurals[ $format ], $count, 'friends' ), number_format_i18n( $count ) );
1077 1243 }
1078 1244
1079 1245 /**
@@ -1093,15 +1259,9 @@
1093 1259 /**
1094 1260 * Get all of the rel links for the HTML head.
1095 1261 */
1096 1262 public static function get_link_rels() {
1097 - $rest_prefix = get_rest_url() . REST::PREFIX;
1098 - $links = array(
1099 - array(
1100 - 'rel' => 'friends-base-url',
1101 - 'href' => $rest_prefix,
1102 - ),
1103 - );
1263 + $links = array();
1104 1264
1105 1265 if ( get_option( 'friends_expose_post_format_feeds' ) && current_theme_supports( 'post-formats' ) ) {
1106 1266 $links = array_merge( $links, self::get_html_link_rel_alternate_post_formats() );
1107 1267 }
@@ -1116,9 +1276,9 @@
1116 1276 *
1117 1277 * @return string The text in ASCII only.
1118 1278 */
1119 1279 private static function strip_non_ascii( $text ) {
1120 - $text = html_entity_decode( $text );
1280 + $text = html_entity_decode( $text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
1121 1281 $text = str_replace( '»', '>', $text );
1122 1282 $text = strtr( $text, '"', '' );
1123 1283 return filter_var( $text, FILTER_DEFAULT, FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_NO_ENCODE_QUOTES );
1124 1284 }
@@ -1201,13 +1361,13 @@
1201 1361 * @param string $url The URL to check.
1202 1362 * @return false|string URL or false on failure.
1203 1363 */
1204 1364 public static function check_url( $url ) {
1205 - $pre = apply_filters( 'friends_pre_check_url', null );
1365 + $pre = apply_filters( 'friends_pre_check_url', null, $url );
1206 1366 if ( ! is_null( $pre ) ) {
1207 1367 return $pre;
1208 1368 }
1209 - $host = parse_url( $url, PHP_URL_HOST );
1369 + $host = wp_parse_url( $url, PHP_URL_HOST );
1210 1370
1211 1371 $check_url = apply_filters( 'friends_host_is_valid', null, $host );
1212 1372 if ( ! is_null( $check_url ) ) {
1213 1373 return $check_url;
@@ -1222,11 +1382,12 @@
1222 1382 if ( trim( $p['path'] ) ) {
1223 1383 $parts = array_merge( $parts, explode( '/', $p['path'] ) );
1224 1384 }
1225 1385
1226 - $url = join( '/', $parts );
1386 + $url = join( '/', array_filter( $parts ) );
1227 1387 $reduce = 4;
1228 - while ( strlen( $url ) > $max_length ) {
1388 + $url_length = strlen( $url );
1389 + while ( $url_length > $max_length ) {
1229 1390 $last_part = array_pop( $parts );
1230 1391 $last_part = substr( $last_part, strlen( $last_part ) - $reduce );
1231 1392 foreach ( $parts as $k => $part ) {
1232 1393 $parts[ $k ] = substr( $part, 0, strlen( $part ) - $reduce );
@@ -1233,23 +1394,317 @@
1233 1394 }
1234 1395 $url = join( '../', array_filter( $parts ) ) . '../..' . $last_part;
1235 1396 array_push( $parts, $last_part );
1236 1397 $reduce = 1;
1398 + $url_length = strlen( $url );
1399 + }
1237 1400
1401 + return $url;
1402 + }
1403 +
1404 + /**
1405 + * Cron function to delete old posts.
1406 + */
1407 + public function cron_friends_delete_outdated_posts() {
1408 + foreach ( User_Feed::get_all_users() as $friend_user ) {
1409 + $friend_user->delete_outdated_posts();
1238 1410 }
1411 + $this->delete_outdated_posts();
1412 + $this->cleanup_orphaned_friend_tags();
1413 + }
1239 1414
1240 - return $url;
1415 + /**
1416 + * Cron function to process post tag migration batches.
1417 + * Ensures the Migration class is loaded before calling the batch method.
1418 + */
1419 + public function cron_migrate_post_tags_batch() {
1420 + require_once __DIR__ . '/class-migration.php';
1421 + Migration::migrate_post_tags_batch();
1241 1422 }
1242 1423
1243 1424 /**
1425 + * Cron function to process ActivityPub attributedTo migration batches.
1426 + * Ensures the Migration class is loaded before calling the batch method.
1427 + */
1428 + public function cron_migrate_ap_attributed_to_batch() {
1429 + require_once __DIR__ . '/class-migration.php';
1430 + Migration::migrate_activitypub_attributed_to_batch();
1431 + }
1432 +
1433 + /**
1434 + * Cron function to process linking AP feeds to actors batches.
1435 + * Ensures the Migration class is loaded before calling the batch method.
1436 + */
1437 + public function cron_link_ap_feeds_batch() {
1438 + require_once __DIR__ . '/class-migration.php';
1439 + Migration::link_activitypub_feeds_to_actors_batch();
1440 + }
1441 +
1442 + /**
1443 + * Cron function to process backfilling External user attributedTo batches.
1444 + * Ensures the Migration class is loaded before calling the batch method.
1445 + */
1446 + public function cron_backfill_external_attributed_to_batch() {
1447 + require_once __DIR__ . '/class-migration.php';
1448 + Migration::backfill_external_attributed_to_batch();
1449 + }
1450 +
1451 + /**
1452 + * Cron function to process converting reply posts to comments batches.
1453 + * Ensures the Migration class is loaded before calling the batch method.
1454 + */
1455 + public function cron_convert_replies_batch() {
1456 + require_once __DIR__ . '/class-migration.php';
1457 + Migration::convert_replies_to_comments_batch();
1458 + }
1459 +
1460 + /**
1461 + * Cron function to process converting friend WP users to virtual subscriptions.
1462 + * Ensures the Migration class is loaded before calling the batch method.
1463 + */
1464 + public function cron_convert_friend_users_batch() {
1465 + require_once __DIR__ . '/class-migration.php';
1466 + Migration::convert_friend_users_batch();
1467 + }
1468 +
1469 + /**
1470 + * Convert a single reply post to a comment.
1471 + * This is scheduled as a single cron event when a new reply post is created.
1472 + *
1473 + * @param int $post_id The post ID to convert.
1474 + */
1475 + public function convert_single_reply_to_comment( $post_id ) {
1476 + $post = get_post( $post_id );
1477 + if ( ! $post || self::CPT !== $post->post_type ) {
1478 + return;
1479 + }
1480 +
1481 + // Skip if already trashed (already processed).
1482 + if ( 'trash' === $post->post_status ) {
1483 + return;
1484 + }
1485 +
1486 + require_once __DIR__ . '/class-migration.php';
1487 +
1488 + // Create a simple object for the process method.
1489 + $post_obj = (object) array(
1490 + 'ID' => $post->ID,
1491 + 'guid' => $post->guid,
1492 + 'post_author' => $post->post_author,
1493 + 'post_content' => $post->post_content,
1494 + 'post_date_gmt' => $post->post_date_gmt,
1495 + );
1496 +
1497 + Migration::process_potential_reply_post( $post_obj );
1498 + }
1499 +
1500 + /**
1501 + * Redirect trashed reply posts to their comment equivalents.
1502 + * When a reply post has been converted to a comment and trashed,
1503 + * visitors to the old post URL are redirected to the comment.
1504 + */
1505 + public function redirect_trashed_reply_to_comment() {
1506 + // Only handle single posts.
1507 + if ( ! is_singular( self::CPT ) ) {
1508 + return;
1509 + }
1510 +
1511 + $post = get_queried_object();
1512 + if ( ! $post || 'trash' !== $post->post_status ) {
1513 + return;
1514 + }
1515 +
1516 + $comment_id = get_post_meta( $post->ID, '_redirects_to_comment', true );
1517 + $redirect_post_id = get_post_meta( $post->ID, '_redirect_post_id', true );
1518 +
1519 + if ( ! $comment_id || ! $redirect_post_id ) {
1520 + return;
1521 + }
1522 +
1523 + $redirect_url = get_permalink( $redirect_post_id ) . '#comment-' . $comment_id;
1524 + wp_safe_redirect( $redirect_url, 301 );
1525 + exit;
1526 + }
1527 +
1528 + /**
1529 + * Fallback redirect for reply posts that have been permanently deleted.
1530 + * Looks up the comment by its original post GUID stored in comment meta.
1531 + */
1532 + public function fallback_redirect_via_comment_meta() {
1533 + // Only on 404.
1534 + if ( ! is_404() ) {
1535 + return;
1536 + }
1537 +
1538 + // Get the requested URL.
1539 + $requested_url = home_url( add_query_arg( array() ) );
1540 +
1541 + // Search for a comment that has this URL as its original post GUID.
1542 + global $wpdb;
1543 +
1544 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1545 + $comment = $wpdb->get_row(
1546 + $wpdb->prepare(
1547 + "SELECT c.comment_ID, c.comment_post_ID
1548 + FROM {$wpdb->comments} c
1549 + INNER JOIN {$wpdb->commentmeta} cm ON c.comment_ID = cm.comment_id
1550 + WHERE cm.meta_key = '_original_post_guid'
1551 + AND cm.meta_value = %s
1552 + LIMIT 1",
1553 + $requested_url
1554 + )
1555 + );
1556 +
1557 + if ( ! $comment ) {
1558 + return;
1559 + }
1560 +
1561 + $redirect_url = get_permalink( $comment->comment_post_ID ) . '#comment-' . $comment->comment_ID;
1562 + wp_safe_redirect( $redirect_url, 301 );
1563 + exit;
1564 + }
1565 +
1566 + /**
1567 + * Maybe delete an outdated post.
1568 + *
1569 + * @param int $post_id The post ID.
1570 + * @param string $reason The reason for deletion.
1571 + * @return int|false The post ID if it was deleted, false otherwise.
1572 + */
1573 + public static function maybe_delete_outdated_post( int $post_id, string $reason = '' ) {
1574 + if ( get_post_type( $post_id ) !== Friends::CPT ) {
1575 + return false;
1576 + }
1577 +
1578 + if ( ! get_option( 'friends_retention_delete_reacted' ) ) {
1579 + // get all terms for a post, no matter whether it's registered or not.
1580 + $term_query = new \WP_Term_Query(
1581 + array(
1582 + 'object_ids' => $post_id,
1583 + )
1584 + );
1585 + $reactions = array();
1586 + foreach ( $term_query->get_terms() as $term ) {
1587 + if ( substr( $term->taxonomy, 0, 16 ) !== 'friend-reaction-' ) {
1588 + continue;
1589 + }
1590 + $reactions[ $term->slug ] = true;
1591 +
1592 + }
1593 + if ( $reactions ) {
1594 + if ( apply_filters( 'friends_debug', false ) && ! wp_doing_cron() ) {
1595 + echo 'Skipping ', esc_html( $post_id ), ' because it has reactions (';
1596 + foreach ( array_keys( $reactions ) as $emoji ) {
1597 + echo esc_html( Reactions::validate_emoji( $emoji ) ), ' ';
1598 + }
1599 + echo ')<br/>', PHP_EOL;
1600 +
1601 + }
1602 + return false;
1603 + }
1604 + }
1605 +
1606 + if ( apply_filters( 'friends_debug', false ) && ! wp_doing_cron() ) {
1607 + echo 'Deleting ', esc_html( $post_id ), '(date: ', esc_html( get_the_date( 'Y-m-d H:i:s', $post_id ) ), ') because ', esc_html( $reason ), '<br/>', PHP_EOL;
1608 + }
1609 + wp_delete_post( $post_id, true );
1610 +
1611 + return $post_id;
1612 + }
1613 +
1614 + /**
1615 + * Delete posts the user decided to automatically delete.
1616 + */
1617 + public function delete_outdated_posts() {
1618 + $deleted_posts = array();
1619 +
1620 + $args = array(
1621 + 'post_type' => Friends::CPT,
1622 + 'post_status' => array( 'publish', 'trash' ),
1623 + 'fields' => 'ids',
1624 + 'posts_per_page' => 10,
1625 + );
1626 +
1627 + if ( get_option( 'friends_enable_retention_days' ) ) {
1628 + $args['date_query'] = array(
1629 + 'before' => gmdate( 'Y-m-d H:i:s', strtotime( '-' . ( Friends::get_retention_days() * 24 ) . 'hours' ) ),
1630 + );
1631 +
1632 + $query = new \WP_Query();
1633 + foreach ( $args as $key => $value ) {
1634 + $query->set( $key, $value );
1635 + }
1636 +
1637 + foreach ( $query->get_posts() as $post_id ) {
1638 + $post_id = self::maybe_delete_outdated_post( $post_id, 'date overflow' );
1639 + if ( $post_id ) {
1640 + $deleted_posts[] = $post_id;
1641 + }
1642 + }
1643 + unset( $args['date_query'] );
1644 + }
1645 +
1646 + $args['orderby'] = 'date';
1647 + $args['order'] = 'desc';
1648 + if ( get_option( 'friends_enable_retention_number' ) ) {
1649 + $args['offset'] = Friends::get_retention_number();
1650 + $query = new \WP_Query();
1651 + foreach ( $args as $key => $value ) {
1652 + $query->set( $key, $value );
1653 + }
1654 +
1655 + foreach ( $query->get_posts() as $post_id ) {
1656 + $post_id = self::maybe_delete_outdated_post( $post_id, 'global number overflow' );
1657 + if ( $post_id ) {
1658 + $deleted_posts[] = $post_id;
1659 + }
1660 + }
1661 + }
1662 +
1663 + // In any case, don't overflow the trash.
1664 + $args = array(
1665 + 'post_type' => Friends::CPT,
1666 + 'post_status' => 'trash',
1667 + 'offset' => 100,
1668 + 'fields' => 'ids',
1669 + 'orderby' => 'date',
1670 + 'order' => 'asc',
1671 + 'posts_per_page' => 10,
1672 + );
1673 +
1674 + $query = new \WP_Query();
1675 + foreach ( $args as $key => $value ) {
1676 + $query->set( $key, $value );
1677 + }
1678 +
1679 + foreach ( $query->get_posts() as $post_id ) {
1680 + $post_id = self::maybe_delete_outdated_post( $post_id, 'trash' );
1681 + if ( $post_id ) {
1682 + $deleted_posts[] = $post_id;
1683 + }
1684 + }
1685 +
1686 + return $deleted_posts;
1687 + }
1688 +
1689 + /**
1690 + * Clean up orphaned friend tags that have no posts.
1691 + */
1692 + public function cleanup_orphaned_friend_tags() {
1693 + Friend_Tag::cleanup_orphaned();
1694 + }
1695 +
1696 + /**
1244 1697 * Delete all the data the plugin has stored in WordPress
1245 1698 */
1246 1699 public static function uninstall_plugin() {
1247 1700 $taxonomies = array(
1248 1701 User_Feed::TAXONOMY,
1702 + Subscription::TAXONOMY,
1703 + self::TAG_TAXONOMY,
1249 1704 );
1250 1705
1251 - $affected_users = new \WP_User_Query( array( 'role__in' => array( 'friend', 'acquaintance', 'friend_request', 'pending_friend_request', 'subscription' ) ) );
1706 + $affected_users = new \WP_User_Query( array( 'role__in' => array( 'subscription' ) ) );
1252 1707 foreach ( $affected_users as $user ) {
1253 1708 $in_token = get_user_option( 'friends_in_token', $user->ID );
1254 1709 delete_option( 'friends_in_token_' . $in_token );
1255 1710 delete_user_option( $user->ID, 'friends_out_token' );
@@ -1258,12 +1713,9 @@
1258 1713 $taxonomies[] = 'friend-reaction-' . $user->ID;
1259 1714 }
1260 1715
1261 1716 delete_option( 'friends_main_user_id' );
1262 - remove_role( 'friend' );
1263 - remove_role( 'acquaintance' );
1264 - remove_role( 'friend_request' );
1265 - remove_role( 'pending_friend_request' );
1717 +
1266 1718 remove_role( 'subscription' );
1267 1719
1268 1720 $friend_posts = new \WP_Query(
1269 1721 array(
@@ -1279,8 +1731,10 @@
1279 1731
1280 1732 // We have to resort to using direct database statements since the taxonomy is not registered because the plugin is deactivated.
1281 1733 global $wpdb;
1282 1734 foreach ( $taxonomies as $taxonomy ) {
1735 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
1736 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
1283 1737 $terms = $wpdb->get_results( $wpdb->prepare( "SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy = %s ORDER BY t.name ASC", $taxonomy ) );
1284 1738
1285 1739 if ( $terms ) {
1286 1740 foreach ( $terms as $term ) {
@@ -1290,7 +1744,9 @@
1290 1744 }
1291 1745 }
1292 1746
1293 1747 $wpdb->delete( $wpdb->term_taxonomy, array( 'taxonomy' => $taxonomy ), array( '%s' ) );
1748 + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
1749 + // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching
1294 1750 }
1295 1751 }
1296 1752 }