PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
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-catalog-booking-resources / class-wpbc-catalog-booking-resource-availability.php

class-wpbc-catalog-booking-resource-availability.php in Booking Calendar 11.7, at includes/page-catalog-booking-resources/class-wpbc-catalog-booking-resource-availability.php

498 lines 21.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Independent availability adapter for the template-driven Resources catalog.
4 *
5 * @package Booking Calendar
6 * @since 11.6.0
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Read, validate, and save availability fields without legacy catalog services.
15 *
16 * The adapter owns only the two settings that are safe to edit in the Resource
17 * inspector. It preserves season filters and every unrelated Search
18 * Availability option when updating their canonical storage records.
19 */
20 final class WPBC_Catalog_Booking_Resource_Availability {
21
22 /**
23 * Return presentation definitions for the Availability and search group.
24 *
25 * @param array<string,mixed> $resource Authorized Resource record.
26 *
27 * @return array<int,array<string,mixed>> Inspector field definitions.
28 */
29 public function get_field_definitions( $resource ) {
30 $resource_id = isset( $resource['id'] ) ? absint( $resource['id'] ) : 0;
31 $owner_user_id = isset( $resource['owner_user_id'] ) ? absint( $resource['owner_user_id'] ) : 0;
32 $resource_type = isset( $resource['resource_type'] ) ? sanitize_key( (string) $resource['resource_type'] ) : 'single';
33 $can_manage_days = current_user_can( wpbc_catalog_booking_resources_get_role_capability( 'booking_user_role_availability' ) );
34 $can_manage_search = current_user_can( wpbc_catalog_booking_resources_get_manage_capability() );
35 $previous_active_user = $this->set_owner_environment( $resource_id );
36 $availability = $this->get_season_availability( $resource_id );
37 $fields = array();
38
39 if ( class_exists( 'wpdev_bk_biz_l' ) && function_exists( 'wpbc_searchable_resources__get_all_options' ) ) {
40 $is_child = 'child' === $resource_type;
41 $searchable = $this->get_searchable_status( $resource_id, $is_child );
42 $search_url = $this->get_searchable_resources_url( $resource_id );
43 $fields[] = array(
44 'key' => 'searchable_status',
45 'label' => __( 'Search Availability results', 'booking' ),
46 'type' => $is_child || ! $can_manage_search ? 'readonly' : 'select',
47 'value' => $searchable,
48 'display_value' => 'on' === $searchable ? __( 'Searchable', 'booking' ) : __( 'Hidden from search results', 'booking' ),
49 'editable' => ! $is_child && $can_manage_search,
50 'options' => array(
51 array( 'value' => 'on', 'label' => __( 'Searchable', 'booking' ) ),
52 array( 'value' => 'off', 'label' => __( 'Hidden from search results', 'booking' ) ),
53 ),
54 'help' => $is_child
55 ? __( 'Child resources stay hidden because Search Availability returns their top-level parent resource.', 'booking' )
56 : __( 'Choose whether this Booking Resource can appear in Search Availability results.', 'booking' ),
57 'link_url' => $search_url,
58 'link_label' => $search_url ? __( 'Manage search presentation and filters', 'booking' ) : '',
59 );
60 } else {
61 $fields[] = array(
62 'key' => 'searchable_status',
63 'label' => __( 'Search Availability results', 'booking' ),
64 'type' => 'readonly',
65 'value' => '',
66 'display_value' => __( 'Not available in this version', 'booking' ),
67 'editable' => false,
68 'help' => __( 'Control whether this Booking Resource appears in Search Availability results.', 'booking' ),
69 );
70 }
71
72 if ( class_exists( 'wpdev_bk_biz_m' ) && function_exists( 'wpbc_get_resource_meta' ) && function_exists( 'wpbc_save_resource_meta' ) ) {
73 $availability_mode = 'Off' === $availability['general'] ? 'unavailable' : 'available';
74 $season_url = $can_manage_days ? $this->get_season_availability_url( $resource_id ) : '';
75 $active_rules = $this->count_active_season_rules( $availability );
76 $fields[] = array(
77 'key' => 'availability_mode',
78 'label' => __( 'Default day availability', 'booking' ),
79 'type' => $can_manage_days ? 'select' : 'readonly',
80 'value' => $availability_mode,
81 'display_value' => 'available' === $availability_mode ? __( 'Available', 'booking' ) : __( 'Unavailable', 'booking' ),
82 'editable' => $can_manage_days,
83 'options' => array(
84 array( 'value' => 'available', 'label' => __( 'Available', 'booking' ) ),
85 array( 'value' => 'unavailable', 'label' => __( 'Unavailable', 'booking' ) ),
86 ),
87 'help' => __( 'Active season rules invert this default only on the dates matched by those rules.', 'booking' ),
88 'link_url' => $season_url,
89 'link_label' => $season_url ? __( 'Manage season availability', 'booking' ) : '',
90 );
91 $fields[] = array(
92 'key' => 'active_season_rules',
93 'label' => __( 'Active season rules', 'booking' ),
94 'type' => 'readonly',
95 'value' => (string) $active_rules,
96 /* translators: %s: Number of active season availability rules. */
97 'display_value' => sprintf( _n( '%s rule', '%s rules', $active_rules, 'booking' ), number_format_i18n( $active_rules ) ),
98 'editable' => false,
99 'link_url' => $season_url,
100 'link_label' => $season_url ? __( 'Review season rules', 'booking' ) : '',
101 );
102 } else {
103 $fields[] = array(
104 'key' => 'availability_mode',
105 'label' => __( 'Season availability', 'booking' ),
106 'type' => 'readonly',
107 'value' => '',
108 'display_value' => __( 'Not available in this version', 'booking' ),
109 'editable' => false,
110 'help' => __( 'Set a default availability state and apply seasonal availability rules.', 'booking' ),
111 );
112 }
113
114 $manual_url = $can_manage_days ? $this->get_manual_availability_url( $resource_id ) : '';
115 $fields[] = array(
116 'key' => 'manual_date_availability',
117 'label' => __( 'Manual date availability', 'booking' ),
118 'type' => 'readonly',
119 'value' => __( 'Per-date overrides', 'booking' ),
120 'display_value' => __( 'Per-date overrides', 'booking' ),
121 'editable' => false,
122 'help' => __( 'Use the availability calendar to mark individual dates available or unavailable.', 'booking' ),
123 'link_url' => $manual_url,
124 'link_label' => $manual_url ? __( 'Manage dates', 'booking' ) : '',
125 );
126
127 $global_url = $can_manage_days && $this->can_edit_owner_settings( $owner_user_id ) && function_exists( 'wpbc_get_general_availability_url' )
128 ? esc_url_raw( wpbc_get_general_availability_url( true, false ) )
129 : '';
130 $fields[] = array(
131 'key' => 'booking_window_start',
132 'label' => __( 'Earliest bookable day', 'booking' ),
133 'type' => 'readonly',
134 'value' => sanitize_text_field( (string) get_bk_option( 'booking_unavailable_days_num_from_today' ) ),
135 'display_value' => $this->format_booking_window_start(),
136 'editable' => false,
137 'help' => __( 'This is a global Booking Calendar rule shared by every Booking Resource.', 'booking' ),
138 'link_url' => $global_url,
139 'link_label' => $global_url ? __( 'Change global booking window', 'booking' ) : '',
140 );
141 $fields[] = array(
142 'key' => 'booking_window_horizon',
143 'label' => __( 'Booking horizon', 'booking' ),
144 'type' => 'readonly',
145 'value' => class_exists( 'wpdev_bk_biz_m' ) ? (string) get_bk_option( 'booking_available_days_num_from_today' ) : '',
146 'display_value' => class_exists( 'wpdev_bk_biz_m' ) ? $this->format_booking_window_horizon() : __( 'Not available in this version', 'booking' ),
147 'editable' => false,
148 'help' => __( 'This is a global Booking Calendar rule shared by every Booking Resource.', 'booking' ),
149 'link_url' => class_exists( 'wpdev_bk_biz_m' ) ? $global_url : '',
150 'link_label' => class_exists( 'wpdev_bk_biz_m' ) && $global_url ? __( 'Change global booking window', 'booking' ) : '',
151 );
152
153 $this->restore_owner_environment( $previous_active_user, $resource_id );
154
155 return $fields;
156 }
157
158 /**
159 * Validate submitted availability values supported by the current edition.
160 *
161 * @param array<string,mixed> $resource Authorized Resource record.
162 * @param array<string,mixed> $submitted_fields Untrusted inspector values.
163 *
164 * @return array<string,string>|WP_Error Validated changes or safe error.
165 */
166 public function validate_fields( $resource, $submitted_fields ) {
167 $validated = array();
168 if ( array_key_exists( 'availability_mode', $submitted_fields ) && is_scalar( $submitted_fields['availability_mode'] ) ) {
169 if ( ! class_exists( 'wpdev_bk_biz_m' ) || ! current_user_can( wpbc_catalog_booking_resources_get_role_capability( 'booking_user_role_availability' ) ) ) {
170 return new WP_Error( 'wpbc_catalog_resource_availability_forbidden', __( 'You cannot change availability for this Booking Resource.', 'booking' ) );
171 }
172 $availability_mode = sanitize_key( (string) $submitted_fields['availability_mode'] );
173 if ( ! in_array( $availability_mode, array( 'available', 'unavailable' ), true ) ) {
174 return new WP_Error( 'wpbc_catalog_resource_availability_invalid', __( 'Select a valid default availability state.', 'booking' ) );
175 }
176 $validated['availability_mode'] = $availability_mode;
177 }
178
179 if ( array_key_exists( 'searchable_status', $submitted_fields ) && is_scalar( $submitted_fields['searchable_status'] ) ) {
180 $is_child = isset( $resource['resource_type'] ) && 'child' === (string) $resource['resource_type'];
181 if ( ! class_exists( 'wpdev_bk_biz_l' ) || $is_child || ! current_user_can( wpbc_catalog_booking_resources_get_manage_capability() ) ) {
182 return new WP_Error( 'wpbc_catalog_resource_searchability_forbidden', __( 'You cannot change Search Availability visibility for this Booking Resource.', 'booking' ) );
183 }
184 $searchable_status = sanitize_key( (string) $submitted_fields['searchable_status'] );
185 if ( ! in_array( $searchable_status, array( 'on', 'off' ), true ) ) {
186 return new WP_Error( 'wpbc_catalog_resource_searchability_invalid', __( 'Select a valid Search Availability visibility.', 'booking' ) );
187 }
188 $validated['searchable_status'] = $searchable_status;
189 }
190
191 return $validated;
192 }
193
194 /**
195 * Save validated availability fields and return an exact rollback snapshot.
196 *
197 * @param array<string,mixed> $resource Authorized Resource record.
198 * @param array<string,string> $validated_fields Validated availability values.
199 *
200 * @return array<string,mixed>|WP_Error Rollback snapshot or safe error.
201 */
202 public function save_fields( $resource, $validated_fields ) {
203 $resource_id = isset( $resource['id'] ) ? absint( $resource['id'] ) : 0;
204 if ( ! $resource_id || empty( $validated_fields ) ) {
205 return array();
206 }
207
208 $previous_active_user = $this->set_owner_environment( $resource_id );
209 $rollback = array();
210 if ( array_key_exists( 'availability_mode', $validated_fields ) ) {
211 $rollback['availability'] = $this->get_season_availability( $resource_id );
212 $availability = $rollback['availability'];
213 $availability['general'] = 'available' === $validated_fields['availability_mode'] ? 'On' : 'Off';
214 if ( ! function_exists( 'wpbc_save_resource_meta' ) || ! wpbc_save_resource_meta( $resource_id, 'availability', $availability ) ) {
215 $this->restore_owner_environment( $previous_active_user, $resource_id );
216
217 return new WP_Error( 'wpbc_catalog_resource_availability_not_saved', __( 'The Booking Resource availability could not be saved.', 'booking' ) );
218 }
219 }
220
221 if ( array_key_exists( 'searchable_status', $validated_fields ) ) {
222 $rollback['search_options'] = function_exists( 'wpbc_searchable_resources__get_all_options' )
223 ? (array) wpbc_searchable_resources__get_all_options()
224 : array();
225 $saved = $this->save_searchable_status( $resource_id, $validated_fields['searchable_status'] );
226 if ( is_wp_error( $saved ) ) {
227 $this->restore_fields_in_owner_context( $resource_id, $rollback );
228 $this->restore_owner_environment( $previous_active_user, $resource_id );
229
230 return $saved;
231 }
232 }
233
234 $this->restore_owner_environment( $previous_active_user, $resource_id );
235 if ( array_key_exists( 'availability_mode', $validated_fields ) && function_exists( 'make_bk_action' ) ) {
236 make_bk_action( 'wpbc_reinit_seasonfilters_cache' );
237 }
238 do_action( 'wpbc_catalog_booking_resource_availability_saved', $resource_id, $validated_fields );
239
240 return $rollback;
241 }
242
243 /**
244 * Restore a snapshot after a later catalog mutation fails.
245 *
246 * @param int $resource_id Booking Resource ID.
247 * @param array<string,mixed> $rollback Snapshot returned by save_fields().
248 *
249 * @return void
250 */
251 public function restore_fields( $resource_id, $rollback ) {
252 if ( ! $resource_id || empty( $rollback ) ) {
253 return;
254 }
255 $previous_active_user = $this->set_owner_environment( $resource_id );
256 $this->restore_fields_in_owner_context( $resource_id, $rollback );
257 $this->restore_owner_environment( $previous_active_user, $resource_id );
258 if ( isset( $rollback['availability'] ) && function_exists( 'make_bk_action' ) ) {
259 make_bk_action( 'wpbc_reinit_seasonfilters_cache' );
260 }
261 }
262
263 /**
264 * Restore canonical values while the correct owner context is active.
265 *
266 * @param int $resource_id Booking Resource ID.
267 * @param array<string,mixed> $rollback Rollback snapshot.
268 *
269 * @return void
270 */
271 private function restore_fields_in_owner_context( $resource_id, $rollback ) {
272 if ( isset( $rollback['availability'] ) && function_exists( 'wpbc_save_resource_meta' ) ) {
273 wpbc_save_resource_meta( $resource_id, 'availability', $rollback['availability'] );
274 }
275 if ( isset( $rollback['search_options'] ) && function_exists( 'wpbc_searchable_resources__save_all_options' ) ) {
276 wpbc_searchable_resources__save_all_options( $rollback['search_options'] );
277 }
278 }
279
280 /**
281 * Read canonical season availability while preserving every filter entry.
282 *
283 * @param int $resource_id Booking Resource ID.
284 *
285 * @return array{general:string,filter:array} Normalized availability record.
286 */
287 private function get_season_availability( $resource_id ) {
288 $availability = array( 'general' => 'On', 'filter' => array() );
289 if ( ! $resource_id || ! function_exists( 'wpbc_get_resource_meta' ) ) {
290 return $availability;
291 }
292 $records = wpbc_get_resource_meta( $resource_id, 'availability' );
293 if ( empty( $records[0]->value ) ) {
294 return $availability;
295 }
296 $stored = maybe_unserialize( $records[0]->value );
297 if ( is_array( $stored ) ) {
298 $availability['general'] = isset( $stored['general'] ) && 'Off' === $stored['general'] ? 'Off' : 'On';
299 $availability['filter'] = isset( $stored['filter'] ) && is_array( $stored['filter'] ) ? $stored['filter'] : array();
300 }
301
302 return $availability;
303 }
304
305 /**
306 * Count enabled season rules.
307 *
308 * @param array<string,mixed> $availability Normalized availability record.
309 *
310 * @return int Enabled rule count.
311 */
312 private function count_active_season_rules( $availability ) {
313 $rule_count = 0;
314 foreach ( isset( $availability['filter'] ) ? (array) $availability['filter'] : array() as $filter_state ) {
315 if ( 'On' === $filter_state ) {
316 ++$rule_count;
317 }
318 }
319
320 return $rule_count;
321 }
322
323 /**
324 * Return whether one top-level Resource appears in Search Availability.
325 *
326 * @param int $resource_id Booking Resource ID.
327 * @param bool $is_child Whether hierarchy forces the Resource hidden.
328 *
329 * @return string `on` or `off`.
330 */
331 private function get_searchable_status( $resource_id, $is_child = false ) {
332 if ( $is_child || ! function_exists( 'wpbc_searchable_resources__get_all_options' ) ) {
333 return 'off';
334 }
335 $all_options = (array) wpbc_searchable_resources__get_all_options();
336
337 return isset( $all_options[ $resource_id ]['is_searchable'] ) && 'On' === $all_options[ $resource_id ]['is_searchable'] ? 'on' : 'off';
338 }
339
340 /**
341 * Save only one Resource's Search Availability visibility.
342 *
343 * @param int $resource_id Booking Resource ID.
344 * @param string $searchable_status Canonical `on` or `off` value.
345 *
346 * @return true|WP_Error True or a verified storage error.
347 */
348 private function save_searchable_status( $resource_id, $searchable_status ) {
349 if ( ! function_exists( 'wpbc_searchable_resources__get_all_options' ) || ! function_exists( 'wpbc_searchable_resources__save_all_options' ) ) {
350 return new WP_Error( 'wpbc_catalog_resource_searchability_unavailable', __( 'Search Availability settings are unavailable.', 'booking' ) );
351 }
352 $all_options = (array) wpbc_searchable_resources__get_all_options();
353 if ( ! isset( $all_options[ $resource_id ] ) || ! is_array( $all_options[ $resource_id ] ) ) {
354 $all_options[ $resource_id ] = array();
355 }
356 $all_options[ $resource_id ]['is_searchable'] = 'on' === $searchable_status ? 'On' : 'Off';
357 wpbc_searchable_resources__save_all_options( $all_options );
358
359 return $searchable_status === $this->get_searchable_status( $resource_id )
360 ? true
361 : new WP_Error( 'wpbc_catalog_resource_searchability_not_saved', __( 'Search Availability visibility could not be saved.', 'booking' ) );
362 }
363
364 /**
365 * Build a direct manual-date availability URL.
366 *
367 * @param int $resource_id Booking Resource ID.
368 *
369 * @return string Authorized URL or an empty string.
370 */
371 private function get_manual_availability_url( $resource_id ) {
372 return function_exists( 'wpbc_get_availability_url' )
373 ? esc_url_raw( add_query_arg( 'resource_id', absint( $resource_id ), wpbc_get_availability_url( true, false ) ) )
374 : '';
375 }
376
377 /**
378 * Build a direct season-availability URL.
379 *
380 * @param int $resource_id Booking Resource ID.
381 *
382 * @return string Authorized URL or an empty string.
383 */
384 private function get_season_availability_url( $resource_id ) {
385 return function_exists( 'wpbc_get_availability_url' )
386 ? esc_url_raw( add_query_arg( array( 'tab' => 'season_availability', 'edit_resource_id' => absint( $resource_id ) ), wpbc_get_availability_url( true, false ) ) )
387 : '';
388 }
389
390 /**
391 * Build a direct Searchable Resources URL.
392 *
393 * @param int $resource_id Booking Resource ID.
394 *
395 * @return string Authorized URL or an empty string.
396 */
397 private function get_searchable_resources_url( $resource_id ) {
398 return function_exists( 'wpbc_get_resources_url' )
399 ? esc_url_raw( add_query_arg( array( 'tab' => 'searchable_resources', 'wh_resource_id' => absint( $resource_id ) ), wpbc_get_resources_url( true, false ) ) )
400 : '';
401 }
402
403 /**
404 * Format the earliest globally bookable day.
405 *
406 * @return string Human-readable booking-window start.
407 */
408 private function format_booking_window_start() {
409 $stored_start = sanitize_text_field( (string) get_bk_option( 'booking_unavailable_days_num_from_today' ) );
410 if ( '' === $stored_start || '0' === $stored_start ) {
411 return __( 'Today', 'booking' );
412 }
413 if ( function_exists( 'wpbc_availability_general__get_unavailable_from_today_options' ) ) {
414 $options = wpbc_availability_general__get_unavailable_from_today_options();
415 if ( isset( $options[ $stored_start ] ) ) {
416 return wp_strip_all_tags( (string) $options[ $stored_start ] ) . ' ' . __( 'from now', 'booking' );
417 }
418 }
419 if ( preg_match( '/^(\d+)m$/', $stored_start, $matches ) ) {
420 $minutes = absint( $matches[1] );
421
422 return sprintf( _n( '%s minute from now', '%s minutes from now', $minutes, 'booking' ), number_format_i18n( $minutes ) );
423 }
424 $days = absint( $stored_start );
425
426 return sprintf( _n( '%s day from today', '%s days from today', $days, 'booking' ), number_format_i18n( $days ) );
427 }
428
429 /**
430 * Format the globally configured booking horizon.
431 *
432 * @return string Human-readable booking horizon.
433 */
434 private function format_booking_window_horizon() {
435 $stored_horizon = get_bk_option( 'booking_available_days_num_from_today' );
436 if ( '' === (string) $stored_horizon || 0 === absint( $stored_horizon ) ) {
437 return __( 'No day limit', 'booking' );
438 }
439 $days = absint( $stored_horizon );
440
441 return sprintf( _n( '%s day from today', '%s days from today', $days, 'booking' ), number_format_i18n( $days ) );
442 }
443
444 /**
445 * Determine whether the current account can edit an owner's global settings.
446 *
447 * @param int $owner_user_id Resource owner user ID.
448 *
449 * @return bool True when the exact settings context is accessible.
450 */
451 private function can_edit_owner_settings( $owner_user_id ) {
452 if ( ! current_user_can( wpbc_catalog_booking_resources_get_role_capability( 'booking_user_role_settings' ) ) ) {
453 return false;
454 }
455 if ( ! class_exists( 'wpdev_bk_multiuser' ) ) {
456 return true;
457 }
458 $owner_user_id = absint( $owner_user_id );
459 if ( $owner_user_id && (bool) apply_bk_filter( 'is_user_super_admin', $owner_user_id ) ) {
460 $owner_user_id = 0;
461 }
462 $current_user = function_exists( 'wpbc_mu__wp_get_current_user' ) ? wpbc_mu__wp_get_current_user() : wp_get_current_user();
463 $current_user_id = is_object( $current_user ) && isset( $current_user->ID ) ? absint( $current_user->ID ) : 0;
464 if ( $current_user_id && (bool) apply_bk_filter( 'is_user_super_admin', $current_user_id ) ) {
465 $current_user_id = 0;
466 }
467
468 return $owner_user_id === $current_user_id;
469 }
470
471 /**
472 * Activate the owning MultiUser option context.
473 *
474 * @param int $resource_id Booking Resource ID.
475 *
476 * @return mixed Previous active-user token.
477 */
478 private function set_owner_environment( $resource_id ) {
479 return function_exists( 'apply_bk_filter' )
480 ? apply_bk_filter( 'wpbc_mu_set_environment_for_owner_of_resource', -1, absint( $resource_id ) )
481 : -1;
482 }
483
484 /**
485 * Restore the prior MultiUser option context.
486 *
487 * @param mixed $previous_active_user Previous active-user token.
488 * @param int $resource_id Resource ID used for the switch.
489 *
490 * @return void
491 */
492 private function restore_owner_environment( $previous_active_user, $resource_id ) {
493 if ( $resource_id && function_exists( 'make_bk_action' ) ) {
494 make_bk_action( 'wpbc_mu_set_environment_for_user', $previous_active_user );
495 }
496 }
497 }
498