PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / inc / Dependencies / ActionScheduler / classes / abstracts / ActionScheduler_Abstract_ListTable.php

ActionScheduler_Abstract_ListTable.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF trunk, at inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php

798 lines 24.5 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 * Querystring arguments to persist between form submissions.
318 *
319 * @since 3.7.3
320 *
321 * @return string[]
322 */
323 protected function get_request_query_args_to_persist() {
324 return array_merge(
325 $this->sort_by,
326 array(
327 'page',
328 'status',
329 'tab',
330 )
331 );
332 }
333
334 /**
335 * Return the sortable column specified for this request to order the results by, if any.
336 *
337 * @return string
338 */
339 protected function get_request_orderby() {
340
341 $valid_sortable_columns = array_values( $this->sort_by );
342
343 if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
344 $orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
345 } else {
346 $orderby = $valid_sortable_columns[0];
347 }
348
349 return $orderby;
350 }
351
352 /**
353 * Return the sortable column order specified for this request.
354 *
355 * @return string
356 */
357 protected function get_request_order() {
358
359 if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
360 $order = 'DESC';
361 } else {
362 $order = 'ASC';
363 }
364
365 return $order;
366 }
367
368 /**
369 * Return the status filter for this request, if any.
370 *
371 * @return string
372 */
373 protected function get_request_status() {
374 $status = ( ! empty( $_GET['status'] ) ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
375 return $status;
376 }
377
378 /**
379 * Return the search filter for this request, if any.
380 *
381 * @return string
382 */
383 protected function get_request_search_query() {
384 $search_query = ( ! empty( $_GET['s'] ) ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
385 return $search_query;
386 }
387
388 /**
389 * Process and return the columns name. This is meant for using with SQL, this means it
390 * always includes the primary key.
391 *
392 * @return array
393 */
394 protected function get_table_columns() {
395 $columns = array_keys( $this->columns );
396 if ( ! in_array( $this->ID, $columns, true ) ) {
397 $columns[] = $this->ID;
398 }
399
400 return $columns;
401 }
402
403 /**
404 * Check if the current request is doing a "full text" search. If that is the case
405 * prepares the SQL to search texts using LIKE.
406 *
407 * If the current request does not have any search or if this list table does not support
408 * that feature it will return an empty string.
409 *
410 * @return string
411 */
412 protected function get_items_query_search() {
413 global $wpdb;
414
415 if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
416 return '';
417 }
418
419 $search_string = sanitize_text_field( wp_unslash( $_GET['s'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
420
421 $filter = array();
422 foreach ( $this->search_by as $column ) {
423 $wild = '%';
424 $sql_like = $wild . $wpdb->esc_like( $search_string ) . $wild;
425 $filter[] = $wpdb->prepare( '`' . $column . '` LIKE %s', $sql_like ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.NotPrepared
426 }
427 return implode( ' OR ', $filter );
428 }
429
430 /**
431 * Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting
432 * any data sent by the user it validates that it is a valid option.
433 */
434 protected function get_items_query_filters() {
435 global $wpdb;
436
437 if ( ! $this->filter_by || empty( $_GET['filter_by'] ) || ! is_array( $_GET['filter_by'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
438 return '';
439 }
440
441 $filter = array();
442
443 foreach ( $this->filter_by as $column => $options ) {
444 if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
445 continue;
446 }
447
448 $filter[] = $wpdb->prepare( "`$column` = %s", sanitize_text_field( wp_unslash( $_GET['filter_by'][ $column ] ) ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
449 }
450
451 return implode( ' AND ', $filter );
452
453 }
454
455 /**
456 * Prepares the data to feed WP_Table_List.
457 *
458 * This has the core for selecting, sorting and filtering data. To keep the code simple
459 * its logic is split among many methods (get_items_query_*).
460 *
461 * Beside populating the items this function will also count all the records that matches
462 * the filtering criteria and will do fill the pagination variables.
463 */
464 public function prepare_items() {
465 global $wpdb;
466
467 $this->process_bulk_action();
468
469 $this->process_row_actions();
470
471 if ( ! empty( $_REQUEST['_wp_http_referer'] && ! empty( $_SERVER['REQUEST_URI'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
472 // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
473 wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
474 exit;
475 }
476
477 $this->prepare_column_headers();
478
479 $limit = $this->get_items_query_limit();
480 $offset = $this->get_items_query_offset();
481 $order = $this->get_items_query_order();
482 $where = array_filter(
483 array(
484 $this->get_items_query_search(),
485 $this->get_items_query_filters(),
486 )
487 );
488 $columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`';
489
490 if ( ! empty( $where ) ) {
491 $where = 'WHERE (' . implode( ') AND (', $where ) . ')';
492 } else {
493 $where = '';
494 }
495
496 $sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}";
497
498 $this->set_items( $wpdb->get_results( $sql, ARRAY_A ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
499
500 $query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}";
501 $total_items = $wpdb->get_var( $query_count ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
502 $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page );
503 $this->set_pagination_args(
504 array(
505 'total_items' => $total_items,
506 'per_page' => $per_page,
507 'total_pages' => ceil( $total_items / $per_page ),
508 )
509 );
510 }
511
512 /**
513 * Display the table.
514 *
515 * @param string $which The name of the table.
516 */
517 public function extra_tablenav( $which ) {
518 if ( ! $this->filter_by || 'top' !== $which ) {
519 return;
520 }
521
522 echo '<div class="alignleft actions">';
523
524 foreach ( $this->filter_by as $id => $options ) {
525 $default = ! empty( $_GET['filter_by'][ $id ] ) ? sanitize_text_field( wp_unslash( $_GET['filter_by'][ $id ] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
526 if ( empty( $options[ $default ] ) ) {
527 $default = '';
528 }
529
530 echo '<select name="filter_by[' . esc_attr( $id ) . ']" class="first" id="filter-by-' . esc_attr( $id ) . '">';
531
532 foreach ( $options as $value => $label ) {
533 echo '<option value="' . esc_attr( $value ) . '" ' . esc_html( $value === $default ? 'selected' : '' ) . '>'
534 . esc_html( $label )
535 . '</option>';
536 }
537
538 echo '</select>';
539 }
540
541 submit_button( esc_html__( 'Filter', 'action-scheduler' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
542 echo '</div>';
543 }
544
545 /**
546 * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns
547 * are serialized). This can be override in child classes for further data transformation.
548 *
549 * @param array $items Items array.
550 */
551 protected function set_items( array $items ) {
552 $this->items = array();
553 foreach ( $items as $item ) {
554 $this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item );
555 }
556 }
557
558 /**
559 * Renders the checkbox for each row, this is the first column and it is named ID regardless
560 * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper
561 * name transformation though using `$this->ID`.
562 *
563 * @param array $row The row to render.
564 */
565 public function column_cb( $row ) {
566 return '<input name="ID[]" type="checkbox" value="' . esc_attr( $row[ $this->ID ] ) . '" />';
567 }
568
569 /**
570 * Renders the row-actions.
571 *
572 * This method renders the action menu, it reads the definition from the $row_actions property,
573 * and it checks that the row action method exists before rendering it.
574 *
575 * @param array $row Row to be rendered.
576 * @param string $column_name Column name.
577 * @return string
578 */
579 protected function maybe_render_actions( $row, $column_name ) {
580 if ( empty( $this->row_actions[ $column_name ] ) ) {
581 return;
582 }
583
584 $row_id = $row[ $this->ID ];
585
586 $actions = '<div class="row-actions">';
587 $action_count = 0;
588 foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) {
589
590 $action_count++;
591
592 if ( ! method_exists( $this, 'row_action_' . $action_key ) ) {
593 continue;
594 }
595
596 $action_link = ! empty( $action['link'] ) ? $action['link'] : add_query_arg(
597 array(
598 'row_action' => $action_key,
599 'row_id' => $row_id,
600 'nonce' => wp_create_nonce( $action_key . '::' . $row_id ),
601 )
602 );
603 $span_class = ! empty( $action['class'] ) ? $action['class'] : $action_key;
604 $separator = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : '';
605
606 $actions .= sprintf( '<span class="%s">', esc_attr( $span_class ) );
607 $actions .= sprintf( '<a href="%1$s" title="%2$s">%3$s</a>', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) );
608 $actions .= sprintf( '%s</span>', $separator );
609 }
610 $actions .= '</div>';
611 return $actions;
612 }
613
614 /**
615 * Process the bulk actions.
616 *
617 * @return void
618 */
619 protected function process_row_actions() {
620 $parameters = array( 'row_action', 'row_id', 'nonce' );
621 foreach ( $parameters as $parameter ) {
622 if ( empty( $_REQUEST[ $parameter ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
623 return;
624 }
625 }
626
627 $action = sanitize_text_field( wp_unslash( $_REQUEST['row_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
628 $row_id = sanitize_text_field( wp_unslash( $_REQUEST['row_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
629 $nonce = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
630 $method = 'row_action_' . $action; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
631
632 if ( wp_verify_nonce( $nonce, $action . '::' . $row_id ) && method_exists( $this, $method ) ) {
633 $this->$method( sanitize_text_field( wp_unslash( $row_id ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
634 }
635
636 if ( isset( $_SERVER['REQUEST_URI'] ) ) {
637 wp_safe_redirect(
638 remove_query_arg(
639 array( 'row_id', 'row_action', 'nonce' ),
640 esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
641 )
642 );
643 exit;
644 }
645 }
646
647 /**
648 * Default column formatting, it will escape everything for security.
649 *
650 * @param array $item The item array.
651 * @param string $column_name Column name to display.
652 *
653 * @return string
654 */
655 public function column_default( $item, $column_name ) {
656 $column_html = esc_html( $item[ $column_name ] );
657 $column_html .= $this->maybe_render_actions( $item, $column_name );
658 return $column_html;
659 }
660
661 /**
662 * Display the table heading and search query, if any
663 */
664 protected function display_header() {
665 echo '<h1 class="wp-heading-inline">' . esc_attr( $this->table_header ) . '</h1>';
666 if ( $this->get_request_search_query() ) {
667 /* translators: %s: search query */
668 echo '<span class="subtitle">' . esc_attr( sprintf( __( 'Search results for "%s"', 'action-scheduler' ), $this->get_request_search_query() ) ) . '</span>';
669 }
670 echo '<hr class="wp-header-end">';
671 }
672
673 /**
674 * Display the table heading and search query, if any
675 */
676 protected function display_admin_notices() {
677 foreach ( $this->admin_notices as $notice ) {
678 echo '<div id="message" class="' . esc_attr( $notice['class'] ) . '">';
679 echo ' <p>' . wp_kses_post( $notice['message'] ) . '</p>';
680 echo '</div>';
681 }
682 }
683
684 /**
685 * Prints the available statuses so the user can click to filter.
686 */
687 protected function display_filter_by_status() {
688
689 $status_list_items = array();
690 $request_status = $this->get_request_status();
691
692 // Helper to set 'all' filter when not set on status counts passed in.
693 if ( ! isset( $this->status_counts['all'] ) ) {
694 $all_count = array_sum( $this->status_counts );
695 if ( isset( $this->status_counts['past-due'] ) ) {
696 $all_count -= $this->status_counts['past-due'];
697 }
698 $this->status_counts = array( 'all' => $all_count ) + $this->status_counts;
699 }
700
701 // Translated status labels.
702 $status_labels = ActionScheduler_Store::instance()->get_status_labels();
703 $status_labels['all'] = esc_html_x( 'All', 'status labels', 'action-scheduler' );
704 $status_labels['past-due'] = esc_html_x( 'Past-due', 'status labels', 'action-scheduler' );
705
706 foreach ( $this->status_counts as $status_slug => $count ) {
707
708 if ( 0 === $count ) {
709 continue;
710 }
711
712 if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) {
713 $status_list_item = '<li class="%1$s"><a href="%2$s" class="current">%3$s</a> (%4$d)</li>';
714 } else {
715 $status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>';
716 }
717
718 $status_name = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug );
719 $status_filter_url = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug );
720 $status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url );
721 $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) );
722 }
723
724 if ( $status_list_items ) {
725 echo '<ul class="subsubsub">';
726 echo implode( " | \n", $status_list_items ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
727 echo '</ul>';
728 }
729 }
730
731 /**
732 * Renders the table list, we override the original class to render the table inside a form
733 * and to render any needed HTML (like the search box). By doing so the callee of a function can simple
734 * forget about any extra HTML.
735 */
736 protected function display_table() {
737 echo '<form id="' . esc_attr( $this->_args['plural'] ) . '-filter" method="get">';
738 foreach ( $this->get_request_query_args_to_persist() as $arg ) {
739 $arg_value = isset( $_GET[ $arg ] ) ? sanitize_text_field( wp_unslash( $_GET[ $arg ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
740 if ( ! $arg_value ) {
741 continue;
742 }
743
744 echo '<input type="hidden" name="' . esc_attr( $arg ) . '" value="' . esc_attr( $arg_value ) . '" />';
745 }
746
747 if ( ! empty( $this->search_by ) ) {
748 echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
749 }
750 parent::display();
751 echo '</form>';
752 }
753
754 /**
755 * Process any pending actions.
756 */
757 public function process_actions() {
758 $this->process_bulk_action();
759 $this->process_row_actions();
760
761 if ( ! empty( $_REQUEST['_wp_http_referer'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
762 // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter
763 wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) );
764 exit;
765 }
766 }
767
768 /**
769 * Render the list table page, including header, notices, status filters and table.
770 */
771 public function display_page() {
772 $this->prepare_items();
773
774 echo '<div class="wrap">';
775 $this->display_header();
776 $this->display_admin_notices();
777 $this->display_filter_by_status();
778 $this->display_table();
779 echo '</div>';
780 }
781
782 /**
783 * Get the text to display in the search box on the list table.
784 */
785 protected function get_search_box_placeholder() {
786 return esc_html__( 'Search', 'action-scheduler' );
787 }
788
789 /**
790 * Gets the screen per_page option name.
791 *
792 * @return string
793 */
794 protected function get_per_page_option_name() {
795 return $this->package . '_items_per_page';
796 }
797 }
798