PluginProbe
TablePress – Tables in WordPress made easy / 2.4
TablePress – Tables in WordPress made easy v2.4
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / controllers / controller-admin.php

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

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