class-events.php 19.8 KB
Newer Older
1
2
3
4
5
<?php

namespace Automattic\WP\Cron_Control\CLI;

/**
6
 * Manage Cron Control's data
7
 */
8
class Events extends \WP_CLI_Command {
9
10
11
12
13
	/**
	 * List cron events
	 *
	 * Intentionally bypasses caching to ensure latest data is shown
	 *
14
	 * @subcommand list
15
	 * @synopsis [--status=<pending|completed>] [--page=<page>] [--limit=<limit>] [--format=<format>]
16
	 */
17
	public function list_events( $args, $assoc_args ) {
18
		$events = $this->get_events( $args, $assoc_args );
19

20
		// Prevent one from requesting a page that doesn't exist
21
22
		// Shouldn't error when first page is requested, though, as that is handled below and is an odd behaviour otherwise
		if ( $events['page'] > $events['total_pages'] && $events['page'] > 1 ) {
23
24
25
			\WP_CLI::error( __( 'Invalid page requested', 'automattic-cron-control' ) );
		}

26
27
28
29
30
31
32
33
34
35
		// Output in the requested format
		if ( isset( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) {
			echo implode( ' ', wp_list_pluck( $events['items'], 'ID' ) );
		} else {
			// Lest someone think the `completed` record should be...complete
			if ( isset( $assoc_args['status'] ) && 'completed' === $assoc_args['status'] ) {
				\WP_CLI::warning( __( 'Entries are purged automatically, so this cannot be relied upon as a record of past event execution.', 'automattic-cron-control' ) );
			}

			// Not much to do
36
37
			if ( 0 === $events['total_items'] || empty( $events['items'] ) ) {
				\WP_CLI::warning( __( 'No events to display', 'automattic-cron-control' ) );
38
39
40
41
42
43
44
45
46
				return;
			}

			// Prepare events for display
			$events_for_display      = $this->format_events( $events['items'] );
			$total_events_to_display = count( $events_for_display );

			// Count, noting if showing fewer than all
			if ( $events['total_items'] <= $total_events_to_display ) {
47
				\WP_CLI::log( sprintf( _n( 'Displaying one entry', 'Displaying all %s entries', $total_events_to_display, 'automattic-cron-control' ), number_format_i18n( $total_events_to_display ) ) );
48
			} else {
49
				\WP_CLI::log( sprintf( __( 'Displaying %1$s of %2$s entries, page %3$s of %4$s', 'automattic-cron-control' ), number_format_i18n( $total_events_to_display ), number_format_i18n( $events['total_items'] ), number_format_i18n( $events['page'] ), number_format_i18n( $events['total_pages'] ) ) );
50
51
52
			}

			// And reformat
53
			$format = \WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' );
Erick Hitter's avatar
Erick Hitter committed
54

55
56
57
58
59
60
			\WP_CLI\Utils\format_items( $format, $events_for_display, array(
				'ID',
				'action',
				'instance',
				'next_run_gmt',
				'next_run_relative',
Erick Hitter's avatar
Erick Hitter committed
61
				'last_updated_gmt',
62
				'recurrence',
63
				'internal_event',
64
				'schedule_name',
65
66
67
				'event_args',
			) );
		}
68
69
	}

70
	/**
71
	 * Remove events
72
73
	 *
	 * @subcommand delete
74
	 * @synopsis [--event_id=<event_id>] [--action=<action>] [--completed]
75
76
77
78
79
	 */
	public function delete_events( $args, $assoc_args ) {
		// Remove a specific event
		if ( isset( $assoc_args['event_id'] ) ) {
			$this->delete_event_by_id( $args, $assoc_args );
80
			return;
81
82
83
84
		}

		// Remove all events with a given action
		if ( isset( $assoc_args['action'] ) ) {
Erick Hitter's avatar
Typo    
Erick Hitter committed
85
			$this->delete_event_by_action( $args, $assoc_args );
86
			return;
87
88
		}

89
90
		// Remove all completed events
		if ( isset( $assoc_args['completed'] ) ) {
91
			$this->delete_completed_events( $args, $assoc_args );
92
93
94
			return;
		}

95
96
97
		\WP_CLI::error( __( 'Specify something to delete, or see the `cron-control-fixers` command to remove all data.', 'automattic-cron-control' ) );
	}

98
99
100
101
	/**
	 * Run an event given an ID
	 *
	 * @subcommand run
102
	 * @synopsis [<event_id>] [--timestamp] [--action_hashed] [--instance] [--force]
103
104
	 */
	public function run_event( $args, $assoc_args ) {
105
106
107
108
109
110
111
112
113
114
115
116
		$event = null;

		// Accept IDs by default, for backcompat
		$id = \WP_CLI\Utils\get_flag_value( $args, 0, false );
		if ( false !== $id ) {
			// Validate ID
			if ( ! is_numeric( $id ) ) {
				\WP_CLI::error( __( 'Specify the ID of an event to run', 'automattic-cron-control' ) );
			}

			// Retrieve information needed to execute event
			$event = \Automattic\WP\Cron_Control\get_event_by_id( $id );
117
118
		}

119
120
121
122
123
124
125
126
127
128
129
		// Also allow event to be run based on arguments
		if ( is_null( $event ) ) {
			$timestamp     = \WP_CLI\Utils\get_flag_value( $assoc_args, 'timestamp',     null );
			$action_hashed = \WP_CLI\Utils\get_flag_value( $assoc_args, 'action_hashed', null );
			$instance      = \WP_CLI\Utils\get_flag_value( $assoc_args, 'instance',      null );

			if ( is_numeric( $timestamp ) && is_string( $action_hashed ) && is_string( $instance ) ) {
				$action = $action_hashed;
				$event = (object) compact( 'timestamp', 'action', 'action_hashed', 'instance' );
			}
		}
130

Erick Hitter's avatar
Erick Hitter committed
131
		if ( ! is_object( $event ) ) {
132
			\WP_CLI::error( __( 'Failed to locate event. Please confirm that the entry exists.', 'automattic-cron-control' ) );
133
134
		}

135
		\WP_CLI::log( sprintf( __( 'Found event %1$d with action `%2$s` and instance identifier `%3$s`', 'automattic-cron-control' ), $args[0], $event->action, $event->instance ) );
136
137

		// Proceed?
138
		$now = time();
Erick Hitter's avatar
Erick Hitter committed
139
140
		if ( $event->timestamp > $now ) {
			\WP_CLI::warning( sprintf( __( 'This event is not scheduled to run until %1$s GMT (%2$s)', 'automattic-cron-control' ), date( TIME_FORMAT, $event->timestamp ), $this->calculate_interval( $event->timestamp - $now ) ) );
141
142
		}

143
144
145
		if ( false === \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ) ) {
			\WP_CLI::confirm( sprintf( __( 'Run this event?', 'automattic-cron-control' ) ) );
		}
146
147
148
149
150
151
152

		// Environment preparation
		if ( ! defined( 'DOING_CRON' ) ) {
			define( 'DOING_CRON', true );
		}

		// Run the event
Erick Hitter's avatar
Erick Hitter committed
153
		$run = \Automattic\WP\Cron_Control\run_event( $event->timestamp, $event->action_hashed, $event->instance, true );
154
155
156
157
158
159
160
161
162
163
164

		// Output based on run attempt
		if ( is_array( $run ) ) {
			\WP_CLI::success( $run['message'] );
		} elseif ( is_wp_error( $run ) ) {
			\WP_CLI::error( $run->get_error_message() );
		} else {
			\WP_CLI::error( __( 'Failed to run event', 'automattic-cron-control' ) );
		}
	}

165
166
167
168
	/**
	 * Retrieve list of events, and related data, for a given request
	 */
	private function get_events( $args, $assoc_args ) {
169
		// Accept a status argument, with a default
170
171
172
173
174
		$status = 'pending';
		if ( isset( $assoc_args['status'] ) ) {
			$status = $assoc_args['status'];
		}

175
		// Convert to status used by Event Store
176
		$event_status = null;
177
178
		switch ( $status ) {
			case 'pending' :
179
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_PENDING;
180
181
				break;

182
183
184
185
			case 'running' :
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_RUNNING;
				break;

186
			case 'completed' :
187
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED;
188
189
190
				break;
		}

191
192
193
194
195
196
		if ( is_null( $event_status ) ) {
			\WP_CLI::error( __( 'Invalid status specified', 'automattic-cron-control' ) );
		}

		unset( $status );

197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
		// Total to show
		$limit = 25;
		if ( isset( $assoc_args['limit'] ) && is_numeric( $assoc_args['limit'] ) ) {
			$limit = max( 1, min( absint( $assoc_args['limit'] ), 500 ) );
		}

		// Pagination
		$page = 1;
		if ( isset( $assoc_args['page'] ) && is_numeric( $assoc_args['page'] ) ) {
			$page = absint( $assoc_args['page'] );
		}

		$offset = absint( ( $page - 1 ) * $limit );

		// Query
Erick Hitter's avatar
Erick Hitter committed
212
		$items = \Automattic\WP\Cron_Control\get_events( array(
213
214
215
216
			'status'   => $event_status,
			'quantity' => $limit,
			'page'     => $page,
		) );
217
218
219
220
221
222

		// Bail if we don't get results
		if ( ! is_array( $items ) ) {
			\WP_CLI::error( __( 'Problem retrieving events', 'automattic-cron-control' ) );
		}

Erick Hitter's avatar
Erick Hitter committed
223
		// Include totals for pagination etc
224
		$total_items = \Automattic\WP\Cron_Control\count_events_by_status( $event_status );
Erick Hitter's avatar
Erick Hitter committed
225
		$total_pages = ceil( $total_items / $limit );
226

Erick Hitter's avatar
Erick Hitter committed
227
		return compact( 'status', 'limit', 'page', 'offset', 'items', 'total_items', 'total_pages' );
228
229
230
	}

