PluginProbe
TablePress – Tables in WordPress made easy / 3.3.4
TablePress – Tables in WordPress made easy v3.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 2.4.4 All 44 releases
tablepress / controllers / controller-admin.php

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

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