PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.2.1
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.2.1
1.7.7 1.7.6 1.7.5 1.7.4 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 All 33 releases
image-optimization / vendor / woocommerce / action-scheduler / classes / data-stores / ActionScheduler_DBStore.php

ActionScheduler_DBStore.php in Image Optimizer – Compress Images and Convert to WebP or AVIF 1.2.1, at vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php

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