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 +78 -70 2.6.22 → 2.7.2 View file →
@@ -286,12 +286,8 @@
286 286 if (empty($whitelabel_settings['hide_indexation_control'])) {
287 287 $menu_items['seo_controls'] = ['title' => 'Indexation Control', 'slug_suffix' => '-seo-controls'];
288 288 }
289 289
290 - if ($general_options['enable_googleinstantindex'] ?? false) {
291 - $menu_items['instant_index'] = ['title' => 'Instant Indexing', 'slug_suffix' => '-instant-index'];
292 - }
293 -
294 290 if ($general_options['enable_google_console'] ?? false) {
295 291 $menu_items['google_console'] = ['title' => 'Google Console', 'slug_suffix' => '-google-console'];
296 292 }
297 293
@@ -506,20 +502,8 @@
506 502 'group' => 'seo'
507 503 ];
508 504 }
509 505
510 - // Instant Indexing - setting now stored in seo_controls (moved from Settings to Indexation Control)
511 - $seo_controls = Metasync::get_option('seo_controls');
512 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
513 - $menu_items['instant_index'] = [
514 - 'title' => 'Instant Indexing',
515 - 'slug_suffix' => '-instant-index',
516 - 'callback' => 'create_admin_google_instant_index_page',
517 - 'internal_nav' => 'Instant Indexing',
518 - 'group' => 'seo'
519 - ];
520 - }
521 -
522 506 if ($general_options['enable_google_console'] ?? false) {
523 507 $menu_items['google_console'] = [
524 508 'title' => 'Google Console',
525 509 'slug_suffix' => '-google-console',
@@ -787,14 +771,19 @@
787 771
788 772 // ── Connect slug ──────────────────────────────────────────────────
789 773 $connect_slug = $menu_slug . '-connect';
790 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 +
791 779 // Local helper: register a submenu page under the plugin parent slug
792 780 // when the item keeps its WordPress admin sidebar row, or under an
793 781 // empty parent slug (hidden from the sidebar but still reachable)
794 782 // otherwise. The Settings page (general) is registered against the
795 783 // bare $menu_slug so it doubles as the parent menu callback.
796 - $register_submenu = function($item_key, $page_title, $menu_title, $capability, $menu_page_slug, $callback) use ($menu_slug) {
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;
797 786 $parent = self::item_keeps_wp_sidebar_row($item_key) ? $menu_slug : '';
798 787 add_submenu_page($parent, $page_title, $menu_title, $capability, $menu_page_slug, $callback);
799 788 };
800 789
@@ -828,13 +817,8 @@
828 817
829 818 // Site Verification (in-page sidebar only in short mode)
830 819 $register_submenu('site_verification', 'Site Verification', 'Site Verification', $menu_capability, $menu_slug . '-search-engine-verify', array($admin, 'create_admin_search_engine_verification_page'));
831 820
832 - // Instant Indexing (conditional)
833 - if ($seo_controls['enable_googleinstantindex'] ?? false) {
834 - $register_submenu('instant_index', 'Instant Indexing', 'Instant Indexing', $menu_capability, $menu_slug . '-instant-index', array($admin, 'create_admin_google_instant_index_page'));
835 - }
836 -
837 821 // Google Console (conditional)
838 822 if ($general_options['enable_google_console'] ?? false) {
839 823 $register_submenu('google_console', 'Google Console', 'Google Console', $menu_capability, $menu_slug . '-google-console', array($admin, 'create_admin_google_console_page'));
840 824 }
@@ -908,11 +892,36 @@
908 892 add_submenu_page($menu_slug, $connect_label, $connect_label, $menu_capability, $connect_slug, array($admin, 'create_admin_settings_page'));
909 893 }
910 894
911 895 // ── Hidden pages (no sidebar entry needed) ────────────────────────
896 + $admin_page_titles[$menu_slug . '-setup-wizard'] = 'Setup Wizard';
912 897 add_submenu_page('', 'Setup Wizard', 'Setup Wizard', $menu_capability, $menu_slug . '-setup-wizard', array($admin->setup_wizard, 'render_wizard_page'));
913 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 + : '';
914 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 ‹ %2$s — WordPress', 'metasync'),
915 + $admin_page_titles[$current_page],
916 + $site_title
917 + );
918 + }
919 +
920 + return $admin_title;
921 + }, 10, 2);
922 +
923 +
915 924 // Rename auto-generated first submenu from plugin name to "Settings" and reorder
916 925 add_action('admin_menu', function() use ($menu_slug) {
917 926 global $submenu;
918 927 if (!isset($submenu[$menu_slug])) {
@@ -1366,9 +1375,9 @@
1366 1375 $status_title_header = 'Not Synced - Heartbeat API not responding or unreachable';
1367 1376 $status_text_header = 'Not Synced';
1368 1377 }
1369 1378 ?>
1370 - <div class="metasync-integration-status <?php echo $status_class_header; ?>"
1379 + <div class="metasync-integration-status <?php echo esc_attr($status_class_header); ?>"
1371 1380 title="<?php echo esc_attr($status_title_header); ?>">
1372 1381 <span class="status-indicator"></span>
1373 1382 <span class="status-text"><?php echo esc_html($status_text_header); ?></span>
1374 1383 </div>
@@ -1448,10 +1457,10 @@
1448 1457
1449 1458 <?php
1450 1459 $page_slug = Metasync_Admin::$page_slug;
1451 1460 $is_plugin_page = (
1452 - (isset($_GET['page']) && strpos($_GET['page'], $page_slug) !== false) ||
1453 - (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
1454 1463 );
1455 1464 if ($is_plugin_page):
1456 1465 ?>
1457 1466 <script type="text/javascript">
@@ -1628,9 +1637,9 @@
1628 1637 * @param string $description Optional subtitle shown below the title.
1629 1638 */
1630 1639 public function render_layout_open($page_title = '', $current_page = '', $description = '')
1631 1640 {
1632 - $theme = esc_attr(get_option('metasync_theme', 'dark'));
1641 + $theme = get_option('metasync_theme', 'dark');
1633 1642 $plugin_name = Metasync::get_effective_plugin_name();
1634 1643 $general = Metasync::get_option('general') ?? [];
1635 1644 $is_connected = Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general);
1636 1645 // badge reflects confirmed heartbeat health (connected / stale /
@@ -1639,9 +1648,9 @@
1639 1648 $logo = $this->resolve_logo_data();
1640 1649
1641 1650 $current_theme = get_option('metasync_theme', 'dark');
1642 1651 ?>
1643 - <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); ?>">
1644 1653
1645 1654 <?php
1646 1655 // Inject whitelabel color palette overrides
1647 1656 $wl_settings = Metasync::get_whitelabel_settings();
@@ -1671,8 +1680,9 @@
1671 1680 $vars .= '--' . esc_attr($grad_var) . ':linear-gradient(135deg,' . esc_attr($from) . ' 0%,' . esc_attr($to) . ' 100%);';
1672 1681 }
1673 1682 }
1674 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
1675 1685 echo $selector . '{' . $vars . '}';
1676 1686 }
1677 1687 }
1678 1688 }
@@ -1747,15 +1757,19 @@
1747 1757 {
1748 1758 ?>
1749 1759 </main><!-- /.metasync-layout-main -->
1750 1760
1751 - <?php if ($show_promo):
1752 - $general = Metasync::get_option('general') ?? [];
1753 - $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):
1754 1768 ?>
1755 1769 <!-- Right promo sidebar -->
1756 1770 <aside class="metasync-layout-promo">
1757 - <?php $this->render_promo_sidebar($is_connected); ?>
1771 + <?php $this->render_promo_sidebar(); ?>
1758 1772 </aside>
1759 1773 <?php endif; ?>
1760 1774
1761 1775 </div><!-- /.metasync-layout -->
@@ -1810,11 +1824,19 @@
1810 1824 $plugin_items[$key] = $item;
1811 1825 }
1812 1826 }
1813 1827
1828 + $total_items = count($seo_items) + count($plugin_items);
1829 +
1814 1830 ?>
1815 1831 <nav class="metasync-sidenav">
1816 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 +
1817 1839 <!-- SEO Features group -->
1818 1840 <div class="metasync-sidenav-group">
1819 1841 <div class="metasync-sidenav-group-title">SEO Features</div>
1820 1842 <ul>
@@ -1820,12 +1842,12 @@
1820 1842 <ul>
1821 1843 <?php foreach ($seo_items as $key => $item):
1822 1844 $is_active = ($current_page === $key);
1823 1845 $icon = $icons[$key] ?? 'admin-generic';
1824 - $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']);
1825 1847 ?>
1826 1848 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1827 - <a href="<?php echo $url; ?>">
1849 + <a href="<?php echo esc_url($url); ?>">
1828 1850 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1829 1851 <?php echo esc_html($item['title']); ?>
1830 1852 </a>
1831 1853 </li>
@@ -1840,12 +1862,12 @@
1840 1862 <?php foreach ($plugin_items as $key => $item):
1841 1863 if ($key === 'report_issue') continue;
1842 1864 $is_active = ($current_page === $key);
1843 1865 $icon = $icons[$key] ?? 'admin-generic';
1844 - $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']);
1845 1867 ?>
1846 1868 <li class="<?php echo $is_active ? 'metasync-sidenav-active' : ''; ?>">
1847 - <a href="<?php echo $url; ?>">
1869 + <a href="<?php echo esc_url($url); ?>">
1848 1870 <span class="metasync-sidenav-icon"><span class="dashicons dashicons-<?php echo esc_attr($icon); ?>"></span></span>
1849 1871 <?php echo esc_html($item['title']); ?>
1850 1872 </a>
1851 1873 </li>
@@ -1880,8 +1902,10 @@
1880 1902 </ul>
1881 1903 </div>
1882 1904 <?php endif; ?>
1883 1905
1906 + <?php endif; ?>
1907 +
1884 1908 </nav>
1885 1909 <?php
1886 1910 }
1887 1911
@@ -1887,17 +1911,22 @@
1887 1911
1888 1912 /**
1889 1913 * Render the right promotional sidebar.
1890 1914 *
1891 - * @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.
1892 1917 */
1893 - public function render_promo_sidebar($is_connected = false)
1918 + public function render_promo_sidebar()
1894 1919 {
1895 1920 $plugin_name = Metasync::get_effective_plugin_name();
1896 1921 $otto_name = Metasync::get_whitelabel_otto_name();
1897 - $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);
1898 1923 $homepage = Metasync::HOMEPAGE_DOMAIN;
1899 - $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();
1900 1929 $whitelabel = Metasync::get_whitelabel_settings();
1901 1930 $custom_links = isset($whitelabel['quick_links']) && is_array($whitelabel['quick_links'])
1902 1931 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1903 1932 : [];
@@ -1902,9 +1931,8 @@
1902 1931 ? array_filter($whitelabel['quick_links'], function($l) { return !empty($l['url']); })
1903 1932 : [];
1904 1933 ?>
1905 1934
1906 - <?php if (!$is_connected): ?>
1907 1935 <!-- Connect CTA card -->
1908 1936 <div class="metasync-promo-card metasync-promo-card--connect">
1909 1937 <div class="metasync-promo-card-header">
1910 1938 <div class="metasync-promo-card-icon">
@@ -1920,40 +1948,12 @@
1920 1948 <li><span class="promo-check">&#10003;</span> Real-time keyword tracking</li>
1921 1949 <li><span class="promo-check">&#10003;</span> Automated schema markup</li>
1922 1950 <li><span class="promo-check">&#10003;</span> Instant Google indexing</li>
1923 1951 </ul>
1924 - <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">
1925 1953 Connect Now
1926 1954 </a>
1927 1955 </div>
1928 - <?php else: ?>
1929 - <!-- Connected — feature highlights -->
1930 - <div class="metasync-promo-card metasync-promo-card--accent">
1931 - <div class="metasync-promo-card-header">
1932 - <div class="metasync-promo-card-icon">
1933 - <span class="dashicons dashicons-performance"></span>
1934 - </div>
1935 - <div>
1936 - <h3><?php echo esc_html($otto_name); ?> Active</h3>
1937 - </div>
1938 - </div>
1939 - <p class="metasync-promo-tagline">Your site is connected and <?php echo esc_html($otto_name); ?> is optimizing pages automatically.</p>
1940 - <ul class="metasync-promo-benefits">
1941 - <li><span class="promo-check">&#10003;</span> Schema markup auto-applied</li>
1942 - <li><span class="promo-check">&#10003;</span> Meta titles &amp; descriptions optimized</li>
1943 - <li><span class="promo-check">&#10003;</span> Internal linking suggestions active</li>
1944 - </ul>
1945 - <?php if ($is_default_brand): ?>
1946 - <a href="<?php echo esc_url($homepage); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1947 - View <?php echo esc_html($plugin_name); ?> Dashboard
1948 - </a>
1949 - <?php elseif (!empty($whitelabel['domain'])): ?>
1950 - <a href="<?php echo esc_url($whitelabel['domain']); ?>" target="_blank" rel="noopener" class="metasync-promo-btn metasync-promo-btn--outline">
1951 - View <?php echo esc_html($plugin_name); ?> Dashboard
1952 - </a>
1953 - <?php endif; ?>
1954 - </div>
1955 - <?php endif; ?>
1956 1956
1957 1957 <?php
1958 1958 // Quick Links: show default links for default brand, custom links if whitelabeled + provided, hide if whitelabeled + none
1959 1959 $show_quick_links = $is_default_brand || !empty($custom_links);
@@ -1963,12 +1963,20 @@
1963 1963 <div class="metasync-promo-card">
1964 1964 <h3 style="margin:0 0 12px;font-size:13px;font-weight:700;color:var(--dashboard-text-primary);">Quick Links</h3>
1965 1965 <ul class="metasync-promo-links">
1966 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>
1967 1968 <li><a href="<?php echo esc_url($homepage . '/blog/'); ?>" target="_blank" rel="noopener"><span class="dashicons dashicons-rss"></span> SEO Blog</a></li>
1968 - <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>
1969 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 + ?>
1970 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; ?>
1971 1979 <?php else: ?>
1972 1980 <?php foreach ($custom_links as $link): ?>
1973 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>
1974 1982 <?php endforeach; ?>