PluginProbe
Booking Calendar / 11.8.4
Booking Calendar v11.8.4
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 10.11 All 204 releases
booking / includes / _shared-ui-catalog / class-wpbc-ui-catalog-response.php

class-wpbc-ui-catalog-response.php in Booking Calendar 11.8.4, at includes/_shared-ui-catalog/class-wpbc-ui-catalog-response.php

330 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shared response normalization for template-driven catalogs.
4 *
5 * @package Booking Calendar
6 * @since 11.6.0
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Produce stable JSON-safe success, empty, and error response contracts.
15 */
16 final class WPBC_UI_Catalog_Response {
17
18 /**
19 * Supported browser response schema version.
20 *
21 * @var int
22 */
23 const SCHEMA_VERSION = 1;
24
25 /**
26 * Normalized response payload.
27 *
28 * @var array
29 */
30 private $response = array();
31
32 /**
33 * Prevent direct construction; callers must use create() or create_empty().
34 *
35 * @param array $response Normalized response payload.
36 */
37 private function __construct( $response ) {
38 $this->response = $response;
39 }
40
41 /**
42 * Create a normalized successful response.
43 *
44 * Domain providers may supply JSON-safe item arrays and optional response
45 * sections, but cannot replace shared request, pagination, sorting, display,
46 * schema, catalog, or request identifiers.
47 *
48 * @param string $catalog_id Registered catalog identifier.
49 * @param WPBC_UI_Catalog_Request $request Validated shared request.
50 * @param mixed $items Normalized provider item arrays.
51 * @param mixed $response_values Optional normalized sections.
52 *
53 * @return WPBC_UI_Catalog_Response|WP_Error Normalized response or safe error.
54 */
55 public static function create( $catalog_id, $request, $items, $response_values = array() ) {
56 $catalog_id = is_scalar( $catalog_id ) ? sanitize_key( (string) $catalog_id ) : '';
57 if ( '' === $catalog_id || ! $request instanceof WPBC_UI_Catalog_Request || $catalog_id !== $request->get_catalog_id() ) {
58 return self::get_error( 'invalid_response_context', __( 'The catalog response context is invalid.', 'booking' ) );
59 }
60
61 if ( ! is_array( $items ) || ! is_array( $response_values ) ) {
62 return self::get_error( 'malformed_response', __( 'The catalog response is malformed.', 'booking' ) );
63 }
64
65 $normalized_items = array();
66 foreach ( $items as $item ) {
67 if ( ! is_array( $item ) ) {
68 return self::get_error( 'malformed_item', __( 'A catalog response item is malformed.', 'booking' ) );
69 }
70
71 $normalized_item = self::normalize_json_value( $item );
72 if ( is_wp_error( $normalized_item ) ) {
73 return $normalized_item;
74 }
75 $normalized_items[] = $normalized_item;
76 }
77
78 $request_values = $request->to_array();
79 $page_number = $request_values['page_number'];
80 $items_per_page = $request_values['items_per_page'];
81 $pagination_values = isset( $response_values['pagination'] ) && is_array( $response_values['pagination'] )
82 ? $response_values['pagination']
83 : array();
84 foreach ( array( 'total_items', 'page_item_count' ) as $pagination_key ) {
85 if (
86 isset( $pagination_values[ $pagination_key ] )
87 && (
88 ! is_scalar( $pagination_values[ $pagination_key ] )
89 || ! preg_match( '/^-?\d+$/', (string) $pagination_values[ $pagination_key ] )
90 )
91 ) {
92 return self::get_error( 'malformed_pagination', __( 'The catalog pagination response is malformed.', 'booking' ) );
93 }
94 }
95 $total_items = isset( $pagination_values['total_items'] )
96 ? max( 0, (int) $pagination_values['total_items'] )
97 : count( $normalized_items );
98 $page_item_count = isset( $pagination_values['page_item_count'] )
99 ? max( 0, (int) $pagination_values['page_item_count'] )
100 : count( $normalized_items );
101 $total_pages = 0 === $total_items ? 0 : (int) ceil( $total_items / $items_per_page );
102 $items_from = 0 === $page_item_count ? 0 : ( ( $page_number - 1 ) * $items_per_page ) + 1;
103 $items_to = 0 === $page_item_count ? 0 : min( $total_items, $items_from + $page_item_count - 1 );
104 $filters = array( 'search' => $request_values['search'] );
105 if (
106 $page_item_count > $items_per_page
107 || $page_item_count > $total_items
108 || ( ! empty( $normalized_items ) && 0 === $page_item_count )
109 || ( empty( $normalized_items ) && 0 < $page_item_count )
110 || ( 0 < $page_item_count && $items_from > $total_items )
111 ) {
112 return self::get_error( 'inconsistent_pagination', __( 'The catalog pagination response is inconsistent.', 'booking' ) );
113 }
114
115 if ( isset( $response_values['filters'] ) ) {
116 $normalized_filters = self::normalize_json_value( $response_values['filters'] );
117 if ( is_wp_error( $normalized_filters ) || ! is_array( $normalized_filters ) ) {
118 return self::get_error( 'malformed_filters', __( 'The catalog filter response is malformed.', 'booking' ) );
119 }
120 $filters = array_merge( $filters, $normalized_filters );
121 }
122
123 $configuration = $request->get_configuration();
124 $features = isset( $configuration['features'] ) && is_array( $configuration['features'] ) ? $configuration['features'] : array();
125 $hierarchy_values = isset( $response_values['hierarchy'] ) ? self::normalize_json_value( $response_values['hierarchy'] ) : array();
126 if ( is_wp_error( $hierarchy_values ) ) {
127 return self::get_error( 'malformed_hierarchy', __( 'The catalog hierarchy response is malformed.', 'booking' ) );
128 }
129 $hierarchy = WPBC_UI_Catalog_Hierarchy::normalize_response( $hierarchy_values, ! empty( $features['hierarchy'] ) );
130 if ( is_wp_error( $hierarchy ) ) {
131 return $hierarchy;
132 }
133
134 $normalized_items = WPBC_UI_Catalog_Hierarchy::normalize_items( $normalized_items, $hierarchy['enabled'] );
135 if ( is_wp_error( $normalized_items ) ) {
136 return $normalized_items;
137 }
138
139 $capabilities = array();
140 if ( isset( $response_values['capabilities'] ) ) {
141 if ( ! is_array( $response_values['capabilities'] ) ) {
142 return self::get_error( 'malformed_capabilities', __( 'The catalog capability response is malformed.', 'booking' ) );
143 }
144 foreach ( $response_values['capabilities'] as $capability_key => $is_allowed ) {
145 $capability_key = is_scalar( $capability_key ) ? sanitize_key( (string) $capability_key ) : '';
146 if ( '' === $capability_key || ! is_bool( $is_allowed ) ) {
147 return self::get_error( 'malformed_capabilities', __( 'The catalog capability response is malformed.', 'booking' ) );
148 }
149 $capabilities[ $capability_key ] = $is_allowed;
150 }
151 }
152
153 $messages = array();
154 if ( isset( $response_values['messages'] ) ) {
155 if ( ! is_array( $response_values['messages'] ) ) {
156 return self::get_error( 'malformed_messages', __( 'The catalog message response is malformed.', 'booking' ) );
157 }
158 foreach ( $response_values['messages'] as $message ) {
159 if ( ! is_scalar( $message ) ) {
160 return self::get_error( 'malformed_messages', __( 'The catalog message response is malformed.', 'booking' ) );
161 }
162 $messages[] = sanitize_text_field( (string) $message );
163 }
164 }
165
166 return new self(
167 array(
168 'schema_version' => self::SCHEMA_VERSION,
169 'success' => true,
170 'catalog_id' => $catalog_id,
171 'request_id' => $request_values['request_id'],
172 'items' => $normalized_items,
173 'pagination' => array(
174 'page_number' => $page_number,
175 'items_per_page' => $items_per_page,
176 'total_items' => $total_items,
177 'page_item_count' => $page_item_count,
178 'total_pages' => $total_pages,
179 'items_from' => $items_from,
180 'items_to' => $items_to,
181 ),
182 'sorting' => array(
183 'sort_by' => $request_values['sort_by'],
184 'sort_order' => $request_values['sort_order'],
185 ),
186 'filters' => $filters,
187 'display' => array(
188 'visible_columns' => $request_values['visible_columns'],
189 'column_order' => $request_values['column_order'],
190 'template_pack' => $request_values['template_pack'],
191 ),
192 'hierarchy' => $hierarchy,
193 'capabilities' => $capabilities,
194 'messages' => $messages,
195 'error' => null,
196 )
197 );
198 }
199
200 /**
201 * Create the canonical empty response contract.
202 *
203 * @param string $catalog_id Registered catalog identifier.
204 * @param WPBC_UI_Catalog_Request $request Validated shared request.
205 * @param mixed $response_values Optional normalized sections.
206 *
207 * @return WPBC_UI_Catalog_Response|WP_Error Empty response or safe error.
208 */
209 public static function create_empty( $catalog_id, $request, $response_values = array() ) {
210 if ( ! is_array( $response_values ) ) {
211 return self::get_error( 'malformed_response', __( 'The catalog response is malformed.', 'booking' ) );
212 }
213
214 $response_values['pagination'] = array( 'total_items' => 0 );
215
216 return self::create( $catalog_id, $request, array(), $response_values );
217 }
218
219 /**
220 * Create a stable non-sensitive browser error contract.
221 *
222 * @param string $catalog_id Catalog identifier.
223 * @param mixed $request_id Client request sequence.
224 * @param string $error_code Stable error code.
225 * @param string $error_message Localized safe message.
226 * @param bool $retryable Whether the browser may offer a retry.
227 *
228 * @return array<string,mixed> Normalized error payload.
229 */
230 public static function get_error_response( $catalog_id, $request_id, $error_code, $error_message, $retryable = false ) {
231 $catalog_id = is_scalar( $catalog_id ) ? sanitize_key( (string) $catalog_id ) : '';
232 $request_id = is_scalar( $request_id ) && preg_match( '/^\d+$/', (string) $request_id ) ? (int) $request_id : 0;
233 $error_code = is_scalar( $error_code ) ? sanitize_key( (string) $error_code ) : 'catalog_error';
234
235 return array(
236 'schema_version' => self::SCHEMA_VERSION,
237 'success' => false,
238 'catalog_id' => $catalog_id,
239 'request_id' => $request_id,
240 'items' => array(),
241 'error' => array(
242 'code' => '' === $error_code ? 'catalog_error' : $error_code,
243 'message' => is_scalar( $error_message ) ? sanitize_text_field( (string) $error_message ) : '',
244 'retryable' => (bool) $retryable,
245 ),
246 );
247 }
248
249 /**
250 * Convert a WordPress error into the shared browser error contract.
251 *
252 * @param string $catalog_id Catalog identifier.
253 * @param mixed $request_id Client request sequence.
254 * @param WP_Error $error Safe WordPress error.
255 * @param bool $retryable Whether the browser may offer a retry.
256 *
257 * @return array<string,mixed> Normalized error payload.
258 */
259 public static function from_wp_error( $catalog_id, $request_id, $error, $retryable = false ) {
260 if ( ! is_wp_error( $error ) ) {
261 $error = self::get_error( 'catalog_error', __( 'The catalog could not be loaded.', 'booking' ) );
262 }
263
264 return self::get_error_response( $catalog_id, $request_id, $error->get_error_code(), $error->get_error_message(), $retryable );
265 }
266
267 /**
268 * Determine whether the normalized response contains no items.
269 *
270 * @return bool True for an empty successful response.
271 */
272 public function is_empty() {
273 return empty( $this->response['items'] );
274 }
275
276 /**
277 * Export the normalized response payload.
278 *
279 * @return array<string,mixed> JSON-safe response payload.
280 */
281 public function to_array() {
282 return $this->response;
283 }
284
285 /**
286 * Recursively normalize JSON-safe scalar and array values.
287 *
288 * @param mixed $response_value Provider response value.
289 *
290 * @return mixed|WP_Error JSON-safe value or safe error.
291 */
292 private static function normalize_json_value( $response_value ) {
293 if ( is_null( $response_value ) || is_string( $response_value ) || is_int( $response_value ) || is_float( $response_value ) || is_bool( $response_value ) ) {
294 return $response_value;
295 }
296
297 if ( ! is_array( $response_value ) ) {
298 return self::get_error( 'unsafe_response_value', __( 'The catalog response contains an unsupported value.', 'booking' ) );
299 }
300
301 $normalized_value = array();
302 foreach ( $response_value as $response_key => $nested_value ) {
303 $normalized_key = is_int( $response_key ) ? $response_key : sanitize_key( (string) $response_key );
304 if ( '' === $normalized_key ) {
305 return self::get_error( 'unsafe_response_key', __( 'The catalog response contains an unsupported key.', 'booking' ) );
306 }
307
308 $normalized_nested_value = self::normalize_json_value( $nested_value );
309 if ( is_wp_error( $normalized_nested_value ) ) {
310 return $normalized_nested_value;
311 }
312 $normalized_value[ $normalized_key ] = $normalized_nested_value;
313 }
314
315 return $normalized_value;
316 }
317
318 /**
319 * Create a namespaced, non-sensitive response error.
320 *
321 * @param string $error_code Short error code.
322 * @param string $error_message User-facing error message.
323 *
324 * @return WP_Error Response error.
325 */
326 private static function get_error( $error_code, $error_message ) {
327 return new WP_Error( 'wpbc_ui_catalog_' . sanitize_key( $error_code ), $error_message );
328 }
329 }
330