PluginProbe
phpinfo() WP – Site Health, PHP Compatibility & Server Audit / 7.2.5
phpinfo() WP – Site Health, PHP Compatibility & Server Audit v7.2.5
7.2.7 7.2.6 7.2.5 7.2.4 7.2.3 7.2.0 7.2.1 7.2.2 7.1.0 7.0.3 7.0.4 7.0.5 trunk 6.0 7.0.0 7.0.1 7.0.2
phpinfo-wp / phpinfo-wp.php

phpinfo-wp.php in phpinfo() WP – Site Health, PHP Compatibility & Server Audit 7.2.5, at phpinfo-wp.php

821 lines 42.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: phpinfo() WP
4 Plugin URI: https://exeebit.com/phpinfo-wp
5 Description: WordPress server health audit — PHP EOL timeline, config grader, security headers, SSL monitor, OPcache, error log, audit reports for clients. Free phpinfo viewer & .htaccess editor included.
6 Version: 7.2.5
7 Requires PHP: 7.3
8 Author: Exeebit
9 Author URI: https://exeebit.com/phpinfo-wp
10 License: GPLv3
11 License URI: https://www.gnu.org/licenses/gpl-3.0.html
12 Text Domain: phpinfo-wp
13 Domain Path: /languages
14 */
15
16 defined('ABSPATH') or die('Unauthorized Access');
17
18 define('PHPINFOWP_VERSION', '7.2.5');
19 define('PHPINFOWP_DIR', plugin_dir_path(__FILE__));
20 define('PHPINFOWP_URL', plugin_dir_url(__FILE__));
21
22 require_once PHPINFOWP_DIR . 'includes/class-license.php';
23 require_once PHPINFOWP_DIR . 'includes/class-eol.php';
24 require_once PHPINFOWP_DIR . 'includes/class-security-headers.php';
25 require_once PHPINFOWP_DIR . 'includes/class-snapshots.php';
26 require_once PHPINFOWP_DIR . 'includes/class-error-log.php';
27 require_once PHPINFOWP_DIR . 'includes/class-opcache.php';
28 require_once PHPINFOWP_DIR . 'includes/class-object-cache.php';
29 require_once PHPINFOWP_DIR . 'includes/class-config-grader.php';
30 require_once PHPINFOWP_DIR . 'includes/class-alerts.php';
31 require_once PHPINFOWP_DIR . 'includes/class-ssl.php';
32 require_once PHPINFOWP_DIR . 'includes/class-site-health.php';
33 require_once PHPINFOWP_DIR . 'includes/class-compat.php';
34 require_once PHPINFOWP_DIR . 'includes/class-update-audit.php';
35 require_once PHPINFOWP_DIR . 'includes/class-db-health.php';
36 require_once PHPINFOWP_DIR . 'includes/class-cron-monitor.php';
37 require_once PHPINFOWP_DIR . 'includes/class-mail-check.php';
38 require_once PHPINFOWP_DIR . 'includes/class-report.php';
39 require_once PHPINFOWP_DIR . 'includes/class-network.php';
40 require_once PHPINFOWP_DIR . 'includes/class-cli.php';
41 require_once PHPINFOWP_DIR . 'includes/class-admin-bar.php';
42 require_once PHPINFOWP_DIR . 'includes/class-admin-nav.php';
43 require_once PHPINFOWP_DIR . 'includes/class-safemode.php';
44 require_once PHPINFOWP_DIR . 'includes/class-config-grader-fixer.php';
45 require_once PHPINFOWP_DIR . 'includes/class-abilities.php';
46 require_once PHPINFOWP_DIR . 'includes/class-ai-explain.php';
47 require_once PHPINFOWP_DIR . 'includes/class-upgrade-notice.php';
48 require_once PHPINFOWP_DIR . 'includes/class-deactivate-modal.php';
49 require_once PHPINFOWP_DIR . 'includes/class-api-monitor.php';
50 require_once PHPINFOWP_DIR . 'includes/class-permissions.php';
51 require_once PHPINFOWP_DIR . 'includes/class-whats-new-notice.php';
52 require_once PHPINFOWP_DIR . 'includes/fn-feature-lock.php';
53
54 if (!class_exists('Phpinfo_wp')):
55
56 class Phpinfo_wp {
57
58 public function register(): void {
59 add_action('admin_menu', [$this, 'add_admin_pages']);
60 add_action('admin_bar_menu', [$this, 'admin_bar_indicator'], 100);
61 add_action('admin_enqueue_scripts', [$this, 'enqueue']);
62 add_action('admin_notices', [$this, 'eol_admin_notice']);
63 add_action('wp_dashboard_setup', [$this, 'register_dashboard_widget']);
64 add_action('phpinfowp_license_ping', ['Phpinfo_WP_License', 'cron_ping']);
65 add_action('phpinfowp_weekly_maintenance',['Phpinfo_WP_Alerts', 'cron_weekly']);
66 add_filter('clean_url', [$this, 'script_async'], 11, 1);
67 add_filter('plugin_row_meta', [$this, 'meta'], 10, 2);
68 add_filter('plugin_action_links', [$this, 'action_links'], 10, 2);
69 add_filter('admin_body_class', [$this, 'admin_body_class']);
70 // When viewing a leaf page (e.g. Config Grader), highlight its group
71 // (Audit) in the WP submenu — the actual leaf <li> is CSS-hidden, so
72 // without this nothing in the submenu appears active.
73 add_filter('submenu_file', [$this, 'fix_submenu_file']);
74 // Inject Elementor-style flyout panels into the WP admin sidebar so
75 // hovering Audit/Tools/Reports reveals all their leaf pages. Loaded
76 // on every admin page because users hover from anywhere in admin.
77 add_action('admin_footer', [$this, 'render_sidebar_flyouts']);
78 // Leaf-hiding CSS must load early (not in admin_footer) because WP 7.0
79 // View Transitions can skip footer scripts on cross-doc navigations.
80 add_action('admin_enqueue_scripts', [$this, 'enqueue_sidebar_css']);
81 // Topbar (logo + page title + CTA) above every plugin page.
82 add_action('in_admin_header', [$this, 'render_topbar']);
83 Phpinfo_WP_Site_Health::register();
84 Phpinfo_WP_Network::register();
85 Phpinfo_WP_Safemode::register();
86 Phpinfo_WP_Compat::register_update_warnings();
87 Phpinfo_WP_Update_Audit::register();
88 Phpinfo_WP_Abilities::register();
89 Phpinfo_WP_AI_Explain::register();
90 Phpinfo_WP_Upgrade_Notice::register();
91 Phpinfo_WP_Deactivate_Modal::register();
92 Phpinfo_WP_API_Monitor::register();
93 Phpinfo_WP_Permissions::register();
94 Phpinfo_WP_Whats_New_Notice::register();
95
96 // Auto-render the tab bar above every grouped page (priority 5 so
97 // it sits at the top of the .wrap before any other admin notices).
98 add_action('admin_notices', ['Phpinfo_WP_Admin_Nav', 'auto_render'], 5);
99
100 if (is_admin()) {
101 register_shutdown_function(['Phpinfo_WP_Admin_Bar', 'record_memory_peak']);
102 }
103 add_action('init', [$this, 'load_textdomain']);
104 add_action('admin_init', [$this, 'admin_init_migrations']);
105 add_action('in_plugin_update_message-' . plugin_basename(__FILE__), [$this, 'show_update_notice'], 10, 2);
106 }
107
108 public function load_textdomain(): void {
109 load_plugin_textdomain('phpinfo-wp', false, dirname(plugin_basename(__FILE__)) . '/languages');
110 }
111
112 public function admin_init_migrations(): void {
113 $b = get_option('phpinfowp_report_branding');
114 if (is_array($b) && isset($b['tagline']) && $b['tagline'] === 'Yearly Report') {
115 $b['tagline'] = 'Server Health Audit';
116 update_option('phpinfowp_report_branding', $b);
117 }
118 }
119
120 public function show_update_notice(array $plugin_data, object $response): void {
121 echo '<div style="margin-top: 10px; padding: 12px 14px; background: #f6f9fc; border-left: 4px solid #777BB3; font-size: 13px; color: #2c3338; border-radius: 0 4px 4px 0; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">';
122 echo '<p style="margin: 0 0 6px; font-weight: 600; color: #1d2327;">';
123 echo '' . esc_html__('What you are unlocking in this version:', 'phpinfo-wp');
124 echo '</p>';
125 echo '<p style="margin: 0 0 8px; color: #475569; line-height: 1.4;">';
126 echo esc_html__('Includes new translation packs (German, French, Spanish, Italian, Dutch) and optimized PHP server lifecycle audits.', 'phpinfo-wp');
127 echo '</p>';
128 echo '<p style="margin: 0; font-size: 12px; color: #64748b; font-style: italic;">';
129 echo esc_html__('💡 Note: Since this is a system diagnostics tool, standard backup practices are always recommended before upgrading.', 'phpinfo-wp');
130 echo '</p>';
131 echo '</div>';
132 }
133
134 public function add_admin_pages(): void {
135 // Top-level: opens the Dashboard storefront. The old top-level slug
136 // 'phpinfo-wp' kept its identity for SEO + backward compatibility,
137 // but now lands on Dashboard instead of phpinfo() output.
138 add_menu_page(
139 'phpinfo() WP', 'phpinfo() WP', 'manage_options',
140 'phpinfo-wp', [$this, 'view_dashboard'],
141 self::menu_icon(), 99
142 );
143
144 // The 5 VISIBLE sidebar items. Everything else is registered below
145 // as a hidden submenu so direct ?page=X links still resolve.
146 add_submenu_page('phpinfo-wp', 'Dashboard', 'Dashboard',
147 'manage_options', 'phpinfo-wp', [$this, 'view_dashboard']);
148
149 add_submenu_page('phpinfo-wp', 'Performance', 'Performance',
150 'manage_options', 'phpinfowp-performance', [$this, 'view_performance_group']);
151
152 add_submenu_page('phpinfo-wp', 'Security & Core', 'Security & Core',
153 'manage_options', 'phpinfowp-audit', [$this, 'view_audit_group']);
154
155 add_submenu_page('phpinfo-wp', 'Tools', 'Tools',
156 'manage_options', 'phpinfowp-tools', [$this, 'view_tools_group']);
157
158 add_submenu_page('phpinfo-wp', 'Reports', 'Reports',
159 'manage_options', 'phpinfowp-reports', [$this, 'view_reports_group']);
160
161 add_submenu_page('phpinfo-wp', 'License', 'License',
162 'manage_options', 'phpinfowp-license', [$this, 'view_license']);
163
164 add_submenu_page('phpinfo-wp', 'Support', 'Support',
165 'manage_options', 'phpinfowp-support', [$this, 'view_support']);
166
167 // Hidden registrations — these slugs work as direct URLs (admin bar,
168 // dashboard widget, admin notices) but never appear in the sidebar.
169 // Passing null as the parent is the WP-canonical way to register a
170 // hidden admin page (supported since WP 5.3): the page is registered
171 // in $_registered_pages and the action hook is bound, but it isn't
172 // appended to $submenu so it never clutters the sidebar.
173 $hidden = [
174 // Audit group
175 ['phpinfowp-config-grader', 'Config Grader', 'view_config_grader'],
176 ['phpinfowp-eol', 'PHP EOL', 'view_eol'],
177 ['phpinfowp-compat', 'PHP Compatibility', 'view_compat'],
178 ['phpinfowp-update-audit', 'Update Guard', 'view_update_audit'],
179 ['phpinfowp-security-headers', 'Security Headers', 'view_security_headers'],
180 ['phpinfowp-ssl', 'SSL Monitor', 'view_ssl'],
181 ['phpinfowp-opcache', 'OPcache', 'view_opcache'],
182 ['phpinfowp-object-cache', 'Object Cache', 'view_object_cache'],
183 ['phpinfowp-db-health', 'Database Health', 'view_db_health'],
184 ['phpinfowp-api-monitor', 'API Monitor', 'view_api_monitor'],
185 ['phpinfowp-permissions', 'Permissions Audit', 'view_permissions'],
186 // Tools group
187 ['phpinfowp-htaccess', 'PHP Config', 'view_htaccess'],
188 ['phpinfowp-safemode', 'Troubleshooting', 'view_safemode'],
189 ['phpinfowp-viewer', 'phpinfo() Viewer', 'view_phpinfo'],
190 ['phpinfowp-extensions', 'Extensions', 'view_extensions'],
191 ['phpinfowp-info', 'Basic Info', 'view_info'],
192 ['phpinfowp-snapshots', 'Config Snapshots', 'view_snapshots'],
193 ['phpinfowp-cron', 'WP Cron Monitor', 'view_cron'],
194 ['phpinfowp-mail', 'Mail', 'view_mail'],
195 ['phpinfowp-error-log', 'Error Log', 'view_error_log'],
196 // Reports group
197 ['phpinfowp-log', 'Activity Log', 'view_log'],
198 ['phpinfowp-report', 'Audit Report', 'view_report'],
199 ['phpinfowp-alerts', 'Alerts', 'view_alerts'],
200 ];
201 // Register every leaf page as a real child of phpinfo-wp so WP
202 // natively highlights the toplevel + its submenu on every leaf
203 // (parent_file/submenu_file filters were unreliable here). We hide
204 // the extra <li>s from the visible sidebar via JS in render_sidebar_flyouts
205 // — only the 5 grouped items above stay visible.
206 foreach ($hidden as [$slug, $title, $cb]) {
207 add_submenu_page('phpinfo-wp', $title, $title, 'manage_options', $slug, [$this, $cb]);
208 }
209 }
210
211 // --- Top-level + group routers ---
212 public function view_dashboard(): void { require PHPINFOWP_DIR . 'views/dashboard.php'; }
213 public function view_performance_group(): void { Phpinfo_WP_Admin_Nav::render_group('performance'); }
214 public function view_audit_group(): void { Phpinfo_WP_Admin_Nav::render_group('audit'); }
215 public function view_tools_group(): void { Phpinfo_WP_Admin_Nav::render_group('tools'); }
216 public function view_reports_group(): void { Phpinfo_WP_Admin_Nav::render_group('reports'); }
217
218 // --- Free views ---
219 public function view_phpinfo(): void { self::thankyou(); require PHPINFOWP_DIR . 'views/phpinfo.php'; }
220 public function view_htaccess(): void { require PHPINFOWP_DIR . 'views/htaccess.php'; }
221 public function view_extensions(): void { require PHPINFOWP_DIR . 'views/extension.php'; }
222 public function view_info(): void { require PHPINFOWP_DIR . 'views/info.php'; }
223 public function view_log(): void { require PHPINFOWP_DIR . 'views/log.php'; }
224 public function view_safemode(): void { require PHPINFOWP_DIR . 'views/safemode.php'; }
225
226 // --- Pro views ---
227 public function view_eol(): void { require PHPINFOWP_DIR . 'views/pro/eol.php'; }
228 public function view_security_headers(): void{ require PHPINFOWP_DIR . 'views/pro/security-headers.php'; }
229 public function view_snapshots(): void { require PHPINFOWP_DIR . 'views/pro/snapshots.php'; }
230 public function view_error_log(): void { require PHPINFOWP_DIR . 'views/pro/error-log.php'; }
231 public function view_opcache(): void { require PHPINFOWP_DIR . 'views/pro/opcache.php'; }
232 public function view_object_cache(): void { require PHPINFOWP_DIR . 'views/pro/object-cache.php'; }
233 public function view_config_grader(): void { require PHPINFOWP_DIR . 'views/pro/config-grader.php'; }
234 public function view_alerts(): void { require PHPINFOWP_DIR . 'views/pro/alerts.php'; }
235 public function view_ssl(): void { require PHPINFOWP_DIR . 'views/pro/ssl.php'; }
236 public function view_compat(): void { require PHPINFOWP_DIR . 'views/pro/compat.php'; }
237 public function view_update_audit(): void { require PHPINFOWP_DIR . 'views/pro/update-audit.php'; }
238 public function view_db_health(): void { require PHPINFOWP_DIR . 'views/pro/db-health.php'; }
239 public function view_cron(): void { require PHPINFOWP_DIR . 'views/pro/cron.php'; }
240 public function view_mail(): void { require PHPINFOWP_DIR . 'views/pro/mail.php'; }
241 public function view_report(): void { require PHPINFOWP_DIR . 'views/pro/report.php'; }
242 public function view_license(): void { require PHPINFOWP_DIR . 'views/pro/license.php'; }
243 public function view_api_monitor(): void { require PHPINFOWP_DIR . 'views/pro/api-monitor.php'; }
244 public function view_permissions(): void { require PHPINFOWP_DIR . 'views/pro/permissions.php'; }
245 public function view_support(): void { require PHPINFOWP_DIR . 'views/pro/support.php'; }
246
247 public function admin_bar_indicator(WP_Admin_Bar $bar): void {
248 Phpinfo_WP_Admin_Bar::render($bar);
249 }
250
251 public function register_dashboard_widget(): void {
252 wp_add_dashboard_widget(
253 'phpinfowp_health',
254 'PHP Health — phpinfo() WP',
255 [$this, 'render_dashboard_widget']
256 );
257 }
258
259 public function render_dashboard_widget(): void {
260 $eol = Phpinfo_WP_EOL::status();
261 $is_pro = Phpinfo_WP_License::is_valid();
262
263 switch ($eol['status']) {
264 case 'eol':
265 $eol_color = '#d63638';
266 break;
267 case 'warning':
268 $eol_color = '#dba617';
269 break;
270 default:
271 $eol_color = '#00a32a';
272 break;
273 }
274 switch ($eol['status']) {
275 case 'eol':
276 $eol_label = 'END OF LIFE';
277 break;
278 case 'warning':
279 $eol_label = 'EXPIRING SOON';
280 break;
281 default:
282 $eol_label = 'SUPPORTED';
283 break;
284 }
285
286 $mem_used_raw = memory_get_usage(true);
287 $mem_limit_raw = self::_parse_bytes(ini_get('memory_limit'));
288 $mem_used_fmt = size_format($mem_used_raw);
289 $mem_limit_fmt = ini_get('memory_limit');
290 $mem_pct = $mem_limit_raw > 0 ? min(100, round($mem_used_raw / $mem_limit_raw * 100)) : 0;
291 $mem_bar_color = $mem_pct > 85 ? '#d63638' : ($mem_pct > 65 ? '#dba617' : '#777BB3');
292 ?>
293 <div style="font-size:13px;line-height:1.6">
294
295 <!-- PHP version + EOL -->
296 <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid #f0f0f0">
297 <div>
298 <span style="font-size:20px;font-weight:700;color:#1d2327">PHP <?php echo esc_html(PHP_VERSION); ?></span>
299 <div style="font-size:11px;color:#666;margin-top:1px">EOL: <?php echo esc_html($eol['eol']); ?>
300 <?php if ($eol['status'] !== 'eol'): ?>
301 · <?php echo esc_html($eol['days']); ?> days
302 <?php endif; ?>
303 </div>
304 </div>
305 <span style="font-size:10px;font-weight:700;letter-spacing:.5px;padding:3px 8px;border-radius:3px;background:<?php echo $eol_color; ?>;color:#fff">
306 <?php echo esc_html($eol_label); ?>
307 </span>
308 </div>
309
310 <!-- Memory -->
311 <div style="padding:10px 0;border-bottom:1px solid #f0f0f0">
312 <div style="display:flex;justify-content:space-between;margin-bottom:5px">
313 <span style="color:#555">Memory</span>
314 <span style="color:#333;font-weight:500"><?php echo esc_html($mem_used_fmt); ?> / <?php echo esc_html($mem_limit_fmt); ?></span>
315 </div>
316 <div style="background:#e0e0e0;border-radius:3px;height:6px;overflow:hidden">
317 <div style="width:<?php echo $mem_pct; ?>%;height:6px;background:<?php echo $mem_bar_color; ?>;border-radius:3px;transition:width .3s"></div>
318 </div>
319 </div>
320
321 <?php
322 // Config grade — shown for everyone; free shows summary, Pro adds OPcache + SSL detail
323 $grade_summary = $is_pro
324 ? Phpinfo_WP_Config_Grader::run()
325 : Phpinfo_WP_Config_Grader::summary();
326 $grade_score = $grade_summary['score'] ?? 0;
327 $grade_letter = $grade_summary['grade'] ?? 'F';
328 switch (true) {
329 case $grade_score >= 85:
330 $grade_color = '#00a32a';
331 break;
332 case $grade_score >= 60:
333 $grade_color = '#dba617';
334 break;
335 default:
336 $grade_color = '#d63638';
337 break;
338 }
339 ?>
340 <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid #f0f0f0">
341 <span style="color:#555">Config Grade</span>
342 <a href="<?php echo esc_url(admin_url('admin.php?page=phpinfowp-config-grader')); ?>" style="text-decoration:none;font-weight:700;font-size:16px;color:<?php echo $grade_color; ?>">
343 <?php echo esc_html($grade_letter); ?> <span style="font-size:11px;font-weight:400;color:#666"><?php echo (int)$grade_score; ?>/100</span>
344 </a>
345 </div>
346
347 <?php if ($is_pro): ?>
348
349 <!-- OPcache -->
350 <?php if (Phpinfo_WP_OPcache::is_available()):
351 $oc = Phpinfo_WP_OPcache::status();
352 if ($oc && $oc['enabled'] && $oc['hit_rate'] !== null): ?>
353 <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid #f0f0f0">
354 <span style="color:#555">OPcache</span>
355 <span style="font-weight:600;color:<?php echo $oc['hit_rate'] >= 90 ? '#00a32a' : ($oc['hit_rate'] >= 70 ? '#dba617' : '#d63638'); ?>">
356 <?php echo esc_html($oc['hit_rate']); ?>% hit rate
357 </span>
358 </div>
359 <?php endif; endif; ?>
360
361 <!-- SSL -->
362 <?php
363 $ssl_results = Phpinfo_WP_SSL::check_all();
364 $ssl_worst = null;
365 foreach ($ssl_results as $r) {
366 if (!empty($r['error'])) continue;
367 if ($ssl_worst === null || $r['days'] < $ssl_worst['days']) $ssl_worst = $r;
368 }
369 if ($ssl_worst): ?>
370 <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid #f0f0f0">
371 <span style="color:#555">SSL <span style="font-size:11px;color:#999">(<?php echo esc_html($ssl_worst['host']); ?>)</span></span>
372 <span style="font-weight:600;color:<?php echo esc_html(Phpinfo_WP_SSL::status_color($ssl_worst['status'])); ?>">
373 <?php echo $ssl_worst['days'] < 0
374 ? 'EXPIRED'
375 : esc_html($ssl_worst['days']) . ' days'; ?>
376 </span>
377 </div>
378 <?php endif; ?>
379
380 <?php else:
381 // Data-driven upsell — speak to what's actually broken on THIS site
382 $upsell_lines = [];
383 if ($eol['status'] === 'eol') {
384 $upsell_lines[] = sprintf('PHP %s reached end-of-life on %s. Pro scans every plugin for compatibility before you upgrade.', esc_html($eol['minor']), esc_html($eol['eol']));
385 } elseif ($eol['status'] === 'warning') {
386 $upsell_lines[] = sprintf('PHP %s reaches EOL in %d days. Pro scans every plugin for compatibility before you upgrade.', esc_html($eol['minor']), (int)$eol['days']);
387 }
388 $fails = (int)($grade_summary['fails'] ?? 0);
389 if ($fails > 0) {
390 $upsell_lines[] = sprintf('%d failing config check%s. Pro shows exactly what to change and where.', $fails, $fails === 1 ? '' : 's');
391 }
392 if (!$upsell_lines) {
393 $upsell_lines[] = 'Track SSL expiry, scan security headers, monitor OPcache, and email yourself a weekly digest.';
394 }
395 ?>
396 <div style="padding:12px 0 8px 0;border-bottom:1px solid #f0f0f0">
397 <?php foreach ($upsell_lines as $line): ?>
398 <div style="font-size:12px;color:#555;margin-bottom:6px;line-height:1.5"> <?php echo $line; ?></div>
399 <?php endforeach; ?>
400 <a href="https://exeebit.com/phpinfo-wp#pricing" target="_blank" style="display:inline-block;margin-top:6px;color:#777BB3;font-weight:600;text-decoration:none">
401 Upgrade to Pro &rarr;
402 </a>
403 </div>
404
405 <?php endif; ?>
406
407 <!-- Footer link -->
408 <div style="padding-top:10px;display:flex;gap:12px;font-size:12px;flex-wrap:wrap">
409 <a href="<?php echo esc_url(admin_url('admin.php?page=phpinfowp-viewer')); ?>">Full phpinfo </a>
410 <a href="<?php echo esc_url(admin_url('admin.php?page=phpinfowp-eol')); ?>">PHP EOL</a>
411 <a href="<?php echo esc_url(admin_url('admin.php?page=phpinfowp-config-grader')); ?>">Config Grader</a>
412 <?php if ($is_pro): ?>
413 <a href="<?php echo esc_url(admin_url('admin.php?page=phpinfowp-report')); ?>">Audit Report</a>
414 <a href="<?php echo esc_url(admin_url('admin.php?page=phpinfowp-alerts')); ?>">Alerts</a>
415 <?php endif; ?>
416 </div>
417
418 </div>
419 <?php
420 }
421
422 // Custom SVG menu icon: phpinfo() — bold parens with center dot.
423 // WP applies admin color scheme via CSS mask, so a single-color SVG is correct here.
424 public static function menu_icon(): string {
425 $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="black">'
426 . '<path d="M7.4 2.6 C 3.4 6.2 3.4 13.8 7.4 17.4 L 9 16 C 5.8 12.8 5.8 7.2 9 4 Z"/>'
427 . '<path d="M12.6 2.6 C 16.6 6.2 16.6 13.8 12.6 17.4 L 11 16 C 14.2 12.8 14.2 7.2 11 4 Z"/>'
428 . '<circle cx="10" cy="10" r="1.6"/>'
429 . '</svg>';
430 return 'data:image/svg+xml;base64,' . base64_encode($svg);
431 }
432
433 private static function _parse_bytes(string $val): int {
434 $val = trim($val);
435 $last = strtolower(substr($val, -1));
436 $num = (int) $val;
437 switch ($last) {
438 case 'g':
439 return $num * 1073741824;
440 case 'm':
441 return $num * 1048576;
442 case 'k':
443 return $num * 1024;
444 default:
445 return $num;
446 }
447 }
448
449 // EOL notice — free feature, only on plugin admin pages
450 public function eol_admin_notice(): void {
451 $page = sanitize_key($_GET['page'] ?? '');
452 if (strncmp($page, 'phpinfo', strlen('phpinfo')) !== 0) return;
453 Phpinfo_WP_EOL::admin_notice();
454 }
455
456 public function enqueue(string $hook): void {
457 $page = sanitize_key($_GET['page'] ?? '');
458 if (strncmp($page, 'phpinfo', strlen('phpinfo')) !== 0 && $hook !== 'plugins.php') return;
459 // Use filemtime as the cache key so any CSS/JS edit auto-busts browser
460 // caches without bumping PHPINFOWP_VERSION. Falls back to plugin version.
461 $css_path = PHPINFOWP_DIR . 'css/style.css';
462 $js_path = PHPINFOWP_DIR . 'js/scripts.js';
463 $css_ver = file_exists($css_path) ? (string) filemtime($css_path) : PHPINFOWP_VERSION;
464 $js_ver = file_exists($js_path) ? (string) filemtime($js_path) : PHPINFOWP_VERSION;
465 wp_enqueue_style('phpinfowp', PHPINFOWP_URL . 'css/style.css', ['dashicons'], $css_ver);
466 wp_enqueue_script('phpinfowp', PHPINFOWP_URL . 'js/scripts.js#async', [], $js_ver, true);
467
468 if (Phpinfo_WP_AI_Explain::available()) {
469 $ai_path = PHPINFOWP_DIR . 'js/ai-explain.js';
470 $ai_ver = file_exists($ai_path) ? (string) filemtime($ai_path) : PHPINFOWP_VERSION;
471 wp_enqueue_script('phpinfowp-ai-explain', PHPINFOWP_URL . 'js/ai-explain.js', [], $ai_ver, true);
472 wp_localize_script('phpinfowp-ai-explain', 'phpinfowpAI', Phpinfo_WP_AI_Explain::js_config());
473 }
474 }
475
476 /**
477 * Inject sidebar leaf-hiding + flyout CSS on every admin page via
478 * wp_add_inline_style. This fires during admin_enqueue_scripts
479 * (not admin_footer) so the CSS is present before the first paint
480 * and survives WP 7.0 View Transition navigations.
481 */
482 public function enqueue_sidebar_css(): void {
483 $groups = Phpinfo_WP_Admin_Nav::groups();
484
485 // Build leaf-hiding selectors for every tab slug.
486 $leaf_css = [];
487 foreach ($groups as $group) {
488 foreach ($group['tabs'] as $slug => $tab) {
489 $leaf_css[] = '#toplevel_page_phpinfo-wp .wp-submenu li:has(> a[href$="page=' . $slug . '"])';
490 $leaf_css[] = '#toplevel_page_phpinfo-wp .wp-submenu li:has(> a[href*="page=' . $slug . '&"])';
491 }
492 }
493
494 $css = '';
495 if ($leaf_css) {
496 $css .= "@supports selector(:has(*)) {\n";
497 $css .= implode(",\n", $leaf_css);
498 $css .= " { display: none !important; }\n}\n";
499 }
500
501 // Flyout panel base styles (structure + WP 7.0 native colors).
502 $css .= <<<'FLYOUT'
503 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout { position: relative; }
504 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout > a { padding-right: 22px; }
505 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout > a::after {
506 content: "\203A"; position: absolute; right: 10px; top: 50%;
507 transform: translateY(-50%); opacity: .55; font-size: 16px; line-height: 1;
508 }
509 .phpinfowp-wpnav-flyout {
510 display: none; position: fixed; min-width: 210px;
511 max-height: calc(100vh - 40px); overflow-y: auto;
512 background-color: rgb(12.15,12.15,12.15); padding: 6px 0;
513 z-index: 9999; box-shadow: 0 3px 5px rgba(0,0,0,.2);
514 }
515 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout:hover > .phpinfowp-wpnav-flyout,
516 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout:focus-within > .phpinfowp-wpnav-flyout {
517 display: block;
518 }
519 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout .phpinfowp-wpnav-flyout a {
520 display: flex !important; align-items: center; justify-content: space-between;
521 gap: 8px; margin: 0; padding: 5px 12px !important;
522 color: rgba(240,246,252,0.7) !important; text-decoration: none !important;
523 font-size: 13px !important; line-height: 1.4 !important; font-weight: 400 !important;
524 }
525 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout .phpinfowp-wpnav-flyout a:hover,
526 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout .phpinfowp-wpnav-flyout a:focus {
527 color: #7b90ff !important; background: none !important;
528 box-shadow: inset 4px 0 0 0 currentColor; transition: box-shadow .1s linear;
529 }
530 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout .phpinfowp-wpnav-flyout a.phpinfowp-flyout-active,
531 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout .phpinfowp-wpnav-flyout a.phpinfowp-flyout-active:hover,
532 #toplevel_page_phpinfo-wp .wp-submenu li.phpinfowp-has-flyout .phpinfowp-wpnav-flyout a.phpinfowp-flyout-active:focus {
533 color: #7b90ff !important; background: none !important;
534 box-shadow: inset 4px 0 0 0 currentColor;
535 }
536 .phpinfowp-wpnav-flyout-pro {
537 display: inline-block; background: #777BB3; color: #fff;
538 font-size: 9px; font-weight: 700; letter-spacing: .4px;
539 padding: 1px 5px; border-radius: 3px;
540 }
541 body.folded #toplevel_page_phpinfo-wp .phpinfowp-wpnav-flyout { display: none !important; }
542
543 #toplevel_page_phpinfo-wp .wp-submenu > li > a:hover,
544 #toplevel_page_phpinfo-wp .wp-submenu > li > a:focus,
545 #toplevel_page_phpinfo-wp .wp-submenu > li.current > a,
546 #toplevel_page_phpinfo-wp .wp-submenu > li.current > a:hover,
547 #toplevel_page_phpinfo-wp .wp-submenu > li.current > a:focus {
548 color: #7b90ff !important;
549 }
550 FLYOUT;
551
552 // Attach to WP's built-in common stylesheet — always loaded.
553 wp_add_inline_style('common', $css);
554 }
555
556 public function script_async(string $url): string {
557 if (strpos($url, '#async') === false) return $url;
558 return str_replace('#async', '', $url) . "' async='async";
559 }
560
561 public function meta(array $links, string $file): array {
562 if (strpos($file, 'phpinfo-wp/phpinfo-wp.php') === false) return $links;
563 if (Phpinfo_WP_License::is_valid()) {
564 $links[] = '<span style="color:#00a32a;font-weight:600">&#10003; Pro Active</span>';
565 } else {
566 $links[] = '<a href="https://exeebit.com/phpinfo-wp#pricing" target="_blank" style="color:#777BB3;font-weight:600">Upgrade to Pro</a>';
567 }
568 return $links;
569 }
570
571 public function action_links(array $links, string $plugin_file): array {
572 if (plugin_basename(__FILE__) !== $plugin_file) return $links;
573 // Order: Dashboard | …WP defaults (Deactivate)… | Get Pro
574 array_unshift($links, '<a href="' . admin_url('admin.php?page=phpinfo-wp') . '">Dashboard</a>');
575 if (!Phpinfo_WP_License::is_valid()) {
576 $links[] = '<a href="https://exeebit.com/phpinfo-wp#pricing" target="_blank" style="color:#777BB3;font-weight:600">Get Pro</a>';
577 }
578 return $links;
579 }
580
581 // Inject hover-flyout panels into the WP admin sidebar for the three
582 // grouped items (Audit / Tools / Reports). The toplevel <li> is found
583 // by id (#toplevel_page_phpinfo-wp); each group's submenu <li> is found
584 // by its admin.php?page= slug. CSS handles the actual hover reveal.
585 public function render_sidebar_flyouts(): void {
586 $groups = Phpinfo_WP_Admin_Nav::groups();
587 $is_pro = Phpinfo_WP_License::is_valid();
588
589 $data = [];
590 foreach ($groups as $key => $group) {
591 $items = [];
592 foreach ($group['tabs'] as $slug => $tab) {
593 $items[] = [
594 'url' => admin_url('admin.php?page=' . $slug),
595 'label' => $tab['label'],
596 'pro' => !empty($tab['pro']) && !$is_pro,
597 ];
598 }
599 $data[$group['slug']] = ['label' => $group['label'], 'items' => $items];
600 }
601 ?>
602 <style id="phpinfowp-wpnav-flyout-css">
603 /* Duplicate of enqueue_sidebar_css — kept as fallback for edge cases
604 where admin_enqueue_scripts didn't fire (e.g. mu-plugin conflicts).
605 The browser deduplicates identical declarations, so no harm. */
606 <?php
607 $leaf_css = [];
608 foreach ($groups as $group) {
609 foreach ($group['tabs'] as $lslug => $ltab) {
610 $leaf_css[] = '#toplevel_page_phpinfo-wp .wp-submenu li:has(> a[href$="page=' . $lslug . '"])';
611 $leaf_css[] = '#toplevel_page_phpinfo-wp .wp-submenu li:has(> a[href*="page=' . $lslug . '&"])';
612 }
613 }
614 if ($leaf_css) {
615 echo "@supports selector(:has(*)) {\n";
616 echo implode(",\n", $leaf_css);
617 echo " { display: none !important; }\n}\n";
618 }
619 ?>
620 </style>
621 <script>
622 (function() {
623 var data = <?php echo wp_json_encode($data); ?>;
624 var root = document.getElementById('toplevel_page_phpinfo-wp');
625 if (!root) return;
626 // Hide every submenu <li> that isn't one of the 5 visible group items.
627 // The leaf pages are registered as real submenus (so WP highlights the
628 // toplevel natively) — we just suppress them in the sidebar list.
629 var visible = ['phpinfo-wp','phpinfowp-performance','phpinfowp-audit','phpinfowp-tools','phpinfowp-reports','phpinfowp-license'];
630 <?php if ($is_pro): ?>
631 visible.push('phpinfowp-support');
632 <?php endif; ?>
633 root.querySelectorAll('.wp-submenu li > a').forEach(function(a) {
634 var m = (a.getAttribute('href') || '').match(/[?&]page=([\w-]+)/);
635 if (!m) return;
636 if (visible.indexOf(m[1]) === -1) {
637 a.parentNode.style.display = 'none';
638 }
639 });
640 // Reposition a flyout so it never overflows the viewport. Uses
641 // position:fixed coords (set on .phpinfowp-wpnav-flyout). Anchors
642 // to the right edge of the parent <li>; flips upward when there
643 // isn't enough room below.
644 function position(li, flyout) {
645 var liRect = li.getBoundingClientRect();
646 var vh = window.innerHeight;
647 flyout.style.left = liRect.right + 'px';
648 // Measure natural height by temporarily clearing top + showing
649 var fh = flyout.offsetHeight; // already display:block via :hover
650 var topPreferred = liRect.top - 6; // default: align near top of <li>
651 var maxTop = vh - fh - 8; // keep 8px from bottom edge
652 var minTop = 40; // keep below WP admin bar
653 var top = Math.max(minTop, Math.min(topPreferred, maxTop));
654 flyout.style.top = top + 'px';
655 }
656
657 Object.keys(data).forEach(function(slug) {
658 var grp = data[slug];
659 var anchor = root.querySelector('.wp-submenu a[href*="page=' + slug + '"]');
660 if (!anchor) return;
661 var li = anchor.parentNode;
662 if (li.classList.contains('phpinfowp-has-flyout')) return;
663 li.classList.add('phpinfowp-has-flyout');
664 var flyout = document.createElement('div');
665 flyout.className = 'phpinfowp-wpnav-flyout';
666 var curMatch = location.search.match(/[?&]page=([\w-]+)/);
667 var currentPage = curMatch ? curMatch[1] : '';
668 grp.items.forEach(function(it) {
669 var a = document.createElement('a');
670 a.href = it.url;
671 var pm = it.url.match(/[?&]page=([\w-]+)/);
672 if (pm && pm[1] === currentPage) {
673 a.classList.add('phpinfowp-flyout-active');
674 li.classList.add('current');
675 }
676 var label = document.createElement('span');
677 label.textContent = it.label;
678 a.appendChild(label);
679 if (it.pro) {
680 var badge = document.createElement('span');
681 badge.className = 'phpinfowp-wpnav-flyout-pro';
682 badge.textContent = 'PRO';
683 a.appendChild(badge);
684 }
685 flyout.appendChild(a);
686 });
687 li.appendChild(flyout);
688
689 // Reposition on hover (and again on window resize while open
690 // so a viewport change doesn't strand the flyout off-screen).
691 var isHovered = false;
692 li.addEventListener('mouseenter', function() {
693 isHovered = true;
694 position(li, flyout);
695 });
696 li.addEventListener('mouseleave', function() {
697 isHovered = false;
698 });
699 li.addEventListener('focusin', function() { position(li, flyout); });
700 window.addEventListener('resize', function() {
701 if (isHovered || li.contains(document.activeElement)) position(li, flyout);
702 });
703 });
704 })();
705 </script>
706 <?php
707 }
708
709 // Body classes for layout:
710 // .phpinfowp-page — on every plugin admin page (topbar + margins)
711 // .phpinfowp-has-sidenav — only on grouped pages (Audit/Tools/Reports)
712 public function admin_body_class(string $classes): string {
713 if ($this->is_plugin_page()) {
714 $classes .= ' phpinfowp-page';
715 }
716 if (Phpinfo_WP_Admin_Nav::is_group_page()) {
717 $classes .= ' phpinfowp-has-sidenav';
718 }
719 return $classes;
720 }
721
722 // Highlight the correct group submenu item (Audit/Tools/Reports) when on
723 // any of its leaf pages. Returning the group's slug tells WP which
724 // submenu <li> to mark current. ?string in/out — WP can pass null.
725 public function fix_submenu_file(?string $submenu_file): ?string {
726 $page = sanitize_key($_GET['page'] ?? '');
727 $info = Phpinfo_WP_Admin_Nav::find($page);
728 if ($info) return $info['group_info']['slug'];
729 return $submenu_file;
730 }
731
732 // True when we're on any phpinfo() WP admin page (toplevel, group, or
733 // hidden leaf). Cheap — just inspects $_GET['page'].
734 private function is_plugin_page(): bool {
735 $page = sanitize_key($_GET['page'] ?? '');
736 return $page === 'phpinfo-wp' || strncmp($page, 'phpinfowp-', strlen('phpinfowp-')) === 0;
737 }
738
739 // Render the dark topbar above every plugin page: logo + plugin name +
740 // current page title (left), Pro CTA / status (right). Hooked to
741 // in_admin_header so it appears above #wpbody-content's normal content.
742 public function render_topbar(): void {
743 if (!$this->is_plugin_page()) return;
744 $page = sanitize_key($_GET['page'] ?? '');
745 $title = $this->resolve_page_title($page);
746 $is_pro = Phpinfo_WP_License::is_valid();
747 ?>
748 <div class="phpinfowp-topbar" role="banner">
749 <div class="phpinfowp-topbar-brand">
750 <span class="phpinfowp-topbar-logo" aria-hidden="true">
751 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#777BB3" width="22" height="22">
752 <path d="M7.4 2.6 C 3.4 6.2 3.4 13.8 7.4 17.4 L 9 16 C 5.8 12.8 5.8 7.2 9 4 Z"/>
753 <path d="M12.6 2.6 C 16.6 6.2 16.6 13.8 12.6 17.4 L 11 16 C 14.2 12.8 14.2 7.2 11 4 Z"/>
754 <circle cx="10" cy="10" r="1.6"/>
755 </svg>
756 </span>
757 <span class="phpinfowp-topbar-name">phpinfo() WP</span>
758 <?php if ($title): ?>
759 <span class="phpinfowp-topbar-sep" aria-hidden="true">/</span>
760 <span class="phpinfowp-topbar-title"><?php echo esc_html($title); ?></span>
761 <?php endif; ?>
762 </div>
763 <div class="phpinfowp-topbar-actions">
764 <?php if ($is_pro): ?>
765 <span class="phpinfowp-topbar-pro-active">&#10003; Pro active</span>
766 <?php else: ?>
767 <a href="https://exeebit.com/phpinfo-wp#pricing" target="_blank" rel="noopener"
768 class="phpinfowp-topbar-cta">Unlock Pro &rarr;</a>
769 <?php endif; ?>
770 </div>
771 </div>
772 <?php
773 }
774
775 private function resolve_page_title(string $page): string {
776 if ($page === 'phpinfo-wp') return 'Dashboard';
777 if ($page === 'phpinfowp-license') return 'License';
778 $info = Phpinfo_WP_Admin_Nav::find($page);
779 if ($info) return $info['tab_info']['label'];
780 // Group landing slugs like phpinfowp-audit / -tools / -reports.
781 foreach (Phpinfo_WP_Admin_Nav::groups() as $g) {
782 if ($g['slug'] === $page) return $g['label'];
783 }
784 return '';
785 }
786
787 public static function thankyou(): void {
788 add_filter('admin_footer_text', function () {
789 return '<span>Thank you for using <a href="https://wordpress.org/plugins/phpinfo-wp/">phpinfo() WP</a>.</span>';
790 }
791 );
792 }
793
794 public function activate(): void {
795 flush_rewrite_rules();
796 Phpinfo_WP_Snapshots::install_table();
797 Phpinfo_WP_License::schedule_remote_check_event();
798 if (!wp_next_scheduled('phpinfowp_weekly_maintenance')) {
799 wp_schedule_event(time() + DAY_IN_SECONDS, 'weekly', 'phpinfowp_weekly_maintenance');
800 }
801 }
802
803 public function deactivate(): void {
804 flush_rewrite_rules();
805 wp_clear_scheduled_hook('phpinfowp_license_ping');
806 wp_clear_scheduled_hook('phpinfowp_weekly_maintenance');
807 // Clean up safemode artifacts — leaving an orphaned mu-plugin behind
808 // could keep filtering plugins even after this one is gone.
809 Phpinfo_WP_Safemode::stop();
810 Phpinfo_WP_Safemode::uninstall_mu_plugin();
811 }
812 }
813
814 $phpinfo_wp = new Phpinfo_wp();
815 $phpinfo_wp->register();
816
817 register_activation_hook(__FILE__, [$phpinfo_wp, 'activate']);
818 register_deactivation_hook(__FILE__, [$phpinfo_wp, 'deactivate']);
819
820 endif;
821