Passed
Push — master ( 495efb...ace576 )
by Brian
06:14 queued 01:46
created

ActionScheduler_DBStore::claim_actions()   B

Complexity

Conditions 6
Paths 20

Size

Total Lines 50
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 28
c 1
b 0
f 0
nc 20
nop 5
dl 0
loc 50
rs 8.8497
1
<?php
2
3
/**
4
 * Class ActionScheduler_DBStore
5
 *
6
 * Action data table data store.
7
 *
8
 * @since 3.0.0
9
 */
10
class ActionScheduler_DBStore extends ActionScheduler_Store {
11
12
	/** @var int */
13
	protected static $max_args_length = 8000;
14
15
	/** @var int */
16
	protected static $max_index_length = 191;
17
18
	/**
19
	 * Initialize the data store
20
	 *
21
	 * @codeCoverageIgnore
22
	 */
23
	public function init() {
24
		$table_maker = new ActionScheduler_StoreSchema();
25
		$table_maker->register_tables();
26
	}
27
28
	/**
29
	 * Save an action.
30
	 *
31
	 * @param ActionScheduler_Action $action Action object.
32
	 * @param DateTime               $date Optional schedule date. Default null.
33
	 *
34
	 * @return int Action ID.
35
	 */
36
	public function save_action( ActionScheduler_Action $action, \DateTime $date = null ) {
37
		try {
38
39
			$this->validate_action( $action );
40
41
			/** @var \wpdb $wpdb */
42
			global $wpdb;
43
			$data = [
44
				'hook'                 => $action->get_hook(),
45
				'status'               => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ),
46
				'scheduled_date_gmt'   => $this->get_scheduled_date_string( $action, $date ),
47
				'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ),
48
				'schedule'             => serialize( $action->get_schedule() ),
49
				'group_id'             => $this->get_group_id( $action->get_group() ),
50
			];
51
			$args = wp_json_encode( $action->get_args() );
52
			if ( strlen( $args ) <= static::$max_index_length ) {
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type false; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

52
			if ( strlen( /** @scrutinizer ignore-type */ $args ) <= static::$max_index_length ) {
Loading history...
53
				$data['args'] = $args;
54
			} else {
55
				$data['args']          = $this->hash_args( $args );
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type false; however, parameter $args of ActionScheduler_DBStore::hash_args() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

55
				$data['args']          = $this->hash_args( /** @scrutinizer ignore-type */ $args );
Loading history...
56
				$data['extended_args'] = $args;
57
			}
58
59
			$table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions';
60
			$wpdb->insert( $table_name, $data );
61
			$action_id = $wpdb->insert_id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $wpdb->insert_id can also be of type string. However, the property $insert_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
62
63
			if ( is_wp_error( $action_id ) ) {
64
				throw new RuntimeException( $action_id->get_error_message() );
65
			}
66
			elseif ( empty( $action_id ) ) {
67
				throw new RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'action-scheduler' ) );
68
			}
69
70
			do_action( 'action_scheduler_stored_action', $action_id );
71
72
			return $action_id;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $action_id also could return the type string which is incompatible with the documented return type integer.
Loading history...
73
		} catch ( \Exception $e ) {
74
			/* translators: %s: error message */
75
			throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 );
76
		}
77
	}
78
79
	/**
80
	 * Generate a hash from json_encoded $args using MD5 as this isn't for security.
81
	 *
82
	 * @param string $args JSON encoded action args.
83
	 * @return string
84
	 */
85
	protected function hash_args( $args ) {
86
		return md5( $args );
87
	}
88
89
	/**
90
	 * Get action args query param value from action args.
91
	 *
92
	 * @param array $args Action args.
93
	 * @return string
94
	 */
