Passed
Push — master ( 495efb...ace576 )
by Brian
06:14 queued 01:46
created

ActionScheduler_Abstract_ListTable   F

Complexity

Total Complexity 94

Size/Duplication

Total Lines 649
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 226
c 1
b 0
f 0
dl 0
loc 649
rs 2
wmc 94

32 Methods

Rating   Name   Duplication   Size   Complexity  
A get_request_orderby() 0 11 3
A get_search_box_placeholder() 0 2 1
A process_row_actions() 0 19 5
A bulk_delete() 0 4 2
A display_admin_notices() 0 5 2
A get_bulk_actions() 0 12 3
A column_cb() 0 2 1
B display_filter_by_status() 0 31 9
A get_columns() 0 7 1
A get_items_query_limit() 0 5 1
A process_bulk_action() 0 21 6
B maybe_render_actions() 0 27 7
A prepare_column_headers() 0 5 1
A get_request_order() 0 9 3
A get_table_columns() 0 7 2
A get_request_status() 0 3 2
A display_table() 0 13 5
B extra_tablenav() 0 26 8
A prepare_items() 0 41 3
A display_header() 0 7 2
A get_items_offset() 0 10 2
A get_items_query_order() 0 9 2
A get_items_query_search() 0 12 4
A column_default() 0 4 1
A get_request_search_query() 0 3 2
A process_actions() 0 8 2
A get_sortable_columns() 0 6 2
A set_items() 0 4 2
A display_page() 0 9 1
A translate() 0 2 1
B get_items_query_filters() 0 18 7
A get_items_query_offset() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ActionScheduler_Abstract_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.

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_Abstract_ListTable, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
if ( ! class_exists( 'WP_List_Table' ) ) {
4
	require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
5
}
6
7
/**
8
 * Action Scheduler Abstract List Table class
9
 *
10
 * This abstract class enhances WP_List_Table making it ready to use.
11
 *
12
 * By extending this class we can focus on describing how our table looks like,
13
 * which columns needs to be shown, filter, ordered by and more and forget about the details.
14
 *
15
 * This class supports:
16
 *	- Bulk actions
17
 *	- Search
18
 *  - Sortable columns
19
 *  - Automatic translations of the columns
20
 *
21
 * @codeCoverageIgnore
22
 * @since  2.0.0
23
 */
