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-booking-resources / class-wpbc-setup-wizard-booking-resources.php

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

491 lines 21.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Booking Resource drafts for the Setup Wizard full-day route.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Read existing Resources and validate browser-staged Resource proposals.
14 */
15 final class WPBC_Setup_Wizard_Booking_Resources {
16
17 const MAX_RESOURCE_DRAFTS = 20;
18 const MAX_EXISTING_RESOURCES = 200;
19 const MAX_DESCRIPTION_LENGTH = 2000;
20 const MAX_PICTURE_URL_LENGTH = 2048;
21
22 /**
23 * Return presentation data for the Booking Resources editor.
24 *
25 * @param array<string,mixed> $step_values Validated step values.
26 *
27 * @return array<string,mixed> Authorized presentation context.
28 */
29 public function get_context( array $step_values ) {
30 $currency_symbol = '$';
31 if ( $this->is_pricing_available() && function_exists( 'wpbc_get_currency_symbol' ) ) {
32 $currency_symbol = html_entity_decode( wp_strip_all_tags( (string) wpbc_get_currency_symbol() ), ENT_QUOTES, get_bloginfo( 'charset' ) );
33 }
34
35 return array(
36 'values' => $step_values,
37 'existing_resources' => $this->get_existing_resources(),
38 'booking_resources' => isset( $step_values['booking_resources'] ) && is_array( $step_values['booking_resources'] ) ? $step_values['booking_resources'] : array(),
39 'existing_resource_updates' => isset( $step_values['existing_booking_resources'] ) && is_array( $step_values['existing_booking_resources'] ) ? $step_values['existing_booking_resources'] : array(),
40 'can_create_resources' => $this->can_create_resources(),
41 'pricing_available' => $this->is_pricing_available(),
42 'media_upload_available' => ! function_exists( 'wpbc_is_this_demo' ) || ! wpbc_is_this_demo(),
43 'currency_symbol' => sanitize_text_field( $currency_symbol ),
44 'max_resource_drafts' => $this->get_maximum_resource_drafts(),
45 );
46 }
47
48 /**
49 * Return the empty initial draft collection.
50 *
51 * Existing Resources are deliberately read separately and are never copied
52 * into a create payload, preventing Save & continue from duplicating them.
53 *
54 * @return array<int,array<string,mixed>> Empty proposal collection.
55 */
56 public function get_initial_resource_drafts() {
57 return array();
58 }
59
60 /**
61 * Validate the complete staged Resource collection.
62 *
63 * @param mixed $raw_resources JSON transport string or stored array.
64 * @param bool $require_complete Whether staged drafts must be complete.
65 *
66 * @return array<int,array<string,mixed>>|WP_Error Normalized proposals or an error.
67 */
68 public function validate_resource_drafts( $raw_resources, $require_complete ) {
69 if ( is_string( $raw_resources ) ) {
70 if ( 100000 < strlen( $raw_resources ) ) {
71 return new WP_Error( 'wpbc_setup_wizard_resources_too_large', __( 'The Booking Resource draft is too large.', 'booking' ) );
72 }
73 $raw_resources = json_decode( $raw_resources, true );
74 if ( JSON_ERROR_NONE !== json_last_error() ) {
75 return new WP_Error( 'wpbc_setup_wizard_resources_invalid_json', __( 'The Booking Resource draft is invalid.', 'booking' ) );
76 }
77 }
78
79 if ( ! is_array( $raw_resources ) ) {
80 return new WP_Error( 'wpbc_setup_wizard_resources_invalid', __( 'Enter valid Booking Resource details.', 'booking' ) );
81 }
82 if ( ! $this->can_create_resources() && ! empty( $raw_resources ) ) {
83 return new WP_Error( 'wpbc_setup_wizard_resources_paid_required', __( 'Additional Booking Resources are available in Pro versions.', 'booking' ) );
84 }
85 if ( count( $raw_resources ) > $this->get_maximum_resource_drafts() ) {
86 return new WP_Error( 'wpbc_setup_wizard_resources_limit', __( 'The Booking Resource limit for this account has been reached.', 'booking' ) );
87 }
88
89 $normalized_resources = array();
90 $used_draft_ids = array();
91 foreach ( array_values( $raw_resources ) as $resource_index => $raw_resource ) {
92 $normalized_resource = $this->validate_resource_draft( $raw_resource, $resource_index, $require_complete );
93 if ( is_wp_error( $normalized_resource ) ) {
94 return $normalized_resource;
95 }
96 if ( isset( $used_draft_ids[ $normalized_resource['draft_id'] ] ) ) {
97 return new WP_Error( 'wpbc_setup_wizard_resource_duplicate_key', __( 'Each Booking Resource draft must have a unique identifier.', 'booking' ) );
98 }
99 $used_draft_ids[ $normalized_resource['draft_id'] ] = true;
100 $normalized_resources[] = $normalized_resource;
101 }
102
103 return $normalized_resources;
104 }
105
106 /**
107 * Validate updates for existing Resources without accepting delete semantics.
108 *
109 * The browser sends only changed Resources. Each update carries a fingerprint
110 * of the authorized source values so the save handler can reject stale edits.
111 *
112 * @param mixed $raw_updates JSON transport string or stored array.
113 *
114 * @return array<int,array<string,mixed>>|WP_Error Normalized updates or an error.
115 */
116 public function validate_existing_resource_updates( $raw_updates ) {
117 if ( is_string( $raw_updates ) ) {
118 if ( 500000 < strlen( $raw_updates ) ) {
119 return new WP_Error( 'wpbc_setup_wizard_resource_updates_too_large', __( 'The Booking Resource updates are too large.', 'booking' ) );
120 }
121 $raw_updates = json_decode( $raw_updates, true );
122 if ( JSON_ERROR_NONE !== json_last_error() ) {
123 return new WP_Error( 'wpbc_setup_wizard_resource_updates_invalid_json', __( 'The Booking Resource updates are invalid.', 'booking' ) );
124 }
125 }
126
127 if ( ! is_array( $raw_updates ) ) {
128 return new WP_Error( 'wpbc_setup_wizard_resource_updates_invalid', __( 'Enter valid Booking Resource details.', 'booking' ) );
129 }
130 if ( self::MAX_EXISTING_RESOURCES < count( $raw_updates ) ) {
131 return new WP_Error( 'wpbc_setup_wizard_resource_updates_limit', __( 'Too many Booking Resources were submitted at once.', 'booking' ) );
132 }
133
134 $normalized_updates = array();
135 $used_resource_ids = array();
136 foreach ( array_values( $raw_updates ) as $raw_update ) {
137 $normalized_update = $this->validate_existing_resource_update( $raw_update );
138 if ( is_wp_error( $normalized_update ) ) {
139 return $normalized_update;
140 }
141 $resource_id = absint( $normalized_update['resource_id'] );
142 if ( isset( $used_resource_ids[ $resource_id ] ) ) {
143 return new WP_Error( 'wpbc_setup_wizard_resource_update_duplicate', __( 'Each existing Booking Resource may be updated only once.', 'booking' ) );
144 }
145 $used_resource_ids[ $resource_id ] = true;
146 $normalized_updates[] = $normalized_update;
147 }
148
149 return $normalized_updates;
150 }
151
152 /**
153 * Return existing Resources authorized for the effective wizard owner.
154 *
155 * @return array<int,array<string,mixed>> Presentation-safe Resource records.
156 */
157 public function get_existing_resources() {
158 if ( ! class_exists( 'WPBC_Catalog_Booking_Resources_Repository' ) ) {
159 return array();
160 }
161 $repository = new WPBC_Catalog_Booking_Resources_Repository();
162 $resources = array();
163 $page_size = min( 100, self::MAX_EXISTING_RESOURCES );
164 $page_number = 1;
165
166 while ( count( $resources ) < self::MAX_EXISTING_RESOURCES ) {
167 $resource_page = $repository->get_resources(
168 array(
169 'page_number' => $page_number,
170 'items_per_page' => $page_size,
171 'sort_by' => 'id',
172 'sort_order' => 'asc',
173 'search' => '',
174 'resource_type' => 'all',
175 )
176 );
177 if ( is_wp_error( $resource_page ) || ! is_array( $resource_page ) ) {
178 return array();
179 }
180 if ( empty( $resource_page ) ) {
181 break;
182 }
183
184 $resources = array_merge( $resources, $resource_page );
185 if ( count( $resource_page ) < $page_size ) {
186 break;
187 }
188 ++$page_number;
189 }
190 $resources = array_slice( $resources, 0, self::MAX_EXISTING_RESOURCES );
191
192 $storage_context = WPBC_Setup_Wizard_Access::get_storage_context();
193 $owner_user_id = absint( $storage_context['owner_user_id'] );
194 $presentation = array();
195 foreach ( $resources as $resource ) {
196 if ( ! is_array( $resource ) || empty( $resource['id'] ) ) {
197 continue;
198 }
199 if ( class_exists( 'wpdev_bk_multiuser' ) && $owner_user_id !== absint( isset( $resource['owner_user_id'] ) ? $resource['owner_user_id'] : 0 ) ) {
200 continue;
201 }
202 $editable_resource = $this->get_existing_resource_editable_fields( $resource );
203 $editable_resource['id'] = absint( $resource['id'] );
204 $editable_resource['resource_id'] = absint( $resource['id'] );
205 $editable_resource['source_fingerprint'] = $this->get_existing_resource_fingerprint( $resource );
206 $presentation[] = $editable_resource;
207 }
208
209 return $presentation;
210 }
211
212 /**
213 * Normalize the Setup Wizard fields editable on one authorized Resource.
214 *
215 * This method is shared by presentation, stale-write detection, retry
216 * recovery, and compensation so all layers compare the same values.
217 *
218 * @param array<string,mixed> $resource Canonical authorized Resource.
219 *
220 * @return array<string,string> Editable Resource fields.
221 */
222 public function get_existing_resource_editable_fields( array $resource ) {
223 $base_cost = '0.00';
224 if ( $this->is_pricing_available() ) {
225 $raw_cost = isset( $resource['cost'] ) && is_numeric( $resource['cost'] ) ? (float) $resource['cost'] : 0.0;
226 $base_cost = number_format( max( 0, $raw_cost ), 2, '.', '' );
227 }
228
229 return array(
230 'title' => sanitize_text_field( isset( $resource['title'] ) ? (string) $resource['title'] : '' ),
231 'description' => sanitize_textarea_field( isset( $resource['description'] ) ? (string) $resource['description'] : '' ),
232 'picture_url' => esc_url_raw( isset( $resource['picture_url'] ) ? (string) $resource['picture_url'] : '' ),
233 'base_cost' => $base_cost,
234 );
235 }
236
237 /**
238 * Build a non-reversible fingerprint for editable Resource values.
239 *
240 * @param array<string,mixed> $resource Canonical authorized Resource.
241 *
242 * @return string SHA-256 fingerprint.
243 */
244 public function get_existing_resource_fingerprint( array $resource ) {
245 $editable_fields = $this->get_existing_resource_editable_fields( $resource );
246
247 return $this->get_editable_resource_fields_fingerprint( $editable_fields );
248 }
249
250 /**
251 * Build a fingerprint from an already normalized editable-field map.
252 *
253 * Extra transport properties are deliberately ignored so a target update and
254 * a reloaded canonical Resource can be compared through the same contract.
255 *
256 * @param array<string,mixed> $editable_fields Normalized editable values.
257 *
258 * @return string SHA-256 fingerprint.
259 */
260 public function get_editable_resource_fields_fingerprint( array $editable_fields ) {
261 $fingerprint_fields = array(
262 'title' => isset( $editable_fields['title'] ) ? (string) $editable_fields['title'] : '',
263 'description' => isset( $editable_fields['description'] ) ? (string) $editable_fields['description'] : '',
264 'picture_url' => isset( $editable_fields['picture_url'] ) ? (string) $editable_fields['picture_url'] : '',
265 'base_cost' => isset( $editable_fields['base_cost'] ) ? (string) $editable_fields['base_cost'] : '0.00',
266 );
267 $encoded_fields = wp_json_encode( $fingerprint_fields );
268
269 return hash( 'sha256', false === $encoded_fields ? '[]' : $encoded_fields );
270 }
271
272 /**
273 * Determine whether the active edition may create additional Resources.
274 *
275 * @return bool True for Personal and higher while capacity remains.
276 */
277 public function can_create_resources() {
278 return class_exists( 'wpdev_bk_personal' )
279 && class_exists( 'WPBC_Catalog_Booking_Resource_Inspector_Schema' )
280 && 0 < $this->get_maximum_resource_drafts();
281 }
282
283 /**
284 * Determine whether Resource cost is supported by the active edition.
285 *
286 * @return bool True for Business Small and higher.
287 */
288 public function is_pricing_available() {
289 return class_exists( 'wpdev_bk_biz_s' );
290 }
291
292 /**
293 * Return the bounded current-edition create limit for this wizard visit.
294 *
295 * @return int Number of proposals allowed between zero and twenty.
296 */
297 public function get_maximum_resource_drafts() {
298 if ( ! class_exists( 'wpdev_bk_personal' ) || ! class_exists( 'WPBC_Catalog_Booking_Resource_Inspector_Schema' ) ) {
299 return 0;
300 }
301 $schema = new WPBC_Catalog_Booking_Resource_Inspector_Schema();
302
303 return min( self::MAX_RESOURCE_DRAFTS, max( 0, absint( $schema->get_maximum_quantity() ) ) );
304 }
305
306 /**
307 * Validate one Resource proposal and reject unregistered properties.
308 *
309 * @param mixed $raw_resource Untrusted Resource value.
310 * @param int $resource_index Zero-based proposal index.
311 * @param bool $require_complete Whether a staged title is required.
312 *
313 * @return array<string,mixed>|WP_Error Normalized proposal or an error.
314 */
315 private function validate_resource_draft( $raw_resource, $resource_index, $require_complete ) {
316 if ( ! is_array( $raw_resource ) ) {
317 return new WP_Error( 'wpbc_setup_wizard_resource_invalid', __( 'Enter valid Booking Resource details.', 'booking' ) );
318 }
319 $allowed_keys = array( 'draft_id', 'title', 'description', 'picture_url', 'base_cost' );
320 if ( array_diff( array_keys( $raw_resource ), $allowed_keys ) ) {
321 return new WP_Error( 'wpbc_setup_wizard_resource_unknown_field', __( 'The Booking Resource draft contains an unsupported field.', 'booking' ) );
322 }
323
324 if ( isset( $raw_resource['title'] ) && ! is_scalar( $raw_resource['title'] ) ) {
325 return new WP_Error( 'wpbc_setup_wizard_resource_title_invalid', __( 'Enter a valid Booking Resource name.', 'booking' ) );
326 }
327 $title = isset( $raw_resource['title'] ) ? sanitize_text_field( (string) $raw_resource['title'] ) : '';
328 if ( $require_complete && '' === trim( $title ) ) {
329 return new WP_Error( 'wpbc_setup_wizard_resource_title_required', __( 'Enter a name for every new Booking Resource.', 'booking' ) );
330 }
331 if ( $this->get_text_length( $title ) > 200 ) {
332 return new WP_Error( 'wpbc_setup_wizard_resource_title_too_long', __( 'Use 200 characters or fewer for each Booking Resource name.', 'booking' ) );
333 }
334
335 if ( isset( $raw_resource['description'] ) && ! is_scalar( $raw_resource['description'] ) ) {
336 return new WP_Error( 'wpbc_setup_wizard_resource_description_invalid', __( 'Enter a valid Booking Resource description.', 'booking' ) );
337 }
338 $description = isset( $raw_resource['description'] ) ? sanitize_textarea_field( (string) $raw_resource['description'] ) : '';
339 if ( $this->get_text_length( $description ) > self::MAX_DESCRIPTION_LENGTH ) {
340 return new WP_Error( 'wpbc_setup_wizard_resource_description_too_long', __( 'Use 2000 characters or fewer for each Booking Resource description.', 'booking' ) );
341 }
342
343 if ( isset( $raw_resource['picture_url'] ) && ! is_scalar( $raw_resource['picture_url'] ) ) {
344 return new WP_Error( 'wpbc_setup_wizard_resource_picture_invalid', __( 'Choose a valid Booking Resource image.', 'booking' ) );
345 }
346 $picture_url = isset( $raw_resource['picture_url'] ) ? trim( (string) $raw_resource['picture_url'] ) : '';
347 if ( $this->get_text_length( $picture_url ) > self::MAX_PICTURE_URL_LENGTH ) {
348 return new WP_Error( 'wpbc_setup_wizard_resource_picture_too_long', __( 'Choose a valid Booking Resource image.', 'booking' ) );
349 }
350 $picture_url = '' === $picture_url ? '' : esc_url_raw( $picture_url );
351 if ( isset( $raw_resource['picture_url'] ) && '' !== trim( (string) $raw_resource['picture_url'] ) && '' === $picture_url ) {
352 return new WP_Error( 'wpbc_setup_wizard_resource_picture_invalid', __( 'Choose a valid Booking Resource image.', 'booking' ) );
353 }
354
355 $base_cost = '0.00';
356 if ( $this->is_pricing_available() ) {
357 $raw_base_cost = isset( $raw_resource['base_cost'] ) ? $raw_resource['base_cost'] : 0;
358 $base_cost_number = is_scalar( $raw_base_cost ) ? str_replace( ',', '.', (string) $raw_base_cost ) : '';
359 if ( ! is_numeric( $base_cost_number ) || ! is_finite( (float) $base_cost_number ) || 0 > (float) $base_cost_number ) {
360 return new WP_Error( 'wpbc_setup_wizard_resource_price_invalid', __( 'Enter a valid non-negative base cost.', 'booking' ) );
361 }
362 $base_cost = number_format( (float) $base_cost_number, 2, '.', '' );
363 }
364
365 if ( isset( $raw_resource['draft_id'] ) && ! is_scalar( $raw_resource['draft_id'] ) ) {
366 return new WP_Error( 'wpbc_setup_wizard_resource_draft_id_invalid', __( 'The Booking Resource draft identifier is invalid.', 'booking' ) );
367 }
368 $draft_id = isset( $raw_resource['draft_id'] ) ? sanitize_key( (string) $raw_resource['draft_id'] ) : '';
369 if ( '' === $draft_id ) {
370 $draft_id = 'draft-' . ( $resource_index + 1 );
371 }
372
373 return array(
374 'draft_id' => substr( $draft_id, 0, 64 ),
375 'title' => $title,
376 'description' => $description,
377 'picture_url' => $picture_url,
378 'base_cost' => $base_cost,
379 );
380 }
381
382 /**
383 * Validate one existing Resource update and its stale-write token.
384 *
385 * @param mixed $raw_update Untrusted decoded update.
386 *
387 * @return array<string,mixed>|WP_Error Normalized update or an error.
388 */
389 private function validate_existing_resource_update( $raw_update ) {
390 if ( ! is_array( $raw_update ) ) {
391 return new WP_Error( 'wpbc_setup_wizard_resource_update_invalid', __( 'Enter valid Booking Resource details.', 'booking' ) );
392 }
393 $allowed_keys = array( 'resource_id', 'source_fingerprint', 'title', 'description', 'picture_url', 'base_cost' );
394 if ( array_diff( array_keys( $raw_update ), $allowed_keys ) ) {
395 return new WP_Error( 'wpbc_setup_wizard_resource_update_unknown_field', __( 'The Booking Resource update contains an unsupported field.', 'booking' ) );
396 }
397
398 $resource_id = isset( $raw_update['resource_id'] ) && is_scalar( $raw_update['resource_id'] ) ? absint( $raw_update['resource_id'] ) : 0;
399 if ( ! $resource_id ) {
400 return new WP_Error( 'wpbc_setup_wizard_resource_update_id_invalid', __( 'The Booking Resource update is invalid.', 'booking' ) );
401 }
402 $source_fingerprint = isset( $raw_update['source_fingerprint'] ) && is_scalar( $raw_update['source_fingerprint'] ) ? strtolower( (string) $raw_update['source_fingerprint'] ) : '';
403 if ( ! preg_match( '/^[a-f0-9]{64}$/', $source_fingerprint ) ) {
404 return new WP_Error( 'wpbc_setup_wizard_resource_update_fingerprint_invalid', __( 'The Booking Resource update is stale. Reload the step and try again.', 'booking' ) );
405 }
406
407 $normalized_fields = $this->validate_editable_fields( $raw_update, false );
408 if ( is_wp_error( $normalized_fields ) ) {
409 return $normalized_fields;
410 }
411
412 return array_merge(
413 array(
414 'resource_id' => $resource_id,
415 'source_fingerprint' => $source_fingerprint,
416 ),
417 $normalized_fields
418 );
419 }
420
421 /**
422 * Validate the common editable Resource fields.
423 *
424 * @param array<string,mixed> $raw_fields Untrusted fields.
425 * @param bool $preserve_markup Whether permitted description markup must be preserved.
426 *
427 * @return array<string,string>|WP_Error Normalized fields or an error.
428 */
429 private function validate_editable_fields( array $raw_fields, $preserve_markup ) {
430 if ( isset( $raw_fields['title'] ) && ! is_scalar( $raw_fields['title'] ) ) {
431 return new WP_Error( 'wpbc_setup_wizard_resource_title_invalid', __( 'Enter a valid Booking Resource name.', 'booking' ) );
432 }
433 $title = isset( $raw_fields['title'] ) ? sanitize_text_field( (string) $raw_fields['title'] ) : '';
434 if ( '' === trim( $title ) ) {
435 return new WP_Error( 'wpbc_setup_wizard_resource_title_required', __( 'Enter a name for every Booking Resource.', 'booking' ) );
436 }
437 if ( $this->get_text_length( $title ) > 200 ) {
438 return new WP_Error( 'wpbc_setup_wizard_resource_title_too_long', __( 'Use 200 characters or fewer for each Booking Resource name.', 'booking' ) );
439 }
440
441 if ( isset( $raw_fields['description'] ) && ! is_scalar( $raw_fields['description'] ) ) {
442 return new WP_Error( 'wpbc_setup_wizard_resource_description_invalid', __( 'Enter a valid Booking Resource description.', 'booking' ) );
443 }
444 $raw_description = isset( $raw_fields['description'] ) ? (string) $raw_fields['description'] : '';
445 $description = $preserve_markup ? wp_kses_post( $raw_description ) : sanitize_textarea_field( $raw_description );
446 if ( $this->get_text_length( $description ) > self::MAX_DESCRIPTION_LENGTH ) {
447 return new WP_Error( 'wpbc_setup_wizard_resource_description_too_long', __( 'Use 2000 characters or fewer for each Booking Resource description.', 'booking' ) );
448 }
449
450 if ( isset( $raw_fields['picture_url'] ) && ! is_scalar( $raw_fields['picture_url'] ) ) {
451 return new WP_Error( 'wpbc_setup_wizard_resource_picture_invalid', __( 'Choose a valid Booking Resource image.', 'booking' ) );
452 }
453 $raw_picture_url = isset( $raw_fields['picture_url'] ) ? trim( (string) $raw_fields['picture_url'] ) : '';
454 if ( $this->get_text_length( $raw_picture_url ) > self::MAX_PICTURE_URL_LENGTH ) {
455 return new WP_Error( 'wpbc_setup_wizard_resource_picture_too_long', __( 'Choose a valid Booking Resource image.', 'booking' ) );
456 }
457 $picture_url = '' === $raw_picture_url ? '' : esc_url_raw( $raw_picture_url );
458 if ( '' !== $raw_picture_url && '' === $picture_url ) {
459 return new WP_Error( 'wpbc_setup_wizard_resource_picture_invalid', __( 'Choose a valid Booking Resource image.', 'booking' ) );
460 }
461
462 $base_cost = '0.00';
463 if ( $this->is_pricing_available() ) {
464 $raw_base_cost = isset( $raw_fields['base_cost'] ) ? $raw_fields['base_cost'] : 0;
465 $normalized_cost = is_scalar( $raw_base_cost ) ? str_replace( ',', '.', (string) $raw_base_cost ) : '';
466 if ( ! is_numeric( $normalized_cost ) || ! is_finite( (float) $normalized_cost ) || 0 > (float) $normalized_cost ) {
467 return new WP_Error( 'wpbc_setup_wizard_resource_price_invalid', __( 'Enter a valid non-negative base cost.', 'booking' ) );
468 }
469 $base_cost = number_format( (float) $normalized_cost, 2, '.', '' );
470 }
471
472 return array(
473 'title' => $title,
474 'description' => $description,
475 'picture_url' => $picture_url,
476 'base_cost' => $base_cost,
477 );
478 }
479
480 /**
481 * Measure text without requiring the multibyte PHP extension.
482 *
483 * @param string $text Text to measure.
484 *
485 * @return int Character or byte length.
486 */
487 private function get_text_length( $text ) {
488 return function_exists( 'mb_strlen' ) ? mb_strlen( (string) $text ) : strlen( (string) $text );
489 }
490 }
491