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

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