PluginProbe
phpinfo() WP – Site Health, PHP Compatibility & Server Audit / 7.2.7
phpinfo() WP – Site Health, PHP Compatibility & Server Audit v7.2.7
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.7, at phpinfo-wp.php

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