95
	protected function get_args_for_query( $args ) {
96
		$encoded = wp_json_encode( $args );
97
		if ( strlen( $encoded ) <= static::$max_index_length ) {
0 ignored issues
show
Bug introduced by
It seems like $encoded can also be of type false; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
		if ( strlen( /** @scrutinizer ignore-type */ $encoded ) <= static::$max_index_length ) {
Loading history...
98
			return $encoded;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $encoded could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
99
		}
100
		return $this->hash_args( $encoded );
0 ignored issues
show
Bug introduced by
It seems like $encoded can also be of type false; however, parameter $args of ActionScheduler_DBStore::hash_args() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

100
		return $this->hash_args( /** @scrutinizer ignore-type */ $encoded );
Loading history...
101
	}
102
	/**
103
	 * Get a group's ID based on its name/slug.
104
	 *
105
	 * @param string $slug The string name of a group.
106
	 * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group.
107
	 *
108
	 * @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created.
109
	 */
110
	protected function get_group_id( $slug, $create_if_not_exists = true ) {
111
		if ( empty( $slug ) ) {
112
			return 0;
113
		}
114
		/** @var \wpdb $wpdb */
115
		global $wpdb;
116
		$group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) );
117
		if ( empty( $group_id ) && $create_if_not_exists ) {
118
			$group_id = $this->create_group( $slug );
119
		}
120
121
		return $group_id;
122
	}
123
124
	/**
125
	 * Create an action group.
126
	 *
127
	 * @param string $slug Group slug.
128
	 *
129
	 * @return int Group ID.
130
	 */
131
	protected function create_group( $slug ) {
132
		/** @var \wpdb $wpdb */
133
		global $wpdb;
134
		$wpdb->insert( $wpdb->actionscheduler_groups, [ 'slug' => $slug ] );
135
136
		return (int) $wpdb->insert_id;
137
	}
138
139
	/**
140
	 * Retrieve an action.
141
	 *
142
	 * @param int $action_id Action ID.
143
	 *
144
	 * @return ActionScheduler_Action
145
	 */
146
	public function fetch_action( $action_id ) {
147
		/** @var \wpdb $wpdb */
148
		global $wpdb;
149
		$data = $wpdb->get_row( $wpdb->prepare(
150
			"SELECT a.*, g.slug AS `group` FROM {$wpdb->actionscheduler_actions} a LEFT JOIN {$wpdb->actionscheduler_groups} g ON a.group_id=g.group_id WHERE a.action_id=%d",
151
			$action_id
152
		) );
153
154
		if ( empty( $data ) ) {
155
			return $this->get_null_action();
156
		}
157
158
		if ( ! empty( $data->extended_args ) ) {
159
			$data->args = $data->extended_args;
160
			unset( $data->extended_args );
161
		}
162
163
		try {
164
			$action = $this->make_action_from_db_record( $data );
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type array; however, parameter $data of ActionScheduler_DBStore:...action_from_db_record() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

164
			$action = $this->make_action_from_db_record( /** @scrutinizer ignore-type */ $data );
Loading history...
165
		} catch ( ActionScheduler_InvalidActionException $exception ) {
166
			do_action( 'action_scheduler_failed_fetch_action', $action_id, $exception );
167
			return $this->get_null_action();
168
		}
169
170
		return $action;
171
	}
172
173
	/**
174
	 * Create a null action.
175
	 *
176
	 * @return ActionScheduler_NullAction
177
	 */
178
	protected function get_null_action() {
179
		return new ActionScheduler_NullAction();
180
	}
181
182
	/**
183
	 * Create an action from a database record.
184
	 *
185
	 * @param object $data Action database record.
186
	 *
187
	 * @return ActionScheduler_Action|ActionScheduler_CanceledAction|ActionScheduler_FinishedAction
188
	 */
