PluginProbe
Booking Calendar / 10.15.6
Booking Calendar v10.15.6
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 10.15.6, at includes/save-load-option/save-load-option.php

422 lines 14.0 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
52 public static function init() {
53 add_action( 'init', array( __CLASS__, 'register_ajax_handlers' ) );
54 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
55 }
56
57 /**
58 * Register AJAX handlers (logged-in admin).
59 *
60 * @return void
61 */
62 public static function register_ajax_handlers() {
63 add_action( 'wp_ajax_' . self::$ajax_action_save, array( __CLASS__, 'handle_ajax_save' ) );
64 add_action( 'wp_ajax_' . self::$ajax_action_load, array( __CLASS__, 'handle_ajax_load' ) );
65 }
66
67 /**
68 * Enqueue JS/CSS for admin pages.
69 *
70 * @return void
71 */
72 public static function enqueue_assets() {
73
74 // Optional screen check.
75 if ( function_exists( 'get_current_screen' ) ) {
76 $screen = get_current_screen();
77 $ok = apply_filters( 'wpbc_option_saver_loader_enqueue', true, $screen );
78 if ( ! $ok ) {
79 return;
80 }
81 }
82
83 $base_url = plugins_url( '', defined( 'WPBC_FILE' ) ? WPBC_FILE : __FILE__ );
84 $js_url = $base_url . '/includes/save-load-option/_out/save-load-option.js';
85 $css_url = $base_url . '/includes/save-load-option/_out/save-load-option.css';
86
87 wp_register_style( 'wpbc-save-load-option', $css_url, array(), self::$asset_version );
88 wp_enqueue_style( 'wpbc-save-load-option' );
89
90 wp_register_script( 'wpbc-save-load-option', $js_url, array( 'jquery' ), self::$asset_version, true );
91 wp_enqueue_script( 'wpbc-save-load-option' );
92
93 wp_localize_script(
94 'wpbc-save-load-option',
95 'wpbc_option_saver_loader_config',
96 array(
97 'ajax_url' => admin_url( 'admin-ajax.php' ),
98 'action_save' => self::$ajax_action_save,
99 'action_load' => self::$ajax_action_load,
100 )
101 );
102 }
103
104 /**
105 * AJAX: Save option.
106 *
107 * Expected POST:
108 * - data_name string Option key.
109 * - data_value string RAW scalar | query-string | JSON string.
110 * - nonce_action string Nonce action name.
111 * - nonce string Nonce value.
112 *
113 * @return void
114 */
115 public static function handle_ajax_save() {
116
117 $capability = apply_filters( 'wpbc_option_saver_loader_cap_save', ( function_exists( 'wpbc_bfb_get_manage_cap' ) ) ? wpbc_bfb_get_manage_cap() : 'manage_options' );
118 if ( ! current_user_can( $capability ) ) {
119 wp_send_json_error( array( 'message' => __( 'You do not have permission to save settings.', 'booking' ) ) );
120 }
121
122 $data_name = isset( $_POST['data_name'] ) ? sanitize_key( wp_unslash( $_POST['data_name'] ) ) : '';
123 /* phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing */
124 $data_raw = isset( $_POST['data_value'] ) ? wp_unslash( $_POST['data_value'] ) : '';
125 // Optional: split JSON object into multiple options.
126 $data_mode = isset( $_POST['data_mode'] ) ? sanitize_key( wp_unslash( $_POST['data_mode'] ) ) : '';
127 $data_fields = isset( $_POST['data_fields'] ) ? sanitize_text_field( wp_unslash( $_POST['data_fields'] ) ) : '';
128
129 $nonce_name = isset( $_POST['nonce_action'] ) ? sanitize_key( wp_unslash( $_POST['nonce_action'] ) ) : '';
130 $nonce_value = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
131
132 if ( empty( $nonce_name ) || ! wp_verify_nonce( $nonce_value, $nonce_name ) ) {
133 wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'booking' ) ) );
134 }
135
136 if ( empty( $data_name ) ) {
137 wp_send_json_error( array( 'message' => __( 'Missing data name.', 'booking' ) ) );
138 }
139
140 $value_to_store = self::normalize_incoming_value( $data_raw );
141
142 // Split mode: JSON object => multiple options saved separately.
143 if ( 'split' === $data_mode && is_array( $value_to_store ) ) {
144
145 $allowed_keys = array();
146 if ( '' !== trim( $data_fields ) ) {
147 $parts = explode( ',', (string) $data_fields );
148 foreach ( $parts as $p ) {
149 $k = sanitize_key( trim( (string) $p ) );
150 if ( '' !== $k ) {
151 $allowed_keys[ $k ] = true;
152 }
153 }
154 }
155
156 $saved = array();
157
158 foreach ( $value_to_store as $k => $v ) {
159
160 if ( ! is_scalar( $k ) ) {
161 continue;
162 }
163
164 $opt_key = sanitize_key( (string) $k );
165 if ( '' === $opt_key ) {
166 continue;
167 }
168
169 // If allowlist provided, only save those keys.
170 if ( ! empty( $allowed_keys ) && ! isset( $allowed_keys[ $opt_key ] ) ) {
171 continue;
172 }
173
174 // Values: allow scalar or arrays (already sanitized by normalize_incoming_value()).
175 $opt_val = $v;
176 if ( is_scalar( $opt_val ) ) {
177 $opt_val = sanitize_text_field( (string) $opt_val );
178 } elseif ( is_array( $opt_val ) ) {
179 $opt_val = self::sanitize_mixed_value( $opt_val );
180 } else {
181 $opt_val = '';
182 }
183
184 self::update_option( self::$option_prefix . $opt_key, $opt_val );
185 $saved[ $opt_key ] = $opt_val;
186 }
187
188 if ( empty( $saved ) ) {
189 wp_send_json_error( array( 'message' => __( 'Nothing to save.', 'booking' ) ) );
190 }
191
192 wp_send_json_success(
193 array(
194 'message' => __( 'Settings saved.', 'booking' ),
195 'value' => $saved,
196 'mode' => 'split',
197 )
198 );
199 }
200
201 // Default: store as a single option (scalar/array).
202 self::update_option( self::$option_prefix . $data_name, $value_to_store );
203
204 // Return stored value (useful for client callbacks / UI sync).
205 wp_send_json_success(
206 array(
207 'message' => __( 'Settings saved.', 'booking' ),
208 'value' => $value_to_store,
209 )
210 );
211 }
212
213 /**
214 * AJAX: Load option.
215 *
216 * Expected GET:
217 * - data_name string Option key.
218 *
219 * @return void
220 */
221 public static function handle_ajax_load() {
222
223 $capability = apply_filters( 'wpbc_option_saver_loader_cap_load', ( function_exists( 'wpbc_bfb_get_manage_cap' ) ) ? wpbc_bfb_get_manage_cap() : 'manage_options' );
224 if ( ! current_user_can( $capability ) ) {
225 wp_send_json_error( array( 'message' => __( 'You do not have permission to load settings.', 'booking' ) ) );
226 }
227
228 /* phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing */
229 $data_name = isset( $_GET['data_name'] ) ? sanitize_key( wp_unslash( $_GET['data_name'] ) ) : '';
230 if ( empty( $data_name ) ) {
231 wp_send_json_error( array( 'message' => __( 'Missing data name.', 'booking' ) ) );
232 }
233
234 $option_key = self::$option_prefix . $data_name;
235 $value = self::get_option( $option_key, array() );
236
237 wp_send_json_success( array( 'value' => $value ) );
238 }
239
240 /**
241 * Normalize payload: prefer JSON -> array; fallback to query-string -> array; else scalar string.
242 *
243 * @param string $data_raw Raw input.
244 * @return mixed
245 */
246 private static function normalize_incoming_value( $data_raw ) {
247
248 if ( ! is_string( $data_raw ) || '' === $data_raw ) {
249 return '';
250 }
251
252 $maybe_json = trim( $data_raw );
253
254 // JSON path.
255 if (
256 0 === strpos( $maybe_json, '{' ) || 0 === strpos( $maybe_json, '[' ) ||
257 'null' === strtolower( $maybe_json ) || 'true' === strtolower( $maybe_json ) ||
258 'false' === strtolower( $maybe_json ) || is_numeric( $maybe_json )
259 ) {
260 $decoded = json_decode( $maybe_json, true );
261 if ( null !== $decoded && JSON_ERROR_NONE === json_last_error() ) {
262 return self::sanitize_mixed_value( $decoded );
263 }
264 }
265
266 // Query-string path.
267 if ( false !== strpos( $data_raw, '=' ) || false !== strpos( $data_raw, '&' ) || false !== strpos( strtolower( $data_raw ), '%5b' ) ) {
268 $parsed = array();
269 parse_str( $data_raw, $parsed ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
270 return self::sanitize_kv_array_preserve_brackets( $parsed );
271 }
272
273 // Scalar.
274 return sanitize_text_field( $data_raw );
275 }
276
277 /**
278 * Recursively sanitize mixed values.
279 *
280 * @param mixed $value Mixed value.
281 * @return mixed
282 */
283 private static function sanitize_mixed_value( $value ) {
284
285 if ( is_array( $value ) ) {
286 $out = array();
287 foreach ( $value as $k => $v ) {
288 $kk = is_string( $k ) ? preg_replace( '/[^a-zA-Z0-9_\-\[\]]/', '', $k ) : $k;
289 $out[ $kk ] = self::sanitize_mixed_value( $v );
290 }
291 return $out;
292 }
293
294 if ( is_scalar( $value ) ) {
295 return sanitize_text_field( (string) $value );
296 }
297
298 return '';
299 }
300
301 /**
302 * Sanitize arrays parsed from query-string, preserving bracket keys.
303 *
304 * @param array $parsed_data Parsed data.
305 * @return array
306 */
307 private static function sanitize_kv_array_preserve_brackets( $parsed_data ) {
308
309 $sanitized_data = array();
310
311 if ( empty( $parsed_data ) || ! is_array( $parsed_data ) ) {
312 return $sanitized_data;
313 }
314
315 foreach ( $parsed_data as $key => $val ) {
316 $key = preg_replace( '/[^a-zA-Z0-9_\-\[\]]/', '', (string) $key );
317 if ( is_array( $val ) ) {
318 $sanitized_data[ $key ] = self::sanitize_mixed_value( $val );
319 } else {
320 $sanitized_data[ $key ] = sanitize_text_field( $val );
321 }
322 }
323
324 return $sanitized_data;
325 }
326
327 /**
328 * Update option (Booking Calendar wrapper if present).
329 *
330 * @param string $option_key Key.
331 * @param mixed $value Value.
332 * @return void
333 */
334 private static function update_option( $option_key, $value ) {
335 if ( function_exists( 'update_bk_option' ) ) {
336 update_bk_option( $option_key, $value );
337 } else {
338 update_option( $option_key, $value );
339 }
340 }
341
342 /**
343 * Get option (Booking Calendar wrapper if present).
344 *
345 * @param string $option_key Key.
346 * @param mixed $default Default.
347 * @return mixed
348 */
349 private static function get_option( $option_key, $default = false ) {
350 if ( function_exists( 'get_bk_option' ) ) {
351 $val = get_bk_option( $option_key );
352 return ( null === $val ) ? $default : $val;
353 }
354 return get_option( $option_key, $default );
355 }
356 }
357
358 add_action( 'plugins_loaded', array( 'wpbc_option_saver_loader', 'init' ) );
359
360
361 /**
362 * == Usage examples ==
363 *
364 * 1) Save RAW scalar (On/Off).
365 *
366
367 <?php
368 $opt_name = 'booking_timeslot_picker';
369 $nonce_action = 'wpbc_nonce_' . $opt_name;
370 ?>
371 <a href="javascript:void(0);"
372 class="button button-secondary"
373 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)"
374 data-wpbc-u-save-name="<?php echo esc_attr( $opt_name ); ?>"
375 data-wpbc-u-save-nonce="<?php echo esc_attr( wp_create_nonce( $nonce_action ) ); ?>"
376 data-wpbc-u-save-action="<?php echo esc_attr( $nonce_action ); ?>"
377 data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving…', 'booking' ); ?>">
378 <?php esc_html_e( 'Save Toggle', 'booking' ); ?>
379 </a>
380
381 *
382 * 2) Save complex structure (RAW JSON)
383 *
384
385 <?php
386 $opt_name = 'wpbc_bfb_form_structure';
387 $nonce_action = 'wpbc_nonce_' . $opt_name;
388 ?>
389 <a href="javascript:void(0);"
390 class="button button-primary"
391 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)"
392 data-wpbc-u-save-name="<?php echo esc_attr( $opt_name ); ?>"
393 data-wpbc-u-save-nonce="<?php echo esc_attr( wp_create_nonce( $nonce_action ) ); ?>"
394 data-wpbc-u-save-action="<?php echo esc_attr( $nonce_action ) ; ?>"
395 data-wpbc-u-busy-text="<?php esc_attr_e( 'Saving…', 'booking' ); ?>">
396 <?php esc_html_e( 'Save Form Structure', 'booking' ); ?>
397 </a>
398
399 *
400 * 3) Load option and apply
401 *
402
403 <a href="javascript:void(0);"
404 class="button"
405 onclick="wpbc_load_option_from_element(this)"
406 data-wpbc-u-load-name="wpbc_bfb_form_structure"
407 data-wpbc-u-load-callback="wpbc_bfb__on_structure_loaded"
408 data-wpbc-u-busy-text="<?php esc_attr_e( 'Loading…', 'booking' ); ?>">
409 <?php esc_html_e( 'Load Form Structure', 'booking' ); ?>
410 </a>
411 <script>
412 function wpbc_bfb__on_structure_loaded(val){
413 try {
414 if ( typeof val === 'string' ) { val = JSON.parse(val); }
415 if ( window.wpbc_bfb && typeof window.wpbc_bfb.load_saved_structure === 'function' ) {
416 window.wpbc_bfb.load_saved_structure( val || [] );
417 }
418 } catch(e){ console.error(e); }
419 }
420 </script>
421 *
422 */