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

ActionScheduler_Abstract_ListTable   F

Complexity

Total Complexity 91

Size/Duplication

Total Lines 633
Duplicated Lines 0.79 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 5
loc 633
rs 1.967
c 0
b 0
f 0
wmc 91
lcom 1
cbo 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A translate() 0 3 1
A get_bulk_actions() 0 13 3
B process_bulk_action() 0 23 6
A bulk_delete() 0 5 1
A prepare_column_headers() 0 7 1
A get_sortable_columns() 0 7 2
A get_columns() 0 8 1
A get_items_query_limit() 0 6 1
A get_items_offset() 0 11 2
A get_items_query_offset() 0 5 1
A get_items_query_order() 0 10 2
A get_request_orderby() 0 12 3
A get_request_order() 0 10 3
A get_request_status() 0 4 2
A get_request_search_query() 0 4 2
A get_table_columns() 0 8 2
A get_items_query_search() 0 13 4
B get_items_query_filters() 0 20 7
A prepare_items() 5 43 3
B extra_tablenav() 0 27 8
A set_items() 0 6 2
A column_cb() 0 3 1
B maybe_render_actions() 0 28 7
A process_row_actions() 0 20 5
A column_default() 0 5 1
A display_header() 0 7 2
A display_admin_notices() 0 7 2
B display_filter_by_status() 0 32 9
A display_table() 0 14 5
A display_page() 0 10 1
A get_search_box_placeholder() 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_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. 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_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 in translations
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
	protected function translate( $text, $context = '' ) {
110
		return _x( $text, $context, $this->package );
0 ignored issues
show
introduced by
Expected next thing to be a escaping function, not '$text'
Loading history...
111
	}
112
113
	/**
114
	 * Reads `$this->bulk_actions` and returns an array that WP_List_Table understands. It
115
	 * also validates that the bulk method handler exists. It throws an exception because
116
	 * this is a library meant for developers and missing a bulk method is a development-time error.
117
	 */
118
	protected function get_bulk_actions() {
119
		$actions = array();
120
121
		foreach ( $this->bulk_actions as $action => $label ) {
122
			if ( ! is_callable( array( $this, 'bulk_' . $action ) ) ) {
123
				throw new RuntimeException( "The bulk action $action does not have a callback method" );
124
			}
125
126
			$actions[ $action ] = $this->translate( $label );
127
		}
128
129
		return $actions;
130
	}
131
132
	/**
133
	 * Checks if the current request has a bulk action. If that is the case it will validate and will
134
	 * execute the bulk method handler. Regardless if the action is valid or not it will redirect to
135
	 * the previous page removing the current arguments that makes this request a bulk action.
136
	 */
137
	protected function process_bulk_action() {
138
		global $wpdb;
139
		// Detect when a bulk action is being triggered.
140
		$action = $this->current_action();
141
142
		if ( ! $action ) {
143
			return;
144
		}
145
146
		check_admin_referer( 'bulk-' . $this->_args['plural'] );
147
148
		$method   = 'bulk_' . $action;
149
		if ( array_key_exists( $action, $this->bulk_actions ) && is_callable( array( $this, $method ) ) && ! empty( $_GET['ID'] ) && is_array( $_GET['ID'] ) ) {
150
			$ids_sql = '(' . implode( ',', array_fill( 0, count( $_GET['ID'] ), '%s' ) ) . ')';
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
151
			$this->$method( $_GET['ID'], $wpdb->prepare( $ids_sql, $_GET['ID'] ) );
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
152
		}
153
154
		wp_redirect( remove_query_arg(
155
			array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ),
156
			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...
157
		) );
158
		exit;
159
	}
160
161
	/**
162
	 * Default code for deleting entries. We trust ids_sql because it is
163
	 * validated already by process_bulk_action()
164
	 */
165
	protected function bulk_delete( array $ids, $ids_sql ) {
166
		global $wpdb;
167
168
		$wpdb->query( "DELETE FROM {$this->table_name} WHERE {$this->ID} IN $ids_sql" );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
169
	}
