class-events-store.php 19.4 KB
Newer Older
Erick Hitter's avatar
Erick Hitter committed
1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace Automattic\WP\Cron_Control;

class Events_Store extends Singleton {
	/**
	 * PLUGIN SETUP
	 */

	/**
	 * Class properties
	 */
13
	const TABLE_SUFFIX = 'a8c_cron_control_jobs';
14

15
16
17
	const DB_VERSION        = 1;
	const DB_VERSION_OPTION = 'a8c_cron_control_db_version';
	const TABLE_CREATE_LOCK = 'a8c_cron_control_creating_table';
18

19
20
21
	const STATUS_PENDING   = 'pending';
	const STATUS_RUNNING   = 'running';
	const STATUS_COMPLETED = 'complete';
22
23
24
25
26
	const ALLOWED_STATUSES = array(
		self::STATUS_PENDING,
		self::STATUS_RUNNING,
		self::STATUS_COMPLETED,
	);
27
28
29
30

	const CACHE_KEY = 'a8c_cron_ctrl_option';

	private $job_creation_suspended = false;
Erick Hitter's avatar
Erick Hitter committed
31
32
33
34

	/**
	 * Register hooks
	 */
35
	protected function class_init() {
36
37
		// Create tables during installation
		add_action( 'wp_install', array( $this, 'create_table_during_install' ) );
Erick Hitter's avatar
Erick Hitter committed
38
		add_action( 'wpmu_new_blog', array( $this, 'create_tables_during_multisite_install' ) );
39

40
41
42
		// Remove table when a multisite subsite is deleted
		add_filter( 'wpmu_drop_tables', array( $this, 'remove_multisite_table' ) );

43
		// Enable plugin when conditions support it, otherwise limit errors as much as possible
44
		if ( self::is_installed() ) {
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
			// Option interception
			add_filter( 'pre_option_cron', array( $this, 'get_option' ) );
			add_filter( 'pre_update_option_cron', array( $this, 'update_option' ), 10, 2 );

			// Disallow duplicates
			add_filter( 'schedule_event', array( $this, 'block_creation_if_job_exists' ) );
		} else {
			// Can't create events since there's no table to hold them
			$this->suspend_event_creation();

			// Prime plugin's options when the options table exists
			if ( ! defined( 'WP_INSTALLING' ) || ! WP_INSTALLING ) {
				$this->prime_options();
			}

			// Don't schedule events that won't be run
			add_filter( 'schedule_event', '__return_false' );
62
63

			// In limited circumstances, try creating the table
64
			add_action( 'shutdown', array( $this, 'maybe_create_table_on_shutdown' ) );
65
		}
66
67
	}

68
69
70
71
72
73
74
75
76
77
78
79
80
	/**
	 * Check if events store is ready
	 *
	 * Plugin breaks spectacularly if events store isn't available
	 *
	 * @return bool
	 */
	public static function is_installed() {
		$db_version = (int) get_option( self::DB_VERSION_OPTION );

		return version_compare( $db_version, 0, '>' );
	}

81
82
83
	/**
	 * Build appropriate table name for this install
	 */
84
	public function get_table_name() {
85
86
		global $wpdb;

87
		return $wpdb->prefix . self::TABLE_SUFFIX;
88
89
90
	}

	/**
91
92
93
94
95
96
97
98
	 * Set initial options that control plugin's behaviour
	 */
	protected function prime_options() {
		// Prime DB option
		add_option( self::DB_VERSION_OPTION, 0, null, false );
	}

