PluginProbe ʕ •ᴥ•ʔ
WP-Sweep / 2.0.1
WP-Sweep v2.0.1
2.0.1 2.0.0 1.2.0 trunk 1.0.10 1.0.11 1.0.12 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9
wp-sweep / includes / class-wp-sweep-admin.php
wp-sweep / includes Last commit date
class-wp-sweep-admin.php 5 days ago class-wp-sweep-api.php 2 weeks ago class-wp-sweep-command.php 2 weeks ago class-wp-sweep-list-table.php 5 days ago class-wp-sweep.php 5 days ago index.php 2 weeks ago
class-wp-sweep-admin.php
789 lines
1 <?php
2 /**
3 * The Sweep screen.
4 *
5 * @package WP-Sweep
6 */
7
8 defined( 'ABSPATH' ) || exit;
9
10 /**
11 * Registers the admin menu and renders the screen every sweep is run from.
12 *
13 * Until 2.0.0 this was includes/admin.php, a template required at global scope
14 * that reached for globals it never declared and repeated the same eleven
15 * lines of markup nineteen times. The rows are now generated from
16 * WP_Sweep::get_sweeps(), so adding a sweep is one entry in one list.
17 */
18 class WP_Sweep_Admin {
19
20 /**
21 * The menu and page slug.
22 *
23 * Until 2.0.0 the screen was registered with 'wp-sweep/admin.php' -- the
24 * legacy "plugin file as menu slug" form. That put the installation
25 * directory name into the page URL and into the hook suffix handed back to
26 * admin_enqueue_scripts, so both broke for anyone who installed the plugin
27 * under a different directory name.
28 *
29 * @var string
30 */
31 const PAGE = 'wp-sweep';
32
33 /**
34 * The id of the region a finished sweep reports into.
35 *
36 * Written once, here, and used twice: on the region itself and in the
37 * aria-controls of every Sweep link the list table draws. The script
38 * follows that association rather than looking for the region in the
39 * markup around the row, because the two are not near each other -- the
40 * region sits above the form, the rows are inside it -- and a script that
41 * navigates the gap breaks the day the gap changes. It did: see
42 * messageContainer() in js/wp-sweep-admin.js.
43 *
44 * @var string
45 */
46 const MESSAGE_ID = 'wp-sweep-message';
47
48 /**
49 * The hook suffix WordPress handed back when the menu was registered.
50 *
51 * Recorded rather than assumed. get_plugin_page_hookname() derives the
52 * prefix from $admin_page_hooks, so the suffix is 'toplevel_page_wp-sweep'
53 * on a real admin request but something else anywhere the admin menu has
54 * not been built. Comparing against a hardcoded string means the script
55 * silently fails to load in the cases that do not match, and the screen
56 * renders with dead buttons.
57 *
58 * @var string
59 */
60 private static $hook_suffix = '';
61
62 /**
63 * The details a row action asked for, keyed by sweep name.
64 *
65 * @var array
66 */
67 private static $details = array();
68
69 /**
70 * Hook registration.
71 *
72 * @return void
73 */
74 public static function init() {
75 add_action( 'admin_menu', array( __CLASS__, 'add_page' ) );
76 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue' ) );
77 add_action( 'wp_ajax_sweep', array( __CLASS__, 'ajax_sweep' ) );
78 add_action( 'wp_ajax_sweep_details', array( __CLASS__, 'ajax_sweep_details' ) );
79 add_action( 'wp_ajax_wp_sweep_count', array( __CLASS__, 'ajax_sweep_count' ) );
80 add_action( 'wp_ajax_wp_sweep_totals', array( __CLASS__, 'ajax_sweep_totals' ) );
81 add_filter(
82 'plugin_action_links_' . plugin_basename( WP_SWEEP_MAIN_FILE ),
83 array( __CLASS__, 'action_links' )
84 );
85 }
86
87 /**
88 * Add a Sweep link on the Plugins screen row.
89 *
90 * @param string[] $links Existing action links.
91 * @return string[]
92 */
93 public static function action_links( $links ) {
94 array_unshift(
95 $links,
96 sprintf(
97 '<a href="%s">%s</a>',
98 esc_url( add_query_arg( 'page', self::PAGE, admin_url( 'tools.php' ) ) ),
99 esc_html__( 'Sweep', 'wp-sweep' )
100 )
101 );
102
103 return $links;
104 }
105
106 /**
107 * The list table, built once the screen is being rendered.
108 *
109 * @return WP_Sweep_List_Table
110 */
111 private static function list_table() {
112 require_once WP_SWEEP_DIR . 'includes/class-wp-sweep-list-table.php';
113
114 return new WP_Sweep_List_Table();
115 }
116
117 /**
118 * Register the menu.
119 *
120 * @return void
121 */
122 public static function add_page() {
123 // Tools, where this screen lived for its whole released life. Sweeping is
124 // maintenance against the installation, which is what core keeps under
125 // Tools beside Site Health, Export and Erase Personal Data.
126 //
127 // This is only possible because there is no settings screen: the one
128 // setting became the wp_sweep_limit_details filter. Tools has no submenus,
129 // so a second screen would have had to become a tab of this one, and that
130 // would have made WP-Sweep the only plugin of the nineteen where a data
131 // screen and a settings screen share a page.
132 self::$hook_suffix = add_management_page(
133 _x( 'Sweep', 'Page title', 'wp-sweep' ),
134 _x( 'WP-Sweep', 'Menu title', 'wp-sweep' ),
135 WP_Sweep::capability( 'sweep' ),
136 self::PAGE,
137 array( __CLASS__, 'render_page' )
138 );
139 }
140
141 /**
142 * The hook suffix WordPress gave the Sweep screen.
143 *
144 * @return string Hook suffix, or an empty string before admin_menu runs.
145 */
146 public static function get_hook_suffix() {
147 return self::$hook_suffix;
148 }
149
150 /**
151 * Load the screen's script, and only on the screen that needs it.
152 *
153 * @param string $hook Hook suffix of the screen being rendered.
154 * @return void
155 */
156 public static function enqueue( $hook ) {
157 if ( '' === self::$hook_suffix || self::$hook_suffix !== $hook ) {
158 return;
159 }
160
161 /*
162 * One unminified file, no dependencies. There is no build step in this
163 * plugin, so a hand-minified twin drifts out of sync with the source it
164 * is supposed to mirror -- and under a kilobyte gzipped the saving was
165 * noise.
166 *
167 * The URL comes from WP_SWEEP_URL, which is derived from the main file.
168 * Building it from the literal 'wp-sweep/js/...' meant the script 404ed
169 * for anyone who installed the plugin under a different directory name.
170 */
171 wp_enqueue_script( 'wp-sweep-admin', WP_SWEEP_URL . 'js/wp-sweep-admin.js', array(), WP_SWEEP_VERSION, true );
172
173 wp_localize_script(
174 'wp-sweep-admin',
175 'wpSweepL10n',
176 array(
177 'textCloseWarning' => __( 'Sweeping is in progress. If you leave now, the process won\'t be completed.', 'wp-sweep' ),
178 'textSweep' => __( 'Sweep', 'wp-sweep' ),
179 'textSweeping' => __( 'Sweeping...', 'wp-sweep' ),
180 'textNa' => __( 'N/A', 'wp-sweep' ),
181 'textNothingToSweep' => __( 'Nothing to sweep', 'wp-sweep' ),
182 )
183 );
184 }
185
186 /**
187 * Render the screen.
188 *
189 * @return void
190 */
191 public static function render_page() {
192 if ( ! current_user_can( WP_Sweep::capability( 'sweep' ) ) ) {
193 wp_die( esc_html__( 'You do not have permission to sweep this site.', 'wp-sweep' ) );
194 }
195
196 $sweep = WP_Sweep::get_instance();
197
198 self::handle_request();
199
200 $table = self::list_table();
201 $table->prepare_items();
202
203 ?>
204 <div class="wrap">
205 <h1><?php echo esc_html_x( 'Sweep', 'Page title', 'wp-sweep' ); ?></h1>
206
207 <?php
208 /*
209 * Core's common.js gathers every notice on the screen and re-inserts
210 * the lot of them directly after this marker, falling back to the
211 * first heading it can find when a screen has not printed one. So the
212 * marker, not the source order, is what decides where the reload
213 * path's message ends up -- and printing no marker was how the
214 * permanent warning below came to sit between the heading and the
215 * region, above a message the script had written underneath it.
216 */
217 ?>
218 <hr class="wp-header-end">
219
220 <?php settings_errors( 'wp_sweep' ); ?>
221
222 <?php
223 /*
224 * One region per screen, immediately after the marker, so the two
225 * ways a sweep reports are neighbours: a bulk sweep posts and
226 * reloads, and settings_errors() above has just printed into the
227 * space the marker anchors; a row's Sweep button does not reload and
228 * the script writes in here. Nothing may be printed between the two,
229 * or one screen answers the same question in two places. This region
230 * sat below the warning, the description and the totals table, which
231 * is where it was when the screen was a page of separate forms.
232 *
233 * role="status" is what tells a screen reader anything happened at
234 * all, since nothing reloads on the script's path.
235 */
236 ?>
237 <div class="sweep-message" id="<?php echo esc_attr( self::MESSAGE_ID ); ?>" role="status"></div>
238
239 <?php
240 /*
241 * `inline` is core's opt-out from that gathering, and this warning is
242 * the one notice on the screen that has to take it. It reports
243 * nothing and never changes, so hoisting it to the marker only puts
244 * it between the reload path's message and the region -- which is
245 * exactly where it was, and why the two paths still did not agree
246 * once the region had been moved up here.
247 */
248 ?>
249 <div class="notice notice-warning inline">
250 <p>
251 <?php
252 echo wp_kses_post(
253 sprintf(
254 /* translators: %s is the URL of the WP-DBManager plugin. */
255 __( 'Before you do any sweep, please <a href="%s">backup your database</a> first, because any sweep done is irreversible.', 'wp-sweep' ),
256 'https://wordpress.org/plugins/wp-dbmanager/'
257 )
258 );
259 ?>
260 </p>
261 </div>
262
263 <p class="description">
264 <?php
265 echo esc_html(
266 sprintf(
267 /* translators: %s is the maximum number of sample items. */
268 __( 'Details lists a sample of up to %s items. Filter wp_sweep_limit_details to change it.', 'wp-sweep' ),
269 number_format_i18n( $sweep->limit_details() )
270 )
271 );
272 ?>
273 </p>
274
275 <?php if ( WP_Sweep_List_Table::defer_counts() ) : ?>
276 <?php
277 /*
278 * The counts are fetched by the script once the screen is up,
279 * which is what lets the screen come up at all on a database
280 * where a single count takes half a minute. Without the script
281 * nothing fetches them, so this link -- and only readers
282 * without JavaScript ever see it -- is the real, reloading
283 * path to the same numbers.
284 */
285 ?>
286 <noscript>
287 <p>
288 <a href="<?php echo esc_url( add_query_arg( 'counts', 'now', self::page_url() ) ); ?>">
289 <?php esc_html_e( 'Show the counts now. On a large database this can take a while.', 'wp-sweep' ); ?>
290 </a>
291 </p>
292 </noscript>
293 <?php endif; ?>
294
295 <?php self::render_totals(); ?>
296
297 <?php $table->views(); ?>
298
299 <form method="post" action="<?php echo esc_url( self::page_url() ); ?>">
300 <?php
301 // No wp_nonce_field() here: the list table prints its own, under
302 // the same _wpnonce name. See WP_Sweep_List_Table::bulk_nonce_action().
303 $table->display();
304 ?>
305 </form>
306
307 <?php self::fire_group_actions(); ?>
308 </div>
309 <?php
310 }
311
312 /**
313 * The screen's own URL, carrying the group currently being shown.
314 *
315 * @return string
316 */
317 private static function page_url() {
318 $args = array(
319 'page' => self::PAGE,
320 'group' => WP_Sweep_List_Table::current_group(),
321 );
322
323 // The bulk form posts here, and a reader who asked for the counts with
324 // the page is a reader without the script to fetch them afterwards --
325 // so the view they chose has to survive the round trip.
326 if ( WP_Sweep_List_Table::counts_requested_now() ) {
327 $args['counts'] = 'now';
328 }
329
330 return add_query_arg( $args, admin_url( 'tools.php' ) );
331 }
332
333 /**
334 * Render the running totals each group is measured against.
335 *
336 * The spans carry the class the script writes updated totals back into
337 * after a sweep, so the numbers do not go stale without a reload.
338 *
339 * @return void
340 */
341 private static function render_totals() {
342 $sweep = WP_Sweep::get_instance();
343 $defer = WP_Sweep_List_Table::defer_counts();
344
345 // A widefat table rather than a bullet list. These are six rows of
346 // numbers, and as a list they ran together as prose with nothing lining
347 // up; in a table the counts sit in a column and can be read down. It uses
348 // core's own admin classes, so it needs no stylesheet of its own.
349 //
350 // When the counts are deferred the table carries the nonce for the one
351 // request that fills every total at once. On the row counts the nonce
352 // rides on each cell; here twelve cells share one fetch, so it rides on
353 // their table.
354 printf(
355 '<table class="widefat striped sweep-totals"%s>',
356 $defer ? ' data-nonce="' . esc_attr( wp_create_nonce( 'wp_sweep_totals' ) ) . '"' : ''
357 );
358 echo '<thead><tr>';
359 printf( '<th scope="col">%s</th>', esc_html__( 'Group', 'wp-sweep' ) );
360 printf( '<th scope="col">%s</th>', esc_html__( 'Currently in your database', 'wp-sweep' ) );
361 echo '</tr></thead><tbody>';
362
363 foreach ( self::totals() as $label => $types ) {
364 $parts = array();
365
366 foreach ( $types as $type => $type_label ) {
367 // Bold unconditionally here, unlike the sweep list's Count
368 // column. There a zero means "nothing to do about this row" and
369 // emphasising it would draw the eye to the one thing not worth
370 // looking at; here the number is simply the content of the cell,
371 // and a site with none of something is as much a fact as a site
372 // with a thousand.
373 $parts[] = sprintf(
374 '<strong class="sweep-count-type-%1$s%2$s">%3$s</strong> %4$s',
375 esc_attr( $type ),
376 $defer ? ' sweep-total-pending' : '',
377 $defer ? '&hellip;' : esc_html( number_format_i18n( $sweep->total_count( $type ) ) ),
378 esc_html( $type_label )
379 );
380 }
381
382 printf(
383 '<tr><th scope="row">%1$s</th><td>%2$s</td></tr>',
384 esc_html( $label ),
385 wp_kses_post( implode( ', ', $parts ) )
386 );
387 }
388
389 echo '</tbody></table>';
390 }
391
392 /**
393 * The table types each group reports a total for.
394 *
395 * @return array Type label keyed by type, keyed by group label.
396 */
397 private static function totals() {
398 /*
399 * Labelled with plain nouns, not with get_sweep_groups().
400 *
401 * Those read "Post Sweep", "Comment Sweep" and so on, which is right
402 * above the table where they filter which sweeps are listed. Here they
403 * would be wrong: these are counts of what is in the database right now,
404 * not of anything a sweep would remove. A row saying "Post Sweep 3 Posts"
405 * reads as three posts waiting to be deleted.
406 */
407 return array(
408 __( 'Posts', 'wp-sweep' ) => array(
409 'posts' => __( 'Posts', 'wp-sweep' ),
410 'postmeta' => __( 'Post Meta', 'wp-sweep' ),
411 ),
412 __( 'Comments', 'wp-sweep' ) => array(
413 'comments' => __( 'Comments', 'wp-sweep' ),
414 'commentmeta' => __( 'Comment Meta', 'wp-sweep' ),
415 ),
416 __( 'Users', 'wp-sweep' ) => array(
417 'users' => __( 'Users', 'wp-sweep' ),
418 'usermeta' => __( 'User Meta', 'wp-sweep' ),
419 ),
420 __( 'Terms', 'wp-sweep' ) => array(
421 'terms' => __( 'Terms', 'wp-sweep' ),
422 'termmeta' => __( 'Term Meta', 'wp-sweep' ),
423 'term_taxonomy' => __( 'Term Taxonomy', 'wp-sweep' ),
424 'term_relationships' => __( 'Term Relationships', 'wp-sweep' ),
425 ),
426 __( 'Options', 'wp-sweep' ) => array(
427 'options' => __( 'Options', 'wp-sweep' ),
428 ),
429 __( 'Database', 'wp-sweep' ) => array(
430 'tables' => __( 'Tables', 'wp-sweep' ),
431 ),
432 );
433 }
434
435 /**
436 * Fire the extension points other plugins hang their own rows off.
437 *
438 * @return void
439 */
440 private static function fire_group_actions() {
441 /**
442 * Fires below the sweep table, where the post sweeps used to end.
443 *
444 * @since 1.0.4
445 */
446 do_action( 'wp_sweep_admin_post_sweep' );
447
448 /**
449 * Fires below the sweep table, where the comment sweeps used to end.
450 *
451 * @since 1.0.4
452 */
453 do_action( 'wp_sweep_admin_comment_sweep' );
454
455 /**
456 * Fires below the sweep table, where the user sweeps used to end.
457 *
458 * @since 1.0.4
459 */
460 do_action( 'wp_sweep_admin_user_sweep' );
461
462 /**
463 * Fires below the sweep table, where the term sweeps used to end.
464 *
465 * @since 1.0.4
466 */
467 do_action( 'wp_sweep_admin_term_sweep' );
468
469 /**
470 * Fires below the sweep table, where the option sweeps used to end.
471 *
472 * @since 1.0.4
473 */
474 do_action( 'wp_sweep_admin_option_sweep' );
475
476 /**
477 * Fires below the sweep table, where the database sweeps used to end.
478 *
479 * @since 1.0.4
480 */
481 do_action( 'wp_sweep_admin_database_sweep' );
482 }
483
484 /**
485 * Act on whatever the request asked for, before anything is rendered.
486 *
487 * Every one of these paths works with JavaScript turned off: the row
488 * actions are real nonced links and the bulk action is a real form post.
489 * The script intercepts the links so the screen does not reload nineteen
490 * times, but nothing depends on it being there.
491 *
492 * @return void
493 */
494 private static function handle_request() {
495 self::$details = array();
496
497 self::handle_bulk_sweep();
498 self::handle_single_sweep();
499 self::handle_details();
500 }
501
502 /**
503 * Sweep everything that was checked.
504 *
505 * @return void
506 */
507 private static function handle_bulk_sweep() {
508 $table = self::list_table();
509
510 if ( 'sweep' !== $table->current_action() ) {
511 return;
512 }
513
514 check_admin_referer( $table->bulk_nonce_action() );
515
516 $sweep = WP_Sweep::get_instance();
517 $posted = isset( $_POST['sweep'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['sweep'] ) ) : array();
518 $messages = array();
519
520 // Filtered from the canonical list rather than taken as given, so the
521 // sweeps run in the order they have to and an unknown name is dropped
522 // rather than passed on.
523 $selected = array_intersect( $sweep->get_sweep_names(), $posted );
524
525 foreach ( $selected as $name ) {
526 $message = $sweep->sweep( $name );
527
528 if ( '' !== $message ) {
529 $messages[] = $message;
530 }
531 }
532
533 /*
534 * Two different outcomes, and they used to share one message.
535 *
536 * Ticking nothing and ticking a sweep that had nothing left in it both
537 * ended with an empty $messages and the screen saying "Nothing was
538 * selected", which is simply false in the second case -- and reachable
539 * now that every row carries a checkbox whether it has anything to sweep
540 * or not.
541 */
542 if ( empty( $selected ) ) {
543 add_settings_error( 'wp_sweep', 'wp_sweep_nothing', __( 'Nothing was selected, so nothing was swept.', 'wp-sweep' ), 'warning' );
544
545 return;
546 }
547
548 if ( empty( $messages ) ) {
549 add_settings_error( 'wp_sweep', 'wp_sweep_nothing', __( 'There was nothing left to sweep.', 'wp-sweep' ), 'warning' );
550
551 return;
552 }
553
554 /*
555 * Joined with wp_sprintf( '%l' ), which builds a localised list -- "a, b
556 * and c" in English, and whatever the locale does instead everywhere
557 * else. Imploding on a space ran them together into "11 Transient Options
558 * Processed 20 Tables Processed", which reads as one broken sentence.
559 *
560 * Escaped here, not by settings_errors(), which prints the message
561 * straight into the page. Every one of these comes back through the
562 * wp_sweep_sweep filter, which is public API and can return anything.
563 */
564 add_settings_error( 'wp_sweep', 'wp_sweep_swept', esc_html( wp_sprintf( '%l', $messages ) ), 'success' );
565 }
566
567 /**
568 * Run the one sweep a row action asked for.
569 *
570 * The name is validated against the plugin's own list before the referer is
571 * checked, so a crafted value never reaches sweep(). The resulting message
572 * is rendered by render_page(); before 2.0.0 it was assigned and then
573 * silently discarded, which meant this path deleted data and told the user
574 * nothing at all.
575 *
576 * @return void
577 */
578 private static function handle_single_sweep() {
579 $sweep = WP_Sweep::get_instance();
580 $name = isset( $_GET['sweep'] ) ? sanitize_key( wp_unslash( $_GET['sweep'] ) ) : '';
581
582 if ( '' === $name || ! $sweep->is_sweep_name_valid( $name ) ) {
583 return;
584 }
585
586 check_admin_referer( 'wp_sweep_' . $name );
587
588 $message = $sweep->sweep( $name );
589
590 if ( '' === $message ) {
591 add_settings_error( 'wp_sweep', 'wp_sweep_nothing', __( 'There was nothing left to sweep.', 'wp-sweep' ), 'warning' );
592
593 return;
594 }
595
596 // Escaped for the reason given in handle_bulk_sweep().
597 add_settings_error( 'wp_sweep', 'wp_sweep_swept', esc_html( $message ), 'success' );
598 }
599
600 /**
601 * Collect the details a row action asked for.
602 *
603 * @return void
604 */
605 private static function handle_details() {
606 $sweep = WP_Sweep::get_instance();
607 $name = isset( $_GET['sweep_details'] ) ? sanitize_key( wp_unslash( $_GET['sweep_details'] ) ) : '';
608
609 if ( '' === $name || ! $sweep->is_sweep_name_valid( $name ) ) {
610 return;
611 }
612
613 check_admin_referer( 'wp_sweep_details_' . $name );
614
615 self::$details = array( $name => (array) $sweep->details( $name ) );
616 }
617
618 /**
619 * The details a row action asked for, keyed by sweep name.
620 *
621 * @return array
622 */
623 public static function requested_details() {
624 return self::$details;
625 }
626
627 /**
628 * Return the details of a sweep over AJAX.
629 *
630 * @return void
631 */
632 public static function ajax_sweep_details() {
633 $sweep = WP_Sweep::get_instance();
634
635 $name = isset( $_GET['sweep_name'] ) ? sanitize_key( wp_unslash( $_GET['sweep_name'] ) ) : '';
636
637 // Permissions and the name are checked before the referer, so a caller
638 // without the capability gets a JSON error rather than the nonce
639 // failure screen.
640 if ( ! current_user_can( WP_Sweep::capability( 'ajax' ) ) || ! $sweep->is_sweep_name_valid( $name ) ) {
641 wp_send_json_error( array( 'error' => __( 'Invalid AJAX request.', 'wp-sweep' ) ) );
642 }
643
644 check_admin_referer( 'wp_sweep_details_' . $name );
645
646 wp_send_json_success( $sweep->details( $name ) );
647 }
648
649 /**
650 * Run a sweep over AJAX and hand back the numbers the screen has to update.
651 *
652 * @return void
653 */
654 public static function ajax_sweep() {
655 $sweep = WP_Sweep::get_instance();
656
657 $name = isset( $_GET['sweep_name'] ) ? sanitize_key( wp_unslash( $_GET['sweep_name'] ) ) : '';
658 $type = isset( $_GET['sweep_type'] ) ? sanitize_key( wp_unslash( $_GET['sweep_type'] ) ) : '';
659
660 if (
661 ! current_user_can( WP_Sweep::capability( 'ajax' ) )
662 || ! $sweep->is_sweep_name_valid( $name )
663 || ! $sweep->is_sweep_type_valid( $type )
664 ) {
665 wp_send_json_error( array( 'error' => __( 'Invalid AJAX request.', 'wp-sweep' ) ) );
666 }
667
668 check_admin_referer( 'wp_sweep_' . $name );
669
670 $message = $sweep->sweep( $name );
671 $count = (int) $sweep->count( $name );
672 $total_count = (int) $sweep->total_count( $type );
673
674 wp_send_json_success(
675 array(
676 'sweep' => $message,
677 'count' => $count,
678 'total' => $total_count,
679 'percentage' => $sweep->format_percentage( $count, $total_count ),
680 'stats' => self::related_totals( $type ),
681 )
682 );
683 }
684
685 /**
686 * Count one sweep over AJAX, for a screen that rendered without counts.
687 *
688 * The screen defers its counts so it can render before the queries run;
689 * this is where the script gets each number afterwards. The shape matches
690 * the fields ajax_sweep() responds with, so the script fills a cell the
691 * same way whichever request produced the value.
692 *
693 * @return void
694 */
695 public static function ajax_sweep_count() {
696 $sweep = WP_Sweep::get_instance();
697
698 $name = isset( $_GET['sweep_name'] ) ? sanitize_key( wp_unslash( $_GET['sweep_name'] ) ) : '';
699 $type = isset( $_GET['sweep_type'] ) ? sanitize_key( wp_unslash( $_GET['sweep_type'] ) ) : '';
700
701 if (
702 ! current_user_can( WP_Sweep::capability( 'ajax' ) )
703 || ! $sweep->is_sweep_name_valid( $name )
704 || ! $sweep->is_sweep_type_valid( $type )
705 ) {
706 wp_send_json_error( array( 'error' => __( 'Invalid AJAX request.', 'wp-sweep' ) ) );
707 }
708
709 check_admin_referer( 'wp_sweep_count_' . $name );
710
711 $count = (int) $sweep->count( $name );
712 $total_count = (int) $sweep->total_count( $type );
713
714 wp_send_json_success(
715 array(
716 'count' => $count,
717 'total' => $total_count,
718 'percentage' => $sweep->format_percentage( $count, $total_count ),
719 )
720 );
721 }
722
723 /**
724 * Count every table's rows over AJAX, for the running totals table.
725 *
726 * One request for all twelve, rather than a total on each count response:
727 * the totals are COUNT(*) scans of whole tables, and answering them once
728 * is the point of deferring them.
729 *
730 * @return void
731 */
732 public static function ajax_sweep_totals() {
733 $sweep = WP_Sweep::get_instance();
734
735 if ( ! current_user_can( WP_Sweep::capability( 'ajax' ) ) ) {
736 wp_send_json_error( array( 'error' => __( 'Invalid AJAX request.', 'wp-sweep' ) ) );
737 }
738
739 check_admin_referer( 'wp_sweep_totals' );
740
741 $totals = array();
742
743 foreach ( $sweep->get_sweep_types() as $type ) {
744 $totals[ $type ] = (int) $sweep->total_count( $type );
745 }
746
747 wp_send_json_success( array( 'stats' => $totals ) );
748 }
749
750 /**
751 * The totals that move when a sweep of a given type runs.
752 *
753 * Deleting a post takes its meta with it, so the postmeta total is stale
754 * the moment a post sweep finishes even though nothing swept postmeta.
755 *
756 * @param string $type Sweep type.
757 * @return array Row counts keyed by type.
758 */
759 private static function related_totals( $type ) {
760 $related = array(
761 'posts' => array( 'posts', 'postmeta' ),
762 'postmeta' => array( 'posts', 'postmeta' ),
763 'comments' => array( 'comments', 'commentmeta' ),
764 'commentmeta' => array( 'comments', 'commentmeta' ),
765 'users' => array( 'users', 'usermeta' ),
766 'usermeta' => array( 'users', 'usermeta' ),
767 'term_relationships' => array( 'term_relationships', 'term_taxonomy', 'terms', 'termmeta' ),
768 'term_taxonomy' => array( 'term_relationships', 'term_taxonomy', 'terms', 'termmeta' ),
769 'terms' => array( 'term_relationships', 'term_taxonomy', 'terms', 'termmeta' ),
770 'termmeta' => array( 'term_relationships', 'term_taxonomy', 'terms', 'termmeta' ),
771 'options' => array( 'options' ),
772 'tables' => array( 'tables' ),
773 );
774
775 if ( ! isset( $related[ $type ] ) ) {
776 return array();
777 }
778
779 $sweep = WP_Sweep::get_instance();
780 $totals = array();
781
782 foreach ( $related[ $type ] as $related_type ) {
783 $totals[ $related_type ] = (int) $sweep->total_count( $related_type );
784 }
785
786 return $totals;
787 }
788 }
789