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-start-end-times / class-wpbc-setup-wizard-start-end-times.php

class-wpbc-setup-wizard-start-end-times.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-start-end-times/class-wpbc-setup-wizard-start-end-times.php

308 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Start and End Times proposal service for the Setup Wizard.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Build, validate, and present reusable start-time and end-time choices.
14 *
15 * The saved DTO deliberately uses the same `HH:MM` values accepted by the
16 * Form Builder `starttime` and `endtime` fields. This module does not mutate a
17 * Booking Form; a later Booking Form step can apply these validated choices to
18 * its selected template without coupling this editor to the wizard shell.
19 */
20 final class WPBC_Setup_Wizard_Start_End_Times {
21
22 const MAX_TIMES_PER_LIST = 288;
23 const MAX_PAYLOAD_LENGTH = 10000;
24 const TIME_INCREMENT = 5;
25
26 /**
27 * Return a safe starter configuration for both time lists.
28 *
29 * @return array{start_times:string[],end_times:string[]} Starter choices.
30 */
31 public function get_initial_start_end_times() {
32 return array(
33 'start_times' => $this->generate_time_values( '08:00', '12:30', 30 ),
34 'end_times' => $this->generate_time_values( '13:00', '17:30', 30 ),
35 );
36 }
37
38 /**
39 * Build presentation data for the reusable time-list editor.
40 *
41 * @param array<string,mixed> $step_values Validated values for this module.
42 * @param string $time_format Validated WordPress time format.
43 *
44 * @return array<string,mixed> Data-only template context.
45 */
46 public function get_context( array $step_values, $time_format ) {
47 $start_end_times = isset( $step_values['start_end_times'] ) && is_array( $step_values['start_end_times'] )
48 ? $step_values['start_end_times']
49 : $this->get_initial_start_end_times();
50
51 $time_options = $this->get_time_options( $time_format );
52
53 return array(
54 'values' => $step_values,
55 'start_end_times' => $start_end_times,
56 'time_options' => $time_options,
57 'draft_field_name' => 'start_end_times',
58 'draft_values' => $start_end_times,
59 'editor_title' => __( 'Set start and end times', 'booking' ),
60 'editor_description' => __( 'Choose the start and end time options customers can use when booking.', 'booking' ),
61 'validation_rule' => 'end_after_start',
62 'list_definitions' => array(
63 'start_times' => array(
64 'title' => __( 'Start times', 'booking' ),
65 'description' => __( 'Choices shown in the booking form.', 'booking' ),
66 'list_label' => __( 'Start times', 'booking' ),
67 'clear_aria_label' => __( 'Clear Start times', 'booking' ),
68 'clear_status' => __( 'Start times cleared.', 'booking' ),
69 'add_label' => __( 'Add one time slot', 'booking' ),
70 'generator_interval' => 30,
71 'options' => $time_options,
72 ),
73 'end_times' => array(
74 'title' => __( 'End times', 'booking' ),
75 'description' => __( 'Choices shown in the booking form.', 'booking' ),
76 'list_label' => __( 'End times', 'booking' ),
77 'clear_aria_label' => __( 'Clear End times', 'booking' ),
78 'clear_status' => __( 'End times cleared.', 'booking' ),
79 'add_label' => __( 'Add one time slot', 'booking' ),
80 'generator_interval' => 30,
81 'options' => $time_options,
82 ),
83 ),
84 'copy_action' => array(
85 'source' => 'start_times',
86 'target' => 'end_times',
87 'label' => __( 'Copy start times to end times', 'booking' ),
88 'aria_label' => __( 'Copy Start times to End times', 'booking' ),
89 'status' => __( 'Start times copied to End times.', 'booking' ),
90 ),
91 'interval_options' => $this->get_interval_options(),
92 'max_times_per_list' => self::MAX_TIMES_PER_LIST,
93 'time_increment' => self::TIME_INCREMENT,
94 );
95 }
96
97 /**
98 * Validate one complete start/end time-choice DTO.
99 *
100 * Lists preserve the user's order because their order is also the future
101 * Form Builder option order. Duplicate values and values outside the shared
102 * five-minute precision are rejected instead of being silently rewritten.
103 *
104 * @param mixed $raw_start_end_times JSON transport string or stored array.
105 * @param bool $require_complete Whether both lists and a usable pair are required.
106 *
107 * @return array{start_times:string[],end_times:string[]}|WP_Error Normalized choices or an error.
108 */
109 public function validate_start_end_times( $raw_start_end_times, $require_complete ) {
110 if ( is_string( $raw_start_end_times ) ) {
111 if ( strlen( $raw_start_end_times ) > self::MAX_PAYLOAD_LENGTH ) {
112 return new WP_Error( 'wpbc_setup_wizard_start_end_times_too_large', __( 'The start and end time draft is too large.', 'booking' ) );
113 }
114
115 $raw_start_end_times = json_decode( $raw_start_end_times, true );
116 if ( JSON_ERROR_NONE !== json_last_error() ) {
117 return new WP_Error( 'wpbc_setup_wizard_start_end_times_invalid_json', __( 'The start and end time draft is invalid.', 'booking' ) );
118 }
119 }
120
121 if ( ! is_array( $raw_start_end_times ) ) {
122 return new WP_Error( 'wpbc_setup_wizard_start_end_times_invalid', __( 'Enter valid start and end time choices.', 'booking' ) );
123 }
124
125 if ( array_diff( array_keys( $raw_start_end_times ), array( 'start_times', 'end_times' ) ) ) {
126 return new WP_Error( 'wpbc_setup_wizard_start_end_times_unknown_field', __( 'The start and end time draft contains an unsupported field.', 'booking' ) );
127 }
128
129 $start_times = $this->validate_time_list(
130 isset( $raw_start_end_times['start_times'] ) ? $raw_start_end_times['start_times'] : array(),
131 $require_complete,
132 __( 'Add at least one start time.', 'booking' )
133 );
134 if ( is_wp_error( $start_times ) ) {
135 return $start_times;
136 }
137
138 $end_times = $this->validate_time_list(
139 isset( $raw_start_end_times['end_times'] ) ? $raw_start_end_times['end_times'] : array(),
140 $require_complete,
141 __( 'Add at least one end time.', 'booking' )
142 );
143 if ( is_wp_error( $end_times ) ) {
144 return $end_times;
145 }
146
147 if ( $require_complete && ! $this->has_usable_time_pair( $start_times, $end_times ) ) {
148 return new WP_Error( 'wpbc_setup_wizard_start_end_times_pair_invalid', __( 'Add an end time that is later than at least one start time.', 'booking' ) );
149 }
150
151 return array(
152 'start_times' => $start_times,
153 'end_times' => $end_times,
154 );
155 }
156
157 /**
158 * Generate inclusive `HH:MM` values for a bounded same-day range.
159 *
160 * @param string $start_time First time in `HH:MM` format.
161 * @param string $end_time Last time in `HH:MM` format.
162 * @param int $interval_minutes Positive interval in minutes.
163 *
164 * @return string[] Ordered time values, or an empty array for invalid input.
165 */
166 public function generate_time_values( $start_time, $end_time, $interval_minutes ) {
167 $start_minutes = $this->time_to_minutes( $start_time );
168 $end_minutes = $this->time_to_minutes( $end_time );
169 $interval_minutes = absint( $interval_minutes );
170
171 if (
172 null === $start_minutes
173 || null === $end_minutes
174 || $end_minutes < $start_minutes
175 || self::TIME_INCREMENT > $interval_minutes
176 || 0 !== $interval_minutes % self::TIME_INCREMENT
177 ) {
178 return array();
179 }
180
181 $time_values = array();
182 for ( $minute = $start_minutes; $minute <= $end_minutes && count( $time_values ) < self::MAX_TIMES_PER_LIST; $minute += $interval_minutes ) {
183 $time_values[] = sprintf( '%02d:%02d', (int) floor( $minute / 60 ), $minute % 60 );
184 }
185
186 return $time_values;
187 }
188
189 /**
190 * Return every time selectable by this editor.
191 *
192 * @param string $time_format Validated WordPress time format.
193 *
194 * @return array<int,array{value:string,label:string}> Ordered options.
195 */
196 public function get_time_options( $time_format ) {
197 $time_format = in_array( $time_format, array( 'g:i a', 'g:i A', 'H:i' ), true ) ? $time_format : 'H:i';
198 $options = array();
199
200 for ( $minute = 0; $minute < 24 * 60; $minute += self::TIME_INCREMENT ) {
201 $options[] = array(
202 'value' => sprintf( '%02d:%02d', (int) floor( $minute / 60 ), $minute % 60 ),
203 'label' => wp_date( $time_format, $minute * MINUTE_IN_SECONDS, new DateTimeZone( 'UTC' ) ),
204 );
205 }
206
207 return $options;
208 }
209
210 /**
211 * Return the supported generator intervals.
212 *
213 * @return array<int,array{value:int,label:string}> Ordered interval choices.
214 */
215 public function get_interval_options() {
216 $intervals = array( 5, 10, 15, 20, 30, 60, 120 );
217 $options = array();
218
219 foreach ( $intervals as $interval_minutes ) {
220 /* translators: %d: Time interval in minutes. */
221 $label = sprintf( _n( '%d minute', '%d minutes', $interval_minutes, 'booking' ), $interval_minutes );
222 $options[] = array(
223 'value' => $interval_minutes,
224 'label' => $label,
225 );
226 }
227
228 return $options;
229 }
230
231 /**
232 * Validate one ordered list of unique time values.
233 *
234 * @param mixed $raw_time_list Untrusted time-list candidate.
235 * @param bool $require_complete Whether an empty list is invalid.
236 * @param string $required_message Translated list-specific required message.
237 *
238 * @return string[]|WP_Error Normalized list or an error.
239 */
240 public function validate_time_list( $raw_time_list, $require_complete, $required_message ) {
241 if ( ! is_array( $raw_time_list ) ) {
242 return new WP_Error( 'wpbc_setup_wizard_time_list_invalid', __( 'Every time list must contain valid time choices.', 'booking' ) );
243 }
244
245 $raw_time_list = array_values( $raw_time_list );
246 if ( count( $raw_time_list ) > self::MAX_TIMES_PER_LIST ) {
247 /* translators: %d: Maximum number of time choices. */
248 return new WP_Error( 'wpbc_setup_wizard_time_list_limit', sprintf( __( 'Add no more than %d choices to one time list.', 'booking' ), self::MAX_TIMES_PER_LIST ) );
249 }
250
251 $normalized_times = array();
252 foreach ( $raw_time_list as $raw_time ) {
253 $time_minutes = $this->time_to_minutes( $raw_time );
254 if ( null === $time_minutes || 0 !== $time_minutes % self::TIME_INCREMENT ) {
255 return new WP_Error( 'wpbc_setup_wizard_time_value_invalid', __( 'Use a valid five-minute time for every choice.', 'booking' ) );
256 }
257
258 $normalized_time = sprintf( '%02d:%02d', (int) floor( $time_minutes / 60 ), $time_minutes % 60 );
259 if ( in_array( $normalized_time, $normalized_times, true ) ) {
260 return new WP_Error( 'wpbc_setup_wizard_time_value_duplicate', __( 'The same time cannot appear twice in one list.', 'booking' ) );
261 }
262 $normalized_times[] = $normalized_time;
263 }
264
265 if ( $require_complete && empty( $normalized_times ) ) {
266 return new WP_Error( 'wpbc_setup_wizard_time_list_required', $required_message );
267 }
268
269 return $normalized_times;
270 }
271
272 /**
273 * Determine whether at least one end choice follows one start choice.
274 *
275 * @param string[] $start_times Validated start times.
276 * @param string[] $end_times Validated end times.
277 *
278 * @return bool True when the two lists can create a positive time range.
279 */
280 private function has_usable_time_pair( array $start_times, array $end_times ) {
281 if ( empty( $start_times ) || empty( $end_times ) ) {
282 return false;
283 }
284
285 $start_minutes = array_map( array( $this, 'time_to_minutes' ), $start_times );
286 $end_minutes = array_map( array( $this, 'time_to_minutes' ), $end_times );
287
288 return max( $end_minutes ) > min( $start_minutes );
289 }
290
291 /**
292 * Convert one exact `HH:MM` value to minutes after midnight.
293 *
294 * @param mixed $raw_time Untrusted time value.
295 *
296 * @return int|null Minutes after midnight, or null when invalid.
297 */
298 public function time_to_minutes( $raw_time ) {
299 if ( ! is_scalar( $raw_time ) || ! preg_match( '/^(?:[01]\d|2[0-3]):[0-5]\d$/', (string) $raw_time ) ) {
300 return null;
301 }
302
303 list( $hour, $minute ) = array_map( 'intval', explode( ':', (string) $raw_time ) );
304
305 return ( $hour * 60 ) + $minute;
306 }
307 }
308