class-events.php 20.2 KB
Newer Older
1
<?php
Erick Hitter's avatar
Erick Hitter committed
2
3
4
5
6
/**
 * Manage events via WP-CLI
 *
 * @package a8c_Cron_Control
 */
7
8
9
10

namespace Automattic\WP\Cron_Control\CLI;

/**
11
 * Manage Cron Control's data
12
 */
13
class Events extends \WP_CLI_Command {
14
15
16
17
18
	/**
	 * List cron events
	 *
	 * Intentionally bypasses caching to ensure latest data is shown
	 *
19
	 * @subcommand list
20
	 * @synopsis [--status=<pending|completed>] [--page=<page>] [--limit=<limit>] [--format=<format>]
Erick Hitter's avatar
Erick Hitter committed
21
22
	 * @param array $args Array of positional arguments.
	 * @param array $assoc_args Array of flags.
23
	 */
24
	public function list_events( $args, $assoc_args ) {
25
		$events = $this->get_events( $args, $assoc_args );
26

Erick Hitter's avatar
Erick Hitter committed
27
28
		// Prevent one from requesting a page that doesn't exist.
		// Shouldn't error when first page is requested, though, as that is handled below and is an odd behaviour otherwise.
29
		if ( $events['page'] > $events['total_pages'] && $events['page'] > 1 ) {
30
31
32
			\WP_CLI::error( __( 'Invalid page requested', 'automattic-cron-control' ) );
		}

Erick Hitter's avatar
Erick Hitter committed
33
		// Output in the requested format.
34
35
36
		if ( isset( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) {
			echo implode( ' ', wp_list_pluck( $events['items'], 'ID' ) );
		} else {
Erick Hitter's avatar
Erick Hitter committed
37
			// Lest someone think the `completed` record should be...complete.
38
39
40
41
			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' ) );
			}

Erick Hitter's avatar
Erick Hitter committed
42
			// Not much to do.
43
44
			if ( 0 === $events['total_items'] || empty( $events['items'] ) ) {
				\WP_CLI::warning( __( 'No events to display', 'automattic-cron-control' ) );
45
46
47
				return;
			}

Erick Hitter's avatar
Erick Hitter committed
48
			// Prepare events for display.
49
50
51
			$events_for_display      = $this->format_events( $events['items'] );
			$total_events_to_display = count( $events_for_display );

Erick Hitter's avatar
Erick Hitter committed
52
			// Count, noting if showing fewer than all.
53
			if ( $events['total_items'] <= $total_events_to_display ) {
Erick Hitter's avatar
Erick Hitter committed
54
				\WP_CLI::log( sprintf( _n( 'Displaying %s entry', 'Displaying all %s entries', $total_events_to_display, 'automattic-cron-control' ), number_format_i18n( $total_events_to_display ) ) );
55
			} else {
56
				\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'] ) ) );
57
58
			}

Erick Hitter's avatar
Erick Hitter committed
59
			// And reformat!
60
61
62
63
			$format = 'table';
			if ( isset( $assoc_args['format'] ) ) {
				$format = $assoc_args['format'];
			}
Erick Hitter's avatar
Erick Hitter committed
64

65
66
67
68
69
70
			\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
71
				'last_updated_gmt',
72
				'recurrence',
73
				'internal_event',
74
				'schedule_name',
75
76
77
				'event_args',
			) );
		}
78
79
	}

