PluginProbe
Booking Calendar / 11.0
Booking Calendar v11.0
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 10.11.2 All 203 releases
booking / includes / save-load-option / save-load-option.php

save-load-option.php in Booking Calendar 11.0, at includes/save-load-option/save-load-option.php

526 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * General Option Loader/Saver (AJAX)
4 *
5 * - Save complex structures by posting RAW JSON (string) -> json_decode() -> array stored via update_option().
6 * - Save simple scalars (e.g., "On"/"Off") as-is.
7 * - Load returns stored value (array/scalar).
8 * - Enqueues small JS/CSS that provide generic save/load helpers with busy (spinner) UI.
9 *
10 * file: ../includes/save-load-option/save-load-option.php
11 *
12 * Data attributes on clickable elements:
13 * Save:
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)
17 * data-wpbc-u-save-value — RAW scalar to save (optional)
18 * data-wpbc-u-save-value-json— JSON string to save (optional)
19 * data-wpbc-u-save-fields — CSV of selectors; values serialized with jQuery.param (optional)
20 * data-wpbc-u-busy-text — custom text during AJAX (optional)
21 * data-wpbc-u-save-callback — window function name to call on success (optional)
22 *
23 * Load:
24 * data-wpbc-u-load-name — option key (required)
25 * data-wpbc-u-busy-text — custom text during AJAX (optional)
26 * data-wpbc-u-load-callback — window function name to receive loaded value (optional)
27 *
28 * JS Events:
29 * jQuery(document)
30 * .on('wpbc:option:beforeSave', function (e, $el, payload) {})
31 * .on('wpbc:option:afterSave', function (e, response) {})
32 * .on('wpbc:option:beforeLoad', function (e, $el, name) {})
33 * .on('wpbc:option:afterLoad', function (e, response) {})
34 *
35 * @package Booking Calendar
36 * @author wpdevelop
37 * @since 11.0.0
38 * @version 1.0.1
39 */
40
41 if ( ! defined( 'ABSPATH' ) ) {
42 exit;
43 }
44
45 class wpbc_option_saver_loader {
46
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();
52
53 public static function init() {
54 add_action( 'init', array( __CLASS__, 'register_ajax_handlers' ) );
55 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
56 }
57
58 /**
59 * Register an option-specific save policy.
60 *
61 * Supported policy keys:
62 * - can_save callable(): bool
63 * - permission_message string
64 * - normalize_raw callable( mixed $data_raw, string $data_name ): mixed
65 * - force_mode string
66 * - allowed_keys array|callable(): array
67 * - normalize_item callable( string $option_key, mixed $value, string $data_name ): mixed
68 *
69 * @param string $option_name Option name from data_name.
70 * @param array $policy Policy definition.
71 *
72 * @return void
73 */
74 public static function register_option_policy( $option_name, $policy ) {
75
76 $option_name = sanitize_key( (string) $option_name );
77
78 if ( empty( $option_name ) || ! is_array( $policy ) ) {
79 return;
80 }
81
82 self::$save_policies[ $option_name ] = $policy;
83 }
84
85 /**
86 * Get a registered option save policy.
87 *
88 * @param string $option_name Option name from data_name.
89 *
90 * @return array
91 */
92 private static function get_option_policy( $option_name ) {
93
94 $option_name = sanitize_key( (string) $option_name );
95
96 return ( isset( self::$save_policies[ $option_name ] ) && is_array( self::$save_policies[ $option_name ] ) )
97 ? self::$save_policies[ $option_name ]
98 : array();
99 }
100
101 /**
102 * Get allowed option keys from a policy.
103 *
104 * @param array $policy Policy definition.
105 *
106 * @return array
107 */
108 private static function get_policy_allowed_keys( $policy ) {
109
110 if ( empty( $policy['allowed_keys'] ) ) {
111 return array();
112 }
113
114 $allowed_keys = is_callable( $policy['allowed_keys'] )
115 ? call_user_func( $policy['allowed_keys'] )
116 : $policy['allowed_keys'];
117
118 if ( ! is_array( $allowed_keys ) ) {
119 return array();
120 }
121
122 return array_values(
123 array_filter(
124 array_map(
125 'sanitize_key',
126 array_map( 'strval', $allowed_keys )
127 )
128 )
129 );
130 }
131
132 /**
133 * Register AJAX handlers (logged-in admin).
134 *
135 * @return void
136 */
137 public static function register_ajax_handlers() {
138 add_action( 'wp_ajax_' . self::$ajax_action_save, array( __CLASS__, 'handle_ajax_save' ) );
139 add_action( 'wp_ajax_' . self::$ajax_action_load, array( __CLASS__, 'handle_ajax_load' ) );
140 }
141
142 /**
143 * Enqueue JS/CSS for admin pages.
144 *
145 * @return void
146 */
147 public static function enqueue_assets() {
148
149 // Optional screen check.
150 if ( function_exists( 'get_current_screen' ) ) {
151 $screen = get_current_screen();
152 $ok = apply_filters( 'wpbc_option_saver_loader_enqueue', true, $screen );
153 if ( ! $ok ) {
154 return;
155 }
156 }
157
158 $base_url = plugins_url( '', defined( 'WPBC_FILE' ) ? WPBC_FILE : __FILE__ );
159 $js_url = $base_url . '/includes/save-load-option/_out/save-load-option.js';
160 $css_url = $base_url . '/includes/save-load-option/_out/save-load-option.css';
161
162 wp_register_style( 'wpbc-save-load-option', $css_url, array(), self::$asset_version );
163 wp_enqueue_style( 'wpbc-save-load-option' );
164
165 wp_register_script( 'wpbc-save-load-option', $js_url, array( 'jquery' ), self::$asset_version, true );
166 wp_enqueue_script( 'wpbc-save-load-option' );
167
168 wp_localize_script(
169 'wpbc-save-load-option',
170 '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 )
176 );
177 }
178
179 /**
180 * AJAX: Save option.
181 *
182 * Expected POST:
183 * - data_name string Option key.
184 * - data_value string RAW scalar | query-string | JSON string.
185 * - nonce_action string Nonce action name.
186 * - nonce string Nonce value.
187 *
188 * @return void
189 */
190 public static function handle_ajax_save() {
191
192 $capability = apply_filters( 'wpbc_option_saver_loader_cap_save', ( function_exists( 'wpbc_bfb_get_manage_cap' ) ) ? wpbc_bfb_get_manage_cap() : 'manage_options' );
193 if ( ! current_user_can( $capability ) ) {
194 wp_send_json_error( array( 'message' => __( 'You do not have permission to save settings.', 'booking' ) ) );
195 }
196
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'] ) ) : '';
203
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'] ) ) : '';
206
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 $save_policy = self::get_option_policy( $data_name );
216
217 if ( ! empty( $save_policy['can_save'] ) && is_callable( $save_policy['can_save'] ) && ! call_user_func( $save_policy['can_save'], $data_name ) ) {
218 $permission_message = ( ! empty( $save_policy['permission_message'] ) && is_string( $save_policy['permission_message'] ) )
219 ? $save_policy['permission_message']
220 : __( 'You do not have permission to save this option.', 'booking' );
221 wp_send_json_error( array( 'message' => $permission_message ) );
222 }
223
224 if ( ! empty( $save_policy['normalize_raw'] ) && is_callable( $save_policy['normalize_raw'] ) ) {
225 $data_raw = call_user_func( $save_policy['normalize_raw'], $data_raw, $data_name );
226 }
227
228 if ( ! empty( $save_policy['force_mode'] ) ) {
229 $data_mode = sanitize_key( (string) $save_policy['force_mode'] );
230 }
231
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 $value_to_store = self::normalize_incoming_value( $data_raw );
238
239 // Split mode: JSON object => multiple options saved separately.
240 if ( 'split' === $data_mode && is_array( $value_to_store ) ) {
241
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 foreach ( $value_to_store as $k => $v ) {
256
257 if ( ! is_scalar( $k ) ) {
258 continue;
259 }
260
261 $opt_key = sanitize_key( (string) $k );
262 if ( '' === $opt_key ) {
263 continue;
264 }
265
266 // If allowlist provided, only save those keys.
267 if ( ! empty( $allowed_keys ) && ! isset( $allowed_keys[ $opt_key ] ) ) {
268 continue;
269 }
270
271 // Values: allow scalar or arrays (already sanitized by normalize_incoming_value()).
272 $opt_val = $v;
273 if ( is_scalar( $opt_val ) ) {
274 $opt_val = sanitize_text_field( (string) $opt_val );
275 } elseif ( is_array( $opt_val ) ) {
276 $opt_val = self::sanitize_mixed_value( $opt_val );
277 } else {
278 $opt_val = '';
279 }
280
281 if ( ! empty( $save_policy['normalize_item'] ) && is_callable( $save_policy['normalize_item'] ) ) {
282 $opt_val = call_user_func( $save_policy['normalize_item'], $opt_key, $opt_val, $data_name );
283 }
284
285 self::update_option( self::$option_prefix . $opt_key, $opt_val );
286 $saved[ $opt_key ] = $opt_val;
287 }
288
289 if ( empty( $saved ) ) {
290 wp_send_json_error( array( 'message' => __( 'Nothing to save.', 'booking' ) ) );
291 }
292
293 wp_send_json_success(
294 array(
295 'message' => __( 'Settings saved.', 'booking' ),
296 'value' => $saved,
297 'mode' => 'split',
298 )
299 );
300 }
301
302 // Default: store as a single option (scalar/array).
303 self::update_option( self::$option_prefix . $data_name, $value_to_store );
304
305 // Return stored value (useful for client callbacks / UI sync).
306 wp_send_json_success(
307 array(
308 'message' => __( 'Settings saved.', 'booking' ),
309 'value' => $value_to_store,
310 )
311 );
312 }
313
314 /**
315 * AJAX: Load option.
316 *
317 * Expected GET:
318 * - data_name string Option key.
319 *
320 * @return void
321 */
322 public static function handle_ajax_load() {
323
324 $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;
336 $value = self::get_option( $option_key, array() );
337
338 wp_send_json_success( array( 'value' => $value ) );
339 }
340
341 /**
342 * Normalize payload: prefer JSON -> array; fallback to query-string -> array; else scalar string.
343 *
344 * @param string $data_raw Raw input.
345 * @return mixed
346 */
347 private static function normalize_incoming_value( $data_raw ) {
348
349 if ( ! is_string( $data_raw ) || '' === $data_raw ) {
350 return '';
351 }
352
353 $maybe_json = trim( $data_raw );
354
355 // JSON path.
356 if (
357 0 === strpos( $maybe_json, '{' ) || 0 === strpos( $maybe_json, '[' ) ||
358 'null' === strtolower( $maybe_json ) || 'true' === strtolower( $maybe_json ) ||
359 'false' === strtolower( $maybe_json ) || is_numeric( $maybe_json )
360 ) {
361 $decoded = json_decode( $maybe_json, true );
362 if ( null !== $decoded && JSON_ERROR_NONE === json_last_error() ) {
363 return self::sanitize_mixed_value( $decoded );
364 }
365 }
366
367 // Query-string path.
368 if ( false !== strpos( $data_raw, '=' ) || false !== strpos( $data_raw, '&' ) || false !== strpos( strtolower( $data_raw ), '%5b' ) ) {
369 $parsed = array();
370 parse_str( $data_raw, $parsed ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
371 return self::sanitize_kv_array_preserve_brackets( $parsed );
372 }
373
374 // Scalar.
375 return sanitize_text_field( $data_raw );
376 }
377
378 /**
379 * Recursively sanitize mixed values.
380 *
381 * @param mixed $value Mixed value.
382 * @return mixed
383 */
384 private static function sanitize_mixed_value( $value ) {
385
386 if ( is_array( $value ) ) {
387 $out = array();
388 foreach ( $value as $k => $v ) {
389 $kk = is_string( $k ) ? preg_replace( '/[^a-zA-Z0-9_\-\[\]]/', '', $k ) : $k;
390 $out[ $kk ] = self::sanitize_mixed_value( $v );
391 }
392 return $out;
393 }
394
395 if ( is_scalar( $value ) ) {
396 return sanitize_text_field( (string) $value );
397 }
398
399 return '';
400 }
401
402 /**
403 * Sanitize arrays parsed from query-string, preserving bracket keys.
404 *
405 * @param array $parsed_data Parsed data.
406 * @return array
407 */
408 private static function sanitize_kv_array_preserve_brackets( $parsed_data ) {
409
410 $sanitized_data = array();
411
412 if ( empty( $parsed_data ) || ! is_array( $parsed_data ) ) {
413 return $sanitized_data;
414 }
415
416 foreach ( $parsed_data as $key => $val ) {
417 $key = preg_replace( '/[^a-zA-Z0-9_\-\[\]]/', '', (string) $key );
418 if ( is_array( $val ) ) {
419 $sanitized_data[ $key ] = self::sanitize_mixed_value( $val );
420 } else {
421 $sanitized_data[ $key ] = sanitize_text_field( $val );
422 }
423 }
424
425 return $sanitized_data;
426 }
427
428 /**
429 * Update option (Booking Calendar wrapper if present).
430 *
431 * @param string $option_key Key.
432 * @param mixed $value Value.
433 * @return void
434 */
435 private static function update_option( $option_key, $value ) {
436 if ( function_exists( 'update_bk_option' ) ) {
437 update_bk_option( $option_key, $value );
438 } else {
439 update_option( $option_key, $value );
440 }
441 }
442
443 /**
444 * Get option (Booking Calendar wrapper if present).
445 *
446 * @param string $option_key Key.
447 * @param mixed $default Default.
448 * @return mixed
449 */
450 private static function get_option( $option_key, $default = false ) {
451 if ( function_exists( 'get_bk_option' ) ) {
452 $val = get_bk_option( $option_key );
453 return ( null === $val ) ? $default : $val;
454 }
455 return get_option( $option_key, $default );
456 }
457 }
458
459 require_once __DIR__ . '/option-save-policies.php';
460
461 add_action( 'plugins_loaded', array( 'wpbc_option_saver_loader', 'init' ) );
462
463
464 /**
465 * == Usage examples ==
466 *
467 * 1) Save RAW scalar (On/Off).
468 *
469
470 <?php
471 $opt_name = 'booking_timeslot_picker';
472 $nonce_action = 'wpbc_nonce_' . $opt_name;
473 ?>
474 <a href="javascript:void(0);"
475 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' ); ?>">
481 <?php esc_html_e( 'Save Toggle', 'booking' ); ?>
482 </a>
483
484 *
485 * 2) Save complex structure (RAW JSON)
486 *
487
488 <?php
489 $opt_name = 'wpbc_bfb_form_structure';
490 $nonce_action = 'wpbc_nonce_' . $opt_name;
491 ?>
492 <a href="javascript:void(0);"
493 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' ); ?>">
499 <?php esc_html_e( 'Save Form Structure', 'booking' ); ?>
500 </a>
501
502 *
503 * 3) Load option and apply
504 *
505
506 <a href="javascript:void(0);"
507 class="button"
508 onclick="wpbc_load_option_from_element(this)"
509 data-wpbc-u-load-name="wpbc_bfb_form_structure"
510 data-wpbc-u-load-callback="wpbc_bfb__on_structure_loaded"
511 data-wpbc-u-busy-text="<?php esc_attr_e( 'Loading…', 'booking' ); ?>">
512 <?php esc_html_e( 'Load Form Structure', 'booking' ); ?>
513 </a>
514 <script>
515 function wpbc_bfb__on_structure_loaded(val){
516 try {
517 if ( typeof val === 'string' ) { val = JSON.parse(val); }
518 if ( window.wpbc_bfb && typeof window.wpbc_bfb.load_saved_structure === 'function' ) {
519 window.wpbc_bfb.load_saved_structure( val || [] );
520 }
521 } catch(e){ console.error(e); }
522 }
523 </script>
524 *
525 */
526