Test Failed
Push — 135-map-multiple-wordpress-obj... ( 3634c2...bb35fb )
by Jonathan
12:00
created

ActionScheduler_ListTable   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 527
Duplicated Lines 0.95 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 5
loc 527
rs 8.72
c 0
b 0
f 0
wmc 46
lcom 1
cbo 7

17 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 94 3
B human_interval() 0 24 7
A get_recurrence() 0 14 4
A column_args() 0 13 3
A column_log_entries() 0 14 2
A get_log_entry_html() 0 5 1
A maybe_render_actions() 0 7 2
B display_admin_notices() 0 44 6
A column_schedule() 0 3 1
A get_schedule_display_string() 0 21 3
A bulk_delete() 0 5 2
A row_action_cancel() 0 3 1
A row_action_run() 0 3 1
A process_row_action() 0 19 4
A prepare_items() 5 54 4
A display_filter_by_status() 0 4 1
A get_search_box_button_text() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ActionScheduler_ListTable often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ActionScheduler_ListTable, and based on these observations, apply Extract Interface, too.

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
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', '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
				'names'   => _n_noop( '%s year', '%s years', 'action-scheduler' ),
144
			),
145
			array(
146
				'seconds' => MONTH_IN_SECONDS,
147
				'names'   => _n_noop( '%s month', '%s months', 'action-scheduler' ),
148
			),
149
			array(
150
				'seconds' => WEEK_IN_SECONDS,
151
				'names'   => _n_noop( '%s week', '%s weeks', 'action-scheduler' ),
152
			),
153
			array(
154
				'seconds' => DAY_IN_SECONDS,
155
				'names'   => _n_noop( '%s day', '%s days', 'action-scheduler' ),
156
			),
157
			array(
158
				'seconds' => HOUR_IN_SECONDS,
159
				'names'   => _n_noop( '%s hour', '%s hours', 'action-scheduler' ),
160
			),
161
			array(
162
				'seconds' => MINUTE_IN_SECONDS,
163
				'names'   => _n_noop( '%s minute', '%s minutes', 'action-scheduler' ),
164
			),
165
			array(
166
				'seconds' => 1,
167
				'names'   => _n_noop( '%s second', '%s seconds', 'action-scheduler' ),
168
			),
169
		);
170
171
		parent::__construct( array(
172
			'singular' => 'action-scheduler',
173
			'plural'   => 'action-scheduler',
174
			'ajax'     => false,
175
		) );
176
	}
177
178
	/**
179
	 * Convert an interval of seconds into a two part human friendly string.
180
	 *
181
	 * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning
182
	 * even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step
183
	 * further to display two degrees of accuracy.
184
	 *
185
	 * Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/
186
	 *
187
	 * @param int $interval A interval in seconds.
188
	 * @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.
189
	 * @return string A human friendly string representation of the interval.
190
	 */
191
	private static function human_interval( $interval, $periods_to_include = 2 ) {
192
193
		if ( $interval <= 0 ) {
194
			return __( 'Now!', 'action-scheduler' );
195
		}
196
197
		$output = '';
198
199
		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++ ) {
200
201
			$periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] );
202
203
			if ( $periods_in_interval > 0 ) {
204
				if ( ! empty( $output ) ) {
205
					$output .= ' ';
206
				}
207
				$output .= sprintf( _n( self::$time_periods[ $time_period_index ]['names'][0], self::$time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'action-scheduler' ), $periods_in_interval );
0 ignored issues
show
introduced by
Expected next thing to be a escaping function, not 'self'
Loading history...
208
				$seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds'];
209
				$periods_included++;
210
			}
211
		}
212
213
		return $output;
214
	}
215
216
	/**
217
	 * Returns the recurrence of an action or 'Non-repeating'. The output is human readable.
218
	 *
219
	 * @param ActionScheduler_Action $action
220
	 *
221
	 * @return string
222
	 */
223
	protected function get_recurrence( $action ) {
224
		$recurrence = $action->get_schedule();
225
		if ( $recurrence->is_recurring() ) {
226
			if ( method_exists( $recurrence, 'interval_in_seconds' ) ) {
227
				return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence->interval_in_seconds() ) );
228
			}
229
230
			if ( method_exists( $recurrence, 'get_recurrence' ) ) {
231
				return sprintf( __( 'Cron %s', 'action-scheduler' ), $recurrence->get_recurrence() );
232
			}
233
		}
234
235
		return __( 'Non-repeating', 'action-scheduler' );
236
	}
237
238
	/**
239
	 * Serializes the argument of an action to render it in a human friendly format.
240
	 *
241
	 * @param array $row The array representation of the current row of the table
242
	 *
243
	 * @return string
244
	 */
