PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
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 / publish / class-wpbc-booking-form-publisher.php

class-wpbc-booking-form-publisher.php in Booking Calendar 11.7, at includes/publish/class-wpbc-booking-form-publisher.php

384 lines 14.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Presentation-neutral Booking Form publishing service.
4 *
5 * @package Booking Calendar
6 * @since 11.6.0
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Publish one Booking Form shortcode into a new or existing WordPress page.
15 *
16 * This service owns request-independent validation and delegates the canonical
17 * content write to wpbc_add_shortcode_into_page(). UI-specific AJAX and modal
18 * controllers remain thin consumers of this neutral boundary.
19 */
20 final class WPBC_Booking_Form_Publisher {
21
22 /**
23 * Determine whether page publishing must be blocked on this website.
24 *
25 * The configured WordPress home host is the canonical site identity for both
26 * normal page loads and AJAX mutations. The request host is used only when a
27 * canonical home host is unavailable, which keeps the decision stable without
28 * trusting a client-controlled Host header over WordPress configuration.
29 * Development hosts such as `beta` require no exception because only the
30 * official wpbookingcalendar.com domain and its subdomains are restricted.
31 *
32 * @return bool True when page discovery and page mutations must be blocked.
33 */
34 public static function is_demo_restricted() {
35 $request_host = self::get_request_host();
36 $site_host = self::normalize_host( wp_parse_url( home_url( '/' ), PHP_URL_HOST ) );
37 $canonical_host = '' !== $site_host ? $site_host : $request_host;
38 $official_demo_host = self::is_official_demo_host( $canonical_host );
39
40 /**
41 * Filter whether neutral Booking Form publishing is restricted as a live demo.
42 *
43 * @since 11.6.0
44 *
45 * @param bool $official_demo_host Whether the current host is restricted.
46 * @param string $site_host Normalized WordPress home URL host.
47 * @param string $request_host Normalized current request host used only as a fallback.
48 */
49 return (bool) apply_filters( 'wpbc_publish_booking_form_is_demo_restricted', $official_demo_host, $site_host, $request_host );
50 }
51
52 /**
53 * Read the current HTTP request host without trusting proxy-only headers.
54 *
55 * This value is a fallback for unusual environments where WordPress cannot
56 * provide a configured home hostname. It never overrides a valid canonical
57 * WordPress site host.
58 *
59 * @return string Normalized request host, or an empty string outside HTTP.
60 */
61 private static function get_request_host() {
62 if ( empty( $_SERVER['HTTP_HOST'] ) ) {
63 return '';
64 }
65
66 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- normalize_host() validates and reduces this value to a hostname.
67 return self::normalize_host( wp_unslash( $_SERVER['HTTP_HOST'] ) );
68 }
69
70 /**
71 * Normalize a URL or HTTP Host value to a lowercase hostname.
72 *
73 * @param mixed $host Hostname, optionally including a port.
74 *
75 * @return string Normalized hostname, or an empty string when invalid.
76 */
77 private static function normalize_host( $host ) {
78 $host = trim( strtolower( (string) $host ) );
79 if ( '' === $host ) {
80 return '';
81 }
82
83 $normalized_host = wp_parse_url( 'http://' . ltrim( $host, '/' ), PHP_URL_HOST );
84 if ( ! is_string( $normalized_host ) ) {
85 return '';
86 }
87
88 return untrailingslashit( strtolower( rtrim( $normalized_host, '.' ) ) );
89 }
90
91 /**
92 * Determine whether a normalized host belongs to the public demo network.
93 *
94 * @param string $host Normalized hostname.
95 *
96 * @return bool True for wpbookingcalendar.com and its subdomains.
97 */
98 private static function is_official_demo_host( $host ) {
99 return 'wpbookingcalendar.com' === $host
100 || ( strlen( $host ) > strlen( '.wpbookingcalendar.com' )
101 && '.wpbookingcalendar.com' === substr( $host, -strlen( '.wpbookingcalendar.com' ) ) );
102 }
103
104 /**
105 * Publish a normalized Booking Form request.
106 *
107 * @param array $publish_request Untrusted publish values from an authorized controller.
108 *
109 * @return array|WP_Error Published-page response or a safe validation error.
110 */
111 public function publish( $publish_request ) {
112 $publish_request = is_array( $publish_request ) ? $publish_request : array();
113
114 if ( self::is_demo_restricted() ) {
115 return new WP_Error( 'wpbc_publish_demo_restricted', __( 'In the demo versions this operation is not allowed.', 'booking' ) );
116 }
117
118 if ( ! function_exists( 'wpbc_add_shortcode_into_page' ) ) {
119 return new WP_Error( 'wpbc_publish_helper_unavailable', __( 'Publishing helper is not available.', 'booking' ) );
120 }
121
122 $publish_mode = isset( $publish_request['publish_mode'] ) ? sanitize_key( $publish_request['publish_mode'] ) : '';
123 $resource_id = isset( $publish_request['resource_id'] ) ? absint( $publish_request['resource_id'] ) : 0;
124 $form_name = $this->normalize_form_name(
125 isset( $publish_request['form_name'] ) ? $publish_request['form_name'] : '',
126 isset( $publish_request['shortcode_raw'] ) ? $publish_request['shortcode_raw'] : ''
127 );
128 $page_id = isset( $publish_request['page_id'] ) ? absint( $publish_request['page_id'] ) : 0;
129 $page_title = isset( $publish_request['page_title'] ) ? sanitize_text_field( $publish_request['page_title'] ) : '';
130 $shortcode_raw = $this->normalize_booking_shortcode(
131 isset( $publish_request['shortcode_raw'] ) ? $publish_request['shortcode_raw'] : '',
132 $resource_id,
133 $form_name
134 );
135
136 if ( ! in_array( $publish_mode, array( 'create', 'edit' ), true ) ) {
137 return new WP_Error( 'wpbc_publish_invalid_mode', __( 'Unknown publish mode.', 'booking' ) );
138 }
139
140 if ( ! $resource_id ) {
141 return new WP_Error( 'wpbc_publish_invalid_resource', __( 'The selected Booking Resource is invalid.', 'booking' ) );
142 }
143
144 $capability_error = $this->validate_page_capability( $publish_mode, $page_id );
145 if ( is_wp_error( $capability_error ) ) {
146 return $capability_error;
147 }
148
149 $helper_params = array(
150 'shortcode' => $this->wrap_shortcode_for_editor( $shortcode_raw ),
151 'check_exist_shortcode' => $this->get_duplicate_check_list( $resource_id, $shortcode_raw, $form_name ),
152 'resource_id' => $resource_id,
153 );
154
155 if ( 'create' === $publish_mode ) {
156 if ( '' === $page_title ) {
157 return new WP_Error( 'wpbc_publish_missing_title', __( 'Please enter a page title.', 'booking' ) );
158 }
159 $helper_params['post_title'] = $page_title;
160 $helper_params['page_post_name'] = sanitize_title( $page_title );
161 } else {
162 $page = get_post( $page_id );
163 if ( ! $page_id ) {
164 return new WP_Error( 'wpbc_publish_missing_page', __( 'Please select an existing page.', 'booking' ) );
165 }
166 if ( ! $page || 'page' !== $page->post_type ) {
167 return new WP_Error( 'wpbc_publish_page_missing', __( 'The selected page does not exist.', 'booking' ) );
168 }
169 $helper_params['page_id'] = $page_id;
170 }
171
172 /**
173 * Filter canonical page-helper parameters for neutral Booking Form publishing.
174 *
175 * @param array $helper_params Canonical wpbc_add_shortcode_into_page() parameters.
176 * @param string $publish_mode Create or edit mode.
177 * @param int $resource_id Booking Resource ID.
178 * @param string $shortcode_raw Normalized raw shortcode.
179 * @param string $form_name Normalized Booking Form name.
180 */
181 $helper_params = apply_filters(
182 'wpbc_publish_booking_form_request_params',
183 $helper_params,
184 $publish_mode,
185 $resource_id,
186 $shortcode_raw,
187 $form_name
188 );
189
190 $publish_result = wpbc_add_shortcode_into_page( $helper_params );
191 if ( ! is_array( $publish_result ) || empty( $publish_result['result'] ) ) {
192 $message = is_array( $publish_result ) && ! empty( $publish_result['message'] )
193 ? wp_kses_post( $publish_result['message'] )
194 : __( 'Unable to publish the booking form into the selected page.', 'booking' );
195 return new WP_Error( 'wpbc_publish_failed', $message );
196 }
197
198 $post_id = $this->resolve_post_id( $publish_result, $helper_params );
199 $view_url = $post_id ? get_permalink( $post_id ) : '';
200 $edit_url = $post_id ? get_edit_post_link( $post_id, '' ) : '';
201 $post_title = $post_id ? get_the_title( $post_id ) : '';
202
203 if ( $view_url ) {
204 $view_url .= '#bklnk' . $resource_id;
205 }
206
207 return array(
208 'message' => ! empty( $publish_result['message'] ) ? wp_kses_post( $publish_result['message'] ) : __( 'Booking form has been published.', 'booking' ),
209 'post_id' => $post_id,
210 'post_title' => $post_title,
211 'view_url' => $view_url,
212 'edit_url' => $edit_url,
213 'form_name' => $form_name,
214 );
215 }
216
217 /**
218 * Normalize a Booking Form name to a stable key.
219 *
220 * @param mixed $form_name Raw Booking Form name.
221 * @param mixed $shortcode_raw Optional shortcode used to recover its form type.
222 *
223 * @return string Normalized name.
224 */
225 private function normalize_form_name( $form_name, $shortcode_raw = '' ) {
226 $form_name = sanitize_key( (string) $form_name );
227 if ( '' === $form_name && preg_match( '/\bform_type\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s\]]+))/i', (string) $shortcode_raw, $matches ) ) {
228 $form_name = sanitize_key( $matches[1] ? $matches[1] : ( $matches[2] ? $matches[2] : $matches[3] ) );
229 }
230 return '' !== $form_name ? $form_name : 'standard';
231 }
232
233 /**
234 * Normalize a raw Booking Form shortcode for one Resource and form.
235 *
236 * @param mixed $shortcode_raw Raw shortcode value.
237 * @param int $resource_id Booking Resource ID.
238 * @param string $form_name Booking Form name.
239 *
240 * @return string Normalized raw shortcode.
241 */
242 private function normalize_booking_shortcode( $shortcode_raw, $resource_id, $form_name ) {
243 $shortcode_raw = preg_replace( '/<!--\s*\/?wp:shortcode\s*-->/', '', (string) $shortcode_raw );
244 $shortcode_raw = trim( wp_strip_all_tags( $shortcode_raw ) );
245
246 if ( ! preg_match( '/^\[booking(?:\s[^\]]*)?\]$/i', $shortcode_raw ) ) {
247 $shortcode_raw = "[booking resource_id={$resource_id} form_type='{$form_name}']";
248 }
249
250 $shortcode_raw = $this->upsert_shortcode_attribute( $shortcode_raw, 'resource_id', (string) $resource_id );
251 $shortcode_raw = $this->upsert_shortcode_attribute( $shortcode_raw, 'form_type', $form_name, '\'' );
252
253 return trim( $shortcode_raw );
254 }
255
256 /**
257 * Insert or replace one Booking shortcode attribute.
258 *
259 * @param string $shortcode_raw Shortcode being normalized.
260 * @param string $attribute Attribute name.
261 * @param string $attribute_value Attribute value.
262 * @param string $quote_character Optional quote character.
263 *
264 * @return string Updated shortcode.
265 */
266 private function upsert_shortcode_attribute( $shortcode_raw, $attribute, $attribute_value, $quote_character = '' ) {
267 $replacement_value = $quote_character ? $quote_character . $attribute_value . $quote_character : $attribute_value;
268 $replacement = $attribute . '=' . $replacement_value;
269 $pattern = '/\b' . preg_quote( $attribute, '/' ) . '\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s\]]+)/i';
270
271 if ( preg_match( $pattern, $shortcode_raw ) ) {
272 return preg_replace( $pattern, $replacement, $shortcode_raw, 1 );
273 }
274
275 return ']' === substr( $shortcode_raw, -1 )
276 ? substr( $shortcode_raw, 0, -1 ) . ' ' . $replacement . ']'
277 : $shortcode_raw . ' ' . $replacement;
278 }
279
280 /**
281 * Wrap a raw shortcode in the WordPress Shortcode block comments.
282 *
283 * @param string $shortcode_raw Raw shortcode.
284 *
285 * @return string Block-editor content.
286 */
287 private function wrap_shortcode_for_editor( $shortcode_raw ) {
288 return '<!-- wp:shortcode -->' . $shortcode_raw . '<!-- /wp:shortcode -->';
289 }
290
291 /**
292 * Build duplicate-detection signatures for the canonical page helper.
293 *
294 * @param int $resource_id Booking Resource ID.
295 * @param string $shortcode_raw Normalized raw shortcode.
296 * @param string $form_name Booking Form name.
297 *
298 * @return array Duplicate signatures.
299 */
300 private function get_duplicate_check_list( $resource_id, $shortcode_raw, $form_name ) {
301 $signatures = array(
302 $shortcode_raw,
303 "[booking resource_id={$resource_id} form_type='{$form_name}']",
304 '[booking resource_id=' . $resource_id . ' form_type="' . $form_name . '"]',
305 );
306
307 if ( 'standard' === $form_name ) {
308 $signatures[] = '[booking resource_id=' . $resource_id . ' ';
309 $signatures[] = '[booking resource_id=' . $resource_id . ']';
310 $signatures[] = '[booking type=' . $resource_id . ' ';
311 $signatures[] = '[booking type=' . $resource_id . ']';
312 if ( 1 === $resource_id ) {
313 $signatures[] = '[booking]';
314 }
315 }
316
317 return array_values( array_unique( array_filter( $signatures ) ) );
318 }
319
320 /**
321 * Validate WordPress page capabilities for the requested operation.
322 *
323 * @param string $publish_mode Create or edit mode.
324 * @param int $page_id Existing page ID for edit mode.
325 *
326 * @return true|WP_Error True when allowed, otherwise a safe error.
327 */
328 private function validate_page_capability( $publish_mode, $page_id ) {
329 if ( 'create' === $publish_mode && ! current_user_can( 'publish_pages' ) ) {
330 return new WP_Error( 'wpbc_publish_create_forbidden', __( 'You do not have permission to create pages.', 'booking' ) );
331 }
332 if ( 'edit' === $publish_mode && ! current_user_can( 'edit_pages' ) ) {
333 return new WP_Error( 'wpbc_publish_edit_forbidden', __( 'You do not have permission to edit pages.', 'booking' ) );
334 }
335 if ( 'edit' === $publish_mode && $page_id && ! current_user_can( 'edit_post', $page_id ) ) {
336 return new WP_Error( 'wpbc_publish_page_forbidden', __( 'You do not have permission to edit the selected page.', 'booking' ) );
337 }
338 return true;
339 }
340
341 /**
342 * Resolve the affected page ID from a canonical helper response.
343 *
344 * @param array $publish_result Canonical helper response.
345 * @param array $helper_params Canonical helper request.
346 *
347 * @return int Affected page ID, or zero when it cannot be resolved.
348 */
349 private function resolve_post_id( $publish_result, $helper_params ) {
350 if ( ! empty( $helper_params['page_id'] ) ) {
351 return absint( $helper_params['page_id'] );
352 }
353 if ( ! empty( $publish_result['post_id'] ) ) {
354 return absint( $publish_result['post_id'] );
355 }
356 if ( ! empty( $publish_result['relative_url'] ) ) {
357 $absolute_url = function_exists( 'wpbc_make_link_absolute' )
358 ? wpbc_make_link_absolute( $publish_result['relative_url'] )
359 : home_url( $publish_result['relative_url'] );
360 $post_id = url_to_postid( $absolute_url );
361 if ( $post_id ) {
362 return absint( $post_id );
363 }
364 }
365 if ( ! empty( $helper_params['page_post_name'] ) ) {
366 $page = get_page_by_path( $helper_params['page_post_name'], OBJECT, 'page' );
367 return $page ? absint( $page->ID ) : 0;
368 }
369 return 0;
370 }
371 }
372
373 /**
374 * Determine whether Booking Form page publishing is restricted on this site.
375 *
376 * This compatibility boundary keeps Catalog, Form Builder, legacy Resources,
377 * and the canonical page helper on the same host-aware demo policy.
378 *
379 * @return bool True when page discovery and mutations must be blocked.
380 */
381 function wpbc_is_booking_form_publishing_restricted() {
382 return WPBC_Booking_Form_Publisher::is_demo_restricted();
383 }
384