PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 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 All 205 releases
booking / includes / page-setup-wizard / booking-pages / class-wpbc-setup-wizard-booking-pages.php

class-wpbc-setup-wizard-booking-pages.php in Booking Calendar 11.9, at includes/page-setup-wizard/booking-pages/class-wpbc-setup-wizard-booking-pages.php

604 lines 21.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Published booking-page discovery and dialog component.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Own published booking-page discovery, presentation, and prompt dismissal.
14 *
15 * The component is reusable from Booking Listing and completed Setup Overview.
16 * It returns only authorized, JSON-safe card data and renders a fixed,
17 * server-owned WordPress template. It never creates, updates, or repairs pages.
18 */
19 final class WPBC_Setup_Wizard_Booking_Pages {
20
21 /** Logged-in AJAX action used to consume the first-install pages prompt. */
22 const AJAX_ACTION = 'WPBC_AJX_SETUP_WIZARD_BOOKING_PAGES_PROMPT';
23
24 /** Nonce action protecting the pages-prompt option update. */
25 const NONCE_ACTION = 'wpbc_setup_wizard_booking_pages_prompt_wpbcnonce';
26
27 /** Runtime browser adapter handle. */
28 const SCRIPT_HANDLE = 'wpbc-setup-wizard-booking-pages';
29
30 /** Runtime presentation handle. */
31 const STYLE_HANDLE = 'wpbc-setup-wizard-booking-pages';
32
33 /** Site-local user option recording that automatic display was consumed. */
34 const USER_DISMISSAL_OPTION = 'booking_setup_wizard_booking_pages_prompt_dismissed';
35
36 /** @var bool Whether dialog configuration was localized in this request. */
37 private static $assets_configured = false;
38
39 /** @var bool Whether the allow-listed dialog template was rendered. */
40 private static $template_rendered = false;
41
42 /**
43 * Register route-scoped assets, template rendering, and the AJAX endpoint.
44 *
45 * @return void
46 */
47 public static function register() {
48 add_action( 'wpbc_enqueue_js_files', array( __CLASS__, 'enqueue_booking_listing_scripts' ), 50 );
49 add_action( 'wpbc_enqueue_css_files', array( __CLASS__, 'enqueue_booking_listing_styles' ), 50 );
50 add_action( 'wpbc_hook_settings_page_footer', array( __CLASS__, 'render_booking_listing_template' ) );
51 add_action( 'wp_ajax_' . self::AJAX_ACTION, array( __CLASS__, 'ajax_dismiss_prompt' ) );
52 }
53
54 /**
55 * Check whether the current user may access Setup-owned administration UI.
56 *
57 * @return bool True when access is allowed.
58 */
59 private static function current_user_can_access() {
60 return WPBC_Setup_Wizard_Access::current_user_can_access();
61 }
62
63 /**
64 * Check whether the current request is the Booking Listing route.
65 *
66 * @return bool True for the authorized Booking Listing request.
67 */
68 public static function is_booking_listing_route() {
69 if ( ! is_admin() || ! wpbc_is_bookings_page() || ! self::current_user_can_access() ) {
70 return false;
71 }
72
73 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only route selection.
74 $requested_tab = isset( $_REQUEST['tab'] ) && is_scalar( $_REQUEST['tab'] )
75 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only route selection.
76 ? sanitize_key( wp_unslash( $_REQUEST['tab'] ) )
77 : '';
78
79 return in_array( $requested_tab, array( '', 'vm_booking_listing' ), true );
80 }
81
82 /**
83 * Check whether automatic booking-page prompting remains enabled.
84 *
85 * @return bool True when the preserved first-install option is On.
86 */
87 private static function is_prompt_enabled() {
88 return WPBC_Setup_Wizard_First_Install_State::is_initial_install_site()
89 && 'On' === get_bk_option( 'booking_setup_wizard_booking_pages_prompt' )
90 && ! self::is_prompt_dismissed_for_current_user();
91 }
92
93 /**
94 * Return the real authenticated user that owns this presentation preference.
95 *
96 * The published-pages prompt is a per-user Booking Listing preference, not an
97 * effective MultiUser owner's Setup checkpoint. The site-local user-option API
98 * supplies multisite isolation without accepting a browser-provided user ID.
99 *
100 * @return int Real authenticated WordPress user ID, or zero when unavailable.
101 */
102 private static function get_prompt_user_id() {
103 $storage_context = WPBC_Setup_Wizard_Access::get_storage_context();
104
105 return isset( $storage_context['real_user_id'] ) ? absint( $storage_context['real_user_id'] ) : 0;
106 }
107
108 /**
109 * Check whether the current user already closed the automatic pages dialog.
110 *
111 * @return bool True when automatic display was consumed for this user/site.
112 */
113 private static function is_prompt_dismissed_for_current_user() {
114 $user_id = self::get_prompt_user_id();
115
116 return $user_id && '1' === (string) get_user_option( self::USER_DISMISSAL_OPTION, $user_id );
117 }
118
119 /**
120 * Persist automatic-dialog dismissal for the current user and site.
121 *
122 * WordPress can return false when an identical value already exists. Verify
123 * storage after the write so an idempotent close is still treated as success.
124 *
125 * @return bool True when the site-local user preference is stored.
126 */
127 private static function persist_prompt_dismissal_for_current_user() {
128 $user_id = self::get_prompt_user_id();
129 if ( ! $user_id ) {
130 return false;
131 }
132
133 $was_updated = update_user_option( $user_id, self::USER_DISMISSAL_OPTION, '1', false );
134
135 return false !== $was_updated || '1' === (string) get_user_option( self::USER_DISMISSAL_OPTION, $user_id );
136 }
137
138 /**
139 * Check whether the pages dialog may follow a dismissed Setup invitation.
140 *
141 * This intentionally does not require the first-run option to be Off because
142 * the browser calls it only after the authenticated dismissal succeeds.
143 *
144 * @return bool True when an unconsumed pages prompt has usable cards.
145 */
146 public static function can_follow_setup_prompt() {
147 return self::is_prompt_enabled() && ! empty( self::get_booking_page_cards() );
148 }
149
150 /**
151 * Check whether the one-time pages dialog should open on page load.
152 *
153 * @return bool True after the Setup invitation has been consumed.
154 */
155 private static function is_prompt_pending() {
156 return self::can_follow_setup_prompt()
157 && 'Off' === get_bk_option( 'booking_setup_wizard_first_run_prompt' );
158 }
159
160 /**
161 * Get presentation metadata for the Setup-created page and starter examples.
162 *
163 * @return array<int,array<string,mixed>> JSON-safe card records.
164 */
165 public static function get_booking_page_cards() {
166 static $booking_page_cards_by_context = array();
167
168 $storage_context = WPBC_Setup_Wizard_Access::get_storage_context();
169 $cache_key = absint( get_current_blog_id() ) . ':' . ( isset( $storage_context['owner_user_id'] ) ? absint( $storage_context['owner_user_id'] ) : 0 );
170 if ( array_key_exists( $cache_key, $booking_page_cards_by_context ) ) {
171 return $booking_page_cards_by_context[ $cache_key ];
172 }
173
174 $page_records = array();
175 $setup_page_record = self::get_setup_created_page_record();
176 if ( ! empty( $setup_page_record ) ) {
177 $page_records[] = $setup_page_record;
178 }
179
180 $page_presentations = array(
181 'full_day_booking' => array(
182 'template_key' => 'dates_2_columns_hints_full_days',
183 'description' => __( 'Try a date-based booking for stays, rentals, events, or other full-day reservations.', 'booking' ),
184 ),
185 'appointment_booking' => array(
186 'template_key' => 'appointments_services_selection_summary',
187 'description' => __( 'Try the guided service and appointment booking experience.', 'booking' ),
188 ),
189 'time_slots_booking' => array(
190 'template_key' => 'time_slots_2_columns_hints',
191 'description' => __( 'Try a two-step time-slot booking with date and time selection followed by customer details.', 'booking' ),
192 ),
193 'resource_selector_booking' => array(
194 'image_url' => 'https://wpbookingcalendar.com/assets/template-img/wp_booking_calendar__resource_selection_card.png',
195 'description' => __( 'Choose a Booking Resource first, then continue through its booking form.', 'booking' ),
196 ),
197 'contact_form' => array(
198 'template_key' => 'contact_form_simple',
199 'description' => __( 'Preview the simple inquiry form included with your starter pages.', 'booking' ),
200 ),
201 );
202
203 if ( function_exists( 'wpbc_get_published_activation_booking_pages' ) ) {
204 $published_pages = wpbc_get_published_activation_booking_pages();
205 foreach ( is_array( $published_pages ) ? $page_presentations : array() as $page_key => $page_presentation ) {
206 if ( empty( $published_pages[ $page_key ] ) || ! is_array( $published_pages[ $page_key ] ) ) {
207 continue;
208 }
209
210 $page_records[] = array_merge(
211 $published_pages[ $page_key ],
212 $page_presentation,
213 array(
214 'key' => $page_key,
215 'section' => 'examples',
216 )
217 );
218 }
219 }
220
221 $booking_page_cards_by_context[ $cache_key ] = self::prepare_booking_page_cards( $page_records );
222
223 return $booking_page_cards_by_context[ $cache_key ];
224 }
225
226 /**
227 * Build the primary card for the latest public page created by Setup Wizard.
228 *
229 * Page discovery is delegated to the publishing domain so this presentation
230 * layer cannot drift from its owner- and multisite-scoped creation markers.
231 * A Form Builder slug is read only from the server-owned managed shortcode and
232 * is used solely to resolve the matching preview image.
233 *
234 * @return array<string,mixed> Authorized page record, or an empty array.
235 */
236 private static function get_setup_created_page_record() {
237 if ( ! class_exists( 'WPBC_Setup_Wizard_Publish_Integration' ) ) {
238 return array();
239 }
240
241 $publish_integration = new WPBC_Setup_Wizard_Publish_Integration();
242 $page = $publish_integration->get_latest_created_page();
243 if ( ! $page || empty( $page->ID ) ) {
244 return array();
245 }
246
247 $page_url = get_permalink( $page->ID );
248 $page_title = wp_strip_all_tags( get_the_title( $page->ID ) );
249 if ( ! is_string( $page_url ) || '' === $page_url || '' === $page_title ) {
250 return array();
251 }
252
253 $template_key = '';
254 if ( preg_match( '/\bform_type\s*=\s*(["\'])([^"\']+)\1/i', (string) $page->post_content, $matches ) ) {
255 $template_key = sanitize_key( $matches[2] );
256 }
257
258 $page_record = array(
259 'key' => 'setup_wizard_booking_page',
260 'section' => 'setup',
261 'url' => $page_url,
262 'page_title' => $page_title,
263 'button_title' => __( 'Open your booking page', 'booking' ),
264 'description' => __( 'Created from the customer journey, Booking Form, appearance, and publishing choices you saved in Setup Wizard.', 'booking' ),
265 'template_key' => $template_key,
266 );
267
268 $published_content_id = (string) get_post_meta(
269 $page->ID,
270 WPBC_Setup_Wizard_Publish_Integration::META_CONTENT,
271 true
272 );
273 if ( WPBC_Setup_Wizard_Publish_Integration::CONTENT_RESOURCE_SELECTION === $published_content_id ) {
274 $page_record['image_url'] = 'https://wpbookingcalendar.com/assets/template-img/wp_booking_calendar__resource_selection_card.png';
275 }
276
277 return $page_record;
278 }
279
280 /**
281 * Normalize authorized published-page records for picture-card presentation.
282 *
283 * @param array<int,array<string,mixed>> $page_records Authorized page records.
284 *
285 * @return array<int,array<string,mixed>> Normalized card records.
286 */
287 public static function prepare_booking_page_cards( array $page_records ) {
288 $booking_page_cards = array();
289
290 foreach ( $page_records as $page_record ) {
291 if ( ! is_array( $page_record ) ) {
292 continue;
293 }
294
295 $page_url = isset( $page_record['url'] ) && is_scalar( $page_record['url'] ) ? esc_url_raw( (string) $page_record['url'] ) : '';
296 $page_title = isset( $page_record['page_title'] ) && is_scalar( $page_record['page_title'] ) ? sanitize_text_field( (string) $page_record['page_title'] ) : '';
297 if ( '' === $page_url || '' === $page_title ) {
298 continue;
299 }
300
301 $image_url = isset( $page_record['image_url'] ) && is_scalar( $page_record['image_url'] ) ? esc_url_raw( (string) $page_record['image_url'] ) : '';
302 $template_key = isset( $page_record['template_key'] ) && is_scalar( $page_record['template_key'] ) ? sanitize_key( (string) $page_record['template_key'] ) : '';
303 if ( '' === $image_url && '' !== $template_key ) {
304 $template_record = self::get_template_record_by_key_or_slug( $template_key );
305 if ( ! empty( $template_record['picture_url'] ) && function_exists( 'wpbc_bfb_resolve_picture_url' ) ) {
306 $image_url = esc_url_raw( wpbc_bfb_resolve_picture_url( $template_record['picture_url'] ) );
307 }
308 }
309
310 $description = isset( $page_record['description'] ) && is_scalar( $page_record['description'] )
311 ? sanitize_text_field( (string) $page_record['description'] )
312 : __( 'Open this published page and try its booking experience.', 'booking' );
313 $image_alt = isset( $page_record['image_alt'] ) && is_scalar( $page_record['image_alt'] ) ? sanitize_text_field( (string) $page_record['image_alt'] ) : '';
314 if ( '' === $image_alt ) {
315 /* translators: %s: published booking page title. */
316 $image_alt = sprintf( __( 'Preview of %s', 'booking' ), $page_title );
317 }
318
319 $button_title = isset( $page_record['button_title'] ) && is_scalar( $page_record['button_title'] ) ? sanitize_text_field( (string) $page_record['button_title'] ) : '';
320 if ( '' === $button_title ) {
321 /* translators: %s: published booking page title. */
322 $button_title = sprintf( __( 'Open %s', 'booking' ), $page_title );
323 }
324
325 $booking_page_cards[] = array(
326 'key' => isset( $page_record['key'] ) && is_scalar( $page_record['key'] ) ? sanitize_key( (string) $page_record['key'] ) : '',
327 'section' => isset( $page_record['section'] ) && 'setup' === sanitize_key( (string) $page_record['section'] ) ? 'setup' : 'examples',
328 'url' => $page_url,
329 'page_title' => $page_title,
330 'button_title' => $button_title,
331 'description' => $description,
332 'image_url' => $image_url,
333 'image_alt' => $image_alt,
334 );
335 }
336
337 return $booking_page_cards;
338 }
339
340 /**
341 * Resolve one allow-listed Form Builder template by key or form slug.
342 *
343 * @param string $template_identifier Sanitized template key or form slug.
344 *
345 * @return array<string,mixed> Bundled template record, or an empty array.
346 */
347 private static function get_template_record_by_key_or_slug( $template_identifier ) {
348 $template_identifier = sanitize_key( (string) $template_identifier );
349 if ( '' === $template_identifier ) {
350 return array();
351 }
352
353 if ( function_exists( 'wpbc_get_bfb_template_record_by_key' ) ) {
354 $template_record = wpbc_get_bfb_template_record_by_key( $template_identifier );
355 if ( ! empty( $template_record ) && is_array( $template_record ) ) {
356 return $template_record;
357 }
358 }
359
360 if ( ! function_exists( 'wpbc_bfb_activation__get_templates_registry' ) || ! function_exists( 'wpbc_bfb_activation__normalize_template_config' ) ) {
361 return array();
362 }
363
364 foreach ( (array) wpbc_bfb_activation__get_templates_registry() as $template_config ) {
365 $template_config = wpbc_bfb_activation__normalize_template_config( $template_config );
366 $template_record = isset( $template_config['record'] ) && is_array( $template_config['record'] ) ? $template_config['record'] : array();
367 $template_slug = isset( $template_record['form_slug'] ) && is_scalar( $template_record['form_slug'] ) ? sanitize_title( (string) $template_record['form_slug'] ) : '';
368 if ( $template_identifier === $template_slug ) {
369 return $template_record;
370 }
371 }
372
373 return array();
374 }
375
376 /**
377 * Check whether Booking Listing may manually reopen validated starter pages.
378 *
379 * @return bool True when at least one page is available on the allowed route.
380 */
381 public static function can_manually_open_booking_pages_dialog() {
382 $is_initial_install_or_demo = WPBC_Setup_Wizard_First_Install_State::is_initial_install_site()
383 || WPBC_Setup_Wizard_Environment_Policy::is_live_demo();
384
385 return self::is_booking_listing_route()
386 && $is_initial_install_or_demo
387 && ! empty( self::get_booking_page_cards() );
388 }
389
390 /**
391 * Check whether Booking Listing requires the dialog component.
392 *
393 * @return bool True when automatic or manual dialog use is available.
394 */
395 private static function should_render_booking_listing() {
396 return self::is_booking_listing_route()
397 && ( self::is_prompt_pending() || self::can_manually_open_booking_pages_dialog() );
398 }
399
400 /**
401 * Enqueue and configure the dialog for an authorized manual caller.
402 *
403 * @param array<int,array<string,mixed>> $booking_page_cards Authorized cards.
404 *
405 * @return bool True when the dialog was configured.
406 */
407 public static function enqueue_manual_dialog( array $booking_page_cards ) {
408 if ( self::$assets_configured ) {
409 return true;
410 }
411
412 $booking_page_cards = self::prepare_booking_page_cards( $booking_page_cards );
413 if ( empty( $booking_page_cards ) ) {
414 return false;
415 }
416
417 self::enqueue_script();
418 self::enqueue_style();
419 self::localize_script(
420 $booking_page_cards,
421 false,
422 '',
423 '',
424 __( 'Open a published booking page in a new tab and try the booking experience as a visitor.', 'booking' ),
425 __( 'Close', 'booking' )
426 );
427
428 return true;
429 }
430
431 /**
432 * Enqueue and configure the component on Booking Listing.
433 *
434 * @param string $where_to_load Booking Calendar asset context.
435 *
436 * @return void
437 */
438 public static function enqueue_booking_listing_scripts( $where_to_load ) {
439 if ( self::$assets_configured || ! in_array( $where_to_load, array( 'admin', 'both' ), true ) || ! self::should_render_booking_listing() ) {
440 return;
441 }
442
443 $booking_page_cards = self::get_booking_page_cards();
444 self::enqueue_script();
445 self::localize_script(
446 $booking_page_cards,
447 self::is_prompt_pending(),
448 self::AJAX_ACTION,
449 wp_create_nonce( self::NONCE_ACTION ),
450 __( 'Open your configured booking page or a starter example in a new tab and try the booking experience as a visitor.', 'booking' ),
451 __( 'Continue to Booking Listing', 'booking' )
452 );
453 }
454
455 /**
456 * Enqueue component styles on Booking Listing.
457 *
458 * @param string $where_to_load Booking Calendar asset context.
459 *
460 * @return void
461 */
462 public static function enqueue_booking_listing_styles( $where_to_load ) {
463 if ( ! in_array( $where_to_load, array( 'admin', 'both' ), true ) || ! self::should_render_booking_listing() ) {
464 return;
465 }
466
467 self::enqueue_style();
468 }
469
470 /**
471 * Localize one authoritative, JSON-safe dialog configuration.
472 *
473 * @param array<int,array<string,mixed>> $booking_page_cards Normalized cards.
474 * @param bool $show Whether to open on load.
475 * @param string $action Optional AJAX action.
476 * @param string $nonce Optional AJAX nonce.
477 * @param string $description Dialog description.
478 * @param string $dismiss_label Dialog close-button label.
479 *
480 * @return void
481 */
482 private static function localize_script( array $booking_page_cards, $show, $action, $nonce, $description, $dismiss_label ) {
483 wp_localize_script(
484 self::SCRIPT_HANDLE,
485 'wpbc_setup_wizard_booking_pages_vars',
486 array(
487 'ajax_url' => admin_url( 'admin-ajax.php', 'relative' ),
488 'action' => (string) $action,
489 'nonce' => (string) $nonce,
490 'show' => (bool) $show,
491 'booking_pages' => $booking_page_cards,
492 'description' => sanitize_text_field( $description ),
493 'dismiss_label' => sanitize_text_field( $dismiss_label ),
494 )
495 );
496
497 self::$assets_configured = true;
498 }
499
500 /**
501 * Enqueue the compiled browser adapter.
502 *
503 * @return void
504 */
505 private static function enqueue_script() {
506 $script_path = __DIR__ . '/_out/booking-pages-dialog.js';
507 $version = file_exists( $script_path ) ? (string) filemtime( $script_path ) : WP_BK_VERSION_NUM;
508
509 wp_enqueue_script(
510 self::SCRIPT_HANDLE,
511 wpbc_plugin_url( '/includes/page-setup-wizard/booking-pages/_out/booking-pages-dialog.js' ),
512 array( 'jquery', 'wp-util', 'wpbc-modal' ),
513 $version,
514 true
515 );
516 }
517
518 /**
519 * Enqueue the compiled dialog presentation.
520 *
521 * @return void
522 */
523 private static function enqueue_style() {
524 $style_path = __DIR__ . '/_out/booking-pages-dialog.css';
525 $version = file_exists( $style_path ) ? (string) filemtime( $style_path ) : WP_BK_VERSION_NUM;
526
527 wp_enqueue_style(
528 self::STYLE_HANDLE,
529 wpbc_plugin_url( '/includes/page-setup-wizard/booking-pages/_out/booking-pages-dialog.css' ),
530 array(),
531 $version
532 );
533 }
534
535 /**
536 * Render the allow-listed template in the Booking Listing footer.
537 *
538 * @param string $page Booking Calendar footer context.
539 *
540 * @return void
541 */
542 public static function render_booking_listing_template( $page ) {
543 if ( 'wpbc-ajx_booking' !== $page || ! self::should_render_booking_listing() ) {
544 return;
545 }
546
547 self::render_template();
548 }
549
550 /**
551 * Render the fixed dialog template once.
552 *
553 * @return bool True when the template is available or already rendered.
554 */
555 public static function render_template() {
556 if ( self::$template_rendered ) {
557 return true;
558 }
559
560 $template_path = __DIR__ . '/templates/booking-pages-popup-wptpl.php';
561 if ( ! is_readable( $template_path ) ) {
562 return false;
563 }
564
565 require $template_path;
566 self::$template_rendered = true;
567
568 return true;
569 }
570
571 /**
572 * Persist dismissal of the first-install pages prompt.
573 *
574 * @return void
575 */
576 public static function ajax_dismiss_prompt() {
577 if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
578 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'booking' ) ), 403 );
579 }
580
581 if ( ! self::current_user_can_access() ) {
582 wp_send_json_error( array( 'message' => __( 'You do not have access to Initial Setup.', 'booking' ) ), 403 );
583 }
584
585 if ( ! WPBC_Setup_Wizard_First_Install_State::is_initial_install_site() ) {
586 wp_send_json_error( array( 'message' => __( 'The starter-pages prompt is not available for this installation.', 'booking' ) ), 409 );
587 }
588
589 if ( self::is_prompt_dismissed_for_current_user() ) {
590 wp_send_json_success( array( 'dismissed' => true ) );
591 }
592
593 if ( 'Off' !== get_bk_option( 'booking_setup_wizard_first_run_prompt' ) ) {
594 wp_send_json_error( array( 'message' => __( 'Finish the Initial Setup choice before dismissing starter pages.', 'booking' ) ), 409 );
595 }
596
597 if ( ! self::persist_prompt_dismissal_for_current_user() ) {
598 wp_send_json_error( array( 'message' => __( 'The starter-pages choice could not be saved.', 'booking' ) ), 500 );
599 }
600
601 wp_send_json_success( array( 'dismissed' => true ) );
602 }
603 }
604