booking
/
includes
/
page-catalog-booking-resources
/
mutations
/
class-wpbc-catalog-booking-resources-inline-editor.php
class-wpbc-catalog-booking-resources-inline-editor.php in Booking Calendar 11.9, at includes/page-catalog-booking-resources/mutations/class-wpbc-catalog-booking-resources-inline-editor.php
| 1 | <?php |
| 2 | /** |
| 3 | * Independent reviewed inline updates for the template-driven Resource catalog. |
| 4 | * |
| 5 | * @package Booking Calendar |
| 6 | * @since 11.6.0 |
| 7 | */ |
| 8 | |
| 9 | if ( ! defined( 'ABSPATH' ) ) { |
| 10 | exit; |
| 11 | } |
| 12 | |
| 13 | /** |
| 14 | * Build, preview, and apply row-specific drafts without legacy editor services. |
| 15 | * |
| 16 | * Browser drafts are never trusted as complete Resource records. Every preview |
| 17 | * and apply rebuilds its plan from current authorized repository values, and a |
| 18 | * signed review binds the normalized replacements to those current values. |
| 19 | */ |
| 20 | final class WPBC_Catalog_Booking_Resources_Inline_Editor { |
| 21 | |
| 22 | /** Maximum number of visible rows accepted from one catalog page. */ |
| 23 | const MAX_ROWS = 100; |
| 24 | |
| 25 | /** Maximum bytes accepted for one submitted scalar field. */ |
| 26 | const MAX_FIELD_BYTES = 10000; |
| 27 | |
| 28 | /** @var WPBC_Catalog_Booking_Resources_Repository Independent read repository. */ |
| 29 | private $repository; |
| 30 | |
| 31 | /** @var WPBC_Catalog_Booking_Resource_Inspector_Schema Current field allow-lists. */ |
| 32 | private $schema; |
| 33 | |
| 34 | /** @var WPBC_Catalog_Booking_Resource_Updater Independent validated updater. */ |
| 35 | private $updater; |
| 36 | |
| 37 | /** @var WPBC_Catalog_Booking_Resources_Inline_Fields Domain field provider. */ |
| 38 | private $inline_fields; |
| 39 | |
| 40 | /** |
| 41 | * Initialize independent collaborators. |
| 42 | * |
| 43 | * @param WPBC_Catalog_Booking_Resources_Repository|null $repository Optional repository for tests. |
| 44 | * @param WPBC_Catalog_Booking_Resource_Inspector_Schema|null $schema Optional schema service for tests. |
| 45 | * @param WPBC_Catalog_Booking_Resource_Updater|null $updater Optional updater for tests. |
| 46 | * @param WPBC_Catalog_Booking_Resources_Inline_Fields|null $inline_fields Optional field provider for tests. |
| 47 | */ |
| 48 | public function __construct( $repository = null, $schema = null, $updater = null, $inline_fields = null ) { |
| 49 | $this->repository = $repository instanceof WPBC_Catalog_Booking_Resources_Repository ? $repository : new WPBC_Catalog_Booking_Resources_Repository(); |
| 50 | $this->schema = $schema instanceof WPBC_Catalog_Booking_Resource_Inspector_Schema ? $schema : new WPBC_Catalog_Booking_Resource_Inspector_Schema(); |
| 51 | $this->updater = $updater instanceof WPBC_Catalog_Booking_Resource_Updater ? $updater : new WPBC_Catalog_Booking_Resource_Updater(); |
| 52 | $this->inline_fields = $inline_fields instanceof WPBC_Catalog_Booking_Resources_Inline_Fields ? $inline_fields : new WPBC_Catalog_Booking_Resources_Inline_Fields( $this->schema ); |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * Return row-specific controls for the requested visible Resources. |
| 57 | * |
| 58 | * @param array<int,mixed> $resource_ids Requested Resource IDs in catalog order. |
| 59 | * @return array<string,mixed>|WP_Error Client-safe schema or validation error. |
| 60 | */ |
| 61 | public function get_schema( $resource_ids ) { |
| 62 | $resources = $this->get_authorized_resources( $resource_ids ); |
| 63 | if ( is_wp_error( $resources ) ) { |
| 64 | return $resources; |
| 65 | } |
| 66 | |
| 67 | $rows = array(); |
| 68 | foreach ( $resources as $resource ) { |
| 69 | $rows[] = $this->build_row_schema( $resource ); |
| 70 | } |
| 71 | |
| 72 | return array( |
| 73 | 'maximum_rows' => self::MAX_ROWS, |
| 74 | 'rows' => $rows, |
| 75 | ); |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Build a signed, non-mutating review of row-specific drafts. |
| 80 | * |
| 81 | * @param array<int,mixed> $raw_rows Untrusted row draft envelopes. |
| 82 | * @return array<string,mixed>|WP_Error Review contract or validation error. |
| 83 | */ |
| 84 | public function preview( $raw_rows ) { |
| 85 | $plan = $this->build_plan( $raw_rows ); |
| 86 | if ( is_wp_error( $plan ) ) { |
| 87 | return $plan; |
| 88 | } |
| 89 | $review_rows = array(); |
| 90 | foreach ( $plan['client_changes'] as $change ) { |
| 91 | $review_fields = array(); |
| 92 | foreach ( $change['fields'] as $field ) { |
| 93 | $review_fields[] = array( |
| 94 | 'key' => isset( $field['key'] ) ? sanitize_key( $field['key'] ) : '', |
| 95 | 'label' => (string) $field['label'], |
| 96 | 'before' => (string) $field['old'], |
| 97 | 'after' => (string) $field['new'], |
| 98 | ); |
| 99 | } |
| 100 | $review_rows[] = array( |
| 101 | 'id' => absint( $change['resource_id'] ), |
| 102 | 'title' => (string) $change['title'], |
| 103 | 'fields' => $review_fields, |
| 104 | 'notes' => array(), |
| 105 | ); |
| 106 | } |
| 107 | |
| 108 | return array( |
| 109 | 'changes' => $plan['client_changes'], |
| 110 | 'review' => array( 'rows' => $review_rows ), |
| 111 | 'review_token' => $this->create_review_token( $plan ), |
| 112 | ); |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Apply a reviewed plan after rebuilding and revalidating current values. |
| 117 | * |
| 118 | * Completed rows are compensated in reverse order when a later update fails. |
| 119 | * |
| 120 | * @param array<int,mixed> $raw_rows Untrusted row draft envelopes. |
| 121 | * @param string $review_token Token returned by preview(). |
| 122 | * @return array<string,mixed>|WP_Error Apply result or safe error. |
| 123 | */ |
| 124 | public function apply( $raw_rows, $review_token ) { |
| 125 | $plan = $this->build_plan( $raw_rows, true ); |
| 126 | if ( is_wp_error( $plan ) ) { |
| 127 | return $plan; |
| 128 | } |
| 129 | $expected_token = $this->create_review_token( $plan ); |
| 130 | if ( '' === $review_token || ! hash_equals( $expected_token, $review_token ) ) { |
| 131 | return new WP_Error( 'wpbc_catalog_inline_review_stale', __( 'One or more Booking Resources changed after the review. Review the inline changes again before applying them.', 'booking' ) ); |
| 132 | } |
| 133 | if ( empty( $plan['rows'] ) ) { |
| 134 | return new WP_Error( 'wpbc_catalog_inline_no_changes', __( 'The inline editor does not contain any changes.', 'booking' ) ); |
| 135 | } |
| 136 | |
| 137 | $saved_rows = array(); |
| 138 | foreach ( $plan['rows'] as $row_plan ) { |
| 139 | $updated = $this->updater->update( absint( $row_plan['resource_id'] ), $row_plan['new_fields'] ); |
| 140 | if ( is_wp_error( $updated ) ) { |
| 141 | $rollback_errors = $this->rollback( $saved_rows ); |
| 142 | if ( ! empty( $rollback_errors ) ) { |
| 143 | return new WP_Error( 'wpbc_catalog_inline_rollback_failed', __( 'The inline update stopped, and one or more completed changes could not be restored. Reload the catalog and verify the affected Booking Resources.', 'booking' ) ); |
| 144 | } |
| 145 | |
| 146 | return $updated; |
| 147 | } |
| 148 | $saved_rows[] = array( |
| 149 | 'resource_id' => absint( $row_plan['resource_id'] ), |
| 150 | 'fields' => $row_plan['old_fields'], |
| 151 | ); |
| 152 | } |
| 153 | |
| 154 | $updated_ids = array_values( array_map( 'absint', wp_list_pluck( $saved_rows, 'resource_id' ) ) ); |
| 155 | do_action( 'wpbc_catalog_booking_resources_inline_updated', $updated_ids ); |
| 156 | |
| 157 | return array( |
| 158 | 'updated_ids' => $updated_ids, |
| 159 | 'updated_count' => count( $updated_ids ), |
| 160 | ); |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Build a current-value-bound plan from submitted row drafts. |
| 165 | * |
| 166 | * @param array<int,mixed> $raw_rows Untrusted row envelopes. |
| 167 | * @param bool $allow_no_changes Whether apply may compare an empty rebuilt plan. |
| 168 | * @return array<string,mixed>|WP_Error Normalized plan or validation error. |
| 169 | */ |
| 170 | private function build_plan( $raw_rows, $allow_no_changes = false ) { |
| 171 | $requested_rows = $this->normalize_requested_rows( $raw_rows ); |
| 172 | if ( is_wp_error( $requested_rows ) ) { |
| 173 | return $requested_rows; |
| 174 | } |
| 175 | $resources = $this->get_authorized_resources( wp_list_pluck( $requested_rows, 'resource_id' ) ); |
| 176 | if ( is_wp_error( $resources ) ) { |
| 177 | return $resources; |
| 178 | } |
| 179 | $resources_by_id = array(); |
| 180 | foreach ( $resources as $resource ) { |
| 181 | $resources_by_id[ absint( $resource['id'] ) ] = $resource; |
| 182 | } |
| 183 | |
| 184 | $plan_rows = array(); |
| 185 | $client_changes = array(); |
| 186 | foreach ( $requested_rows as $requested_row ) { |
| 187 | $resource_id = absint( $requested_row['resource_id'] ); |
| 188 | $resource = isset( $resources_by_id[ $resource_id ] ) ? $resources_by_id[ $resource_id ] : null; |
| 189 | if ( null === $resource ) { |
| 190 | return new WP_Error( 'wpbc_catalog_inline_resource_unavailable', __( 'One of the Booking Resources is no longer available to this account.', 'booking' ) ); |
| 191 | } |
| 192 | |
| 193 | $row_schema = $this->build_row_schema( $resource ); |
| 194 | $fields_by_key = array(); |
| 195 | foreach ( $row_schema['fields'] as $field ) { |
| 196 | $fields_by_key[ $field['key'] ] = $field; |
| 197 | } |
| 198 | |
| 199 | $old_fields = $this->get_complete_fields( $resource ); |
| 200 | $new_fields = $old_fields; |
| 201 | $field_changes = array(); |
| 202 | $has_changes = false; |
| 203 | foreach ( $requested_row['fields'] as $field_key => $submitted_value ) { |
| 204 | if ( ! isset( $fields_by_key[ $field_key ] ) ) { |
| 205 | return new WP_Error( 'wpbc_catalog_inline_field_unsupported', __( 'One of the inline fields is no longer available for this Booking Resource.', 'booking' ) ); |
| 206 | } |
| 207 | $field = $fields_by_key[ $field_key ]; |
| 208 | $normalized = $this->normalize_field_value( $submitted_value, $field ); |
| 209 | $current_value = (string) $field['value']; |
| 210 | if ( is_wp_error( $normalized ) ) { |
| 211 | return $normalized; |
| 212 | } |
| 213 | if ( (string) $normalized === $current_value ) { |
| 214 | continue; |
| 215 | } |
| 216 | $new_fields[ $field_key ] = $normalized; |
| 217 | $has_changes = true; |
| 218 | $field_changes[] = array( |
| 219 | 'key' => $field_key, |
| 220 | 'label' => $field['label'], |
| 221 | 'old' => $this->format_value( $current_value, $field ), |
| 222 | 'new' => $this->format_value( $normalized, $field ), |
| 223 | ); |
| 224 | } |
| 225 | |
| 226 | if ( ! $has_changes ) { |
| 227 | continue; |
| 228 | } |
| 229 | $plan_rows[] = array( |
| 230 | 'resource_id' => absint( $resource['id'] ), |
| 231 | 'old_fields' => $old_fields, |
| 232 | 'new_fields' => $new_fields, |
| 233 | ); |
| 234 | $client_changes[] = array( |
| 235 | 'resource_id' => absint( $resource['id'] ), |
| 236 | 'title' => (string) $resource['title'], |
| 237 | 'fields' => $field_changes, |
| 238 | ); |
| 239 | } |
| 240 | |
| 241 | if ( empty( $plan_rows ) && ! $allow_no_changes ) { |
| 242 | return new WP_Error( 'wpbc_catalog_inline_no_changes', __( 'The inline editor does not contain any changes.', 'booking' ) ); |
| 243 | } |
| 244 | |
| 245 | return array( |
| 246 | 'rows' => $plan_rows, |
| 247 | 'client_changes' => $client_changes, |
| 248 | ); |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Normalize bounded row envelopes while rejecting duplicates and objects. |
| 253 | * |
| 254 | * @param array<int,mixed> $raw_rows Untrusted row envelopes. |
| 255 | * @return array<int,array<string,mixed>>|WP_Error Normalized rows or error. |
| 256 | */ |
| 257 | private function normalize_requested_rows( $raw_rows ) { |
| 258 | if ( ! is_array( $raw_rows ) || empty( $raw_rows ) || count( $raw_rows ) > self::MAX_ROWS ) { |
| 259 | return new WP_Error( 'wpbc_catalog_inline_rows_invalid', __( 'The inline Booking Resource changes are invalid.', 'booking' ) ); |
| 260 | } |
| 261 | $normalized_rows = array(); |
| 262 | $seen_ids = array(); |
| 263 | foreach ( $raw_rows as $raw_row ) { |
| 264 | if ( ! is_array( $raw_row ) || empty( $raw_row['resource_id'] ) || empty( $raw_row['fields'] ) || ! is_array( $raw_row['fields'] ) ) { |
| 265 | return new WP_Error( 'wpbc_catalog_inline_row_invalid', __( 'One of the inline Booking Resource rows is invalid.', 'booking' ) ); |
| 266 | } |
| 267 | $resource_id = absint( $raw_row['resource_id'] ); |
| 268 | if ( ! $resource_id || isset( $seen_ids[ $resource_id ] ) || count( $raw_row['fields'] ) > 5 ) { |
| 269 | return new WP_Error( 'wpbc_catalog_inline_row_invalid', __( 'One of the inline Booking Resource rows is invalid.', 'booking' ) ); |
| 270 | } |
| 271 | $normalized_fields = array(); |
| 272 | foreach ( $raw_row['fields'] as $field_key => $field_value ) { |
| 273 | $field_key = sanitize_key( $field_key ); |
| 274 | if ( '' === $field_key || ! is_scalar( $field_value ) || strlen( (string) $field_value ) > self::MAX_FIELD_BYTES ) { |
| 275 | return new WP_Error( 'wpbc_catalog_inline_value_invalid', __( 'One of the inline Booking Resource values is invalid.', 'booking' ) ); |
| 276 | } |
| 277 | $normalized_fields[ $field_key ] = (string) $field_value; |
| 278 | } |
| 279 | $seen_ids[ $resource_id ] = true; |
| 280 | $normalized_rows[] = array( 'resource_id' => $resource_id, 'fields' => $normalized_fields ); |
| 281 | } |
| 282 | |
| 283 | return $normalized_rows; |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * Load unique authorized Resources while preserving requested order. |
| 288 | * |
| 289 | * @param array<int,mixed> $resource_ids Requested IDs. |
| 290 | * @return array<int,array<string,mixed>>|WP_Error Resources or validation error. |
| 291 | */ |
| 292 | private function get_authorized_resources( $resource_ids ) { |
| 293 | $normalized_ids = array(); |
| 294 | foreach ( is_array( $resource_ids ) ? $resource_ids : array() as $resource_id ) { |
| 295 | $resource_id = absint( $resource_id ); |
| 296 | if ( $resource_id ) { |
| 297 | $normalized_ids[ $resource_id ] = $resource_id; |
| 298 | } |
| 299 | } |
| 300 | $normalized_ids = array_values( $normalized_ids ); |
| 301 | if ( empty( $normalized_ids ) || count( $normalized_ids ) > self::MAX_ROWS ) { |
| 302 | return new WP_Error( 'wpbc_catalog_inline_selection_invalid', __( 'The inline Booking Resource selection is invalid.', 'booking' ) ); |
| 303 | } |
| 304 | $resources = array(); |
| 305 | foreach ( $normalized_ids as $resource_id ) { |
| 306 | $resource = $this->repository->get_resource( $resource_id ); |
| 307 | if ( is_wp_error( $resource ) ) { |
| 308 | return $resource; |
| 309 | } |
| 310 | if ( null === $resource ) { |
| 311 | return new WP_Error( 'wpbc_catalog_inline_resource_unavailable', __( 'One of the Booking Resources is no longer available to this account.', 'booking' ) ); |
| 312 | } |
| 313 | $resources[] = $resource; |
| 314 | } |
| 315 | $shortcodes = $this->repository->get_publishing_shortcodes( $normalized_ids ); |
| 316 | foreach ( $resources as $resource_index => $resource ) { |
| 317 | $resources[ $resource_index ] = $this->prepare_resource( $resource, $shortcodes ); |
| 318 | } |
| 319 | |
| 320 | return $resources; |
| 321 | } |
| 322 | |
| 323 | /** |
| 324 | * Attach persisted values that are intentionally absent from list records. |
| 325 | * |
| 326 | * Inline updates submit a complete updater snapshot. Loading the current |
| 327 | * publishing shortcode here prevents an unrelated inline title, cost, or |
| 328 | * priority change from replacing a customized shortcode with its fallback. |
| 329 | * |
| 330 | * @param array<string,mixed> $resource Authorized Resource record. |
| 331 | * @param array<int,string> $shortcodes Persisted shortcodes keyed by Resource ID. |
| 332 | * @return array<string,mixed> Resource with its persisted shortcode. |
| 333 | */ |
| 334 | private function prepare_resource( $resource, $shortcodes ) { |
| 335 | $resource_id = isset( $resource['id'] ) ? absint( $resource['id'] ) : 0; |
| 336 | |
| 337 | $resource['publishing_shortcode'] = isset( $shortcodes[ $resource_id ] ) |
| 338 | ? (string) $shortcodes[ $resource_id ] |
| 339 | : '[booking resource_id=' . $resource_id . ']'; |
| 340 | |
| 341 | return $resource; |
| 342 | } |
| 343 | |
| 344 | /** |
| 345 | * Build executable-free inline controls from the independent inspector schema. |
| 346 | * |
| 347 | * @param array<string,mixed> $resource Authorized Resource record. |
| 348 | * @return array<string,mixed> Client-safe row schema. |
| 349 | */ |
| 350 | private function build_row_schema( $resource ) { |
| 351 | return array( |
| 352 | 'resource_id' => absint( $resource['id'] ), |
| 353 | 'title' => (string) $resource['title'], |
| 354 | 'fields' => $this->inline_fields->get_inline_fields( $resource ), |
| 355 | ); |
| 356 | } |
| 357 | |
| 358 | /** |
| 359 | * Return the complete validated-updater submission for one Resource. |
| 360 | * |
| 361 | * Partial inline drafts are merged into this snapshot before persistence so |
| 362 | * fields outside the current table view retain their canonical values. |
| 363 | * |
| 364 | * @param array<string,mixed> $resource Current authorized Resource. |
| 365 | * @return array<string,mixed> Complete editable field map. |
| 366 | */ |
| 367 | private function get_complete_fields( $resource ) { |
| 368 | $fields = array( |
| 369 | 'title' => isset( $resource['title'] ) ? (string) $resource['title'] : '', |
| 370 | 'description' => isset( $resource['description'] ) ? (string) $resource['description'] : '', |
| 371 | 'picture_url' => isset( $resource['picture_url'] ) ? (string) $resource['picture_url'] : '', |
| 372 | ); |
| 373 | $edit_schema = $this->schema->get_edit_schema( $resource ); |
| 374 | foreach ( ! is_wp_error( $edit_schema ) && isset( $edit_schema['sections'] ) ? $edit_schema['sections'] : array() as $section ) { |
| 375 | foreach ( isset( $section['fields'] ) ? (array) $section['fields'] : array() as $field ) { |
| 376 | $field_key = isset( $field['key'] ) ? sanitize_key( $field['key'] ) : ''; |
| 377 | if ( '' !== $field_key && ! empty( $field['editable'] ) && array_key_exists( 'value', $field ) && is_scalar( $field['value'] ) ) { |
| 378 | $fields[ $field_key ] = (string) $field['value']; |
| 379 | } |
| 380 | } |
| 381 | } |
| 382 | |
| 383 | return $fields; |
| 384 | } |
| 385 | |
| 386 | /** |
| 387 | * Normalize a draft using the server-authoritative field definition. |
| 388 | * |
| 389 | * @param string $submitted_value Submitted scalar value. |
| 390 | * @param array<string,mixed> $field Current field definition. |
| 391 | * @return string|WP_Error Normalized value or validation error. |
| 392 | */ |
| 393 | private function normalize_field_value( $submitted_value, $field ) { |
| 394 | $field_key = $field['key']; |
| 395 | if ( 'title' === $field_key ) { |
| 396 | $submitted_value = sanitize_text_field( $submitted_value ); |
| 397 | if ( '' === $submitted_value || $this->get_text_length( $submitted_value ) > 200 ) { |
| 398 | return new WP_Error( 'wpbc_catalog_inline_title_invalid', __( 'Enter a valid Booking Resource title.', 'booking' ) ); |
| 399 | } |
| 400 | return $submitted_value; |
| 401 | } |
| 402 | if ( 'description' === $field_key ) { |
| 403 | $submitted_value = wp_kses_post( $submitted_value ); |
| 404 | return $this->get_text_length( $submitted_value ) <= 2000 ? $submitted_value : new WP_Error( 'wpbc_catalog_inline_description_invalid', __( 'The Booking Resource description is too long.', 'booking' ) ); |
| 405 | } |
| 406 | if ( 'select' === $field['type'] ) { |
| 407 | $allowed_values = array_map( 'strval', wp_list_pluck( $field['options'], 'value' ) ); |
| 408 | return in_array( (string) $submitted_value, $allowed_values, true ) ? (string) $submitted_value : new WP_Error( 'wpbc_catalog_inline_option_invalid', __( 'Select an available value for this Booking Resource.', 'booking' ) ); |
| 409 | } |
| 410 | if ( 'number' === $field['type'] ) { |
| 411 | $submitted_value = str_replace( ',', '.', trim( (string) $submitted_value ) ); |
| 412 | if ( '' === $submitted_value || ! is_numeric( $submitted_value ) || ! is_finite( (float) $submitted_value ) ) { |
| 413 | return new WP_Error( 'wpbc_catalog_inline_number_invalid', __( 'Enter a valid number for this Booking Resource.', 'booking' ) ); |
| 414 | } |
| 415 | if ( isset( $field['min'] ) && (float) $submitted_value < (float) $field['min'] ) { |
| 416 | return new WP_Error( 'wpbc_catalog_inline_number_invalid', __( 'Enter a valid number for this Booking Resource.', 'booking' ) ); |
| 417 | } |
| 418 | if ( 'priority' === $field_key && ! preg_match( '/^\d+$/', $submitted_value ) ) { |
| 419 | return new WP_Error( 'wpbc_catalog_inline_priority_invalid', __( 'Priority must be a non-negative whole number.', 'booking' ) ); |
| 420 | } |
| 421 | return 'priority' === $field_key ? (string) absint( $submitted_value ) : (string) (float) $submitted_value; |
| 422 | } |
| 423 | |
| 424 | return sanitize_text_field( $submitted_value ); |
| 425 | } |
| 426 | |
| 427 | /** |
| 428 | * Format a review value with its field affixes or select label. |
| 429 | * |
| 430 | * @param string $field_value Normalized value. |
| 431 | * @param array<string,mixed> $field Field definition. |
| 432 | * @return string Human-readable plain value. |
| 433 | */ |
| 434 | private function format_value( $field_value, $field ) { |
| 435 | foreach ( isset( $field['options'] ) ? (array) $field['options'] : array() as $option ) { |
| 436 | if ( (string) $option['value'] === (string) $field_value ) { |
| 437 | return (string) $option['label']; |
| 438 | } |
| 439 | } |
| 440 | $prefix = isset( $field['prefix'] ) ? (string) $field['prefix'] : ''; |
| 441 | $suffix = isset( $field['suffix'] ) && '' !== (string) $field['suffix'] ? ' ' . (string) $field['suffix'] : ''; |
| 442 | |
| 443 | return trim( $prefix . wp_strip_all_tags( wpbc_lang( (string) $field_value ) ) . $suffix ); |
| 444 | } |
| 445 | |
| 446 | /** |
| 447 | * Sign current old values and normalized replacements for this user. |
| 448 | * |
| 449 | * @param array<string,mixed> $plan Current inline plan. |
| 450 | * @return string Review token. |
| 451 | */ |
| 452 | private function create_review_token( $plan ) { |
| 453 | return wp_hash( |
| 454 | wp_json_encode( |
| 455 | array( |
| 456 | 'user_id' => get_current_user_id(), |
| 457 | 'rows' => isset( $plan['rows'] ) ? $plan['rows'] : array(), |
| 458 | ) |
| 459 | ), |
| 460 | 'nonce' |
| 461 | ); |
| 462 | } |
| 463 | |
| 464 | /** |
| 465 | * Restore completed Resources in reverse order after a later failure. |
| 466 | * |
| 467 | * @param array<int,array<string,mixed>> $saved_rows Completed rows. |
| 468 | * @return array<int,string> Rollback errors. |
| 469 | */ |
| 470 | private function rollback( $saved_rows ) { |
| 471 | $errors = array(); |
| 472 | foreach ( array_reverse( $saved_rows ) as $saved_row ) { |
| 473 | $restored = $this->updater->update( absint( $saved_row['resource_id'] ), $saved_row['fields'] ); |
| 474 | if ( is_wp_error( $restored ) ) { |
| 475 | $errors[] = $restored->get_error_message(); |
| 476 | } |
| 477 | } |
| 478 | |
| 479 | return $errors; |
| 480 | } |
| 481 | |
| 482 | /** |
| 483 | * Return string length without requiring the multibyte extension. |
| 484 | * |
| 485 | * @param string $text Text to measure. |
| 486 | * @return int Character or byte length. |
| 487 | */ |
| 488 | private function get_text_length( $text ) { |
| 489 | return function_exists( 'mb_strlen' ) ? mb_strlen( (string) $text ) : strlen( (string) $text ); |
| 490 | } |
| 491 | } |
| 492 |