PluginProbe
WANotifier for Forms and Actions / 1.0.0
WANotifier for Forms and Actions v1.0.0
3.1.1 3.1.0 3.0.4 2.7.10 2.7.11 2.7.12 2.7.13 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 3.0.0 3.0.1 3.0.2 3.0.3 trunk 0.1.0 0.1.1 1.0.0 1.0.1 1.0.2 All 67 releases
notifier / libraries / action-scheduler / classes / data-stores / ActionScheduler_DBStore.php

ActionScheduler_DBStore.php in WANotifier for Forms and Actions 1.0.0, at libraries/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php

878 lines 26.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 /**
13 * Used to share information about the before_date property of claims internally.
14 *
15 * This is used in preference to passing the same information as a method param
16 * for backwards-compatibility reasons.
17 *
18 * @var DateTime|null
19 */
20 private $claim_before_date = null;
21
22 /** @var int */
23 protected static $max_args_length = 8000;
24
25 /** @var int */
26 protected static $max_index_length = 191;
27
28 /**
29 * Initialize the data store
30 *
31 * @codeCoverageIgnore
32 */
33 public function init() {
34 $table_maker = new ActionScheduler_StoreSchema();
35 $table_maker->init();
36 $table_maker->register_tables();
37 }
38
39 /**
40 * Save an action.
41 *
42 * @param ActionScheduler_Action $action Action object.
43 * @param DateTime $date Optional schedule date. Default null.
44 *
45 * @return int Action ID.
46 * @throws RuntimeException Throws exception when saving the action fails.
47 */
48 public function save_action( ActionScheduler_Action $action, \DateTime $date = null ) {
49 try {
50
51 $this->validate_action( $action );
52
53 /** @var \wpdb $wpdb */
54 global $wpdb;
55 $data = array(
56 'hook' => $action->get_hook(),
57 'status' => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ),
58 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ),
59 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ),
60 'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
61 'group_id' => $this->get_group_id( $action->get_group() ),
62 );
63 $args = wp_json_encode( $action->get_args() );
64 if ( strlen( $args ) <= static::$max_index_length ) {
65 $data['args'] = $args;
66 } else {
67 $data['args'] = $this->hash_args( $args );
68 $data['extended_args'] = $args;
69 }
70
71 $table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions';
72 $wpdb->insert( $table_name, $data );
73 $action_id = $wpdb->insert_id;
74
75 if ( is_wp_error( $action_id ) ) {
76 throw new \RuntimeException( $action_id->get_error_message() );
77 } elseif ( empty( $action_id ) ) {
78 throw new \RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'action-scheduler' ) );
79 }
80
81 do_action( 'action_scheduler_stored_action', $action_id );
82
83 return $action_id;
84 } catch ( \Exception $e ) {
85 /* translators: %s: error message */
86 throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 );
87 }
88 }
89
90 /**
91 * Generate a hash from json_encoded $args using MD5 as this isn't for security.
92 *
93 * @param string $args JSON encoded action args.
94 * @return string
95 */
96 protected function hash_args( $args ) {
97 return md5( $args );
98 }
99
100 /**
101 * Get action args query param value from action args.
102 *
103 * @param array $args Action args.
104 * @return string
105 */
106 protected function get_args_for_query( $args ) {
107 $encoded = wp_json_encode( $args );
108 if ( strlen( $encoded ) <= static::$max_index_length ) {
109 return $encoded;
110 }
111 return $this->hash_args( $encoded );
112 }
113 /**
114 * Get a group's ID based on its name/slug.
115 *
116 * @param string $slug The string name of a group.
117 * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group.
118 *
119 * @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created.
120 */
121 protected function get_group_id( $slug, $create_if_not_exists = true ) {
122 if ( empty( $slug ) ) {
123 return 0;
124 }
125 /** @var \wpdb $wpdb */
126 global $wpdb;
127 $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) );
128 if ( empty( $group_id ) && $create_if_not_exists ) {
129 $group_id = $this->create_group( $slug );
130 }
131
132 return $group_id;
133 }
134
135 /**
136 * Create an action group.
137 *
138 * @param string $slug Group slug.
139 *
140 * @return int Group ID.
141 */
142 protected function create_group( $slug ) {
143 /** @var \wpdb $wpdb */
144 global $wpdb;
145 $wpdb->insert( $wpdb->actionscheduler_groups, array( 'slug' => $slug ) );
146
147 return (int) $wpdb->insert_id;
148 }
149
150 /**
151 * Retrieve an action.
152 *
153 * @param int $action_id Action ID.
154 *
155 * @return ActionScheduler_Action
156 */
157 public function fetch_action( $action_id ) {
158 /** @var \wpdb $wpdb */
159 global $wpdb;
160 $data = $wpdb->get_row(
161 $wpdb->prepare(
162 "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",
163 $action_id
164 )
165 );
166
167 if ( empty( $data ) ) {
168 return $this->get_null_action();
169 }
170
171 if ( ! empty( $data->extended_args ) ) {
172 $data->args = $data->extended_args;
173 unset( $data->extended_args );
174 }
175
176 // Convert NULL dates to zero dates.
177 $date_fields = array(
178 'scheduled_date_gmt',
179 'scheduled_date_local',
180 'last_attempt_gmt',
181 'last_attempt_gmt',
182 );
183 foreach ( $date_fields as $date_field ) {
184 if ( is_null( $data->$date_field ) ) {
185 $data->$date_field = ActionScheduler_StoreSchema::DEFAULT_DATE;
186 }
187 }
188
189 try {
190 $action = $this->make_action_from_db_record( $data );
191 } catch ( ActionScheduler_InvalidActionException $exception ) {
192 do_action( 'action_scheduler_failed_fetch_action', $action_id, $exception );
193 return $this->get_null_action();
194 }
195
196 return $action;
197 }
198
199 /**
200 * Create a null action.
201 *
202 * @return ActionScheduler_NullAction
203 */
204 protected function get_null_action() {
205 return new ActionScheduler_NullAction();
206 }
207
208 /**
209 * Create an action from a database record.
210 *
211 * @param object $data Action database record.
212 *
213 * @return ActionScheduler_Action|ActionScheduler_CanceledAction|ActionScheduler_FinishedAction
214 */
215 protected function make_action_from_db_record( $data ) {
216
217 $hook = $data->hook;
218 $args = json_decode( $data->args, true );
219 $schedule = unserialize( $data->schedule ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
220
221 $this->validate_args( $args, $data->action_id );
222 $this->validate_schedule( $schedule, $data->action_id );
223
224 if ( empty( $schedule ) ) {
225 $schedule = new ActionScheduler_NullSchedule();
226 }
227 $group = $data->group ? $data->group : '';
228
229 return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group );
230 }
231
232 /**
233 * Returns the SQL statement to query (or count) actions.
234 *
235 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
236 *
237 * @param array $query Filtering options.
238 * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count.
239 *
240 * @return string SQL statement already properly escaped.
241 * @throws InvalidArgumentException If the query is invalid.
242 */
243 protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {
244
245 if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) {
246 throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'action-scheduler' ) );
247 }
248
249 $query = wp_parse_args(
250 $query,
251 array(
252 'hook' => '',
253 'args' => null,
254 'date' => null,
255 'date_compare' => '<=',
256 'modified' => null,
257 'modified_compare' => '<=',
258 'group' => '',
259 'status' => '',
260 'claimed' => null,
261 'per_page' => 5,
262 'offset' => 0,
263 'orderby' => 'date',
264 'order' => 'ASC',
265 )
266 );
267
268 /** @var \wpdb $wpdb */
269 global $wpdb;
270 $sql = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id';
271 $sql .= " FROM {$wpdb->actionscheduler_actions} a";
272 $sql_params = array();
273
274 if ( ! empty( $query['group'] ) || 'group' === $query['orderby'] ) {
275 $sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id";
276 }
277
278 $sql .= ' WHERE 1=1';
279
280 if ( ! empty( $query['group'] ) ) {
281 $sql .= ' AND g.slug=%s';
282 $sql_params[] = $query['group'];
283 }
284
285 if ( $query['hook'] ) {
286 $sql .= ' AND a.hook=%s';
287 $sql_params[] = $query['hook'];
288 }
289 if ( ! is_null( $query['args'] ) ) {
290 $sql .= ' AND a.args=%s';
291 $sql_params[] = $this->get_args_for_query( $query['args'] );
292 }
293
294 if ( $query['status'] ) {
295 $statuses = (array) $query['status'];
296 $placeholders = array_fill( 0, count( $statuses ), '%s' );
297 $sql .= ' AND a.status IN (' . join( ', ', $placeholders ) . ')';
298 $sql_params = array_merge( $sql_params, array_values( $statuses ) );
299 }
300
301 if ( $query['date'] instanceof \DateTime ) {
302 $date = clone $query['date'];
303 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
304 $date_string = $date->format( 'Y-m-d H:i:s' );
305 $comparator = $this->validate_sql_comparator( $query['date_compare'] );
306 $sql .= " AND a.scheduled_date_gmt $comparator %s";
307 $sql_params[] = $date_string;
308 }
309
310 if ( $query['modified'] instanceof \DateTime ) {
311 $modified = clone $query['modified'];
312 $modified->setTimezone( new \DateTimeZone( 'UTC' ) );
313 $date_string = $modified->format( 'Y-m-d H:i:s' );
314 $comparator = $this->validate_sql_comparator( $query['modified_compare'] );
315 $sql .= " AND a.last_attempt_gmt $comparator %s";
316 $sql_params[] = $date_string;
317 }
318
319 if ( true === $query['claimed'] ) {
320 $sql .= ' AND a.claim_id != 0';
321 } elseif ( false === $query['claimed'] ) {
322 $sql .= ' AND a.claim_id = 0';
323 } elseif ( ! is_null( $query['claimed'] ) ) {
324 $sql .= ' AND a.claim_id = %d';
325 $sql_params[] = $query['claimed'];
326 }
327
328 if ( ! empty( $query['search'] ) ) {
329 $sql .= ' AND (a.hook LIKE %s OR (a.extended_args IS NULL AND a.args LIKE %s) OR a.extended_args LIKE %s';
330 for ( $i = 0; $i < 3; $i++ ) {
331 $sql_params[] = sprintf( '%%%s%%', $query['search'] );
332 }
333
334 $search_claim_id = (int) $query['search'];
335 if ( $search_claim_id ) {
336 $sql .= ' OR a.claim_id = %d';
337 $sql_params[] = $search_claim_id;
338 }
339
340 $sql .= ')';
341 }
342
343 if ( 'select' === $select_or_count ) {
344 if ( 'ASC' === strtoupper( $query['order'] ) ) {
345 $order = 'ASC';
346 } else {
347 $order = 'DESC';
348 }
349 switch ( $query['orderby'] ) {
350 case 'hook':
351 $sql .= " ORDER BY a.hook $order";
352 break;
353 case 'group':
354 $sql .= " ORDER BY g.slug $order";
355 break;
356 case 'modified':
357 $sql .= " ORDER BY a.last_attempt_gmt $order";
358 break;
359 case 'none':
360 break;
361 case 'action_id':
362 $sql .= " ORDER BY a.action_id $order";
363 break;
364 case 'date':
365 default:
366 $sql .= " ORDER BY a.scheduled_date_gmt $order";
367 break;
368 }
369
370 if ( $query['per_page'] > 0 ) {
371 $sql .= ' LIMIT %d, %d';
372 $sql_params[] = $query['offset'];
373 $sql_params[] = $query['per_page'];
374 }
375 }
376
377 if ( ! empty( $sql_params ) ) {
378 $sql = $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
379 }
380
381 return $sql;
382 }
383
384 /**
385 * Query for action count or list of action IDs.
386 *
387 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
388 *
389 * @see ActionScheduler_Store::query_actions for $query arg usage.
390 *
391 * @param array $query Query filtering options.
392 * @param string $query_type Whether to select or count the results. Defaults to select.
393 *
394 * @return string|array|null The IDs of actions matching the query. Null on failure.
395 */
396 public function query_actions( $query = array(), $query_type = 'select' ) {
397 /** @var wpdb $wpdb */
398 global $wpdb;
399
400 $sql = $this->get_query_actions_sql( $query, $query_type );
401
402 return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoSql, WordPress.DB.DirectDatabaseQuery.NoCaching
403 }
404
405 /**
406 * Get a count of all actions in the store, grouped by status.
407 *
408 * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status.
409 */
410 public function action_counts() {
411 global $wpdb;
412
413 $sql = "SELECT a.status, count(a.status) as 'count'";
414 $sql .= " FROM {$wpdb->actionscheduler_actions} a";
415 $sql .= ' GROUP BY a.status';
416
417 $actions_count_by_status = array();
418 $action_stati_and_labels = $this->get_status_labels();
419
420 foreach ( $wpdb->get_results( $sql ) as $action_data ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
421 // Ignore any actions with invalid status.
422 if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) {
423 $actions_count_by_status[ $action_data->status ] = $action_data->count;
424 }
425 }
426
427 return $actions_count_by_status;
428 }
429
430 /**
431 * Cancel an action.
432 *
433 * @param int $action_id Action ID.
434 *
435 * @return void
436 * @throws \InvalidArgumentException If the action update failed.
437 */
438 public function cancel_action( $action_id ) {
439 /** @var \wpdb $wpdb */
440 global $wpdb;
441
442 $updated = $wpdb->update(
443 $wpdb->actionscheduler_actions,
444 array( 'status' => self::STATUS_CANCELED ),
445 array( 'action_id' => $action_id ),
446 array( '%s' ),
447 array( '%d' )
448 );
449 if ( false === $updated ) {
450 /* translators: %s: action ID */
451 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) );
452 }
453 do_action( 'action_scheduler_canceled_action', $action_id );
454 }
455
456 /**
457 * Cancel pending actions by hook.
458 *
459 * @since 3.0.0
460 *
461 * @param string $hook Hook name.
462 *
463 * @return void
464 */
465 public function cancel_actions_by_hook( $hook ) {
466 $this->bulk_cancel_actions( array( 'hook' => $hook ) );
467 }
468
469 /**
470 * Cancel pending actions by group.
471 *
472 * @param string $group Group slug.
473 *
474 * @return void
475 */
476 public function cancel_actions_by_group( $group ) {
477 $this->bulk_cancel_actions( array( 'group' => $group ) );
478 }
479
480 /**
481 * Bulk cancel actions.
482 *
483 * @since 3.0.0
484 *
485 * @param array $query_args Query parameters.
486 */
487 protected function bulk_cancel_actions( $query_args ) {
488 /** @var \wpdb $wpdb */
489 global $wpdb;
490
491 if ( ! is_array( $query_args ) ) {
492 return;
493 }
494
495 // Don't cancel actions that are already canceled.
496 if ( isset( $query_args['status'] ) && self::STATUS_CANCELED === $query_args['status'] ) {
497 return;
498 }
499
500 $action_ids = true;
501 $query_args = wp_parse_args(
502 $query_args,
503 array(
504 'per_page' => 1000,
505 'status' => self::STATUS_PENDING,
506 'orderby' => 'action_id',
507 )
508 );
509
510 while ( $action_ids ) {
511 $action_ids = $this->query_actions( $query_args );
512 if ( empty( $action_ids ) ) {
513 break;
514 }
515
516 $format = array_fill( 0, count( $action_ids ), '%d' );
517 $query_in = '(' . implode( ',', $format ) . ')';
518 $parameters = $action_ids;
519 array_unshift( $parameters, self::STATUS_CANCELED );
520
521 $wpdb->query(
522 $wpdb->prepare(
523 "UPDATE {$wpdb->actionscheduler_actions} SET status = %s WHERE action_id IN {$query_in}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
524 $parameters
525 )
526 );
527
528 do_action( 'action_scheduler_bulk_cancel_actions', $action_ids );
529 }
530 }
531
532 /**
533 * Delete an action.
534 *
535 * @param int $action_id Action ID.
536 * @throws \InvalidArgumentException If the action deletion failed.
537 */
538 public function delete_action( $action_id ) {
539 /** @var \wpdb $wpdb */
540 global $wpdb;
541 $deleted = $wpdb->delete( $wpdb->actionscheduler_actions, array( 'action_id' => $action_id ), array( '%d' ) );
542 if ( empty( $deleted ) ) {
543 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment
544 }
545 do_action( 'action_scheduler_deleted_action', $action_id );
546 }
547
548 /**
549 * Get the schedule date for an action.
550 *
551 * @param string $action_id Action ID.
552 *
553 * @return \DateTime The local date the action is scheduled to run, or the date that it ran.
554 */
555 public function get_date( $action_id ) {
556 $date = $this->get_date_gmt( $action_id );
557 ActionScheduler_TimezoneHelper::set_local_timezone( $date );
558 return $date;
559 }
560
561 /**
562 * Get the GMT schedule date for an action.
563 *
564 * @param int $action_id Action ID.
565 *
566 * @throws \InvalidArgumentException If action cannot be identified.
567 * @return \DateTime The GMT date the action is scheduled to run, or the date that it ran.
568 */
569 protected function get_date_gmt( $action_id ) {
570 /** @var \wpdb $wpdb */
571 global $wpdb;
572 $record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) );
573 if ( empty( $record ) ) {
574 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment
575 }
576 if ( self::STATUS_PENDING === $record->status ) {
577 return as_get_datetime_object( $record->scheduled_date_gmt );
578 } else {
579 return as_get_datetime_object( $record->last_attempt_gmt );
580 }
581 }
582
583 /**
584 * Stake a claim on actions.
585 *
586 * @param int $max_actions Maximum number of action to include in claim.
587 * @param \DateTime $before_date Jobs must be schedule before this date. Defaults to now.
588 * @param array $hooks Hooks to filter for.
589 * @param string $group Group to filter for.
590 *
591 * @return ActionScheduler_ActionClaim
592 */
593 public function stake_claim( $max_actions = 10, \DateTime $before_date = null, $hooks = array(), $group = '' ) {
594 $claim_id = $this->generate_claim_id();
595
596 $this->claim_before_date = $before_date;
597 $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
598 $action_ids = $this->find_actions_by_claim_id( $claim_id );
599 $this->claim_before_date = null;
600
601 return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
602 }
603
604 /**
605 * Generate a new action claim.
606 *
607 * @return int Claim ID.
608 */
609 protected function generate_claim_id() {
610 /** @var \wpdb $wpdb */
611 global $wpdb;
612 $now = as_get_datetime_object();
613 $wpdb->insert( $wpdb->actionscheduler_claims, array( 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ) );
614
615 return $wpdb->insert_id;
616 }
617
618 /**
619 * Mark actions claimed.
620 *
621 * @param string $claim_id Claim Id.
622 * @param int $limit Number of action to include in claim.
623 * @param \DateTime $before_date Should use UTC timezone.
624 * @param array $hooks Hooks to filter for.
625 * @param string $group Group to filter for.
626 *
627 * @return int The number of actions that were claimed.
628 * @throws \InvalidArgumentException Throws InvalidArgumentException if group doesn't exist.
629 * @throws \RuntimeException Throws RuntimeException if unable to claim action.
630 */
631 protected function claim_actions( $claim_id, $limit, \DateTime $before_date = null, $hooks = array(), $group = '' ) {
632 /** @var \wpdb $wpdb */
633 global $wpdb;
634
635 $now = as_get_datetime_object();
636 $date = is_null( $before_date ) ? $now : clone $before_date;
637
638 // can't use $wpdb->update() because of the <= condition.
639 $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s";
640 $params = array(
641 $claim_id,
642 $now->format( 'Y-m-d H:i:s' ),
643 current_time( 'mysql' ),
644 );
645
646 $where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s';
647 $params[] = $date->format( 'Y-m-d H:i:s' );
648 $params[] = self::STATUS_PENDING;
649
650 if ( ! empty( $hooks ) ) {
651 $placeholders = array_fill( 0, count( $hooks ), '%s' );
652 $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')';
653 $params = array_merge( $params, array_values( $hooks ) );
654 }
655
656 if ( ! empty( $group ) ) {
657
658 $group_id = $this->get_group_id( $group, false );
659
660 // throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour.
661 if ( empty( $group_id ) ) {
662 /* translators: %s: group name */
663 throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) );
664 }
665
666 $where .= ' AND group_id = %d';
667 $params[] = $group_id;
668 }
669
670 /**
671 * Sets the order-by clause used in the action claim query.
672 *
673 * @since 3.4.0
674 *
675 * @param string $order_by_sql
676 */
677 $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC' );
678 $params[] = $limit;
679
680 $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders
681 $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
682 if ( false === $rows_affected ) {
683 throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) );
684 }
685
686 return (int) $rows_affected;
687 }
688
689 /**
690 * Get the number of active claims.
691 *
692 * @return int
693 */
694 public function get_claim_count() {
695 global $wpdb;
696
697 $sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)";
698 $sql = $wpdb->prepare( $sql, array( self::STATUS_PENDING, self::STATUS_RUNNING ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
699
700 return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
701 }
702
703 /**
704 * Return an action's claim ID, as stored in the claim_id column.
705 *
706 * @param string $action_id Action ID.
707 * @return mixed
708 */
709 public function get_claim_id( $action_id ) {
710 /** @var \wpdb $wpdb */
711 global $wpdb;
712
713 $sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
714 $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
715
716 return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
717 }
718
719 /**
720 * Retrieve the action IDs of action in a claim.
721 *
722 * @param int $claim_id Claim ID.
723 * @return int[]
724 */
725 public function find_actions_by_claim_id( $claim_id ) {
726 /** @var \wpdb $wpdb */
727 global $wpdb;
728
729 $action_ids = array();
730 $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object();
731 $cut_off = $before_date->format( 'Y-m-d H:i:s' );
732
733 $sql = $wpdb->prepare(
734 "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d",
735 $claim_id
736 );
737
738 // Verify that the scheduled date for each action is within the expected bounds (in some unusual
739 // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify).
740 foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
741 if ( $claimed_action->scheduled_date_gmt <= $cut_off ) {
742 $action_ids[] = absint( $claimed_action->action_id );
743 }
744 }
745
746 return $action_ids;
747 }
748
749 /**
750 * Release actions from a claim and delete the claim.
751 *
752 * @param ActionScheduler_ActionClaim $claim Claim object.
753 */
754 public function release_claim( ActionScheduler_ActionClaim $claim ) {
755 /** @var \wpdb $wpdb */
756 global $wpdb;
757 $wpdb->update( $wpdb->actionscheduler_actions, array( 'claim_id' => 0 ), array( 'claim_id' => $claim->get_id() ), array( '%d' ), array( '%d' ) );
758 $wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) );
759 }
760
761 /**
762 * Remove the claim from an action.
763 *
764 * @param int $action_id Action ID.
765 *
766 * @return void
767 */
768 public function unclaim_action( $action_id ) {
769 /** @var \wpdb $wpdb */
770 global $wpdb;
771 $wpdb->update(
772 $wpdb->actionscheduler_actions,
773 array( 'claim_id' => 0 ),
774 array( 'action_id' => $action_id ),
775 array( '%s' ),
776 array( '%d' )
777 );
778 }
779
780 /**
781 * Mark an action as failed.
782 *
783 * @param int $action_id Action ID.
784 * @throws \InvalidArgumentException Throw an exception if action was not updated.
785 */
786 public function mark_failure( $action_id ) {
787 /** @var \wpdb $wpdb */
788 global $wpdb;
789 $updated = $wpdb->update(
790 $wpdb->actionscheduler_actions,
791 array( 'status' => self::STATUS_FAILED ),
792 array( 'action_id' => $action_id ),
793 array( '%s' ),
794 array( '%d' )
795 );
796 if ( empty( $updated ) ) {
797 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment
798 }
799 }
800
801 /**
802 * Add execution message to action log.
803 *
804 * @param int $action_id Action ID.
805 *
806 * @return void
807 */
808 public function log_execution( $action_id ) {
809 /** @var \wpdb $wpdb */
810 global $wpdb;
811
812 $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d";
813 $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
814 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
815 }
816
817 /**
818 * Mark an action as complete.
819 *
820 * @param int $action_id Action ID.
821 *
822 * @return void
823 * @throws \InvalidArgumentException Throw an exception if action was not updated.
824 */
825 public function mark_complete( $action_id ) {
826 /** @var \wpdb $wpdb */
827 global $wpdb;
828 $updated = $wpdb->update(
829 $wpdb->actionscheduler_actions,
830 array(
831 'status' => self::STATUS_COMPLETE,
832 'last_attempt_gmt' => current_time( 'mysql', true ),
833 'last_attempt_local' => current_time( 'mysql' ),
834 ),
835 array( 'action_id' => $action_id ),
836 array( '%s' ),
837 array( '%d' )
838 );
839 if ( empty( $updated ) ) {
840 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment
841 }
842
843 /**
844 * Fires after a scheduled action has been completed.
845 *
846 * @since 3.4.2
847 *
848 * @param int $action_id Action ID.
849 */
850 do_action( 'action_scheduler_completed_action', $action_id );
851 }
852
853 /**
854 * Get an action's status.
855 *
856 * @param int $action_id Action ID.
857 *
858 * @return string
859 * @throws \InvalidArgumentException Throw an exception if not status was found for action_id.
860 * @throws \RuntimeException Throw an exception if action status could not be retrieved.
861 */
862 public function get_status( $action_id ) {
863 /** @var \wpdb $wpdb */
864 global $wpdb;
865 $sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
866 $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
867 $status = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
868
869 if ( null === $status ) {
870 throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) );
871 } elseif ( empty( $status ) ) {
872 throw new \RuntimeException( __( 'Unknown status found for action.', 'action-scheduler' ) );
873 } else {
874 return $status;
875 }
876 }
877 }
878