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

ActionScheduler_ListTable.php in Image Optimizer – Compress Images and Convert to WebP or AVIF 1.5.4, at vendor/woocommerce/action-scheduler/classes/ActionScheduler_ListTable.php

672 lines 20.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Implements the admin view of the actions.
5 * @codeCoverageIgnore
6 */
7 class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
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 Store object.
80 * @param ActionScheduler_Logger $logger Logger object.
81 * @param ActionScheduler_QueueRunner $runner Runner object.
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', 'action-scheduler' );
90
91 $this->bulk_actions = array(
92 'delete' => __( 'Delete', 'action-scheduler' ),
93 );
94
95 $this->columns = array(
96 'hook' => __( 'Hook', 'action-scheduler' ),
97 'status' => __( 'Status', 'action-scheduler' ),
98 'args' => __( 'Arguments', 'action-scheduler' ),
99 'group' => __( 'Group', 'action-scheduler' ),
100 'recurrence' => __( 'Recurrence', 'action-scheduler' ),
101 'schedule' => __( 'Scheduled Date', 'action-scheduler' ),
102 'log_entries' => __( 'Log', 'action-scheduler' ),
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', 'action-scheduler' ) );
123 $this->sort_by[] = 'claim_id';
124 }
125
126 $this->row_actions = array(
127 'hook' => array(
128 'run' => array(
129 'name' => __( 'Run', 'action-scheduler' ),
130 'desc' => __( 'Process the action now as if it were run as part of a queue', 'action-scheduler' ),
131 ),
132 'cancel' => array(
133 'name' => __( 'Cancel', 'action-scheduler' ),
134 'desc' => __( 'Cancel the action now to avoid it being run in future', 'action-scheduler' ),
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', 'action-scheduler' ),
145 ),
146 array(
147 'seconds' => MONTH_IN_SECONDS,
148 /* translators: %s: amount of time */
149 'names' => _n_noop( '%s month', '%s months', 'action-scheduler' ),
150 ),
151 array(
152 'seconds' => WEEK_IN_SECONDS,
153 /* translators: %s: amount of time */
154 'names' => _n_noop( '%s week', '%s weeks', 'action-scheduler' ),
155 ),
156 array(
157 'seconds' => DAY_IN_SECONDS,
158 /* translators: %s: amount of time */
159 'names' => _n_noop( '%s day', '%s days', 'action-scheduler' ),
160 ),
161 array(
162 'seconds' => HOUR_IN_SECONDS,
163 /* translators: %s: amount of time */
164 'names' => _n_noop( '%s hour', '%s hours', 'action-scheduler' ),
165 ),
166 array(
167 'seconds' => MINUTE_IN_SECONDS,
168 /* translators: %s: amount of time */
169 'names' => _n_noop( '%s minute', '%s minutes', 'action-scheduler' ),
170 ),
171 array(
172 'seconds' => 1,
173 /* translators: %s: amount of time */
174 'names' => _n_noop( '%s second', '%s seconds', 'action-scheduler' ),
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!', 'action-scheduler' );
225 }
226
227 $output = '';
228 $num_time_periods = count( self::$time_periods );
229
230 for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < $num_time_periods && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) {
231
232 $periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] );
233
234 if ( $periods_in_interval > 0 ) {
235 if ( ! empty( $output ) ) {
236 $output .= ' ';
237 }
238 $output .= sprintf( translate_nooped_plural( self::$time_periods[ $time_period_index ]['names'], $periods_in_interval, 'action-scheduler' ), $periods_in_interval );
239 $seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds'];
240 $periods_included++;
241 }
242 }
243
244 return $output;
245 }
246
247 /**
248 * Returns the recurrence of an action or 'Non-repeating'. The output is human readable.
249 *
250 * @param ActionScheduler_Action $action Action object.
251 *
252 * @return string
253 */
254 protected function get_recurrence( $action ) {
255 $schedule = $action->get_schedule();
256 if ( $schedule->is_recurring() && method_exists( $schedule, 'get_recurrence' ) ) {
257 $recurrence = $schedule->get_recurrence();
258
259 if ( is_numeric( $recurrence ) ) {
260 /* translators: %s: time interval */
261 return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence ) );
262 } else {
263 return $recurrence;
264 }
265 }
266
267 return __( 'Non-repeating', 'action-scheduler' );
268 }
269
270 /**
271 * Serializes the argument of an action to render it in a human friendly format.
272 *
273 * @param array $row The array representation of the current row of the table.
274 *
275 * @return string
276 */
277 public function column_args( array $row ) {
278 if ( empty( $row['args'] ) ) {
279 return apply_filters( 'action_scheduler_list_table_column_args', '', $row );
280 }
281
282 $row_html = '<ul>';
283 foreach ( $row['args'] as $key => $value ) {
284 $row_html .= sprintf( '<li><code>%s => %s</code></li>', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) );
285 }
286 $row_html .= '</ul>';
287
288 return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row );
289 }
290
291 /**
292 * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
293 *
294 * @param array $row Action array.
295 * @return string
296 */
297 public function column_log_entries( array $row ) {
298
299 $log_entries_html = '<ol>';
300
301 $timezone = new DateTimezone( 'UTC' );
302
303 foreach ( $row['log_entries'] as $log_entry ) {
304 $log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone );
305 }
306
307 $log_entries_html .= '</ol>';
308
309 return $log_entries_html;
310 }
311
312 /**
313 * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
314 *
315 * @param ActionScheduler_LogEntry $log_entry Log entry object.
316 * @param DateTimezone $timezone Timestamp.
317 * @return string
318 */
319 protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
320 $date = $log_entry->get_date();
321 $date->setTimezone( $timezone );
322 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() ) );
323 }
324
325 /**
326 * Only display row actions for pending actions.
327 *
328 * @param array $row Row to render.
329 * @param string $column_name Current row.
330 *
331 * @return string
332 */
333 protected function maybe_render_actions( $row, $column_name ) {
334 if ( 'pending' === strtolower( $row[ 'status_name' ] ) ) {
335 return parent::maybe_render_actions( $row, $column_name );
336 }
337
338 return '';
339 }
340
341 /**
342 * Renders admin notifications
343 *
344 * Notifications:
345 * 1. When the maximum number of tasks are being executed simultaneously.
346 * 2. Notifications when a task is manually executed.
347 * 3. Tables are missing.
348 */
349 public function display_admin_notices() {
350 global $wpdb;
351
352 if ( ( is_a( $this->store, 'ActionScheduler_HybridStore' ) || is_a( $this->store, 'ActionScheduler_DBStore' ) ) && apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) {
353 $table_list = array(
354 'actionscheduler_actions',
355 'actionscheduler_logs',
356 'actionscheduler_groups',
357 'actionscheduler_claims',
358 );
359
360 $found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
361 foreach ( $table_list as $table_name ) {
362 if ( ! in_array( $wpdb->prefix . $table_name, $found_tables ) ) {
363 $this->admin_notices[] = array(
364 'class' => 'error',
365 'message' => __( 'It appears one or more database tables were missing. Attempting to re-create the missing table(s).' , 'action-scheduler' ),
366 );
367 $this->recreate_tables();
368 parent::display_admin_notices();
369
370 return;
371 }
372 }
373 }
374
375 if ( $this->runner->has_maximum_concurrent_batches() ) {
376 $claim_count = $this->store->get_claim_count();
377 $this->admin_notices[] = array(
378 'class' => 'updated',
379 'message' => sprintf(
380 /* translators: %s: amount of claims */
381 _n(
382 'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.',
383 'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.',
384 $claim_count,
385 'action-scheduler'
386 ),
387 $claim_count
388 ),
389 );
390 } elseif ( $this->store->has_pending_actions_due() ) {
391
392 $async_request_lock_expiration = ActionScheduler::lock()->get_expiration( 'async-request-runner' );
393
394 // No lock set or lock expired.
395 if ( false === $async_request_lock_expiration || $async_request_lock_expiration < time() ) {
396 $in_progress_url = add_query_arg( 'status', 'in-progress', remove_query_arg( 'status' ) );
397 /* translators: %s: process URL */
398 $async_request_message = sprintf( __( 'A new queue has begun processing. <a href="%s">View actions in-progress &raquo;</a>', 'action-scheduler' ), esc_url( $in_progress_url ) );
399 } else {
400 /* translators: %d: seconds */
401 $async_request_message = sprintf( __( 'The next queue will begin processing in approximately %d seconds.', 'action-scheduler' ), $async_request_lock_expiration - time() );
402 }
403
404 $this->admin_notices[] = array(
405 'class' => 'notice notice-info',
406 'message' => $async_request_message,
407 );
408 }
409
410 $notification = get_transient( 'action_scheduler_admin_notice' );
411
412 if ( is_array( $notification ) ) {
413 delete_transient( 'action_scheduler_admin_notice' );
414
415 $action = $this->store->fetch_action( $notification['action_id'] );
416 $action_hook_html = '<strong><code>' . $action->get_hook() . '</code></strong>';
417 if ( 1 == $notification['success'] ) {
418 $class = 'updated';
419 switch ( $notification['row_action_type'] ) {
420 case 'run' :
421 /* translators: %s: action HTML */
422 $action_message_html = sprintf( __( 'Successfully executed action: %s', 'action-scheduler' ), $action_hook_html );
423 break;
424 case 'cancel' :
425 /* translators: %s: action HTML */
426 $action_message_html = sprintf( __( 'Successfully canceled action: %s', 'action-scheduler' ), $action_hook_html );
427 break;
428 default :
429 /* translators: %s: action HTML */
430 $action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'action-scheduler' ), $action_hook_html );
431 break;
432 }
433 } else {
434 $class = 'error';
435 /* translators: 1: action HTML 2: action ID 3: error message */
436 $action_message_html = sprintf( __( 'Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'action-scheduler' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) );
437 }
438
439 $action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification );
440
441 $this->admin_notices[] = array(
442 'class' => $class,
443 'message' => $action_message_html,
444 );
445 }
446
447 parent::display_admin_notices();
448 }
449
450 /**
451 * Prints the scheduled date in a human friendly format.
452 *
453 * @param array $row The array representation of the current row of the table.
454 *
455 * @return string
456 */
457 public function column_schedule( $row ) {
458 return $this->get_schedule_display_string( $row['schedule'] );
459 }
460
461 /**
462 * Get the scheduled date in a human friendly format.
463 *
464 * @param ActionScheduler_Schedule $schedule Action's schedule.
465 * @return string
466 */
467 protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) {
468
469 $schedule_display_string = '';
470
471 if ( is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) {
472 return __( 'async', 'action-scheduler' );
473 }
474
475 if ( ! method_exists( $schedule, 'get_date' ) || ! $schedule->get_date() ) {
476 return '0000-00-00 00:00:00';
477 }
478
479 $next_timestamp = $schedule->get_date()->getTimestamp();
480
481 $schedule_display_string .= $schedule->get_date()->format( 'Y-m-d H:i:s O' );
482 $schedule_display_string .= '<br/>';
483
484 if ( gmdate( 'U' ) > $next_timestamp ) {
485 /* translators: %s: date interval */
486 $schedule_display_string .= sprintf( __( ' (%s ago)', 'action-scheduler' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) );
487 } else {
488 /* translators: %s: date interval */
489 $schedule_display_string .= sprintf( __( ' (%s)', 'action-scheduler' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) );
490 }
491
492 return $schedule_display_string;
493 }
494
495 /**
496 * Bulk delete.
497 *
498 * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data
499 * properly validated by the callee and it will delete the actions without any extra validation.
500 *
501 * @param int[] $ids Action IDs.
502 * @param string $ids_sql Inherited and unused.
503 */
504 protected function bulk_delete( array $ids, $ids_sql ) {
505 foreach ( $ids as $id ) {
506 try {
507 $this->store->delete_action( $id );
508 } catch ( Exception $e ) {
509 // A possible reason for an exception would include a scenario where the same action is deleted by a
510 // concurrent request.
511 error_log(
512 sprintf(
513 /* translators: 1: action ID 2: exception message. */
514 __( 'Action Scheduler was unable to delete action %1$d. Reason: %2$s', 'action-scheduler' ),
515 $id,
516 $e->getMessage()
517 )
518 );
519 }
520 }
521 }
522
523 /**
524 * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
525 * parameters are valid.
526 *
527 * @param int $action_id Action ID.
528 */
529 protected function row_action_cancel( $action_id ) {
530 $this->process_row_action( $action_id, 'cancel' );
531 }
532
533 /**
534 * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
535 * parameters are valid.
536 *
537 * @param int $action_id Action ID.
538 */
539 protected function row_action_run( $action_id ) {
540 $this->process_row_action( $action_id, 'run' );
541 }
542
543 /**
544 * Force the data store schema updates.
545 */
546 protected function recreate_tables() {
547 if ( is_a( $this->store, 'ActionScheduler_HybridStore' ) ) {
548 $store = $this->store;
549 } else {
550 $store = new ActionScheduler_HybridStore();
551 }
552 add_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10, 2 );
553
554 $store_schema = new ActionScheduler_StoreSchema();
555 $logger_schema = new ActionScheduler_LoggerSchema();
556 $store_schema->register_tables( true );
557 $logger_schema->register_tables( true );
558
559 remove_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10 );
560 }
561 /**
562 * Implements the logic behind processing an action once an action link is clicked on the list table.
563 *
564 * @param int $action_id Action ID.
565 * @param string $row_action_type The type of action to perform on the action.
566 */
567 protected function process_row_action( $action_id, $row_action_type ) {
568 try {
569 switch ( $row_action_type ) {
570 case 'run' :
571 $this->runner->process_action( $action_id, 'Admin List Table' );
572 break;
573 case 'cancel' :
574 $this->store->cancel_action( $action_id );
575 break;
576 }
577 $success = 1;
578 $error_message = '';
579 } catch ( Exception $e ) {
580 $success = 0;
581 $error_message = $e->getMessage();
582 }
583
584 set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 );
585 }
586
587 /**
588 * {@inheritDoc}
589 */
590 public function prepare_items() {
591 $this->prepare_column_headers();
592
593 $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
594
595 $query = array(
596 'per_page' => $per_page,
597 'offset' => $this->get_items_offset(),
598 'status' => $this->get_request_status(),
599 'orderby' => $this->get_request_orderby(),
600 'order' => $this->get_request_order(),
601 'search' => $this->get_request_search_query(),
602 );
603
604 /**
605 * Change query arguments to query for past-due actions.
606 * Past-due actions have the 'pending' status and are in the past.
607 * This is needed because registering 'past-due' as a status is overkill.
608 */
609 if ( 'past-due' === $this->get_request_status() ) {
610 $query['status'] = ActionScheduler_Store::STATUS_PENDING;
611 $query['date'] = as_get_datetime_object();
612 }
613
614 $this->items = array();
615
616 $total_items = $this->store->query_actions( $query, 'count' );
617
618 $status_labels = $this->store->get_status_labels();
619
620 foreach ( $this->store->query_actions( $query ) as $action_id ) {
621 try {
622 $action = $this->store->fetch_action( $action_id );
623 } catch ( Exception $e ) {
624 continue;
625 }
626 if ( is_a( $action, 'ActionScheduler_NullAction' ) ) {
627 continue;
628 }
629 $this->items[ $action_id ] = array(
630 'ID' => $action_id,
631 'hook' => $action->get_hook(),
632 'status_name' => $this->store->get_status( $action_id ),
633 'status' => $status_labels[ $this->store->get_status( $action_id ) ],
634 'args' => $action->get_args(),
635 'group' => $action->get_group(),
636 'log_entries' => $this->logger->get_logs( $action_id ),
637 'claim_id' => $this->store->get_claim_id( $action_id ),
638 'recurrence' => $this->get_recurrence( $action ),
639 'schedule' => $action->get_schedule(),
640 );
641 }
642
643 $this->set_pagination_args( array(
644 'total_items' => $total_items,
645 'per_page' => $per_page,
646 'total_pages' => ceil( $total_items / $per_page ),
647 ) );
648 }
649
650 /**
651 * Prints the available statuses so the user can click to filter.
652 */
653 protected function display_filter_by_status() {
654 $this->status_counts = $this->store->action_counts() + $this->store->extra_action_counts();
655 parent::display_filter_by_status();
656 }
657
658 /**
659 * Get the text to display in the search box on the list table.
660 */
661 protected function get_search_box_button_text() {
662 return __( 'Search hook, args and claim ID', 'action-scheduler' );
663 }
664
665 /**
666 * {@inheritDoc}
667 */
668 protected function get_per_page_option_name() {
669 return str_replace( '-', '_', $this->screen->id ) . '_per_page';
670 }
671 }
672