189
	protected function make_action_from_db_record( $data ) {
190
191
		$hook     = $data->hook;
0 ignored issues
show
Unused Code introduced by
The assignment to $hook is dead and can be removed.
Loading history...
192
		$args     = json_decode( $data->args, true );
193
		$schedule = unserialize( $data->schedule );
194
195
		$this->validate_args( $args, $data->action_id );
196
		$this->validate_schedule( $schedule, $data->action_id );
197
198
		if ( empty( $schedule ) ) {
199
			$schedule = new ActionScheduler_NullSchedule();
200
		}
201
		$group = $data->group ? $data->group : '';
202
203
		return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group );
204
	}
205
206
	/**
207
	 * Find an action.
208
	 *
209
	 * @param string $hook Action hook.
210
	 * @param array  $params Parameters of the action to find.
211
	 *
212
	 * @return string|null ID of the next action matching the criteria or NULL if not found.
213
	 */
214
	public function find_action( $hook, $params = [] ) {
215
		$params = wp_parse_args( $params, [
216
			'args'   => null,
217
			'status' => self::STATUS_PENDING,
218
			'group'  => '',
219
		] );
220
221
		/** @var wpdb $wpdb */
222
		global $wpdb;
223
		$query = "SELECT a.action_id FROM {$wpdb->actionscheduler_actions} a";
224
		$args  = [];
225
		if ( ! empty( $params[ 'group' ] ) ) {
226
			$query  .= " INNER JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id AND g.slug=%s";
227
			$args[] = $params[ 'group' ];
228
		}
229
		$query  .= " WHERE a.hook=%s";
230
		$args[] = $hook;
231
		if ( ! is_null( $params[ 'args' ] ) ) {
232
			$query  .= " AND a.args=%s";
233
			$args[] = $this->get_args_for_query( $params[ 'args' ] );
234
		}
235
236
		$order = 'ASC';
237
		if ( ! empty( $params[ 'status' ] ) ) {
238
			$query  .= " AND a.status=%s";
239
			$args[] = $params[ 'status' ];
240
241
			if ( self::STATUS_PENDING == $params[ 'status' ] ) {
242
				$order = 'ASC'; // Find the next action that matches.
243
			} else {
244
				$order = 'DESC'; // Find the most recent action that matches.
245
			}
246
		}
247
248
		$query .= " ORDER BY scheduled_date_gmt $order LIMIT 1";
249
250
		$query = $wpdb->prepare( $query, $args );
251
252
		$id = $wpdb->get_var( $query );
253
254
		return $id;
255
	}
256
257
	/**
258
	 * Returns the SQL statement to query (or count) actions.
259
	 *
260
	 * @param array  $query Filtering options.
261
	 * @param string $select_or_count  Whether the SQL should select and return the IDs or just the row count.
262
	 *
263
	 * @return string SQL statement already properly escaped.
264
	 */
265
	protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {
266
267
		if ( ! in_array( $select_or_count, array( 'select', 'count' ) ) ) {
268
			throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'action-scheduler' ) );
269
		}
270
271
		$query = wp_parse_args( $query, [
272
			'hook'             => '',
273
			'args'             => null,
274
			'date'             => null,
275
			'date_compare'     => '<=',
276
			'modified'         => null,
277
			'modified_compare' => '<=',
278
			'group'            => '',
279
			'status'           => '',
280
			'claimed'          => null,
281
			'per_page'         => 5,
282
			'offset'           => 0,
283
			'orderby'          => 'date',
284
			'order'            => 'ASC',
285
		] );
286
287
		/** @var \wpdb $wpdb */
288
		global $wpdb;
289
		$sql  = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id';
290
		$sql .= " FROM {$wpdb->actionscheduler_actions} a";
291
		$sql_params = [];
292
293
		if ( ! empty( $query[ 'group' ] ) || 'group' === $query[ 'orderby' ] ) {
294
			$sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id";
295
		}
296
297
		$sql .= " WHERE 1=1";
298
299
		if ( ! empty( $query[ 'group' ] ) ) {
300
			$sql          .= " AND g.slug=%s";
301
			$sql_params[] = $query[ 'group' ];
302
		}
