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

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