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
tablepress / controllers / controller-admin.php

controller-admin.php in TablePress – Tables in WordPress made easy 3.4, at controllers/controller-admin.php

1,493 lines 63.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Controller for TablePress with the functionality for the non-AJAX backend
4 *
5 * @package TablePress
6 * @subpackage Controllers
7 * @author Tobias Bäthge
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 // Prohibit direct script loading.
14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
15
16 /**
17 * Admin Controller class, extends Base Controller Class
18 *
19 * @package TablePress
20 * @subpackage Controllers
21 * @author Tobias Bäthge
22 * @since 1.0.0
23 */
24 class TablePress_Admin_Controller extends TablePress_Controller {
25
26 /**
27 * Page hooks (i.e. names) WordPress uses for the TablePress admin screens,
28 * populated in add_admin_menu_entry().
29 *
30 * @since 1.0.0
31 * @var string[]
32 */
33 protected array $page_hooks = array();
34
35 /**
36 * Actions that have a view and admin menu or nav tab menu entry.
37 *
38 * @since 1.0.0
39 * @var array<string, array<string, bool|string>>
40 */
41 protected array $view_actions = array();
42
43 /**
44 * Instance of the TablePress Admin View that is rendered.
45 *
46 * @since 1.0.0
47 */
48 protected \TablePress_View $view;
49
50 /**
51 * Initialize the Admin Controller, determine location the admin menu, set up actions.
52 *
53 * @since 1.0.0
54 */
55 public function __construct() {
56 parent::__construct();
57
58 // Handler for changing the number of shown tables in the list of tables (via WP List Table class).
59 add_filter( 'set_screen_option_tablepress_list_per_page', array( $this, 'save_list_tables_screen_option' ), 10, 3 );
60
61 add_action( 'admin_menu', array( $this, 'add_admin_menu_entry' ) );
62 add_action( 'admin_init', array( $this, 'add_admin_actions' ) );
63
64 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) );
65 add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
66 }
67
68 /**
69 * Handler for changing the number of shown tables in the list of tables (via WP List Table class).
70 *
71 * @since 1.0.0
72 *
73 * @param mixed $screen_option Current value of the filter (probably bool false).
74 * @param string $option Option in which the setting is stored.
75 * @param int $value Current value of the setting.
76 * @return int Changed value of the setting
77 */
78 public function save_list_tables_screen_option( /* mixed */ $screen_option, string $option, int $value ): int {
79 return $value;
80 }
81
82 /**
83 * Add admin screens to the correct place in the admin menu.
84 *
85 * @since 1.0.0
86 */
87 public function add_admin_menu_entry(): void {
88 // Callback for all menu entries.
89 $callback = array( $this, 'show_admin_page' );
90 /**
91 * Filters the TablePress admin menu entry name.
92 *
93 * @since 1.0.0
94 *
95 * @param string $entry_name The admin menu entry name. Default "TablePress".
96 */
97 $admin_menu_entry_name = apply_filters( 'tablepress_admin_menu_entry_name', 'TablePress' );
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
103 $this->init_view_actions();
104 $min_access_cap = $this->view_actions['list']['required_cap'];
105
106 if ( TablePress::$controller->is_top_level_page ) {
107 $icon_url = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iLTMyIC0zMiA2NCA2NCIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTAtMjUuODU0aC0yNS44NTR2NTEuNzA4aDUxLjcwOFYwSDIxdjIxaC00MnYtNDJIMFoiLz48cGF0aCBkPSJNLTE4LTE4aDEwdjEwaC0xMHpNLTE4LTVoMTBWNWgtMTB6TS01LTVINVY1SC01ek0tMTggOGgxMHYxMGgtMTB6TS01IDhINXYxMEgtNXpNOCA4aDEwdjEwSDh6TTUtMzFoNi4xOHY2LjE4SDV6TTE5LTI1aDYuMTh2Ni4xOEgxOXpNMC0xNWgzLjgydjMuODJIMHpNMTAtMjBoMy44MnYzLjgySDEwek0yNS0xMmgzLjgydjMuODJIMjV6TTgtMTNoMTB2MTBIOHoiLz48L3N2Zz4=';
108 switch ( TablePress::$controller->parent_page ) {
109 case 'top':
110 $position = 3; // Position of Dashboard + 1.
111 break;
112 case 'bottom':
113 $position = isset( $GLOBALS['_wp_last_utility_menu'] ) ? ++$GLOBALS['_wp_last_utility_menu'] : 80;
114 break;
115 case 'middle':
116 default:
117 $position = isset( $GLOBALS['_wp_last_object_menu'] ) ? ++$GLOBALS['_wp_last_object_menu'] : 25;
118 break;
119 }
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
125 foreach ( $this->view_actions as $action => $entry ) {
126 if ( ! $entry['show_entry'] ) {
127 continue;
128 }
129 $slug = 'tablepress';
130 if ( 'list' !== $action ) {
131 $slug .= '_' . $action;
132 }
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
135 if ( false !== $page_hook ) {
136 $this->page_hooks[] = $page_hook;
137 }
138 }
139 } else {
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 );
142 if ( false !== $page_hook ) {
143 $this->page_hooks[] = $page_hook;
144 }
145 }
146 }
147
148 /**
149 * Set up handlers for user actions in the backend that exceed plain viewing.
150 *
151 * @since 1.0.0
152 */
153 public function add_admin_actions(): void {
154 // Register the callbacks for processing action requests.
155 $post_actions = array( 'list', 'add', 'options', 'export', 'import' );
156 $get_actions = array( 'hide_message', 'delete_table', 'copy_table', 'preview_table', 'editor_button_thickbox', 'uninstall_tablepress' );
157 foreach ( $post_actions as $action ) {
158 add_action( "admin_post_tablepress_{$action}", array( $this, "handle_post_action_{$action}" ) );
159 }
160 foreach ( $get_actions as $action ) {
161 add_action( "admin_post_tablepress_{$action}", array( $this, "handle_get_action_{$action}" ) );
162 }
163
164 // Register callbacks to trigger load behavior for admin pages.
165 foreach ( $this->page_hooks as $page_hook ) {
166 add_action( "load-{$page_hook}", array( $this, 'load_admin_page' ) );
167 }
168
169 /**
170 * Filters whether the legacy editor button should be loaded on the post editing screen.
171 *
172 * @since 2.1.0
173 *
174 * @param bool $load_button Whether to load the legacy editor button. Default true.
175 */
176 if ( apply_filters( 'tablepress_add_legacy_editor_button', true ) ) {
177 $pages_with_editor_button = array( 'post.php', 'post-new.php' );
178 foreach ( $pages_with_editor_button as $editor_page ) {
179 add_action( "load-{$editor_page}", array( $this, 'add_editor_buttons' ) );
180 }
181 }
182
183 if ( ! is_network_admin() && ! is_user_admin() ) {
184 add_action( 'admin_bar_menu', array( $this, 'add_wp_admin_bar_new_content_menu_entry' ), 71 );
185 }
186
187 add_action( 'load-plugins.php', array( $this, 'plugins_page' ) );
188 }
189
190 /**
191 * Loads additional JavaScript code for the TablePress table block (in the block editor context).
192 *
193 * @since 2.2.0
194 */
195 public function enqueue_block_editor_assets(): void {
196 $handle = generate_block_asset_handle( 'tablepress/table', 'editorScript' );
197 $data = $this->get_block_editor_data();
198 wp_add_inline_script( $handle, $data, 'before' );
199 }
200
201 /**
202 * Loads additional CSS code for the TablePress table block (inside the block editor iframe).
203 *
204 * @since 2.2.0
205 */
206 public function enqueue_block_assets(): void {
207 // Load the TablePress default CSS and the user's "Custom CSS" in the block editor iframe.
208 if ( is_admin() ) {
209 TablePress::$controller->maybe_enqueue_css();
210 }
211 }
212
213 /**
214 * Gets the inline data that is referenced by the Block Editor JavaScript code for the TablePress blocks.
215 *
216 * @since 2.0.0
217 *
218 * @return string JavaScript code for the Block Editor.
219 */
220 protected function get_block_editor_data(): string {
221 $tables = array();
222 // Load all table IDs without priming the post meta cache, as table options/visibility are not needed.
223 $table_ids = TablePress::$model_table->load_all( false );
224 foreach ( $table_ids as $table_id ) {
225 // Load table, without table data, options, and visibility settings.
226 $table = TablePress::$model_table->load( $table_id, false, false );
227
228 // Skip tables that could not be loaded.
229 if ( is_wp_error( $table ) ) {
230 continue;
231 }
232
233 if ( '' === trim( $table['name'] ) ) {
234 $table['name'] = __( '(no name)', 'tablepress' );
235 }
236 $tables[ $table_id ] = esc_html( $table['name'] );
237 }
238
239 /**
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.
241 *
242 * @since 2.0.0
243 *
244 * @param array<string, string> $tables List of table names, the table ID is the array key.
245 */
246 $tables = apply_filters( 'tablepress_block_editor_tables_list', $tables );
247
248 $tables = wp_json_encode( $tables, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
249 if ( false === $tables ) {
250 // JSON encoding failed, return an error object. Use a prefixed "_error" key to avoid conflicts with intentionally added "error" keys.
251 $tables = '{ "_error": "The data could not be encoded to JSON!" }';
252 }
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 );
255
256 $shortcode = esc_js( TablePress::$shortcode );
257
258 $template = TablePress::$model_table->get_table_template();
259 $template = wp_json_encode( $template['options'], JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
260 if ( false === $template ) {
261 // JSON encoding failed, return an error object. Use a prefixed "_error" key to avoid conflicts with intentionally added "error" keys.
262 $template = '{ "_error": "The data could not be encoded to JSON!" }';
263 }
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 );
266
267 /**
268 * Filters whether the table block preview should be loaded via a <ServerSideRender> in the block editor.
269 *
270 * @since 2.0.0
271 *
272 * @param bool $load_block_preview Whether the table block preview should be loaded.
273 */
274 $load_block_preview = apply_filters( 'tablepress_show_block_editor_preview', true );
275 $load_block_preview = (bool) $load_block_preview ? 'true' : 'false';
276
277 $url = '';
278 if ( current_user_can( 'tablepress_list_tables' ) ) {
279 $url = TablePress::url( array( 'action' => 'list' ) );
280 }
281
282 return <<<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;
292 }
293
294 /**
295 * Register actions to add "Table" button to "HTML editor" and "Visual editor" toolbars.
296 *
297 * @since 1.0.0
298 */
299 public function add_editor_buttons(): void {
300 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
301 return;
302 }
303
304 // Only load the toolbar integration if the Block Editor is not used.
305 if ( 'block' === TablePress::site_used_editor() ) {
306 return;
307 }
308
309 add_thickbox(); // The files are usually already loaded by media upload functions.
310 TablePress::enqueue_script(
311 'quicktags-button',
312 array( 'quicktags', 'media-upload' ),
313 array(
314 'editor_button' => array(
315 'caption' => __( 'Table', 'tablepress' ),
316 'title' => __( 'Insert a TablePress table', 'tablepress' ),
317 'thickbox_title' => __( 'Insert a TablePress table', 'tablepress' ),
318 'thickbox_url' => TablePress::url( array( 'action' => 'editor_button_thickbox' ), true, 'admin-post.php' ),
319 ),
320 ),
321 );
322
323 // TinyMCE integration.
324 if ( user_can_richedit() ) {
325 add_filter( 'mce_external_plugins', array( $this, 'add_tinymce_plugin' ) );
326 add_filter( 'mce_buttons', array( $this, 'add_tinymce_button' ) );
327 }
328 }
329
330 /**
331 * Adds the "Table" button to the TinyMCE toolbar.
332 *
333 * @since 1.0.0
334 *
335 * @param string[] $buttons Current set of buttons in the TinyMCE toolbar.
336 * @return string[] Extended set of buttons in the TinyMCE toolbar, including the "Table" button.
337 */
338 public function add_tinymce_button( array $buttons ): array {
339 $buttons[] = 'tablepress_insert_table';
340 return $buttons;
341 }
342
343 /**
344 * Registers the "Table" button plugin for the TinyMCE editor.
345 *
346 * @since 1.0.0
347 *
348 * @param array<string, string> $plugins Current set of registered TinyMCE plugins.
349 * @return array<string, string> Extended set of registered TinyMCE plugins, including the "Table" button plugin.
350 */
351 public function add_tinymce_plugin( array $plugins ): array {
352 $plugins['tablepress_tinymce'] = plugins_url( 'admin/js/build/tinymce-button.js', TABLEPRESS__FILE__ );
353 return $plugins;
354 }
355
356 /**
357 * Add "TablePress Table" entry to "New" dropdown menu in the WP Admin Bar.
358 *
359 * @since 1.0.0
360 *
361 * @param WP_Admin_Bar $wp_admin_bar The current WP Admin Bar object.
362 */
363 public function add_wp_admin_bar_new_content_menu_entry( WP_Admin_Bar $wp_admin_bar ): void {
364 if ( ! current_user_can( 'tablepress_add_tables' ) ) {
365 return;
366 }
367
368 // Don't load TablePress assets on the Freemius opt-in/activation screen.
369 if ( tb_tp_fs()->is_activation_mode() && tb_tp_fs()->is_activation_page() ) {
370 return;
371 }
372
373 $wp_admin_bar->add_menu( array(
374 'parent' => 'new-content',
375 'id' => 'new-tablepress-table',
376 'title' => __( 'TablePress table', 'tablepress' ),
377 'href' => TablePress::url( array( 'action' => 'add' ) ),
378 ) );
379 }
380
381 /**
382 * Handle actions for loading of Plugins page.
383 *
384 * @since 1.0.0
385 */
386 public function plugins_page(): void {
387 // Add additional links on Plugins page.
388 add_filter( 'plugin_action_links_' . TABLEPRESS_BASENAME, array( $this, 'add_plugin_action_links' ) );
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 }
403 }
404
405 /**
406 * Add links to the TablePress entry in the "Plugin" column on the Plugins page.
407 *
408 * @since 1.0.0
409 *
410 * @param string[] $links List of links to print in the "Plugin" column on the Plugins page.
411 * @return string[] Extended list of links to print in the "Plugin" column on the Plugins page.
412 */
413 public function add_plugin_action_links( array $links ): array {
414 if ( current_user_can( 'tablepress_list_tables' ) ) {
415 $links[] = '<a href="' . esc_url( TablePress::url() ) . '">' . __( 'Plugin page', 'tablepress' ) . '</a>';
416 }
417 return $links;
418 }
419
420 /**
421 * Add links to the TablePress entry in the "Description" column on the Plugins page.
422 *
423 * @since 1.0.0
424 *
425 * @param string[] $links List of links to print in the "Description" column on the Plugins page.
426 * @param string $file Name of the plugin.
427 * @return string[] Extended list of links to print in the "Description" column on the Plugins page.
428 */
429 public function add_plugin_row_meta( array $links, string $file ): array {
430 if ( TABLEPRESS_BASENAME === $file ) {
431 $links[] = '<a href="https://tablepress.org/faq/" title="' . esc_attr__( 'Frequently Asked Questions', 'tablepress' ) . '">' . __( 'FAQ', 'tablepress' ) . '</a>';
432 $links[] = '<a href="https://tablepress.org/documentation/">' . __( 'Documentation', 'tablepress' ) . '</a>';
433 $links[] = '<a href="https://tablepress.org/support/">' . __( 'Support', 'tablepress' ) . '</a>';
434 if ( ! TABLEPRESS_IS_PLAYGROUND_PREVIEW && tb_tp_fs()->is_free_plan() ) {
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>';
436 }
437 }
438 return $links;
439 }
440
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 /**
488 * Prepare the rendering of an admin screen, by determining the current action, loading necessary data and initializing the view.
489 *
490 * @since 1.0.0
491 */
492 public function load_admin_page(): void {
493 // Determine the action from either the GET parameter (for sub-menu entries, and the main admin menu entry).
494 $action = ( ! empty( $_GET['action'] ) ) ? $_GET['action'] : 'list'; // Default action is list.
495 if ( TablePress::$controller->is_top_level_page ) {
496 // Or, for sub-menu entry of an admin menu "TablePress" entry, get it from the "page" GET parameter.
497 if ( 'tablepress' !== $_GET['page'] ) {
498 // Actions that are top-level entries, but don't have an action GET parameter (action is after last _ in string).
499 $action = substr( $_GET['page'], 11 ); // $_GET['page'] has the format 'tablepress_{$action}'
500 }
501 }
502
503 // Check if action is a supported action, and whether the user is allowed to access this screen.
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.)
505 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
506 }
507
508 // Don't load TablePress assets on the Freemius opt-in/activation screen.
509 if ( tb_tp_fs()->is_activation_mode() && tb_tp_fs()->is_activation_page() ) {
510 return;
511 }
512
513 // Changes current screen ID and pagenow variable in JS, to enable automatic meta box JS handling.
514 set_current_screen( "tablepress_{$action}" );
515
516 /*
517 * Set the `$typenow` global to the current CPT ourselves, as `WP_Screen::get()` does not determine the CPT correctly.
518 * This is necessary as the WP Admin Menu can otherwise highlight wrong entries, see https://github.com/TablePress/TablePress/issues/24.
519 */
520 if ( isset( $_GET['post_type'] ) && post_type_exists( $_GET['post_type'] ) ) {
521 $GLOBALS['typenow'] = $_GET['post_type']; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
522 }
523
524 // Pre-define some view data.
525 $data = array(
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(),
530 );
531
532 // Depending on the action, load more necessary data for the corresponding view.
533 switch ( $action ) {
534 case 'list':
535 $data['table_id'] = ( isset( $_GET['table_id'] ) ) ? preg_replace( '/[^a-zA-Z0-9_-]/', '', $_GET['table_id'] ) : false;
536 // Prime the post meta cache for cached loading of last_editor.
537 $data['table_ids'] = TablePress::$model_table->load_all( true );
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' );
542 $data['table_count'] = count( $data['table_ids'] );
543 break;
544 case 'about':
545 $data['first_activation'] = TablePress::$model_options->get( 'first_activation' );
546 break;
547 case 'options':
548 /*
549 * Maybe try saving "Custom CSS" to a file:
550 * (called here, as the credentials form posts to this handler again, due to how `request_filesystem_credentials()` works)
551 */
552 if ( isset( $_GET['item'] ) && 'save_custom_css' === $_GET['item'] ) {
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.
554 $action = 'options_custom_css'; // To load a different view.
555 // Try saving "Custom CSS" to a file, otherwise this gets the HTML for the credentials form.
556 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
557 $result = $tablepress_css->save_custom_css_to_file_plugin_options( TablePress::$model_options->get( 'custom_css' ), TablePress::$model_options->get( 'custom_css_minified' ) );
558 if ( is_string( $result ) ) {
559 $data['credentials_form'] = $result; // This will only be called if the save function doesn't do a redirect.
560 } elseif ( true === $result ) {
561 /*
562 * At this point, saving was successful, so enable usage of CSS in files again,
563 * and also increase the "Custom CSS" version number (for cache busting).
564 */
565 TablePress::$model_options->update( array(
566 'use_custom_css_file' => true,
567 'custom_css_version' => TablePress::$model_options->get( 'custom_css_version' ) + 1,
568 ) );
569 TablePress::redirect( array( 'action' => 'options', 'message' => 'success_save' ) );
570 } else { // Leaves only $result === false.
571 TablePress::redirect( array( 'action' => 'options', 'message' => 'success_save_error_custom_css' ) );
572 }
573 break;
574 }
575 $data['frontend_options']['use_custom_css'] = TablePress::$model_options->get( 'use_custom_css' );
576 $data['frontend_options']['custom_css'] = TablePress::$model_options->get( 'custom_css' );
577 $data['user_options']['parent_page'] = TablePress::$controller->parent_page;
578 break;
579 case 'edit':
580 if ( ! isset( $_GET['table_id'] ) || ! preg_match( '/^[a-zA-Z0-9_-]+$/', $_GET['table_id'] ) ) {
581 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_no_table' ) );
582 }
583 // Load table, with table data, options, and visibility settings.
584 $data['table'] = TablePress::$model_table->load( $_GET['table_id'], true, true );
585 if ( is_wp_error( $data['table'] ) ) {
586 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_load_table', 'error_details' => TablePress::get_wp_error_string( $data['table'] ) ) );
587 }
588 if ( ! current_user_can( 'tablepress_edit_table', $_GET['table_id'] ) ) {
589 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
590 }
591 break;
592 case 'export':
593 // Load all table IDs without priming the post meta cache, as table options/visibility are not needed.
594 $table_ids = TablePress::$model_table->load_all( false );
595 $data['tables'] = array();
596 foreach ( $table_ids as $table_id ) {
597 if ( ! current_user_can( 'tablepress_export_table', $table_id ) ) {
598 continue;
599 }
600 // Load table, without table data, options, and visibility settings.
601 $table = TablePress::$model_table->load( $table_id, false, false );
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'];
609 }
610 $data['tables_count'] = TablePress::$model_table->count_tables();
611 $data['export_ids'] = ( isset( $_GET['table_id'] ) && preg_match( '/^[,a-zA-Z0-9_-]+$/', $_GET['table_id'] ) ) ? explode( ',', $_GET['table_id'] ) : array();
612 $exporter = TablePress::load_class( 'TablePress_Export', 'class-export.php', 'classes' );
613 $data['zip_support_available'] = $exporter->zip_support_available;
614 $data['export_formats'] = $exporter->export_formats;
615 $data['csv_delimiters'] = $exporter->csv_delimiters;
616 $data['export_format'] = ( ! empty( $_GET['export_format'] ) ) ? $_GET['export_format'] : 'csv';
617 $data['csv_delimiter'] = ( ! empty( $_GET['csv_delimiter'] ) ) ? $_GET['csv_delimiter'] : _x( ',', 'Default CSV delimiter in the translated language (";", ",", or "tab")', 'tablepress' );
618 break;
619 case 'import':
620 // Load all table IDs without priming the post meta cache, as table options/visibility are not needed.
621 $table_ids = TablePress::$model_table->load_all( false );
622 $data['tables'] = array();
623 foreach ( $table_ids as $table_id ) {
624 if ( ! current_user_can( 'tablepress_edit_table', $table_id ) ) {
625 continue;
626 }
627 // Load table, without table data, options, and visibility settings.
628 $table = TablePress::$model_table->load( $table_id, false, false );
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'];
636 }
637 $data['table_ids'] = $table_ids; // Backward compatibility for the retired "Table Auto Update" Extension, which still relies on this variable name.
638 $data['tables_count'] = TablePress::$model_table->count_tables();
639 $importer = TablePress::load_class( 'TablePress_Import', 'class-import.php', 'classes' );
640 $data['import_type'] = ( ! empty( $_GET['import_type'] ) ) ? $_GET['import_type'] : 'add';
641 $data['import_existing_table'] = $_GET['import_existing_table'] ?? '';
642 $data['import_source'] = ( ! empty( $_GET['import_source'] ) ) ? $_GET['import_source'] : 'file-upload';
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'] ) ) : '';
646 $data['legacy_import'] = ( ! empty( $_GET['legacy_import'] ) ) ? $_GET['legacy_import'] : 'false';
647 break;
648 }
649
650 /**
651 * Filters the data that is passed to the current TablePress View.
652 *
653 * @since 1.0.0
654 *
655 * @param array<string, mixed> $data Data for the view.
656 * @param string $action The current action for the view.
657 */
658 $data = apply_filters( 'tablepress_view_data', $data, $action );
659
660 // Prepare and initialize the view.
661 $this->view = TablePress::load_view( $action, $data );
662 }
663
664 /**
665 * Render the view that has been initialized in load_admin_page() (called by WordPress when the actual page content is needed).
666 *
667 * @since 1.0.0
668 */
669 public function show_admin_page(): void {
670 $this->view->render();
671 }
672
673 /**
674 * Decides whether a message about Premium versions (previously, about donations) shall be shown on the "All Tables" screen, depending on passed days since installation and whether it was shown before.
675 *
676 * @since 1.0.0
677 *
678 * @return bool Whether the message shall be shown on the "All Tables" screen.
679 */
680 protected function maybe_show_donation_message(): bool {
681 // Only show the message to plugin admins.
682 if ( ! current_user_can( 'tablepress_edit_options' ) ) {
683 return false;
684 }
685
686 if ( ! TablePress::$model_options->get( 'message_donation_nag' ) ) {
687 return false;
688 }
689
690 // Determine, how long has the plugin been installed.
691 $seconds_installed = time() - TablePress::$model_options->get( 'first_activation' );
692 return ( $seconds_installed > MONTH_IN_SECONDS / 2 );
693 }
694
695 /**
696 * Init list of actions that have a view with their titles/names/caps.
697 *
698 * @since 1.0.0
699 */
700 protected function init_view_actions(): void {
701 $this->view_actions = array(
702 'list' => array(
703 'show_entry' => true,
704 'page_title' => __( 'All Tables', 'tablepress' ),
705 'admin_menu_title' => __( 'All Tables', 'tablepress' ),
706 'nav_tab_title' => __( 'All Tables', 'tablepress' ),
707 'required_cap' => 'tablepress_list_tables',
708 ),
709 'add' => array(
710 'show_entry' => true,
711 'page_title' => __( 'Add New Table', 'tablepress' ),
712 'admin_menu_title' => __( 'Add New Table', 'tablepress' ),
713 'nav_tab_title' => __( 'Add New', 'tablepress' ),
714 'required_cap' => 'tablepress_add_tables',
715 ),
716 'edit' => array(
717 'show_entry' => false,
718 'page_title' => __( 'Edit Table', 'tablepress' ),
719 'admin_menu_title' => '',
720 'nav_tab_title' => '',
721 'required_cap' => 'tablepress_edit_tables',
722 ),
723 'import' => array(
724 'show_entry' => true,
725 'page_title' => __( 'Import a Table', 'tablepress' ),
726 'admin_menu_title' => __( 'Import a Table', 'tablepress' ),
727 'nav_tab_title' => _x( 'Import', 'navigation bar', 'tablepress' ),
728 'required_cap' => 'tablepress_import_tables',
729 ),
730 'export' => array(
731 'show_entry' => true,
732 'page_title' => __( 'Export a Table', 'tablepress' ),
733 'admin_menu_title' => __( 'Export a Table', 'tablepress' ),
734 'nav_tab_title' => _x( 'Export', 'navigation bar', 'tablepress' ),
735 'required_cap' => 'tablepress_export_tables',
736 ),
737 'options' => array(
738 'show_entry' => true,
739 'page_title' => __( 'Plugin Options', 'tablepress' ),
740 'admin_menu_title' => __( 'Plugin Options', 'tablepress' ),
741 'nav_tab_title' => __( 'Plugin Options', 'tablepress' ),
742 'required_cap' => 'tablepress_access_options_screen',
743 ),
744 'about' => array(
745 'show_entry' => true,
746 'page_title' => __( 'About', 'tablepress' ),
747 'admin_menu_title' => __( 'About TablePress', 'tablepress' ),
748 'nav_tab_title' => __( 'About', 'tablepress' ),
749 'required_cap' => 'tablepress_access_about_screen',
750 ),
751 );
752
753 /**
754 * Filters the available TablePres Views/Actions and their parameters.
755 *
756 * @since 1.0.0
757 *
758 * @param array<string, array<string, bool|string>> $view_actions The available Views/Actions and their parameters.
759 */
760 $this->view_actions = apply_filters( 'tablepress_admin_view_actions', $this->view_actions );
761 }
762
763 /*
764 * HTTP POST actions.
765 */
766
767 /**
768 * Handle Bulk Actions (Copy, Export, Delete) on "All Tables" list screen.
769 *
770 * @since 1.0.0
771 */
772 public function handle_post_action_list(): void {
773 TablePress::check_nonce( 'list' );
774
775 if ( isset( $_POST['bulk-action-selector-top'] ) && '-1' !== $_POST['bulk-action-selector-top'] ) {
776 $bulk_action = $_POST['bulk-action-selector-top'];
777 } elseif ( isset( $_POST['bulk-action-selector-bottom'] ) && '-1' !== $_POST['bulk-action-selector-bottom'] ) {
778 $bulk_action = $_POST['bulk-action-selector-bottom'];
779 } else {
780 $bulk_action = false;
781 }
782
783 if ( ! in_array( $bulk_action, array( 'copy', 'export', 'delete' ), true ) ) {
784 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_bulk_action_invalid' ) );
785 }
786
787 if ( empty( $_POST['table'] ) || ! is_array( $_POST['table'] ) ) {
788 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_no_selection' ) );
789 }
790
791 $tables = wp_unslash( $_POST['table'] );
792
793 $no_success = array(); // To store table IDs that failed.
794
795 switch ( $bulk_action ) {
796 case 'copy':
797 foreach ( $tables as $table_id ) {
798 if ( current_user_can( 'tablepress_copy_table', $table_id ) ) {
799 $copy_table_id = TablePress::$model_table->copy( $table_id );
800 if ( is_wp_error( $copy_table_id ) ) {
801 $no_success[] = $table_id;
802 }
803 } else {
804 $no_success[] = $table_id;
805 }
806 }
807 break;
808 case 'export':
809 /*
810 * Cap check is done on redirect target page.
811 * To export, redirect to "Export" screen, with selected table IDs.
812 */
813 $table_ids = implode( ',', $tables );
814 TablePress::redirect( array( 'action' => 'export', 'table_id' => $table_ids ) );
815 // break; // unreachable.
816 case 'delete':
817 foreach ( $tables as $table_id ) {
818 if ( current_user_can( 'tablepress_delete_table', $table_id ) ) {
819 $deleted = TablePress::$model_table->delete( $table_id );
820 if ( is_wp_error( $deleted ) ) {
821 $no_success[] = $table_id;
822 }
823 } else {
824 $no_success[] = $table_id;
825 }
826 }
827 break;
828 }
829
830 if ( 0 !== count( $no_success ) ) { // @todo maybe pass this information to the view?
831 $message = "error_{$bulk_action}_not_all_tables";
832 } else {
833 $plural = ( count( $tables ) > 1 ) ? '_plural' : '';
834 $message = "success_{$bulk_action}{$plural}";
835 }
836
837 /*
838 * Slightly more complex redirect method, to account for sort, search, and pagination in the WP_List_Table on the List View,
839 * but only if this action succeeds, to have everything fresh in the event of an error.
840 */
841 $sendback = wp_get_referer();
842 if ( ! $sendback ) {
843 $sendback = TablePress::url( array( 'action' => 'list', 'message' => $message ) );
844 } else {
845 $sendback = remove_query_arg( array( 'action', 'message', 'table_id' ), $sendback );
846 $sendback = add_query_arg( array( 'action' => 'list', 'message' => $message ), $sendback );
847 }
848 wp_redirect( $sendback );
849 exit;
850 }
851
852 /**
853 * Add a table, according to the parameters on the "Add new Table" screen.
854 *
855 * @since 1.0.0
856 */
857 public function handle_post_action_add(): void {
858 TablePress::check_nonce( 'add' );
859
860 if ( ! current_user_can( 'tablepress_add_tables' ) ) {
861 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
862 }
863
864 if ( empty( $_POST['table'] ) || ! is_array( $_POST['table'] ) ) {
865 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add', 'error_details' => 'The HTTP POST data is empty.' ) );
866 }
867
868 $add_table = wp_unslash( $_POST['table'] );
869
870 // Perform confidence checks of posted data.
871 $name = $add_table['name'] ?? '';
872 $description = $add_table['description'] ?? '';
873 if ( ! isset( $add_table['rows'], $add_table['columns'] ) ) {
874 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add', 'error_details' => 'The HTTP POST data does not contain the table size.' ) );
875 }
876
877 $num_rows = absint( $add_table['rows'] );
878 $num_columns = absint( $add_table['columns'] );
879 if ( 0 === $num_rows || 0 === $num_columns ) {
880 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add', 'error_details' => 'The table size is invalid.' ) );
881 }
882
883 // Create a new table array with information from the posted data.
884 $new_table = array(
885 'name' => $name,
886 'description' => $description,
887 'data' => array_fill( 0, $num_rows, array_fill( 0, $num_columns, '' ) ),
888 'visibility' => array(
889 'rows' => array_fill( 0, $num_rows, 1 ),
890 'columns' => array_fill( 0, $num_columns, 1 ),
891 ),
892 );
893 // Merge this data into an empty table template.
894 $table = TablePress::$model_table->prepare_table( TablePress::$model_table->get_table_template(), $new_table, false );
895 if ( is_wp_error( $table ) ) {
896 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add', 'error_details' => TablePress::get_wp_error_string( $table ) ) );
897 }
898
899 // Add the new table (and get its first ID).
900 $table_id = TablePress::$model_table->add( $table );
901 if ( is_wp_error( $table_id ) ) {
902 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add', 'error_details' => TablePress::get_wp_error_string( $table_id ) ) );
903 }
904
905 TablePress::redirect( array( 'action' => 'edit', 'table_id' => $table_id, 'message' => 'success_add' ) );
906 }
907
908 /**
909 * Save changed "Plugin Options".
910 *
911 * @since 1.0.0
912 */
913 public function handle_post_action_options(): void {
914 TablePress::check_nonce( 'options' );
915
916 if ( ! current_user_can( 'tablepress_access_options_screen' ) ) {
917 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
918 }
919
920 if ( empty( $_POST['options'] ) || ! is_array( $_POST['options'] ) ) {
921 TablePress::redirect( array( 'action' => 'options', 'message' => 'error_save' ) );
922 }
923
924 $posted_options = wp_unslash( $_POST['options'] );
925
926 // Valid new options that will be merged into existing ones.
927 $new_options = array();
928
929 // Check each posted option value, and (maybe) add it to the new options.
930 if ( ! empty( $posted_options['admin_menu_parent_page'] ) && '-' !== $posted_options['admin_menu_parent_page'] ) {
931 $new_options['admin_menu_parent_page'] = $posted_options['admin_menu_parent_page'];
932 // Re-init parent information, as `TablePress::redirect()` URL might be wrong otherwise.
933 /** This filter is documented in classes/class-controller.php */
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 );
936 }
937
938 // Custom CSS can only be saved if the user is allowed to do so.
939 $update_custom_css_files = false;
940 if ( current_user_can( 'tablepress_edit_options' ) ) {
941 // Checkbox.
942 $new_options['use_custom_css'] = ( isset( $posted_options['use_custom_css'] ) && 'true' === $posted_options['use_custom_css'] );
943
944 if ( isset( $posted_options['custom_css'] ) ) {
945 $new_options['custom_css'] = $posted_options['custom_css'];
946
947 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
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
960 // Maybe update CSS files as well.
961 $custom_css_file_contents = $tablepress_css->load_custom_css_from_file( 'normal' );
962 if ( false === $custom_css_file_contents ) {
963 $custom_css_file_contents = '';
964 }
965 // Don't write to file if it already has the desired content.
966 if ( $new_options['custom_css'] !== $custom_css_file_contents ) {
967 $update_custom_css_files = true;
968 // Set to false again. As it was set here, it will be set true again, if file saving succeeds.
969 $new_options['use_custom_css_file'] = false;
970 }
971 }
972 }
973
974 // Save gathered new options (will be merged into existing ones), and flush caches of caching plugins, to make sure that the new Custom CSS is used.
975 if ( ! empty( $new_options ) ) {
976 TablePress::$model_options->update( $new_options );
977 TablePress::$model_table->_flush_caching_plugins_caches();
978 }
979
980 if ( $update_custom_css_files ) { // Capability check is performed above.
981 TablePress::redirect( array( 'action' => 'options', 'item' => 'save_custom_css' ), true );
982 }
983
984 TablePress::redirect( array( 'action' => 'options', 'message' => 'success_save' ) );
985 }
986
987 /**
988 * Export selected tables.
989 *
990 * @since 1.0.0
991 */
992 public function handle_post_action_export(): void {
993 TablePress::check_nonce( 'export' );
994
995 if ( ! current_user_can( 'tablepress_export_tables' ) ) {
996 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
997 }
998
999 if ( empty( $_POST['export'] ) || ! is_array( $_POST['export'] ) ) {
1000 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export', 'error_details' => 'The HTTP POST data is empty.' ) );
1001 }
1002
1003 $export = wp_unslash( $_POST['export'] );
1004
1005 if ( empty( $export['tables_list'] ) ) {
1006 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export', 'error_details' => 'The HTTP POST data does not contain tables.' ) );
1007 }
1008
1009 /** @var TablePress_Export $exporter */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
1010 $exporter = TablePress::load_class( 'TablePress_Export', 'class-export.php', 'classes' );
1011
1012 if ( empty( $export['format'] ) || ! isset( $exporter->export_formats[ $export['format'] ] ) ) {
1013 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export', 'error_details' => 'The export format is invalid.' ) );
1014 }
1015 if ( ! isset( $export['csv_delimiter'] ) ) {
1016 $export['csv_delimiter'] = '';
1017 }
1018 if ( 'csv' === $export['format'] && ! isset( $exporter->csv_delimiters[ $export['csv_delimiter'] ] ) ) {
1019 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export', 'error_details' => 'The CSV delimiter is invalid.' ) );
1020 }
1021
1022 $tables = explode( ',', $export['tables_list'] );
1023
1024 // Determine if ZIP file support is available.
1025 if ( $exporter->zip_support_available
1026 && ( ( isset( $export['zip_file'] ) && 'true' === $export['zip_file'] ) || count( $tables ) > 1 ) ) {
1027 // Export to ZIP only if ZIP is desired or if more than one table were selected (mandatory then).
1028 $export_to_zip = true;
1029 } else {
1030 $export_to_zip = false;
1031 }
1032
1033 if ( ! $export_to_zip ) {
1034 // Exporting without a ZIP file is only possible for one table, so take the first one.
1035 if ( ! current_user_can( 'tablepress_export_table', $tables[0] ) ) {
1036 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1037 }
1038 // Load table, with table data, options, and visibility settings.
1039 $table = TablePress::$model_table->load( $tables[0], true, true );
1040 if ( is_wp_error( $table ) ) {
1041 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_load_table', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'], 'error_details' => TablePress::get_wp_error_string( $table ) ) );
1042 }
1043 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
1044 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_table_corrupted', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'] ) );
1045 }
1046 $download_filename = sprintf( '%1$s-%2$s-%3$s.%4$s', $table['id'], $table['name'], wp_date( 'Y-m-d' ), $export['format'] );
1047 /**
1048 * Filters the download filename of the exported table.
1049 *
1050 * @since 2.0.0
1051 *
1052 * @param string $download_filename The download filename of exported table.
1053 * @param string $table_id Table ID of the exported table.
1054 * @param string $table_name Table name of the exported table.
1055 * @param string $export_format Format for the export ('csv', 'html', 'json', 'zip').
1056 * @param bool $export_to_zip Whether the export is to a ZIP file (of multiple export files).
1057 */
1058 $download_filename = apply_filters( 'tablepress_export_filename', $download_filename, $table['id'], $table['name'], $export['format'], $export_to_zip );
1059 $download_filename = sanitize_file_name( $download_filename );
1060 // Export the table.
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 );
1066 /**
1067 * Filters the exported table data.
1068 *
1069 * @since 1.6.0
1070 *
1071 * @param string $export_data The exported table data.
1072 * @param array<string, mixed> $table Table to be exported.
1073 * @param string $export_format Format for the export ('csv', 'html', 'json').
1074 * @param string $csv_delimiter Delimiter for CSV export.
1075 */
1076 $export_data = apply_filters( 'tablepress_export_data', $export_data, $table, $export['format'], $export['csv_delimiter'] );
1077 $download_data = $export_data;
1078 } else {
1079 // Zipping can use a lot of memory and execution time, but not this much hopefully.
1080 wp_raise_memory_limit( 'admin' );
1081 if ( function_exists( 'set_time_limit' ) ) {
1082 @set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1083 }
1084
1085 $zip_file = new ZipArchive();
1086 $download_filename = sprintf( 'tablepress-export-%1$s-%2$s.zip', wp_date( 'Y-m-d-H-i-s' ), $export['format'] );
1087 /** This filter is documented in controllers/controller-admin.php */
1088 $download_filename = apply_filters( 'tablepress_export_filename', $download_filename, '', '', $export['format'], $export_to_zip );
1089 $download_filename = sanitize_file_name( $download_filename );
1090 $full_filename = wp_tempnam( $download_filename );
1091 if ( true !== $zip_file->open( $full_filename, ZipArchive::OVERWRITE ) ) {
1092 @unlink( $full_filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
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.' ) );
1094 }
1095
1096 foreach ( $tables as $table_id ) {
1097 // Don't export tables for which the user doesn't have the necessary export rights.
1098 if ( ! current_user_can( 'tablepress_export_table', $table_id ) ) {
1099 continue;
1100 }
1101 // Load table, with table data, options, and visibility settings.
1102 $table = TablePress::$model_table->load( $table_id, true, true );
1103 // Don't export if the table could not be loaded.
1104 if ( is_wp_error( $table ) ) {
1105 continue;
1106 }
1107 // Don't export if the table is corrupted.
1108 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
1109 continue;
1110 }
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 );
1116 /** This filter is documented in controllers/controller-admin.php */
1117 $export_data = apply_filters( 'tablepress_export_data', $export_data, $table, $export['format'], $export['csv_delimiter'] );
1118 $export_filename = sprintf( '%1$s-%2$s-%3$s.%4$s', $table['id'], $table['name'], wp_date( 'Y-m-d' ), $export['format'] );
1119 /** This filter is documented in controllers/controller-admin.php */
1120 $export_filename = apply_filters( 'tablepress_export_filename', $export_filename, $table['id'], $table['name'], $export['format'], $export_to_zip );
1121 $export_filename = sanitize_file_name( $export_filename );
1122 $zip_file->addFromString( $export_filename, $export_data );
1123 }
1124
1125 // If something went wrong, or no files were added to the ZIP file, bail out.
1126 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1127 if ( ZipArchive::ER_OK !== $zip_file->status || 0 === $zip_file->numFiles ) {
1128 $zip_file->close();
1129 @unlink( $full_filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
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.' ) );
1131 }
1132 $zip_file->close();
1133
1134 // Load contents of the ZIP file, to send it as a download.
1135 $download_data = file_get_contents( $full_filename );
1136 if ( false === $download_data ) {
1137 @unlink( $full_filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1138 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_create_zip_file', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'], 'error_details' => 'The ZIP file content could not be read.' ) );
1139 }
1140 @unlink( $full_filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1141 }
1142
1143 // Send download headers for export file.
1144 header( 'Content-Description: File Transfer' );
1145 header( 'Content-Type: application/octet-stream' );
1146 header( "Content-Disposition: attachment; filename=\"{$download_filename}\"" );
1147 header( 'Content-Transfer-Encoding: binary' );
1148 header( 'Expires: 0' );
1149 header( 'Cache-Control: must-revalidate' );
1150 header( 'Pragma: public' );
1151 header( 'Content-Length: ' . strlen( $download_data ) );
1152 // $filetype = text/csv, text/html, application/json
1153 // header( 'Content-Type: ' . $filetype. '; charset=' . get_option( 'blog_charset' ) );
1154 @ob_end_clean(); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1155 flush();
1156 echo $download_data;
1157 exit;
1158 }
1159
1160 /**
1161 * Import data from existing source (Upload, URL, Server, Direct input).
1162 *
1163 * @since 1.0.0
1164 */
1165 public function handle_post_action_import(): void {
1166 TablePress::check_nonce( 'import' );
1167
1168 if ( ! current_user_can( 'tablepress_import_tables' ) ) {
1169 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1170 }
1171
1172 if ( empty( $_POST['import'] ) || ! is_array( $_POST['import'] ) ) {
1173 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import', 'error_details' => 'The HTTP POST data is empty.' ) );
1174 }
1175
1176 $import_config = wp_unslash( $_POST['import'] );
1177
1178 if ( empty( $import_config['source'] ) ) {
1179 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import', 'error_details' => 'The HTTP POST does not contain an import configuration.' ) );
1180 }
1181
1182 // For security reasons, the "server" source is only available for super admins on multisite and admins on single sites.
1183 if ( 'server' === $import_config['source'] ) {
1184 if ( ! is_super_admin() && ! ( ! is_multisite() && current_user_can( 'manage_options' ) ) ) {
1185 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import', 'error_details' => 'You do not have the required access rights.' ) );
1186 }
1187 }
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
1196 // Move file upload data to the main import configuration.
1197 $import_config['file-upload'] = $_FILES['import_file_upload'] ?? null;
1198
1199 // Check if the source data for the chosen import source is defined.
1200 if ( empty( $import_config[ $import_config['source'] ] ) ) {
1201 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import', 'error_details' => 'The HTTP POST data does not contain an import source.' ) );
1202 }
1203
1204 // Set default values for non-essential configuration variables.
1205 if ( ! isset( $import_config['type'] ) ) {
1206 $import_config['type'] = 'add';
1207 }
1208 if ( ! isset( $import_config['existing_table'] ) ) {
1209 $import_config['existing_table'] = '';
1210 }
1211
1212 $import_config['legacy_import'] = ( isset( $import_config['legacy_import'] ) && 'true' === $import_config['legacy_import'] );
1213
1214 $importer = TablePress::load_class( 'TablePress_Import', 'class-import.php', 'classes' );
1215 $import = $importer->run( $import_config );
1216
1217 if ( is_wp_error( $import ) || 0 < count( $import['errors'] ) ) {
1218 $redirect_parameters = array(
1219 'action' => 'import',
1220 'message' => 'error_import',
1221 'import_type' => $import_config['type'],
1222 'import_existing_table' => $import_config['existing_table'],
1223 'import_source' => $import_config['source'],
1224 'legacy_import' => $import_config['legacy_import'],
1225 );
1226 if ( in_array( $import_config['source'], array( 'url', 'server' ), true ) ) {
1227 $redirect_parameters[ "import_{$import_config['source']}" ] = rawurlencode( $import_config[ $import_config['source'] ] );
1228 }
1229 if ( is_wp_error( $import ) ) {
1230 $redirect_parameters['error_details'] = TablePress::get_wp_error_string( $import );
1231 } elseif ( 0 < count( $import['errors'] ) ) {
1232 $wp_error_strings = array();
1233 foreach ( $import['errors'] as $file ) {
1234 $wp_error_strings[] = TablePress::get_wp_error_string( $file->error );
1235 }
1236 $redirect_parameters['error_details'] = implode( ', ', $wp_error_strings );
1237 }
1238 TablePress::redirect( $redirect_parameters );
1239 }
1240
1241 // At this point, there were no import errors.
1242 if ( count( $import['tables'] ) > 1 ) {
1243 TablePress::redirect( array( 'action' => 'list', 'message' => 'success_import' ) );
1244 } elseif ( 1 === count( $import['tables'] ) ) {
1245 TablePress::redirect( array( 'action' => 'edit', 'table_id' => $import['tables'][0]['id'], 'message' => 'success_import' ) );
1246 } else {
1247 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import', 'error_details' => 'The number of imported tables is invalid.' ) );
1248 }
1249 }
1250
1251 /*
1252 * HTTP GET actions.
1253 */
1254
1255 /**
1256 * Hide a header message on an admin screen.
1257 *
1258 * @since 1.0.0
1259 */
1260 public function handle_get_action_hide_message(): void {
1261 $message_item = $_GET['item'] ?? '';
1262 TablePress::check_nonce( 'hide_message', $message_item );
1263
1264 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
1265 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1266 }
1267
1268 TablePress::$model_options->update( "message_{$message_item}", false );
1269
1270 $return = ! empty( $_GET['return'] ) ? $_GET['return'] : 'list';
1271 TablePress::redirect( array( 'action' => $return ) );
1272 }
1273
1274 /**
1275 * Delete a table.
1276 *
1277 * @since 1.0.0
1278 */
1279 public function handle_get_action_delete_table(): void {
1280 $table_id = ( ! empty( $_GET['item'] ) ) ? $_GET['item'] : false;
1281 TablePress::check_nonce( 'delete_table', $table_id );
1282
1283 $return = ! empty( $_GET['return'] ) ? $_GET['return'] : 'list';
1284 $return_item = ! empty( $_GET['return_item'] ) ? $_GET['return_item'] : false;
1285
1286 // The nonce check should actually catch this already.
1287 if ( false === $table_id ) {
1288 TablePress::redirect( array( 'action' => $return, 'message' => 'error_delete', 'table_id' => $return_item ) );
1289 }
1290
1291 if ( ! current_user_can( 'tablepress_delete_table', $table_id ) ) {
1292 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1293 }
1294
1295 $deleted = TablePress::$model_table->delete( $table_id );
1296 if ( is_wp_error( $deleted ) ) {
1297 TablePress::redirect( array( 'action' => $return, 'message' => 'error_delete', 'table_id' => $return_item, 'error_details' => TablePress::get_wp_error_string( $deleted ) ) );
1298 }
1299
1300 /*
1301 * Slightly more complex redirect method, to account for sort, search, and pagination in the WP_List_Table on the List View,
1302 * but only if this action succeeds, to have everything fresh in the event of an error.
1303 */
1304 $sendback = wp_get_referer();
1305 if ( ! $sendback ) {
1306 $sendback = TablePress::url( array( 'action' => 'list', 'message' => 'success_delete', 'table_id' => $return_item ) );
1307 } else {
1308 $sendback = remove_query_arg( array( 'action', 'message', 'table_id' ), $sendback );
1309 $sendback = add_query_arg( array( 'action' => 'list', 'message' => 'success_delete', 'table_id' => $return_item ), $sendback );
1310 }
1311 wp_redirect( $sendback );
1312 exit;
1313 }
1314
1315 /**
1316 * Copy a table.
1317 *
1318 * @since 1.0.0
1319 */
1320 public function handle_get_action_copy_table(): void {
1321 $table_id = ( ! empty( $_GET['item'] ) ) ? $_GET['item'] : false;
1322 TablePress::check_nonce( 'copy_table', $table_id );
1323
1324 $return = ! empty( $_GET['return'] ) ? $_GET['return'] : 'list';
1325 $return_item = ! empty( $_GET['return_item'] ) ? $_GET['return_item'] : false;
1326
1327 // The nonce check should actually catch this already.
1328 if ( false === $table_id ) {
1329 TablePress::redirect( array( 'action' => $return, 'message' => 'error_copy', 'table_id' => $return_item ) );
1330 }
1331
1332 if ( ! current_user_can( 'tablepress_copy_table', $table_id ) ) {
1333 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1334 }
1335
1336 $copy_table_id = TablePress::$model_table->copy( $table_id );
1337 if ( is_wp_error( $copy_table_id ) ) {
1338 TablePress::redirect( array( 'action' => $return, 'message' => 'error_copy', 'table_id' => $return_item, 'error_details' => TablePress::get_wp_error_string( $copy_table_id ) ) );
1339 }
1340 $return_item = $copy_table_id;
1341
1342 /*
1343 * Slightly more complex redirect method, to account for sort, search, and pagination in the WP_List_Table on the List View,
1344 * but only if this action succeeds, to have everything fresh in the event of an error.
1345 */
1346 $sendback = wp_get_referer();
1347 if ( ! $sendback ) {
1348 $sendback = TablePress::url( array( 'action' => $return, 'message' => 'success_copy', 'table_id' => $return_item ) );
1349 } else {
1350 $sendback = remove_query_arg( array( 'action', 'message', 'table_id' ), $sendback );
1351 $sendback = add_query_arg( array( 'action' => $return, 'message' => 'success_copy', 'table_id' => $return_item ), $sendback );
1352 }
1353 wp_redirect( $sendback );
1354 exit;
1355 }
1356
1357 /**
1358 * Preview a table.
1359 *
1360 * @since 1.0.0
1361 */
1362 public function handle_get_action_preview_table(): void {
1363 $table_id = ( ! empty( $_GET['item'] ) ) ? $_GET['item'] : false;
1364 TablePress::check_nonce( 'preview_table', $table_id );
1365
1366 // Nonce check should actually catch this already.
1367 if ( false === $table_id ) {
1368 wp_die( __( 'The preview could not be loaded.', 'tablepress' ), __( 'Preview', 'tablepress' ) );
1369 }
1370
1371 if ( ! current_user_can( 'tablepress_preview_table', $table_id ) ) {
1372 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1373 }
1374
1375 // Load table, with table data, options, and visibility settings.
1376 $table = TablePress::$model_table->load( $table_id, true, true );
1377 if ( is_wp_error( $table ) ) {
1378 wp_die( __( 'The table could not be loaded.', 'tablepress' ), __( 'Preview', 'tablepress' ) );
1379 }
1380
1381 // Sanitize all table data to remove unsafe HTML from the preview output, if the user is not allowed to work with unfiltered HTML.
1382 if ( ! current_user_can( 'unfiltered_html' ) ) {
1383 $table = TablePress::$model_table->sanitize( $table );
1384 }
1385
1386 // Create a render class instance.
1387 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
1388 // Merge desired options with default render options (see TablePress_Controller_Frontend::shortcode_table()).
1389 $default_render_options = $_render->get_default_render_options();
1390 /** This filter is documented in controllers/controller-frontend.php */
1391 $default_render_options = apply_filters( 'tablepress_shortcode_table_default_shortcode_atts', $default_render_options );
1392 $render_options = shortcode_atts( $default_render_options, $table['options'] );
1393 /** This filter is documented in controllers/controller-frontend.php */
1394 $render_options = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $render_options );
1395 $render_options['html_id'] = "tablepress-{$table['id']}";
1396 $render_options['block_preview'] = true;
1397 $_render->set_input( $table, $render_options );
1398 $view_data = array(
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(),
1403 );
1404
1405 $custom_css = TablePress::$model_options->get( 'custom_css' );
1406 $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
1407 if ( $use_custom_css ) {
1408 $view_data['head_html'] .= "<style>\n{$custom_css}\n</style>\n";
1409 }
1410
1411 // Prepare, initialize, and render the view.
1412 $this->view = TablePress::load_view( 'preview_table', $view_data );
1413 $this->view->render();
1414 }
1415
1416 /**
1417 * Shows a list of tables in the Editor toolbar Thickbox (opened by TinyMCE or Quicktags button).
1418 *
1419 * @since 1.0.0
1420 */
1421 public function handle_get_action_editor_button_thickbox(): void {
1422 TablePress::check_nonce( 'editor_button_thickbox' );
1423
1424 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
1425 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1426 }
1427
1428 $view_data = array(
1429 // Load all table IDs without priming the post meta cache, as table options/visibility are not needed.
1430 'table_ids' => TablePress::$model_table->load_all( false ),
1431 );
1432
1433 set_current_screen( 'tablepress_editor_button_thickbox' );
1434
1435 // Prepare, initialize, and render the view.
1436 $this->view = TablePress::load_view( 'editor_button_thickbox', $view_data );
1437 $this->view->render();
1438 }
1439
1440 /**
1441 * Uninstall TablePress, and delete all tables and options.
1442 *
1443 * @since 1.0.0
1444 */
1445 public function handle_get_action_uninstall_tablepress(): void {
1446 TablePress::check_nonce( 'uninstall_tablepress' );
1447
1448 $plugin = TABLEPRESS_BASENAME;
1449
1450 if ( ! current_user_can( 'deactivate_plugin', $plugin ) || ! current_user_can( 'tablepress_edit_options' ) || ! current_user_can( 'tablepress_delete_tables' ) || is_plugin_active_for_network( $plugin ) ) {
1451 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1452 }
1453
1454 // Deactivate TablePress for the site (but not for the network).
1455 deactivate_plugins( $plugin, false, false );
1456 update_option( 'recently_activated', array( $plugin => time() ) + (array) get_option( 'recently_activated', array() ) );
1457
1458 // Delete all tables, "Custom CSS" files, and options.
1459 TablePress::$model_table->delete_all();
1460 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
1461 $css_files_deleted = $tablepress_css->delete_custom_css_files();
1462 TablePress::$model_options->remove_access_capabilities();
1463
1464 TablePress::$model_table->destroy();
1465 TablePress::$model_options->destroy();
1466
1467 $output = '<strong>' . __( 'TablePress was uninstalled successfully.', 'tablepress' ) . '</strong><br><br>';
1468 $output .= __( 'All tables, data, and options were deleted.', 'tablepress' );
1469 if ( is_multisite() ) {
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' );
1471 } else {
1472 $output .= ' ' . __( 'You may now manually delete the plugin&#8217;s folder <code>tablepress</code> from the <code>plugins</code> directory on your server or use the &#8220;Delete&#8221; link for TablePress on the WordPress &#8220;Plugins&#8221; page.', 'tablepress' );
1473 }
1474 if ( $css_files_deleted ) {
1475 $output .= ' ' . __( 'Your TablePress &#8220;Custom CSS&#8221; files have been deleted automatically.', 'tablepress' );
1476 } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
1477 if ( is_multisite() ) {
1478 $output .= ' ' . __( 'Please also ask him to delete your TablePress &#8220;Custom CSS&#8221; files from the server.', 'tablepress' );
1479 } else {
1480 $output .= ' ' . __( 'You may now also delete your TablePress &#8220;Custom CSS&#8221; files in the <code>wp-content</code> folder.', 'tablepress' );
1481 }
1482 }
1483 $output .= "</p>\n<p>";
1484 if ( ! is_multisite() || is_super_admin() ) {
1485 $output .= '<a class="button" href="' . esc_url( admin_url( 'plugins.php' ) ) . '">' . __( 'Go to &#8220;Plugins&#8221; page', 'tablepress' ) . '</a> ';
1486 }
1487 $output .= '<a class="button" href="' . esc_url( admin_url( 'index.php' ) ) . '">' . __( 'Go to Dashboard', 'tablepress' ) . '</a>';
1488
1489 wp_die( $output, __( 'Uninstall TablePress', 'tablepress' ), array( 'response' => 200, 'back_link' => false ) );
1490 }
1491
1492 } // class TablePress_Admin_Controller
1493