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
| 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 |