Passed
Push — main ( aaef5c...e4c121 )
by TARIQ
71:39
created
action-scheduler/classes/migration/ActionScheduler_DBStoreMigrator.php 1 patch
Indentation   +31 added lines, -31 removed lines patch added patch discarded remove patch
@@ -9,39 +9,39 @@
 block discarded – undo
9 9
  */
10 10
 class ActionScheduler_DBStoreMigrator extends ActionScheduler_DBStore {
11 11
 
12
-	/**
13
-	 * Save an action with optional last attempt date.
14
-	 *
15
-	 * Normally, saving an action sets its attempted date to 0000-00-00 00:00:00 because when an action is first saved,
16
-	 * it can't have been attempted yet, but migrated completed actions will have an attempted date, so we need to save
17
-	 * that when first saving the action.
18
-	 *
19
-	 * @param ActionScheduler_Action $action
20
-	 * @param \DateTime $scheduled_date Optional date of the first instance to store.
21
-	 * @param \DateTime $last_attempt_date Optional date the action was last attempted.
22
-	 *
23
-	 * @return string The action ID
24
-	 * @throws \RuntimeException When the action is not saved.
25
-	 */
26
-	public function save_action( ActionScheduler_Action $action, \DateTime $scheduled_date = null, \DateTime $last_attempt_date = null ){
27
-		try {
28
-			/** @var \wpdb $wpdb */
29
-			global $wpdb;
12
+    /**
13
+     * Save an action with optional last attempt date.
14
+     *
15
+     * Normally, saving an action sets its attempted date to 0000-00-00 00:00:00 because when an action is first saved,
16
+     * it can't have been attempted yet, but migrated completed actions will have an attempted date, so we need to save
17
+     * that when first saving the action.
18
+     *
19
+     * @param ActionScheduler_Action $action
20
+     * @param \DateTime $scheduled_date Optional date of the first instance to store.
21
+     * @param \DateTime $last_attempt_date Optional date the action was last attempted.
22
+     *
23
+     * @return string The action ID
24
+     * @throws \RuntimeException When the action is not saved.
25
+     */
26
+    public function save_action( ActionScheduler_Action $action, \DateTime $scheduled_date = null, \DateTime $last_attempt_date = null ){
27
+        try {
28
+            /** @var \wpdb $wpdb */
29
+            global $wpdb;
30 30
 
31
-			$action_id = parent::save_action( $action, $scheduled_date );
31
+            $action_id = parent::save_action( $action, $scheduled_date );
32 32
 
33
-			if ( null !== $last_attempt_date ) {
34
-				$data = [
35
-					'last_attempt_gmt'   => $this->get_scheduled_date_string( $action, $last_attempt_date ),
36
-					'last_attempt_local' => $this->get_scheduled_date_string_local( $action, $last_attempt_date ),
37
-				];
33
+            if ( null !== $last_attempt_date ) {
34
+                $data = [
35
+                    'last_attempt_gmt'   => $this->get_scheduled_date_string( $action, $last_attempt_date ),
36
+                    'last_attempt_local' => $this->get_scheduled_date_string_local( $action, $last_attempt_date ),
37
+                ];
38 38
 
39
-				$wpdb->update( $wpdb->actionscheduler_actions, $data, array( 'action_id' => $action_id ), array( '%s', '%s' ), array( '%d' ) );
40
-			}
39
+                $wpdb->update( $wpdb->actionscheduler_actions, $data, array( 'action_id' => $action_id ), array( '%s', '%s' ), array( '%d' ) );
40
+            }
41 41
 
42
-			return $action_id;
43
-		} catch ( \Exception $e ) {
44
-			throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'woocommerce' ), $e->getMessage() ), 0 );
45
-		}
46
-	}
42
+            return $action_id;
43
+        } catch ( \Exception $e ) {
44
+            throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'woocommerce' ), $e->getMessage() ), 0 );
45
+        }
46
+    }
47 47
 }
Please login to merge, or discard this patch.
packages/action-scheduler/classes/migration/DryRun_LogMigrator.php 1 patch
Indentation   +9 added lines, -9 removed lines patch added patch discarded remove patch
@@ -11,13 +11,13 @@
 block discarded – undo
11 11
  * @codeCoverageIgnore
12 12
  */
13 13
 class DryRun_LogMigrator extends LogMigrator {
14
-	/**
15
-	 * Simulate migrating an action log.
16
-	 *
17
-	 * @param int $source_action_id Source logger object.
18
-	 * @param int $destination_action_id Destination logger object.
19
-	 */
20
-	public function migrate( $source_action_id, $destination_action_id ) {
21
-		// no-op
22
-	}
14
+    /**
15
+     * Simulate migrating an action log.
16
+     *
17
+     * @param int $source_action_id Source logger object.
18
+     * @param int $destination_action_id Destination logger object.
19
+     */
20
+    public function migrate( $source_action_id, $destination_action_id ) {
21
+        // no-op
22
+    }
23 23
 }
24 24
\ No newline at end of file
Please login to merge, or discard this patch.
woocommerce/packages/action-scheduler/classes/ActionScheduler_ListTable.php 1 patch
Indentation   +634 added lines, -634 removed lines patch added patch discarded remove patch
@@ -6,638 +6,638 @@
 block discarded – undo
6 6
  */
7 7
 class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
8 8
 
