PluginProbe
Booking Calendar / 11.2
Booking Calendar v11.2
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 / page-form-builder / save-load / bfb-form-storage.php

bfb-form-storage.php in Booking Calendar 11.2, at includes/page-form-builder/save-load/bfb-form-storage.php

436 lines 16.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Storage of Booking form structure.
4 *
5 * == Columns (booking_form_structures) ==
6 *
7 * Identity & keys
8 * booking_form_id – primary key.
9 * form_slug – machine-readable key/slug, logical identity of form (e.g. 'standard').
10 * status – 'published', 'preview', 'draft', 'archived', etc.
11 * scope – how this form is used: 'global', 'resource', 'user', 'template', etc.
12 * version – integer version for the (form_slug, status) pair; incremented on each save.
13 *
14 * Context
15 * booking_resource_id – nullable; if you bind a specific form to a particular booking resource (calendar).
16 * owner_user_id – nullable; owner of the form (MultiUser).
17 *
18 * Engine & payloads
19 * engine – engine key: 'bfb', 'legacy_simple', 'legacy_advanced', etc. Default 'bfb'.
20 * engine_version – engine version, e.g. '1.0'.
21 * structure_json – canonical builder structure (pages -> sections -> columns -> fields).
22 * settings_json – extra non-structural options (CSS classes, theme/skin, internal flags, etc.).
23 * advanced_form – shortcodes / markup for booking form (optional).
24 * content_form – shortcodes / markup for "Content of booking fields data" (optional).
25 *
26 * Flags & meta
27 * is_default – 0/1 flag; default form for a given scope/resource.
28 * title – form name shown in admin (e.g., “'Standard'”).
29 * description – optional admin description.
30 * picture_url – optional image/preview URL.
31 *
32 * Audit
33 * created_at – datetime (site local time).
34 * updated_at – datetime (site local time).
35 * created_by – bigint user ID (creator).
36 * updated_by – bigint user ID (last editor).
37 *
38 * Uniqueness
39 * UNIQUE (form_slug, status) – single row per slug+status pair.
40 *
41 * @package Booking Calendar.
42 * @author wpdevelop, oplugins
43 * @web-site https://wpbookingcalendar.com/
44 * @email info@wpbookingcalendar.com
45 * @file ../includes/page-form-builder/save-load/bfb-form-storage.php
46 *
47 * @since 11.0.0
48 * @modified 2025-12-07
49 * @version 1.1
50 */
51
52 if ( ! defined( 'ABSPATH' ) ) {
53 exit; // Exit if accessed directly.
54 }
55
56 /**
57 * Storage layer for Booking Form Builder configurations.
58 *
59 * - Stores canonical Builder JSON structure.
60 * - Supports per-user ownership via owner_user_id (MultiUser).
61 * - Supports status-based variants per form_slug (active/preview/draft).
62 */
63 class WPBC_BFB_Form_Storage {
64
65 /**
66 * Get form row by (form_slug, status, owner_user_id).
67 *
68 * Field owner_user_id: > 0 : owner-specific row only
69 * = 0 : global row (owner_user_id = 0 OR NULL)
70 *
71 * @param string $form_slug
72 * @param string $status
73 * @param int $owner_user_id
74 *
75 * @return object|null
76 */
77 public static function get_form_row_by_key( $form_slug, $status = 'published', $owner_user_id = 0 ) {
78 global $wpdb;
79
80 $form_slug = sanitize_text_field( (string) $form_slug );
81 $status = sanitize_key( (string) $status );
82 $owner_user_id = max( 0, (int) $owner_user_id );
83
84 if ( '' === $form_slug ) {
85 return null;
86 }
87
88 $status = ( in_array( $status, array( 'publish', 'published' ), true ) ) ? 'published' : $status;
89 $status = ( 'preview' !== $status ) ? 'published' : $status;
90
91 // If your table still has duplicates (before UNIQUE upgrade), choose newest deterministically. $order_by = 'ORDER BY version DESC, updated_at DESC, booking_form_id DESC';
92
93 // Regular User in MultiUser version context.
94 if ( $owner_user_id > 0 ) {
95
96 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
97 return $wpdb->get_row( $wpdb->prepare( "SELECT *
98 FROM {$wpdb->prefix}booking_form_structures
99 WHERE form_slug = %s
100 AND status = %s
101 AND owner_user_id = %d
102 ORDER BY version DESC, updated_at DESC, booking_form_id DESC
103 LIMIT 1", $form_slug, $status, $owner_user_id ) );
104 }
105
106 // Global: owner_user_id=0 OR NULL (migration safe).
107 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
108 return $wpdb->get_row( $wpdb->prepare( "SELECT *
109 FROM {$wpdb->prefix}booking_form_structures
110 WHERE form_slug = %s
111 AND status = %s
112 AND ( owner_user_id = 0 OR owner_user_id IS NULL )
113 ORDER BY version DESC, updated_at DESC, booking_form_id DESC
114 LIMIT 1", $form_slug, $status ) );
115 }
116
117 /**
118 * Get the current (active) form row by form_slug.
119 *
120 * In MultiUser mode you can pass $user_id to limit search
121 * to forms owned by that user (not implemented yet).
122 *
123 * Note:
124 * - We currently define "current" as the row with status = 'published'
125 * for a given form_slug.
126 * - Scope and booking_resource_id are not used to distinguish rows
127 * because the DB constraint is UNIQUE(form_slug, status).
128 *
129 * @param string $form_slug Form slug.
130 * @param int|bool $user_id Optional owner user ID (for future use).
131 *
132 * @return object|null
133 */
134 public static function get_current_form_by_key( $form_slug, $user_id = 0, $status = 'published' ) {
135 global $wpdb;
136
137 $form_slug = sanitize_text_field( (string) $form_slug );
138 $status = sanitize_key( (string) $status );
139 $user_id = max( 0, (int) $user_id );
140
141 if ( '' === $form_slug ) {
142 return null;
143 }
144
145 if ( $user_id > 0 ) {
146 if ( 'template' === $status ) {
147 // Exception for templates loading, for regular users.
148 $sql = $wpdb->prepare( "SELECT *
149 FROM {$wpdb->prefix}booking_form_structures
150 WHERE form_slug = %s
151 AND status = %s
152 AND ( owner_user_id = %d OR owner_user_id = 0 OR owner_user_id IS NULL )
153 ORDER BY version DESC, updated_at DESC, booking_form_id DESC
154 LIMIT 1", $form_slug, $status, $user_id );
155 } else {
156 $sql = $wpdb->prepare( "SELECT *
157 FROM {$wpdb->prefix}booking_form_structures
158 WHERE form_slug = %s
159 AND status = %s
160 AND owner_user_id = %d
161 ORDER BY version DESC, updated_at DESC, booking_form_id DESC
162 LIMIT 1", $form_slug, $status, $user_id );
163 }
164 } else {
165 $sql = $wpdb->prepare( "SELECT *
166 FROM {$wpdb->prefix}booking_form_structures
167 WHERE form_slug = %s
168 AND status = %s
169 AND ( owner_user_id = 0 OR owner_user_id IS NULL )
170 ORDER BY version DESC, updated_at DESC, booking_form_id DESC
171 LIMIT 1", $form_slug, $status );
172 }
173
174 return $wpdb->get_row( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
175 }
176
177 /**
178 * Update compiled HTML cache for an existing form row.
179 *
180 * NOTE:
181 * The current DB schema example for booking_form_structures does not
182 * yet define compiled_html / compiled_version / compiled_updated_at
183 * columns. To avoid SQL errors, this method currently only bumps
184 * updated_at for the given booking_form_id.
185 *
186 * You can extend the table and this method later when you add
187 * compiled_html support.
188 *
189 * @param int $booking_form_id Primary key of the row.
190 * @param string $compiled_html Precompiled HTML (unused in current schema).
191 * @param int $compiled_version Optional exporter/schema version (unused).
192 *
193 * @return bool True on success, false on failure.
194 */
195 public static function update_compiled_html( $booking_form_id, $compiled_html, $compiled_version = 1 ) {
196
197 global $wpdb;
198
199 $now = current_time( 'mysql' );
200
201 $data = array(
202 'updated_at' => $now,
203 );
204
205 $where = array(
206 'booking_form_id' => intval( $booking_form_id ),
207 );
208
209 $data_formats = array( '%s' );
210 $where_formats = array( '%d' );
211
212 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
213 $updated = $wpdb->update( $wpdb->prefix . 'booking_form_structures', $data, $where, $data_formats, $where_formats );
214
215 if ( false === $updated ) {
216 return false;
217 }
218
219 return true;
220 }
221
222 /**
223 * Insert or update a form row for a given (form_slug, status) pair.
224 *
225 * Behavior with current schema:
226 * - UNIQUE KEY (form_slug, status, owner_user_id) guarantees a single row per
227 * slug+status.
228 * - On first save for (form_slug, status) -> INSERT with version = 1.
229 * - On subsequent saves -> UPDATE existing row, bumping version.
230 *
231 * @param array $data {
232 *
233 * @type string $form_slug Required form slug (logical identity).
234 * Backwards-compat: 'form_key' is also accepted.
235 * @type string $title Human-readable title.
236 * @type string $description Optional description.
237 * @type string $scope Scope, e.g. 'global', 'resource', 'user', 'template'. Default 'global'.
238 * @type string $status 'published', 'preview', 'draft', 'archived', ... Default 'draft'.
239 * @type int $booking_resource_id Optional booking resource ID.
240 * @type int $owner_user_id Optional owner ID (MultiUser). Backwards-compat: 'user_id'.
241 * @type int $is_default 1 if default, 0 otherwise.
242 *
243 * @type string $engine Engine key: 'bfb', 'legacy_simple', 'legacy_advanced'. Default 'bfb'.
244 * @type string $engine_version Engine version. Default '1.0'.
245 * @type string $structure_json Canonical JSON structure (required).
246 * @type string $settings_json Optional JSON with extra settings.
247 * @type string $advanced_form Optional shortcodes string for booking form.
248 * @type string $content_form Optional "Content of booking fields data" shortcodes.
249 * @type string $picture_url Optional preview image URL.
250 *
251 * @return int|false Booking form ID or false on failure.
252 */
253 public static function save_form( $data ) {
254 global $wpdb;
255
256 $now = current_time( 'mysql' );
257 $current_id = wpbc_get_current_user_id();
258
259 // -----------------------------------------------------------------
260 // Required: form_slug and structure_json
261 // -----------------------------------------------------------------
262 $form_slug = isset( $data['form_slug'] ) ? $data['form_slug'] : '';
263 // Backwards-compat: older callers may still pass 'form_key'.
264 if ( '' === $form_slug && isset( $data['form_key'] ) ) {
265 $form_slug = $data['form_key'];
266 }
267
268 $structure_json = isset( $data['structure_json'] ) ? $data['structure_json'] : wpbc_form_config__encode_json( array() );
269
270 if ( '' === $form_slug || '' === $structure_json ) {
271 return false;
272 }
273
274 // -----------------------------------------------------------------
275 // Optional fields with defaults
276 // -----------------------------------------------------------------
277 $title = isset( $data['title'] ) ? $data['title'] : '';
278 $description = isset( $data['description'] ) ? $data['description'] : '';
279 $scope = isset( $data['scope'] ) ? $data['scope'] : 'global';
280 $status = isset( $data['status'] ) ? $data['status'] : 'draft';
281
282 $booking_resource_id = isset( $data['booking_resource_id'] ) ? intval( $data['booking_resource_id'] ) : null;
283
284 // Owner user ID: prefer explicit owner_user_id, fall back to user_id, then current user.
285 if ( isset( $data['owner_user_id'] ) ) {
286 $owner_user_id = intval( $data['owner_user_id'] );
287 } elseif ( isset( $data['user_id'] ) ) {
288 $owner_user_id = intval( $data['user_id'] );
289 } else {
290 $owner_user_id = $current_id ? $current_id : 0;
291 }
292 $owner_user_id = (int) $owner_user_id;
293 if ( $owner_user_id < 0 ) {
294 $owner_user_id = 0;
295 }
296
297 $is_default = ! empty( $data['is_default'] ) ? 1 : 0;
298
299 $settings_json = isset( $data['settings_json'] ) ? $data['settings_json'] : null;
300 $engine = isset( $data['engine'] ) ? $data['engine'] : 'bfb';
301 $engine_version = isset( $data['engine_version'] ) ? $data['engine_version'] : '1.0';
302 $advanced_form = isset( $data['advanced_form'] ) ? $data['advanced_form'] : null;
303 $content_form = isset( $data['content_form'] ) ? $data['content_form'] : null;
304 $picture_url = isset( $data['picture_url'] ) ? $data['picture_url'] : null;
305
306 // -----------------------------------------------------------------
307 // 1) Try to find existing row for this (form_slug, status) pair. Because of UNIQUE(form_slug, status, $owner_user_id), this will be at most one row.
308 // -----------------------------------------------------------------
309 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
310 $existing = $wpdb->get_row( $wpdb->prepare( "SELECT *
311 FROM {$wpdb->prefix}booking_form_structures
312 WHERE form_slug = %s
313 AND status = %s
314 AND owner_user_id = %d
315 LIMIT 1", $form_slug, $status, $owner_user_id ) );
316
317 if ( $existing ) {
318 // --- UPDATE existing row ---------------------------------------------------------------
319 $booking_form_id = intval( $existing->booking_form_id );
320 $version = intval( $existing->version );
321 $version = ( $version > 0 ) ? ( $version + 1 ) : 1;
322
323 $update_data = array(
324 'title' => $title,
325 'description' => $description,
326 'scope' => $scope,
327 'status' => $status,
328 'version' => $version,
329 'booking_resource_id' => $booking_resource_id,
330 'owner_user_id' => $owner_user_id,
331 'engine' => $engine,
332 'engine_version' => $engine_version,
333 'structure_json' => $structure_json,
334 'settings_json' => $settings_json,
335 'advanced_form' => $advanced_form,
336 'content_form' => $content_form,
337 'is_default' => $is_default,
338 'picture_url' => $picture_url,
339 'updated_at' => $now,
340 'updated_by' => $current_id ? $current_id : null,
341 );
342
343 $update_formats = array(
344 '%s', // title.
345 '%s', // description.
346 '%s', // scope.
347 '%s', // status.
348 '%d', // version.
349 '%d', // booking_resource_id.
350 '%d', // owner_user_id.
351 '%s', // engine.
352 '%s', // engine_version.
353 '%s', // structure_json.
354 '%s', // settings_json.
355 '%s', // advanced_form.
356 '%s', // content_form.
357 '%d', // is_default.
358 '%s', // picture_url.
359 '%s', // updated_at.
360 '%d', // updated_by.
361 );
362
363 $where = array( 'booking_form_id' => $booking_form_id );
364 $where_formats = array( '%d' );
365
366 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
367 $updated = $wpdb->update( $wpdb->prefix . 'booking_form_structures', $update_data, $where, $update_formats, $where_formats );
368
369 if ( false === $updated ) {
370 return false;
371 }
372
373 return $booking_form_id;
374 }
375
376 // -----------------------------------------------------------------
377 // 2) No row yet – INSERT first version for this (form_slug, status)
378 // -----------------------------------------------------------------
379 $version = 1;
380
381 $insert_data = array(
382 'form_slug' => $form_slug,
383 'status' => $status,
384 'scope' => $scope,
385 'version' => $version,
386 'booking_resource_id' => $booking_resource_id,
387 'owner_user_id' => $owner_user_id,
388 'engine' => $engine,
389 'engine_version' => $engine_version,
390 'structure_json' => $structure_json,
391 'settings_json' => $settings_json,
392 'advanced_form' => $advanced_form,
393 'content_form' => $content_form,
394 'is_default' => $is_default,
395 'title' => $title,
396 'description' => $description,
397 'picture_url' => $picture_url,
398 'created_at' => $now,
399 'updated_at' => $now,
400 'created_by' => $current_id ? $current_id : null,
401 'updated_by' => $current_id ? $current_id : null,
402 );
403
404 $insert_formats = array(
405 '%s', // form_slug.
406 '%s', // status.
407 '%s', // scope.
408 '%d', // version.
409 '%d', // booking_resource_id.
410 '%d', // owner_user_id.
411 '%s', // engine.
412 '%s', // engine_version.
413 '%s', // structure_json.
414 '%s', // settings_json.
415 '%s', // advanced_form.
416 '%s', // content_form.
417 '%d', // is_default.
418 '%s', // title.
419 '%s', // description.
420 '%s', // picture_url.
421 '%s', // created_at.
422 '%s', // updated_at.
423 '%d', // created_by.
424 '%d', // updated_by.
425 );
426
427 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
428 $inserted = $wpdb->insert( $wpdb->prefix . 'booking_form_structures', $insert_data, $insert_formats );
429
430 if ( false === $inserted ) {
431 return false;
432 }
433 return intval( $wpdb->insert_id );
434 }
435 }
436