PluginProbe
WooCommerce / 11.0.1
WooCommerce v11.0.1
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / packages / action-scheduler / classes / data-stores / ActionScheduler_DBStore.php
ActionScheduler_DBStore.php
1,363 lines 40.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 /**
23 * Maximum length of args.
24 *
25 * @var int
26 */
27 protected static $max_args_length = 8000;
28
29 /**
30 * Maximum length of index.
31 *
32 * @var int
33 */
34 protected static $max_index_length = 191;
35
36 /**
37 * List of claim filters.
38 *
39 * @var array
40 */
41 protected $claim_filters = array(
42 'group' => '',
43 'hooks' => '',
44 'exclude-groups' => '',
45 );
46
47 /**
48 * Initialize the data store
49 *
50 * @codeCoverageIgnore
51 */
52 public function init() {
53 $table_maker = new ActionScheduler_StoreSchema();
54 $table_maker->init();
55 $table_maker->register_tables();
56 }
57
58 /**
59 * Save an action, checks if this is a unique action before actually saving.
60 *
61 * @param ActionScheduler_Action $action Action object.
62 * @param DateTime|null $scheduled_date Optional schedule date. Default null.
63 *
64 * @return int Action ID.
65 * @throws RuntimeException Throws exception when saving the action fails.
66 */
67 public function save_unique_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
68 return $this->save_action_to_db( $action, $scheduled_date, true );
69 }
70
71 /**
72 * Save an action. Can save duplicate action as well, prefer using `save_unique_action` instead.
73 *
74 * @param ActionScheduler_Action $action Action object.
75 * @param DateTime|null $scheduled_date Optional schedule date. Default null.
76 *
77 * @return int Action ID.
78 * @throws RuntimeException Throws exception when saving the action fails.
79 */
80 public function save_action( ActionScheduler_Action $action, ?DateTime $scheduled_date = null ) {
81 return $this->save_action_to_db( $action, $scheduled_date, false );
82 }
83
84 /**
85 * Save an action.
86 *
87 * @param ActionScheduler_Action $action Action object.
88 * @param ?DateTime $date Optional schedule date. Default null.
89 * @param bool $unique Whether the action should be unique.
90 *
91 * @return int Action ID.
92 * @throws \RuntimeException Throws exception when saving the action fails.
93 */
94 private function save_action_to_db( ActionScheduler_Action $action, ?DateTime $date = null, $unique = false ) {
95 global $wpdb;
96
97 try {
98 $this->validate_action( $action );
99
100 $data = array(
101 'hook' => $action->get_hook(),
102 'status' => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ),
103 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ),
104 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ),
105 'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
106 'group_id' => current( $this->get_group_ids( $action->get_group() ) ),
107 'priority' => $action->get_priority(),
108 );
109
110 $args = wp_json_encode( $action->get_args() );
111 if ( strlen( $args ) <= static::$max_index_length ) {
112 $data['args'] = $args;
113 } else {
114 $data['args'] = $this->hash_args( $args );
115 $data['extended_args'] = $args;
116 }
117
118 $insert_sql = $this->build_insert_sql( $data, $unique );
119
120 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_sql should be already prepared.
121 $wpdb->query( $insert_sql );
122 $action_id = $wpdb->insert_id;
123
124 if ( is_wp_error( $action_id ) ) {
125 throw new \RuntimeException( $action_id->get_error_message() );
126 } elseif ( empty( $action_id ) ) {
127 if ( $unique ) {
128 return 0;
129 }
130 throw new \RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'woocommerce' ) );
131 }
132
133 do_action( 'action_scheduler_stored_action', $action_id );
134
135 return $action_id;
136 } catch ( \Exception $e ) {
137 /* translators: %s: error message */
138 throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'woocommerce' ), $e->getMessage() ), 0 );
139 }
140 }
141
142 /**
143 * Helper function to build insert query.
144 *
145 * @param array $data Row data for action.
146 * @param bool $unique Whether the action should be unique.
147 *
148 * @return string Insert query.
149 */
150 private function build_insert_sql( array $data, $unique ) {
151 global $wpdb;
152
153 $columns = array_keys( $data );
154 $values = array_values( $data );
155 $placeholders = array_map( array( $this, 'get_placeholder_for_column' ), $columns );
156
157 $table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions';
158
159 $column_sql = '`' . implode( '`, `', $columns ) . '`';
160 $placeholder_sql = implode( ', ', $placeholders );
161 $where_clause = $this->build_where_clause_for_insert( $data, $table_name, $unique );
162
163 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $column_sql and $where_clause are already prepared. $placeholder_sql is hardcoded.
164 $insert_query = $wpdb->prepare(
165 "
166 INSERT INTO $table_name ( $column_sql )
167 SELECT $placeholder_sql FROM DUAL
168 WHERE ( $where_clause ) IS NULL",
169 $values
170 );
171 // phpcs:enable
172
173 return $insert_query;
174 }
175
176 /**
177 * Helper method to build where clause for action insert statement.
178 *
179 * @param array $data Row data for action.
180 * @param string $table_name Action table name.
181 * @param bool $unique Where action should be unique.
182 *
183 * @return string Where clause to be used with insert.
184 */
185 private function build_where_clause_for_insert( $data, $table_name, $unique ) {
186 global $wpdb;
187
188 if ( ! $unique ) {
189 return 'SELECT NULL FROM DUAL';
190 }
191
192 $pending_statuses = array(
193 ActionScheduler_Store::STATUS_PENDING,
194 ActionScheduler_Store::STATUS_RUNNING,
195 );
196 $pending_status_placeholders = implode( ', ', array_fill( 0, count( $pending_statuses ), '%s' ) );
197
198 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $pending_status_placeholders is hardcoded.
199 $where_clause = $wpdb->prepare(
200 "
201 SELECT action_id FROM $table_name
202 WHERE status IN ( $pending_status_placeholders )
203 AND hook = %s
204 AND `group_id` = %d
205 AND args = %s
206 ",
207 array_merge(
208 $pending_statuses,
209 array(
210 $data['hook'],
211 $data['group_id'],
212 $data['args'],
213 )
214 )
215 );
216 // phpcs:enable
217
218 return "$where_clause" . ' LIMIT 1';
219 }
220
221 /**
222 * Helper method to get $wpdb->prepare placeholder for a given column name.
223 *
224 * @param string $column_name Name of column in actions table.
225 *
226 * @return string Placeholder to use for given column.
227 */
228 private function get_placeholder_for_column( $column_name ) {
229 $string_columns = array(
230 'hook',
231 'status',
232 'scheduled_date_gmt',
233 'scheduled_date_local',
234 'args',
235 'schedule',
236 'last_attempt_gmt',
237 'last_attempt_local',
238 'extended_args',
239 );
240
241 return in_array( $column_name, $string_columns, true ) ? '%s' : '%d';
242 }
243
244 /**
245 * Generate a hash from json_encoded $args using MD5 as this isn't for security.
246 *
247 * @param string $args JSON encoded action args.
248 * @return string
249 */
250 protected function hash_args( $args ) {
251 return md5( $args );
252 }
253
254 /**
255 * Get action args query param value from action args.
256 *
257 * @param array $args Action args.
258 * @return string
259 */
260 protected function get_args_for_query( $args ) {
261 $encoded = wp_json_encode( $args );
262 if ( strlen( $encoded ) <= static::$max_index_length ) {
263 return $encoded;
264 }
265 return $this->hash_args( $encoded );
266 }
267 /**
268 * Get a group's ID based on its name/slug.
269 *
270 * @param string|array $slugs The string name of a group, or names for several groups.
271 * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group.
272 *
273 * @return array The group IDs, if they exist or were successfully created. May be empty.
274 */
275 protected function get_group_ids( $slugs, $create_if_not_exists = true ) {
276 $slugs = (array) $slugs;
277 $group_ids = array();
278
279 if ( empty( $slugs ) ) {
280 return array();
281 }
282
283 /**
284 * Global.
285 *
286 * @var \wpdb $wpdb
287 */
288 global $wpdb;
289
290 foreach ( $slugs as $slug ) {
291 $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) );
292
293 if ( empty( $group_id ) && $create_if_not_exists ) {
294 $group_id = $this->create_group( $slug );
295 }
296
297 if ( $group_id ) {
298 $group_ids[] = $group_id;
299 }
300 }
301
302 return $group_ids;
303 }
304
305 /**
306 * Create an action group.
307 *
308 * @param string $slug Group slug.
309 *
310 * @return int Group ID.
311 */
312 protected function create_group( $slug ) {
313 /**
314 * Global.
315 *
316 * @var \wpdb $wpdb
317 */
318 global $wpdb;
319
320 $wpdb->insert( $wpdb->actionscheduler_groups, array( 'slug' => $slug ) );
321
322 return (int) $wpdb->insert_id;
323 }
324
325 /**
326 * Retrieve an action.
327 *
328 * @param int $action_id Action ID.
329 *
330 * @return ActionScheduler_Action
331 */
332 public function fetch_action( $action_id ) {
333 /**
334 * Global.
335 *
336 * @var \wpdb $wpdb
337 */
338 global $wpdb;
339
340 $data = $wpdb->get_row(
341 $wpdb->prepare(
342 "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",
343 $action_id
344 )
345 );
346
347 if ( empty( $data ) ) {
348 return $this->get_null_action();
349 }
350
351 if ( ! empty( $data->extended_args ) ) {
352 $data->args = $data->extended_args;
353 unset( $data->extended_args );
354 }
355
356 // Convert NULL dates to zero dates.
357 $date_fields = array(
358 'scheduled_date_gmt',
359 'scheduled_date_local',
360 'last_attempt_gmt',
361 'last_attempt_gmt',
362 );
363 foreach ( $date_fields as $date_field ) {
364 if ( is_null( $data->$date_field ) ) {
365 $data->$date_field = ActionScheduler_StoreSchema::DEFAULT_DATE;
366 }
367 }
368
369 try {
370 $action = $this->make_action_from_db_record( $data );
371 } catch ( ActionScheduler_InvalidActionException $exception ) {
372 do_action( 'action_scheduler_failed_fetch_action', $action_id, $exception );
373 return $this->get_null_action();
374 }
375
376 return $action;
377 }
378
379 /**
380 * Create a null action.
381 *
382 * @return ActionScheduler_NullAction
383 */
384 protected function get_null_action() {
385 return new ActionScheduler_NullAction();
386 }
387
388 /**
389 * Create an action from a database record.
390 *
391 * @param object $data Action database record.
392 *
393 * @return ActionScheduler_Action|ActionScheduler_CanceledAction|ActionScheduler_FinishedAction
394 */
395 protected function make_action_from_db_record( $data ) {
396 $args = json_decode( $data->args, true );
397 if ( ! is_array( $args ) && $data->status === self::STATUS_CANCELED ) {
398 // The handled corrupted action should appear in UI.
399 $args = array();
400 } else {
401 $this->validate_args( $args, $data->action_id );
402 }
403
404 $schedule = @unserialize( $data->schedule ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize WordPress.PHP.NoSilencingOperator
405 if ( false === $schedule && $data->status === self::STATUS_CANCELED ) {
406 // The handled corrupted action should appear in UI.
407 $schedule = new ActionScheduler_NullSchedule();
408 } else {
409 $this->validate_schedule( $schedule, $data->action_id );
410 }
411
412 $group = $data->group ? $data->group : '';
413
414 return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group, $data->priority );
415 }
416
417 /**
418 * Returns the SQL statement to query (or count) actions.
419 *
420 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
421 *
422 * @param array $query Filtering options.
423 * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count.
424 *
425 * @return string SQL statement already properly escaped.
426 * @throws \InvalidArgumentException If the query is invalid.
427 * @throws \RuntimeException When "unknown partial args matching value".
428 */
429 protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) {
430
431 if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) {
432 throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'woocommerce' ) );
433 }
434
435 $query = wp_parse_args(
436 $query,
437 array(
438 'hook' => '',
439 'args' => null,
440 'partial_args_matching' => 'off', // can be 'like' or 'json'.
441 'date' => null,
442 'date_compare' => '<=',
443 'modified' => null,
444 'modified_compare' => '<=',
445 'group' => '',
446 'status' => '',
447 'claimed' => null,
448 'per_page' => 5,
449 'offset' => 0,
450 'orderby' => 'date',
451 'order' => 'ASC',
452 )
453 );
454
455 /**
456 * Global.
457 *
458 * @var \wpdb $wpdb
459 */
460 global $wpdb;
461
462 $db_server_info = is_callable( array( $wpdb, 'db_server_info' ) ) ? $wpdb->db_server_info() : $wpdb->db_version();
463 if ( false !== strpos( $db_server_info, 'MariaDB' ) ) {
464 $supports_json = version_compare(
465 PHP_VERSION_ID >= 80016 ? $wpdb->db_version() : preg_replace( '/[^0-9.].*/', '', str_replace( '5.5.5-', '', $db_server_info ) ),
466 '10.2',
467 '>='
468 );
469 } else {
470 $supports_json = version_compare( $wpdb->db_version(), '5.7', '>=' );
471 }
472
473 $sql = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id';
474 $sql .= " FROM {$wpdb->actionscheduler_actions} a";
475 $sql_params = array();
476
477 if ( ! empty( $query['group'] ) || 'group' === $query['orderby'] ) {
478 $sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id";
479 }
480
481 $sql .= ' WHERE 1=1';
482
483 if ( ! empty( $query['group'] ) ) {
484 $sql .= ' AND g.slug=%s';
485 $sql_params[] = $query['group'];
486 }
487
488 if ( ! empty( $query['hook'] ) ) {
489 $sql .= ' AND a.hook=%s';
490 $sql_params[] = $query['hook'];
491 }
492
493 if ( ! is_null( $query['args'] ) ) {
494 switch ( $query['partial_args_matching'] ) {
495 case 'json':
496 if ( ! $supports_json ) {
497 throw new \RuntimeException( __( 'JSON partial matching not supported in your environment. Please check your MySQL/MariaDB version.', 'woocommerce' ) );
498 }
499 $supported_types = array(
500 'integer' => '%d',
501 'boolean' => '%s',
502 'double' => '%f',
503 'string' => '%s',
504 );
505 foreach ( $query['args'] as $key => $value ) {
506 $value_type = gettype( $value );
507 if ( 'boolean' === $value_type ) {
508 $value = $value ? 'true' : 'false';
509 }
510 $placeholder = isset( $supported_types[ $value_type ] ) ? $supported_types[ $value_type ] : false;
511 if ( ! $placeholder ) {
512 throw new \RuntimeException(
513 sprintf(
514 /* translators: %s: provided value type */
515 __( 'The value type for the JSON partial matching is not supported. Must be either integer, boolean, double or string. %s type provided.', 'woocommerce' ),
516 $value_type
517 )
518 );
519 }
520 $sql .= ' AND JSON_EXTRACT(a.args, %s)=' . $placeholder;
521 $sql_params[] = '$.' . $key;
522 $sql_params[] = $value;
523 }
524 break;
525 case 'like':
526 foreach ( $query['args'] as $key => $value ) {
527 $sql .= ' AND a.args LIKE %s';
528 $json_partial = $wpdb->esc_like( trim( wp_json_encode( array( $key => $value ) ), '{}' ) );
529 $sql_params[] = "%{$json_partial}%";
530 }
531 break;
532 case 'off':
533 $sql .= ' AND a.args=%s';
534 $sql_params[] = $this->get_args_for_query( $query['args'] );
535 break;
536 default:
537 throw new \RuntimeException( __( 'Unknown partial args matching value.', 'woocommerce' ) );
538 }
539 }
540
541 if ( $query['status'] ) {
542 $statuses = (array) $query['status'];
543 $placeholders = array_fill( 0, count( $statuses ), '%s' );
544 $sql .= ' AND a.status IN (' . join( ', ', $placeholders ) . ')';
545 $sql_params = array_merge( $sql_params, array_values( $statuses ) );
546 }
547
548 if ( $query['date'] instanceof \DateTime ) {
549 $date = clone $query['date'];
550 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
551 $date_string = $date->format( 'Y-m-d H:i:s' );
552 $comparator = $this->validate_sql_comparator( $query['date_compare'] );
553 $sql .= " AND a.scheduled_date_gmt $comparator %s";
554 $sql_params[] = $date_string;
555 }
556
557 if ( $query['modified'] instanceof \DateTime ) {
558 $modified = clone $query['modified'];
559 $modified->setTimezone( new \DateTimeZone( 'UTC' ) );
560 $date_string = $modified->format( 'Y-m-d H:i:s' );
561 $comparator = $this->validate_sql_comparator( $query['modified_compare'] );
562 $sql .= " AND a.last_attempt_gmt $comparator %s";
563 $sql_params[] = $date_string;
564 }
565
566 if ( true === $query['claimed'] ) {
567 $sql .= ' AND a.claim_id != 0';
568 } elseif ( false === $query['claimed'] ) {
569 $sql .= ' AND a.claim_id = 0';
570 } elseif ( ! is_null( $query['claimed'] ) ) {
571 $sql .= ' AND a.claim_id = %d';
572 $sql_params[] = $query['claimed'];
573 }
574
575 if ( ! empty( $query['search'] ) ) {
576 $sql .= ' AND (a.hook LIKE %s OR (a.extended_args IS NULL AND a.args LIKE %s) OR a.extended_args LIKE %s';
577 for ( $i = 0; $i < 3; $i++ ) {
578 $sql_params[] = sprintf( '%%%s%%', $query['search'] );
579 }
580
581 $search_claim_id = (int) $query['search'];
582 if ( $search_claim_id ) {
583 $sql .= ' OR a.claim_id = %d';
584 $sql_params[] = $search_claim_id;
585 }
586
587 $sql .= ')';
588 }
589
590 if ( 'select' === $select_or_count ) {
591 if ( 'ASC' === strtoupper( $query['order'] ) ) {
592 $order = 'ASC';
593 } else {
594 $order = 'DESC';
595 }
596 switch ( $query['orderby'] ) {
597 case 'hook':
598 $sql .= " ORDER BY a.hook $order";
599 break;
600 case 'group':
601 $sql .= " ORDER BY g.slug $order";
602 break;
603 case 'modified':
604 $sql .= " ORDER BY a.last_attempt_gmt $order";
605 break;
606 case 'none':
607 break;
608 case 'action_id':
609 $sql .= " ORDER BY a.action_id $order";
610 break;
611 case 'date':
612 default:
613 $sql .= " ORDER BY a.scheduled_date_gmt $order";
614 break;
615 }
616
617 if ( $query['per_page'] > 0 ) {
618 $sql .= ' LIMIT %d, %d';
619 $sql_params[] = $query['offset'];
620 $sql_params[] = $query['per_page'];
621 }
622 }
623
624 if ( ! empty( $sql_params ) ) {
625 $sql = $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
626 }
627
628 return $sql;
629 }
630
631 /**
632 * Query for action count or list of action IDs.
633 *
634 * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status.
635 *
636 * @see ActionScheduler_Store::query_actions for $query arg usage.
637 *
638 * @param array $query Query filtering options.
639 * @param string $query_type Whether to select or count the results. Defaults to select.
640 *
641 * @return string|array|null The IDs of actions matching the query. Null on failure.
642 */
643 public function query_actions( $query = array(), $query_type = 'select' ) {
644 /**
645 * Global.
646 *
647 * @var wpdb $wpdb
648 */
649 global $wpdb;
650
651 $sql = $this->get_query_actions_sql( $query, $query_type );
652
653 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
654 }
655
656 /**
657 * Get a count of all actions in the store, grouped by status.
658 *
659 * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status.
660 */
661 public function action_counts() {
662 global $wpdb;
663
664 $sql = "SELECT a.status, count(a.status) as 'count'";
665 $sql .= " FROM {$wpdb->actionscheduler_actions} a";
666 $sql .= ' GROUP BY a.status';
667
668 $actions_count_by_status = array();
669 $action_stati_and_labels = $this->get_status_labels();
670
671 foreach ( $wpdb->get_results( $sql ) as $action_data ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
672 // Ignore any actions with invalid status.
673 if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) {
674 $actions_count_by_status[ $action_data->status ] = $action_data->count;
675 }
676 }
677
678 return $actions_count_by_status;
679 }
680
681 /**
682 * Cancel an action.
683 *
684 * @param int $action_id Action ID.
685 *
686 * @return void
687 * @throws \InvalidArgumentException If the action update failed.
688 */
689 public function cancel_action( $action_id ) {
690 /**
691 * Global.
692 *
693 * @var \wpdb $wpdb
694 */
695 global $wpdb;
696
697 $updated = $wpdb->update(
698 $wpdb->actionscheduler_actions,
699 array( 'status' => self::STATUS_CANCELED ),
700 array( 'action_id' => $action_id ),
701 array( '%s' ),
702 array( '%d' )
703 );
704 if ( false === $updated ) {
705 /* translators: %s: action ID */
706 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to cancel this action. It may may have been deleted by another process.', 'woocommerce' ), $action_id ) );
707 }
708 do_action( 'action_scheduler_canceled_action', $action_id );
709 }
710
711 /**
712 * Cancel pending actions by hook.
713 *
714 * @since 3.0.0
715 *
716 * @param string $hook Hook name.
717 *
718 * @return void
719 */
720 public function cancel_actions_by_hook( $hook ) {
721 $this->bulk_cancel_actions( array( 'hook' => $hook ) );
722 }
723
724 /**
725 * Cancel pending actions by group.
726 *
727 * @param string $group Group slug.
728 *
729 * @return void
730 */
731 public function cancel_actions_by_group( $group ) {
732 $this->bulk_cancel_actions( array( 'group' => $group ) );
733 }
734
735 /**
736 * Bulk cancel actions.
737 *
738 * @since 3.0.0
739 *
740 * @param array $query_args Query parameters.
741 */
742 protected function bulk_cancel_actions( $query_args ) {
743 /**
744 * Global.
745 *
746 * @var \wpdb $wpdb
747 */
748 global $wpdb;
749
750 if ( ! is_array( $query_args ) ) {
751 return;
752 }
753
754 // Don't cancel actions that are already canceled.
755 if ( isset( $query_args['status'] ) && self::STATUS_CANCELED === $query_args['status'] ) {
756 return;
757 }
758
759 $action_ids = true;
760 $query_args = wp_parse_args(
761 $query_args,
762 array(
763 'per_page' => 1000,
764 'status' => self::STATUS_PENDING,
765 'orderby' => 'none',
766 )
767 );
768
769 while ( $action_ids ) {
770 $action_ids = $this->query_actions( $query_args );
771 if ( empty( $action_ids ) ) {
772 break;
773 }
774
775 $format = array_fill( 0, count( $action_ids ), '%d' );
776 $query_in = '(' . implode( ',', $format ) . ')';
777 $parameters = $action_ids;
778 array_unshift( $parameters, self::STATUS_CANCELED );
779
780 $wpdb->query(
781 $wpdb->prepare(
782 "UPDATE {$wpdb->actionscheduler_actions} SET status = %s WHERE action_id IN {$query_in}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
783 $parameters
784 )
785 );
786
787 do_action( 'action_scheduler_bulk_cancel_actions', $action_ids );
788 }
789 }
790
791 /**
792 * Delete an action.
793 *
794 * @param int $action_id Action ID.
795 * @throws \InvalidArgumentException If the action deletion failed.
796 */
797 public function delete_action( $action_id ) {
798 /**
799 * Global.
800 *
801 * @var \wpdb $wpdb
802 */
803 global $wpdb;
804
805 $deleted = $wpdb->delete( $wpdb->actionscheduler_actions, array( 'action_id' => $action_id ), array( '%d' ) );
806 if ( empty( $deleted ) ) {
807 /* translators: %s is the action ID */
808 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to delete this action. It may may have been deleted by another process.', 'woocommerce' ), $action_id ) );
809 }
810 do_action( 'action_scheduler_deleted_action', $action_id );
811 }
812
813 /**
814 * Get the schedule date for an action.
815 *
816 * @param string $action_id Action ID.
817 *
818 * @return \DateTime The local date the action is scheduled to run, or the date that it ran.
819 */
820 public function get_date( $action_id ) {
821 $date = $this->get_date_gmt( $action_id );
822 ActionScheduler_TimezoneHelper::set_local_timezone( $date );
823 return $date;
824 }
825
826 /**
827 * Get the GMT schedule date for an action.
828 *
829 * @param int $action_id Action ID.
830 *
831 * @throws \InvalidArgumentException If action cannot be identified.
832 * @return \DateTime The GMT date the action is scheduled to run, or the date that it ran.
833 */
834 protected function get_date_gmt( $action_id ) {
835 /**
836 * Global.
837 *
838 * @var \wpdb $wpdb
839 */
840 global $wpdb;
841
842 $record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) );
843 if ( empty( $record ) ) {
844 /* translators: %s is the action ID */
845 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to determine the date of this action. It may may have been deleted by another process.', 'woocommerce' ), $action_id ) );
846 }
847 if ( self::STATUS_PENDING === $record->status ) {
848 return as_get_datetime_object( $record->scheduled_date_gmt );
849 } else {
850 return as_get_datetime_object( $record->last_attempt_gmt );
851 }
852 }
853
854 /**
855 * Stake a claim on actions.
856 *
857 * @param int $max_actions Maximum number of action to include in claim.
858 * @param DateTime|null $before_date Jobs must be schedule before this date. Defaults to now.
859 * @param array $hooks Hooks to filter for.
860 * @param string $group Group to filter for.
861 *
862 * @return ActionScheduler_ActionClaim
863 */
864 public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
865 $claim_id = $this->generate_claim_id();
866
867 $this->claim_before_date = $before_date;
868 $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
869 $action_ids = $this->find_actions_by_claim_id( $claim_id );
870 $this->claim_before_date = null;
871
872 return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
873 }
874
875 /**
876 * Generate a new action claim.
877 *
878 * @return int Claim ID.
879 */
880 protected function generate_claim_id() {
881 /**
882 * Global.
883 *
884 * @var \wpdb $wpdb
885 */
886 global $wpdb;
887
888 $now = as_get_datetime_object();
889 $wpdb->insert( $wpdb->actionscheduler_claims, array( 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ) );
890
891 return $wpdb->insert_id;
892 }
893
894 /**
895 * Set a claim filter.
896 *
897 * @param string $filter_name Claim filter name.
898 * @param mixed $filter_values Values to filter.
899 * @return void
900 */
901 public function set_claim_filter( $filter_name, $filter_values ) {
902 if ( isset( $this->claim_filters[ $filter_name ] ) ) {
903 $this->claim_filters[ $filter_name ] = $filter_values;
904 }
905 }
906
907 /**
908 * Get the claim filter value.
909 *
910 * @param string $filter_name Claim filter name.
911 * @return mixed
912 */
913 public function get_claim_filter( $filter_name ) {
914 if ( isset( $this->claim_filters[ $filter_name ] ) ) {
915 return $this->claim_filters[ $filter_name ];
916 }
917
918 return '';
919 }
920
921 /**
922 * Mark actions claimed.
923 *
924 * @param string $claim_id Claim Id.
925 * @param int $limit Number of action to include in claim.
926 * @param DateTime|null $before_date Should use UTC timezone.
927 * @param array $hooks Hooks to filter for.
928 * @param string $group Group to filter for.
929 *
930 * @return int The number of actions that were claimed.
931 * @throws \InvalidArgumentException Throws InvalidArgumentException if group doesn't exist.
932 * @throws \RuntimeException Throws RuntimeException if unable to claim action.
933 */
934 protected function claim_actions( $claim_id, $limit, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
935 /**
936 * Global.
937 *
938 * @var \wpdb $wpdb
939 */
940 global $wpdb;
941 $now = as_get_datetime_object();
942 $date = is_null( $before_date ) ? $now : clone $before_date;
943
944 // Set claim filters.
945 if ( ! empty( $hooks ) ) {
946 $this->set_claim_filter( 'hooks', $hooks );
947 } else {
948 $hooks = $this->get_claim_filter( 'hooks' );
949 }
950 if ( ! empty( $group ) ) {
951 $this->set_claim_filter( 'group', $group );
952 } else {
953 $group = $this->get_claim_filter( 'group' );
954 }
955
956 $where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s';
957 $where_params = array(
958 $date->format( 'Y-m-d H:i:s' ),
959 self::STATUS_PENDING,
960 );
961
962 if ( ! empty( $hooks ) ) {
963 $placeholders = array_fill( 0, count( $hooks ), '%s' );
964 $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')';
965 $where_params = array_merge( $where_params, array_values( $hooks ) );
966 }
967
968 $group_operator = 'IN';
969 if ( empty( $group ) ) {
970 $group = $this->get_claim_filter( 'exclude-groups' );
971 $group_operator = 'NOT IN';
972 }
973
974 if ( ! empty( $group ) ) {
975 $group_ids = $this->get_group_ids( $group, false );
976
977 // throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour.
978 if ( empty( $group_ids ) ) {
979 throw new InvalidArgumentException(
980 sprintf(
981 /* translators: %s: group name(s) */
982 _n(
983 'The group "%s" does not exist.',
984 'The groups "%s" do not exist.',
985 is_array( $group ) ? count( $group ) : 1,
986 'woocommerce'
987 ),
988 $group
989 )
990 );
991 }
992
993 $id_list = implode( ',', array_map( 'intval', $group_ids ) );
994 $where .= " AND group_id {$group_operator} ( $id_list )";
995 }
996
997 /**
998 * Sets the order-by clause used in the action claim query.
999 *
1000 * @param string $order_by_sql
1001 * @param string $claim_id Claim Id.
1002 * @param array $hooks Hooks to filter for.
1003 *
1004 * @since 3.8.3 Made $claim_id and $hooks available.
1005 * @since 3.4.0
1006 */
1007 $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC', $claim_id, $hooks );
1008 $skip_locked = $this->db_supports_skip_locked() ? ' SKIP LOCKED' : '';
1009
1010 // Selecting the action_ids that we plan to claim, while skipping any locked rows to avoid deadlocking.
1011 $select_sql = $wpdb->prepare( "SELECT action_id from {$wpdb->actionscheduler_actions} {$where} {$order} LIMIT %d FOR UPDATE{$skip_locked}", array_merge( $where_params, array( $limit ) ) );
1012
1013 // Now place it into an UPDATE statement by joining the result sets, allowing for the SKIP LOCKED behavior to take effect.
1014 $update_sql = "UPDATE {$wpdb->actionscheduler_actions} t1 JOIN ( $select_sql ) t2 ON t1.action_id = t2.action_id SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s";
1015 $update_params = array(
1016 $claim_id,
1017 $now->format( 'Y-m-d H:i:s' ),
1018 current_time( 'mysql' ),
1019 );
1020
1021 $rows_affected = $wpdb->query( $wpdb->prepare( $update_sql, $update_params ) );
1022 if ( false === $rows_affected ) {
1023 $error = empty( $wpdb->last_error )
1024 ? _x( 'unknown', 'database error', 'woocommerce' )
1025 : $wpdb->last_error;
1026 throw new \RuntimeException(
1027 sprintf(
1028 /* translators: %s database error. */
1029 __( 'Unable to claim actions. Database error: %s.', 'woocommerce' ),
1030 $error
1031 )
1032 );
1033 }
1034
1035 return (int) $rows_affected;
1036 }
1037
1038 /**
1039 * Determines whether the database supports using SKIP LOCKED. This logic mimicks the $wpdb::has_cap() logic.
1040 *
1041 * SKIP_LOCKED support was added to MariaDB in 10.6.0 and to MySQL in 8.0.1
1042 *
1043 * @return bool
1044 */
1045 private function db_supports_skip_locked() {
1046 global $wpdb;
1047 $db_version = $wpdb->db_version();
1048 $db_server_info = $wpdb->db_server_info();
1049 $is_mariadb = ( false !== strpos( $db_server_info, 'MariaDB' ) );
1050
1051 if ( $is_mariadb &&
1052 '5.5.5' === $db_version &&
1053 PHP_VERSION_ID < 80016 // PHP 8.0.15 or older.
1054 ) {
1055 /*
1056 * Account for MariaDB version being prefixed with '5.5.5-' on older PHP versions.
1057 */
1058 $db_server_info = preg_replace( '/^5\.5\.5-(.*)/', '$1', $db_server_info );
1059 $db_version = preg_replace( '/[^0-9.].*/', '', $db_server_info );
1060 }
1061
1062 $is_supported = ( $is_mariadb && version_compare( $db_version, '10.6.0', '>=' ) ) ||
1063 ( ! $is_mariadb && version_compare( $db_version, '8.0.1', '>=' ) );
1064
1065 /**
1066 * Filter whether the database supports the SKIP LOCKED modifier for queries.
1067 *
1068 * @param bool $is_supported Whether SKIP LOCKED is supported.
1069 *
1070 * @since 3.9.3
1071 */
1072 return apply_filters( 'action_scheduler_db_supports_skip_locked', $is_supported );
1073 }
1074
1075 /**
1076 * Get the number of active claims.
1077 *
1078 * @return int
1079 */
1080 public function get_claim_count() {
1081 global $wpdb;
1082
1083 $sql = "
1084 SELECT COUNT(*)
1085 FROM {$wpdb->actionscheduler_claims} c
1086 WHERE EXISTS (
1087 SELECT 1
1088 FROM {$wpdb->actionscheduler_actions} a
1089 WHERE a.claim_id = c.claim_id
1090 AND a.status IN ( %s, %s )
1091 )
1092 ";
1093 $sql = $wpdb->prepare( $sql, array( self::STATUS_PENDING, self::STATUS_RUNNING ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1094
1095 return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1096 }
1097
1098 /**
1099 * Return an action's claim ID, as stored in the claim_id column.
1100 *
1101 * @param string $action_id Action ID.
1102 * @return mixed
1103 */
1104 public function get_claim_id( $action_id ) {
1105 /**
1106 * Global.
1107 *
1108 * @var \wpdb $wpdb
1109 */
1110 global $wpdb;
1111
1112 $sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
1113 $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1114
1115 return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1116 }
1117
1118 /**
1119 * Retrieve the action IDs of action in a claim.
1120 *
1121 * @param int $claim_id Claim ID.
1122 * @return int[]
1123 */
1124 public function find_actions_by_claim_id( $claim_id ) {
1125 /**
1126 * Global.
1127 *
1128 * @var \wpdb $wpdb
1129 */
1130 global $wpdb;
1131
1132 $action_ids = array();
1133 $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object();
1134 $cut_off = $before_date->format( 'Y-m-d H:i:s' );
1135
1136 $sql = $wpdb->prepare(
1137 "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC",
1138 $claim_id
1139 );
1140
1141 // Verify that the scheduled date for each action is within the expected bounds (in some unusual
1142 // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify).
1143 foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1144 if ( $claimed_action->scheduled_date_gmt <= $cut_off ) {
1145 $action_ids[] = absint( $claimed_action->action_id );
1146 }
1147 }
1148
1149 return $action_ids;
1150 }
1151
1152 /**
1153 * Release pending actions from a claim and delete the claim.
1154 *
1155 * @param ActionScheduler_ActionClaim $claim Claim object.
1156 * @throws \RuntimeException When unable to release actions from claim.
1157 */
1158 public function release_claim( ActionScheduler_ActionClaim $claim ) {
1159 /**
1160 * Global.
1161 *
1162 * @var \wpdb $wpdb
1163 */
1164 global $wpdb;
1165
1166 if ( 0 === intval( $claim->get_id() ) ) {
1167 // Verify that the claim_id is valid before attempting to release it.
1168 return;
1169 }
1170
1171 /**
1172 * Deadlock warning: This function modifies actions to release them from claims that have been processed. Earlier, we used to it in a atomic query, i.e. we would update all actions belonging to a particular claim_id with claim_id = 0.
1173 * While this was functionally correct, it would cause deadlock, since this update query will hold a lock on the claim_id_.. index on the action table.
1174 * This allowed the possibility of a race condition, where the claimer query is also running at the same time, then the claimer query will also try to acquire a lock on the claim_id_.. index, and in this case if claim release query has already progressed to the point of acquiring the lock, but have not updated yet, it would cause a deadlock.
1175 *
1176 * We resolve this by getting all the actions_id that we want to release claim from in a separate query, and then releasing the claim on each of them. This way, our lock is acquired on the action_id index instead of the claim_id index. Note that the lock on claim_id will still be acquired, but it will only when we actually make the update, rather than when we select the actions.
1177 *
1178 * We only release pending actions in order for them to be claimed by another process.
1179 */
1180 $action_ids = $wpdb->get_col( $wpdb->prepare( "SELECT action_id FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d AND status = %s", $claim->get_id(), self::STATUS_PENDING ) );
1181
1182 $row_updates = 0;
1183 if ( count( $action_ids ) > 0 ) {
1184 $action_id_string = implode( ',', array_map( 'absint', $action_ids ) );
1185 $row_updates = $wpdb->query( "UPDATE {$wpdb->actionscheduler_actions} SET claim_id = 0 WHERE action_id IN ({$action_id_string})" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1186 }
1187
1188 $wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) );
1189
1190 if ( $row_updates < count( $action_ids ) ) {
1191 throw new RuntimeException(
1192 sprintf(
1193 // translators: %d is an id.
1194 __( 'Unable to release actions from claim id %d.', 'woocommerce' ),
1195 $claim->get_id()
1196 )
1197 );
1198 }
1199 }
1200
1201 /**
1202 * Remove the claim from an action.
1203 *
1204 * @param int $action_id Action ID.
1205 *
1206 * @return void
1207 */
1208 public function unclaim_action( $action_id ) {
1209 /**
1210 * Global.
1211 *
1212 * @var \wpdb $wpdb
1213 */
1214 global $wpdb;
1215
1216 $wpdb->update(
1217 $wpdb->actionscheduler_actions,
1218 array( 'claim_id' => 0 ),
1219 array( 'action_id' => $action_id ),
1220 array( '%s' ),
1221 array( '%d' )
1222 );
1223 }
1224
1225 /**
1226 * Mark an action as failed.
1227 *
1228 * @param int $action_id Action ID.
1229 * @throws \InvalidArgumentException Throw an exception if action was not updated.
1230 */
1231 public function mark_failure( $action_id ) {
1232 /**
1233 * Global.
1234
1235 * @var \wpdb $wpdb
1236 */
1237 global $wpdb;
1238
1239 $updated = $wpdb->update(
1240 $wpdb->actionscheduler_actions,
1241 array(
1242 'status' => self::STATUS_FAILED,
1243 'last_attempt_gmt' => current_time( 'mysql', true ),
1244 'last_attempt_local' => current_time( 'mysql' ),
1245 ),
1246 array( 'action_id' => $action_id ),
1247 array( '%s', '%s', '%s' ),
1248 array( '%d' )
1249 );
1250 if ( empty( $updated ) ) {
1251 /* translators: %s is the action ID */
1252 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having failed. It may may have been deleted by another process.', 'woocommerce' ), $action_id ) );
1253 }
1254 }
1255
1256 /**
1257 * Add execution message to action log.
1258 *
1259 * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress').
1260 *
1261 * @param int $action_id Action ID.
1262 *
1263 * @return void
1264 */
1265 public function log_execution( $action_id ) {
1266 /**
1267 * Global.
1268 *
1269 * @var \wpdb $wpdb
1270 */
1271 global $wpdb;
1272
1273 $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d";
1274 $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1275
1276 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1277 $status_updated = $wpdb->query( $sql );
1278
1279 if ( ! $status_updated ) {
1280 throw new Exception(
1281 sprintf(
1282 /* translators: 1: action ID. 2: status slug. */
1283 __( 'Unable to update the status of action %1$d to %2$s.', 'woocommerce' ),
1284 $action_id,
1285 self::STATUS_RUNNING
1286 )
1287 );
1288 }
1289 }
1290
1291 /**
1292 * Mark an action as complete.
1293 *
1294 * @param int $action_id Action ID.
1295 *
1296 * @return void
1297 * @throws \InvalidArgumentException Throw an exception if action was not updated.
1298 */
1299 public function mark_complete( $action_id ) {
1300 /**
1301 * Global.
1302 *
1303 * @var \wpdb $wpdb
1304 */
1305 global $wpdb;
1306
1307 $updated = $wpdb->update(
1308 $wpdb->actionscheduler_actions,
1309 array(
1310 'status' => self::STATUS_COMPLETE,
1311 'last_attempt_gmt' => current_time( 'mysql', true ),
1312 'last_attempt_local' => current_time( 'mysql' ),
1313 ),
1314 array( 'action_id' => $action_id ),
1315 array( '%s', '%s', '%s' ),
1316 array( '%d' )
1317 );
1318 if ( empty( $updated ) ) {
1319 /* translators: %s is the action ID */
1320 throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having completed. It may may have been deleted by another process.', 'woocommerce' ), $action_id ) );
1321 }
1322
1323 /**
1324 * Fires after a scheduled action has been completed.
1325 *
1326 * @since 3.4.2
1327 *
1328 * @param int $action_id Action ID.
1329 */
1330 do_action( 'action_scheduler_completed_action', $action_id );
1331 }
1332
1333 /**
1334 * Get an action's status.
1335 *
1336 * @param int $action_id Action ID.
1337 *
1338 * @return string
1339 * @throws \InvalidArgumentException Throw an exception if not status was found for action_id.
1340 * @throws \RuntimeException Throw an exception if action status could not be retrieved.
1341 */
1342 public function get_status( $action_id ) {
1343 /**
1344 * Global.
1345 *
1346 * @var \wpdb $wpdb
1347 */
1348 global $wpdb;
1349
1350 $sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
1351 $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1352 $status = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1353
1354 if ( is_null( $status ) ) {
1355 throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'woocommerce' ) );
1356 } elseif ( empty( $status ) ) {
1357 throw new \RuntimeException( __( 'Unknown status found for action.', 'woocommerce' ) );
1358 } else {
1359 return $status;
1360 }
1361 }
1362 }
1363