PluginProbe
WANotifier for Forms and Actions / 2.7.5
WANotifier for Forms and Actions v2.7.5
3.1.1 3.1.0 3.0.4 2.7.10 2.7.11 2.7.12 2.7.13 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 3.0.0 3.0.1 3.0.2 3.0.3 trunk 0.1.0 0.1.1 1.0.0 1.0.1 1.0.2 All 67 releases
notifier / libraries / action-scheduler / classes / abstracts / ActionScheduler_Abstract_ListTable.php

ActionScheduler_Abstract_ListTable.php in WANotifier for Forms and Actions 2.7.5, at libraries/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php

777 lines 24.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 * @var string
30 */
31 protected $table_name;
32
33 /**
34 * Package name, used to get options from WP_List_Table::get_items_per_page.
35 *
36 * @var string
37 */
38 protected $package;
39
40 /**
41 * How many items do we render per page?
42 *
43 * @var int
44 */
45 protected $items_per_page = 10;
46
47 /**
48 * Enables search in this table listing. If this array
49 * is empty it means the listing is not searchable.
50 *
51 * @var array
52 */
53 protected $search_by = array();
54
55 /**
56 * Columns to show in the table listing. It is a key => value pair. The
57 * key must much the table column name and the value is the label, which is
58 * automatically translated.
59 *
60 * @var array
61 */
62 protected $columns = array();
63
64 /**
65 * Defines the row-actions. It expects an array where the key
66 * is the column name and the value is an array of actions.
67 *
68 * The array of actions are key => value, where key is the method name
69 * (with the prefix row_action_<key>) and the value is the label
70 * and title.
71 *
72 * @var array
73 */
74 protected $row_actions = array();
75
76 /**
77 * The Primary key of our table
78 *
79 * @var string
80 */
81 protected $ID = 'ID';
82
83 /**
84 * Enables sorting, it expects an array
85 * of columns (the column names are the values)
86 *
87 * @var array
88 */
89 protected $sort_by = array();
90
91 /**
92 * The default sort order
93 *
94 * @var string
95 */
96 protected $filter_by = array();
97
98 /**
99 * The status name => count combinations for this table's items. Used to display status filters.
100 *
101 * @var array
102 */
103 protected $status_counts = array();
104
105 /**
106 * Notices to display when loading the table. Array of arrays of form array( 'class' => {updated|error}, 'message' => 'This is the notice text display.' ).
107 *
108 * @var array
109 */
110 protected $admin_notices = array();
111
112 /**
113 * Localised string displayed in the <h1> element above the able.
114 *
115 * @var string
116 */
117 protected $table_header;
118
119 /**
120 * Enables bulk actions. It must be an array where the key is the action name
121 * and the value is the label (which is translated automatically). It is important
122 * to notice that it will check that the method exists (`bulk_$name`) and will throw
123 * an exception if it does not exists.
124 *
125 * This class will automatically check if the current request has a bulk action, will do the
126 * validations and afterwards will execute the bulk method, with two arguments. The first argument
127 * is the array with primary keys, the second argument is a string with a list of the primary keys,
128 * escaped and ready to use (with `IN`).
129 *
130 * @var array
131 */
132 protected $bulk_actions = array();
133
134 /**
135 * Makes translation easier, it basically just wraps
136 * `_x` with some default (the package name).
137 *
138 * @param string $text The new text to translate.
139 * @param string $context The context of the text.
140 * @return string|void The translated text.
141 *
142 * @deprecated 3.0.0 Use `_x()` instead.
143 */
144 protected function translate( $text, $context = '' ) {
145 return $text;
146 }
147
148 /**
149 * Reads `$this->bulk_actions` and returns an array that WP_List_Table understands. It
150 * also validates that the bulk method handler exists. It throws an exception because
151 * this is a library meant for developers and missing a bulk method is a development-time error.
152 *
153 * @return array
154 *
155 * @throws RuntimeException Throws RuntimeException when the bulk action does not have a callback method.
156 */
157 protected function get_bulk_actions() {
158 $actions = array();
159
160 foreach ( $this->bulk_actions as $action => $label ) {
161 if ( ! is_callable( array( $this, 'bulk_' . $action ) ) ) {
162 throw new RuntimeException( "The bulk action $action does not have a callback method" );
163 }
164
165 $actions[ $action ] = $label;
166 }
167
168 return $actions;
169 }
170
171 /**
172 * Checks if the current request has a bulk action. If that is the case it will validate and will
173 * execute the bulk method handler. Regardless if the action is valid or not it will redirect to
174 * the previous page removing the current arguments that makes this request a bulk action.
175 */
176 protected function process_bulk_action() {
177 global $wpdb;
178 // Detect when a bulk action is being triggered.
179 $action = $this->current_action();
180 if ( ! $action ) {
181 return;
182 }
183
184 check_admin_referer( 'bulk-' . $this->_args['plural'] );
185
186 $method = 'bulk_' . $action;
187 if ( array_key_exists( $action, $this->bulk_actions ) && is_callable( array( $this, $method ) ) && ! empty( $_GET['ID'] ) && is_array( $_GET['ID'] ) ) {
188 $ids_sql = '(' . implode( ',', array_fill( 0, count( $_GET['ID'] ), '%s' ) ) . ')';
189 $id = array_map( 'absint', $_GET['ID'] );
190 $this->$method( $id, $wpdb->prepare( $ids_sql, $id ) ); //phpcs:ignore WordPress.DB.PreparedSQL
191 }
192
193 if ( isset( $_SERVER['REQUEST_URI'] ) ) {
194 wp_safe_redirect(
195 remove_query_arg(
196 array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ),
197 esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
198 )
199 );
200 exit;
201 }
202 }
203
204 /**
205 * Default code for deleting entries.
206 * validated already by process_bulk_action()
207 *
208 * @param array $ids ids of the items to delete.
209 * @param string $ids_sql the sql for the ids.
210 * @return void
211 */
212 protected function bulk_delete( array $ids, $ids_sql ) {
213 $store = ActionScheduler::store();
214 foreach ( $ids as $action_id ) {
215 $store->delete( $action_id );
216 }
217 }
218
219 /**
220 * Prepares the _column_headers property which is used by WP_Table_List at rendering.
221 * It merges the columns and the sortable columns.
222 */
223 protected function prepare_column_headers() {
224 $this->_column_headers = array(
225 $this->get_columns(),
226 get_hidden_columns( $this->screen ),
227 $this->get_sortable_columns(),
228 );
229 }
230
231 /**
232 * Reads $this->sort_by and returns the columns name in a format that WP_Table_List
233 * expects
234 */
235 public function get_sortable_columns() {
236 $sort_by = array();
237 foreach ( $this->sort_by as $column ) {
238 $sort_by[ $column ] = array( $column, true );
239 }
240 return $sort_by;
241 }
242
243 /**
244 * Returns the columns names for rendering. It adds a checkbox for selecting everything
245 * as the first column
246 */
247 public function get_columns() {
248 $columns = array_merge(
249 array( 'cb' => '<input type="checkbox" />' ),
250 $this->columns
251 );
252
253 return $columns;
254 }
255
256 /**
257 * Get prepared LIMIT clause for items query
258 *
259 * @global wpdb $wpdb
260 *
261 * @return string Prepared LIMIT clause for items query.
262 */
263 protected function get_items_query_limit() {
264 global $wpdb;
265
266 $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
267 return $wpdb->prepare( 'LIMIT %d', $per_page );
268 }
269
270 /**
271 * Returns the number of items to offset/skip for this current view.
272 *
273 * @return int
274 */
275 protected function get_items_offset() {
276 $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
277 $current_page = $this->get_pagenum();
278 if ( 1 < $current_page ) {
279 $offset = $per_page * ( $current_page - 1 );
280 } else {
281 $offset = 0;
282 }
283
284 return $offset;
285 }
286
287 /**
288 * Get prepared OFFSET clause for items query
289 *
290 * @global wpdb $wpdb
291 *
292 * @return string Prepared OFFSET clause for items query.
293 */
294 protected function get_items_query_offset() {
295 global $wpdb;
296
297 return $wpdb->prepare( 'OFFSET %d', $this->get_items_offset() );
298 }
299
300 /**
301 * Prepares the ORDER BY sql statement. It uses `$this->sort_by` to know which
302 * columns are sortable. This requests validates the orderby $_GET parameter is a valid
303 * column and sortable. It will also use order (ASC|DESC) using DESC by default.
304 */
305 protected function get_items_query_order() {
306 if ( empty( $this->sort_by ) ) {
307 return '';
308 }
309
310 $orderby = esc_sql( $this->get_request_orderby() );
311 $order = esc_sql( $this->get_request_order() );
312
313 return "ORDER BY {$orderby} {$order}";
314 }
315
316 /**
317 * Return the sortable column specified for this request to order the results by, if any.
318 *
319 * @return string
320 */
321 protected function get_request_orderby() {
322
323 $valid_sortable_columns = array_values( $this->sort_by );
324
325 if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
326 $orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
327 } else {
328 $orderby = $valid_sortable_columns[0];
329 }
330
331 return $orderby;
332 }
333
334 /**
335 * Return the sortable column order specified for this request.
336 *
337 * @return string
338 */
339 protected function get_request_order() {
340
341 if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
342 $order = 'DESC';
343 } else {
344 $order = 'ASC';
345 }
346
347 return $order;
348 }
349
350 /**
351 * Return the status filter for this request, if any.
352 *
353 * @return string
354 */
355 protected function get_request_status() {
356 $status = ( ! empty( $_GET['status'] ) ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
357 return $status;
358 }
359
360 /**
361 * Return the search filter for this request, if any.
362 *
363 * @return string
364 */
365 protected function get_request_search_query() {
366 $search_query = ( ! empty( $_GET['s'] ) ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
367 return $search_query;
368 }
369
370 /**
371 * Process and return the columns name. This is meant for using with SQL, this means it
372 * always includes the primary key.
373 *
374 * @return array
375 */
376 protected function get_table_columns() {
377 $columns = array_keys( $this->columns );
378 if ( ! in_array( $this->ID, $columns, true ) ) {
379 $columns[] = $this->ID;
380 }
381
382 return $columns;
383 }
384
385 /**
386 * Check if the current request is doing a "full text" search. If that is the case
387 * prepares the SQL to search texts using LIKE.
388 *
389 * If the current request does not have any search or if this list table does not support
390 * that feature it will return an empty string.
391 *
392 * @return string
393 */
394 protected function get_items_query_search() {
395 global $wpdb;
396
397 if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
398 return '';
399 }
400
401 $search_string = sanitize_text_field( wp_unslash( $_GET['s'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
402
403 $filter = array();
404 foreach ( $this->search_by as $column ) {
405 $wild = '%';
406 $sql_like = $wild . $wpdb->esc_like( $search_string ) . $wild;
407 $filter[] = $wpdb->prepare( '`' . $column . '` LIKE %s', $sql_like ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.NotPrepared
408 }
409 return implode( ' OR ', $filter );
410 }
411
412 /**
413 * Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting
414 * any data sent by the user it validates that it is a valid option.
415 */
416 protected function get_items_query_filters() {
417 global $wpdb;
418
419 if ( ! $this->filter_by || empty( $_GET['filter_by'] ) || ! is_array( $_GET['filter_by'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
420 return '';
421 }
422
423 $filter = array();
424
425 foreach ( $this->filter_by as $column => $options ) {
426 if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
427 continue;
428 }
429
430 $filter[] = $wpdb->prepare( "`$column` = %s", sanitize_text_field( wp_unslash( $_GET['filter_by'][ $column ] ) ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
431 }
432
433 return implode( ' AND ', $filter );
434
435 }
436
437 /**
438 * Prepares the data to feed WP_Table_List.
439 *
440 * This has the core for selecting, sorting and filting data. To keep the code simple
441 * its logic is split among many methods (get_items_query_*).
442 *
443 * Beside populating the items this function will also count all the records that matches
444 * the filtering criteria and will do fill the pagination variables.
445 */
446 public function prepare_items() {
447 global $wpdb;
448
449 $this->process_bulk_action();
450
451 $this->process_row_actions();
452
453 if ( ! empty( $_REQUEST['_wp_http_referer'] && ! empty( $_SERVER['REQUEST_URI'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
454 // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
455 wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
456 exit;
457 }
458
459 $this->prepare_column_headers();
460
461 $limit = $this->get_items_query_limit();
462 $offset = $this->get_items_query_offset();
463 $order = $this->get_items_query_order();
464 $where = array_filter(
465 array(
466 $this->get_items_query_search(),
467 $this->get_items_query_filters(),
468 )
469 );
470 $columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`';
471
472 if ( ! empty( $where ) ) {
473 $where = 'WHERE (' . implode( ') AND (', $where ) . ')';
474 } else {
475 $where = '';
476 }
477
478 $sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}";
479
480 $this->set_items( $wpdb->get_results( $sql, ARRAY_A ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
481
482 $query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}";
483 $total_items = $wpdb->get_var( $query_count ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
484 $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
485 $this->set_pagination_args(
486 array(
487 'total_items' => $total_items,
488 'per_page' => $per_page,
489 'total_pages' => ceil( $total_items / $per_page ),
490 )
491 );
492 }
493
494 /**
495 * Display the table.
496 *
497 * @param string $which The name of the table.
498 */
499 public function extra_tablenav( $which ) {
500 if ( ! $this->filter_by || 'top' !== $which ) {
501 return;
502 }
503
504 echo '<div class="alignleft actions">';
505
506 foreach ( $this->filter_by as $id => $options ) {
507 $default = ! empty( $_GET['filter_by'][ $id ] ) ? sanitize_text_field( wp_unslash( $_GET['filter_by'][ $id ] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
508 if ( empty( $options[ $default ] ) ) {
509 $default = '';
510 }
511
512 echo '<select name="filter_by[' . esc_attr( $id ) . ']" class="first" id="filter-by-' . esc_attr( $id ) . '">';
513
514 foreach ( $options as $value => $label ) {
515 echo '<option value="' . esc_attr( $value ) . '" ' . esc_html( $value === $default ? 'selected' : '' ) . '>'
516 . esc_html( $label )
517 . '</option>';
518 }
519
520 echo '</select>';
521 }
522
523 submit_button( esc_html__( 'Filter', 'action-scheduler' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
524 echo '</div>';
525 }
526
527 /**
528 * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns
529 * are serialized). This can be override in child classes for futher data transformation.
530 *
531 * @param array $items Items array.
532 */
533 protected function set_items( array $items ) {
534 $this->items = array();
535 foreach ( $items as $item ) {
536 $this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item );
537 }
538 }
539
540 /**
541 * Renders the checkbox for each row, this is the first column and it is named ID regardless
542 * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper
543 * name transformation though using `$this->ID`.
544 *
545 * @param array $row The row to render.
546 */
547 public function column_cb( $row ) {
548 return '<input name="ID[]" type="checkbox" value="' . esc_attr( $row[ $this->ID ] ) . '" />';
549 }
550
551 /**
552 * Renders the row-actions.
553 *
554 * This method renders the action menu, it reads the definition from the $row_actions property,
555 * and it checks that the row action method exists before rendering it.
556 *
557 * @param array $row Row to be rendered.
558 * @param string $column_name Column name.
559 * @return string
560 */
561 protected function maybe_render_actions( $row, $column_name ) {
562 if ( empty( $this->row_actions[ $column_name ] ) ) {
563 return;
564 }
565
566 $row_id = $row[ $this->ID ];
567
568 $actions = '<div class="row-actions">';
569 $action_count = 0;
570 foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) {
571
572 $action_count++;
573
574 if ( ! method_exists( $this, 'row_action_' . $action_key ) ) {
575 continue;
576 }
577
578 $action_link = ! empty( $action['link'] ) ? $action['link'] : add_query_arg(
579 array(
580 'row_action' => $action_key,
581 'row_id' => $row_id,
582 'nonce' => wp_create_nonce( $action_key . '::' . $row_id ),
583 )
584 );
585 $span_class = ! empty( $action['class'] ) ? $action['class'] : $action_key;
586 $separator = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : '';
587
588 $actions .= sprintf( '<span class="%s">', esc_attr( $span_class ) );
589 $actions .= sprintf( '<a href="%1$s" title="%2$s">%3$s</a>', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) );
590 $actions .= sprintf( '%s</span>', $separator );
591 }
592 $actions .= '</div>';
593 return $actions;
594 }
595
596 /**
597 * Process the bulk actions.
598 *
599 * @return void
600 */
601 protected function process_row_actions() {
602 $parameters = array( 'row_action', 'row_id', 'nonce' );
603 foreach ( $parameters as $parameter ) {
604 if ( empty( $_REQUEST[ $parameter ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
605 return;
606 }
607 }
608
609 $action = sanitize_text_field( wp_unslash( $_REQUEST['row_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
610 $row_id = sanitize_text_field( wp_unslash( $_REQUEST['row_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
611 $nonce = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
612 $method = 'row_action_' . $action; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
613
614 if ( wp_verify_nonce( $nonce, $action . '::' . $row_id ) && method_exists( $this, $method ) ) {
615 $this->$method( sanitize_text_field( wp_unslash( $row_id ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
616 }
617
618 if ( isset( $_SERVER['REQUEST_URI'] ) ) {
619 wp_safe_redirect(
620 remove_query_arg(
621 array( 'row_id', 'row_action', 'nonce' ),
622 esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
623 )
624 );
625 exit;
626 }
627 }
628
629 /**
630 * Default column formatting, it will escape everythig for security.
631 *
632 * @param array $item The item array.
633 * @param string $column_name Column name to display.
634 *
635 * @return string
636 */
637 public function column_default( $item, $column_name ) {
638 $column_html = esc_html( $item[ $column_name ] );
639 $column_html .= $this->maybe_render_actions( $item, $column_name );
640 return $column_html;
641 }
642
643 /**
644 * Display the table heading and search query, if any
645 */
646 protected function display_header() {
647 echo '<h1 class="wp-heading-inline">' . esc_attr( $this->table_header ) . '</h1>';
648 if ( $this->get_request_search_query() ) {
649 /* translators: %s: search query */
650 echo '<span class="subtitle">' . esc_attr( sprintf( __( 'Search results for "%s"', 'action-scheduler' ), $this->get_request_search_query() ) ) . '</span>';
651 }
652 echo '<hr class="wp-header-end">';
653 }
654
655 /**
656 * Display the table heading and search query, if any
657 */
658 protected function display_admin_notices() {
659 foreach ( $this->admin_notices as $notice ) {
660 echo '<div id="message" class="' . esc_attr( $notice['class'] ) . '">';
661 echo ' <p>' . wp_kses_post( $notice['message'] ) . '</p>';
662 echo '</div>';
663 }
664 }
665
666 /**
667 * Prints the available statuses so the user can click to filter.
668 */
669 protected function display_filter_by_status() {
670
671 $status_list_items = array();
672 $request_status = $this->get_request_status();
673
674 // Helper to set 'all' filter when not set on status counts passed in.
675 if ( ! isset( $this->status_counts['all'] ) ) {
676 $all_count = array_sum( $this->status_counts );
677 if ( isset( $this->status_counts['past-due'] ) ) {
678 $all_count -= $this->status_counts['past-due'];
679 }
680 $this->status_counts = array( 'all' => $all_count ) + $this->status_counts;
681 }
682
683 // Translated status labels.
684 $status_labels = ActionScheduler_Store::instance()->get_status_labels();
685 $status_labels['all'] = _x( 'All', 'status labels', 'action-scheduler' );
686 $status_labels['past-due'] = _x( 'Past-due', 'status labels', 'action-scheduler' );
687
688 foreach ( $this->status_counts as $status_slug => $count ) {
689
690 if ( 0 === $count ) {
691 continue;
692 }
693
694 if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) {
695 $status_list_item = '<li class="%1$s"><a href="%2$s" class="current">%3$s</a> (%4$d)</li>';
696 } else {
697 $status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>';
698 }
699
700 $status_name = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug );
701 $status_filter_url = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug );
702 $status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url );
703 $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) );
704 }
705
706 if ( $status_list_items ) {
707 echo '<ul class="subsubsub">';
708 echo implode( " | \n", $status_list_items ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
709 echo '</ul>';
710 }
711 }
712
713 /**
714 * Renders the table list, we override the original class to render the table inside a form
715 * and to render any needed HTML (like the search box). By doing so the callee of a function can simple
716 * forget about any extra HTML.
717 */
718 protected function display_table() {
719 echo '<form id="' . esc_attr( $this->_args['plural'] ) . '-filter" method="get">';
720 foreach ( $_GET as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
721 if ( '_' === $key[0] || 'paged' === $key || 'ID' === $key ) {
722 continue;
723 }
724 echo '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />';
725 }
726 if ( ! empty( $this->search_by ) ) {
727 echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
728 }
729 parent::display();
730 echo '</form>';
731 }
732
733 /**
734 * Process any pending actions.
735 */
736 public function process_actions() {
737 $this->process_bulk_action();
738 $this->process_row_actions();
739
740 if ( ! empty( $_REQUEST['_wp_http_referer'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
741 // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
742 wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
743 exit;
744 }
745 }
746
747 /**
748 * Render the list table page, including header, notices, status filters and table.
749 */
750 public function display_page() {
751 $this->prepare_items();
752
753 echo '<div class="wrap">';
754 $this->display_header();
755 $this->display_admin_notices();
756 $this->display_filter_by_status();
757 $this->display_table();
758 echo '</div>';
759 }
760
761 /**
762 * Get the text to display in the search box on the list table.
763 */
764 protected function get_search_box_placeholder() {
765 return esc_html__( 'Search', 'action-scheduler' );
766 }
767
768 /**
769 * Gets the screen per_page option name.
770 *
771 * @return string
772 */
773 protected function get_per_page_option_name() {
774 return $this->package . '_items_per_page';
775 }
776 }
777