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

class-friends.php in Friends 4.2.1, at includes/class-friends.php

1,744 lines 49.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Friends
4 *
5 * A plugin to connect WordPresses and communicate privately with your friends.
6 *
7 * @package Friends
8 */
9
10 namespace Friends;
11
12 /**
13 * This is the class for the Friends Plugin.
14 *
15 * @package Friends
16 * @author Alex Kirk
17 */
18 class Friends {
19 const VERSION = FRIENDS_VERSION;
20 const CPT = 'friend_post_cache';
21 const TAG_TAXONOMY = Friend_Tag::TAXONOMY;
22 const FEED_URL = 'friends-feed-url';
23 const PLUGIN_URL = 'https://wordpress.org/plugins/friends/';
24 const REQUIRED_ROLE = 'edit_private_posts';
25
26 /**
27 * Initialize the plugin
28 */
29 public static function init() {
30 static::get_instance();
31 }
32
33 /**
34 * A reference to the Abilities object.
35 *
36 * @var Abilities
37 */
38 public $abilities;
39
40 /**
41 * A reference to the Admin object.
42 *
43 * @var Admin
44 */
45 public $admin;
46
47 /**
48 * A reference to the Feed object.
49 *
50 * @var Feed
51 */
52 public $feed;
53
54 /**
55 * A reference to the Messages object.
56 *
57 * @var Messages
58 */
59 public $messages;
60
61 /**
62 * A reference to the Notifications object.
63 *
64 * @var Notifications
65 */
66 public $notifications;
67
68 /**
69 * A reference to the Frontend object.
70 *
71 * @var Frontend
72 */
73 public $frontend;
74
75 /**
76 * A reference to the REST object.
77 *
78 * @var REST
79 */
80 public $rest;
81
82 /**
83 * A reference to the Reactions object.
84 *
85 * @var Reactions
86 */
87 public $reactions;
88
89 /**
90 * Get the class singleton
91 *
92 * @return Friends A class instance.
93 */
94 public static function get_instance() {
95 static $instance;
96 if ( ! isset( $instance ) ) {
97 $self = get_called_class();
98 $instance = new $self();
99 }
100 return $instance;
101 }
102
103 /**
104 * Get the Template_Loader singleton
105 *
106 * @return Template_Loader A class instance.
107 */
108 public static function template_loader() {
109 static $template_loader;
110 if ( ! isset( $template_loader ) ) {
111 $template_loader = new Template_Loader();
112 }
113 return $template_loader;
114 }
115
116 /**
117 * Constructor
118 */
119 public function __construct() {
120 $this->admin = new Admin( $this );
121 $this->feed = new Feed( $this );
122 $this->messages = new Messages( $this );
123 $this->notifications = new Notifications( $this );
124 $this->frontend = new Frontend( $this );
125 $this->reactions = new Reactions( $this );
126 $this->rest = new REST( $this );
127 $this->abilities = new Abilities( $this );
128
129 new Third_Parties( $this );
130 new Blocks( $this );
131 new Logging( $this );
132 new Shortcodes( $this );
133 new Site_Health();
134 new Migration();
135 $this->register_hooks();
136 load_plugin_textdomain( 'friends', false, FRIENDS_PLUGIN_FILE . '/languages/' );
137
138 /**
139 * Friends has loaded.
140 *
141 * @param Friends $friends The friends object.
142 *
143 * You can now assume that all the Friends hooks and objects are available.
144 *
145 * Example:
146 * ```php
147 * add_action( 'friends_loaded', function( Friends $friends ) {
148 * add_action( 'init', 'initialize_my_plugin' );
149 * } );
150 * ```
151 */
152 do_action( 'friends_loaded', $this );
153
154 /**
155 * Time to register your parser.
156 *
157 * @param Feed $feed The feed object.
158 *
159 * You'll receive the feed object on which you need to call `register_parser()`.
160 *
161 * Example:
162 * ```php
163 * add_action( 'friends_load_parsers', function( Friends\Feed $friends_feed ) {
164 * $friends_feed->register_parser( 'simplepie', new Feed_Parser_SimplePie( $friends_feed ) );
165 * } );
166 * ```
167 */
168 do_action( 'friends_load_parsers', $this->feed );
169
170 /**
171 * Time to register your theme.
172 *
173 * @param Frontend $frontend The frontend object.
174 *
175 * You'll receive the frontend object on which you need to call `register_theme()`.
176 *
177 * Example:
178 * ```php
179 * add_action( 'friends_load_themes', function( Friends\Frontend $friends_frontend ) {
180 * $friends_frontend->register_theme( 'Mastodon', 'mastodon' );
181 * } );
182 * ```
183 */
184 do_action( 'friends_load_themes', $this->frontend );
185 }
186
187 /**
188 * Register the WordPress hooks
189 */
190 private function register_hooks() {
191 add_action( 'init', array( $this, 'register_custom_post_type' ) );
192 add_action( 'init', array( $this, 'register_friend_tag_taxonomy' ) );
193 add_action( 'init', array( 'Friends\Subscription', 'register_taxonomy' ) );
194
195 add_action( 'init', array( 'Friends\User_Feed', 'register_taxonomy' ) );
196 add_action( 'wp', array( $this, 'allow_browser_extension_request' ) );
197 add_filter( 'get_avatar_data', array( $this, 'get_avatar_data' ), 10, 2 );
198
199 add_action( 'template_redirect', array( $this, 'http_header' ), 5 );
200 add_filter( 'wp_head', array( $this, 'html_rel_links' ) );
201 add_filter( 'login_head', array( $this, 'html_rel_links' ) );
202
203 add_filter( 'after_setup_theme', array( $this, 'enable_post_formats' ) );
204 add_filter( 'cron_schedules', array( $this, 'add_fifteen_minutes_interval' ) ); // phpcs:ignore WordPressVIPMinimum.Performance.IntervalInSeconds.IntervalInSeconds
205 add_action( 'cron_friends_delete_old_posts', array( $this, 'cron_friends_delete_outdated_posts' ) );
206 add_action( 'friends_migrate_post_tags_batch', array( $this, 'cron_migrate_post_tags_batch' ) );
207 add_action( 'friends_migrate_ap_attributed_to_batch', array( $this, 'cron_migrate_ap_attributed_to_batch' ) );
208 add_action( 'friends_link_ap_feeds_batch', array( $this, 'cron_link_ap_feeds_batch' ) );
209 add_action( 'friends_backfill_external_attributed_to_batch', array( $this, 'cron_backfill_external_attributed_to_batch' ) );
210 add_action( 'friends_convert_replies_batch', array( $this, 'cron_convert_replies_batch' ) );
211 add_action( 'friends_convert_friend_users_batch', array( $this, 'cron_convert_friend_users_batch' ) );
212 add_action( 'friends_convert_single_reply', array( $this, 'convert_single_reply_to_comment' ) );
213 add_action( 'template_redirect', array( $this, 'redirect_trashed_reply_to_comment' ), 4 );
214 add_action( 'template_redirect', array( $this, 'fallback_redirect_via_comment_meta' ), 4 );
215
216 add_action( 'comment_form_defaults', array( $this, 'comment_form_defaults' ) );
217 add_filter( 'friends_frontend_post_types', array( $this, 'add_frontend_post_types' ) );
218
219 add_filter( 'request', array( $this, 'limit_post_format_request' ), 20 );
220 add_filter( 'my_apps_plugins', array( $this, 'register_my_apps' ) );
221 User::register_wrapper_hooks();
222 }
223
224 /**
225 * Register the friend tag taxonomy
226 */
227 public function register_friend_tag_taxonomy() {
228 Friend_Tag::register();
229 }
230
231 /**
232 * Registers the custom post type
233 */
234 public function register_custom_post_type() {
235 $labels = array(
236 'name' => __( 'Friend Posts', 'friends' ),
237 'singular_name' => __( 'Friend Post', 'friends' ),
238 'add_new' => _x( 'Add New', 'cached friend post', 'friends' ),
239 'add_new_item' => __( 'Add New Friend Post', 'friends' ),
240 'edit_item' => __( 'Edit Friend Post', 'friends' ),
241 'new_item' => __( 'New Friend Post', 'friends' ),
242 'all_items' => __( 'All Friend Posts', 'friends' ),
243 'view_item' => __( 'View Friend Post', 'friends' ),
244 'search_items' => __( 'Search Friend Posts', 'friends' ),
245 'not_found' => __( 'No Friend Posts found', 'friends' ),
246 'not_found_in_trash' => __( 'No Friend Posts found in the Trash', 'friends' ),
247 'parent_item_colon' => '',
248 'menu_name' => __( 'Cached Friend Posts', 'friends' ),
249 );
250
251 $args = array(
252 'labels' => $labels,
253 'description' => "A cached friend's post",
254 'publicly_queryable' => is_admin() && self::is_main_user() && apply_filters( 'friends_show_cached_posts', false ),
255 'show_ui' => true,
256 'show_in_menu' => apply_filters( 'friends_show_cached_posts', false ),
257 'show_in_nav_menus' => false,
258 'show_in_admin_bar' => false,
259 'show_in_rest' => is_user_logged_in(),
260 'exclude_from_search' => true,
261 'public' => false,
262 'delete_with_user' => true,
263 'menu_position' => 5,
264 'menu_icon' => 'dashicons-groups',
265 'supports' => array( 'title', 'editor', 'author', 'revisions', 'thumbnail', 'excerpt', 'comments', 'post-formats' ),
266 'taxonomies' => array( self::TAG_TAXONOMY, 'post_format', 'friend-reaction-' . get_current_user_id() ),
267 'has_archive' => true,
268 'rewrite' => false,
269 );
270
271 register_post_type( self::CPT, $args );
272
273 register_post_meta(
274 self::CPT,
275 Feed::COMMENTS_FEED_META,
276 array(
277 'show_in_rest' => true,
278 'single' => true,
279 'type' => 'string',
280 )
281 );
282
283 register_post_meta(
284 self::CPT,
285 'remote_post_id',
286 array(
287 'show_in_rest' => true,
288 'single' => true,
289 'type' => 'string',
290 )
291 );
292
293 register_post_meta(
294 self::CPT,
295 'parser',
296 array(
297 'show_in_rest' => true,
298 'single' => true,
299 'type' => 'string',
300 )
301 );
302
303 register_post_meta(
304 self::CPT,
305 'feed_url',
306 array(
307 'show_in_rest' => true,
308 'single' => true,
309 'type' => 'string',
310 )
311 );
312
313 register_post_meta(
314 self::CPT,
315 'reblogged',
316 array(
317 'show_in_rest' => true,
318 'single' => true,
319 'type' => 'string',
320 )
321 );
322
323 register_post_meta(
324 self::CPT,
325 'reblogged_by',
326 array(
327 'show_in_rest' => true,
328 'single' => true,
329 'type' => 'integer',
330 )
331 );
332 }
333
334 public function allow_browser_extension_request() {
335 if ( get_http_origin() && is_home() ) {
336 $scheme = wp_parse_url( get_http_origin(), PHP_URL_SCHEME );
337 if ( 'moz-extension' === $scheme ) {
338 header( 'access-control-allow-origin: ' . get_http_origin() );
339 }
340 }
341 }
342
343 public static function get_role_capabilities( $role ) {
344 $capabilities = array();
345
346 $capabilities['subscription'] = array(
347 'subscription' => true,
348 'friends_plugin' => true,
349 );
350
351 if ( ! isset( $capabilities[ $role ] ) ) {
352 return array();
353 }
354
355 return $capabilities[ $role ];
356 }
357
358 /**
359 * Create the Friend user roles
360 */
361 private static function setup_roles() {
362 $default_roles = array(
363 'subscription' => _x( 'Subscription', 'User role', 'friends' ),
364 );
365
366 $roles = new \WP_Roles();
367
368 foreach ( $default_roles as $type => $name ) {
369 $role = false;
370 foreach ( $roles->roles as $slug => $data ) {
371 if ( isset( $data['capabilities'][ $type ] ) ) {
372 $role = get_role( $slug );
373 break;
374 }
375 }
376 if ( ! $role ) {
377 $role = add_role( $type, $name, self::get_role_capabilities( $type ) );
378 continue;
379 }
380
381 // This might update missing capabilities.
382 foreach ( array_keys( self::get_role_capabilities( $type ) ) as $cap ) {
383 $role->add_cap( $cap );
384 }
385 }
386 }
387
388 /**
389 * Enable translated user roles.
390 * props https://wordpress.stackexchange.com/a/141705/74893
391 *
392 * @param string $translations The potentially translated text.
393 * @param string $text Text to translate.
394 * @param string $context Context information for the translators.
395 * @param string $domain Unique identifier for retrieving translated strings.
396 * @return string Translated text on success, original text on failure.
397 */
398 public static function translate_user_role( $translations, $text, $context, $domain ) {
399 $roles = array(
400 'Subscription',
401 );
402
403 if (
404 'User role' === $context
405 && in_array( $text, $roles, true )
406 && 'friends' !== $domain
407 ) {
408 // @codingStandardsIgnoreLine
409 return translate_with_gettext_context( $text, $context, 'friends' );
410 }
411
412 return $translations;
413 }
414
415 /**
416 * Gets the roles that the plugin uses.
417 *
418 * @return array The roles.
419 */
420 public static function get_friends_plugin_roles() {
421 return apply_filters( 'friends_plugin_roles', array( 'subscription' ) );
422 }
423
424 /**
425 * If a plugin version upgrade requires changes, they can be done here
426 *
427 * @param \WP_Upgrader $upgrader_object The WP_Upgrader instance.
428 * @param array $options Array of bulk item update data.
429 */
430 public static function upgrade_plugin_trigger( $upgrader_object, $options ) {
431 $upgraded = false;
432 if ( 'update' === $options['action'] && 'plugin' === $options['type'] && isset( $options['plugins'] ) ) {
433 foreach ( $options['plugins'] as $plugin ) {
434 if ( FRIENDS_PLUGIN_BASENAME === $plugin ) {
435 return self::upgrade_plugin();
436 }
437 }
438 }
439 }
440
441 public static function has_pending_migrations() {
442 $status = get_option( 'friends_migration_status' );
443 if ( ! $status || 'completed' === $status ) {
444 return false;
445 }
446 if ( is_numeric( $status ) && ( time() - intval( $status ) ) < 2 * DAY_IN_SECONDS ) {
447 return false;
448 }
449 require_once __DIR__ . '/class-migration.php';
450 if ( ! Migration::has_pending_tracked_migrations() ) {
451 return false;
452 }
453 return true;
454 }
455
456 public static function upgrade_plugin() {
457 $previous_version = get_option( 'friends_plugin_version' );
458
459 // Bail early if no migration is necessary.
460 if ( version_compare( $previous_version, Friends::VERSION, '>=' ) ) {
461 return;
462 }
463
464 // Load migration class for any upgrade.
465 require_once __DIR__ . '/class-migration.php';
466
467 if ( version_compare( $previous_version, '0.20.1', '<' ) ) {
468 Migration::migrate_gravatar_to_user_icon_url();
469 }
470
471 if ( version_compare( $previous_version, '2.1.3', '<' ) ) {
472 self::setup_roles();
473 }
474
475 if ( version_compare( $previous_version, '2.6.0', '<' ) ) {
476 Migration::migrate_feed_options_to_user_options();
477 }
478
479 if ( version_compare( $previous_version, '2.8.7', '<' ) ) {
480 Migration::enable_wp_friendships_if_used();
481 }
482
483 if ( version_compare( $previous_version, '2.9.4', '<' ) ) {
484 Migration::migrate_external_user_and_cron();
485 }
486
487 if ( version_compare( $previous_version, '3.1.8', '<' ) ) {
488 Migration::migrate_frontend_default_view_to_user_option();
489 }
490
491 if ( version_compare( $previous_version, '4.0.0', '<' ) ) {
492 Migration::migrate_post_tags_to_friend_tags();
493 Migration::backfill_mention_tags_from_mastodon_html();
494 Migration::migrate_activitypub_attributed_to();
495 Migration::import_activitypub_followings();
496 Migration::link_activitypub_feeds_to_actors();
497 Migration::link_feeds_as_term_children();
498
499 // Show the welcome/update screen if this is an upgrade (not a fresh install).
500 if ( $previous_version ) {
501 update_option( 'friends_welcome_version', '4.0' );
502 if ( Migration::has_pending_tracked_migrations() ) {
503 update_option( 'friends_migration_status', 'pending', false );
504 } else {
505 update_option( 'friends_migration_status', 'completed', false );
506 }
507 }
508 }
509
510 Migration::backfill_activitypub_attributed_to_acct();
511
512 update_option( 'friends_plugin_version', Friends::VERSION );
513 }
514
515 /**
516 * Actions to take upon plugin activation.
517 *
518 * @param bool $network_activate Whether the plugin has been activated network-wide.
519 */
520 public static function activate_plugin( $network_activate = null ) {
521 if ( function_exists( 'is_multisite' ) && is_multisite() ) {
522 if ( $network_activate ) {
523 // Only Super Admins can use Network Activate.
524 if ( ! is_super_admin() ) {
525 return;
526 }
527
528 // Activate for each site.
529 foreach ( get_sites() as $blog ) {
530 switch_to_blog( $blog->blog_id );
531 self::setup();
532 restore_current_blog();
533 }
534 } elseif ( current_user_can( 'activate_plugins' ) ) {
535 self::setup();
536 }
537 return;
538 }
539
540 self::setup();
541 }
542
543 /**
544 * Actions to take upon plugin activation.
545 *
546 * @param int|WP_Site $blog_id Blog ID.
547 */
548 public static function activate_for_blog( $blog_id ) {
549 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
550 require_once ABSPATH . 'wp-admin/includes/plugin.php';
551 }
552
553 if ( $blog_id instanceof \WP_Site ) {
554 $blog_id = (int) $blog_id->blog_id;
555 }
556
557 if ( is_plugin_active_for_network( FRIENDS_PLUGIN_BASENAME ) ) {
558 switch_to_blog( $blog_id );
559 self::setup();
560 restore_current_blog();
561 }
562 }
563
564 /**
565 * Actions to take upon plugin activation.
566 */
567 private static function setup() {
568 self::setup_roles();
569
570 self::upgrade_plugin(
571 null,
572 array(
573 'action' => 'update',
574 'plugins' => array( FRIENDS_PLUGIN_BASENAME ),
575 'type' => 'plugin',
576 )
577 );
578
579 if ( false === get_option( 'friends_main_user_id' ) ) {
580 update_option( 'friends_main_user_id', get_current_user_id() );
581 }
582
583 if ( false === get_option( 'friends_private_rss_key' ) ) {
584 update_option( 'friends_private_rss_key', wp_generate_password( 128, false ) );
585 }
586
587 if ( ! wp_next_scheduled( 'cron_friends_refresh_feeds' ) ) {
588 wp_schedule_event( time(), 'fifteen-minutes', 'cron_friends_refresh_feeds' );
589 }
590
591 if ( ! wp_next_scheduled( 'cron_friends_delete_old_posts' ) ) {
592 wp_schedule_event( time(), 'hourly', 'cron_friends_delete_old_posts' );
593 }
594
595 self::add_default_sidebars_widgets();
596 flush_rewrite_rules();
597 }
598
599 public function add_fifteen_minutes_interval( $schedules ) {
600 $schedules['fifteen-minutes'] = array(
601 'interval' => 900,
602 'display' => __( 'Every 15 Minutes', 'friends' ),
603 );
604
605 return $schedules;
606 }
607
608 /**
609 * Add default widgets to the sidebars.
610 */
611 public static function add_default_sidebars_widgets() {
612 $sidebars_widgets = get_option( 'sidebars_widgets' );
613
614 if ( ! is_array( $sidebars_widgets ) ) {
615 $sidebars_widgets = array();
616 }
617 $id = count( $sidebars_widgets );
618
619 foreach ( array(
620 'friends-topbar' => array(
621 'friends-widget-header' => array(
622 'title' => 'Friends',
623 ),
624 'friends-widget-new-private-post' => array(),
625 ),
626 'friends-sidebar' => array(
627 'friends-widget-stats' => array(),
628 'friends-widget-refresh' => array(),
629 'friends-widget-post-formats' => array(),
630 'friends-widget-friend-list' => array(
631 'title' => 'Friends',
632 ),
633 'friends-widget-friend-request' => array(),
634 ),
635 ) as $sidebar_id => $default_widgets ) {
636 if ( ! empty( $sidebars_widgets[ $sidebar_id ] ) ) {
637 continue;
638 }
639 $sidebars_widgets[ $sidebar_id ] = array();
640 foreach ( $default_widgets as $widget_id => $options ) {
641 ++$id;
642 $sidebars_widgets[ $sidebar_id ][] = $widget_id . '-' . $id;
643 update_option( 'widget_' . $widget_id, array( $id => $options ) );
644 }
645 }
646
647 update_option( 'sidebars_widgets', $sidebars_widgets );
648 }
649
650 /**
651 * Actions to take upon plugin deactivation.
652 */
653 public static function deactivate_plugin() {
654 $timestamp = wp_next_scheduled( 'cron_friends_refresh_feeds' );
655 wp_unschedule_event( $timestamp, 'cron_friends_refresh_feeds' );
656 }
657
658 /**
659 * Get the required capability for the menu entries.
660 */
661 public static function required_menu_role() {
662 return 'edit_private_posts';
663 }
664
665 /**
666 * Check if the user hast the required priviliges.
667 */
668 public static function has_required_privileges() {
669 return self::is_main_user() || current_user_can( 'manage_options' );
670 }
671
672 /**
673 * Check if the current user is the main user.
674 */
675 public static function is_main_user() {
676 return is_user_logged_in() && get_current_user_id() === self::get_main_friend_user_id();
677 }
678
679 /**
680 * Get the main friend user id.
681 *
682 * @return int The user_id.
683 */
684 public static function get_main_friend_user_id() {
685 $main_user_id = get_option( 'friends_main_user_id' );
686
687 if (
688 false === $main_user_id
689 || ( is_multisite() && ! is_user_member_of_blog( $main_user_id, get_current_blog_id() ) )
690 ) {
691 $users = User_Query::all_admin_users();
692 foreach ( $users->get_results() as $user ) {
693 $main_user_id = $user->ID;
694 break;
695 }
696 if ( $main_user_id ) {
697 update_option( 'friends_main_user_id', $main_user_id );
698 }
699 }
700 return intval( $main_user_id );
701 }
702
703 /**
704 * Override Avatar URL for friends
705 *
706 * @param array $args The avatar details array.
707 * @param mixed $id_or_email Identifies what to retrieve the avatar for.
708 * @return array The avatar (potentially modified) details array.
709 */
710 public function get_avatar_data( $args, $id_or_email ) {
711 if ( is_object( $id_or_email ) ) {
712 if ( $id_or_email instanceof User ) {
713 $id_or_email = $id_or_email->ID;
714 } elseif ( $id_or_email instanceof \WP_Post ) {
715 $id_or_email = $id_or_email->post_author;
716 } elseif ( $id_or_email instanceof \WP_Comment ) {
717 $id_or_email = $id_or_email->user_id;
718 }
719 }
720
721 $user = false;
722 if ( is_numeric( $id_or_email ) && $id_or_email > 0 ) {
723 $user = get_user_by( 'ID', $id_or_email );
724 if ( $user ) {
725 $user = new User( $user );
726 }
727 } elseif ( is_string( $id_or_email ) ) {
728 $user = User::get_by_username( $id_or_email );
729 }
730
731 if ( $user ) {
732 if ( is_wp_error( $user ) ) {
733 return $args;
734 }
735
736 $url = $user->get_avatar_url();
737 if ( $url ) {
738 $args['url'] = $url;
739 $args['found_avatar'] = true;
740 }
741 }
742 return $args;
743 }
744
745 /**
746 * Enables the post formats.
747 */
748 public function enable_post_formats() {
749 if ( get_option( 'friends_force_enable_post_formats' ) ) {
750 add_theme_support( 'post-formats', get_post_format_slugs() );
751 }
752 }
753
754 /**
755 * Determine whether we are on the /friends/ page or a subpage.
756 *
757 * @return boolean Whether we are on a friends page URL.
758 */
759 public static function on_frontend() {
760 global $wp_query;
761
762 if ( ! isset( $wp_query ) || ! isset( $wp_query->query['pagename'] ) ) {
763 // The request has not yet been parsed but we need to know, so this snippet is from wp->parse_request().
764 list( $req_uri ) = explode( '?', remove_query_arg( '' ) );
765 $home_path = wp_parse_url( home_url(), PHP_URL_PATH );
766 $home_path_regex = '';
767 if ( is_string( $home_path ) && '' !== $home_path ) {
768 $home_path = trim( $home_path, '/' );
769 $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) );
770 }
771 $req_uri = trim( $req_uri, '/' );
772
773 if ( ! empty( $home_path_regex ) ) {
774 $req_uri = preg_replace( $home_path_regex, '', $req_uri );
775 $req_uri = trim( $req_uri, '/' );
776 }
777 $pagename = $req_uri;
778 } else {
779 $pagename = $wp_query->query['pagename'];
780 }
781
782 // A nonce is not needed here since this is to show the public page.
783 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
784 if ( isset( $_GET['public'] ) ) {
785 return false;
786 }
787
788 if ( ! current_user_can( self::REQUIRED_ROLE ) || ( is_multisite() && ! is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) && is_super_admin( get_current_user_id() ) ) ) {
789 return false;
790 }
791
792 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
793 if ( count( $pagename_parts ) > 0 && 'friends' === $pagename_parts[0] ) {
794 return true;
795 }
796 // phpcs:disable WordPress.Security.NonceVerification.Recommended
797 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
798 if ( implode( '/', array_slice( $pagename_parts, 0, 2 ) ) === 'wp-admin/customize.php' && isset( $_REQUEST['url'] ) ) {
799 $pagename_parts = explode( '/', trim( str_replace( '://', '', wp_unslash( $_REQUEST['url'] ) ), '/' ) );
800 if ( count( $pagename_parts ) > 0 && 'friends' === $pagename_parts[1] ) {
801 return true;
802 }
803 }
804
805 if ( implode( '/', array_slice( $pagename_parts, 0, 2 ) ) === 'wp-admin/site-editor.php' && isset( $_REQUEST['postId'] ) ) {
806 $pagename_parts = explode( '//', wp_unslash( $_REQUEST['postId'] ) );
807 if ( count( $pagename_parts ) > 0 && 'friends' === $pagename_parts[0] ) {
808 return true;
809 }
810 }
811 // phpcs:enable WordPress.Security.NonceVerification.Recommended
812 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
813
814 if ( implode( '/', array_slice( $pagename_parts, 0, 5 ) ) === 'wp-json/wp/v2/templates/friends' ) {
815 return true;
816 }
817
818 if ( implode( '/', array_slice( $pagename_parts, 0, 5 ) ) === 'wp-json/wp/v2/template-parts/friends' ) {
819 return true;
820 }
821
822 return false;
823 }
824
825 /**
826 * Adds the post types to be displayed on the frontend.
827 *
828 * @param string $post_types The incoming post types.
829 *
830 * @return array The frontend post types.
831 */
832 public static function add_frontend_post_types( $post_types ) {
833 return array_merge( array( Friends::CPT ), $post_types );
834 }
835
836 /**
837 * Gets the post count by post status.
838 *
839 * @param bool $force_fetching Whether to force fetching.
840 *
841 * @return object The post count.
842 */
843 public function get_post_count_by_post_status( $force_fetching = false ) {
844 $cache_key = 'friends_post_count';
845
846 $post_types = apply_filters( 'friends_frontend_post_types', array() );
847
848 $counts = wp_cache_get( $cache_key, 'friends' );
849 if ( false !== $counts ) {
850 return $counts;
851 }
852
853 $counts = get_transient( $cache_key );
854 if ( false !== $counts ) {
855 return $counts;
856 } elseif ( ! $force_fetching ) {
857 return (object) array(
858 'trash' => '...',
859 );
860 }
861
862 $counts = new \stdClass();
863
864 foreach ( $post_types as $post_type ) {
865 $count = wp_count_posts( $post_type );
866 foreach ( (array) $count as $post_status => $c ) {
867 if ( ! isset( $counts->$post_status ) ) {
868 $counts->$post_status = 0;
869 }
870 $counts->$post_status += $c;
871 }
872 }
873
874 set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
875 wp_cache_set( $cache_key, $counts, 'friends', HOUR_IN_SECONDS );
876
877 return $counts;
878 }
879
880 /**
881 * Gets the post count by post format.
882 *
883 * @param bool $force_fetching Whether to force fetching.
884 *
885 * @return array The post count by post format.
886 */
887 public function get_post_count_by_post_format( $force_fetching = false ) {
888 $cache_key = 'post_count_by_post_format';
889
890 $post_types = apply_filters( 'friends_frontend_post_types', array() );
891
892 $counts = wp_cache_get( $cache_key, 'friends' );
893 if ( false !== $counts ) {
894 return $counts;
895 }
896
897 $counts = get_transient( $cache_key );
898 if ( false !== $counts ) {
899 return $counts;
900 } elseif ( ! $force_fetching ) {
901 $post_formats = get_post_format_slugs();
902 uksort(
903 $post_formats,
904 function ( $a, $b ) {
905 // Sort standard to the top.
906 if ( 'standard' === $a ) {
907 return -1;
908 } elseif ( 'standard' === $b ) {
909 return 1;
910 }
911 return strnatcmp( $a, $b );
912 }
913 );
914 $counts = array_fill_keys( $post_formats, '...' );
915 return $counts;
916 }
917
918 $counts = array();
919
920 global $wpdb;
921
922 $counts = array();
923
924 $post_format_counts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
925 $wpdb->prepare(
926 "SELECT terms.slug AS post_format, COUNT(terms.slug) AS count
927 FROM {$wpdb->posts} AS posts
928 JOIN {$wpdb->term_relationships} AS relationships
929 JOIN {$wpdb->term_taxonomy} AS taxonomy
930 JOIN {$wpdb->terms} AS terms
931
932 WHERE posts.post_status IN ( 'publish', 'private' )
933 AND posts.post_type IN ( " . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . " )
934 AND relationships.object_id = posts.ID
935 AND relationships.term_taxonomy_id = taxonomy.term_taxonomy_id
936 AND taxonomy.taxonomy = 'post_format'
937
938 AND terms.term_id = taxonomy.term_id
939 GROUP BY terms.slug",
940 $post_types
941 )
942 );
943 $post_count = null;
944 foreach ( $post_types as $post_type ) {
945 $count = wp_count_posts( $post_type );
946 if ( is_null( $post_count ) ) {
947 $post_count = $count;
948 } else {
949 foreach ( (array) $count as $post_status => $c ) {
950 $post_count->$post_status = ( isset( $post_count->$post_status ) ? $post_count->$post_status : 0 ) + $c;
951 }
952 }
953 }
954 $counts['standard'] = $post_count->publish + $post_count->private;
955
956 foreach ( $post_format_counts as $row ) {
957 $counts[ str_replace( 'post-format-', '', $row->post_format ) ] = $row->count;
958 $counts['standard'] -= $row->count;
959 }
960
961 $counts = array_filter( $counts );
962
963 set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
964 wp_cache_set( $cache_key, $counts, 'friends', HOUR_IN_SECONDS );
965
966 return $counts;
967 }
968
969 /**
970 * Get the retention by number of posts.
971 *
972 * @return int The retention by number of posts.
973 */
974 public static function get_retention_number() {
975 $number = get_option( 'friends_retention_number' );
976 if ( $number <= 0 ) {
977 return 2000;
978 }
979
980 return $number;
981 }
982
983 /**
984 * Get the retention by days of posts.
985 *
986 * @return int The retention by days of posts.
987 */
988 public static function get_retention_days() {
989 $days = get_option( 'friends_retention_days' );
990 if ( $days <= 0 ) {
991 return 30;
992 }
993
994 return $days;
995 }
996
997 /**
998 * Gets the post stats.
999 *
1000 * @return object The post stats.
1001 */
1002 public static function get_post_stats() {
1003 global $wpdb;
1004 $cache_key = 'post_stats';
1005 $post_stats = wp_cache_get( $cache_key, 'friends' );
1006 if ( false !== $post_stats ) {
1007 return $post_stats;
1008 }
1009
1010 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1011 $post_stats = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1012 $wpdb->prepare(
1013 'SELECT SUM(
1014 LENGTH( ID ) +
1015 LENGTH( post_author ) +
1016 LENGTH( post_date ) +
1017 LENGTH( post_date_gmt ) +
1018 LENGTH( post_content ) +
1019 LENGTH( post_title ) +
1020 LENGTH( post_excerpt ) +
1021 LENGTH( post_status ) +
1022 LENGTH( comment_status ) +
1023 LENGTH( ping_status ) +
1024 LENGTH( post_password ) +
1025 LENGTH( post_name ) +
1026 LENGTH( to_ping ) +
1027 LENGTH( pinged ) +
1028 LENGTH( post_modified ) +
1029 LENGTH( post_modified_gmt ) +
1030 LENGTH( post_content_filtered ) +
1031 LENGTH( post_parent ) +
1032 LENGTH( guid ) +
1033 LENGTH( menu_order ) +
1034 LENGTH( post_type ) +
1035 LENGTH( post_mime_type ) +
1036 LENGTH( comment_count )
1037 ) AS total_size,
1038 COUNT(*) as post_count
1039 FROM ' . $wpdb->posts . ' WHERE post_type IN ( ' . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )',
1040 $post_types
1041 ),
1042 ARRAY_A
1043 );
1044 $post_stats['earliest_post_date'] = mysql2date(
1045 'U',
1046 $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1047 $wpdb->prepare(
1048 "SELECT MIN(post_date) FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ( " . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )',
1049 $post_types
1050 )
1051 )
1052 );
1053
1054 wp_cache_set( $cache_key, $post_stats, 'friends', HOUR_IN_SECONDS );
1055 return $post_stats;
1056 }
1057
1058 /**
1059 * Gets the main header data.
1060 *
1061 * @return array The main header data.
1062 */
1063 public function get_main_header_data() {
1064 $data = array(
1065 'description' => '',
1066 'post_count_by_post_format' => $this->get_post_count_by_post_format(),
1067 'post_count_by_post_status' => $this->get_post_count_by_post_status(),
1068 );
1069
1070 return $data;
1071 }
1072
1073 /**
1074 * Fix a bug in core where it outputs cached friend posts.
1075 *
1076 * @param array $qvs Query variables.
1077 * @return array
1078 */
1079 public function limit_post_format_request( $qvs ) {
1080 if ( isset( $qvs['post_type'] ) && ! is_admin() ) {
1081 if ( is_array( $qvs['post_type'] ) ) {
1082 $qvs['post_type'] = array_filter( $qvs['post_type'], 'is_post_type_viewable' );
1083 } elseif ( ! is_post_type_viewable( $qvs['post_type'] ) ) {
1084 unset( $qvs['post_type'] );
1085 }
1086 }
1087
1088 return $qvs;
1089 }
1090
1091 public function register_my_apps( $apps ) {
1092 $apps['friends'] = array(
1093 'name' => __( 'Friends', 'friends' ),
1094 'icon_url' => 'https://ps.w.org/friends/assets/icon-256x256.png',
1095 'url' => '/friends/',
1096 );
1097 return $apps;
1098 }
1099
1100 /**
1101 * Add a post_format filter to a \WP_Query.
1102 *
1103 * @param array $tax_query The tax query.
1104 * @param string|array $filter_by_post_format The filter by post format.
1105 *
1106 * @return array|null The tax query, if any.
1107 */
1108 public function wp_query_get_post_format_tax_query( $tax_query, $filter_by_post_format ) {
1109 if ( empty( $filter_by_post_format ) ) {
1110 return $tax_query;
1111 }
1112
1113 if ( ! is_array( $filter_by_post_format ) ) {
1114 $filter_by_post_format = array( $filter_by_post_format );
1115 }
1116
1117 $post_formats = get_post_format_slugs();
1118 $filter_by_post_format = array_filter(
1119 $filter_by_post_format,
1120 function ( $post_format ) use ( $post_formats ) {
1121 return in_array( $post_format, $post_formats, true );
1122 }
1123 );
1124 if ( empty( $filter_by_post_format ) ) {
1125 return $tax_query;
1126 }
1127
1128 if ( ! empty( $tax_query ) ) {
1129 $tax_query['relation'] = 'AND';
1130 }
1131 $post_format_query = array(
1132 'taxonomy' => 'post_format',
1133 'field' => 'slug',
1134 );
1135
1136 if ( in_array( 'standard', $filter_by_post_format, true ) ) {
1137 $post_format_query['operator'] = 'NOT IN';
1138 $post_format_query['terms'] = array_values(
1139 array_map(
1140 function ( $post_format ) {
1141 return 'post-format-' . $post_format;
1142 },
1143 array_diff( $post_formats, $filter_by_post_format )
1144 )
1145 );
1146 } else {
1147 $post_format_query['operator'] = 'IN';
1148 $post_format_query['terms'] = array_map(
1149 function ( $post_format ) {
1150 return 'post-format-' . $post_format;
1151 },
1152 $filter_by_post_format
1153 );
1154 }
1155 if ( ! empty( $tax_query ) ) {
1156 $tax_query[] = $post_format_query;
1157 } else {
1158 $tax_query = array( $post_format_query );
1159 }
1160
1161 return $tax_query;
1162 }
1163
1164 public function wp_query_get_post_tag_tax_query( $tax_query, $filter_by_post_tag ) {
1165 if ( empty( $filter_by_post_tag ) ) {
1166 return $tax_query;
1167 }
1168
1169 if ( ! is_array( $filter_by_post_tag ) ) {
1170 $filter_by_post_tag = array( $filter_by_post_tag );
1171 }
1172
1173 if ( ! empty( $tax_query ) ) {
1174 $tax_query['relation'] = 'AND';
1175 }
1176 $post_tag_query = array(
1177 'taxonomy' => self::TAG_TAXONOMY,
1178 'field' => 'slug',
1179 'operator' => 'IN',
1180 'terms' => $filter_by_post_tag,
1181 );
1182
1183 if ( ! empty( $tax_query ) ) {
1184 $tax_query[] = $post_tag_query;
1185 } else {
1186 $tax_query = array( $post_tag_query );
1187 }
1188
1189 return $tax_query;
1190 }
1191
1192
1193 /**
1194 * Plural texts for post formats.
1195 *
1196 * @param string $format The post format.
1197 * @param int $count The count.
1198 *
1199 * @return string The plural string.
1200 */
1201 public function get_post_format_plural_string( $format, $count ) {
1202 if ( ! in_array( $format, get_post_format_slugs(), true ) ) {
1203 $format = 'standard';
1204 }
1205
1206 $plurals = array(
1207 // translators: %s is a count.
1208 'standard' => _nx_noop( '%s post', '%s posts', 'Post format', 'friends' ), // Special case. Any value that evals to false will be considered standard.
1209 // translators: %s is a count.
1210 'aside' => _nx_noop( '%s aside', '%s asides', 'Post format', 'friends' ),
1211 // translators: %s is a count.
1212 'chat' => _nx_noop( '%s chat', '%s chats', 'Post format', 'friends' ),
1213 // translators: %s is a count.
1214 'gallery' => _nx_noop( '%s gallery', '%s galleries', 'Post format', 'friends' ),
1215 // translators: %s is a count.
1216 'link' => _nx_noop( '%s link', '%s links', 'Post format', 'friends' ),
1217 // translators: %s is a count.
1218 'image' => _nx_noop( '%s image', '%s images', 'Post format', 'friends' ),
1219 // translators: %s is a count.
1220 'quote' => _nx_noop( '%s quote', '%s quotes', 'Post format', 'friends' ),
1221 // translators: %s is a count.
1222 'status' => _nx_noop( '%s status', '%s statuses', 'Post format', 'friends' ),
1223 // translators: %s is a count.
1224 'video' => _nx_noop( '%s video', '%s videos', 'Post format', 'friends' ),
1225 // translators: %s is a count.
1226 'audio' => _nx_noop( '%s audio', '%s audios', 'Post format', 'friends' ),
1227 );
1228
1229 if ( ! is_numeric( $count ) ) {
1230 return sprintf( translate_nooped_plural( $plurals[ $format ], 0, 'friends' ), $count );
1231 }
1232
1233 return sprintf( translate_nooped_plural( $plurals[ $format ], $count, 'friends' ), number_format_i18n( $count ) );
1234 }
1235
1236 /**
1237 * Overwrite the must_log_in message.
1238 *
1239 * @param array $defaults The default strings.
1240 *
1241 * @return array The modified defaults.
1242 */
1243 public function comment_form_defaults( $defaults ) {
1244 $comment_registration_message = get_option( 'friends_comment_registration_message', __( 'Only people in my network can comment.', 'friends' ) );
1245 $comment_registration_message = str_replace( __( 'my network', 'friends' ), '<a href="' . esc_attr( home_url() . '/friends/' ) . '">' . __( 'my network', 'friends' ) . '</a>', $comment_registration_message );
1246 $defaults['must_log_in'] = $comment_registration_message;
1247 return $defaults;
1248 }
1249
1250 /**
1251 * Get all of the rel links for the HTML head.
1252 */
1253 public static function get_link_rels() {
1254 $links = array();
1255
1256 if ( get_option( 'friends_expose_post_format_feeds' ) && current_theme_supports( 'post-formats' ) ) {
1257 $links = array_merge( $links, self::get_html_link_rel_alternate_post_formats() );
1258 }
1259
1260 return $links;
1261 }
1262
1263 /**
1264 * Strip non-ascii bytes.
1265 *
1266 * @param string $text The text.
1267 *
1268 * @return string The text in ASCII only.
1269 */
1270 private static function strip_non_ascii( $text ) {
1271 $text = html_entity_decode( $text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
1272 $text = str_replace( '»', '>', $text );
1273 $text = strtr( $text, '"', '' );
1274 return filter_var( $text, FILTER_DEFAULT, FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_NO_ENCODE_QUOTES );
1275 }
1276
1277 /**
1278 * Get all of the rel links for the HTTP header.
1279 */
1280 public static function http_header() {
1281 foreach ( self::get_link_rels() as $link ) {
1282 $header = 'Link: <' . esc_url( $link['href'] ) . '>; rel="' . esc_attr( $link['rel'] ) . '"';
1283 if ( isset( $link['type'] ) ) {
1284 $header .= '; type="' . self::strip_non_ascii( $link['type'] ) . '"';
1285 }
1286 if ( isset( $link['title'] ) ) {
1287 $header .= '; title="' . self::strip_non_ascii( $link['title'] ) . '"';
1288 }
1289 header( $header, false );
1290 }
1291 }
1292
1293 /**
1294 * Get all of the rel links for the HTML head.
1295 */
1296 public static function get_html_rel_links() {
1297 return array_map(
1298 function ( $link ) {
1299 return '<link' .
1300 ' rel="' . esc_attr( $link['rel'] ) . '"' .
1301 ' href="' . esc_url( $link['href'] ) . '"' .
1302 ( isset( $link['type'] ) ? ( ' type="' . esc_attr( $link['type'] ) . '"' ) : '' ) .
1303 ( isset( $link['title'] ) ? ( ' title="' . esc_attr( $link['title'] ) . '"' ) : '' ) .
1304 ' />';
1305 },
1306 self::get_link_rels()
1307 );
1308 }
1309
1310 /**
1311 * Output all of the rel links for the HTML head.
1312 */
1313 public static function html_rel_links() {
1314 echo wp_kses(
1315 implode( PHP_EOL, self::get_html_rel_links() ),
1316 array(
1317 'link' => array(
1318 'rel' => array(),
1319 'type' => array(),
1320 'href' => array(),
1321 'title' => array(),
1322 ),
1323 )
1324 ), PHP_EOL;
1325 }
1326
1327 /**
1328 * Generate link tag(s) with the alternate post formats.
1329 */
1330 public static function get_html_link_rel_alternate_post_formats() {
1331 $separator = _x( '&raquo;', 'feed link' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1332 $blog_title = get_bloginfo( 'name' );
1333
1334 $links = array();
1335 foreach ( get_post_format_strings() as $format => $title ) {
1336 // translators: 1: Blog title, 2: Separator (raquo), 3: Post Format.
1337 $title = sprintf( __( '%1$s %2$s %3$s Feed', 'friends' ), $blog_title, $separator, $title );
1338 $links[] = array(
1339 'rel' => 'alternate',
1340 'type' => 'application/rss+xml',
1341 'href' => esc_url( home_url( '/type/' . $format . '/feed/' ) ),
1342 'title' => $title,
1343 );
1344 }
1345
1346 return $links;
1347 }
1348
1349 /**
1350 * Check whether this is a valid URL
1351 *
1352 * @param string $url The URL to check.
1353 * @return false|string URL or false on failure.
1354 */
1355 public static function check_url( $url ) {
1356 $pre = apply_filters( 'friends_pre_check_url', null, $url );
1357 if ( ! is_null( $pre ) ) {
1358 return $pre;
1359 }
1360 $host = wp_parse_url( $url, PHP_URL_HOST );
1361
1362 $check_url = apply_filters( 'friends_host_is_valid', null, $host );
1363 if ( ! is_null( $check_url ) ) {
1364 return $check_url;
1365 }
1366
1367 return wp_http_validate_url( $url );
1368 }
1369
1370 public static function url_truncate( $url, $max_length = 50 ) {
1371 $p = wp_parse_url( untrailingslashit( $url ) );
1372 $parts = array( $p['host'] );
1373 if ( trim( $p['path'] ) ) {
1374 $parts = array_merge( $parts, explode( '/', $p['path'] ) );
1375 }
1376
1377 $url = join( '/', array_filter( $parts ) );
1378 $reduce = 4;
1379 $url_length = strlen( $url );
1380 while ( $url_length > $max_length ) {
1381 $last_part = array_pop( $parts );
1382 $last_part = substr( $last_part, strlen( $last_part ) - $reduce );
1383 foreach ( $parts as $k => $part ) {
1384 $parts[ $k ] = substr( $part, 0, strlen( $part ) - $reduce );
1385 }
1386 $url = join( '../', array_filter( $parts ) ) . '../..' . $last_part;
1387 array_push( $parts, $last_part );
1388 $reduce = 1;
1389 $url_length = strlen( $url );
1390 }
1391
1392 return $url;
1393 }
1394
1395 /**
1396 * Cron function to delete old posts.
1397 */
1398 public function cron_friends_delete_outdated_posts() {
1399 foreach ( User_Feed::get_all_users() as $friend_user ) {
1400 $friend_user->delete_outdated_posts();
1401 }
1402 $this->delete_outdated_posts();
1403 $this->cleanup_orphaned_friend_tags();
1404 }
1405
1406 /**
1407 * Cron function to process post tag migration batches.
1408 * Ensures the Migration class is loaded before calling the batch method.
1409 */
1410 public function cron_migrate_post_tags_batch() {
1411 require_once __DIR__ . '/class-migration.php';
1412 Migration::migrate_post_tags_batch();
1413 }
1414
1415 /**
1416 * Cron function to process ActivityPub attributedTo migration batches.
1417 * Ensures the Migration class is loaded before calling the batch method.
1418 */
1419 public function cron_migrate_ap_attributed_to_batch() {
1420 require_once __DIR__ . '/class-migration.php';
1421 Migration::migrate_activitypub_attributed_to_batch();
1422 }
1423
1424 /**
1425 * Cron function to process linking AP feeds to actors batches.
1426 * Ensures the Migration class is loaded before calling the batch method.
1427 */
1428 public function cron_link_ap_feeds_batch() {
1429 require_once __DIR__ . '/class-migration.php';
1430 Migration::link_activitypub_feeds_to_actors_batch();
1431 }
1432
1433 /**
1434 * Cron function to process backfilling External user attributedTo batches.
1435 * Ensures the Migration class is loaded before calling the batch method.
1436 */
1437 public function cron_backfill_external_attributed_to_batch() {
1438 require_once __DIR__ . '/class-migration.php';
1439 Migration::backfill_external_attributed_to_batch();
1440 }
1441
1442 /**
1443 * Cron function to process converting reply posts to comments batches.
1444 * Ensures the Migration class is loaded before calling the batch method.
1445 */
1446 public function cron_convert_replies_batch() {
1447 require_once __DIR__ . '/class-migration.php';
1448 Migration::convert_replies_to_comments_batch();
1449 }
1450
1451 /**
1452 * Cron function to process converting friend WP users to virtual subscriptions.
1453 * Ensures the Migration class is loaded before calling the batch method.
1454 */
1455 public function cron_convert_friend_users_batch() {
1456 require_once __DIR__ . '/class-migration.php';
1457 Migration::convert_friend_users_batch();
1458 }
1459
1460 /**
1461 * Convert a single reply post to a comment.
1462 * This is scheduled as a single cron event when a new reply post is created.
1463 *
1464 * @param int $post_id The post ID to convert.
1465 */
1466 public function convert_single_reply_to_comment( $post_id ) {
1467 $post = get_post( $post_id );
1468 if ( ! $post || self::CPT !== $post->post_type ) {
1469 return;
1470 }
1471
1472 // Skip if already trashed (already processed).
1473 if ( 'trash' === $post->post_status ) {
1474 return;
1475 }
1476
1477 require_once __DIR__ . '/class-migration.php';
1478
1479 // Create a simple object for the process method.
1480 $post_obj = (object) array(
1481 'ID' => $post->ID,
1482 'guid' => $post->guid,
1483 'post_author' => $post->post_author,
1484 'post_content' => $post->post_content,
1485 'post_date_gmt' => $post->post_date_gmt,
1486 );
1487
1488 Migration::process_potential_reply_post( $post_obj );
1489 }
1490
1491 /**
1492 * Redirect trashed reply posts to their comment equivalents.
1493 * When a reply post has been converted to a comment and trashed,
1494 * visitors to the old post URL are redirected to the comment.
1495 */
1496 public function redirect_trashed_reply_to_comment() {
1497 // Only handle single posts.
1498 if ( ! is_singular( self::CPT ) ) {
1499 return;
1500 }
1501
1502 $post = get_queried_object();
1503 if ( ! $post || 'trash' !== $post->post_status ) {
1504 return;
1505 }
1506
1507 $comment_id = get_post_meta( $post->ID, '_redirects_to_comment', true );
1508 $redirect_post_id = get_post_meta( $post->ID, '_redirect_post_id', true );
1509
1510 if ( ! $comment_id || ! $redirect_post_id ) {
1511 return;
1512 }
1513
1514 $redirect_url = get_permalink( $redirect_post_id ) . '#comment-' . $comment_id;
1515 wp_safe_redirect( $redirect_url, 301 );
1516 exit;
1517 }
1518
1519 /**
1520 * Fallback redirect for reply posts that have been permanently deleted.
1521 * Looks up the comment by its original post GUID stored in comment meta.
1522 */
1523 public function fallback_redirect_via_comment_meta() {
1524 // Only on 404.
1525 if ( ! is_404() ) {
1526 return;
1527 }
1528
1529 // Get the requested URL.
1530 $requested_url = home_url( add_query_arg( array() ) );
1531
1532 // Search for a comment that has this URL as its original post GUID.
1533 global $wpdb;
1534
1535 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1536 $comment = $wpdb->get_row(
1537 $wpdb->prepare(
1538 "SELECT c.comment_ID, c.comment_post_ID
1539 FROM {$wpdb->comments} c
1540 INNER JOIN {$wpdb->commentmeta} cm ON c.comment_ID = cm.comment_id
1541 WHERE cm.meta_key = '_original_post_guid'
1542 AND cm.meta_value = %s
1543 LIMIT 1",
1544 $requested_url
1545 )
1546 );
1547
1548 if ( ! $comment ) {
1549 return;
1550 }
1551
1552 $redirect_url = get_permalink( $comment->comment_post_ID ) . '#comment-' . $comment->comment_ID;
1553 wp_safe_redirect( $redirect_url, 301 );
1554 exit;
1555 }
1556
1557 /**
1558 * Maybe delete an outdated post.
1559 *
1560 * @param int $post_id The post ID.
1561 * @param string $reason The reason for deletion.
1562 * @return int|false The post ID if it was deleted, false otherwise.
1563 */
1564 public static function maybe_delete_outdated_post( int $post_id, string $reason = '' ) {
1565 if ( get_post_type( $post_id ) !== Friends::CPT ) {
1566 return false;
1567 }
1568
1569 if ( ! get_option( 'friends_retention_delete_reacted' ) ) {
1570 // get all terms for a post, no matter whether it's registered or not.
1571 $term_query = new \WP_Term_Query(
1572 array(
1573 'object_ids' => $post_id,
1574 )
1575 );
1576 $reactions = array();
1577 foreach ( $term_query->get_terms() as $term ) {
1578 if ( substr( $term->taxonomy, 0, 16 ) !== 'friend-reaction-' ) {
1579 continue;
1580 }
1581 $reactions[ $term->slug ] = true;
1582
1583 }
1584 if ( $reactions ) {
1585 if ( apply_filters( 'friends_debug', false ) && ! wp_doing_cron() ) {
1586 echo 'Skipping ', esc_html( $post_id ), ' because it has reactions (';
1587 foreach ( array_keys( $reactions ) as $emoji ) {
1588 echo esc_html( Reactions::validate_emoji( $emoji ) ), ' ';
1589 }
1590 echo ')<br/>', PHP_EOL;
1591
1592 }
1593 return false;
1594 }
1595 }
1596
1597 if ( apply_filters( 'friends_debug', false ) && ! wp_doing_cron() ) {
1598 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;
1599 }
1600 wp_delete_post( $post_id, true );
1601
1602 return $post_id;
1603 }
1604
1605 /**
1606 * Delete posts the user decided to automatically delete.
1607 */
1608 public function delete_outdated_posts() {
1609 $deleted_posts = array();
1610
1611 $args = array(
1612 'post_type' => Friends::CPT,
1613 'post_status' => array( 'publish', 'trash' ),
1614 'fields' => 'ids',
1615 'posts_per_page' => 10,
1616 );
1617
1618 if ( get_option( 'friends_enable_retention_days' ) ) {
1619 $args['date_query'] = array(
1620 'before' => gmdate( 'Y-m-d H:i:s', strtotime( '-' . ( Friends::get_retention_days() * 24 ) . 'hours' ) ),
1621 );
1622
1623 $query = new \WP_Query();
1624 foreach ( $args as $key => $value ) {
1625 $query->set( $key, $value );
1626 }
1627
1628 foreach ( $query->get_posts() as $post_id ) {
1629 $post_id = self::maybe_delete_outdated_post( $post_id, 'date overflow' );
1630 if ( $post_id ) {
1631 $deleted_posts[] = $post_id;
1632 }
1633 }
1634 unset( $args['date_query'] );
1635 }
1636
1637 $args['orderby'] = 'date';
1638 $args['order'] = 'desc';
1639 if ( get_option( 'friends_enable_retention_number' ) ) {
1640 $args['offset'] = Friends::get_retention_number();
1641 $query = new \WP_Query();
1642 foreach ( $args as $key => $value ) {
1643 $query->set( $key, $value );
1644 }
1645
1646 foreach ( $query->get_posts() as $post_id ) {
1647 $post_id = self::maybe_delete_outdated_post( $post_id, 'global number overflow' );
1648 if ( $post_id ) {
1649 $deleted_posts[] = $post_id;
1650 }
1651 }
1652 }
1653
1654 // In any case, don't overflow the trash.
1655 $args = array(
1656 'post_type' => Friends::CPT,
1657 'post_status' => 'trash',
1658 'offset' => 100,
1659 'fields' => 'ids',
1660 'orderby' => 'date',
1661 'order' => 'asc',
1662 'posts_per_page' => 10,
1663 );
1664
1665 $query = new \WP_Query();
1666 foreach ( $args as $key => $value ) {
1667 $query->set( $key, $value );
1668 }
1669
1670 foreach ( $query->get_posts() as $post_id ) {
1671 $post_id = self::maybe_delete_outdated_post( $post_id, 'trash' );
1672 if ( $post_id ) {
1673 $deleted_posts[] = $post_id;
1674 }
1675 }
1676
1677 return $deleted_posts;
1678 }
1679
1680 /**
1681 * Clean up orphaned friend tags that have no posts.
1682 */
1683 public function cleanup_orphaned_friend_tags() {
1684 Friend_Tag::cleanup_orphaned();
1685 }
1686
1687 /**
1688 * Delete all the data the plugin has stored in WordPress
1689 */
1690 public static function uninstall_plugin() {
1691 $taxonomies = array(
1692 User_Feed::TAXONOMY,
1693 Subscription::TAXONOMY,
1694 self::TAG_TAXONOMY,
1695 );
1696
1697 $affected_users = new \WP_User_Query( array( 'role__in' => array( 'subscription' ) ) );
1698 foreach ( $affected_users as $user ) {
1699 $in_token = get_user_option( 'friends_in_token', $user->ID );
1700 delete_option( 'friends_in_token_' . $in_token );
1701 delete_user_option( $user->ID, 'friends_out_token' );
1702 delete_user_option( $user->ID, 'friends_in_token' );
1703 delete_user_option( $user->ID, 'friends_new_friend' );
1704 $taxonomies[] = 'friend-reaction-' . $user->ID;
1705 }
1706
1707 delete_option( 'friends_main_user_id' );
1708
1709 remove_role( 'subscription' );
1710
1711 $friend_posts = new \WP_Query(
1712 array(
1713 'post_type' => Friends::CPT,
1714 'post_status' => array( 'publish', 'private', 'trash' ),
1715 )
1716 );
1717
1718 while ( $friend_posts->have_posts() ) {
1719 $post = $friend_posts->next_post();
1720 wp_delete_post( $post->ID, true );
1721 }
1722
1723 // We have to resort to using direct database statements since the taxonomy is not registered because the plugin is deactivated.
1724 global $wpdb;
1725 foreach ( $taxonomies as $taxonomy ) {
1726 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
1727 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
1728 $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 ) );
1729
1730 if ( $terms ) {
1731 foreach ( $terms as $term ) {
1732 $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $term->term_taxonomy_id ) );
1733 $wpdb->delete( $wpdb->terms, array( 'term_id' => $term->term_id ) );
1734 delete_option( 'prefix_' . $term->slug . '_option_name' );
1735 }
1736 }
1737
1738 $wpdb->delete( $wpdb->term_taxonomy, array( 'taxonomy' => $taxonomy ), array( '%s' ) );
1739 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
1740 // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching
1741 }
1742 }
1743 }
1744