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
booking / includes / booking_modes_v3 / quickstart / booking_modes-quickstart.php

booking_modes-quickstart.php in Booking Calendar 11.8.4, at includes/booking_modes_v3/quickstart/booking_modes-quickstart.php

474 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shared, explicit QuickStart operations for Booking Calendar modes.
4 *
5 * @package Booking Calendar
6 * @since 11.5.0
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Map a stored minimum WordPress role to its hierarchical capability.
15 *
16 * Booking Calendar stores role names for its administration boundaries. Using
17 * the matching primitive capability keeps higher roles authorized as expected.
18 *
19 * @param string $role_name Stored WordPress role name.
20 *
21 * @return string WordPress capability name.
22 */
23 function wpbc_booking_modes_get_role_capability( $role_name ) {
24
25 $role_capabilities = array(
26 'administrator' => 'activate_plugins',
27 'editor' => 'publish_pages',
28 'author' => 'publish_posts',
29 'contributor' => 'edit_posts',
30 'subscriber' => 'read',
31 );
32 $role_name = sanitize_key( (string) $role_name );
33
34 return isset( $role_capabilities[ $role_name ] ) ? $role_capabilities[ $role_name ] : 'manage_options';
35 }
36
37 /**
38 * Get the capability required to run a mode QuickStart operation.
39 *
40 * Page publishing remains a separate mandatory check because every QuickStart
41 * creates a public test page. This filter can tighten the Booking Calendar
42 * settings boundary but cannot bypass that WordPress page capability.
43 *
44 * @return string WordPress capability name.
45 */
46 function wpbc_booking_modes_get_quickstart_capability() {
47
48 $capability = wpbc_booking_modes_get_role_capability( get_bk_option( 'booking_user_role_settings' ) );
49
50 /**
51 * Filter the capability required to run Booking Mode QuickStart operations.
52 *
53 * @param string $capability WordPress capability name.
54 */
55 $capability = apply_filters( 'wpbc_booking_modes_quickstart_capability', $capability );
56
57 return is_scalar( $capability ) ? sanitize_key( (string) $capability ) : 'manage_options';
58 }
59
60 /**
61 * Check whether the active Booking Calendar owner may run QuickStart.
62 *
63 * @return bool True when the user can manage Booking Calendar settings and
64 * publish the required public test page.
65 */
66 function wpbc_booking_modes_current_user_can_quickstart() {
67
68 return wpbc_booking_modes_current_user_can_switch()
69 && current_user_can( wpbc_booking_modes_get_quickstart_capability() )
70 && current_user_can( 'publish_pages' );
71 }
72
73 /**
74 * Validate a state-changing QuickStart request.
75 *
76 * @param string $mode_id Requested administration mode identifier.
77 * @param string $nonce Request nonce.
78 *
79 * @return true|WP_Error True when valid, otherwise a request error.
80 */
81 function wpbc_booking_modes_validate_quickstart_request( $mode_id, $nonce ) {
82
83 if ( ! wp_verify_nonce( $nonce, 'wpbc_booking_modes_quickstart_nonce' ) ) {
84 return new WP_Error( 'wpbc_booking_modes_quickstart_invalid_nonce', __( 'The QuickStart request expired. Reload the page and try again.', 'booking' ) );
85 }
86
87 if ( ! wpbc_booking_modes_current_user_can_quickstart() ) {
88 return new WP_Error( 'wpbc_booking_modes_quickstart_forbidden', __( 'You are not allowed to create Booking Calendar QuickStart content.', 'booking' ) );
89 }
90
91 if ( function_exists( 'wpbc_is_this_demo' ) && wpbc_is_this_demo() ) {
92 return new WP_Error( 'wpbc_booking_modes_quickstart_demo', __( 'QuickStart content creation is disabled on live demo websites.', 'booking' ) );
93 }
94
95 $mode_id = sanitize_key( (string) $mode_id );
96 $mode = wpbc_booking_modes_get_mode( $mode_id );
97
98 if (
99 ! is_array( $mode )
100 || ! in_array( $mode_id, wpbc_booking_modes_get_allowed_mode_ids(), true )
101 || empty( $mode['quickstart_id'] )
102 || $mode_id !== sanitize_key( (string) $mode['quickstart_id'] )
103 ) {
104 return new WP_Error( 'wpbc_booking_modes_quickstart_invalid_mode', __( 'QuickStart is not available for the selected administration mode.', 'booking' ) );
105 }
106
107 if ( $mode_id !== wpbc_booking_modes_get_selected_mode_id() ) {
108 return new WP_Error( 'wpbc_booking_modes_quickstart_mode_changed', __( 'The active administration mode changed. Reload the page before running QuickStart.', 'booking' ) );
109 }
110
111 return true;
112 }
113
114 /**
115 * Get the owner-scoped user option name for one QuickStart mode.
116 *
117 * @param string $mode_id Appointment or Rental mode identifier.
118 *
119 * @return string User option name, or an empty string for an unsupported mode.
120 */
121 function wpbc_booking_modes_get_quickstart_option_name( $mode_id ) {
122
123 $mode_id = sanitize_key( (string) $mode_id );
124
125 return in_array( $mode_id, array( 'appointment', 'rental' ), true )
126 ? 'booking_admin_quickstart_' . $mode_id
127 : '';
128 }
129
130 /**
131 * Read normalized owner-scoped QuickStart progress.
132 *
133 * @param string $mode_id Appointment or Rental mode identifier.
134 *
135 * @return array<string,mixed> Stored progress, or an empty array.
136 */
137 function wpbc_booking_modes_get_quickstart_state( $mode_id ) {
138
139 $option_name = wpbc_booking_modes_get_quickstart_option_name( $mode_id );
140 $context = wpbc_booking_modes_get_context();
141 $owner_user_id = absint( $context['owner_user_id'] );
142
143 if ( '' === $option_name || ! $owner_user_id ) {
144 return array();
145 }
146
147 $state = get_user_option( $option_name, $owner_user_id );
148
149 return is_array( $state ) ? $state : array();
150 }
151
152 /**
153 * Persist owner-scoped QuickStart progress for repeat-safe retries.
154 *
155 * Progress is saved after each durable stage. A later retry therefore resumes
156 * without reapplying a configuration profile that the owner may have adjusted.
157 *
158 * @param string $mode_id Appointment or Rental mode identifier.
159 * @param array<string,mixed> $state Complete normalized progress state.
160 *
161 * @return bool|WP_Error True on success, otherwise a storage error.
162 */
163 function wpbc_booking_modes_set_quickstart_state( $mode_id, $state ) {
164
165 $option_name = wpbc_booking_modes_get_quickstart_option_name( $mode_id );
166 $context = wpbc_booking_modes_get_context();
167 $owner_user_id = absint( $context['owner_user_id'] );
168
169 if ( '' === $option_name || ! $owner_user_id ) {
170 return new WP_Error( 'wpbc_booking_modes_quickstart_owner_required', __( 'A Booking Calendar owner is required to save QuickStart progress.', 'booking' ) );
171 }
172
173 $state = is_array( $state ) ? $state : array();
174 $state['schema_version'] = 1;
175 $is_updated = update_user_option( $owner_user_id, $option_name, $state );
176
177 if ( false === $is_updated && $state !== get_user_option( $option_name, $owner_user_id ) ) {
178 return new WP_Error( 'wpbc_booking_modes_quickstart_state_not_saved', __( 'QuickStart progress could not be saved.', 'booking' ) );
179 }
180
181 return true;
182 }
183
184 /**
185 * Build the site-scoped option name used as an atomic QuickStart lock.
186 *
187 * @param string $mode_id Appointment or Rental mode identifier.
188 * @param int $owner_user_id Active Booking Calendar owner user ID.
189 *
190 * @return string Sanitized WordPress option name.
191 */
192 function wpbc_booking_modes_get_quickstart_lock_name( $mode_id, $owner_user_id ) {
193
194 return '_wpbc_booking_modes_quickstart_lock_' . absint( $owner_user_id ) . '_' . sanitize_key( (string) $mode_id );
195 }
196
197 /**
198 * Acquire an atomic owner-and-mode QuickStart lock.
199 *
200 * WordPress option names are unique, so `add_option()` prevents simultaneous
201 * browser tabs from creating the same page or Service twice. A stale lock is
202 * recoverable after two minutes if a request terminates unexpectedly.
203 *
204 * @param string $mode_id Appointment or Rental mode identifier.
205 *
206 * @return string|WP_Error Lock option name, or an already-running error.
207 */
208 function wpbc_booking_modes_acquire_quickstart_lock( $mode_id ) {
209
210 $context = wpbc_booking_modes_get_context();
211 $owner_user_id = absint( $context['owner_user_id'] );
212 $lock_name = wpbc_booking_modes_get_quickstart_lock_name( $mode_id, $owner_user_id );
213 $lock_time = time();
214
215 if ( add_option( $lock_name, $lock_time, '', false ) ) {
216 return $lock_name;
217 }
218
219 $existing_lock_time = absint( get_option( $lock_name, 0 ) );
220 if ( ! $existing_lock_time || $existing_lock_time < ( $lock_time - 120 ) ) {
221 delete_option( $lock_name );
222 if ( add_option( $lock_name, $lock_time, '', false ) ) {
223 return $lock_name;
224 }
225 }
226
227 return new WP_Error( 'wpbc_booking_modes_quickstart_in_progress', __( 'QuickStart is already running for this mode. Wait a moment and try again.', 'booking' ) );
228 }
229
230 /**
231 * Release an acquired QuickStart lock.
232 *
233 * @param string $lock_name Exact lock option name returned by the acquire helper.
234 *
235 * @return void
236 */
237 function wpbc_booking_modes_release_quickstart_lock( $lock_name ) {
238
239 $lock_name = is_scalar( $lock_name ) ? sanitize_key( (string) $lock_name ) : '';
240
241 if ( 0 === strpos( $lock_name, '_wpbc_booking_modes_quickstart_lock_' ) ) {
242 delete_option( $lock_name );
243 }
244 }
245
246 /**
247 * Resolve the first existing owner-visible Booking Resource.
248 *
249 * QuickStart deliberately reuses resources and never renames them. This keeps
250 * existing bookings, availability, pricing, and MultiUser ownership intact.
251 *
252 * @return int|WP_Error Resource ID, or an error when no resource is available.
253 */
254 function wpbc_booking_modes_quickstart_get_first_resource_id() {
255
256 $resource_options = (array) apply_bk_filter( 'wpdebk_get_keyed_all_bk_resources', array() );
257
258 if ( empty( $resource_options ) && function_exists( 'wpbc_appointment_services_get_provider_options' ) ) {
259 $resource_options = wpbc_appointment_services_get_provider_options();
260 }
261
262 foreach ( (array) $resource_options as $resource_id => $resource ) {
263 $resource_values = is_object( $resource ) ? get_object_vars( $resource ) : (array) $resource;
264 $resolved_id = ! empty( $resource_values['id'] ) ? absint( $resource_values['id'] ) : absint( $resource_id );
265
266 if ( $resolved_id ) {
267 return $resolved_id;
268 }
269 }
270
271 $default_resource_id = function_exists( 'wpbc_get_default_resource' ) ? absint( wpbc_get_default_resource() ) : 0;
272
273 if ( $default_resource_id ) {
274 return $default_resource_id;
275 }
276
277 return new WP_Error( 'wpbc_booking_modes_quickstart_resource_missing', __( 'Create a Booking Resource before running QuickStart.', 'booking' ) );
278 }
279
280 /**
281 * Find an existing page containing one stable QuickStart shortcode prefix.
282 *
283 * Marker lookup is owner-scoped. A published page created outside QuickStart is
284 * also reused when its content already contains the prefix, preventing
285 * duplicate public booking pages on upgraded sites. In MultiUser, this fallback
286 * is restricted to pages authored by the active Booking Calendar owner.
287 *
288 * @param string $purpose Stable page purpose marker.
289 * @param string $shortcode_match Stable shortcode prefix expected in page content.
290 *
291 * @return WP_Post|null Existing page, or null when no reusable page exists.
292 */
293 function wpbc_booking_modes_quickstart_find_page( $purpose, $shortcode_match ) {
294 global $wpdb;
295
296 $context = wpbc_booking_modes_get_context();
297 $owner_user_id = absint( $context['owner_user_id'] );
298 $marked_pages = get_posts(
299 array(
300 'post_type' => 'page',
301 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future', 'trash' ),
302 'posts_per_page' => 1,
303 'orderby' => 'ID',
304 'order' => 'ASC',
305 'meta_query' => array(
306 'relation' => 'AND',
307 array(
308 'key' => '_wpbc_booking_modes_quickstart_purpose',
309 'value' => sanitize_key( (string) $purpose ),
310 ),
311 array(
312 'key' => '_wpbc_booking_modes_quickstart_owner',
313 'value' => $owner_user_id,
314 'type' => 'NUMERIC',
315 ),
316 ),
317 )
318 );
319
320 if ( ! empty( $marked_pages[0] ) ) {
321 return $marked_pages[0];
322 }
323
324 if ( ! empty( $context['is_multiuser'] ) ) {
325 $published_page_query = $wpdb->prepare(
326 "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s AND post_author = %d AND post_content LIKE %s ORDER BY ID ASC LIMIT 1",
327 'page',
328 'publish',
329 $owner_user_id,
330 '%' . $wpdb->esc_like( $shortcode_match ) . '%'
331 );
332 } else {
333 $published_page_query = $wpdb->prepare(
334 "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s AND post_content LIKE %s ORDER BY ID ASC LIMIT 1",
335 'page',
336 'publish',
337 '%' . $wpdb->esc_like( $shortcode_match ) . '%'
338 );
339 }
340
341 $published_page_id = $wpdb->get_var( $published_page_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared in the ownership-specific branches above.
342
343 if ( $published_page_id ) {
344 $published_page = get_post( absint( $published_page_id ) );
345 if ( $published_page instanceof WP_Post ) {
346 return $published_page;
347 }
348 }
349
350 return null;
351 }
352
353 /**
354 * Reuse or create one owner-marked QuickStart page.
355 *
356 * A previously marked non-published page is never duplicated or silently
357 * republished. The owner receives an actionable error and can restore it.
358 *
359 * @param string $purpose Stable page purpose marker.
360 * @param string $page_slug Preferred page slug.
361 * @param string $page_title Translatable page title.
362 * @param string $shortcode Exact Booking Calendar shortcode used for a new page.
363 * @param string $shortcode_match Optional stable shortcode prefix used to find a reusable page.
364 *
365 * @return array<string,mixed>|WP_Error Page ID and public test URL, or an error.
366 */
367 function wpbc_booking_modes_quickstart_ensure_page( $purpose, $page_slug, $page_title, $shortcode, $shortcode_match = '' ) {
368
369 if ( function_exists( 'wpbc_is_this_demo' ) && wpbc_is_this_demo() ) {
370 return new WP_Error( 'wpbc_booking_modes_quickstart_demo_page', __( 'QuickStart cannot create or modify pages on a live demo website.', 'booking' ) );
371 }
372
373 $shortcode_match = '' !== (string) $shortcode_match ? (string) $shortcode_match : (string) $shortcode;
374 $existing_page = wpbc_booking_modes_quickstart_find_page( $purpose, $shortcode_match );
375
376 if ( $existing_page instanceof WP_Post ) {
377 if ( 'publish' !== $existing_page->post_status ) {
378 return new WP_Error( 'wpbc_booking_modes_quickstart_page_not_published', __( 'A previous QuickStart page exists but is not published. Restore or publish that page before trying again.', 'booking' ) );
379 }
380 if ( false === strpos( (string) $existing_page->post_content, $shortcode_match ) ) {
381 return new WP_Error( 'wpbc_booking_modes_quickstart_page_changed', __( 'A previous QuickStart page no longer contains its Booking Calendar shortcode. Restore the shortcode before trying again.', 'booking' ) );
382 }
383
384 $test_url = get_permalink( $existing_page->ID );
385 if ( ! $test_url ) {
386 return new WP_Error( 'wpbc_booking_modes_quickstart_page_url_missing', __( 'The QuickStart booking page does not have a public URL.', 'booking' ) );
387 }
388
389 return array(
390 'page_id' => absint( $existing_page->ID ),
391 'test_url' => $test_url,
392 'created' => false,
393 );
394 }
395
396 if ( ! function_exists( 'wpbc_create_page' ) ) {
397 return new WP_Error( 'wpbc_booking_modes_quickstart_page_api_missing', __( 'The Booking Calendar page creation API is not available.', 'booking' ) );
398 }
399
400 $context = wpbc_booking_modes_get_context();
401 $page_id = wpbc_create_page(
402 array(
403 'post_name' => sanitize_title( $page_slug ),
404 'post_title' => $page_title,
405 'post_content' => $shortcode,
406 'post_author' => absint( $context['owner_user_id'] ),
407 )
408 );
409
410 if ( ! $page_id ) {
411 return new WP_Error( 'wpbc_booking_modes_quickstart_page_not_created', __( 'The QuickStart booking page could not be created.', 'booking' ) );
412 }
413
414 update_post_meta( $page_id, '_wpbc_booking_modes_quickstart_purpose', sanitize_key( (string) $purpose ) );
415 update_post_meta( $page_id, '_wpbc_booking_modes_quickstart_owner', absint( $context['owner_user_id'] ) );
416
417 $test_url = get_permalink( $page_id );
418 if ( ! $test_url ) {
419 return new WP_Error( 'wpbc_booking_modes_quickstart_page_url_missing', __( 'The QuickStart booking page does not have a public URL.', 'booking' ) );
420 }
421
422 return array(
423 'page_id' => absint( $page_id ),
424 'test_url' => $test_url,
425 'created' => true,
426 );
427 }
428
429 /**
430 * Run the explicit QuickStart operation registered by one mode definition.
431 *
432 * Mode switching never calls this dispatcher. Only the protected QuickStart
433 * AJAX endpoint invokes it after nonce, capability, owner, and demo checks.
434 *
435 * @param string $mode_id Appointment or Rental mode identifier.
436 *
437 * @return array<string,mixed>|WP_Error Operation result, or an error.
438 */
439 function wpbc_booking_modes_run_quickstart( $mode_id ) {
440
441 $mode_id = sanitize_key( (string) $mode_id );
442
443 if ( 'appointment' === $mode_id && function_exists( 'wpbc_booking_modes_run_appointment_quickstart' ) ) {
444 return wpbc_booking_modes_run_appointment_quickstart();
445 }
446
447 if ( 'rental' === $mode_id && function_exists( 'wpbc_booking_modes_run_rental_quickstart' ) ) {
448 return wpbc_booking_modes_run_rental_quickstart();
449 }
450
451 return new WP_Error( 'wpbc_booking_modes_quickstart_not_registered', __( 'No QuickStart operation is registered for this mode.', 'booking' ) );
452 }
453
454 /**
455 * Resolve the public test URL stored for a completed QuickStart operation.
456 *
457 * @param string $mode_id Appointment or Rental mode identifier.
458 *
459 * @return string Published same-site page URL, or an empty string.
460 */
461 function wpbc_booking_modes_get_quickstart_test_url( $mode_id ) {
462
463 $state = wpbc_booking_modes_get_quickstart_state( $mode_id );
464 $page_id = ! empty( $state['page_id'] ) ? absint( $state['page_id'] ) : 0;
465
466 if ( ! $page_id || 'publish' !== get_post_status( $page_id ) ) {
467 return '';
468 }
469
470 $test_url = get_permalink( $page_id );
471
472 return $test_url ? wp_validate_redirect( $test_url, '' ) : '';
473 }
474