'0',
'admbud_core_remove_help' => '0',
'admbud_core_remove_screen_options' => '0',
'admbud_notices_suppress' => '0',
'admbud_core_custom_footer_enabled' => '0',
'admbud_core_custom_footer_text' => '',
'admbud_wl_sidebar_logo_url' => '',
'admbud_wl_sidebar_logo_width' => 84,
'admbud_wl_sidebar_logo_height' => 0,
'admbud_wl_sidebar_width' => 160,
'admbud_wl_favicon_id' => '',
'admbud_wl_agency_name' => '',
'admbud_wl_agency_url' => '',
'admbud_wl_greeting' => '',
'admbud_wl_remove_wp_links' => '0',
'admbud_wl_footer_version' => '',
'admbud_wl_footer_quote' => '0',
'admbud_wl_hide_wp_news' => '0',
// White Label - Cleanup (dissolved - items moved to Quick Settings)
// White Label - Updates
'admbud_wl_hide_core_update' => '0',
'admbud_wl_hide_plugin_update' => '0',
'admbud_wl_hide_theme_update' => '0',
'admbud_wl_hide_php_nag' => '0',
'admbud_wl_update_policy' => '',
'admbud_wl_disable_update_emails' => '0',
'admbud_wl_disable_all_updates' => '0',
// White Label - Gutenberg
// -- Admin Bar --
'admbud_show_in_adminbar' => '0',
// -- Dashboard --
'admbud_dashboard_role_pages' => '{}',
'admbud_dashboard_keep_widgets' => '',
'admbud_dashboard_custom_widgets' => '[]',
// -- Modules --
'admbud_modules_enabled_tabs' => '',
// -- Source --
'admbud_source_enabled' => '0',
'admbud_source_modules' => '',
'admbud_source_whitelist' => '[]',
'admbud_source_log' => '[]',
'admbud_receiver_sources' => '[]',
// -- Activity Log --
'admbud_activity_log_retention' => 90,
'admbud_activity_log_per_page' => 20,
// -- Colours (defaults derived from Colours::DEFAULT_PRESET_SLUG;
// today: 'violety'. To swap AB's default palette, update that
// constant AND the values below — they must match the same
// preset's `values` array in self::get_colour_presets().) --
'admbud_colours_primary' => Colours::DEFAULT_PRIMARY,
'admbud_colours_secondary' => Colours::DEFAULT_SECONDARY,
'admbud_colours_menu_text' => Colours::DEFAULT_MENU_TEXT,
'admbud_colours_menu_bg' => Colours::DEFAULT_MENU_BG,
'admbud_colours_submenu_bg' => Colours::DEFAULT_SUBMENU_BG,
'admbud_colours_active_text' => '#ffffff',
'admbud_colours_sep_color' => '#5600ed',
'admbud_colours_body_bg' => '#ffffff',
'admbud_colours_shadow_colour' => '',
'admbud_colours_menu_item_sep' => '1',
'admbud_colours_menu_bg_opacity' => '100',
'admbud_colours_menu_text_opacity' => '100',
'admbud_colours_sidebar_gradient' => '1',
'admbud_colours_sidebar_grad_dir' => 'to top',
'admbud_colours_sidebar_grad_from' => '#2e1065',
'admbud_colours_sidebar_grad_to' => '#1e1b2e',
'admbud_colours_submenu_text' => '#ede9fe',
'admbud_colours_hover_text' => '#ede9fe',
'admbud_colours_hover_bg' => '',
'admbud_colours_active_bg' => '',
'admbud_colours_active_parent_text' => '#ffffff',
'admbud_colours_submenu_hover_bg' => Colours::DEFAULT_PRIMARY,
'admbud_colours_submenu_hover_text' => '#ede9fe',
'admbud_colours_submenu_active_bg' => '',
'admbud_colours_submenu_active_text' => '',
'admbud_colours_adminbar_bg' => '#1a1828',
'admbud_colours_adminbar_text' => '#d4d1e0',
'admbud_colours_adminbar_hover_bg' => Colours::DEFAULT_PRIMARY,
'admbud_colours_adminbar_submenu_bg' => '#1a1828',
'admbud_colours_adminbar_hover_text' => '#ede9fe',
'admbud_colours_adminbar_sub_text' => '#d4d1e0',
'admbud_colours_adminbar_sub_hover_bg' => '',
'admbud_colours_adminbar_sub_hover_text' => '#ffffff',
'admbud_colours_pill_maintenance' => '#dd3333',
'admbud_colours_pill_coming_soon' => '#dd3333',
'admbud_colours_pill_noindex' => '#dd9933',
// Admin-bar pills: status indicators with meaningful semantic colours
// (red=blocking, orange=warning). pill_admin_buddy is the AB brand
// marker — neutral slate so a Colours-module-off site doesn't leak
// AB's marketing purple onto every admin page's top bar.
'admbud_colours_pill_admin_buddy' => '#475569', // Slate 600 — neutral.
// Content area tokens
'admbud_colours_content_heading' => '#1d2327',
'admbud_colours_content_text' => '#3c434a',
'admbud_colours_content_link' => Colours::DEFAULT_PRIMARY,
'admbud_colours_content_link_hover' => Colours::DEFAULT_SECONDARY,
'admbud_colours_table_header_bg' => '#f4effd',
'admbud_colours_table_header_text' => '#1d2327',
'admbud_colours_table_header_link' => '#1d2327',
'admbud_colours_table_row_bg' => '#ffffff',
'admbud_colours_table_row_text' => '#3c434a',
'admbud_colours_table_row_alt_bg' => '#f9f7fe',
'admbud_colours_table_row_alt_text' => '#3c434a',
'admbud_colours_table_row_hover' => '#efe7fc',
'admbud_colours_table_border' => '#decdfa',
'admbud_colours_table_row_separator' => '#ebe1fc',
'admbud_colours_table_action_link' => Colours::DEFAULT_PRIMARY,
'admbud_colours_table_title_link' => Colours::DEFAULT_PRIMARY,
'admbud_colours_input_bg' => '#ffffff',
'admbud_colours_input_border' => '#d1baf8',
'admbud_colours_input_focus' => Colours::DEFAULT_PRIMARY,
'admbud_colours_btn_primary_bg' => Colours::DEFAULT_PRIMARY,
'admbud_colours_btn_primary_text' => '#ffffff',
'admbud_colours_btn_primary_hover' => Colours::DEFAULT_SECONDARY,
'admbud_colours_btn_secondary_bg' => '#f7f3fd',
'admbud_colours_postbox_bg' => '#ffffff',
'admbud_colours_postbox_header_bg' => '#f9f7fe',
'admbud_colours_postbox_border' => '#decdfa',
'admbud_colours_postbox_text' => '',
'admbud_colours_notice_bg' => '#fbf9fe',
'admbud_colours_ui_radius' => 'off',
'admbud_css_exclusions' => '',
// -- Login / Maintenance / Coming Soon front-end pages --
//
// These render on public-facing pages (login screen, locked-site
// maintenance + coming-soon pages), not in the admin chrome — they
// must NOT default to AB's brand palette. The violet defaults
// previously inherited from Colours::DEFAULT_PAGE_* leaked AB's
// marketing identity into every visitor's first view. Defaults
// are now neutral white/dark-slate, professional and brand-neutral.
// Users who want the violet "Violety" preset still get it by
// clicking the preset card under Colours → Presets.
//
// -- Login --
'admbud_login_logo_url' => '',
'admbud_login_logo_width' => 84,
'admbud_login_logo_height' => 0,
'admbud_login_card_position' => 'center',
'admbud_login_bg_type' => 'solid',
'admbud_login_bg_color' => '#f1f1f1', // WP core's default wp-login bg.
'admbud_login_grad_from' => '#ffffff',
'admbud_login_grad_to' => '#f1f1f1',
'admbud_login_grad_direction' => 'to bottom right',
'admbud_login_bg_image_url' => '',
'admbud_login_bg_overlay_color' => '#000000',
'admbud_login_bg_overlay_opacity' => 30,
'admbud_login_card_bg_color' => '#ffffff',
'admbud_login_card_text_auto' => '1',
'admbud_login_card_text_color' => '#3c434a',
'admbud_login_card_width' => 400,
'admbud_login_btn_auto' => '1',
'admbud_login_btn_bg' => '#2271b1',
// -- Maintenance --
'admbud_maintenance_mode' => 'off',
'admbud_coming_soon_title' => 'Coming Soon',
'admbud_coming_soon_message' => "We're working on something exciting. Stay tuned!",
'admbud_maintenance_title' => 'Under Maintenance',
'admbud_maintenance_message' => "We're performing scheduled maintenance. We'll be back shortly!",
'admbud_maintenance_bypass_urls' => '',
'admbud_cs_bg_type' => 'solid',
'admbud_cs_bg_color' => '#ffffff',
'admbud_cs_grad_from' => '#ffffff',
'admbud_cs_grad_to' => '#f1f5f9',
'admbud_cs_grad_direction' => 'to bottom right',
'admbud_cs_bg_image_url' => '',
'admbud_cs_bg_overlay_color' => '#000000',
'admbud_cs_bg_overlay_opacity' => 30,
'admbud_cs_text_color' => '#1e293b',
'admbud_cs_message_color' => '#64748b',
'admbud_maint_bg_type' => 'solid',
'admbud_maint_bg_color' => '#ffffff',
'admbud_maint_grad_from' => '#ffffff',
'admbud_maint_grad_to' => '#f1f5f9',
'admbud_maint_grad_direction' => 'to bottom right',
'admbud_maint_bg_image_url' => '',
'admbud_maint_bg_overlay_color' => '#000000',
'admbud_maint_bg_overlay_opacity' => 30,
'admbud_maint_text_color' => '#1e293b',
'admbud_maint_message_color' => '#64748b',
// -- SMTP --
'admbud_smtp_enabled' => '0',
'admbud_smtp_mailer' => 'smtp',
'admbud_smtp_host' => '',
'admbud_smtp_port' => 587,
'admbud_smtp_username' => '',
'admbud_smtp_encryption' => 'tls',
'admbud_smtp_auth' => '1',
'admbud_smtp_from_name' => '',
'admbud_smtp_from_email' => '',
'admbud_smtp_fallback' => '0',
'admbud_smtp_disable_ssl_verify' => '0',
'admbud_smtp_preset' => 'custom',
// -- Menu Customiser --
'admbud_menu_config' => '{}',
'admbud_menu_show_item_borders' => '0',
// -- Bricks --
'admbud_bricks_enabled' => '0',
'admbud_bricks_color_mapping_enabled' => '1',
'admbud_bricks_custom_logo_url' => '',
'admbud_bricks_custom_spinner_url' => '',
// -- Bricks BEM Generator --
'admbud_bricks_bem_enabled' => '0',
'admbud_bricks_bem_auto_sync_labels' => '1',
'admbud_bricks_bem_show_modifiers' => '0',
'admbud_bricks_bem_default_action' => 'rename',
'admbud_bricks_bem_blacklist_extra' => '',
// -- Bricks Quick Insert --
'admbud_bricks_quick_enabled' => '0',
'admbud_bricks_quick_favourites' => '["section","container","heading","text-basic","image","button","icon","list"]',
// -- Option Pages --
'admbud_option_pages' => '{}',
// -- Collections --
'admbud_collections' => '{}',
// -- Media Manager prefs (Pro) --
'admbud_mm_default_color' => '',
'admbud_mm_default_expanded' => '1',
'admbud_mm_trash_enabled' => '1',
'admbud_mm_show_count' => '1',
'admbud_mm_show_size' => '1',
'admbud_mm_recursive_view' => '0',
'admbud_mm_tool_roles' => [],
];
}
// -- Multisite notice ------------------------------------------------------
/**
* Warn administrators on multisite installs that Admin Buddy does not
* support network-synced settings. Shows once per admin, dismissible.
*/
// -- Menu ------------------------------------------------------------------
public function register_page(): void {
// Menu icon: AB lettermark v3 - base64 data URI with fill="white" for WP menu rendering.
$icon_svg = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSI+PHBhdGggZD0iTTcyLjY2NzMgNi44MDM4N0M3Ny4wNTg4IDExLjE5NCA3OS40NzEzIDE3LjAyMjEgNzkuNDcxMyAyMy4yMjg0Qzc5LjQ3MDYgMzAuOTE3NSA3NC44MjM4IDQ2LjMyOCA1Ni4yNDE3IDQ2LjQ1OEw0Ni40NTcyIDQ2LjU2NDVMNDYuNDU3MiAzMy40NzI0QzUxLjgyNzMgMzMuNTAwOSA2NS4yMjc1IDMxLjUzNjggNjYuMDAwNCAyMy4yMjg3QzY2LjAwMDQgMjAuNjI2NCA2NC45OTc1IDE4LjE2MDMgNjMuMTUzOSAxNi4yOTA1QzYxLjMxMTEgMTQuNDQ3MSA1OC44NDUxIDEzLjQ0NDIgNTYuMjE1NCAxMy40NDQyQzUxLjAxMTcgMTMuNDQ0MiA0Ni40NTcyIDE3Ljk5ODIgNDYuNDU3MiAyMy4yMjg3TDQ2LjQ1NzIgMzMuNDcyNEwzMy4wMTg4IDMzLjVMMzMuMDE0IDIzLjA5MzZDMzMuMDE0IDE3LjAyMjEgMzUuNDI2OCAxMS4xOTQyIDM5LjgxNzQgNi44MDM4N0M0NC4yMDgzIDIuNDEyNCA1MC4wMzU5IC02LjQxMzUzZS0wNSA1Ni4yNDI1IC02LjQxMzUzZS0wNUM2Mi40NDkxIC02LjQxMzUzZS0wNSA2OC4yNzcgMi40MTI0IDcyLjY2NzMgNi44MDM4N1oiIGZpbGw9IndoaXRlIi8+PHBhdGggZD0iTTcyLjY2NzMgNzMuMTk2MUM3Ny4wNTg4IDY4LjgwNiA3OS40NzEzIDYyLjk3NzkgNzkuNDcxMyA1Ni43NzE2Qzc5LjQ3MDYgNDkuMDgyNSA3NC44MjM4IDMzLjY3MiA1Ni4yNDE3IDMzLjU0Mkw0Ni40NTcyIDMzLjQzNTVMNDYuNDU3MiA0Ni41Mjc2QzUxLjgyNzMgNDYuNDk5MSA2NS4yMjc1IDQ4LjQ2MzIgNjYuMDAwNCA1Ni43NzEzQzY2LjAwMDQgNTkuMzczNiA2NC45OTc1IDYxLjgzOTcgNjMuMTUzOSA2My43MDk1QzYxLjMxMTEgNjUuNTUyOSA1OC44NDUxIDY2LjU1NTggNTYuMjE1NCA2Ni41NTU4QzUxLjAxMTcgNjYuNTU1OCA0Ni40NTcyIDYyLjAwMTggNDYuNDU3MiA1Ni43NzEzTDQ2LjQ1NzIgNDYuNTI3NkwzMy4wMTg4IDQ2LjVMMzMuMDE0IDU2LjkwNjRDMzMuMDE0IDYyLjk3NzkgMzUuNDI2OCA2OC44MDU4IDM5LjgxNzQgNzMuMTk2MUM0NC4yMDgzIDc3LjU4NzYgNTAuMDM1OSA4MC4wMDAxIDU2LjI0MjUgODAuMDAwMUM2Mi40NDkxIDgwLjAwMDEgNjguMjc3IDc3LjU4NzYgNzIuNjY3MyA3My4xOTYxWiIgZmlsbD0id2hpdGUiLz48cGF0aCBkPSJNNi44MDM4OCA3My4xOTYxQzIuNDEyNDEgNjguODA2IC02LjU0NzE5ZS0wNSA2Mi45Nzc5IC02LjU0NzE5ZS0wNSA1Ni43NzE2QzAuMDAwNTgxMTIgNDkuMDgyNSA0LjY0NzQxIDMzLjY3MiAyMy4yMjk1IDMzLjU0MkwzMy4wMTQgMzMuNDM1NUwzMy4wMTQgNDYuNTI3NkMyNy42NDM5IDQ2LjQ5OTEgMTQuMjQzNyA0OC40NjMyIDEzLjQ3MDggNTYuNzcxM0MxMy40NzA4IDU5LjM3MzYgMTQuNDczNyA2MS44Mzk3IDE2LjMxNzMgNjMuNzA5NUMxOC4xNjAxIDY1LjU1MjkgMjAuNjI2MSA2Ni41NTU4IDIzLjI1NTggNjYuNTU1OEMyOC40NTk1IDY2LjU1NTggMzMuMDE0IDYyLjAwMTggMzMuMDE0IDU2Ljc3MTNMMzMuMDE0IDQ2LjUyNzZMNDYuNDUyNCA0Ni41TDQ2LjQ1NzIgNTYuOTA2NEM0Ni40NTcyIDYyLjk3NzkgNDQuMDQ0NCA2OC44MDU4IDM5LjY1MzggNzMuMTk2MUMzNS4yNjI5IDc3LjU4NzYgMjkuNDM1MyA4MC4wMDAxIDIzLjIyODcgODAuMDAwMUMxNy4wMjIxIDgwLjAwMDEgMTEuMTk0MiA3Ny41ODc2IDYuODAzODggNzMuMTk2MVoiIGZpbGw9IndoaXRlIi8+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0tNi41NDcxOWUtMDUgMjMuMjI4NEMtNi41NDcxOWUtMDUgMTcuMDIyMSAyLjQxMjQxIDExLjE5NCA2LjgwMzg4IDYuODAzODdDMTEuMTk0MiAyLjQxMjQgMTcuMDIyMSAtNi40MTM1M2UtMDUgMjMuMjI4NyAtNi40MTM1M2UtMDVDMjkuNDM1MyAtNi40MTM1M2UtMDUgMzUuMjYyOSAyLjQxMjQgMzkuNjUzOCA2LjgwMzg3QzQ0LjA0NDQgMTEuMTk0MiA0Ni40NTcyIDE3LjAyMjEgNDYuNDU3MiAyMy4wOTM2TDQ2LjQ1MjQgMzMuNUwzMy4wMTQgMzMuNDcyNEwzMy4wMTQgMjMuMjI4N0MzMy4wMTQgMTcuOTk4MiAyOC40NTk1IDEzLjQ0NDIgMjMuMjU1OCAxMy40NDQyQzIwLjYyNjEgMTMuNDQ0MiAxOC4xNjAxIDE0LjQ0NzEgMTYuMzE3MyAxNi4yOTA1QzE0LjQ3MzcgMTguMTYwMyAxMy40NzA4IDIwLjYyNjQgMTMuNDcwOCAyMy4yMjg3QzEzLjgyOTIgMjcuMDgxMyAxNi45MDI5IDI5LjU2OTcgMjAuNjMyIDMxLjEyNzlDMTIuMDUxOCAzMS4xMjc5IDYuMzE5NiAzNS4zODE5IDQuNTI2MDEgMzcuNTA4OUMxLjEzNTI1IDMyLjcxMzcgMC4wMDAyNTQwNjMgMjcuMDI4MiAtNi41NDcxOWUtMDUgMjMuMjI4NFoiIGZpbGw9IndoaXRlIi8+PHJlY3QgeD0iMzMuMDEzMiIgeT0iMzMuNDM1NSIgd2lkdGg9IjEzLjQ0NDciIGhlaWdodD0iMTMuMTMwMiIgZmlsbD0id2hpdGUiLz48L3N2Zz4=';
$page = add_menu_page(
__( 'Admin Buddy', 'admin-buddy' ),
__( 'Admin Buddy', 'admin-buddy' ),
'manage_options',
'admbud',
[ $this, 'render_page' ],
$icon_svg,
80
);
// First submenu: same slug as parent so WP doesn't create a duplicate.
// Clicking parent "Admin Buddy" goes to last visited tab.
add_submenu_page(
'admbud',
__( 'Admin Buddy', 'admin-buddy' ),
__( 'Admin Buddy', 'admin-buddy' ),
'manage_options',
'admbud',
[ $this, 'render_page' ]
);
// Explicit Modules submenu - always goes to Modules tab.
add_submenu_page(
'admbud',
__( 'Modules', 'admin-buddy' ),
__( 'Modules', 'admin-buddy' ),
'manage_options',
'admbud&tab=modules',
[ $this, 'render_page' ]
);
// Plugin Data submenu.
add_submenu_page(
'admbud',
__( 'Plugin Data', 'admin-buddy' ),
__( 'Plugin Data', 'admin-buddy' ),
'manage_options',
'admbud&tab=plugin-data',
[ $this, 'render_page' ]
);
}
/** Optionally add Admin Buddy to the WP admin bar with submenu. */
public function maybe_register_adminbar( \WP_Admin_Bar $bar ): void {
if ( admbud_get_option( 'admbud_show_in_adminbar', '0' ) !== '1' ) { return; }
if ( ! current_user_can( 'manage_options' ) ) { return; }
$base = admin_url( 'admin.php?page=admbud' );
$bar->add_node( [
'id' => 'admbud',
'title' => '' . esc_html__( 'Admin Buddy', 'admin-buddy' ) . '',
'href' => $base,
'meta' => [ 'title' => __( 'Admin Buddy Settings', 'admin-buddy' ) ],
] );
// Modules link.
$bar->add_node( [
'parent' => 'admbud',
'id' => 'ab-bar-modules',
'title' => __( 'Modules', 'admin-buddy' ),
'href' => $base . '&tab=modules',
] );
// Enabled module links grouped with separators.
$enabled = $this->get_enabled_tabs();
$tabs = $this->get_manageable_tabs();
$groups = [ 'interface', 'utilities', 'integrations' ];
foreach ( $groups as $group_key ) {
$group_tabs = array_filter( $tabs, function ( $t ) use ( $group_key ) {
return ( $t['group'] ?? '' ) === $group_key;
} );
foreach ( $group_tabs as $gslug => $gtab ) {
if ( ! in_array( $gslug, $enabled, true ) ) { continue; }
$bar->add_node( [
'parent' => 'admbud',
'id' => 'ab-bar-' . $gslug,
'title' => $gtab['label'],
// Tab-less modules (e.g. Media Manager) carry their own 'url'
// - they have no &tab= page, so link to where they actually live.
'href' => $gtab['url'] ?? ( $base . '&tab=' . $gslug ),
] );
}
}
// Always-visible manage tabs - same array as sidebar, kept in sync.
$manage_tabs = [
'quick-settings' => __( 'Quick Settings', 'admin-buddy' ),
'source' => __( 'Remote', 'admin-buddy' ),
'plugin-data' => __( 'Plugin Data', 'admin-buddy' ),
];
foreach ( $manage_tabs as $mtab_slug => $mtab_label ) {
$bar->add_node( [
'parent' => 'admbud',
'id' => 'ab-bar-' . $mtab_slug,
'title' => $mtab_label,
'href' => $base . '&tab=' . $mtab_slug,
] );
}
}
/**
* Intercept options.php's post-save redirect on our settings pages.
*
* When any of our settings groups are saved, options.php redirects back
* with ?settings-updated=true which triggers WP's settings_errors() system
* and produces notices we can't control placement of. We swap that param
* for our own ?admbud_notice=saved so we can render it exactly where we want.
*/
public function intercept_redirect( string $location ): string {
if (
strpos( $location, 'page=admbud' ) !== false &&
strpos( $location, 'settings-updated=true' ) !== false
) {
// This filter runs on the wp_redirect hook AFTER WordPress's
// options.php pipeline has already verified the form's nonce
// ($option_page-options) and saved the registered settings. The
// 'settings-updated=true' marker in $location confirms that path
// executed successfully — we only get here on a verified save.
//
// The $_POST reads below are read-only sanitisation of two custom
// hidden fields (admbud_tab, admbud_subtab) that the form sets so
// we can preserve nav state in the redirect URL. No state change,
// no database writes — just URL massaging via add_query_arg().
// Force-clear the CSS cache on every settings save.
// Belt-and-suspenders: updated_option should bust it per-key,
// but some toggles (separator, gradient) can be missed if WP
// skips the update when the value hasn't changed.
Colours::maybe_bust_cache();
// Swap WP's settings-updated for our own notice param.
$location = remove_query_arg( 'settings-updated', $location );
$location = add_query_arg( 'admbud_notice', 'saved', $location );
// Restore the main tab. Strip any tab= already in the URL (from the
// HTTP referer) then add ours cleanly, so there's no duplication.
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WP options.php pipeline upstream (see comment above).
$tab = sanitize_key( wp_unslash( $_POST['admbud_tab'] ?? '' ) );
if ( $tab !== '' ) {
$location = remove_query_arg( 'tab', $location );
$location = add_query_arg( 'tab', $tab, $location );
}
// Restore the active sub-tab.
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WP options.php pipeline upstream (see comment above).
$subtab = sanitize_key( wp_unslash( $_POST['admbud_subtab'] ?? '' ) );
if ( $subtab !== '' ) {
$location = remove_query_arg( 'admbud_subtab', $location );
$location = add_query_arg( 'admbud_subtab', $subtab, $location );
}
}
return $location;
}
/**
* Render our own inline success/error notice inside .ab-panel.
* Reads ?admbud_notice= set by intercept_redirect() or handle_advanced().
* WP's settings_errors() system is never called for our settings pages.
*/
public function render_notice(): void {
// phpcs:ignore WordPress.Security.NonceVerification
if ( empty( $_GET['admbud_notice'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput
// phpcs:ignore WordPress.Security.NonceVerification
$key = sanitize_key( $_GET['admbud_notice'] ); // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput
// These notices are rendered as JS toasts instead of HTML banners.
$js_toasted = [ 'saved', 'import_ok', 'reset_ok', 'import_empty', 'import_read_fail', 'import_invalid' ];
if ( in_array( $key, $js_toasted, true ) ) { return; }
// Demo mode blocked action notice.
if ( $key === 'demo_blocked' ) {
?>
🔒
'boolean', 'sanitize_callback' => [ $this, 'sanitize_checkbox' ], 'default' => '0',
] );
}
register_setting( 'admbud_core_group', 'admbud_core_custom_footer_enabled', [
'type' => 'boolean', 'sanitize_callback' => [ $this, 'sanitize_checkbox' ], 'default' => '0',
] );
register_setting( 'admbud_core_group', 'admbud_core_custom_footer_text', [
'type' => 'string', 'sanitize_callback' => 'wp_kses_post', 'default' => '',
] );
register_setting( 'admbud_core_group', 'admbud_wl_sidebar_logo_url', [
'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'default' => '',
] );
register_setting( 'admbud_core_group', 'admbud_wl_sidebar_logo_width', [
'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 84,
] );
register_setting( 'admbud_core_group', 'admbud_wl_sidebar_logo_height', [
'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 0,
] );
register_setting( 'admbud_core_group', 'admbud_wl_sidebar_width', [
'type' => 'integer', 'sanitize_callback' => [ $this, 'sanitize_sidebar_width' ], 'default' => 160,
] );
// Favicon: stored in WP's native 'site_icon' option (attachment ID).
// We register under our group so it saves via our form; WP reads it natively.
register_setting( 'admbud_core_group', 'admbud_wl_favicon_id', [
'type' => 'integer',
'sanitize_callback' => function( $val ) {
$id = absint( $val );
// Sync to WP native option so Settings > General reflects the change.
update_option( 'site_icon', $id );
return $id; // stored as admbud_wl_favicon_id but ignored at read - we always read site_icon directly.
},
'default' => 0,
] );
register_setting( 'admbud_core_group', 'admbud_dashboard_role_pages', [
'type' => 'string', 'sanitize_callback' => [ $this, 'sanitize_dashboard_role_pages' ], 'default' => '{}',
] );
// -- Phase 1 White Label options ---------------------------------------
$wl_toggles = [
'admbud_wl_remove_wp_links', 'admbud_wl_hide_wp_news',
'admbud_wl_footer_quote',
];
foreach ( $wl_toggles as $key ) {
register_setting( 'admbud_core_group', $key, [
'type' => 'boolean', 'sanitize_callback' => [ $this, 'sanitize_checkbox' ], 'default' => '0',
] );
}
$wl_text_fields = [
'admbud_wl_agency_name' => 'sanitize_text_field',
'admbud_wl_agency_url' => 'esc_url_raw',
'admbud_wl_greeting' => 'sanitize_text_field',
'admbud_wl_footer_version' => 'sanitize_text_field',
];
foreach ( $wl_text_fields as $key => $sanitizer ) {
register_setting( 'admbud_core_group', $key, [
'type' => 'string', 'sanitize_callback' => $sanitizer, 'default' => '',
] );
}
register_setting( 'admbud_core_group', 'admbud_dashboard_keep_widgets', [
'type' => 'string',
'sanitize_callback' => function( $val ) {
$decoded = json_decode( wp_unslash( $val ), true );
if ( ! is_array( $decoded ) ) { return ''; }
return wp_json_encode( array_map( 'sanitize_key', $decoded ) );
},
'default' => '',
] );
register_setting( 'admbud_core_group', 'admbud_dashboard_custom_widgets', [
'type' => 'string',
'sanitize_callback' => function( $val ) {
$decoded = json_decode( wp_unslash( $val ), true );
if ( ! is_array( $decoded ) ) { return '[]'; }
$clean = [];
foreach ( $decoded as $w ) {
if ( ! is_array( $w ) ) { continue; }
$clean[] = [
'title' => sanitize_text_field( $w['title'] ?? '' ),
'content' => wp_kses_post( $w['content'] ?? '' ),
];
}
return wp_json_encode( $clean );
},
'default' => '[]',
] );
register_setting( 'admbud_core_group', 'admbud_notices_suppress', [
'type' => 'boolean', 'sanitize_callback' => [ $this, 'sanitize_checkbox' ], 'default' => '1',
] );
register_setting( 'admbud_core_group', 'admbud_show_in_adminbar', [
'type' => 'boolean', 'sanitize_callback' => [ $this, 'sanitize_checkbox' ], 'default' => '0',
] );
register_setting( 'admbud_modules_group', 'admbud_modules_enabled_tabs', [
'type' => 'string',
'sanitize_callback' => [ $this, 'sanitize_setup_tabs' ],
'default' => '',
] );
// -- Activity Log -------------------------------------------------------
register_setting( 'admbud_activity_log_group', 'admbud_activity_log_per_page', [
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 20,
] );
register_setting( 'admbud_activity_log_group', 'admbud_activity_log_retention', [
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 90,
] );
// -- Quick Settings -----------------------------------------------------
$qs_keys = [
'admbud_qs_disable_emoji',
'admbud_qs_disable_jquery_migrate',
'admbud_qs_remove_feed_links',
'admbud_qs_remove_rsd',
'admbud_qs_remove_wlw',
'admbud_qs_remove_shortlink',
'admbud_qs_remove_restapi_link',
'admbud_qs_disable_embeds',
'admbud_qs_remove_version',
'admbud_qs_disable_xmlrpc',
'admbud_qs_disable_rest_api',
'admbud_qs_disable_file_edit',
'admbud_qs_disable_feeds',
'admbud_qs_disable_self_ping',
'admbud_qs_disable_comments_default',
'admbud_qs_duplicate_post',
'admbud_qs_user_last_seen',
'admbud_qs_allow_svg',
];
foreach ( $qs_keys as $qs_key ) {
register_setting( 'admbud_quick_settings', $qs_key, [
'type' => 'boolean',
'sanitize_callback' => [ $this, 'sanitize_checkbox' ],
'default' => '0',
] );
}
register_setting( 'admbud_quick_settings', 'admbud_qs_svg_roles', [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => 'administrator',
] );
// -- Bricks Builder Integration ----------------------------------------
register_setting( 'admbud_bricks_group', 'admbud_bricks_enabled', [
'type' => 'boolean',
'sanitize_callback' => function( $val ) {
$enabled = ( $val === '1' || $val === true || $val === 1 );
// If toggled off, revert Bricks to its default builder mode.
if ( ! $enabled && class_exists( \Admbud\Bricks::class ) ) {
\Admbud\Bricks::revert_bricks_builder_mode();
}
return $enabled ? '1' : '0';
},
'default' => '0',
] );
// Sub-toggle for the AB-to-Bricks colour mapping. When off, we leave
// Bricks' --builder-* vars alone and revert builderMode to 'light' so
// the builder uses Bricks' own native palette - even if other parts of
// the integration (dynamic data, query loops, logo replacement) stay on.
register_setting( 'admbud_bricks_group', 'admbud_bricks_color_mapping_enabled', [
'type' => 'boolean',
'sanitize_callback' => function( $val ) {
$enabled = ( $val === '1' || $val === true || $val === 1 );
if ( ! $enabled && class_exists( \Admbud\Bricks::class ) ) {
\Admbud\Bricks::revert_bricks_builder_mode();
}
return $enabled ? '1' : '0';
},
'default' => '1',
] );
// Per-variable colour overrides - empty string = use derived value.
$bricks_var_keys = [
'builder_bg', 'builder_bg_2', 'builder_bg_3', 'builder_bg_accent',
'builder_color', 'builder_color_description', 'builder_color_accent',
'builder_color_accent_inverse', 'builder_color_knob',
'builder_border_color', 'bricks_tooltip_bg', 'bricks_tooltip_text',
];
foreach ( $bricks_var_keys as $vk ) {
register_setting( 'admbud_bricks_group', 'admbud_bricks_' . $vk, [
'type' => 'string',
'sanitize_callback' => function( $v ) {
// Empty = "use derived". Non-empty must be a hex colour.
$v = trim( $v );
if ( $v === '' ) { return ''; }
return sanitize_hex_color( $v ) ?? '';
},
'default' => '',
] );
}
// -- Bricks Other Tweaks -----------------------------------------------
foreach ( [ 'admbud_bricks_hide_logo', 'admbud_bricks_hide_spinner' ] as $tweak_key ) {
register_setting( 'admbud_bricks_group', $tweak_key, [
'type' => 'boolean',
'sanitize_callback' => [ $this, 'sanitize_checkbox' ],
'default' => '0',
] );
}
// Custom logo / spinner URLs - paired with the toggles above. Empty
// string falls back to AB's default styled logo and CSS ring spinner.
foreach ( [ 'admbud_bricks_custom_logo_url', 'admbud_bricks_custom_spinner_url' ] as $url_key ) {
register_setting( 'admbud_bricks_group', $url_key, [
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
'default' => '',
] );
}
// -- Bricks BEM Generator ----------------------------------------------
foreach ( [
'admbud_bricks_bem_enabled',
'admbud_bricks_bem_auto_sync_labels',
'admbud_bricks_bem_show_modifiers',
] as $bem_bool_key ) {
register_setting( 'admbud_bricks_group', $bem_bool_key, [
'type' => 'boolean',
'sanitize_callback' => [ $this, 'sanitize_checkbox' ],
'default' => ( $bem_bool_key === 'admbud_bricks_bem_auto_sync_labels' ) ? '1' : '0',
] );
}
register_setting( 'admbud_bricks_group', 'admbud_bricks_bem_default_action', [
'type' => 'string',
'sanitize_callback' => function( $v ) {
$allowed = [ 'rename', 'remove', 'delete', 'keep', 'copy-id' ];
return in_array( $v, $allowed, true ) ? $v : 'rename';
},
'default' => 'rename',
] );
register_setting( 'admbud_bricks_group', 'admbud_bricks_bem_blacklist_extra', [
'type' => 'string',
'sanitize_callback' => function( $v ) {
// Comma-separated list of property keys to treat as content
// (not CSS styles) during the copy-id action. Bounded at
// 64 tokens * 32 chars so hostile input can't balloon.
$raw = (string) $v;
$tokens = array_filter( array_map( 'trim', explode( ',', $raw ) ) );
$tokens = array_slice( $tokens, 0, 64 );
$tokens = array_map( function ( $t ) {
return substr( sanitize_key( $t ), 0, 32 );
}, $tokens );
$tokens = array_values( array_filter( $tokens ) );
return implode( ',', $tokens );
},
'default' => '',
] );
// -- Bricks Quick Insert -----------------------------------------------
register_setting( 'admbud_bricks_group', 'admbud_bricks_quick_enabled', [
'type' => 'boolean',
'sanitize_callback' => [ $this, 'sanitize_checkbox' ],
'default' => '0',
] );
register_setting( 'admbud_bricks_group', 'admbud_bricks_quick_favourites', [
'type' => 'string',
'sanitize_callback' => function( $v ) {
// Stored as JSON array of slugs. Drop anything not in the
// canonical catalog on save so the client never sees
// unknown slugs.
$catalog = class_exists( '\Admbud\BricksQuick' )
? array_keys( \Admbud\BricksQuick::CATALOG )
: [];
$raw = is_string( $v ) ? $v : wp_json_encode( $v );
$arr = json_decode( $raw, true );
if ( ! is_array( $arr ) ) { return '[]'; }
$arr = array_values( array_filter(
array_map( 'sanitize_key', $arr ),
function ( $slug ) use ( $catalog ) {
return in_array( $slug, $catalog, true );
}
) );
$arr = array_slice( $arr, 0, 30 );
return wp_json_encode( $arr );
},
'default' => '[]',
] );
// -- Admin UI Colours --------------------------------------------------
$colour_opts = [
// hex-only: these feed into darken/lighten/hex_to_rgb_triplet calculations.
'admbud_colours_primary' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_PRIMARY ],
'admbud_colours_menu_bg' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_MENU_BG ],
'admbud_colours_sidebar_grad_from'=> [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_SIDEBAR_GRAD_FROM ],
'admbud_colours_sidebar_grad_to' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_SIDEBAR_GRAD_TO ],
// rgba/hsla allowed: used directly in CSS output only.
'admbud_colours_secondary' => [ 'string', [ $this, 'sanitize_color' ], Colours::DEFAULT_SECONDARY ],
'admbud_colours_menu_text' => [ 'string', [ $this, 'sanitize_color' ], Colours::DEFAULT_MENU_TEXT ],
'admbud_colours_active_text' => [ 'string', [ $this, 'sanitize_color' ], Colours::DEFAULT_ACTIVE_TEXT ],
'admbud_colours_sep_color' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_body_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_adminbar_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_adminbar_hover_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_adminbar_submenu_bg'=> [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_adminbar_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_adminbar_hover_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_adminbar_sub_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_adminbar_sub_hover_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_pill_maintenance' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_pill_coming_soon' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_pill_noindex' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_pill_admin_buddy' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_submenu_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_hover_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_hover_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_active_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_active_parent_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_submenu_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_submenu_hover_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_submenu_hover_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_submenu_active_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_submenu_active_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_adminbar_sub_hover_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_shadow_colour' => [ 'string', [ $this, 'sanitize_color' ], '' ],
// -- Content area tokens ------------------------------------------
'admbud_colours_content_heading' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_content_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_content_link' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_content_link_hover' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_header_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_header_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_header_link' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_row_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_row_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_row_alt_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_row_alt_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_row_hover' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_border' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_row_separator' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_action_link' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_table_title_link' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_input_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_input_border' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_input_focus' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_btn_primary_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_btn_primary_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_btn_primary_hover' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_btn_secondary_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_postbox_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_postbox_header_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_postbox_border' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_postbox_text' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_notice_bg' => [ 'string', [ $this, 'sanitize_color' ], '' ],
'admbud_colours_sidebar_gradient' => [ 'boolean', [ $this, 'sanitize_checkbox' ], '0' ],
'admbud_colours_sidebar_grad_dir' => [ 'string', [ $this, 'sanitize_grad_direction' ], Colours::DEFAULT_SIDEBAR_GRAD_DIR ],
'admbud_colours_menu_item_sep' => [ 'boolean', [ $this, 'sanitize_checkbox' ], '1' ],
'admbud_colours_ui_radius' => [ 'string', [ $this, 'sanitize_ui_radius' ], 'off' ],
];
foreach ( $colour_opts as $key => [ $type, $cb, $default ] ) {
register_setting( 'admbud_colours_group', $key, [
'type' => $type, 'sanitize_callback' => $cb, 'default' => $default,
] );
}
// -- CSS Exclusions ----------------------------------------------------
register_setting( 'admbud_css_exclusions_group', 'admbud_css_exclusions', [
'type' => 'string',
'sanitize_callback' => [ $this, 'sanitize_css_exclusions' ],
'default' => '',
] );
// -- Login branding ----------------------------------------------------
$login_opts = [
'admbud_login_logo_url' => [ 'string', 'esc_url_raw', '' ],
'admbud_login_logo_width' => [ 'integer', [ $this, 'sanitize_logo_width' ], 84 ],
'admbud_login_logo_height' => [ 'integer', [ $this, 'sanitize_logo_height' ], 0 ],
'admbud_login_card_position' => [ 'string', [ $this, 'sanitize_card_position' ], 'center' ],
'admbud_login_bg_type' => [ 'string', [ $this, 'sanitize_bg_type' ], 'solid' ],
'admbud_login_bg_color' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_PAGE_BG ],
'admbud_login_grad_from' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_SIDEBAR_GRAD_FROM ],
'admbud_login_grad_to' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_SIDEBAR_GRAD_TO ],
'admbud_login_grad_direction' => [ 'string', [ $this, 'sanitize_grad_direction' ], 'to bottom right' ],
'admbud_login_bg_image_url' => [ 'string', 'esc_url_raw', '' ],
'admbud_login_bg_overlay_color' => [ 'string', [ $this, 'sanitize_hex_color' ], '#000000' ],
'admbud_login_bg_overlay_opacity' => [ 'integer', [ $this, 'sanitize_overlay_opacity' ], 30 ],
'admbud_login_card_bg_color' => [ 'string', [ $this, 'sanitize_hex_color' ], '#ffffff' ],
'admbud_login_card_text_auto' => [ 'string', [ $this, 'sanitize_checkbox' ], '1' ],
'admbud_login_card_text_color' => [ 'string', [ $this, 'sanitize_hex_color' ], '#3c434a' ],
'admbud_login_card_width' => [ 'integer', [ $this, 'sanitize_card_width' ], 400 ],
'admbud_login_btn_auto' => [ 'string', [ $this, 'sanitize_checkbox' ], '1' ],
'admbud_login_btn_bg' => [ 'string', [ $this, 'sanitize_hex_color' ], '#2271b1' ],
];
foreach ( $login_opts as $key => [ $type, $cb, $default ] ) {
register_setting( 'admbud_login_group', $key, [
'type' => $type, 'sanitize_callback' => $cb, 'default' => $default,
] );
}
// -- Maintenance -------------------------------------------------------
$maint_opts = [
'admbud_maintenance_mode' => [ 'string', [ $this, 'sanitize_maintenance_mode' ], 'off' ],
// Coming Soon
'admbud_coming_soon_title' => [ 'string', 'sanitize_text_field', __( 'Coming Soon', 'admin-buddy' ) ],
'admbud_coming_soon_message' => [ 'string', 'wp_kses_post', __( "We're working on something exciting. Stay tuned!", 'admin-buddy' ) ],
'admbud_cs_bg_type' => [ 'string', [ $this, 'sanitize_bg_type' ], 'solid' ],
'admbud_cs_bg_color' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_PAGE_BG ],
'admbud_cs_grad_from' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_SIDEBAR_GRAD_FROM ],
'admbud_cs_grad_to' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_SIDEBAR_GRAD_TO ],
'admbud_cs_grad_direction' => [ 'string', [ $this, 'sanitize_grad_direction' ], 'to bottom right' ],
'admbud_cs_bg_image_url' => [ 'string', 'esc_url_raw', '' ],
'admbud_cs_bg_overlay_color' => [ 'string', [ $this, 'sanitize_hex_color' ], '#000000' ],
'admbud_cs_bg_overlay_opacity' => [ 'integer', [ $this, 'sanitize_overlay_opacity' ], 30 ],
// Maintenance
'admbud_maintenance_title' => [ 'string', 'sanitize_text_field', __( 'Under Maintenance', 'admin-buddy' ) ],
'admbud_maintenance_message' => [ 'string', 'wp_kses_post', __( "We're performing scheduled maintenance. We'll be back shortly!", 'admin-buddy' ) ],
'admbud_maint_bg_type' => [ 'string', [ $this, 'sanitize_bg_type' ], 'solid' ],
'admbud_maint_bg_color' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_PAGE_BG ],
'admbud_maint_grad_from' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_SIDEBAR_GRAD_FROM ],
'admbud_maint_grad_to' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_SIDEBAR_GRAD_TO ],
'admbud_maint_grad_direction' => [ 'string', [ $this, 'sanitize_grad_direction' ], 'to bottom right' ],
'admbud_maint_bg_image_url' => [ 'string', 'esc_url_raw', '' ],
'admbud_maint_bg_overlay_color' => [ 'string', [ $this, 'sanitize_hex_color' ], '#000000' ],
'admbud_maint_bg_overlay_opacity' => [ 'integer', [ $this, 'sanitize_overlay_opacity' ], 30 ],
'admbud_maintenance_bypass_urls' => [ 'string', [ $this, 'sanitize_bypass_urls' ], '' ],
// Text colours for intercept pages
'admbud_cs_text_color' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_PAGE_TEXT ],
'admbud_cs_message_color' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_PAGE_MESSAGE ],
'admbud_maint_text_color' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_PAGE_TEXT ],
'admbud_maint_message_color' => [ 'string', [ $this, 'sanitize_hex_color' ], Colours::DEFAULT_PAGE_MESSAGE ],
];
foreach ( $maint_opts as $key => [ $type, $cb, $default ] ) {
register_setting( 'admbud_maintenance_group', $key, [
'type' => $type, 'sanitize_callback' => $cb, 'default' => $default,
] );
}
// -- SMTP --------------------------------------------------------------
$smtp_opts = [
'admbud_smtp_enabled' => [ 'boolean', [ $this, 'sanitize_checkbox' ], '0' ],
'admbud_smtp_mailer' => [ 'string', [ $this, 'sanitize_smtp_mailer' ], 'smtp' ],
'admbud_smtp_host' => [ 'string', 'sanitize_text_field', '' ],
'admbud_smtp_port' => [ 'integer', [ $this, 'sanitize_smtp_port' ], 587 ],
'admbud_smtp_username' => [ 'string', 'sanitize_text_field', '' ],
'admbud_smtp_encryption' => [ 'string', [ $this, 'sanitize_smtp_enc' ], 'tls' ],
'admbud_smtp_auth' => [ 'boolean', [ $this, 'sanitize_checkbox' ], '1' ],
'admbud_smtp_from_name' => [ 'string', 'sanitize_text_field', '' ],
'admbud_smtp_from_email' => [ 'string', 'sanitize_email', '' ],
'admbud_smtp_fallback' => [ 'boolean', [ $this, 'sanitize_checkbox' ], '0' ], // legacy - kept for back-compat
'admbud_smtp_disable_ssl_verify'=> [ 'boolean', [ $this, 'sanitize_checkbox' ], '0' ],
'admbud_smtp_preset' => [ 'string', 'sanitize_key', 'custom' ],
];
foreach ( $smtp_opts as $key => [ $type, $cb, $default ] ) {
register_setting( 'admbud_smtp_group', $key, [
'type' => $type, 'sanitize_callback' => $cb, 'default' => $default,
] );
}
// Password is handled separately - encrypted before storage.
add_filter( 'pre_update_option_ab_smtp_group', [ $this, 'maybe_encrypt_smtp_password' ] );
}
// ============================================================================
// ASSETS
// ============================================================================
public function enqueue_assets( string $hook ): void {
// Hook suffix follows the menu slug ('admbud'), not the plugin folder name.
if ( ! in_array( $hook, [ 'settings_page_admbud', 'toplevel_page_admbud' ], true ) ) return;
// Dev cache-buster: in non-production envs, suffix the plugin
// version with a timestamp so every page load fetches fresh
// CSS/JS. Edits to admin.css / ab-dropdown.js / etc. show up
// without needing a plugin-version bump (the user opposes per-fix
// bumps). Production envs keep the canonical ADMBUD_VERSION so
// HTTP caches + CDNs can cache normally.
// Gate covers both classic `WP_DEBUG` and modern `wp_get_environment_type()`.
$v = ADMBUD_VERSION;
$is_dev = ( defined( 'WP_DEBUG' ) && WP_DEBUG );
if ( ! $is_dev && function_exists( 'wp_get_environment_type' ) ) {
$env = wp_get_environment_type();
$is_dev = ( $env === 'local' || $env === 'development' );
}
if ( $is_dev ) {
$v .= '.' . time();
}
$url = ADMBUD_URL . 'assets/';
$js_url = ADMBUD_URL . 'assets/js/';
// Phase 1.1.0: minified files removed - all assets are unminified source.
// WordPress caching / CDN handles production optimisation.
$min_js = '';
$min_css = '';
// Tokens must load before component CSS.
wp_enqueue_style( 'admbud-tokens', $url . 'tokens.css', [], $v );
// The WP-admin-scheme accent override (used when the Colours module is
// OFF) is attached to `admbud-tokens` centrally by attach_scheme_accent()
// - see that method. It applies on every AB surface, not just this
// settings page, so off-canvas panels match the admin scheme too.
// Core shared CSS - always loaded.
wp_enqueue_style( 'admbud-core', $url . 'admin.css', [ 'admbud-tokens' ], $v );
wp_enqueue_style( 'admbud-dropdown', $url . "ab-dropdown{$min_css}.css", [ 'admbud-core' ], $v );
wp_enqueue_style( 'admbud-datepicker', $url . "ab-datepicker{$min_css}.css", [ 'admbud-core' ], $v );
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_script( 'admbud-dropdown', $js_url . 'ab-dropdown.js', [], $v, true );
wp_enqueue_script( 'admbud-datepicker', $js_url . 'ab-datepicker.js', [], $v, true );
wp_enqueue_media();
// Core shared JS - Vanilla ES6+, no jQuery dependency.
wp_enqueue_script( 'admbud-core', $js_url . 'admin.js', [ 'wp-color-picker', 'admbud-dropdown' ], $v, true );
// Tab-specific assets - only loaded on the relevant tab.
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'adminui'; // phpcs:ignore WordPress.Security.NonceVerification
switch ( $active_tab ) {
case 'smtp':
wp_enqueue_style( 'admbud-tab-smtp', $url . "tab-smtp{$min_css}.css", [ 'admbud-core' ], $v );
wp_enqueue_script( 'admbud-tab-smtp', $js_url . 'tab-smtp.js', [ 'admbud-core' ], $v, true );
break;
case 'roles':
wp_enqueue_style( 'admbud-tab-roles', $url . "tab-roles{$min_css}.css", [ 'admbud-core' ], $v );
wp_enqueue_script( 'admbud-tab-roles', $js_url . 'tab-roles.js', [ 'admbud-core' ], $v, true );
break;
case 'quick-settings':
wp_enqueue_script( 'admbud-tab-quick-settings', $js_url . 'tab-quick-settings.js', [ 'admbud-core' ], $v, true );
wp_localize_script( 'admbud-tab-quick-settings', 'admbudQs', [
'nonce' => wp_create_nonce( 'admbud_qs_nonce' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'i18n' => [
'enabled' => __( 'Setting enabled.', 'admin-buddy' ),
'disabled' => __( 'Setting disabled.', 'admin-buddy' ),
'failed' => __( 'Failed to save. Please try again.', 'admin-buddy' ),
'autoEnabled' => __( "Sidebar User Menu turned on so you can still navigate. Toggle off below if you don't want it.", 'admin-buddy' ),
'rolesSaved' => __( 'Roles saved.', 'admin-buddy' ),
'rolesFailed' => __( 'Failed to save roles.', 'admin-buddy' ),
'allEnabled' => __( 'All settings enabled.', 'admin-buddy' ),
'allDisabled' => __( 'All settings disabled.', 'admin-buddy' ),
'confirmEnableTitle' => __( 'Enable all Quick Settings?', 'admin-buddy' ),
'confirmEnableBody' => __( 'All 16 settings will be turned on. You can disable individual ones afterwards.', 'admin-buddy' ),
'confirmEnableYes' => __( 'Yes, enable all', 'admin-buddy' ),
'confirmDisableTitle' => __( 'Disable all Quick Settings?', 'admin-buddy' ),
'confirmDisableBody' => __( 'All 16 settings will be turned off.', 'admin-buddy' ),
'confirmDisableYes' => __( 'Yes, disable all', 'admin-buddy' ),
],
] );
break;
case 'modules':
wp_enqueue_script( 'wp-element' );
wp_enqueue_script(
'admbud-setup-modules',
$js_url . 'setup-modules.js',
[ 'admbud-core', 'wp-element' ],
$v,
true
);
$manageable = $this->get_manageable_tabs();
$enabled = $this->get_enabled_tabs();
$modules_data = [];
foreach ( $manageable as $slug => $tab ) {
$modules_data[] = [
'slug' => $slug,
'label' => $tab['label'],
'icon' => $tab['icon'],
'group' => $tab['group'] ?? 'interface',
'enabled' => in_array( $slug, $enabled, true ),
'always_on' => false,
];
}
// Use wp_add_inline_script + wp_json_encode to preserve boolean types.
// wp_localize_script stringifies booleans (false→"", true→"1") which
// breaks strict equality checks in nested data structures.
$setup_data = [
'modules' => $modules_data,
'nonce' => wp_create_nonce( 'admbud_modules_toggle' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'maintenanceMode' => admbud_get_option( 'admbud_maintenance_mode', 'off' ),
'maintenanceUrl' => admin_url( 'admin.php?page=admbud&tab=maintenance' ),
];
wp_add_inline_script(
'admbud-setup-modules',
'var admbudSetupData=' . wp_json_encode( $setup_data ) . ';',
'before'
);
$icons_map = [];
foreach ( $manageable as $slug => $tab ) {
$icons_map[ $slug ] = $tab['icon'];
}
wp_localize_script( 'admbud-setup-modules', 'admbudIcons', $icons_map );
break;
}
// Shared localised strings - passed to admbud-core but also available to tab scripts.
wp_localize_script( 'admbud-core', 'admbudSettings', [
'chooseImageTitle' => __( 'Choose Image', 'admin-buddy' ),
'useImageText' => __( 'Use this image', 'admin-buddy' ),
'copiedText' => __( '✓ Copied', 'admin-buddy' ),
'settingsSaved' => __( 'Settings saved.', 'admin-buddy' ),
'formSaveConfirmTitle' => __( 'Save changes?', 'admin-buddy' ),
'formSaveConfirmBody' => __( 'Your settings will be saved.', 'admin-buddy' ),
'regenerateConfirm' => __( 'This will invalidate the current emergency URL. Continue?', 'admin-buddy' ),
'regeneratingText' => __( 'Regenerating…', 'admin-buddy' ),
'regenerateText' => __( 'Regenerate', 'admin-buddy' ),
'regeneratedText' => __( '✓ Token regenerated', 'admin-buddy' ),
'errorText' => __( '✗ Something went wrong.', 'admin-buddy' ),
// Primary colour - used by login preview button when #admbud_colours_primary input is not on the page.
'primaryColour' => admbud_get_option( 'admbud_colours_primary', \Admbud\Colours::DEFAULT_PRIMARY ),
// Import confirmation modal strings.
'importConfirmTitle' => __( 'Import Settings?', 'admin-buddy' ),
'importConfirmBody' => __( 'This will replace all current settings with the contents of the uploaded file. This cannot be undone.', 'admin-buddy' ),
// Unsaved changes warning.
'unsavedTitle' => __( 'Unsaved changes', 'admin-buddy' ),
'unsavedBody' => __( 'You have unsaved changes that will be lost if you leave this tab.', 'admin-buddy' ),
'unsavedLeave' => __( 'Leave', 'admin-buddy' ),
// SMTP.
'smtpTesting' => __( 'Sending…', 'admin-buddy' ),
'smtpClearConfirmTitle' => __( 'Clear Email Log?', 'admin-buddy' ),
'smtpClearConfirmBody' => __( 'All logged entries will be permanently deleted.', 'admin-buddy' ),
// Roles.
'roleSaving' => __( 'Saving…', 'admin-buddy' ),
'roleSaved' => __( 'Saved.', 'admin-buddy' ),
'roleSaveConfirmTitle' => __( 'Save capabilities?', 'admin-buddy' ),
'roleDeleteConfirmTitle' => __( 'Delete Role?', 'admin-buddy' ),
'roleDeleteConfirmBody' => __( 'Users with this role will be moved to Subscriber. This cannot be undone.', 'admin-buddy' ),
'roleResetConfirmTitle' => __( 'Reset Role?', 'admin-buddy' ),
'roleResetConfirmBody' => __( 'This will restore the default capabilities for this role.', 'admin-buddy' ),
// Menu.
'menuSaving' => __( 'Saving…', 'admin-buddy' ),
'menuSaved' => __( 'Saved.', 'admin-buddy' ),
// Custom Pages.
'newPage' => __( 'New Page', 'admin-buddy' ),
'editPage' => __( 'Edit Page', 'admin-buddy' ),
'pageSaved' => __( 'Page saved.', 'admin-buddy' ),
'pageDeleted' => __( 'Page deleted.', 'admin-buddy' ),
'pageEnabled' => __( 'Page enabled.', 'admin-buddy' ),
'pageDisabled' => __( 'Page disabled.', 'admin-buddy' ),
'titleRequired' => __( 'Page title is required.', 'admin-buddy' ),
'deletePage' => __( 'Delete page?', 'admin-buddy' ),
/* translators: %s: page title */
'deletePageBody' => __( 'Delete "%s"? This cannot be undone.', 'admin-buddy' ),
// Theme URLs - used by Bricks integration modal redirects.
'themeInstallUrl' => esc_url( admin_url( 'theme-install.php?browse=popular' ) ),
'themesUrl' => esc_url( admin_url( 'themes.php' ) ),
// -- Toast / notification strings used across all tab JS files --
'importSuccess' => __( 'Settings imported successfully.', 'admin-buddy' ),
'resetSuccess' => __( 'Settings reset to defaults.', 'admin-buddy' ),
'importEmpty' => __( 'Import failed: no file selected.', 'admin-buddy' ),
'importBadFile' => __( 'Import failed: invalid file.', 'admin-buddy' ),
'saveFailed' => __( 'Save failed.', 'admin-buddy' ),
'requestFailed' => __( 'Request failed.', 'admin-buddy' ),
'toggleFailed' => __( 'Toggle failed.', 'admin-buddy' ),
'uploadFailed' => __( 'Upload failed.', 'admin-buddy' ),
'iconUploaded' => __( 'Icon uploaded.', 'admin-buddy' ),
'iconRenamed' => __( 'Icon renamed.', 'admin-buddy' ),
'colourSettingSaved' => __( 'Colour setting saved.', 'admin-buddy' ),
'slugCopied' => __( 'Slug copied.', 'admin-buddy' ),
'onlySvgAllowed' => __( 'Only SVG files are supported.', 'admin-buddy' ),
'roleCreated' => __( 'Role created.', 'admin-buddy' ),
'roleDeleted' => __( 'Role deleted.', 'admin-buddy' ),
'roleRenamed' => __( 'Role renamed.', 'admin-buddy' ),
'roleDefaultsRestored' => __( 'Defaults restored.', 'admin-buddy' ),
'menuSaveConfirmTitle' => __( 'Save menu configuration?', 'admin-buddy' ),
/* translators: %s: role name */
'menuSaveConfirmBody' => __( 'Save settings for "%s".', 'admin-buddy' ),
'menuResetConfirmTitle' => __( 'Reset to defaults?', 'admin-buddy' ),
'paletteGenerated' => __( 'Palette generated.', 'admin-buddy' ),
'preserveColours' => __( 'Preserve colours', 'admin-buddy' ),
'copySlug' => __( 'Copy', 'admin-buddy' ),
'deleteIcon' => __( 'Delete', 'admin-buddy' ),
'clickToRename' => __( 'Click to rename', 'admin-buddy' ),
'saveChanges' => __( 'Save Changes', 'admin-buddy' ),
'removeWidget' => __( 'Remove widget?', 'admin-buddy' ),
'removeWidgetBody' => __( 'This widget will be removed. Save to apply.', 'admin-buddy' ),
// Setup modules.
'enableAllModules' => __( 'Enable all', 'admin-buddy' ),
'disableAllModules' => __( 'Disable all', 'admin-buddy' ),
'enableModulesConfirm' => __( 'All modules in this group will be enabled.', 'admin-buddy' ),
'disableModulesConfirm' => __( 'All modules will be removed from the navigation.', 'admin-buddy' ),
'moduleAlwaysOn' => __( 'This module cannot be disabled', 'admin-buddy' ),
'disableModule' => __( 'Disable', 'admin-buddy' ),
'enableModule' => __( 'Enable', 'admin-buddy' ),
'maintenanceWarning' => __( 'Turn it off in the Maintenance tab first, then you can disable this module.', 'admin-buddy' ),
'goToMaintenance' => __( 'Go to Maintenance', 'admin-buddy' ),
// Group labels.
'groupInterface' => __( 'Interface', 'admin-buddy' ),
'groupUtilities' => __( 'Utilities', 'admin-buddy' ),
'groupIntegrations' => __( 'Integrations', 'admin-buddy' ),
// Quick Settings AJAX nonce.
'qsNonce' => wp_create_nonce( 'admbud_qs_nonce' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
// General-purpose nonce for nav reorder etc.
'admbudNonce' => wp_create_nonce( 'admbud_nonce' ),
] );
}
public function action_links( array $links ): array {
array_unshift( $links, sprintf(
'%s',
esc_url( admin_url( 'admin.php?page=admbud' ) ),
esc_html__( 'Settings', 'admin-buddy' )
) );
return $links;
}
/**
* Add Subresource Integrity (SRI) attributes to vendor scripts.
* Protects against CDN/supply-chain tampering (I-2 hardening).
*
* @param string $tag HTML script tag.
* @param string $handle Script handle name.
* @return string Modified tag with integrity + crossorigin attributes.
*/
public function add_sri_attributes( string $tag, string $handle ): string {
$sri_map = [
];
if ( isset( $sri_map[ $handle ] ) && strpos( $tag, 'integrity=' ) === false ) {
$tag = str_replace(
' src=',
' integrity="' . esc_attr( $sri_map[ $handle ] ) . '" crossorigin="anonymous" src=',
$tag
);
}
return $tag;
}
// ============================================================================
// TOOLS ACTIONS (export / import / reset variants)
// ============================================================================
// ============================================================================
// PAGE RENDER
// ============================================================================
/**
* Redirect to the last visited tab+subtab if arriving at admin-buddy without ?tab=.
* Runs on admin_init (priority 1) - before any output so headers can be sent.
*/
public function maybe_redirect_to_last_tab(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ( $_GET['page'] ?? '' ) !== 'admin-buddy' ) { return; } // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['tab'] ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput,WordPress.Security.NonceVerification
// Don't redirect if there's a POST action (reset/import/export)
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_POST['admbud_action'] ) || ! empty( $_GET['admbud_action'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput
// Don't redirect after a save/reset/import (notice in URL)
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['admbud_notice'] ) || ! empty( $_GET['settings-updated'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput
if ( empty( $_COOKIE['admbud_last_tab'] ) ) { return; }
$saved_tab = sanitize_key( wp_unslash( $_COOKIE['admbud_last_tab'] ) );
if ( ! $saved_tab ) { return; }
$url = admin_url( 'admin.php?page=admbud&tab=' . $saved_tab );
$sub_cookie = 'admbud_subtab_' . $saved_tab;
if ( ! empty( $_COOKIE[ $sub_cookie ] ) ) {
$url = add_query_arg( 'admbud_subtab', sanitize_key( wp_unslash( $_COOKIE[ $sub_cookie ] ) ), $url );
}
wp_safe_redirect( $url );
exit;
}
/**
* Read the active subtab from GET params or cookie (set by JS).
* Allows PHP to render the correct pane on first paint, eliminating flicker.
*/
/**
* Return all registered tab slugs → labels.
* Used by Search to build the dynamic index without duplicating the tab list.
*/
public static function get_all_tab_labels(): array {
return [
'adminui' => __( 'White Label', 'admin-buddy' ),
'colours' => __( 'Colours', 'admin-buddy' ),
'login' => __( 'Login', 'admin-buddy' ),
'maintenance' => __( 'Maintenance', 'admin-buddy' ),
'smtp' => __( 'SMTP', 'admin-buddy' ),
'roles' => __( 'User Roles', 'admin-buddy' ),
'modules' => __( 'Modules', 'admin-buddy' ),
'plugin-data' => __( 'Plugin Data', 'admin-buddy' ),
'quick-settings' => __( 'Quick Settings', 'admin-buddy' ),
];
}
public static function get_active_subtab( string $tab_slug, string $default ): string {
// Read-only navigation state: returns which subtab to render. No state
// change, no database writes, no nonce required. The value is
// sanitize_key()'d before returning. Cookie fallback persists the user's
// last-viewed subtab between requests.
if ( ! empty( $_GET['admbud_subtab'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput
return sanitize_key( wp_unslash( $_GET['admbud_subtab'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
$cookie_key = 'admbud_subtab_' . $tab_slug;
if ( ! empty( $_COOKIE[ $cookie_key ] ) ) {
return sanitize_key( wp_unslash( $_COOKIE[ $cookie_key ] ) );
}
return $default;
}
/**
* Enqueue assets that must be available on EVERY admin page, not just the AB settings page.
* - admbud-icon-inject (script): provides window.AdmbudIcon.injectSidebarIcons(),
* called by inline scripts that each module appends via wp_add_inline_script().
* - admbud-icon-inject (style): empty handle used as the attachment point for
* per-module inline CSS that targets the WP admin sidebar icons.
*/
public function enqueue_global_assets(): void {
wp_enqueue_script(
'admbud-icon-inject',
ADMBUD_URL . 'assets/js/ab-icon-inject.js',
[],
ADMBUD_VERSION,
true // footer
);
wp_register_style( 'admbud-icon-inject', false, [], ADMBUD_VERSION );
wp_enqueue_style( 'admbud-icon-inject' );
}
/**
* Attach the WP-admin-scheme accent override to the shared `admbud-tokens`
* stylesheet - once, regardless of which module enqueued it.
*
* Hooked late on both admin_enqueue_scripts and wp_enqueue_scripts so every
* AB surface picks up the accent from ONE source: the settings page, the
* Checklist panel on any admin page or the front-end admin bar, Pro Option
* Pages, the Command palette. Previously the accent was emitted only by
* enqueue_assets() and scoped to .ab-wrap, so panels rendered outside that
* wrapper fell back to the hardcoded Fresh blue.
*/
public function attach_scheme_accent(): void {
if ( ! wp_style_is( 'admbud-tokens', 'registered' ) ) {
return; // No AB surface loaded on this request.
}
$css = self::scheme_accent_css();
if ( '' !== $css ) {
wp_add_inline_style( 'admbud-tokens', $css );
}
}
/**
* True when the Colours module is active. It then owns every AB accent -
* admin chrome AND the Login / Maintenance pages - so the WP-scheme
* fallbacks in this group stand down.
*/
public static function colours_module_active(): bool {
$enabled = admbud_enabled_modules();
return is_array( $enabled ) && in_array( 'colours', $enabled, true );
}
/**
* Resolve the active WP admin colour scheme's accent as a hex string.
*
* Shared by scheme_accent_css() (admin chrome) and scheme_page_palette()
* (Login / Maintenance pages) - one resolver, no duplicated logic.
*
* On the front end admin_init never fires, so the colour schemes are not
* registered yet; we register them on demand. With no logged-in user
* get_user_option() returns false and we fall back to core's
* version-dependent default scheme.
*/
public static function scheme_accent_hex(): string {
global $_wp_admin_css_colors;
if ( empty( $_wp_admin_css_colors ) && function_exists( 'register_admin_color_schemes' ) ) {
register_admin_color_schemes();
}
// WP 7.0 made 'modern' the default admin colour scheme (it was 'fresh'
// on 6.x). Mirror core's version-dependent default for users who have
// never explicitly picked a scheme.
$default = version_compare( get_bloginfo( 'version' ), '7.0', '>=' ) ? 'modern' : 'fresh';
$scheme = get_user_option( 'admin_color' ) ?: $default;
// A scheme's `colors` array ends [..., accent, accent-light] - the
// accent is the second-to-last entry. The index is NOT fixed: classic
// 4-colour schemes (Fresh, Coffee, ...) carry it at [2], but WP 7.0's
// 3-colour 'modern' scheme carries it at [1]. count() - 2 resolves it
// for every registered scheme.
$colors = $_wp_admin_css_colors[ $scheme ]->colors ?? null;
if ( is_array( $colors ) && count( $colors ) >= 2 ) {
$hex = (string) $colors[ count( $colors ) - 2 ];
if ( preg_match( '/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/', $hex ) ) {
return $hex;
}
}
return '#2271b1'; // Fresh accent; hard fallback if the scheme is gone.
}
/**
* The `:root` custom-property block that points AB chrome at the WP admin
* scheme accent, used when the Colours module is OFF. Emitted at :root (not
* .ab-wrap) so off-canvas panels inherit it too. The derived accent vars
* are redeclared because tokens.css computes them at :root from
* var(--ab-primary, …) - overriding --ab-primary alone would not
* re-substitute the :root copies. See attach_scheme_accent().
*
* @return string CSS, or '' when the Colours module owns the accent.
*/
public static function scheme_accent_css(): string {
if ( self::colours_module_active() ) {
return '';
}
$a = esc_attr( self::scheme_accent_hex() );
return ':root{'
. '--ab-primary:' . $a . ';'
. '--ab-secondary:' . $a . ';'
. '--ab-accent:' . $a . ';'
. '--ab-accent-hover:color-mix(in srgb,' . $a . ' 85%,#000);'
. '--ab-accent-subtle:color-mix(in srgb,' . $a . ' 8%,transparent);'
. '--ab-border-focus:' . $a . ';'
. '}';
}
/**
* Dark, accent-tinted palette for the Login / Maintenance / Coming-Soon
* pages, derived from the WP admin scheme accent. Used when the Colours
* module is OFF so those public pages follow the admin scheme instead of
* the brand-violet defaults. ONE source, so both pages render identically.
*
* @return array{bg:string,grad_from:string,grad_to:string,heading:string,message:string}
*/
public static function scheme_page_palette(): array {
$a = self::scheme_accent_hex();
return [
'bg' => self::mix_hex( $a, '#0d0d14', 12 ), // near-black, accent-tinted
'grad_from' => self::mix_hex( $a, '#0d0d14', 24 ), // upper gradient stop
'grad_to' => self::mix_hex( $a, '#0d0d14', 12 ), // lower gradient stop
'heading' => self::mix_hex( $a, '#ffffff', 14 ), // near-white heading
'message' => self::mix_hex( $a, '#ffffff', 45 ), // softer body copy
];
}
/**
* Blend two hex colours in sRGB. $weight_a is the percentage (0-100) of
* $hex_a; the remainder is $hex_b. Returns a 6-digit hex (or $hex_a
* unchanged if either input is not parseable).
*/
public static function mix_hex( string $hex_a, string $hex_b, int $weight_a ): string {
$a = self::hex_to_rgb( $hex_a );
$b = self::hex_to_rgb( $hex_b );
if ( null === $a || null === $b ) {
return $hex_a;
}
$w = max( 0, min( 100, $weight_a ) ) / 100;
return sprintf(
'#%02x%02x%02x',
(int) round( $a[0] * $w + $b[0] * ( 1 - $w ) ),
(int) round( $a[1] * $w + $b[1] * ( 1 - $w ) ),
(int) round( $a[2] * $w + $b[2] * ( 1 - $w ) )
);
}
/**
* Pick a readable text colour (near-black or near-white) for content that
* sits on $hex, using perceived (YIQ) luminance.
*/
public static function contrast_text( string $hex ): string {
$rgb = self::hex_to_rgb( $hex );
if ( null === $rgb ) {
return '#1d2327';
}
$yiq = ( $rgb[0] * 299 + $rgb[1] * 587 + $rgb[2] * 114 ) / 1000;
return $yiq >= 150 ? '#1d2327' : '#f0f0f1';
}
/** Parse a 3- or 6-digit hex colour to an [r, g, b] array, or null. */
private static function hex_to_rgb( string $hex ): ?array {
$hex = ltrim( trim( $hex ), '#' );
if ( 3 === strlen( $hex ) ) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
if ( ! preg_match( '/^[A-Fa-f0-9]{6}$/', $hex ) ) {
return null;
}
return [
hexdec( substr( $hex, 0, 2 ) ),
hexdec( substr( $hex, 2, 2 ) ),
hexdec( substr( $hex, 4, 2 ) ),
];
}
public function render_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'admin-buddy' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'adminui'; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput
// Legacy tab slug redirects.
$legacy_map = [ 'core' => 'adminui', 'dashboard' => 'adminui', 'notices' => 'adminui', 'advanced' => 'plugin-data', 'tools' => 'plugin-data', 'datatools' => 'plugin-data', 'setup' => 'modules', 'tweaks' => 'quick-settings' ];
if ( isset( $legacy_map[ $active_tab ] ) ) {
$active_tab = $legacy_map[ $active_tab ];
// If redirected from tools, pre-select the tools subtab.
if ( ! isset( $_GET['admbud_subtab'] ) && ( $_GET['tab'] ?? '' ) === 'tools' ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput,WordPress.Security.NonceVerification
$_GET['admbud_subtab'] = 'tools'; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput
}
}
$tabs = [
'adminui' => [
'label' => __( 'White Label', 'admin-buddy' ),
'icon' => '',
],
'colours' => [
'label' => __( 'Colours', 'admin-buddy' ),
'icon' => '',
],
'login' => [
'label' => __( 'Login', 'admin-buddy' ),
'icon' => '',
],
'maintenance' => [
'label' => __( 'Maintenance', 'admin-buddy' ),
'icon' => '',
],
'smtp' => [
'label' => __( 'SMTP', 'admin-buddy' ),
'icon' => '',
],
'roles' => [
'label' => __( 'User Roles', 'admin-buddy' ),
'icon' => '',
'keys' => [ 'wp_user_roles' ],
'warn' => __( 'Importing roles will overwrite ALL role definitions. Use with care.', 'admin-buddy' ),
],
'modules' => [
'group' => 'manage',
'label' => __( 'Modules', 'admin-buddy' ),
'icon' => '',
],
'plugin-data' => [
'group' => 'manage',
'label' => __( 'Plugin Data', 'admin-buddy' ),
'icon' => '',
],
'quick-settings' => [
'group' => 'manage',
'label' => __( 'Quick Settings', 'admin-buddy' ),
'icon' => '',
],
];
// Only show the License tab when the licensing SDK is present.
if ( function_exists( 'admbud_fs' ) ) {
$tabs['license'] = [
'group' => 'standalone',
'label' => __( 'License', 'admin-buddy' ),
'icon' => '',
];
}
if ( ! array_key_exists( $active_tab, $tabs ) ) $active_tab = 'adminui';
// Apply module visibility from Modules tab.
// Standalone tabs (modules, plugin-data, license) and manage-group tabs (quick-settings, source)
// are always kept; module tabs are filtered by enabled modules.
$enabled_slugs = $this->get_enabled_tabs();
$always_keep = [ 'modules', 'plugin-data', 'license' ];
foreach ( array_keys( $tabs ) as $slug ) {
if ( in_array( $slug, $always_keep, true ) ) { continue; }
if ( ! in_array( $slug, $enabled_slugs, true ) ) {
unset( $tabs[ $slug ] );
}
}
// Ensure active_tab still exists after filter.
if ( ! array_key_exists( $active_tab, $tabs ) ) $active_tab = array_key_first( $tabs );
$maintenance_mode = admbud_get_option( 'admbud_maintenance_mode', 'off' );
// ab-loading hides the frame until JS confirms the correct tab - prevents flicker.
$loading_class = empty( $_GET['tab'] ) ? ' ab-loading' : ''; // phpcs:ignore WordPress.Security.NonceVerification
?>
/
inside `.wrap`. Our chrome renders its title as an SVG
* logo (no heading element), so without this marker the FS notice
* was being injected after the Welcome banner's
, landing in
* the middle of the banner. Emitting `.wp-header-end` here pins
* all notices to the very top of the page, above the topbar.
*/
?>
v
response (e.g. Hide Plugin/Theme Update Nag toggles).
$admbud_updates_core = [];
$admbud_updates_plugins = [];
$admbud_updates_themes = [];
if ( function_exists( 'get_core_updates' ) ) {
// get_core_updates() uses its own option, not affected by transient filters.
require_once ABSPATH . 'wp-admin/includes/update.php';
$core_updates = get_core_updates( [ 'dismissed' => false ] );
if ( is_array( $core_updates ) ) {
foreach ( $core_updates as $cu ) {
if ( isset( $cu->response ) && $cu->response === 'upgrade' ) {
$admbud_updates_core[] = [
'current' => get_bloginfo( 'version' ),
'new' => $cu->current ?? '',
'url' => admin_url( 'update-core.php' ),
];
}
}
}
}
// Read the plugin and theme update site-transients via the WordPress API.
// If the transient is missing or stale (>12h), trigger a fresh check first.
$raw_plugin_transient = get_site_transient( 'update_plugins' );
$raw_theme_transient = get_site_transient( 'update_themes' );
$stale_threshold = time() - ( 12 * HOUR_IN_SECONDS );
$plugins_stale = ! $raw_plugin_transient
|| ! is_object( $raw_plugin_transient )
|| empty( $raw_plugin_transient->last_checked )
|| $raw_plugin_transient->last_checked < $stale_threshold;
$themes_stale = ! $raw_theme_transient
|| ! is_object( $raw_theme_transient )
|| empty( $raw_theme_transient->last_checked )
|| $raw_theme_transient->last_checked < $stale_threshold;
if ( $plugins_stale || $themes_stale ) {
require_once ABSPATH . WPINC . '/update.php';
if ( $plugins_stale ) {
wp_update_plugins();
$raw_plugin_transient = get_site_transient( 'update_plugins' );
}
if ( $themes_stale ) {
wp_update_themes();
$raw_theme_transient = get_site_transient( 'update_themes' );
}
}
if ( $raw_plugin_transient && ! empty( $raw_plugin_transient->response ) ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
foreach ( $raw_plugin_transient->response as $plugin_file => $plugin_data ) {
if ( 0 !== validate_file( $plugin_file ) ) { continue; }
$plugin_info = $all_plugins[ $plugin_file ] ?? [];
$admbud_updates_plugins[] = [
'name' => $plugin_info['Name'] ?? $plugin_file,
'current' => $plugin_info['Version'] ?? '',
'new' => $plugin_data->new_version ?? '',
'url' => admin_url( 'plugins.php' ),
'details' => $plugin_data->url ?? '',
];
}
}
// Themes.
if ( $raw_theme_transient && ! empty( $raw_theme_transient->response ) ) {
foreach ( $raw_theme_transient->response as $theme_slug => $theme_data ) {
$theme = wp_get_theme( $theme_slug );
$admbud_updates_themes[] = [
'name' => $theme->get( 'Name' ) ?: $theme_slug,
'current' => $theme->get( 'Version' ) ?: '',
'new' => $theme_data['new_version'] ?? '',
'url' => admin_url( 'themes.php' ),
];
}
}
$admbud_update_count = count( $admbud_updates_core ) + count( $admbud_updates_plugins ) + count( $admbud_updates_themes );
?>
__( 'Maintenance', 'admin-buddy' ),
'class' => 'ab-badge--danger',
'tab' => 'maintenance',
'title' => __( 'Maintenance Mode is active: visitors see a maintenance page', 'admin-buddy' ),
'icon' => '',
];
}
// Coming Soon active
if ( $admbud_maint_mode === 'coming_soon' ) {
$status_indicators[] = [
'label' => __( 'Coming Soon', 'admin-buddy' ),
'class' => 'ab-badge--info',
'tab' => 'maintenance',
'title' => __( 'Coming Soon mode is active: visitors see a coming soon page', 'admin-buddy' ),
'icon' => '',
];
}
// Search Engine Visibility discouraged
if ( get_option( 'blog_public' ) === '0' ) {
$status_indicators[] = [
'label' => __( 'Noindex', 'admin-buddy' ),
'class' => 'ab-badge--warning',
'tab' => '',
'title' => __( 'Search engines are discouraged from indexing this site (Settings → Reading)', 'admin-buddy' ),
'icon' => '',
'url' => admin_url( 'options-reading.php' ),
];
}
// Checklist pill - always rendered. Opens the same
// off-canvas panel that the WP admin bar pill opens, via
// the data-ab-checklist-open hook in checklist-panel.js.
//
// Severity-aware rendering MUST mirror Checklist::add_bar_node()
// exactly so the two pills never disagree. Count shown reflects
// the worst tier only — critical first, warning only when no
// critical exists — so "Checklist (3)" means the same thing
// in both the WP admin bar and the AB topbar.
$admbud_checklist_counts = [ 'critical' => 0, 'warning' => 0 ];
if ( class_exists( '\\Admbud\\Checklist' ) ) {
$admbud_checklist_counts = \Admbud\Checklist::get_instance()->counts();
}
if ( $admbud_checklist_counts['critical'] > 0 ) {
$admbud_checklist_class = 'ab-badge--danger';
/* translators: %d: number of critical issues */
$admbud_checklist_label = sprintf( _n( 'Checklist (%d)', 'Checklist (%d)', $admbud_checklist_counts['critical'], 'admin-buddy' ), $admbud_checklist_counts['critical'] );
} elseif ( $admbud_checklist_counts['warning'] > 0 ) {
$admbud_checklist_class = 'ab-badge--warning';
/* translators: %d: number of warnings */
$admbud_checklist_label = sprintf( _n( 'Checklist (%d)', 'Checklist (%d)', $admbud_checklist_counts['warning'], 'admin-buddy' ), $admbud_checklist_counts['warning'] );
} else {
$admbud_checklist_class = 'ab-badge--success';
$admbud_checklist_label = __( 'Checklist', 'admin-buddy' );
}
// Checkmark-in-square icon for the Checklist badge.
$admbud_checklist_icon = '';
?>
WordPress →
→
→
render_tab_adminui(); break;
case 'colours': $this->render_tab_colours(); break;
case 'login': $this->render_tab_login(); break;
case 'maintenance': $this->render_tab_maintenance(); break;
case 'smtp': $this->render_tab_smtp(); break;
case 'roles': $this->render_tab_roles(); break;
case 'modules': $this->render_tab_modules(); break;
case 'plugin-data': $this->render_tab_plugin_data(); break;
case 'quick-settings': $this->render_tab_quick_settings(); break;
}
?>