9
-	/**
10
-	 * The package name.
11
-	 *
12
-	 * @var string
13
-	 */
14
-	protected $package = 'action-scheduler';
15
-
16
-	/**
17
-	 * Columns to show (name => label).
18
-	 *
19
-	 * @var array
20
-	 */
21
-	protected $columns = array();
22
-
23
-	/**
24
-	 * Actions (name => label).
25
-	 *
26
-	 * @var array
27
-	 */
28
-	protected $row_actions = array();
29
-
30
-	/**
31
-	 * The active data stores
32
-	 *
33
-	 * @var ActionScheduler_Store
34
-	 */
35
-	protected $store;
36
-
37
-	/**
38
-	 * A logger to use for getting action logs to display
39
-	 *
40
-	 * @var ActionScheduler_Logger
41
-	 */
42
-	protected $logger;
43
-
44
-	/**
45
-	 * A ActionScheduler_QueueRunner runner instance (or child class)
46
-	 *
47
-	 * @var ActionScheduler_QueueRunner
48
-	 */
49
-	protected $runner;
50
-
51
-	/**
52
-	 * Bulk actions. The key of the array is the method name of the implementation:
53
-	 *
54
-	 *     bulk_<key>(array $ids, string $sql_in).
55
-	 *
56
-	 * See the comments in the parent class for further details
57
-	 *
58
-	 * @var array
59
-	 */
60
-	protected $bulk_actions = array();
61
-
62
-	/**
63
-	 * Flag variable to render our notifications, if any, once.
64
-	 *
65
-	 * @var bool
66
-	 */
67
-	protected static $did_notification = false;
68
-
69
-	/**
70
-	 * Array of seconds for common time periods, like week or month, alongside an internationalised string representation, i.e. "Day" or "Days"
71
-	 *
72
-	 * @var array
73
-	 */
74
-	private static $time_periods;
75
-
76
-	/**
77
-	 * Sets the current data store object into `store->action` and initialises the object.
78
-	 *
79
-	 * @param ActionScheduler_Store $store
80
-	 * @param ActionScheduler_Logger $logger
81
-	 * @param ActionScheduler_QueueRunner $runner
82
-	 */
83
-	public function __construct( ActionScheduler_Store $store, ActionScheduler_Logger $logger, ActionScheduler_QueueRunner $runner ) {
84
-
85
-		$this->store  = $store;
86
-		$this->logger = $logger;
87
-		$this->runner = $runner;
88
-
89
-		$this->table_header = __( 'Scheduled Actions', 'woocommerce' );
90
-
91
-		$this->bulk_actions = array(
92
-			'delete' => __( 'Delete', 'woocommerce' ),
93
-		);
94
-
95
-		$this->columns = array(
96
-			'hook'        => __( 'Hook', 'woocommerce' ),
97
-			'status'      => __( 'Status', 'woocommerce' ),
98
-			'args'        => __( 'Arguments', 'woocommerce' ),
99
-			'group'       => __( 'Group', 'woocommerce' ),
100
-			'recurrence'  => __( 'Recurrence', 'woocommerce' ),
101
-			'schedule'    => __( 'Scheduled Date', 'woocommerce' ),
102
-			'log_entries' => __( 'Log', 'woocommerce' ),
103
-		);
104
-
105
-		$this->sort_by = array(
106
-			'schedule',
107
-			'hook',
108
-			'group',
109
-		);
110
-
111
-		$this->search_by = array(
112
-			'hook',
113
-			'args',
114
-			'claim_id',
115
-		);
116
-
117
-		$request_status = $this->get_request_status();
118
-
119
-		if ( empty( $request_status ) ) {
120
-			$this->sort_by[] = 'status';
121
-		} elseif ( in_array( $request_status, array( 'in-progress', 'failed' ) ) ) {
122
-			$this->columns  += array( 'claim_id' => __( 'Claim ID', 'woocommerce' ) );
123
-			$this->sort_by[] = 'claim_id';
124
-		}
125
-
126
-		$this->row_actions = array(
127
-			'hook' => array(
128
-				'run' => array(
129
-					'name'  => __( 'Run', 'woocommerce' ),
130
-					'desc'  => __( 'Process the action now as if it were run as part of a queue', 'woocommerce' ),
131
-				),
132
-				'cancel' => array(
133
-					'name'  => __( 'Cancel', 'woocommerce' ),
134
-					'desc'  => __( 'Cancel the action now to avoid it being run in future', 'woocommerce' ),
135
-					'class' => 'cancel trash',
136
-				),
137
-			),
138
-		);
139
-
140
-		self::$time_periods = array(
141
-			array(
142
-				'seconds' => YEAR_IN_SECONDS,
143
-				/* translators: %s: amount of time */
144
-				'names'   => _n_noop( '%s year', '%s years', 'woocommerce' ),
145
-			),
146
-			array(
147
-				'seconds' => MONTH_IN_SECONDS,
148
-				/* translators: %s: amount of time */
149
-				'names'   => _n_noop( '%s month', '%s months', 'woocommerce' ),
150
-			),
151
-			array(
152
-				'seconds' => WEEK_IN_SECONDS,
153
-				/* translators: %s: amount of time */
154
-				'names'   => _n_noop( '%s week', '%s weeks', 'woocommerce' ),
155
-			),
156
-			array(
157
-				'seconds' => DAY_IN_SECONDS,
158
-				/* translators: %s: amount of time */
159
-				'names'   => _n_noop( '%s day', '%s days', 'woocommerce' ),
160
-			),
161
-			array(
162
-				'seconds' => HOUR_IN_SECONDS,
163
-				/* translators: %s: amount of time */
164
-				'names'   => _n_noop( '%s hour', '%s hours', 'woocommerce' ),
165
-			),
166
-			array(
167
-				'seconds' => MINUTE_IN_SECONDS,
168
-				/* translators: %s: amount of time */
169
-				'names'   => _n_noop( '%s minute', '%s minutes', 'woocommerce' ),
170
-			),
171
-			array(
172
-				'seconds' => 1,
173
-				/* translators: %s: amount of time */
174
-				'names'   => _n_noop( '%s second', '%s seconds', 'woocommerce' ),
175
-			),
176
-		);
177
-
178
-		parent::__construct(
179
-			array(
180
-				'singular' => 'action-scheduler',
181
-				'plural'   => 'action-scheduler',
182
-				'ajax'     => false,
183
-			)
184
-		);
185
-
186
-		add_screen_option(
187
-			'per_page',
188
-			array(
189
-				'default' => $this->items_per_page,
190
-			)
191
-		);
192
-
193
-		add_filter( 'set_screen_option_' . $this->get_per_page_option_name(), array( $this, 'set_items_per_page_option' ), 10, 3 );
194
-		set_screen_options();
195
-	}
196
-
197
-	/**
198
-	 * Handles setting the items_per_page option for this screen.
199
-	 *
200
-	 * @param mixed  $status Default false (to skip saving the current option).
201
-	 * @param string $option Screen option name.
202
-	 * @param int    $value  Screen option value.
203
-	 * @return int
204
-	 */
205
-	public function set_items_per_page_option( $status, $option, $value ) {
206
-		return $value;
207
-	}
208
-	/**
209
-	 * Convert an interval of seconds into a two part human friendly string.
210
-	 *
211
-	 * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning
212
-	 * even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step
213
-	 * further to display two degrees of accuracy.
214
-	 *
215
-	 * Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/
216
-	 *
217
-	 * @param int $interval A interval in seconds.
218
-	 * @param int $periods_to_include Depth of time periods to include, e.g. for an interval of 70, and $periods_to_include of 2, both minutes and seconds would be included. With a value of 1, only minutes would be included.
219
-	 * @return string A human friendly string representation of the interval.
220
-	 */
221
-	private static function human_interval( $interval, $periods_to_include = 2 ) {
222
-
223
-		if ( $interval <= 0 ) {
224
-			return __( 'Now!', 'woocommerce' );
225
-		}
226
-
227
-		$output = '';
228
-
229
-		for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < count( self::$time_periods ) && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) {
230
-
231
-			$periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] );
232
-
233
-			if ( $periods_in_interval > 0 ) {
234
-				if ( ! empty( $output ) ) {
235
-					$output .= ' ';
236
-				}
237
-				$output .= sprintf( _n( self::$time_periods[ $time_period_index ]['names'][0], self::$time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'woocommerce' ), $periods_in_interval );
238
-				$seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds'];
239
-				$periods_included++;
240
-			}
241
-		}
242
-
243
-		return $output;
244
-	}
245
-
246
-	/**
247
-	 * Returns the recurrence of an action or 'Non-repeating'. The output is human readable.
248
-	 *
249
-	 * @param ActionScheduler_Action $action
250
-	 *
251
-	 * @return string
252
-	 */
253
-	protected function get_recurrence( $action ) {
254
-		$schedule = $action->get_schedule();
255
-		if ( $schedule->is_recurring() ) {
256
-			$recurrence = $schedule->get_recurrence();
257
-
258
-			if ( is_numeric( $recurrence ) ) {
259
-				/* translators: %s: time interval */
260
-				return sprintf( __( 'Every %s', 'woocommerce' ), self::human_interval( $recurrence ) );
261
-			} else {
262
-				return $recurrence;
263
-			}
264
-		}
265
-
266
-		return __( 'Non-repeating', 'woocommerce' );
267
-	}
268
-
269
-	/**
270
-	 * Serializes the argument of an action to render it in a human friendly format.
271
-	 *
272
-	 * @param array $row The array representation of the current row of the table
273
-	 *
274
-	 * @return string
275
-	 */
276
-	public function column_args( array $row ) {
277
-		if ( empty( $row['args'] ) ) {
278
-			return apply_filters( 'action_scheduler_list_table_column_args', '', $row );
279
-		}
280
-
281
-		$row_html = '<ul>';
282
-		foreach ( $row['args'] as $key => $value ) {
283
-			$row_html .= sprintf( '<li><code>%s => %s</code></li>', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) );
284
-		}
285
-		$row_html .= '</ul>';
286
-
287
-		return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row );
288
-	}
289
-
290
-	/**
291
-	 * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
292
-	 *
293
-	 * @param array $row Action array.
294
-	 * @return string
295
-	 */
296
-	public function column_log_entries( array $row ) {
297
-
298
-		$log_entries_html = '<ol>';
299
-
300
-		$timezone = new DateTimezone( 'UTC' );
301
-
302
-		foreach ( $row['log_entries'] as $log_entry ) {
303
-			$log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone );
304
-		}
305
-
306
-		$log_entries_html .= '</ol>';
307
-
308
-		return $log_entries_html;
309
-	}
310
-
311
-	/**
312
-	 * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
313
-	 *
314
-	 * @param ActionScheduler_LogEntry $log_entry
315
-	 * @param DateTimezone $timezone
316
-	 * @return string
317
-	 */
318
-	protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
319
-		$date = $log_entry->get_date();
320
-		$date->setTimezone( $timezone );
321
-		return sprintf( '<li><strong>%s</strong><br/>%s</li>', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) );
322
-	}
323
-
324
-	/**
325
-	 * Only display row actions for pending actions.
326
-	 *
327
-	 * @param array  $row         Row to render
328
-	 * @param string $column_name Current row
329
-	 *
330
-	 * @return string
331
-	 */
332
-	protected function maybe_render_actions( $row, $column_name ) {
333
-		if ( 'pending' === strtolower( $row[ 'status_name' ] ) ) {
334
-			return parent::maybe_render_actions( $row, $column_name );
335
-		}
336
-
337
-		return '';
338
-	}
339
-
340
-	/**
341
-	 * Renders admin notifications
342
-	 *
343
-	 * Notifications:
344
-	 *  1. When the maximum number of tasks are being executed simultaneously.
345
-	 *  2. Notifications when a task is manually executed.
346
-	 *  3. Tables are missing.
347
-	 */
348
-	public function display_admin_notices() {
349
-		global $wpdb;
350
-
351
-		if ( ( is_a( $this->store, 'ActionScheduler_HybridStore' ) || is_a( $this->store, 'ActionScheduler_DBStore' ) ) && apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) {
352
-			$table_list = array(
353
-				'actionscheduler_actions',
354
-				'actionscheduler_logs',
355
-				'actionscheduler_groups',
356
-				'actionscheduler_claims',
357
-			);
358
-
359
-			$found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
360
-			foreach ( $table_list as $table_name ) {
361
-				if ( ! in_array( $wpdb->prefix . $table_name, $found_tables ) ) {
362
-					$this->admin_notices[] = array(
363
-						'class'   => 'error',
364
-						'message' => __( 'It appears one or more database tables were missing. Attempting to re-create the missing table(s).' , 'woocommerce' ),
365
-					);
366
-					$this->recreate_tables();
367
-					parent::display_admin_notices();
368
-
369
-					return;
370
-				}
371
-			}
372
-		}
373
-
374
-		if ( $this->runner->has_maximum_concurrent_batches() ) {
375
-			$claim_count           = $this->store->get_claim_count();
376
-			$this->admin_notices[] = array(
377
-				'class'   => 'updated',
378
-				'message' => sprintf(
379
-					/* translators: %s: amount of claims */
380
-					_n(
381
-						'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.',
382
-						'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.',
383
-						$claim_count,
384
-						'woocommerce'
385
-					),
386
-					$claim_count
387
-				),
388
-			);
389
-		} elseif ( $this->store->has_pending_actions_due() ) {
390
-
391
-			$async_request_lock_expiration = ActionScheduler::lock()->get_expiration( 'async-request-runner' );
392
-
393
-			// No lock set or lock expired
394
-			if ( false === $async_request_lock_expiration || $async_request_lock_expiration < time() ) {
395
-				$in_progress_url       = add_query_arg( 'status', 'in-progress', remove_query_arg( 'status' ) );
396
-				/* translators: %s: process URL */
397
-				$async_request_message = sprintf( __( 'A new queue has begun processing. <a href="%s">View actions in-progress &raquo;</a>', 'woocommerce' ), esc_url( $in_progress_url ) );
398
-			} else {
399
-				/* translators: %d: seconds */
400
-				$async_request_message = sprintf( __( 'The next queue will begin processing in approximately %d seconds.', 'woocommerce' ), $async_request_lock_expiration - time() );
401
-			}
402
-
403
-			$this->admin_notices[] = array(
404
-				'class'   => 'notice notice-info',
405
-				'message' => $async_request_message,
406
-			);
407
-		}
408
-
409
-		$notification = get_transient( 'action_scheduler_admin_notice' );
410
-
411
-		if ( is_array( $notification ) ) {
412
-			delete_transient( 'action_scheduler_admin_notice' );
413
-
414
-			$action = $this->store->fetch_action( $notification['action_id'] );
415
-			$action_hook_html = '<strong><code>' . $action->get_hook() . '</code></strong>';
416
-			if ( 1 == $notification['success'] ) {
417
-				$class = 'updated';
418
-				switch ( $notification['row_action_type'] ) {
419
-					case 'run' :
420
-						/* translators: %s: action HTML */
421
-						$action_message_html = sprintf( __( 'Successfully executed action: %s', 'woocommerce' ), $action_hook_html );
422
-						break;
423
-					case 'cancel' :
424
-						/* translators: %s: action HTML */
425
-						$action_message_html = sprintf( __( 'Successfully canceled action: %s', 'woocommerce' ), $action_hook_html );
426
-						break;
427
-					default :
428
-						/* translators: %s: action HTML */
429
-						$action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'woocommerce' ), $action_hook_html );
430
-						break;
431
-				}
432
-			} else {
433
-				$class = 'error';
434
-				/* translators: 1: action HTML 2: action ID 3: error message */
435
-				$action_message_html = sprintf( __( 'Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'woocommerce' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) );
436
-			}
437
-
438
-			$action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification );
439
-
440
-			$this->admin_notices[] = array(
441
-				'class'   => $class,
442
-				'message' => $action_message_html,
443
-			);
444
-		}
445
-
446
-		parent::display_admin_notices();
447
-	}
448
-
449
-	/**
450
-	 * Prints the scheduled date in a human friendly format.
451
-	 *
452
-	 * @param array $row The array representation of the current row of the table
453
-	 *
454
-	 * @return string
455
-	 */
456
-	public function column_schedule( $row ) {
457
-		return $this->get_schedule_display_string( $row['schedule'] );
458
-	}
459
-
460
-	/**
461
-	 * Get the scheduled date in a human friendly format.
462
-	 *
463
-	 * @param ActionScheduler_Schedule $schedule
464
-	 * @return string
465
-	 */
466
-	protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) {
467
-
468
-		$schedule_display_string = '';
469
-
470
-		if ( ! $schedule->get_date() ) {
471
-			return '0000-00-00 00:00:00';
472
-		}
473
-
474
-		$next_timestamp = $schedule->get_date()->getTimestamp();
475
-
476
-		$schedule_display_string .= $schedule->get_date()->format( 'Y-m-d H:i:s O' );
477
-		$schedule_display_string .= '<br/>';
478
-
479
-		if ( gmdate( 'U' ) > $next_timestamp ) {
480
-			/* translators: %s: date interval */
481
-			$schedule_display_string .= sprintf( __( ' (%s ago)', 'woocommerce' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) );
482
-		} else {
483
-			/* translators: %s: date interval */
484
-			$schedule_display_string .= sprintf( __( ' (%s)', 'woocommerce' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) );
485
-		}
486
-
487
-		return $schedule_display_string;
488
-	}
489
-
490
-	/**
491
-	 * Bulk delete
492
-	 *
493
-	 * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data
494
-	 * properly validated by the callee and it will delete the actions without any extra validation.
495
-	 *
496
-	 * @param array $ids
497
-	 * @param string $ids_sql Inherited and unused
498
-	 */
499
-	protected function bulk_delete( array $ids, $ids_sql ) {
500
-		foreach ( $ids as $id ) {
501
-			$this->store->delete_action( $id );
502
-		}
503
-	}
504
-
505
-	/**
506
-	 * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
507
-	 * parameters are valid.
508
-	 *
509
-	 * @param int $action_id
510
-	 */
511
-	protected function row_action_cancel( $action_id ) {
512
-		$this->process_row_action( $action_id, 'cancel' );
513
-	}
514
-
515
-	/**
516
-	 * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
517
-	 * parameters are valid.
518
-	 *
519
-	 * @param int $action_id
520
-	 */
521
-	protected function row_action_run( $action_id ) {
522
-		$this->process_row_action( $action_id, 'run' );
523
-	}
524
-
525
-	/**
526
-	 * Force the data store schema updates.
527
-	 */
528
-	protected function recreate_tables() {
529
-		if ( is_a( $this->store, 'ActionScheduler_HybridStore' ) ) {
530
-			$store = $this->store;
531
-		} else {
532
-			$store = new ActionScheduler_HybridStore();
533
-		}
534
-		add_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10, 2 );
535
-
536
-		$store_schema  = new ActionScheduler_StoreSchema();
537
-		$logger_schema = new ActionScheduler_LoggerSchema();
538
-		$store_schema->register_tables( true );
539
-		$logger_schema->register_tables( true );
540
-
541
-		remove_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10 );
542
-	}
543
-	/**
544
-	 * Implements the logic behind processing an action once an action link is clicked on the list table.
545
-	 *
546
-	 * @param int $action_id
547
-	 * @param string $row_action_type The type of action to perform on the action.
548
-	 */
549
-	protected function process_row_action( $action_id, $row_action_type ) {
550
-		try {
551
-			switch ( $row_action_type ) {
552
-				case 'run' :
553
-					$this->runner->process_action( $action_id, 'Admin List Table' );
554
-					break;
555
-				case 'cancel' :
556
-					$this->store->cancel_action( $action_id );
557
-					break;
558
-			}
559
-			$success = 1;
560
-			$error_message = '';
561
-		} catch ( Exception $e ) {
562
-			$success = 0;
563
-			$error_message = $e->getMessage();
564
-		}
565
-
566
-		set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 );
567
-	}
568
-
569
-	/**
570
-	 * {@inheritDoc}
571
-	 */
572
-	public function prepare_items() {
573
-		$this->prepare_column_headers();
574
-
575
-		$per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
576
-
577
-		$query = array(
578
-			'per_page' => $per_page,
579
-			'offset'   => $this->get_items_offset(),
580
-			'status'   => $this->get_request_status(),
581
-			'orderby'  => $this->get_request_orderby(),
582
-			'order'    => $this->get_request_order(),
583
-			'search'   => $this->get_request_search_query(),
584
-		);
585
-
586
-		$this->items = array();
587
-
588
-		$total_items = $this->store->query_actions( $query, 'count' );
589
-
590
-		$status_labels = $this->store->get_status_labels();
591
-
592
-		foreach ( $this->store->query_actions( $query ) as $action_id ) {
593
-			try {
594
-				$action = $this->store->fetch_action( $action_id );
595
-			} catch ( Exception $e ) {
596
-				continue;
597
-			}
598
-			if ( is_a( $action, 'ActionScheduler_NullAction' ) ) {
599
-				continue;
600
-			}
601
-			$this->items[ $action_id ] = array(
602
-				'ID'          => $action_id,
603
-				'hook'        => $action->get_hook(),
604
-				'status_name' => $this->store->get_status( $action_id ),
605
-				'status'      => $status_labels[ $this->store->get_status( $action_id ) ],
606
-				'args'        => $action->get_args(),
607
-				'group'       => $action->get_group(),
608
-				'log_entries' => $this->logger->get_logs( $action_id ),
609
-				'claim_id'    => $this->store->get_claim_id( $action_id ),
610
-				'recurrence'  => $this->get_recurrence( $action ),
611
-				'schedule'    => $action->get_schedule(),
612
-			);
613
-		}
614
-
615
-		$this->set_pagination_args( array(
616
-			'total_items' => $total_items,
617
-			'per_page'    => $per_page,
618
-			'total_pages' => ceil( $total_items / $per_page ),
619
-		) );
620
-	}
621
-
622
-	/**
623
-	 * Prints the available statuses so the user can click to filter.
624
-	 */
625
-	protected function display_filter_by_status() {
626
-		$this->status_counts = $this->store->action_counts();
627
-		parent::display_filter_by_status();
628
-	}
629
-
630
-	/**
631
-	 * Get the text to display in the search box on the list table.
632
-	 */
633
-	protected function get_search_box_button_text() {
634
-		return __( 'Search hook, args and claim ID', 'woocommerce' );
635
-	}
636
-
637
-	/**
638
-	 * {@inheritDoc}
639
-	 */
640
-	protected function get_per_page_option_name() {
641
-		return str_replace( '-', '_', $this->screen->id ) . '_per_page';
642
-	}
9
+    /**
10
+     * The package name.
11
+     *
12
+     * @var string
13
+     */
14
+    protected $package = 'action-scheduler';
15
+
16
+    /**
17
+     * Columns to show (name => label).
18
+     *
19
+     * @var array
20
+     */
21
+    protected $columns = array();
22
+
23
+    /**
24
+     * Actions (name => label).
25
+     *
26
+     * @var array
27
+     */
28
+    protected $row_actions = array();
29
+
30
+    /**
31
+     * The active data stores
32
+     *
33
+     * @var ActionScheduler_Store
34
+     */
35
+    protected $store;
36
+
37
+    /**
38
+     * A logger to use for getting action logs to display
39
+     *
40
+     * @var ActionScheduler_Logger
41
+     */
42
+    protected $logger;
43
+
44
+    /**
45
+     * A ActionScheduler_QueueRunner runner instance (or child class)
46
+     *
47
+     * @var ActionScheduler_QueueRunner
48
+     */
49
+    protected $runner;
50
+
51
+    /**
52
+     * Bulk actions. The key of the array is the method name of the implementation:
53
+     *
54
+     *     bulk_<key>(array $ids, string $sql_in).
55
+     *
56
+     * See the comments in the parent class for further details
57
+     *
58
+     * @var array
59
+     */
60
+    protected $bulk_actions = array();
61
+
62
+    /**
63
+     * Flag variable to render our notifications, if any, once.
64
+     *
65
+     * @var bool
66
+     */
67
+    protected static $did_notification = false;
68
+
69
+    /**
70
+     * Array of seconds for common time periods, like week or month, alongside an internationalised string representation, i.e. "Day" or "Days"
71
+     *
72
+     * @var array
73
+     */
74
+    private static $time_periods;
75
+
76
+    /**
77
+     * Sets the current data store object into `store->action` and initialises the object.
78
+     *
79
+     * @param ActionScheduler_Store $store
80
+     * @param ActionScheduler_Logger $logger
81
+     * @param ActionScheduler_QueueRunner $runner
82
+     */
83
+    public function __construct( ActionScheduler_Store $store, ActionScheduler_Logger $logger, ActionScheduler_QueueRunner $runner ) {
84
+
85
+        $this->store  = $store;
86
+        $this->logger = $logger;
87
+        $this->runner = $runner;
88
+
89
+        $this->table_header = __( 'Scheduled Actions', 'woocommerce' );
90
+
91
+        $this->bulk_actions = array(
92
+            'delete' => __( 'Delete', 'woocommerce' ),
93
+        );
94
+
95
+        $this->columns = array(
96
+            'hook'        => __( 'Hook', 'woocommerce' ),
97
+            'status'      => __( 'Status', 'woocommerce' ),
98
+            'args'        => __( 'Arguments', 'woocommerce' ),
99
+            'group'       => __( 'Group', 'woocommerce' ),
100
+            'recurrence'  => __( 'Recurrence', 'woocommerce' ),
101
+            'schedule'    => __( 'Scheduled Date', 'woocommerce' ),
102
+            'log_entries' => __( 'Log', 'woocommerce' ),
103
+        );
104
+
105
+        $this->sort_by = array(
106
+            'schedule',
107
+            'hook',
108
+            'group',
109
+        );
110
+
111
+        $this->search_by = array(
112
+            'hook',
113
+            'args',
114
+            'claim_id',
115
+        );
116
+
117
+        $request_status = $this->get_request_status();
118
+
119
+        if ( empty( $request_status ) ) {
120
+            $this->sort_by[] = 'status';
121
+        } elseif ( in_array( $request_status, array( 'in-progress', 'failed' ) ) ) {
122
+            $this->columns  += array( 'claim_id' => __( 'Claim ID', 'woocommerce' ) );
123
+            $this->sort_by[] = 'claim_id';
124
+        }
125
+
126
+        $this->row_actions = array(
127
+            'hook' => array(
128
+                'run' => array(
129
+                    'name'  => __( 'Run', 'woocommerce' ),
130
+                    'desc'  => __( 'Process the action now as if it were run as part of a queue', 'woocommerce' ),
131
+                ),
132
+                'cancel' => array(
133
+                    'name'  => __( 'Cancel', 'woocommerce' ),
134
+                    'desc'  => __( 'Cancel the action now to avoid it being run in future', 'woocommerce' ),
135
+                    'class' => 'cancel trash',
136
+                ),
137
+            ),
138
+        );
139
+
140
+        self::$time_periods = array(
141
+            array(
142
+                'seconds' => YEAR_IN_SECONDS,
143
+                /* translators: %s: amount of time */
144
+                'names'   => _n_noop( '%s year', '%s years', 'woocommerce' ),
145
+            ),
146
+            array(
147
+                'seconds' => MONTH_IN_SECONDS,
148
+                /* translators: %s: amount of time */
149
+                'names'   => _n_noop( '%s month', '%s months', 'woocommerce' ),
150
+            ),
151
+            array(
152
+                'seconds' => WEEK_IN_SECONDS,
153
+                /* translators: %s: amount of time */
154
+                'names'   => _n_noop( '%s week', '%s weeks', 'woocommerce' ),
155
+            ),
156
+            array(
157
+                'seconds' => DAY_IN_SECONDS,
158
+                /* translators: %s: amount of time */
159
+                'names'   => _n_noop( '%s day', '%s days', 'woocommerce' ),
160
+            ),
161
+            array(
162
+                'seconds' => HOUR_IN_SECONDS,
163
+                /* translators: %s: amount of time */
164
+                'names'   => _n_noop( '%s hour', '%s hours', 'woocommerce' ),
165
+            ),
166
+            array(
167
+                'seconds' => MINUTE_IN_SECONDS,
168
+                /* translators: %s: amount of time */
169
+                'names'   => _n_noop( '%s minute', '%s minutes', 'woocommerce' ),
170
+            ),
171
+            array(
172
+                'seconds' => 1,
173
+                /* translators: %s: amount of time */
174
+                'names'   => _n_noop( '%s second', '%s seconds', 'woocommerce' ),
175
+            ),
176
+        );
177
+
178
+        parent::__construct(
179
+            array(
180
+                'singular' => 'action-scheduler',
181
+                'plural'   => 'action-scheduler',
182
+                'ajax'     => false,
183
+            )
184
+        );
185
+
186
+        add_screen_option(
187
+            'per_page',
188
+            array(
189
+                'default' => $this->items_per_page,
190
+            )
191
+        );
192
+
193
+        add_filter( 'set_screen_option_' . $this->get_per_page_option_name(), array( $this, 'set_items_per_page_option' ), 10, 3 );
194
+        set_screen_options();
195
+    }
196
+
197
+    /**
198
+     * Handles setting the items_per_page option for this screen.
199
+     *
200
+     * @param mixed  $status Default false (to skip saving the current option).
201
+     * @param string $option Screen option name.
202
+     * @param int    $value  Screen option value.
203
+     * @return int
204
+     */
205
+    public function set_items_per_page_option( $status, $option, $value ) {
206
+        return $value;
207
+    }
208
+    /**
209
+     * Convert an interval of seconds into a two part human friendly string.
210
+     *
211
+     * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning
212
+     * even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step
213
+     * further to display two degrees of accuracy.
214
+     *
215
+     * Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/
216
+     *
217
+     * @param int $interval A interval in seconds.
218
+     * @param int $periods_to_include Depth of time periods to include, e.g. for an interval of 70, and $periods_to_include of 2, both minutes and seconds would be included. With a value of 1, only minutes would be included.
219
+     * @return string A human friendly string representation of the interval.
220
+     */
221
+    private static function human_interval( $interval, $periods_to_include = 2 ) {
222
+
223
+        if ( $interval <= 0 ) {
224
+            return __( 'Now!', 'woocommerce' );
225
+        }
226
+
227
+        $output = '';
228
+
229
+        for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < count( self::$time_periods ) && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) {
230
+
231
+            $periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] );
232
+
233
+            if ( $periods_in_interval > 0 ) {
234
+                if ( ! empty( $output ) ) {
235
+                    $output .= ' ';
236
+                }
237
+                $output .= sprintf( _n( self::$time_periods[ $time_period_index ]['names'][0], self::$time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'woocommerce' ), $periods_in_interval );
238
+                $seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds'];
239
+                $periods_included++;
240
+            }
241
+        }
242
+
243
+        return $output;
244
+    }
245
+
246
+    /**
247
+     * Returns the recurrence of an action or 'Non-repeating'. The output is human readable.
248
+     *
249
+     * @param ActionScheduler_Action $action
250
+     *
251
+     * @return string
252
+     */
253
+    protected function get_recurrence( $action ) {
254
+        $schedule = $action->get_schedule();
255
+        if ( $schedule->is_recurring() ) {
256
+            $recurrence = $schedule->get_recurrence();
257
+
258
+            if ( is_numeric( $recurrence ) ) {
259
+                /* translators: %s: time interval */
260
+                return sprintf( __( 'Every %s', 'woocommerce' ), self::human_interval( $recurrence ) );
261
+            } else {
262
+                return $recurrence;
263
+            }
264
+        }
265
+
266
+        return __( 'Non-repeating', 'woocommerce' );
267
+    }
268
+
269
+    /**
270
+     * Serializes the argument of an action to render it in a human friendly format.
271
+     *
272
+     * @param array $row The array representation of the current row of the table
273
+     *
274
+     * @return string
275
+     */
276
+    public function column_args( array $row ) {
277
+        if ( empty( $row['args'] ) ) {
278
+            return apply_filters( 'action_scheduler_list_table_column_args', '', $row );
279
+        }
280
+
281
+        $row_html = '<ul>';
282
+        foreach ( $row['args'] as $key => $value ) {
283
+            $row_html .= sprintf( '<li><code>%s => %s</code></li>', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) );
284
+        }
285
+        $row_html .= '</ul>';
286
+
287
+        return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row );
288
+    }
289
+
290
+    /**
291
+     * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
292
+     *
293
+     * @param array $row Action array.
294
+     * @return string
295
+     */
296
+    public function column_log_entries( array $row ) {
297
+
298
+        $log_entries_html = '<ol>';
299
+
300
+        $timezone = new DateTimezone( 'UTC' );
301
+
302
+        foreach ( $row['log_entries'] as $log_entry ) {
303
+            $log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone );
304
+        }
305
+
306
+        $log_entries_html .= '</ol>';
307
+
308
+        return $log_entries_html;
309
+    }
310
+
311
+    /**
312
+     * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
313
+     *
314
+     * @param ActionScheduler_LogEntry $log_entry
315
+     * @param DateTimezone $timezone
316
+     * @return string
317
+     */
318
+    protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
319
+        $date = $log_entry->get_date();
320
+        $date->setTimezone( $timezone );
321
+        return sprintf( '<li><strong>%s</strong><br/>%s</li>', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) );
322
+    }
323
+
324
+    /**
325
+     * Only display row actions for pending actions.
326
+     *
327
+     * @param array  $row         Row to render
328
+     * @param string $column_name Current row
329
+     *
330
+     * @return string
331
+     */
332
+    protected function maybe_render_actions( $row, $column_name ) {
333
+        if ( 'pending' === strtolower( $row[ 'status_name' ] ) ) {
334
+            return parent::maybe_render_actions( $row, $column_name );
335
+        }
336
+
337
+        return '';
338
+    }
339
+
340
+    /**
341
+     * Renders admin notifications
342
+     *
343
+     * Notifications:
344
+     *  1. When the maximum number of tasks are being executed simultaneously.
345
+     *  2. Notifications when a task is manually executed.
346
+     *  3. Tables are missing.
347
+     */
348
+    public function display_admin_notices() {
349
+        global $wpdb;
350
+
351
+        if ( ( is_a( $this->store, 'ActionScheduler_HybridStore' ) || is_a( $this->store, 'ActionScheduler_DBStore' ) ) && apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) {
352
+            $table_list = array(
353
+                'actionscheduler_actions',
354
+                'actionscheduler_logs',
355
+                'actionscheduler_groups',
356
+                'actionscheduler_claims',
357
+            );
358
+
359
+            $found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
360
+            foreach ( $table_list as $table_name ) {
361
+                if ( ! in_array( $wpdb->prefix . $table_name, $found_tables ) ) {
362
+                    $this->admin_notices[] = array(
363
+                        'class'   => 'error',
364
+                        'message' => __( 'It appears one or more database tables were missing. Attempting to re-create the missing table(s).' , 'woocommerce' ),
365
+                    );
366
+                    $this->recreate_tables();
367
+                    parent::display_admin_notices();
368
+
369
+                    return;
370
+                }
371
+            }
372
+        }
373
+
374
+        if ( $this->runner->has_maximum_concurrent_batches() ) {
375
+            $claim_count           = $this->store->get_claim_count();
376
+            $this->admin_notices[] = array(
377
+                'class'   => 'updated',
378
+                'message' => sprintf(
379
+                    /* translators: %s: amount of claims */
380
+                    _n(
381
+                        'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.',
382
+                        'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.',
383
+                        $claim_count,
384
+                        'woocommerce'
385
+                    ),
386
+                    $claim_count
387
+                ),
388
+            );
389
+        } elseif ( $this->store->has_pending_actions_due() ) {
390
+
391
+            $async_request_lock_expiration = ActionScheduler::lock()->get_expiration( 'async-request-runner' );
392
+
393
+            // No lock set or lock expired
394
+            if ( false === $async_request_lock_expiration || $async_request_lock_expiration < time() ) {
395
+                $in_progress_url       = add_query_arg( 'status', 'in-progress', remove_query_arg( 'status' ) );
396
+                /* translators: %s: process URL */
397
+                $async_request_message = sprintf( __( 'A new queue has begun processing. <a href="%s">View actions in-progress &raquo;</a>', 'woocommerce' ), esc_url( $in_progress_url ) );
398
+            } else {
399
+                /* translators: %d: seconds */
400
+                $async_request_message = sprintf( __( 'The next queue will begin processing in approximately %d seconds.', 'woocommerce' ), $async_request_lock_expiration - time() );
401
+            }
402
+
403
+            $this->admin_notices[] = array(
404
+                'class'   => 'notice notice-info',
405
+                'message' => $async_request_message,
406
+            );
407
+        }
408
+
409
+        $notification = get_transient( 'action_scheduler_admin_notice' );
410
+
411
+        if ( is_array( $notification ) ) {
412
+            delete_transient( 'action_scheduler_admin_notice' );
413
+
414
+            $action = $this->store->fetch_action( $notification['action_id'] );
415
+            $action_hook_html = '<strong><code>' . $action->get_hook() . '</code></strong>';
416
+            if ( 1 == $notification['success'] ) {
417
+                $class = 'updated';
418
+                switch ( $notification['row_action_type'] ) {
419
+                    case 'run' :
420
+                        /* translators: %s: action HTML */
421
+                        $action_message_html = sprintf( __( 'Successfully executed action: %s', 'woocommerce' ), $action_hook_html );
422
+                        break;
423
+                    case 'cancel' :
424
+                        /* translators: %s: action HTML */
425
+                        $action_message_html = sprintf( __( 'Successfully canceled action: %s', 'woocommerce' ), $action_hook_html );
426
+                        break;
427
+                    default :
428
+                        /* translators: %s: action HTML */
429
+                        $action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'woocommerce' ), $action_hook_html );
430
+                        break;
431
+                }
432
+            } else {
433
+                $class = 'error';
434
+                /* translators: 1: action HTML 2: action ID 3: error message */
435
+                $action_message_html = sprintf( __( 'Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'woocommerce' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) );
436
+            }
437
+
438
+            $action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification );
439
+
440
+            $this->admin_notices[] = array(
441
+                'class'   => $class,
442
+                'message' => $action_message_html,
443
+            );
444
+        }
445
+
446
+        parent::display_admin_notices();
447
+    }
448
+
449
+    /**
450
+     * Prints the scheduled date in a human friendly format.
451
+     *
452
+     * @param array $row The array representation of the current row of the table
453
+     *
454
+     * @return string
455
+     */
456
+    public function column_schedule( $row ) {
457
+        return $this->get_schedule_display_string( $row['schedule'] );
458
+    }
459
+
460
+    /**
461
+     * Get the scheduled date in a human friendly format.
462
+     *
463
+     * @param ActionScheduler_Schedule $schedule
464
+     * @return string
465
+     */
466
+    protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) {
467
+
468
+        $schedule_display_string = '';
469
+
470
+        if ( ! $schedule->get_date() ) {
471
+            return '0000-00-00 00:00:00';
472
+        }
473
+
474
+        $next_timestamp = $schedule->get_date()->getTimestamp();
475
+
476
+        $schedule_display_string .= $schedule->get_date()->format( 'Y-m-d H:i:s O' );
477
+        $schedule_display_string .= '<br/>';
478
+
479
+        if ( gmdate( 'U' ) > $next_timestamp ) {
480
+            /* translators: %s: date interval */
481
+            $schedule_display_string .= sprintf( __( ' (%s ago)', 'woocommerce' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) );
482
+        } else {
483
+            /* translators: %s: date interval */
484
+            $schedule_display_string .= sprintf( __( ' (%s)', 'woocommerce' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) );
485
+        }
486
+
487
+        return $schedule_display_string;
488
+    }
489
+
490
+    /**
491
+     * Bulk delete
492
+     *
493
+     * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data
494
+     * properly validated by the callee and it will delete the actions without any extra validation.
495
+     *
496
+     * @param array $ids
497
+     * @param string $ids_sql Inherited and unused
498
+     */
499
+    protected function bulk_delete( array $ids, $ids_sql ) {
500
+        foreach ( $ids as $id ) {
501
+            $this->store->delete_action( $id );
502
+        }
503
+    }
504
+
505
+    /**
506
+     * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
507
+     * parameters are valid.
508
+     *
509
+     * @param int $action_id
510
+     */
511
+    protected function row_action_cancel( $action_id ) {
512
+        $this->process_row_action( $action_id, 'cancel' );
513
+    }
514
+
515
+    /**
516
+     * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
517
+     * parameters are valid.
518
+     *
519
+     * @param int $action_id
520
+     */
521
+    protected function row_action_run( $action_id ) {
522
+        $this->process_row_action( $action_id, 'run' );
523
+    }
524
+
525
+    /**
526
+     * Force the data store schema updates.
527
+     */
528
+    protected function recreate_tables() {
529
+        if ( is_a( $this->store, 'ActionScheduler_HybridStore' ) ) {
530
+            $store = $this->store;
531
+        } else {
532
+            $store = new ActionScheduler_HybridStore();
533
+        }
534
+        add_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10, 2 );
535
+
536
+        $store_schema  = new ActionScheduler_StoreSchema();
537
+        $logger_schema = new ActionScheduler_LoggerSchema();
538
+        $store_schema->register_tables( true );
539
+        $logger_schema->register_tables( true );
540
+
541
+        remove_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10 );
542
+    }
543
+    /**
544
+     * Implements the logic behind processing an action once an action link is clicked on the list table.
545
+     *
546
+     * @param int $action_id
547
+     * @param string $row_action_type The type of action to perform on the action.
548
+     */
549
+    protected function process_row_action( $action_id, $row_action_type ) {
550
+        try {
551
+            switch ( $row_action_type ) {
552
+                case 'run' :
553
+                    $this->runner->process_action( $action_id, 'Admin List Table' );
554
+                    break;
555
+                case 'cancel' :
556
+                    $this->store->cancel_action( $action_id );
557
+                    break;
558
+            }
559
+            $success = 1;
560
+            $error_message = '';
561
+        } catch ( Exception $e ) {
562
+            $success = 0;
563
+            $error_message = $e->getMessage();
564
+        }
565
+
566
+        set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 );
567
+    }
568
+
569
+    /**
570
+     * {@inheritDoc}
571
+     */
572
+    public function prepare_items() {
573
+        $this->prepare_column_headers();
574
+
575
+        $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
576
+
577
+        $query = array(
578
+            'per_page' => $per_page,
579
+            'offset'   => $this->get_items_offset(),
580
+            'status'   => $this->get_request_status(),
581
+            'orderby'  => $this->get_request_orderby(),
582
+            'order'    => $this->get_request_order(),
583
+            'search'   => $this->get_request_search_query(),
584
+        );
585
+
586
+        $this->items = array();
587
+
588
+        $total_items = $this->store->query_actions( $query, 'count' );
589
+
590
+        $status_labels = $this->store->get_status_labels();
591
+
592
+        foreach ( $this->store->query_actions( $query ) as $action_id ) {
593
+            try {
594
+                $action = $this->store->fetch_action( $action_id );
595
+            } catch ( Exception $e ) {
596
+                continue;
597
+            }
598
+            if ( is_a( $action, 'ActionScheduler_NullAction' ) ) {
599
+                continue;
600
+            }
601
+            $this->items[ $action_id ] = array(
602
+                'ID'          => $action_id,
603
+                'hook'        => $action->get_hook(),
604
+                'status_name' => $this->store->get_status( $action_id ),
605
+                'status'      => $status_labels[ $this->store->get_status( $action_id ) ],
606
+                'args'        => $action->get_args(),
607
+                'group'       => $action->get_group(),
608
+                'log_entries' => $this->logger->get_logs( $action_id ),
609
+                'claim_id'    => $this->store->get_claim_id( $action_id ),
610
+                'recurrence'  => $this->get_recurrence( $action ),
611
+                'schedule'    => $action->get_schedule(),
612
+            );
613
+        }
614
+
615
+        $this->set_pagination_args( array(
616
+            'total_items' => $total_items,
617
+            'per_page'    => $per_page,
618
+            'total_pages' => ceil( $total_items / $per_page ),
619
+        ) );
620
+    }
621
+
622
+    /**
623
+     * Prints the available statuses so the user can click to filter.
624
+     */
625
+    protected function display_filter_by_status() {
626
+        $this->status_counts = $this->store->action_counts();
627
+        parent::display_filter_by_status();
628
+    }
629
+
630
+    /**
631
+     * Get the text to display in the search box on the list table.
632
+     */
633
+    protected function get_search_box_button_text() {
634
+        return __( 'Search hook, args and claim ID', 'woocommerce' );
635
+    }
636
+
637
+    /**
638
+     * {@inheritDoc}
639
+     */
640
+    protected function get_per_page_option_name() {
641
+        return str_replace( '-', '_', $this->screen->id ) . '_per_page';
642
+    }
643 643
 }
