PluginProbe
Friends / 4.3.0
Friends v4.3.0
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-site-health.php

class-site-health.php in Friends 4.3.0, at includes/class-site-health.php

601 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Site Health Integration for Friends Plugin
4 *
5 * This class handles all Site Health related functionality including
6 * migration status checks and post tag cleanup diagnostics.
7 *
8 * @package Friends
9 */
10
11 namespace Friends;
12
13 /**
14 * Site Health integration class
15 */
16 class Site_Health {
17
18 /**
19 * Constructor
20 */
21 public function __construct() {
22 add_filter( 'site_status_tests', array( $this, 'add_tests' ) );
23 add_filter( 'debug_information', array( $this, 'site_health_debug' ) );
24 add_action( 'wp_ajax_friends_restart_migration', array( $this, 'ajax_restart_migration' ) );
25 add_action( 'wp_ajax_friends_cleanup_post_tags', array( $this, 'ajax_cleanup_post_tags' ) );
26 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
27 }
28
29 /**
30 * Add Site Health tests.
31 *
32 * @param array $tests Existing tests.
33 * @return array Modified tests.
34 */
35 public function add_tests( $tests ) {
36 $tests['direct']['friends_migration'] = array(
37 'label' => __( 'Friends Post Tag Migration', 'friends' ),
38 'test' => array( $this, 'test_migration' ),
39 );
40
41 $tests['direct']['friends_orphaned_post_tags'] = array(
42 'label' => __( 'Friends Orphaned Post Tags', 'friends' ),
43 'test' => array( $this, 'test_post_tag_cleanup' ),
44 );
45
46 $tests['direct']['friends-roles'] = array(
47 'label' => __( 'Friend roles were created', 'friends' ),
48 'test' => array( $this, 'friend_roles_test' ),
49 );
50
51 $tests['direct']['friends-cron'] = array(
52 'label' => __( 'Friends cron job is enabled', 'friends' ),
53 'test' => array( $this, 'friends_cron_test' ),
54 );
55
56 $tests['direct']['friends-delete-cron'] = array(
57 'label' => __( 'Friends delete old posts cron job is enabled', 'friends' ),
58 'test' => array( $this, 'friends_cron_delete_test' ),
59 );
60
61 return $tests;
62 }
63
64 /**
65 * Site Health test for post tag migration status.
66 *
67 * @return array Site Health test result.
68 */
69 public function test_migration() {
70 require_once __DIR__ . '/class-migration.php';
71 $status = Migration::get_migration_status();
72
73 if ( $status['completed'] ) {
74 $result = array(
75 'label' => __( 'Post tag migration completed', 'friends' ),
76 'status' => 'good',
77 'badge' => array(
78 'label' => __( 'Friends', 'friends' ),
79 'color' => 'green',
80 ),
81 'description' => sprintf(
82 '<p>%s</p>',
83 $status['completed_time'] ? sprintf(
84 /* translators: %s: completion time */
85 __( 'The post tag migration was completed on %s.', 'friends' ),
86 $status['completed_time']
87 ) : __( 'The post tag migration has been completed.', 'friends' )
88 ),
89 'test' => 'friends_migration',
90 );
91 } elseif ( $status['in_progress'] ) {
92 $result = array(
93 'label' => __( 'Post tag migration in progress', 'friends' ),
94 'status' => 'recommended',
95 'badge' => array(
96 'label' => __( 'Friends', 'friends' ),
97 'color' => 'orange',
98 ),
99 'description' => sprintf(
100 '<p>%s</p><p>%s</p>',
101 sprintf(
102 /* translators: %1$d: migrated count, %2$d: total count */
103 __( 'Migration progress: %1$d/%2$d posts processed.', 'friends' ),
104 isset( $status['migrated'] ) ? $status['migrated'] : 0,
105 isset( $status['total'] ) ? $status['total'] : 0
106 ),
107 __( 'The migration will continue automatically in the background.', 'friends' )
108 ),
109 'test' => 'friends_migration',
110 );
111 } else {
112 // No migration status options are set, check the database to see current state
113 global $wpdb;
114 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
115 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
116 $friends_posts_with_post_tags = $wpdb->get_var(
117 $wpdb->prepare(
118 "SELECT COUNT(DISTINCT p.ID)
119 FROM {$wpdb->posts} p
120 INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
121 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
122 WHERE p.post_type = %s
123 AND tt.taxonomy = 'post_tag'
124 AND p.post_status IN ('publish', 'private')",
125 Friends::CPT
126 )
127 );
128 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
129 // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching
130
131 if ( $friends_posts_with_post_tags > 0 ) {
132 $result = array(
133 'label' => __( 'Friends posts need tag migration', 'friends' ),
134 'status' => 'recommended',
135 'badge' => array(
136 'label' => __( 'Friends', 'friends' ),
137 'color' => 'orange',
138 ),
139 'description' => sprintf(
140 '<p>%s</p><p><button type="button" class="button button-primary" onclick="friendsRestartMigration(this)">%s</button></p>',
141 sprintf(
142 /* translators: %d: number of posts */
143 _n(
144 '%d Friends post is still using the post_tag taxonomy. A migration to the friend_tag taxonomy is recommended for better organization.',
145 '%d Friends posts are still using the post_tag taxonomy. A migration to the friend_tag taxonomy is recommended for better organization.',
146 $friends_posts_with_post_tags,
147 'friends'
148 ),
149 $friends_posts_with_post_tags
150 ),
151 __( 'Start Migration', 'friends' )
152 ),
153 'test' => 'friends_migration',
154 );
155 } else {
156 $result = array(
157 'label' => __( 'No Friends tag migration needed', 'friends' ),
158 'status' => 'good',
159 'badge' => array(
160 'label' => __( 'Friends', 'friends' ),
161 'color' => 'green',
162 ),
163 'description' => sprintf(
164 '<p>%s</p>',
165 __( 'No Friends posts are using the post_tag taxonomy. All Friends posts are properly using the friend_tag taxonomy.', 'friends' )
166 ),
167 'test' => 'friends_migration',
168 );
169 }
170 }
171
172 return $result;
173 }
174
175 /**
176 * Site Health test for orphaned post tags cleanup.
177 *
178 * @return array Site Health test result.
179 */
180 public function test_post_tag_cleanup() {
181 require_once __DIR__ . '/class-migration.php';
182
183 global $wpdb;
184 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
185 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
186
187 // Count ALL post_tag terms that have no actual post associations from supported post types.
188
189 // Get post types that support the post_tag taxonomy
190 $post_types = get_post_types_by_support( array(), 'and' );
191 $post_tag_post_types = array();
192 foreach ( $post_types as $post_type ) {
193 if ( is_object_in_taxonomy( $post_type, 'post_tag' ) ) {
194 $post_tag_post_types[] = $post_type;
195 }
196 }
197
198 if ( empty( $post_tag_post_types ) ) {
199 $orphaned_tags = $wpdb->get_results(
200 "SELECT t.term_id, t.name, t.slug
201 FROM {$wpdb->terms} t
202 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
203 WHERE tt.taxonomy = 'post_tag'
204 ORDER BY t.name ASC"
205 );
206 } else {
207 $placeholders = implode( ',', array_fill( 0, count( $post_tag_post_types ), '%s' ) );
208 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
209 // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
210 $orphaned_tags = $wpdb->get_results(
211 $wpdb->prepare(
212 "SELECT t.term_id, t.name, t.slug
213 FROM {$wpdb->terms} t
214 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
215 LEFT JOIN (
216 SELECT DISTINCT tt2.term_id
217 FROM {$wpdb->term_relationships} tr
218 INNER JOIN {$wpdb->term_taxonomy} tt2 ON tr.term_taxonomy_id = tt2.term_taxonomy_id
219 INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
220 WHERE tt2.taxonomy = 'post_tag'
221 AND p.post_status IN ('publish', 'private')
222 AND p.post_type IN ($placeholders)
223 ) used_terms ON t.term_id = used_terms.term_id
224 WHERE tt.taxonomy = 'post_tag' AND used_terms.term_id IS NULL
225 ORDER BY t.name ASC",
226 $post_tag_post_types
227 )
228 );
229 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
230 // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
231 }
232
233 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
234 // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching
235
236 $orphaned_count = count( $orphaned_tags );
237
238 if ( $orphaned_count > 0 ) {
239 $description = sprintf(
240 /* translators: %d: number of orphaned tags */
241 _n(
242 '%d orphaned post_tag term was found with no associated posts. These tags may have been created by Friends posts before migration or other sources and can be safely removed to keep your tag cloud clean.',
243 '%d orphaned post_tag terms were found with no associated posts. These tags may have been created by Friends posts before migration or other sources and can be safely removed to keep your tag cloud clean.',
244 $orphaned_count,
245 'friends'
246 ),
247 $orphaned_count
248 );
249
250 $tag_list_limit = 15;
251 if ( $orphaned_count <= $tag_list_limit ) {
252 $tag_names = array_map(
253 function ( $tag ) {
254 return '<code>' . esc_html( $tag->name ) . '</code>';
255 },
256 $orphaned_tags
257 );
258 $description .= '<br><br><strong>' . __( 'Tags to be deleted:', 'friends' ) . '</strong> ' . implode( ', ', $tag_names );
259 } else {
260 $shown_tags = array_slice( $orphaned_tags, 0, $tag_list_limit );
261 $tag_names = array_map(
262 function ( $tag ) {
263 return '<code>' . esc_html( $tag->name ) . '</code>';
264 },
265 $shown_tags
266 );
267 $remaining = $orphaned_count - $tag_list_limit;
268 $description .= '<br><br><strong>' . __( 'Tags to be deleted:', 'friends' ) . '</strong> ' . implode( ', ', $tag_names ) . sprintf(
269 /* translators: %d: number of additional tags */
270 _n( ' and %d more', ' and %d more', $remaining, 'friends' ),
271 $remaining
272 );
273 }
274
275 $result = array(
276 'label' => __( 'Orphaned post tags found', 'friends' ),
277 'status' => 'recommended',
278 'badge' => array(
279 'label' => __( 'Friends', 'friends' ),
280 'color' => 'orange',
281 ),
282 'description' => sprintf(
283 '<p>%s</p><p><button type="button" class="button button-primary" onclick="friendsCleanupPostTags(this)">%s</button></p>',
284 $description,
285 __( 'Clean Up Orphaned Tags', 'friends' )
286 ),
287 'test' => 'friends_orphaned_post_tags',
288 );
289 } else {
290 $result = array(
291 'label' => __( 'No orphaned post tags found', 'friends' ),
292 'status' => 'good',
293 'badge' => array(
294 'label' => __( 'Friends', 'friends' ),
295 'color' => 'green',
296 ),
297 'description' => sprintf(
298 '<p>%s</p>',
299 __( 'All post_tag terms have associated posts. Your tag system is clean.', 'friends' )
300 ),
301 'test' => 'friends_orphaned_post_tags',
302 );
303 }
304
305 return $result;
306 }
307
308 /**
309 * Get missing Friends plugin roles.
310 *
311 * @return array Missing role names.
312 */
313 public function get_missing_friends_plugin_roles() {
314 $missing = Friends::get_friends_plugin_roles();
315 $roles = new \WP_Roles();
316 foreach ( $roles->roles as $role => $data ) {
317 if ( isset( $data['capabilities']['friends_plugin'] ) ) {
318 foreach ( $missing as $k => $cap ) {
319 if ( isset( $data['capabilities'][ $cap ] ) ) {
320 unset( $missing[ $k ] );
321 break;
322 }
323 }
324 }
325 }
326
327 return array_values( $missing );
328 }
329
330 /**
331 * Site Health test for friend roles.
332 *
333 * @return array Site Health test result.
334 */
335 public function friend_roles_test() {
336 $result = array(
337 'label' => __( 'The friend roles have been installed', 'friends' ),
338 'status' => 'good',
339 'badge' => array(
340 'label' => __( 'Friends', 'friends' ),
341 'color' => 'green',
342 ),
343 'description' =>
344 '<p>' .
345 __( 'The Friends Plugin uses users and user roles to determine friendship status between sites.', 'friends' ) .
346 '</p>' .
347 '<p>' .
348 sprintf(
349 /* translators: %s is a list of roles. */
350 __( 'These are the roles required for the friends plugin: %s', 'friends' ),
351 implode( ', ', Friends::get_friends_plugin_roles() )
352 ) .
353 '</p>',
354 'test' => 'friends-roles',
355 );
356
357 $missing_friend_roles = $this->get_missing_friends_plugin_roles();
358 if ( ! empty( $missing_friend_roles ) ) {
359
360 $result['label'] = sprintf(
361 /* translators: %s is a list of missing roles. */
362 __( 'Not all friend roles have been installed. Missing: %s', 'friends' ),
363 implode( ', ', $missing_friend_roles )
364 );
365 $result['badge']['color'] = 'red';
366 $result['status'] = 'critical';
367 $result['description'] .= '<p>';
368 $result['description'] .= wp_kses_post(
369 sprintf(
370 /* translators: %s is a URL. */
371 __( '<strong>To fix this:</strong> <a href="%s">Re-run activation of the Friends plugin</a>.', 'friends' ),
372 esc_url( wp_nonce_url( add_query_arg( '_wp_http_referer', remove_query_arg( '_wp_http_referer' ), admin_url( 'admin.php?page=friends-settings&rerun-activate' ) ), 'friends-settings' ) )
373 )
374 );
375 $result['description'] .= '</p>';
376 }
377
378 return $result;
379 }
380
381 /**
382 * Site Health test for Friends cron job.
383 *
384 * @return array Site Health test result.
385 */
386 public function friends_cron_test() {
387 $result = array(
388 'label' => __( 'The refresh cron job is enabled', 'friends' ),
389 'status' => 'good',
390 'badge' => array(
391 'label' => __( 'Friends', 'friends' ),
392 'color' => 'green',
393 ),
394 'description' =>
395 '<p>' .
396 __( 'The Friends Plugin uses a cron job to fetch your friends\' feeds.', 'friends' ) .
397 '</p>',
398 'test' => 'friends-cron',
399 );
400 if ( ! wp_next_scheduled( 'cron_friends_refresh_feeds' ) ) {
401 $result['label'] = __( 'The refresh cron job is not enabled', 'friends' );
402 $result['badge']['color'] = 'red';
403 $result['status'] = 'critical';
404 $result['description'] .= '<p>';
405 $result['description'] .= wp_kses_post(
406 sprintf(
407 /* translators: %s is a URL. */
408 __( '<strong>To fix this:</strong> <a href="%s">Enable the Friends cron job</a>.', 'friends' ),
409 esc_url( wp_nonce_url( add_query_arg( '_wp_http_referer', remove_query_arg( '_wp_http_referer' ), admin_url( 'admin.php?page=friends-settings&rerun-activate' ) ), 'friends-settings' ) )
410 )
411 );
412 $result['description'] .= '</p>';
413 }
414
415 return $result;
416 }
417
418 /**
419 * Site Health test for Friends delete old posts cron job.
420 *
421 * @return array Site Health test result.
422 */
423 public function friends_cron_delete_test() {
424 $result = array(
425 'label' => __( 'The cron job to delete old posts is enabled', 'friends' ),
426 'status' => 'good',
427 'badge' => array(
428 'label' => __( 'Friends', 'friends' ),
429 'color' => 'green',
430 ),
431 'description' =>
432 '<p>' .
433 __( 'The Friends Plugin uses a cron job to delete old posts your friends.', 'friends' ) .
434 '</p>',
435 'test' => 'friends-delete-cron',
436 );
437
438 if ( ! wp_next_scheduled( 'cron_friends_delete_old_posts' ) ) {
439 $result['label'] = __( 'The cron job to delete old posts is not enabled', 'friends' );
440 $result['badge']['color'] = 'orange';
441 $result['status'] = 'recommended';
442 $result['description'] .= '<p>';
443 $result['description'] .= wp_kses_post(
444 sprintf(
445 /* translators: %s is a URL. */
446 __( '<strong>To fix this:</strong> <a href="%s">Enable the Friends cron job</a>.', 'friends' ),
447 esc_url( wp_nonce_url( add_query_arg( '_wp_http_referer', remove_query_arg( '_wp_http_referer' ), admin_url( 'admin.php?page=friends-settings&rerun-activate' ) ), 'friends-settings' ) )
448 )
449 );
450 $result['description'] .= '</p>';
451 }
452
453 return $result;
454 }
455
456 /**
457 * Add debug information to Site Health.
458 *
459 * @param array $debug_info Debug information array.
460 * @return array Modified debug information.
461 */
462 public function site_health_debug( $debug_info ) {
463 $missing_friend_roles = $this->get_missing_friends_plugin_roles();
464 $debug_info['friends'] = array(
465 'label' => __( 'Friends', 'friends' ),
466 'fields' => array(
467 'version' => array(
468 'label' => __( 'Friends Version', 'friends' ),
469 'value' => Friends::VERSION,
470 ),
471 'mbstring' => array(
472 'label' => __( 'mbstring is available', 'friends' ),
473 'value' => function_exists( 'mb_check_encoding' ) ? __( 'Yes' ) : __( 'No' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
474 ),
475 'roles' => array(
476 'label' => __( 'Friend roles missing', 'friends' ),
477 'value' => empty( $missing_friend_roles ) ? sprintf(
478 /* translators: %s is a list of roles. */
479 __( 'All roles found: %s', 'friends' ),
480 implode( ', ', Friends::get_friends_plugin_roles() )
481 ) : implode( ', ', $missing_friend_roles ),
482 ),
483 'main_user' => array(
484 'label' => __( 'Main Friend User', 'friends' ),
485 'value' => $this->human_readable_main_user(),
486 ),
487 'parsers' => array(
488 'label' => __( 'Registered Parsers', 'friends' ),
489 'value' => wp_strip_all_tags( implode( ', ', Friends::get_instance()->feed->get_registered_parsers() ) ),
490 ),
491 ),
492 );
493
494 return $debug_info;
495 }
496
497 /**
498 * Returns a human readable string for which user is the main user.
499 *
500 * @return string
501 */
502 private function human_readable_main_user() {
503 $main_user = Friends::get_main_friend_user_id();
504
505 if ( ! $main_user ) {
506 /* translators: %d is the number of users. */
507 return esc_html( sprintf( __( 'No main user set. Admin users: %d', 'friends' ), User_Query::all_admin_users()->get_total() ) );
508 }
509
510 $user = new \WP_User( $main_user );
511
512 if ( ! $user->exists() ) {
513 /* translators: %d is the user ID. */
514 return esc_html( sprintf( __( 'Main user ID %d does not exist', 'friends' ), $main_user ) );
515 }
516
517 /* translators: %1$s is the user login, %2$d is the user ID. */
518 return esc_html( sprintf( __( '%1$s (#%2$d)', 'friends' ), $user->user_login, $main_user ) );
519 }
520
521 /**
522 * AJAX handler for restarting migration.
523 */
524 public function ajax_restart_migration() {
525 check_ajax_referer( 'friends_restart_migration', 'nonce' );
526
527 if ( ! current_user_can( 'manage_options' ) ) {
528 wp_die( __( 'You do not have permission to perform this action.', 'friends' ) );
529 }
530
531 require_once __DIR__ . '/class-migration.php';
532 Migration::trigger_migration_manually();
533
534 wp_send_json_success( array( 'message' => __( 'Migration restarted successfully.', 'friends' ) ) );
535 }
536
537 /**
538 * AJAX handler for cleaning up orphaned post tags.
539 */
540 public function ajax_cleanup_post_tags() {
541 check_ajax_referer( 'friends_cleanup_post_tags', 'nonce' );
542
543 if ( ! current_user_can( 'manage_options' ) ) {
544 wp_die( __( 'You do not have permission to perform this action.', 'friends' ) );
545 }
546
547 require_once __DIR__ . '/class-migration.php';
548 $results = Migration::cleanup_orphaned_post_tags();
549
550 if ( $results['deleted'] > 0 ) {
551 wp_send_json_success(
552 array(
553 'message' => sprintf(
554 /* translators: %d: number of deleted tags */
555 _n(
556 'Successfully deleted %d orphaned post tag.',
557 'Successfully deleted %d orphaned post tags.',
558 $results['deleted'],
559 'friends'
560 ),
561 $results['deleted']
562 ),
563 )
564 );
565 } else {
566 wp_send_json_success( array( 'message' => __( 'No orphaned post_tags were found to clean up.', 'friends' ) ) );
567 }
568 }
569
570 /**
571 * Enqueue scripts for Site Health pages.
572 *
573 * @param string $hook Current admin page.
574 */
575 public function enqueue_scripts( $hook ) {
576 if ( 'site-health.php' !== $hook ) {
577 return;
578 }
579
580 wp_enqueue_script(
581 'friends-site-health',
582 plugins_url( 'site-health.js', __DIR__ ),
583 array( 'jquery' ),
584 filemtime( plugin_dir_path( __DIR__ ) . 'site-health.js' ),
585 true
586 );
587
588 wp_localize_script(
589 'friends-site-health',
590 'friendsSiteHealth',
591 array(
592 'restartMigrationNonce' => wp_create_nonce( 'friends_restart_migration' ),
593 'cleanupPostTagsNonce' => wp_create_nonce( 'friends_cleanup_post_tags' ),
594 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
595 'confirmRestart' => __( 'This will restart the migration process. Any posts that were previously migrated will be re-processed. Continue?', 'friends' ),
596 'confirmCleanup' => __( 'This will clean up orphaned post_tag terms that were created by Friends posts. Only unused tags will be deleted. Continue?', 'friends' ),
597 )
598 );
599 }
600 }
601