	/**
231
	 * Format event data into something human-readable
232
	 */
233
234
235
236
237
238
	private function format_events( $events ) {
		$formatted_events = array();

		// Reformat events
		foreach ( $events as $event ) {
			$row = array(
Erick Hitter's avatar
Erick Hitter committed
239
				'ID'                => $event->ID,
240
241
242
				'action'            => $event->action,
				'instance'          => $event->instance,
				'next_run_gmt'      => date( TIME_FORMAT, $event->timestamp ),
243
				'next_run_relative' => '',
244
				'last_updated_gmt'  => date( TIME_FORMAT, strtotime( $event->last_modified ) ),
245
				'recurrence'        => __( 'Non-repeating', 'automattic-cron-control' ),
246
				'internal_event'    => '',
247
				'schedule_name'     => __( 'n/a', 'automattic-cron-control' ),
248
249
250
				'event_args'        => '',
			);

251
252
			if ( $event->status === \Automattic\WP\Cron_Control\Events_Store::STATUS_PENDING ) {
				$row['next_run_relative'] = $this->calculate_interval( $event->timestamp - time() );
253
254
			}

255
256
257
258
			$row['internal_event'] = \Automattic\WP\Cron_Control\is_internal_event( $event->action ) ? __( 'true', 'automattic-cron-control' ) : '';

			$row['event_args'] = maybe_serialize( $event->args );

259
260
261
262
263
			if ( \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED === $event->status ) {
				$instance = md5( $row['event_args'] );
				$row['instance'] = "{$instance} - {$row['instance']}";
			}

264
265
266
267
268
269
			if ( isset( $event->interval ) && $event->interval ) {
				$row['recurrence'] = $this->calculate_interval( $event->interval );
			}

			if ( isset( $event->schedule ) && $event->schedule ) {
				$row['schedule_name'] = $event->schedule;
Erick Hitter's avatar
Erick Hitter committed
270
271
			}

272
273
274
			$formatted_events[] = $row;
		}

275
276
277
278
279
		// Sort results
		if ( ! empty( $formatted_events ) ) {
			usort( $formatted_events, array( $this, 'sort_events' ) );
		}

280
281
		return $formatted_events;
	}
Erick Hitter's avatar
Erick Hitter committed
282

283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
	/**
	 * Sort events by timestamp, then action name
	 */
	private function sort_events( $first, $second ) {
		// Timestamp is usually sufficient
		if ( isset( $first['next_run_gmt'] ) ) {
			$first_timestamp = strtotime( $first['next_run_gmt'] );
			$second_timestamp = strtotime( $second['next_run_gmt'] );
		} elseif ( isset( $first['timestamp'] ) ) {
			$first_timestamp = $first['timestamp'];
			$second_timestamp = $second['timestamp'];
		} else {
			return 0;
		}

298
299
		if ( $first_timestamp !== $second_timestamp ) {
			return $first_timestamp - $second_timestamp;
300
301
302
		}

		// If timestamps are equal, consider action
303
		return strnatcasecmp( $first['action'], $second['action'] );
304
305
	}

Erick Hitter's avatar
Erick Hitter committed
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
	/**
	 * Convert a time interval into human-readable format.
	 *
	 * Similar to WordPress' built-in `human_time_diff()` but returns two time period chunks instead of just one.
	 *
	 * Borrowed from WP-CLI
	 *
	 * @param int $since An interval of time in seconds
	 * @return string The interval in human readable format
	 */
	private function calculate_interval( $since ) {
		if ( $since <= 0 ) {
			return 'now';
		}

		$since = absint( $since );

		// array of time period chunks
		$chunks = array(
			array( 60 * 60 * 24 * 365 , \_n_noop( '%s year', '%s years' ) ),
			array( 60 * 60 * 24 * 30 , \_n_noop( '%s month', '%s months' ) ),
			array( 60 * 60 * 24 * 7, \_n_noop( '%s week', '%s weeks' ) ),
			array( 60 * 60 * 24 , \_n_noop( '%s day', '%s days' ) ),
			array( 60 * 60 , \_n_noop( '%s hour', '%s hours' ) ),
			array( 60 , \_n_noop( '%s minute', '%s minutes' ) ),
			array(  1 , \_n_noop( '%s second', '%s seconds' ) ),
		);

		// we only want to output two chunks of time here, eg:
		// x years, xx months
		// x days, xx hours
		// so there's only two bits of calculation below:

		// step one: the first chunk
		for ( $i = 0, $j = count( $chunks ); $i < $j; $i++ ) {
			$seconds = $chunks[$i][0];
			$name = $chunks[$i][1];

			// finding the biggest chunk (if the chunk fits, break)
			if ( ( $count = floor( $since / $seconds ) ) != 0 ){
				break;
			}
		}

		// set output var
		$output = sprintf( \_n( $name[0], $name[1], $count ), $count );

		// step two: the second chunk
		if ( $i + 1 < $j ) {
			$seconds2 = $chunks[$i + 1][0];
			$name2    = $chunks[$i + 1][1];

			if ( ( $count2 = floor( ( $since - ( $seconds * $count ) ) / $seconds2 ) ) != 0 ) {
				// add to output var
				$output .= ' ' . sprintf( \_n( $name2[0], $name2[1], $count2 ), $count2 );
			}
		}

		return $output;
	}
366
367
368
369
370
371
372
373
374
375
376
377

