PluginProbe
phpinfo() WP – Site Health, PHP Compatibility & Server Audit / 7.2.6
phpinfo() WP – Site Health, PHP Compatibility & Server Audit v7.2.6
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 / includes / class-admin-bar.php

class-admin-bar.php in phpinfo() WP – Site Health, PHP Compatibility & Server Audit 7.2.6, at includes/class-admin-bar.php

444 lines 18.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined('ABSPATH') or die('Unauthorized Access');
3
4 /**
5 * Admin bar indicator — turns the pill from a static "PHP X.Y" label into a
6 * live health scoreboard. Aggregates signals from existing classes; never
7 * triggers expensive operations (HTTP/SSL handshakes) — only reads cached data.
8 *
9 * Free pill : letter grade + most-urgent free issue
10 * Pro pill : adds SSL, cron, headers, snapshot drift, OPcache regression
11 */
12 class Phpinfo_WP_Admin_Bar {
13
14 const OPT_LAST_UNHEALTHY = 'phpinfowp_last_unhealthy_ts';
15 const OPT_OPCACHE_BASELINE = 'phpinfowp_opcache_baseline_hit';
16
17 private const COLOR_GOOD = '#00a32a';
18 private const COLOR_WARN = '#dba617';
19 private const COLOR_BAD = '#d63638';
20
21 public static function render(WP_Admin_Bar $bar): void {
22 if (!current_user_can('manage_options')) return;
23
24 $status = self::status();
25 self::persist_streak_state($status);
26
27 $bar->add_node([
28 'id' => 'phpinfowp-indicator',
29 'title' => self::pill_title($status),
30 'href' => self::primary_href($status),
31 'meta' => ['class' => 'phpinfowp-adminbar', 'title' => self::pill_tooltip($status)],
32 ]);
33
34 // Issues — only shown when there are any
35 foreach ($status['issues'] as $i => $issue) {
36 $bar->add_node([
37 'parent' => 'phpinfowp-indicator',
38 'id' => 'phpinfowp-issue-' . $i,
39 'title' => self::issue_row($issue),
40 'href' => $issue['href'],
41 ]);
42 }
43
44 if ($status['issues']) {
45 $bar->add_group([
46 'parent' => 'phpinfowp-indicator',
47 'id' => 'phpinfowp-server',
48 ]);
49 }
50
51 // Server section — PHP version, memory peak, OPcache hit rate (Pro)
52 $bar->add_node([
53 'parent' => $status['issues'] ? 'phpinfowp-server' : 'phpinfowp-indicator',
54 'id' => 'phpinfowp-php',
55 'title' => 'PHP ' . PHP_VERSION,
56 'href' => admin_url('admin.php?page=phpinfowp-viewer'),
57 ]);
58
59 $bar->add_node([
60 'parent' => $status['issues'] ? 'phpinfowp-server' : 'phpinfowp-indicator',
61 'id' => 'phpinfowp-mem',
62 'title' => self::memory_label($status['memory']),
63 'href' => admin_url('admin.php?page=phpinfowp-info'),
64 ]);
65
66 if ($status['is_pro'] && $status['opcache']) {
67 $oc = $status['opcache'];
68 $arrow = $oc['delta'] < -5 ? '' : '';
69 $bar->add_node([
70 'parent' => $status['issues'] ? 'phpinfowp-server' : 'phpinfowp-indicator',
71 'id' => 'phpinfowp-opcache',
72 'title' => 'OPcache: ' . $oc['hit_rate'] . '%' . $arrow,
73 'href' => admin_url('admin.php?page=phpinfowp-opcache'),
74 ]);
75 }
76
77 // Streak footer — only shown when we have a streak worth bragging about
78 if ($status['streak_days'] >= 3) {
79 $bar->add_node([
80 'parent' => $status['issues'] ? 'phpinfowp-server' : 'phpinfowp-indicator',
81 'id' => 'phpinfowp-streak',
82 'title' => '✓ Healthy for ' . $status['streak_days'] . ' days',
83 'href' => admin_url('admin.php?page=phpinfowp-config-grader'),
84 ]);
85 }
86
87 if (!$status['is_pro']) {
88 $bar->add_node([
89 'parent' => $status['issues'] ? 'phpinfowp-server' : 'phpinfowp-indicator',
90 'id' => 'phpinfowp-upgrade',
91 'title' => '�
92 Unlock SSL · headers · cron alerts',
93 'href' => 'https://exeebit.com/phpinfo-wp#pricing',
94 'meta' => ['target' => '_blank'],
95 ]);
96 }
97 }
98
99 /**
100 * Builds the unified status payload from all data sources.
101 * Reads cached data only — no fresh HTTP / SSL / OPcache reset calls.
102 */
103 public static function status(): array {
104 $is_pro = Phpinfo_WP_License::is_valid();
105 $grader = Phpinfo_WP_Config_Grader::summary();
106 $eol = Phpinfo_WP_EOL::status();
107 $issues = [];
108
109 // --- EOL (free) ---
110 if ($eol['status'] === 'eol') {
111 $issues[] = [
112 'level' => 'critical',
113 'label' => 'PHP ' . $eol['minor'] . ' EOL',
114 'detail' => 'EOL ' . $eol['eol'] . ' — past',
115 'href' => admin_url('admin.php?page=phpinfowp-eol'),
116 'sort' => 0,
117 ];
118 } elseif ($eol['status'] === 'warning') {
119 $issues[] = [
120 'level' => 'warning',
121 'label' => 'EOL ' . $eol['days'] . 'd',
122 'detail' => 'PHP ' . $eol['minor'] . ' EOL ' . $eol['eol'],
123 'href' => admin_url('admin.php?page=phpinfowp-eol'),
124 'sort' => 10,
125 ];
126 }
127
128 // --- Errors today (free count, viewer is Pro) ---
129 $err_count = Phpinfo_WP_Error_Log::today_count();
130 if ($err_count > 0) {
131 $issues[] = [
132 'level' => $err_count >= 100 ? 'critical' : 'warning',
133 'label' => $err_count . ' error' . ($err_count === 1 ? '' : 's') . ' today',
134 'detail' => $err_count . ' PHP error log entries today',
135 'href' => admin_url('admin.php?page=phpinfowp-error-log'),
136 'sort' => 30,
137 ];
138 }
139
140 // --- Pro signals ---
141 $opcache = null;
142 if ($is_pro) {
143 // SSL — read cached transients only, don't trigger fresh checks
144 $site_host = parse_url(get_site_url(), PHP_URL_HOST) ?: '';
145 $hosts = array_unique(array_filter(array_merge(
146 [$site_host],
147 Phpinfo_WP_SSL::get_extra_domains()
148 )));
149 foreach ($hosts as $h) {
150 $cached = get_transient('phpinfowp_ssl_' . md5($h));
151 if (!$cached || !empty($cached['error'])) continue;
152 $d = (int) ($cached['days'] ?? 999);
153 if ($d < 0) {
154 $issues[] = [
155 'level' => 'critical', 'label' => 'SSL expired (' . $h . ')',
156 'detail' => $h . ' expired ' . abs($d) . 'd ago',
157 'href' => admin_url('admin.php?page=phpinfowp-ssl'),
158 'sort' => 1,
159 ];
160 } elseif ($d < 7) {
161 $issues[] = [
162 'level' => 'critical', 'label' => 'SSL ' . $d . 'd (' . $h . ')',
163 'detail' => $h . ' expires in ' . $d . ' days',
164 'href' => admin_url('admin.php?page=phpinfowp-ssl'),
165 'sort' => 2,
166 ];
167 } elseif ($d < 30) {
168 $issues[] = [
169 'level' => 'warning', 'label' => 'SSL ' . $d . 'd',
170 'detail' => $h . ' expires in ' . $d . ' days',
171 'href' => admin_url('admin.php?page=phpinfowp-ssl'),
172 'sort' => 15,
173 ];
174 }
175 }
176
177 // Cron health
178 $cron = Phpinfo_WP_Cron_Monitor::summary();
179 if ($cron) {
180 if (!empty($cron['disabled'])) {
181 $issues[] = [
182 'level' => 'critical', 'label' => 'WP-Cron disabled',
183 'detail' => 'DISABLE_WP_CRON is true — scheduled tasks will not run',
184 'href' => admin_url('admin.php?page=phpinfowp-cron'),
185 'sort' => 3,
186 ];
187 } elseif (!empty($cron['overdue'])) {
188 $issues[] = [
189 'level' => 'warning', 'label' => $cron['overdue'] . ' cron overdue',
190 'detail' => $cron['overdue'] . ' scheduled events past due',
191 'href' => admin_url('admin.php?page=phpinfowp-cron'),
192 'sort' => 20,
193 ];
194 }
195 if (!empty($cron['orphan'])) {
196 $issues[] = [
197 'level' => 'warning', 'label' => $cron['orphan'] . ' orphan cron',
198 'detail' => $cron['orphan'] . ' events with no registered callback',
199 'href' => admin_url('admin.php?page=phpinfowp-cron'),
200 'sort' => 25,
201 ];
202 }
203 }
204
205 // Security headers — cached only
206 $hdr_cache = get_transient('phpinfowp_sec_headers');
207 if (is_array($hdr_cache) && isset($hdr_cache['grade'])) {
208 $g = $hdr_cache['grade'];
209 if (in_array($g, ['F', 'D'], true)) {
210 $issues[] = [
211 'level' => 'warning', 'label' => 'Headers ' . $g,
212 'detail' => 'Security headers score: ' . ($hdr_cache['score'] ?? '?') . '/100',
213 'href' => admin_url('admin.php?page=phpinfowp-security-headers'),
214 'sort' => 22,
215 ];
216 }
217 }
218
219 // OPcache — cheap to read, surface regressions
220 $oc = Phpinfo_WP_OPcache::status();
221 if ($oc && $oc['enabled'] && $oc['hit_rate'] !== null) {
222 $baseline = (float) get_option(self::OPT_OPCACHE_BASELINE, 0);
223 if ($baseline === 0.0 || $oc['hit_rate'] > $baseline) {
224 update_option(self::OPT_OPCACHE_BASELINE, $oc['hit_rate'], false);
225 $baseline = $oc['hit_rate'];
226 }
227 $delta = $oc['hit_rate'] - $baseline;
228 $opcache = ['hit_rate' => $oc['hit_rate'], 'baseline' => $baseline, 'delta' => $delta];
229
230 if (!empty($oc['full'])) {
231 $issues[] = [
232 'level' => 'critical', 'label' => 'OPcache full',
233 'detail' => 'Cache is full — increase opcache.memory_consumption',
234 'href' => admin_url('admin.php?page=phpinfowp-opcache'),
235 'sort' => 4,
236 ];
237 } elseif ($delta < -15) {
238 $issues[] = [
239 'level' => 'warning', 'label' => 'OPcache ↓ ' . round($delta) . '%',
240 'detail' => 'Hit rate ' . $oc['hit_rate'] . '% vs baseline ' . $baseline . '%',
241 'href' => admin_url('admin.php?page=phpinfowp-opcache'),
242 'sort' => 28,
243 ];
244 }
245 }
246
247 // Config drift since last snapshot
248 $latest = Phpinfo_WP_Snapshots::get_latest();
249 if ($latest) {
250 $prev = Phpinfo_WP_Snapshots::get_previous_to((int) $latest->id);
251 if ($prev) {
252 $diff = Phpinfo_WP_Snapshots::diff($prev->snapshot_data, $latest->snapshot_data);
253 $n = count($diff);
254 if ($n > 0) {
255 $issues[] = [
256 'level' => 'warning', 'label' => $n . ' config change' . ($n === 1 ? '' : 's'),
257 'detail' => 'php.ini drift since last weekly snapshot',
258 'href' => admin_url('admin.php?page=phpinfowp-snapshots'),
259 'sort' => 26,
260 ];
261 }
262 }
263 }
264 }
265
266 // Sort issues — critical first, then by their `sort` rank
267 usort($issues, function($a, $b) {
268 $rank = ['critical' => 0, 'warning' => 1];
269 $ra = $rank[$a['level']] ?? 9;
270 $rb = $rank[$b['level']] ?? 9;
271 if ($ra !== $rb) return $ra <=> $rb;
272 return ($a['sort'] ?? 99) <=> ($b['sort'] ?? 99);
273 });
274
275 $crit_count = 0;
276 $warn_count = 0;
277 foreach ($issues as $i) {
278 if ($i['level'] === 'critical') $crit_count++;
279 elseif ($i['level'] === 'warning') $warn_count++;
280 }
281
282 $overall = $crit_count > 0 ? 'critical' : ($warn_count > 0 ? 'warning' : 'good');
283
284 return [
285 'is_pro' => $is_pro,
286 'grade' => $grader['grade'] ?? '?',
287 'score' => $grader['score'] ?? 0,
288 'eol' => $eol,
289 'issues' => $issues,
290 'crit_count' => $crit_count,
291 'warn_count' => $warn_count,
292 'overall' => $overall,
293 'memory' => self::memory_payload(),
294 'opcache' => $opcache,
295 'streak_days' => self::streak_days(),
296 ];
297 }
298
299 private static function pill_title(array $s): string {
300 $color = self::color_for($s['overall']);
301
302 if ($s['overall'] === 'critical') {
303 // Show count if multiple criticals, else first label
304 $label = $s['crit_count'] > 1
305 ? $s['crit_count'] . ' issues'
306 : $s['issues'][0]['label'];
307 $extra = $s['warn_count'] > 0 ? ' +' . $s['warn_count'] : '';
308
309 $style = '<style>' .
310 '@keyframes phpinfowp-pulse-glow {' .
311 ' 0% { transform: scale(0.9); opacity: 0.6; box-shadow: 0 0 0 0 rgba(214, 54, 56, 0.7); }' .
312 ' 70% { transform: scale(1.1); opacity: 1; box-shadow: 0 0 0 6px rgba(214, 54, 56, 0); }' .
313 ' 100% { transform: scale(0.9); opacity: 0.6; box-shadow: 0 0 0 0 rgba(214, 54, 56, 0); }' .
314 '}' .
315 '</style>';
316
317 $dot = '<span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:#d63638;margin-right:6px;vertical-align:middle;animation:phpinfowp-pulse-glow 1.6s infinite ease-in-out;"></span>';
318
319 return $style . $dot . '<span style="color:' . $color . ';font-weight:700">' . esc_html($label . $extra) . '</span>';
320 }
321
322 if ($s['overall'] === 'warning') {
323 // Lead with grade, then first warning label
324 $lead = $s['issues'][0]['label'];
325 $extra = $s['warn_count'] > 1 ? ' (+' . ($s['warn_count'] - 1) . ')' : '';
326 return '<span style="color:' . $color . ';font-weight:600">'
327 . esc_html($s['grade'] . ' · ' . $lead . $extra)
328 . '</span>';
329 }
330
331 return '<span style="color:' . $color . ';font-weight:600">'
332 . esc_html($s['grade'] . ' · PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION) . ''
333 . '</span>';
334 }
335
336 private static function pill_tooltip(array $s): string {
337 if ($s['overall'] === 'good') {
338 return sprintf('Config grade %s · score %d/100 · no active issues', $s['grade'], $s['score']);
339 }
340 $parts = [];
341 foreach ($s['issues'] as $i) $parts[] = $i['label'];
342 return implode(' · ', $parts);
343 }
344
345 private static function primary_href(array $s): string {
346 if ($s['issues']) return $s['issues'][0]['href'];
347 return admin_url('admin.php?page=phpinfowp-config-grader');
348 }
349
350 private static function issue_row(array $issue): string {
351 $color = $issue['level'] === 'critical' ? self::COLOR_BAD : self::COLOR_WARN;
352 $dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' . $color . ';margin-right:8px;vertical-align:middle"></span>';
353 return $dot . '<strong>' . esc_html($issue['label']) . '</strong>'
354 . '<span style="opacity:.7;margin-left:8px;font-size:11px">' . esc_html($issue['detail']) . '</span>';
355 }
356
357 private static function memory_label(array $m): string {
358 if ($m['peak_bytes'] <= 0) {
359 return 'Memory: ' . size_format(memory_get_usage(true)) . ' / ' . $m['limit_label'];
360 }
361 $pct = $m['pct'];
362 $arrow = $pct >= 85 ? '' : '';
363 return 'Peak today: ' . size_format($m['peak_bytes']) . ' / ' . $m['limit_label']
364 . ' (' . $pct . '%)' . $arrow;
365 }
366
367 private static function memory_payload(): array {
368 $key = 'phpinfowp_mem_peak_' . date('Ymd');
369 $peak = (int) get_transient($key);
370 $limit_raw = ini_get('memory_limit');
371 $limit_bytes = self::parse_bytes($limit_raw);
372 $pct = ($peak > 0 && $limit_bytes > 0) ? (int) round($peak / $limit_bytes * 100) : 0;
373 return [
374 'peak_bytes' => $peak,
375 'limit_bytes' => $limit_bytes,
376 'limit_label' => $limit_raw,
377 'pct' => $pct,
378 ];
379 }
380
381 /**
382 * Called from a shutdown hook — only writes when peak grows, so the DB
383 * cost is at most one transient write per request on admin pages.
384 */
385 public static function record_memory_peak(): void {
386 $peak = memory_get_peak_usage(true);
387 $key = 'phpinfowp_mem_peak_' . date('Ymd');
388 $cur = (int) get_transient($key);
389 if ($peak > $cur) {
390 set_transient($key, $peak, 36 * HOUR_IN_SECONDS);
391 }
392 }
393
394 private static function streak_days(): int {
395 $ts = (int) get_option(self::OPT_LAST_UNHEALTHY, 0);
396 if ($ts <= 0) {
397 $install = (int) get_option('phpinfowp_install_ts', 0);
398 if ($install <= 0) {
399 update_option('phpinfowp_install_ts', time(), false);
400 return 0;
401 }
402 $ts = $install;
403 }
404 return max(0, (int) floor((time() - $ts) / DAY_IN_SECONDS));
405 }
406
407 private static function persist_streak_state(array $status): void {
408 if ($status['overall'] === 'good') return;
409 $existing = (int) get_option(self::OPT_LAST_UNHEALTHY, 0);
410 // Only update if last unhealthy was more than 24h ago, to keep writes bounded
411 if (time() - $existing > DAY_IN_SECONDS) {
412 update_option(self::OPT_LAST_UNHEALTHY, time(), false);
413 }
414 }
415
416 private static function color_for(string $level): string {
417 switch ($level) {
418 case 'critical':
419 return self::COLOR_BAD;
420 case 'warning':
421 return self::COLOR_WARN;
422 default:
423 return self::COLOR_GOOD;
424 }
425 }
426
427 private static function parse_bytes(string $val): int {
428 $val = trim($val);
429 if ($val === '' || $val === '-1') return 0;
430 $last = strtolower($val[strlen($val) - 1]);
431 $num = (int) $val;
432 switch ($last) {
433 case 'g':
434 return $num * GB_IN_BYTES;
435 case 'm':
436 return $num * MB_IN_BYTES;
437 case 'k':
438 return $num * KB_IN_BYTES;
439 default:
440 return $num;
441 }
442 }
443 }
444