24
abstract class ActionScheduler_Abstract_ListTable extends WP_List_Table {
25
26
	/**
27
	 * The table name
28
	 */
29
	protected $table_name;
30
31
	/**
32
	 * Package name, used to get options from WP_List_Table::get_items_per_page.
33
	 */
34
	protected $package;
35
36
	/**
37
	 * How many items do we render per page?
38
	 */
39
	protected $items_per_page = 10;
40
41
	/**
42
	 * Enables search in this table listing. If this array
43
	 * is empty it means the listing is not searchable.
44
	 */
45
	protected $search_by = array();
46
47
	/**
48
	 * Columns to show in the table listing. It is a key => value pair. The
49
	 * key must much the table column name and the value is the label, which is
50
	 * automatically translated.
51
	 */
52
	protected $columns = array();
53
54
	/**
55
	 * Defines the row-actions. It expects an array where the key
56
	 * is the column name and the value is an array of actions.
57
	 *
58
	 * The array of actions are key => value, where key is the method name
59
	 * (with the prefix row_action_<key>) and the value is the label
60
	 * and title.
61
	 */
62
	protected $row_actions = array();
63
64
	/**
65
	 * The Primary key of our table
66
	 */
67
	protected $ID = 'ID';
68
69
	/**
70
	 * Enables sorting, it expects an array
71
	 * of columns (the column names are the values)
72
	 */
73
	protected $sort_by = array();
74
75
	protected $filter_by = array();
76
77
	/**
78
	 * @var array The status name => count combinations for this table's items. Used to display status filters.
79
	 */
80
	protected $status_counts = array();
81
82
	/**
83
	 * @var array Notices to display when loading the table. Array of arrays of form array( 'class' => {updated|error}, 'message' => 'This is the notice text display.' ).
84
	 */
85
	protected $admin_notices = array();
86
87
	/**
88
	 * @var string Localised string displayed in the <h1> element above the able.
89
	 */
90
	protected $table_header;
91
92
	/**
93
	 * Enables bulk actions. It must be an array where the key is the action name
94
	 * and the value is the label (which is translated automatically). It is important
95
	 * to notice that it will check that the method exists (`bulk_$name`) and will throw
96
	 * an exception if it does not exists.
97
	 *
98
	 * This class will automatically check if the current request has a bulk action, will do the
99
	 * validations and afterwards will execute the bulk method, with two arguments. The first argument
100
	 * is the array with primary keys, the second argument is a string with a list of the primary keys,
101
	 * escaped and ready to use (with `IN`).
102
	 */
103
	protected $bulk_actions = array();
104
105
	/**
106
	 * Makes translation easier, it basically just wraps
107
	 * `_x` with some default (the package name).
108
	 * 
109
	 * @deprecated 3.0.0
110
	 */
111
	protected function translate( $text, $context = '' ) {
112
		return $text;
113
	}
114
115
	/**
116
	 * Reads `$this->bulk_actions` and returns an array that WP_List_Table understands. It
117
	 * also validates that the bulk method handler exists. It throws an exception because
118
	 * this is a library meant for developers and missing a bulk method is a development-time error.
119
	 */
120
	protected function get_bulk_actions() {
121
		$actions = array();
122
123
		foreach ( $this->bulk_actions as $action => $label ) {
124
			if ( ! is_callable( array( $this, 'bulk_' . $action ) ) ) {
125
				throw new RuntimeException( "The bulk action $action does not have a callback method" );
126
			}
127
128
			$actions[ $action ] = $label;
129
		}
130
131
		return $actions;
132
	}
133
134
	/**
135
	 * Checks if the current request has a bulk action. If that is the case it will validate and will
136
	 * execute the bulk method handler. Regardless if the action is valid or not it will redirect to
137
	 * the previous page removing the current arguments that makes this request a bulk action.
138
	 */
139
	protected function process_bulk_action() {
140
		global $wpdb;
141
		// Detect when a bulk action is being triggered.
142
		$action = $this->current_action();
143
		if ( ! $action ) {
144
			return;
145
		}
146
147
		check_admin_referer( 'bulk-' . $this->_args['plural'] );
148
149
		$method   = 'bulk_' . $action;
150
		if ( array_key_exists( $action, $this->bulk_actions ) && is_callable( array( $this, $method ) ) && ! empty( $_GET['ID'] ) && is_array( $_GET['ID'] ) ) {
151
			$ids_sql = '(' . implode( ',', array_fill( 0, count( $_GET['ID'] ), '%s' ) ) . ')';
152
			$this->$method( $_GET['ID'], $wpdb->prepare( $ids_sql, $_GET['ID'] ) );
0 ignored issues
show
Security Code Execution introduced by
$method can contain request data and is used in code execution context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_REQUEST, and $_REQUEST['action'] is returned in wordpress/wp-admin/includes/class-wp-list-table.php on line 493
  1. Read from $_REQUEST, and $_REQUEST['action'] is returned
    in wordpress/wp-admin/includes/class-wp-list-table.php on line 493
  2. $this->current_action() is assigned to $action
    in includes/libraries/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php on line 142
  3. 'bulk_' . $action is assigned to $method
    in includes/libraries/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php on line 149
  2. Path: Read from $_REQUEST, and $_REQUEST['action2'] is returned in wordpress/wp-admin/includes/class-wp-list-table.php on line 497
  1. Read from $_REQUEST, and $_REQUEST['action2'] is returned
    in wordpress/wp-admin/includes/class-wp-list-table.php on line 497
  2. $this->current_action() is assigned to $action
    in includes/libraries/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php on line 142
  3. 'bulk_' . $action is assigned to $method
    in includes/libraries/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php on line 149

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
153
		}
154
155
		wp_redirect( remove_query_arg(
156
			array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ),
157
			wp_unslash( $_SERVER['REQUEST_URI'] )
158
		) );
159
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
160
	}
161
162
	/**
163
	 * Default code for deleting entries.
164
	 * validated already by process_bulk_action()
165
	 */
166
	protected function bulk_delete( array $ids, $ids_sql ) {
167
		$store = ActionScheduler::store();
168
		foreach ( $ids as $action_id ) {
169
			$store->delete( $action_id );
0 ignored issues
show
Bug introduced by
The method delete() does not exist on ActionScheduler_Store. Did you maybe mean delete_action()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
			$store->/** @scrutinizer ignore-call */ 
170
           delete( $action_id );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
170
		}
171
	}
