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 / step-working-hours / class-wpbc-setup-wizard-working-hours.php

class-wpbc-setup-wizard-working-hours.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-working-hours/class-wpbc-setup-wizard-working-hours.php

363 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Working Hours draft integration for the modular Setup Wizard Working Hours step.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Read, present, and validate a progressively saved Working Time schedule.
14 *
15 * The DTO intentionally matches the canonical weekday interval shape used by
16 * `booking_working_time_rules`, while excluding resource-specific overrides.
17 * This class never updates canonical Working Time or unavailable weekdays.
18 */
19 final class WPBC_Setup_Wizard_Working_Hours {
20
21 const MAX_INTERVALS_PER_DAY = 8;
22 const MAX_PAYLOAD_LENGTH = 50000;
23 const TIME_INCREMENT_SECONDS = 1800;
24
25 /**
26 * Return the template context for the Working Hours step.
27 *
28 * @param array<string,mixed> $step_values Validated values for this step.
29 * @param string $time_format Validated WordPress time format.
30 *
31 * @return array<string,mixed> Presentation-only schedule editor context.
32 */
33 public function get_context( array $step_values, $time_format ) {
34 $working_hours = isset( $step_values['working_hours'] ) && is_array( $step_values['working_hours'] )
35 ? $step_values['working_hours']
36 : $this->get_initial_working_hours();
37
38 return array(
39 'values' => $step_values,
40 'working_hours' => $working_hours,
41 'weekdays' => $this->get_weekday_records(),
42 'time_options' => $this->get_time_options( $working_hours, $time_format ),
43 'max_intervals_per_day' => self::MAX_INTERVALS_PER_DAY,
44 );
45 }
46
47 /**
48 * Load the authorized canonical default schedule or a safe starter proposal.
49 *
50 * The released General Availability page restricts this global setting to
51 * its configured capability and to the MultiUser super administrator. The
52 * wizard preserves that read boundary instead of exposing the schedule to a
53 * regular MultiUser account.
54 *
55 * @return array{enabled:string,weekdays:array<int,array<int,array{start_second:int,end_second:int}>>} Working Hours DTO.
56 */
57 public function get_initial_working_hours() {
58 $weekdays = $this->get_starter_weekdays();
59 $enabled = 'On';
60
61 if ( $this->current_user_can_manage() && function_exists( 'wpbc_working_time__get_settings' ) ) {
62 $settings = wpbc_working_time__get_settings();
63 if ( isset( $settings['enabled'] ) && in_array( $settings['enabled'], array( 'On', 'Off' ), true ) ) {
64 $enabled = $settings['enabled'];
65 }
66 if ( isset( $settings['default']['weekdays'] ) && is_array( $settings['default']['weekdays'] ) ) {
67 $weekdays = $settings['default']['weekdays'];
68 }
69 }
70
71 return array(
72 'enabled' => $enabled,
73 'weekdays' => $this->normalize_weekdays_for_draft( $weekdays ),
74 );
75 }
76
77 /**
78 * Validate the complete Working Hours proposal from one draft field.
79 *
80 * Distinct intervals are sorted and may touch at a boundary, but duplicates
81 * and overlaps are rejected because they create an ambiguous setup review.
82 *
83 * @param mixed $raw_working_hours JSON transport string or stored array.
84 * @param bool $require_complete Whether at least one interval is required.
85 *
86 * @return array<string,mixed>|WP_Error Normalized schedule or an error.
87 */
88 public function validate_working_hours( $raw_working_hours, $require_complete ) {
89 if ( is_string( $raw_working_hours ) ) {
90 if ( strlen( $raw_working_hours ) > self::MAX_PAYLOAD_LENGTH ) {
91 return new WP_Error( 'wpbc_setup_wizard_working_hours_too_large', __( 'The Working Hours draft is too large.', 'booking' ) );
92 }
93
94 $raw_working_hours = json_decode( $raw_working_hours, true );
95 if ( JSON_ERROR_NONE !== json_last_error() ) {
96 return new WP_Error( 'wpbc_setup_wizard_working_hours_invalid_json', __( 'The Working Hours draft is invalid.', 'booking' ) );
97 }
98 }
99
100 if ( ! is_array( $raw_working_hours ) ) {
101 return new WP_Error( 'wpbc_setup_wizard_working_hours_invalid', __( 'Enter valid Working Hours.', 'booking' ) );
102 }
103
104 if ( array_diff( array_keys( $raw_working_hours ), array( 'enabled', 'weekdays' ) ) ) {
105 return new WP_Error( 'wpbc_setup_wizard_working_hours_unknown_field', __( 'The Working Hours draft contains an unsupported field.', 'booking' ) );
106 }
107
108 $enabled = isset( $raw_working_hours['enabled'] ) && is_scalar( $raw_working_hours['enabled'] )
109 ? (string) $raw_working_hours['enabled']
110 : 'On';
111 if ( ! in_array( $enabled, array( 'On', 'Off' ), true ) ) {
112 return new WP_Error( 'wpbc_setup_wizard_working_hours_enabled_invalid', __( 'The Working Hours draft has an invalid status.', 'booking' ) );
113 }
114
115 $raw_weekdays = isset( $raw_working_hours['weekdays'] ) && is_array( $raw_working_hours['weekdays'] )
116 ? $raw_working_hours['weekdays']
117 : array();
118
119 foreach ( array_keys( $raw_weekdays ) as $day_key ) {
120 if ( ! preg_match( '/^[0-6]$/', (string) $day_key ) ) {
121 return new WP_Error( 'wpbc_setup_wizard_working_hours_day_invalid', __( 'The Working Hours draft contains an invalid weekday.', 'booking' ) );
122 }
123 }
124
125 $normalized_weekdays = array();
126 $interval_count = 0;
127
128 for ( $day_number = 0; $day_number <= 6; $day_number++ ) {
129 $raw_intervals = isset( $raw_weekdays[ $day_number ] ) && is_array( $raw_weekdays[ $day_number ] )
130 ? array_values( $raw_weekdays[ $day_number ] )
131 : array();
132
133 if ( count( $raw_intervals ) > self::MAX_INTERVALS_PER_DAY ) {
134 return new WP_Error(
135 'wpbc_setup_wizard_working_hours_interval_limit',
136 /* translators: %d: Maximum intervals per weekday. */
137 sprintf( __( 'Add no more than %d working intervals to one day.', 'booking' ), self::MAX_INTERVALS_PER_DAY )
138 );
139 }
140
141 $normalized_intervals = array();
142 foreach ( $raw_intervals as $raw_interval ) {
143 $normalized_interval = $this->validate_interval( $raw_interval );
144 if ( is_wp_error( $normalized_interval ) ) {
145 return $normalized_interval;
146 }
147 $normalized_intervals[] = $normalized_interval;
148 }
149
150 usort( $normalized_intervals, array( $this, 'compare_intervals' ) );
151 $previous_end = -1;
152 foreach ( $normalized_intervals as $normalized_interval ) {
153 if ( $normalized_interval['start_second'] < $previous_end ) {
154 return new WP_Error( 'wpbc_setup_wizard_working_hours_overlap', __( 'Working intervals on the same day cannot overlap or repeat.', 'booking' ) );
155 }
156 $previous_end = $normalized_interval['end_second'];
157 }
158
159 $normalized_weekdays[ $day_number ] = $normalized_intervals;
160 $interval_count += count( $normalized_intervals );
161 }
162
163 if ( $require_complete && 'On' === $enabled && 0 === $interval_count ) {
164 return new WP_Error( 'wpbc_setup_wizard_working_hours_required', __( 'Enable at least one weekday and add its working hours.', 'booking' ) );
165 }
166
167 return array(
168 'enabled' => $enabled,
169 'weekdays' => $normalized_weekdays,
170 );
171 }
172
173 /**
174 * Validate one canonical interval-shaped draft record.
175 *
176 * @param mixed $raw_interval Untrusted interval candidate.
177 *
178 * @return array{start_second:int,end_second:int}|WP_Error Normalized interval or an error.
179 */
180 private function validate_interval( $raw_interval ) {
181 if ( ! is_array( $raw_interval ) || array_diff( array_keys( $raw_interval ), array( 'start_second', 'end_second' ) ) ) {
182 return new WP_Error( 'wpbc_setup_wizard_working_hours_interval_invalid', __( 'Enter a valid start and end time for every working interval.', 'booking' ) );
183 }
184
185 $start_second = $this->validate_second( isset( $raw_interval['start_second'] ) ? $raw_interval['start_second'] : null );
186 $end_second = $this->validate_second( isset( $raw_interval['end_second'] ) ? $raw_interval['end_second'] : null );
187
188 if ( null === $start_second || null === $end_second || $start_second >= $end_second ) {
189 return new WP_Error( 'wpbc_setup_wizard_working_hours_interval_invalid', __( 'Every working interval must end after it starts.', 'booking' ) );
190 }
191
192 return array(
193 'start_second' => $start_second,
194 'end_second' => $end_second,
195 );
196 }
197
198 /**
199 * Normalize a minute-aligned second value within one day.
200 *
201 * @param mixed $raw_second Untrusted second value.
202 *
203 * @return int|null Normalized value, or null when invalid.
204 */
205 private function validate_second( $raw_second ) {
206 if ( ! is_scalar( $raw_second ) || ! preg_match( '/^\d+$/', (string) $raw_second ) ) {
207 return null;
208 }
209
210 $second = absint( $raw_second );
211 if ( DAY_IN_SECONDS < $second || 0 !== $second % MINUTE_IN_SECONDS ) {
212 return null;
213 }
214
215 return $second;
216 }
217
218 /**
219 * Compare two Working Time intervals by start and then end.
220 *
221 * @param array<string,int> $first_interval First interval.
222 * @param array<string,int> $second_interval Second interval.
223 *
224 * @return int Comparison result for `usort()`.
225 */
226 private function compare_intervals( $first_interval, $second_interval ) {
227 if ( $first_interval['start_second'] === $second_interval['start_second'] ) {
228 return $first_interval['end_second'] - $second_interval['end_second'];
229 }
230
231 return $first_interval['start_second'] - $second_interval['start_second'];
232 }
233
234 /**
235 * Keep a canonical weekday array bounded and JSON-safe for the draft UI.
236 *
237 * @param mixed $weekdays Canonical weekday interval candidate.
238 *
239 * @return array<int,array<int,array{start_second:int,end_second:int}>> Normalized weekdays.
240 */
241 private function normalize_weekdays_for_draft( $weekdays ) {
242 $normalized_weekdays = array();
243
244 for ( $day_number = 0; $day_number <= 6; $day_number++ ) {
245 $normalized_weekdays[ $day_number ] = array();
246 $day_intervals = isset( $weekdays[ $day_number ] ) && is_array( $weekdays[ $day_number ] )
247 ? array_slice( $weekdays[ $day_number ], 0, self::MAX_INTERVALS_PER_DAY )
248 : array();
249
250 foreach ( $day_intervals as $day_interval ) {
251 $normalized_interval = $this->validate_interval( $day_interval );
252 if ( ! is_wp_error( $normalized_interval ) ) {
253 $normalized_weekdays[ $day_number ][] = $normalized_interval;
254 }
255 }
256
257 usort( $normalized_weekdays[ $day_number ], array( $this, 'compare_intervals' ) );
258 }
259
260 return $normalized_weekdays;
261 }
262
263 /**
264 * Return the safe proposal shown when canonical Working Time cannot be read.
265 *
266 * @return array<int,array<int,array{start_second:int,end_second:int}>> Starter weekdays.
267 */
268 private function get_starter_weekdays() {
269 $weekdays = array_fill( 0, 7, array() );
270 $weekdays[1] = array(
271 array( 'start_second' => 9 * HOUR_IN_SECONDS, 'end_second' => 12 * HOUR_IN_SECONDS ),
272 array( 'start_second' => 13 * HOUR_IN_SECONDS, 'end_second' => 17 * HOUR_IN_SECONDS ),
273 );
274
275 for ( $day_number = 2; $day_number <= 5; $day_number++ ) {
276 $weekdays[ $day_number ][] = array( 'start_second' => 9 * HOUR_IN_SECONDS, 'end_second' => 17 * HOUR_IN_SECONDS );
277 }
278
279 $weekdays[6][] = array( 'start_second' => 10 * HOUR_IN_SECONDS, 'end_second' => 14 * HOUR_IN_SECONDS );
280
281 return $weekdays;
282 }
283
284 /**
285 * Return Monday-through-Sunday labels without changing storage day numbers.
286 *
287 * @return array<int,array{day_number:int,label:string}> Weekday presentation records.
288 */
289 private function get_weekday_records() {
290 return array(
291 array( 'day_number' => 1, 'label' => __( 'Monday', 'booking' ) ),
292 array( 'day_number' => 2, 'label' => __( 'Tuesday', 'booking' ) ),
293 array( 'day_number' => 3, 'label' => __( 'Wednesday', 'booking' ) ),
294 array( 'day_number' => 4, 'label' => __( 'Thursday', 'booking' ) ),
295 array( 'day_number' => 5, 'label' => __( 'Friday', 'booking' ) ),
296 array( 'day_number' => 6, 'label' => __( 'Saturday', 'booking' ) ),
297 array( 'day_number' => 0, 'label' => __( 'Sunday', 'booking' ) ),
298 );
299 }
300
301 /**
302 * Return 30-minute choices plus any minute-aligned canonical endpoints.
303 *
304 * @param array<string,mixed> $working_hours Current Working Hours DTO.
305 * @param string $time_format Validated WordPress time format.
306 *
307 * @return array<int,array{value:int,label:string}> Ordered time options.
308 */
309 private function get_time_options( array $working_hours, $time_format ) {
310 $time_format = in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ? $time_format : 'H:i';
311 $seconds = array();
312
313 for ( $second = 0; $second <= DAY_IN_SECONDS; $second += self::TIME_INCREMENT_SECONDS ) {
314 $seconds[ $second ] = true;
315 }
316
317 if ( isset( $working_hours['weekdays'] ) && is_array( $working_hours['weekdays'] ) ) {
318 foreach ( $working_hours['weekdays'] as $day_intervals ) {
319 foreach ( (array) $day_intervals as $day_interval ) {
320 if ( isset( $day_interval['start_second'] ) ) {
321 $seconds[ absint( $day_interval['start_second'] ) ] = true;
322 }
323 if ( isset( $day_interval['end_second'] ) ) {
324 $seconds[ absint( $day_interval['end_second'] ) ] = true;
325 }
326 }
327 }
328 }
329
330 ksort( $seconds, SORT_NUMERIC );
331 $time_options = array();
332 foreach ( array_keys( $seconds ) as $second ) {
333 $second = min( DAY_IN_SECONDS, absint( $second ) );
334 $label = DAY_IN_SECONDS === $second
335 ? __( '24:00', 'booking' )
336 : wp_date( $time_format, $second, new DateTimeZone( 'UTC' ) );
337 $time_options[] = array(
338 'value' => $second,
339 'label' => $label,
340 );
341 }
342
343 return $time_options;
344 }
345
346 /**
347 * Check the released General Availability read boundary.
348 *
349 * @return bool True when the current user may read global Working Time.
350 */
351 public function current_user_can_manage() {
352 if ( function_exists( 'wpbc_is_mu_user_can_be_here' ) && ! wpbc_is_mu_user_can_be_here( 'only_super_admin' ) ) {
353 return false;
354 }
355
356 $capability = function_exists( 'wpbc_availability_general__get_manage_cap' )
357 ? wpbc_availability_general__get_manage_cap()
358 : 'manage_options';
359
360 return current_user_can( $capability );
361 }
362 }
363