Please login to merge, or discard this patch.
classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php 1 patch
Indentation   +44 added lines, -44 removed lines patch added patch discarded remove patch
@@ -5,54 +5,54 @@
 block discarded – undo
5 5
  * @codeCoverageIgnore
6 6
  */
7 7
 class ActionScheduler_wpPostStore_PostStatusRegistrar {
8
-	public function register() {
9
-		register_post_status( ActionScheduler_Store::STATUS_RUNNING, array_merge( $this->post_status_args(), $this->post_status_running_labels() ) );
10
-		register_post_status( ActionScheduler_Store::STATUS_FAILED, array_merge( $this->post_status_args(), $this->post_status_failed_labels() ) );
11
-	}
8
+    public function register() {
9
+        register_post_status( ActionScheduler_Store::STATUS_RUNNING, array_merge( $this->post_status_args(), $this->post_status_running_labels() ) );
10
+        register_post_status( ActionScheduler_Store::STATUS_FAILED, array_merge( $this->post_status_args(), $this->post_status_failed_labels() ) );
11
+    }
12 12
 
13
-	/**
14
-	 * Build the args array for the post type definition
15
-	 *
16
-	 * @return array
17
-	 */
18
-	protected function post_status_args() {
19
-		$args = array(
20
-			'public'                    => false,
21
-			'exclude_from_search'       => false,
22
-			'show_in_admin_all_list'    => true,
23
-			'show_in_admin_status_list' => true,
24
-		);
13
+    /**
14
+     * Build the args array for the post type definition
15
+     *
16
+     * @return array
17
+     */
18
+    protected function post_status_args() {
19
+        $args = array(
20
+            'public'                    => false,
21
+            'exclude_from_search'       => false,
22
+            'show_in_admin_all_list'    => true,
23
+            'show_in_admin_status_list' => true,
24
+        );
25 25
 
26
-		return apply_filters( 'action_scheduler_post_status_args', $args );
27
-	}
26
+        return apply_filters( 'action_scheduler_post_status_args', $args );
27
+    }
28 28
 
29
-	/**
30
-	 * Build the args array for the post type definition
31
-	 *
32
-	 * @return array
33
-	 */
34
-	protected function post_status_failed_labels() {
35
-		$labels = array(
36
-			'label'       => _x( 'Failed', 'post', 'woocommerce' ),
37
-			/* translators: %s: count */
38
-			'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'woocommerce' ),
39
-		);
29
+    /**
30
+     * Build the args array for the post type definition
31
+     *
32
+     * @return array
33
+     */
34
+    protected function post_status_failed_labels() {
35
+        $labels = array(
36
+            'label'       => _x( 'Failed', 'post', 'woocommerce' ),
37
+            /* translators: %s: count */
38
+            'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'woocommerce' ),
39
+        );
40 40
 
41
-		return apply_filters( 'action_scheduler_post_status_failed_labels', $labels );
42
-	}
41
+        return apply_filters( 'action_scheduler_post_status_failed_labels', $labels );
42
+    }
43 43
 
44
-	/**
45
-	 * Build the args array for the post type definition
46
-	 *
47
-	 * @return array
48
-	 */
49
-	protected function post_status_running_labels() {
50
-		$labels = array(
51
-			'label'       => _x( 'In-Progress', 'post', 'woocommerce' ),
52
-			/* translators: %s: count */
53
-			'label_count' => _n_noop( 'In-Progress <span class="count">(%s)</span>', 'In-Progress <span class="count">(%s)</span>', 'woocommerce' ),
54
-		);
44
+    /**
45
+     * Build the args array for the post type definition
46
+     *
47
+     * @return array
48
+     */
49
+    protected function post_status_running_labels() {
50
+        $labels = array(
51
+            'label'       => _x( 'In-Progress', 'post', 'woocommerce' ),
52
+            /* translators: %s: count */
53
+            'label_count' => _n_noop( 'In-Progress <span class="count">(%s)</span>', 'In-Progress <span class="count">(%s)</span>', 'woocommerce' ),
54
+        );
55 55
 
56
-		return apply_filters( 'action_scheduler_post_status_running_labels', $labels );
57
-	}
56
+        return apply_filters( 'action_scheduler_post_status_running_labels', $labels );
57
+    }
58 58
 }