	/**
	 * Delete an event by ID
	 */
	private function delete_event_by_id( $args, $assoc_args ) {
		$jid = absint( $assoc_args['event_id'] );

		// Validate ID
		if ( ! $jid ) {
			\WP_CLI::error( __( 'Invalid event ID', 'automattic-cron-control' ) );
		}

378
		\WP_CLI::log( __( 'Locating event...', 'automattic-cron-control' ) . "\n" );
379

380
		// Look up full event object
381
		$event = \Automattic\WP\Cron_Control\get_event_by_id( $jid );
382

Erick Hitter's avatar
Erick Hitter committed
383
		if ( is_object( $event ) ) {
384
			// Warning about Internal Events
Erick Hitter's avatar
Erick Hitter committed
385
			if ( \Automattic\WP\Cron_Control\is_internal_event( $event->action ) ) {
386
387
388
				\WP_CLI::warning( __( 'This is an event created by the Cron Control plugin. It will recreated automatically.', 'automattic-cron-control' ) );
			}

389
390
391
392
			\WP_CLI::log( sprintf( __( 'Execution time: %s GMT', 'automattic-cron-control' ), date( TIME_FORMAT, $event->timestamp ) ) );
			\WP_CLI::log( sprintf( __( 'Action: %s', 'automattic-cron-control' ), $event->action ) );
			\WP_CLI::log( sprintf( __( 'Instance identifier: %s', 'automattic-cron-control' ), $event->instance ) );
			\WP_CLI::log( '' );
393
394
			\WP_CLI::confirm( sprintf( __( 'Are you sure you want to delete this event?', 'automattic-cron-control' ) ) );

Erick Hitter's avatar
Erick Hitter committed
395
			// Try to delete the item and provide some relevant output
Erick Hitter's avatar
Erick Hitter committed
396
397
398
			\Automattic\WP\Cron_Control\_suspend_event_creation();
			$deleted = \Automattic\WP\Cron_Control\delete_event_by_id( $event->ID, true );
			\Automattic\WP\Cron_Control\_resume_event_creation();
399

Erick Hitter's avatar
Erick Hitter committed
400
			if ( false === $deleted ) {
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
				\WP_CLI::error( sprintf( __( 'Failed to delete event %d', 'automattic-cron-control' ), $jid ) );
			} else {
				\Automattic\WP\Cron_Control\_flush_internal_caches();
				\WP_CLI::success( sprintf( __( 'Removed event %d', 'automattic-cron-control' ), $jid ) );
				return;
			}
		}

		\WP_CLI::error( sprintf( __( 'Failed to delete event %d. Please confirm that the entry exists and that the ID is that of an event.', 'automattic-cron-control' ), $jid ) );
	}

