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-form-builder / publish / class-wpbc-bfb-publish-ajax.php

class-wpbc-bfb-publish-ajax.php in Booking Calendar 11.9, at includes/page-form-builder/publish/class-wpbc-bfb-publish-ajax.php

629 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * BFB AJAX Publish controller.
4 *
5 * Publishes the current booking form shortcode into:
6 * - an existing page, or
7 * - a new page
8 *
9 * This controller is intentionally thin and reuses the generic page/shortcode
10 * helper functions that already exist in Booking Calendar.
11 *
12 * Expected request args:
13 * - nonce
14 * - publish_mode : create | edit
15 * - resource_id
16 * - form_name
17 * - shortcode_raw
18 * - page_id (for edit)
19 * - page_title (for create)
20 *
21 * @package Booking Calendar
22 * @subpackage Booking Form Builder
23 * @since 11.0.0
24 * @file ../includes/page-form-builder/publish/class-wpbc-bfb-publish-ajax.php
25 */
26
27 if ( ! defined( 'ABSPATH' ) ) {
28 exit;
29 }
30
31 class WPBC_BFB_Publish_Ajax {
32
33 const ACTION = 'WPBC_AJX_BFB_PUBLISH_FORM';
34 const NONCE_ACTION = 'wpbc_bfb_publish_form';
35
36 /**
37 * Init hooks.
38 *
39 * @return void
40 */
41 public static function init() {
42 add_action( 'wp_ajax_' . self::ACTION, array( __CLASS__, 'ajax_publish_form' ) );
43 }
44
45 /**
46 * Send JSON error and finish.
47 *
48 * @param string $message Error message.
49 *
50 * @return void
51 */
52 private static function send_error( $message ) {
53 wp_send_json_error(
54 array(
55 'message' => $message,
56 )
57 );
58 }
59
60 /**
61 * Get text request value.
62 *
63 * @param string $key Request key.
64 *
65 * @return string
66 */
67 private static function get_request_text( $key ) {
68
69 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
70 if ( ! isset( $_POST[ $key ] ) ) {
71 return '';
72 }
73 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
74 return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
75 }
76
77 /**
78 * Get raw request value.
79 *
80 * @param string $key Request key.
81 *
82 * @return string
83 */
84 private static function get_request_raw( $key ) {
85 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
86 if ( ! isset( $_POST[ $key ] ) ) {
87 return '';
88 }
89 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
90 return trim( wp_unslash( $_POST[ $key ] ) );
91 }
92
93 /**
94 * Get integer request value.
95 *
96 * @param string $key Request key.
97 *
98 * @return int
99 */
100 private static function get_request_absint( $key ) {
101 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
102 if ( ! isset( $_POST[ $key ] ) ) {
103 return 0;
104 }
105 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
106 return absint( $_POST[ $key ] );
107 }
108
109 /**
110 * Normalize form name.
111 *
112 * @param string $form_name Form name.
113 *
114 * @return string
115 */
116 private static function normalize_form_name( $form_name ) {
117
118 $form_name = sanitize_key( $form_name );
119
120 if ( empty( $form_name ) ) {
121 $form_name = 'standard';
122 }
123
124 return $form_name;
125 }
126
127 /**
128 * Get form name from request.
129 *
130 * @return string
131 */
132 private static function get_request_form_name() {
133 return self::normalize_form_name( self::get_request_text( 'form_name' ) );
134 }
135
136 /**
137 * Normalize raw shortcode string.
138 *
139 * Important:
140 * - Removes Gutenberg shortcode block comments if they were passed accidentally.
141 * - Keeps only raw shortcode text like: [booking resource_id=1 form_type='standard']
142 *
143 * @param string $shortcode_raw Raw shortcode.
144 *
145 * @return string
146 */
147 private static function normalize_shortcode_raw( $shortcode_raw ) {
148
149 $shortcode_raw = (string) $shortcode_raw;
150
151 $shortcode_raw = preg_replace( '/<!--\s*wp:shortcode\s*-->/', '', $shortcode_raw );
152 $shortcode_raw = preg_replace( '/<!--\s*\/wp:shortcode\s*-->/', '', $shortcode_raw );
153
154 $shortcode_raw = wp_strip_all_tags( $shortcode_raw );
155 $shortcode_raw = trim( $shortcode_raw );
156
157 return $shortcode_raw;
158 }
159
160 /**
161 * Build default raw shortcode.
162 *
163 * @param int $resource_id Booking resource ID.
164 * @param string $form_name Form name.
165 *
166 * @return string
167 */
168 private static function build_default_shortcode_raw( $resource_id, $form_name ) {
169
170 $resource_id = absint( $resource_id );
171 $form_name = self::normalize_form_name( $form_name );
172
173 return "[booking resource_id={$resource_id} form_type='{$form_name}']";
174 }
175
176 /**
177 * Upsert one shortcode attribute inside [booking ...].
178 *
179 * @param string $shortcode_raw Shortcode.
180 * @param string $attr_name Attribute name.
181 * @param string $attr_value Attribute value.
182 * @param string $quote_char Quote char. Use empty string for unquoted values.
183 *
184 * @return string
185 */
186 private static function upsert_shortcode_attr( $shortcode_raw, $attr_name, $attr_value, $quote_char = '' ) {
187
188 $shortcode_raw = (string) $shortcode_raw;
189 $attr_name = trim( (string) $attr_name );
190 $attr_value = trim( (string) $attr_value );
191 $quote_char = (string) $quote_char;
192
193 if ( empty( $attr_name ) ) {
194 return $shortcode_raw;
195 }
196
197 $replacement_value = $attr_value;
198 if ( '' !== $quote_char ) {
199 $replacement_value = $quote_char . $attr_value . $quote_char;
200 }
201
202 $replacement = $attr_name . '=' . $replacement_value;
203 $pattern = '/\b' . preg_quote( $attr_name, '/' ) . '\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s\]]+)/i';
204
205 if ( preg_match( $pattern, $shortcode_raw ) ) {
206 return preg_replace( $pattern, $replacement, $shortcode_raw, 1 );
207 }
208
209 if ( ']' === substr( $shortcode_raw, -1 ) ) {
210 return substr( $shortcode_raw, 0, -1 ) . ' ' . $replacement . ']';
211 }
212
213 return $shortcode_raw . ' ' . $replacement;
214 }
215
216 /**
217 * Normalize booking shortcode to current resource + form name.
218 *
219 * @param string $shortcode_raw Raw shortcode.
220 * @param int $resource_id Booking resource ID.
221 * @param string $form_name Form name.
222 *
223 * @return string
224 */
225 private static function normalize_booking_shortcode_raw( $shortcode_raw, $resource_id, $form_name ) {
226
227 $shortcode_raw = trim( (string) $shortcode_raw );
228 $resource_id = absint( $resource_id );
229 $form_name = self::normalize_form_name( $form_name );
230
231 if ( empty( $shortcode_raw ) ) {
232 return self::build_default_shortcode_raw( $resource_id, $form_name );
233 }
234
235 if ( 0 !== strpos( ltrim( $shortcode_raw ), '[booking' ) ) {
236 return self::build_default_shortcode_raw( $resource_id, $form_name );
237 }
238
239 $shortcode_raw = self::upsert_shortcode_attr( $shortcode_raw, 'resource_id', (string) $resource_id );
240 $shortcode_raw = self::upsert_shortcode_attr( $shortcode_raw, 'form_type', $form_name, '\'' );
241
242 return trim( $shortcode_raw );
243 }
244
245 /**
246 * Get raw shortcode for publishing.
247 *
248 * @param int $resource_id Booking resource ID.
249 * @param string $form_name Form name.
250 *
251 * @return string
252 */
253 private static function get_shortcode_raw( $resource_id, $form_name ) {
254
255 $resource_id = absint( $resource_id );
256 $form_name = self::normalize_form_name( $form_name );
257 $shortcode_raw = self::normalize_shortcode_raw( self::get_request_raw( 'shortcode_raw' ) );
258
259 if ( empty( $shortcode_raw ) ) {
260 $shortcode_raw = self::build_default_shortcode_raw( $resource_id, $form_name );
261 }
262
263 $shortcode_raw = self::normalize_booking_shortcode_raw( $shortcode_raw, $resource_id, $form_name );
264
265 /**
266 * Filter raw shortcode before publishing from BFB.
267 *
268 * @param string $shortcode_raw Raw shortcode.
269 * @param int $resource_id Booking resource ID.
270 * @param string $form_name Form name.
271 */
272 $shortcode_raw = apply_filters( 'wpbc_bfb_publish_shortcode_raw', $shortcode_raw, $resource_id, $form_name );
273
274 return trim( $shortcode_raw );
275 }
276
277 /**
278 * Wrap raw shortcode into Gutenberg shortcode block comments.
279 *
280 * @param string $shortcode_raw Raw shortcode.
281 *
282 * @return string
283 */
284 private static function wrap_shortcode_for_editor( $shortcode_raw ) {
285 return '<!-- wp:shortcode -->' . $shortcode_raw . '<!-- /wp:shortcode -->';
286 }
287
288 /**
289 * Build duplicate detection signatures for existing page content.
290 *
291 * Important:
292 * - We always check exact current shortcode.
293 * - For "standard" form we also check older legacy variants without form_type.
294 *
295 * @param int $resource_id Booking resource ID.
296 * @param string $shortcode_raw Raw shortcode.
297 * @param string $form_name Form name.
298 *
299 * @return array
300 */
301 private static function get_duplicate_check_list( $resource_id, $shortcode_raw, $form_name ) {
302
303 $resource_id = absint( $resource_id );
304 $form_name = self::normalize_form_name( $form_name );
305
306 $check_exist_shortcode_arr = array();
307
308 if ( ! empty( $shortcode_raw ) ) {
309 $check_exist_shortcode_arr[] = $shortcode_raw;
310 }
311
312 $check_exist_shortcode_arr[] = self::build_default_shortcode_raw( $resource_id, $form_name );
313 $check_exist_shortcode_arr[] = '[booking resource_id=' . $resource_id . ' form_type="' . $form_name . '"]';
314
315 if ( 'standard' === $form_name ) {
316 $check_exist_shortcode_arr[] = '[booking resource_id=' . $resource_id . ' ';
317 $check_exist_shortcode_arr[] = '[booking resource_id=' . $resource_id . ']';
318 $check_exist_shortcode_arr[] = '[booking type=' . $resource_id . ' ';
319 $check_exist_shortcode_arr[] = '[booking type=' . $resource_id . ']';
320
321 if ( 1 === $resource_id ) {
322 $check_exist_shortcode_arr[] = '[booking]';
323 }
324 }
325
326 return array_values( array_unique( array_filter( $check_exist_shortcode_arr ) ) );
327 }
328
329 /**
330 * Resolve page ID from publish result.
331 *
332 * This keeps the AJAX layer compatible with the existing helper,
333 * even if that helper does not yet return post_id directly.
334 *
335 * @param array $result_arr Helper result array.
336 * @param array $request Original request info.
337 *
338 * @return int
339 */
340 private static function resolve_post_id_from_result( $result_arr, $request ) {
341
342 if ( ! empty( $result_arr['post_id'] ) ) {
343 return absint( $result_arr['post_id'] );
344 }
345
346 if ( ! empty( $request['page_id'] ) ) {
347 return absint( $request['page_id'] );
348 }
349
350 if ( ! empty( $result_arr['relative_url'] ) ) {
351
352 $relative_url = trim( (string) $result_arr['relative_url'] );
353
354 if ( ! empty( $relative_url ) ) {
355
356 if ( function_exists( 'wpbc_make_link_absolute' ) ) {
357 $absolute_url = wpbc_make_link_absolute( $relative_url );
358 } else {
359 $absolute_url = home_url( $relative_url );
360 }
361
362 $post_id = url_to_postid( $absolute_url );
363
364 if ( ! empty( $post_id ) ) {
365 return absint( $post_id );
366 }
367
368 $path = wp_parse_url( $absolute_url, PHP_URL_PATH );
369
370 if ( ! empty( $path ) ) {
371 $path = trim( $path, '/' );
372 $post_obj = get_page_by_path( $path, OBJECT, 'page' );
373
374 if ( ! empty( $post_obj ) ) {
375 return absint( $post_obj->ID );
376 }
377 }
378 }
379 }
380
381 if ( ! empty( $request['page_post_name'] ) ) {
382 $post_obj = get_page_by_path( $request['page_post_name'], OBJECT, 'page' );
383
384 if ( ! empty( $post_obj ) ) {
385 return absint( $post_obj->ID );
386 }
387 }
388
389 return 0;
390 }
391
392 /**
393 * Check whether a page template value is already valid.
394 *
395 * @param string $template_value Existing template value.
396 *
397 * @return bool
398 */
399 private static function is_page_template_value_valid( $template_value ) {
400
401 $template_value = trim( (string) $template_value );
402
403 if ( '' === $template_value || 'default' === $template_value ) {
404 return false;
405 }
406
407 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
408 return true;
409 }
410
411 $located = locate_template( array( $template_value ), false, false );
412
413 return ( ! empty( $located ) );
414 }
415
416 /**
417 * Try to ensure a full width template on the target page.
418 *
419 * Notes:
420 * - Reuses existing helper wpbc_try_assign_full_width_template() when available.
421 * - Does not override an already valid custom template,
422 * except excluded ones like Elementor Full Width.
423 *
424 * @param int $post_id Page ID.
425 *
426 * @return bool
427 */
428 private static function maybe_ensure_full_width_template( $post_id ) {
429
430 $post_id = absint( $post_id );
431
432 if ( $post_id <= 0 ) {
433 return false;
434 }
435
436 if ( ! function_exists( 'wpbc_try_assign_full_width_template' ) ) {
437 return false;
438 }
439
440 $current_template = (string) get_post_meta( $post_id, '_wp_page_template', true );
441
442 /*
443 * Keep existing valid template,
444 * except Elementor Full Width which we want to avoid.
445 */
446 if (
447 self::is_page_template_value_valid( $current_template ) &&
448 ( 'elementor_header_footer' !== $current_template )
449 ) {
450 return false;
451 }
452
453 return (bool) wpbc_try_assign_full_width_template( $post_id ,
454 array(
455 'excluded_title_parts' => array( 'wide image' ),
456 'excluded_classic_template_files' => array( 'elementor_header_footer' ),
457 'force_elementor_default_template' => true,
458 ) );
459 }
460
461 /**
462 * Build public page URL.
463 *
464 * @param int $post_id Page ID.
465 * @param int $resource_id Booking resource ID.
466 *
467 * @return string
468 */
469 private static function get_view_url( $post_id, $resource_id ) {
470
471 $post_id = absint( $post_id );
472 $resource_id = absint( $resource_id );
473
474 if ( empty( $post_id ) ) {
475 return '';
476 }
477
478 $view_url = get_permalink( $post_id );
479
480 if ( ! empty( $resource_id ) ) {
481 $view_url .= '#bklnk' . $resource_id;
482 }
483
484 return $view_url;
485 }
486
487 /**
488 * Validate user capability for requested publish operation.
489 *
490 * @param string $publish_mode Publish mode.
491 * @param int $page_id Page ID for edit mode.
492 *
493 * @return void
494 */
495 private static function validate_capability( $publish_mode, $page_id ) {
496
497 if ( 'create' === $publish_mode ) {
498 if ( ! current_user_can( 'publish_pages' ) ) {
499 self::send_error( __( 'You do not have permission to create pages.', 'booking' ) );
500 }
501 return;
502 }
503
504 if ( 'edit' === $publish_mode ) {
505 if ( ! current_user_can( 'edit_pages' ) ) {
506 self::send_error( __( 'You do not have permission to edit pages.', 'booking' ) );
507 }
508
509 if ( ! empty( $page_id ) && ! current_user_can( 'edit_post', $page_id ) ) {
510 self::send_error( __( 'You do not have permission to edit the selected page.', 'booking' ) );
511 }
512 }
513 }
514
515 /**
516 * AJAX: Publish booking form into page.
517 *
518 * @return void
519 */
520 public static function ajax_publish_form() {
521
522 check_ajax_referer( self::NONCE_ACTION, 'nonce' );
523
524 if ( wpbc_is_booking_form_publishing_restricted() ) {
525 self::send_error( __( 'This operation is restricted in the demo version.', 'booking' ) );
526 }
527
528 if ( ! function_exists( 'wpbc_add_shortcode_into_page' ) ) {
529 self::send_error( __( 'Publishing helper is not available.', 'booking' ) );
530 }
531
532 $publish_mode = self::get_request_text( 'publish_mode' );
533 $resource_id = self::get_request_absint( 'resource_id' );
534 $form_name = self::get_request_form_name();
535 $page_id = self::get_request_absint( 'page_id' );
536 $page_title = self::get_request_text( 'page_title' );
537 $shortcode_raw = self::get_shortcode_raw( $resource_id, $form_name );
538
539 if ( ( 'create' !== $publish_mode ) && ( 'edit' !== $publish_mode ) ) {
540 self::send_error( __( 'Unknown publish mode.', 'booking' ) );
541 }
542
543 self::validate_capability( $publish_mode, $page_id );
544
545 if ( empty( $shortcode_raw ) ) {
546 self::send_error( __( 'Please save the form first before publishing it.', 'booking' ) );
547 }
548
549 $request_params = array(
550 'shortcode' => self::wrap_shortcode_for_editor( $shortcode_raw ),
551 'check_exist_shortcode' => self::get_duplicate_check_list( $resource_id, $shortcode_raw, $form_name ),
552 'resource_id' => $resource_id,
553 'form_name' => $form_name,
554 );
555
556 if ( 'create' === $publish_mode ) {
557
558 if ( empty( $page_title ) ) {
559 self::send_error( __( 'Please enter a page title.', 'booking' ) );
560 }
561
562 $request_params['post_title'] = $page_title;
563 $request_params['page_post_name'] = sanitize_title( $page_title );
564
565 } elseif ( 'edit' === $publish_mode ) {
566
567 if ( empty( $page_id ) ) {
568 self::send_error( __( 'Please select an existing page.', 'booking' ) );
569 }
570
571 $page_obj = get_post( $page_id );
572
573 if ( empty( $page_obj ) || ( 'page' !== $page_obj->post_type ) ) {
574 self::send_error( __( 'The selected page does not exist.', 'booking' ) );
575 }
576
577 $request_params['page_id'] = $page_id;
578 }
579
580 /**
581 * Final publish params before calling the shared page helper.
582 *
583 * @param array $request_params Final params.
584 * @param string $publish_mode Publish mode.
585 * @param int $resource_id Resource ID.
586 * @param string $shortcode_raw Raw shortcode.
587 * @param string $form_name Form name.
588 */
589 $request_params = apply_filters(
590 'wpbc_bfb_publish_request_params',
591 $request_params,
592 $publish_mode,
593 $resource_id,
594 $shortcode_raw,
595 $form_name
596 );
597
598 $result_arr = wpbc_add_shortcode_into_page( $request_params );
599
600 if ( empty( $result_arr['result'] ) ) {
601 self::send_error(
602 ! empty( $result_arr['message'] )
603 ? $result_arr['message']
604 : __( 'Unable to publish booking form into the selected page.', 'booking' )
605 );
606 }
607
608 $post_id = self::resolve_post_id_from_result( $result_arr, $request_params );
609 $template_applied = self::maybe_ensure_full_width_template( $post_id );
610 $view_url = self::get_view_url( $post_id, $resource_id );
611 $edit_url = ( ! empty( $post_id ) ) ? get_edit_post_link( $post_id, '' ) : '';
612 $post_title = ( ! empty( $post_id ) ) ? get_the_title( $post_id ) : '';
613
614 wp_send_json_success(
615 array(
616 'message' => ! empty( $result_arr['message'] ) ? $result_arr['message'] : __( 'Booking form has been published.', 'booking' ),
617 'post_id' => $post_id,
618 'post_title' => $post_title,
619 'view_url' => $view_url,
620 'edit_url' => $edit_url,
621 'form_name' => $form_name,
622 'template_applied' => $template_applied ? 1 : 0,
623 )
624 );
625 }
626 }
627
628 WPBC_BFB_Publish_Ajax::init();
629