	/**
Erick Hitter's avatar
Erick Hitter committed
99
	 * Create table during initial install
100
	 */
101
102
103
104
105
106
107
108
	public function create_table_during_install() {
		if ( 'wp_install' !== current_action() ) {
			return;
		}

		$this->_prepare_table();
	}

Erick Hitter's avatar
Erick Hitter committed
109
110
111
112
113
114
115
	/**
	 * Create table when new subsite is added to a multisite
	 */
	public function create_tables_during_multisite_install( $bid ) {
		switch_to_blog( $bid );

		if ( ! self::is_installed() ) {
116
			$this->_prepare_table();
Erick Hitter's avatar
Erick Hitter committed
117
118
119
120
121
		}

		restore_current_blog();
	}

122
123
124
125
126
	/**
	 * For certain requests, create the table on shutdown
	 * Does not include front-end requests
	 */
	public function maybe_create_table_on_shutdown() {
127
		if ( ! is_admin() && ! is_rest_endpoint_request( REST_API::ENDPOINT_LIST ) ) {
128
129
130
131
132
133
			return;
		}

		$this->prepare_table();
	}

134
135
136
	/**
	 * Create table in non-setup contexts, with some protections
	 */
137
	public function prepare_table() {
138
		// Table installed
139
		if ( self::is_installed() ) {
140
141
142
			return;
		}

143
		// Nothing to do
144
145
		$current_version = (int) get_option( self::DB_VERSION_OPTION );
		if ( version_compare( $current_version, self::DB_VERSION, '>=' ) ) {
146
147
148
			return;
		}

149
		// Limit chance of race conditions when creating table
Erick Hitter's avatar
Erick Hitter committed
150
151
		$create_lock_set = wp_cache_add( self::TABLE_CREATE_LOCK, 1, null, 1 * \MINUTE_IN_SECONDS );
		if ( false === $create_lock_set ) {
152
153
154
			return;
		}

155
156
157
158
159
160
161
		$this->_prepare_table();
	}

