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 +721 -231 2.8.14.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,35 +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 ) {
448 + }
449 +
450 + public static function has_pending_migrations() {
451 + $status = get_option( 'friends_migration_status' );
452 + if ( ! $status || 'completed' === $status ) {
453 + return false;
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() {
466 + $previous_version = get_option( 'friends_plugin_version' );
467 +
468 + // Bail early if no migration is necessary.
469 + if ( version_compare( $previous_version, Friends::VERSION, '>=' ) ) {
443 470 return;
444 471 }
445 - $previous_version = get_option( 'friends_plugin_version' );
446 472
473 + // Load migration class for any upgrade.
474 + require_once __DIR__ . '/class-migration.php';
475 +
447 476 if ( version_compare( $previous_version, '0.20.1', '<' ) ) {
448 - $users = User_Query::all_associated_users();
449 - foreach ( $users->get_results() as $user ) {
450 - $gravatar = get_user_option( 'friends_gravatar', $user->ID );
451 - $user_icon_url = get_user_option( 'friends_user_icon_url', $user->ID );
452 - if ( $gravatar ) {
453 - if ( ! $user_icon_url ) {
454 - update_user_option( $user->ID, 'friends_user_icon_url', $gravatar );
455 - }
456 - delete_user_option( $user->ID, 'friends_gravatar' );
457 - }
458 - }
477 + Migration::migrate_gravatar_to_user_icon_url();
459 478 }
460 479
461 480 if ( version_compare( $previous_version, '2.1.3', '<' ) ) {
462 481 self::setup_roles();
@@ -462,20 +481,45 @@
462 481 self::setup_roles();
463 482 }
464 483
465 484 if ( version_compare( $previous_version, '2.6.0', '<' ) ) {
466 - $users = User_Query::all_associated_users();
467 - foreach ( $users->get_results() as $user ) {
468 - if ( get_option( 'friends_feed_rules_' . $user->ID ) ) {
469 - $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 );
470 515 }
471 - if ( get_option( 'friends_feed_catch_all_' . $user->ID ) ) {
472 - $user->update_user_option( 'friends_feed_catch_all', get_option( 'friends_feed_catch_all_' . $user->ID ) );
473 - }
474 516 }
475 517 }
476 518
477 - update_option( 'friends_plugin_version', Friends::VERSION );
519 + Migration::backfill_activitypub_attributed_to_acct();
520 +
521 + update_option( 'friends_plugin_version', Friends::VERSION );
478 522 }
479 523
480 524 /**
481 525 * Actions to take upon plugin activation.
@@ -530,9 +574,8 @@
530 574 * Actions to take upon plugin activation.
531 575 */
532 576 private static function setup() {
533 577 self::setup_roles();
534 - self::create_friends_page();
535 578
536 579 self::upgrade_plugin(
537 580 null,
538 581 array(
@@ -549,14 +592,14 @@
549 592 if ( false === get_option( 'friends_private_rss_key' ) ) {
550 593 update_option( 'friends_private_rss_key', wp_generate_password( 128, false ) );
551 594 }
552 595
553 - if ( false === get_option( 'friends_default_friend_role' ) ) {
554 - 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' );
555 598 }
556 599
557 - if ( ! wp_next_scheduled( 'cron_friends_refresh_feeds' ) ) {
558 - 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' );
559 602 }
560 603
561 604 self::add_default_sidebars_widgets();
562 605 flush_rewrite_rules();
@@ -561,8 +604,17 @@
561 604 self::add_default_sidebars_widgets();
562 605 flush_rewrite_rules();
563 606 }
564 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 +
565 617 /**
566 618 * Add default widgets to the sidebars.
567 619 */
568 620 public static function add_default_sidebars_widgets() {
@@ -580,8 +632,9 @@
580 632 ),
581 633 'friends-widget-new-private-post' => array(),
582 634 ),
583 635 'friends-sidebar' => array(
636 + 'friends-widget-stats' => array(),
584 637 'friends-widget-refresh' => array(),
585 638 'friends-widget-post-formats' => array(),
586 639 'friends-widget-friend-list' => array(
587 640 'title' => 'Friends',
@@ -593,9 +646,9 @@
593 646 continue;
594 647 }
595 648 $sidebars_widgets[ $sidebar_id ] = array();
596 649 foreach ( $default_widgets as $widget_id => $options ) {
597 - $id += 1;
650 + ++$id;
598 651 $sidebars_widgets[ $sidebar_id ][] = $widget_id . '-' . $id;
599 652 update_option( 'widget_' . $widget_id, array( $id => $options ) );
600 653 }
601 654 }
@@ -610,18 +663,27 @@
610 663 $timestamp = wp_next_scheduled( 'cron_friends_refresh_feeds' );
611 664 wp_unschedule_event( $timestamp, 'cron_friends_refresh_feeds' );
612 665 }
613 666
667 + /**
668 + * Get the required capability for the menu entries.
669 + */
614 670 public static function required_menu_role() {
615 671 return 'edit_private_posts';
616 672 }
617 673
674 + /**
675 + * Check if the user hast the required priviliges.
676 + */
618 677 public static function has_required_privileges() {
619 - return self::is_main_user() || current_user_can( 'administrator' );
678 + return self::is_main_user() || current_user_can( 'manage_options' );
620 679 }
621 680
681 + /**
682 + * Check if the current user is the main user.
683 + */
622 684 public static function is_main_user() {
623 - return get_current_user_id() === self::get_main_friend_user_id();
685 + return is_user_logged_in() && get_current_user_id() === self::get_main_friend_user_id();
624 686 }
625 687
626 688 /**
627 689 * Get the main friend user id.
@@ -707,10 +769,10 @@
707 769 global $wp_query;
708 770
709 771 if ( ! isset( $wp_query ) || ! isset( $wp_query->query['pagename'] ) ) {
710 772 // The request has not yet been parsed but we need to know, so this snippet is from wp->parse_request().
711 - list( $req_uri ) = explode( '?', $_SERVER['REQUEST_URI'] );
712 - $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 );
713 775 $home_path_regex = '';
714 776 if ( is_string( $home_path ) && '' !== $home_path ) {
715 777 $home_path = trim( $home_path, '/' );
716 778 $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) );
@@ -725,8 +787,10 @@
725 787 } else {
726 788 $pagename = $wp_query->query['pagename'];
727 789 }
728 790
791 + // A nonce is not needed here since this is to show the public page.
792 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
729 793 if ( isset( $_GET['public'] ) ) {
730 794 return false;
731 795 }
732 796
@@ -734,18 +798,38 @@
734 798 return false;
735 799 }
736 800
737 801 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
738 - return count( $pagename_parts ) > 0 && 'friends' === $pagename_parts[0];
739 - }
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 + }
740 813
741 - /**
742 - * Check whether the request has been authenticated to display (private) posts.
743 - *
744 - * @return bool Whether the posts can be accessed.
745 - */
746 - public static function authenticated_for_posts() {
747 - 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;
748 832 }
749 833
750 834 /**
751 835 * Adds the post types to be displayed on the frontend.
@@ -758,65 +842,136 @@
758 842 return array_merge( array( Friends::CPT ), $post_types );
759 843 }
760 844
761 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 + /**
762 890 * Gets the post count by post format.
763 891 *
892 + * @param bool $force_fetching Whether to force fetching.
893 + *
764 894 * @return array The post count by post format.
765 895 */
766 - public function get_post_count_by_post_format() {
767 - $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';
768 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 +
769 906 $counts = get_transient( $cache_key );
770 - if ( false === $counts ) {
771 - $counts = array();
772 - $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 + }
773 926
774 - global $wpdb;
927 + $counts = array();
775 928
776 - $counts = array();
929 + global $wpdb;
777 930
778 - $post_format_counts = $wpdb->get_results(
779 - $wpdb->prepare(
780 - "SELECT terms.slug AS post_format, COUNT(terms.slug) AS count
781 - FROM {$wpdb->posts} AS posts
782 - JOIN {$wpdb->term_relationships} AS relationships
783 - JOIN {$wpdb->term_taxonomy} AS taxonomy
784 - JOIN {$wpdb->terms} AS terms
931 + $counts = array();
785 932
786 - WHERE posts.post_status IN ( 'publish', 'private' )
787 - AND posts.post_type IN ( " . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . " )
788 - AND relationships.object_id = posts.ID
789 - AND relationships.term_taxonomy_id = taxonomy.term_taxonomy_id
790 - 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
791 940
792 - AND terms.term_id = taxonomy.term_id
793 - GROUP BY terms.slug",
794 - $post_types
795 - )
796 - );
797 - $post_count = null;
798 - foreach ( $post_types as $post_type ) {
799 - $count = wp_count_posts( $post_type );
800 - if ( is_null( $post_count ) ) {
801 - $post_count = $count;
802 - } else {
803 - foreach ( (array) $count as $post_status => $c ) {
804 - $post_count->$post_status = ( isset( $post_count->$post_status ) ? $post_count->$post_status : 0 ) + $c;
805 - }
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;
806 960 }
807 961 }
808 - $counts['standard'] = $post_count->publish + $post_count->private;
962 + }
963 + $counts['standard'] = $post_count->publish + $post_count->private;
809 964
810 - foreach ( $post_format_counts as $row ) {
811 - $counts[ str_replace( 'post-format-', '', $row->post_format ) ] = $row->count;
812 - $counts['standard'] -= $row->count;
813 - }
965 + foreach ( $post_format_counts as $row ) {
966 + $counts[ str_replace( 'post-format-', '', $row->post_format ) ] = $row->count;
967 + $counts['standard'] -= $row->count;
968 + }
814 969
815 - $counts = array_filter( $counts );
970 + $counts = array_filter( $counts );
816 971
817 - set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
818 - }
972 + set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
973 + wp_cache_set( $cache_key, $counts, 'friends', HOUR_IN_SECONDS );
819 974
820 975 return $counts;
821 976 }
822 977
@@ -854,10 +1009,16 @@
854 1009 * @return object The post stats.
855 1010 */
856 1011 public static function get_post_stats() {
857 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 +
858 1019 $post_types = apply_filters( 'friends_frontend_post_types', array() );
859 - $post_stats = $wpdb->get_row(
1020 + $post_stats = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
860 1021 $wpdb->prepare(
861 1022 'SELECT SUM(
862 1023 LENGTH( ID ) +
863 1024 LENGTH( post_author ) +
@@ -890,9 +1051,9 @@
890 1051 ARRAY_A
891 1052 );
892 1053 $post_stats['earliest_post_date'] = mysql2date(
893 1054 'U',
894 - $wpdb->get_var(
1055 + $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
895 1056 $wpdb->prepare(
896 1057 "SELECT MIN(post_date) FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ( " . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )',
897 1058 $post_types
898 1059 )
@@ -898,8 +1059,9 @@
898 1059 )
899 1060 )
900 1061 );
901 1062
1063 + wp_cache_set( $cache_key, $post_stats, 'friends', HOUR_IN_SECONDS );
902 1064 return $post_stats;
903 1065 }
904 1066
905 1067 /**
@@ -910,9 +1072,9 @@
910 1072 public function get_main_header_data() {
911 1073 $data = array(
912 1074 'description' => '',
913 1075 'post_count_by_post_format' => $this->get_post_count_by_post_format(),
914 - 'post_count_by_post_status' => \wp_count_posts( self::CPT ),
1076 + 'post_count_by_post_status' => $this->get_post_count_by_post_status(),
915 1077 );
916 1078
917 1079 return $data;
918 1080 }
@@ -917,41 +1079,8 @@
917 1079 return $data;
918 1080 }
919 1081
920 1082 /**
921 - * Disables the author page for friends users.
922 - */
923 - public function disable_friends_author_page() {
924 - global $wp_query;
925 -
926 - if ( is_author() && ! self::authenticated_for_posts() ) {
927 - $author_obj = $wp_query->get_queried_object();
928 - if ( $author_obj instanceof \WP_User && User::is_friends_plugin_user( $author_obj ) && ! self::on_frontend() ) {
929 - $wp_query->set_404();
930 - status_header( 404 );
931 - }
932 - }
933 - }
934 -
935 - /**
936 - * Modify the main query to allow limiting the post format on the homepage.
937 - *
938 - * @param \WP_Query $query The query.
939 - */
940 - public function pre_get_posts_filter_by_post_format( $query ) {
941 - global $wp_query;
942 -
943 - if ( ! ( $query->is_main_query() || $query->is_feed() ) || ! empty( $wp_query->query['post_format'] ) || $query->is_friends_page ) {
944 - return;
945 - }
946 -
947 - $tax_query = $this->wp_query_get_post_format_tax_query( array(), get_option( 'friends_limit_homepage_post_format', false ) );
948 - if ( $tax_query ) {
949 - $query->set( 'tax_query', $tax_query );
950 - }
951 - }
952 -
953 - /**
954 1083 * Fix a bug in core where it outputs cached friend posts.
955 1084 *
956 1085 * @param array $qvs Query variables.
957 1086 * @return array
@@ -966,8 +1095,18 @@
966 1095 }
967 1096
968 1097 return $qvs;
969 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 +
970 1109 /**
971 1110 * Add a post_format filter to a \WP_Query.
972 1111 *
973 1112 * @param array $tax_query The tax query.
@@ -1021,13 +1160,46 @@
1021 1160 },
1022 1161 $filter_by_post_format
1023 1162 );
1024 1163 }
1025 - $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 + }
1026 1169
1027 1170 return $tax_query;
1028 1171 }
1029 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 +
1030 1202 /**
1031 1203 * Plural texts for post formats.
1032 1204 *
1033 1205 * @param string $format The post format.
@@ -1062,8 +1234,12 @@
1062 1234 // translators: %s is a count.
1063 1235 'audio' => _nx_noop( '%s audio', '%s audios', 'Post format', 'friends' ),
1064 1236 );
1065 1237
1238 + if ( ! is_numeric( $count ) ) {
1239 + return sprintf( translate_nooped_plural( $plurals[ $format ], 0, 'friends' ), $count );
1240 + }
1241 +
1066 1242 return sprintf( translate_nooped_plural( $plurals[ $format ], $count, 'friends' ), number_format_i18n( $count ) );
1067 1243 }
1068 1244
1069 1245 /**
@@ -1083,15 +1259,9 @@
1083 1259 /**
1084 1260 * Get all of the rel links for the HTML head.
1085 1261 */
1086 1262 public static function get_link_rels() {
1087 - $rest_prefix = get_rest_url() . REST::PREFIX;
1088 - $links = array(
1089 - array(
1090 - 'rel' => 'friends-base-url',
1091 - 'href' => $rest_prefix,
1092 - ),
1093 - );
1263 + $links = array();
1094 1264
1095 1265 if ( get_option( 'friends_expose_post_format_feeds' ) && current_theme_supports( 'post-formats' ) ) {
1096 1266 $links = array_merge( $links, self::get_html_link_rel_alternate_post_formats() );
1097 1267 }
@@ -1106,9 +1276,9 @@
1106 1276 *
1107 1277 * @return string The text in ASCII only.
1108 1278 */
1109 1279 private static function strip_non_ascii( $text ) {
1110 - $text = html_entity_decode( $text );
1280 + $text = html_entity_decode( $text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
1111 1281 $text = str_replace( '»', '>', $text );
1112 1282 $text = strtr( $text, '"', '' );
1113 1283 return filter_var( $text, FILTER_DEFAULT, FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_NO_ENCODE_QUOTES );
1114 1284 }
@@ -1191,13 +1361,13 @@
1191 1361 * @param string $url The URL to check.
1192 1362 * @return false|string URL or false on failure.
1193 1363 */
1194 1364 public static function check_url( $url ) {
1195 - $pre = apply_filters( 'friends_pre_check_url', null );
1365 + $pre = apply_filters( 'friends_pre_check_url', null, $url );
1196 1366 if ( ! is_null( $pre ) ) {
1197 1367 return $pre;
1198 1368 }
1199 - $host = parse_url( $url, PHP_URL_HOST );
1369 + $host = wp_parse_url( $url, PHP_URL_HOST );
1200 1370
1201 1371 $check_url = apply_filters( 'friends_host_is_valid', null, $host );
1202 1372 if ( ! is_null( $check_url ) ) {
1203 1373 return $check_url;
@@ -1205,17 +1375,336 @@
1205 1375
1206 1376 return wp_http_validate_url( $url );
1207 1377 }
1208 1378
1379 + public static function url_truncate( $url, $max_length = 50 ) {
1380 + $p = wp_parse_url( untrailingslashit( $url ) );
1381 + $parts = array( $p['host'] );
1382 + if ( trim( $p['path'] ) ) {
1383 + $parts = array_merge( $parts, explode( '/', $p['path'] ) );
1384 + }
1385 +
1386 + $url = join( '/', array_filter( $parts ) );
1387 + $reduce = 4;
1388 + $url_length = strlen( $url );
1389 + while ( $url_length > $max_length ) {
1390 + $last_part = array_pop( $parts );
1391 + $last_part = substr( $last_part, strlen( $last_part ) - $reduce );
1392 + foreach ( $parts as $k => $part ) {
1393 + $parts[ $k ] = substr( $part, 0, strlen( $part ) - $reduce );
1394 + }
1395 + $url = join( '../', array_filter( $parts ) ) . '../..' . $last_part;
1396 + array_push( $parts, $last_part );
1397 + $reduce = 1;
1398 + $url_length = strlen( $url );
1399 + }
1400 +
1401 + return $url;
1402 + }
1403 +
1209 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();
1410 + }
1411 + $this->delete_outdated_posts();
1412 + $this->cleanup_orphaned_friend_tags();
1413 + }
1414 +
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();
1422 + }
1423 +
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 + /**
1210 1697 * Delete all the data the plugin has stored in WordPress
1211 1698 */
1212 1699 public static function uninstall_plugin() {
1213 1700 $taxonomies = array(
1214 1701 User_Feed::TAXONOMY,
1702 + Subscription::TAXONOMY,
1703 + self::TAG_TAXONOMY,
1215 1704 );
1216 1705
1217 - $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' ) ) );
1218 1707 foreach ( $affected_users as $user ) {
1219 1708 $in_token = get_user_option( 'friends_in_token', $user->ID );
1220 1709 delete_option( 'friends_in_token_' . $in_token );
1221 1710 delete_user_option( $user->ID, 'friends_out_token' );
@@ -1224,12 +1713,9 @@
1224 1713 $taxonomies[] = 'friend-reaction-' . $user->ID;
1225 1714 }
1226 1715
1227 1716 delete_option( 'friends_main_user_id' );
1228 - remove_role( 'friend' );
1229 - remove_role( 'acquaintance' );
1230 - remove_role( 'friend_request' );
1231 - remove_role( 'pending_friend_request' );
1717 +
1232 1718 remove_role( 'subscription' );
1233 1719
1234 1720 $friend_posts = new \WP_Query(
1235 1721 array(
@@ -1245,8 +1731,10 @@
1245 1731
1246 1732 // We have to resort to using direct database statements since the taxonomy is not registered because the plugin is deactivated.
1247 1733 global $wpdb;
1248 1734 foreach ( $taxonomies as $taxonomy ) {
1735 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
1736 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
1249 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 ) );
1250 1738
1251 1739 if ( $terms ) {
1252 1740 foreach ( $terms as $term ) {
@@ -1256,7 +1744,9 @@
1256 1744 }
1257 1745 }
1258 1746
1259 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
1260 1750 }
1261 1751 }
1262 1752 }