303
304
		if ( $query[ 'hook' ] ) {
305
			$sql          .= " AND a.hook=%s";
306
			$sql_params[] = $query[ 'hook' ];
307
		}
308
		if ( ! is_null( $query[ 'args' ] ) ) {
309
			$sql          .= " AND a.args=%s";
310
			$sql_params[] = $this->get_args_for_query( $query[ 'args' ] );
311
		}
312
313
		if ( $query[ 'status' ] ) {
314
			$sql          .= " AND a.status=%s";
315
			$sql_params[] = $query[ 'status' ];
316
		}
317
318
		if ( $query[ 'date' ] instanceof \DateTime ) {
319
			$date = clone $query[ 'date' ];
320
			$date->setTimezone( new \DateTimeZone( 'UTC' ) );
321
			$date_string  = $date->format( 'Y-m-d H:i:s' );
322
			$comparator   = $this->validate_sql_comparator( $query[ 'date_compare' ] );
323
			$sql          .= " AND a.scheduled_date_gmt $comparator %s";
324
			$sql_params[] = $date_string;
325
		}
326
327
		if ( $query[ 'modified' ] instanceof \DateTime ) {
328
			$modified = clone $query[ 'modified' ];
329
			$modified->setTimezone( new \DateTimeZone( 'UTC' ) );
330
			$date_string  = $modified->format( 'Y-m-d H:i:s' );
331
			$comparator   = $this->validate_sql_comparator( $query[ 'modified_compare' ] );
332
			$sql          .= " AND a.last_attempt_gmt $comparator %s";
333
			$sql_params[] = $date_string;
334
		}
335
336
		if ( $query[ 'claimed' ] === true ) {
337
			$sql .= " AND a.claim_id != 0";
338
		} elseif ( $query[ 'claimed' ] === false ) {
339
			$sql .= " AND a.claim_id = 0";
340
		} elseif ( ! is_null( $query[ 'claimed' ] ) ) {
341
			$sql          .= " AND a.claim_id = %d";
342
			$sql_params[] = $query[ 'claimed' ];
343
		}
344
345
		if ( ! empty( $query['search'] ) ) {
346
			$sql .= " AND (a.hook LIKE %s OR (a.extended_args IS NULL AND a.args LIKE %s) OR a.extended_args LIKE %s";
347
			for( $i = 0; $i < 3; $i++ ) {
348
				$sql_params[] = sprintf( '%%%s%%', $query['search'] );
349
			}
350
351
			$search_claim_id = (int) $query['search'];
352
			if ( $search_claim_id ) {
353
				$sql .= ' OR a.claim_id = %d';
354
				$sql_params[] = $search_claim_id;
355
			}
356
357
			$sql .= ')';
358
		}
359
360
		if ( 'select' === $select_or_count ) {
361
			switch ( $query['orderby'] ) {
362
				case 'hook':
363
					$orderby = 'a.hook';
364
					break;
365
				case 'group':
366
					$orderby = 'g.slug';
367
					break;
368
				case 'modified':
369
					$orderby = 'a.last_attempt_gmt';
370
					break;
371
				case 'date':
372
				default:
373
					$orderby = 'a.scheduled_date_gmt';
374
					break;
375
			}
376
			if ( strtoupper( $query[ 'order' ] ) == 'ASC' ) {
377
				$order = 'ASC';
378
			} else {
379
				$order = 'DESC';
380
			}
381
			$sql .= " ORDER BY $orderby $order";
382
			if ( $query[ 'per_page' ] > 0 ) {
383
				$sql          .= " LIMIT %d, %d";
384
				$sql_params[] = $query[ 'offset' ];
385
				$sql_params[] = $query[ 'per_page' ];
386
			}
387
		}
388
389
		if ( ! empty( $sql_params ) ) {
390
			$sql = $wpdb->prepare( $sql, $sql_params );
391
		}
392
393
		return $sql;
394
	}
