| 1 |
<?php |
| 2 |
defined('ABSPATH') or die('Unauthorized Access'); |
| 3 |
|
| 4 |
/** |
| 5 |
* Admin navigation — collapses 18+ submenu items into 5 logical groups. |
| 6 |
* |
| 7 |
* The sidebar shows only: Dashboard, Audit, Tools, Reports, License. |
| 8 |
* Within Audit / Tools / Reports, a horizontal tab bar lets users move |
| 9 |
* between related screens. All legacy slugs (?page=phpinfowp-eol etc.) |
| 10 |
* still work for backward compatibility — direct links from the admin |
| 11 |
* bar, dashboard widget, and admin notices keep functioning. |
| 12 |
*/ |
| 13 |
class Phpinfo_WP_Admin_Nav { |
| 14 |
|
| 15 |
/** |
| 16 |
* @var mixed[]|null |
| 17 |
*/ |
| 18 |
private static $cache; |
| 19 |
/** |
| 20 |
* @var bool |
| 21 |
*/ |
| 22 |
private static $tabs_rendered = false; |
| 23 |
|
| 24 |
/** |
| 25 |
* Group definitions: each group has a label, a slug for its sidebar |
| 26 |
* entry, and an ordered map of tab-slug => tab-info. |
| 27 |
*/ |
| 28 |
public static function groups(): array { |
| 29 |
if (self::$cache !== null) return self::$cache; |
| 30 |
|
| 31 |
self::$cache = [ |
| 32 |
'performance' => [ |
| 33 |
'label' => __('Performance', 'phpinfo-wp'), |
| 34 |
'slug' => 'phpinfowp-performance', |
| 35 |
'desc' => __('Analyze and optimize speed, caching, and database bottlenecks', 'phpinfo-wp'), |
| 36 |
'tabs' => [ |
| 37 |
'phpinfowp-config-grader' => ['label' => __('Config Grader', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-chart-bar'], |
| 38 |
'phpinfowp-opcache' => ['label' => __('OPcache', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-performance'], |
| 39 |
'phpinfowp-object-cache' => ['label' => __('Object Cache', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-database'], |
| 40 |
'phpinfowp-db-health' => ['label' => __('Database', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-database'], |
| 41 |
'phpinfowp-api-monitor' => ['label' => __('API Monitor', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-networking'], |
| 42 |
], |
| 43 |
], |
| 44 |
'audit' => [ |
| 45 |
'label' => __('Security & Core', 'phpinfo-wp'), |
| 46 |
'slug' => 'phpinfowp-audit', |
| 47 |
'desc' => __('Read-only health checks across PHP, config, security, and infrastructure', 'phpinfo-wp'), |
| 48 |
'tabs' => [ |
| 49 |
'phpinfowp-eol' => ['label' => __('PHP EOL', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-calendar-alt'], |
| 50 |
'phpinfowp-compat' => ['label' => __('PHP Compatibility', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-yes-alt'], |
| 51 |
'phpinfowp-update-audit' => ['label' => __('Update Guard', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-shield'], |
| 52 |
'phpinfowp-permissions' => ['label' => __('Permissions Audit', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-admin-network'], |
| 53 |
'phpinfowp-security-headers' => ['label' => __('Security Headers', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-shield-alt'], |
| 54 |
'phpinfowp-ssl' => ['label' => __('SSL Monitor', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-lock'], |
| 55 |
], |
| 56 |
], |
| 57 |
'tools' => [ |
| 58 |
'label' => __('Page Audit Tools', 'phpinfo-wp'), |
| 59 |
'slug' => 'phpinfowp-tools', |
| 60 |
'desc' => __('Active operations — edit config, troubleshoot, take snapshots, run diagnostics', 'phpinfo-wp'), |
| 61 |
'tabs' => [ |
| 62 |
'phpinfowp-viewer' => ['label' => __('phpinfo() Viewer', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-info'], |
| 63 |
'phpinfowp-htaccess' => ['label' => __('PHP Config Editor', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-editor-code'], |
| 64 |
'phpinfowp-safemode' => ['label' => __('Troubleshooting', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-sos'], |
| 65 |
'phpinfowp-info' => ['label' => __('Basic Info', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-clipboard'], |
| 66 |
'phpinfowp-extensions' => ['label' => __('Extensions', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-admin-plugins'], |
| 67 |
'phpinfowp-snapshots' => ['label' => __('Config Snapshots', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-camera'], |
| 68 |
], |
| 69 |
], |
| 70 |
'reports' => [ |
| 71 |
'label' => __('Reports & Logs', 'phpinfo-wp'), |
| 72 |
'slug' => 'phpinfowp-reports', |
| 73 |
'desc' => __('Monitoring tools, logs, and outbound alerts', 'phpinfo-wp'), |
| 74 |
'tabs' => [ |
| 75 |
'phpinfowp-log' => ['label' => __('Activity Log', 'phpinfo-wp'), 'pro' => false, 'icon' => 'dashicons-list-view'], |
| 76 |
'phpinfowp-report' => ['label' => __('Audit Report', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-media-document'], |
| 77 |
'phpinfowp-cron' => ['label' => __('WP-Cron Monitor', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-clock'], |
| 78 |
'phpinfowp-mail' => ['label' => __('Mail', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-email-alt'], |
| 79 |
'phpinfowp-alerts' => ['label' => __('Alerts', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-bell'], |
| 80 |
'phpinfowp-error-log' => ['label' => __('Error Log', 'phpinfo-wp'), 'pro' => true, 'icon' => 'dashicons-warning'], |
| 81 |
], |
| 82 |
], |
| 83 |
]; |
| 84 |
return self::$cache; |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Look up which group/tab a slug belongs to. Returns |
| 89 |
* ['group' => key, 'group_info' => array, 'tab' => slug, 'tab_info' => array] |
| 90 |
* or null if the slug isn't in any group. |
| 91 |
*/ |
| 92 |
public static function find(string $slug): ?array { |
| 93 |
foreach (self::groups() as $key => $group) { |
| 94 |
if (isset($group['tabs'][$slug])) { |
| 95 |
return [ |
| 96 |
'group' => $key, |
| 97 |
'group_info' => $group, |
| 98 |
'tab' => $slug, |
| 99 |
'tab_info' => $group['tabs'][$slug], |
| 100 |
]; |
| 101 |
} |
| 102 |
} |
| 103 |
return null; |
| 104 |
} |
| 105 |
|
| 106 |
/** |
| 107 |
* The first tab in a group — used as the default when the user lands |
| 108 |
* on the group page without a specific tab. |
| 109 |
*/ |
| 110 |
public static function first_tab(string $group_key): ?string { |
| 111 |
$g = self::groups()[$group_key] ?? null; |
| 112 |
if (!$g) return null; |
| 113 |
return array_key_first($g['tabs']); |
| 114 |
} |
| 115 |
|
| 116 |
/** |
| 117 |
* Render the vertical secondary sidebar for the current screen. Pass the |
| 118 |
* current page slug; if omitted we read it from $_GET['page']. Silently |
| 119 |
* no-ops if the slug isn't part of any group. |
| 120 |
* |
| 121 |
* Layout mirrors Elementor's in-page secondary nav: a fixed-width column |
| 122 |
* on the left of #wpbody-content with grouped vertical links. CSS pushes |
| 123 |
* the page's .wrap to the right via a body class added in admin_body_class. |
| 124 |
*/ |
| 125 |
public static function render_tabs(?string $current_slug = null): void { |
| 126 |
// Idempotent — render_group() calls this, then dispatches to a view |
| 127 |
// file that may also call it. Render only once per request. |
| 128 |
if (self::$tabs_rendered) return; |
| 129 |
|
| 130 |
$current_slug = $current_slug ?? self::current_tab_slug() ?? sanitize_key($_GET['page'] ?? ''); |
| 131 |
$info = self::find($current_slug); |
| 132 |
if (!$info) return; |
| 133 |
|
| 134 |
self::$tabs_rendered = true; |
| 135 |
|
| 136 |
$is_pro = Phpinfo_WP_License::is_valid(); |
| 137 |
$group = $info['group_info']; |
| 138 |
|
| 139 |
echo '<aside class="phpinfowp-side-nav" role="navigation" aria-label="' . esc_attr($group['label']) . '">'; |
| 140 |
echo '<ul class="phpinfowp-side-nav-list">'; |
| 141 |
foreach ($group['tabs'] as $slug => $tab) { |
| 142 |
$is_active = $slug === $current_slug; |
| 143 |
$url = admin_url('admin.php?page=' . $slug); |
| 144 |
$classes = 'phpinfowp-side-nav-item' . ($is_active ? ' is-active' : ''); |
| 145 |
$icon = !empty($tab['icon']) ? $tab['icon'] : 'dashicons-admin-generic'; |
| 146 |
$badge = (!$is_pro && !empty($tab['pro'])) |
| 147 |
? '<span class="phpinfowp-side-nav-pro">PRO</span>' |
| 148 |
: ''; |
| 149 |
echo '<li><a href="' . esc_url($url) . '" class="' . esc_attr($classes) . '">'; |
| 150 |
echo '<span class="dashicons ' . esc_attr($icon) . ' phpinfowp-side-nav-icon" aria-hidden="true"></span>'; |
| 151 |
echo '<span class="phpinfowp-side-nav-label-text">' . esc_html($tab['label']) . '</span>' . $badge; |
| 152 |
echo '</a></li>'; |
| 153 |
} |
| 154 |
echo '</ul>'; |
| 155 |
echo '</aside>'; |
| 156 |
} |
| 157 |
|
| 158 |
/** |
| 159 |
* Render the group landing page. The tab bar itself is injected by the |
| 160 |
* admin_notices hook (see auto_render below) so it appears above every |
| 161 |
* grouped page — not just the group landing. Here we just dispatch to |
| 162 |
* the active tab's view file. |
| 163 |
*/ |
| 164 |
public static function render_group(string $group_key): void { |
| 165 |
$groups = self::groups(); |
| 166 |
if (!isset($groups[$group_key])) wp_die(__('Unknown group.', 'phpinfo-wp')); |
| 167 |
|
| 168 |
$tab = self::resolve_group_tab($group_key); |
| 169 |
|
| 170 |
// Briefly impersonate the tab slug so the dispatched view's internal |
| 171 |
// POST handlers and "is this my page?" checks work unchanged. |
| 172 |
$original_page = $_GET['page'] ?? ''; |
| 173 |
$_GET['page'] = $tab; |
| 174 |
self::dispatch_view($tab); |
| 175 |
$_GET['page'] = $original_page; |
| 176 |
} |
| 177 |
|
| 178 |
/** |
| 179 |
* Resolve which tab is active when on a group landing page. Reads ?tab= |
| 180 |
* if present and valid; otherwise returns the first tab. |
| 181 |
*/ |
| 182 |
public static function resolve_group_tab(string $group_key): string { |
| 183 |
$g = self::groups()[$group_key] ?? null; |
| 184 |
if (!$g) return ''; |
| 185 |
$req = sanitize_key($_GET['tab'] ?? ''); |
| 186 |
return isset($g['tabs'][$req]) ? $req : array_key_first($g['tabs']); |
| 187 |
} |
| 188 |
|
| 189 |
/** |
| 190 |
* Map group-landing slugs back to their group key. Used by auto_render |
| 191 |
* to know "if user is on phpinfowp-performance or phpinfowp-audit, what group is that?" |
| 192 |
*/ |
| 193 |
public static function group_for_slug(string $slug): ?string { |
| 194 |
$m = [ |
| 195 |
'phpinfowp-performance' => 'performance', |
| 196 |
'phpinfowp-audit' => 'audit', |
| 197 |
'phpinfowp-tools' => 'tools', |
| 198 |
'phpinfowp-reports' => 'reports', |
| 199 |
]; |
| 200 |
return $m[$slug] ?? null; |
| 201 |
} |
| 202 |
|
| 203 |
/** |
| 204 |
* Hook target — emits the tab bar at the very top of every grouped page, |
| 205 |
* including direct legacy slugs and the new group landing slugs. |
| 206 |
*/ |
| 207 |
public static function auto_render(): void { |
| 208 |
$tab = self::current_tab_slug(); |
| 209 |
if (!$tab) return; |
| 210 |
|
| 211 |
// admin_notices fires above the page's .wrap. The sidebar is |
| 212 |
// position:absolute via CSS, so it lifts out of normal flow and the |
| 213 |
// body-class added in admin_body_class pushes .wrap right to make room. |
| 214 |
self::render_tabs($tab); |
| 215 |
} |
| 216 |
|
| 217 |
/** |
| 218 |
* True when the current admin page belongs to a grouped phpinfo screen |
| 219 |
* (so it needs the secondary sidebar layout). Used by admin_body_class. |
| 220 |
*/ |
| 221 |
public static function is_group_page(): bool { |
| 222 |
return self::current_tab_slug() !== null; |
| 223 |
} |
| 224 |
|
| 225 |
/** |
| 226 |
* Resolve the active tab slug for the current request, or null if the |
| 227 |
* current page isn't part of any group. |
| 228 |
*/ |
| 229 |
private static function current_tab_slug(): ?string { |
| 230 |
$page = sanitize_key($_GET['page'] ?? ''); |
| 231 |
if (!$page || strncmp($page, 'phpinfo', strlen('phpinfo')) !== 0) return null; |
| 232 |
if (self::find($page)) return $page; |
| 233 |
$group_key = self::group_for_slug($page); |
| 234 |
return $group_key ? self::resolve_group_tab($group_key) : null; |
| 235 |
} |
| 236 |
|
| 237 |
/** |
| 238 |
* Slug → view file mapping. Mirrors the require statements that the |
| 239 |
* Phpinfo_wp view_* methods used to do, keeping a single source of truth. |
| 240 |
*/ |
| 241 |
private static function dispatch_view(string $slug): void { |
| 242 |
$map = [ |
| 243 |
'phpinfowp-config-grader' => 'views/pro/config-grader.php', |
| 244 |
'phpinfowp-eol' => 'views/pro/eol.php', |
| 245 |
'phpinfowp-compat' => 'views/pro/compat.php', |
| 246 |
'phpinfowp-security-headers' => 'views/pro/security-headers.php', |
| 247 |
'phpinfowp-ssl' => 'views/pro/ssl.php', |
| 248 |
'phpinfowp-opcache' => 'views/pro/opcache.php', |
| 249 |
'phpinfowp-object-cache' => 'views/pro/object-cache.php', |
| 250 |
'phpinfowp-db-health' => 'views/pro/db-health.php', |
| 251 |
'phpinfowp-api-monitor' => 'views/pro/api-monitor.php', |
| 252 |
'phpinfowp-permissions' => 'views/pro/permissions.php', |
| 253 |
'phpinfowp-htaccess' => 'views/htaccess.php', |
| 254 |
'phpinfowp-safemode' => 'views/safemode.php', |
| 255 |
'phpinfowp-viewer' => 'views/phpinfo.php', |
| 256 |
'phpinfowp-extensions' => 'views/extension.php', |
| 257 |
'phpinfowp-info' => 'views/info.php', |
| 258 |
'phpinfowp-snapshots' => 'views/pro/snapshots.php', |
| 259 |
'phpinfowp-cron' => 'views/pro/cron.php', |
| 260 |
'phpinfowp-mail' => 'views/pro/mail.php', |
| 261 |
'phpinfowp-error-log' => 'views/pro/error-log.php', |
| 262 |
'phpinfowp-log' => 'views/log.php', |
| 263 |
'phpinfowp-report' => 'views/pro/report.php', |
| 264 |
'phpinfowp-alerts' => 'views/pro/alerts.php', |
| 265 |
]; |
| 266 |
$file = $map[$slug] ?? null; |
| 267 |
if (!$file) { echo '<p>' . __('Unknown view.', 'phpinfo-wp') . '</p>'; return; } |
| 268 |
require PHPINFOWP_DIR . $file; |
| 269 |
} |
| 270 |
} |
| 271 |
|