$status ) ); } /** Connection ID is part of the capability: a new pairing never receives an old POST. */ public function context(): array|WP_Error { $store = new TokenStore(); $state = $store->read(); if ( $store->disabled() || ! in_array( $state['status'], array( 'connected', 'offline' ), true ) || ! empty( $state['verify_account_slug'] ) || empty( $state['access_token'] ) || time() >= ( $state['expires_at'] ?? 0 ) || empty( $state['connection_id'] ) || empty( $state['account_id'] ) || ( $state['account']['id'] ?? 0 ) !== $state['account_id'] ) { return self::error(); } return $state; } public function request( array $binding, string $endpoint, array $query = array(), ?array $body = null ): array|WP_Error { $create = null !== $body; if ( ! ( $create ? 'bookings' === $endpoint : preg_match( '~\Aservices/[1-9][0-9]*(?:/(?:slots|occurrences))?\z~', $endpoint ) ) || array_diff_key( $query, array_flip( array( 'locale', 'date' ) ) ) ) { return self::error( 'booking_parameters', 400 ); } $mutex = new Mutex(); return $mutex->run( function () use ( $binding, $endpoint, $query, $body, $create, $mutex ) { $state = $this->context(); if ( is_wp_error( $state ) ) { return $state; } if ( ( $binding['account'] ?? 0 ) !== $state['account_id'] || ( $binding['connection'] ?? 0 ) !== $state['connection_id'] ) { return self::error( 'booking_binding', 409 ); } $scope = $create ? 'public_bookings:create' : ( str_ends_with( $endpoint, '/slots' ) || str_ends_with( $endpoint, '/occurrences' ) ? 'availability:read' : 'services:read' ); if ( ! in_array( $scope, explode( ' ', $state['scope'] ?? '' ), true ) ) { return self::error( 'booking_scope', 403 ); } $origin = ( new Client() )->origin(); if ( is_wp_error( $origin ) ) { return $origin; } $response = wp_remote_request( add_query_arg( $query, $origin . Client::PREFIX . $endpoint ), array( 'method' => $create ? 'POST' : 'GET', 'headers' => array( 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $state['access_token'], ), 'body' => $create ? wp_json_encode( $body ) : null, 'timeout' => 12, 'redirection' => 0, 'sslverify' => true, 'limit_response_size' => 1048576, 'cookies' => array(), ) ); $after = $this->context(); if ( ! $mutex->owned() || is_wp_error( $after ) || $state['connection_id'] !== $after['connection_id'] || $state['account_id'] !== $after['account_id'] ) { return self::error( 'booking_uncertain', 503 ); } if ( is_wp_error( $response ) ) { return self::error( 'booking_uncertain', 504 ); } $status = (int) wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 !== $status ) { $error = self::error( 'booking_api', in_array( $status, array( 401, 403, 404, 410, 422, 429 ), true ) ? $status : 502 ); if ( 422 === $status && is_array( $data['error']['details'] ?? null ) ) { $error->add_data( array( 'status' => 422, 'fields' => $this->fields( $data['error']['details'] ), ) ); } return $error; } return is_array( $data ) ? $data : self::error( 'booking_contract', 502 ); }, 2 ); } private function fields( array $errors ): array { $result = array(); foreach ( array( 'customer_name', 'customer_email', 'customer_phone', 'gdpr', 'listamester_consent', 'start_utc', 'resource_id', 'occurrence_id' ) as $key ) { if ( isset( $errors[ $key ] ) && is_array( $errors[ $key ] ) ) { $result[ $key ] = array_values( array_filter( $errors[ $key ], 'is_string' ) ); } } return $result; } }