PluginProbe
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards / 5.5.77
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards v5.5.77
5.5.84 5.5.83 5.5.82 5.5.81 5.5.80 5.5.79 5.5.77 5.5.76 5.5.75 5.5.73 5.5.72 5.5.22 5.5.23 5.5.29 5.5.3 5.5.31 5.5.32 5.5.34 5.5.35 5.5.36 5.5.37 5.5.4 5.5.40 5.5.41 5.5.42 All 160 releases
wp-data-access / WPDataAccess / List_Table / WPDA_List_Table.php

WPDA_List_Table.php in WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards 5.5.77, at WPDataAccess/List_Table/WPDA_List_Table.php

2,803 lines 112.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Suppress "error - 0 - No summary was found for this file" on phpdoc generation
5 *
6 * @package WPDataAccess\List_Table
7 */
8 // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- verified on page
9 namespace WPDataAccess\List_Table;
10
11 use WPDataAccess\Connection\WPDADB;
12 use WPDataAccess\Dashboard\WPDA_Dashboard;
13 use WPDataAccess\Data_Dictionary\WPDA_Dictionary_Exist;
14 use WPDataAccess\Data_Dictionary\WPDA_Dictionary_Lists;
15 use WPDataAccess\Data_Dictionary\WPDA_List_Columns;
16 use WPDataAccess\Macro\WPDA_Macro;
17 use WPDataAccess\Plugin_Table_Models\WPDA_CSV_Uploads_Model;
18 use WPDataAccess\Plugin_Table_Models\WPDA_Media_Model;
19 use WPDataAccess\Utilities\WPDA_Import;
20 use WPDataAccess\Utilities\WPDA_Message_Box;
21 use WPDataAccess\Utilities\WPDA_Repository;
22 use WPDataAccess\Wordpress_Original;
23 use WPDataAccess\WPDA;
24 use WPDataProjects\WPDP;
25 use WPDataAccess\Plugin_Table_Models\WPDA_Table_Settings_Model;
26 use WPDataAccess\Plugin_Table_Models\WPDP_Project_Design_Table_Model;
27 /**
28 * Class WPDA_List_Table
29 *
30 * This class extends WordPress class WP_List_Table. The WordPress WP_List_Table is contained in the package as
31 * advised by WordPress and might be updated in future releases.
32 *
33 * @author Peter Schulz
34 * @since 1.0.0
35 */
36 class WPDA_List_Table extends Wordpress_Original\WP_List_Table {
37 /**
38 * Base table for list of all tables
39 */
40 const LIST_BASE_TABLE = 'information_schema.tables';
41
42 /**
43 * Default value bulk actions enabled
44 */
45 const DEFAULT_BULK_ACTIONS_ENABLED = true;
46
47 /**
48 * Default value bulk export enabled
49 */
50 const DEFAULT_BULK_EXPORT_ENABLED = true;
51
52 /**
53 * Default value bulk search enabled
54 */
55 const DEFAULT_SEARCH_BOX_ENABLED = true;
56
57 /**
58 * Table row number within the current response
59 *
60 * @var int
61 */
62 protected static $list_number = 0;
63
64 /**
65 * Menu slug of the current page
66 *
67 * @var string
68 */
69 protected $page;
70
71 /**
72 * Page title
73 *
74 * @var string
75 */
76 protected $title;
77
78 /**
79 * Page subtitle
80 *
81 * @var string
82 */
83 protected $subtitle = '';
84
85 protected $show_page_title = true;
86
87 /**
88 * Database schema name
89 *
90 * @var string
91 */
92 protected $schema_name = '';
93
94 /**
95 * Database table name
96 *
97 * @var string
98 */
99 protected $table_name = '';
100
101 /**
102 * Where clause
103 *
104 * @var string
105 */
106 protected $where = '';
107
108 /**
109 * Order by clause
110 *
111 * @var string
112 */
113 protected $orderby = '';
114
115 /**
116 * Columns in query
117 *
118 * @var array
119 */
120 protected $columns_queried;
121
122 /**
123 * Number of rows displayed in list box
124 *
125 * @var int
126 */
127 protected $items_per_page;
128
129 /**
130 * Page number
131 *
132 * @var int
133 */
134 protected $current_page = 1;
135
136 /**
137 * Add search box?
138 *
139 * @var bool
140 */
141 protected $search_box_enabled = self::DEFAULT_SEARCH_BOX_ENABLED;
142
143 /**
144 * Enable bulk actions?
145 *
146 * @var bool
147 */
148 protected $bulk_actions_enabled = self::DEFAULT_BULK_ACTIONS_ENABLED;
149
150 /**
151 * Enable exports?
152 *
153 * @var bool
154 */
155 protected $bulk_export_enabled = self::DEFAULT_BULK_EXPORT_ENABLED;
156
157 /**
158 * Show view link?
159 *
160 * @var string on|off
161 */
162 protected $show_view_link = null;
163
164 /**
165 * Allow inserts?
166 *
167 * @var string on|off
168 */
169 protected $allow_insert = null;
170
171 /**
172 * Allow updates?
173 *
174 * @var string on|off
175 */
176 protected $allow_update = null;
177
178 /**
179 * Allow deletes?
180 *
181 * @var string on|off
182 */
183 protected $allow_delete = null;
184
185 /**
186 * Hides tablenav
187 *
188 * @var bool
189 */
190 protected $hide_navigation = false;
191
192 /**
193 * Reference to column list
194 *
195 * @var WPDA_List_Columns
196 */
197 protected $wpda_list_columns;
198
199 /**
200 * Reference to dictionary object
201 *
202 * @var WPDA_Dictionary_Exist
203 */
204 protected $wpda_data_dictionary;
205
206 /**
207 * Column headers (labels)
208 *
209 * @var array
210 */
211 protected $column_headers;
212
213 /**
214 * Reference to import object
215 *
216 * @var WPDA_Import
217 */
218 protected $wpda_import = null;
219
220 /**
221 * Child tab clicked (used for parent child relationships only)
222 *
223 * @var null
224 */
225 protected $child_tab = '';
226
227 /**
228 * Search string (entered by user or taken from cookie)
229 *
230 * @var null|string
231 */
232 protected $search_value = null;
233
234 /**
235 * Previous search string
236 *
237 * @var null|string
238 */
239 protected $search_value_old = null;
240
241 /**
242 * Page number item name (default 'page_number')
243 *
244 * The name can be changed for pages on multiple levels. This is needed to get back to the right page in
245 * parent-child page.
246 *
247 * @var string
248 */
249 protected $page_number_item_name = 'page_number';
250
251 /**
252 * Real page number
253 *
254 * @var null
255 */
256 protected $page_number_link = '';
257
258 /**
259 * Page number text item
260 *
261 * @var null
262 */
263 protected $page_number_item = '';
264
265 /**
266 * Make the default accessible to other classes
267 */
268 const SEARCH_ITEM_NAME_DEFAULT = 'wpda_s';
269
270 /**
271 * Name of search item (WP default 's')
272 *
273 * @var string
274 */
275 protected $search_item_name = self::SEARCH_ITEM_NAME_DEFAULT;
276
277 /**
278 * Name of the column containing action links
279 *
280 * Default is the first column displayed
281 *
282 * @var string
283 */
284 protected $first_display_column = '';
285
286 /**
287 * Table settings
288 *
289 * @var null|object
290 */
291 protected $wpda_table_settings = null;
292
293 /**
294 * Project table settings
295 *
296 * @var null|object
297 */
298 protected $wpda_project_table_settings = null;
299
300 /**
301 * Show help icon if help url is available
302 *
303 * @var null|string
304 */
305 protected $help_url = null;
306
307 /**
308 * Store columns in array with column name as index for fast access
309 *
310 * @var array
311 */
312 protected $columns_indexed = array();
313
314 /**
315 * Variables used to store table estimate row count data
316 *
317 * @var string
318 */
319 protected $table_engine = '';
320
321 /**
322 * Table rows
323 *
324 * @var mixed|string
325 */
326 protected $table_rows = '';
327
328 /**
329 * Estimated row count
330 *
331 * @var array|int
332 */
333 protected $row_count_estimate = array();
334
335 /**
336 * Project page id
337 *
338 * Helper to hide schema and table name in export links on web pages
339 *
340 * @var null
341 */
342 protected $pid = null;
343
344 /**
345 * Cache columns
346 *
347 * @var null
348 */
349 protected $wpda_cached_columns = null;
350
351 /**
352 * WPDA_List_Table constructor
353 *
354 * A table name must be provided in the constructor call. The table must be a valid MySQL database table to
355 * which access (to the back-end) is granted. If no table name is provided, the table does not exist or access
356 * to the back-end for the given table is not granted, processing is stopped. It makes no sense to continue
357 * without a valid table. A check for table existence is performed to prevent SQL injection.
358 *
359 * There are two types of tables to build a list table on:
360 * + List of tables in the WordPress database schema
361 * + List of rows in a specific table
362 *
363 * To build a list of tables in the WordPress database schema, we query table 'information_schema.tables'
364 * (which is in fact a view). This is the only query allowed outside the WordPress database schema. The
365 * table/view name is stored in constant WPDA_List_Table::LIST_BASE_TABLE for validation purposes.
366 *
367 * A list of rows for a specific table is based on WordPress class WP_List_Table.
368 *
369 * WPDA_List_Table can be used to build list tables for views as well. View based list tables however, do not
370 * support insert, update, delete, import and export actions.
371 *
372 * A table name is not the only thing we need to build a list table. We also need to have access to the
373 * table columns. If no table columns are provided execution is stopped as well.
374 *
375 * @param array $args [
376 *
377 * 'table_name' => (string) Database table name
378 *
379 * 'wpda_list_columns' => (object) Reference to a WPDA_List_Columns object
380 *
381 * 'singular' => (string) Singular object label
382 *
383 * 'plural' => (string) plural object label
384 *
385 * 'ajax' => (string) TRUE = list table support Ajax
386 *
387 * 'column_headers' => (array|string) Column headers
388 *
389 * 'title' => (string) Page title
390 *
391 * 'subtitle' => (string) Page subtitle
392 *
393 * 'bulk_export_enabled' => (boolean)
394 *
395 * 'search_box_enabled' => (boolean)
396 *
397 * 'bulk_actions_enabled' => (boolean)
398 *
399 * 'show_view_link' => (string) on|off
400 *
401 * 'allow_insert' => (string) on|off
402 *
403 * 'allow_update' => (string) on|off
404 *
405 * 'allow_delete' => (string) on|off
406 *
407 * 'allow_import' => (string) on|off
408 *
409 * 'hide_navigation' => (boolean)
410 *
411 * 'default_where' => (string)
412 *
413 * ]
414 * @since 1.0.0
415 */
416 public function __construct( $args = array() ) {
417 global $wpdb;
418 if ( !isset( $args['table_name'] ) ) {
419 // Calling WPDA_List_Table without a table_name doesn't make sense.
420 wp_die( esc_attr__( 'ERROR: Wrong arguments [no table argument]', 'wp-data-access' ) );
421 }
422 if ( !isset( $args['wpda_list_columns'] ) ) {
423 // Calling WPDA_List_Table without a column list is not allowed.
424 wp_die( esc_attr__( 'ERROR: Wrong arguments [no columns argument]', 'wp-data-access' ) );
425 }
426 parent::__construct( array(
427 'singular' => ( isset( $args['singular'] ) ? $args['singular'] : __( 'Row', 'wp-data-access' ) ),
428 'plural' => ( isset( $args['plural'] ) ? $args['plural'] : __( 'Rows', 'wp-data-access' ) ),
429 'ajax' => ( isset( $args['ajax'] ) ? $args['ajax'] : false ),
430 ) );
431 // Set schema name is available.
432 if ( isset( $args['wpdaschema_name'] ) ) {
433 $this->schema_name = $args['wpdaschema_name'];
434 }
435 // Set table name (availability already checked).
436 $this->table_name = $args['table_name'];
437 if ( self::LIST_BASE_TABLE !== $this->table_name ) {
438 // Whenever a table name is provided through a URL we are risking an SQL injection attack. Our
439 // defence mechanism here is to check whether the table name provided exists in our database.
440 // If it does not we'll terminate the process with an error.
441 // Although this check is only needed when a table name is provided through the URL we will perform
442 // it in all situations. It is a fast query which makes our application much more safe and reliable.
443 $this->wpda_data_dictionary = new WPDA_Dictionary_Exist($this->schema_name, $this->table_name);
444 if ( !$this->wpda_data_dictionary->table_exists() ) {
445 wp_die( esc_attr__( 'ERROR: Invalid table name or not authorized', 'wp-data-access' ) );
446 }
447 }
448 $this->pid = ( isset( $args['pid'] ) ? $args['pid'] : '' );
449 // Get menu slag of current page.
450 if ( isset( $_REQUEST['page'] ) ) {
451 $this->page = sanitize_text_field( wp_unslash( $_REQUEST['page'] ) );
452 } else {
453 // In order to show a list table we need a page.
454 wp_die( esc_attr__( 'ERROR: Wrong arguments [no page argument]', 'wp-data-access' ) );
455 }
456 // Use column list: argument wpda_list_columns (availability already checked).
457 $this->wpda_list_columns =& $args['wpda_list_columns'];
458 // Set pagination.
459 if ( is_admin() ) {
460 $this->items_per_page = WPDA::get_option( WPDA::OPTION_BE_PAGINATION );
461 } else {
462 $this->items_per_page = WPDA::get_option( WPDA::OPTION_FE_PAGINATION );
463 }
464 // Overwrite column header text if column headers were provided.
465 $this->column_headers = ( isset( $args['column_headers'] ) ? $args['column_headers'] : '' );
466 // Set title.
467 if ( isset( $args['title'] ) ) {
468 $this->title = $args['title'];
469 } else {
470 $this->title = __( 'Table', 'wp-data-access' ) . ' ' . strtoupper( $this->table_name );
471 }
472 // Set subtitle.
473 if ( isset( $args['subtitle'] ) ) {
474 $this->subtitle = $args['subtitle'];
475 } else {
476 $wp_tables = $wpdb->tables( 'all', true );
477 if ( isset( $wp_tables[substr( $this->table_name, strlen( $wpdb->prefix ) )] ) ) {
478 $this->subtitle = '<span class="dashicons dashicons-warning"></span> ' . WPDA::get_table_type_text( WPDA::TABLE_TYPE_WP );
479 } elseif ( WPDA::is_wpda_table( $this->table_name ) ) {
480 $this->subtitle = '<span class="dashicons dashicons-warning"></span> ' . WPDA::get_table_type_text( WPDA::TABLE_TYPE_WPDA );
481 }
482 }
483 if ( !(isset( $args['allow_import'] ) && 'off' === $args['allow_import']) ) {
484 try {
485 // Instantiate WPDA_Import.
486 $this->wpda_import = new WPDA_Import(( is_admin() ? "?page={$this->page}" : '' ), $this->schema_name, $this->table_name);
487 } catch ( \Exception $e ) {
488 // If import is turned off instantiation will fail. Handle is set to null (check in future calls).
489 $this->wpda_import = null;
490 }
491 }
492 if ( isset( $args['bulk_export_enabled'] ) ) {
493 $this->bulk_export_enabled = $args['bulk_export_enabled'];
494 }
495 if ( isset( $args['search_box_enabled'] ) ) {
496 $this->search_box_enabled = $args['search_box_enabled'];
497 }
498 if ( isset( $args['bulk_actions_enabled'] ) ) {
499 $this->bulk_actions_enabled = $args['bulk_actions_enabled'];
500 }
501 if ( 'on' !== WPDA::get_option( WPDA::OPTION_BE_VIEW_LINK ) ) {
502 $this->show_view_link = WPDA::get_option( WPDA::OPTION_BE_VIEW_LINK );
503 } else {
504 if ( isset( $args['show_view_link'] ) ) {
505 $this->show_view_link = $args['show_view_link'];
506 } else {
507 $this->show_view_link = WPDA::get_option( WPDA::OPTION_BE_VIEW_LINK );
508 }
509 }
510 if ( 'on' !== WPDA::get_option( WPDA::OPTION_BE_ALLOW_INSERT ) ) {
511 $this->allow_insert = WPDA::get_option( WPDA::OPTION_BE_ALLOW_INSERT );
512 } else {
513 if ( isset( $args['allow_insert'] ) ) {
514 $this->allow_insert = $args['allow_insert'];
515 } else {
516 $this->allow_insert = WPDA::get_option( WPDA::OPTION_BE_ALLOW_INSERT );
517 }
518 }
519 if ( 'on' !== WPDA::get_option( WPDA::OPTION_BE_ALLOW_UPDATE ) ) {
520 $this->allow_update = WPDA::get_option( WPDA::OPTION_BE_ALLOW_UPDATE );
521 } else {
522 if ( isset( $args['allow_update'] ) ) {
523 $this->allow_update = $args['allow_update'];
524 } else {
525 $this->allow_update = WPDA::get_option( WPDA::OPTION_BE_ALLOW_UPDATE );
526 }
527 }
528 if ( 'on' !== WPDA::get_option( WPDA::OPTION_BE_ALLOW_DELETE ) ) {
529 $this->allow_delete = WPDA::get_option( WPDA::OPTION_BE_ALLOW_DELETE );
530 } else {
531 if ( isset( $args['allow_delete'] ) ) {
532 $this->allow_delete = $args['allow_delete'];
533 } else {
534 $this->allow_delete = WPDA::get_option( WPDA::OPTION_BE_ALLOW_DELETE );
535 }
536 }
537 if ( isset( $args['hide_navigation'] ) ) {
538 $this->hide_navigation = $args['hide_navigation'];
539 }
540 $this->search_value = ( is_scalar( $this->get_search_value() ) ? str_replace( "\\'", '', (string) $this->get_search_value() ) : '' );
541 if ( isset( $_REQUEST["{$this->search_item_name}_old_value"] ) ) {
542 $this->search_value_old = sanitize_text_field( wp_unslash( $_REQUEST["{$this->search_item_name}_old_value"] ) );
543 } else {
544 $this->search_value_old = $this->search_value;
545 }
546 // Get page number(s).
547 if ( 'page_number' !== $this->page_number_item_name ) {
548 if ( isset( $_REQUEST['page_number'] ) ) {
549 $requested_page_number = sanitize_text_field( wp_unslash( $_REQUEST['page_number'] ) );
550 $this->page_number_link = '&page_number=' . $requested_page_number;
551 $this->page_number_item = "<input type='hidden' name='page_number' value='" . $requested_page_number . "' />";
552 }
553 }
554 $this->page_number_link .= '&paged=' . $this->get_pagenum();
555 $this->page_number_item .= "<input type='hidden' name='" . esc_attr( $this->page_number_item_name ) . "' value='" . $this->get_pagenum() . "' />";
556 // Add search arguments to link to return to same page.
557 foreach ( $_REQUEST as $key => $value ) {
558 if ( substr( $key, 0, 19 ) === 'wpda_search_column_' && count( array_filter( $this->wpda_list_columns->get_table_columns(), function ( $column ) use($key) {
559 return $column['column_name'] === substr( $key, 19 );
560 } ) ) > 0 ) {
561 if ( is_array( $value ) ) {
562 foreach ( $value as $elem_key => $elem_value ) {
563 $this->page_number_link .= '&' . esc_attr( $elem_key ) . '=' . esc_attr( $elem_value );
564 $this->page_number_item .= "<input type='hidden' name='{" . esc_attr( $elem_key ) . "[]' value='" . esc_attr( $elem_value ) . "' />";
565 }
566 } else {
567 $this->page_number_link .= '&' . esc_attr( $key ) . '=' . esc_attr( $value );
568 $this->page_number_item .= "<input type='hidden' name='{" . esc_attr( $key ) . "' value='" . esc_attr( $value ) . "' />";
569 }
570 }
571 }
572 // Check if a WHERE clause (filter) was defined.
573 if ( isset( $args['default_where'] ) ) {
574 $this->where = $args['default_where'];
575 }
576 // Get table settings.
577 $wpda_table_settings = WPDA_Table_Settings_Model::query( $this->table_name, $this->schema_name );
578 if ( isset( $wpda_table_settings[0]['wpda_table_settings'] ) ) {
579 $this->wpda_table_settings = json_decode( $wpda_table_settings[0]['wpda_table_settings'] );
580 }
581 // Get estimated row count.
582 $this->row_count_estimate = WPDA::get_row_count_estimate( $this->schema_name, $this->table_name, $this->wpda_table_settings );
583 $this->table_rows = $this->row_count_estimate['row_count'];
584 // Get project table settings.
585 global $wpda_project_mode;
586 if ( is_array( $wpda_project_mode ) ) {
587 $setname = $wpda_project_mode['setname'];
588 $wpda_project_table_settings = null;
589 $wpda_project_table_settings_db = WPDP_Project_Design_Table_Model::static_query( $this->schema_name, $this->table_name, $setname );
590 if ( isset( $wpda_project_table_settings_db->tableinfo ) ) {
591 $this->wpda_project_table_settings = $wpda_project_table_settings_db->tableinfo;
592 }
593 }
594 if ( isset( $args['show_page_title'] ) && false === $args['show_page_title'] ) {
595 $this->show_page_title = $args['show_page_title'];
596 }
597 if ( isset( $args['help_url'] ) ) {
598 $this->help_url = $args['help_url'];
599 }
600 foreach ( $this->wpda_list_columns->get_table_columns() as $table_columns ) {
601 $this->columns_indexed[$table_columns['column_name']] = $table_columns;
602 }
603 }
604
605 /**
606 * Overwrite method
607 *
608 * @return int|void
609 */
610 public function get_pagenum() {
611 if ( isset( $_REQUEST[$this->page_number_item_name] ) ) {
612 $pagenum = ( isset( $_REQUEST[$this->page_number_item_name] ) ? absint( $_REQUEST[$this->page_number_item_name] ) : 0 );
613 if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) {
614 $pagenum = $this->_pagination_args['total_pages'];
615 }
616 return max( 1, $pagenum );
617 } else {
618 return parent::get_pagenum();
619 }
620 }
621
622 /**
623 * Set columns to be queries
624 *
625 * Set columns to be queried and shown in the list table.
626 * By default all columns in the table will be displayed.
627 *
628 * @param mixed $columns_queried Column list.
629 *
630 * @since 1.0.0
631 */
632 public function set_columns_queried( $columns_queried ) {
633 $this->columns_queried = $columns_queried;
634 }
635
636 /**
637 * Enable or disable bulk actions
638 *
639 * @param boolean $bulk_actions_enabled TRUE = allowed, FALSE = not allowed.
640 *
641 * @since 1.0.0
642 */
643 public function set_bulk_actions_enabled( $bulk_actions_enabled ) {
644 $this->bulk_actions_enabled = $bulk_actions_enabled;
645 }
646
647 /**
648 * Enable or disable bulk export options
649 *
650 * @param boolean $bulk_export_enabled TRUE = allowed, FALSE = not allowed.
651 *
652 * @since 1.0.0
653 */
654 public function set_bulk_export_enabled( $bulk_export_enabled ) {
655 $this->bulk_export_enabled = $bulk_export_enabled;
656 }
657
658 /**
659 * Show or hide search box
660 *
661 * @param boolean $search_box_enabled TRUE = show search box, FALSE = do not show search bow.
662 *
663 * @since 1.0.0
664 */
665 public function set_search_box_enabled( $search_box_enabled ) {
666 $this->search_box_enabled = $search_box_enabled;
667 }
668
669 /**
670 * Set page title
671 *
672 * @param string $title Page title.
673 *
674 * @since 1.0.0
675 */
676 public function set_title( $title ) {
677 $this->title = $title;
678 }
679
680 /**
681 * Set page subtitle
682 *
683 * @param string $subtitle Page subtitle.
684 *
685 * @since 1.0.0
686 */
687 public function set_subtitle( $subtitle ) {
688 $this->subtitle = $subtitle;
689 }
690
691 /**
692 * I18n text displayed when no data found
693 *
694 * @since 1.0.0
695 */
696 public function no_items() {
697 echo esc_attr__( 'No data found', 'wp-data-access' );
698 }
699
700 /**
701 * Use this method to build parent child relationships.
702 *
703 * Overwrite this function if you want to use the list table as a child list table related to some parent
704 * form. You can add parent arguments to calls to make sure you get back to the right parent.
705 *
706 * @param array $item Column info.
707 * @return string String contains parent arguments.
708 * @since 1.5.0
709 */
710 protected function add_parent_args_as_string( $item ) {
711 return '';
712 }
713
714 /**
715 * Render columns
716 *
717 * Three links are provided for every list table row:
718 * + 'View' => Link to view form (readonly data entry form)
719 * + 'Edit' => Link to data entry form (updatable)
720 * + 'Delete' => Link to delete record from database table
721 *
722 * Links to view, edit and delete records are only available if a primary key for the table is found. Without
723 * a primary key unique identification of records is not possible.
724 *
725 * Links to action are offered through hidden forms to prevent long URL's and problem when refreshing pages
726 * manually. More information about how and why is provided in source code at place where I thought the
727 * information could be helpful.
728 *
729 * @param array $item Column info.
730 * @param string $column_name Column name.
731 *
732 * @return mixed Value display in list table column
733 * @since 1.0.0
734 */
735 public function column_default( $item, $column_name ) {
736 if ( $this->wpda_list_columns->get_table_columns()[0]['column_name'] === $column_name || $column_name === $this->first_display_column ) {
737 // First column: add row actions.
738 $count = count( $this->wpda_list_columns->get_table_primary_key() );
739 // phpcs:ignore -- 8.1 proof
740 if ( 0 === $count ) {
741 // No actions without a primary key!
742 // This automatically covers view processing correctly.
743 return $this->render_column_content( $item, $column_name ) . $this->row_actions( array(
744 'wpda_hide_column' => '',
745 ) );
746 } else {
747 // Check rights.
748 if ( 'off' === $this->show_view_link && 'off' === $this->allow_update && 'off' === $this->allow_delete ) {
749 // No rights!
750 $actions = array();
751 $this->column_default_add_action( $item, $column_name, $actions );
752 if ( is_array( $actions ) && count( $actions ) > 0 ) {
753 // phpcs:ignore -- 8.1 proof
754 return sprintf( '%1$s %2$s', $this->render_column_content( $item, $column_name ), $this->row_actions( $actions ) );
755 } else {
756 return sprintf( '%1$s', $this->render_column_content( $item, $column_name ) );
757 }
758 }
759 // Check if row security is enabled.
760 $row_security = isset( $this->wpda_table_settings->table_settings->row_level_security ) && 'true' === $this->wpda_table_settings->table_settings->row_level_security;
761 // To prevent our URLs containing many arguments, we'll use post to submit row actions. Since our
762 // list table is build within a form and we cannot use nested forms we'll use a container (id =
763 // wpda_invisible_container) defined outside the list table, and add our forms to that container.
764 // We'll use jQuery to add our forms to the container. From the links in our rows we can then just
765 // submit any form in that container with jQuery as well.
766 $form_id = '_' . self::$list_number++;
767 $wp_nonce_keys = '';
768 $row_security_keys = '';
769 // We need to add keys and values for multi-column primary keys.
770 foreach ( $this->wpda_list_columns->get_table_primary_key() as $key ) {
771 $wp_nonce_keys .= "-{$item[$key]}";
772 if ( $row_security ) {
773 $row_security_keys .= "-{$key}-{$item[$key]}";
774 }
775 }
776 if ( $row_security ) {
777 $row_security_nonce = wp_create_nonce( "wpda-row-level-security-{$this->table_name}{$row_security_keys}" );
778 $row_security_nonce_field = "<input type='hidden' name='rownonce' value='" . esc_attr( $row_security_nonce ) . "' />";
779 } else {
780 $row_security_nonce_field = '';
781 }
782 // Prepare url.
783 if ( is_admin() ) {
784 $url = "?page={$this->page}";
785 } else {
786 $url = '';
787 }
788 if ( 'on' === $this->show_view_link ) {
789 $wp_nonce_action = "wpda-query-{$this->table_name}{$wp_nonce_keys}";
790 $wp_nonce = wp_create_nonce( $wp_nonce_action );
791 // Build the row action. Use jQuery to add form to container.
792 // All items escaped in create_post_form().
793 $view_form = $this->create_post_form(
794 $item,
795 "view_form{$form_id}",
796 'view',
797 $url,
798 $wp_nonce,
799 $row_security_nonce_field,
800 $this->schema_name,
801 $this->table_name
802 );
803 ?>
804
805 <script type='text/javascript'>
806 jQuery("#wpda_invisible_container").append("<?php
807 // phpcs:disable WordPress.Security.EscapeOutput
808 echo $view_form;
809 // phpcs:enable WordPress.Security.EscapeOutput
810 ?>");
811 </script>
812
813 <?php
814 // Add link to submit form.
815 $actions['view'] = sprintf(
816 '<a href="javascript:void(0)"
817 class="edit wpda_tooltip"
818 title="%s"
819 onclick="jQuery(\'#%s\').submit()">
820 <span style="white-space: nowrap">
821 <i class="fas fa-eye wpda_icon_on_button"></i>
822 %s
823 </span>
824 </a>
825 ',
826 __( 'View row data', 'wp-data-access' ),
827 "view_form{$form_id}",
828 __( 'View', 'wp-data-access' )
829 );
830 }
831 if ( 'on' === $this->allow_update || WPDA::is_wpda_table( $this->table_name ) ) {
832 $wp_nonce_action = "wpda-query-{$this->table_name}{$wp_nonce_keys}";
833 $wp_nonce = wp_create_nonce( $wp_nonce_action );
834 // Build the row action. Use jQuery to add form to container.
835 // All items escaped in create_post_form().
836 $edit_form = $this->create_post_form(
837 $item,
838 "edit_form{$form_id}",
839 'edit',
840 $url,
841 $wp_nonce,
842 $row_security_nonce_field,
843 $this->schema_name,
844 $this->table_name
845 );
846 ?>
847
848 <script type='text/javascript'>
849 jQuery("#wpda_invisible_container").append("<?php
850 // phpcs:disable WordPress.Security.EscapeOutput
851 echo $edit_form;
852 // phpcs:enable WordPress.Security.EscapeOutput
853 ?>");
854 </script>
855
856 <?php
857 // Add link to submit form.
858 $actions['edit'] = sprintf(
859 '<a href="javascript:void(0)"
860 class="edit wpda_tooltip"
861 title="%s"
862 onclick="jQuery(\'#%s\').submit()">
863 <span style="white-space: nowrap">
864 <i class="fas fa-pen wpda_icon_on_button"></i>
865 %s
866 </span>
867 </a>
868 ',
869 __( 'Edit row data', 'wp-data-access' ),
870 "edit_form{$form_id}",
871 __( 'Edit', 'wp-data-access' )
872 );
873 }
874 if ( 'on' === $this->allow_delete || WPDA::is_wpda_table( $this->table_name ) ) {
875 $wp_nonce_action = "wpda-delete-{$this->table_name}{$wp_nonce_keys}";
876 $wp_nonce = wp_create_nonce( $wp_nonce_action );
877 // Build the row action. Use jQuery to add form to container.
878 // All items escaped in create_post_form().
879 $delete_form = $this->create_post_form(
880 $item,
881 "delete_form{$form_id}",
882 'delete',
883 $url,
884 $wp_nonce,
885 $row_security_nonce_field,
886 $this->schema_name,
887 $this->table_name
888 );
889 ?>
890
891 <script type='text/javascript'>
892 jQuery("#wpda_invisible_container").append("<?php
893 // phpcs:disable WordPress.Security.EscapeOutput
894 echo $delete_form;
895 // phpcs:enable WordPress.Security.EscapeOutput
896 ?>");
897 </script>
898
899 <?php
900 // Add link to submit form.
901 $warning = __( "You are about to permanently delete these items from your site.\\nThis action cannot be undone.\\n\\'Cancel\\' to stop, \\'OK\\' to delete.", 'wp-data-access' );
902 $actions['delete'] = sprintf(
903 '<a href="javascript:void(0)"
904 class="delete wpda_tooltip"
905 title="%s"
906 onclick="if (confirm(\'%s\')) jQuery(\'#%s\').submit()">
907 <span style="white-space: nowrap">
908 <i class="fas fa-trash wpda_icon_on_button"></i>
909 %s
910 </span>
911 </a>
912 ',
913 __( 'Delete row data (this cannot be undone)', 'wp-data-access' ),
914 $warning,
915 "delete_form{$form_id}",
916 __( 'Delete', 'wp-data-access' )
917 );
918 }
919 // Developers can add actions by adding their own implementation of following method.
920 $this->column_default_add_action( $item, $column_name, $actions );
921 if ( has_filter( 'wpda_column_default' ) ) {
922 // Use filter.
923 $filter = apply_filters(
924 'wpda_column_default',
925 '',
926 $item,
927 $column_name,
928 $this->table_name,
929 $this->schema_name,
930 $this->wpda_list_columns,
931 self::$list_number,
932 $this
933 );
934 if ( null !== $filter ) {
935 return sprintf( '%1$s %2$s', $filter, $this->row_actions( $actions ) );
936 }
937 }
938 return sprintf( '%1$s %2$s', $this->render_column_content( $item, $column_name ), $this->row_actions( $actions ) );
939 }
940 } else {
941 if ( substr( $column_name, 0, 15 ) === 'wpda_hyperlink_' ) {
942 $hyperlink_no = substr( $column_name, 15 );
943 if ( isset( $this->wpda_table_settings->hyperlinks[$hyperlink_no] ) ) {
944 $hyperlink = $this->wpda_table_settings->hyperlinks[$hyperlink_no];
945 $hyperlink_html = ( isset( $hyperlink->hyperlink_html ) ? $hyperlink->hyperlink_html : '' );
946 if ( '' !== $hyperlink_html ) {
947 // Substitute values.
948 foreach ( $item as $key => $value ) {
949 $hyperlink_html = str_replace( "\$\${$key}\$\$", str_replace( ' ', '%20', (string) $value ), $hyperlink_html );
950 }
951 }
952 $macro = new WPDA_Macro($hyperlink_html);
953 $hyperlink_html = $macro->exe_macro();
954 if ( '' !== $hyperlink_html ) {
955 // Link is not empty AFTER substitution.
956 if ( false !== strpos( ltrim( $hyperlink_html ), '&lt;' ) ) {
957 return html_entity_decode( $hyperlink_html );
958 } else {
959 $hyperlink_label = ( isset( $hyperlink->hyperlink_label ) ? $hyperlink->hyperlink_label : '' );
960 $hyperlink_target = ( isset( $hyperlink->hyperlink_target ) ? $hyperlink->hyperlink_target : false );
961 $target = ( true === $hyperlink_target ? "target='_blank'" : '' );
962 return "<a href='" . str_replace( ' ', '+', trim( $hyperlink_html ) ) . "' {$target}>{$hyperlink_label}</a>";
963 }
964 } else {
965 return '';
966 }
967 }
968 return 'ERROR';
969 } else {
970 // Check if column is of type media.
971 $media_type = WPDA_Media_Model::get_column_media( $this->table_name, $column_name, $this->schema_name );
972 if ( 'Image' === $media_type ) {
973 $image_ids = explode( ',', (string) $item[$column_name] );
974 // phpcs:ignore -- 8.1 proof
975 $image_src = '';
976 foreach ( $image_ids as $image_id ) {
977 $url = wp_get_attachment_url( esc_attr( $image_id ) );
978 if ( false !== $url ) {
979 $title = get_the_title( esc_attr( $image_id ) );
980 $image_src .= ( '' !== $image_src ? '<br/>' : '' );
981 $image_src .= sprintf( '<img src="%s" class="wpda_tooltip" title="%s" width="100%%">', $url, $title );
982 }
983 }
984 return $image_src;
985 } elseif ( 'ImageURL' === $media_type ) {
986 return sprintf( '<img src="%s" class="wpda_tooltip" width="100%%">', $item[$column_name] );
987 } elseif ( 'Attachment' === $media_type ) {
988 $media_ids = explode( ',', (string) $item[$column_name] );
989 // phpcs:ignore -- 8.1 proof
990 $media_links = '';
991 foreach ( $media_ids as $media_id ) {
992 $url = wp_get_attachment_url( esc_attr( $media_id ) );
993 if ( false !== $url ) {
994 $mime_type = get_post_mime_type( $media_id );
995 if ( false !== $mime_type ) {
996 $title = get_the_title( esc_attr( $media_id ) );
997 $media_links .= self::column_media_attachment( $url, $title, $mime_type );
998 }
999 }
1000 }
1001 return $media_links;
1002 } elseif ( 'Hyperlink' === $media_type ) {
1003 if ( null === $item[$column_name] || '' === $item[$column_name] ) {
1004 return '';
1005 } else {
1006 if ( !(isset( $this->wpda_table_settings->table_settings->hyperlink_definition ) && 'text' === $this->wpda_table_settings->table_settings->hyperlink_definition) ) {
1007 $hyperlink = json_decode( $item[$column_name], true );
1008 if ( is_array( $hyperlink ) && isset( $hyperlink['label'] ) && isset( $hyperlink['url'] ) && isset( $hyperlink['target'] ) ) {
1009 if ( '' === $hyperlink['url'] ) {
1010 return '';
1011 } else {
1012 return "<a href='{$hyperlink['url']}' target='{$hyperlink['target']}'>{$hyperlink['label']}</a>";
1013 }
1014 } else {
1015 return '';
1016 }
1017 } else {
1018 $hyperlink_label = $this->wpda_list_columns->get_column_label( $column_name );
1019 return "<a href='{$item[$column_name]}' target='_blank'>{$hyperlink_label}</a>";
1020 }
1021 }
1022 } elseif ( 'Audio' === $media_type ) {
1023 $audio_ids = explode( ',', (string) $item[$column_name] );
1024 // phpcs:ignore -- 8.1 proof
1025 $audio_src = '';
1026 foreach ( $audio_ids as $audio_id ) {
1027 if ( 'audio' === substr( get_post_mime_type( $audio_id ), 0, 5 ) ) {
1028 $url = wp_get_attachment_url( esc_attr( $audio_id ) );
1029 if ( false !== $url ) {
1030 $title = get_the_title( esc_attr( $audio_id ) );
1031 if ( false !== $url ) {
1032 $audio_src .= '<div title="' . $title . '" class="wpda_tooltip">' . do_shortcode( '[audio src="' . $url . '"]' ) . '</div>';
1033 }
1034 }
1035 }
1036 }
1037 return $audio_src;
1038 } elseif ( 'Video' === $media_type ) {
1039 $video_ids = explode( ',', (string) $item[$column_name] );
1040 // phpcs:ignore -- 8.1 proof
1041 $video_src = '';
1042 foreach ( $video_ids as $video_id ) {
1043 if ( 'video' === substr( get_post_mime_type( $video_id ), 0, 5 ) ) {
1044 $url = wp_get_attachment_url( esc_attr( $video_id ) );
1045 if ( false !== $url ) {
1046 if ( false !== $url ) {
1047 $video_src .= do_shortcode( '[video src="' . $url . '"]' );
1048 }
1049 }
1050 }
1051 }
1052 return $video_src;
1053 }
1054 }
1055 if ( has_filter( 'wpda_column_default' ) ) {
1056 // Use filter.
1057 $filter = apply_filters(
1058 'wpda_column_default',
1059 '',
1060 $item,
1061 $column_name,
1062 $this->table_name,
1063 $this->schema_name,
1064 $this->wpda_list_columns,
1065 self::$list_number,
1066 $this
1067 );
1068 if ( null !== $filter ) {
1069 return $filter;
1070 }
1071 }
1072 if ( 'csv' !== WPDA::get_option( WPDA::OPTION_PLUGIN_SET_FORMAT ) && isset( $this->columns_indexed[$column_name]['data_type'] ) && 'set' === $this->columns_indexed[$column_name]['data_type'] ) {
1073 $list = '<' . WPDA::get_option( WPDA::OPTION_PLUGIN_SET_FORMAT ) . '>';
1074 $listarray = explode( ',', (string) $item[$column_name] );
1075 // phpcs:ignore -- 8.1 proof
1076 foreach ( $listarray as $listitem ) {
1077 $list .= "<li>{$listitem}</li>";
1078 }
1079 $list .= '</' . WPDA::get_option( WPDA::OPTION_PLUGIN_SET_FORMAT ) . '>';
1080 return $list;
1081 }
1082 return $this->render_column_content( $item, $column_name );
1083 }
1084 }
1085
1086 /**
1087 * Create post form which is added to the invisible container
1088 *
1089 * @param array $item Column info.
1090 * @param string $form_id Form ID.
1091 * @param string $action Form action.
1092 * @param string $url URL admin page.
1093 * @param string $wp_nonce Nonce.
1094 * @param string $row_security_nonce_field Nonce for row level access security.
1095 * @param string $schema_name Database schema name.
1096 * @param string $table_name Database table name.
1097 *
1098 * @return array|string|string[]
1099 */
1100 private function create_post_form(
1101 $item,
1102 $form_id,
1103 $action,
1104 $url,
1105 $wp_nonce,
1106 $row_security_nonce_field,
1107 $schema_name,
1108 $table_name
1109 ) {
1110 // Already escaped: $url, $row_security_nonce_field, get_key_input_fields(), add_parent_args_as_string().
1111 $esc_attr = 'esc_attr';
1112 $get_key_input_fields = $this->get_key_input_fields( $item );
1113 $add_parent_args_as_string = $this->add_parent_args_as_string( $item );
1114 $page_number_item = $this->page_number_item;
1115 $case_sensitive_search = ( isset( $_REQUEST['wpda_c'] ) && 'true' === $_REQUEST['wpda_c'] ? "<input type='hidden' name='wpda_c' value='true'>" : '' );
1116 $add_schema_and_table_name = ( !is_admin() ? '' : "\n\t\t\t\t\t<input type='hidden' name='wpdaschema_name' value='{$esc_attr( $schema_name )}' />\n\t\t\t\t\t<input type='hidden' name='table_name' value='{$esc_attr( $table_name )}' />\n\t\t\t\t" );
1117 // Hide schema and table name on front-end
1118 // phpcs:ignore PluginCheck.CodeAnalysis.Heredoc.NotAllowed
1119 $form = <<<EOT
1120 \t\t\t\t<form id='{$esc_attr( $form_id )}' action='{$url}' method='post'>
1121 \t\t\t\t\t{$get_key_input_fields}
1122 \t\t\t\t\t{$add_parent_args_as_string}
1123 \t\t\t\t\t{$add_schema_and_table_name}
1124 \t\t\t\t\t<input type='hidden' name='action' value='{$esc_attr( $action )}' />
1125 \t\t\t\t\t<input type='hidden' name='_wpnonce' value='{$esc_attr( $wp_nonce )}'>
1126 \t\t\t\t\t{$row_security_nonce_field}
1127 \t\t\t\t\t{$page_number_item}
1128 \t\t\t\t\t{$case_sensitive_search}
1129 \t\t\t\t</form>
1130 EOT;
1131 return str_replace( array("\n", "\r"), '', $form );
1132 }
1133
1134 /**
1135 * Create media link
1136 *
1137 * @param string $url Media URL.
1138 * @param string $title Media title.
1139 * @param string $mime_type Mime type.
1140 *
1141 * @return string
1142 */
1143 public static function column_media_attachment( $url, $title, $mime_type ) {
1144 if ( 'image' === substr( $mime_type, 0, 5 ) ) {
1145 $class = 'dashicons-format-image';
1146 } elseif ( 'audio' === substr( $mime_type, 0, 5 ) ) {
1147 $class = 'dashicons-playlist-audio';
1148 } elseif ( 'video' === substr( $mime_type, 0, 5 ) ) {
1149 $class = 'dashicons-playlist-video';
1150 } elseif ( 'application' === substr( $mime_type, 0, 11 ) ) {
1151 $class = 'dashicons-media-document';
1152 } else {
1153 $class = 'dashicons-external';
1154 }
1155 return sprintf(
1156 '<a href="%s" title="%s" target="_blank"><span class="dashicons %s wpda_attachment_icon"></span></a>',
1157 $url,
1158 $title,
1159 $class
1160 );
1161 }
1162
1163 /**
1164 * Overwrite method to prevent double row actions
1165 *
1166 * @param object $item Table column.
1167 * @param string $column_name Column name.
1168 * @param string $primary Primary key.
1169 *
1170 * @return string
1171 */
1172 protected function handle_row_actions( $item, $column_name, $primary ) {
1173 return '';
1174 }
1175
1176 /**
1177 * Render column content
1178 *
1179 * Strip content if too long and replace & character.
1180 *
1181 * @param object $item Table column.
1182 * @param string $column_name Database column name.
1183 *
1184 * @return string Rendered column content
1185 */
1186 protected function render_column_content( $item, $column_name, $substitute_newlines = true ) {
1187 $column_content = ( isset( $item["lookup_value_{$column_name}"] ) ? $item["lookup_value_{$column_name}"] : $item[$column_name] );
1188 if ( 'off' === WPDA::get_option( WPDA::OPTION_BE_TEXT_WRAP_SWITCH ) && WPDA::get_option( WPDA::OPTION_BE_TEXT_WRAP ) < strlen( (string) $column_content ) ) {
1189 $title = sprintf(
1190 /* translators: %s = number of characters before text is wrapped */
1191 __( 'Output limited to %1$s characters', 'wp-data-access' ),
1192 WPDA::get_option( WPDA::OPTION_BE_TEXT_WRAP )
1193 );
1194 if ( $substitute_newlines ) {
1195 return str_replace( "\n", '<br/>', substr( esc_html( str_replace( '&', '&amp;', (string) $column_content ) ), 0, WPDA::get_option( WPDA::OPTION_BE_TEXT_WRAP ) ) . ' <a href="javascript:void(0)" title="' . $title . '">&bull;&bull;&bull;</a>' );
1196 } else {
1197 return substr( esc_html( str_replace( '&', '&amp;', (string) $column_content ) ), 0, WPDA::get_option( WPDA::OPTION_BE_TEXT_WRAP ) ) . ' <a href="javascript:void(0)" title="' . $title . '">&bull;&bull;&bull;</a>';
1198 }
1199 } else {
1200 $column_data_type = $this->wpda_list_columns->get_column_data_type( $column_name );
1201 switch ( $column_data_type ) {
1202 case 'date':
1203 // date only.
1204 if ( '' === $column_content || null === $column_content ) {
1205 $column_content = '';
1206 } else {
1207 $column_content = date_i18n( get_option( 'date_format' ), strtotime( $column_content ) );
1208 }
1209 break;
1210 case 'time':
1211 // time only.
1212 if ( '' === $column_content || null === $column_content ) {
1213 $column_content = '';
1214 } else {
1215 $column_content = date_i18n( get_option( 'time_format' ), strtotime( $column_content ) );
1216 }
1217 break;
1218 case 'datetime':
1219 // date + time.
1220 case 'timestamp':
1221 if ( '' === $column_content || null === $column_content ) {
1222 $column_content = '';
1223 } else {
1224 $column_content = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $column_content ) );
1225 }
1226 }
1227 if ( $substitute_newlines ) {
1228 return str_replace( "\n", '<br/>', esc_html( str_replace( '&', '&amp;', (string) $column_content ) ) );
1229 } else {
1230 return esc_html( str_replace( '&', '&amp;', (string) $column_content ) );
1231 }
1232 }
1233 }
1234
1235 /**
1236 * Generate hidden fields for keys
1237 *
1238 * @param array $item Column info.
1239 *
1240 * @return string String containing key items and values.
1241 */
1242 protected function get_key_input_fields( $item ) {
1243 $key_input_fields = '';
1244 foreach ( $this->wpda_list_columns->get_table_primary_key() as $key ) {
1245 $key_name = esc_attr( $key );
1246 $key_value = esc_attr( $item[$key] );
1247 $key_input_fields .= "<input type='hidden' name='{$key_name}' value='{$key_value}' />";
1248 }
1249 return $key_input_fields;
1250 }
1251
1252 /**
1253 * Override this method to add actions to first column of row
1254 *
1255 * @param array $item Item information.
1256 * @param string $column_name Column name.
1257 * @param array $actions Array of actions to be added to row.
1258 */
1259 protected function column_default_add_action( $item, $column_name, &$actions ) {
1260 if ( has_filter( 'wpda_column_default_add_action' ) ) {
1261 $filter = apply_filters(
1262 'wpda_column_default_add_action',
1263 '',
1264 $item,
1265 $column_name,
1266 $this->table_name,
1267 $this->schema_name,
1268 $this->wpda_list_columns,
1269 self::$list_number,
1270 $actions,
1271 $this
1272 );
1273 if ( null !== $filter ) {
1274 $actions = $filter;
1275 }
1276 }
1277 }
1278
1279 /**
1280 * Render bulk edit checkbox
1281 *
1282 * @param array $item Column list.
1283 *
1284 * @return string Content for checkbox column.
1285 * @since 1.0.0
1286 */
1287 public function column_cb( $item ) {
1288 if ( !$this->bulk_actions_enabled ) {
1289 // Bulk actions disabled.
1290 return '';
1291 }
1292 if ( empty( $this->wpda_list_columns->get_table_primary_key() ) ) {
1293 // Table has no primary key: no bulk actions allowed!
1294 // Primary key is used to ensure uniqueness!
1295 return '';
1296 }
1297 // Build CB value: Use json format for multi column primary keys.
1298 $cb_value = (object) null;
1299 foreach ( $this->wpda_list_columns->get_table_primary_key() as $primary_key_column ) {
1300 // JSON string key and values will be double quoted. Therefore a slash must be added to double quotes in values.
1301 $cb_value->{$primary_key_column} = esc_attr( str_replace( '"', '\\"', $item[$primary_key_column] ) );
1302 }
1303 return "<input type='checkbox' name='bulk-selected[]' value='" . wp_json_encode( $cb_value ) . "' />";
1304 }
1305
1306 /**
1307 * Show page title
1308 *
1309 * @return void
1310 */
1311 protected function show_title() {
1312 if ( self::LIST_BASE_TABLE !== $this->table_name && (substr( $this->page, 0, 13 ) === \WP_Data_Access_Admin::PAGE_EXPLORER || \WP_Data_Access_Admin::PAGE_MAIN === $this->page) ) {
1313 $this->subtitle = $this->title;
1314 $this->title = 'Data Explorer';
1315 }
1316 echo esc_attr( $this->title );
1317 }
1318
1319 /**
1320 * Show page sub title
1321 *
1322 * @return void
1323 */
1324 protected function show_sub_title() {
1325 ?>
1326 <div class="wpda_subtitle"><strong><?php
1327 echo wp_kses( $this->subtitle, array(
1328 'span' => array(
1329 'class' => array(),
1330 ),
1331 ) );
1332 ?></strong></div>
1333 <?php
1334 }
1335
1336 /**
1337 * Show list table page
1338 *
1339 * Inside the list table page, the list table is shown. The necessary functionality to show the list table
1340 * specifically is found in method {@see WPDA_List_Table::display()}.
1341 *
1342 * @since 1.0.0
1343 *
1344 * @see WPDA_List_Table::display()
1345 */
1346 public function show() {
1347 // Check for import requested.
1348 if ( null !== $this->wpda_import ) {
1349 $this->wpda_import->check_post();
1350 }
1351 // Prepare list table items.
1352 $this->prepare_items();
1353 // Show list table.
1354 ?>
1355
1356 <div class="wrap<?php
1357 echo ( is_admin() ? '' : ' wp-core-ui' );
1358 ?>">
1359 <?php
1360 if ( $this->show_page_title ) {
1361 ?>
1362 <h1 class="wp-heading-inline">
1363 <?php
1364 if ( self::LIST_BASE_TABLE !== $this->table_name && (WPDA::current_user_is_admin() && \WP_Data_Access_Admin::PAGE_MAIN === $this->page) ) {
1365 ?>
1366 <a
1367 href="?page=<?php
1368 echo esc_attr( $this->page );
1369 ?>"
1370 class="dashicons dashicons-arrow-left-alt2 wpda_tooltip"
1371 title="Data Explorer"
1372 ></a>
1373 <?php
1374 }
1375 $this->show_title();
1376 ?>
1377 </h1>
1378 <?php
1379 }
1380 ?>
1381
1382 <?php
1383 if ( !is_admin() || 'wpda_wpdp_' === substr( $this->page, 0, 10 ) || WPDP::PAGE_TEMPLATES === $this->page || substr( $this->page, 0, 13 ) === \WP_Data_Access_Admin::PAGE_EXPLORER ) {
1384 $this->add_header_button();
1385 }
1386 $this->add_header_actions();
1387 if ( 'diehard' === $this->page ) {
1388 $action_url = admin_url( 'admin-ajax.php' );
1389 } else {
1390 $action_url = admin_url( 'admin.php' );
1391 }
1392 ?>
1393 <div id="wpda_invisible_container" style="display:none;">
1394 <form id="wpda_main_export_form" method="post" action="<?php
1395 echo esc_url( $action_url );
1396 ?>?action=wpda_export" target="_blank">
1397 <input type="text" name="type" id="wpda_main_export_form__type" value="table" />
1398 <input type="text" name="wpdaschema_name" id="wpda_main_export_form__schema_name" />
1399 <input type="text" name="_wpnonce" id="wpda_main_export_form__wpnonce" />
1400 </form>
1401 <form id="wpda_table_export_form" method="post" action="<?php
1402 echo esc_url( $action_url );
1403 ?>?action=wpda_export" target="_blank">
1404 <input type="text" name="type" id="wpda_table_export_form__type" value="table" />
1405 <input type="text" name="wpdaschema_name" id="wpda_table_export_form__schema_name" />
1406 <input type="text" name="table_names" id="wpda_table_export_form__table_names" />
1407 <input type="text" name="format_type" id="wpda_table_export_form__format_type" />
1408 <input type="text" name="include_table_settings" id="wpda_table_export_form__include_table_settings" />
1409 <input type="text" name="_wpnonce" id="wpda_table_export_form__wpnonce" />
1410 </form>
1411 <form id="wpda_row_export_form" method="post" action="<?php
1412 echo esc_url( $action_url );
1413 ?>?action=wpda_export" target="_blank">
1414 <input type="text" name="type" id="wpda_row_export_form__type" value="row" />
1415 <input type="text" name="pid" id="wpda_row_export_form__pid" />
1416 <input type="text" name="wpdaschema_name" id="wpda_row_export_form__schema_name" />
1417 <input type="text" name="table_names" id="wpda_row_export_form__table_names" />
1418 <input type="text" name="mysql_set" id="wpda_row_export_form__mysql_set" />
1419 <input type="text" name="show_create" id="wpda_row_export_form__show_create" />
1420 <input type="text" name="show_comments" id="wpda_row_export_form__show_comments" />
1421 <input type="text" name="format_type" id="wpda_row_export_form__format_type" />
1422 <input type="text" name="_wpnonce" id="wpda_row_export_form__wpnonce" />
1423 </form>
1424 </div>
1425 <?php
1426 // Add import container.
1427 if ( null !== $this->wpda_import ) {
1428 $this->wpda_import->add_container();
1429 }
1430 // Add custom code before the list table.
1431 do_action_ref_array( 'wpda_before_list_table', array($this) );
1432 // Prepare url.
1433 if ( is_admin() ) {
1434 $url = "?page={$this->page}";
1435 } else {
1436 $url = '';
1437 }
1438 ?>
1439
1440 <form id="wpda_main_form"
1441 method="post"
1442 action="<?php
1443 echo esc_attr( $url );
1444 ?>"
1445 style="
1446 <?php
1447 if ( '' !== $this->subtitle ) {
1448 echo 'margin-top: 10px';
1449 }
1450 ?>
1451 "
1452 >
1453
1454 <?php
1455 $this->show_sub_title();
1456 if ( $this->search_box_enabled ) {
1457 $this->search_box( __( 'search', 'wp-data-access' ), 'search_id' );
1458 }
1459 $this->display();
1460 if ( '' === $this->get_bulk_actions() ) {
1461 // Add action item containing value -1. This will allow sorting if no bulk action listbox is displayed.
1462 ?>
1463 <input type="hidden" name="action" value="-1"/>
1464 <?php
1465 }
1466 if ( self::LIST_BASE_TABLE !== $this->table_name ) {
1467 ?>
1468 <input type="hidden" name="wpdaschema_name"
1469 value="<?php
1470 echo esc_attr( $this->schema_name );
1471 // input var okay.
1472 ?>"/>
1473 <input type="hidden" name="table_name"
1474 value="<?php
1475 echo esc_attr( $this->table_name );
1476 // input var okay.
1477 ?>"/>
1478 <?php
1479 }
1480 ?>
1481 <input id="wpda_main_form_orderby" type="hidden" name="orderby"
1482 value="<?php
1483 echo ( isset( $_REQUEST['orderby'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) ) : '' );
1484 ?>"/>
1485 <input id="wpda_main_form_order" type="hidden" name="order"
1486 value="<?php
1487 echo ( isset( $_REQUEST['order'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : '' );
1488 ?>"/>
1489 <input id="wpda_main_form_post_mime_type" type="hidden" name="post_mime_type"
1490 value="<?php
1491 echo ( isset( $_REQUEST['post_mime_type'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['post_mime_type'] ) ) ) : '' );
1492 ?>"/>
1493 <input id="wpda_main_form_detached" type="hidden" name="detached"
1494 value="<?php
1495 echo ( isset( $_REQUEST['detached'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['detached'] ) ) ) : '' );
1496 ?>"/>
1497 <input id="wpda_main_db_schema" type="hidden" name="wpda_main_db_schema"
1498 value="<?php
1499 echo ( isset( $_REQUEST['wpda_main_db_schema'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['wpda_main_db_schema'] ) ) ) : '' );
1500 ?>"/>
1501 <input id="wpda_main_favourites" type="hidden" name="wpda_main_favourites"
1502 value="<?php
1503 echo ( isset( $_REQUEST['wpda_main_favourites'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['wpda_main_favourites'] ) ) ) : '' );
1504 ?>"/>
1505 <?php
1506 wp_nonce_field( 'wpda-export-' . wp_json_encode( $this->table_name ), '_wpnonce', false );
1507 ?>
1508 <?php
1509 wp_nonce_field( "wpda-delete-{$this->table_name}", '_wpnonce2', false );
1510 ?>
1511 <?php
1512 if ( self::LIST_BASE_TABLE === $this->table_name ) {
1513 wp_nonce_field( 'wpda-drop-' . WPDA::get_current_user_login(), '_wpnonce3', false );
1514 wp_nonce_field( 'wpda-truncate-' . WPDA::get_current_user_login(), '_wpnonce4', false );
1515 }
1516 ?>
1517 <?php
1518 foreach ( $_REQUEST as $key => $value ) {
1519 if ( substr( $key, 0, 19 ) === 'wpda_search_column_' ) {
1520 if ( is_array( $value ) ) {
1521 foreach ( $value as $elem_key => $elem_value ) {
1522 echo '<input type="hidden" name="' . esc_attr( $key ) . '[]" value="' . esc_attr( $elem_value ) . '"/>';
1523 }
1524 } else {
1525 echo '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '"/>';
1526 }
1527 }
1528 }
1529 ?>
1530 </form>
1531 </div>
1532 <script type='text/javascript'>
1533 jQuery(function() {
1534 jQuery( '.wpda_tooltip' ).tooltip();
1535 // Add toolbar events
1536 jQuery("#wpda_toolbar_icon_go_backup").on("click", function() {
1537 jQuery("#wpda_goto_backup").submit();
1538 });
1539 jQuery("#wpda_toolbar_icon_add_row").on("click", function() {
1540 jQuery("#wpda_new_row_table_name").val("<?php
1541 echo esc_attr( $this->table_name );
1542 ?>");
1543 jQuery("#wpda_new_row").submit();
1544 });
1545 jQuery("#wpda_toolbar_icon_add_publication").on("click", function() {
1546 jQuery("#wpda_new_publication_table_name").val("<?php
1547 echo esc_attr( $this->table_name );
1548 ?>");
1549 jQuery("#wpda_new_publication").submit();
1550 });
1551 });
1552 </script>
1553 <?php
1554 $this->bind_action_buttons();
1555 ?>
1556 <?php
1557 // Add custom code after the list table.
1558 do_action_ref_array( 'wpda_after_list_table', array($this) );
1559 }
1560
1561 /**
1562 * Placeholder for subclasses to add additional actions
1563 *
1564 * @return void
1565 */
1566 protected function add_header_actions() {
1567 }
1568
1569 /**
1570 * Bind javascript code to action buttons
1571 *
1572 * @since 1.6.2
1573 */
1574 protected function bind_action_buttons() {
1575 ?>
1576 <script type='text/javascript'>
1577 jQuery(function () {
1578 jQuery("#doaction").click(function () {
1579 return wpda_action_button();
1580 });
1581 jQuery("#doaction2").click(function () {
1582 return wpda_action_button();
1583 });
1584 });
1585 </script>
1586 <?php
1587 }
1588
1589 /**
1590 * Add button to page header
1591 *
1592 * By default "add new" and "import" buttons are added (depending on the settings). Overwrite this method to
1593 * add your own buttons.
1594 *
1595 * @since 1.0.1
1596 */
1597 protected function add_header_button() {
1598 if ( 'off' === $this->allow_insert ) {
1599 if ( null !== $this->wpda_import ) {
1600 $this->wpda_import->add_button();
1601 }
1602 } else {
1603 // phpcs:ignore -- 8.1 proof
1604 if ( WPDA::is_wpda_table( $this->table_name ) || ('on' === WPDA::get_option( WPDA::OPTION_BE_ALLOW_INSERT ) && count( $this->wpda_list_columns->get_table_primary_key() )) > 0 ) {
1605 $storage_type = ( WPDA::is_wpda_table( $this->table_name ) ? __( 'respository', 'wp-data-access' ) : __( 'table', 'wp-data-access' ) );
1606 // Prepare url.
1607 if ( is_admin() ) {
1608 $url = "?page={$this->page}";
1609 } else {
1610 $url = '';
1611 }
1612 ?>
1613 <form
1614 method="post"
1615 action="<?php
1616 echo esc_attr( $url );
1617 ?>"
1618 style="display: inline-block; vertical-align: baseline;"
1619 >
1620 <div>
1621 <input type="hidden" name="wpdaschema_name"
1622 value="<?php
1623 echo esc_attr( $this->schema_name );
1624 // input var okay.
1625 ?>"/>
1626 <input type="hidden" name="table_name"
1627 value="<?php
1628 echo esc_attr( $this->table_name );
1629 // input var okay.
1630 ?>"/>
1631 <input type="hidden" name="action" value="new">
1632 <button type="submit" class="page-title-action wpda_tooltip"
1633 title="<?php
1634 /* translators: 1 = entiry name; 2: storage type */
1635 echo esc_attr( sprintf( __( 'Add new %1$s to %2$s', 'wp-data-access' ), $this->_args['singular'], $storage_type ) );
1636 ?>"
1637 >
1638 <i class="fas fa-plus-circle wpda_icon_on_button"></i>
1639 <?php
1640 echo esc_attr__( 'Add New', 'wp-data-access' );
1641 ?>
1642 </button>
1643 <?php
1644 // Add import button to title.
1645 if ( null !== $this->wpda_import ) {
1646 $this->wpda_import->add_button();
1647 }
1648 ?>
1649 </div>
1650 </form>
1651 <?php
1652 } else {
1653 // Add import button to title.
1654 if ( null !== $this->wpda_import ) {
1655 $this->wpda_import->add_button();
1656 }
1657 }
1658 }
1659 }
1660
1661 /**
1662 * Prepares the list of items for displaying
1663 *
1664 * Overwrites WP_List_Table::prepare_items()
1665 *
1666 * @since 1.0.0
1667 */
1668 public function prepare_items() {
1669 // Construct where clause with search values provided in the search box.
1670 // Result (where clause) is written to $this->where.
1671 $this->construct_where_clause();
1672 $this->process_bulk_action();
1673 if ( is_admin() ) {
1674 if ( $this->table_name === self::LIST_BASE_TABLE ) {
1675 $option = 'wpda_rows_per_page_' . str_replace( '.', '_', self::LIST_BASE_TABLE );
1676 } else {
1677 $option = 'wpda_rows_per_page_' . str_replace( '.', '_', $this->schema_name . $this->table_name );
1678 }
1679 $this->items_per_page = $this->get_items_per_page( $option, WPDA::get_option( WPDA::OPTION_BE_PAGINATION ) );
1680 } else {
1681 $this->items_per_page = WPDA::get_option( WPDA::OPTION_FE_PAGINATION );
1682 }
1683 $this->current_page = $this->get_pagenum();
1684 $total_items = $this->record_count();
1685 $total_pages = ceil( $total_items / $this->items_per_page );
1686 if ( $this->search_value !== $this->search_value_old ) {
1687 $this->current_page = 1;
1688 }
1689 if ( $this->current_page > $total_pages ) {
1690 $this->current_page = $total_pages;
1691 }
1692 $this->set_pagination_args( array(
1693 'total_items' => $total_items,
1694 'total_pages' => $total_pages,
1695 'per_page' => $this->items_per_page,
1696 ) );
1697 $this->get_rows();
1698 // Written to $this->items in base class (WP_List_Table).
1699 $columns = $this->get_columns();
1700 $hidden = $this->get_hidden_columns();
1701 if ( false === $hidden ) {
1702 if ( is_admin() ) {
1703 // List table in backend.
1704 if ( self::LIST_BASE_TABLE === $this->table_name ) {
1705 $table_name = str_replace( '.', '_', self::LIST_BASE_TABLE );
1706 } else {
1707 global $wpdb;
1708 if ( $this->schema_name === $wpdb->dbname && WPDA_CSV_Uploads_Model::get_base_table_name() === $this->table_name ) {
1709 $table_name = $this->table_name;
1710 // csv upload = exception.
1711 } else {
1712 $table_name = str_replace( '.', '_', $this->schema_name . $this->table_name );
1713 }
1714 }
1715 $hidden = get_user_option( WPDA_List_View::HIDDENCOLUMNS_PREFIX . get_current_screen()->id . $table_name );
1716 if ( false === $hidden ) {
1717 $hidden = array();
1718 }
1719 }
1720 }
1721 if ( !is_array( $hidden ) ) {
1722 $hidden = array();
1723 }
1724 $sortable = $this->get_sortable_columns();
1725 $primary = $this->get_primary_column();
1726 $this->_column_headers = array(
1727 $columns,
1728 $hidden,
1729 $sortable,
1730 $primary
1731 );
1732 }
1733
1734 /**
1735 * Build where clause
1736 *
1737 * Arguments might come from the URL so we need to check for SQL injection and use prepare. The resulting
1738 * where clause is written directly to member $this->where.
1739 *
1740 * @since 1.0.0
1741 */
1742 protected function construct_where_clause() {
1743 $whereclause = $this->get_where_clause();
1744 if ( '' !== $whereclause ) {
1745 $this->where = ( '' === $this->where ? " where ({$whereclause}) " : " {$this->where} and ({$whereclause}) " );
1746 }
1747 }
1748
1749 /**
1750 * Returns the where clause (uses filters if available)
1751 *
1752 * @return string
1753 */
1754 public function get_where_clause() {
1755 return WPDA::construct_where_clause(
1756 $this->schema_name,
1757 $this->table_name,
1758 $this->wpda_list_columns->get_searchable_table_columns(),
1759 $this->search_value,
1760 isset( $_REQUEST['wpda_c'] ) && 'true' === $_REQUEST['wpda_c']
1761 );
1762 }
1763
1764 /**
1765 * Process bulk actions
1766 *
1767 * Delete and bulk-delete actions use the primary key list as a reference. Before performing the actual delete
1768 * statement(s) we'll check the provided column names against the data dictionary. This will protect us against
1769 * SQL injection attacks.
1770 *
1771 * For export actions table names will not be checked here. They are handed over WPDA_Export where the validity
1772 * and access rights are checked anyway (to be safe).
1773 *
1774 * All actions can be processed only with a valid wpnonce value.
1775 *
1776 * To perform actions the user must have the appropriate rights.
1777 *
1778 * @since 1.0.0
1779 */
1780 public function process_bulk_action() {
1781 switch ( $this->current_action() ) {
1782 case 'delete':
1783 // Check access rights.
1784 if ( 'on' !== $this->allow_delete ) {
1785 // Deleting records from list table is not allowed.
1786 wp_die( esc_attr__( 'ERROR: Not authorized [delete not allowed]', 'wp-data-access' ) );
1787 }
1788 // Prepare wp_nonce action security check.
1789 $wp_nonce_action = "wpda-delete-{$this->table_name}";
1790 $row_to_be_deleted = array();
1791 // Gonna hold the row to be deleted.
1792 $i = 0;
1793 // Index, necessary for multi column keys.
1794 // Check all key columns.
1795 foreach ( $this->wpda_list_columns->get_table_primary_key() as $key ) {
1796 // Check if key is available.
1797 if ( !isset( $_REQUEST[$key] ) ) {
1798 // input var okay.
1799 wp_die( esc_attr__( 'ERROR: Invalid URL [missing primary key values]', 'wp-data-access' ) );
1800 }
1801 // Write key value pair to array.
1802 $row_to_be_deleted[$i]['key'] = $key;
1803 $row_to_be_deleted[$i]['value'] = sanitize_text_field( wp_unslash( $_REQUEST[$key] ) );
1804 // input var okay.
1805 $i++;
1806 // Add key values to wp_nonce action.
1807 $wp_nonce_action .= '-' . sanitize_text_field( wp_unslash( $_REQUEST[$key] ) );
1808 // input var okay.
1809 }
1810 // Check if delete is allowed.
1811 $wp_nonce = ( isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '' );
1812 // input var okay.
1813 if ( !wp_verify_nonce( $wp_nonce, $wp_nonce_action ) ) {
1814 wp_die( esc_attr__( 'ERROR: Not authorized', 'wp-data-access' ) );
1815 }
1816 // All key column values available: delete record.
1817 // Prepare named array for delete operation.
1818 $next_row_to_be_deleted = array();
1819 $count_rows = count( $row_to_be_deleted );
1820 // phpcs:ignore -- 8.1 proof
1821 for ($i = 0; $i < $count_rows; $i++) {
1822 $next_row_to_be_deleted[$row_to_be_deleted[$i]['key']] = $row_to_be_deleted[$i]['value'];
1823 }
1824 $engine = WPDA::get_table_engine( $this->schema_name, $this->table_name );
1825 if ( $this->delete_row( $next_row_to_be_deleted, $engine ) ) {
1826 $msg = new WPDA_Message_Box(array(
1827 'message_text' => __( 'Row deleted', 'wp-data-access' ),
1828 ));
1829 $msg->box();
1830 } else {
1831 $msg = new WPDA_Message_Box(array(
1832 'message_text' => __( 'Could not delete row', 'wp-data-access' ),
1833 'message_type' => 'error',
1834 'message_is_dismissible' => false,
1835 ));
1836 $msg->box();
1837 }
1838 break;
1839 case 'bulk-delete':
1840 // Check access rights.
1841 if ( 'on' !== $this->allow_delete ) {
1842 // Deleting records from list table is not allowed.
1843 die( esc_attr__( 'ERROR: Not authorized [delete not allowed]', 'wp-data-access' ) );
1844 }
1845 // We first need to check if all the necessary information is available.
1846 if ( !isset( $_REQUEST['bulk-selected'] ) ) {
1847 // input var okay.
1848 // Nothing to delete.
1849 $msg = new WPDA_Message_Box(array(
1850 'message_text' => __( 'Nothing to delete', 'wp-data-access' ),
1851 ));
1852 $msg->box();
1853 return;
1854 }
1855 // Check if delete is allowed.
1856 $wp_nonce_action = "wpda-delete-{$this->table_name}";
1857 $wp_nonce = ( isset( $_REQUEST['_wpnonce2'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce2'] ) ) : '' );
1858 // input var okay.
1859 if ( !wp_verify_nonce( $wp_nonce, $wp_nonce_action ) ) {
1860 die( esc_attr__( 'ERROR: Not authorized', 'wp-data-access' ) );
1861 }
1862 // phpcs:disable WordPress.Security.ValidatedSanitizedInput
1863 $bulk_rows = (array) $_REQUEST['bulk-selected'];
1864 // phpcs:enable WordPress.Security.ValidatedSanitizedInput
1865 $no_rows = count( $bulk_rows );
1866 // # rows to be deleted. // phpcs:ignore -- 8.1 proof being safe
1867 $rows_to_be_deleted = array();
1868 // Gonna hold rows to be deleted.
1869 for ($i = 0; $i < $no_rows; $i++) {
1870 // Write "json" to named array. Need to strip slashes twice. Once for the normal conversion
1871 // and once extra for the pre-conversion of double quotes in method column_cb().
1872 $row_object = json_decode( stripslashes( stripslashes( $bulk_rows[$i] ) ), true );
1873 if ( $row_object ) {
1874 $j = 0;
1875 // Index used to build array.
1876 // Check all key columns.
1877 foreach ( $this->wpda_list_columns->get_table_primary_key() as $key ) {
1878 // Check if key is available.
1879 if ( !isset( $row_object[$key] ) ) {
1880 wp_die( esc_attr__( 'ERROR: Invalid URL [missing primary key values]', 'wp-data-access' ) );
1881 }
1882 // Write key value pair to array.
1883 $rows_to_be_deleted[$i][$j]['key'] = $key;
1884 $rows_to_be_deleted[$i][$j]['value'] = $row_object[$key];
1885 $j++;
1886 }
1887 }
1888 }
1889 // Looks like everything is there. Delete records from table...
1890 $no_key_cols = count( $this->wpda_list_columns->get_table_primary_key() );
1891 // phpcs:ignore -- 8.1 proof
1892 $rows_successfully_deleted = 0;
1893 // Number of rows successfully deleted.
1894 $rows_with_errors = 0;
1895 // Number of rows that could not be deleted.
1896 $engine = WPDA::get_table_engine( $this->schema_name, $this->table_name );
1897 for ($i = 0; $i < $no_rows; $i++) {
1898 // Prepare named array for delete operation.
1899 $next_row_to_be_deleted = array();
1900 $row_found = true;
1901 for ($j = 0; $j < $no_key_cols; $j++) {
1902 if ( isset( $rows_to_be_deleted[$i][$j]['key'] ) ) {
1903 $next_row_to_be_deleted[$rows_to_be_deleted[$i][$j]['key']] = $rows_to_be_deleted[$i][$j]['value'];
1904 } else {
1905 $row_found = false;
1906 }
1907 }
1908 if ( $row_found ) {
1909 if ( $this->delete_row( $next_row_to_be_deleted, $engine ) ) {
1910 // Row(s) successfully deleted.
1911 $rows_successfully_deleted++;
1912 } else {
1913 // An error occurred during the delete operation: increase error count.
1914 $rows_with_errors++;
1915 }
1916 } else {
1917 // An error occurred during the delete operation: increase error count.
1918 $rows_with_errors++;
1919 }
1920 }
1921 // Inform user about the results of the operation.
1922 $message = '';
1923 if ( 1 === $rows_successfully_deleted ) {
1924 $message = __( 'Row deleted', 'wp-data-access' );
1925 } elseif ( $rows_successfully_deleted > 1 ) {
1926 $message = "{$rows_successfully_deleted} " . __( 'rows deleted', 'wp-data-access' );
1927 }
1928 if ( '' !== $message ) {
1929 $msg = new WPDA_Message_Box(array(
1930 'message_text' => $message,
1931 ));
1932 $msg->box();
1933 }
1934 $message = '';
1935 if ( $rows_with_errors > 0 ) {
1936 $message = __( 'Not all rows have been deleted', 'wp-data-access' );
1937 }
1938 if ( '' !== $message ) {
1939 $msg = new WPDA_Message_Box(array(
1940 'message_text' => $message,
1941 'message_type' => 'error',
1942 'message_is_dismissible' => false,
1943 ));
1944 $msg->box();
1945 }
1946 break;
1947 case 'bulk-export':
1948 case 'bulk-export-xml':
1949 case 'bulk-export-json':
1950 case 'bulk-export-excel':
1951 case 'bulk-export-csv':
1952 // Check access rights.
1953 if ( !WPDA::is_wpda_table( $this->table_name ) ) {
1954 if ( 'on' !== WPDA::get_option( WPDA::OPTION_BE_EXPORT_ROWS ) ) {
1955 // Exporting rows from list table is not allowed.
1956 die( esc_attr__( 'ERROR: Not authorized [export not allowed]', 'wp-data-access' ) );
1957 }
1958 }
1959 // We first need to check if all the necessary information is available.
1960 if ( !isset( $_REQUEST['bulk-selected'] ) ) {
1961 // input var okay.
1962 // Nothing to export.
1963 $msg = new WPDA_Message_Box(array(
1964 'message_text' => __( 'Nothing to export', 'wp-data-access' ),
1965 ));
1966 $msg->box();
1967 return;
1968 }
1969 // Check if export is allowed.
1970 $wp_nonce = ( isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '' );
1971 // input var okay.
1972 if ( !wp_verify_nonce( $wp_nonce, 'wpda-export-' . wp_json_encode( $this->table_name ) ) ) {
1973 die( esc_attr__( 'ERROR: Not authorized', 'wp-data-access' ) );
1974 }
1975 // phpcs:disable WordPress.Security.ValidatedSanitizedInput
1976 $bulk_rows = (array) $_REQUEST['bulk-selected'];
1977 // phpcs:enable WordPress.Security.ValidatedSanitizedInput
1978 $no_rows = count( $bulk_rows );
1979 // # rows to be exported. // phpcs:ignore -- 8.1 proof being safe
1980 $format_type = '';
1981 switch ( $this->current_action() ) {
1982 case 'bulk-export-xml':
1983 $format_type = 'xml';
1984 break;
1985 case 'bulk-export-json':
1986 $format_type = 'json';
1987 break;
1988 case 'bulk-export-excel':
1989 $format_type = 'excel';
1990 break;
1991 case 'bulk-export-csv':
1992 $format_type = 'csv';
1993 }
1994 if ( 'diehard' === $this->page && null !== $this->pid ) {
1995 $wp_nonce = wp_create_nonce( "wpda-export-{$this->pid}" );
1996 $submit_schema_name = '';
1997 $submit_table_name = '';
1998 } else {
1999 $submit_schema_name = $this->schema_name;
2000 $submit_table_name = $this->table_name;
2001 }
2002 $j = 0;
2003 $columns = array();
2004 for ($i = 0; $i < $no_rows; $i++) {
2005 // Write "json" to named array. Need to strip slashes twice. Once for the normal conversion
2006 // and once extra for the pre-conversion of double quotes in method column_cb().
2007 $row_object = json_decode( stripslashes( stripslashes( $bulk_rows[$i] ) ), true );
2008 if ( $row_object ) {
2009 // Check all key columns.
2010 foreach ( $this->wpda_list_columns->get_table_primary_key() as $key ) {
2011 // Check if key is available.
2012 if ( !isset( $row_object[$key] ) ) {
2013 wp_die( esc_attr__( 'ERROR: Invalid URL', 'wp-data-access' ) );
2014 }
2015 if ( !isset( $columns[$key] ) ) {
2016 $columns[$key] = array();
2017 }
2018 $columns[$key][] = $row_object[$key];
2019 }
2020 $j++;
2021 }
2022 }
2023 ?>
2024 <script type="application/javascript">
2025 jQuery(function() {
2026 wpda_row_export(
2027 '<?php
2028 echo esc_attr( $this->pid );
2029 ?>',
2030 '<?php
2031 echo esc_attr( $submit_schema_name );
2032 ?>',
2033 '<?php
2034 echo esc_attr( $submit_table_name );
2035 ?>',
2036 '<?php
2037 echo esc_attr( $wp_nonce );
2038 ?>',
2039 '<?php
2040 echo esc_attr( $format_type );
2041 ?>',
2042 'off',
2043 'off',
2044 'off',
2045 '<?php
2046 echo wp_json_encode( $columns );
2047 ?>'
2048 );
2049 });
2050 </script>
2051 <?php
2052 }
2053 }
2054
2055 /**
2056 * Delete record from database table
2057 *
2058 * The table must have a primary key and the values for all primary key columns must be provided in the
2059 * request. The where clause must be a named array in format: ['column_name'] = 'value'
2060 *
2061 * @param string $where where clause.
2062 * @param string $engine connect or empty.
2063 *
2064 * @return mixed
2065 * @since 1.0.0
2066 */
2067 public function delete_row( $where, $engine = '' ) {
2068 $wpdadb = WPDADB::get_db_connection( $this->schema_name );
2069 if ( null === $wpdadb ) {
2070 if ( is_admin() ) {
2071 /* translators: %s = remote database name */
2072 wp_die( esc_attr( sprintf( __( 'ERROR - Remote database %s not available', 'wp-data-access' ), $this->schema_name ) ) );
2073 } else {
2074 /* translators: %s = remote database name */
2075 die( esc_attr( sprintf( __( 'ERROR - Remote database %s not available', 'wp-data-access' ), $this->schema_name ) ) );
2076 }
2077 }
2078 $row_deleted = $wpdadb->delete( $this->table_name, $where );
2079 if ( 'connect' === strtolower( $engine ) ) {
2080 // Connect engine does not return number of rows deleted.
2081 // Presuming delete was successful when no error message was returned.
2082 return '' === $wpdadb->last_error;
2083 }
2084 return $row_deleted;
2085 }
2086
2087 /**
2088 * Number of records in database table
2089 *
2090 * Returns the number of records in the database table. Where clause must be prepared in advance and written
2091 * to $this->where ({@see WPDA_List_Table::construct_where_clause()})
2092 *
2093 * @return null|string Number of rows in the current table
2094 * @since 1.0.0
2095 */
2096 public function record_count() {
2097 if ( !$this->row_count_estimate['do_real_count'] && '' === $this->where ) {
2098 // Use estimate row count.
2099 return $this->table_rows;
2100 }
2101 // Get real row count.
2102 $wpdadb = WPDADB::get_db_connection( $this->schema_name );
2103 if ( null === $wpdadb ) {
2104 if ( is_admin() ) {
2105 /* translators: %s = remote database name */
2106 wp_die( esc_attr( sprintf( __( 'ERROR - Remote database %s not available', 'wp-data-access' ), $this->schema_name ) ) );
2107 } else {
2108 /* translators: %s = remote database name */
2109 die( esc_attr( sprintf( __( 'ERROR - Remote database %s not available', 'wp-data-access' ), $this->schema_name ) ) );
2110 }
2111 }
2112 if ( '' === $this->schema_name ) {
2113 $query = "\n\t\t\t\t\tselect count(*)\n\t\t\t\t\tfrom `{$this->table_name}`\n\t\t\t\t\t{$this->where}\n\t\t\t\t";
2114 } else {
2115 if ( self::LIST_BASE_TABLE === $this->table_name ) {
2116 $query = "\n\t\t\t\t\t\tselect count(*)\n\t\t\t\t\t\tfrom {$this->table_name}\n\t\t\t\t\t\t{$this->where}\n\t\t\t\t\t";
2117 } else {
2118 $query = "\n\t\t\t\t\t\tselect count(*)\n\t\t\t\t\t\tfrom `{$wpdadb->dbname}`.`{$this->table_name}`\n\t\t\t\t\t\t{$this->where}\n\t\t\t\t\t";
2119 }
2120 }
2121 return $wpdadb->get_var( $query );
2122 }
2123
2124 /**
2125 * Perform query to retrieve rows from database
2126 *
2127 * Where clause must be prepared in advance and written to $this->where
2128 * ({@see WPDA_List_Table::construct_where_clause()}).
2129 *
2130 * No return value. Result is directly written to $this->items (base class member).
2131 *
2132 * @since 1.0.0
2133 */
2134 public function get_rows() {
2135 $wpdadb = WPDADB::get_db_connection( $this->schema_name );
2136 if ( null === $wpdadb ) {
2137 if ( is_admin() ) {
2138 /* translators: %s = remote database name */
2139 wp_die( esc_attr( sprintf( __( 'ERROR - Remote database %s not available', 'wp-data-access' ), $this->schema_name ) ) );
2140 } else {
2141 /* translators: %s = remote database name */
2142 die( esc_attr( sprintf( __( 'ERROR - Remote database %s not available', 'wp-data-access' ), $this->schema_name ) ) );
2143 }
2144 }
2145 // Selected columns cannot be changed by the user at this time. No check for SQL injection needed now.
2146 // This might change in the future when users are allowed to change or set this value. A method named
2147 // column_exists() in WPDA_Dictionary_Checks is already available for this purpose.
2148 $selected_columns = '*';
2149 if ( isset( $this->columns_queried ) ) {
2150 $selected_columns = implode( ',', $this->columns_queried );
2151 }
2152 if ( isset( $this->wpda_table_settings->hyperlinks ) ) {
2153 $i = 0;
2154 foreach ( $this->wpda_table_settings->hyperlinks as $hyperlink ) {
2155 $skip_column = false;
2156 if ( null !== $this->wpda_project_table_settings ) {
2157 $hyperlink_label = $hyperlink->hyperlink_label;
2158 if ( !property_exists( $this, 'is_child' ) ) {
2159 if ( isset( $this->wpda_project_table_settings->hyperlinks_parent->{$hyperlink_label} ) && !$this->wpda_project_table_settings->hyperlinks_parent->{$hyperlink_label} ) {
2160 $skip_column = true;
2161 }
2162 } else {
2163 if ( isset( $this->wpda_project_table_settings->hyperlinks_child->{$hyperlink_label} ) && !$this->wpda_project_table_settings->hyperlinks_child->{$hyperlink_label} ) {
2164 $skip_column = true;
2165 }
2166 }
2167 }
2168 if ( !$skip_column && isset( $hyperlink->hyperlink_list ) && true === $hyperlink->hyperlink_list ) {
2169 $hyperlink_label = ( isset( $hyperlink->hyperlink_label ) ? $hyperlink->hyperlink_label : '' );
2170 $hyperlink_html = ( isset( $hyperlink->hyperlink_html ) ? $hyperlink->hyperlink_html : '' );
2171 if ( '' !== $hyperlink_label && '' !== $hyperlink_html ) {
2172 // Add hyperlink columns (details are added in method column_default).
2173 $selected_columns .= ", '' as wpda_hyperlink_{$i}";
2174 }
2175 }
2176 $i++;
2177 }
2178 }
2179 if ( '' === $this->schema_name ) {
2180 $query = "\n\t\t\t\t\tselect {$selected_columns}\n\t\t\t\t\tfrom `{$this->table_name}`\n\t\t\t\t\t{$this->where}\n\t\t\t\t";
2181 } else {
2182 if ( self::LIST_BASE_TABLE === $this->table_name ) {
2183 $query = "\n\t\t\t\t\t\tselect {$selected_columns}\n\t\t\t\t\t\tfrom {$this->table_name}\n\t\t\t\t\t\t{$this->where}\n\t\t\t\t\t";
2184 } else {
2185 $query = "\n\t\t\t\t\t\tselect {$selected_columns}\n\t\t\t\t\t\tfrom `{$wpdadb->dbname}`.`{$this->table_name}`\n\t\t\t\t\t\t{$this->where}\n\t\t\t\t\t";
2186 }
2187 }
2188 $query .= $this->get_order_by();
2189 // Add order by.
2190 $query .= " limit {$this->items_per_page}";
2191 $offset = ($this->current_page - 1) * $this->items_per_page;
2192 if ( $offset > 0 ) {
2193 $query .= " offset {$offset}";
2194 }
2195 // Debug query.
2196 // var_dump( $query );
2197 $this->items = $wpdadb->get_results( $query, 'ARRAY_A' );
2198 }
2199
2200 /**
2201 * Get order by
2202 *
2203 * @return string|void
2204 */
2205 protected function get_order_by() {
2206 if ( !empty( $_REQUEST['orderby'] ) ) {
2207 $orderby_arg = sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) );
2208 if ( !empty( $_REQUEST['order'] ) ) {
2209 $order_arg = sanitize_text_field( wp_unslash( $_REQUEST['order'] ) );
2210 } else {
2211 $order_arg = '';
2212 }
2213 $columns = $this->get_sortable_columns();
2214 // Check column name for SQL injection.
2215 if ( isset( $columns[$orderby_arg] ) || $this->wpda_data_dictionary->column_exists( $orderby_arg ) ) {
2216 // Column name exists in current table, safely continue...
2217 $orderby = " order by {$orderby_arg}";
2218 // Prevent SQL injection for order. If 'desc' is found result will be ordered desc. In all other
2219 // cases we'll order asc.
2220 $orderby .= ( strtolower( trim( $order_arg ) ) === 'desc' ? ' desc' : ' asc' );
2221 return $orderby;
2222 } else {
2223 // The user provided a column name which is not in the table. Most probably the result of a
2224 // SQL injection attack, so let's terminate.
2225 wp_die( esc_attr__( 'ERROR: Invalid URL [invalid column name]', 'wp-data-access' ) );
2226 }
2227 } else {
2228 return '';
2229 }
2230 }
2231
2232 /**
2233 * Return an associative array of columns
2234 *
2235 * @return array
2236 * @since 1.0.0
2237 */
2238 public function get_columns() {
2239 if ( null !== $this->wpda_cached_columns && is_array( $this->wpda_cached_columns ) ) {
2240 return $this->wpda_cached_columns;
2241 }
2242 $columns = array();
2243 if ( $this->bulk_actions_enabled ) {
2244 if ( !empty( $this->wpda_list_columns->get_table_primary_key() ) ) {
2245 // Tables has primary key: bulk actions allowed!
2246 // Primary key is used to ensure uniqueness.
2247 $actions = $this->get_bulk_actions();
2248 if ( is_array( $actions ) && 0 < count( $actions ) ) {
2249 // phpcs:ignore -- 8.1 proof
2250 $columns = array(
2251 'cb' => '<input type="checkbox" />',
2252 );
2253 }
2254 }
2255 }
2256 $columnlist = $this->wpda_list_columns->get_table_column_headers();
2257 foreach ( $columnlist as $key => $value ) {
2258 $columns[$key] = $value;
2259 // Check for alternative column header.
2260 if ( isset( $this->column_headers[$key] ) ) {
2261 // Alternative header found: use it.
2262 $columns[$key] = $this->column_headers[$key];
2263 } else {
2264 // Default behaviour: get column header from generated label.
2265 $columns[$key] = $this->wpda_list_columns->get_column_label( $key );
2266 }
2267 }
2268 if ( isset( $this->wpda_table_settings->hyperlinks ) ) {
2269 $i = 0;
2270 foreach ( $this->wpda_table_settings->hyperlinks as $hyperlink ) {
2271 if ( isset( $hyperlink->hyperlink_list ) && true === $hyperlink->hyperlink_list ) {
2272 $skip_column = false;
2273 if ( null !== $this->wpda_project_table_settings ) {
2274 $hyperlink_label = $hyperlink->hyperlink_label;
2275 if ( !property_exists( $this, 'is_child' ) ) {
2276 if ( isset( $this->wpda_project_table_settings->hyperlinks_parent->{$hyperlink_label} ) && !$this->wpda_project_table_settings->hyperlinks_parent->{$hyperlink_label} ) {
2277 $skip_column = true;
2278 }
2279 } else {
2280 if ( isset( $this->wpda_project_table_settings->hyperlinks_child->{$hyperlink_label} ) && !$this->wpda_project_table_settings->hyperlinks_child->{$hyperlink_label} ) {
2281 $skip_column = true;
2282 }
2283 }
2284 }
2285 if ( !$skip_column ) {
2286 $hyperlink_label = ( isset( $hyperlink->hyperlink_label ) ? $hyperlink->hyperlink_label : '' );
2287 $columns["wpda_hyperlink_{$i}"] = $hyperlink_label;
2288 // Add hyperlink label.
2289 }
2290 }
2291 $i++;
2292 }
2293 }
2294 // Cache columns.
2295 $this->wpda_cached_columns = $columns;
2296 return $columns;
2297 }
2298
2299 /**
2300 * List of columns to make sortable
2301 *
2302 * @return array
2303 * @since 1.0.0
2304 */
2305 public function get_sortable_columns() {
2306 $columns = array();
2307 // Get column names from result set.
2308 if ( $this->items ) {
2309 foreach ( $this->items[0] as $key => $value ) {
2310 if ( 'wpda_hyperlink_' !== substr( $key, 0, 15 ) ) {
2311 $columns[$key] = array($key, false);
2312 }
2313 }
2314 }
2315 return $columns;
2316 }
2317
2318 /**
2319 * Display the search box
2320 *
2321 * @param string $text The 'submit' button label.
2322 * @param string $input_id ID attribute value for the search input field.
2323 *
2324 * @since 1.0.0
2325 */
2326 public function search_box( $text, $input_id ) {
2327 $input_id = $input_id . '-search-input';
2328 // Allow external user to add search actions (like icons) to the search box.
2329 do_action(
2330 'wpda_add_search_actions',
2331 $this->schema_name,
2332 $this->table_name,
2333 $this->wpda_table_settings,
2334 $this->wpda_list_columns
2335 );
2336 ?>
2337
2338 <p class="search-box" <?php
2339 echo ( !$this->has_items() ? 'style="padding-bottom:10px;"' : '' );
2340 ?>>
2341 <input type="search" id="<?php
2342 echo esc_attr( $input_id );
2343 ?>"
2344 name="<?php
2345 echo esc_attr( $this->search_item_name );
2346 ?>"
2347 value="<?php
2348 echo esc_attr( $this->search_value );
2349 ?>"/>
2350 <?php
2351 if ( is_admin() ) {
2352 submit_button(
2353 $text,
2354 '',
2355 '',
2356 false,
2357 array(
2358 'id' => 'search-submit',
2359 )
2360 );
2361 } else {
2362 wpdadiehard_submit_button(
2363 $text,
2364 '',
2365 '',
2366 false,
2367 array(
2368 'id' => 'search-submit',
2369 )
2370 );
2371 }
2372 ?>
2373 <input type="hidden" name="<?php
2374 echo esc_attr( $this->search_item_name );
2375 ?>_old_value"
2376 value="<?php
2377 echo esc_attr( $this->search_value );
2378 ?>"/>
2379 </p>
2380
2381 <?php
2382 // Allow external user to add search html (like extra items) below the search box.
2383 do_action(
2384 'wpda_add_search_filter',
2385 $this->schema_name,
2386 $this->table_name,
2387 $this->wpda_table_settings,
2388 $this->wpda_list_columns
2389 );
2390 }
2391
2392 /**
2393 * Get search value (entered by the user or taken from cookie).
2394 *
2395 * @return string
2396 * @since 1.5.0
2397 */
2398 protected function get_search_value() {
2399 if ( 'off' === WPDA::get_option( WPDA::OPTION_BE_REMEMBER_SEARCH ) ) {
2400 if ( isset( $_REQUEST[$this->search_item_name] ) ) {
2401 // phpcs:disable WordPress.Security.ValidatedSanitizedInput
2402 $value = wp_filter_nohtml_kses( wp_unslash( $_REQUEST[$this->search_item_name] ) );
2403 // phpcs:enable WordPress.Security.ValidatedSanitizedInput
2404 return $value;
2405 }
2406 }
2407 if ( 'wpda_wpdp_' === substr( $this->page, 0, 10 ) ) {
2408 $cookie_name = $this->page;
2409 if ( isset( $_REQUEST['child_request'] ) ) {
2410 $cookie_name = 'NOCOOKIESFORCHILDREQUESTS';
2411 // No search values stored for child tables.
2412 }
2413 foreach ( $_REQUEST as $key => $val ) {
2414 if ( strpos( $key, 'WPDA_PARENT_KEY*' ) === 0 ) {
2415 $cookie_name = 'NOCOOKIESFORCHILDREQUESTS';
2416 // No search values stored for child tables.
2417 }
2418 }
2419 } else {
2420 $cookie_name = $this->page . '_search_' . str_replace( '.', '_', $this->table_name );
2421 }
2422 if ( isset( $_REQUEST[$this->search_item_name] ) && '' !== $_REQUEST[$this->search_item_name] ) {
2423 // phpcs:disable WordPress.Security.ValidatedSanitizedInput
2424 $value = wp_filter_nohtml_kses( wp_unslash( $_REQUEST[$this->search_item_name] ) );
2425 // phpcs:enable WordPress.Security.ValidatedSanitizedInput
2426 return $value;
2427 } elseif ( isset( $_COOKIE[$cookie_name] ) ) {
2428 // phpcs:disable WordPress.Security.ValidatedSanitizedInput
2429 $value = wp_filter_nohtml_kses( wp_unslash( $_COOKIE[$cookie_name] ) );
2430 // phpcs:enable WordPress.Security.ValidatedSanitizedInput
2431 return $value;
2432 } else {
2433 return null;
2434 }
2435 }
2436
2437 /**
2438 * Print column headers
2439 *
2440 * Overriding original method print_column_headers to support post instead of get.
2441 * Changes are marked!
2442 *
2443 * @param boolean $with_id Whether to set the id attribute or not.
2444 *
2445 * @since 1.0.0
2446 *
2447 * @staticvar $cb_counter int
2448 */
2449 public function print_column_headers( $with_id = true ) {
2450 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
2451 // phpcs:ignore -- 8.1 proof
2452 // *********************
2453 // *** BEGIN CHANGES ***
2454 // *********************
2455 // Code removed.
2456 // *******************
2457 // *** END CHANGES ***
2458 // *******************
2459 if ( isset( $_REQUEST['orderby'] ) ) {
2460 $current_orderby = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) );
2461 } else {
2462 $current_orderby = '';
2463 }
2464 if ( isset( $_REQUEST['order'] ) && 'desc' === $_REQUEST['order'] ) {
2465 $current_order = 'desc';
2466 } else {
2467 $current_order = 'asc';
2468 }
2469 if ( !empty( $columns['cb'] ) ) {
2470 static $cb_counter = 1;
2471 $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All', 'wp-data-access' ) . '</label>' . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
2472 $cb_counter++;
2473 }
2474 foreach ( $columns as $column_key => $column_display_name ) {
2475 $class = array('manage-column', "column-{$column_key}");
2476 if ( in_array( $column_key, (array) $hidden ) ) {
2477 // phpcs:ignore -- 8.1 proof
2478 $class[] = 'hidden';
2479 }
2480 if ( 'cb' === $column_key ) {
2481 $class[] = 'check-column';
2482 } elseif ( in_array( $column_key, array('posts', 'comments', 'links') ) ) {
2483 $class[] = 'num';
2484 }
2485 if ( $column_key === $primary ) {
2486 $class[] = 'column-primary';
2487 }
2488 if ( isset( $sortable[$column_key] ) ) {
2489 list( $orderby, $desc_first ) = (array) $sortable[$column_key];
2490 // phpcs:ignore -- 8.1 proof
2491 if ( $current_orderby === $orderby ) {
2492 $order = ( 'asc' === $current_order ? 'desc' : 'asc' );
2493 $class[] = 'sorted';
2494 $class[] = $current_order;
2495 } else {
2496 $order = ( $desc_first ? 'desc' : 'asc' );
2497 $class[] = 'sortable';
2498 $class[] = ( $desc_first ? 'asc' : 'desc' );
2499 }
2500 // *********************
2501 // *** BEGIN CHANGES ***
2502 // *********************
2503 // Code removed.
2504 // *******************
2505 // *** END CHANGES ***
2506 // *******************
2507 }
2508 $tag = ( 'cb' === $column_key ? 'td' : 'th' );
2509 $scope = ( 'th' === $tag ? 'scope="col"' : '' );
2510 $id = ( $with_id ? "id='{$column_key}'" : '' );
2511 if ( !empty( $class ) ) {
2512 $class = "class='" . join( ' ', $class ) . "'";
2513 }
2514 // *********************
2515 // *** BEGIN CHANGES ***
2516 // *********************
2517 echo '<' . esc_attr( $tag ) . ' ' . wp_kses_data( $scope ) . ' ' . wp_kses_data( $id ) . ' ' . wp_kses_data( $class ) . '>';
2518 if ( isset( $sortable[$column_key] ) ) {
2519 ?>
2520 <a href="javascript:void(0)"
2521 onclick="jQuery('#wpda_main_form_orderby').val('<?php
2522 echo esc_attr( $orderby );
2523 ?>'); jQuery('#wpda_main_form_order').val('<?php
2524 echo esc_attr( $order );
2525 ?>'); jQuery('#wpda_main_form').submit();">
2526 <span><?php
2527 echo wp_kses_data( $column_display_name );
2528 ?></span><span
2529 class="sorting-indicator"></span>
2530 </a>
2531 <?php
2532 } else {
2533 echo wp_kses( $column_display_name, array(
2534 'input' => array(
2535 'id' => array(),
2536 'type' => array(),
2537 ),
2538 'label' => array(
2539 'class' => array(),
2540 'for' => array(),
2541 ),
2542 ) );
2543 }
2544 echo '</' . esc_attr( $tag ) . '>';
2545 // *******************
2546 // *** END CHANGES ***
2547 // *******************
2548 }
2549 }
2550
2551 /**
2552 * Returns an associative array containing the bulk action
2553 *
2554 * @return array
2555 * @since 1.0.0
2556 */
2557 public function get_bulk_actions() {
2558 if ( !$this->bulk_actions_enabled ) {
2559 // Bulk actions disabled.
2560 return '';
2561 }
2562 if ( empty( $this->wpda_list_columns->get_table_primary_key() ) ) {
2563 // Tables has no primary key: no bulk actions allowed!
2564 // Primary key is necessary to ensure uniqueness.
2565 return '';
2566 }
2567 $actions = array();
2568 if ( 'on' === $this->allow_delete ) {
2569 $actions = array(
2570 'bulk-delete' => __( 'Delete Permanently', 'wp-data-access' ),
2571 );
2572 }
2573 if ( $this->bulk_export_enabled && (WPDA::is_wpda_table( $this->table_name ) || WPDA::get_option( WPDA::OPTION_BE_EXPORT_ROWS ) === 'on') ) {
2574 if ( 'diehard' !== $this->page ) {
2575 // Not allowed on web page.
2576 $actions['bulk-export'] = __( 'Export to SQL', 'wp-data-access' );
2577 }
2578 $actions['bulk-export-xml'] = __( 'Export to XML', 'wp-data-access' );
2579 $actions['bulk-export-json'] = __( 'Export to JSON', 'wp-data-access' );
2580 $actions['bulk-export-excel'] = __( 'Export to Excel', 'wp-data-access' );
2581 $actions['bulk-export-csv'] = __( 'Export to CSV', 'wp-data-access' );
2582 }
2583 return $actions;
2584 }
2585
2586 /**
2587 * Generates the table navigation
2588 *
2589 * Generates the table navigation above or bellow the table and removes the
2590 * _wp_http_referrer and _wpnonce because it generates a error about URL too large.
2591 *
2592 * @param string $which CSS Class name.
2593 *
2594 * @return void
2595 * @since 1.0.0
2596 */
2597 protected function display_tablenav( $which ) {
2598 if ( !$this->hide_navigation && $this->items ) {
2599 ?>
2600 <div class="tablenav <?php
2601 echo esc_attr( $which );
2602 ?>">
2603 <div class="alignleft actions">
2604 <?php
2605 $this->bulk_actions( $which );
2606 ?>
2607 </div>
2608 <span id="wpda_row_export_anchor"></span>
2609 <?php
2610 $this->add_full_table_downloads();
2611 $this->extra_tablenav( $which );
2612 $this->pagination( $which );
2613 ?>
2614 <br class="clear"/>
2615 </div>
2616 <?php
2617 } else {
2618 ?>
2619 <br class="clear"/>
2620 <?php
2621 }
2622 }
2623
2624 /**
2625 * Add full export to CSV and JSON buttons
2626 * Needs to be granted in project page configuration
2627 */
2628 protected function add_full_table_downloads() {
2629 }
2630
2631 // Override to add arguments to CSV and JSON full table exports.
2632 protected function add_full_table_downloads_add_args() {
2633 }
2634
2635 /**
2636 * Display the pagination
2637 *
2638 * Overriding original method pagination to support post instead of get.
2639 * Changes are marked!
2640 *
2641 * @param string $which CSS Class name.
2642 *
2643 * @since 1.0.0
2644 */
2645 protected function pagination( $which ) {
2646 if ( empty( $this->_pagination_args ) ) {
2647 return;
2648 }
2649 $total_items = $this->_pagination_args['total_items'];
2650 $total_pages = $this->_pagination_args['total_pages'];
2651 $infinite_scroll = false;
2652 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
2653 $infinite_scroll = $this->_pagination_args['infinite_scroll'];
2654 }
2655 if ( 'top' === $which && $total_pages > 1 ) {
2656 $this->screen->render_screen_reader_content( 'heading_pagination' );
2657 }
2658 // Add estimate character if row_count_estimate is enabled.
2659 $estimate = ( $this->row_count_estimate['is_estimate'] ? '~' : '' );
2660 /* translators: %s: number of items (2x) */
2661 $output = '<span class="displaying-num">' . sprintf( _n(
2662 '%s item',
2663 '%s items',
2664 $total_items,
2665 'wp-data-access'
2666 ), $estimate . number_format_i18n( $total_items ) ) . '</span>';
2667 $current = $this->get_pagenum();
2668 if ( $this->search_value !== $this->search_value_old ) {
2669 $current = 1;
2670 }
2671 // *********************
2672 // *** BEGIN CHANGES ***
2673 // *********************
2674 // Code removed.
2675 // *******************
2676 // *** END CHANGES ***
2677 // *******************
2678 $page_links = array();
2679 $total_pages_before = '<span class="paging-input">';
2680 $total_pages_after = '</span></span>';
2681 $disable_first = $disable_last = $disable_prev = $disable_next = false;
2682 if ( 1 === (int) $current ) {
2683 $disable_first = true;
2684 $disable_prev = true;
2685 }
2686 if ( 2 === (int) $current ) {
2687 $disable_first = true;
2688 }
2689 if ( (int) $current === (int) $total_pages ) {
2690 $disable_last = true;
2691 $disable_next = true;
2692 }
2693 if ( (int) $current === (int) $total_pages - 1 ) {
2694 $disable_last = true;
2695 }
2696 // *********************
2697 // *** BEGIN CHANGES ***
2698 // *********************
2699 $link_with_post_support = "\n <a class='%s'\n href='javascript:void(0)'\n onclick='jQuery(\"#current-page-selector\").val(\"%s\"); jQuery(\"#wpda_main_form\").submit();'>\n <span class='screen-reader-text'>%s</span>\n <span aria-hidden='true'>%s</span>\n </a>";
2700 // *******************
2701 // *** END CHANGES ***
2702 // *******************
2703 if ( $disable_first ) {
2704 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
2705 } else {
2706 // *********************
2707 // *** BEGIN CHANGES ***
2708 // *********************
2709 $page_links[] = sprintf(
2710 $link_with_post_support,
2711 'first-page button',
2712 '',
2713 __( 'First page', 'wp-data-access' ),
2714 '&laquo;'
2715 );
2716 // *******************
2717 // *** END CHANGES ***
2718 // *******************
2719 }
2720 if ( $disable_prev ) {
2721 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
2722 } else {
2723 // *********************
2724 // *** BEGIN CHANGES ***
2725 // *********************
2726 $page_links[] = sprintf(
2727 $link_with_post_support,
2728 'prev-page button',
2729 max( 1, $current - 1 ),
2730 __( 'Previous page', 'wp-data-access' ),
2731 '&lsaquo;'
2732 );
2733 // *******************
2734 // *** END CHANGES ***
2735 // *******************
2736 }
2737 if ( 'bottom' === $which ) {
2738 $html_current_page = $current;
2739 $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page', 'wp-data-access' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">';
2740 } else {
2741 $html_current_page = sprintf(
2742 "%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
2743 '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page', 'wp-data-access' ) . '</label>',
2744 $current,
2745 strlen( (string) $total_pages )
2746 );
2747 }
2748 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
2749 /* translators: %s: current page/total pages */
2750 $page_links[] = $total_pages_before . sprintf( _x( '%1$s of %2$s', 'paging', 'wp-data-access' ), $html_current_page, $html_total_pages ) . $total_pages_after;
2751 if ( $disable_next ) {
2752 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
2753 } else {
2754 // *********************
2755 // *** BEGIN CHANGES ***
2756 // *********************
2757 $page_links[] = sprintf(
2758 $link_with_post_support,
2759 'next-page button',
2760 min( $total_pages, $current + 1 ),
2761 __( 'Next page', 'wp-data-access' ),
2762 '&rsaquo;'
2763 );
2764 // *******************
2765 // *** END CHANGES ***
2766 // *******************
2767 }
2768 if ( $disable_last ) {
2769 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
2770 } else {
2771 // *********************
2772 // *** BEGIN CHANGES ***
2773 // *********************
2774 $page_links[] = sprintf(
2775 $link_with_post_support,
2776 'last-page button',
2777 $total_pages,
2778 __( 'Last page', 'wp-data-access' ),
2779 '&raquo;'
2780 );
2781 // *******************
2782 // *** END CHANGES ***
2783 // *******************
2784 }
2785 $pagination_links_class = 'pagination-links';
2786 if ( !empty( $infinite_scroll ) ) {
2787 $pagination_links_class = ' hide-if-js';
2788 }
2789 $output .= "\n<span class='{$pagination_links_class}'>" . join( "\n", $page_links ) . '</span>';
2790 if ( $total_pages ) {
2791 $page_class = ( $total_pages < 2 ? ' one-page' : '' );
2792 } else {
2793 $page_class = ' no-pages';
2794 }
2795 $this->_pagination = "<div class='tablenav-pages{$page_class}'>{$output}</div>";
2796 // phpcs:disable WordPress.Security.EscapeOutput
2797 echo $this->_pagination;
2798 // phpcs:enable WordPress.Security.EscapeOutput
2799 }
2800
2801 }
2802
2803 // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing