PluginProbe
TablePress – Tables in WordPress made easy / 3.2.2
TablePress – Tables in WordPress made easy v3.2.2
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.2.2, at controllers/controller-admin.php

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