80
	/**
81
	 * Remove events
82
83
	 *
	 * @subcommand delete
84
	 * @synopsis [--event_id=<event_id>] [--action=<action>] [--completed]
Erick Hitter's avatar
Erick Hitter committed
85
86
	 * @param array $args Array of positional arguments.
	 * @param array $assoc_args Array of flags.
87
88
	 */
	public function delete_events( $args, $assoc_args ) {
Erick Hitter's avatar
Erick Hitter committed
89
		// Remove a specific event.
90
91
		if ( isset( $assoc_args['event_id'] ) ) {
			$this->delete_event_by_id( $args, $assoc_args );
92
			return;
93
94
		}

Erick Hitter's avatar
Erick Hitter committed
95
		// Remove all events with a given action.
96
		if ( isset( $assoc_args['action'] ) ) {
Erick Hitter's avatar
Typo  
Erick Hitter committed
97
			$this->delete_event_by_action( $args, $assoc_args );
98
			return;
99
100
		}

Erick Hitter's avatar
Erick Hitter committed
101
		// Remove all completed events.
102
		if ( isset( $assoc_args['completed'] ) ) {
103
			$this->delete_completed_events( $args, $assoc_args );
104
105
106
			return;
		}

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

110
111
112
113
	/**
	 * Run an event given an ID
	 *
	 * @subcommand run
114
	 * @synopsis <event_id>
Erick Hitter's avatar
Erick Hitter committed
115
116
	 * @param array $args Array of positional arguments.
	 * @param array $assoc_args Array of flags.
117
118
	 */
	public function run_event( $args, $assoc_args ) {
Erick Hitter's avatar
Erick Hitter committed
119
		// Validate ID.
120
121
		if ( ! is_numeric( $args[0] ) ) {
			\WP_CLI::error( __( 'Specify the ID of an event to run', 'automattic-cron-control' ) );
122
123
		}

Erick Hitter's avatar
Erick Hitter committed
124
		// Retrieve information needed to execute event.
125
		$event = \Automattic\WP\Cron_Control\get_event_by_id( $args[0] );
126

Erick Hitter's avatar
Erick Hitter committed
127
		if ( ! is_object( $event ) ) {
128
			\WP_CLI::error( sprintf( __( 'Failed to locate event %d. Please confirm that the entry exists and that the ID is that of an event.', 'automattic-cron-control' ), $args[0] ) );
129
130
		}

131
		\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 ) );
132
133

		// Proceed?
134
		$now = time();
Erick Hitter's avatar
Erick Hitter committed
135
		if ( $event->timestamp > $now ) {
136
			\WP_CLI::warning( sprintf( __( 'This event is not scheduled to run until %1$s UTC (%2$s)', 'automattic-cron-control' ), date_i18n( TIME_FORMAT, $event->timestamp ), $this->calculate_interval( $event->timestamp - $now ) ) );
137
138
		}

139
		\WP_CLI::confirm( sprintf( __( 'Run this event?', 'automattic-cron-control' ) ) );
140

Erick Hitter's avatar
Erick Hitter committed
141
		// Environment preparation.
142
		\Automattic\WP\Cron_Control\set_doing_cron();
143

Erick Hitter's avatar
Erick Hitter committed
144
		// Run the event!
Erick Hitter's avatar
Erick Hitter committed
145
		$run = \Automattic\WP\Cron_Control\run_event( $event->timestamp, $event->action_hashed, $event->instance, true );
146

Erick Hitter's avatar
Erick Hitter committed
147
		// Output based on run attempt.
148
149
150
151
152
153
154
155
156
		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' ) );
		}
	}

157
158
	/**
	 * Retrieve list of events, and related data, for a given request
Erick Hitter's avatar
Erick Hitter committed
159
160
161
162
	 *
	 * @param array $args Array of positional arguments.
	 * @param array $assoc_args Array of flags.
	 * @return array
163
164
	 */
	private function get_events( $args, $assoc_args ) {
Erick Hitter's avatar
Erick Hitter committed
165
		// Accept a status argument, with a default.
166
167
168
169
170
		$status = 'pending';
		if ( isset( $assoc_args['status'] ) ) {
			$status = $assoc_args['status'];
		}

Erick Hitter's avatar
Erick Hitter committed
171
		// Convert to status used by Event Store.
172
		$event_status = null;
173
174
		switch ( $status ) {
			case 'pending' :
175
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_PENDING;
176
177
				break;

178
179
180
181
			case 'running' :
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_RUNNING;
				break;

182
			case 'completed' :
183
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED;
184
185
186
				break;
		}

187
188
189
190
191
192
		if ( is_null( $event_status ) ) {
			\WP_CLI::error( __( 'Invalid status specified', 'automattic-cron-control' ) );
		}

		unset( $status );

Erick Hitter's avatar
Erick Hitter committed
193
		// Total to show.
194
195
196
197
198
		$limit = 25;
		if ( isset( $assoc_args['limit'] ) && is_numeric( $assoc_args['limit'] ) ) {
			$limit = max( 1, min( absint( $assoc_args['limit'] ), 500 ) );
		}

Erick Hitter's avatar
Erick Hitter committed
199
		// Pagination.
200
201
202
203
204
205
206
		$page = 1;
		if ( isset( $assoc_args['page'] ) && is_numeric( $assoc_args['page'] ) ) {
			$page = absint( $assoc_args['page'] );
		}

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

Erick Hitter's avatar
Erick Hitter committed
207
		// Query!
Erick Hitter's avatar
Erick Hitter committed
208
		$items = \Automattic\WP\Cron_Control\get_events( array(
209
210
211
212
			'status'     => $event_status,
			'quantity'   => $limit,
			'page'       => $page,
			'force_sort' => true,
213
		) );
214

Erick Hitter's avatar
Erick Hitter committed
215
		// Bail if we don't get results!
216
217
218
219
		if ( ! is_array( $items ) ) {
			\WP_CLI::error( __( 'Problem retrieving events', 'automattic-cron-control' ) );
		}

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

Erick Hitter's avatar
Erick Hitter committed
224
		return compact( 'status', 'limit', 'page', 'offset', 'items', 'total_items', 'total_pages' );
225
226
227
	}

	/**
228
	 * Format event data into something human-readable
Erick Hitter's avatar
Erick Hitter committed
229
230
231
	 *
	 * @param array $events Array of events to reformat.
	 * @return array
232
	 */