395
396
	/**
397
	 * Query for action count of list of action IDs.
398
	 *
399
	 * @param array  $query Query parameters.
400
	 * @param string $query_type Whether to select or count the results. Default, select.
401
	 *
402
	 * @return null|string|array The IDs of actions matching the query
403
	 */
404
	public function query_actions( $query = [], $query_type = 'select' ) {
405
		/** @var wpdb $wpdb */
406
		global $wpdb;
407
408
		$sql = $this->get_query_actions_sql( $query, $query_type );
409
410
		return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql );
0 ignored issues
show
Bug Best Practice introduced by
The expression return 'count' === $quer... : $wpdb->get_col($sql) also could return the type string which is incompatible with the return type mandated by ActionScheduler_Store::query_actions() of array|integer.
Loading history...
411
	}
412
413
	/**
414
	 * Get a count of all actions in the store, grouped by status.
415
	 *
416
	 * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status.
417
	 */
418
	public function action_counts() {
419
		global $wpdb;
420
421
		$sql  = "SELECT a.status, count(a.status) as 'count'";
422
		$sql .= " FROM {$wpdb->actionscheduler_actions} a";
423
		$sql .= " GROUP BY a.status";
424
425
		$actions_count_by_status = array();
426
		$action_stati_and_labels = $this->get_status_labels();
427
428
		foreach ( $wpdb->get_results( $sql ) as $action_data ) {
429
			// Ignore any actions with invalid status
430
			if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) {
431
				$actions_count_by_status[ $action_data->status ] = $action_data->count;
432
			}
433
		}
434
435
		return $actions_count_by_status;
436
	}
437
438
	/**
439
	 * Cancel an action.
440
	 *
441
	 * @param int $action_id Action ID.
442
	 *
443
	 * @return void
444
	 */
445
	public function cancel_action( $action_id ) {
446
		/** @var \wpdb $wpdb */
447
		global $wpdb;
448
449
		$updated = $wpdb->update(
450
			$wpdb->actionscheduler_actions,
451
			[ 'status' => self::STATUS_CANCELED ],
452
			[ 'action_id' => $action_id ],
453
			[ '%s' ],
454
			[ '%d' ]
455
		);
456
		if ( empty( $updated ) ) {
457
			/* translators: %s: action ID */
458
			throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
459
		}
460
		do_action( 'action_scheduler_canceled_action', $action_id );
461
	}
462
463
	/**
464
	 * Cancel pending actions by hook.
465
	 *
466
	 * @since 3.0.0
467
	 *
468
	 * @param string $hook Hook name.
469
	 *
470
	 * @return void
471
	 */
472
	public function cancel_actions_by_hook( $hook ) {
473
		$this->bulk_cancel_actions( [ 'hook' => $hook ] );
474
	}
475
476
	/**
477
	 * Cancel pending actions by group.
478
	 *
479
	 * @param string $group Group slug.
480
	 *
481
	 * @return void
482
	 */
483
	public function cancel_actions_by_group( $group ) {
484
		$this->bulk_cancel_actions( [ 'group' => $group ] );
485
	}
486
487
	/**
488
	 * Bulk cancel actions.
489
	 *
490
	 * @since 3.0.0
491
	 *
492
	 * @param array $query_args Query parameters.
493
	 */
