PluginProbe
Booking Calendar / 11.8.3
Booking Calendar v11.8.3
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-setup / first-run-popup.php

first-run-popup.php in Booking Calendar 11.8.3, at includes/page-setup/first-run-popup.php

492 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * First-install onboarding prompts on the Booking Listing page.
4 *
5 * @package Booking Calendar
6 * @since 11.8.1
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Coordinate first-run prompt eligibility, assets, templates, and dismissal.
15 *
16 * The Setup domain owns the persisted state and markup. The Booking Listing is
17 * only the route where this non-mutating entry prompt is presented.
18 *
19 * @since 11.8.1
20 */
21 final class WPBC_Setup_Wizard_First_Run_Popup {
22
23 /**
24 * Logged-in WordPress AJAX action used to consume the Setup prompt.
25 *
26 * @var string
27 */
28 const AJAX_ACTION = 'WPBC_AJX_SETUP_WIZARD_FIRST_RUN_PROMPT';
29
30 /**
31 * Logged-in WordPress AJAX action used to consume the booking-pages prompt.
32 *
33 * @var string
34 */
35 const BOOKING_PAGES_AJAX_ACTION = 'WPBC_AJX_SETUP_WIZARD_BOOKING_PAGES_PROMPT';
36
37 /**
38 * Nonce action protecting prompt state changes.
39 *
40 * @var string
41 */
42 const NONCE_ACTION = 'wpbc_setup_wizard_first_run_prompt_wpbcnonce';
43
44 /**
45 * Nonce action protecting booking-pages prompt state changes.
46 *
47 * @var string
48 */
49 const BOOKING_PAGES_NONCE_ACTION = 'wpbc_setup_wizard_booking_pages_prompt_wpbcnonce';
50
51 /**
52 * WordPress script handle for the route-scoped browser adapter.
53 *
54 * @var string
55 */
56 const SCRIPT_HANDLE = 'wpbc-setup-wizard-first-run-popup';
57
58 /**
59 * WordPress style handle for the route-scoped popup presentation.
60 *
61 * @var string
62 */
63 const STYLE_HANDLE = 'wpbc-setup-wizard-first-run-popup';
64
65 /**
66 * Whether route-scoped assets were configured during this request.
67 *
68 * @var bool
69 */
70 private static $assets_configured = false;
71
72 /**
73 * Register prompt assets, allow-listed templates, and AJAX endpoints.
74 *
75 * @return void
76 */
77 public static function init() {
78
79 add_action( 'wpbc_enqueue_js_files', array( __CLASS__, 'enqueue_js_files' ), 60 );
80 add_action( 'wpbc_enqueue_css_files', array( __CLASS__, 'enqueue_css_files' ), 60 );
81 add_action( 'wpbc_hook_settings_page_footer', array( __CLASS__, 'render_template' ) );
82 add_action( 'wp_ajax_' . self::AJAX_ACTION, array( __CLASS__, 'ajax_dismiss' ) );
83 add_action( 'wp_ajax_' . self::BOOKING_PAGES_AJAX_ACTION, array( __CLASS__, 'ajax_dismiss_booking_pages_prompt' ) );
84 }
85
86 /**
87 * Check whether the current administrator can use Initial Setup.
88 *
89 * This mirrors the Setup menu's role and MultiUser ownership boundaries. The
90 * AJAX endpoint repeats the same check because browser visibility is never an
91 * authorization boundary.
92 *
93 * @return bool True when the current user may access Setup.
94 */
95 private static function current_user_can_access() {
96
97 if ( ! wpbc_is_user_can_access_wizard_page() ) {
98 return false;
99 }
100
101 $minimum_role = get_bk_option( 'booking_user_role_settings' );
102
103 return wpbc_is_current_user_have_this_role( $minimum_role );
104 }
105
106 /**
107 * Check whether the first-run prompt remains pending for this site.
108 *
109 * @return bool True when the prompt belongs to an unfinished first install.
110 */
111 private static function is_setup_prompt_pending() {
112
113 return wpbc_setup_wizard_is_initial_install_site()
114 && 'On' === get_bk_option( 'booking_setup_wizard_first_run_prompt' )
115 && 'On' !== get_bk_option( 'booking_setup_wizard_page_is_completed' );
116 }
117
118 /**
119 * Check whether the booking-pages prompt is enabled for this first install.
120 *
121 * This deliberately does not inspect the Setup prompt flag. The browser needs
122 * the cards preloaded so it can open the second dialog immediately after a
123 * successful first-dialog dismissal.
124 *
125 * @return bool True when this installation owns an unconsumed pages prompt.
126 */
127 private static function is_booking_pages_prompt_enabled() {
128
129 return wpbc_setup_wizard_is_initial_install_site()
130 && 'On' === get_bk_option( 'booking_setup_wizard_booking_pages_prompt' );
131 }
132
133 /**
134 * Check whether the booking-pages prompt is next in the first-run sequence.
135 *
136 * @return bool True after the Setup prompt has been consumed.
137 */
138 private static function is_booking_pages_prompt_pending() {
139
140 return self::is_booking_pages_prompt_enabled()
141 && 'Off' === get_bk_option( 'booking_setup_wizard_first_run_prompt' )
142 && ! empty( self::get_booking_page_cards() );
143 }
144
145 /**
146 * Get presentation metadata for valid public starter booking pages.
147 *
148 * Page existence and shortcode validation remain owned by the publishing
149 * module. This Setup-owned mapper adds only translated onboarding copy and
150 * resolved local previews for the allow-listed starter page purposes.
151 *
152 * @return array<int,array<string,string>> JSON-safe card records.
153 */
154 private static function get_booking_page_cards() {
155
156 static $booking_page_cards_by_site = array();
157
158 $site_id = get_current_blog_id();
159
160 if ( array_key_exists( $site_id, $booking_page_cards_by_site ) ) {
161 return $booking_page_cards_by_site[ $site_id ];
162 }
163
164 $booking_page_cards_by_site[ $site_id ] = array();
165
166 if ( ! function_exists( 'wpbc_get_published_activation_booking_pages' ) ) {
167 return $booking_page_cards_by_site[ $site_id ];
168 }
169
170 $published_pages = wpbc_get_published_activation_booking_pages();
171 if ( empty( $published_pages ) || ! is_array( $published_pages ) ) {
172 return $booking_page_cards_by_site[ $site_id ];
173 }
174
175 $page_presentations = array(
176 'full_day_booking' => array(
177 'template_key' => 'dates_2_columns_hints_full_days',
178 'description' => __( 'Try a date-based booking for stays, rentals, events, or other full-day reservations.', 'booking' ),
179 ),
180 'appointment_booking' => array(
181 'template_key' => 'appointments_services_flow',
182 'description' => __( 'Try the guided service and appointment booking experience.', 'booking' ),
183 ),
184 'time_slots_booking' => array(
185 'template_key' => 'time_slots_2_columns_hints',
186 'description' => __( 'Try a two-step time-slot booking with date and time selection followed by customer details.', 'booking' ),
187 ),
188 'resource_selector_booking' => array(
189 'image_url' => 'https://wpbookingcalendar.com/assets/template-img/wp_booking_calendar__resource_selection_card.png',
190 'description' => __( 'Choose a Booking Resource first, then continue through its booking form.', 'booking' ),
191 ),
192 'contact_form' => array(
193 'template_key' => 'contact_form_simple',
194 'description' => __( 'Preview the simple inquiry form included with your starter pages.', 'booking' ),
195 ),
196 );
197
198 foreach ( $page_presentations as $page_key => $page_presentation ) {
199 if ( empty( $published_pages[ $page_key ] ) || ! is_array( $published_pages[ $page_key ] ) ) {
200 continue;
201 }
202
203 $published_page = $published_pages[ $page_key ];
204 $image_url = isset( $page_presentation['image_url'] )
205 ? esc_url_raw( (string) $page_presentation['image_url'] )
206 : '';
207 $template_key = isset( $page_presentation['template_key'] )
208 ? sanitize_key( (string) $page_presentation['template_key'] )
209 : '';
210 $template_record = '' !== $template_key && function_exists( 'wpbc_get_bfb_template_record_by_key' )
211 ? wpbc_get_bfb_template_record_by_key( $template_key )
212 : array();
213
214 if (
215 '' === $image_url
216 && ! empty( $template_record['picture_url'] )
217 && function_exists( 'wpbc_bfb_resolve_picture_url' )
218 ) {
219 $image_url = esc_url_raw( wpbc_bfb_resolve_picture_url( $template_record['picture_url'] ) );
220 }
221
222 $page_title = isset( $published_page['page_title'] ) ? (string) $published_page['page_title'] : '';
223 /* translators: %s: starter booking page title. */
224 $image_alt = sprintf( __( 'Preview of %s', 'booking' ), $page_title );
225 /* translators: %s: starter booking page title. */
226 $button_title = sprintf( __( 'Open %s', 'booking' ), $page_title );
227
228 $booking_page_cards_by_site[ $site_id ][] = array(
229 'key' => $page_key,
230 'url' => esc_url_raw( (string) $published_page['url'] ),
231 'page_title' => $page_title,
232 'button_title' => $button_title,
233 'description' => $page_presentation['description'],
234 'image_url' => $image_url,
235 'image_alt' => $image_alt,
236 );
237 }
238
239 return $booking_page_cards_by_site[ $site_id ];
240 }
241
242 /**
243 * Check whether the current authorized user can manually reopen starter pages.
244 *
245 * Automatic display remains controlled by the one-time prompt option. This
246 * separate read-only capability keeps the approved starter-page window
247 * available from the first-install welcome panel after that option is Off.
248 * Updates remain excluded by the persisted initial-install marker. Live demo
249 * sites are the only presentation-only exception: they may open existing demo
250 * pages manually, but never enable either automatic prompt. No missing page is
251 * created or repaired while evaluating this method. The Booking Listing route
252 * is part of this decision so the launcher is never emitted on Timeline where
253 * its route-scoped assets are intentionally absent.
254 *
255 * @return bool True when at least one validated starter page can be shown.
256 */
257 public static function can_manually_open_booking_pages_dialog() {
258
259 $is_initial_install_or_demo = wpbc_setup_wizard_is_initial_install_site()
260 || ( function_exists( 'wpbc_is_this_demo' ) && wpbc_is_this_demo() );
261
262 return self::is_booking_listing_route()
263 && $is_initial_install_or_demo
264 && ! empty( self::get_booking_page_cards() );
265 }
266
267 /**
268 * Check whether the current request is the Booking Listing route.
269 *
270 * The Booking Listing controller normalizes an omitted tab into `$_REQUEST`
271 * before administration assets are enqueued. Reading that normalized value
272 * avoids loading the interface over a saved Timeline default.
273 *
274 * @return bool True when the current authorized request is Booking Listing.
275 */
276 private static function is_booking_listing_route() {
277
278 if ( ! is_admin() || ! wpbc_is_bookings_page() || ! self::current_user_can_access() ) {
279 return false;
280 }
281
282 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only route selection.
283 $requested_tab = isset( $_REQUEST['tab'] ) && is_scalar( $_REQUEST['tab'] )
284 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only route selection.
285 ? sanitize_key( wp_unslash( $_REQUEST['tab'] ) )
286 : '';
287
288 return in_array( $requested_tab, array( '', 'vm_booking_listing' ), true );
289 }
290
291 /**
292 * Check whether the current request needs the popup interface.
293 *
294 * A pending first-run prompt can open automatically. After the starter-pages
295 * prompt is consumed, the same interface remains available only through the
296 * explicit welcome-panel button. Both paths retain the exact route, user, and
297 * first-install boundaries.
298 *
299 * @return bool True when assets and allow-listed templates are required.
300 */
301 private static function should_render() {
302
303 if ( ! self::is_booking_listing_route() ) {
304 return false;
305 }
306
307 return self::is_setup_prompt_pending()
308 || self::can_manually_open_booking_pages_dialog();
309 }
310
311 /**
312 * Enqueue the thin browser adapter on an eligible Booking Listing request.
313 *
314 * @param string $where_to_load Booking Calendar asset context.
315 *
316 * @return void
317 */
318 public static function enqueue_js_files( $where_to_load ) {
319
320 if ( self::$assets_configured || ! in_array( $where_to_load, array( 'admin', 'both' ), true ) || ! self::should_render() ) {
321 return;
322 }
323
324 $script_path = WPBC_PLUGIN_DIR . '/includes/page-setup/_out/setup_first_run_popup.js';
325 $version = file_exists( $script_path ) ? (string) filemtime( $script_path ) : WP_BK_VERSION_NUM;
326
327 wp_enqueue_script(
328 self::SCRIPT_HANDLE,
329 wpbc_plugin_url( '/includes/page-setup/_out/setup_first_run_popup.js' ),
330 array( 'jquery', 'wp-util', 'wpbc-modal' ),
331 $version,
332 true
333 );
334
335 $can_open_booking_pages = self::can_manually_open_booking_pages_dialog();
336 $booking_page_cards = $can_open_booking_pages ? self::get_booking_page_cards() : array();
337
338 wp_localize_script(
339 self::SCRIPT_HANDLE,
340 'wpbc_setup_wizard_first_run_popup_vars',
341 array(
342 'ajax_url' => admin_url( 'admin-ajax.php', 'relative' ),
343 'setup_prompt' => array(
344 'action' => self::AJAX_ACTION,
345 'nonce' => wp_create_nonce( self::NONCE_ACTION ),
346 'setup_url' => wpbc_get_setup_wizard_page_url(),
347 'show' => self::is_setup_prompt_pending(),
348 ),
349 'booking_pages_prompt' => array(
350 'action' => self::BOOKING_PAGES_AJAX_ACTION,
351 'nonce' => wp_create_nonce( self::BOOKING_PAGES_NONCE_ACTION ),
352 'available' => $can_open_booking_pages,
353 'show' => self::is_booking_pages_prompt_pending(),
354 'booking_pages' => $booking_page_cards,
355 ),
356 )
357 );
358
359 self::$assets_configured = true;
360 }
361
362 /**
363 * Enqueue route-scoped popup presentation styles.
364 *
365 * @param string $where_to_load Booking Calendar asset context.
366 *
367 * @return void
368 */
369 public static function enqueue_css_files( $where_to_load ) {
370
371 if ( ! in_array( $where_to_load, array( 'admin', 'both' ), true ) || ! self::should_render() ) {
372 return;
373 }
374
375 $style_path = WPBC_PLUGIN_DIR . '/includes/page-setup/_out/setup_first_run_popup.css';
376 $version = file_exists( $style_path ) ? (string) filemtime( $style_path ) : WP_BK_VERSION_NUM;
377
378 wp_enqueue_style(
379 self::STYLE_HANDLE,
380 wpbc_plugin_url( '/includes/page-setup/_out/setup_first_run_popup.css' ),
381 array(),
382 $version
383 );
384 }
385
386 /**
387 * Render the Setup-owned, allow-listed WordPress template.
388 *
389 * @param string $page Booking Calendar footer template context.
390 *
391 * @return void
392 */
393 public static function render_template( $page ) {
394
395 if ( 'wpbc-ajx_booking' !== $page || ! self::should_render() ) {
396 return;
397 }
398
399 $template_paths = array(
400 WPBC_PLUGIN_DIR . '/includes/page-setup/templates/first-run-popup-wptpl.php',
401 WPBC_PLUGIN_DIR . '/includes/page-setup/templates/booking-pages-popup-wptpl.php',
402 );
403
404 foreach ( $template_paths as $template_path ) {
405 if ( is_readable( $template_path ) ) {
406 require $template_path;
407 }
408 }
409 }
410
411 /**
412 * Persist that the first-run prompt must not be shown again.
413 *
414 * Both the start and dismiss choices consume the one-time prompt. Starting the
415 * wizard does not mark Setup complete; completion remains owned by its existing
416 * step workflow.
417 *
418 * @return void
419 */
420 public static function ajax_dismiss() {
421
422 if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
423 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'booking' ) ), 403 );
424 }
425
426 if ( ! self::current_user_can_access() ) {
427 wp_send_json_error( array( 'message' => __( 'You do not have access to Initial Setup.', 'booking' ) ), 403 );
428 }
429
430 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above with check_ajax_referer().
431 $choice = isset( $_POST['choice'] ) && is_scalar( $_POST['choice'] )
432 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above with check_ajax_referer().
433 ? sanitize_key( wp_unslash( $_POST['choice'] ) )
434 : '';
435
436 if ( ! in_array( $choice, array( 'dismiss', 'start' ), true ) ) {
437 wp_send_json_error( array( 'message' => __( 'The Initial Setup choice is not valid.', 'booking' ) ), 400 );
438 }
439
440 if ( ! wpbc_setup_wizard_is_initial_install_site() ) {
441 wp_send_json_error( array( 'message' => __( 'The Initial Setup prompt is not available for this installation.', 'booking' ) ), 409 );
442 }
443
444 update_bk_option( 'booking_setup_wizard_first_run_prompt', 'Off' );
445 if ( 'Off' !== get_bk_option( 'booking_setup_wizard_first_run_prompt' ) ) {
446 wp_send_json_error( array( 'message' => __( 'The Initial Setup choice could not be saved.', 'booking' ) ), 500 );
447 }
448
449 wp_send_json_success( array( 'choice' => $choice ) );
450 }
451
452 /**
453 * Permanently dismiss the first-install booking-pages prompt.
454 *
455 * The endpoint is intentionally idempotent. Closing the dialog twice because
456 * of overlapping browser events still leaves the one-time option safely Off.
457 *
458 * @return void
459 */
460 public static function ajax_dismiss_booking_pages_prompt() {
461
462 if ( ! check_ajax_referer( self::BOOKING_PAGES_NONCE_ACTION, 'nonce', false ) ) {
463 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'booking' ) ), 403 );
464 }
465
466 if ( ! self::current_user_can_access() ) {
467 wp_send_json_error( array( 'message' => __( 'You do not have access to Initial Setup.', 'booking' ) ), 403 );
468 }
469
470 if ( ! wpbc_setup_wizard_is_initial_install_site() ) {
471 wp_send_json_error( array( 'message' => __( 'The starter-pages prompt is not available for this installation.', 'booking' ) ), 409 );
472 }
473
474 if ( 'Off' === get_bk_option( 'booking_setup_wizard_booking_pages_prompt' ) ) {
475 wp_send_json_success( array( 'dismissed' => true ) );
476 }
477
478 if ( 'Off' !== get_bk_option( 'booking_setup_wizard_first_run_prompt' ) ) {
479 wp_send_json_error( array( 'message' => __( 'Finish the Initial Setup choice before dismissing starter pages.', 'booking' ) ), 409 );
480 }
481
482 update_bk_option( 'booking_setup_wizard_booking_pages_prompt', 'Off' );
483 if ( 'Off' !== get_bk_option( 'booking_setup_wizard_booking_pages_prompt' ) ) {
484 wp_send_json_error( array( 'message' => __( 'The starter-pages choice could not be saved.', 'booking' ) ), 500 );
485 }
486
487 wp_send_json_success( array( 'dismissed' => true ) );
488 }
489 }
490
491 WPBC_Setup_Wizard_First_Run_Popup::init();
492