Please login to merge, or discard this patch.
action-scheduler/classes/data-stores/ActionScheduler_HybridStore.php 1 patch
Indentation   +406 added lines, -406 removed lines patch added patch discarded remove patch
@@ -13,414 +13,414 @@
 block discarded – undo
13 13
  * @since 3.0.0
14 14
  */
15 15
 class ActionScheduler_HybridStore extends Store {
16
-	const DEMARKATION_OPTION = 'action_scheduler_hybrid_store_demarkation';
17
-
18
-	private $primary_store;
19
-	private $secondary_store;
20
-	private $migration_runner;
21
-
22
-	/**
23
-	 * @var int The dividing line between IDs of actions created
24
-	 *          by the primary and secondary stores.
25
-	 *
26
-	 * Methods that accept an action ID will compare the ID against
27
-	 * this to determine which store will contain that ID. In almost
28
-	 * all cases, the ID should come from the primary store, but if
29
-	 * client code is bypassing the API functions and fetching IDs
30
-	 * from elsewhere, then there is a chance that an unmigrated ID
31
-	 * might be requested.
32
-	 */
33
-	private $demarkation_id = 0;
34
-
35
-	/**
36
-	 * ActionScheduler_HybridStore constructor.
37
-	 *
38
-	 * @param Config $config Migration config object.
39
-	 */
40
-	public function __construct( Config $config = null ) {
41
-		$this->demarkation_id = (int) get_option( self::DEMARKATION_OPTION, 0 );
42
-		if ( empty( $config ) ) {
43
-			$config = Controller::instance()->get_migration_config_object();
44
-		}
45
-		$this->primary_store    = $config->get_destination_store();
46
-		$this->secondary_store  = $config->get_source_store();
47
-		$this->migration_runner = new Runner( $config );
48
-	}
49
-
50
-	/**
51
-	 * Initialize the table data store tables.
52
-	 *
53
-	 * @codeCoverageIgnore
54
-	 */
55
-	public function init() {
56
-		add_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10, 2 );
57
-		$this->primary_store->init();
58
-		$this->secondary_store->init();
59
-		remove_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10 );
60
-	}
61
-
62
-	/**
63
-	 * When the actions table is created, set its autoincrement
64
-	 * value to be one higher than the posts table to ensure that
65
-	 * there are no ID collisions.
66
-	 *
67
-	 * @param string $table_name
68
-	 * @param string $table_suffix
69
-	 *
70
-	 * @return void
71
-	 * @codeCoverageIgnore
72
-	 */
73
-	public function set_autoincrement( $table_name, $table_suffix ) {
74
-		if ( ActionScheduler_StoreSchema::ACTIONS_TABLE === $table_suffix ) {
75
-			if ( empty( $this->demarkation_id ) ) {
76
-				$this->demarkation_id = $this->set_demarkation_id();
77
-			}
78
-			/** @var \wpdb $wpdb */
79
-			global $wpdb;
80
-			/**
81
-			 * A default date of '0000-00-00 00:00:00' is invalid in MySQL 5.7 when configured with 
82
-			 * sql_mode including both STRICT_TRANS_TABLES and NO_ZERO_DATE.
83
-			 */
84
-			$default_date = new DateTime( 'tomorrow' );
85
-			$null_action  = new ActionScheduler_NullAction();
86
-			$date_gmt     = $this->get_scheduled_date_string( $null_action, $default_date );
87
-			$date_local   = $this->get_scheduled_date_string_local( $null_action, $default_date );
88
-
89
-			$row_count = $wpdb->insert(
90
-				$wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE},
91
-				[
92
-					'action_id'            => $this->demarkation_id,
93
-					'hook'                 => '',
94
-					'status'               => '',
95
-					'scheduled_date_gmt'   => $date_gmt,
96
-					'scheduled_date_local' => $date_local,
97
-					'last_attempt_gmt'     => $date_gmt,
98
-					'last_attempt_local'   => $date_local,
99
-				]
100
-			);
101
-			if ( $row_count > 0 ) {
102
-				$wpdb->delete(
103
-					$wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE},
104
-					[ 'action_id' => $this->demarkation_id ]
105
-				);
106
-			}
107
-		}
108
-	}
109
-
110
-	/**
111
-	 * Store the demarkation id in WP options.
112
-	 *
113
-	 * @param int $id The ID to set as the demarkation point between the two stores
114
-	 *                Leave null to use the next ID from the WP posts table.
115
-	 *
116
-	 * @return int The new ID.
117
-	 *
118
-	 * @codeCoverageIgnore
119
-	 */
120
-	private function set_demarkation_id( $id = null ) {
121
-		if ( empty( $id ) ) {
122
-			/** @var \wpdb $wpdb */
123
-			global $wpdb;
124
-			$id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM $wpdb->posts" );
125
-			$id ++;
126
-		}
127
-		update_option( self::DEMARKATION_OPTION, $id );
128
-
129
-		return $id;
130
-	}
131
-
132
-	/**
133
-	 * Find the first matching action from the secondary store.
134
-	 * If it exists, migrate it to the primary store immediately.
135
-	 * After it migrates, the secondary store will logically contain
136
-	 * the next matching action, so return the result thence.
137
-	 *
138
-	 * @param string $hook
139
-	 * @param array  $params
140
-	 *
141
-	 * @return string
142
-	 */
143
-	public function find_action( $hook, $params = [] ) {
144
-		$found_unmigrated_action = $this->secondary_store->find_action( $hook, $params );
145
-		if ( ! empty( $found_unmigrated_action ) ) {
146
-			$this->migrate( [ $found_unmigrated_action ] );
147
-		}
148
-
149
-		return $this->primary_store->find_action( $hook, $params );
150
-	}
151
-
152
-	/**
153
-	 * Find actions matching the query in the secondary source first.
154
-	 * If any are found, migrate them immediately. Then the secondary
155
-	 * store will contain the canonical results.
156
-	 *
157
-	 * @param array $query
158
-	 * @param string $query_type Whether to select or count the results. Default, select.
159
-	 *
160
-	 * @return int[]
161
-	 */
162
-	public function query_actions( $query = [], $query_type = 'select' ) {
163
-		$found_unmigrated_actions = $this->secondary_store->query_actions( $query, 'select' );
164
-		if ( ! empty( $found_unmigrated_actions ) ) {
165
-			$this->migrate( $found_unmigrated_actions );
166
-		}
167
-
168
-		return $this->primary_store->query_actions( $query, $query_type );
169
-	}
170
-
171
-	/**
172
-	 * Get a count of all actions in the store, grouped by status
173
-	 *
174
-	 * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status.
175
-	 */
176
-	public function action_counts() {
177
-		$unmigrated_actions_count = $this->secondary_store->action_counts();
178
-		$migrated_actions_count   = $this->primary_store->action_counts();
179
-		$actions_count_by_status  = array();
180
-
181
-		foreach ( $this->get_status_labels() as $status_key => $status_label ) {
182
-
183
-			$count = 0;
184
-
185
-			if ( isset( $unmigrated_actions_count[ $status_key ] ) ) {
186
-				$count += $unmigrated_actions_count[ $status_key ];
187
-			}
188
-
189
-			if ( isset( $migrated_actions_count[ $status_key ] ) ) {
190
-				$count += $migrated_actions_count[ $status_key ];
191
-			}
192
-
193
-			$actions_count_by_status[ $status_key ] = $count;
194
-		}
195
-
196
-		$actions_count_by_status = array_filter( $actions_count_by_status );
197
-
198
-		return $actions_count_by_status;
199
-	}
200
-
201
-	/**
202
-	 * If any actions would have been claimed by the secondary store,
203
-	 * migrate them immediately, then ask the primary store for the
204
-	 * canonical claim.
205
-	 *
206
-	 * @param int           $max_actions
207
-	 * @param DateTime|null $before_date
208
-	 *
209
-	 * @return ActionScheduler_ActionClaim
210
-	 */
211
-	public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) {
212
-		$claim = $this->secondary_store->stake_claim( $max_actions, $before_date, $hooks, $group );
213
-
214
-		$claimed_actions = $claim->get_actions();
215
-		if ( ! empty( $claimed_actions ) ) {
216
-			$this->migrate( $claimed_actions );
217
-		}
218
-
219
-		$this->secondary_store->release_claim( $claim );
220
-
221
-		return $this->primary_store->stake_claim( $max_actions, $before_date, $hooks, $group );
222
-	}
223
-
224
-	/**
225
-	 * Migrate a list of actions to the table data store.
226
-	 *
227
-	 * @param array $action_ids List of action IDs.
228
-	 */
229
-	private function migrate( $action_ids ) {
230
-		$this->migration_runner->migrate_actions( $action_ids );
231
-	}
232
-
233
-	/**
234
-	 * Save an action to the primary store.
235
-	 *
236
-	 * @param ActionScheduler_Action $action Action object to be saved.
237
-	 * @param DateTime               $date Optional. Schedule date. Default null.
238
-	 *
239
-	 * @return int The action ID
240
-	 */
241
-	public function save_action( ActionScheduler_Action $action, DateTime $date = null ) {
242
-		return $this->primary_store->save_action( $action, $date );
243
-	}
244
-
245
-	/**
246
-	 * Retrieve an existing action whether migrated or not.
247
-	 *
248
-	 * @param int $action_id Action ID.
249
-	 */
250
-	public function fetch_action( $action_id ) {
251
-		$store = $this->get_store_from_action_id( $action_id, true );
252
-		if ( $store ) {
253
-			return $store->fetch_action( $action_id );
254
-		} else {
255
-			return new ActionScheduler_NullAction();
256
-		}
257
-	}
258
-
259
-	/**
260
-	 * Cancel an existing action whether migrated or not.
261
-	 *
262
-	 * @param int $action_id Action ID.
263
-	 */
264
-	public function cancel_action( $action_id ) {
265
-		$store = $this->get_store_from_action_id( $action_id );
266
-		if ( $store ) {
267
-			$store->cancel_action( $action_id );
268
-		}
269
-	}
270
-
271
-	/**
272
-	 * Delete an existing action whether migrated or not.
273
-	 *
274
-	 * @param int $action_id Action ID.
275
-	 */
276
-	public function delete_action( $action_id ) {
277
-		$store = $this->get_store_from_action_id( $action_id );
278
-		if ( $store ) {
279
-			$store->delete_action( $action_id );
280
-		}
281
-	}
282
-
283
-	/**
284
-	 * Get the schedule date an existing action whether migrated or not.
285
-	 *
286
-	 * @param int $action_id Action ID.
287
-	 */
288
-	public function get_date( $action_id ) {
289
-		$store = $this->get_store_from_action_id( $action_id );
290
-		if ( $store ) {
291
-			return $store->get_date( $action_id );
292
-		} else {
293
-			return null;
294
-		}
295
-	}
296
-
297
-	/**
298
-	 * Mark an existing action as failed whether migrated or not.
299
-	 *
300
-	 * @param int $action_id Action ID.
301
-	 */
302
-	public function mark_failure( $action_id ) {
303
-		$store = $this->get_store_from_action_id( $action_id );
304
-		if ( $store ) {
305
-			$store->mark_failure( $action_id );
306
-		}
307
-	}
308
-
309
-	/**
310
-	 * Log the execution of an existing action whether migrated or not.
311
-	 *
312
-	 * @param int $action_id Action ID.
313
-	 */
314
-	public function log_execution( $action_id ) {
315
-		$store = $this->get_store_from_action_id( $action_id );
316
-		if ( $store ) {
317
-			$store->log_execution( $action_id );
318
-		}
319
-	}
320
-
321
-	/**
322
-	 * Mark an existing action complete whether migrated or not.
323
-	 *
324
-	 * @param int $action_id Action ID.
325
-	 */
326
-	public function mark_complete( $action_id ) {
327
-		$store = $this->get_store_from_action_id( $action_id );
328
-		if ( $store ) {
329
-			$store->mark_complete( $action_id );
330
-		}
331
-	}
332
-
333
-	/**
334
-	 * Get an existing action status whether migrated or not.
335
-	 *
336
-	 * @param int $action_id Action ID.
337
-	 */
338
-	public function get_status( $action_id ) {
339
-		$store = $this->get_store_from_action_id( $action_id );
340
-		if ( $store ) {
341
-			return $store->get_status( $action_id );
342
-		}
343
-		return null;
344
-	}
345
-
346
-	/**
347
-	 * Return which store an action is stored in.
348
-	 *
349
-	 * @param int  $action_id ID of the action.
350
-	 * @param bool $primary_first Optional flag indicating search the primary store first.
351
-	 * @return ActionScheduler_Store
352
-	 */
353
-	protected function get_store_from_action_id( $action_id, $primary_first = false ) {
354
-		if ( $primary_first ) {
355
-			$stores = [
356
-				$this->primary_store,
357
-				$this->secondary_store,
358
-			];
359
-		} elseif ( $action_id < $this->demarkation_id ) {
360
-			$stores = [
361
-				$this->secondary_store,
362
-				$this->primary_store,
363
-			];
364
-		} else {
365
-			$stores = [
366
-				$this->primary_store,
367
-			];
368
-		}
369
-
370
-		foreach ( $stores as $store ) {
371
-			$action = $store->fetch_action( $action_id );
372
-			if ( ! is_a( $action, 'ActionScheduler_NullAction' ) ) {
373
-				return $store;
374
-			}
375
-		}
376
-		return null;
377
-	}
378
-
379
-	/* * * * * * * * * * * * * * * * * * * * * * * * * * *
16
+    const DEMARKATION_OPTION = 'action_scheduler_hybrid_store_demarkation';
17
+
18
+    private $primary_store;
19
+    private $secondary_store;
20
+    private $migration_runner;
21
+
22
+    /**
23
+     * @var int The dividing line between IDs of actions created
24
+     *          by the primary and secondary stores.
25
+     *
26
+     * Methods that accept an action ID will compare the ID against
27
+     * this to determine which store will contain that ID. In almost
28
+     * all cases, the ID should come from the primary store, but if
29
+     * client code is bypassing the API functions and fetching IDs
30
+     * from elsewhere, then there is a chance that an unmigrated ID
31
+     * might be requested.
32
+     */
33
+    private $demarkation_id = 0;
34
+
35
+    /**
36
+     * ActionScheduler_HybridStore constructor.
37
+     *
38
+     * @param Config $config Migration config object.
39
+     */
40
+    public function __construct( Config $config = null ) {
41
+        $this->demarkation_id = (int) get_option( self::DEMARKATION_OPTION, 0 );
42
+        if ( empty( $config ) ) {
43
+            $config = Controller::instance()->get_migration_config_object();
44
+        }
45
+        $this->primary_store    = $config->get_destination_store();
46
+        $this->secondary_store  = $config->get_source_store();
47
+        $this->migration_runner = new Runner( $config );
48
+    }
49
+
50
+    /**
51
+     * Initialize the table data store tables.
52
+     *
53
+     * @codeCoverageIgnore
54
+     */
55
+    public function init() {
56
+        add_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10, 2 );
57
+        $this->primary_store->init();
58
+        $this->secondary_store->init();
59
+        remove_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10 );
60
+    }
61
+
62
+    /**
63
+     * When the actions table is created, set its autoincrement
64
+     * value to be one higher than the posts table to ensure that
65
+     * there are no ID collisions.
66
+     *
67
+     * @param string $table_name
68
+     * @param string $table_suffix
69
+     *
70
+     * @return void
71
+     * @codeCoverageIgnore
72
+     */
73
+    public function set_autoincrement( $table_name, $table_suffix ) {
74
+        if ( ActionScheduler_StoreSchema::ACTIONS_TABLE === $table_suffix ) {
75
+            if ( empty( $this->demarkation_id ) ) {
76
+                $this->demarkation_id = $this->set_demarkation_id();
77
+            }
78
+            /** @var \wpdb $wpdb */
79
+            global $wpdb;
80
+            /**
81
+             * A default date of '0000-00-00 00:00:00' is invalid in MySQL 5.7 when configured with 
82
+             * sql_mode including both STRICT_TRANS_TABLES and NO_ZERO_DATE.
83
+             */
84
+            $default_date = new DateTime( 'tomorrow' );
85
+            $null_action  = new ActionScheduler_NullAction();
86
+            $date_gmt     = $this->get_scheduled_date_string( $null_action, $default_date );
87
+            $date_local   = $this->get_scheduled_date_string_local( $null_action, $default_date );
88
+
89
+            $row_count = $wpdb->insert(
90
+                $wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE},
91
+                [
92
+                    'action_id'            => $this->demarkation_id,
93
+                    'hook'                 => '',
94
+                    'status'               => '',
95
+                    'scheduled_date_gmt'   => $date_gmt,
96
+                    'scheduled_date_local' => $date_local,
97
+                    'last_attempt_gmt'     => $date_gmt,
98
+                    'last_attempt_local'   => $date_local,
99
+                ]
100
+            );
101
+            if ( $row_count > 0 ) {
102
+                $wpdb->delete(
103
+                    $wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE},
104
+                    [ 'action_id' => $this->demarkation_id ]
105
+                );
106
+            }
107
+        }
108
+    }
109
+
110
+    /**
111
+     * Store the demarkation id in WP options.
112
+     *
113
+     * @param int $id The ID to set as the demarkation point between the two stores
114
+     *                Leave null to use the next ID from the WP posts table.
115
+     *
116
+     * @return int The new ID.
117
+     *
118
+     * @codeCoverageIgnore
119
+     */
120
+    private function set_demarkation_id( $id = null ) {
121
+        if ( empty( $id ) ) {
122
+            /** @var \wpdb $wpdb */
123
+            global $wpdb;
124
+            $id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM $wpdb->posts" );
125
+            $id ++;
126
+        }
127
+        update_option( self::DEMARKATION_OPTION, $id );
128
+
129
+        return $id;
130
+    }
131
+
132
+    /**
133
+     * Find the first matching action from the secondary store.
134
+     * If it exists, migrate it to the primary store immediately.
135
+     * After it migrates, the secondary store will logically contain
136
+     * the next matching action, so return the result thence.
137
+     *
138
+     * @param string $hook
139
+     * @param array  $params
140
+     *
141
+     * @return string
142
+     */
143
+    public function find_action( $hook, $params = [] ) {
144
+        $found_unmigrated_action = $this->secondary_store->find_action( $hook, $params );
145
+        if ( ! empty( $found_unmigrated_action ) ) {
146
+            $this->migrate( [ $found_unmigrated_action ] );
147
+        }
148
+
149
+        return $this->primary_store->find_action( $hook, $params );
150
+    }
151
+
152
+    /**
153
+     * Find actions matching the query in the secondary source first.
154
+     * If any are found, migrate them immediately. Then the secondary
155
+     * store will contain the canonical results.
156
+     *
157
+     * @param array $query
158
+     * @param string $query_type Whether to select or count the results. Default, select.
159
+     *
160
+     * @return int[]
161
+     */
162
+    public function query_actions( $query = [], $query_type = 'select' ) {
163
+        $found_unmigrated_actions = $this->secondary_store->query_actions( $query, 'select' );
164
+        if ( ! empty( $found_unmigrated_actions ) ) {
165
+            $this->migrate( $found_unmigrated_actions );
166
+        }
167
+
168
+        return $this->primary_store->query_actions( $query, $query_type );
169
+    }
170
+
171
+    /**
172
+     * Get a count of all actions in the store, grouped by status
173
+     *
174
+     * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status.
175
+     */
176
+    public function action_counts() {
177
+        $unmigrated_actions_count = $this->secondary_store->action_counts();
178
+        $migrated_actions_count   = $this->primary_store->action_counts();
179
+        $actions_count_by_status  = array();
180
+
181
+        foreach ( $this->get_status_labels() as $status_key => $status_label ) {
182
+
183
+            $count = 0;
184
+
185
+            if ( isset( $unmigrated_actions_count[ $status_key ] ) ) {
186
+                $count += $unmigrated_actions_count[ $status_key ];
187
+            }
188
+
189
+            if ( isset( $migrated_actions_count[ $status_key ] ) ) {
190
+                $count += $migrated_actions_count[ $status_key ];
191
+            }
192
+
193
+            $actions_count_by_status[ $status_key ] = $count;
194
+        }
195
+
196
+        $actions_count_by_status = array_filter( $actions_count_by_status );
197
+
198
+        return $actions_count_by_status;
199
+    }
200
+
201
+    /**
202
+     * If any actions would have been claimed by the secondary store,
203
+     * migrate them immediately, then ask the primary store for the
204
+     * canonical claim.
205
+     *
206
+     * @param int           $max_actions
207
+     * @param DateTime|null $before_date
208
+     *
209
+     * @return ActionScheduler_ActionClaim
210
+     */
211
+    public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) {
212
+        $claim = $this->secondary_store->stake_claim( $max_actions, $before_date, $hooks, $group );
213
+
214
+        $claimed_actions = $claim->get_actions();
215
+        if ( ! empty( $claimed_actions ) ) {
216
+            $this->migrate( $claimed_actions );
217
+        }
218
+
219
+        $this->secondary_store->release_claim( $claim );
220
+
221
+        return $this->primary_store->stake_claim( $max_actions, $before_date, $hooks, $group );
222
+    }
223
+
224
+    /**
225
+     * Migrate a list of actions to the table data store.
226
+     *
227
+     * @param array $action_ids List of action IDs.
228
+     */
229
+    private function migrate( $action_ids ) {
230
+        $this->migration_runner->migrate_actions( $action_ids );
231
+    }
232
+
233
+    /**
234
+     * Save an action to the primary store.
235
+     *
236
+     * @param ActionScheduler_Action $action Action object to be saved.
237
+     * @param DateTime               $date Optional. Schedule date. Default null.
238
+     *
239
+     * @return int The action ID
240
+     */
241
+    public function save_action( ActionScheduler_Action $action, DateTime $date = null ) {
242
+        return $this->primary_store->save_action( $action, $date );
243
+    }
244
+
245
+    /**
246
+     * Retrieve an existing action whether migrated or not.
247
+     *
248
+     * @param int $action_id Action ID.
249
+     */
250
+    public function fetch_action( $action_id ) {
251
+        $store = $this->get_store_from_action_id( $action_id, true );
252
+        if ( $store ) {
253
+            return $store->fetch_action( $action_id );
254
+        } else {
255
+            return new ActionScheduler_NullAction();
256
+        }
257
+    }
258
+
259
+    /**
260
+     * Cancel an existing action whether migrated or not.
261
+     *
262
+     * @param int $action_id Action ID.
263
+     */
264
+    public function cancel_action( $action_id ) {
265
+        $store = $this->get_store_from_action_id( $action_id );
266
+        if ( $store ) {
267
+            $store->cancel_action( $action_id );
268
+        }
269
+    }
270
+
271
+    /**
272
+     * Delete an existing action whether migrated or not.
273
+     *
274
+     * @param int $action_id Action ID.
275
+     */
276
+    public function delete_action( $action_id ) {
277
+        $store = $this->get_store_from_action_id( $action_id );
278
+        if ( $store ) {
279
+            $store->delete_action( $action_id );
280
+        }
281
+    }
282
+
283
+    /**
284
+     * Get the schedule date an existing action whether migrated or not.
285
+     *
286
+     * @param int $action_id Action ID.
287
+     */
288
+    public function get_date( $action_id ) {
289
+        $store = $this->get_store_from_action_id( $action_id );
290
+        if ( $store ) {
291
+            return $store->get_date( $action_id );
292
+        } else {
293
+            return null;
294
+        }
295
+    }
296
+
297
+    /**
298
+     * Mark an existing action as failed whether migrated or not.
299
+     *
300
+     * @param int $action_id Action ID.
301
+     */
302
+    public function mark_failure( $action_id ) {
303
+        $store = $this->get_store_from_action_id( $action_id );
304
+        if ( $store ) {
305
+            $store->mark_failure( $action_id );
306
+        }
307
+    }
308
+
309
+    /**
310
+     * Log the execution of an existing action whether migrated or not.
311
+     *
312
+     * @param int $action_id Action ID.
313
+     */
314
+    public function log_execution( $action_id ) {
315
+        $store = $this->get_store_from_action_id( $action_id );
316
+        if ( $store ) {
317
+            $store->log_execution( $action_id );
318
+        }
319
+    }
320
+
321
+    /**
322
+     * Mark an existing action complete whether migrated or not.
323
+     *
324
+     * @param int $action_id Action ID.
325
+     */
326
+    public function mark_complete( $action_id ) {
327
+        $store = $this->get_store_from_action_id( $action_id );
328
+        if ( $store ) {
329
+            $store->mark_complete( $action_id );
330
+        }
331
+    }
332
+
333
+    /**
334
+     * Get an existing action status whether migrated or not.
335
+     *
336
+     * @param int $action_id Action ID.
337
+     */
338
+    public function get_status( $action_id ) {
339
+        $store = $this->get_store_from_action_id( $action_id );
340
+        if ( $store ) {
341
+            return $store->get_status( $action_id );
342
+        }
343
+        return null;
344
+    }
345
+
346
+    /**
347
+     * Return which store an action is stored in.
348
+     *
349
+     * @param int  $action_id ID of the action.
350
+     * @param bool $primary_first Optional flag indicating search the primary store first.
351
+     * @return ActionScheduler_Store
352
+     */
353
+    protected function get_store_from_action_id( $action_id, $primary_first = false ) {
354
+        if ( $primary_first ) {
355
+            $stores = [
356
+                $this->primary_store,
357
+                $this->secondary_store,
358
+            ];
359
+        } elseif ( $action_id < $this->demarkation_id ) {
360
+            $stores = [
361
+                $this->secondary_store,
362
+                $this->primary_store,
363
+            ];
364
+        } else {
365
+            $stores = [
366
+                $this->primary_store,
367
+            ];
368
+        }
369
+
370
+        foreach ( $stores as $store ) {
371
+            $action = $store->fetch_action( $action_id );
372
+            if ( ! is_a( $action, 'ActionScheduler_NullAction' ) ) {
373
+                return $store;
374
+            }
375
+        }
376
+        return null;
377
+    }
378
+
379
+    /* * * * * * * * * * * * * * * * * * * * * * * * * * *
380 380
 	 * All claim-related functions should operate solely
381 381
 	 * on the primary store.
382 382
 	 * * * * * * * * * * * * * * * * * * * * * * * * * * */