	/**
	 * Delete all events of the same action
	 */
	private function delete_event_by_action( $args, $assoc_args ) {
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
		$action = $assoc_args['action'];

		// Validate entry
		if ( empty( $action ) ) {
			\WP_CLI::error( __( 'Invalid action', 'automattic-cron-control' ) );
		}

		// Warning about Internal Events
		if ( \Automattic\WP\Cron_Control\is_internal_event( $action ) ) {
			\WP_CLI::warning( __( 'This is an event created by the Cron Control plugin. It will recreated automatically.', 'automattic-cron-control' ) );
		}

		// Set defaults needed to gather all events
		$assoc_args['page']  = 1;
		$assoc_args['limit'] = 50;

		// Gather events
433
		\WP_CLI::log( __( 'Gathering events...', 'automattic-cron-control' ) );
434
435
436
437
438

		$events_to_delete = array();

		$events = $this->get_events( $args, $assoc_args );

439
		\WP_CLI::log( sprintf( _n( 'Found one event to check', 'Found %s events to check', $events['total_items'], 'automattic-cron-control' ), number_format_i18n( $events['total_items'] ) ) );
440
441
442
443
444
445
446
447
448
449
450

		$search_progress = \WP_CLI\Utils\make_progress_bar( sprintf( __( 'Searching events for those with the action `%s`', 'automattic-cron-control' ), $action ), $events['total_items'] );

		// Loop and pull out events to be deleted
		do {
			if ( ! is_array( $events ) || empty( $events['items'] ) ) {
				break;
			}

			// Check events for those that should be deleted
			foreach ( $events['items'] as $single_event ) {
Erick Hitter's avatar
Erick Hitter committed
451
452
				if ( $single_event->action === $action ) {
					$events_to_delete[] = (array) $single_event;
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
				}

				$search_progress->tick();
			}

			// Proceed to next batch
			$assoc_args['page']++;

			if ( $assoc_args['page'] > $events['total_pages'] ) {
				break;
			}

			$events = $this->get_events( $args, $assoc_args );
		} while( $events['page'] <= $events['total_pages'] );

		$search_progress->finish();

470
		\WP_CLI::log( '' );
471
472
473
474
475
476
477
478
479

		// Nothing more to do
		if ( empty( $events_to_delete ) ) {
			\WP_CLI::error( sprintf( __( 'No events with action `%s` found', 'automattic-cron-control' ), $action ) );
		}

		// List the items to remove
		$total_to_delete = count( $events_to_delete );

480
		\WP_CLI::log( sprintf( _n( 'Found one event with action `%2$s`:', 'Found %1$s events with action `%2$s`:', $total_to_delete, 'automattic-cron-control' ), number_format_i18n( $total_to_delete ), $action ) );
481
482

		if ( $total_to_delete <= $assoc_args['limit'] ) {
483
484
485
486
487
488
			// Sort results
			if ( ! empty( $events_to_delete ) ) {
				usort( $events_to_delete, array( $this, 'sort_events' ) );
			}


489
490
			\WP_CLI\Utils\format_items( 'table', $events_to_delete, array(
				'ID',
Erick Hitter's avatar
Erick Hitter committed
491
492
				'created',
				'last_modified',
493
494
495
496
497
498
499
				'timestamp',
				'instance',
			) );
		} else {
			\WP_CLI::warning( sprintf( __( 'Events are not displayed as there are more than %s to remove', 'automattic-cron-control' ), number_format_i18n( $assoc_args['limit'] ) ) );
		}

500
		\WP_CLI::log( '' );
501
502
503
504
505
506
507
508
		\WP_CLI::confirm( _n( 'Are you sure you want to delete this event?', 'Are you sure you want to delete these events?', $total_to_delete, 'automattic-cron-control' ) );

		// Remove the items
		$delete_progress = \WP_CLI\Utils\make_progress_bar( __( 'Deleting events', 'automattic-cron-control' ), $total_to_delete );

		$events_deleted       = array();
		$events_deleted_count = $events_failed_delete = 0;

509
		// Don't create new events while deleting events
Erick Hitter's avatar
Erick Hitter committed
510
		\Automattic\WP\Cron_Control\_suspend_event_creation();
511

512
		foreach ( $events_to_delete as $event_to_delete ) {
Erick Hitter's avatar
Erick Hitter committed
513
			$deleted = \Automattic\WP\Cron_Control\delete_event_by_id( $event_to_delete['ID'], false );
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535

			$events_deleted[] = array(
				'ID'      => $event_to_delete['ID'],
				'deleted' => false === $deleted ? 'no' : 'yes',
			);

			if ( $deleted ) {
				$events_deleted_count++;
			} else {
				$events_failed_delete++;
			}

			$delete_progress->tick();
		}

		$delete_progress->finish();

		// When deletes succeed, sync internal caches
		if ( $events_deleted_count > 0 ) {
			\Automattic\WP\Cron_Control\_flush_internal_caches();
		}

536
		// New events can be created now that removal is complete
Erick Hitter's avatar
Erick Hitter committed
537
		\Automattic\WP\Cron_Control\_resume_event_creation();
538

539
		// List the removed items
540
		\WP_CLI::log( "\n" . __( 'RESULTS:', 'automattic-cron-control' ) );
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561

		if ( 1 === $total_to_delete && 1 === $events_deleted_count ) {
			\WP_CLI::success( sprintf( __( 'Deleted one event: %d', 'automattic-cron-control' ), $events_deleted[0]['ID'] ) );
		} else {
			if ( $events_deleted_count === $total_to_delete ) {
				\WP_CLI::success( sprintf( __( 'Deleted %s events', 'automattic-cron-control' ), number_format_i18n( $events_deleted_count ) ) );
			} else {
				\WP_CLI::warning( sprintf( __( 'Expected to delete %1$s events, but could only delete %2$s events. It\'s likely that some events were executed while this command ran.', 'automattic-cron-control' ), number_format_i18n( $total_to_delete ), number_format_i18n( $events_deleted_count ) ) );
			}

			// Limit just to failed deletes when many events are removed
			if ( count( $events_deleted ) > $assoc_args['limit'] ) {
				$events_deleted = array_filter( $events_deleted, function( $event ) {
					if ( 'no' === $event['deleted'] ) {
						return $event;
					} else {
						return false;
					}
				} );

				if ( count( $events_deleted ) > 0 ) {
562
					\WP_CLI::log( "\n" . __( 'Events that couldn\'t be deleted:', 'automattic-cron-control' ) );
563
564
				}
			} else {
565
				\WP_CLI::log( "\n" . __( 'Events deleted:', 'automattic-cron-control' ) );
566
567
568
569
570
571
572
573
574
575
			}

			// Don't display a table if there's nothing to display
			if ( count( $events_deleted ) > 0 ) {
				\WP_CLI\Utils\format_items( 'table', $events_deleted, array(
					'ID',
					'deleted',
				) );
			}
		}
576
577
578

		return;
	}
579
580
581
582
583
584
585
586
587
588
589
590
591

	/**
	 * Delete all completed events
	 */
	private function delete_completed_events( $args, $assoc_args ) {
		$count = \Automattic\WP\Cron_Control\count_events_by_status( \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED );

		\WP_CLI::confirm( sprintf( _n( 'Found one completed event to remove. Continue?', 'Found %s completed events to remove. Continue?', $count, 'automattic-cron-control' ), number_format_i18n( $count ) ) );

		\Automattic\WP\Cron_Control\Events_Store::instance()->purge_completed_events( false );

		\WP_CLI::success( __( 'Entries removed', 'automattic-cron-control' ) );
	}
592
593
}

594
\WP_CLI::add_command( 'cron-control events', 'Automattic\WP\Cron_Control\CLI\Events' );