233
234
235
	private function format_events( $events ) {
		$formatted_events = array();

Erick Hitter's avatar
Erick Hitter committed
236
		// Reformat events.
237
238
		foreach ( $events as $event ) {
			$row = array(
Erick Hitter's avatar
Erick Hitter committed
239
				'ID'                => $event->ID,
240
241
				'action'            => $event->action,
				'instance'          => $event->instance,
242
				'next_run_gmt'      => date_i18n( TIME_FORMAT, $event->timestamp ),
243
				'next_run_relative' => '',
244
				'last_updated_gmt'  => date_i18n( 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'        => '',
			);

Erick Hitter's avatar
Erick Hitter committed
251
			if ( \Automattic\WP\Cron_Control\Events_Store::STATUS_PENDING === $event->status ) {
252
				$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;
		}

Erick Hitter's avatar
Erick Hitter committed
275
		// Sort results.
276
277
278
279
		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
	/**
	 * Sort events by timestamp, then action name
Erick Hitter's avatar
Erick Hitter committed
285
286
287
288
	 *
	 * @param array $first First event to compare.
	 * @param array $second Second event to compare.
	 * @return int
289
290
	 */
	private function sort_events( $first, $second ) {
Erick Hitter's avatar
Erick Hitter committed
291
		// Timestamp is usually sufficient.
292
293
294
295
296
297
298
299
300
301
		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;
		}

302
303
		if ( $first_timestamp !== $second_timestamp ) {
			return $first_timestamp - $second_timestamp;
304
305
		}

Erick Hitter's avatar
Erick Hitter committed
306
		// If timestamps are equal, consider action.
307
		return strnatcasecmp( $first['action'], $second['action'] );
308
309
	}

Erick Hitter's avatar
Erick Hitter committed
310
311
312
313
314
315
316
	/**
	 * 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
	 *
Erick Hitter's avatar
Erick Hitter committed
317
318
	 * @param int $since An interval of time in seconds.
	 * @return string
Erick Hitter's avatar
Erick Hitter committed
319
320
321
322
323
324
325
326
	 */
	private function calculate_interval( $since ) {
		if ( $since <= 0 ) {
			return 'now';
		}

		$since = absint( $since );

Erick Hitter's avatar
Erick Hitter committed
327
		// array of time period chunks.
Erick Hitter's avatar
Erick Hitter committed
328
329
330
331
332
333
334
		$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' ) ),
Erick Hitter's avatar
Erick Hitter committed
335
			array( 01 , \_n_noop( '%s second', '%s seconds' ) ),
Erick Hitter's avatar
Erick Hitter committed
336
337
		);

Erick Hitter's avatar
Erick Hitter committed
338
339
340
341
342
343
		/**
		 * 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:
		 */
Erick Hitter's avatar
Erick Hitter committed
344

Erick Hitter's avatar
Erick Hitter committed
345
		// step one: the first chunk.
Erick Hitter's avatar
Erick Hitter committed
346
		for ( $i = 0, $j = count( $chunks ); $i < $j; $i++ ) {
Erick Hitter's avatar
Erick Hitter committed
347
348
			$seconds = $chunks[ $i ][0];
			$name = $chunks[ $i ][1];
Erick Hitter's avatar
Erick Hitter committed
349

Erick Hitter's avatar
Erick Hitter committed
350
351
			// finding the biggest chunk (if the chunk fits, break).
			if ( ( $count = floor( $since / $seconds ) ) != 0 ) {
Erick Hitter's avatar
Erick Hitter committed
352
353
354
355
				break;
			}
		}

Erick Hitter's avatar
Erick Hitter committed
356
357
		// set output var.
		$output = sprintf( \_n( $name[0], $name[1], $count ), $count ); // @codingStandardsIgnoreLine
Erick Hitter's avatar
Erick Hitter committed
358

Erick Hitter's avatar
Erick Hitter committed
359
		// step two: the second chunk.
Erick Hitter's avatar
Erick Hitter committed
360
		if ( $i + 1 < $j ) {
Erick Hitter's avatar
Erick Hitter committed
361
362
			$seconds2 = $chunks[ $i + 1 ][0];
			$name2    = $chunks[ $i + 1 ][1];
Erick Hitter's avatar
Erick Hitter committed
363
364

			if ( ( $count2 = floor( ( $since - ( $seconds * $count ) ) / $seconds2 ) ) != 0 ) {
Erick Hitter's avatar
Erick Hitter committed
365
366
				// add to output var.
				$output .= ' ' . sprintf( \_n( $name2[0], $name2[1], $count2 ), $count2 ); // @codingStandardsIgnoreLine
Erick Hitter's avatar
Erick Hitter committed
367
368
369
370
371
			}
		}

		return $output;
	}
372
373
374

	/**
	 * Delete an event by ID
Erick Hitter's avatar
Erick Hitter committed
375
376
377
	 *
	 * @param array $args Array of positional arguments.
	 * @param array $assoc_args Array of flags.
378
379
380
381
	 */
	private function delete_event_by_id( $args, $assoc_args ) {
		$jid = absint( $assoc_args['event_id'] );

Erick Hitter's avatar
Erick Hitter committed
382
		// Validate ID.
383
384
385
386
		if ( ! $jid ) {
			\WP_CLI::error( __( 'Invalid event ID', 'automattic-cron-control' ) );
		}

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

Erick Hitter's avatar
Erick Hitter committed
389
		// Look up full event object.
390
		$event = \Automattic\WP\Cron_Control\get_event_by_id( $jid );
391

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

398
			\WP_CLI::log( sprintf( __( 'Execution time: %s UTC', 'automattic-cron-control' ), date_i18n( TIME_FORMAT, $event->timestamp ) ) );
399
400
401
			\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( '' );
402
403
			\WP_CLI::confirm( sprintf( __( 'Are you sure you want to delete this event?', 'automattic-cron-control' ) ) );

Erick Hitter's avatar
Erick Hitter committed
404
			// Try to delete the item and provide some relevant output.
Erick Hitter's avatar
Erick Hitter committed
405
406
407
			\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();
408

Erick Hitter's avatar
Erick Hitter committed
409
			if ( false === $deleted ) {
410
411
412
413
414
415
416
417
418
419
420
421
422
				\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
Erick Hitter's avatar
Erick Hitter committed
423
424
425
	 *
	 * @param array $args Array of positional arguments.
	 * @param array $assoc_args Array of flags.
426
427
	 */
	private function delete_event_by_action( $args, $assoc_args ) {
428
429
		$action = $assoc_args['action'];

Erick Hitter's avatar
Erick Hitter committed
430
		// Validate entry.
431
432
433
434
		if ( empty( $action ) ) {
			\WP_CLI::error( __( 'Invalid action', 'automattic-cron-control' ) );
		}

Erick Hitter's avatar
Erick Hitter committed
435
		// Warning about Internal Events.
436
437
438
439
		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' ) );
		}

Erick Hitter's avatar
Erick Hitter committed
440
		// Set defaults needed to gather all events.
441
442
443
		$assoc_args['page']  = 1;
		$assoc_args['limit'] = 50;

Erick Hitter's avatar
Erick Hitter committed
444
		// Gather events.
445
		\WP_CLI::log( __( 'Gathering events...', 'automattic-cron-control' ) );
446
447
448
449
450

		$events_to_delete = array();

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

Erick Hitter's avatar
Erick Hitter committed
451
		\WP_CLI::log( sprintf( _n( 'Found %s event to check', 'Found %s events to check', $events['total_items'], 'automattic-cron-control' ), number_format_i18n( $events['total_items'] ) ) );
452
453
454

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

Erick Hitter's avatar
Erick Hitter committed
455
		// Loop and pull out events to be deleted.
456
457
458
459
460
		do {
			if ( ! is_array( $events ) || empty( $events['items'] ) ) {
				break;
			}

Erick Hitter's avatar
Erick Hitter committed
461
			// Check events for those that should be deleted.
462
			foreach ( $events['items'] as $single_event ) {
Erick Hitter's avatar
Erick Hitter committed
463
464
				if ( $single_event->action === $action ) {
					$events_to_delete[] = (array) $single_event;
465
466
467
468
469
				}

				$search_progress->tick();
			}

Erick Hitter's avatar
Erick Hitter committed
470
			// Proceed to next batch.
471
472
473
474
475
476
477
			$assoc_args['page']++;

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

			$events = $this->get_events( $args, $assoc_args );
Erick Hitter's avatar
Erick Hitter committed
478
		} while ( $events['page'] <= $events['total_pages'] );
479
480
481

		$search_progress->finish();

482
		\WP_CLI::log( '' );
483

Erick Hitter's avatar
Erick Hitter committed
484
		// Nothing more to do.
485
486
487
488
		if ( empty( $events_to_delete ) ) {
			\WP_CLI::error( sprintf( __( 'No events with action `%s` found', 'automattic-cron-control' ), $action ) );
		}

Erick Hitter's avatar
Erick Hitter committed
489
		// List the items to remove.
490
491
		$total_to_delete = count( $events_to_delete );

Erick Hitter's avatar
Erick Hitter committed
492
		\WP_CLI::log( sprintf( _n( 'Found %1$s 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 ) );
493
494

		if ( $total_to_delete <= $assoc_args['limit'] ) {
Erick Hitter's avatar
Erick Hitter committed
495
			// Sort results.
496
497
498
499
			if ( ! empty( $events_to_delete ) ) {
				usort( $events_to_delete, array( $this, 'sort_events' ) );
			}

500
501
			\WP_CLI\Utils\format_items( 'table', $events_to_delete, array(
				'ID',
Erick Hitter's avatar
Erick Hitter committed
502
503
				'created',
				'last_modified',
504
505
506
507
508
509
510
				'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'] ) ) );
		}

511
		\WP_CLI::log( '' );
512
513
		\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' ) );

Erick Hitter's avatar
Erick Hitter committed
514
		// Remove the items.
515
516
517
518
519
		$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;

Erick Hitter's avatar
Erick Hitter committed
520
		// Don't create new events while deleting events.
Erick Hitter's avatar
Erick Hitter committed
521
		\Automattic\WP\Cron_Control\_suspend_event_creation();
522

523
		foreach ( $events_to_delete as $event_to_delete ) {
Erick Hitter's avatar
Erick Hitter committed
524
			$deleted = \Automattic\WP\Cron_Control\delete_event_by_id( $event_to_delete['ID'], false );
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541

			$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();

Erick Hitter's avatar
Erick Hitter committed
542
		// When deletes succeed, sync internal caches.
543
544
545
546
		if ( $events_deleted_count > 0 ) {
			\Automattic\WP\Cron_Control\_flush_internal_caches();
		}

Erick Hitter's avatar
Erick Hitter committed
547
		// New events can be created now that removal is complete.
Erick Hitter's avatar
Erick Hitter committed
548
		\Automattic\WP\Cron_Control\_resume_event_creation();
549

Erick Hitter's avatar
Erick Hitter committed
550
		// List the removed items.
551
		\WP_CLI::log( "\n" . __( 'RESULTS:', 'automattic-cron-control' ) );
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 ) ) );
			}

Erick Hitter's avatar
Erick Hitter committed
562
			// Limit just to failed deletes when many events are removed.
563
564
565
566
567
568
569
570
571
572
			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 ) {
573
					\WP_CLI::log( "\n" . __( 'Events that couldn\'t be deleted:', 'automattic-cron-control' ) );
574
575
				}
			} else {
576
				\WP_CLI::log( "\n" . __( 'Events deleted:', 'automattic-cron-control' ) );
577
578
			}

Erick Hitter's avatar
Erick Hitter committed
579
			// Don't display a table if there's nothing to display.
580
581
582
583
584
585
586
			if ( count( $events_deleted ) > 0 ) {
				\WP_CLI\Utils\format_items( 'table', $events_deleted, array(
					'ID',
					'deleted',
				) );
			}
		}
587
588
589

		return;
	}
590
591
592

	/**
	 * Delete all completed events
Erick Hitter's avatar
Erick Hitter committed
593
594
595
	 *
	 * @param array $args Array of positional arguments.
	 * @param array $assoc_args Array of flags.
596
597
598
599
	 */
	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 );

Erick Hitter's avatar
Erick Hitter committed
600
		\WP_CLI::confirm( sprintf( _n( 'Found %s completed event to remove. Continue?', 'Found %s completed events to remove. Continue?', $count, 'automattic-cron-control' ), number_format_i18n( $count ) ) );
601
602
603
604
605

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

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

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