booking
/
includes
/
page-appointment-services
/
mutations
/
class-wpbc-appointment-services-catalog-editor.php
class-wpbc-appointment-services-catalog-editor.php in Booking Calendar 11.9, at includes/page-appointment-services/mutations/class-wpbc-appointment-services-catalog-editor.php
| 1 | <?php |
| 2 | /** |
| 3 | * Reviewed inline and bulk mutation service for the Appointment Services catalog. |
| 4 | * |
| 5 | * @package Booking Calendar |
| 6 | * @since 11.6.0 |
| 7 | */ |
| 8 | |
| 9 | if ( ! defined( 'ABSPATH' ) ) { exit; } |
| 10 | |
| 11 | /** |
| 12 | * Apply allow-listed Service changes only after a signed, current-state review. |
| 13 | * |
| 14 | * Service SQL, edition checks, ownership, and validation remain in the Service |
| 15 | * domain. The shared catalog supplies selection mechanics only. |
| 16 | */ |
| 17 | final class WPBC_Appointment_Services_Catalog_Editor { |
| 18 | |
| 19 | /** Maximum number of rows accepted by one catalog editing request. */ |
| 20 | const MAX_ROWS = 100; |
| 21 | |
| 22 | /** Maximum lifetime of a signed review in seconds. */ |
| 23 | const REVIEW_LIFETIME = 600; |
| 24 | |
| 25 | /** @var object Service-domain repository. */ |
| 26 | private $repository; |
| 27 | |
| 28 | /** @var WPBC_Appointment_Services_Catalog_Inline_Fields Domain field provider. */ |
| 29 | private $inline_fields; |
| 30 | |
| 31 | /** |
| 32 | * Construct the editor with an optional repository for runtime tests. |
| 33 | * |
| 34 | * @param object|null $repository Service repository exposing find() and save(). |
| 35 | * @param WPBC_Appointment_Services_Catalog_Inline_Fields|null $inline_fields Optional field provider for tests. |
| 36 | */ |
| 37 | public function __construct( $repository = null, $inline_fields = null ) { |
| 38 | $this->repository = is_object( $repository ) ? $repository : wpbc_appointment_services_get_data_provider(); |
| 39 | $this->inline_fields = $inline_fields instanceof WPBC_Appointment_Services_Catalog_Inline_Fields ? $inline_fields : new WPBC_Appointment_Services_Catalog_Inline_Fields(); |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * Return authorized row-specific controls for the visible Services. |
| 44 | * |
| 45 | * The browser receives only executable-free field metadata. Current edition, |
| 46 | * ownership, and field restrictions are recalculated again during preview |
| 47 | * and apply, so this presentation contract is never treated as authority. |
| 48 | * |
| 49 | * @param mixed $service_ids Visible Service identifiers in catalog order. |
| 50 | * |
| 51 | * @return array<string,mixed>|WP_Error Browser-safe schema or validation error. |
| 52 | */ |
| 53 | public function get_inline_schema( $service_ids ) { |
| 54 | $services = $this->load_services( $service_ids ); |
| 55 | if ( is_wp_error( $services ) ) { |
| 56 | return $services; |
| 57 | } |
| 58 | |
| 59 | $rows = array(); |
| 60 | foreach ( $services as $service ) { |
| 61 | $rows[] = array( |
| 62 | 'service_id' => absint( $service['service_id'] ), |
| 63 | 'title' => sanitize_text_field( (string) $service['title'] ), |
| 64 | 'fields' => $this->get_inline_fields( $service ), |
| 65 | ); |
| 66 | } |
| 67 | |
| 68 | return array( |
| 69 | 'maximum_rows' => self::MAX_ROWS, |
| 70 | 'rows' => $rows, |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Return the safe bulk-field intersection for an authorized selection. |
| 76 | * |
| 77 | * Booking Form choices are owner-scoped. They are removed when a selection |
| 78 | * spans owners even if a future permission allows those rows to be selected |
| 79 | * together. All other returned fields are domain-safe for every loaded row. |
| 80 | * |
| 81 | * @param mixed $service_ids Selected Service identifiers. |
| 82 | * |
| 83 | * @return array<string,mixed>|WP_Error Browser-safe bulk contract or error. |
| 84 | */ |
| 85 | public function get_bulk_contract( $service_ids ) { |
| 86 | $services = $this->load_services( $service_ids ); |
| 87 | if ( is_wp_error( $services ) ) { |
| 88 | return $services; |
| 89 | } |
| 90 | |
| 91 | return array( |
| 92 | 'fields' => $this->get_bulk_fields_for_services( $services ), |
| 93 | 'message' => __( 'Only fields that are safe for every selected Service are available.', 'booking' ), |
| 94 | ); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Return browser-safe definitions for fields shared by selected Services. |
| 99 | * |
| 100 | * @return array<int,array<string,mixed>> Allow-listed bulk field definitions. |
| 101 | */ |
| 102 | public function get_bulk_fields() { |
| 103 | return $this->inline_fields->get_bulk_fields( array() ); |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * Build a non-mutating signed review for inline or bulk Service changes. |
| 108 | * |
| 109 | * @param string $mode Either inline or bulk. |
| 110 | * @param mixed $ids Selected Service identifiers. |
| 111 | * @param mixed $changes Inline changes keyed by ID, or shared bulk fields. |
| 112 | * |
| 113 | * @return array<string,mixed>|WP_Error Review rows and signed token, or error. |
| 114 | */ |
| 115 | public function preview( $mode, $ids, $changes ) { |
| 116 | $mode = in_array( $mode, array( 'inline', 'bulk' ), true ) ? $mode : ''; |
| 117 | $ids = $this->normalize_ids( $ids ); |
| 118 | if ( '' === $mode || is_wp_error( $ids ) ) { |
| 119 | return new WP_Error( 'wpbc_service_invalid_selection', __( 'Select between 1 and 100 Services.', 'booking' ) ); |
| 120 | } |
| 121 | if ( ! is_object( $this->repository ) || ! method_exists( $this->repository, 'find' ) || ! method_exists( $this->repository, 'save' ) ) { |
| 122 | return wpbc_appointment_services_storage_error(); |
| 123 | } |
| 124 | |
| 125 | $current_services = $this->load_services( $ids ); |
| 126 | if ( is_wp_error( $current_services ) ) { |
| 127 | return $current_services; |
| 128 | } |
| 129 | $services_by_id = array(); |
| 130 | foreach ( $current_services as $current_service ) { |
| 131 | $services_by_id[ absint( $current_service['service_id'] ) ] = $current_service; |
| 132 | } |
| 133 | $bulk_allowed = 'bulk' === $mode ? wp_list_pluck( $this->get_bulk_fields_for_services( $current_services ), 'key' ) : null; |
| 134 | |
| 135 | $changes = is_array( $changes ) ? $changes : array(); |
| 136 | $plan = array( |
| 137 | 'version' => 1, |
| 138 | 'mode' => $mode, |
| 139 | 'site_id' => get_current_blog_id(), |
| 140 | 'user_id' => get_current_user_id(), |
| 141 | 'expires_at' => time() + self::REVIEW_LIFETIME, |
| 142 | 'services' => array(), |
| 143 | ); |
| 144 | $rows = array(); |
| 145 | foreach ( $ids as $service_id ) { |
| 146 | $current = isset( $services_by_id[ $service_id ] ) ? $services_by_id[ $service_id ] : null; |
| 147 | if ( ! is_array( $current ) ) { return new WP_Error( 'service_not_found', __( 'Service not found.', 'booking' ) ); } |
| 148 | $requested = 'bulk' === $mode ? $changes : ( isset( $changes[ $service_id ] ) && is_array( $changes[ $service_id ] ) ? $changes[ $service_id ] : array() ); |
| 149 | $validated = $this->validate_changes( $requested, 'inline' === $mode, $current, $bulk_allowed ); |
| 150 | if ( is_wp_error( $validated ) ) { return $validated; } |
| 151 | $row_changes = $this->build_row_changes( $current, $validated ); |
| 152 | if ( empty( $row_changes ) ) { continue; } |
| 153 | $plan['services'][] = array( |
| 154 | 'id' => $service_id, |
| 155 | 'changes' => $validated, |
| 156 | 'snapshot' => $this->snapshot_hash( $current ), |
| 157 | ); |
| 158 | $rows[] = array( 'id' => $service_id, 'title' => sanitize_text_field( $current['title'] ), 'changes' => $row_changes ); |
| 159 | } |
| 160 | if ( empty( $rows ) ) { |
| 161 | return new WP_Error( 'wpbc_service_no_changes', __( 'No Service changes require review.', 'booking' ) ); |
| 162 | } |
| 163 | |
| 164 | $review_rows = array(); |
| 165 | foreach ( $rows as $row ) { |
| 166 | $review_rows[] = array( |
| 167 | 'id' => absint( $row['id'] ), |
| 168 | 'title' => (string) $row['title'], |
| 169 | 'fields' => array_values( $row['changes'] ), |
| 170 | 'notes' => array(), |
| 171 | ); |
| 172 | } |
| 173 | |
| 174 | return array( 'rows' => $rows, 'review' => array( 'rows' => $review_rows ), 'token' => $this->sign_plan( $plan ), 'plan' => $plan ); |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | * Apply a previously reviewed plan after current-state revalidation. |
| 179 | * |
| 180 | * Completed saves are compensated in reverse order if a later save fails. |
| 181 | * |
| 182 | * @param mixed $plan Review plan returned by preview(). |
| 183 | * @param string $token Signed review token. |
| 184 | * |
| 185 | * @return array<string,mixed>|WP_Error Changed IDs or mutation error. |
| 186 | */ |
| 187 | public function apply( $plan, $token ) { |
| 188 | $plan = is_array( $plan ) ? $plan : array(); |
| 189 | $token = is_scalar( $token ) ? (string) $token : ''; |
| 190 | $mode = isset( $plan['mode'] ) && in_array( $plan['mode'], array( 'inline', 'bulk' ), true ) ? $plan['mode'] : ''; |
| 191 | if ( ! $this->is_valid_plan_envelope( $plan, $token, $mode ) ) { |
| 192 | return new WP_Error( 'wpbc_service_invalid_review', __( 'This Service review is invalid or has expired.', 'booking' ) ); |
| 193 | } |
| 194 | if ( ! is_object( $this->repository ) || ! method_exists( $this->repository, 'find' ) || ! method_exists( $this->repository, 'save' ) ) { |
| 195 | return wpbc_appointment_services_storage_error(); |
| 196 | } |
| 197 | |
| 198 | $before = array(); |
| 199 | $validated_plans = array(); |
| 200 | foreach ( $plan['services'] as $service_plan ) { |
| 201 | $service_id = isset( $service_plan['id'] ) ? absint( $service_plan['id'] ) : 0; |
| 202 | $current = $service_id ? $this->repository->find( $service_id ) : new WP_Error( 'invalid_service', __( 'The Service selection is invalid.', 'booking' ) ); |
| 203 | if ( is_wp_error( $current ) ) { return $current; } |
| 204 | if ( empty( $service_plan['snapshot'] ) || ! hash_equals( (string) $service_plan['snapshot'], $this->snapshot_hash( $current ) ) ) { |
| 205 | return new WP_Error( 'wpbc_service_stale_review', __( 'A selected Service changed after review. Review the changes again.', 'booking' ) ); |
| 206 | } |
| 207 | $before[ $service_id ] = $current; |
| 208 | } |
| 209 | $bulk_allowed = 'bulk' === $mode ? wp_list_pluck( $this->get_bulk_fields_for_services( array_values( $before ) ), 'key' ) : null; |
| 210 | foreach ( $plan['services'] as $service_plan ) { |
| 211 | $service_id = absint( $service_plan['id'] ); |
| 212 | $validated_changes = $this->validate_changes( |
| 213 | isset( $service_plan['changes'] ) && is_array( $service_plan['changes'] ) ? $service_plan['changes'] : array(), |
| 214 | 'inline' === $mode, |
| 215 | $before[ $service_id ], |
| 216 | $bulk_allowed |
| 217 | ); |
| 218 | if ( is_wp_error( $validated_changes ) || empty( $validated_changes ) ) { |
| 219 | return is_wp_error( $validated_changes ) ? $validated_changes : new WP_Error( 'wpbc_service_no_changes', __( 'No valid Service changes remain to apply.', 'booking' ) ); |
| 220 | } |
| 221 | $validated_plans[ $service_id ] = $validated_changes; |
| 222 | } |
| 223 | |
| 224 | $changed_ids = array(); |
| 225 | foreach ( $plan['services'] as $service_plan ) { |
| 226 | $service_id = absint( $service_plan['id'] ); |
| 227 | $payload = array_merge( $before[ $service_id ], $validated_plans[ $service_id ], array( 'service_id' => $service_id ) ); |
| 228 | $result = $this->repository->save( $payload ); |
| 229 | if ( is_wp_error( $result ) ) { |
| 230 | // A repository may fail after updating its Service row but before its Provider assignments. |
| 231 | $this->repository->save( $before[ $service_id ] ); |
| 232 | foreach ( array_reverse( $changed_ids ) as $changed_id ) { |
| 233 | $this->repository->save( $before[ $changed_id ] ); |
| 234 | } |
| 235 | return new WP_Error( 'wpbc_service_apply_failed', __( 'The Service changes could not be completed. Previous values were restored where possible.', 'booking' ) ); |
| 236 | } |
| 237 | $changed_ids[] = $service_id; |
| 238 | } |
| 239 | |
| 240 | do_action( 'wpbc_appointment_services_catalog_updated', $changed_ids, $mode ); |
| 241 | |
| 242 | return array( 'changed_ids' => $changed_ids ); |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * Validate an allow-listed set of Service fields. |
| 247 | * |
| 248 | * @param array<string,mixed> $changes Requested changes. |
| 249 | * @param bool $inline_mode Whether the request is row-specific inline editing. |
| 250 | * @param array<string,mixed> $service Current authorized Service row. |
| 251 | * @param array<int,string>|null $allowed_override Selection-specific bulk allow-list. |
| 252 | * |
| 253 | * @return array<string,mixed>|WP_Error Valid changes or validation error. |
| 254 | */ |
| 255 | private function validate_changes( $changes, $inline_mode, $service, $allowed_override = null ) { |
| 256 | $field_definitions = $inline_mode ? $this->get_inline_fields( $service ) : $this->get_bulk_fields(); |
| 257 | $allowed = is_array( $allowed_override ) ? $allowed_override : wp_list_pluck( $field_definitions, 'key' ); |
| 258 | $changes = is_array( $changes ) ? $changes : array(); |
| 259 | if ( array_diff( array_keys( $changes ), $allowed ) ) { |
| 260 | return new WP_Error( 'wpbc_service_unsupported_field', __( 'One or more Service fields are no longer available for this operation.', 'booking' ) ); |
| 261 | } |
| 262 | $validated = array(); |
| 263 | foreach ( $changes as $field_id => $raw_value ) { |
| 264 | if ( ! is_scalar( $raw_value ) ) { |
| 265 | return new WP_Error( 'wpbc_service_invalid_field', __( 'A Service field contains an invalid value.', 'booking' ) ); |
| 266 | } |
| 267 | switch ( $field_id ) { |
| 268 | case 'title': |
| 269 | $validated[ $field_id ] = wp_html_excerpt( sanitize_text_field( $raw_value ), 200, '' ); |
| 270 | if ( '' === $validated[ $field_id ] ) { return new WP_Error( 'wpbc_service_title_required', __( 'Every Service must have a title.', 'booking' ) ); } |
| 271 | break; |
| 272 | case 'description': |
| 273 | $description = sanitize_textarea_field( $raw_value ); |
| 274 | $description_length = function_exists( 'mb_strlen' ) ? mb_strlen( $description ) : strlen( $description ); |
| 275 | if ( 2000 < $description_length ) { return new WP_Error( 'wpbc_service_description_too_long', __( 'The Service description is too long.', 'booking' ) ); } |
| 276 | $validated[ $field_id ] = $description; |
| 277 | break; |
| 278 | case 'duration_minutes': |
| 279 | if ( ! $this->is_integer_value( $raw_value ) || 1 > (int) $raw_value || 1440 < (int) $raw_value ) { return new WP_Error( 'wpbc_service_invalid_duration', __( 'Enter a Service duration between 1 and 1440 minutes.', 'booking' ) ); } |
| 280 | $validated[ $field_id ] = absint( $raw_value ); |
| 281 | break; |
| 282 | case 'buffer_before_minutes': |
| 283 | case 'buffer_after_minutes': |
| 284 | if ( ! $this->is_integer_value( $raw_value ) || 0 > (int) $raw_value || 1440 < (int) $raw_value ) { return new WP_Error( 'wpbc_service_invalid_buffer', __( 'Enter a Service buffer between 0 and 1440 minutes.', 'booking' ) ); } |
| 285 | $validated[ $field_id ] = absint( $raw_value ); |
| 286 | break; |
| 287 | case 'base_cost': |
| 288 | if ( ! wpbc_appointment_services_is_pricing_available() || ! is_numeric( $raw_value ) || 0 > (float) $raw_value || 1000 < (float) $raw_value ) { return new WP_Error( 'wpbc_service_invalid_price', __( 'Enter a Service price between 0 and 1000.', 'booking' ) ); } |
| 289 | $validated[ $field_id ] = number_format( (float) $raw_value, 2, '.', '' ); |
| 290 | break; |
| 291 | case 'booking_form_id': |
| 292 | $form_id = absint( $raw_value ); |
| 293 | if ( ! array_key_exists( $form_id, wpbc_appointment_services_get_form_options() ) ) { return new WP_Error( 'wpbc_service_invalid_form', __( 'The selected Booking Form is unavailable.', 'booking' ) ); } |
| 294 | $validated[ $field_id ] = $form_id; |
| 295 | break; |
| 296 | case 'status': |
| 297 | $status = sanitize_key( $raw_value ); |
| 298 | if ( ! in_array( $status, array( 'active', 'inactive', 'archived' ), true ) ) { return new WP_Error( 'wpbc_service_invalid_status', __( 'The selected Service status is invalid.', 'booking' ) ); } |
| 299 | $validated[ $field_id ] = $status; |
| 300 | break; |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | return $validated; |
| 305 | } |
| 306 | |
| 307 | /** |
| 308 | * Determine whether a submitted scalar represents a whole integer. |
| 309 | * |
| 310 | * Number controls can be manipulated outside the browser, so this prevents |
| 311 | * decimal values from being silently truncated during server validation. |
| 312 | * |
| 313 | * @param mixed $raw_value Submitted field value. |
| 314 | * @return bool True when the value contains only an optional minus sign and digits. |
| 315 | */ |
| 316 | private function is_integer_value( $raw_value ) { |
| 317 | return is_scalar( $raw_value ) && 1 === preg_match( '/^-?\d+$/', (string) $raw_value ); |
| 318 | } |
| 319 | |
| 320 | /** |
| 321 | * Build the current row-specific inline field definitions for one Service. |
| 322 | * |
| 323 | * @param array<string,mixed> $service Current authorized Service row. |
| 324 | * |
| 325 | * @return array<int,array<string,mixed>> Executable-free field definitions. |
| 326 | */ |
| 327 | private function get_inline_fields( $service ) { |
| 328 | return $this->inline_fields->get_inline_fields( $service ); |
| 329 | } |
| 330 | |
| 331 | /** |
| 332 | * Calculate fields shared safely by a loaded Service collection. |
| 333 | * |
| 334 | * @param array<int,array<string,mixed>> $services Authorized Service rows. |
| 335 | * |
| 336 | * @return array<int,array<string,mixed>> Selection-specific field definitions. |
| 337 | */ |
| 338 | private function get_bulk_fields_for_services( $services ) { |
| 339 | return $this->inline_fields->get_bulk_fields( $services ); |
| 340 | } |
| 341 | |
| 342 | /** |
| 343 | * Normalize one bounded identifier collection without accepting partial input. |
| 344 | * |
| 345 | * @param mixed $service_ids Requested Service identifiers. |
| 346 | * |
| 347 | * @return array<int,int>|WP_Error Unique positive identifiers or error. |
| 348 | */ |
| 349 | private function normalize_ids( $service_ids ) { |
| 350 | $raw_ids = is_array( $service_ids ) ? $service_ids : array(); |
| 351 | $ids = array_values( array_unique( array_filter( array_map( 'absint', $raw_ids ) ) ) ); |
| 352 | if ( empty( $ids ) || self::MAX_ROWS < count( $ids ) || count( $ids ) !== count( $raw_ids ) ) { |
| 353 | return new WP_Error( 'wpbc_service_invalid_selection', __( 'Select between 1 and 100 Services.', 'booking' ) ); |
| 354 | } |
| 355 | |
| 356 | return $ids; |
| 357 | } |
| 358 | |
| 359 | /** |
| 360 | * Load an authorized bounded Service collection in request order. |
| 361 | * |
| 362 | * @param mixed $service_ids Requested Service identifiers. |
| 363 | * |
| 364 | * @return array<int,array<string,mixed>>|WP_Error Service rows or safe error. |
| 365 | */ |
| 366 | private function load_services( $service_ids ) { |
| 367 | $service_ids = $this->normalize_ids( $service_ids ); |
| 368 | if ( is_wp_error( $service_ids ) ) { |
| 369 | return $service_ids; |
| 370 | } |
| 371 | if ( ! is_object( $this->repository ) || ! method_exists( $this->repository, 'find' ) ) { |
| 372 | return wpbc_appointment_services_storage_error(); |
| 373 | } |
| 374 | |
| 375 | $services = array(); |
| 376 | foreach ( $service_ids as $service_id ) { |
| 377 | $service = $this->repository->find( $service_id ); |
| 378 | if ( is_wp_error( $service ) ) { |
| 379 | return $service; |
| 380 | } |
| 381 | $services[] = $service; |
| 382 | } |
| 383 | |
| 384 | return $services; |
| 385 | } |
| 386 | |
| 387 | /** |
| 388 | * Build human-readable field differences for one review card. |
| 389 | * |
| 390 | * @param array<string,mixed> $current Current Service row. |
| 391 | * @param array<string,mixed> $changes Validated changes. |
| 392 | * |
| 393 | * @return array<int,array<string,string>> Changed field records. |
| 394 | */ |
| 395 | private function build_row_changes( $current, $changes ) { |
| 396 | $labels = array(); |
| 397 | foreach ( array_merge( $this->inline_fields->get_inline_fields( $current ), $this->inline_fields->get_bulk_fields( array( $current ) ) ) as $field ) { |
| 398 | $labels[ $field['key'] ] = $field['label']; |
| 399 | } |
| 400 | $rows = array(); |
| 401 | foreach ( $changes as $field_id => $after ) { |
| 402 | $before = isset( $current[ $field_id ] ) ? $current[ $field_id ] : ''; |
| 403 | if ( (string) $before === (string) $after || ( 'base_cost' === $field_id && (float) $before === (float) $after ) ) { continue; } |
| 404 | $rows[] = array( 'key' => sanitize_key( $field_id ), 'label' => isset( $labels[ $field_id ] ) ? $labels[ $field_id ] : $field_id, 'before' => (string) $before, 'after' => (string) $after ); |
| 405 | } |
| 406 | |
| 407 | return $rows; |
| 408 | } |
| 409 | |
| 410 | /** |
| 411 | * Hash values that must remain current between review and apply. |
| 412 | * |
| 413 | * @param array<string,mixed> $service Current repository row. |
| 414 | * |
| 415 | * @return string Current-state hash. |
| 416 | */ |
| 417 | private function snapshot_hash( $service ) { |
| 418 | return hash( 'sha256', wp_json_encode( $service ) ); |
| 419 | } |
| 420 | |
| 421 | /** |
| 422 | * Sign a review plan with the current WordPress nonce salt. |
| 423 | * |
| 424 | * @param array<string,mixed> $plan Review plan. |
| 425 | * |
| 426 | * @return string Signed plan token. |
| 427 | */ |
| 428 | private function sign_plan( $plan ) { |
| 429 | return hash_hmac( 'sha256', wp_json_encode( $plan ), wp_salt( 'nonce' ) ); |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * Validate one signed review envelope against the current user and site. |
| 434 | * |
| 435 | * @param array $plan Submitted review plan. |
| 436 | * @param string $token Submitted review signature. |
| 437 | * @param string $mode Normalized editing mode. |
| 438 | * @return bool True when the review is authentic, current, and bounded. |
| 439 | */ |
| 440 | private function is_valid_plan_envelope( $plan, $token, $mode ) { |
| 441 | return '' !== $mode |
| 442 | && '' !== $token |
| 443 | && isset( $plan['version'], $plan['mode'], $plan['site_id'], $plan['user_id'], $plan['expires_at'], $plan['services'] ) |
| 444 | && 1 === absint( $plan['version'] ) |
| 445 | && $mode === $plan['mode'] |
| 446 | && get_current_blog_id() === absint( $plan['site_id'] ) |
| 447 | && get_current_user_id() === absint( $plan['user_id'] ) |
| 448 | && time() <= absint( $plan['expires_at'] ) |
| 449 | && is_array( $plan['services'] ) |
| 450 | && ! empty( $plan['services'] ) |
| 451 | && self::MAX_ROWS >= count( $plan['services'] ) |
| 452 | && hash_equals( $this->sign_plan( $plan ), $token ); |
| 453 | } |
| 454 | } |
| 455 |