172
173
	/**
174
	 * Prepares the _column_headers property which is used by WP_Table_List at rendering.
175
	 * It merges the columns and the sortable columns.
176
	 */
177
	protected function prepare_column_headers() {
178
		$this->_column_headers = array(
179
			$this->get_columns(),
180
			array(),
181
			$this->get_sortable_columns(),
182
		);
183
	}
184
185
	/**
186
	 * Reads $this->sort_by and returns the columns name in a format that WP_Table_List
187
	 * expects
188
	 */
189
	public function get_sortable_columns() {
190
		$sort_by = array();
191
		foreach ( $this->sort_by as $column ) {
192
			$sort_by[ $column ] = array( $column, true );
193
		}
194
		return $sort_by;
195
	}
196
197
	/**
198
	 * Returns the columns names for rendering. It adds a checkbox for selecting everything
199
	 * as the first column
200
	 */
201
	public function get_columns() {
202
		$columns = array_merge(
203
			array( 'cb' => '<input type="checkbox" />' ),
204
			$this->columns
205
		);
206
207
		return $columns;
208
	}
209
210
	/**
211
	 * Get prepared LIMIT clause for items query
212
	 *
213
	 * @global wpdb $wpdb
214
	 *
215
	 * @return string Prepared LIMIT clause for items query.
216
	 */
217
	protected function get_items_query_limit() {
218
		global $wpdb;
219
220
		$per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
221
		return $wpdb->prepare( 'LIMIT %d', $per_page );
222
	}
223
224
	/**
225
	 * Returns the number of items to offset/skip for this current view.
226
	 *
227
	 * @return int
228
	 */
229
	protected function get_items_offset() {
230
		$per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
231
		$current_page = $this->get_pagenum();
232
		if ( 1 < $current_page ) {
233
			$offset = $per_page * ( $current_page - 1 );
234
		} else {
235
			$offset = 0;
236
		}
237
238
		return $offset;
239
	}
240
241
	/**
242
	 * Get prepared OFFSET clause for items query
243
	 *
244
	 * @global wpdb $wpdb
245
	 *
246
	 * @return string Prepared OFFSET clause for items query.
247
	 */
248
	protected function get_items_query_offset() {
249
		global $wpdb;
250
251
		return $wpdb->prepare( 'OFFSET %d', $this->get_items_offset() );
252
	}
253
254
	/**
255
	 * Prepares the ORDER BY sql statement. It uses `$this->sort_by` to know which
256
	 * columns are sortable. This requests validates the orderby $_GET parameter is a valid
257
	 * column and sortable. It will also use order (ASC|DESC) using DESC by default.
258
	 */
259
	protected function get_items_query_order() {
260
		if ( empty( $this->sort_by ) ) {
261
			return '';
262
		}
263
264
		$orderby = esc_sql( $this->get_request_orderby() );
265
		$order   = esc_sql( $this->get_request_order() );
266
267
		return "ORDER BY {$orderby} {$order}";
268
	}
269
270
	/**
271
	 * Return the sortable column specified for this request to order the results by, if any.
272
	 *
273
	 * @return string
274
	 */
275
	protected function get_request_orderby() {
276
277
		$valid_sortable_columns = array_values( $this->sort_by );
278
279
		if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns ) ) {
280
			$orderby = sanitize_text_field( $_GET['orderby'] );
281
		} else {
282
			$orderby = $valid_sortable_columns[0];
283
		}
284
285
		return $orderby;
286
	}
287
288
	/**
289
	 * Return the sortable column order specified for this request.
290
	 *
291
	 * @return string
292
	 */
293
	protected function get_request_order() {
294
295
		if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( $_GET['order'] ) ) {
296
			$order = 'DESC';
297
		} else {
298
			$order = 'ASC';
299
		}
300
301
		return $order;
302
	}
303
304
	/**
305
	 * Return the status filter for this request, if any.
306
	 *
307
	 * @return string
308
	 */
309
	protected function get_request_status() {
310
		$status = ( ! empty( $_GET['status'] ) ) ? $_GET['status'] : '';
311
		return $status;
312
	}
313
314
	/**
315
	 * Return the search filter for this request, if any.
316
	 *
317
	 * @return string
318
	 */