170
171
	/**
172
	 * Prepares the _column_headers property which is used by WP_Table_List at rendering.
173
	 * It merges the columns and the sortable columns.
174
	 */
175
	protected function prepare_column_headers() {
176
		$this->_column_headers = array(
177
			$this->get_columns(),
178
			array(),
179
			$this->get_sortable_columns(),
180
		);
181
	}
182
183
	/**
184
	 * Reads $this->sort_by and returns the columns name in a format that WP_Table_List
185
	 * expects
186
	 */
187
	public function get_sortable_columns() {
188
		$sort_by = array();
189
		foreach ( $this->sort_by as $column ) {
190
			$sort_by[ $column ] = array( $column, true );
191
		}
192
		return $sort_by;
193
	}
194
195
	/**
196
	 * Returns the columns names for rendering. It adds a checkbox for selecting everything
197
	 * as the first column
198
	 */
199
	public function get_columns() {
200
		$columns = array_merge(
201
			array( 'cb' => '<input type="checkbox" />' ),
202
			array_map( array( $this, 'translate' ), $this->columns )
203
		);
204
205
		return $columns;
206
	}
207
208
	/**
209
	 * Get prepared LIMIT clause for items query
210
	 *
211
	 * @global wpdb $wpdb
212
	 *
213
	 * @return string Prepared LIMIT clause for items query.
214
	 */
215
	protected function get_items_query_limit() {
216
		global $wpdb;
217
218
		$per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
219
		return $wpdb->prepare( 'LIMIT %d', $per_page );
220
	}
221
222
	/**
223
	 * Returns the number of items to offset/skip for this current view.
224
	 *
225
	 * @return int
226
	 */
227
	protected function get_items_offset() {
228
		$per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
229
		$current_page = $this->get_pagenum();
230
		if ( 1 < $current_page ) {
231
			$offset = $per_page * ( $current_page - 1 );
232
		} else {
233
			$offset = 0;
234
		}
235
236
		return $offset;
237
	}
238
239
	/**
240
	 * Get prepared OFFSET clause for items query
241
	 *
242
	 * @global wpdb $wpdb
243
	 *
244
	 * @return string Prepared OFFSET clause for items query.
245
	 */
246
	protected function get_items_query_offset() {
247
		global $wpdb;
248
249
		return $wpdb->prepare( 'OFFSET %d', $this->get_items_offset() );
250
	}
251
252
	/**
253
	 * Prepares the ORDER BY sql statement. It uses `$this->sort_by` to know which
254
	 * columns are sortable. This requests validates the orderby $_GET parameter is a valid
255
	 * column and sortable. It will also use order (ASC|DESC) using DESC by default.
256
	 */
257
	protected function get_items_query_order() {
258
		if ( empty( $this->sort_by ) ) {
259
			return '';
260
		}
261
262
		$orderby = esc_sql( $this->get_request_orderby() );
263
		$order   = esc_sql( $this->get_request_order() );
264
265
		return "ORDER BY {$orderby} {$order}";
266
	}
267
268
	/**
269
	 * Return the sortable column specified for this request to order the results by, if any.
270
	 *
271
	 * @return string
272
	 */
273
	protected function get_request_orderby() {
274
275
		$valid_sortable_columns = array_values( $this->sort_by );
276
277
		if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns ) ) {
278
			$orderby = sanitize_text_field( $_GET['orderby'] );
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
279
		} else {
280
			$orderby = $valid_sortable_columns[0];
281
		}
282
283
		return $orderby;
284
	}
285
286
	/**
287
	 * Return the sortable column order specified for this request.
288
	 *
289
	 * @return string
290
	 */
291
	protected function get_request_order() {
292
293
		if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( $_GET['order'] ) ) {
294
			$order = 'DESC';
295
		} else {
296
			$order = 'ASC';
297
		}
298
299
		return $order;
300
	}
301
302
	/**
303
	 * Return the status filter for this request, if any.
304
	 *
305
	 * @return string
306
	 */