494
	protected function bulk_cancel_actions( $query_args ) {
495
		/** @var \wpdb $wpdb */
496
		global $wpdb;
497
498
		if ( ! is_array( $query_args ) ) {
0 ignored issues
show
introduced by
The condition is_array($query_args) is always true.
Loading history...
499
			return;
500
		}
501
502
		// Don't cancel actions that are already canceled.
503
		if ( isset( $query_args['status'] ) && $query_args['status'] == self::STATUS_CANCELED ) {
504
			return;
505
		}
506
507
		$action_ids = true;
508
		$query_args = wp_parse_args(
509
			$query_args,
510
			[
511
				'per_page' => 1000,
512
				'status' => self::STATUS_PENDING,
513
			]
514
		);
515
516
		while ( $action_ids ) {
517
			$action_ids = $this->query_actions( $query_args );
518
			if ( empty( $action_ids ) ) {
519
				break;
520
			}
521
522
			$format     = array_fill( 0, count( $action_ids ), '%d' );
523
			$query_in   = '(' . implode( ',', $format ) . ')';
524
			$parameters = $action_ids;
525
			array_unshift( $parameters, self::STATUS_CANCELED );
526
527
			$wpdb->query(
528
				$wpdb->prepare( // wpcs: PreparedSQLPlaceholders replacement count ok.
529
					"UPDATE {$wpdb->actionscheduler_actions} SET status = %s WHERE action_id IN {$query_in}",
530
					$parameters
531
				)
532
			);
533
534
			do_action( 'action_scheduler_bulk_cancel_actions', $action_ids );
535
		}
536
	}
537
538
	/**
539
	 * Delete an action.
540
	 *
541
	 * @param int $action_id Action ID.
542
	 */
543
	public function delete_action( $action_id ) {
544
		/** @var \wpdb $wpdb */
545
		global $wpdb;
546
		$deleted = $wpdb->delete( $wpdb->actionscheduler_actions, [ 'action_id' => $action_id ], [ '%d' ] );
547
		if ( empty( $deleted ) ) {
548
			throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
549
		}
550
		do_action( 'action_scheduler_deleted_action', $action_id );
551
	}
552
553
	/**
554
	 * Get the schedule date for an action.
555
	 *
556
	 * @param string $action_id Action ID.
557
	 *
558
	 * @throws \InvalidArgumentException
559
	 * @return \DateTime The local date the action is scheduled to run, or the date that it ran.
560
	 */
561
	public function get_date( $action_id ) {
562
		$date = $this->get_date_gmt( $action_id );
0 ignored issues
show
Bug introduced by
$action_id of type string is incompatible with the type integer expected by parameter $action_id of ActionScheduler_DBStore::get_date_gmt(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

562
		$date = $this->get_date_gmt( /** @scrutinizer ignore-type */ $action_id );
Loading history...
563
		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
564
		return $date;
565
	}
566
567
	/**
568
	 * Get the GMT schedule date for an action.
569
	 *
570
	 * @param int $action_id Action ID.
571
	 *
572
	 * @throws \InvalidArgumentException
573
	 * @return \DateTime The GMT date the action is scheduled to run, or the date that it ran.
574
	 */
575
	protected function get_date_gmt( $action_id ) {
576
		/** @var \wpdb $wpdb */
577
		global $wpdb;
578
		$record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) );
579
		if ( empty( $record ) ) {
580
			throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
581
		}
582
		if ( $record->status == self::STATUS_PENDING ) {
583
			return as_get_datetime_object( $record->scheduled_date_gmt );
584
		} else {
585
			return as_get_datetime_object( $record->last_attempt_gmt );
586
		}
587
	}
588
589
	/**
590
	 * Stake a claim on actions.
591
	 *
592
	 * @param int       $max_actions Maximum number of action to include in claim.
593
	 * @param \DateTime $before_date Jobs must be schedule before this date. Defaults to now.
594
	 *
595
	 * @return ActionScheduler_ActionClaim
596
	 */
597
	public function stake_claim( $max_actions = 10, \DateTime $before_date = null, $hooks = array(), $group = '' ) {
598
		$claim_id = $this->generate_claim_id();
599
		$this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
600
		$action_ids = $this->find_actions_by_claim_id( $claim_id );
601
602
		return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
603
	}
604
605
	/**
606
	 * Generate a new action claim.
607
	 *
608
	 * @return int Claim ID.
609
	 */