319
	protected function get_request_search_query() {
320
		$search_query = ( ! empty( $_GET['s'] ) ) ? $_GET['s'] : '';
321
		return $search_query;
322
	}
323
324
	/**
325
	 * Process and return the columns name. This is meant for using with SQL, this means it
326
	 * always includes the primary key.
327
	 *
328
	 * @return array
329
	 */
330
	protected function get_table_columns() {
331
		$columns = array_keys( $this->columns );
332
		if ( ! in_array( $this->ID, $columns ) ) {
333
			$columns[] = $this->ID;
334
		}
335
336
		return $columns;
337
	}
338
339
	/**
340
	 * Check if the current request is doing a "full text" search. If that is the case
341
	 * prepares the SQL to search texts using LIKE.
342
	 *
343
	 * If the current request does not have any search or if this list table does not support
344
	 * that feature it will return an empty string.
345
	 *
346
	 * TODO:
347
	 *   - Improve search doing LIKE by word rather than by phrases.
348
	 *
349
	 * @return string
350
	 */
351
	protected function get_items_query_search() {
352
		global $wpdb;
353
354
		if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) {
355
			return '';
356
		}
357
358
		$filter  = array();
359
		foreach ( $this->search_by as $column ) {
360
			$filter[] = $wpdb->prepare('`' . $column . '` like "%%s%"', $wpdb->esc_like( $_GET['s'] ));
361
		}
362
		return implode( ' OR ', $filter );
363
	}
364
365
	/**
366
	 * Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting
367
	 * any data sent by the user it validates that it is a valid option.
368
	 */
369
	protected function get_items_query_filters() {
370
		global $wpdb;
371
372
		if ( ! $this->filter_by || empty( $_GET['filter_by'] ) || ! is_array( $_GET['filter_by'] ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->filter_by of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
373
			return '';
374
		}
375
376
		$filter = array();
377
378
		foreach ( $this->filter_by as $column => $options ) {
379
			if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) {
380
				continue;
381
			}
382
383
			$filter[] = $wpdb->prepare( "`$column` = %s", $_GET['filter_by'][ $column ] );
384
		}
385
386
		return implode( ' AND ', $filter );
387
388
	}
389
390
	/**
391
	 * Prepares the data to feed WP_Table_List.
392
	 *
393
	 * This has the core for selecting, sorting and filting data. To keep the code simple
394
	 * its logic is split among many methods (get_items_query_*).
395
	 *
396
	 * Beside populating the items this function will also count all the records that matches
397
	 * the filtering criteria and will do fill the pagination variables.
398
	 */
399
	public function prepare_items() {
400
		global $wpdb;
401
402
		$this->process_bulk_action();
403
404
		$this->process_row_actions();
405
406
		if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) {
407
			// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
408
			wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
409
			exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
410
		}
411
412
		$this->prepare_column_headers();
413
414
		$limit   = $this->get_items_query_limit();
415
		$offset  = $this->get_items_query_offset();
416
		$order   = $this->get_items_query_order();
417
		$where   = array_filter(array(
418
			$this->get_items_query_search(),
419
			$this->get_items_query_filters(),
420
		));
