ScheduledActionsTable::display_admin_notices()   C
last analyzed

Complexity

Conditions 15
Paths 49

Size

Total Lines 90
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 240

Importance

Changes 0
Metric Value
eloc 65
dl 0
loc 90
ccs 0
cts 70
cp 0
rs 5.9166
c 0
b 0
f 0
cc 15
nc 49
nop 0
crap 240

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace GeminiLabs\SiteReviews\Overrides;
4
5
use ActionScheduler;
6
use ActionScheduler_Abstract_ListTable;
7
use GeminiLabs\SiteReviews\Modules\Date;
8
use GeminiLabs\SiteReviews\Modules\Queue;
9
10
class ScheduledActionsTable extends \ActionScheduler_Abstract_ListTable
11
{
12
    /**
13
     * @var array
14
     */
15
    protected $bulk_actions = [];
16
17
    /**
18
     * @var array
19
     */
20
    protected $columns = [];
21
22
    /**
23
     * @var bool
24
     */
25
    protected static $did_notification = false;
26
27
    /**
28
     * @var int
29
     */
30
    protected $items_per_page = 50;
31
32
    /**
33
     * @var \ActionScheduler_Logger
34
     */
35
    protected $logger;
36
37
    /**
38
     * @var string
39
     */
40
    protected $package = 'action-scheduler';
41
42
    /**
43
     * @var array
44
     */
45
    protected $row_actions = [];
46
47
    /**
48
     * @var array
49
     */
50
    protected $row_actions_all = [];
51
52
    /**
53
     * @var \ActionScheduler_QueueRunner
54
     */
55
    protected $runner;
56
57
    /**
58
     * @var array
59
     */
60
    protected $search_by = [
61
        'hook',
62
        'args',
63
        'claim_id',
64
    ];
65
66
    /**
67
     * @var array
68
     */
69
    protected $sort_by = [
70
        'schedule',
71
        'hook',
72
    ];
73
74
    /**
75
     * @var \ActionScheduler_Store
76
     */
77
    protected $store;
78
79
    public function __construct()
80
    {
81
        $this->store = \ActionScheduler::store();
82
        $this->logger = \ActionScheduler::logger();
83
        $this->runner = \ActionScheduler::runner();
84
        $this->items_per_page = glsr()->filterInt('scheduler/per-page', $this->items_per_page);
85
        $this->bulk_actions = [
86
            'delete' => _x('Delete', 'admin-text', 'site-reviews'),
87
        ];
88
        $this->columns = [
89
            'hook' => _x('Hook', 'admin-text', 'site-reviews'),
90
            'status' => _x('Status', 'admin-text', 'site-reviews'),
91
            'recurrence' => _x('Recurrence', 'admin-text', 'site-reviews'),
92
            'schedule' => _x('Scheduled Date', 'admin-text', 'site-reviews'),
93
            'args' => _x('Args', 'admin-text', 'site-reviews'),
94
            'log_entries' => _x('Log', 'admin-text', 'site-reviews'),
95
        ];
96
        $request_status = $this->get_request_status();
97
        if (empty($request_status)) {
98
            $this->sort_by[] = 'schedule';
99
        } elseif (in_array($request_status, ['in-progress', 'failed'])) {
100
            $this->columns += ['claim_id' => _x('Claim ID', 'admin-text', 'site-reviews')];
101
            $this->sort_by[] = 'claim_id';
102
        }
103
        $this->row_actions_all = [
104
            'hook' => [
105
                'retry' => [
106
                    'name' => _x('Retry', 'admin-text', 'site-reviews'),
107
                    'desc' => _x('Retry the action now as if it were run as part of a queue', 'admin-text', 'site-reviews'),
108
                ],
109
                'run' => [
110
                    'name' => _x('Run Now', 'admin-text', 'site-reviews'),
111
                    'desc' => _x('Process the action now as if it were run as part of a queue', 'admin-text', 'site-reviews'),
112
                ],
113
                'cancel' => [
114
                    'name' => _x('Cancel', 'admin-text', 'site-reviews'),
115
                    'desc' => _x('Cancel the action now to avoid it being run in future', 'admin-text', 'site-reviews'),
116
                    'class' => 'cancel trash',
117
                ],
118
                'delete' => [
119
                    'name' => _x('Delete', 'admin-text', 'site-reviews'),
120
                    'desc' => _x('Delete the action now', 'admin-text', 'site-reviews'),
121
                    'class' => 'trash',
122
                ],
123
            ],
124
        ];
125
        parent::__construct([
126
            'ajax' => true,
127
            'plural' => 'action-scheduler',
128
            'singular' => 'action-scheduler',
129
        ]);
130
    }
131
132
    /**
133
     * Serializes the argument of an action to render it in a human friendly format.
134
     *
135
     * @param array $row The array representation of the current row of the table
136
     *
137
     * @return string
138
     */
139
    public function column_args(array $row)
140
    {
141
        if (empty($row['args'])) {
142
            return apply_filters('action_scheduler_list_table_column_args', '', $row);
143
        }
144
        $tooltip = var_export($row['args'], true);
145
        $patterns = [
146
            "/array \(/" => '[',
147
            "/^(\s*)\)(,?)$/m" => '$1]$2',
148
            "/=>\s?\\n\s+\[/" => '=> [',
149
            "/(\s*)(\'[^\']+\') => ([\[\'])/" => '$1$2 => $3',
150
            "/\[\\n\s+\]/" => '[]',
151
            "/\s+\d+\s=>\s(\d+,)\\n/" => '$1',
152
            "/(\d+),\s+(\],\\n)/" => '$1$2',
153
        ];
154
        $tooltip = preg_replace(array_keys($patterns), array_values($patterns), $tooltip);
155
        $row_html = sprintf('<span class="glsr-tooltip dashicons-before dashicons-format-aside" data-syntax="php" data-tippy-allowHTML="1" data-tippy-content="%s" data-tippy-delay="0" data-tippy-interactive="1" data-tippy-maxWidth="640" data-tippy-trigger="click"></span>',
156
            esc_html($tooltip)
157
        );
158
        return apply_filters('action_scheduler_list_table_column_args', $row_html, $row);
159
    }
160
161
    /**
162
     * Default column formatting, it will escape everythig for security.
163
     */
164
    public function column_hook(array $row)
165
    {
166
        $column_html = sprintf('<span class="row-title">%s</span>', esc_html($row['hook']));
167
        $column_html .= $this->maybe_render_actions($row, 'hook');
168
        return $column_html;
169
    }
170
171
    /**
172
     * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
173
     *
174
     * @param array $row action array
175
     *
176
     * @return string
177
     */
178
    public function column_log_entries(array $row)
179
    {
180
        $log_entries_html = '<ol>';
181
        $timezone = new \DateTimeZone('UTC');
182
        foreach ($row['log_entries'] as $log_entry) {
183
            $log_entries_html .= $this->get_log_entry_html($log_entry, $timezone);
184
        }
185
        $log_entries_html .= '</ol>';
186
        return sprintf('<span class="glsr-tooltip dashicons-before dashicons-format-aside" data-tippy-allowHTML="1" data-tippy-content="%s" data-tippy-delay="0" data-tippy-interactive="1" data-tippy-trigger="click"></span>',
187
            esc_html($log_entries_html)
188
        );
189
    }
190
191
    /**
192
     * Prints the scheduled date in a human friendly format.
193
     *
194
     * @param array $row The array representation of the current row of the table
195
     *
196
     * @return string
197
     */
198
    public function column_schedule($row)
199
    {
200
        return $this->get_schedule_display_string($row['schedule']);
201
    }
202
203
    /**
204
     * Renders admin notifications.
205
     *
206
     * Notifications:
207
     *  1. When the maximum number of tasks are being executed simultaneously.
208
     *  2. Notifications when a task is manually executed.
209
     *  3. Tables are missing.
210
     */
211
    public function display_admin_notices()
212
    {
213
        global $wpdb;
214
        if ((is_a($this->store, 'ActionScheduler_HybridStore') || is_a($this->store, 'ActionScheduler_DBStore')) && apply_filters('action_scheduler_enable_recreate_data_store', true)) {
215
            $table_list = [
216
                'actionscheduler_actions',
217
                'actionscheduler_logs',
218
                'actionscheduler_groups',
219
                'actionscheduler_claims',
220
            ];
221
            $found_tables = $wpdb->get_col("SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
222
            foreach ($table_list as $table_name) {
223
                if (!in_array($wpdb->prefix.$table_name, $found_tables)) {
224
                    $this->admin_notices[] = [
225
                        'class' => 'error',
226
                        'message' => _x('It appears one or more database tables were missing. Attempting to re-create the missing table(s).', 'admin-text', 'site-reviews'),
227
                    ];
228
                    $this->recreate_tables();
229
                    parent::display_admin_notices();
230
                    return;
231
                }
232
            }
233
        }
234
        if ($this->runner->has_maximum_concurrent_batches()) {
235
            $claim_count = $this->store->get_claim_count();
236
            $this->admin_notices[] = [
237
                'class' => 'updated',
238
                'message' => sprintf(
239
                    /* translators: %s: amount of claims */
240
                    _n(
241
                        'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.',
242
                        'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.',
243
                        $claim_count,
244
                        'site-reviews'
245
                    ),
246
                    $claim_count
247
                ),
248
            ];
249
        } elseif ($this->store->has_pending_actions_due()) {
250
            $async_request_lock_expiration = \ActionScheduler::lock()->get_expiration('async-request-runner');
251
            // No lock set or lock expired
252
            if (false === $async_request_lock_expiration || $async_request_lock_expiration < time()) {
253
                $in_progress_url = add_query_arg('status', 'in-progress', remove_query_arg('status'));
254
                /* translators: %s: process URL */
255
                $async_request_message = sprintf(_x('A new queue has begun processing. <a href="%s">View actions in-progress &raquo;</a>', 'admin-text', 'site-reviews'), esc_url($in_progress_url));
256
            } else {
257
                /* translators: %d: seconds */
258
                $async_request_message = sprintf(_x('The next queue will begin processing in approximately %d seconds.', 'admin-text', 'site-reviews'), $async_request_lock_expiration - time());
259
            }
260
            $this->admin_notices[] = [
261
                'class' => 'notice notice-info is-dismissible',
262
                'message' => $async_request_message,
263
            ];
264
        }
265
        $notification = get_transient('action_scheduler_admin_notice');
266
        if (is_array($notification)) {
267
            delete_transient('action_scheduler_admin_notice');
268
            $action = $this->store->fetch_action($notification['action_id']);
269
            $action_hook_html = "<strong><code>{$action->get_hook()}</code></strong>";
270
            if (1 == $notification['success']) {
271
                $class = 'success';
272
                switch ($notification['row_action_type']) {
273
                    case 'run':
274
                        /* translators: %s: action HTML */
275
                        $action_message_html = sprintf(_x('Successfully executed action: %s', 'admin-text', 'site-reviews'), $action_hook_html);
276
                        break;
277
                    case 'cancel':
278
                        /* translators: %s: action HTML */
279
                        $action_message_html = sprintf(_x('Successfully canceled action: %s', 'admin-text', 'site-reviews'), $action_hook_html);
280
                        break;
281
                    case 'delete':
282
                        $action_message_html = _x('Successfully deleted action', 'admin-text', 'site-reviews');
283
                        break;
284
                    default:
285
                        /* translators: %s: action HTML */
286
                        $action_message_html = sprintf(_x('Successfully processed change for action: %s', 'admin-text', 'site-reviews'), $action_hook_html);
287
                        break;
288
                }
289
            } else {
290
                $class = 'error';
291
                /* translators: 1: action HTML 2: action ID 3: error message */
292
                $action_message_html = sprintf(_x('Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'admin-text', 'site-reviews'), $action_hook_html, esc_html($notification['action_id']), esc_html($notification['error_message']));
293
            }
294
            $action_message_html = apply_filters('action_scheduler_admin_notice_html', $action_message_html, $action, $notification);
295
            $this->admin_notices[] = [
296
                'class' => sprintf('notice notice-%s is-dismissible', $class),
297
                'message' => $action_message_html,
298
            ];
299
        }
300
        parent::display_admin_notices();
301
    }
302
303
    /**
304
     * Render the list table page, including header, notices, status filters and table.
305
     */
306
    public function display_page()
307
    {
308
        $this->process_bulk_action();
309
        $this->process_row_actions();
310
        $this->prepare_items();
311
        $this->display_admin_notices();
312
        $this->display_filter_by_status();
313
        $this->display_table();
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     */
319
    public function prepare_items()
320
    {
321
        $this->prepare_column_headers();
322
        $per_page = (int) $this->get_items_per_page($this->package.'_items_per_page', $this->items_per_page);
323
        $query = [
324
            'group' => glsr()->id,
325
            'per_page' => $per_page,
326
            'offset' => $this->get_items_offset(),
327
            'status' => $this->get_request_status(),
328
            'orderby' => $this->get_request_orderby(),
329
            'order' => $this->get_request_order(),
330
            'search' => $this->get_request_search_query(),
331
        ];
332
        $this->items = [];
333
        $total_items = (int) $this->store->query_actions($query, 'count');
334
        $status_labels = $this->store->get_status_labels();
335
        foreach ($this->store->query_actions($query) as $action_id) {
336
            try {
337
                $action = $this->store->fetch_action($action_id);
338
            } catch (\Exception $e) {
339
                continue;
340
            }
341
            if (is_a($action, 'ActionScheduler_NullAction')) {
342
                continue;
343
            }
344
            $this->items[$action_id] = [
345
                'ID' => $action_id,
346
                'hook' => $action->get_hook(),
347
                'status_name' => $this->store->get_status($action_id),
348
                'status' => $status_labels[$this->store->get_status($action_id)],
349
                'args' => $action->get_args(),
350
                'group' => $action->get_group(),
351
                'log_entries' => $this->logger->get_logs($action_id),
352
                'claim_id' => $this->store->get_claim_id($action_id),
353
                'recurrence' => $this->get_recurrence($action),
354
                'schedule' => $action->get_schedule(),
355
            ];
356
        }
357
        $this->set_pagination_args([
358
            'total_items' => $total_items,
359
            'per_page' => $per_page,
360
            'total_pages' => (int) ceil($total_items / $per_page),
361
        ]);
362
    }
363
364
    /**
365
     * Displays the search box.
366
     *
367
     * @param string $text     the 'submit' button label
368
     * @param string $input_id ID attribute value for the search input field
369
     */
370
    public function search_box($text, $input_id)
371
    {
372
        return; // hide the search box
373
    }
374
375
    /**
376
     * Generates content for a single row of the table.
377
     *
378
     * @param object|array $item The current item
379
     */
380
    public function single_row($item)
381
    {
382
        printf('<tr class="action-%s">', esc_attr($item['status_name']));
383
        $this->single_row_columns($item);
384
        echo '</tr>';
385
    }
386
387
    /**
388
     * Bulk delete.
389
     *
390
     * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data
391
     * properly validated by the callee and it will delete the actions without any extra validation.
392
     *
393
     * @param string $ids_sql Inherited and unused
394
     */
395
    protected function bulk_delete(array $ids, $ids_sql)
396
    {
397
        foreach ($ids as $action_id) {
398
            try {
399
                $this->store->delete_action((string) $action_id);
400
            } catch (\Exception $e) {
401
                glsr_log()->error($e->getMessage());
402
            }
403
        }
404
    }
405
406
    /**
407
     * Prints the available statuses so the user can click to filter.
408
     */
409
    protected function display_filter_by_status()
410
    {
411
        global $wpdb;
412
        $sql = "
413
            SELECT a.status, count(a.status) as 'count'
414
            FROM {$wpdb->actionscheduler_actions} a 
415
            INNER JOIN {$wpdb->actionscheduler_groups} g ON (g.group_id = a.group_id)
416
            WHERE g.slug = %s
417
            GROUP BY a.status
418
        ";
419
        $this->status_counts = [];
420
        $action_stati_and_labels = $this->store->get_status_labels();
421
        $results = $wpdb->get_results($wpdb->prepare($sql, glsr()->id));
422
        foreach ($results as $action_data) {
423
            if (array_key_exists($action_data->status, $action_stati_and_labels)) {
424
                $this->status_counts[$action_data->status] = $action_data->count;
425
            }
426
        }
427
        parent::display_filter_by_status();
428
    }
429
430
    /**
431
     * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
432
     *
433
     * @return string
434
     */
435
    protected function get_log_entry_html(\ActionScheduler_LogEntry $log_entry, \DateTimeZone $timezone)
436
    {
437
        $date = $log_entry->get_date();
438
        $date->setTimezone($timezone);
439
        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()));
440
    }
441
442
    /**
443
     * Returns the recurrence of an action or 'Non-repeating'. The output is human readable.
444
     *
445
     * @param \ActionScheduler_Action $action
446
     *
447
     * @return string
448
     */
449
    protected function get_recurrence($action)
450
    {
451
        $schedule = $action->get_schedule();
452
        if (!$schedule->is_recurring()) {
453
            return _x('Non-repeating', 'admin-text', 'site-reviews');
454
        }
455
        $recurrence = $schedule->get_recurrence();
456
        if (is_numeric($recurrence)) {
457
            return sprintf(_x('Every %s', '%s: time interval (admin-text)', 'site-reviews'), glsr(Date::class)->interval($recurrence));
458
        }
459
        return $recurrence;
460
    }
461
462
    /**
463
     * Return the sortable column order specified for this request.
464
     *
465
     * @return string
466
     */
467
    protected function get_request_order()
468
    {
469
        $order = strtolower((string) filter_input(INPUT_GET, 'order'));
470
        if ('desc' === $order) {
471
            return 'DESC';
472
        }
473
        if (empty($order) && 'schedule' === $this->get_request_orderby()) {
474
            return 'DESC';
475
        }
476
        return 'ASC';
477
    }
478
479
    /**
480
     * Querystring arguments to persist between form submissions.
481
     *
482
     * @since 3.7.3
483
     *
484
     * @return string[]
485
     */
486
    protected function get_request_query_args_to_persist()
487
    {
488
        return array_merge($this->sort_by, [
489
            'page',
490
            'post_type',
491
            'status',
492
            'tab',
493
        ]);
494
    }
495
496
    /**
497
     * Get the scheduled date in a human friendly format.
498
     *
499
     * @return string
500
     */
501
    protected function get_schedule_display_string(\ActionScheduler_Schedule $schedule)
502
    {
503
        $schedule_display_string = '';
504
        if (!$schedule->get_date()) {
505
            return '0000-00-00 00:00:00';
506
        }
507
        $next_timestamp = $schedule->get_date()->getTimestamp();
508
        $schedule_display_string .= $schedule->get_date()->format('Y-m-d H:i:s O');
509
        $schedule_display_string .= '<br/>';
510
        if (gmdate('U') > $next_timestamp) {
511
            $schedule_display_string .= sprintf(' (%s)', glsr(Date::class)->interval(gmdate('U') - $next_timestamp, 'past'));
512
        } else {
513
            $schedule_display_string .= sprintf(' (%s)', glsr(Date::class)->interval($next_timestamp - gmdate('U')));
514
        }
515
        return $schedule_display_string;
516
    }
517
518
    /**
519
     * Get the text to display in the search box on the list table.
520
     */
521
    protected function get_search_box_button_text()
522
    {
523
        return ''; // we are not displaying the search box
524
    }
525
526
    /**
527
     * Only display row actions for pending actions.
528
     *
529
     * @param array  $row         Row to render
530
     * @param string $column_name Current row
531
     *
532
     * @return string
533
     */
534
    protected function maybe_render_actions($row, $column_name)
535
    {
536
        $actions = $this->row_actions_all;
537
        $status = strtolower($row['status_name']);
538
        switch ($status) {
539
            case \ActionScheduler_Store::STATUS_CANCELED:
540
            case \ActionScheduler_Store::STATUS_RUNNING:
541
                return '';
542
            case \ActionScheduler_Store::STATUS_COMPLETE:
543
                unset($actions['hook']['cancel']);
544
                unset($actions['hook']['retry']);
545
                unset($actions['hook']['run']);
546
                break;
547
            case \ActionScheduler_Store::STATUS_FAILED:
548
                unset($actions['hook']['cancel']);
549
                unset($actions['hook']['run']);
550
                break;
551
            case \ActionScheduler_Store::STATUS_PENDING:
552
                unset($actions['hook']['delete']);
553
                unset($actions['hook']['retry']);
554
                break;
555
        }
556
        foreach ($actions as $key => $action) {
557
            $action['link'] = add_query_arg([
558
                'nonce' => wp_create_nonce("{$key}::{$row[$this->ID]}"),
559
                'row_action' => $key,
560
                'row_id' => $row[$this->ID],
561
                'tab' => 'scheduled', // ensure correct tab on redirect!
562
            ]);
563
        }
564
        $this->row_actions = $actions;
565
        return parent::maybe_render_actions($row, $column_name);
566
    }
567
568
    /**
569
     * Implements the logic behind processing an action once an action link is clicked on the list table.
570
     *
571
     * @param int    $action_id
572
     * @param string $row_action_type the type of action to perform on the action
573
     */
574
    protected function process_row_action($action_id, $row_action_type)
575
    {
576
        try {
577
            switch ($row_action_type) {
578
                case 'cancel':
579
                    $this->store->cancel_action((string) $action_id);
580
                    break;
581
                case 'delete':
582
                    $this->store->delete_action((string) $action_id);
583
                    break;
584
                case 'retry':
585
                    $action = $this->store->fetch_action((string) $action_id);
586
                    // don't use Queue because we want to keep the original hook
587
                    as_schedule_single_action(time(), $action->get_hook(), $action->get_args(), glsr()->id);
588
                    $this->store->delete_action((string) $action_id);
589
                    break;
590
                case 'run':
591
                    $this->runner->process_action($action_id, 'Admin List Table');
592
                    break;
593
            }
594
            $success = 1;
595
            $error_message = '';
596
        } catch (\Exception $e) {
597
            $success = 0;
598
            $error_message = $e->getMessage();
599
        }
600
        set_transient('action_scheduler_admin_notice', compact('action_id', 'success', 'error_message', 'row_action_type'), 30);
601
    }
602
603
    /**
604
     * Force the data store schema updates.
605
     */
606
    protected function recreate_tables()
607
    {
608
        if (is_a($this->store, 'ActionScheduler_HybridStore')) {
609
            $store = $this->store;
610
        } else {
611
            $store = new \ActionScheduler_HybridStore();
612
        }
613
        add_action('action_scheduler/created_table', [$store, 'set_autoincrement'], 10, 2);
614
        $store_schema = new \ActionScheduler_StoreSchema();
615
        $logger_schema = new \ActionScheduler_LoggerSchema();
616
        $store_schema->register_tables(true);
617
        $logger_schema->register_tables(true);
618
        remove_action('action_scheduler/created_table', [$store, 'set_autoincrement'], 10);
619
    }
620
621
    /**
622
     * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
623
     * parameters are valid.
624
     *
625
     * @param int $action_id
626
     */
627
    protected function row_action_cancel($action_id)
628
    {
629
        $this->process_row_action($action_id, 'cancel');
630
    }
631
632
    /**
633
     * @param int $action_id
634
     */
635
    protected function row_action_delete($action_id)
636
    {
637
        $this->process_row_action($action_id, 'delete');
638
    }
639
640
    /**
641
     * @param int $action_id
642
     */
643
    protected function row_action_retry($action_id)
644
    {
645
        $this->process_row_action($action_id, 'retry');
646
    }
647
648
    /**
649
     * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
650
     * parameters are valid.
651
     *
652
     * @param int $action_id
653
     */
654
    protected function row_action_run($action_id)
655
    {
656
        $this->process_row_action($action_id, 'run');
657
    }
658
}
659