PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
← All changes | controllers/controller-admin.php +188 -86 2.3.1 → 3.4 View file →
@@ -7,8 +7,10 @@
7 7 * @author Tobias Bäthge
8 8 * @since 1.0.0
9 9 */
10 10
11 +declare(strict_types=1);
12 +
11 13 // Prohibit direct script loading.
12 14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13 15
14 16 /**
@@ -27,9 +29,9 @@
27 29 *
28 30 * @since 1.0.0
29 31 * @var string[]
30 32 */
31 - protected $page_hooks = array();
33 + protected array $page_hooks = array();
32 34
33 35 /**
34 36 * Actions that have a view and admin menu or nav tab menu entry.
35 37 *
@@ -35,17 +37,16 @@
35 37 *
36 38 * @since 1.0.0
37 39 * @var array<string, array<string, bool|string>>
38 40 */
39 - protected $view_actions = array();
41 + protected array $view_actions = array();
40 42
41 43 /**
42 44 * Instance of the TablePress Admin View that is rendered.
43 45 *
44 46 * @since 1.0.0
45 - * @var TablePress_View
46 47 */
47 - protected $view;
48 + protected \TablePress_View $view;
48 49
49 50 /**
50 51 * Initialize the Admin Controller, determine location the admin menu, set up actions.
51 52 *
@@ -94,26 +95,34 @@
94 95 * @param string $entry_name The admin menu entry name. Default "TablePress".
95 96 */
96 97 $admin_menu_entry_name = apply_filters( 'tablepress_admin_menu_entry_name', 'TablePress' );
97 98
99 + if ( TablePress::$model_options->get( 'message_plugin_update' ) && strtotime( '2026-10-10' ) >= strtotime( 'today' ) ) {
100 + $admin_menu_entry_name .= ' <span class="dashicons dashicons-buddicons-community" aria-hidden="true" style="color:orange"></span>';
101 + }
102 +
98 103 $this->init_view_actions();
99 104 $min_access_cap = $this->view_actions['list']['required_cap'];
100 105
101 - if ( $this->is_top_level_page ) {
102 - $icon_url = 'dashicons-list-view';
103 - switch ( $this->parent_page ) {
106 + if ( TablePress::$controller->is_top_level_page ) {
107 + $icon_url = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iLTMyIC0zMiA2NCA2NCIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTAtMjUuODU0aC0yNS44NTR2NTEuNzA4aDUxLjcwOFYwSDIxdjIxaC00MnYtNDJIMFoiLz48cGF0aCBkPSJNLTE4LTE4aDEwdjEwaC0xMHpNLTE4LTVoMTBWNWgtMTB6TS01LTVINVY1SC01ek0tMTggOGgxMHYxMGgtMTB6TS01IDhINXYxMEgtNXpNOCA4aDEwdjEwSDh6TTUtMzFoNi4xOHY2LjE4SDV6TTE5LTI1aDYuMTh2Ni4xOEgxOXpNMC0xNWgzLjgydjMuODJIMHpNMTAtMjBoMy44MnYzLjgySDEwek0yNS0xMmgzLjgydjMuODJIMjV6TTgtMTNoMTB2MTBIOHoiLz48L3N2Zz4=';
108 + switch ( TablePress::$controller->parent_page ) {
104 109 case 'top':
105 110 $position = 3; // Position of Dashboard + 1.
106 111 break;
107 112 case 'bottom':
108 - $position = ( ++$GLOBALS['_wp_last_utility_menu'] );
113 + $position = isset( $GLOBALS['_wp_last_utility_menu'] ) ? ++$GLOBALS['_wp_last_utility_menu'] : 80;
109 114 break;
110 115 case 'middle':
111 116 default:
112 - $position = ( ++$GLOBALS['_wp_last_object_menu'] );
117 + $position = isset( $GLOBALS['_wp_last_object_menu'] ) ? ++$GLOBALS['_wp_last_object_menu'] : 25;
113 118 break;
114 119 }
115 - add_menu_page( 'TablePress', $admin_menu_entry_name, $min_access_cap, 'tablepress', $callback, $icon_url, $position ); // @phpstan-ignore-line
120 + // Prevent overwriting existing menu entries.
121 + while ( isset( $GLOBALS['menu'][ $position ] ) ) {
122 + ++$position;
123 + }
124 + add_menu_page( 'TablePress', $admin_menu_entry_name, $min_access_cap, 'tablepress', $callback, $icon_url, $position ); // @phpstan-ignore argument.type
116 125 foreach ( $this->view_actions as $action => $entry ) {
117 126 if ( ! $entry['show_entry'] ) {
118 127 continue;
119 128 }
@@ -120,17 +129,17 @@
120 129 $slug = 'tablepress';
121 130 if ( 'list' !== $action ) {
122 131 $slug .= '_' . $action;
123 132 }
124 - // @phpstan-ignore-next-line
125 - $page_hook = add_submenu_page( 'tablepress', sprintf( __( '%1$s &lsaquo; %2$s', 'tablepress' ), $entry['page_title'], 'TablePress' ), $entry['admin_menu_title'], $entry['required_cap'], $slug, $callback );
133 + /* translators: %1$s: Page title, %2$s: Plugin name (TablePress) */
134 + $page_hook = add_submenu_page( 'tablepress', sprintf( __( '%1$s &lsaquo; %2$s', 'tablepress' ), $entry['page_title'], 'TablePress' ), $entry['admin_menu_title'], $entry['required_cap'], $slug, $callback ); // @phpstan-ignore argument.type, argument.type
126 135 if ( false !== $page_hook ) {
127 136 $this->page_hooks[] = $page_hook;
128 137 }
129 138 }
130 139 } else {
131 - // @phpstan-ignore-next-line
132 - $page_hook = add_submenu_page( $this->parent_page, 'TablePress', $admin_menu_entry_name, $min_access_cap, 'tablepress', $callback );
140 + // @phpstan-ignore argument.type
141 + $page_hook = add_submenu_page( TablePress::$controller->parent_page, 'TablePress', $admin_menu_entry_name, $min_access_cap, 'tablepress', $callback );
133 142 if ( false !== $page_hook ) {
134 143 $this->page_hooks[] = $page_hook;
135 144 }
136 145 }
@@ -175,13 +184,8 @@
175 184 add_action( 'admin_bar_menu', array( $this, 'add_wp_admin_bar_new_content_menu_entry' ), 71 );
176 185 }
177 186
178 187 add_action( 'load-plugins.php', array( $this, 'plugins_page' ) );
179 -
180 - // Add filters and actions for the integration into the WP WXR exporter and importer.
181 - add_action( 'wp_import_insert_post', array( TablePress::$model_table, 'add_table_id_on_wp_import' ), 10, 4 );
182 - add_filter( 'wp_import_post_meta', array( TablePress::$model_table, 'prevent_table_id_post_meta_import_on_wp_import' ), 10, 3 );
183 - add_filter( 'wxr_export_skip_postmeta', array( TablePress::$model_table, 'add_table_id_to_wp_export' ), 10, 3 );
184 188 }
185 189
186 190 /**
187 191 * Loads additional JavaScript code for the TablePress table block (in the block editor context).
@@ -188,9 +192,8 @@
188 192 *
189 193 * @since 2.2.0
190 194 */
191 195 public function enqueue_block_editor_assets(): void {
192 - // Add table information for the block editor to the page.
193 196 $handle = generate_block_asset_handle( 'tablepress/table', 'editorScript' );
194 197 $data = $this->get_block_editor_data();
195 198 wp_add_inline_script( $handle, $data, 'before' );
196 199 }
@@ -202,9 +205,9 @@
202 205 */
203 206 public function enqueue_block_assets(): void {
204 207 // Load the TablePress default CSS and the user's "Custom CSS" in the block editor iframe.
205 208 if ( is_admin() ) {
206 - TablePress::$controller->enqueue_css();
209 + TablePress::$controller->maybe_enqueue_css();
207 210 }
208 211 }
209 212
210 213 /**
@@ -220,12 +223,18 @@
220 223 $table_ids = TablePress::$model_table->load_all( false );
221 224 foreach ( $table_ids as $table_id ) {
222 225 // Load table, without table data, options, and visibility settings.
223 226 $table = TablePress::$model_table->load( $table_id, false, false );
224 - if ( '' === trim( $table['name'] ) ) { // @phpstan-ignore-line
225 - $table['name'] = __( '(no name)', 'tablepress' ); // @phpstan-ignore-line
227 +
228 + // Skip tables that could not be loaded.
229 + if ( is_wp_error( $table ) ) {
230 + continue;
226 231 }
227 - $tables[ $table_id ] = esc_html( $table['name'] ); // @phpstan-ignore-line
232 +
233 + if ( '' === trim( $table['name'] ) ) {
234 + $table['name'] = __( '(no name)', 'tablepress' );
235 + }
236 + $tables[ $table_id ] = esc_html( $table['name'] );
228 237 }
229 238
230 239 /**
231 240 * Filters the list of table IDs and names that is passed to the block editor, and is then used in the dropdown of the TablePress table block.
@@ -235,26 +244,26 @@
235 244 * @param array<string, string> $tables List of table names, the table ID is the array key.
236 245 */
237 246 $tables = apply_filters( 'tablepress_block_editor_tables_list', $tables );
238 247
239 - $tables = wp_json_encode( $tables, TABLEPRESS_JSON_OPTIONS );
248 + $tables = wp_json_encode( $tables, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
240 249 if ( false === $tables ) {
241 250 // JSON encoding failed, return an error object. Use a prefixed "_error" key to avoid conflicts with intentionally added "error" keys.
242 251 $tables = '{ "_error": "The data could not be encoded to JSON!" }';
243 252 }
244 - // Print them inside a `JSON.parse()` call in JS for speed gains, with necessary escaping of `</script>`, `'`, and `\`.
245 - $tables = str_replace( array( '</script>', '\\', "'" ), array( '<\/script>', '\\\\', "\'" ), $tables );
253 + // Print the JSON data inside a `JSON.parse()` call in JS for speed gains, with necessary escaping of `\` and `'`.
254 + $tables = str_replace( array( '\\', "'" ), array( '\\\\', "\'" ), $tables );
246 255
247 256 $shortcode = esc_js( TablePress::$shortcode );
248 257
249 258 $template = TablePress::$model_table->get_table_template();
250 - $template = wp_json_encode( $template['options'], TABLEPRESS_JSON_OPTIONS );
259 + $template = wp_json_encode( $template['options'], JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
251 260 if ( false === $template ) {
252 261 // JSON encoding failed, return an error object. Use a prefixed "_error" key to avoid conflicts with intentionally added "error" keys.
253 262 $template = '{ "_error": "The data could not be encoded to JSON!" }';
254 263 }
255 - // Print them inside a `JSON.parse()` call in JS for speed gains, with necessary escaping of `</script>`, `'`, and `\`.
256 - $template = str_replace( array( '</script>', '\\', "'" ), array( '<\/script>', '\\\\', "\'" ), $template );
264 + // Print the JSON data inside a `JSON.parse()` call in JS for speed gains, with necessary escaping of `\` and `'`.
265 + $template = str_replace( array( '\\', "'" ), array( '\\\\', "\'" ), $template );
257 266
258 267 /**
259 268 * Filters whether the table block preview should be loaded via a <ServerSideRender> in the block editor.
260 269 *
@@ -270,17 +279,17 @@
270 279 $url = TablePress::url( array( 'action' => 'list' ) );
271 280 }
272 281
273 282 return <<<JS
274 -// Ensure the global `tp` object exists.
275 -window.tp = window.tp || {};
276 -tp.url = '{$url}';
277 -tp.load_block_preview = {$load_block_preview};
278 -tp.table = {};
279 -tp.table.shortcode = '{$shortcode}';
280 -tp.table.template = JSON.parse( '{$template}' );
281 -tp.tables = JSON.parse( '{$tables}' );
282 -JS;
283 + // Ensure the global `tp` object exists.
284 + window.tp = window.tp || {};
285 + tp.url = '{$url}';
286 + tp.load_block_preview = {$load_block_preview};
287 + tp.table = {};
288 + tp.table.shortcode = '{$shortcode}';
289 + tp.table.template = JSON.parse( '{$template}' );
290 + tp.tables = JSON.parse( '{$tables}' );
291 + JS;
283 292 }
284 293
285 294 /**
286 295 * Register actions to add "Table" button to "HTML editor" and "Visual editor" toolbars.
@@ -292,15 +301,14 @@
292 301 return;
293 302 }
294 303
295 304 // Only load the toolbar integration if the Block Editor is not used.
296 - if ( TablePress::site_uses_block_editor() ) {
305 + if ( 'block' === TablePress::site_used_editor() ) {
297 306 return;
298 307 }
299 308
300 309 add_thickbox(); // The files are usually already loaded by media upload functions.
301 - $admin_page = TablePress::load_class( 'TablePress_Admin_Page', 'class-admin-page-helper.php', 'classes' );
302 - $admin_page->enqueue_script(
310 + TablePress::enqueue_script(
303 311 'quicktags-button',
304 312 array( 'quicktags', 'media-upload' ),
305 313 array(
306 314 'editor_button' => array(
@@ -308,9 +316,9 @@
308 316 'title' => __( 'Insert a TablePress table', 'tablepress' ),
309 317 'thickbox_title' => __( 'Insert a TablePress table', 'tablepress' ),
310 318 'thickbox_url' => TablePress::url( array( 'action' => 'editor_button_thickbox' ), true, 'admin-post.php' ),
311 319 ),
312 - )
320 + ),
313 321 );
314 322
315 323 // TinyMCE integration.
316 324 if ( user_can_richedit() ) {
@@ -364,9 +372,9 @@
364 372
365 373 $wp_admin_bar->add_menu( array(
366 374 'parent' => 'new-content',
367 375 'id' => 'new-tablepress-table',
368 - 'title' => __( 'TablePress Table', 'tablepress' ),
376 + 'title' => __( 'TablePress table', 'tablepress' ),
369 377 'href' => TablePress::url( array( 'action' => 'add' ) ),
370 378 ) );
371 379 }
372 380
@@ -378,8 +386,21 @@
378 386 public function plugins_page(): void {
379 387 // Add additional links on Plugins page.
380 388 add_filter( 'plugin_action_links_' . TABLEPRESS_BASENAME, array( $this, 'add_plugin_action_links' ) );
381 389 add_filter( 'plugin_row_meta', array( $this, 'add_plugin_row_meta' ), 10, 2 );
390 + $incompatible_superseded_extensions = array(
391 + 'tablepress-datatables-alphabetsearch/tablepress-datatables-alphabetsearch.php',
392 + 'tablepress-datatables-column-filter-widgets/tablepress-datatables-column-filter-widgets.php',
393 + 'tablepress-datatables-columnfilter/tablepress-datatables-columnfilter.php',
394 + 'tablepress-datatables-fixedcolumns/tablepress-datatables-fixedcolumns.php',
395 + 'tablepress-datatables-inverted-filter/tablepress-datatables-inverted-filter.php',
396 + 'tablepress-datatables-row-details/tablepress-datatables-row-details.php',
397 + 'tablepress-datatables-rowgroup/tablepress-datatables-rowgroup.php',
398 + 'tablepress-responsive-tables/tablepress-responsive-tables.php',
399 + );
400 + foreach ( $incompatible_superseded_extensions as $plugin_file ) {
401 + add_action( "after_plugin_row_{$plugin_file}", array( $this, 'add_superseded_extension_meta_row' ), 10, 3 );
402 + }
382 403 }
383 404
384 405 /**
385 406 * Add links to the TablePress entry in the "Plugin" column on the Plugins page.
@@ -390,9 +411,9 @@
390 411 * @return string[] Extended list of links to print in the "Plugin" column on the Plugins page.
391 412 */
392 413 public function add_plugin_action_links( array $links ): array {
393 414 if ( current_user_can( 'tablepress_list_tables' ) ) {
394 - $links[] = '<a href="' . TablePress::url() . '">' . __( 'Plugin page', 'tablepress' ) . '</a>';
415 + $links[] = '<a href="' . esc_url( TablePress::url() ) . '">' . __( 'Plugin page', 'tablepress' ) . '</a>';
395 416 }
396 417 return $links;
397 418 }
398 419
@@ -417,8 +438,54 @@
417 438 return $links;
418 439 }
419 440
420 441 /**
442 + * Prints a superseded extension notice below certain TablePress Extension plugins' meta rows on the "Plugins" screen.
443 + *
444 + * @since 2.4.1
445 + *
446 + * @param string $plugin_file Path to the plugin file relative to the plugins directory.
447 + * @param array<int, string|string[]|bool> $plugin_data An array of plugin data.
448 + * @param string $status Status filter currently applied to the plugin list.
449 + */
450 + public function add_superseded_extension_meta_row( string $plugin_file, array $plugin_data, string $status ): void {
451 + if ( ! is_plugin_active( $plugin_file ) ) {
452 + return;
453 + }
454 + ?>
455 + <tr class="plugin-update-tr active">
456 + <td colspan="<?php echo esc_attr( $GLOBALS['wp_list_table']->get_column_count() ); ?>" class="plugin-update colspanchange">
457 + <div class="update-message notice inline notice-error notice-alt">
458 + <?php
459 + if ( tb_tp_fs()->is_free_plan() ) {
460 + echo '<p style="font-size:14px;">';
461 + _e( 'This TablePress Extension was retired.', 'tablepress' );
462 + echo ' ';
463 + _e( '<strong>The plugin does no longer work</strong> and will no longer receive updates or support!', 'tablepress' );
464 + echo '<br>';
465 + _e( 'Keeping it activated can lead to errors on your website!', 'tablepress' );
466 + echo ' <strong>' . sprintf( __( '<a href="%s">Find out what you can do to continue using its features!</a>', 'tablepress' ), 'https://tablepress.org/upgrade-extensions/?utm_source=plugin&utm_medium=textlink&utm_content=plugins-list-table' ) . '</strong>';
467 + echo '</p>';
468 + }
469 + ?>
470 + <style>
471 + /* Remove the separator line between the plugin's and the notice's table row. */
472 + .plugins .active[data-plugin="<?php echo $plugin_file; ?>"] th,
473 + .plugins .active[data-plugin="<?php echo $plugin_file; ?>"] td {
474 + box-shadow: none;
475 + }
476 + /* Hide the plugin update row for the Extension as those won't work anymore anyways. */
477 + .plugins .plugin-update-tr[data-plugin="<?php echo $plugin_file; ?>"] {
478 + display: none;
479 + }
480 + </style>
481 + </div>
482 + </td>
483 + </tr>
484 + <?php
485 + }
486 +
487 + /**
421 488 * Prepare the rendering of an admin screen, by determining the current action, loading necessary data and initializing the view.
422 489 *
423 490 * @since 1.0.0
424 491 */
@@ -424,9 +491,9 @@
424 491 */
425 492 public function load_admin_page(): void {
426 493 // Determine the action from either the GET parameter (for sub-menu entries, and the main admin menu entry).
427 494 $action = ( ! empty( $_GET['action'] ) ) ? $_GET['action'] : 'list'; // Default action is list.
428 - if ( $this->is_top_level_page ) {
495 + if ( TablePress::$controller->is_top_level_page ) {
429 496 // Or, for sub-menu entry of an admin menu "TablePress" entry, get it from the "page" GET parameter.
430 497 if ( 'tablepress' !== $_GET['page'] ) {
431 498 // Actions that are top-level entries, but don't have an action GET parameter (action is after last _ in string).
432 499 $action = substr( $_GET['page'], 11 ); // $_GET['page'] has the format 'tablepress_{$action}'
@@ -433,9 +500,9 @@
433 500 }
434 501 }
435 502
436 503 // Check if action is a supported action, and whether the user is allowed to access this screen.
437 - if ( ! isset( $this->view_actions[ $action ] ) || ! current_user_can( $this->view_actions[ $action ]['required_cap'] ) ) { // @phpstan-ignore-line
504 + if ( ! isset( $this->view_actions[ $action ] ) || ! current_user_can( $this->view_actions[ $action ]['required_cap'] ) ) { // @phpstan-ignore argument.type (The array value for the capability is always a string.)
438 505 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
439 506 }
440 507
441 508 // Don't load TablePress assets on the Freemius opt-in/activation screen.
@@ -455,23 +522,24 @@
455 522 }
456 523
457 524 // Pre-define some view data.
458 525 $data = array(
459 - 'view_actions' => $this->view_actions,
460 - 'message' => ( ! empty( $_GET['message'] ) ) ? $_GET['message'] : false,
461 - 'error_details' => ( ! empty( $_GET['error_details'] ) ) ? $_GET['error_details'] : '',
462 - 'site_uses_block_editor' => TablePress::site_uses_block_editor(),
526 + 'view_actions' => $this->view_actions,
527 + 'message' => ( ! empty( $_GET['message'] ) ) ? $_GET['message'] : false,
528 + 'error_details' => ( ! empty( $_GET['error_details'] ) ) ? rawurldecode( wp_unslash( $_GET['error_details'] ) ) : '',
529 + 'site_used_editor' => TablePress::site_used_editor(),
463 530 );
464 531
465 532 // Depending on the action, load more necessary data for the corresponding view.
466 533 switch ( $action ) {
467 534 case 'list':
468 - $data['table_id'] = ( ! empty( $_GET['table_id'] ) ) ? $_GET['table_id'] : false;
535 + $data['table_id'] = ( isset( $_GET['table_id'] ) ) ? preg_replace( '/[^a-zA-Z0-9_-]/', '', $_GET['table_id'] ) : false;
469 536 // Prime the post meta cache for cached loading of last_editor.
470 537 $data['table_ids'] = TablePress::$model_table->load_all( true );
471 - $data['messages']['donation_message'] = $this->maybe_show_donation_message();
472 - $data['messages']['first_visit'] = ! $data['messages']['donation_message'] && TablePress::$model_options->get( 'message_first_visit' );
473 - $data['messages']['plugin_update_message'] = TablePress::$model_options->get( 'message_plugin_update' );
538 + $data['messages']['donation_nag'] = $this->maybe_show_donation_message();
539 + $data['messages']['first_visit'] = ! $data['messages']['donation_nag'] && TablePress::$model_options->get( 'message_first_visit' );
540 + $data['messages']['plugin_update'] = TablePress::$model_options->get( 'message_plugin_update' );
541 + $data['messages']['superseded_extensions'] = current_user_can( 'manage_options' ) && TablePress::$model_options->get( 'message_superseded_extensions' );
474 542 $data['table_count'] = count( $data['table_ids'] );
475 543 break;
476 544 case 'about':
477 545 $data['first_activation'] = TablePress::$model_options->get( 'first_activation' );
@@ -482,9 +550,9 @@
482 550 * (called here, as the credentials form posts to this handler again, due to how `request_filesystem_credentials()` works)
483 551 */
484 552 if ( isset( $_GET['item'] ) && 'save_custom_css' === $_GET['item'] ) {
485 553 TablePress::check_nonce( 'options', $_GET['item'] ); // Nonce check here, as we don't have an explicit handler, and even viewing the screen needs to be checked.
486 - $action = 'options_custom_css'; // to load a different view
554 + $action = 'options_custom_css'; // To load a different view.
487 555 // Try saving "Custom CSS" to a file, otherwise this gets the HTML for the credentials form.
488 556 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
489 557 $result = $tablepress_css->save_custom_css_to_file_plugin_options( TablePress::$model_options->get( 'custom_css' ), TablePress::$model_options->get( 'custom_css_minified' ) );
490 558 if ( is_string( $result ) ) {
@@ -505,12 +573,12 @@
505 573 break;
506 574 }
507 575 $data['frontend_options']['use_custom_css'] = TablePress::$model_options->get( 'use_custom_css' );
508 576 $data['frontend_options']['custom_css'] = TablePress::$model_options->get( 'custom_css' );
509 - $data['user_options']['parent_page'] = $this->parent_page;
577 + $data['user_options']['parent_page'] = TablePress::$controller->parent_page;
510 578 break;
511 579 case 'edit':
512 - if ( empty( $_GET['table_id'] ) ) {
580 + if ( ! isset( $_GET['table_id'] ) || ! preg_match( '/^[a-zA-Z0-9_-]+$/', $_GET['table_id'] ) ) {
513 581 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_no_table' ) );
514 582 }
515 583 // Load table, with table data, options, and visibility settings.
516 584 $data['table'] = TablePress::$model_table->load( $_GET['table_id'], true, true );
@@ -530,12 +598,18 @@
530 598 continue;
531 599 }
532 600 // Load table, without table data, options, and visibility settings.
533 601 $table = TablePress::$model_table->load( $table_id, false, false );
534 - $data['tables'][ $table['id'] ] = $table['name']; // @phpstan-ignore-line
602 +
603 + // Skip tables that could not be loaded.
604 + if ( is_wp_error( $table ) ) {
605 + continue;
606 + }
607 +
608 + $data['tables'][ $table['id'] ] = $table['name'];
535 609 }
536 610 $data['tables_count'] = TablePress::$model_table->count_tables();
537 - $data['export_ids'] = ( ! empty( $_GET['table_id'] ) ) ? explode( ',', $_GET['table_id'] ) : array();
611 + $data['export_ids'] = ( isset( $_GET['table_id'] ) && preg_match( '/^[,a-zA-Z0-9_-]+$/', $_GET['table_id'] ) ) ? explode( ',', $_GET['table_id'] ) : array();
538 612 $exporter = TablePress::load_class( 'TablePress_Export', 'class-export.php', 'classes' );
539 613 $data['zip_support_available'] = $exporter->zip_support_available;
540 614 $data['export_formats'] = $exporter->export_formats;
541 615 $data['csv_delimiters'] = $exporter->csv_delimiters;
@@ -551,19 +625,25 @@
551 625 continue;
552 626 }
553 627 // Load table, without table data, options, and visibility settings.
554 628 $table = TablePress::$model_table->load( $table_id, false, false );
555 - $data['tables'][ $table['id'] ] = $table['name']; // @phpstan-ignore-line
629 +
630 + // Skip tables that could not be loaded.
631 + if ( is_wp_error( $table ) ) {
632 + continue;
633 + }
634 +
635 + $data['tables'][ $table['id'] ] = $table['name'];
556 636 }
557 637 $data['table_ids'] = $table_ids; // Backward compatibility for the retired "Table Auto Update" Extension, which still relies on this variable name.
558 638 $data['tables_count'] = TablePress::$model_table->count_tables();
559 639 $importer = TablePress::load_class( 'TablePress_Import', 'class-import.php', 'classes' );
560 640 $data['import_type'] = ( ! empty( $_GET['import_type'] ) ) ? $_GET['import_type'] : 'add';
561 - $data['import_existing_table'] = ( ! empty( $_GET['import_existing_table'] ) ) ? $_GET['import_existing_table'] : '';
641 + $data['import_existing_table'] = $_GET['import_existing_table'] ?? '';
562 642 $data['import_source'] = ( ! empty( $_GET['import_source'] ) ) ? $_GET['import_source'] : 'file-upload';
563 - $data['import_url'] = ( ! empty( $_GET['import_url'] ) ) ? wp_unslash( $_GET['import_url'] ) : 'https://';
564 - $data['import_server'] = ( ! empty( $_GET['import_server'] ) ) ? wp_unslash( $_GET['import_server'] ) : ABSPATH;
565 - $data['import_form-field'] = ( ! empty( $_GET['import_form-field'] ) ) ? wp_unslash( $_GET['import_form-field'] ) : '';
643 + $data['import_url'] = ( ! empty( $_GET['import_url'] ) ) ? rawurldecode( wp_unslash( $_GET['import_url'] ) ) : 'https://';
644 + $data['import_server'] = ( ! empty( $_GET['import_server'] ) ) ? rawurldecode( wp_unslash( $_GET['import_server'] ) ) : ABSPATH;
645 + $data['import_form-field'] = ( ! empty( $_GET['import_form-field'] ) ) ? rawurldecode( wp_unslash( $_GET['import_form-field'] ) ) : '';
566 646 $data['legacy_import'] = ( ! empty( $_GET['legacy_import'] ) ) ? $_GET['legacy_import'] : 'false';
567 647 break;
568 648 }
569 649
@@ -787,10 +867,10 @@
787 867
788 868 $add_table = wp_unslash( $_POST['table'] );
789 869
790 870 // Perform confidence checks of posted data.
791 - $name = ( isset( $add_table['name'] ) ) ? $add_table['name'] : '';
792 - $description = ( isset( $add_table['description'] ) ) ? $add_table['description'] : '';
871 + $name = $add_table['name'] ?? '';
872 + $description = $add_table['description'] ?? '';
793 873 if ( ! isset( $add_table['rows'], $add_table['columns'] ) ) {
794 874 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add', 'error_details' => 'The HTTP POST data does not contain the table size.' ) );
795 875 }
796 876
@@ -850,10 +930,10 @@
850 930 if ( ! empty( $posted_options['admin_menu_parent_page'] ) && '-' !== $posted_options['admin_menu_parent_page'] ) {
851 931 $new_options['admin_menu_parent_page'] = $posted_options['admin_menu_parent_page'];
852 932 // Re-init parent information, as `TablePress::redirect()` URL might be wrong otherwise.
853 933 /** This filter is documented in classes/class-controller.php */
854 - $this->parent_page = apply_filters( 'tablepress_admin_menu_parent_page', $posted_options['admin_menu_parent_page'] );
855 - $this->is_top_level_page = in_array( $this->parent_page, array( 'top', 'middle', 'bottom' ), true );
934 + TablePress::$controller->parent_page = apply_filters( 'tablepress_admin_menu_parent_page', $posted_options['admin_menu_parent_page'] );
935 + TablePress::$controller->is_top_level_page = in_array( TablePress::$controller->parent_page, array( 'top', 'middle', 'bottom' ), true );
856 936 }
857 937
858 938 // Custom CSS can only be saved if the user is allowed to do so.
859 939 $update_custom_css_files = false;
@@ -864,13 +944,20 @@
864 944 if ( isset( $posted_options['custom_css'] ) ) {
865 945 $new_options['custom_css'] = $posted_options['custom_css'];
866 946
867 947 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
868 - // Sanitize and tidy up Custom CSS.
869 - $new_options['custom_css'] = $tablepress_css->sanitize_css( $new_options['custom_css'] );
870 - // Minify Custom CSS.
871 - $new_options['custom_css_minified'] = $tablepress_css->minify_css( $new_options['custom_css'] );
872 948
949 + if ( '' !== $new_options['custom_css'] ) {
950 + // Update "Custom CSS" to use DataTables 2 variants instead of old DataTables 1.x CSS classes.
951 + $new_options['custom_css'] = TablePress::convert_datatables_api_data( $new_options['custom_css'] );
952 + // Sanitize and tidy up Custom CSS.
953 + $new_options['custom_css'] = $tablepress_css->sanitize_css( $new_options['custom_css'] );
954 + // Minify Custom CSS.
955 + $new_options['custom_css_minified'] = $tablepress_css->minify_css( $new_options['custom_css'] );
956 + } else {
957 + $new_options['custom_css_minified'] = '';
958 + }
959 +
873 960 // Maybe update CSS files as well.
874 961 $custom_css_file_contents = $tablepress_css->load_custom_css_from_file( 'normal' );
875 962 if ( false === $custom_css_file_contents ) {
876 963 $custom_css_file_contents = '';
@@ -924,10 +1011,9 @@
924 1011
925 1012 if ( empty( $export['format'] ) || ! isset( $exporter->export_formats[ $export['format'] ] ) ) {
926 1013 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export', 'error_details' => 'The export format is invalid.' ) );
927 1014 }
928 - if ( empty( $export['csv_delimiter'] ) ) {
929 - // Set a value, so that the variable exists.
1015 + if ( ! isset( $export['csv_delimiter'] ) ) {
930 1016 $export['csv_delimiter'] = '';
931 1017 }
932 1018 if ( 'csv' === $export['format'] && ! isset( $exporter->csv_delimiters[ $export['csv_delimiter'] ] ) ) {
933 1019 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export', 'error_details' => 'The CSV delimiter is invalid.' ) );
@@ -971,9 +1057,13 @@
971 1057 */
972 1058 $download_filename = apply_filters( 'tablepress_export_filename', $download_filename, $table['id'], $table['name'], $export['format'], $export_to_zip );
973 1059 $download_filename = sanitize_file_name( $download_filename );
974 1060 // Export the table.
975 - $export_data = $exporter->export_table( $table, $export['format'], $export['csv_delimiter'] );
1061 + $options = array();
1062 + if ( 'csv' === $export['format'] ) {
1063 + $options['csv_delimiter'] = $export['csv_delimiter'];
1064 + }
1065 + $export_data = $exporter->export_table( $table, $export['format'], $options );
976 1066 /**
977 1067 * Filters the exported table data.
978 1068 *
979 1069 * @since 1.6.0
@@ -997,9 +1087,9 @@
997 1087 /** This filter is documented in controllers/controller-admin.php */
998 1088 $download_filename = apply_filters( 'tablepress_export_filename', $download_filename, '', '', $export['format'], $export_to_zip );
999 1089 $download_filename = sanitize_file_name( $download_filename );
1000 1090 $full_filename = wp_tempnam( $download_filename );
1001 - if ( true !== $zip_file->open( $full_filename, ZIPARCHIVE::OVERWRITE ) ) {
1091 + if ( true !== $zip_file->open( $full_filename, ZipArchive::OVERWRITE ) ) {
1002 1092 @unlink( $full_filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1003 1093 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_create_zip_file', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'], 'error_details' => 'The ZIP file could not be opened for writing.' ) );
1004 1094 }
1005 1095
@@ -1017,9 +1107,13 @@
1017 1107 // Don't export if the table is corrupted.
1018 1108 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
1019 1109 continue;
1020 1110 }
1021 - $export_data = $exporter->export_table( $table, $export['format'], $export['csv_delimiter'] );
1111 + $options = array();
1112 + if ( 'csv' === $export['format'] ) {
1113 + $options['csv_delimiter'] = $export['csv_delimiter'];
1114 + }
1115 + $export_data = $exporter->export_table( $table, $export['format'], $options );
1022 1116 /** This filter is documented in controllers/controller-admin.php */
1023 1117 $export_data = apply_filters( 'tablepress_export_data', $export_data, $table, $export['format'], $export['csv_delimiter'] );
1024 1118 $export_filename = sprintf( '%1$s-%2$s-%3$s.%4$s', $table['id'], $table['name'], wp_date( 'Y-m-d' ), $export['format'] );
1025 1119 /** This filter is documented in controllers/controller-admin.php */
@@ -1029,9 +1123,9 @@
1029 1123 }
1030 1124
1031 1125 // If something went wrong, or no files were added to the ZIP file, bail out.
1032 1126 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1033 - if ( ZIPARCHIVE::ER_OK !== $zip_file->status || 0 === $zip_file->numFiles ) {
1127 + if ( ZipArchive::ER_OK !== $zip_file->status || 0 === $zip_file->numFiles ) {
1034 1128 $zip_file->close();
1035 1129 @unlink( $full_filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1036 1130 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_create_zip_file', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'], 'error_details' => 'The ZIP file could not be written or is empty.' ) );
1037 1131 }
@@ -1091,8 +1185,15 @@
1091 1185 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import', 'error_details' => 'You do not have the required access rights.' ) );
1092 1186 }
1093 1187 }
1094 1188
1189 + // For security reasons, the "url" source is only available admins and editors via a custom capability.
1190 + if ( 'url' === $import_config['source'] ) {
1191 + if ( ! current_user_can( 'tablepress_import_tables_url' ) ) {
1192 + TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import', 'error_details' => 'You do not have the required access rights.' ) );
1193 + }
1194 + }
1195 +
1095 1196 // Move file upload data to the main import configuration.
1096 1197 $import_config['file-upload'] = $_FILES['import_file_upload'] ?? null;
1097 1198
1098 1199 // Check if the source data for the chosen import source is defined.
@@ -1122,9 +1223,9 @@
1122 1223 'import_source' => $import_config['source'],
1123 1224 'legacy_import' => $import_config['legacy_import'],
1124 1225 );
1125 1226 if ( in_array( $import_config['source'], array( 'url', 'server' ), true ) ) {
1126 - $redirect_parameters[ "import_{$import_config['source']}" ] = $import_config[ $import_config['source'] ];
1227 + $redirect_parameters[ "import_{$import_config['source']}" ] = rawurlencode( $import_config[ $import_config['source'] ] );
1127 1228 }
1128 1229 if ( is_wp_error( $import ) ) {
1129 1230 $redirect_parameters['error_details'] = TablePress::get_wp_error_string( $import );
1130 1231 } elseif ( 0 < count( $import['errors'] ) ) {
@@ -1156,9 +1257,9 @@
1156 1257 *
1157 1258 * @since 1.0.0
1158 1259 */
1159 1260 public function handle_get_action_hide_message(): void {
1160 - $message_item = ! empty( $_GET['item'] ) ? $_GET['item'] : '';
1261 + $message_item = $_GET['item'] ?? '';
1161 1262 TablePress::check_nonce( 'hide_message', $message_item );
1162 1263
1163 1264 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
1164 1265 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
@@ -1291,14 +1392,15 @@
1291 1392 $render_options = shortcode_atts( $default_render_options, $table['options'] );
1292 1393 /** This filter is documented in controllers/controller-frontend.php */
1293 1394 $render_options = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $render_options );
1294 1395 $render_options['html_id'] = "tablepress-{$table['id']}";
1396 + $render_options['block_preview'] = true;
1295 1397 $_render->set_input( $table, $render_options );
1296 1398 $view_data = array(
1297 - 'table_id' => $table_id,
1298 - 'head_html' => $_render->get_preview_css(),
1299 - 'body_html' => $_render->get_output( 'html' ),
1300 - 'site_uses_block_editor' => TablePress::site_uses_block_editor(),
1399 + 'table_id' => $table_id,
1400 + 'head_html' => $_render->get_preview_css(),
1401 + 'body_html' => $_render->get_output( 'html' ),
1402 + 'site_used_editor' => TablePress::site_used_editor(),
1301 1403 );
1302 1404
1303 1405 $custom_css = TablePress::$model_options->get( 'custom_css' );
1304 1406 $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
@@ -1361,9 +1463,9 @@
1361 1463
1362 1464 TablePress::$model_table->destroy();
1363 1465 TablePress::$model_options->destroy();
1364 1466
1365 - $output = '<strong>' . __( 'TablePress was uninstalled successfully.', 'tablepress' ) . '</strong><br /><br />';
1467 + $output = '<strong>' . __( 'TablePress was uninstalled successfully.', 'tablepress' ) . '</strong><br><br>';
1366 1468 $output .= __( 'All tables, data, and options were deleted.', 'tablepress' );
1367 1469 if ( is_multisite() ) {
1368 1470 $output .= ' ' . __( 'You may now ask the network admin to delete the plugin&#8217;s folder <code>tablepress</code> from the server, if no other site in the network uses it.', 'tablepress' );
1369 1471 } else {