PluginProbe
Booking Calendar / 11.8.4
Booking Calendar v11.8.4
11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 10.11 All 204 releases
← All changes | includes/save-load-option/save-load-option.php +116 -91 11.511.8.4 View file →
@@ -1,4 +1,4 @@
1 1 <?php
2 2 /**
3 3 * General Option Loader/Saver (AJAX)
4 4 *
@@ -11,10 +11,9 @@
11 11 *
12 12 * Data attributes on clickable elements:
13 13 * Save:
14 14 * data-wpbc-u-save-name — option key (required)
15 - * data-wpbc-u-save-nonce — nonce value (required for SAVE)
16 - * data-wpbc-u-save-action — nonce action (required for SAVE)
15 + * SAVE requests use a fixed, server-generated nonce localized with this module.
17 16 * data-wpbc-u-save-value — RAW scalar to save (optional)
18 17 * data-wpbc-u-save-value-json— JSON string to save (optional)
19 18 * data-wpbc-u-save-fields — CSV of selectors; values serialized with jQuery.param (optional)
20 19 * data-wpbc-u-busy-text — custom text during AJAX (optional)
@@ -34,9 +33,9 @@
34 33 *
35 34 * @package Booking Calendar
36 35 * @author wpdevelop
37 36 * @since 11.0.0
38 - * @version 1.0.1
37 + * @version 1.0.2
39 38 */
40 39
41 40 if ( ! defined( 'ABSPATH' ) ) {
42 41 exit;
@@ -43,13 +42,15 @@
43 42 }
44 43
45 44 class wpbc_option_saver_loader {
46 45
47 - private static $ajax_action_save = 'wpbc_ajax_option_save';
48 - private static $ajax_action_load = 'wpbc_ajax_option_load';
49 - private static $option_prefix = '';
50 - private static $asset_version = '1.0.1';
51 - private static $save_policies = array();
46 + private static $ajax_action_save = 'wpbc_ajax_option_save';
47 + private static $ajax_action_load = 'wpbc_ajax_option_load';
48 + private static $nonce_action_save = 'wpbc_option_save';
49 + private static $nonce_action_load = 'wpbc_option_load';
50 + private static $option_prefix = '';
51 + private static $asset_version = '1.0.2';
52 + private static $save_policies = array();
52 53
53 54 public static function init() {
54 55 add_action( 'init', array( __CLASS__, 'register_ajax_handlers' ) );
55 56 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
@@ -55,9 +56,12 @@
55 56 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
56 57 }
57 58
58 59 /**
59 - * Register an option-specific save policy.
60 + * Register an option-specific save and load policy.
61 + *
62 + * Registration is the endpoint allowlist. Unregistered option names are
63 + * rejected even when the current user has the configured capability.
60 64 *
61 65 * Supported policy keys:
62 66 * - can_save callable(): bool
63 67 * - permission_message string
@@ -88,16 +92,35 @@
88 92 * @param string $option_name Option name from data_name.
89 93 *
90 94 * @return array
91 95 */
92 - private static function get_option_policy( $option_name ) {
96 + private static function get_option_policy( $option_name ) {
93 97
94 98 $option_name = sanitize_key( (string) $option_name );
95 99
96 100 return ( isset( self::$save_policies[ $option_name ] ) && is_array( self::$save_policies[ $option_name ] ) )
97 101 ? self::$save_policies[ $option_name ]
98 - : array();
99 - }
102 + : array();
103 + }
104 +
105 + /**
106 + * Check whether an option name was explicitly registered for this endpoint.
107 + *
108 + * Registration is the writable and readable option allowlist. Sanitizing an
109 + * arbitrary WordPress option name does not make that option safe to expose.
110 + *
111 + * @param string $option_name Option name from data_name.
112 + *
113 + * @return bool True when a policy was explicitly registered.
114 + */
115 + private static function has_option_policy( $option_name ) {
116 +
117 + $option_name = sanitize_key( (string) $option_name );
118 +
119 + return '' !== $option_name
120 + && isset( self::$save_policies[ $option_name ] )
121 + && is_array( self::$save_policies[ $option_name ] );
122 + }
100 123
101 124 /**
102 125 * Get allowed option keys from a policy.
103 126 *
@@ -167,13 +190,15 @@
167 190
168 191 wp_localize_script(
169 192 'wpbc-save-load-option',
170 193 'wpbc_option_saver_loader_config',
171 - array(
172 - 'ajax_url' => admin_url( 'admin-ajax.php' ),
173 - 'action_save' => self::$ajax_action_save,
174 - 'action_load' => self::$ajax_action_load,
175 - )
194 + array(
195 + 'ajax_url' => admin_url( 'admin-ajax.php' ),
196 + 'action_save' => self::$ajax_action_save,
197 + 'action_load' => self::$ajax_action_load,
198 + 'save_nonce' => wp_create_nonce( self::$nonce_action_save ),
199 + 'load_nonce' => wp_create_nonce( self::$nonce_action_load ),
200 + )
176 201 );
177 202 }
178 203
179 204 /**
@@ -181,10 +206,9 @@
181 206 *
182 207 * Expected POST:
183 208 * - data_name string Option key.
184 209 * - data_value string RAW scalar | query-string | JSON string.
185 - * - nonce_action string Nonce action name.
186 - * - nonce string Nonce value.
210 + * - nonce string Nonce for the fixed wpbc_option_save action.
187 211 *
188 212 * @return void
189 213 */
190 214 public static function handle_ajax_save() {
@@ -193,26 +217,25 @@
193 217 if ( ! current_user_can( $capability ) ) {
194 218 wp_send_json_error( array( 'message' => __( 'You do not have permission to save settings.', 'booking' ) ) );
195 219 }
196 220
197 - $data_name = isset( $_POST['data_name'] ) ? sanitize_key( wp_unslash( $_POST['data_name'] ) ) : '';
198 - /* phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing */
199 - $data_raw = isset( $_POST['data_value'] ) ? wp_unslash( $_POST['data_value'] ) : '';
200 - // Optional: split JSON object into multiple options.
201 - $data_mode = isset( $_POST['data_mode'] ) ? sanitize_key( wp_unslash( $_POST['data_mode'] ) ) : '';
202 - $data_fields = isset( $_POST['data_fields'] ) ? sanitize_text_field( wp_unslash( $_POST['data_fields'] ) ) : '';
221 + $data_name = isset( $_POST['data_name'] ) ? sanitize_key( wp_unslash( $_POST['data_name'] ) ) : '';
222 + /* phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing */
223 + $data_raw = isset( $_POST['data_value'] ) ? wp_unslash( $_POST['data_value'] ) : '';
224 + $nonce_value = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
225 +
226 + if ( ! wp_verify_nonce( $nonce_value, self::$nonce_action_save ) ) {
227 + wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'booking' ) ) );
228 + }
203 229
204 - $nonce_name = isset( $_POST['nonce_action'] ) ? sanitize_key( wp_unslash( $_POST['nonce_action'] ) ) : '';
205 - $nonce_value = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
230 + if ( empty( $data_name ) ) {
231 + wp_send_json_error( array( 'message' => __( 'Missing data name.', 'booking' ) ) );
232 + }
233 +
234 + if ( ! self::has_option_policy( $data_name ) ) {
235 + wp_send_json_error( array( 'message' => __( 'This option cannot be saved by this request.', 'booking' ) ) );
236 + }
206 237
207 - if ( empty( $nonce_name ) || ! wp_verify_nonce( $nonce_value, $nonce_name ) ) {
208 - wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'booking' ) ) );
209 - }
210 -
211 - if ( empty( $data_name ) ) {
212 - wp_send_json_error( array( 'message' => __( 'Missing data name.', 'booking' ) ) );
213 - }
214 -
215 238 $save_policy = self::get_option_policy( $data_name );
216 239
217 240 if ( ! empty( $save_policy['can_save'] ) && is_callable( $save_policy['can_save'] ) && ! call_user_func( $save_policy['can_save'], $data_name ) ) {
218 241 $permission_message = ( ! empty( $save_policy['permission_message'] ) && is_string( $save_policy['permission_message'] ) )
@@ -224,35 +247,27 @@
224 247 if ( ! empty( $save_policy['normalize_raw'] ) && is_callable( $save_policy['normalize_raw'] ) ) {
225 248 $data_raw = call_user_func( $save_policy['normalize_raw'], $data_raw, $data_name );
226 249 }
227 250
228 - if ( ! empty( $save_policy['force_mode'] ) ) {
229 - $data_mode = sanitize_key( (string) $save_policy['force_mode'] );
230 - }
251 + $data_mode = ( ! empty( $save_policy['force_mode'] ) && 'split' === sanitize_key( (string) $save_policy['force_mode'] ) ) ? 'split' : '';
252 +
253 + $policy_allowed_keys = self::get_policy_allowed_keys( $save_policy );
254 + if ( 'split' === $data_mode && empty( $policy_allowed_keys ) ) {
255 + wp_send_json_error( array( 'message' => __( 'This option does not define any writable fields.', 'booking' ) ) );
256 + }
231 257
232 - $policy_allowed_keys = self::get_policy_allowed_keys( $save_policy );
233 - if ( ! empty( $policy_allowed_keys ) ) {
234 - $data_fields = implode( ',', $policy_allowed_keys );
235 - }
236 -
237 258 $value_to_store = self::normalize_incoming_value( $data_raw );
238 259
239 260 // Split mode: JSON object => multiple options saved separately.
240 - if ( 'split' === $data_mode && is_array( $value_to_store ) ) {
261 + if ( 'split' === $data_mode ) {
262 +
263 + if ( ! is_array( $value_to_store ) ) {
264 + wp_send_json_error( array( 'message' => __( 'Invalid option data.', 'booking' ) ) );
265 + }
266 +
267 + $allowed_keys = array_fill_keys( $policy_allowed_keys, true );
268 + $saved = array();
241 269
242 - $allowed_keys = array();
243 - if ( '' !== trim( $data_fields ) ) {
244 - $parts = explode( ',', (string) $data_fields );
245 - foreach ( $parts as $p ) {
246 - $k = sanitize_key( trim( (string) $p ) );
247 - if ( '' !== $k ) {
248 - $allowed_keys[ $k ] = true;
249 - }
250 - }
251 - }
252 -
253 - $saved = array();
254 -
255 270 foreach ( $value_to_store as $k => $v ) {
256 271
257 272 if ( ! is_scalar( $k ) ) {
258 273 continue;
@@ -262,10 +277,9 @@
262 277 if ( '' === $opt_key ) {
263 278 continue;
264 279 }
265 280
266 - // If allowlist provided, only save those keys.
267 - if ( ! empty( $allowed_keys ) && ! isset( $allowed_keys[ $opt_key ] ) ) {
281 + if ( ! isset( $allowed_keys[ $opt_key ] ) ) {
268 282 continue;
269 283 }
270 284
271 285 // Values: allow scalar or arrays (already sanitized by normalize_incoming_value()).
@@ -313,10 +327,11 @@
313 327
314 328 /**
315 329 * AJAX: Load option.
316 330 *
317 - * Expected GET:
318 - * - data_name string Option key.
331 + * Expected GET:
332 + * - data_name string Option key.
333 + * - nonce string Nonce for the fixed wpbc_option_load action.
319 334 *
320 335 * @return void
321 336 */
322 337 public static function handle_ajax_load() {
@@ -321,19 +336,32 @@
321 336 */
322 337 public static function handle_ajax_load() {
323 338
324 339 $capability = apply_filters( 'wpbc_option_saver_loader_cap_load', ( function_exists( 'wpbc_bfb_get_manage_cap' ) ) ? wpbc_bfb_get_manage_cap() : 'manage_options' );
325 - if ( ! current_user_can( $capability ) ) {
326 - wp_send_json_error( array( 'message' => __( 'You do not have permission to load settings.', 'booking' ) ) );
327 - }
328 -
329 - /* phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing */
330 - $data_name = isset( $_GET['data_name'] ) ? sanitize_key( wp_unslash( $_GET['data_name'] ) ) : '';
331 - if ( empty( $data_name ) ) {
332 - wp_send_json_error( array( 'message' => __( 'Missing data name.', 'booking' ) ) );
333 - }
334 -
335 - $option_key = self::$option_prefix . $data_name;
340 + if ( ! current_user_can( $capability ) ) {
341 + wp_send_json_error( array( 'message' => __( 'You do not have permission to load settings.', 'booking' ) ) );
342 + }
343 +
344 + $nonce_value = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : '';
345 + if ( ! wp_verify_nonce( $nonce_value, self::$nonce_action_load ) ) {
346 + wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'booking' ) ) );
347 + }
348 +
349 + $data_name = isset( $_GET['data_name'] ) ? sanitize_key( wp_unslash( $_GET['data_name'] ) ) : '';
350 + if ( empty( $data_name ) ) {
351 + wp_send_json_error( array( 'message' => __( 'Missing data name.', 'booking' ) ) );
352 + }
353 +
354 + if ( ! self::has_option_policy( $data_name ) ) {
355 + wp_send_json_error( array( 'message' => __( 'This option cannot be loaded by this request.', 'booking' ) ) );
356 + }
357 +
358 + $load_policy = self::get_option_policy( $data_name );
359 + if ( ! empty( $load_policy['can_save'] ) && is_callable( $load_policy['can_save'] ) && ! call_user_func( $load_policy['can_save'], $data_name ) ) {
360 + wp_send_json_error( array( 'message' => __( 'You do not have permission to load this option.', 'booking' ) ) );
361 + }
362 +
363 + $option_key = self::$option_prefix . $data_name;
336 364 $value = self::get_option( $option_key, array() );
337 365
338 366 wp_send_json_success( array( 'value' => $value ) );
339 367 }
@@ -466,37 +494,34 @@
466 494 *
467 495 * 1) Save RAW scalar (On/Off).
468 496 *
469 497
470 -<?php
471 -$opt_name = 'booking_timeslot_picker';
472 -$nonce_action = 'wpbc_nonce_' . $opt_name;
473 -?>
498 +<?php
499 +$opt_name = 'booking_timeslot_picker';
500 +?>
474 501 <a href="javascript:void(0);"
475 502 class="button button-secondary"
476 - onclick="(function(btn){var $=jQuery, $chk=$('.js-toggle-timeslot-picker').first(); $(btn).data('wpbc-u-save-value',$chk.is(':checked')?'On':'Off'); wpbc_save_option_from_element(btn);})(this)"
477 - data-wpbc-u-save-name="<?php echo esc_attr( $opt_name ); ?>"
478 - data-wpbc-u-save-nonce="<?php echo esc_attr( wp_create_nonce( $nonce_action ) ); ?>"
479 - data-wpbc-u-save-action="<?php echo esc_attr( $nonce_action ); ?>"
480 - data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving…', 'booking' ); ?>">
503 + onclick="(function(btn){var $=jQuery, $chk=$('.js-toggle-timeslot-picker').first(); $(btn).data('wpbc-u-save-value',$chk.is(':checked')?'On':'Off'); wpbc_save_option_from_element(btn);})(this)"
504 + data-wpbc-u-save-name="<?php echo esc_attr( $opt_name ); ?>"
505 + data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving…', 'booking' ); ?>">
481 506 <?php esc_html_e( 'Save Toggle', 'booking' ); ?>
482 507 </a>
483 508
484 509 *
485 - * 2) Save complex structure (RAW JSON)
510 + * 2) Save complex structure (RAW JSON).
511 + *
512 + * Register an exact server-side policy for wpbc_bfb_form_structure before
513 + * rendering this control. Client attributes never register writable options.
486 514 *
487 515
488 -<?php
489 -$opt_name = 'wpbc_bfb_form_structure';
490 -$nonce_action = 'wpbc_nonce_' . $opt_name;
491 -?>
516 +<?php
517 +$opt_name = 'wpbc_bfb_form_structure';
518 +?>
492 519 <a href="javascript:void(0);"
493 520 class="button button-primary"
494 - onclick="(function(btn){var s=window.wpbc_bfb && window.wpbc_bfb.get_structure ? window.wpbc_bfb.get_structure() : []; jQuery(btn).data('wpbc-u-save-value-json', JSON.stringify(s)); wpbc_save_option_from_element(btn);})(this)"
495 - data-wpbc-u-save-name="<?php echo esc_attr( $opt_name ); ?>"
496 - data-wpbc-u-save-nonce="<?php echo esc_attr( wp_create_nonce( $nonce_action ) ); ?>"
497 - data-wpbc-u-save-action="<?php echo esc_attr( $nonce_action ) ; ?>"
498 - data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving…', 'booking' ); ?>">
521 + onclick="(function(btn){var s=window.wpbc_bfb && window.wpbc_bfb.get_structure ? window.wpbc_bfb.get_structure() : []; jQuery(btn).data('wpbc-u-save-value-json', JSON.stringify(s)); wpbc_save_option_from_element(btn);})(this)"
522 + data-wpbc-u-save-name="<?php echo esc_attr( $opt_name ); ?>"
523 + data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving…', 'booking' ); ?>">
499 524 <?php esc_html_e( 'Save Form Structure', 'booking' ); ?>
500 525 </a>
501 526
502 527 *