383 383
 
384
-	/**
385
-	 * Get the claim count from the table data store.
386
-	 */
387
-	public function get_claim_count() {
388
-		return $this->primary_store->get_claim_count();
389
-	}
390
-
391
-	/**
392
-	 * Retrieve the claim ID for an action from the table data store.
393
-	 *
394
-	 * @param int $action_id Action ID.
395
-	 */
396
-	public function get_claim_id( $action_id ) {
397
-		return $this->primary_store->get_claim_id( $action_id );
398
-	}
399
-
400
-	/**
401
-	 * Release a claim in the table data store.
402
-	 *
403
-	 * @param ActionScheduler_ActionClaim $claim Claim object.
404
-	 */
405
-	public function release_claim( ActionScheduler_ActionClaim $claim ) {
406
-		$this->primary_store->release_claim( $claim );
407
-	}
408
-
409
-	/**
410
-	 * Release claims on an action in the table data store.
411
-	 *
412
-	 * @param int $action_id Action ID.
413
-	 */
414
-	public function unclaim_action( $action_id ) {
415
-		$this->primary_store->unclaim_action( $action_id );
416
-	}
417
-
418
-	/**
419
-	 * Retrieve a list of action IDs by claim.
420
-	 *
421
-	 * @param int $claim_id Claim ID.
422
-	 */
423
-	public function find_actions_by_claim_id( $claim_id ) {
424
-		return $this->primary_store->find_actions_by_claim_id( $claim_id );
425
-	}
384
+    /**
385
+     * Get the claim count from the table data store.
386
+     */
387
+    public function get_claim_count() {
388
+        return $this->primary_store->get_claim_count();
389
+    }
390
+
391
+    /**
392
+     * Retrieve the claim ID for an action from the table data store.
393
+     *
394
+     * @param int $action_id Action ID.
395
+     */
396
+    public function get_claim_id( $action_id ) {
397
+        return $this->primary_store->get_claim_id( $action_id );
398
+    }
399
+
400
+    /**
401
+     * Release a claim in the table data store.
402
+     *
403
+     * @param ActionScheduler_ActionClaim $claim Claim object.
404
+     */
405
+    public function release_claim( ActionScheduler_ActionClaim $claim ) {
406
+        $this->primary_store->release_claim( $claim );
407
+    }
408
+
409
+    /**
410
+     * Release claims on an action in the table data store.
411
+     *
412
+     * @param int $action_id Action ID.
413
+     */
414
+    public function unclaim_action( $action_id ) {
415
+        $this->primary_store->unclaim_action( $action_id );
416
+    }
417
+
418
+    /**
419
+     * Retrieve a list of action IDs by claim.
420
+     *
421
+     * @param int $claim_id Claim ID.
422
+     */
423
+    public function find_actions_by_claim_id( $claim_id ) {
424
+        return $this->primary_store->find_actions_by_claim_id( $claim_id );
425
+    }
426 426
 }