610
	protected function generate_claim_id() {
611
		/** @var \wpdb $wpdb */
612
		global $wpdb;
613
		$now = as_get_datetime_object();
614
		$wpdb->insert( $wpdb->actionscheduler_claims, [ 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ] );
615
616
		return $wpdb->insert_id;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $wpdb->insert_id also could return the type string which is incompatible with the documented return type integer.
Loading history...
617
	}
618
619
	/**
620
	 * Mark actions claimed.
621
	 *
622
	 * @param string    $claim_id Claim Id.
623
	 * @param int       $limit Number of action to include in claim.
624
	 * @param \DateTime $before_date Should use UTC timezone.
625
	 *
626
	 * @return int The number of actions that were claimed.
627
	 * @throws \RuntimeException
628
	 */
629
	protected function claim_actions( $claim_id, $limit, \DateTime $before_date = null, $hooks = array(), $group = '' ) {
630
		/** @var \wpdb $wpdb */
631
		global $wpdb;
632
633
		$now  = as_get_datetime_object();
634
		$date = is_null( $before_date ) ? $now : clone $before_date;
635
636
		// can't use $wpdb->update() because of the <= condition
637
		$update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s";
638
		$params = array(
639
			$claim_id,
640
			$now->format( 'Y-m-d H:i:s' ),
641
			current_time( 'mysql' ),
642
		);
643
644
		$where    = "WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s";
645
		$params[] = $date->format( 'Y-m-d H:i:s' );
646
		$params[] = self::STATUS_PENDING;
647
648
		if ( ! empty( $hooks ) ) {
649
			$placeholders = array_fill( 0, count( $hooks ), '%s' );
650
			$where       .= ' AND hook IN (' . join( ', ', $placeholders ) . ')';
651
			$params       = array_merge( $params, array_values( $hooks ) );
652
		}
653
654
		if ( ! empty( $group ) ) {
655
656
			$group_id = $this->get_group_id( $group, false );
657
658
			// throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour
659
			if ( empty( $group_id ) ) {
660
				/* translators: %s: group name */
661
				throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) );
662
			}
663
664
			$where   .= ' AND group_id = %d';
665
			$params[] = $group_id;
666
		}
667
668
		$order    = "ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC LIMIT %d";
669
		$params[] = $limit;
670
671
		$sql = $wpdb->prepare( "{$update} {$where} {$order}", $params );
672
673
		$rows_affected = $wpdb->query( $sql );
674
		if ( $rows_affected === false ) {
675
			throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) );
676
		}
677
678
		return (int) $rows_affected;
679
	}
680
681
	/**
682
	 * Get the number of active claims.
683
	 *
684
	 * @return int
685
	 */
686
	public function get_claim_count() {
687
		global $wpdb;
688
689
		$sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)";
690
		$sql = $wpdb->prepare( $sql, [ self::STATUS_PENDING, self::STATUS_RUNNING ] );
691
692
		return (int) $wpdb->get_var( $sql );
693
	}
694
695
	/**
696
	 * Return an action's claim ID, as stored in the claim_id column.
697
	 *
698
	 * @param string $action_id Action ID.
699
	 * @return mixed
700
	 */
701
	public function get_claim_id( $action_id ) {
702
		/** @var \wpdb $wpdb */
703
		global $wpdb;
704
705
		$sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
706
		$sql = $wpdb->prepare( $sql, $action_id );
707
708
		return (int) $wpdb->get_var( $sql );
709
	}
710
711
	/**
712
	 * Retrieve the action IDs of action in a claim.
713
	 *
714
	 * @param string $claim_id Claim ID.
715
	 *
716
	 * @return int[]
717
	 */
718
	public function find_actions_by_claim_id( $claim_id ) {
719
		/** @var \wpdb $wpdb */
720
		global $wpdb;
721
722
		$sql = "SELECT action_id FROM {$wpdb->actionscheduler_actions} WHERE claim_id=%d";
723
		$sql = $wpdb->prepare( $sql, $claim_id );
724
725
		$action_ids = $wpdb->get_col( $sql );
726
727
		return array_map( 'intval', $action_ids );
728
	}