307
	protected function get_request_status() {
308
		$status = ( ! empty( $_GET['status'] ) ) ? $_GET['status'] : '';
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
309
		return $status;
310
	}
311
312
	/**
313
	 * Return the search filter for this request, if any.
314
	 *
315
	 * @return string
316
	 */
317
	protected function get_request_search_query() {
318
		$search_query = ( ! empty( $_GET['s'] ) ) ? $_GET['s'] : '';
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
319
		return $search_query;
320
	}
321
322
	/**
323
	 * Process and return the columns name. This is meant for using with SQL, this means it
324
	 * always includes the primary key.
325
	 *
326
	 * @return array
327
	 */
328
	protected function get_table_columns() {
329
		$columns = array_keys( $this->columns );
330
		if ( ! in_array( $this->ID, $columns ) ) {
331
			$columns[] = $this->ID;
332
		}
333
334
		return $columns;
335
	}
336
337
	/**
338
	 * Check if the current request is doing a "full text" search. If that is the case
339
	 * prepares the SQL to search texts using LIKE.
340
	 *
341
	 * If the current request does not have any search or if this list table does not support
342
	 * that feature it will return an empty string.
343
	 *
344
	 * TODO:
345
	 *   - Improve search doing LIKE by word rather than by phrases.
346
	 *
347
	 * @return string
348
	 */
349
	protected function get_items_query_search() {
350
		global $wpdb;
351
352
		if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) {
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
353
			return '';
354
		}
355
356
		$filter  = array();
357
		foreach ( $this->search_by as $column ) {
358
			$filter[] = '`' . $column . '` like "%' . $wpdb->esc_like( $_GET['s'] ) . '%"';
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
359
		}
360
		return implode( ' OR ', $filter );
361
	}
362
363
	/**
364
	 * Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting
365
	 * any data sent by the user it validates that it is a valid option.
366
	 */
367
	protected function get_items_query_filters() {
368
		global $wpdb;
369
370
		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...
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
371
			return '';
372
		}
373
374
		$filter = array();
375
376
		foreach ( $this->filter_by as $column => $options ) {
377
			if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) {
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
378
				continue;
379
			}
380
381
			$filter[] = $wpdb->prepare( "`$column` = %s", $_GET['filter_by'][ $column ] );
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
382
		}
383
384
		return implode( ' AND ', $filter );
385
386
	}
387
388
	/**
389
	 * Prepares the data to feed WP_Table_List.
390
	 *
391
	 * This has the core for selecting, sorting and filting data. To keep the code simple
392
	 * its logic is split among many methods (get_items_query_*).
393
	 *
394
	 * Beside populating the items this function will also count all the records that matches
395
	 * the filtering criteria and will do fill the pagination variables.
396
	 */
397
	public function prepare_items() {
398
		global $wpdb;
399
400
		$this->process_bulk_action();
401
402
		$this->process_row_actions();
403
404 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...
405
			// _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
406
			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...
407
			exit;
408
		}
409
410
		$this->prepare_column_headers();
411
412
		$limit   = $this->get_items_query_limit();
413
		$offset  = $this->get_items_query_offset();
414
		$order   = $this->get_items_query_order();
415
		$where   = array_filter(array(
416
			$this->get_items_query_search(),
417
			$this->get_items_query_filters(),
418
		));