245
	public function column_args( array $row ) {
246
		if ( empty( $row['args'] ) ) {
247
			return '';
248
		}
249
250
		$row_html = '<ul>';
251
		foreach ( $row['args'] as $key => $value ) {
252
			$row_html .= sprintf( '<li><code>%s => %s</code></li>', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) );
253
		}
254
		$row_html .= '</ul>';
255
256
		return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row );
257
	}
258
259
	/**
260
	 * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
261
	 *
262
	 * @param array $row Action array.
263
	 * @return string
264
	 */
265
	public function column_log_entries( array $row ) {
266
267
		$log_entries_html = '<ol>';
268
269
		$timezone = new DateTimezone( 'UTC' );
270
271
		foreach ( $row['log_entries'] as $log_entry ) {
272
			$log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone );
273
		}
274
275
		$log_entries_html .= '</ol>';
276
277
		return $log_entries_html;
278
	}
279
280
	/**
281
	 * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal.
282
	 *
283
	 * @param ActionScheduler_LogEntry $log_entry
284
	 * @param DateTimezone $timezone
285
	 * @return string
286
	 */
287
	protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
288
		$date = $log_entry->get_date();
289
		$date->setTimezone( $timezone );
290
		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() ) );
291
	}
292
293
	/**
294
	 * Only display row actions for pending actions.
295
	 *
296
	 * @param array  $row         Row to render
297
	 * @param string $column_name Current row
298
	 *
299
	 * @return string
300
	 */
301
	protected function maybe_render_actions( $row, $column_name ) {
302
		if ( 'pending' === strtolower( $row['status'] ) ) {
303
			return parent::maybe_render_actions( $row, $column_name );
304
		}
305
306
		return '';
307
	}
308
309
	/**
310
	 * Renders admin notifications
311
	 *
312
	 * Notifications:
313
	 *  1. When the maximum number of tasks are being executed simultaneously
314
	 *  2. Notifications when a task us manually executed
315
	 */
316
	public function display_admin_notices() {
317
318
		if ( $this->store->get_claim_count() >= $this->runner->get_allowed_concurrent_batches() ) {
319
			$this->admin_notices[] = array(
320
				'class'   => 'updated',
321
				'message' => sprintf( __( 'Maximum simultaneous batches already in progress (%s queues). No actions will be processed until the current batches are complete.', 'action-scheduler' ), $this->store->get_claim_count() ),
322
			);
323
		}
324
325
		$notification = get_transient( 'action_scheduler_admin_notice' );
326
327
		if ( is_array( $notification ) ) {
328
			delete_transient( 'action_scheduler_admin_notice' );
329
330
			$action = $this->store->fetch_action( $notification['action_id'] );
331
			$action_hook_html = '<strong><code>' . $action->get_hook() . '</code></strong>';
332
			if ( 1 == $notification['success'] ) {
333
				$class = 'updated';
334
				switch ( $notification['row_action_type'] ) {
335
					case 'run' :
336
						$action_message_html = sprintf( __( 'Successfully executed action: %s', 'action-scheduler' ), $action_hook_html );
337
						break;
338
					case 'cancel' :
339
						$action_message_html = sprintf( __( 'Successfully canceled action: %s', 'action-scheduler' ), $action_hook_html );
340
						break;
341
					default :
342
						$action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'action-scheduler' ), $action_hook_html );
343
						break;
344
				}
345
			} else {
346
				$class = 'error';
347
				$action_message_html = sprintf( __( 'Could not process change for action: "%s" (ID: %d). Error: %s', 'action-scheduler' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) );
348
			}
349
350
			$action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification );
351
352
			$this->admin_notices[] = array(
353
				'class'   => $class,
354
				'message' => $action_message_html,
355
			);
356
		}
357
358
		parent::display_admin_notices();
359
	}
360
361
	/**
362
	 * Prints the scheduled date in a human friendly format.
363
	 *
364
	 * @param array $row The array representation of the current row of the table
365
	 *
366
	 * @return string
367
	 */
368
	public function column_schedule( $row ) {
369
		return $this->get_schedule_display_string( $row['schedule'] );
370
	}
371
372
	/**
373
	 * Get the scheduled date in a human friendly format.
374
	 *
375
	 * @param ActionScheduler_Schedule $schedule
376
	 * @return string
377
	 */
378
	protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) {
379
380
		$schedule_display_string = '';
381
382
		if ( ! $schedule->next() ) {
383
			return $schedule_display_string;
384
		}
385
386
		$next_timestamp = $schedule->next()->getTimestamp();
387
388
		$schedule_display_string .= $schedule->next()->format( 'Y-m-d H:i:s O' );
389
		$schedule_display_string .= '<br/>';
390
391
		if ( gmdate( 'U' ) > $next_timestamp ) {
392
			$schedule_display_string .= sprintf( __( ' (%s ago)', 'action-scheduler' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) );
393
		} else {
394
			$schedule_display_string .= sprintf( __( ' (%s)', 'action-scheduler' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) );
395
		}
396
397
		return $schedule_display_string;
398
	}