Please login to merge, or discard this patch.
action-scheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php 1 patch
Indentation   +232 added lines, -232 removed lines patch added patch discarded remove patch
@@ -4,237 +4,237 @@
 block discarded – undo
4 4
  * Class ActionScheduler_wpCommentLogger
5 5
  */
6 6
 class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger {
7
-	const AGENT = 'ActionScheduler';
8
-	const TYPE = 'action_log';
9
-
10
-	/**
11
-	 * @param string $action_id
12
-	 * @param string $message
13
-	 * @param DateTime $date
14
-	 *
15
-	 * @return string The log entry ID
16
-	 */
17
-	public function log( $action_id, $message, DateTime $date = NULL ) {
18
-		if ( empty($date) ) {
19
-			$date = as_get_datetime_object();
20
-		} else {
21
-			$date = as_get_datetime_object( clone $date );
22
-		}
23
-		$comment_id = $this->create_wp_comment( $action_id, $message, $date );
24
-		return $comment_id;
25
-	}
26
-
27
-	protected function create_wp_comment( $action_id, $message, DateTime $date ) {
28
-
29
-		$comment_date_gmt = $date->format('Y-m-d H:i:s');
30
-		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
31
-		$comment_data = array(
32
-			'comment_post_ID' => $action_id,
33
-			'comment_date' => $date->format('Y-m-d H:i:s'),
34
-			'comment_date_gmt' => $comment_date_gmt,
35
-			'comment_author' => self::AGENT,
36
-			'comment_content' => $message,
37
-			'comment_agent' => self::AGENT,
38
-			'comment_type' => self::TYPE,
39
-		);
40
-		return wp_insert_comment($comment_data);
41
-	}
42
-
43
-	/**
44
-	 * @param string $entry_id
45
-	 *
46
-	 * @return ActionScheduler_LogEntry
47
-	 */
48
-	public function get_entry( $entry_id ) {
49
-		$comment = $this->get_comment( $entry_id );
50
-		if ( empty($comment) || $comment->comment_type != self::TYPE ) {
51
-			return new ActionScheduler_NullLogEntry();
52
-		}
53
-
54
-		$date = as_get_datetime_object( $comment->comment_date_gmt );
55
-		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
56
-		return new ActionScheduler_LogEntry( $comment->comment_post_ID, $comment->comment_content, $date );
57
-	}
58
-
59
-	/**
60
-	 * @param string $action_id
61
-	 *
62
-	 * @return ActionScheduler_LogEntry[]
63
-	 */
64
-	public function get_logs( $action_id ) {
65
-		$status = 'all';
66
-		if ( get_post_status($action_id) == 'trash' ) {
67
-			$status = 'post-trashed';
68
-		}
69
-		$comments = get_comments(array(
70
-			'post_id' => $action_id,
71
-			'orderby' => 'comment_date_gmt',
72
-			'order' => 'ASC',
73
-			'type' => self::TYPE,
74
-			'status' => $status,
75
-		));
76
-		$logs = array();
77
-		foreach ( $comments as $c ) {
78
-			$entry = $this->get_entry( $c );
79
-			if ( !empty($entry) ) {
80
-				$logs[] = $entry;
81
-			}
82
-		}
83
-		return $logs;
84
-	}
85
-
86
-	protected function get_comment( $comment_id ) {
87
-		return get_comment( $comment_id );
88
-	}
89
-
90
-
91
-
92
-	/**
93
-	 * @param WP_Comment_Query $query
94
-	 */
95
-	public function filter_comment_queries( $query ) {
96
-		foreach ( array('ID', 'parent', 'post_author', 'post_name', 'post_parent', 'type', 'post_type', 'post_id', 'post_ID') as $key ) {
97
-			if ( !empty($query->query_vars[$key]) ) {
98
-				return; // don't slow down queries that wouldn't include action_log comments anyway
99
-			}
100
-		}
101
-		$query->query_vars['action_log_filter'] = TRUE;
102
-		add_filter( 'comments_clauses', array( $this, 'filter_comment_query_clauses' ), 10, 2 );
103
-	}
104
-
105
-	/**
106
-	 * @param array $clauses
107
-	 * @param WP_Comment_Query $query
108
-	 *
109
-	 * @return array
110
-	 */
111
-	public function filter_comment_query_clauses( $clauses, $query ) {
112
-		if ( !empty($query->query_vars['action_log_filter']) ) {
113
-			$clauses['where'] .= $this->get_where_clause();
114
-		}
115
-		return $clauses;
116
-	}
117
-
118
-	/**
119
-	 * Make sure Action Scheduler logs are excluded from comment feeds, which use WP_Query, not
120
-	 * the WP_Comment_Query class handled by @see self::filter_comment_queries().
121
-	 *
122
-	 * @param string $where
123
-	 * @param WP_Query $query
124
-	 *
125
-	 * @return string
126
-	 */
127
-	public function filter_comment_feed( $where, $query ) {
128
-		if ( is_comment_feed() ) {
129
-			$where .= $this->get_where_clause();
130
-		}
131
-		return $where;
132
-	}
133
-
134
-	/**
135
-	 * Return a SQL clause to exclude Action Scheduler comments.
136
-	 *
137
-	 * @return string
138
-	 */
139
-	protected function get_where_clause() {
140
-		global $wpdb;
141
-		return sprintf( " AND {$wpdb->comments}.comment_type != '%s'", self::TYPE );
142
-	}
143
-
144
-	/**
145
-	 * Remove action log entries from wp_count_comments()
146
-	 *
147
-	 * @param array $stats
148
-	 * @param int $post_id
149
-	 *
150
-	 * @return object
151
-	 */
152
-	public function filter_comment_count( $stats, $post_id ) {
153
-		global $wpdb;
154
-
155
-		if ( 0 === $post_id ) {
156
-			$stats = $this->get_comment_count();
157
-		}
158
-
159
-		return $stats;
160
-	}
161
-
162
-	/**
163
-	 * Retrieve the comment counts from our cache, or the database if the cached version isn't set.
164
-	 *
165
-	 * @return object
166
-	 */
167
-	protected function get_comment_count() {
168
-		global $wpdb;
169
-
170
-		$stats = get_transient( 'as_comment_count' );
171
-
172
-		if ( ! $stats ) {
173
-			$stats = array();
174
-
175
-			$count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN('order_note','action_log') GROUP BY comment_approved", ARRAY_A );
176
-
177
-			$total = 0;
178
-			$stats = array();
179
-			$approved = array( '0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed' );
180
-
181
-			foreach ( (array) $count as $row ) {
182
-				// Don't count post-trashed toward totals
183
-				if ( 'post-trashed' != $row['comment_approved'] && 'trash' != $row['comment_approved'] ) {
184
-					$total += $row['num_comments'];
185
-				}
186
-				if ( isset( $approved[ $row['comment_approved'] ] ) ) {
187
-					$stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments'];
188
-				}
189
-			}
190
-
191
-			$stats['total_comments'] = $total;
192
-			$stats['all']            = $total;
193
-
194
-			foreach ( $approved as $key ) {
195
-				if ( empty( $stats[ $key ] ) ) {
196
-					$stats[ $key ] = 0;
197
-				}
198
-			}
199
-
200
-			$stats = (object) $stats;
201
-			set_transient( 'as_comment_count', $stats );
202
-		}
203
-
204
-		return $stats;
205
-	}
206
-
207
-	/**
208
-	 * Delete comment count cache whenever there is new comment or the status of a comment changes. Cache
209
-	 * will be regenerated next time ActionScheduler_wpCommentLogger::filter_comment_count() is called.
210
-	 */
211
-	public function delete_comment_count_cache() {
212
-		delete_transient( 'as_comment_count' );
213
-	}
214
-
215
-	/**
216
-	 * @codeCoverageIgnore
217
-	 */
218
-	public function init() {
219
-		add_action( 'action_scheduler_before_process_queue', array( $this, 'disable_comment_counting' ), 10, 0 );
220
-		add_action( 'action_scheduler_after_process_queue', array( $this, 'enable_comment_counting' ), 10, 0 );
221
-
222
-		parent::init();
223
-
224
-		add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 );
225
-		add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs
226
-		add_action( 'comment_feed_where', array( $this, 'filter_comment_feed' ), 10, 2 );
227
-
228
-		// Delete comments count cache whenever there is a new comment or a comment status changes
229
-		add_action( 'wp_insert_comment', array( $this, 'delete_comment_count_cache' ) );
230
-		add_action( 'wp_set_comment_status', array( $this, 'delete_comment_count_cache' ) );
231
-	}
232
-
233
-	public function disable_comment_counting() {
234
-		wp_defer_comment_counting(true);
235
-	}
236
-	public function enable_comment_counting() {
237
-		wp_defer_comment_counting(false);
238
-	}
7
+    const AGENT = 'ActionScheduler';
8
+    const TYPE = 'action_log';
9
+
10
+    /**
11
+     * @param string $action_id
12
+     * @param string $message
13
+     * @param DateTime $date
14
+     *
15
+     * @return string The log entry ID
16
+     */
17
+    public function log( $action_id, $message, DateTime $date = NULL ) {
18
+        if ( empty($date) ) {
19
+            $date = as_get_datetime_object();
20
+        } else {
21
+            $date = as_get_datetime_object( clone $date );
22
+        }
23
+        $comment_id = $this->create_wp_comment( $action_id, $message, $date );
24
+        return $comment_id;
25
+    }
26
+
27
+    protected function create_wp_comment( $action_id, $message, DateTime $date ) {
28
+
29
+        $comment_date_gmt = $date->format('Y-m-d H:i:s');
30
+        ActionScheduler_TimezoneHelper::set_local_timezone( $date );
31
+        $comment_data = array(
32
+            'comment_post_ID' => $action_id,
33
+            'comment_date' => $date->format('Y-m-d H:i:s'),
34
+            'comment_date_gmt' => $comment_date_gmt,
35
+            'comment_author' => self::AGENT,
36
+            'comment_content' => $message,
37
+            'comment_agent' => self::AGENT,
38
+            'comment_type' => self::TYPE,
39
+        );
40
+        return wp_insert_comment($comment_data);
41
+    }
42
+
43
+    /**
44
+     * @param string $entry_id
45
+     *
46
+     * @return ActionScheduler_LogEntry
47
+     */
48
+    public function get_entry( $entry_id ) {
49
+        $comment = $this->get_comment( $entry_id );
50
+        if ( empty($comment) || $comment->comment_type != self::TYPE ) {
51
+            return new ActionScheduler_NullLogEntry();
52
+        }
53
+
54
+        $date = as_get_datetime_object( $comment->comment_date_gmt );
55
+        ActionScheduler_TimezoneHelper::set_local_timezone( $date );
56
+        return new ActionScheduler_LogEntry( $comment->comment_post_ID, $comment->comment_content, $date );
57
+    }
58
+
59
+    /**
60
+     * @param string $action_id
61
+     *
62
+     * @return ActionScheduler_LogEntry[]
63
+     */
64
+    public function get_logs( $action_id ) {
65
+        $status = 'all';
66
+        if ( get_post_status($action_id) == 'trash' ) {
67
+            $status = 'post-trashed';
68
+        }
69
+        $comments = get_comments(array(
70
+            'post_id' => $action_id,
71
+            'orderby' => 'comment_date_gmt',
72
+            'order' => 'ASC',
73
+            'type' => self::TYPE,
74
+            'status' => $status,
75
+        ));
76
+        $logs = array();
77
+        foreach ( $comments as $c ) {
78
+            $entry = $this->get_entry( $c );
79
+            if ( !empty($entry) ) {
80
+                $logs[] = $entry;
81
+            }
82
+        }
83
+        return $logs;
84
+    }
85
+
86
+    protected function get_comment( $comment_id ) {
87
+        return get_comment( $comment_id );
88
+    }
89
+
90
+
91
+
92
+    /**
93
+     * @param WP_Comment_Query $query
94
+     */
95
+    public function filter_comment_queries( $query ) {
96
+        foreach ( array('ID', 'parent', 'post_author', 'post_name', 'post_parent', 'type', 'post_type', 'post_id', 'post_ID') as $key ) {
97
+            if ( !empty($query->query_vars[$key]) ) {
98
+                return; // don't slow down queries that wouldn't include action_log comments anyway
99
+            }
100
+        }
101
+        $query->query_vars['action_log_filter'] = TRUE;
102
+        add_filter( 'comments_clauses', array( $this, 'filter_comment_query_clauses' ), 10, 2 );
103
+    }
104
+
105
+    /**
106
+     * @param array $clauses
107
+     * @param WP_Comment_Query $query
108
+     *
109
+     * @return array
110
+     */
111
+    public function filter_comment_query_clauses( $clauses, $query ) {
112
+        if ( !empty($query->query_vars['action_log_filter']) ) {
113
+            $clauses['where'] .= $this->get_where_clause();
114
+        }
115
+        return $clauses;
116
+    }
117
+
118
+    /**
119
+     * Make sure Action Scheduler logs are excluded from comment feeds, which use WP_Query, not
120
+     * the WP_Comment_Query class handled by @see self::filter_comment_queries().
121
+     *
122
+     * @param string $where
123
+     * @param WP_Query $query
124
+     *
125
+     * @return string
126
+     */
127
+    public function filter_comment_feed( $where, $query ) {
128
+        if ( is_comment_feed() ) {
129
+            $where .= $this->get_where_clause();
130
+        }
131
+        return $where;
132
+    }
133
+
134
+    /**
135
+     * Return a SQL clause to exclude Action Scheduler comments.
136
+     *
137
+     * @return string
138
+     */
139
+    protected function get_where_clause() {
140
+        global $wpdb;
141
+        return sprintf( " AND {$wpdb->comments}.comment_type != '%s'", self::TYPE );
142
+    }
143
+
144
+    /**
145
+     * Remove action log entries from wp_count_comments()
146
+     *
147
+     * @param array $stats
148
+     * @param int $post_id
149
+     *
150
+     * @return object
151
+     */
152
+    public function filter_comment_count( $stats, $post_id ) {
153
+        global $wpdb;
154
+
155
+        if ( 0 === $post_id ) {
156
+            $stats = $this->get_comment_count();
157
+        }
158
+
159
+        return $stats;
160
+    }
161
+
162
+    /**
163
+     * Retrieve the comment counts from our cache, or the database if the cached version isn't set.
164
+     *
165
+     * @return object
166
+     */
167
+    protected function get_comment_count() {
168
+        global $wpdb;
169
+
170
+        $stats = get_transient( 'as_comment_count' );
171
+
172
+        if ( ! $stats ) {
173
+            $stats = array();
174
+
175
+            $count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN('order_note','action_log') GROUP BY comment_approved", ARRAY_A );
176
+
177
+            $total = 0;
178
+            $stats = array();
179
+            $approved = array( '0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed' );
180
+
181
+            foreach ( (array) $count as $row ) {
182
+                // Don't count post-trashed toward totals
183
+                if ( 'post-trashed' != $row['comment_approved'] && 'trash' != $row['comment_approved'] ) {
184
+                    $total += $row['num_comments'];
185
+                }
186
+                if ( isset( $approved[ $row['comment_approved'] ] ) ) {
187
+                    $stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments'];
188
+                }
189
+            }
190
+
191
+            $stats['total_comments'] = $total;
192
+            $stats['all']            = $total;
193
+
194
+            foreach ( $approved as $key ) {
195
+                if ( empty( $stats[ $key ] ) ) {
196
+                    $stats[ $key ] = 0;
197
+                }
198
+            }
199
+
200
+            $stats = (object) $stats;
201
+            set_transient( 'as_comment_count', $stats );
202
+        }
203
+
204
+        return $stats;
205
+    }
206
+
207
+    /**
208
+     * Delete comment count cache whenever there is new comment or the status of a comment changes. Cache
209
+     * will be regenerated next time ActionScheduler_wpCommentLogger::filter_comment_count() is called.
210
+     */
211
+    public function delete_comment_count_cache() {
212
+        delete_transient( 'as_comment_count' );
213
+    }
214
+
215
+    /**
216
+     * @codeCoverageIgnore
217
+     */
218
+    public function init() {
219
+        add_action( 'action_scheduler_before_process_queue', array( $this, 'disable_comment_counting' ), 10, 0 );
220
+        add_action( 'action_scheduler_after_process_queue', array( $this, 'enable_comment_counting' ), 10, 0 );
221
+
222
+        parent::init();
223
+
224
+        add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 );
225
+        add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs
226
+        add_action( 'comment_feed_where', array( $this, 'filter_comment_feed' ), 10, 2 );
227
+
228
+        // Delete comments count cache whenever there is a new comment or a comment status changes
229
+        add_action( 'wp_insert_comment', array( $this, 'delete_comment_count_cache' ) );
230
+        add_action( 'wp_set_comment_status', array( $this, 'delete_comment_count_cache' ) );
231
+    }
232
+
233
+    public function disable_comment_counting() {
234
+        wp_defer_comment_counting(true);
235
+    }
236
+    public function enable_comment_counting() {
237
+        wp_defer_comment_counting(false);
238
+    }
239 239
 