419
		$columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`';
420
421
		if ( ! empty( $where ) ) {
422
			$where = 'WHERE ('. implode( ') AND (', $where ) . ')';
423
		} else {
424
			$where = '';
425
		}
426
427
		$sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}";
428
429
		$this->set_items( $wpdb->get_results( $sql, ARRAY_A ) );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
430
431
		$query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}";
432
		$total_items = $wpdb->get_var( $query_count );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
433
		$per_page    = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page );
434
		$this->set_pagination_args( array(
435
			'total_items' => $total_items,
436
			'per_page'    => $per_page,
437
			'total_pages' => ceil( $total_items / $per_page ),
438
		) );
439
	}
440
441
	public function extra_tablenav( $which ) {
442
		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...
443
			return;
444
		}
445
446
		echo '<div class="alignleft actions">';
447
448
		foreach ( $this->filter_by as $id => $options ) {
449
			$default = ! empty( $_GET['filter_by'][ $id ] ) ? $_GET['filter_by'][ $id ] : '';
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
450
			if ( empty( $options[ $default ] ) ) {
451
				$default = '';
452
			}
453
454
			echo '<select name="filter_by[' . esc_attr( $id ) . ']" class="first" id="filter-by-' . esc_attr( $id ) . '">';
455
456
			foreach ( $options as $value => $label ) {
457
				echo '<option value="' . esc_attr( $value ) . '" ' . esc_html( $value == $default ? 'selected' : '' )  .'>'
458
					. esc_html( $this->translate( $label ) )
459
				. '</option>';
460
			}
461
462
			echo '</select>';
463
		}
464
465
		submit_button( $this->translate( 'Filter' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
466
		echo '</div>';
467
	}
468
469
	/**
470
	 * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns
471
	 * are serialized). This can be override in child classes for futher data transformation.
472
	 */
473
	protected function set_items( array $items ) {
474
		$this->items = array();
475
		foreach ( $items as $item ) {
476
			$this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item );
477
		}
478
	}
479
480
	/**
481
	 * Renders the checkbox for each row, this is the first column and it is named ID regardless
482
	 * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper
483
	 * name transformation though using `$this->ID`.
484
	 */
485
	public function column_cb( $row ) {
486
		return '<input name="ID[]" type="checkbox" value="' . esc_attr( $row[ $this->ID ] ) .'" />';
487
	}
488
489
	/**
490
	 * Renders the row-actions.
491
	 *
492
	 * This method renders the action menu, it reads the definition from the $row_actions property,
493
	 * and it checks that the row action method exists before rendering it.
494
	 *
495
	 * @param array $row     Row to render
496
	 * @param $column_name   Current row
497
	 * @return
498
	 */
499
	protected function maybe_render_actions( $row, $column_name ) {
500
		if ( empty( $this->row_actions[ $column_name ] ) ) {
501
			return;
502
		}
503
504
		$row_id = $row[ $this->ID ];
505
506
		$actions = '<div class="row-actions">';
507
		$action_count = 0;
508
		foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) {
509
510
			$action_count++;
511
512
			if ( ! method_exists( $this, 'row_action_' . $action_key ) ) {
513
				continue;
514
			}
515
516
			$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 ) ) );
0 ignored issues
show
introduced by
Expected 1 space between "'nonce'" and double arrow; 2 found
Loading history...
517
			$span_class  = ! empty( $action['class'] ) ? $action['class'] : $action_key;
518
			$separator   = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : '';
519
520
			$actions .= sprintf( '<span class="%s">', esc_attr( $span_class ) );
521
			$actions .= sprintf( '<a href="%1$s" title="%2$s">%3$s</a>', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) );
522
			$actions .= sprintf( '%s</span>', $separator );
523
		}
524
		$actions .= '</div>';
525
		return $actions;
526
	}
527
528
	protected function process_row_actions() {
529
		$parameters = array( 'row_action', 'row_id', 'nonce' );
530
		foreach ( $parameters as $parameter ) {
531
			if ( empty( $_REQUEST[ $parameter ] ) ) {
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
532
				return;
533
			}
534
		}
535
536
		$method = 'row_action_' . $_REQUEST['row_action'];
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_REQUEST
Loading history...
537
538
		if ( $_REQUEST['nonce'] === wp_create_nonce( $_REQUEST[ 'row_action' ] . '::' . $_REQUEST[ 'row_id' ] ) && method_exists( $this, $method ) ) {
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-validated input variable: $_REQUEST
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_REQUEST
Loading history...
introduced by
Array keys should NOT be surrounded by spaces if they only contain a string or an integer.
Loading history...
539
			$this->$method( $_REQUEST['row_id'] );
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-validated input variable: $_REQUEST
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_REQUEST
Loading history...
540
		}
541
542
		wp_redirect( remove_query_arg(
543
			array( 'row_id', 'row_action', 'nonce' ),
544
			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...
545
		) );
546
		exit;
547
	}
548
549
	/**
550
	 * Default column formatting, it will escape everythig for security.
551
	 */
552
	public function column_default( $item, $column_name ) {
553
		$column_html = esc_html( $item[ $column_name ] );
554
		$column_html .= $this->maybe_render_actions( $item, $column_name );
555
		return $column_html;
556
	}
557
558
	/**
559
	 * Display the table heading and search query, if any
560
	 */
561
	protected function display_header() {
562
		echo '<h1 class="wp-heading-inline">' . esc_attr( $this->table_header ) . '</h1>';
563
		if ( $this->get_request_search_query() ) {
564
			echo '<span class="subtitle">' . esc_attr( $this->translate( sprintf( 'Search results for "%s"', $this->get_request_search_query() ) ) ) . '</span>';
565
		}
566
		echo '<hr class="wp-header-end">';
567
	}
568
569
	/**
570
	 * Display the table heading and search query, if any
571
	 */
572
	protected function display_admin_notices() {
573
		foreach ( $this->admin_notices as $notice ) {
574
			echo '<div id="message" class="' . $notice['class'] . '">';
0 ignored issues
show
introduced by
Expected next thing to be a escaping function, not '$notice'
Loading history...
575
			echo '	<p>' . wp_kses_post( $notice['message'] ) . '</p>';
576
			echo '</div>';
577
		}
578
	}
579
580
	/**
581
	 * Prints the available statuses so the user can click to filter.
582
	 */
583
	protected function display_filter_by_status() {
584
585
		$status_list_items = array();
586
		$request_status    = $this->get_request_status();
587
588
		// Helper to set 'all' filter when not set on status counts passed in
589
		if ( ! isset( $this->status_counts['all'] ) ) {
590
			$this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts;
591
		}
592
593
		foreach ( $this->status_counts as $status_name => $count ) {
594
595
			if ( 0 === $count ) {
596
				continue;
597
			}
598
599
			if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) {
600
				$status_list_item = '<li class="%1$s"><strong>%3$s</strong> (%4$d)</li>';
601
			} else {
602
				$status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>';
603
			}
604
605
			$status_filter_url   = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name );
606
			$status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) );
607
		}
608
609
		if ( $status_list_items ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $status_list_items 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...
610
			echo '<ul class="subsubsub">';
611
			echo implode( " | \n", $status_list_items );
0 ignored issues
show
introduced by
Expected a sanitizing function (see Codex for 'Data Validation'), but instead saw 'implode'
Loading history...
612
			echo '</ul>';
613
		}
614
	}
615
616
	/**
617
	 * Renders the table list, we override the original class to render the table inside a form
618
	 * and to render any needed HTML (like the search box). By doing so the callee of a function can simple
619
	 * forget about any extra HTML.
620
	 */
621
	protected function display_table() {
622
		echo '<form id="' . esc_attr( $this->_args['plural'] ) . '-filter" method="get">';
623
		foreach ( $_GET as $key => $value ) {
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
624
			if ( '_' === $key[0] || 'paged' === $key ) {
625
				continue;
626
			}
627
			echo '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />';
628
		}
629
		if ( ! empty( $this->search_by ) ) {
630
			echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // WPCS: XSS OK
631
		}
632
		parent::display();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (display() instead of display_table()). Are you sure this is correct? If so, you might want to change this to $this->display().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
633
		echo '</form>';
634
	}
635
636
	/**
637
	 * Render the list table page, including header, notices, status filters and table.
638
	 */
639
	public function display_page() {
640
		$this->prepare_items();
641
642
		echo '<div class="wrap">';
643
		$this->display_header();
644
		$this->display_admin_notices();
645
		$this->display_filter_by_status();
646
		$this->display_table();
647
		echo '</div>';
648
	}
649
650
	/**
651
	 * Get the text to display in the search box on the list table.
652
	 */
653
	protected function get_search_box_placeholder() {
654
		return $this->translate( 'Search' );
655
	}
656
}
657