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

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

366 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin AJAX Controller for TablePress with functionality for the 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 AJAX Controller class, extends Base Controller Class
16 *
17 * @package TablePress
18 * @subpackage Controllers
19 * @author Tobias Bäthge
20 * @since 1.0.0
21 */
22 class TablePress_Admin_AJAX_Controller extends TablePress_Controller {
23
24 /**
25 * Initiates the Admin AJAX functionality.
26 *
27 * @since 1.0.0
28 */
29 public function __construct() {
30 // Buffer all outputs, to prevent errors/warnings being printed that make the JSON invalid.
31 ob_start();
32
33 parent::__construct();
34
35 $ajax_actions = array( 'hide_message', 'save_table', 'preview_table', 'save_screen_options' );
36 foreach ( $ajax_actions as $action ) {
37 add_action( "wp_ajax_tablepress_{$action}", array( $this, "ajax_action_{$action}" ) );
38 }
39 }
40
41 /**
42 * Hides a header message on an admin screen.
43 *
44 * @since 1.0.0
45 */
46 public function ajax_action_hide_message(): void {
47 if ( empty( $_GET['item'] ) ) {
48 wp_die( '0' );
49 }
50
51 $message_item = $_GET['item'];
52
53 TablePress::check_nonce( 'hide_message', $message_item, '_wpnonce', true );
54
55 if ( ! current_user_can( 'tablepress_list_tables' ) ) {
56 wp_die( '-1' );
57 }
58
59 TablePress::$model_options->update( "message_{$message_item}", false );
60
61 wp_die( '1' );
62 }
63
64 /**
65 * Saves the table after the "Save Changes" button on the "Edit" screen has been clicked.
66 *
67 * @since 1.0.0
68 */
69 public function ajax_action_save_table(): void {
70 if ( empty( $_POST['tablepress']['id'] ) ) {
71 wp_die( '-1' );
72 }
73
74 $edit_table = wp_unslash( $_POST['tablepress'] );
75
76 // Check if the submitted nonce matches the generated nonce we created earlier, dies -1 on failure.
77 TablePress::check_nonce( 'edit', $edit_table['id'], '_ajax_nonce', true );
78
79 // Ignore the request if the current user doesn't have sufficient permissions.
80 if ( ! current_user_can( 'tablepress_edit_table', $edit_table['id'] ) ) {
81 wp_die( '-1' );
82 }
83
84 // Default response data.
85 $success = false;
86 $message = 'error_save';
87 $error_details = '';
88 do { // To be able to "break;" (allows for better readable code).
89 // Load table, without table data, but with options and visibility settings.
90 $existing_table = TablePress::$model_table->load( $edit_table['id'], false, true );
91 if ( is_wp_error( $existing_table ) ) { // @todo Maybe somehow load a new table here? (TablePress::$model_table->get_table_template())?
92 $error = new WP_Error( 'ajax_save_table_load', '', $edit_table['id'] );
93 $error->merge_from( $existing_table );
94 $error_details = TablePress::get_wp_error_string( $error );
95 break;
96 }
97
98 // Check and convert all data that was transmitted as valid JSON.
99 $keys = array( 'data', 'options', 'visibility' );
100 foreach ( $keys as $key ) {
101 if ( empty( $edit_table[ $key ] ) ) {
102 $error = new WP_Error( "ajax_save_table_{$key}_empty", '', $edit_table['id'] );
103 $error_details = TablePress::get_wp_error_string( $error );
104 break 2;
105 }
106 $edit_table[ $key ] = json_decode( $edit_table[ $key ], true );
107 if ( is_null( $edit_table[ $key ] ) ) {
108 $error = new WP_Error( "ajax_save_table_{$key}_invalid_json", '', $edit_table['id'] );
109 $error_details = TablePress::get_wp_error_string( $error );
110 break 2;
111 }
112 $edit_table[ $key ] = (array) $edit_table[ $key ]; // Cast to array again, to catch strings, etc.
113 }
114
115 // Check consistency of new table, and then merge with existing table.
116 $table = TablePress::$model_table->prepare_table( $existing_table, $edit_table, true );
117 if ( is_wp_error( $table ) ) {
118 $error = new WP_Error( 'ajax_save_table_prepare', '', $edit_table['id'] );
119 $error->merge_from( $table );
120 $error_details = TablePress::get_wp_error_string( $error );
121 break;
122 }
123
124 // DataTables Custom Commands can only be edited by trusted users.
125 if ( ! current_user_can( 'unfiltered_html' ) ) {
126 $table['options']['datatables_custom_commands'] = $existing_table['options']['datatables_custom_commands'];
127 }
128
129 // Save updated table.
130 $saved = TablePress::$model_table->save( $table );
131 if ( is_wp_error( $saved ) ) {
132 $error = new WP_Error( 'ajax_save_table_save', '', $table['id'] );
133 $error->merge_from( $saved );
134 $error_details = TablePress::get_wp_error_string( $error );
135 break;
136 }
137
138 // At this point, the table was saved successfully, possible ID change remains.
139 $success = true;
140 $message = 'success_save';
141
142 // Check if ID change is desired.
143 if ( $table['id'] === $table['new_id'] ) {
144 // If not, we are done.
145 break;
146 }
147
148 // Change table ID.
149 if ( current_user_can( 'tablepress_edit_table_id', $table['id'] ) ) {
150 $id_changed = TablePress::$model_table->change_table_id( $table['id'], $table['new_id'] );
151 if ( ! is_wp_error( $id_changed ) ) {
152 $message = 'success_save_success_id_change';
153 $table['id'] = $table['new_id'];
154 } else {
155 $message = 'success_save_error_id_change';
156 $error = new WP_Error( 'ajax_save_table_id_change', '', $table['new_id'] );
157 $error->merge_from( $id_changed );
158 $error_details = TablePress::get_wp_error_string( $error );
159 }
160 } else {
161 $message = 'success_save_error_id_change';
162 $error_details = 'table_id_could_not_be_changed: capability_check_failed';
163 }
164 } while ( false ); // Do-while-loop through this exactly once, to be able to "break;" early. // @phpstan-ignore-line .
165
166 // Generate the response.
167
168 // Common data for all responses.
169 $response = array(
170 'success' => $success,
171 'message' => $message,
172 );
173 if ( $success ) {
174 // For the phpstan ignores in the next lines: If this is reached, $table is guaranteed to exist and is a valid array.
175 $response['table_id'] = $table['id']; // @phpstan-ignore-line
176 $response['new_edit_nonce'] = wp_create_nonce( TablePress::nonce( 'edit', $table['id'] ) ); // @phpstan-ignore-line
177 $response['new_preview_nonce'] = wp_create_nonce( TablePress::nonce( 'preview_table', $table['id'] ) ); // @phpstan-ignore-line
178 $response['new_copy_nonce'] = wp_create_nonce( TablePress::nonce( 'copy_table', $table['id'] ) ); // @phpstan-ignore-line
179 $response['new_delete_nonce'] = wp_create_nonce( TablePress::nonce( 'delete_table', $table['id'] ) ); // @phpstan-ignore-line
180 $response['last_modified'] = TablePress::format_datetime( $table['last_modified'] ); // @phpstan-ignore-line
181 $response['last_editor'] = TablePress::get_user_display_name( $table['options']['last_editor'] ); // @phpstan-ignore-line
182 }
183 if ( ! empty( $error_details ) ) {
184 $response['error_details'] = esc_html( $error_details );
185 }
186 // Buffer all outputs, to prevent errors/warnings being printed that make the JSON invalid.
187 $output_buffer = ob_get_clean();
188 if ( ! empty( $output_buffer ) ) {
189 $response['output_buffer'] = $output_buffer;
190 }
191
192 // Send the response.
193 wp_send_json( $response );
194 }
195
196 /**
197 * Returns the live preview data of table that has non-saved changes.
198 *
199 * @since 1.0.0
200 */
201 public function ajax_action_preview_table(): void {
202 if ( empty( $_POST['tablepress']['id'] ) ) {
203 wp_die( '-1' );
204 }
205
206 $preview_table = wp_unslash( $_POST['tablepress'] );
207
208 // Check if the submitted nonce matches the generated nonce we created earlier, dies -1 on failure.
209 TablePress::check_nonce( 'preview_table', $preview_table['id'], '_ajax_nonce', true );
210
211 // Ignore the request if the current user doesn't have sufficient permissions.
212 if ( ! current_user_can( 'tablepress_preview_table', $preview_table['id'] ) ) {
213 wp_die( '-1' );
214 }
215
216 // Default response data.
217 $success = false;
218 do { // To be able to "break;" (allows for better readable code).
219 // Load table, without table data, but with options and visibility settings.
220 $existing_table = TablePress::$model_table->load( $preview_table['id'], false, true );
221 if ( is_wp_error( $existing_table ) ) { // @todo Maybe somehow load a new table here? (TablePress::$model_table->get_table_template())?
222 break;
223 }
224
225 // Check and convert all data that was transmitted as valid JSON.
226 $keys = array( 'data', 'options', 'visibility' );
227 foreach ( $keys as $key ) {
228 if ( empty( $preview_table[ $key ] ) ) {
229 break 2;
230 }
231 $preview_table[ $key ] = json_decode( $preview_table[ $key ], true );
232 if ( is_null( $preview_table[ $key ] ) ) {
233 break 2;
234 }
235 $preview_table[ $key ] = (array) $preview_table[ $key ]; // Cast to array again, to catch strings, etc.
236 }
237
238 // Check consistency of new table, and then merge with existing table.
239 $table = TablePress::$model_table->prepare_table( $existing_table, $preview_table, true );
240 if ( is_wp_error( $table ) ) {
241 break;
242 }
243
244 // DataTables Custom Commands can only be edited by trusted users.
245 if ( ! current_user_can( 'unfiltered_html' ) ) {
246 $table['options']['datatables_custom_commands'] = $existing_table['options']['datatables_custom_commands'];
247 }
248
249 // If the ID has changed, and the new ID is valid, render with the new ID (important e.g. for CSS classes/HTML ID).
250 if ( $table['id'] !== $table['new_id'] && 0 === preg_match( '/[^a-zA-Z0-9_-]/', $table['new_id'] ) ) {
251 $table['id'] = $table['new_id'];
252 }
253
254 // Sanitize all table data to remove unsafe HTML from the preview output, if the user is not allowed to work with unfiltered HTML.
255 if ( ! current_user_can( 'unfiltered_html' ) ) {
256 $table = TablePress::$model_table->sanitize( $table );
257 }
258
259 // At this point, the table data is valid and sanitized and can be rendered.
260 $success = true;
261 } while ( false ); // Do-while-loop through this exactly once, to be able to "break;" early. // @phpstan-ignore-line .
262
263 if ( $success ) {
264 // Create a render class instance.
265 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
266 // Merge desired options with default render options (see TablePress_Controller_Frontend::shortcode_table()).
267 $default_render_options = $_render->get_default_render_options();
268 /** This filter is documented in controllers/controller-frontend.php */
269 $default_render_options = apply_filters( 'tablepress_shortcode_table_default_shortcode_atts', $default_render_options );
270 // For the phpstan ignores in the next lines: If this is reached, $table is guaranteed to exist and is a valid array.
271 $render_options = shortcode_atts( $default_render_options, $table['options'] ); // @phpstan-ignore-line
272 /** This filter is documented in controllers/controller-frontend.php */
273 $render_options = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $render_options );
274 $render_options['html_id'] = "tablepress-{$table['id']}"; // @phpstan-ignore-line
275 $render_options['block_preview'] = true;
276 $_render->set_input( $table, $render_options ); // @phpstan-ignore-line
277 $head_html = $_render->get_preview_css();
278 $custom_css = TablePress::$model_options->get( 'custom_css' );
279 $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
280 if ( $use_custom_css ) {
281 $head_html .= "<style>\n{$custom_css}\n</style>\n";
282 }
283
284 $body_html = '<div id="tablepress-page"><p>'
285 . __( 'This is a preview of your table.', 'tablepress' ) . ' '
286 . __( 'Because of CSS styling in your theme, the table might look different on your page!', 'tablepress' ) . ' '
287 . __( 'The Table Features for Site Visitors, like sorting, filtering, and pagination, are also not available in this preview!', 'tablepress' ) . '<br>';
288 // Show the instructions string depending on whether the Block Editor is used on the site or not.
289 if ( TablePress::site_uses_block_editor() ) {
290 $body_html .= sprintf( __( 'To insert a table into a post or page, add a “%1$s” block in the block editor and select the desired table.', 'tablepress' ), __( 'TablePress table', 'tablepress' ) );
291 } else {
292 $body_html .= __( 'To insert a table into a post or page, paste its Shortcode at the desired place in the editor.', 'tablepress' ) . ' '
293 . __( 'Each table has a unique ID that needs to be adjusted in that Shortcode.', 'tablepress' );
294 }
295 $body_html .= '</p>' . $_render->get_output( 'html' ) . '</div>';
296 } else {
297 $head_html = '';
298 $body_html = __( 'The preview could not be loaded.', 'tablepress' );
299 }
300
301 // Generate the response.
302 $response = array(
303 'success' => $success,
304 'head_html' => $head_html,
305 'body_html' => $body_html,
306 );
307 // Buffer all outputs, to prevent errors/warnings being printed that make the JSON invalid.
308 $output_buffer = ob_get_clean();
309 if ( ! empty( $output_buffer ) ) {
310 $response['output_buffer'] = $output_buffer;
311 }
312
313 // Send the response.
314 wp_send_json( $response );
315 }
316
317 /**
318 * Saves the screen options on the "Edit" screen when they are changed.
319 *
320 * @since 2.1.0
321 */
322 public function ajax_action_save_screen_options(): void {
323 // Check if the submitted nonce matches the generated nonce we created earlier, dies -1 on failure.
324 TablePress::check_nonce( 'screen_options', false, '_ajax_nonce', true );
325
326 if ( empty( $_POST['tablepress'] ) ) {
327 wp_die( '-1' );
328 }
329 $screen_options = wp_unslash( $_POST['tablepress'] );
330
331 // Sanitize and limit values to a minimum and a maximum.
332 $new_screen_options = array();
333
334 if ( isset( $screen_options['table_editor_column_width'] ) ) {
335 $new_screen_options['table_editor_column_width'] = absint( $screen_options['table_editor_column_width'] );
336 $new_screen_options['table_editor_column_width'] = max( $new_screen_options['table_editor_column_width'], 30 ); // Minimum width: 30 pixels.
337 $new_screen_options['table_editor_column_width'] = min( $new_screen_options['table_editor_column_width'], 9999 ); // Maximum width: 9999 pixels.
338 }
339
340 if ( isset( $screen_options['table_editor_line_clamp'] ) ) {
341 $new_screen_options['table_editor_line_clamp'] = absint( $screen_options['table_editor_line_clamp'] );
342 $new_screen_options['table_editor_line_clamp'] = min( $new_screen_options['table_editor_line_clamp'], 999 ); // Maximum lines: 999. Minimum of 0 (for all lines) is ensured by absint().
343 }
344
345 if ( empty( $new_screen_options ) ) {
346 wp_die( '-1' );
347 }
348 TablePress::$model_options->update( $new_screen_options );
349
350 // Generate the response.
351 $response = array(
352 'success' => true,
353 );
354
355 // Buffer all outputs, to prevent errors/warnings being printed that make the JSON invalid.
356 $output_buffer = ob_get_clean();
357 if ( ! empty( $output_buffer ) ) {
358 $response['output_buffer'] = $output_buffer;
359 }
360
361 // Send the response.
362 wp_send_json( $response );
363 }
364
365 } // class TablePress_Admin_AJAX_Controller
366