class-events-store.php 18.9 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
		// Enable plugin when conditions support it, otherwise limit errors as much as possible
41
		if ( self::is_installed() ) {
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
			// 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' );
59
60

			// In limited circumstances, try creating the table
61
			add_action( 'shutdown', array( $this, 'maybe_create_table_on_shutdown' ) );
62
		}
63
64
	}

65
66
67
68
69
70
71
72
73
74
75
76
77
	/**
	 * 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, '>' );
	}

78
79
80
	/**
	 * Build appropriate table name for this install
	 */
81
	public function get_table_name() {
82
83
		global $wpdb;

84
		return $wpdb->prefix . self::TABLE_SUFFIX;
85
86
87
	}

	/**
88
89
90
91
92
93
94
95
	 * 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
96
	 * Create table during initial install
97
	 */
98
99
100
101
102
103
104
105
	public function create_table_during_install() {
		if ( 'wp_install' !== current_action() ) {
			return;
		}

		$this->_prepare_table();
	}

Erick Hitter's avatar
Erick Hitter committed
106
107
108
109
110
111
112
	/**
	 * 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() ) {
113
			$this->_prepare_table();
Erick Hitter's avatar
Erick Hitter committed
114
115
116
117
118
		}

		restore_current_blog();
	}

119
120
121
122
123
124
125
126
127
128
129
130
	/**
	 * For certain requests, create the table on shutdown
	 * Does not include front-end requests
	 */
	public function maybe_create_table_on_shutdown() {
		if ( ! is_admin() && ! is_rest_endpoint_request( 'list' ) ) {
			return;
		}

		$this->prepare_table();
	}

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

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

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

152
153
154
155
156
157
158
		$this->_prepare_table();
	}

	/**
	 * Create the plugin's DB table when necessary
	 */
	protected function _prepare_table() {
159
160
161
162
163
164
165
		// 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
166
167
		$table_name = $this->get_table_name();

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

			`timestamp` bigint(20) unsigned NOT NULL,
			`action` varchar(255) NOT NULL,
174
			`action_hashed` varchar(32) NOT NULL,
175
176
177
178
			`instance` varchar(32) NOT NULL,

			`args` longtext NOT NULL,
			`schedule` varchar(255) DEFAULT NULL,
179
180
			`interval` int unsigned DEFAULT 0,
			`status` varchar(32) NOT NULL DEFAULT 'pending',
181
182
183
184

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

Erick Hitter's avatar
Erick Hitter committed
185
			PRIMARY KEY (`ID`),
Erick Hitter's avatar
Erick Hitter committed
186
187
			UNIQUE KEY `ts_action_instance_status` (`timestamp`, `action` (191), `instance`, `status`),
			KEY `status` (`status`)
188
189
190
191
192
		) 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
193
		$table_count = count( $wpdb->get_col( "SHOW TABLES LIKE '{$table_name}'" ) );
194
195

		if ( 1 === $table_count ) {
196
			update_option( self::DB_VERSION_OPTION, self::DB_VERSION );
197
		}
198
199
200

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

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

		$this->_prepare_table();
	}

214
215
216
217
218
	/**
	 * PLUGIN FUNCTIONALITY
	 */

	/**
219
	 * Override cron option requests with data from custom table
220
221
	 */
	public function get_option() {
222
223
		// Use cached value when available
		$cached_option = wp_cache_get( self::CACHE_KEY, null, true );
224

225
226
		if ( false !== $cached_option ) {
			return $cached_option;
227
228
229
230
231
232
233
234
		}

		// 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
235
236
		$page     = 1;
		$quantity = 100;
237
238

		do {
239
			$jobs = $this->get_jobs( array(
240
				'status'   => self::STATUS_PENDING,
241
				'quantity' => $quantity,
242
				'page'     => $page++,
243
244
245
			) );

			// Nothing more to add
246
			if ( empty( $jobs ) ) {
247
248
249
250
				break;
			}

			// Loop through results and built output Core expects
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
			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;
274
				}
275
			}
276
		} while( count( $jobs ) >= $quantity );
