$value ) { $base[ $key ] = $value; } return $base; } /** * Persist settings into Astra's own settings option. * * Why this exists at all (#48): the wizard previously wrote Astra's settings * with set_theme_mod(), which lands in the theme_mods_astra option. Astra never * reads that — every one of its settings is read through astra_get_option(), * which resolves against the astra-settings option (Astra 4.13.6: * inc/core/common-functions.php:558 -> Astra_Theme_Options::get_options() -> * inc/core/class-astra-theme-options.php:933 get_option( ASTRA_THEME_SETTINGS )). * Astra's only get_theme_mod() calls are for WordPress core's 'custom_logo'. * So those writes were silent no-ops: no error, no log, no effect. * * @param array $settings Settings to set, keyed by Astra option name. * @return void */ function contai_astra_apply_settings( array $settings ): void { if ( empty( $settings ) ) { return; } $current = get_option( CONTAI_ASTRA_SETTINGS_OPTION, array() ); update_option( CONTAI_ASTRA_SETTINGS_OPTION, contai_astra_settings_merge( $current, $settings ) ); } /** * Astra option holding the footer builder layout, and the menu component slug. * * Astra's footer is a BUILDER: the row template walks the layout and emits one * `do_action()` per component slug it finds * (astra 4.13.6: template-parts/footer/builder/components.php:152-158, the * `case 'menu':` arm firing `astra_footer_menu`). The nav menu itself is only * rendered from inside that arm, by Astra_Footer_Menu_Component::menu_markup() * (inc/builder/type/footer/menu/class-astra-footer-menu-component.php:91-104). * * So `has_nav_menu( 'footer_menu' )` being true is NECESSARY BUT NOT SUFFICIENT: * with the slug absent from the layout, the component never runs and the bound * menu renders nowhere — which is this issue's signature failure, one layer * further downstream than the seven before it (#48). */ const CONTAI_ASTRA_FOOTER_ITEMS_KEY = 'footer-desktop-items'; const CONTAI_ASTRA_FOOTER_MENU_COMPONENT = 'menu'; /** * The zone the menu component is placed into. * * It has to be `below_1`, not merely "some empty zone". The row template stops * emitting columns past the row's configured column count * (astra 4.13.6: template-parts/footer/builder/footer-row.php:56-59, * `if ( $astra_builder_zones > $astra_footer_columns ) { break; }`) and the * below row defaults to ONE column (`hbb-footer-column` = '1', * inc/core/builder/class-astra-builder-options.php:590). Measured on a live * install: the same component in `below_2` renders nothing, in `below_1` it * renders. Zone 1 is the only one guaranteed to be inside the rendered range * whatever the column count is. * * Astra renders every component in a zone, so sharing `below_1` with the * default `copyright` is supported and was verified live. */ const CONTAI_ASTRA_FOOTER_MENU_ZONE = 'below_1'; /** * Astra's own default footer builder layout. * * Transcribed from Astra 4.13.6 * (inc/core/builder/class-astra-builder-options.php:404-425). It is the * baseline when the site has no saved layout yet: Astra resolves that key * against these defaults, so persisting a partial structure would drop rows it * expects to find. * * @return array>> The default layout. */ function contai_astra_default_footer_items(): array { return array( 'above' => array( 'above_1' => array(), 'above_2' => array(), 'above_3' => array(), 'above_4' => array(), 'above_5' => array(), ), 'primary' => array( 'primary_1' => array(), 'primary_2' => array(), 'primary_3' => array(), 'primary_4' => array(), 'primary_5' => array(), ), 'below' => array( 'below_1' => array( 'copyright' ), 'below_2' => array(), 'below_3' => array(), 'below_4' => array(), 'below_5' => array(), ), ); } /** * Add the menu component to a footer builder layout, if it is not already in it. * * Returns null when there is nothing to do — the component is already placed * somewhere — so the caller can skip the write entirely rather than persisting * an identical payload. A layout that already mentions the component is left * untouched even if it sits in an unrendered zone: that is a choice the site * owner made in the customizer, and silently relocating their component is the * kind of unannounced act on someone else's data this issue is about. * * Pure function: no WordPress calls, so it is directly unit-testable. * * @param mixed $current Current value of Astra's footer-desktop-items option. * @return array|null Payload to persist, or null when unchanged. */ function contai_astra_footer_items_with_menu( $current ): ?array { // Type guard only. Seeding the defaults here as well would be dead code: // the row-restoration loop below already completes an empty or partial // payload, so both spellings produce byte-identical output — which is // exactly what a surviving mutant on this line proved. $items = is_array( $current ) ? $current : array(); // Already placed anywhere in the layout → leave it alone. foreach ( $items as $zones ) { if ( ! is_array( $zones ) ) { continue; } foreach ( $zones as $components ) { if ( is_array( $components ) && in_array( CONTAI_ASTRA_FOOTER_MENU_COMPONENT, $components, true ) ) { return null; } } } // Restore any row the saved payload is missing, so what we persist is a // complete structure Astra can read back. foreach ( contai_astra_default_footer_items() as $row => $zones ) { if ( ! isset( $items[ $row ] ) || ! is_array( $items[ $row ] ) ) { $items[ $row ] = $zones; } } $zone = isset( $items['below'][ CONTAI_ASTRA_FOOTER_MENU_ZONE ] ) && is_array( $items['below'][ CONTAI_ASTRA_FOOTER_MENU_ZONE ] ) ? $items['below'][ CONTAI_ASTRA_FOOTER_MENU_ZONE ] : array(); $zone[] = CONTAI_ASTRA_FOOTER_MENU_COMPONENT; $items['below'][ CONTAI_ASTRA_FOOTER_MENU_ZONE ] = array_values( $zone ); return $items; } /** * Make sure the theme will actually RENDER the footer menu we just bound. * * Binding a nav menu location is where this plugin's footer work used to stop. * On a builder-driven theme that is only half the job, and the missing half is * invisible: WordPress reports the location as taken, the menu holds its items, * and the theme emits nothing because the component was never placed in the * layout (see CONTAI_ASTRA_FOOTER_ITEMS_KEY). * * Only Astra is handled, deliberately. Astra is this plugin's default theme * (`get_option( 'contai_wordpress_theme', 'astra' )`), and its layout shape was * measured against a live install before being written here. Writing a * plausible-looking layout into a theme we have NOT measured is not a harmless * no-op the way a wrong nav slug is: a hand-built payload for Neve's * header-footer-grid took the whole front end down with a fatal * (`array_combine(): ... must have the same number of elements`, * neve 4.2.9 header-footer-grid/Core/Builder/Abstract_Builder.php:898). So * every other theme gets the diagnostic instead of a guess — the trace this * issue has gone without eight times. * * @param string $theme Theme slug the wizard installed. * @param string $location Nav menu location the footer menu was bound to. * @return void */ function contai_astra_ensure_footer_menu_renders( string $theme, string $location ): void { if ( 'astra' !== $theme ) { // The old wording ("the legal links stay invisible until the footer menu // element is added") is no longer true in either branch, and a warning // that overstates the damage is as misleading as one that understates // it: measured themes render the menu themselves, and unmeasured ones // now get contai_render_footer_legal_fallback() on wp_footer. $outcome = contai_theme_renders_bound_footer_menu( $theme ) ? 'that theme was measured rendering the bound menu in its own footer' : "its legal links are served by this plugin's own wp_footer fallback until that layout is measured"; contai_record_site_warning( 'footer menu render', sprintf( "the footer menu was bound to '%s', but this plugin does not manage theme '%s' footer layout: %s", $location, $theme, $outcome ) ); return; } $current = get_option( CONTAI_ASTRA_SETTINGS_OPTION, array() ); $items = contai_astra_footer_items_with_menu( is_array( $current ) && isset( $current[ CONTAI_ASTRA_FOOTER_ITEMS_KEY ] ) ? $current[ CONTAI_ASTRA_FOOTER_ITEMS_KEY ] : null ); if ( null === $items ) { return; } contai_astra_apply_settings( array( CONTAI_ASTRA_FOOTER_ITEMS_KEY => $items ) ); }