| 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 |
|