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

1,586 lines 65.2 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 if ( 'tablepress_list_per_page' === $option ) {
83 return $value;
84 } else {
85 return $false;
86 }
87 }
88
89 /**
90 * Add admin screens to the correct place in the admin menu.
91 *
92 * @since 1.0.0
93 */
94 public function add_admin_menu_entry() {
95 // Callback for all menu entries.
96 $callback = array( $this, 'show_admin_page' );
97 /**
98 * Filter the TablePress admin menu entry name.
99 *
100 * @since 1.0.0
101 *
102 * @param string $entry_name The admin menu entry name. Default "TablePress".
103 */
104 $admin_menu_entry_name = apply_filters( 'tablepress_admin_menu_entry_name', 'TablePress' );
105
106 $this->init_view_actions();
107 $min_access_cap = $this->view_actions['list']['required_cap'];
108
109 if ( $this->is_top_level_page ) {
110 $icon_url = 'dashicons-list-view';
111 switch ( $this->parent_page ) {
112 case 'top':
113 $position = 3; // position of Dashboard + 1
114 break;
115 case 'bottom':
116 $position = ( ++$GLOBALS['_wp_last_utility_menu'] );
117 break;
118 case 'middle':
119 default:
120 $position = ( ++$GLOBALS['_wp_last_object_menu'] );
121 break;
122 }
123 add_menu_page( 'TablePress', $admin_menu_entry_name, $min_access_cap, 'tablepress', $callback, $icon_url, $position );
124 foreach ( $this->view_actions as $action => $entry ) {
125 if ( ! $entry['show_entry'] ) {
126 continue;
127 }
128 $slug = 'tablepress';
129 if ( 'list' !== $action ) {
130 $slug .= '_' . $action;
131 }
132 $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 );
133 }
134 } else {
135 $this->page_hooks[] = add_submenu_page( $this->parent_page, 'TablePress', $admin_menu_entry_name, $min_access_cap, 'tablepress', $callback );
136 }
137 }
138
139 /**
140 * Set up handlers for user actions in the backend that exceed plain viewing.
141 *
142 * @since 1.0.0
143 */
144 public function add_admin_actions() {
145 // Register the callbacks for processing action requests.
146 $post_actions = array( 'list', 'add', 'edit', 'options', 'export', 'import' );
147 $get_actions = array( 'hide_message', 'delete_table', 'copy_table', 'preview_table', 'editor_button_thickbox', 'uninstall_tablepress' );
148 foreach ( $post_actions as $action ) {
149 add_action( "admin_post_tablepress_{$action}", array( $this, "handle_post_action_{$action}" ) );
150 }
151 foreach ( $get_actions as $action ) {
152 add_action( "admin_post_tablepress_{$action}", array( $this, "handle_get_action_{$action}" ) );
153 }
154
155 // Register callbacks to trigger load behavior for admin pages.
156 foreach ( $this->page_hooks as $page_hook ) {
157 add_action( "load-{$page_hook}", array( $this, 'load_admin_page' ) );
158 }
159
160 $pages_with_editor_button = array( 'post.php', 'post-new.php' );
161 foreach ( $pages_with_editor_button as $editor_page ) {
162 add_action( "load-{$editor_page}", array( $this, 'add_editor_buttons' ) );
163 }
164
165 if ( ! is_network_admin() && ! is_user_admin() ) {
166 add_action( 'admin_bar_menu', array( $this, 'add_wp_admin_bar_new_content_menu_entry' ), 71 );
167 }
168
169 add_action( 'load-plugins.php', array( $this, 'plugins_page' ) );
170
171 // Add filters and actions for the integration into the WP WXR exporter and importer.
172 add_action( 'wp_import_insert_post', array( TablePress::$model_table, 'add_table_id_on_wp_import' ), 10, 4 );
173 add_filter( 'wp_import_post_meta', array( TablePress::$model_table, 'prevent_table_id_post_meta_import_on_wp_import' ), 10, 3 );
174 add_filter( 'wxr_export_skip_postmeta', array( TablePress::$model_table, 'add_table_id_to_wp_export' ), 10, 3 );
175 }
176
177 /**
178 * Register actions to add "Table" button to "HTML editor" and "Visual editor" toolbars.
179 *
180 * @since 1.0.0
181 */
182 public function add_editor_buttons() {
183 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
184 return;
185 }
186
187 /*
188 * Only load the toolbar integration when the Classic Editor plugin (https://wordpress.org/plugins/classic-editor/) is activated.
189 * Without it, the Block Editor user interface is used, which can not directly use these buttons.
190 */
191 if ( ! class_exists( 'Classic_Editor' ) ) {
192 return;
193 }
194
195 add_thickbox(); // usually already loaded by media upload functions
196 $admin_page = TablePress::load_class( 'TablePress_Admin_Page', 'class-admin-page-helper.php', 'classes' );
197 $admin_page->enqueue_script( 'quicktags-button', array( 'quicktags', 'media-upload' ), array(
198 'editor_button' => array(
199 'caption' => __( 'Table', 'tablepress' ),
200 'title' => __( 'Insert a Table from TablePress', 'tablepress' ),
201 'thickbox_title' => __( 'Insert a Table from TablePress', 'tablepress' ),
202 'thickbox_url' => TablePress::url( array( 'action' => 'editor_button_thickbox' ), true, 'admin-post.php' ),
203 ),
204 ) );
205
206 // TinyMCE integration.
207 if ( user_can_richedit() ) {
208 add_filter( 'mce_external_plugins', array( $this, 'add_tinymce_plugin' ) );
209 add_filter( 'mce_buttons', array( $this, 'add_tinymce_button' ) );
210 add_action( 'admin_print_styles', array( $this, 'add_tablepress_hidpi_css' ), 21 );
211 }
212 }
213
214 /**
215 * Add "Table" button and separator to the TinyMCE toolbar.
216 *
217 * @since 1.0.0
218 *
219 * @param array $buttons Current set of buttons in the TinyMCE toolbar.
220 * @return array Current set of buttons in the TinyMCE toolbar, including "Table" button.
221 */
222 public function add_tinymce_button( array $buttons ) {
223 $buttons[] = 'tablepress_insert_table';
224 return $buttons;
225 }
226
227 /**
228 * Register "Table" button plugin to TinyMCE.
229 *
230 * @since 1.0.0
231 *
232 * @param array $plugins Current set of registered TinyMCE plugins.
233 * @return array Current set of registered TinyMCE plugins, including "Table" button plugin.
234 */
235 public function add_tinymce_plugin( array $plugins ) {
236 $suffix = SCRIPT_DEBUG ? '' : '.min';
237 $js_file = "admin/js/tinymce-button{$suffix}.js";
238 $plugins['tablepress_tinymce'] = plugins_url( $js_file, TABLEPRESS__FILE__ );
239 return $plugins;
240 }
241
242 /**
243 * Print TablePress HiDPI CSS to the <head> for TinyMCE button.
244 *
245 * @since 1.0.0
246 */
247 public function add_tablepress_hidpi_css() {
248 echo '<style type="text/css">@media print,(-webkit-min-device-pixel-ratio:1.25),(min-resolution:120dpi){';
249 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}';
250 echo '#content_tablepress_insert_table img{display:none}';
251 echo '}</style>' . "\n";
252 }
253
254 /**
255 * Add "TablePress Table" entry to "New" dropdown menu in the WP Admin Bar.
256 *
257 * @since 1.0.0
258 *
259 * @param WP_Admin_Bar $wp_admin_bar The current WP Admin Bar object.
260 */
261 public function add_wp_admin_bar_new_content_menu_entry( $wp_admin_bar ) {
262 if ( ! current_user_can( 'tablepress_add_tables' ) ) {
263 return;
264 }
265
266 $wp_admin_bar->add_menu( array(
267 'parent' => 'new-content',
268 'id' => 'new-tablepress-table',
269 'title' => __( 'TablePress Table', 'tablepress' ),
270 'href' => TablePress::url( array( 'action' => 'add' ) ),
271 ) );
272 }
273
274 /**
275 * Handle actions for loading of Plugins page.
276 *
277 * @since 1.0.0
278 */
279 public function plugins_page() {
280 // Add additional links on Plugins page.
281 add_filter( 'plugin_action_links_' . TABLEPRESS_BASENAME, array( $this, 'add_plugin_action_links' ) );
282 add_filter( 'plugin_row_meta', array( $this, 'add_plugin_row_meta' ), 10, 2 );
283 }
284
285 /**
286 * Add links to the TablePress entry in the "Plugin" column on the Plugins page.
287 *
288 * @since 1.0.0
289 *
290 * @param array $links List of links to print in the "Plugin" column on the Plugins page.
291 * @return array Extended list of links to print in the "Plugin" column on the Plugins page.
292 */
293 public function add_plugin_action_links( array $links ) {
294 if ( current_user_can( 'tablepress_list_tables' ) ) {
295 $links[] = '<a href="' . TablePress::url() . '">' . __( 'Plugin page', 'tablepress' ) . '</a>';
296 }
297 return $links;
298 }
299 /**
300 * Add links to the TablePress entry in the "Description" column on the Plugins page.
301 *
302 * @since 1.0.0
303 *
304 * @param array $links List of links to print in the "Description" column on the Plugins page.
305 * @param string $file Name of the plugin.
306 * @return array Extended list of links to print in the "Description" column on the Plugins page.
307 */
308 public function add_plugin_row_meta( array $links, $file ) {
309 if ( TABLEPRESS_BASENAME === $file ) {
310 $links[] = '<a href="https://tablepress.org/faq/" title="' . esc_attr__( 'Frequently Asked Questions', 'tablepress' ) . '">' . __( 'FAQ', 'tablepress' ) . '</a>';
311 $links[] = '<a href="https://tablepress.org/documentation/">' . __( 'Documentation', 'tablepress' ) . '</a>';
312 $links[] = '<a href="https://tablepress.org/support/">' . __( 'Support', 'tablepress' ) . '</a>';
313 $links[] = '<a href="https://tablepress.org/donate/" title="' . esc_attr__( 'Support TablePress with your donation!', 'tablepress' ) . '"><strong>' . __( 'Donate', 'tablepress' ) . '</strong></a>';
314 }
315 return $links;
316 }
317
318 /**
319 * Prepare the rendering of an admin screen, by determining the current action, loading necessary data and initializing the view.
320 *
321 * @since 1.0.0
322 */
323 public function load_admin_page() {
324 // Determine the action from either the GET parameter (for sub-menu entries, and the main admin menu entry).
325 $action = ( ! empty( $_GET['action'] ) ) ? $_GET['action'] : 'list'; // default action is list
326 if ( $this->is_top_level_page ) {
327 // Or, for sub-menu entry of an admin menu "TablePress" entry, get it from the "page" GET parameter.
328 if ( 'tablepress' !== $_GET['page'] ) {
329 // Actions that are top-level entries, but don't have an action GET parameter (action is after last _ in string).
330 $action = substr( $_GET['page'], 11 ); // $_GET['page'] has the format 'tablepress_{$action}'
331 }
332 }
333
334 // Check if action is a supported action, and whether the user is allowed to access this screen.
335 if ( ! isset( $this->view_actions[ $action ] ) || ! current_user_can( $this->view_actions[ $action ]['required_cap'] ) ) {
336 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
337 }
338
339 // Changes current screen ID and pagenow variable in JS, to enable automatic meta box JS handling.
340 set_current_screen( "tablepress_{$action}" );
341 // Set the $typenow global to the current CPT ourselves, as WP_Screen::get() does not determine the CPT correctly.
342 // This is necessary as the WP Admin Menu can otherwise highlight wrong entries, see https://github.com/TobiasBg/TablePress/issues/24.
343 if ( isset( $_GET['post_type'] ) && post_type_exists( $_GET['post_type'] ) ) {
344 $GLOBALS['typenow'] = $_GET['post_type'];
345 }
346
347 // Pre-define some view data.
348 $data = array(
349 'view_actions' => $this->view_actions,
350 'message' => ( ! empty( $_GET['message'] ) ) ? $_GET['message'] : false,
351 );
352
353 // Depending on the action, load more necessary data for the corresponding view.
354 switch ( $action ) {
355 case 'list':
356 $data['table_id'] = ( ! empty( $_GET['table_id'] ) ) ? $_GET['table_id'] : false;
357 // Prime the post meta cache for cached loading of last_editor.
358 $data['table_ids'] = TablePress::$model_table->load_all( true );
359 $data['messages']['first_visit'] = TablePress::$model_options->get( 'message_first_visit' );
360 // Check if WP-Table Reloaded is activated and show a warning.
361 $data['messages']['wp_table_reloaded_warning'] = is_plugin_active( 'wp-table-reloaded/wp-table-reloaded.php' );
362 $data['messages']['plugin_update_message'] = TablePress::$model_options->get( 'message_plugin_update' );
363 $data['messages']['donation_message'] = $this->maybe_show_donation_message();
364 $data['table_count'] = count( $data['table_ids'] );
365 break;
366 case 'about':
367 $data['first_activation'] = TablePress::$model_options->get( 'first_activation' );
368 $exporter = TablePress::load_class( 'TablePress_Export', 'class-export.php', 'classes' );
369 $data['zip_support_available'] = $exporter->zip_support_available;
370 break;
371 case 'options':
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 if ( isset( $_GET['item'] ) && 'save_custom_css' === $_GET['item'] ) {
375 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.
376 $action = 'options_custom_css'; // to load a different view
377 // Try saving "Custom CSS" to a file, otherwise this gets the HTML for the credentials form.
378 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
379 $result = $tablepress_css->save_custom_css_to_file_plugin_options( TablePress::$model_options->get( 'custom_css' ), TablePress::$model_options->get( 'custom_css_minified' ) );
380 if ( is_string( $result ) ) {
381 $data['credentials_form'] = $result; // This will only be called if the save function doesn't do a redirect.
382 } elseif ( true === $result ) {
383 /*
384 * At this point, saving was successful, so enable usage of CSS in files again,
385 * and also increase the "Custom CSS" version number (for cache busting).
386 */
387 TablePress::$model_options->update( array(
388 'use_custom_css_file' => true,
389 'custom_css_version' => TablePress::$model_options->get( 'custom_css_version' ) + 1,
390 ) );
391 TablePress::redirect( array( 'action' => 'options', 'message' => 'success_save' ) );
392 } else { // leaves only $result = false
393 TablePress::redirect( array( 'action' => 'options', 'message' => 'success_save_error_custom_css' ) );
394 }
395 break;
396 }
397 $data['frontend_options']['use_custom_css'] = TablePress::$model_options->get( 'use_custom_css' );
398 $data['frontend_options']['custom_css'] = TablePress::$model_options->get( 'custom_css' );
399 $data['user_options']['parent_page'] = $this->parent_page;
400 break;
401 case 'edit':
402 if ( ! empty( $_GET['table_id'] ) ) {
403 // Load table, with table data, options, and visibility settings.
404 $data['table'] = TablePress::$model_table->load( $_GET['table_id'], true, true );
405 if ( is_wp_error( $data['table'] ) ) {
406 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_load_table' ) );
407 }
408 if ( ! current_user_can( 'tablepress_edit_table', $_GET['table_id'] ) ) {
409 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
410 }
411 } else {
412 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_no_table' ) );
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'] ) : 'http://';
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 > 30 * DAY_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-top'] ) && '-1' !== $_POST['bulk-action-top'] ) {
576 $bulk_action = $_POST['bulk-action-top'];
577 } elseif ( isset( $_POST['bulk-action-bottom'] ) && '-1' !== $_POST['bulk-action-bottom'] ) {
578 $bulk_action = $_POST['bulk-action-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 } else {
590 $tables = wp_unslash( $_POST['table'] );
591 }
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'] ) || empty( $_POST['table']['id'] ) ) {
659 TablePress::redirect( array( 'action' => 'list', 'message' => 'error_save' ) );
660 } else {
661 $edit_table = wp_unslash( $_POST['table'] );
662 }
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 $id_changed = TablePress::$model_table->change_table_id( $table['id'], $table['new_id'] );
728 if ( ! is_wp_error( $id_changed ) ) {
729 TablePress::redirect( array( 'action' => 'edit', 'table_id' => $table['new_id'], 'message' => 'success_save_success_id_change' ) );
730 } else {
731 TablePress::redirect( array( 'action' => 'edit', 'table_id' => $table['id'], 'message' => 'success_save_error_id_change' ) );
732 }
733 } else {
734 TablePress::redirect( array( 'action' => 'edit', 'table_id' => $table['id'], 'message' => 'success_save_error_id_change' ) );
735 }
736 }
737
738 /**
739 * Add a table, according to the parameters on the "Add new Table" screen.
740 *
741 * @since 1.0.0
742 */
743 public function handle_post_action_add() {
744 TablePress::check_nonce( 'add' );
745
746 if ( ! current_user_can( 'tablepress_add_tables' ) ) {
747 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
748 }
749
750 if ( empty( $_POST['table'] ) || ! is_array( $_POST['table'] ) ) {
751 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add' ) );
752 } else {
753 $add_table = wp_unslash( $_POST['table'] );
754 }
755
756 // Perform sanity checks of posted data.
757 $name = ( isset( $add_table['name'] ) ) ? $add_table['name'] : '';
758 $description = ( isset( $add_table['description'] ) ) ? $add_table['description'] : '';
759 if ( ! isset( $add_table['rows'] ) || ! isset( $add_table['columns'] ) ) {
760 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add' ) );
761 }
762
763 $num_rows = absint( $add_table['rows'] );
764 $num_columns = absint( $add_table['columns'] );
765 if ( 0 === $num_rows || 0 === $num_columns ) {
766 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add' ) );
767 }
768
769 // Create a new table array with information from the posted data.
770 $new_table = array(
771 'name' => $name,
772 'description' => $description,
773 'data' => array_fill( 0, $num_rows, array_fill( 0, $num_columns, '' ) ),
774 'visibility' => array(
775 'rows' => array_fill( 0, $num_rows, 1 ),
776 'columns' => array_fill( 0, $num_columns, 1 ),
777 ),
778 );
779 // Merge this data into an empty table template.
780 $table = TablePress::$model_table->prepare_table( TablePress::$model_table->get_table_template(), $new_table, false );
781 if ( is_wp_error( $table ) ) {
782 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add' ) );
783 }
784
785 // Add the new table (and get its first ID).
786 $table_id = TablePress::$model_table->add( $table );
787 if ( is_wp_error( $table_id ) ) {
788 TablePress::redirect( array( 'action' => 'add', 'message' => 'error_add' ) );
789 }
790
791 TablePress::redirect( array( 'action' => 'edit', 'table_id' => $table_id, 'message' => 'success_add' ) );
792 }
793
794 /**
795 * Save changed "Plugin Options".
796 *
797 * @since 1.0.0
798 */
799 public function handle_post_action_options() {
800 TablePress::check_nonce( 'options' );
801
802 if ( ! current_user_can( 'tablepress_access_options_screen' ) ) {
803 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
804 }
805
806 if ( empty( $_POST['options'] ) || ! is_array( $_POST['options'] ) ) {
807 TablePress::redirect( array( 'action' => 'options', 'message' => 'error_save' ) );
808 } else {
809 $posted_options = wp_unslash( $_POST['options'] );
810 }
811
812 // Valid new options that will be merged into existing ones.
813 $new_options = array();
814
815 // Check each posted option value, and (maybe) add it to the new options.
816 if ( ! empty( $posted_options['admin_menu_parent_page'] ) && '-' !== $posted_options['admin_menu_parent_page'] ) {
817 $new_options['admin_menu_parent_page'] = $posted_options['admin_menu_parent_page'];
818 // Re-init parent information, as TablePress::redirect() URL might be wrong otherwise.
819 /** This filter is documented in classes/class-controller.php */
820 $this->parent_page = apply_filters( 'tablepress_admin_menu_parent_page', $posted_options['admin_menu_parent_page'] );
821 $this->is_top_level_page = in_array( $this->parent_page, array( 'top', 'middle', 'bottom' ), true );
822 }
823
824 // Custom CSS can only be saved if the user is allowed to do so.
825 $update_custom_css_files = false;
826 if ( current_user_can( 'tablepress_edit_options' ) ) {
827 // Checkbox
828 $new_options['use_custom_css'] = ( isset( $posted_options['use_custom_css'] ) && 'true' === $posted_options['use_custom_css'] );
829
830 if ( isset( $posted_options['custom_css'] ) ) {
831 $new_options['custom_css'] = $posted_options['custom_css'];
832
833 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
834 // Sanitize and tidy up Custom CSS.
835 $new_options['custom_css'] = $tablepress_css->sanitize_css( $new_options['custom_css'] );
836 // Minify Custom CSS
837 $new_options['custom_css_minified'] = $tablepress_css->minify_css( $new_options['custom_css'] );
838
839 // Maybe update CSS files as well.
840 $custom_css_file_contents = $tablepress_css->load_custom_css_from_file( 'normal' );
841 if ( false === $custom_css_file_contents ) {
842 $custom_css_file_contents = '';
843 }
844 // Don't write to file if it already has the desired content.
845 if ( $new_options['custom_css'] !== $custom_css_file_contents ) {
846 $update_custom_css_files = true;
847 // Set to false again. As it was set here, it will be set true again, if file saving succeeds.
848 $new_options['use_custom_css_file'] = false;
849 }
850 }
851 }
852
853 // 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.
854 if ( ! empty( $new_options ) ) {
855 TablePress::$model_options->update( $new_options );
856 TablePress::$model_table->_flush_caching_plugins_caches();
857 }
858
859 if ( $update_custom_css_files ) { // Capability check is performed above.
860 TablePress::redirect( array( 'action' => 'options', 'item' => 'save_custom_css' ), true );
861 }
862
863 TablePress::redirect( array( 'action' => 'options', 'message' => 'success_save' ) );
864 }
865
866 /**
867 * Export selected tables.
868 *
869 * @since 1.0.0
870 */
871 public function handle_post_action_export() {
872 TablePress::check_nonce( 'export' );
873
874 if ( ! current_user_can( 'tablepress_export_tables' ) ) {
875 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
876 }
877
878 if ( empty( $_POST['export'] ) || ! is_array( $_POST['export'] ) ) {
879 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export' ) );
880 } else {
881 $export = wp_unslash( $_POST['export'] );
882 }
883
884 $exporter = TablePress::load_class( 'TablePress_Export', 'class-export.php', 'classes' );
885
886 if ( empty( $export['tables'] ) ) {
887 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export' ) );
888 }
889 if ( empty( $export['format'] ) || ! isset( $exporter->export_formats[ $export['format'] ] ) ) {
890 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export' ) );
891 }
892 if ( empty( $export['csv_delimiter'] ) ) {
893 // Set a value, so that the variable exists.
894 $export['csv_delimiter'] = '';
895 }
896 if ( 'csv' === $export['format'] && ! isset( $exporter->csv_delimiters[ $export['csv_delimiter'] ] ) ) {
897 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_export' ) );
898 }
899
900 // Use list of tables from concatenated field if available (as that's hopefully not truncated by Suhosin, which is possible for $export['tables']).
901 $tables = ( ! empty( $export['tables_list'] ) ) ? explode( ',', $export['tables_list'] ) : $export['tables'];
902
903 // Determine if ZIP file support is available.
904 if ( $exporter->zip_support_available
905 && ( ( isset( $export['zip_file'] ) && 'true' === $export['zip_file'] ) || count( $tables ) > 1 ) ) {
906 // Export to ZIP only if ZIP is desired or if more than one table were selected (mandatory then).
907 $export_to_zip = true;
908 } else {
909 $export_to_zip = false;
910 }
911
912 if ( ! $export_to_zip ) {
913 // This is only possible for one table, so take the first one.
914 if ( ! current_user_can( 'tablepress_export_table', $tables[0] ) ) {
915 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
916 }
917 // Load table, with table data, options, and visibility settings.
918 $table = TablePress::$model_table->load( $tables[0], true, true );
919 if ( is_wp_error( $table ) ) {
920 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_load_table', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'] ) );
921 }
922 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
923 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_table_corrupted', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'] ) );
924 }
925 $download_filename = sprintf( '%1$s-%2$s-%3$s.%4$s', $table['id'], $table['name'], date( 'Y-m-d' ), $export['format'] );
926 $download_filename = sanitize_file_name( $download_filename );
927 // Export the table.
928 $export_data = $exporter->export_table( $table, $export['format'], $export['csv_delimiter'] );
929 /**
930 * Filter the exported table data.
931 *
932 * @since 1.6.0
933 *
934 * @param string $export_data The exported table data.
935 * @param array $table Table to be exported.
936 * @param string $export_format Format for the export ('csv', 'html', 'json').
937 * @param string $csv_delimiter Delimiter for CSV export.
938 */
939 $export_data = apply_filters( 'tablepress_export_data', $export_data, $table, $export['format'], $export['csv_delimiter'] );
940 $download_data = $export_data;
941 } else {
942 // Zipping can use a lot of memory and execution time, but not this much hopefully.
943 /** This filter is documented in the WordPress file wp-admin/admin.php */
944 @ini_set( 'memory_limit', apply_filters( 'admin_memory_limit', WP_MAX_MEMORY_LIMIT ) );
945 @set_time_limit( 300 );
946
947 $zip_file = new ZipArchive();
948 $download_filename = sprintf( 'tablepress-export-%1$s-%2$s.zip', date_i18n( 'Y-m-d-H-i-s' ), $export['format'] );
949 $download_filename = sanitize_file_name( $download_filename );
950 $full_filename = wp_tempnam( $download_filename );
951 if ( true !== $zip_file->open( $full_filename, ZIPARCHIVE::OVERWRITE ) ) {
952 @unlink( $full_filename );
953 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_create_zip_file', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'] ) );
954 }
955
956 foreach ( $tables as $table_id ) {
957 // Don't export tables for which the user doesn't have the necessary export rights.
958 if ( ! current_user_can( 'tablepress_export_table', $table_id ) ) {
959 continue;
960 }
961 // Load table, with table data, options, and visibility settings.
962 $table = TablePress::$model_table->load( $table_id, true, true );
963 // Don't export if the table could not be loaded.
964 if ( is_wp_error( $table ) ) {
965 continue;
966 }
967 // Don't export if the table is corrupted.
968 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
969 continue;
970 }
971 $export_data = $exporter->export_table( $table, $export['format'], $export['csv_delimiter'] );
972 /** This filter is documented in controllers/controller-admin.php */
973 $export_data = apply_filters( 'tablepress_export_data', $export_data, $table, $export['format'], $export['csv_delimiter'] );
974 $export_filename = sprintf( '%1$s-%2$s-%3$s.%4$s', $table['id'], $table['name'], date( 'Y-m-d' ), $export['format'] );
975 $export_filename = sanitize_file_name( $export_filename );
976 $zip_file->addFromString( $export_filename, $export_data );
977 }
978
979 // If something went wrong, or no files were added to the ZIP file, bail out.
980 if ( ! ZIPARCHIVE::ER_OK === $zip_file->status || 0 === $zip_file->numFiles ) {
981 $zip_file->close();
982 @unlink( $full_filename );
983 TablePress::redirect( array( 'action' => 'export', 'message' => 'error_create_zip_file', 'export_format' => $export['format'], 'csv_delimiter' => $export['csv_delimiter'] ) );
984 }
985 $zip_file->close();
986
987 // Load contents of the ZIP file, to send it as a download.
988 $download_data = file_get_contents( $full_filename );
989 @unlink( $full_filename );
990 }
991
992 // Send download headers for export file.
993 header( 'Content-Description: File Transfer' );
994 header( 'Content-Type: application/octet-stream' );
995 header( "Content-Disposition: attachment; filename=\"{$download_filename}\"" );
996 header( 'Content-Transfer-Encoding: binary' );
997 header( 'Expires: 0' );
998 header( 'Cache-Control: must-revalidate' );
999 header( 'Pragma: public' );
1000 header( 'Content-Length: ' . strlen( $download_data ) );
1001 // $filetype = text/csv, text/html, application/json
1002 // header( 'Content-Type: ' . $filetype. '; charset=' . get_option( 'blog_charset' ) );
1003 @ob_end_clean();
1004 flush();
1005 echo $download_data;
1006 exit;
1007 }
1008
1009 /**
1010 * Import data from existing source (Upload, URL, Server, Direct input).
1011 *
1012 * @since 1.0.0
1013 */
1014 public function handle_post_action_import() {
1015 TablePress::check_nonce( 'import' );
1016
1017 if ( ! current_user_can( 'tablepress_import_tables' ) ) {
1018 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1019 }
1020
1021 if ( empty( $_POST['import'] ) || ! is_array( $_POST['import'] ) ) {
1022 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import' ) );
1023 } else {
1024 $import = wp_unslash( $_POST['import'] );
1025 }
1026
1027 if ( ! isset( $import['type'] ) ) {
1028 $import['type'] = 'add';
1029 }
1030 if ( ! isset( $import['existing_table'] ) ) {
1031 $import['existing_table'] = '';
1032 }
1033 if ( ! isset( $import['source'] ) ) {
1034 $import['source'] = '';
1035 }
1036
1037 $import_error = true;
1038 $unlink_file = false;
1039 $import_data = array();
1040 switch ( $import['source'] ) {
1041 case 'file-upload':
1042 if ( ! empty( $_FILES['import_file_upload'] ) && UPLOAD_ERR_OK === $_FILES['import_file_upload']['error'] ) {
1043 $import_data['file_location'] = $_FILES['import_file_upload']['tmp_name'];
1044 $import_data['file_name'] = $_FILES['import_file_upload']['name'];
1045 // $_FILES['import_file_upload']['type'];
1046 // $_FILES['import_file_upload']['size']
1047 $import_error = false;
1048 $unlink_file = true;
1049 }
1050 break;
1051 case 'url':
1052 if ( ! empty( $import['url'] ) && 'http://' !== $import['url'] ) {
1053 // Check the host of the Import URL against a blacklist of hosts, which should not be accessible, e.g. for security considerations.
1054 $host = wp_parse_url( $import['url'], PHP_URL_HOST );
1055 $blocked_hosts = array(
1056 '169.254.169.254' // AWS Meta-data API
1057 );
1058 if ( in_array( $host, $blocked_hosts, true ) ) {
1059 $import_error = true;
1060 break;
1061 }
1062
1063 // Download URL to local file.
1064 $import_data['file_location'] = download_url( $import['url'] );
1065 $import_data['file_name'] = $import['url'];
1066 if ( ! is_wp_error( $import_data['file_location'] ) ) {
1067 $import_error = false;
1068 }
1069 $unlink_file = true;
1070 }
1071 break;
1072 case 'server':
1073 if ( ! empty( $import['server'] ) && ABSPATH !== $import['server']
1074 && ( ( ! is_multisite() && current_user_can( 'manage_options' ) ) || is_super_admin() )
1075 ) {
1076 // For security reasons, the `server` source is only available for administrators.
1077 $import_data['file_location'] = $import['server'];
1078 $import_data['file_name'] = pathinfo( $import['server'], PATHINFO_BASENAME );
1079 if ( is_readable( $import['server'] ) ) {
1080 $import_error = false;
1081 }
1082 }
1083 break;
1084 case 'form-field':
1085 if ( ! empty( $import['form_field'] ) ) {
1086 $import_data['file_location'] = '';
1087 $import_data['file_name'] = __( 'Imported from Manual Input', 'tablepress' ); // Description of the table.
1088 $import_data['data'] = $import['form_field'];
1089 $import_error = false;
1090 }
1091 break;
1092 }
1093
1094 if ( $import_error ) {
1095 if ( $unlink_file ) {
1096 @unlink( $import_data['file_location'] );
1097 }
1098 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'] ) );
1099 }
1100
1101 $this->importer = TablePress::load_class( 'TablePress_Import', 'class-import.php', 'classes' );
1102
1103 $import_zip = ( 'zip' === pathinfo( $import_data['file_name'], PATHINFO_EXTENSION ) );
1104
1105 // Determine if ZIP file support is available.
1106 if ( $import_zip && ! $this->importer->zip_support_available ) {
1107 if ( $unlink_file ) {
1108 @unlink( $import_data['file_location'] );
1109 }
1110 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'] ) );
1111 }
1112
1113 if ( ! $import_zip ) {
1114 // Check if a table to replace or append to was selected (which is only necessary for import from non-ZIP files).
1115 if ( in_array( $import['type'], array( 'replace', 'append' ), true ) && empty( $import['existing_table'] ) ) {
1116 if ( $unlink_file ) {
1117 @unlink( $import_data['file_location'] );
1118 }
1119 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import_no_existing_id', 'import_format' => $import['format'], 'import_type' => $import['type'], 'import_source' => $import['source'] ) );
1120 }
1121
1122 if ( ! isset( $import_data['data'] ) ) {
1123 $import_data['data'] = file_get_contents( $import_data['file_location'] );
1124 }
1125 if ( false === $import_data['data'] ) {
1126 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import' ) );
1127 }
1128
1129 $name = $import_data['file_name'];
1130 $description = $import_data['file_name'];
1131 $existing_table_id = ( in_array( $import['type'], array( 'replace', 'append' ), true ) && ! empty( $import['existing_table'] ) ) ? $import['existing_table'] : false;
1132 $table_id = $this->_import_tablepress_table( $import['format'], $import_data['data'], $name, $description, $existing_table_id, $import['type'] );
1133
1134 if ( $unlink_file ) {
1135 @unlink( $import_data['file_location'] );
1136 }
1137
1138 if ( is_wp_error( $table_id ) ) {
1139 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import_data' ) );
1140 } else {
1141 TablePress::redirect( array( 'action' => 'edit', 'table_id' => $table_id, 'message' => 'success_import' ) );
1142 }
1143 } else {
1144 // Zipping can use a lot of memory and execution time, but not this much hopefully.
1145 /** This filter is documented in the WordPress file wp-admin/admin.php */
1146 @ini_set( 'memory_limit', apply_filters( 'admin_memory_limit', WP_MAX_MEMORY_LIMIT ) );
1147 @set_time_limit( 300 );
1148
1149 $zip = new ZipArchive();
1150 if ( true !== $zip->open( $import_data['file_location'], ZIPARCHIVE::CHECKCONS ) ) {
1151 if ( $unlink_file ) {
1152 @unlink( $import_data['file_location'] );
1153 }
1154 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import_zip_open' ) );
1155 }
1156
1157 $imported_files = array();
1158 for ( $file_idx = 0; $file_idx < $zip->numFiles; $file_idx++ ) {
1159 $file_name = $zip->getNameIndex( $file_idx );
1160 // Skip directories.
1161 if ( '/' === substr( $file_name, -1 ) ) {
1162 continue;
1163 }
1164 // Skip the __MACOSX directory that Mac OSX adds to archives.
1165 if ( '__MACOSX/' === substr( $file_name, 0, 9 ) ) {
1166 continue;
1167 }
1168 $data = $zip->getFromIndex( $file_idx );
1169 if ( false === $data ) {
1170 continue;
1171 }
1172
1173 $name = $file_name;
1174 $description = $file_name;
1175 $existing_table_id = ( in_array( $import['type'], array( 'replace', 'append' ), true ) ) ? false : false; // @TODO: Find a way to extract the replace/append ID from the filename, maybe? For the JSON format, a check is done after the import.
1176 $table_id = $this->_import_tablepress_table( $import['format'], $data, $name, $description, $existing_table_id, $import['type'] );
1177 if ( is_wp_error( $table_id ) ) {
1178 continue;
1179 } else {
1180 $imported_files[] = $table_id;
1181 }
1182 };
1183 $zip->close();
1184
1185 if ( $unlink_file ) {
1186 @unlink( $import_data['file_location'] );
1187 }
1188
1189 if ( count( $imported_files ) > 1 ) {
1190 TablePress::redirect( array( 'action' => 'list', 'message' => 'success_import' ) );
1191 } elseif ( 1 === count( $imported_files ) ) {
1192 TablePress::redirect( array( 'action' => 'edit', 'table_id' => $imported_files[0], 'message' => 'success_import' ) );
1193 } else {
1194 TablePress::redirect( array( 'action' => 'import', 'message' => 'error_import_zip_content' ) );
1195 }
1196 }
1197
1198 }
1199
1200 /**
1201 * Import a table by either replacing an existing table or adding it as a new table.
1202 *
1203 * @since 1.0.0
1204 *
1205 * @param string $format Import format.
1206 * @param string $data Data to import.
1207 * @param string $name Name of the table.
1208 * @param string $description Description of the table.
1209 * @param bool|string $existing_table_id False if table shall be added new, ID of the table to be replaced or appended to otherwise.
1210 * @param string $import_type What to do with the imported data: "add", "replace", "append".
1211 * @return string|WP_Error WP_Error on error, table ID on success.
1212 */
1213 protected function _import_tablepress_table( $format, $data, $name, $description, $existing_table_id, $import_type ) {
1214 $imported_table = $this->importer->import_table( $format, $data );
1215 if ( false === $imported_table ) {
1216 return new WP_Error( 'table_import_import_failed' );
1217 }
1218
1219 // Full JSON format table can contain a table ID, try to keep that.
1220 $table_id_in_import = isset( $imported_table['id'] ) ? $imported_table['id'] : false;
1221
1222 // If no ID for an existing table was specified in the import form, we add the imported table,
1223 // except for replacing and appending of JSON files in ZIP archives, where we try to use the imported table ID.
1224 if ( false === $existing_table_id ) {
1225 if ( false !== $table_id_in_import && TablePress::$model_table->table_exists( $table_id_in_import ) ) {
1226 $existing_table_id = $table_id_in_import;
1227 } else {
1228 $import_type = 'add';
1229 }
1230 }
1231
1232 // To be able to replace or append to a table, editing that table must be allowed.
1233 if ( in_array( $import_type, array( 'replace', 'append' ), true ) && ! current_user_can( 'tablepress_edit_table', $existing_table_id ) ) {
1234 return new WP_Error( 'table_import_replace_append_capability_check_failed' );
1235 }
1236
1237 switch ( $import_type ) {
1238 case 'add':
1239 $existing_table = TablePress::$model_table->get_table_template();
1240 // If name and description are imported from a new table, use those.
1241 if ( ! isset( $imported_table['name'] ) ) {
1242 $imported_table['name'] = $name;
1243 }
1244 if ( ! isset( $imported_table['description'] ) ) {
1245 $imported_table['description'] = $description;
1246 }
1247 if ( isset( $imported_table['visibility'] ) && isset( $imported_table['visibility']['rows'] ) && isset( $imported_table['visibility']['columns'] ) ) {
1248 $existing_table['visibility']['rows'] = $imported_table['visibility']['rows'];
1249 $existing_table['visibility']['columns'] = $imported_table['visibility']['columns'];
1250 }
1251 break;
1252 case 'replace':
1253 // Load table, without table data, but with options and visibility settings.
1254 $existing_table = TablePress::$model_table->load( $existing_table_id, false, true );
1255 if ( is_wp_error( $existing_table ) ) {
1256 // Add an error code to the existing WP_Error.
1257 $existing_table->add( 'table_import_replace_table_load', '', $existing_table_id );
1258 return $existing_table;
1259 }
1260 // Don't change name and description when a table is replaced.
1261 $imported_table['name'] = $existing_table['name'];
1262 $imported_table['description'] = $existing_table['description'];
1263 if ( isset( $imported_table['visibility'] ) && isset( $imported_table['visibility']['rows'] ) && isset( $imported_table['visibility']['columns'] ) ) {
1264 $existing_table['visibility']['rows'] = $imported_table['visibility']['rows'];
1265 $existing_table['visibility']['columns'] = $imported_table['visibility']['columns'];
1266 }
1267 break;
1268 case 'append':
1269 // Load table, with table data, options, and visibility settings.
1270 $existing_table = TablePress::$model_table->load( $existing_table_id, true, true );
1271 if ( is_wp_error( $existing_table ) ) {
1272 // Add an error code to the existing WP_Error.
1273 $existing_table->add( 'table_import_append_table_load', '', $existing_table_id );
1274 return $existing_table;
1275 }
1276 if ( isset( $existing_table['is_corrupted'] ) && $existing_table['is_corrupted'] ) {
1277 return new WP_Error( 'table_import_append_table_load_corrupted', '', $existing_table_id );
1278 }
1279 // Don't change name and description when a table is appended to.
1280 $imported_table['name'] = $existing_table['name'];
1281 $imported_table['description'] = $existing_table['description'];
1282 // Actual appending:
1283 $imported_table['data'] = array_merge( $existing_table['data'], $imported_table['data'] );
1284 $imported_table['data'] = $this->importer->pad_array_to_max_cols( $imported_table['data'] );
1285 // Append visibility information for rows.
1286 if ( isset( $imported_table['visibility'] ) && isset( $imported_table['visibility']['rows'] ) ) {
1287 $existing_table['visibility']['rows'] = array_merge( $existing_table['visibility']['rows'], $imported_table['visibility']['rows'] );
1288 }
1289 // When appending, do not overwrite options.
1290 if ( isset( $imported_table['options'] ) ) {
1291 unset( $imported_table['options'] );
1292 }
1293 break;
1294 default:
1295 return new WP_Error( 'table_import_import_type_invalid', '', $import_type );
1296 }
1297
1298 // Merge new or existing table with information from the imported table.
1299 $imported_table['id'] = $existing_table['id']; // will be false for new table or the existing table ID
1300 // Cut visibility array (if the imported table is smaller), and pad correctly if imported table is bigger than existing table (or new template).
1301 $num_rows = count( $imported_table['data'] );
1302 $num_columns = count( $imported_table['data'][0] );
1303 $imported_table['visibility'] = array(
1304 'rows' => array_pad( array_slice( $existing_table['visibility']['rows'], 0, $num_rows ), $num_rows, 1 ),
1305 'columns' => array_pad( array_slice( $existing_table['visibility']['columns'], 0, $num_columns ), $num_columns, 1 ),
1306 );
1307
1308 // Check if new data is ok.
1309 $table = TablePress::$model_table->prepare_table( $existing_table, $imported_table, false );
1310 if ( is_wp_error( $table ) ) {
1311 // Add an error code to the existing WP_Error.
1312 $table->add( 'table_import_table_prepare', '' );
1313 return $table;
1314 }
1315
1316 // DataTables Custom Commands can only be edit by trusted users.
1317 if ( ! current_user_can( 'unfiltered_html' ) ) {
1318 $table['options']['datatables_custom_commands'] = $existing_table['options']['datatables_custom_commands'];
1319 }
1320
1321 // Replace existing table or add new table.
1322 if ( in_array( $import_type, array( 'replace', 'append' ), true ) ) {
1323 // Replace existing table with imported/appended table.
1324 $table_id = TablePress::$model_table->save( $table );
1325 } else {
1326 // Add the imported table (and get its first ID).
1327 $table_id = TablePress::$model_table->add( $table );
1328 }
1329
1330 if ( is_wp_error( $table_id ) ) {
1331 // Add an error code to the existing WP_Error.
1332 $table_id->add( 'table_import_table_save_or_add', '' );
1333 return $table_id;
1334 }
1335
1336 // Try to use ID from imported file (e.g. in full JSON format table).
1337 if ( false !== $table_id_in_import && $table_id !== $table_id_in_import && current_user_can( 'tablepress_edit_table_id', $table_id ) ) {
1338 $id_changed = TablePress::$model_table->change_table_id( $table_id, $table_id_in_import );
1339 if ( ! is_wp_error( $id_changed ) ) {
1340 $table_id = $table_id_in_import;
1341 }
1342 }
1343
1344 return $table_id;
1345 }
1346
1347 /*
1348 * Save GET actions.
1349 */
1350
1351 /**
1352 * Hide a header message on an admin screen.
1353 *
1354 * @since 1.0.0
1355 */
1356 public function handle_get_action_hide_message() {
1357 $message_item = ! empty( $_GET['item'] ) ? $_GET['item'] : '';
1358 TablePress::check_nonce( 'hide_message', $message_item );
1359
1360 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
1361 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1362 }
1363
1364 TablePress::$model_options->update( "message_{$message_item}", false );
1365
1366 $return = ! empty( $_GET['return'] ) ? $_GET['return'] : 'list';
1367 TablePress::redirect( array( 'action' => $return ) );
1368 }
1369
1370 /**
1371 * Delete a table.
1372 *
1373 * @since 1.0.0
1374 */
1375 public function handle_get_action_delete_table() {
1376 $table_id = ( ! empty( $_GET['item'] ) ) ? $_GET['item'] : false;
1377 TablePress::check_nonce( 'delete_table', $table_id );
1378
1379 $return = ! empty( $_GET['return'] ) ? $_GET['return'] : 'list';
1380 $return_item = ! empty( $_GET['return_item'] ) ? $_GET['return_item'] : false;
1381
1382 // Nonce check should actually catch this already.
1383 if ( false === $table_id ) {
1384 TablePress::redirect( array( 'action' => $return, 'message' => 'error_delete', 'table_id' => $return_item ) );
1385 }
1386
1387 if ( ! current_user_can( 'tablepress_delete_table', $table_id ) ) {
1388 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1389 }
1390
1391 $deleted = TablePress::$model_table->delete( $table_id );
1392 if ( is_wp_error( $deleted ) ) {
1393 TablePress::redirect( array( 'action' => $return, 'message' => 'error_delete', 'table_id' => $return_item ) );
1394 }
1395
1396 /*
1397 * Slightly more complex redirect method, to account for sort, search, and pagination in the WP_List_Table on the List View,
1398 * but only if this action succeeds, to have everything fresh in the event of an error.
1399 */
1400 $sendback = wp_get_referer();
1401 if ( ! $sendback ) {
1402 $sendback = TablePress::url( array( 'action' => 'list', 'message' => 'success_delete', 'table_id' => $return_item ) );
1403 } else {
1404 $sendback = remove_query_arg( array( 'action', 'message', 'table_id' ), $sendback );
1405 $sendback = add_query_arg( array( 'action' => 'list', 'message' => 'success_delete', 'table_id' => $return_item ), $sendback );
1406 }
1407 wp_redirect( $sendback );
1408 exit;
1409 }
1410
1411 /**
1412 * Copy a table.
1413 *
1414 * @since 1.0.0
1415 */
1416 public function handle_get_action_copy_table() {
1417 $table_id = ( ! empty( $_GET['item'] ) ) ? $_GET['item'] : false;
1418 TablePress::check_nonce( 'copy_table', $table_id );
1419
1420 $return = ! empty( $_GET['return'] ) ? $_GET['return'] : 'list';
1421 $return_item = ! empty( $_GET['return_item'] ) ? $_GET['return_item'] : false;
1422
1423 // Nonce check should actually catch this already.
1424 if ( false === $table_id ) {
1425 TablePress::redirect( array( 'action' => $return, 'message' => 'error_copy', 'table_id' => $return_item ) );
1426 }
1427
1428 if ( ! current_user_can( 'tablepress_copy_table', $table_id ) ) {
1429 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1430 }
1431
1432 $copy_table_id = TablePress::$model_table->copy( $table_id );
1433 if ( is_wp_error( $copy_table_id ) ) {
1434 TablePress::redirect( array( 'action' => $return, 'message' => 'error_copy', 'table_id' => $return_item ) );
1435 } else {
1436 $return_item = $copy_table_id;
1437 }
1438
1439 /*
1440 * Slightly more complex redirect method, to account for sort, search, and pagination in the WP_List_Table on the List View,
1441 * but only if this action succeeds, to have everything fresh in the event of an error.
1442 */
1443 $sendback = wp_get_referer();
1444 if ( ! $sendback ) {
1445 $sendback = TablePress::url( array( 'action' => $return, 'message' => 'success_copy', 'table_id' => $return_item ) );
1446 } else {
1447 $sendback = remove_query_arg( array( 'action', 'message', 'table_id' ), $sendback );
1448 $sendback = add_query_arg( array( 'action' => $return, 'message' => 'success_copy', 'table_id' => $return_item ), $sendback );
1449 }
1450 wp_redirect( $sendback );
1451 exit;
1452 }
1453
1454 /**
1455 * Preview a table.
1456 *
1457 * @since 1.0.0
1458 */
1459 public function handle_get_action_preview_table() {
1460 $table_id = ( ! empty( $_GET['item'] ) ) ? $_GET['item'] : false;
1461 TablePress::check_nonce( 'preview_table', $table_id );
1462
1463 // Nonce check should actually catch this already.
1464 if ( false === $table_id ) {
1465 wp_die( __( 'The preview could not be loaded.', 'tablepress' ), __( 'Preview', 'tablepress' ) );
1466 }
1467
1468 if ( ! current_user_can( 'tablepress_preview_table', $table_id ) ) {
1469 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1470 }
1471
1472 // Load table, with table data, options, and visibility settings.
1473 $table = TablePress::$model_table->load( $table_id, true, true );
1474 if ( is_wp_error( $table ) ) {
1475 wp_die( __( 'The table could not be loaded.', 'tablepress' ), __( 'Preview', 'tablepress' ) );
1476 }
1477
1478 // Sanitize all table data to remove unsafe HTML from the preview output, if the user is not allowed to work with unfiltered HTML.
1479 if ( ! current_user_can( 'unfiltered_html' ) ) {
1480 $table = TablePress::$model_table->sanitize( $table );
1481 }
1482
1483 // Create a render class instance.
1484 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
1485 // Merge desired options with default render options (see TablePress_Controller_Frontend::shortcode_table()).
1486 $default_render_options = $_render->get_default_render_options();
1487 /** This filter is documented in controllers/controller-frontend.php */
1488 $default_render_options = apply_filters( 'tablepress_shortcode_table_default_shortcode_atts', $default_render_options );
1489 $render_options = shortcode_atts( $default_render_options, $table['options'] );
1490 /** This filter is documented in controllers/controller-frontend.php */
1491 $render_options = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $render_options );
1492 $_render->set_input( $table, $render_options );
1493 $view_data = array(
1494 'table_id' => $table_id,
1495 'head_html' => $_render->get_preview_css(),
1496 'body_html' => $_render->get_output(),
1497 );
1498
1499 $custom_css = TablePress::$model_options->get( 'custom_css' );
1500 if ( ! empty( $custom_css ) ) {
1501 $view_data['head_html'] .= "<style type=\"text/css\">\n{$custom_css}\n</style>\n";
1502 }
1503
1504 // Prepare, initialize, and render the view.
1505 $this->view = TablePress::load_view( 'preview_table', $view_data );
1506 $this->view->render();
1507 }
1508
1509 /**
1510 * Show a list of tables in the Editor toolbar Thickbox (opened by TinyMCE or Quicktags button).
1511 *
1512 * @since 1.0.0
1513 */
1514 public function handle_get_action_editor_button_thickbox() {
1515 TablePress::check_nonce( 'editor_button_thickbox' );
1516
1517 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
1518 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1519 }
1520
1521 $view_data = array(
1522 // Load all table IDs without priming the post meta cache, as table options/visibility are not needed.
1523 'table_ids' => TablePress::$model_table->load_all( false ),
1524 );
1525
1526 set_current_screen( 'tablepress_editor_button_thickbox' );
1527
1528 // Prepare, initialize, and render the view.
1529 $this->view = TablePress::load_view( 'editor_button_thickbox', $view_data );
1530 $this->view->render();
1531 }
1532
1533 /**
1534 * Uninstall TablePress, and delete all tables and options.
1535 *
1536 * @since 1.0.0
1537 */
1538 public function handle_get_action_uninstall_tablepress() {
1539 TablePress::check_nonce( 'uninstall_tablepress' );
1540
1541 $plugin = TABLEPRESS_BASENAME;
1542
1543 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 ) ) {
1544 wp_die( __( 'Sorry, you are not allowed to access this page.', 'default' ), 403 );
1545 }
1546
1547 // Deactivate TablePress for the site (but not for the network).
1548 deactivate_plugins( $plugin, false, false );
1549 update_option( 'recently_activated', array( $plugin => time() ) + (array) get_option( 'recently_activated', array() ) );
1550
1551 // Delete all tables, "Custom CSS" files, and options.
1552 TablePress::$model_table->delete_all();
1553 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
1554 $css_files_deleted = $tablepress_css->delete_custom_css_files();
1555 TablePress::$model_options->remove_access_capabilities();
1556
1557 TablePress::$model_table->destroy();
1558 TablePress::$model_options->destroy();
1559
1560 $output = '<strong>' . __( 'TablePress was uninstalled successfully.', 'tablepress' ) . '</strong><br /><br />';
1561 $output .= __( 'All tables, data, and options were deleted.', 'tablepress' );
1562 if ( is_multisite() ) {
1563 $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' );
1564 } else {
1565 $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' );
1566 }
1567 if ( $css_files_deleted ) {
1568 $output .= ' ' . __( 'Your TablePress &#8220;Custom CSS&#8221; files have been deleted automatically.', 'tablepress' );
1569 } else {
1570 if ( is_multisite() ) {
1571 $output .= ' ' . __( 'Please also ask him to delete your TablePress &#8220;Custom CSS&#8221; files from the server.', 'tablepress' );
1572 } else {
1573 $output .= ' ' . __( 'You may now also delete your TablePress &#8220;Custom CSS&#8221; files in the <code>wp-content</code> folder.', 'tablepress' );
1574 }
1575 }
1576 $output .= "</p>\n<p>";
1577 if ( ! is_multisite() || is_super_admin() ) {
1578 $output .= '<a class="button" href="' . esc_url( admin_url( 'plugins.php' ) ) . '">' . __( 'Go to &#8220;Plugins&#8221; page', 'tablepress' ) . '</a> ';
1579 }
1580 $output .= '<a class="button" href="' . esc_url( admin_url( 'index.php' ) ) . '">' . __( 'Go to Dashboard', 'tablepress' ) . '</a>';
1581
1582 wp_die( $output, __( 'Uninstall TablePress', 'tablepress' ), array( 'response' => 200, 'back_link' => false ) );
1583 }
1584
1585 } // class TablePress_Admin_Controller
1586