PluginProbe
Booking Calendar / 11.8
Booking Calendar v11.8
11.9 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 All 205 releases
booking / includes / booking_modes_v3 / class-wpbc-booking-modes-v3-compatibility-registry.php

class-wpbc-booking-modes-v3-compatibility-registry.php in Booking Calendar 11.8, at includes/booking_modes_v3/class-wpbc-booking-modes-v3-compatibility-registry.php

572 lines 20.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Released-shape compatibility registry for Booking Modes V3.
4 *
5 * @package Booking Calendar
6 * @since 11.8.0
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Expose V1-shaped summaries to existing paid and extension callbacks once.
15 */
16 final class WPBC_Booking_Modes_V3_Compatibility_Registry {
17
18 /** @var int Maximum normalized modes accepted from the released filter. */
19 const MAX_MODES = 64;
20
21 /** @var WPBC_Booking_Modes_V3_Compatibility_Registry|null */
22 private static $instance = null;
23
24 /** @var array<string,array<string,mixed>>|null */
25 private $modes = null;
26
27 /** @var array<string,array<string,mixed>> */
28 private $default_modes = array();
29
30 /** @var array<int,string>|null */
31 private $allowed_mode_ids = null;
32
33 /** @var array<string,array<string,mixed>> */
34 private $page_placement_changes = array();
35
36 /** @var array<string,array<string,mixed>> */
37 private $compiler_definitions = array();
38
39 /** @var int */
40 private $build_count = 0;
41
42 /**
43 * Prevent direct construction.
44 */
45 private function __construct() {}
46
47 /**
48 * Return the shared request-local compatibility registry.
49 *
50 * @return WPBC_Booking_Modes_V3_Compatibility_Registry Registry instance.
51 */
52 public static function get_instance() {
53 if ( null === self::$instance ) {
54 self::$instance = new self();
55 }
56
57 return self::$instance;
58 }
59
60 /**
61 * Return all modes in the released V1 public shape.
62 *
63 * The focused V3 compiler never consumes the filtered result, preventing old
64 * presentation arrays from becoming authorization or declaration input.
65 *
66 * @return array<string,array<string,mixed>> Compatibility mode summaries.
67 */
68 public function get_all() {
69 if ( null !== $this->modes ) {
70 return $this->modes;
71 }
72
73 $modes = array();
74 foreach ( WPBC_Booking_Modes_V3_Definition_Registry::get_instance()->get_definitions() as $mode_id => $definition ) {
75 $modes[ $mode_id ] = $this->build_legacy_mode( $definition );
76 }
77 if ( ! WPBC_Booking_Modes_V3_Definition_Translator::is_translation_ready() ) {
78 return $this->normalize_modes( $modes, $modes );
79 }
80
81 ++$this->build_count;
82 $this->default_modes = $modes;
83
84 /**
85 * Filter registered Booking Calendar modes using the released V1 shape.
86 *
87 * @param array $modes Mode summaries keyed by stable ID.
88 */
89 $filtered_modes = apply_filters( 'wpbc_booking_modes_registered_modes', $modes );
90 $this->modes = $this->normalize_modes( $filtered_modes, $modes );
91
92 return $this->modes;
93 }
94
95 /**
96 * Return one released-shape mode summary.
97 *
98 * @param string $mode_id Stable mode ID.
99 *
100 * @return array<string,mixed>|null Mode summary or null.
101 */
102 public function get( $mode_id ) {
103 $mode_id = is_scalar( $mode_id ) ? sanitize_key( (string) $mode_id ) : '';
104 $modes = $this->get_all();
105
106 return isset( $modes[ $mode_id ] ) ? $modes[ $mode_id ] : null;
107 }
108
109 /**
110 * Return mode IDs allowed for this owner, restricted to validated V3 modes.
111 *
112 * @return array<int,string> Allowed mode identifiers.
113 */
114 public function get_allowed_mode_ids() {
115 if ( null !== $this->allowed_mode_ids ) {
116 return $this->allowed_mode_ids;
117 }
118
119 $modes = $this->get_all();
120 $allowed_mode_ids = array_keys( $modes );
121 if ( ! WPBC_Booking_Modes_V3_Definition_Translator::is_translation_ready() ) {
122 return in_array( 'classic', $allowed_mode_ids, true ) ? $allowed_mode_ids : array_merge( array( 'classic' ), $allowed_mode_ids );
123 }
124 $context = WPBC_Booking_Modes_V3_Context::get_instance()->get_context();
125
126 /**
127 * Filter allowed mode IDs using the released hook arguments.
128 *
129 * @param array $allowed_mode_ids Validated bundled mode IDs.
130 * @param array $modes Released-shape mode summaries.
131 * @param array $context Cached request context.
132 */
133 $allowed_mode_ids = apply_filters( 'wpbc_booking_modes_allowed_mode_ids', $allowed_mode_ids, $modes, $context );
134 $allowed_mode_ids = is_array( $allowed_mode_ids ) ? $allowed_mode_ids : array();
135 $normalized = array();
136 foreach ( array_slice( $allowed_mode_ids, 0, WPBC_Booking_Modes_V3_Definition_Validator::MAX_COLLECTION_ENTRIES ) as $mode_id ) {
137 $mode_id = is_scalar( $mode_id ) ? sanitize_key( (string) $mode_id ) : '';
138 if ( isset( $modes[ $mode_id ] ) && ! in_array( $mode_id, $normalized, true ) ) {
139 $normalized[] = $mode_id;
140 }
141 }
142 if ( ! in_array( 'classic', $normalized, true ) ) {
143 array_unshift( $normalized, 'classic' );
144 }
145
146 $this->allowed_mode_ids = $normalized;
147 return $normalized;
148 }
149
150 /**
151 * Return a validated bundled definition or a bounded custom-mode adapter.
152 *
153 * Custom modes remain a released public contract. Their legacy placement and
154 * native-menu records are processed separately by the compiler; this compact
155 * declaration supplies only identity, default route, and canonical routes.
156 *
157 * @param string $mode_id Selected registered mode ID.
158 *
159 * @return array<string,mixed>|null Compiler definition or null when unknown.
160 */
161 public function get_compiler_definition( $mode_id ) {
162 $mode_id = is_scalar( $mode_id ) ? sanitize_key( (string) $mode_id ) : '';
163 $bundled = WPBC_Booking_Modes_V3_Definition_Registry::get_instance()->get_definition( $mode_id );
164 if ( is_array( $bundled ) ) {
165 return $bundled;
166 }
167 if ( isset( $this->compiler_definitions[ $mode_id ] ) ) {
168 return $this->compiler_definitions[ $mode_id ];
169 }
170
171 $mode = $this->get( $mode_id );
172 if ( ! is_array( $mode ) ) {
173 return null;
174 }
175
176 $default_page = WPBC_Booking_Modes_V3_Page_Registry::get_instance()->get( $mode['default_page'] );
177 $default_route = is_array( $default_page )
178 ? array( 'page' => $default_page['page'], 'tab' => $default_page['tab'], 'subtab' => $default_page['subtab'] )
179 : array( 'page' => 'wpbc', 'tab' => 'vm_booking_listing', 'subtab' => '' );
180 $pages = array();
181 foreach ( $mode['pages'] as $canonical_page_id => $placement ) {
182 unset( $placement );
183 $canonical_page = WPBC_Booking_Modes_V3_Page_Registry::get_instance()->get( $canonical_page_id );
184 if ( ! is_array( $canonical_page ) ) {
185 continue;
186 }
187 $pages[ $canonical_page_id ] = array(
188 'route' => array(
189 'page' => $canonical_page['page'],
190 'tab' => $canonical_page['tab'],
191 'subtab' => $canonical_page['subtab'],
192 ),
193 );
194 }
195
196 $this->compiler_definitions[ $mode_id ] = array(
197 'id' => $mode_id,
198 'label' => $mode['label'],
199 'description' => $mode['description'],
200 'default_route' => $default_route,
201 'quickstart_id' => $mode['quickstart_id'],
202 'pages' => $pages,
203 'wordpress_menu' => array(),
204 'sidebar_menu' => array(),
205 );
206
207 return $this->compiler_definitions[ $mode_id ];
208 }
209
210 /**
211 * Return normalized native overrides after the released registry filter.
212 *
213 * @param string $mode_id Registered mode ID.
214 *
215 * @return array<string,array<string,mixed>> Native overrides.
216 */
217 public function get_native_menu( $mode_id ) {
218 $mode = $this->get( $mode_id );
219
220 return is_array( $mode ) ? $mode['native_menu'] : array();
221 }
222
223 /**
224 * Return normalized legacy root-group presentation for one mode.
225 *
226 * @param string $mode_id Registered mode ID.
227 *
228 * @return array<string,array<string,mixed>> Root group presentation.
229 */
230 public function get_groups( $mode_id ) {
231 $mode = $this->get( $mode_id );
232
233 return is_array( $mode ) ? $mode['groups'] : array();
234 }
235
236 /**
237 * Apply the released placement hook once and return changes from V3 defaults.
238 *
239 * A custom mode has no bundled baseline, so its complete bounded placement map
240 * is returned as the compatibility change set.
241 *
242 * @param string $mode_id Selected registered mode ID.
243 * @param array $available_page_ids Canonical source IDs available now.
244 *
245 * @return array<string,mixed> Changed placements and removed canonical IDs.
246 */
247 public function get_page_placement_changes( $mode_id, $available_page_ids ) {
248 $cache_key = $mode_id . '|' . implode( ',', $available_page_ids );
249 if ( isset( $this->page_placement_changes[ $cache_key ] ) ) {
250 return $this->page_placement_changes[ $cache_key ];
251 }
252
253 $mode = $this->get( $mode_id );
254 $placement = is_array( $mode ) ? $mode['pages'] : array();
255 $context = WPBC_Booking_Modes_V3_Context::get_instance()->get_context();
256 $placement = apply_filters( 'wpbc_booking_modes_mode_page_placement', $placement, $mode_id, $mode, $available_page_ids, $context );
257 $placement = $this->normalize_page_placements( $placement );
258 $baseline = isset( $this->default_modes[ $mode_id ] ) ? $this->default_modes[ $mode_id ]['pages'] : array();
259 $changes = array();
260 $removed = array();
261
262 foreach ( $placement as $page_id => $page_placement ) {
263 if ( ! isset( $baseline[ $page_id ] ) || $baseline[ $page_id ] !== $page_placement ) {
264 $changes[ $page_id ] = $page_placement;
265 }
266 }
267 foreach ( $baseline as $page_id => $page_placement ) {
268 unset( $page_placement );
269 if ( ! isset( $placement[ $page_id ] ) ) {
270 $removed[] = $page_id;
271 }
272 }
273 if ( ! isset( $this->default_modes[ $mode_id ] ) && is_array( $mode ) && empty( $mode['preserve_unmapped_pages'] ) ) {
274 foreach ( $available_page_ids as $page_id ) {
275 $page_id = is_scalar( $page_id ) ? sanitize_key( (string) $page_id ) : '';
276 if ( '' !== $page_id && ! isset( $placement[ $page_id ] ) && ! in_array( $page_id, $removed, true ) ) {
277 $removed[] = $page_id;
278 }
279 }
280 }
281
282 $this->page_placement_changes[ $cache_key ] = array(
283 'placements' => $changes,
284 'removed' => $removed,
285 'resolved' => $placement,
286 );
287
288 return $this->page_placement_changes[ $cache_key ];
289 }
290
291 /**
292 * Return the bounded placement map passed through the released page hook.
293 *
294 * @param string $mode_id Selected registered mode ID.
295 * @param array $available_page_ids Canonical source IDs available now.
296 *
297 * @return array<string,array<string,mixed>> Resolved canonical placements.
298 */
299 public function get_resolved_page_placements( $mode_id, $available_page_ids ) {
300 $change_set = $this->get_page_placement_changes( $mode_id, $available_page_ids );
301
302 return isset( $change_set['resolved'] ) && is_array( $change_set['resolved'] ) ? $change_set['resolved'] : array();
303 }
304
305 /**
306 * Return the number of public compatibility builds.
307 *
308 * @return int Build count.
309 */
310 public function get_build_count() {
311 return $this->build_count;
312 }
313
314 /**
315 * Translate one validated V3 definition to the released summary shape.
316 *
317 * @param array $definition Validated V3 definition.
318 *
319 * @return array<string,mixed> Released-shape summary.
320 */
321 private function build_legacy_mode( $definition ) {
322 $mode = array(
323 'id' => $definition['id'],
324 'label' => $definition['label'],
325 'description' => $definition['description'],
326 'default_page' => $this->find_canonical_page_id( $definition['default_route'] ),
327 'preserve_unmapped_pages' => true,
328 'groups' => array(),
329 'pages' => array(),
330 'native_menu' => array(),
331 'quickstart_id' => $definition['quickstart_id'],
332 );
333
334 $group_position = 0;
335 foreach ( $definition['sidebar_menu'] as $group_id => $group_definition ) {
336 $group_position += 10;
337 $mode['groups'][ $group_id ] = array(
338 'title' => isset( $group_definition['title'] ) ? $group_definition['title'] : $group_id,
339 'position' => $group_position,
340 'font_icon' => isset( $group_definition['font_icon'] ) ? $group_definition['font_icon'] : '',
341 'visible' => ! isset( $group_definition['visible'] ) || false !== $group_definition['visible'],
342 );
343 }
344
345 foreach ( $definition['sidebar_menu'] as $group_id => $group_definition ) {
346 if ( isset( $group_definition['page_id'] ) ) {
347 $this->add_legacy_sidebar_placement( $mode['pages'], $definition, $group_definition['page_id'], $group_id, $group_id, $group_definition, 10 );
348 }
349 if ( isset( $group_definition['items'] ) && is_array( $group_definition['items'] ) ) {
350 $this->add_legacy_sidebar_placements( $mode['pages'], $definition, $group_definition['items'], $group_id );
351 }
352 }
353
354 $native_position = 0;
355 foreach ( $definition['wordpress_menu'] as $menu_slug => $menu_definition ) {
356 $native_position += 10;
357 $native_menu = array(
358 'position' => $native_position,
359 'visible' => isset( $menu_definition['visible'] ) ? $menu_definition['visible'] : true,
360 );
361 if ( array_key_exists( 'title', $menu_definition ) ) {
362 $native_menu['title'] = $menu_definition['title'];
363 }
364 $mode['native_menu'][ $menu_slug ] = $native_menu;
365 }
366
367 return $mode;
368 }
369
370 /**
371 * Normalize filtered mode summaries and always preserve bundled Classic.
372 *
373 * @param mixed $filtered_modes Filter result.
374 * @param array $default_modes Bundled summaries.
375 *
376 * @return array<string,array<string,mixed>> Normalized summaries.
377 */
378 private function normalize_modes( $filtered_modes, $default_modes ) {
379 $normalized = array();
380 $filtered_modes = is_array( $filtered_modes ) ? $filtered_modes : array();
381 foreach ( array_slice( $filtered_modes, 0, WPBC_Booking_Modes_V3_Definition_Validator::MAX_COLLECTION_ENTRIES, true ) as $mode_id => $mode ) {
382 if ( count( $normalized ) >= self::MAX_MODES ) {
383 break;
384 }
385 $mode_id = is_scalar( $mode_id ) ? sanitize_key( (string) $mode_id ) : '';
386 if ( '' === $mode_id || ! is_array( $mode ) ) {
387 continue;
388 }
389 $mode = wp_parse_args(
390 $mode,
391 array( 'id' => $mode_id, 'label' => $mode_id, 'description' => '', 'default_page' => '', 'preserve_unmapped_pages' => false, 'groups' => array(), 'pages' => array(), 'native_menu' => array(), 'quickstart_id' => '' )
392 );
393 $mode['id'] = $mode_id;
394 $mode['label'] = is_scalar( $mode['label'] ) ? wp_strip_all_tags( (string) $mode['label'] ) : $mode_id;
395 $mode['description'] = is_scalar( $mode['description'] ) ? wp_strip_all_tags( (string) $mode['description'] ) : '';
396 $mode['default_page'] = is_scalar( $mode['default_page'] ) ? sanitize_key( (string) $mode['default_page'] ) : '';
397 $mode['preserve_unmapped_pages'] = (bool) $mode['preserve_unmapped_pages'];
398 $mode['groups'] = $this->normalize_groups( $mode['groups'] );
399 $mode['pages'] = $this->normalize_page_placements( $mode['pages'] );
400 $mode['native_menu'] = $this->normalize_native_menu( $mode['native_menu'] );
401 $mode['quickstart_id'] = is_scalar( $mode['quickstart_id'] ) ? sanitize_key( (string) $mode['quickstart_id'] ) : '';
402 $normalized[ $mode_id ] = $mode;
403 }
404 if ( ! isset( $normalized['classic'] ) ) {
405 $normalized['classic'] = $default_modes['classic'];
406 }
407
408 return $normalized;
409 }
410
411 /**
412 * Add ordered legacy placements from a recursive V3 sidebar declaration.
413 *
414 * @param array $placements Legacy placements passed by reference.
415 * @param array $definition Validated V3 definition.
416 * @param array $nodes Sidebar node declarations.
417 * @param string $group_id Root presentation group.
418 *
419 * @return void
420 */
421 private function add_legacy_sidebar_placements( &$placements, $definition, $nodes, $group_id ) {
422 $position = 0;
423 foreach ( $nodes as $navigation_key => $node ) {
424 $position += 10;
425 if ( isset( $node['page_id'] ) ) {
426 $this->add_legacy_sidebar_placement( $placements, $definition, $node['page_id'], $group_id, $navigation_key, $node, $position );
427 }
428 if ( isset( $node['items'] ) && is_array( $node['items'] ) ) {
429 $this->add_legacy_sidebar_placements( $placements, $definition, $node['items'], $group_id );
430 }
431 }
432 }
433
434 /**
435 * Add one V3 local page to the released canonical placement shape.
436 *
437 * @param array $placements Legacy placements passed by reference.
438 * @param array $definition Validated V3 definition.
439 * @param string $local_page_id Local V3 page ID.
440 * @param string $group_id Root presentation group.
441 * @param string $navigation_key Presentation key.
442 * @param array $node Sidebar presentation overrides.
443 * @param int $position Stable sibling position.
444 *
445 * @return void
446 */
447 private function add_legacy_sidebar_placement( &$placements, $definition, $local_page_id, $group_id, $navigation_key, $node, $position ) {
448 if ( ! isset( $definition['pages'][ $local_page_id ] ) ) {
449 return;
450 }
451 $canonical_page_id = $this->find_canonical_page_id( $definition['pages'][ $local_page_id ]['route'] );
452 if ( '' === $canonical_page_id ) {
453 return;
454 }
455 $placement = array(
456 'visible' => ! isset( $node['visible'] ) || false !== $node['visible'],
457 'position' => absint( $position ),
458 'group' => sanitize_key( $group_id ),
459 'navigation_key' => sanitize_key( $navigation_key ),
460 );
461 foreach ( array( 'title', 'font_icon', 'font_icon_right' ) as $presentation_key ) {
462 if ( array_key_exists( $presentation_key, $node ) ) {
463 $placement[ $presentation_key ] = $node[ $presentation_key ];
464 }
465 }
466 $placements[ $canonical_page_id ] = $placement;
467 }
468
469 /**
470 * Normalize released group presentation records with bounded work.
471 *
472 * @param mixed $groups Filtered group map.
473 *
474 * @return array<string,array<string,mixed>> Normalized groups.
475 */
476 private function normalize_groups( $groups ) {
477 $normalized = array();
478 foreach ( is_array( $groups ) ? array_slice( $groups, 0, WPBC_Booking_Modes_V3_Definition_Validator::MAX_COLLECTION_ENTRIES, true ) : array() as $group_id => $group ) {
479 $group_id = is_scalar( $group_id ) ? sanitize_key( (string) $group_id ) : '';
480 if ( '' === $group_id || ! is_array( $group ) ) {
481 continue;
482 }
483 $normalized[ $group_id ] = array(
484 'title' => isset( $group['title'] ) && is_scalar( $group['title'] ) ? wp_strip_all_tags( (string) $group['title'] ) : $group_id,
485 'position' => isset( $group['position'] ) ? absint( $group['position'] ) : 1000,
486 'font_icon' => isset( $group['font_icon'] ) && is_scalar( $group['font_icon'] ) ? (string) $group['font_icon'] : '',
487 'visible' => ! isset( $group['visible'] ) || (bool) $group['visible'],
488 );
489 }
490
491 return $normalized;
492 }
493
494 /**
495 * Normalize released canonical page placements with bounded scalar fields.
496 *
497 * @param mixed $placements Filtered placement map.
498 *
499 * @return array<string,array<string,mixed>> Normalized placements.
500 */
501 private function normalize_page_placements( $placements ) {
502 $normalized = array();
503 foreach ( is_array( $placements ) ? array_slice( $placements, 0, WPBC_Booking_Modes_V3_Definition_Validator::MAX_COLLECTION_ENTRIES, true ) : array() as $page_id => $placement ) {
504 $page_id = is_scalar( $page_id ) ? sanitize_key( (string) $page_id ) : '';
505 if ( '' === $page_id || ! is_array( $placement ) ) {
506 continue;
507 }
508 $normalized_placement = array(
509 'visible' => ! isset( $placement['visible'] ) || (bool) $placement['visible'],
510 'position' => isset( $placement['position'] ) ? absint( $placement['position'] ) : 1000,
511 );
512 foreach ( array( 'group', 'navigation_key' ) as $identifier_key ) {
513 if ( isset( $placement[ $identifier_key ] ) && is_scalar( $placement[ $identifier_key ] ) ) {
514 $normalized_placement[ $identifier_key ] = sanitize_key( (string) $placement[ $identifier_key ] );
515 }
516 }
517 foreach ( array( 'title', 'font_icon', 'font_icon_right' ) as $display_key ) {
518 if ( array_key_exists( $display_key, $placement ) && is_scalar( $placement[ $display_key ] ) ) {
519 $normalized_placement[ $display_key ] = 'title' === $display_key ? wp_strip_all_tags( (string) $placement[ $display_key ] ) : (string) $placement[ $display_key ];
520 }
521 }
522 $normalized[ $page_id ] = $normalized_placement;
523 }
524
525 return $normalized;
526 }
527
528 /**
529 * Normalize native submenu presentation records with bounded work.
530 *
531 * @param mixed $native_menu Filtered native submenu map.
532 *
533 * @return array<string,array<string,mixed>> Normalized native overrides.
534 */
535 private function normalize_native_menu( $native_menu ) {
536 $normalized = array();
537 foreach ( is_array( $native_menu ) ? array_slice( $native_menu, 0, WPBC_Booking_Modes_V3_Definition_Validator::MAX_COLLECTION_ENTRIES, true ) : array() as $menu_slug => $menu ) {
538 $menu_slug = is_scalar( $menu_slug ) ? sanitize_key( (string) $menu_slug ) : '';
539 if ( '' === $menu_slug || ! is_array( $menu ) ) {
540 continue;
541 }
542 $normalized_menu = array(
543 'position' => isset( $menu['position'] ) ? absint( $menu['position'] ) : 1000,
544 'visible' => ! isset( $menu['visible'] ) || (bool) $menu['visible'],
545 );
546 if ( array_key_exists( 'title', $menu ) && is_scalar( $menu['title'] ) ) {
547 $normalized_menu['title'] = wp_strip_all_tags( (string) $menu['title'] );
548 }
549 $normalized[ $menu_slug ] = $normalized_menu;
550 }
551
552 return $normalized;
553 }
554
555 /**
556 * Resolve a source route to an existing released canonical ID.
557 *
558 * @param array $route Normalized V3 route.
559 *
560 * @return string Canonical page ID or an empty string.
561 */
562 private function find_canonical_page_id( $route ) {
563 foreach ( WPBC_Booking_Modes_V3_Page_Registry::get_instance()->get_all() as $page_id => $page ) {
564 if ( $route['page'] === $page['page'] && $route['tab'] === $page['tab'] && $route['subtab'] === $page['subtab'] ) {
565 return $page_id;
566 }
567 }
568
569 return '';
570 }
571 }
572