	/**
	 * Create the plugin's DB table when necessary
	 */
	protected function _prepare_table() {
162
163
164
165
166
167
168
		// Use Core's method of creating/updating tables
		if ( ! function_exists( 'dbDelta' ) ) {
			require_once ABSPATH . '/wp-admin/includes/upgrade.php';
		}

		global $wpdb;

Erick Hitter's avatar
Erick Hitter committed
169
170
		$table_name = $this->get_table_name();

171
		// Define schema and create the table
Erick Hitter's avatar
Erick Hitter committed
172
		$schema = "CREATE TABLE `{$table_name}` (
173
			`ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
174
175
176

			`timestamp` bigint(20) unsigned NOT NULL,
			`action` varchar(255) NOT NULL,
177
			`action_hashed` varchar(32) NOT NULL,
178
179
180
181
			`instance` varchar(32) NOT NULL,

			`args` longtext NOT NULL,
			`schedule` varchar(255) DEFAULT NULL,
182
183
			`interval` int unsigned DEFAULT 0,
			`status` varchar(32) NOT NULL DEFAULT 'pending',
184
185
186
187

			`created` datetime NOT NULL,
			`last_modified` datetime NOT NULL,

Erick Hitter's avatar
Erick Hitter committed
188
			PRIMARY KEY (`ID`),
Erick Hitter's avatar
Erick Hitter committed
189
190
			UNIQUE KEY `ts_action_instance_status` (`timestamp`, `action` (191), `instance`, `status`),
			KEY `status` (`status`)
191
192
193
194
195
		) ENGINE=InnoDB;\n";

		dbDelta( $schema, true );

		// Confirm that the table was created, and set the option to prevent further updates
Erick Hitter's avatar
Erick Hitter committed
196
		$table_count = count( $wpdb->get_col( "SHOW TABLES LIKE '{$table_name}'" ) );
197
198

		if ( 1 === $table_count ) {
199
			update_option( self::DB_VERSION_OPTION, self::DB_VERSION );
200
		}
201
202
203

		// Clear caches now that table exists
		$this->flush_internal_caches();
204
	}
205

206
207
208
209
210
211
212
213
214
215
216
	/**
	 * Prepare table on demand via CLI
	 */
	public function cli_create_tables() {
		if ( ! defined( 'WP_CLI' ) || ! \WP_CLI ) {
			return;
		}

		$this->_prepare_table();
	}

217
218
219
220
221
222
223
224
225
226
227
228
229
230
	/**
	 * When deleting a subsite from a multisite instance, include the plugin's table
	 *
	 * Core only drops its tables
	 *
	 * @param  array $tables_to_drop Array of prefixed table names to drop
	 * @return array
	 */
	public function remove_multisite_table( $tables_to_drop ) {
		$tables_to_drop[] = $this->get_table_name();

		return $tables_to_drop;
	}

231
232
233
234
235
	/**
	 * PLUGIN FUNCTIONALITY
	 */

	/**
236
	 * Override cron option requests with data from custom table
237
238
	 */
	public function get_option() {
239
240
		// Use cached value when available
		$cached_option = wp_cache_get( self::CACHE_KEY, null, true );
241

242
243
		if ( false !== $cached_option ) {
			return $cached_option;
244
245
246
247
248
249
250
251
		}

		// Start building a new cron option
		$cron_array = array(
			'version' => 2, // Core versions the cron array; without this, events will continually requeue
		);

		// Get events to re-render as the cron option
252
253
		$page     = 1;
		$quantity = 100;
254
255

		do {
256
			$jobs = $this->get_jobs( array(
257
				'status'   => self::STATUS_PENDING,
258
				'quantity' => $quantity,
259
				'page'     => $page++,
260
261
262
			) );

			// Nothing more to add
263
			if ( empty( $jobs ) ) {
264
265
266
267
				break;
			}

			// Loop through results and built output Core expects
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
			foreach ( $jobs as $job ) {
				// Alias event timestamp
				$timestamp = $job->timestamp;

				// If timestamp is invalid, event is removed to let its source fix it
				if ( $timestamp <= 0 ) {
					$this->mark_job_record_completed( $job->ID );
					continue;
				}

				// Basic arguments to add a job to the array format Core expects
				$action   = $job->action;
				$instance = $job->instance;

				// Populate remaining job data
				$cron_array[ $timestamp ][ $action ][ $instance ] = array(
					'schedule' => $job->schedule,
					'args'     => $job->args,
					'interval' => 0,
				);

				if ( isset( $job->interval ) ) {
					$cron_array[ $timestamp ][ $action ][ $instance ]['interval'] = $job->interval;
291
				}
292
			}
293
		} while( count( $jobs ) >= $quantity );
294
295
296
297
298

		// Re-sort the array just as Core does when events are scheduled
		// Ensures events are sorted chronologically
		uksort( $cron_array, 'strnatcasecmp' );

Erick Hitter's avatar
Erick Hitter committed
299
		// Cache the results
300
301
302
303
304
305
306
307
308
309
310
		wp_cache_set( self::CACHE_KEY, $cron_array, null, 1 * \HOUR_IN_SECONDS );

		return $cron_array;
	}

	/**
	 * Handle requests to update the cron option
	 *
	 * By returning $old_value, `cron` option won't be updated
	 */
	public function update_option( $new_value, $old_value ) {
311
312
313
		// Find changes to record
		$new_events     = $this->find_cron_array_differences( $new_value, $old_value );
		$deleted_events = $this->find_cron_array_differences( $old_value, $new_value );
314

315
316
		// Add/update new events
		foreach ( $new_events as $new_event ) {
317
			$job_id = $this->get_job_id( $new_event['timestamp'], $new_event['action'], $new_event['instance'] );
318

319
320
321
			if ( 0 === $job_id ) {
				$job_id = null;
			}
322

323
			$this->create_or_update_job( $new_event['timestamp'], $new_event['action'], $new_event['args'], $job_id, false );
324
325
		}

326
327
		// Mark deleted entries for removal
		foreach ( $deleted_events as $deleted_event ) {
328
			$this->mark_job_completed( $deleted_event['timestamp'], $deleted_event['action'], $deleted_event['instance'], false );
329
		}
330

331
332
		$this->flush_internal_caches();

333
		return $old_value;
334
335
	}

Erick Hitter's avatar
Erick Hitter committed
336
337
338
339
	/**
	 * When an entry exists, don't try to create it again
	 */
	public function block_creation_if_job_exists( $job ) {
340
341
342
343
344
		// Job already disallowed, carry on
		if ( ! is_object( $job ) ) {
			return $job;
		}

Erick Hitter's avatar
Erick Hitter committed
345
		$instance = md5( maybe_serialize( $job->args ) );
346
		if ( 0 !== $this->get_job_id( $job->timestamp, $job->hook, $instance ) ) {
Erick Hitter's avatar
Erick Hitter committed
347
348
349
350
351
352
			return false;
		}

		return $job;
	}

353
354
355
356
357
	/**
	 * PLUGIN UTILITY METHODS
	 */

	/**
Erick Hitter's avatar
Erick Hitter committed
358
359
360
	 * Retrieve jobs given a set of parameters
	 *
	 * @param array $args
361
	 * @return array
362
	 */
363
	public function get_jobs( $args ) {
364
365
366
367
368
369
370
371
372
373
374
375
376
		global $wpdb;

		if ( ! isset( $args['quantity'] ) || ! is_numeric( $args['quantity'] ) ) {
			$args['quantity'] = 100;
		}

		if ( isset( $args['page'] ) ) {
			$page  = max( 0, $args['page'] - 1 );
			$offset = $page * $args['quantity'];
		} else {
			$offset = 0;
		}

Erick Hitter's avatar
Erick Hitter committed
377
378
		// Do not sort, otherwise index isn't used
		$jobs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$this->get_table_name()} WHERE status = %s LIMIT %d,%d;", $args['status'], $offset, $args['quantity'] ), 'OBJECT' );
379
380
381
382

		if ( is_array( $jobs ) ) {
			$jobs = array_map( array( $this, 'format_job' ), $jobs );
		} else {
383
			$jobs = array();
384
385
386
		}

		return $jobs;
387
388
	}

389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
	/**
	 * Retrieve a single event by its ID
	 *
	 * @param  int $jid Job ID
	 * @return object|false
	 */
	public function get_job_by_id( $jid ) {
		global $wpdb;

		// Validate ID
		$jid = absint( $jid );
		if ( ! $jid ) {
			return false;
		}

		$job = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->get_table_name()} WHERE ID = %d AND status = %s LIMIT 1", $jid, self::STATUS_PENDING ) );

		if ( is_object( $job ) && ! is_wp_error( $job ) ) {
			$job = $this->format_job( $job );
		} else {
			$job = false;
		}

		return $job;
	}

415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
	/**
	 * Retrieve a single event by a combination of its timestamp, instance identifier, and either action or the action's hashed representation
	 *
	 * @param  array $attrs Array of event attributes to query by
	 * @return object|false
	 */
	public function get_job_by_attributes( $attrs ) {
		global $wpdb;

		// Validate basic inputs
		if ( ! is_array( $attrs ) || empty( $attrs ) ) {
			return false;
		}

		// Validate requested status
		$allowed_status = self::ALLOWED_STATUSES;
		$allowed_status[] = 'any';

		if ( ! isset( $attrs['status'] ) || ! in_array( $attrs['status'], $allowed_status, true ) ) {
			$attrs['status'] = self::STATUS_PENDING;
		}

		// Need a timestamp, an instance, and either an action or its hashed representation
		if ( ! isset( $attrs['timestamp'] ) || ! isset( $attrs['instance'] ) ) {
			return false;
		} elseif ( ! isset( $attrs['action'] ) && ! isset( $attrs['action_hashed'] ) ) {
			return false;
		}

		// Build query
		if ( isset( $attrs['action'] ) ) {
			$action_column = 'action';
			$action_value  = $attrs['action'];
		} else {
			$action_column = 'action_hashed';
			$action_value  = $attrs['action_hashed'];
		}

		// Do not sort, otherwise index isn't used
		$query = $wpdb->prepare( "SELECT * FROM {$this->get_table_name()} WHERE timestamp = %d AND {$action_column} = %s AND instance = %s", $attrs['timestamp'], $action_value, $attrs['instance'] );

		// Final query preparations
		if ( 'any' !== $attrs['status'] ) {
			$query .= $wpdb->prepare( ' AND status = %s', $attrs['status'] );
		}

		$query .= ' LIMIT 1';

		// Query and format results
		$job = $wpdb->get_row( $query );

		if ( is_object( $job ) && ! is_wp_error( $job ) ) {
			$job = $this->format_job( $job );
		} else {
			$job = false;
		}

		return $job;
	}

475
476
477
	/**
	 * Get ID for given event details
	 *
478
479
	 * Used in situations where performance matters, which is why it exists despite duplicating `get_job_by_attributes()`
	 * Queries outside of this class should use `get_job_by_attributes()`
480
481
482
483
484
485
486
487
488
489
490
491
492
493
	 *
	 * @param  int    $timestamp    Unix timestamp event executes at
	 * @param  string $action       Name of action used when the event is registered (unhashed)
	 * @param  string $instance     md5 hash of the event's arguments array, which Core uses to index the `cron` option
	 * @return int
	 */
	private function get_job_id( $timestamp, $action, $instance ) {
		global $wpdb;

		$job = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$this->get_table_name()} WHERE timestamp = %d AND action = %s AND instance = %s AND status = %s LIMIT 1;", $timestamp, $action, $instance, self::STATUS_PENDING ) );

		return empty( $job ) ? 0 : (int) array_shift( $job );
	}

494
495
	/**
	 * Standardize formatting and expand serialized data
496
497
498
	 *
	 * @param  object $job Job row from DB, in object form
	 * @return object
499
	 */
500
501
502
	private function format_job( $job ) {
		if ( ! is_object( $job ) || is_wp_error( $job ) ) {
			return $job;
503
504
		}

505
506
507
508
		$job->ID        = (int) $job->ID;
		$job->timestamp = (int) $job->timestamp;
		$job->interval  = (int) $job->interval;
		$job->args      = maybe_unserialize( $job->args );
509

510
511
512
513
		if ( empty( $job->schedule ) ) {
			$job->schedule = false;
		}

514
		return $job;
515
516
517
	}

	/**
518
	 * Create or update entry for a given job
519
520
521
522
523
524
	 *
	 * @param int    $timestamp    Unix timestamp event executes at
	 * @param string $action       Hook event fires
	 * @param array  $args         Array of event's schedule, arguments, and interval
	 * @param bool   $update_id    ID of existing entry to update, rather than creating a new entry
	 * @param bool   $flush_cache  Whether or not to flush internal caches after creating/updating the event
525
	 */
526
	public function create_or_update_job( $timestamp, $action, $args, $update_id = null, $flush_cache = true ) {
527
528
529
530
531
532
533
534
535
536
		// Don't create new jobs when manipulating jobs via the plugin's CLI commands
		if ( $this->job_creation_suspended ) {
			return;
		}

		global $wpdb;

		$job_post = array(
			'timestamp'     => $timestamp,
			'action'        => $action,
537
			'action_hashed' => md5( $action ),
538
			'instance'      => md5( maybe_serialize( $args['args'] ) ),
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
			'args'          => maybe_serialize( $args['args'] ),
			'last_modified' => current_time( 'mysql', true ),
		);

		if ( isset( $args['schedule'] ) && ! empty( $args['schedule'] ) ) {
			$job_post['schedule'] = $args['schedule'];
		}

		if ( isset( $args['interval'] ) && ! empty( $args['interval'] ) && is_numeric( $args['interval'] ) ) {
			$job_post['interval'] = (int) $args['interval'];
		}

		// Create the post, or update an existing entry to run again in the future
		if ( is_int( $update_id ) && $update_id > 0 ) {
			$wpdb->update( $this->get_table_name(), $job_post, array( 'ID' => $update_id, ) );
		} else {
Erick Hitter's avatar
Erick Hitter committed
555
			$job_post['created'] = current_time( 'mysql', true );
556
557
558
559

			$wpdb->insert( $this->get_table_name(), $job_post );
		}

560
561
562
563
564
		// Delete internal cache
		// Should only be skipped during bulk operations
		if ( $flush_cache ) {
			$this->flush_internal_caches();
		}
565
566
567
	}

	/**
568
	 * Mark an event's entry as completed
569
570
571
	 *
	 * Completed entries will be cleaned up by an internal job
	 *
572
573
574
575
	 * @param int    $timestamp    Unix timestamp event executes at
	 * @param string $action       Name of action used when the event is registered (unhashed)
	 * @param string $instance     md5 hash of the event's arguments array, which Core uses to index the `cron` option
	 * @param bool   $flush_cache  Whether or not to flush internal caches after creating/updating the event
576
577
	 * @return bool
	 */
578
	public function mark_job_completed( $timestamp, $action, $instance, $flush_cache = true ) {
579
		$job_id = $this->get_job_id( $timestamp, $action, $instance );
580
581
582
583
584

		if ( ! $job_id ) {
			return false;
		}

585
		return $this->mark_job_record_completed( $job_id, $flush_cache );
586
587
588
589
	}

	/**
	 * Set a job post to the "completed" status
590
591
592
593
	 *
	 * @param int $job_id        ID of job's record
	 * @param bool $flush_cache  Whether or not to flush internal caches after creating/updating the event
	 * @return bool
594
	 */
Erick Hitter's avatar
Erick Hitter committed
595
	public function mark_job_record_completed( $job_id, $flush_cache = true ) {
596
597
		global $wpdb;

598
599
600
601
602
603
604
605
		/**
		 * Constraint is broken to accommodate the following situation:
		 * 1. Event with specific timestamp is scheduled.
		 * 2. Event is unscheduled.
		 * 3. Event is rescheduled.
		 * 4. Event runs, or is unscheduled, but unique constraint prevents query from succeeding.
		 * 5. Event retains `pending` status and runs again. Repeat steps 4 and 5 until `a8c_cron_control_purge_completed_events` runs and removes the entry from step 2.
		 */
606
607
		$updates = array(
			'status'   => self::STATUS_COMPLETED,
608
			'instance' => mt_rand( 1000000, 999999999 ), // Breaks unique constraint, and can be recreated from entry's remaining data
609
610
611
		);

		$success = $wpdb->update( $this->get_table_name(), $updates, array( 'ID' => $job_id, ) );
612
613

		// Delete internal cache
614
		// Should only be skipped during bulk operations
615
		if ( $flush_cache ) {
Erick Hitter's avatar
Erick Hitter committed
616
			$this->flush_internal_caches();
617
618
		}

Erick Hitter's avatar
Erick Hitter committed
619
		return (bool) $success;
620
621
	}

622
	/**
623
	 * Compare two arrays and return collapsed representation of the items present in one but not the other
624
	 *
625
626
	 * @param array $changed   Array to identify additional items from
	 * @param array $reference Array to compare against
627
	 *
628
	 * @return array
629
	 */
630
	private function find_cron_array_differences( $changed, $reference ) {
631
632
		$differences = array();

633
		$changed = collapse_events_array( $changed );
634

635
		foreach ( $changed as $event ) {
636
			$event = (object) $event;
637

638
			if ( ! isset( $reference[ $event->timestamp ][ $event->action ][ $event->instance ] ) ) {
639
				$differences[] = array(
640
641
642
643
					'timestamp' => $event->timestamp,
					'action'    => $event->action,
					'instance'  => $event->instance,
					'args'      => $event->args,
644
645
646
647
648
649
650
				);
			}
		}

		return $differences;
	}

Erick Hitter's avatar
Erick Hitter committed
651
652
653
654
655
656
657
	/**
	 * Delete the cached representation of the cron option
	 */
	public function flush_internal_caches() {
		return wp_cache_delete( self::CACHE_KEY );
	}

658
	/**
659
	 * Prevent event store from creating new entries
660
661
662
663
664
665
666
667
	 *
	 * Should be used sparingly, and followed by a call to resume_event_creation(), during bulk operations
	 */
	public function suspend_event_creation() {
		$this->job_creation_suspended = true;
	}

	/**
668
	 * Stop discarding events, once again storing them in the table
669
670
671
672
673
674
675
676
	 */
	public function resume_event_creation() {
		$this->job_creation_suspended = false;
	}

	/**
	 * Remove entries for non-recurring events that have been run
	 */
677
	public function purge_completed_events( $count_first = true ) {
678
679
		global $wpdb;

680
681
		// Skip count if already performed
		if ( $count_first ) {
682
683
684
685
686
			if ( property_exists( $wpdb, 'srtm' ) ) {
				$srtm = $wpdb->srtm;
				$wpdb->srtm = true;
			}

687
			$count = $this->count_events_by_status( self::STATUS_COMPLETED );
688
689
690
691

			if ( isset( $srtm ) ) {
				$wpdb->srtm = $srtm;
			}
692
693
694
695
696
697
698
		} else {
			$count = 1;
		}

		if ( $count > 0 ) {
			$wpdb->delete( $this->get_table_name(), array( 'status' => self::STATUS_COMPLETED, ) );
		}
699
	}
Erick Hitter's avatar
Erick Hitter committed
700
701
702
703
704
705
706
707
708
709

	/**
	 * Count number of events with a given status
	 *
	 * @param string $status
	 * @return int|false
	 */
	public function count_events_by_status( $status ) {
		global $wpdb;

710
		if ( ! in_array( $status, self::ALLOWED_STATUSES, true ) ) {
Erick Hitter's avatar
Erick Hitter committed
711
712
713
			return false;
		}

714
		return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM {$this->get_table_name()} WHERE status = %s", $status ) );
Erick Hitter's avatar
Erick Hitter committed
715
	}
Erick Hitter's avatar
Erick Hitter committed
716
717
718
}

Events_Store::instance();