277
278
279
280
281

		// 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
282
		// Cache the results
283
284
285
286
287
288
289
290
291
292
293
		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 ) {
294
295
296
		// 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 );
297

298
299
		// Add/update new events
		foreach ( $new_events as $new_event ) {
300
			$job_id = $this->get_job_id( $new_event['timestamp'], $new_event['action'], $new_event['instance'] );
301

302
303
304
			if ( 0 === $job_id ) {
				$job_id = null;
			}
305

306
			$this->create_or_update_job( $new_event['timestamp'], $new_event['action'], $new_event['args'], $job_id, false );
307
308
		}

309
310
		// Mark deleted entries for removal
		foreach ( $deleted_events as $deleted_event ) {
311
			$this->mark_job_completed( $deleted_event['timestamp'], $deleted_event['action'], $deleted_event['instance'], false );
312
		}
313

314
315
		$this->flush_internal_caches();

316
		return $old_value;
317
318
	}

Erick Hitter's avatar
Erick Hitter committed
319
320
321
322
	/**
	 * When an entry exists, don't try to create it again
	 */
	public function block_creation_if_job_exists( $job ) {
323
324
325
326
327
		// Job already disallowed, carry on
		if ( ! is_object( $job ) ) {
			return $job;
		}

Erick Hitter's avatar
Erick Hitter committed
328
		$instance = md5( maybe_serialize( $job->args ) );
329
		if ( 0 !== $this->get_job_id( $job->timestamp, $job->hook, $instance ) ) {
Erick Hitter's avatar
Erick Hitter committed
330
331
332
333
334
335
			return false;
		}

		return $job;
	}

336
337
338
339
340
	/**
	 * PLUGIN UTILITY METHODS
	 */

	/**
Erick Hitter's avatar
Erick Hitter committed
341
342
343
	 * Retrieve jobs given a set of parameters
	 *
	 * @param array $args
344
	 * @return array
345
	 */
346
	public function get_jobs( $args ) {
347
348
349
350
351
352
353
354
355
356
357
358
359
		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
360
361
		// 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' );
362
363
364
365

		if ( is_array( $jobs ) ) {
			$jobs = array_map( array( $this, 'format_job' ), $jobs );
		} else {
366
			$jobs = array();
367
368
369
		}

		return $jobs;
370
371
	}

372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
	/**
	 * 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;
	}

398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
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
	/**
	 * 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;
	}

458
459
460
	/**
	 * Get ID for given event details
	 *
461
462
	 * 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()`
463
464
465
466
467
468
469
470
471
472
473
474
475
476
	 *
	 * @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 );
	}

477
478
	/**
	 * Standardize formatting and expand serialized data
479
480
481
	 *
	 * @param  object $job Job row from DB, in object form
	 * @return object
482
	 */
483
484
485
	private function format_job( $job ) {
		if ( ! is_object( $job ) || is_wp_error( $job ) ) {
			return $job;
486
487
		}

488
489
490
491
		$job->ID        = (int) $job->ID;
		$job->timestamp = (int) $job->timestamp;
		$job->interval  = (int) $job->interval;
		$job->args      = maybe_unserialize( $job->args );
492

493
494
495
496
		if ( empty( $job->schedule ) ) {
			$job->schedule = false;
		}

497
		return $job;
498
499
500
	}

	/**
501
	 * Create or update entry for a given job
502
503
504
505
506
507
	 *
	 * @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
508
	 */
509
	public function create_or_update_job( $timestamp, $action, $args, $update_id = null, $flush_cache = true ) {
510
511
512
513
514
515
516
517
518
519
		// 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,
520
			'action_hashed' => md5( $action ),
521
			'instance'      => md5( maybe_serialize( $args['args'] ) ),
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
			'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
538
			$job_post['created'] = current_time( 'mysql', true );
539
540
541
542

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

543
544
545
546
547
		// Delete internal cache
		// Should only be skipped during bulk operations
		if ( $flush_cache ) {
			$this->flush_internal_caches();
		}
548
549
550
	}

	/**
551
	 * Mark an event's entry as completed
552
553
554
	 *
	 * Completed entries will be cleaned up by an internal job
	 *
555
556
557
558
	 * @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
559
560
	 * @return bool
	 */
