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 +193 -94 2.2.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
@@ -409,9 +430,9 @@
409 430 if ( TABLEPRESS_BASENAME === $file ) {
410 431 $links[] = '<a href="https://tablepress.org/faq/" title="' . esc_attr__( 'Frequently Asked Questions', 'tablepress' ) . '">' . __( 'FAQ', 'tablepress' ) . '</a>';
411 432 $links[] = '<a href="https://tablepress.org/documentation/">' . __( 'Documentation', 'tablepress' ) . '</a>';
412 433 $links[] = '<a href="https://tablepress.org/support/">' . __( 'Support', 'tablepress' ) . '</a>';
413 - if ( tb_tp_fs()->is_free_plan() ) {
434 + if ( ! TABLEPRESS_IS_PLAYGROUND_PREVIEW && tb_tp_fs()->is_free_plan() ) {
414 435 $links[] = '<a href="https://tablepress.org/premium/?utm_source=plugin&utm_medium=textlink&utm_content=plugins-screen" title="' . esc_attr__( 'Check out the Premium version of TablePress!', 'tablepress' ) . '"><strong>' . __( 'Go Premium', 'tablepress' ) . '</strong></a>';
415 436 }
416 437 }
417 438 return $links;
@@ -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,29 +522,28 @@
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' );
478 - $exporter = TablePress::load_class( 'TablePress_Export', 'class-export.php', 'classes' );
479 - $data['zip_support_available'] = $exporter->zip_support_available;
480 546 break;
481 547 case 'options':
482 548 /*
483 549 * Maybe try saving "Custom CSS" to a file:
@@ -484,9 +550,9 @@
484 550 * (called here, as the credentials form posts to this handler again, due to how `request_filesystem_credentials()` works)
485 551 */
486 552 if ( isset( $_GET['item'] ) && 'save_custom_css' === $_GET['item'] ) {
487 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.
488 - $action = 'options_custom_css'; // to load a different view
554 + $action = 'options_custom_css'; // To load a different view.
489 555 // Try saving "Custom CSS" to a file, otherwise this gets the HTML for the credentials form.
490 556 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
491 557 $result = $tablepress_css->save_custom_css_to_file_plugin_options( TablePress::$model_options->get( 'custom_css' ), TablePress::$model_options->get( 'custom_css_minified' ) );
492 558 if ( is_string( $result ) ) {
@@ -507,12 +573,12 @@
507 573 break;
508 574 }
509 575 $data['frontend_options']['use_custom_css'] = TablePress::$model_options->get( 'use_custom_css' );
510 576 $data['frontend_options']['custom_css'] = TablePress::$model_options->get( 'custom_css' );
511 - $data['user_options']['parent_page'] = $this->parent_page;
577 + $data['user_options']['parent_page'] = TablePress::$controller->parent_page;
512 578 break;
513 579 case 'edit':
514 - if ( empty( $_GET['table_id'] ) ) {
580 + if ( ! isset( $_GET['table_id'] ) || ! preg_match( '/^[a-zA-Z0-9_-]+$/', $_GET['table_id'] ) ) {
515 581 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_no_table' ) );
516 582 }
517 583 // Load table, with table data, options, and visibility settings.
518 584 $data['table'] = TablePress::$model_table->load( $_GET['table_id'], true, true );
@@ -532,12 +598,18 @@
532 598 continue;
533 599 }
534 600 // Load table, without table data, options, and visibility settings.
535 601 $table = TablePress::$model_table->load( $table_id, false, false );
536 - $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'];
537 609 }
538 610 $data['tables_count'] = TablePress::$model_table->count_tables();
539 - $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();
540 612 $exporter = TablePress::load_class( 'TablePress_Export', 'class-export.php', 'classes' );
541 613 $data['zip_support_available'] = $exporter->zip_support_available;
542 614 $data['export_formats'] = $exporter->export_formats;
543 615 $data['csv_delimiters'] = $exporter->csv_delimiters;
@@ -553,20 +625,25 @@
553 625 continue;
554 626 }
555 627 // Load table, without table data, options, and visibility settings.
556 628 $table = TablePress::$model_table->load( $table_id, false, false );
557 - $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'];
558 636 }
559 - $data['table_ids'] = $table_ids; // Backwards compatibility for the retired "Table Auto Update" Extension, which still relies on this variable name.
637 + $data['table_ids'] = $table_ids; // Backward compatibility for the retired "Table Auto Update" Extension, which still relies on this variable name.
560 638 $data['tables_count'] = TablePress::$model_table->count_tables();
561 639 $importer = TablePress::load_class( 'TablePress_Import', 'class-import.php', 'classes' );
562 - $data['zip_support_available'] = $importer->zip_support_available;
563 640 $data['import_type'] = ( ! empty( $_GET['import_type'] ) ) ? $_GET['import_type'] : 'add';
564 - $data['import_existing_table'] = ( ! empty( $_GET['import_existing_table'] ) ) ? $_GET['import_existing_table'] : '';
641 + $data['import_existing_table'] = $_GET['import_existing_table'] ?? '';
565 642 $data['import_source'] = ( ! empty( $_GET['import_source'] ) ) ? $_GET['import_source'] : 'file-upload';
566 - $data['import_url'] = ( ! empty( $_GET['import_url'] ) ) ? wp_unslash( $_GET['import_url'] ) : 'https://';
567 - $data['import_server'] = ( ! empty( $_GET['import_server'] ) ) ? wp_unslash( $_GET['import_server'] ) : ABSPATH;
568 - $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'] ) ) : '';
569 646 $data['legacy_import'] = ( ! empty( $_GET['legacy_import'] ) ) ? $_GET['legacy_import'] : 'false';
570 647 break;
571 648 }
572 649
@@ -749,9 +826,9 @@
749 826 }
750 827 break;
751 828 }
752 829
753 - if ( 0 !== count( $no_success ) ) { // @TODO: maybe pass this information to the view?
830 + if ( 0 !== count( $no_success ) ) { // @todo maybe pass this information to the view?
754 831 $message = "error_{$bulk_action}_not_all_tables";
755 832 } else {
756 833 $plural = ( count( $tables ) > 1 ) ? '_plural' : '';
757 834 $message = "success_{$bulk_action}{$plural}";
@@ -789,11 +866,11 @@
789 866 }
790 867
791 868 $add_table = wp_unslash( $_POST['table'] );
792 869
793 - // Perform sanity checks of posted data.
794 - $name = ( isset( $add_table['name'] ) ) ? $add_table['name'] : '';
795 - $description = ( isset( $add_table['description'] ) ) ? $add_table['description'] : '';
870 + // Perform confidence checks of posted data.
871 + $name = $add_table['name'] ?? '';
872 + $description = $add_table['description'] ?? '';
796 873 if ( ! isset( $add_table['rows'], $add_table['columns'] ) ) {
797 874 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add', 'error_details' => 'The HTTP POST data does not contain the table size.' ) );
798 875 }
799 876
@@ -853,10 +930,10 @@
853 930 if ( ! empty( $posted_options['admin_menu_parent_page'] ) && '-' !== $posted_options['admin_menu_parent_page'] ) {
854 931 $new_options['admin_menu_parent_page'] = $posted_options['admin_menu_parent_page'];
855 932 // Re-init parent information, as `TablePress::redirect()` URL might be wrong otherwise.
856 933 /** This filter is documented in classes/class-controller.php */
857 - $this->parent_page = apply_filters( 'tablepress_admin_menu_parent_page', $posted_options['admin_menu_parent_page'] );
858 - $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 );
859 936 }
860 937
861 938 // Custom CSS can only be saved if the user is allowed to do so.
862 939 $update_custom_css_files = false;
@@ -867,13 +944,20 @@
867 944 if ( isset( $posted_options['custom_css'] ) ) {
868 945 $new_options['custom_css'] = $posted_options['custom_css'];
869 946
870 947 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
871 - // Sanitize and tidy up Custom CSS.
872 - $new_options['custom_css'] = $tablepress_css->sanitize_css( $new_options['custom_css'] );
873 - // Minify Custom CSS.
874 - $new_options['custom_css_minified'] = $tablepress_css->minify_css( $new_options['custom_css'] );
875 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 +
876 960 // Maybe update CSS files as well.
877 961 $custom_css_file_contents = $tablepress_css->load_custom_css_from_file( 'normal' );
878 962 if ( false === $custom_css_file_contents ) {
879 963 $custom_css_file_contents = '';
@@ -927,10 +1011,9 @@
927 1011
928 1012 if ( empty( $export['format'] ) || ! isset( $exporter->export_formats[ $export['format'] ] ) ) {
929 1013 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export', 'error_details' => 'The export format is invalid.' ) );
930 1014 }
931 - if ( empty( $export['csv_delimiter'] ) ) {
932 - // Set a value, so that the variable exists.
1015 + if ( ! isset( $export['csv_delimiter'] ) ) {
933 1016 $export['csv_delimiter'] = '';
934 1017 }
935 1018 if ( 'csv' === $export['format'] && ! isset( $exporter->csv_delimiters[ $export['csv_delimiter'] ] ) ) {
936 1019 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export', 'error_details' => 'The CSV delimiter is invalid.' ) );
@@ -974,9 +1057,13 @@
974 1057 */
975 1058 $download_filename = apply_filters( 'tablepress_export_filename', $download_filename, $table['id'], $table['name'], $export['format'], $export_to_zip );
976 1059 $download_filename = sanitize_file_name( $download_filename );
977 1060 // Export the table.
978 - $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 );
979 1066 /**
980 1067 * Filters the exported table data.
981 1068 *
982 1069 * @since 1.6.0
@@ -1000,9 +1087,9 @@
1000 1087 /** This filter is documented in controllers/controller-admin.php */
1001 1088 $download_filename = apply_filters( 'tablepress_export_filename', $download_filename, '', '', $export['format'], $export_to_zip );
1002 1089 $download_filename = sanitize_file_name( $download_filename );
1003 1090 $full_filename = wp_tempnam( $download_filename );
1004 - if ( true !== $zip_file->open( $full_filename, ZIPARCHIVE::OVERWRITE ) ) {
1091 + if ( true !== $zip_file->open( $full_filename, ZipArchive::OVERWRITE ) ) {
1005 1092 @unlink( $full_filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1006 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.' ) );
1007 1094 }
1008 1095
@@ -1020,9 +1107,13 @@
1020 1107 // Don't export if the table is corrupted.
1021 1108 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
1022 1109 continue;
1023 1110 }
1024 - $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 );
1025 1116 /** This filter is documented in controllers/controller-admin.php */
1026 1117 $export_data = apply_filters( 'tablepress_export_data', $export_data, $table, $export['format'], $export['csv_delimiter'] );
1027 1118 $export_filename = sprintf( '%1$s-%2$s-%3$s.%4$s', $table['id'], $table['name'], wp_date( 'Y-m-d' ), $export['format'] );
1028 1119 /** This filter is documented in controllers/controller-admin.php */
@@ -1032,9 +1123,9 @@
1032 1123 }
1033 1124
1034 1125 // If something went wrong, or no files were added to the ZIP file, bail out.
1035 1126 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1036 - if ( ZIPARCHIVE::ER_OK !== $zip_file->status || 0 === $zip_file->numFiles ) {
1127 + if ( ZipArchive::ER_OK !== $zip_file->status || 0 === $zip_file->numFiles ) {
1037 1128 $zip_file->close();
1038 1129 @unlink( $full_filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1039 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.' ) );
1040 1131 }
@@ -1094,8 +1185,15 @@
1094 1185 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import', 'error_details' => 'You do not have the required access rights.' ) );
1095 1186 }
1096 1187 }
1097 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 +
1098 1196 // Move file upload data to the main import configuration.
1099 1197 $import_config['file-upload'] = $_FILES['import_file_upload'] ?? null;
1100 1198
1101 1199 // Check if the source data for the chosen import source is defined.
@@ -1125,9 +1223,9 @@
1125 1223 'import_source' => $import_config['source'],
1126 1224 'legacy_import' => $import_config['legacy_import'],
1127 1225 );
1128 1226 if ( in_array( $import_config['source'], array( 'url', 'server' ), true ) ) {
1129 - $redirect_parameters[ "import_{$import_config['source']}" ] = $import_config[ $import_config['source'] ];
1227 + $redirect_parameters[ "import_{$import_config['source']}" ] = rawurlencode( $import_config[ $import_config['source'] ] );
1130 1228 }
1131 1229 if ( is_wp_error( $import ) ) {
1132 1230 $redirect_parameters['error_details'] = TablePress::get_wp_error_string( $import );
1133 1231 } elseif ( 0 < count( $import['errors'] ) ) {
@@ -1132,9 +1230,9 @@
1132 1230 $redirect_parameters['error_details'] = TablePress::get_wp_error_string( $import );
1133 1231 } elseif ( 0 < count( $import['errors'] ) ) {
1134 1232 $wp_error_strings = array();
1135 1233 foreach ( $import['errors'] as $file ) {
1136 - $wp_error_strings[] = TablePress::get_wp_error_string( $file['error'] );
1234 + $wp_error_strings[] = TablePress::get_wp_error_string( $file->error );
1137 1235 }
1138 1236 $redirect_parameters['error_details'] = implode( ', ', $wp_error_strings );
1139 1237 }
1140 1238 TablePress::redirect( $redirect_parameters );
@@ -1159,9 +1257,9 @@
1159 1257 *
1160 1258 * @since 1.0.0
1161 1259 */
1162 1260 public function handle_get_action_hide_message(): void {
1163 - $message_item = ! empty( $_GET['item'] ) ? $_GET['item'] : '';
1261 + $message_item = $_GET['item'] ?? '';
1164 1262 TablePress::check_nonce( 'hide_message', $message_item );
1165 1263
1166 1264 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
1167 1265 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
@@ -1294,14 +1392,15 @@
1294 1392 $render_options = shortcode_atts( $default_render_options, $table['options'] );
1295 1393 /** This filter is documented in controllers/controller-frontend.php */
1296 1394 $render_options = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $render_options );
1297 1395 $render_options['html_id'] = "tablepress-{$table['id']}";
1396 + $render_options['block_preview'] = true;
1298 1397 $_render->set_input( $table, $render_options );
1299 1398 $view_data = array(
1300 - 'table_id' => $table_id,
1301 - 'head_html' => $_render->get_preview_css(),
1302 - 'body_html' => $_render->get_output(),
1303 - '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(),
1304 1403 );
1305 1404
1306 1405 $custom_css = TablePress::$model_options->get( 'custom_css' );
1307 1406 $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
@@ -1364,9 +1463,9 @@
1364 1463
1365 1464 TablePress::$model_table->destroy();
1366 1465 TablePress::$model_options->destroy();
1367 1466
1368 - $output = '<strong>' . __( 'TablePress was uninstalled successfully.', 'tablepress' ) . '</strong><br /><br />';
1467 + $output = '<strong>' . __( 'TablePress was uninstalled successfully.', 'tablepress' ) . '</strong><br><br>';
1369 1468 $output .= __( 'All tables, data, and options were deleted.', 'tablepress' );
1370 1469 if ( is_multisite() ) {
1371 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' );
1372 1471 } else {