| 1 |
<?php |
| 2 |
defined('ABSPATH') or die('Unauthorized Access'); |
| 3 |
|
| 4 |
class Phpinfo_WP_Error_Log { |
| 5 |
|
| 6 |
private static function _pro(): bool { return Phpinfo_WP_License::is_valid(); } |
| 7 |
|
| 8 |
public static function find_path(): ?string { |
| 9 |
foreach (self::candidate_paths() as $c) { |
| 10 |
if (@file_exists($c)) return $c; |
| 11 |
} |
| 12 |
return null; |
| 13 |
} |
| 14 |
|
| 15 |
// List of every path the plugin considers a possible PHP error log. Used |
| 16 |
// both by find_path() and by the diagnostic view that explains why |
| 17 |
// nothing was discovered. Order is the discovery priority. |
| 18 |
public static function candidate_paths(): array { |
| 19 |
$paths = []; |
| 20 |
|
| 21 |
// 1. WP_DEBUG_LOG — true (default) or explicit path |
| 22 |
if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG !== false) { |
| 23 |
if (is_string(WP_DEBUG_LOG) && WP_DEBUG_LOG !== '1' && WP_DEBUG_LOG !== 'true') { |
| 24 |
$paths[] = WP_DEBUG_LOG; |
| 25 |
} else { |
| 26 |
$paths[] = WP_CONTENT_DIR . '/debug.log'; |
| 27 |
} |
| 28 |
} |
| 29 |
|
| 30 |
// 2. php.ini error_log directive |
| 31 |
$ini = ini_get('error_log'); |
| 32 |
if ($ini) $paths[] = $ini; |
| 33 |
|
| 34 |
// 3. Common WordPress + shared-host fallbacks |
| 35 |
$paths[] = WP_CONTENT_DIR . '/debug.log'; |
| 36 |
$paths[] = ABSPATH . 'error_log'; |
| 37 |
$paths[] = ABSPATH . 'php_errors.log'; |
| 38 |
$paths[] = ABSPATH . 'php-error.log'; |
| 39 |
$paths[] = dirname(ABSPATH) . '/error_log'; |
| 40 |
$paths[] = dirname(ABSPATH) . '/php_errors.log'; |
| 41 |
// Cloudways, SiteGround, Kinsta-style log directories above webroot |
| 42 |
$paths[] = dirname(ABSPATH, 2) . '/logs/error.log'; |
| 43 |
$paths[] = dirname(ABSPATH, 2) . '/logs/php_errors.log'; |
| 44 |
|
| 45 |
return array_values(array_unique(array_filter($paths))); |
| 46 |
} |
| 47 |
|
| 48 |
// Reports the full diagnostic: WP_DEBUG state, each candidate path's |
| 49 |
// status (defined/not-defined, found, missing, unreadable), and a |
| 50 |
// human summary that tells the user exactly what to do. |
| 51 |
public static function diagnose(): array { |
| 52 |
$debug_const = defined('WP_DEBUG') && WP_DEBUG; |
| 53 |
$debug_log_const = defined('WP_DEBUG_LOG') && WP_DEBUG_LOG !== false; |
| 54 |
$debug_display_const = defined('WP_DEBUG_DISPLAY') ? (bool) WP_DEBUG_DISPLAY : true; |
| 55 |
|
| 56 |
$candidates = self::candidate_paths(); |
| 57 |
$checks = []; |
| 58 |
$found = null; |
| 59 |
|
| 60 |
foreach ($candidates as $p) { |
| 61 |
$entry = ['path' => $p, 'status' => 'missing', 'note' => '']; |
| 62 |
if (@file_exists($p)) { |
| 63 |
if (!@is_readable($p)) { |
| 64 |
$entry['status'] = 'unreadable'; |
| 65 |
$entry['note'] = 'File exists but PHP cannot read it (check permissions).'; |
| 66 |
} else { |
| 67 |
$entry['status'] = 'found'; |
| 68 |
$entry['size'] = (int) @filesize($p); |
| 69 |
$entry['mtime'] = (int) @filemtime($p); |
| 70 |
if ($found === null) $found = $p; |
| 71 |
} |
| 72 |
} else { |
| 73 |
$entry['note'] = 'Not found.'; |
| 74 |
} |
| 75 |
$checks[] = $entry; |
| 76 |
} |
| 77 |
|
| 78 |
// Build a one-line summary |
| 79 |
if ($found) { |
| 80 |
$summary = 'Log file found.'; |
| 81 |
$verdict = 'found'; |
| 82 |
} elseif (!$debug_log_const) { |
| 83 |
$summary = 'WP_DEBUG_LOG is off, so WordPress is not writing a log. If your host writes PHP errors somewhere else, point WP_DEBUG_LOG at that path (or check your host control panel).'; |
| 84 |
$verdict = 'disabled'; |
| 85 |
} else { |
| 86 |
$summary = "WP_DEBUG_LOG is on but no log file has been created — your site hasn't logged any PHP errors recently. That's usually a good thing. If you expected errors and don't see them, your host may be writing logs to a path we don't check (check your hosting control panel for an error_log location)."; |
| 87 |
$verdict = 'empty'; |
| 88 |
} |
| 89 |
|
| 90 |
return [ |
| 91 |
'constants' => [ |
| 92 |
'WP_DEBUG' => $debug_const, |
| 93 |
'WP_DEBUG_LOG' => $debug_log_const, |
| 94 |
'WP_DEBUG_DISPLAY' => $debug_display_const, |
| 95 |
], |
| 96 |
'wp_debug_log_value' => defined('WP_DEBUG_LOG') ? WP_DEBUG_LOG : null, |
| 97 |
'ini_error_log' => ini_get('error_log') ?: null, |
| 98 |
'checks' => $checks, |
| 99 |
'found' => $found, |
| 100 |
'summary' => $summary, |
| 101 |
'verdict' => $verdict, |
| 102 |
]; |
| 103 |
} |
| 104 |
|
| 105 |
// Returns last $lines lines, newest first |
| 106 |
public static function tail(string $path, int $lines = 150): array { |
| 107 |
if (!@file_exists($path) || !@is_readable($path)) return []; |
| 108 |
|
| 109 |
$file = new SplFileObject($path, 'r'); |
| 110 |
$file->seek(PHP_INT_MAX); |
| 111 |
$total = $file->key(); |
| 112 |
|
| 113 |
$start = max(0, $total - $lines); |
| 114 |
$result = []; |
| 115 |
|
| 116 |
$file->seek($start); |
| 117 |
while (!$file->eof()) { |
| 118 |
$line = rtrim((string) $file->current(), "\r\n"); |
| 119 |
if ($line !== '') $result[] = $line; |
| 120 |
$file->next(); |
| 121 |
} |
| 122 |
|
| 123 |
return array_reverse($result); |
| 124 |
} |
| 125 |
|
| 126 |
public static function size(string $path): int { |
| 127 |
return @file_exists($path) ? (int) @filesize($path) : 0; |
| 128 |
} |
| 129 |
|
| 130 |
public static function clear(string $path): bool { |
| 131 |
if (!self::_pro()) return false; |
| 132 |
if (!@file_exists($path) || !@is_writable($path)) return false; |
| 133 |
return (bool) @file_put_contents($path, ''); |
| 134 |
} |
| 135 |
|
| 136 |
public static function format_bytes(int $bytes): string { |
| 137 |
if ($bytes >= 1048576) return round($bytes / 1048576, 2) . ' MB'; |
| 138 |
if ($bytes >= 1024) return round($bytes / 1024, 1) . ' KB'; |
| 139 |
return $bytes . ' B'; |
| 140 |
} |
| 141 |
|
| 142 |
// Counts PHP error log lines stamped with today's date. |
| 143 |
// Free-safe: returns 0 if no log discovered. Reads only the tail (~512 KB) to stay cheap on huge logs. |
| 144 |
public static function today_count(?string $path = null): int { |
| 145 |
$path = $path ?? self::find_path(); |
| 146 |
if (!$path || !@is_readable($path)) return 0; |
| 147 |
|
| 148 |
$size = (int) @filesize($path); |
| 149 |
if ($size <= 0) return 0; |
| 150 |
|
| 151 |
$chunk = 512 * 1024; |
| 152 |
$offset = max(0, $size - $chunk); |
| 153 |
|
| 154 |
$fh = @fopen($path, 'rb'); |
| 155 |
if (!$fh) return 0; |
| 156 |
@fseek($fh, $offset); |
| 157 |
$data = @fread($fh, $chunk); |
| 158 |
@fclose($fh); |
| 159 |
if ($data === false || $data === '') return 0; |
| 160 |
|
| 161 |
// PHP error log lines start with: "[DD-Mon-YYYY HH:MM:SS TZ] ..." |
| 162 |
// Use local date — error_log writes timestamps in PHP's configured timezone. |
| 163 |
$today = date('d-M-Y'); |
| 164 |
$count = preg_match_all('/^\[' . preg_quote($today, '/') . ' /m', $data); |
| 165 |
return (int) $count; |
| 166 |
} |
| 167 |
|
| 168 |
// Classifies a log line for color-coding |
| 169 |
public static function classify(string $line): string { |
| 170 |
$l = strtolower($line); |
| 171 |
if (strpos($l, 'fatal error') !== false || strpos($l, 'uncaught') !== false) return 'log-fatal'; |
| 172 |
if (strpos($l, 'parse error') !== false) return 'log-fatal'; |
| 173 |
if (strpos($l, 'warning') !== false) return 'log-warning'; |
| 174 |
if (strpos($l, 'notice') !== false || strpos($l, 'deprecated') !== false) return 'log-notice'; |
| 175 |
if (strpos($l, 'wp_debug') !== false || strpos($l, '[debug]') !== false) return 'log-debug'; |
| 176 |
return 'log-default'; |
| 177 |
} |
| 178 |
} |
| 179 |
|