240 240
 }
Please login to merge, or discard this patch.
packages/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php 1 patch
Indentation   +865 added lines, -865 removed lines patch added patch discarded remove patch
@@ -9,869 +9,869 @@
 block discarded – undo
9 9
  */
10 10
 class ActionScheduler_DBStore extends ActionScheduler_Store {
11 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.', 'woocommerce' ) );
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', 'woocommerce' ), $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 x.x.x $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.', 'woocommerce' ) );
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 x.x.x $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', 'woocommerce' ), $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', 'woocommerce' ), $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', 'woocommerce' ), $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.', 'woocommerce' ), $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 x.x.x
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.', 'woocommerce' ) );
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', 'woocommerce' ), $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', 'woocommerce' ), $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.', 'woocommerce' ) );
871
-		} elseif ( empty( $status ) ) {
872
-			throw new \RuntimeException( __( 'Unknown status found for action.', 'woocommerce' ) );
873
-		} else {
874
-			return $status;
875
-		}
876
-	}
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.', 'woocommerce' ) );
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', 'woocommerce' ), $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 x.x.x $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.', 'woocommerce' ) );
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 x.x.x $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', 'woocommerce' ), $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', 'woocommerce' ), $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', 'woocommerce' ), $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.', 'woocommerce' ), $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 x.x.x
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.', 'woocommerce' ) );
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', 'woocommerce' ), $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', 'woocommerce' ), $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.', 'woocommerce' ) );
871
+        } elseif ( empty( $status ) ) {
872
+            throw new \RuntimeException( __( 'Unknown status found for action.', 'woocommerce' ) );
873
+        } else {
874
+            return $status;
875
+        }
876
+    }
877 877
 }
