PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | includes/class-metasync-admin-navigation.php +231 -127 2.6.17 → 2.7.2 View file →
@@ -20,8 +20,107 @@
20 20
21 21 const CACHE_KEY = 'metasync_admin_bar_status';
22 22
23 23 /**
24 + * Menu item keys that retain a WordPress admin sidebar row in short mode.
25 + *
26 + * Everything else is registered with an empty parent slug
27 + * (add_submenu_page('', ...)) so the page stays reachable at its
28 + * existing URL but does not claim a sidebar row. The in-page sidebar
29 + * (render_sidenav) continues to show every item.
30 + */
31 + const WP_MENU_KEEP_LIST = [
32 + 'dashboard', // Dashboard
33 + 'seo_controls', // Indexation Control
34 + 'redirections', // Redirections
35 + 'monitor_404', // 404 Monitor
36 + 'xml_sitemap', // XML Sitemap
37 + 'robots_txt', // Robots.txt
38 + 'schema_markup', // Schema Markup
39 + 'seo_health', // SEO Health
40 + 'general', // Settings
41 + ];
42 +
43 + /**
44 + * Whether the Settings page (general) is reachable for the current user.
45 + *
46 + * Settings is the floor — the top-level "Search Atlas" menu item is
47 + * registered with the Settings callback, so it must always point at a
48 + * page the user is permitted to see. When Settings is hidden by access
49 + * control, the top-level callback falls back to the first reachable
50 + * keep-list page (see resolve_top_level_callback()).
51 + *
52 + * @return bool
53 + */
54 + public static function is_settings_reachable()
55 + {
56 + return Metasync_Access_Control::user_can_access('hide_settings');
57 + }
58 +
59 + /**
60 + * Resolve the callback the top-level menu item should use.
61 + *
62 + * The top-level "Search Atlas" menu item is the floor — the plugin must
63 + * always present at least one working entry point. When Settings is
64 + * reachable it is the floor (its callback also renders the Settings
65 + * page). When Settings is hidden, fall back to the first reachable
66 + * keep-list page callback so the parent never leads to a permission
67 + * error or blank page.
68 + *
69 + * @param Metasync_Admin $admin The admin instance whose callbacks are used.
70 + * @return array WordPress-style callback array ([$admin, 'method']).
71 + */
72 + public static function resolve_top_level_callback($admin)
73 + {
74 + if (self::is_settings_reachable()) {
75 + return array($admin, 'create_admin_settings_page');
76 + }
77 +
78 + // Fall back to the first reachable keep-list item callback. The
79 + // keep-list is ordered by priority (Dashboard, Indexation Control,
80 + // Redirections, 404 Monitor, XML Sitemap, Robots.txt, SEO Health,
81 + // Settings). We reuse get_available_menu_items() so access-control
82 + // and connection-state gates are respected identically to the
83 + // sidebar.
84 + $available = self::instance()->get_available_menu_items();
85 + foreach (self::WP_MENU_KEEP_LIST as $key) {
86 + if (isset($available[$key])) {
87 + return array($admin, $available[$key]['callback']);
88 + }
89 + }
90 +
91 + // No keep-list item is reachable — keep Settings as the last resort
92 + // so the menu never leads to a blank page. Settings itself handles
93 + // its own access-control rendering.
94 + return array($admin, 'create_admin_settings_page');
95 + }
96 +
97 + /**
98 + * The ordered keep-list of menu item keys that always retain a
99 + * WordPress admin sidebar row (in short mode).
100 + *
101 + * @return string[]
102 + */
103 + public static function get_wp_menu_keep_list()
104 + {
105 + return self::WP_MENU_KEEP_LIST;
106 + }
107 +
108 + /**
109 + * Whether a given menu item key retains a WordPress admin sidebar row.
110 + *
111 + * Only keep-list items retain a WordPress sidebar row. All other pages
112 + * remain registered and available from the in-page navigation.
113 + *
114 + * @param string $item_key Menu item key from get_available_menu_items().
115 + * @return bool
116 + */
117 + public static function item_keeps_wp_sidebar_row($item_key)
118 + {
119 + return in_array($item_key, self::WP_MENU_KEEP_LIST, true);
120 + }
121 +
122 + /**
24 123 * Get singleton instance.
25 124 *
26 125 * @return self
27 126 */
@@ -167,9 +266,8 @@
167 266 $menu_items = [];
168 267 $menu_icons = [
169 268 'dashboard' => 'dashboard',
170 269 'seo_controls' => 'search',
171 - 'optimal_settings' => 'superhero-alt',
172 270 'instant_index' => 'performance',
173 271 'google_console' => 'chart-area',
174 272 'compatibility' => 'admin-tools',
175 273 'sync_log' => 'list-view',
@@ -187,17 +285,9 @@
187 285
188 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
189 287 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
190 288 }
191 -
192 - if ($general_options['enable_optimal_settings'] ?? false) {
193 - $menu_items['optimal_settings'] = ['title' => 'Optimal Settings', 'slug_suffix' => '-optimal-settings'];
194 - }
195 -
196 - if ($general_options['enable_googleinstantindex'] ?? false) {
197 - $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
198 - }
199 -
289 +
200 290 if ($general_options['enable_google_console'] ?? false) {
201 291 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
202 292 }
203 293
@@ -223,9 +313,9 @@
223 313 <div class="metasync-nav-tabs">
224 314 <div class="metasync-nav-left">
225 315 <?php foreach ($menu_items as $key => $menu_item):
226 316 $is_active = ($current_page === $key);
227 - $icon = $menu_icons[$key] ?? 'admin-generic';
317 + $icon = $menu_icons[$key];
228 318 $page_url = '?page=' . $page_slug . $menu_item['slug_suffix'];
229 319 ?>
230 320 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
231 321 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
@@ -412,20 +502,8 @@
412 502 'group' => 'seo'
413 503 ];
414 504 }
415 505
416 - // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
417 - $seo_controls = Metasync::get_option('seo_controls');
418 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
419 - $menu_items['instant_index'] = [
420 - 'title' => 'Instant Indexing',
421 - 'slug_suffix' => '-instant-index',
422 - 'callback' => 'create_admin_google_instant_index_page',
423 - 'internal_nav' => 'Instant Indexing',
424 - 'group' => 'seo'
425 - ];
426 - }
427 -
428 506 if ($general_options['enable_google_console'] ?? false) {
429 507 $menu_items['google_console'] = [
430 508 'title' => 'Google Console',
431 509 'slug_suffix' => '-google-console',
@@ -593,18 +671,8 @@
593 671 'group' => 'plugin'
594 672 ];
595 673 }
596 674
597 - if ($general_options['enable_optimal_settings'] ?? false) {
598 - $menu_items['optimal_settings'] = [
599 - 'title' => 'Optimal Settings',
600 - 'slug_suffix' => '-optimal-settings',
601 - 'callback' => 'create_admin_optimal_settings_page',
602 - 'internal_nav' => 'Optimal Settings',
603 - 'group' => 'plugin'
604 - ];
605 - }
606 -
607 675 // Compatibility page (check access control)
608 676 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
609 677 $menu_items['compatibility'] = [
610 678 'title' => 'Compatibility',
@@ -677,15 +745,19 @@
677 745 global $menu;
678 746 $menu['100'] = array('', 'read', 'separator-metasync-before', '', 'wp-menu-separator');
679 747 }, 5);
680 748
681 - // Main menu page at position 100.1 — bottom of the menu after all standard items
749 + // Main menu page at position 100.1 — bottom of the menu after all standard items.
750 + // The top-level callback is the floor: when Settings is reachable it renders
751 + // Settings; when Settings is hidden it falls back to the first reachable
752 + // keep-list page so the menu never leads to a blank page.
753 + $top_level_callback = self::resolve_top_level_callback($admin);
682 754 add_menu_page(
683 755 $menu_name,
684 756 $menu_title,
685 757 $menu_capability,
686 758 $menu_slug,
687 - array($admin, 'create_admin_settings_page'),
759 + $top_level_callback,
688 760 $menu_icon,
689 761 '100.1'
690 762 );
691 763
@@ -699,114 +771,123 @@
699 771
700 772 // ── Connect slug ──────────────────────────────────────────────────
701 773 $connect_slug = $menu_slug . '-connect';
702 774
775 + // Keep a slug-to-title map so hidden pages can still provide WordPress
776 + // with a browser-tab title when they are registered without a parent.
777 + $admin_page_titles = array();
778 +
779 + // Local helper: register a submenu page under the plugin parent slug
780 + // when the item keeps its WordPress admin sidebar row, or under an
781 + // empty parent slug (hidden from the sidebar but still reachable)
782 + // otherwise. The Settings page (general) is registered against the
783 + // bare $menu_slug so it doubles as the parent menu callback.
784 + $register_submenu = function($item_key, $page_title, $menu_title, $capability, $menu_page_slug, $callback) use ($menu_slug, &$admin_page_titles) {
785 + $admin_page_titles[$menu_page_slug] = $page_title;
786 + $parent = self::item_keeps_wp_sidebar_row($item_key) ? $menu_slug : '';
787 + add_submenu_page($parent, $page_title, $menu_title, $capability, $menu_page_slug, $callback);
788 + };
789 +
703 790 // Dashboard (check access control + "Hide Dashboard" general setting)
704 791 if (Metasync_Access_Control::user_can_access('hide_dashboard') && !self::is_dashboard_hidden_by_framework()) {
705 - add_submenu_page($menu_slug, 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
792 + $register_submenu('dashboard', 'Dashboard', 'Dashboard', $menu_capability, $menu_slug . '-dashboard', array($admin, 'create_admin_dashboard_iframe'));
706 793 }
707 794
708 795 // Indexation Control
709 796 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
710 - add_submenu_page($menu_slug, 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page'));
797 + $register_submenu('seo_controls', 'Indexation Control', 'Indexation Control', $menu_capability, $menu_slug . '-seo-controls', array($admin, 'create_admin_seo_controls_page'));
711 798 }
712 799
713 800 // 404 Monitor
714 - add_submenu_page($menu_slug, '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page'));
801 + $register_submenu('monitor_404', '404 Monitor', '404 Monitor', $menu_capability, $menu_slug . '-404-monitor', array($admin, 'create_admin_404_monitor_page'));
715 802
716 803 // Redirections
717 804 if (Metasync_Access_Control::user_can_access('hide_redirections')) {
718 - add_submenu_page($menu_slug, 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page'));
805 + $register_submenu('redirections', 'Redirections', 'Redirections', $menu_capability, $menu_slug . '-redirections', array($admin, 'create_admin_redirections_page'));
719 806 }
720 807
721 808 // XML Sitemap
722 809 if (Metasync_Access_Control::user_can_access('hide_xml_sitemap')) {
723 - add_submenu_page($menu_slug, 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page'));
810 + $register_submenu('xml_sitemap', 'XML Sitemap', 'XML Sitemap', $menu_capability, $menu_slug . '-xml-sitemap', array($admin, 'create_admin_xml_sitemap_page'));
724 811 }
725 812
726 813 // Robots.txt
727 814 if (Metasync_Access_Control::user_can_access('hide_robots')) {
728 - add_submenu_page($menu_slug, 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page'));
815 + $register_submenu('robots_txt', 'Robots.txt', 'Robots.txt', $menu_capability, $menu_slug . '-robots-txt', array($admin, 'create_admin_robots_txt_page'));
729 816 }
730 817
731 - // Site Verification
732 - add_submenu_page($menu_slug, 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page'));
818 + // Site Verification (in-page sidebar only in short mode)
819 + $register_submenu('site_verification', 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page'));
733 820
734 - // Instant Indexing (conditional)
735 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
736 - add_submenu_page($menu_slug, 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
737 - }
738 -
739 821 // Google Console (conditional)
740 822 if ($general_options['enable_google_console'] ?? false) {
741 - add_submenu_page($menu_slug, 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page'));
823 + $register_submenu('google_console', 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page'));
742 824 }
743 825
744 826 // Bing Console (conditional)
745 827 if ($seo_controls['enable_binginstantindex'] ?? false) {
746 - add_submenu_page($menu_slug, 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page'));
828 + $register_submenu('bing_console', 'Bing Console', 'Bing Console', $menu_capability, $menu_slug . '-bing-console', array($admin, 'create_admin_bing_console_page'));
747 829 }
748 830
749 831 // Schema Markup
750 - add_submenu_page($menu_slug, 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page'));
832 + $register_submenu('schema_markup', 'Schema Markup', 'Schema Markup', $menu_capability, $menu_slug . '-schema-markup', array($admin, 'create_admin_schema_markup_page'));
751 833
752 834 // Import SEO Data
753 835 if (Metasync_Access_Control::user_can_access('hide_import_seo')) {
754 - add_submenu_page($menu_slug, 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page'));
836 + $register_submenu('import_seo', 'Import SEO Data', 'Import SEO Data', $menu_capability, $menu_slug . '-import-external', array($admin, 'render_import_external_data_page'));
755 837 }
756 838
757 - // Settings
839 + // Settings — registered against the bare $menu_slug (parent menu floor).
840 + // It always keeps its sidebar row so the plugin always has at least
841 + // one working entry point (see is_settings_reachable()).
758 842 if (Metasync_Access_Control::user_can_access('hide_settings')) {
759 - add_submenu_page($menu_slug, 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page'));
843 + $register_submenu('general', 'Settings', 'Settings', $menu_capability, $menu_slug, array($admin, 'create_admin_settings_page'));
760 844 }
761 845
762 - // Optimal Settings (conditional)
763 - if ($general_options['enable_optimal_settings'] ?? false) {
764 - add_submenu_page($menu_slug, 'Optimal Settings', 'Optimal Settings', $menu_capability, $menu_slug . '-optimal-settings', array($admin, 'create_admin_optimal_settings_page'));
765 - }
846 + // Local Business (in-page sidebar only in short mode)
847 + $register_submenu('local_business', 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page'));
766 848
767 - // Local Business
768 - add_submenu_page($menu_slug, 'Local Business', 'Local Business', $menu_capability, $menu_slug . '-local-business', array($admin, 'create_admin_local_business_page'));
849 + // Breadcrumbs (in-page sidebar only in short mode)
850 + $register_submenu('breadcrumbs', 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page'));
769 851
770 - // Breadcrumbs
771 - add_submenu_page($menu_slug, 'Breadcrumbs', 'Breadcrumbs', $menu_capability, $menu_slug . '-breadcrumbs', array($admin, 'create_admin_breadcrumbs_page'));
852 + // Code Snippets (in-page sidebar only in short mode)
853 + $register_submenu('code_snippets', 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page'));
772 854
773 - // Code Snippets
774 - add_submenu_page($menu_slug, 'Code Snippets', 'Code Snippets', $menu_capability, $menu_slug . '-code-snippets', array($admin, 'create_admin_code_snippets_page'));
855 + // Code Minification (in-page sidebar only in short mode)
856 + $register_submenu('code_minification', 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page'));
775 857
776 - // Code Minification
777 - add_submenu_page($menu_slug, 'Code Minification', 'Code Minification', $menu_capability, $menu_slug . '-code-minification', array($admin, 'create_admin_code_minification_page'));
858 + // Media Optimization (in-page sidebar only in short mode)
859 + $register_submenu('media_optimization', 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_page'));
778 860
779 - // Media Optimization
780 - add_submenu_page($menu_slug, 'Media Optimization', 'Media Optimization', $menu_capability, $menu_slug . '-media-optimization', array($admin, 'create_admin_media_optimization_page'));
781 -
782 861 // Custom Pages
783 862 if (Metasync_Access_Control::user_can_access('hide_custom_pages')) {
784 - add_submenu_page($menu_slug, 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page'));
863 + $register_submenu('custom_pages', 'Custom Pages', 'Custom Pages', $menu_capability, $menu_slug . '-custom-pages', array($admin, 'create_admin_custom_pages_page'));
785 864 }
786 865
787 - // Bot Statistics
788 - add_submenu_page($menu_slug, 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page'));
866 + // Bot Statistics (in-page sidebar only in short mode)
867 + $register_submenu('bot_statistics', 'Bot Statistics', 'Bot Statistics', $menu_capability, $menu_slug . '-bot-statistics', array($admin, 'create_admin_bot_statistics_page'));
789 868
790 869 // SEO Health dashboard
791 - add_submenu_page($menu_slug, 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page'));
870 + $register_submenu('seo_health', 'SEO Health', 'SEO Health', $menu_capability, $menu_slug . '-seo-health', array($admin, 'create_admin_seo_health_page'));
792 871
793 872 // Compatibility
794 873 if (Metasync_Access_Control::user_can_access('hide_compatibility')) {
795 - add_submenu_page($menu_slug, 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page'));
874 + $register_submenu('compatibility', 'Compatibility', 'Compatibility', $menu_capability, $menu_slug . '-compatibility', array($admin, 'create_admin_compatibility_page'));
796 875 }
797 876
798 877 // Changes Log
799 878 if (Metasync_Access_Control::user_can_access('hide_sync_log')) {
800 - add_submenu_page($menu_slug, 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page'));
879 + $register_submenu('sync_log', 'Changes Log', 'Changes Log', $menu_capability, $menu_slug . '-sync-log', array($admin, 'create_admin_sync_log_page'));
801 880 }
802 881
803 882 // Report Issue
804 883 if (Metasync_Access_Control::user_can_access('hide_report_issue')) {
805 - add_submenu_page($menu_slug, 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page'));
884 + $register_submenu('report_issue', 'Report Issue', 'Report Issue', $menu_capability, $menu_slug . '-report-issue', array($admin, 'create_admin_report_issue_page'));
806 885 }
807 886
808 887 // ── Connect CTA (shown when not authenticated) ────────────────────
888 + // Kept as a sidebar row so an unconnected site always has a visible
889 + // entry point alongside the top-level menu item.
809 890 if (!$is_fully_connected) {
810 891 $connect_label = sprintf('Connect to %s', Metasync::get_effective_plugin_name());
811 892 add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
812 893 }
@@ -811,11 +892,36 @@
811 892 add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
812 893 }
813 894
814 895 // ── Hidden pages (no sidebar entry needed) ────────────────────────
896 + $admin_page_titles[$menu_slug . '-setup-wizard'] = 'Setup Wizard';
815 897 add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page'));
816 898
899 + // WordPress cannot resolve titles for pages registered under an empty
900 + // parent, even though those pages remain reachable by URL. Restore the
901 + // standard admin title format for our registered pages only.
902 + add_filter('admin_title', function($admin_title, $title) use (&$admin_page_titles) {
903 + $current_page = isset($_GET['page']) && is_string($_GET['page']) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page slug for the admin title
904 + ? sanitize_text_field(wp_unslash($_GET['page'])) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page slug for the admin title
905 + : '';
817 906
907 + if (empty($title) && isset($admin_page_titles[$current_page])) {
908 + $site_title = is_network_admin() || is_user_admin()
909 + ? get_network()->site_name
910 + : get_bloginfo('name');
911 +
912 + $admin_title = sprintf(
913 + /* translators: 1: Admin page title, 2: Site or network title. */
914 + __('%1$s &lsaquo; %2$s &#8212; WordPress', 'metasync'),
915 + $admin_page_titles[$current_page],
916 + $site_title
917 + );
918 + }
919 +
920 + return $admin_title;
921 + }, 10, 2);
922 +
923 +
818 924 // Rename auto-generated first submenu from plugin name to "Settings" and reorder
819 925 add_action('admin_menu', function() use ($menu_slug) {
820 926 global $submenu;
821 927 if (!isset($submenu[$menu_slug])) {
@@ -845,9 +951,8 @@
845 951 'dashboard' => 'dashboard',
846 952 'compatibility' => 'admin-tools',
847 953 'sync_log' => 'list-view',
848 954 'seo_controls' => 'search',
849 - 'optimal_settings' => 'superhero-alt',
850 955 'instant_index' => 'performance',
851 956 'google_console' => 'chart-area',
852 957 'bing_console' => 'chart-bar',
853 958 'redirections' => 'undo',
@@ -887,9 +992,9 @@
887 992 <?php
888 993 // Dashboard tab (standalone)
889 994 if (isset($seo_items['dashboard'])) {
890 995 $is_active = ($current_page === 'dashboard');
891 - $icon = $menu_icons['dashboard'] ?? 'admin-generic';
996 + $icon = $menu_icons['dashboard'];
892 997 $page_url = '?page=' . $page_slug . $seo_items['dashboard']['slug_suffix'];
893 998 ?>
894 999 <a href="<?php echo esc_url($page_url); ?>" class="metasync-nav-tab <?php echo $is_active ? 'active' : ''; ?>">
895 1000 <span class="tab-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
@@ -1270,9 +1375,9 @@
1270 1375 $status_title_header = 'Not Synced - Heartbeat API not responding or unreachable';
1271 1376 $status_text_header = 'Not Synced';
1272 1377 }
1273 1378 ?>
1274 - <div class="metasync-integration-status <?php echo $status_class_header; ?>"
1379 + <div class="metasync-integration-status <?php echo esc_attr($status_class_header); ?>"
1275 1380 title="<?php echo esc_attr($status_title_header); ?>">
1276 1381 <span class="status-indicator"></span>
1277 1382 <span class="status-text"><?php echo esc_html($status_text_header); ?></span>
1278 1383 </div>
@@ -1352,10 +1457,10 @@
1352 1457
1353 1458 <?php
1354 1459 $page_slug = Metasync_Admin::$page_slug;
1355 1460 $is_plugin_page = (
1356 - (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) ||
1357 - (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false)
1461 + (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for an inline config script
1462 + (isset($_GET['page']) && strpos($_GET['page'], 'searchatlas') !== false) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for an inline config script
1358 1463 );
1359 1464 if ($is_plugin_page):
1360 1465 ?>
1361 1466 <script type="text/javascript">
@@ -1532,9 +1637,9 @@
1532 1637 * @param string $description Optional subtitle shown below the title.
1533 1638 */
1534 1639 public function render_layout_open($page_title = '', $current_page = '', $description = '')
1535 1640 {
1536 - $theme = esc_attr(get_option('metasync_theme', 'dark'));
1641 + $theme = get_option('metasync_theme', 'dark');
1537 1642 $plugin_name = Metasync::get_effective_plugin_name();
1538 1643 $general = Metasync::get_option('general') ?? [];
1539 1644 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1540 1645 // badge reflects confirmed heartbeat health (connected / stale /
@@ -1543,9 +1648,9 @@
1543 1648 $logo = $this->resolve_logo_data();
1544 1649
1545 1650 $current_theme = get_option('metasync_theme', 'dark');
1546 1651 ?>
1547 - <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo $theme; ?>">
1652 + <div class="wrap metasync-dashboard-wrap" data-theme="<?php echo esc_attr($theme); ?>">
1548 1653
1549 1654 <?php
1550 1655 // Inject whitelabel color palette overrides
1551 1656 $wl_settings = Metasync::get_whitelabel_settings();
@@ -1575,8 +1680,9 @@
1575 1680 $vars .= '--' . esc_attr($grad_var) . ':linear-gradient(135deg,' . esc_attr($from) . ' 0%,' . esc_attr($to) . ' 100%);';
1576 1681 }
1577 1682 }
1578 1683 if ($vars) {
1684 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSS custom properties inside a raw <style> element; every value is a regex-validated hex color and HTML entities are not decoded in raw text elements
1579 1685 echo $selector . '{' . $vars . '}';
1580 1686 }
1581 1687 }
1582 1688 }
@@ -1651,15 +1757,19 @@
1651 1757 {
1652 1758 ?>
1653 1759 </main><!-- /.metasync-layout-main -->
1654 1760
1655 - <?php if ($show_promo):
1656 - $general = Metasync::get_option('general') ?? [];
1657 - $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1761 + <?php
1762 + // The sidebar exists to get an unconnected site connected. Once the
1763 + // site is linked it has nothing further to offer, so the whole
1764 + // column is dropped and the grid reflows to two columns.
1765 + $show_promo = $show_promo && ! Metasync_Heartbeat_Manager::instance()
1766 + ->is_heartbeat_connected(Metasync::get_option('general') ?? []);
1767 + if ($show_promo):
1658 1768 ?>
1659 1769 <!-- Right promo sidebar -->
1660 1770 <aside class="metasync-layout-promo">
1661 - <?php $this->render_promo_sidebar($is_connected); ?>
1771 + <?php $this->render_promo_sidebar(); ?>
1662 1772 </aside>
1663 1773 <?php endif; ?>
1664 1774
1665 1775 </div><!-- /.metasync-layout -->
@@ -1696,9 +1806,8 @@
1696 1806 'breadcrumbs' => 'menu',
1697 1807 'code_snippets' => 'editor-code',
1698 1808 'code_minification'=> 'media-code',
1699 1809 'custom_pages' => 'admin-page',
1700 - 'optimal_settings' => 'superhero-alt',
1701 1810 'compatibility' => 'admin-tools',
1702 1811 'sync_log' => 'list-view',
1703 1812 'bot_statistics' => 'visibility',
1704 1813 'report_issue' => 'sos',
@@ -1714,11 +1823,20 @@
1714 1823 } else {
1715 1824 $plugin_items[$key] = $item;
1716 1825 }
1717 1826 }
1827 +
1828 + $total_items = count($seo_items) + count($plugin_items);
1829 +
1718 1830 ?>
1719 1831 <nav class="metasync-sidenav">
1720 1832
1833 + <?php if ($total_items === 0): ?>
1834 + <div class="metasync-sidenav-empty">
1835 + <?php esc_html_e('No pages are available.', 'metasync'); ?>
1836 + </div>
1837 + <?php else: ?>
1838 +
1721 1839 <!-- SEO Features group -->
1722 1840 <div class="metasync-sidenav-group">
1723 1841 <div class="metasync-sidenav-group-title">SEO Features</div>
1724 1842 <ul>
@@ -1724,12 +1842,12 @@
1724 1842 <ul>
1725 1843 <?php foreach ($seo_items as $key => $item):
1726 1844 $is_active = ($current_page === $key);
1727 1845 $icon = $icons[$key] ?? 'admin-generic';
1728 - $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1846 + $url = admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']);
1729 1847 ?>
1730 1848 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1731 - <a href="<?php echo $url; ?>">
1849 + <a href="<?php echo esc_url($url); ?>">
1732 1850 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1733 1851 <?php echo esc_html($item['title']); ?>
1734 1852 </a>
1735 1853 </li>
@@ -1744,12 +1862,12 @@
1744 1862 <?php foreach ($plugin_items as $key => $item):
1745 1863 if ($key === 'report_issue') continue;
1746 1864 $is_active = ($current_page === $key);
1747 1865 $icon = $icons[$key] ?? 'admin-generic';
1748 - $url = esc_url(admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']));
1866 + $url = admin_url('admin.php?page=' . $page_slug . $item['slug_suffix']);
1749 1867 ?>
1750 1868 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1751 - <a href="<?php echo $url; ?>">
1869 + <a href="<?php echo esc_url($url); ?>">
1752 1870 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1753 1871 <?php echo esc_html($item['title']); ?>
1754 1872 </a>
1755 1873 </li>
@@ -1784,8 +1902,10 @@
1784 1902 </ul>
1785 1903 </div>
1786 1904 <?php endif; ?>
1787 1905
1906 + <?php endif; ?>
1907 +
1788 1908 </nav>
1789 1909 <?php
1790 1910 }
1791 1911
@@ -1791,17 +1911,22 @@
1791 1911
1792 1912 /**
1793 1913 * Render the right promotional sidebar.
1794 1914 *
1795 - * @param bool $is_connected Whether the plugin is authenticated.
1915 + * Only reached while the site is unconnected; the caller drops the
1916 + * whole column once the plugin is linked.
1796 1917 */
1797 - public function render_promo_sidebar($is_connected = false)
1918 + public function render_promo_sidebar()
1798 1919 {
1799 1920 $plugin_name = Metasync::get_effective_plugin_name();
1800 1921 $otto_name = Metasync::get_whitelabel_otto_name();
1801 - $settings_url = esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug));
1922 + $settings_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug);
1802 1923 $homepage = Metasync::HOMEPAGE_DOMAIN;
1803 - $is_default_brand = ( $plugin_name === 'Search Atlas' );
1924 + $docs_url = Metasync::DOCUMENTATION_DOMAIN;
1925 + // These links point at Search Atlas properties, so they only belong
1926 + // here while the plugin runs under its own name AND branding — a
1927 + // reseller can leave the name untouched and still be whitelabeled.
1928 + $is_default_brand = ( $plugin_name === 'Search Atlas' ) && ! Metasync::is_whitelabel_enabled();
1804 1929 $whitelabel = Metasync::get_whitelabel_settings();
1805 1930 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1806 1931 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1807 1932 : [];
@@ -1806,9 +1931,8 @@
1806 1931 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1807 1932 : [];
1808 1933 ?>
1809 1934
1810 - <?php if (!$is_connected): ?>
1811 1935 <!-- Connect CTA card -->
1812 1936 <div class="metasync-promo-card metasync-promo-card--connect">
1813 1937 <div class="metasync-promo-card-header">
1814 1938 <div class="metasync-promo-card-icon">
@@ -1824,40 +1948,12 @@
1824 1948 <li><span class="promo-check">&#10003;</span> Real-time keyword tracking</li>
1825 1949 <li><span class="promo-check">&#10003;</span> Automated schema markup</li>
1826 1950 <li><span class="promo-check">&#10003;</span> Instant Google indexing</li>
1827 1951 </ul>
1828 - <a href="<?php echo $settings_url; ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1952 + <a href="<?php echo esc_url($settings_url); ?>" class="metasync-promo-btn metasync-promo-btn--primary">
1829 1953 Connect Now
1830 1954 </a>
1831 1955 </div>
1832 - <?php else: ?>
1833 - <!-- Connected — feature highlights -->
1834 - <div class="metasync-promo-card metasync-promo-card--accent">
1835 - <div class="metasync-promo-card-header">
1836 - <div class="metasync-promo-card-icon">
1837 - <span class="dashicons dashicons-performance"></span>
1838 - </div>
1839 - <div>
1840 - <h3><?php echo esc_html($otto_name); ?> Active</h3>
1841 - </div>
1842 - </div>
1843 - <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1844 - <ul class="metasync-promo-benefits">
1845 - <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1846 - <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1847 - <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1848 - </ul>
1849 - <?php if ($is_default_brand): ?>
1850 - <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1851 - View <?php echo esc_html($plugin_name); ?> Dashboard
1852 - </a>
1853 - <?php elseif (!empty($whitelabel['domain'])): ?>
1854 - <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1855 - View <?php echo esc_html($plugin_name); ?> Dashboard
1856 - </a>
1857 - <?php endif; ?>
1858 - </div>
1859 - <?php endif; ?>
1860 1956
1861 1957 <?php
1862 1958 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1863 1959 $show_quick_links = $is_default_brand || !empty($custom_links);
@@ -1867,12 +1963,20 @@
1867 1963 <div class="metasync-promo-card">
1868 1964 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1869 1965 <ul class="metasync-promo-links">
1870 1966 <?php if ($is_default_brand): ?>
1967 + <li><a href="<?php echo esc_url($docs_url); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-book-alt"></span> Documentation</a></li>
1871 1968 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1872 - <li><a href="<?php echo esc_url($homepage . '/academy/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-welcome-learn-more"></span> SEO Academy</a></li>
1969 + <li><a href="<?php echo esc_url($homepage . '/contact/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-email-alt"></span> Contact Support</a></li>
1873 1970 <li><a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-setup-wizard')); ?>"><span class="dashicons dashicons-admin-customizer"></span> Setup Wizard</a></li>
1971 + <?php
1972 + // Mirrors the gate that decides whether the page is registered
1973 + // at all. Without it the link outlives the page for any user
1974 + // the access rules hide it from, and leads nowhere.
1975 + if (Metasync_Access_Control::user_can_access('hide_report_issue')):
1976 + ?>
1874 1977 <li><a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-report-issue')); ?>"><span class="dashicons dashicons-sos"></span> Report Issue</a></li>
1978 + <?php endif; ?>
1875 1979 <?php else: ?>
1876 1980 <?php foreach ($custom_links as $link): ?>
1877 1981 <li><a href="<?php echo esc_url($link['url']); ?>" <?php echo !empty($link['external']) ? 'target="_blank" rel="noopener"' : ''; ?>><span class="dashicons dashicons-admin-links"></span> <?php echo esc_html($link['label'] ?: $link['url']); ?></a></li>
1878 1982 <?php endforeach; ?>