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

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

1,753 lines 49.5 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 Link_Preview object.
77 *
78 * @var Link_Preview
79 */
80 public $link_preview;
81
82 /**
83 * A reference to the REST object.
84 *
85 * @var REST
86 */
87 public $rest;
88
89 /**
90 * A reference to the Reactions object.
91 *
92 * @var Reactions
93 */
94 public $reactions;
95
96 /**
97 * Get the class singleton
98 *
99 * @return Friends A class instance.
100 */
101 public static function get_instance() {
102 static $instance;
103 if ( ! isset( $instance ) ) {
104 $self = get_called_class();
105 $instance = new $self();
106 }
107 return $instance;
108 }
109
110 /**
111 * Get the Template_Loader singleton
112 *
113 * @return Template_Loader A class instance.
114 */
115 public static function template_loader() {
116 static $template_loader;
117 if ( ! isset( $template_loader ) ) {
118 $template_loader = new Template_Loader();
119 }
120 return $template_loader;
121 }
122
123 /**
124 * Constructor
125 */
126 public function __construct() {
127 $this->admin = new Admin( $this );
128 $this->feed = new Feed( $this );
129 $this->messages = new Messages( $this );
130 $this->notifications = new Notifications( $this );
131 $this->frontend = new Frontend( $this );
132 $this->reactions = new Reactions( $this );
133 $this->rest = new REST( $this );
134 $this->abilities = new Abilities( $this );
135
136 $this->link_preview = new Link_Preview( $this );
137
138 new Third_Parties( $this );
139 new Blocks( $this );
140 new Logging( $this );
141 new Shortcodes( $this );
142 new Site_Health();
143 new Migration();
144 $this->register_hooks();
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 */
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 */
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 );
194 }
195
196 /**
197 * Register the WordPress hooks
198 */
199 private function register_hooks() {
200 add_action( 'init', array( $this, 'register_custom_post_type' ) );
201 add_action( 'init', array( $this, 'register_friend_tag_taxonomy' ) );
202 add_action( 'init', array( 'Friends\Subscription', 'register_taxonomy' ) );
203
204 add_action( 'init', array( 'Friends\User_Feed', 'register_taxonomy' ) );
205 add_action( 'wp', array( $this, 'allow_browser_extension_request' ) );
206 add_filter( 'get_avatar_data', array( $this, 'get_avatar_data' ), 10, 2 );
207
208 add_action( 'template_redirect', array( $this, 'http_header' ), 5 );
209 add_filter( 'wp_head', array( $this, 'html_rel_links' ) );
210 add_filter( 'login_head', array( $this, 'html_rel_links' ) );
211
212 add_filter( 'after_setup_theme', array( $this, 'enable_post_formats' ) );
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 );
224
225 add_action( 'comment_form_defaults', array( $this, 'comment_form_defaults' ) );
226 add_filter( 'friends_frontend_post_types', array( $this, 'add_frontend_post_types' ) );
227
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 }
232
233 /**
234 * Register the friend tag taxonomy
235 */
236 public function register_friend_tag_taxonomy() {
237 Friend_Tag::register();
238 }
239
240 /**
241 * Registers the custom post type
242 */
243 public function register_custom_post_type() {
244 $labels = array(
245 'name' => __( 'Friend Posts', 'friends' ),
246 'singular_name' => __( 'Friend Post', 'friends' ),
247 'add_new' => _x( 'Add New', 'cached friend post', 'friends' ),
248 'add_new_item' => __( 'Add New Friend Post', 'friends' ),
249 'edit_item' => __( 'Edit Friend Post', 'friends' ),
250 'new_item' => __( 'New Friend Post', 'friends' ),
251 'all_items' => __( 'All Friend Posts', 'friends' ),
252 'view_item' => __( 'View Friend Post', 'friends' ),
253 'search_items' => __( 'Search Friend Posts', 'friends' ),
254 'not_found' => __( 'No Friend Posts found', 'friends' ),
255 'not_found_in_trash' => __( 'No Friend Posts found in the Trash', 'friends' ),
256 'parent_item_colon' => '',
257 'menu_name' => __( 'Cached Friend Posts', 'friends' ),
258 );
259
260 $args = array(
261 'labels' => $labels,
262 'description' => "A cached friend's post",
263 'publicly_queryable' => is_admin() && self::is_main_user() && apply_filters( 'friends_show_cached_posts', false ),
264 'show_ui' => true,
265 'show_in_menu' => apply_filters( 'friends_show_cached_posts', false ),
266 'show_in_nav_menus' => false,
267 'show_in_admin_bar' => false,
268 'show_in_rest' => is_user_logged_in(),
269 'exclude_from_search' => true,
270 'public' => false,
271 'delete_with_user' => true,
272 'menu_position' => 5,
273 'menu_icon' => 'dashicons-groups',
274 'supports' => array( 'title', 'editor', 'author', 'revisions', 'thumbnail', 'excerpt', 'comments', 'post-formats' ),
275 'taxonomies' => array( self::TAG_TAXONOMY, 'post_format', 'friend-reaction-' . get_current_user_id() ),
276 'has_archive' => true,
277 'rewrite' => false,
278 );
279
280 register_post_type( self::CPT, $args );
281
282 register_post_meta(
283 self::CPT,
284 Feed::COMMENTS_FEED_META,
285 array(
286 'show_in_rest' => true,
287 'single' => true,
288 'type' => 'string',
289 )
290 );
291
292 register_post_meta(
293 self::CPT,
294 'remote_post_id',
295 array(
296 'show_in_rest' => true,
297 'single' => true,
298 'type' => 'string',
299 )
300 );
301
302 register_post_meta(
303 self::CPT,
304 'parser',
305 array(
306 'show_in_rest' => true,
307 'single' => true,
308 'type' => 'string',
309 )
310 );
311
312 register_post_meta(
313 self::CPT,
314 'feed_url',
315 array(
316 'show_in_rest' => true,
317 'single' => true,
318 'type' => 'string',
319 )
320 );
321
322 register_post_meta(
323 self::CPT,
324 'reblogged',
325 array(
326 'show_in_rest' => true,
327 'single' => true,
328 'type' => 'string',
329 )
330 );
331
332 register_post_meta(
333 self::CPT,
334 'reblogged_by',
335 array(
336 'show_in_rest' => true,
337 'single' => true,
338 'type' => 'integer',
339 )
340 );
341 }
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
352 public static function get_role_capabilities( $role ) {
353 $capabilities = array();
354
355 $capabilities['subscription'] = array(
356 'subscription' => true,
357 'friends_plugin' => true,
358 );
359
360 if ( ! isset( $capabilities[ $role ] ) ) {
361 return array();
362 }
363
364 return $capabilities[ $role ];
365 }
366
367 /**
368 * Create the Friend user roles
369 */
370 private static function setup_roles() {
371 $default_roles = array(
372 'subscription' => _x( 'Subscription', 'User role', 'friends' ),
373 );
374
375 $roles = new \WP_Roles();
376
377 foreach ( $default_roles as $type => $name ) {
378 $role = false;
379 foreach ( $roles->roles as $slug => $data ) {
380 if ( isset( $data['capabilities'][ $type ] ) ) {
381 $role = get_role( $slug );
382 break;
383 }
384 }
385 if ( ! $role ) {
386 $role = add_role( $type, $name, self::get_role_capabilities( $type ) );
387 continue;
388 }
389
390 // This might update missing capabilities.
391 foreach ( array_keys( self::get_role_capabilities( $type ) ) as $cap ) {
392 $role->add_cap( $cap );
393 }
394 }
395 }
396
397 /**
398 * Enable translated user roles.
399 * props https://wordpress.stackexchange.com/a/141705/74893
400 *
401 * @param string $translations The potentially translated text.
402 * @param string $text Text to translate.
403 * @param string $context Context information for the translators.
404 * @param string $domain Unique identifier for retrieving translated strings.
405 * @return string Translated text on success, original text on failure.
406 */
407 public static function translate_user_role( $translations, $text, $context, $domain ) {
408 $roles = array(
409 'Subscription',
410 );
411
412 if (
413 'User role' === $context
414 && in_array( $text, $roles, true )
415 && 'friends' !== $domain
416 ) {
417 // @codingStandardsIgnoreLine
418 return translate_with_gettext_context( $text, $context, 'friends' );
419 }
420
421 return $translations;
422 }
423
424 /**
425 * Gets the roles that the plugin uses.
426 *
427 * @return array The roles.
428 */
429 public static function get_friends_plugin_roles() {
430 return apply_filters( 'friends_plugin_roles', array( 'subscription' ) );
431 }
432
433 /**
434 * If a plugin version upgrade requires changes, they can be done here
435 *
436 * @param \WP_Upgrader $upgrader_object The WP_Upgrader instance.
437 * @param array $options Array of bulk item update data.
438 */
439 public static function upgrade_plugin_trigger( $upgrader_object, $options ) {
440 $upgraded = false;
441 if ( 'update' === $options['action'] && 'plugin' === $options['type'] && isset( $options['plugins'] ) ) {
442 foreach ( $options['plugins'] as $plugin ) {
443 if ( FRIENDS_PLUGIN_BASENAME === $plugin ) {
444 return self::upgrade_plugin();
445 }
446 }
447 }
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, '>=' ) ) {
470 return;
471 }
472
473 // Load migration class for any upgrade.
474 require_once __DIR__ . '/class-migration.php';
475
476 if ( version_compare( $previous_version, '0.20.1', '<' ) ) {
477 Migration::migrate_gravatar_to_user_icon_url();
478 }
479
480 if ( version_compare( $previous_version, '2.1.3', '<' ) ) {
481 self::setup_roles();
482 }
483
484 if ( version_compare( $previous_version, '2.6.0', '<' ) ) {
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 );
515 }
516 }
517 }
518
519 Migration::backfill_activitypub_attributed_to_acct();
520
521 update_option( 'friends_plugin_version', Friends::VERSION );
522 }
523
524 /**
525 * Actions to take upon plugin activation.
526 *
527 * @param bool $network_activate Whether the plugin has been activated network-wide.
528 */
529 public static function activate_plugin( $network_activate = null ) {
530 if ( function_exists( 'is_multisite' ) && is_multisite() ) {
531 if ( $network_activate ) {
532 // Only Super Admins can use Network Activate.
533 if ( ! is_super_admin() ) {
534 return;
535 }
536
537 // Activate for each site.
538 foreach ( get_sites() as $blog ) {
539 switch_to_blog( $blog->blog_id );
540 self::setup();
541 restore_current_blog();
542 }
543 } elseif ( current_user_can( 'activate_plugins' ) ) {
544 self::setup();
545 }
546 return;
547 }
548
549 self::setup();
550 }
551
552 /**
553 * Actions to take upon plugin activation.
554 *
555 * @param int|WP_Site $blog_id Blog ID.
556 */
557 public static function activate_for_blog( $blog_id ) {
558 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
559 require_once ABSPATH . 'wp-admin/includes/plugin.php';
560 }
561
562 if ( $blog_id instanceof \WP_Site ) {
563 $blog_id = (int) $blog_id->blog_id;
564 }
565
566 if ( is_plugin_active_for_network( FRIENDS_PLUGIN_BASENAME ) ) {
567 switch_to_blog( $blog_id );
568 self::setup();
569 restore_current_blog();
570 }
571 }
572
573 /**
574 * Actions to take upon plugin activation.
575 */
576 private static function setup() {
577 self::setup_roles();
578
579 self::upgrade_plugin(
580 null,
581 array(
582 'action' => 'update',
583 'plugins' => array( FRIENDS_PLUGIN_BASENAME ),
584 'type' => 'plugin',
585 )
586 );
587
588 if ( false === get_option( 'friends_main_user_id' ) ) {
589 update_option( 'friends_main_user_id', get_current_user_id() );
590 }
591
592 if ( false === get_option( 'friends_private_rss_key' ) ) {
593 update_option( 'friends_private_rss_key', wp_generate_password( 128, false ) );
594 }
595
596 if ( ! wp_next_scheduled( 'cron_friends_refresh_feeds' ) ) {
597 wp_schedule_event( time(), 'fifteen-minutes', 'cron_friends_refresh_feeds' );
598 }
599
600 if ( ! wp_next_scheduled( 'cron_friends_delete_old_posts' ) ) {
601 wp_schedule_event( time(), 'hourly', 'cron_friends_delete_old_posts' );
602 }
603
604 self::add_default_sidebars_widgets();
605 flush_rewrite_rules();
606 }
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
617 /**
618 * Add default widgets to the sidebars.
619 */
620 public static function add_default_sidebars_widgets() {
621 $sidebars_widgets = get_option( 'sidebars_widgets' );
622
623 if ( ! is_array( $sidebars_widgets ) ) {
624 $sidebars_widgets = array();
625 }
626 $id = count( $sidebars_widgets );
627
628 foreach ( array(
629 'friends-topbar' => array(
630 'friends-widget-header' => array(
631 'title' => 'Friends',
632 ),
633 'friends-widget-new-private-post' => array(),
634 ),
635 'friends-sidebar' => array(
636 'friends-widget-stats' => array(),
637 'friends-widget-refresh' => array(),
638 'friends-widget-post-formats' => array(),
639 'friends-widget-friend-list' => array(
640 'title' => 'Friends',
641 ),
642 'friends-widget-friend-request' => array(),
643 ),
644 ) as $sidebar_id => $default_widgets ) {
645 if ( ! empty( $sidebars_widgets[ $sidebar_id ] ) ) {
646 continue;
647 }
648 $sidebars_widgets[ $sidebar_id ] = array();
649 foreach ( $default_widgets as $widget_id => $options ) {
650 ++$id;
651 $sidebars_widgets[ $sidebar_id ][] = $widget_id . '-' . $id;
652 update_option( 'widget_' . $widget_id, array( $id => $options ) );
653 }
654 }
655
656 update_option( 'sidebars_widgets', $sidebars_widgets );
657 }
658
659 /**
660 * Actions to take upon plugin deactivation.
661 */
662 public static function deactivate_plugin() {
663 $timestamp = wp_next_scheduled( 'cron_friends_refresh_feeds' );
664 wp_unschedule_event( $timestamp, 'cron_friends_refresh_feeds' );
665 }
666
667 /**
668 * Get the required capability for the menu entries.
669 */
670 public static function required_menu_role() {
671 return 'edit_private_posts';
672 }
673
674 /**
675 * Check if the user hast the required priviliges.
676 */
677 public static function has_required_privileges() {
678 return self::is_main_user() || current_user_can( 'manage_options' );
679 }
680
681 /**
682 * Check if the current user is the main user.
683 */
684 public static function is_main_user() {
685 return is_user_logged_in() && get_current_user_id() === self::get_main_friend_user_id();
686 }
687
688 /**
689 * Get the main friend user id.
690 *
691 * @return int The user_id.
692 */
693 public static function get_main_friend_user_id() {
694 $main_user_id = get_option( 'friends_main_user_id' );
695
696 if (
697 false === $main_user_id
698 || ( is_multisite() && ! is_user_member_of_blog( $main_user_id, get_current_blog_id() ) )
699 ) {
700 $users = User_Query::all_admin_users();
701 foreach ( $users->get_results() as $user ) {
702 $main_user_id = $user->ID;
703 break;
704 }
705 if ( $main_user_id ) {
706 update_option( 'friends_main_user_id', $main_user_id );
707 }
708 }
709 return intval( $main_user_id );
710 }
711
712 /**
713 * Override Avatar URL for friends
714 *
715 * @param array $args The avatar details array.
716 * @param mixed $id_or_email Identifies what to retrieve the avatar for.
717 * @return array The avatar (potentially modified) details array.
718 */
719 public function get_avatar_data( $args, $id_or_email ) {
720 if ( is_object( $id_or_email ) ) {
721 if ( $id_or_email instanceof User ) {
722 $id_or_email = $id_or_email->ID;
723 } elseif ( $id_or_email instanceof \WP_Post ) {
724 $id_or_email = $id_or_email->post_author;
725 } elseif ( $id_or_email instanceof \WP_Comment ) {
726 $id_or_email = $id_or_email->user_id;
727 }
728 }
729
730 $user = false;
731 if ( is_numeric( $id_or_email ) && $id_or_email > 0 ) {
732 $user = get_user_by( 'ID', $id_or_email );
733 if ( $user ) {
734 $user = new User( $user );
735 }
736 } elseif ( is_string( $id_or_email ) ) {
737 $user = User::get_by_username( $id_or_email );
738 }
739
740 if ( $user ) {
741 if ( is_wp_error( $user ) ) {
742 return $args;
743 }
744
745 $url = $user->get_avatar_url();
746 if ( $url ) {
747 $args['url'] = $url;
748 $args['found_avatar'] = true;
749 }
750 }
751 return $args;
752 }
753
754 /**
755 * Enables the post formats.
756 */
757 public function enable_post_formats() {
758 if ( get_option( 'friends_force_enable_post_formats' ) ) {
759 add_theme_support( 'post-formats', get_post_format_slugs() );
760 }
761 }
762
763 /**
764 * Determine whether we are on the /friends/ page or a subpage.
765 *
766 * @return boolean Whether we are on a friends page URL.
767 */
768 public static function on_frontend() {
769 global $wp_query;
770
771 if ( ! isset( $wp_query ) || ! isset( $wp_query->query['pagename'] ) ) {
772 // The request has not yet been parsed but we need to know, so this snippet is from wp->parse_request().
773 list( $req_uri ) = explode( '?', remove_query_arg( '' ) );
774 $home_path = wp_parse_url( home_url(), PHP_URL_PATH );
775 $home_path_regex = '';
776 if ( is_string( $home_path ) && '' !== $home_path ) {
777 $home_path = trim( $home_path, '/' );
778 $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) );
779 }
780 $req_uri = trim( $req_uri, '/' );
781
782 if ( ! empty( $home_path_regex ) ) {
783 $req_uri = preg_replace( $home_path_regex, '', $req_uri );
784 $req_uri = trim( $req_uri, '/' );
785 }
786 $pagename = $req_uri;
787 } else {
788 $pagename = $wp_query->query['pagename'];
789 }
790
791 // A nonce is not needed here since this is to show the public page.
792 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
793 if ( isset( $_GET['public'] ) ) {
794 return false;
795 }
796
797 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() ) ) ) {
798 return false;
799 }
800
801 $pagename_parts = explode( '/', trim( $pagename, '/' ) );
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 }
813
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;
832 }
833
834 /**
835 * Adds the post types to be displayed on the frontend.
836 *
837 * @param string $post_types The incoming post types.
838 *
839 * @return array The frontend post types.
840 */
841 public static function add_frontend_post_types( $post_types ) {
842 return array_merge( array( Friends::CPT ), $post_types );
843 }
844
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 /**
890 * Gets the post count by post format.
891 *
892 * @param bool $force_fetching Whether to force fetching.
893 *
894 * @return array The post count by post format.
895 */
896 public function get_post_count_by_post_format( $force_fetching = false ) {
897 $cache_key = 'post_count_by_post_format';
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
906 $counts = get_transient( $cache_key );
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 }
926
927 $counts = array();
928
929 global $wpdb;
930
931 $counts = array();
932
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
940
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;
960 }
961 }
962 }
963 $counts['standard'] = $post_count->publish + $post_count->private;
964
965 foreach ( $post_format_counts as $row ) {
966 $counts[ str_replace( 'post-format-', '', $row->post_format ) ] = $row->count;
967 $counts['standard'] -= $row->count;
968 }
969
970 $counts = array_filter( $counts );
971
972 set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
973 wp_cache_set( $cache_key, $counts, 'friends', HOUR_IN_SECONDS );
974
975 return $counts;
976 }
977
978 /**
979 * Get the retention by number of posts.
980 *
981 * @return int The retention by number of posts.
982 */
983 public static function get_retention_number() {
984 $number = get_option( 'friends_retention_number' );
985 if ( $number <= 0 ) {
986 return 2000;
987 }
988
989 return $number;
990 }
991
992 /**
993 * Get the retention by days of posts.
994 *
995 * @return int The retention by days of posts.
996 */
997 public static function get_retention_days() {
998 $days = get_option( 'friends_retention_days' );
999 if ( $days <= 0 ) {
1000 return 30;
1001 }
1002
1003 return $days;
1004 }
1005
1006 /**
1007 * Gets the post stats.
1008 *
1009 * @return object The post stats.
1010 */
1011 public static function get_post_stats() {
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
1019 $post_types = apply_filters( 'friends_frontend_post_types', array() );
1020 $post_stats = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1021 $wpdb->prepare(
1022 'SELECT SUM(
1023 LENGTH( ID ) +
1024 LENGTH( post_author ) +
1025 LENGTH( post_date ) +
1026 LENGTH( post_date_gmt ) +
1027 LENGTH( post_content ) +
1028 LENGTH( post_title ) +
1029 LENGTH( post_excerpt ) +
1030 LENGTH( post_status ) +
1031 LENGTH( comment_status ) +
1032 LENGTH( ping_status ) +
1033 LENGTH( post_password ) +
1034 LENGTH( post_name ) +
1035 LENGTH( to_ping ) +
1036 LENGTH( pinged ) +
1037 LENGTH( post_modified ) +
1038 LENGTH( post_modified_gmt ) +
1039 LENGTH( post_content_filtered ) +
1040 LENGTH( post_parent ) +
1041 LENGTH( guid ) +
1042 LENGTH( menu_order ) +
1043 LENGTH( post_type ) +
1044 LENGTH( post_mime_type ) +
1045 LENGTH( comment_count )
1046 ) AS total_size,
1047 COUNT(*) as post_count
1048 FROM ' . $wpdb->posts . ' WHERE post_type IN ( ' . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )',
1049 $post_types
1050 ),
1051 ARRAY_A
1052 );
1053 $post_stats['earliest_post_date'] = mysql2date(
1054 'U',
1055 $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1056 $wpdb->prepare(
1057 "SELECT MIN(post_date) FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ( " . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )',
1058 $post_types
1059 )
1060 )
1061 );
1062
1063 wp_cache_set( $cache_key, $post_stats, 'friends', HOUR_IN_SECONDS );
1064 return $post_stats;
1065 }
1066
1067 /**
1068 * Gets the main header data.
1069 *
1070 * @return array The main header data.
1071 */
1072 public function get_main_header_data() {
1073 $data = array(
1074 'description' => '',
1075 'post_count_by_post_format' => $this->get_post_count_by_post_format(),
1076 'post_count_by_post_status' => $this->get_post_count_by_post_status(),
1077 );
1078
1079 return $data;
1080 }
1081
1082 /**
1083 * Fix a bug in core where it outputs cached friend posts.
1084 *
1085 * @param array $qvs Query variables.
1086 * @return array
1087 */
1088 public function limit_post_format_request( $qvs ) {
1089 if ( isset( $qvs['post_type'] ) && ! is_admin() ) {
1090 if ( is_array( $qvs['post_type'] ) ) {
1091 $qvs['post_type'] = array_filter( $qvs['post_type'], 'is_post_type_viewable' );
1092 } elseif ( ! is_post_type_viewable( $qvs['post_type'] ) ) {
1093 unset( $qvs['post_type'] );
1094 }
1095 }
1096
1097 return $qvs;
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
1109 /**
1110 * Add a post_format filter to a \WP_Query.
1111 *
1112 * @param array $tax_query The tax query.
1113 * @param string|array $filter_by_post_format The filter by post format.
1114 *
1115 * @return array|null The tax query, if any.
1116 */
1117 public function wp_query_get_post_format_tax_query( $tax_query, $filter_by_post_format ) {
1118 if ( empty( $filter_by_post_format ) ) {
1119 return $tax_query;
1120 }
1121
1122 if ( ! is_array( $filter_by_post_format ) ) {
1123 $filter_by_post_format = array( $filter_by_post_format );
1124 }
1125
1126 $post_formats = get_post_format_slugs();
1127 $filter_by_post_format = array_filter(
1128 $filter_by_post_format,
1129 function ( $post_format ) use ( $post_formats ) {
1130 return in_array( $post_format, $post_formats, true );
1131 }
1132 );
1133 if ( empty( $filter_by_post_format ) ) {
1134 return $tax_query;
1135 }
1136
1137 if ( ! empty( $tax_query ) ) {
1138 $tax_query['relation'] = 'AND';
1139 }
1140 $post_format_query = array(
1141 'taxonomy' => 'post_format',
1142 'field' => 'slug',
1143 );
1144
1145 if ( in_array( 'standard', $filter_by_post_format, true ) ) {
1146 $post_format_query['operator'] = 'NOT IN';
1147 $post_format_query['terms'] = array_values(
1148 array_map(
1149 function ( $post_format ) {
1150 return 'post-format-' . $post_format;
1151 },
1152 array_diff( $post_formats, $filter_by_post_format )
1153 )
1154 );
1155 } else {
1156 $post_format_query['operator'] = 'IN';
1157 $post_format_query['terms'] = array_map(
1158 function ( $post_format ) {
1159 return 'post-format-' . $post_format;
1160 },
1161 $filter_by_post_format
1162 );
1163 }
1164 if ( ! empty( $tax_query ) ) {
1165 $tax_query[] = $post_format_query;
1166 } else {
1167 $tax_query = array( $post_format_query );
1168 }
1169
1170 return $tax_query;
1171 }
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
1202 /**
1203 * Plural texts for post formats.
1204 *
1205 * @param string $format The post format.
1206 * @param int $count The count.
1207 *
1208 * @return string The plural string.
1209 */
1210 public function get_post_format_plural_string( $format, $count ) {
1211 if ( ! in_array( $format, get_post_format_slugs(), true ) ) {
1212 $format = 'standard';
1213 }
1214
1215 $plurals = array(
1216 // translators: %s is a count.
1217 'standard' => _nx_noop( '%s post', '%s posts', 'Post format', 'friends' ), // Special case. Any value that evals to false will be considered standard.
1218 // translators: %s is a count.
1219 'aside' => _nx_noop( '%s aside', '%s asides', 'Post format', 'friends' ),
1220 // translators: %s is a count.
1221 'chat' => _nx_noop( '%s chat', '%s chats', 'Post format', 'friends' ),
1222 // translators: %s is a count.
1223 'gallery' => _nx_noop( '%s gallery', '%s galleries', 'Post format', 'friends' ),
1224 // translators: %s is a count.
1225 'link' => _nx_noop( '%s link', '%s links', 'Post format', 'friends' ),
1226 // translators: %s is a count.
1227 'image' => _nx_noop( '%s image', '%s images', 'Post format', 'friends' ),
1228 // translators: %s is a count.
1229 'quote' => _nx_noop( '%s quote', '%s quotes', 'Post format', 'friends' ),
1230 // translators: %s is a count.
1231 'status' => _nx_noop( '%s status', '%s statuses', 'Post format', 'friends' ),
1232 // translators: %s is a count.
1233 'video' => _nx_noop( '%s video', '%s videos', 'Post format', 'friends' ),
1234 // translators: %s is a count.
1235 'audio' => _nx_noop( '%s audio', '%s audios', 'Post format', 'friends' ),
1236 );
1237
1238 if ( ! is_numeric( $count ) ) {
1239 return sprintf( translate_nooped_plural( $plurals[ $format ], 0, 'friends' ), $count );
1240 }
1241
1242 return sprintf( translate_nooped_plural( $plurals[ $format ], $count, 'friends' ), number_format_i18n( $count ) );
1243 }
1244
1245 /**
1246 * Overwrite the must_log_in message.
1247 *
1248 * @param array $defaults The default strings.
1249 *
1250 * @return array The modified defaults.
1251 */
1252 public function comment_form_defaults( $defaults ) {
1253 $comment_registration_message = get_option( 'friends_comment_registration_message', __( 'Only people in my network can comment.', 'friends' ) );
1254 $comment_registration_message = str_replace( __( 'my network', 'friends' ), '<a href="' . esc_attr( home_url() . '/friends/' ) . '">' . __( 'my network', 'friends' ) . '</a>', $comment_registration_message );
1255 $defaults['must_log_in'] = $comment_registration_message;
1256 return $defaults;
1257 }
1258
1259 /**
1260 * Get all of the rel links for the HTML head.
1261 */
1262 public static function get_link_rels() {
1263 $links = array();
1264
1265 if ( get_option( 'friends_expose_post_format_feeds' ) && current_theme_supports( 'post-formats' ) ) {
1266 $links = array_merge( $links, self::get_html_link_rel_alternate_post_formats() );
1267 }
1268
1269 return $links;
1270 }
1271
1272 /**
1273 * Strip non-ascii bytes.
1274 *
1275 * @param string $text The text.
1276 *
1277 * @return string The text in ASCII only.
1278 */
1279 private static function strip_non_ascii( $text ) {
1280 $text = html_entity_decode( $text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
1281 $text = str_replace( '»', '>', $text );
1282 $text = strtr( $text, '"', '' );
1283 return filter_var( $text, FILTER_DEFAULT, FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_NO_ENCODE_QUOTES );
1284 }
1285
1286 /**
1287 * Get all of the rel links for the HTTP header.
1288 */
1289 public static function http_header() {
1290 foreach ( self::get_link_rels() as $link ) {
1291 $header = 'Link: <' . esc_url( $link['href'] ) . '>; rel="' . esc_attr( $link['rel'] ) . '"';
1292 if ( isset( $link['type'] ) ) {
1293 $header .= '; type="' . self::strip_non_ascii( $link['type'] ) . '"';
1294 }
1295 if ( isset( $link['title'] ) ) {
1296 $header .= '; title="' . self::strip_non_ascii( $link['title'] ) . '"';
1297 }
1298 header( $header, false );
1299 }
1300 }
1301
1302 /**
1303 * Get all of the rel links for the HTML head.
1304 */
1305 public static function get_html_rel_links() {
1306 return array_map(
1307 function ( $link ) {
1308 return '<link' .
1309 ' rel="' . esc_attr( $link['rel'] ) . '"' .
1310 ' href="' . esc_url( $link['href'] ) . '"' .
1311 ( isset( $link['type'] ) ? ( ' type="' . esc_attr( $link['type'] ) . '"' ) : '' ) .
1312 ( isset( $link['title'] ) ? ( ' title="' . esc_attr( $link['title'] ) . '"' ) : '' ) .
1313 ' />';
1314 },
1315 self::get_link_rels()
1316 );
1317 }
1318
1319 /**
1320 * Output all of the rel links for the HTML head.
1321 */
1322 public static function html_rel_links() {
1323 echo wp_kses(
1324 implode( PHP_EOL, self::get_html_rel_links() ),
1325 array(
1326 'link' => array(
1327 'rel' => array(),
1328 'type' => array(),
1329 'href' => array(),
1330 'title' => array(),
1331 ),
1332 )
1333 ), PHP_EOL;
1334 }
1335
1336 /**
1337 * Generate link tag(s) with the alternate post formats.
1338 */
1339 public static function get_html_link_rel_alternate_post_formats() {
1340 $separator = _x( '&raquo;', 'feed link' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1341 $blog_title = get_bloginfo( 'name' );
1342
1343 $links = array();
1344 foreach ( get_post_format_strings() as $format => $title ) {
1345 // translators: 1: Blog title, 2: Separator (raquo), 3: Post Format.
1346 $title = sprintf( __( '%1$s %2$s %3$s Feed', 'friends' ), $blog_title, $separator, $title );
1347 $links[] = array(
1348 'rel' => 'alternate',
1349 'type' => 'application/rss+xml',
1350 'href' => esc_url( home_url( '/type/' . $format . '/feed/' ) ),
1351 'title' => $title,
1352 );
1353 }
1354
1355 return $links;
1356 }
1357
1358 /**
1359 * Check whether this is a valid URL
1360 *
1361 * @param string $url The URL to check.
1362 * @return false|string URL or false on failure.
1363 */
1364 public static function check_url( $url ) {
1365 $pre = apply_filters( 'friends_pre_check_url', null, $url );
1366 if ( ! is_null( $pre ) ) {
1367 return $pre;
1368 }
1369 $host = wp_parse_url( $url, PHP_URL_HOST );
1370
1371 $check_url = apply_filters( 'friends_host_is_valid', null, $host );
1372 if ( ! is_null( $check_url ) ) {
1373 return $check_url;
1374 }
1375
1376 return wp_http_validate_url( $url );
1377 }
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
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 /**
1697 * Delete all the data the plugin has stored in WordPress
1698 */
1699 public static function uninstall_plugin() {
1700 $taxonomies = array(
1701 User_Feed::TAXONOMY,
1702 Subscription::TAXONOMY,
1703 self::TAG_TAXONOMY,
1704 );
1705
1706 $affected_users = new \WP_User_Query( array( 'role__in' => array( 'subscription' ) ) );
1707 foreach ( $affected_users as $user ) {
1708 $in_token = get_user_option( 'friends_in_token', $user->ID );
1709 delete_option( 'friends_in_token_' . $in_token );
1710 delete_user_option( $user->ID, 'friends_out_token' );
1711 delete_user_option( $user->ID, 'friends_in_token' );
1712 delete_user_option( $user->ID, 'friends_new_friend' );
1713 $taxonomies[] = 'friend-reaction-' . $user->ID;
1714 }
1715
1716 delete_option( 'friends_main_user_id' );
1717
1718 remove_role( 'subscription' );
1719
1720 $friend_posts = new \WP_Query(
1721 array(
1722 'post_type' => Friends::CPT,
1723 'post_status' => array( 'publish', 'private', 'trash' ),
1724 )
1725 );
1726
1727 while ( $friend_posts->have_posts() ) {
1728 $post = $friend_posts->next_post();
1729 wp_delete_post( $post->ID, true );
1730 }
1731
1732 // We have to resort to using direct database statements since the taxonomy is not registered because the plugin is deactivated.
1733 global $wpdb;
1734 foreach ( $taxonomies as $taxonomy ) {
1735 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
1736 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
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 ) );
1738
1739 if ( $terms ) {
1740 foreach ( $terms as $term ) {
1741 $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $term->term_taxonomy_id ) );
1742 $wpdb->delete( $wpdb->terms, array( 'term_id' => $term->term_id ) );
1743 delete_option( 'prefix_' . $term->slug . '_option_name' );
1744 }
1745 }
1746
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
1750 }
1751 }
1752 }
1753