Please login to merge, or discard this patch.
packages/action-scheduler/classes/data-stores/ActionScheduler_DBLogger.php 1 patch
Indentation   +142 added lines, -142 removed lines patch added patch discarded remove patch
@@ -9,146 +9,146 @@
 block discarded – undo
9 9
  */
10 10
 class ActionScheduler_DBLogger extends ActionScheduler_Logger {
11 11
 
12
-	/**
13
-	 * Add a record to an action log.
14
-	 *
15
-	 * @param int      $action_id Action ID.
16
-	 * @param string   $message Message to be saved in the log entry.
17
-	 * @param DateTime $date Timestamp of the log entry.
18
-	 *
19
-	 * @return int     The log entry ID.
20
-	 */
21
-	public function log( $action_id, $message, DateTime $date = null ) {
22
-		if ( empty( $date ) ) {
23
-			$date = as_get_datetime_object();
24
-		} else {
25
-			$date = clone $date;
26
-		}
27
-
28
-		$date_gmt = $date->format( 'Y-m-d H:i:s' );
29
-		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
30
-		$date_local = $date->format( 'Y-m-d H:i:s' );
31
-
32
-		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
33
-		global $wpdb;
34
-		$wpdb->insert(
35
-			$wpdb->actionscheduler_logs,
36
-			array(
37
-				'action_id'      => $action_id,
38
-				'message'        => $message,
39
-				'log_date_gmt'   => $date_gmt,
40
-				'log_date_local' => $date_local,
41
-			),
42
-			array( '%d', '%s', '%s', '%s' )
43
-		);
44
-
45
-		return $wpdb->insert_id;
46
-	}
47
-
48
-	/**
49
-	 * Retrieve an action log entry.
50
-	 *
51
-	 * @param int $entry_id Log entry ID.
52
-	 *
53
-	 * @return ActionScheduler_LogEntry
54
-	 */
55
-	public function get_entry( $entry_id ) {
56
-		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
57
-		global $wpdb;
58
-		$entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) );
59
-
60
-		return $this->create_entry_from_db_record( $entry );
61
-	}
62
-
63
-	/**
64
-	 * Create an action log entry from a database record.
65
-	 *
66
-	 * @param object $record Log entry database record object.
67
-	 *
68
-	 * @return ActionScheduler_LogEntry
69
-	 */
70
-	private function create_entry_from_db_record( $record ) {
71
-		if ( empty( $record ) ) {
72
-			return new ActionScheduler_NullLogEntry();
73
-		}
74
-
75
-		if ( is_null( $record->log_date_gmt ) ) {
76
-			$date = as_get_datetime_object( ActionScheduler_StoreSchema::DEFAULT_DATE );
77
-		} else {
78
-			$date = as_get_datetime_object( $record->log_date_gmt );
79
-		}
80
-
81
-		return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date );
82
-	}
83
-
84
-	/**
85
-	 * Retrieve the an action's log entries from the database.
86
-	 *
87
-	 * @param int $action_id Action ID.
88
-	 *
89
-	 * @return ActionScheduler_LogEntry[]
90
-	 */
91
-	public function get_logs( $action_id ) {
92
-		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
93
-		global $wpdb;
94
-
95
-		$records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) );
96
-
97
-		return array_map( array( $this, 'create_entry_from_db_record' ), $records );
98
-	}
99
-
100
-	/**
101
-	 * Initialize the data store.
102
-	 *
103
-	 * @codeCoverageIgnore
104
-	 */
105
-	public function init() {
106
-		$table_maker = new ActionScheduler_LoggerSchema();
107
-		$table_maker->init();
108
-		$table_maker->register_tables();
109
-
110
-		parent::init();
111
-
112
-		add_action( 'action_scheduler_deleted_action', array( $this, 'clear_deleted_action_logs' ), 10, 1 );
113
-	}
114
-
115
-	/**
116
-	 * Delete the action logs for an action.
117
-	 *
118
-	 * @param int $action_id Action ID.
119
-	 */
120
-	public function clear_deleted_action_logs( $action_id ) {
121
-		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
122
-		global $wpdb;
123
-		$wpdb->delete( $wpdb->actionscheduler_logs, array( 'action_id' => $action_id ), array( '%d' ) );
124
-	}
125
-
126
-	/**
127
-	 * Bulk add cancel action log entries.
128
-	 *
129
-	 * @param array $action_ids List of action ID.
130
-	 */
131
-	public function bulk_log_cancel_actions( $action_ids ) {
132
-		if ( empty( $action_ids ) ) {
133
-			return;
134
-		}
135
-
136
-		/** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
137
-		global $wpdb;
138
-		$date     = as_get_datetime_object();
139
-		$date_gmt = $date->format( 'Y-m-d H:i:s' );
140
-		ActionScheduler_TimezoneHelper::set_local_timezone( $date );
141
-		$date_local = $date->format( 'Y-m-d H:i:s' );
142
-		$message    = __( 'action canceled', 'woocommerce' );
143
-		$format     = '(%d, ' . $wpdb->prepare( '%s, %s, %s', $message, $date_gmt, $date_local ) . ')';
144
-		$sql_query  = "INSERT {$wpdb->actionscheduler_logs} (action_id, message, log_date_gmt, log_date_local) VALUES ";
145
-		$value_rows = array();
146
-
147
-		foreach ( $action_ids as $action_id ) {
148
-			$value_rows[] = $wpdb->prepare( $format, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
149
-		}
150
-		$sql_query .= implode( ',', $value_rows );
151
-
152
-		$wpdb->query( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
153
-	}
12
+    /**
13
+     * Add a record to an action log.
14
+     *
15
+     * @param int      $action_id Action ID.
16
+     * @param string   $message Message to be saved in the log entry.
17
+     * @param DateTime $date Timestamp of the log entry.
18
+     *
19
+     * @return int     The log entry ID.
20
+     */
21
+    public function log( $action_id, $message, DateTime $date = null ) {
22
+        if ( empty( $date ) ) {
23
+            $date = as_get_datetime_object();
24
+        } else {
25
+            $date = clone $date;
26
+        }
27
+
28
+        $date_gmt = $date->format( 'Y-m-d H:i:s' );
29
+        ActionScheduler_TimezoneHelper::set_local_timezone( $date );
30
+        $date_local = $date->format( 'Y-m-d H:i:s' );
31
+
32
+        /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
33
+        global $wpdb;
34
+        $wpdb->insert(
35
+            $wpdb->actionscheduler_logs,
36
+            array(
37
+                'action_id'      => $action_id,
38
+                'message'        => $message,
39
+                'log_date_gmt'   => $date_gmt,
40
+                'log_date_local' => $date_local,
41
+            ),
42
+            array( '%d', '%s', '%s', '%s' )
43
+        );
44
+
45
+        return $wpdb->insert_id;
46
+    }
47
+
48
+    /**
49
+     * Retrieve an action log entry.
50
+     *
51
+     * @param int $entry_id Log entry ID.
52
+     *
53
+     * @return ActionScheduler_LogEntry
54
+     */
55
+    public function get_entry( $entry_id ) {
56
+        /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
57
+        global $wpdb;
58
+        $entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) );
59
+
60
+        return $this->create_entry_from_db_record( $entry );
61
+    }
62
+
63
+    /**
64
+     * Create an action log entry from a database record.
65
+     *
66
+     * @param object $record Log entry database record object.
67
+     *
68
+     * @return ActionScheduler_LogEntry
69
+     */
70
+    private function create_entry_from_db_record( $record ) {
71
+        if ( empty( $record ) ) {
72
+            return new ActionScheduler_NullLogEntry();
73
+        }
74
+
75
+        if ( is_null( $record->log_date_gmt ) ) {
76
+            $date = as_get_datetime_object( ActionScheduler_StoreSchema::DEFAULT_DATE );
77
+        } else {
78
+            $date = as_get_datetime_object( $record->log_date_gmt );
79
+        }
80
+
81
+        return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date );
82
+    }
83
+
84
+    /**
85
+     * Retrieve the an action's log entries from the database.
86
+     *
87
+     * @param int $action_id Action ID.
88
+     *
89
+     * @return ActionScheduler_LogEntry[]
90
+     */
91
+    public function get_logs( $action_id ) {
92
+        /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
93
+        global $wpdb;
94
+
95
+        $records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) );
96
+
97
+        return array_map( array( $this, 'create_entry_from_db_record' ), $records );
98
+    }
99
+
100
+    /**
101
+     * Initialize the data store.
102
+     *
103
+     * @codeCoverageIgnore
104
+     */
105
+    public function init() {
106
+        $table_maker = new ActionScheduler_LoggerSchema();
107
+        $table_maker->init();
108
+        $table_maker->register_tables();
109
+
110
+        parent::init();
111
+
112
+        add_action( 'action_scheduler_deleted_action', array( $this, 'clear_deleted_action_logs' ), 10, 1 );
113
+    }
114
+
115
+    /**
116
+     * Delete the action logs for an action.
117
+     *
118
+     * @param int $action_id Action ID.
119
+     */
120
+    public function clear_deleted_action_logs( $action_id ) {
121
+        /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
122
+        global $wpdb;
123
+        $wpdb->delete( $wpdb->actionscheduler_logs, array( 'action_id' => $action_id ), array( '%d' ) );
124
+    }
125
+
126
+    /**
127
+     * Bulk add cancel action log entries.
128
+     *
129
+     * @param array $action_ids List of action ID.
130
+     */
131
+    public function bulk_log_cancel_actions( $action_ids ) {
132
+        if ( empty( $action_ids ) ) {
133
+            return;
134
+        }
135
+
136
+        /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort
137
+        global $wpdb;
138
+        $date     = as_get_datetime_object();
139
+        $date_gmt = $date->format( 'Y-m-d H:i:s' );
140
+        ActionScheduler_TimezoneHelper::set_local_timezone( $date );
141
+        $date_local = $date->format( 'Y-m-d H:i:s' );
142
+        $message    = __( 'action canceled', 'woocommerce' );
143
+        $format     = '(%d, ' . $wpdb->prepare( '%s, %s, %s', $message, $date_gmt, $date_local ) . ')';
144
+        $sql_query  = "INSERT {$wpdb->actionscheduler_logs} (action_id, message, log_date_gmt, log_date_local) VALUES ";
145
+        $value_rows = array();
146
+
147
+        foreach ( $action_ids as $action_id ) {
148
+            $value_rows[] = $wpdb->prepare( $format, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
149
+        }
150
+        $sql_query .= implode( ',', $value_rows );
151
+
152
+        $wpdb->query( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
153
+    }
154 154
 }
Please login to merge, or discard this patch.
classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php 1 patch
Indentation   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -5,22 +5,22 @@
 block discarded – undo
5 5
  * @codeCoverageIgnore
6 6
  */
7 7
 class ActionScheduler_wpPostStore_TaxonomyRegistrar {
8
-	public function register() {
9
-		register_taxonomy( ActionScheduler_wpPostStore::GROUP_TAXONOMY, ActionScheduler_wpPostStore::POST_TYPE, $this->taxonomy_args() );
10
-	}
8
+    public function register() {
9
+        register_taxonomy( ActionScheduler_wpPostStore::GROUP_TAXONOMY, ActionScheduler_wpPostStore::POST_TYPE, $this->taxonomy_args() );
10
+    }
11 11
 
12
-	protected function taxonomy_args() {
13
-		$args = array(
14
-			'label' => __( 'Action Group', 'woocommerce' ),
15
-			'public' => false,
16
-			'hierarchical' => false,
17
-			'show_admin_column' => true,
18
-			'query_var' => false,
19
-			'rewrite' => false,
20
-		);
12
+    protected function taxonomy_args() {
13
+        $args = array(
14
+            'label' => __( 'Action Group', 'woocommerce' ),
15
+            'public' => false,
16
+            'hierarchical' => false,
17
+            'show_admin_column' => true,
18
+            'query_var' => false,
19
+            'rewrite' => false,
20
+        );
21 21
 
22
-		$args = apply_filters( 'action_scheduler_taxonomy_args', $args );
23
-		return $args;
24
-	}
22
+        $args = apply_filters( 'action_scheduler_taxonomy_args', $args );
23
+        return $args;
24
+    }
25 25
 }
26
- 
27 26
\ No newline at end of file
27
+    
28 28
\ No newline at end of file
Please login to merge, or discard this patch.