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 / class-wpbc-setup-wizard-draft-validator.php

class-wpbc-setup-wizard-draft-validator.php in Booking Calendar 11.9, at includes/page-setup-wizard/class-wpbc-setup-wizard-draft-validator.php

256 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Draft validation for the Setup Wizard module.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Validate and normalize only fields owned by the current wizard step.
14 */
15 final class WPBC_Setup_Wizard_Draft_Validator {
16
17 /** @var WPBC_Setup_Wizard_Step_Data */
18 private $step_data;
19
20 /**
21 * Build the validator from the server-owned step field provider.
22 *
23 * @param WPBC_Setup_Wizard_Step_Data $step_data Read-only field contracts.
24 */
25 public function __construct( WPBC_Setup_Wizard_Step_Data $step_data ) {
26 $this->step_data = $step_data;
27 }
28
29 /**
30 * Validate one submitted step before draft persistence.
31 *
32 * Required-field checks run only for forward navigation. Back navigation
33 * still rejects malformed non-empty values and unsupported fields.
34 *
35 * @param string $step_id Registered step identifier.
36 * @param array<string,mixed> $submitted_fields Untrusted request fields.
37 * @param bool $require_complete Whether required fields must be present.
38 * @param array<string,mixed> $draft_values Current normalized draft values keyed by step.
39 *
40 * @return array<string,mixed>|WP_Error Sanitized fields or field-level errors.
41 */
42 public function validate_step( $step_id, array $submitted_fields, $require_complete, array $draft_values = array() ) {
43 $allowed_field_names = $this->step_data->get_field_names( $step_id );
44 $unknown_field_names = array_diff( array_keys( $submitted_fields ), $allowed_field_names );
45
46 if ( ! empty( $unknown_field_names ) || count( $submitted_fields ) > count( $allowed_field_names ) ) {
47 return new WP_Error(
48 'wpbc_setup_wizard_invalid_fields',
49 __( 'The Setup Wizard received fields that are not available on this step.', 'booking' )
50 );
51 }
52
53 $sanitized_fields = array();
54 $field_errors = array();
55 $required_fields = $require_complete ? $this->step_data->get_required_fields( $step_id ) : array();
56
57 foreach ( $allowed_field_names as $field_id ) {
58 $is_submitted = array_key_exists( $field_id, $submitted_fields );
59 $raw_value = $is_submitted ? $submitted_fields[ $field_id ] : '';
60
61 if ( ! $is_submitted && in_array( $field_id, $required_fields, true ) ) {
62 $field_errors[ $field_id ] = __( 'This field is required.', 'booking' );
63 continue;
64 }
65
66 if ( ! $is_submitted ) {
67 continue;
68 }
69
70 $validated_value = $this->validate_field( $step_id, $field_id, $raw_value, in_array( $field_id, $required_fields, true ) );
71 if ( is_wp_error( $validated_value ) ) {
72 $field_errors[ $field_id ] = $validated_value->get_error_message();
73 continue;
74 }
75
76 $sanitized_fields[ $field_id ] = $validated_value;
77 }
78
79 if ( ! empty( $field_errors ) ) {
80 return new WP_Error(
81 'wpbc_setup_wizard_validation_failed',
82 __( 'Review the highlighted fields before continuing.', 'booking' ),
83 array( 'field_errors' => $field_errors )
84 );
85 }
86
87 if ( $this->step_data->is_module_step( $step_id ) ) {
88 $validated_module_values = $this->step_data->validate_module_values( $step_id, $sanitized_fields, $draft_values );
89 if ( is_wp_error( $validated_module_values ) ) {
90 return $validated_module_values;
91 }
92 $sanitized_fields = $validated_module_values;
93 }
94
95 return $sanitized_fields;
96 }
97
98 /**
99 * Revalidate every persisted value in the active route before approval.
100 *
101 * Approval must not trust values merely because they were valid in an older
102 * request. Each active step is rebuilt in route order so contextual module
103 * rules see only values already revalidated during this request.
104 *
105 * @param array<string,mixed> $draft_values Persisted draft values keyed by step.
106 * @param string[] $step_ids Current server-owned active route.
107 *
108 * @return array<string,array<string,mixed>>|WP_Error Revalidated route values or the first invalid step.
109 */
110 public function validate_active_route( array $draft_values, array $step_ids ) {
111 $validated_route_values = array();
112
113 foreach ( $step_ids as $step_id ) {
114 $step_id = sanitize_key( (string) $step_id );
115 if ( in_array( $step_id, array( 'welcome', 'review_setup' ), true ) ) {
116 continue;
117 }
118
119 $step_values = $this->step_data->get_step_values( $step_id, $draft_values );
120 $validated_step_values = $this->validate_step( $step_id, $step_values, true, $validated_route_values );
121
122 if ( is_wp_error( $validated_step_values ) ) {
123 $error_data = $validated_step_values->get_error_data();
124 $field_errors = is_array( $error_data ) && isset( $error_data['field_errors'] ) && is_array( $error_data['field_errors'] )
125 ? $error_data['field_errors']
126 : array();
127
128 return new WP_Error(
129 'wpbc_setup_wizard_review_invalid',
130 __( 'One of the saved setup steps is no longer valid. Edit that step and review the plan again.', 'booking' ),
131 array(
132 'invalid_step_id' => $step_id,
133 'field_errors' => $field_errors,
134 )
135 );
136 }
137
138 if ( ! empty( $validated_step_values ) ) {
139 $validated_route_values[ $step_id ] = $validated_step_values;
140 }
141 }
142
143 return $validated_route_values;
144 }
145
146 /**
147 * Normalize stored step values without trusting legacy or malformed records.
148 *
149 * Invalid and unsupported stored values are dropped. This method performs no
150 * persistence and is used only while normalizing the isolated draft option.
151 *
152 * @param array<string,mixed> $stored_values Stored values keyed by step.
153 * @param string[] $step_ids Registered step identifiers.
154 *
155 * @return array<string,array<string,mixed>> Safe values keyed by registered step.
156 */
157 public function normalize_stored_values( array $stored_values, array $step_ids ) {
158 $normalized_values = array();
159
160 foreach ( $step_ids as $step_id ) {
161 if ( ! isset( $stored_values[ $step_id ] ) || ! is_array( $stored_values[ $step_id ] ) ) {
162 continue;
163 }
164
165 $allowed_fields = array_intersect_key( $stored_values[ $step_id ], array_flip( $this->step_data->get_field_names( $step_id ) ) );
166 $step_values = $this->validate_step( $step_id, $allowed_fields, false, $normalized_values );
167
168 if ( ! is_wp_error( $step_values ) && ! empty( $step_values ) ) {
169 $normalized_values[ $step_id ] = $step_values;
170 }
171 }
172
173 return $normalized_values;
174 }
175
176 /**
177 * Validate one field against its exact server-side contract.
178 *
179 * @param string $step_id Registered step identifier.
180 * @param string $field_id Stable field identifier.
181 * @param mixed $raw_value Untrusted field value.
182 * @param bool $is_required Whether an empty value is invalid.
183 *
184 * @return string|bool|array|WP_Error Sanitized value or validation error.
185 */
186 private function validate_field( $step_id, $field_id, $raw_value, $is_required ) {
187 if ( $this->step_data->is_module_step( $step_id ) ) {
188 return $this->step_data->validate_module_field( $step_id, $field_id, $raw_value, $is_required );
189 }
190
191 if ( 'personalization_consent' === $field_id ) {
192 if ( is_bool( $raw_value ) ) {
193 return $raw_value;
194 }
195
196 if ( is_scalar( $raw_value ) && in_array( (string) $raw_value, array( '0', '1' ), true ) ) {
197 return '1' === (string) $raw_value;
198 }
199
200 return new WP_Error( 'wpbc_setup_wizard_invalid_consent', __( 'Choose a valid personalization preference.', 'booking' ) );
201 }
202
203 if ( ! is_scalar( $raw_value ) ) {
204 return new WP_Error( 'wpbc_setup_wizard_invalid_field', __( 'Enter a valid value.', 'booking' ) );
205 }
206
207 $value = sanitize_text_field( (string) $raw_value );
208 if ( $is_required && '' === trim( $value ) ) {
209 return new WP_Error( 'wpbc_setup_wizard_required_field', __( 'This field is required.', 'booking' ) );
210 }
211
212 if ( ! $is_required && '' === trim( $value ) ) {
213 return '';
214 }
215
216 if ( 'business_name' === $field_id ) {
217 if ( $this->get_text_length( $value ) > 200 ) {
218 return new WP_Error( 'wpbc_setup_wizard_business_name_too_long', __( 'Use 200 characters or fewer for the business name.', 'booking' ) );
219 }
220
221 return $value;
222 }
223
224 if ( 'booking_email' === $field_id ) {
225 if ( '' === $value && ! $is_required ) {
226 return '';
227 }
228
229 $email_address = sanitize_email( $value );
230 if ( '' === $email_address || $this->get_text_length( $email_address ) > 254 || ! is_email( $email_address ) ) {
231 return new WP_Error( 'wpbc_setup_wizard_invalid_email', __( 'Enter a valid booking email address.', 'booking' ) );
232 }
233
234 return $email_address;
235 }
236
237 $allowed_values = $this->step_data->get_allowed_values( $step_id, $field_id );
238 if ( ! empty( $allowed_values ) && ! in_array( $value, $allowed_values, true ) ) {
239 return new WP_Error( 'wpbc_setup_wizard_invalid_choice', __( 'Choose one of the available options.', 'booking' ) );
240 }
241
242 return $value;
243 }
244
245 /**
246 * Measure a text value without requiring the multibyte PHP extension.
247 *
248 * @param string $text Text value.
249 *
250 * @return int Character or byte length.
251 */
252 private function get_text_length( $text ) {
253 return function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text );
254 }
255 }
256