561
	public function mark_job_completed( $timestamp, $action, $instance, $flush_cache = true ) {
562
		$job_id = $this->get_job_id( $timestamp, $action, $instance );
563
564
565
566
567

		if ( ! $job_id ) {
			return false;
		}

568
		return $this->mark_job_record_completed( $job_id, $flush_cache );
569
570
571
572
	}

	/**
	 * Set a job post to the "completed" status
573
574
575
576
	 *
	 * @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
577
	 */
Erick Hitter's avatar
Erick Hitter committed
578
	public function mark_job_record_completed( $job_id, $flush_cache = true ) {
579
580
		global $wpdb;

581
582
583
584
585
586
587
588
		/**
		 * 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.
		 */
589
590
		$updates = array(
			'status'   => self::STATUS_COMPLETED,
591
			'instance' => mt_rand( 1000000, 999999999 ), // Breaks unique constraint, and can be recreated from entry's remaining data
592
593
594
		);

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

		// Delete internal cache
597
		// Should only be skipped during bulk operations
598
		if ( $flush_cache ) {
Erick Hitter's avatar
Erick Hitter committed
599
			$this->flush_internal_caches();
600
601
		}

Erick Hitter's avatar
Erick Hitter committed
602
		return (bool) $success;
603
604
	}

605
	/**
606
	 * Compare two arrays and return collapsed representation of the items present in one but not the other
607
	 *
608
609
	 * @param array $changed   Array to identify additional items from
	 * @param array $reference Array to compare against
610
	 *
611
	 * @return array
612
	 */
613
	private function find_cron_array_differences( $changed, $reference ) {
614
615
		$differences = array();

616
		$changed = collapse_events_array( $changed );
617

618
		foreach ( $changed as $event ) {
619
			$event = (object) $event;
620

621
			if ( ! isset( $reference[ $event->timestamp ][ $event->action ][ $event->instance ] ) ) {
622
				$differences[] = array(
623
624
625
626
					'timestamp' => $event->timestamp,
					'action'    => $event->action,
					'instance'  => $event->instance,
					'args'      => $event->args,
627
628
629
630
631
632
633
				);
			}
		}

		return $differences;
	}

Erick Hitter's avatar
Erick Hitter committed
634
635
636
637
638
639
640
	/**
	 * Delete the cached representation of the cron option
	 */
	public function flush_internal_caches() {
		return wp_cache_delete( self::CACHE_KEY );
	}

641
	/**
642
	 * Prevent event store from creating new entries
643
644
645
646
647
648
649
650
	 *
	 * 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;
	}

	/**
651
	 * Stop discarding events, once again storing them in the table
652
653
654
655
656
657
658
659
	 */
	public function resume_event_creation() {
		$this->job_creation_suspended = false;
	}

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

663
664
		// Skip count if already performed
		if ( $count_first ) {
665
666
667
668
669
			if ( property_exists( $wpdb, 'srtm' ) ) {
				$srtm = $wpdb->srtm;
				$wpdb->srtm = true;
			}

670
			$count = $this->count_events_by_status( self::STATUS_COMPLETED );
671
672
673
674

			if ( isset( $srtm ) ) {
				$wpdb->srtm = $srtm;
			}
675
676
677
678
679
680
681
		} else {
			$count = 1;
		}

		if ( $count > 0 ) {
			$wpdb->delete( $this->get_table_name(), array( 'status' => self::STATUS_COMPLETED, ) );
		}
682
	}
Erick Hitter's avatar
Erick Hitter committed
683
684
685
686
687
688
689
690
691
692

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

693
		if ( ! in_array( $status, self::ALLOWED_STATUSES, true ) ) {
Erick Hitter's avatar
Erick Hitter committed
694
695
696
			return false;
		}

697
		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
698
	}
Erick Hitter's avatar
Erick Hitter committed
699
700
701
}

Events_Store::instance();