729
730
	/**
731
	 * Release actions from a claim and delete the claim.
732
	 *
733
	 * @param ActionScheduler_ActionClaim $claim Claim object.
734
	 */
735
	public function release_claim( ActionScheduler_ActionClaim $claim ) {
736
		/** @var \wpdb $wpdb */
737
		global $wpdb;
738
		$wpdb->update( $wpdb->actionscheduler_actions, [ 'claim_id' => 0 ], [ 'claim_id' => $claim->get_id() ], [ '%d' ], [ '%d' ] );
739
		$wpdb->delete( $wpdb->actionscheduler_claims, [ 'claim_id' => $claim->get_id() ], [ '%d' ] );
740
	}
741
742
	/**
743
	 * Remove the claim from an action.
744
	 *
745
	 * @param int $action_id Action ID.
746
	 *
747
	 * @return void
748
	 */
749
	public function unclaim_action( $action_id ) {
750
		/** @var \wpdb $wpdb */
751
		global $wpdb;
752
		$wpdb->update(
753
			$wpdb->actionscheduler_actions,
754
			[ 'claim_id' => 0 ],
755
			[ 'action_id' => $action_id ],
756
			[ '%s' ],
757
			[ '%d' ]
758
		);
759
	}
760
761
	/**
762
	 * Mark an action as failed.
763
	 *
764
	 * @param int $action_id Action ID.
765
	 */
766
	public function mark_failure( $action_id ) {
767
		/** @var \wpdb $wpdb */
768
		global $wpdb;
769
		$updated = $wpdb->update(
770
			$wpdb->actionscheduler_actions,
771
			[ 'status' => self::STATUS_FAILED ],
772
			[ 'action_id' => $action_id ],
773
			[ '%s' ],
774
			[ '%d' ]
775
		);
776
		if ( empty( $updated ) ) {
777
			throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
778
		}
779
	}
780
781
	/**
782
	 * Add execution message to action log.
783
	 *
784
	 * @param int $action_id Action ID.
785
	 *
786
	 * @return void
787
	 */
788
	public function log_execution( $action_id ) {
789
		/** @var \wpdb $wpdb */
790
		global $wpdb;
791
792
		$sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d";
793
		$sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id );
794
		$wpdb->query( $sql );
795
	}
796
797
	/**
798
	 * Mark an action as complete.
799
	 *
800
	 * @param int $action_id Action ID.
801
	 *
802
	 * @return void
803
	 */
804
	public function mark_complete( $action_id ) {
805
		/** @var \wpdb $wpdb */
806
		global $wpdb;
807
		$updated = $wpdb->update(
808
			$wpdb->actionscheduler_actions,
809
			[
810
				'status'             => self::STATUS_COMPLETE,
811
				'last_attempt_gmt'   => current_time( 'mysql', true ),
812
				'last_attempt_local' => current_time( 'mysql' ),
813
			],
814
			[ 'action_id' => $action_id ],
815
			[ '%s' ],
816
			[ '%d' ]
817
		);
818
		if ( empty( $updated ) ) {
819
			throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
820
		}
821
	}
822
823
	/**
824
	 * Get an action's status.
825
	 *
826
	 * @param int $action_id Action ID.
827
	 *
828
	 * @return string
829
	 */
830
	public function get_status( $action_id ) {
831
		/** @var \wpdb $wpdb */
832
		global $wpdb;
833
		$sql    = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
834
		$sql    = $wpdb->prepare( $sql, $action_id );
835
		$status = $wpdb->get_var( $sql );
836
837
		if ( $status === null ) {
838
			throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) );
839
		} elseif ( empty( $status ) ) {
840
			throw new \RuntimeException( __( 'Unknown status found for action.', 'action-scheduler' ) );
841
		} else {
842
			return $status;
843
		}
844
	}
845
}
846