399
400
	/**
401
	 * Bulk delete
402
	 *
403
	 * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data
404
	 * properly validated by the callee and it will delete the actions without any extra validation.
405
	 *
406
	 * @param array $ids
407
	 * @param string $ids_sql Inherited and unused
408
	 */
409
	protected function bulk_delete( array $ids, $ids_sql ) {
410
		foreach ( $ids as $id ) {
411
			$this->store->delete_action( $id );
412
		}
413
	}
414
415
	/**
416
	 * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
417
	 * parameters are valid.
418
	 *
419
	 * @param int $action_id
420
	 */
421
	protected function row_action_cancel( $action_id ) {
422
		$this->process_row_action( $action_id, 'cancel' );
423
	}
424
425
	/**
426
	 * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their
427
	 * parameters are valid.
428
	 *
429
	 * @param int $action_id
430
	 */
431
	protected function row_action_run( $action_id ) {
432
		$this->process_row_action( $action_id, 'run' );
433
	}
434
435
	/**
436
	 * Implements the logic behind processing an action once an action link is clicked on the list table.
437
	 *
438
	 * @param int $action_id
439
	 * @param string $row_action_type The type of action to perform on the action.
440
	 */
441
	protected function process_row_action( $action_id, $row_action_type ) {
442
		try {
443
			switch ( $row_action_type ) {
444
				case 'run' :
445
					$this->runner->process_action( $action_id );
446
					break;
447
				case 'cancel' :
448
					$this->store->cancel_action( $action_id );
449
					break;
450
			}
451
			$success = 1;
452
			$error_message = '';
453
		} catch ( Exception $e ) {
454
			$success = 0;
455
			$error_message = $e->getMessage();
456
		}
457
458
		set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 );
459
	}
460
461
	/**
462
	 * {@inheritDoc}
463
	 */
464
	public function prepare_items() {
465
		$this->process_bulk_action();
466
467
		$this->process_row_actions();
468
469 View Code Duplication
		if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
470
			// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
471
			wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
0 ignored issues
show
introduced by
Detected usage of a non-validated input variable: $_SERVER
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_SERVER
Loading history...
472
			exit;
473
		}
474
475
		$this->prepare_column_headers();
476
477
		$per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
478
		$query = array(
479
			'per_page' => $per_page,
480
			'offset'   => $this->get_items_offset(),
481
			'status'   => $this->get_request_status(),
482
			'orderby'  => $this->get_request_orderby(),
483
			'order'    => $this->get_request_order(),
484
			'search'   => $this->get_request_search_query(),
485
		);
486
487
		$this->items = array();
488
489
		$total_items = $this->store->query_actions( $query, 'count' );
0 ignored issues
show
Unused Code introduced by
The call to ActionScheduler_Store::query_actions() has too many arguments starting with 'count'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
490
491
		$status_labels = $this->store->get_status_labels();
492
493
		foreach ( $this->store->query_actions( $query ) as $action_id ) {
494
			try {
495
				$action = $this->store->fetch_action( $action_id );
496
			} catch ( Exception $e ) {
497
				continue;
498
			}
499
			$this->items[ $action_id ] = array(
500
				'ID'          => $action_id,
501
				'hook'        => $action->get_hook(),
502
				'status'      => $status_labels[ $this->store->get_status( $action_id ) ],
503
				'args'        => $action->get_args(),
504
				'group'       => $action->get_group(),
505
				'log_entries' => $this->logger->get_logs( $action_id ),
506
				'claim_id'    => $this->store->get_claim_id( $action_id ),
507
				'recurrence'  => $this->get_recurrence( $action ),
508
				'schedule'    => $action->get_schedule(),
509
			);
510
		}
511
512
		$this->set_pagination_args( array(
513
			'total_items' => $total_items,
514
			'per_page'    => $per_page,
515
			'total_pages' => ceil( $total_items / $per_page ),
516
		) );
517
	}
518
519
	/**
520
	 * Prints the available statuses so the user can click to filter.
521
	 */
522
	protected function display_filter_by_status() {
523
		$this->status_counts = $this->store->action_counts();
524
		parent::display_filter_by_status();
525
	}
526
527
	/**
528
	 * Get the text to display in the search box on the list table.
529
	 */
530
	protected function get_search_box_button_text() {
531
		return __( 'Search hook, args and claim ID', 'action-scheduler' );
532
	}
533
}
534