421
		$columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`';
422
423
		if ( ! empty( $where ) ) {
424
			$where = 'WHERE ('. implode( ') AND (', $where ) . ')';
425
		} else {
426
			$where = '';
427
		}
428
429
		$sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}";
430
431
		$this->set_items( $wpdb->get_results( $sql, ARRAY_A ) );
432
433
		$query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}";
434
		$total_items = $wpdb->get_var( $query_count );
435
		$per_page    = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
436
		$this->set_pagination_args( array(
437
			'total_items' => $total_items,
438
			'per_page'    => $per_page,
439
			'total_pages' => ceil( $total_items / $per_page ),
440
		) );
441
	}
442
443
	public function extra_tablenav( $which ) {
444
		if ( ! $this->filter_by || 'top' !== $which ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->filter_by of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
445
			return;
446
		}
447
448
		echo '<div class="alignleft actions">';
449
450
		foreach ( $this->filter_by as $id => $options ) {
451
			$default = ! empty( $_GET['filter_by'][ $id ] ) ? $_GET['filter_by'][ $id ] : '';
452
			if ( empty( $options[ $default ] ) ) {
453
				$default = '';
454
			}
455
456
			echo '<select name="filter_by[' . esc_attr( $id ) . ']" class="first" id="filter-by-' . esc_attr( $id ) . '">';
457
458
			foreach ( $options as $value => $label ) {
459
				echo '<option value="' . esc_attr( $value ) . '" ' . esc_html( $value == $default ? 'selected' : '' )  .'>'
460
					. esc_html( $label )
461
				. '</option>';
462
			}
463
464
			echo '</select>';
465
		}
466
467
		submit_button( esc_html__( 'Filter', 'action-scheduler' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
468
		echo '</div>';
469
	}
470
471
	/**
472
	 * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns
473
	 * are serialized). This can be override in child classes for futher data transformation.
474
	 */
475
	protected function set_items( array $items ) {
476
		$this->items = array();
477
		foreach ( $items as $item ) {
478
			$this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item );
479
		}
480
	}
481
482
	/**
483
	 * Renders the checkbox for each row, this is the first column and it is named ID regardless
484
	 * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper
485
	 * name transformation though using `$this->ID`.
486
	 */
487
	public function column_cb( $row ) {
488
		return '<input name="ID[]" type="checkbox" value="' . esc_attr( $row[ $this->ID ] ) .'" />';
489
	}
490
491
	/**
492
	 * Renders the row-actions.
493
	 *
494
	 * This method renders the action menu, it reads the definition from the $row_actions property,
495
	 * and it checks that the row action method exists before rendering it.
496
	 *
497
	 * @param array $row     Row to render
498
	 * @param $column_name   Current row
0 ignored issues
show
Bug introduced by
The type Current was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
499
	 * @return
500
	 */
501
	protected function maybe_render_actions( $row, $column_name ) {
502
		if ( empty( $this->row_actions[ $column_name ] ) ) {
503
			return;
504
		}
505
506
		$row_id = $row[ $this->ID ];
507
508
		$actions = '<div class="row-actions">';
509
		$action_count = 0;
510
		foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) {
511
512
			$action_count++;
513
514
			if ( ! method_exists( $this, 'row_action_' . $action_key ) ) {
515
				continue;
516
			}
517
518
			$action_link = ! empty( $action['link'] ) ? $action['link'] : add_query_arg( array( 'row_action' => $action_key, 'row_id' => $row_id, 'nonce'  => wp_create_nonce( $action_key . '::' . $row_id ) ) );
519
			$span_class  = ! empty( $action['class'] ) ? $action['class'] : $action_key;
520
			$separator   = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : '';
521
522
			$actions .= sprintf( '<span class="%s">', esc_attr( $span_class ) );
523
			$actions .= sprintf( '<a href="%1$s" title="%2$s">%3$s</a>', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) );
524
			$actions .= sprintf( '%s</span>', $separator );
525
		}
526
		$actions .= '</div>';
527
		return $actions;
528
	}
529
530
	protected function process_row_actions() {
531
		$parameters = array( 'row_action', 'row_id', 'nonce' );
532
		foreach ( $parameters as $parameter ) {
533
			if ( empty( $_REQUEST[ $parameter ] ) ) {
534
				return;
535
			}
536
		}
537
538
		$method = 'row_action_' . $_REQUEST['row_action'];
539
540
		if ( $_REQUEST['nonce'] === wp_create_nonce( $_REQUEST[ 'row_action' ] . '::' . $_REQUEST[ 'row_id' ] ) && method_exists( $this, $method ) ) {
541
			$this->$method( $_REQUEST['row_id'] );
0 ignored issues
show
Security Code Execution introduced by
$method can contain request data and is used in code execution context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and 'row_action_' . $_REQUEST['row_action'] is assigned to $method
    in includes/libraries/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php on line 538

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
542
		}
543
544
		wp_redirect( remove_query_arg(
545
			array( 'row_id', 'row_action', 'nonce' ),
546
			wp_unslash( $_SERVER['REQUEST_URI'] )
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_SERVER['REQUEST_URI']) can also be of type array; however, parameter $query of remove_query_arg() does only seem to accept boolean|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

546
			/** @scrutinizer ignore-type */ wp_unslash( $_SERVER['REQUEST_URI'] )
Loading history...
547
		) );
548
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
549
	}
550
551
	/**
552
	 * Default column formatting, it will escape everythig for security.
553
	 */
554
	public function column_default( $item, $column_name ) {
555
		$column_html = esc_html( $item[ $column_name ] );
556
		$column_html .= $this->maybe_render_actions( $item, $column_name );
557
		return $column_html;
558
	}
559
560
	/**
561
	 * Display the table heading and search query, if any
562
	 */
563
	protected function display_header() {
564
		echo '<h1 class="wp-heading-inline">' . esc_attr( $this->table_header ) . '</h1>';
565
		if ( $this->get_request_search_query() ) {
566
			/* translators: %s: search query */
567
			echo '<span class="subtitle">' . esc_attr( sprintf( __( 'Search results for "%s"', 'action-scheduler' ), $this->get_request_search_query() ) ) . '</span>';
568
		}
569
		echo '<hr class="wp-header-end">';
570
	}
571
572
	/**
573
	 * Display the table heading and search query, if any
574
	 */
575
	protected function display_admin_notices() {
576
		foreach ( $this->admin_notices as $notice ) {
577
			echo '<div id="message" class="' . $notice['class'] . '">';
578
			echo '	<p>' . wp_kses_post( $notice['message'] ) . '</p>';
579
			echo '</div>';
580
		}
581
	}
582
583
	/**
584
	 * Prints the available statuses so the user can click to filter.
585
	 */
586
	protected function display_filter_by_status() {
587
588
		$status_list_items = array();
589
		$request_status    = $this->get_request_status();
590
591
		// Helper to set 'all' filter when not set on status counts passed in
592
		if ( ! isset( $this->status_counts['all'] ) ) {
593
			$this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts;
594
		}
595
596
		foreach ( $this->status_counts as $status_name => $count ) {
597
598
			if ( 0 === $count ) {
599
				continue;
600
			}
601
602
			if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) {
603
				$status_list_item = '<li class="%1$s"><strong>%3$s</strong> (%4$d)</li>';
604
			} else {
605
				$status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>';
606
			}
607
608
			$status_filter_url   = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name );
609
			$status_filter_url   = remove_query_arg( array( 'paged', 's' ), $status_filter_url );
610
			$status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) );
611
		}
612
613
		if ( $status_list_items ) {
614
			echo '<ul class="subsubsub">';
615
			echo implode( " | \n", $status_list_items );
616
			echo '</ul>';
617
		}
618
	}
619
620
	/**
621
	 * Renders the table list, we override the original class to render the table inside a form
622
	 * and to render any needed HTML (like the search box). By doing so the callee of a function can simple
623
	 * forget about any extra HTML.
624
	 */
625
	protected function display_table() {
626
		echo '<form id="' . esc_attr( $this->_args['plural'] ) . '-filter" method="get">';
627
		foreach ( $_GET as $key => $value ) {
628
			if ( '_' === $key[0] || 'paged' === $key ) {
629
				continue;
630
			}
631
			echo '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />';
632
		}
633
		if ( ! empty( $this->search_by ) ) {
634
			echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // WPCS: XSS OK
0 ignored issues
show
Bug introduced by
The method get_search_box_button_text() does not exist on ActionScheduler_Abstract_ListTable. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

634
			echo $this->search_box( $this->/** @scrutinizer ignore-call */ get_search_box_button_text(), 'plugin' ); // WPCS: XSS OK
Loading history...
Bug introduced by
Are you sure the usage of $this->search_box($this-...utton_text(), 'plugin') targeting WP_List_Table::search_box() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
635
		}
636
		parent::display();
637
		echo '</form>';
638
	}
639
640
	/**
641
	 * Process any pending actions.
642
	 */
643
	public function process_actions() {
644
		$this->process_bulk_action();
645
		$this->process_row_actions();
646
647
		if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) {
648
			// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
649
			wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
650
			exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
651
		}
652
	}
653
654
	/**
655
	 * Render the list table page, including header, notices, status filters and table.
656
	 */
657
	public function display_page() {
658
		$this->prepare_items();
659
660
		echo '<div class="wrap">';
661
		$this->display_header();
662
		$this->display_admin_notices();
663
		$this->display_filter_by_status();
664
		$this->display_table();
665
		echo '</div>';
666
	}
667
668
	/**
669
	 * Get the text to display in the search box on the list table.
670
	 */
671
	protected function get_search_box_placeholder() {
672
		return esc_html__( 'Search', 'action-scheduler' );
673
	}
674
}
675