PluginProbe
TablePress – Tables in WordPress made easy / 2.2.4
TablePress – Tables in WordPress made easy v2.2.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.2.4, at controllers/controller-admin_ajax.php

361 lines 13.9 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 $response['table_id'] = $table['id']; // @phpstan-ignore-line
175 $response['new_edit_nonce'] = wp_create_nonce( TablePress::nonce( 'edit', $table['id'] ) ); // @phpstan-ignore-line
176 $response['new_preview_nonce'] = wp_create_nonce( TablePress::nonce( 'preview_table', $table['id'] ) ); // @phpstan-ignore-line
177 $response['last_modified'] = TablePress::format_datetime( $table['last_modified'] ); // @phpstan-ignore-line
178 $response['last_editor'] = TablePress::get_user_display_name( $table['options']['last_editor'] ); // @phpstan-ignore-line
179 }
180 if ( ! empty( $error_details ) ) {
181 $response['error_details'] = esc_html( $error_details );
182 }
183 // Buffer all outputs, to prevent errors/warnings being printed that make the JSON invalid.
184 $output_buffer = ob_get_clean();
185 if ( ! empty( $output_buffer ) ) {
186 $response['output_buffer'] = $output_buffer;
187 }
188
189 // Send the response.
190 wp_send_json( $response );
191 }
192
193 /**
194 * Returns the live preview data of table that has non-saved changes.
195 *
196 * @since 1.0.0
197 */
198 public function ajax_action_preview_table(): void {
199 if ( empty( $_POST['tablepress']['id'] ) ) {
200 wp_die( '-1' );
201 }
202
203 $preview_table = wp_unslash( $_POST['tablepress'] );
204
205 // Check if the submitted nonce matches the generated nonce we created earlier, dies -1 on failure.
206 TablePress::check_nonce( 'preview_table', $preview_table['id'], '_ajax_nonce', true );
207
208 // Ignore the request if the current user doesn't have sufficient permissions.
209 if ( ! current_user_can( 'tablepress_preview_table', $preview_table['id'] ) ) {
210 wp_die( '-1' );
211 }
212
213 // Default response data.
214 $success = false;
215 do { // To be able to "break;" (allows for better readable code).
216 // Load table, without table data, but with options and visibility settings.
217 $existing_table = TablePress::$model_table->load( $preview_table['id'], false, true );
218 if ( is_wp_error( $existing_table ) ) { // @todo Maybe somehow load a new table here? (TablePress::$model_table->get_table_template())?
219 break;
220 }
221
222 // Check and convert all data that was transmitted as valid JSON.
223 $keys = array( 'data', 'options', 'visibility' );
224 foreach ( $keys as $key ) {
225 if ( empty( $preview_table[ $key ] ) ) {
226 break 2;
227 }
228 $preview_table[ $key ] = json_decode( $preview_table[ $key ], true );
229 if ( is_null( $preview_table[ $key ] ) ) {
230 break 2;
231 }
232 $preview_table[ $key ] = (array) $preview_table[ $key ]; // Cast to array again, to catch strings, etc.
233 }
234
235 // Check consistency of new table, and then merge with existing table.
236 $table = TablePress::$model_table->prepare_table( $existing_table, $preview_table, true );
237 if ( is_wp_error( $table ) ) {
238 break;
239 }
240
241 // DataTables Custom Commands can only be edited by trusted users.
242 if ( ! current_user_can( 'unfiltered_html' ) ) {
243 $table['options']['datatables_custom_commands'] = $existing_table['options']['datatables_custom_commands'];
244 }
245
246 // If the ID has changed, and the new ID is valid, render with the new ID (important e.g. for CSS classes/HTML ID).
247 if ( $table['id'] !== $table['new_id'] && 0 === preg_match( '/[^a-zA-Z0-9_-]/', $table['new_id'] ) ) {
248 $table['id'] = $table['new_id'];
249 }
250
251 // Sanitize all table data to remove unsafe HTML from the preview output, if the user is not allowed to work with unfiltered HTML.
252 if ( ! current_user_can( 'unfiltered_html' ) ) {
253 $table = TablePress::$model_table->sanitize( $table );
254 }
255
256 // At this point, the table data is valid and sanitized and can be rendered.
257 $success = true;
258 } while ( false ); // Do-while-loop through this exactly once, to be able to "break;" early. // @phpstan-ignore-line .
259
260 if ( $success ) {
261 // Create a render class instance.
262 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
263 // Merge desired options with default render options (see TablePress_Controller_Frontend::shortcode_table()).
264 $default_render_options = $_render->get_default_render_options();
265 /** This filter is documented in controllers/controller-frontend.php */
266 $default_render_options = apply_filters( 'tablepress_shortcode_table_default_shortcode_atts', $default_render_options );
267 $render_options = shortcode_atts( $default_render_options, $table['options'] ); // @phpstan-ignore-line
268 /** This filter is documented in controllers/controller-frontend.php */
269 $render_options = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $render_options );
270 $render_options['html_id'] = "tablepress-{$table['id']}"; // @phpstan-ignore-line
271 $_render->set_input( $table, $render_options ); // @phpstan-ignore-line
272 $head_html = $_render->get_preview_css();
273 $custom_css = TablePress::$model_options->get( 'custom_css' );
274 $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
275 if ( $use_custom_css ) {
276 $head_html .= "<style>\n{$custom_css}\n</style>\n";
277 }
278
279 $body_html = '<div id="tablepress-page"><p>'
280 . __( 'This is a preview of your table.', 'tablepress' ) . ' '
281 . __( 'Because of CSS styling in your theme, the table might look different on your page!', 'tablepress' ) . ' '
282 . __( 'The Table Features for Site Visitors, like sorting, filtering, and pagination, are also not available in this preview!', 'tablepress' ) . '<br />';
283 // Show the instructions string depending on whether the Block Editor is used on the site or not.
284 if ( TablePress::site_uses_block_editor() ) {
285 $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' ) );
286 } else {
287 $body_html .= __( 'To insert a table into a post or page, paste its Shortcode at the desired place in the editor.', 'tablepress' ) . ' '
288 . __( 'Each table has a unique ID that needs to be adjusted in that Shortcode.', 'tablepress' );
289 }
290 $body_html .= '</p>' . $_render->get_output() . '</div>';
291 } else {
292 $head_html = '';
293 $body_html = __( 'The preview could not be loaded.', 'tablepress' );
294 }
295
296 // Generate the response.
297 $response = array(
298 'success' => $success,
299 'head_html' => $head_html,
300 'body_html' => $body_html,
301 );
302 // Buffer all outputs, to prevent errors/warnings being printed that make the JSON invalid.
303 $output_buffer = ob_get_clean();
304 if ( ! empty( $output_buffer ) ) {
305 $response['output_buffer'] = $output_buffer;
306 }
307
308 // Send the response.
309 wp_send_json( $response );
310 }
311
312 /**
313 * Saves the screen options on the "Edit" screen when they are changed.
314 *
315 * @since 2.1.0
316 */
317 public function ajax_action_save_screen_options(): void {
318 // Check if the submitted nonce matches the generated nonce we created earlier, dies -1 on failure.
319 TablePress::check_nonce( 'screen_options', false, '_ajax_nonce', true );
320
321 if ( empty( $_POST['tablepress'] ) ) {
322 wp_die( '-1' );
323 }
324 $screen_options = wp_unslash( $_POST['tablepress'] );
325
326 // Sanitize and limit values to a minimum and a maximum.
327 $new_screen_options = array();
328
329 if ( isset( $screen_options['table_editor_column_width'] ) ) {
330 $new_screen_options['table_editor_column_width'] = absint( $screen_options['table_editor_column_width'] );
331 $new_screen_options['table_editor_column_width'] = max( $new_screen_options['table_editor_column_width'], 30 ); // Minimum width: 30 pixels.
332 $new_screen_options['table_editor_column_width'] = min( $new_screen_options['table_editor_column_width'], 9999 ); // Maximum width: 9999 pixels.
333 }
334
335 if ( isset( $screen_options['table_editor_line_clamp'] ) ) {
336 $new_screen_options['table_editor_line_clamp'] = absint( $screen_options['table_editor_line_clamp'] );
337 $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().
338 }
339
340 if ( empty( $new_screen_options ) ) {
341 wp_die( '-1' );
342 }
343 TablePress::$model_options->update( $new_screen_options );
344
345 // Generate the response.
346 $response = array(
347 'success' => true,
348 );
349
350 // Buffer all outputs, to prevent errors/warnings being printed that make the JSON invalid.
351 $output_buffer = ob_get_clean();
352 if ( ! empty( $output_buffer ) ) {
353 $response['output_buffer'] = $output_buffer;
354 }
355
356 // Send the response.
357 wp_send_json( $response );
358 }
359
360 } // class TablePress_Admin_AJAX_Controller
361