1<?php
2/**
3 * Title: Error Log
4 * Icon: fa-exclamation-triangle
5 * Group: Overview
6 * Order: 40
7 * Description: Tail the PHP error log - website, plugin and theme errors all land here.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15function acp_error_log_ini_path(): string {
16 $v = @ini_get('error_log');
17 return is_string($v) ? trim($v) : '';
18}
19
20function acp_error_log_path(): string {
21 $configured = trim((string) setting('error_log:path', ''));
22 return $configured !== '' ? $configured : acp_error_log_ini_path();
23}
24
25/** Last $lines lines of a (possibly large) text file, without loading it all into memory. */
26function acp_error_log_tail(string $file, int $lines): array {
27 $handle = @fopen($file, 'rb');
28 if ($handle === false) {
29 return array();
30 }
31
32 $chunkSize = 8192;
33 $buffer = '';
34 $foundLines = 0;
35 fseek($handle, 0, SEEK_END);
36 $pos = ftell($handle);
37
38 while ($pos > 0 && $foundLines <= $lines) {
39 $read = min($chunkSize, $pos);
40 $pos -= $read;
41 fseek($handle, $pos);
42 $buffer = fread($handle, $read) . $buffer;
43 $foundLines = substr_count($buffer, "\n");
44 }
45 fclose($handle);
46
47 $rows = explode("\n", rtrim($buffer, "\n"));
48 $rows = array_slice($rows, -$lines);
49 return array_reverse($rows);
50}
51
52function acp_error_log_severity(string $line): string {
53 if (preg_match('/\b(Fatal error|SQL ERROR|Uncaught|Parse error|ZnoteX DB)\b/i', $line)) {
54 return 'red';
55 }
56 if (preg_match('/\b(Warning|Deprecated)\b/i', $line)) {
57 return 'amber';
58 }
59 return '';
60}
61
62$module = 'error_log';
63
64if ($_SERVER['REQUEST_METHOD'] === 'POST') {
65 $do = (string) ($_POST['do'] ?? '');
66 if ($do === 'save_path') {
67 $newPath = trim((string) ($_POST['path'] ?? ''));
68 if (setting_set('error_log:path', $newPath)) {
69 acp_log('error_log.path_save', $newPath);
70 acp_flash_success(t_default('acp.error_log.saved', 'Log path saved.'));
71 } else {
72 acp_flash_error(t_default('acp.error_log.save_failed', 'The log path could not be saved.'));
73 }
74 } elseif ($do === 'reset_path') {
75 if (setting_set('error_log:path', '')) {
76 acp_log('error_log.path_reset');
77 acp_flash_success(t_default('acp.error_log.reset', 'Reverted to the auto-detected path.'));
78 } else {
79 acp_flash_error(t_default('acp.error_log.reset_failed', 'The log path could not be reset.'));
80 }
81 }
82 acp_redirect($module);
83}
84
85$path = acp_error_log_path();
86$hasFile = $path !== '' && is_file($path) && is_readable($path);
87$linesWant = in_array((string) ($_GET['lines'] ?? ''), array('100', '300', '1000'), true) ? (int) $_GET['lines'] : 300;
88$rows = $hasFile ? acp_error_log_tail($path, $linesWant) : array();
89$configured = trim((string) setting('error_log:path', ''));
90?>
91
92<?php if (!$hasFile): ?>
93 <div class="acp-flash acp-flash--error">
94 <i class="fa fa-exclamation-triangle"></i>
95 <span>
96 <?php if ($path === ''): ?>
97 <?= t_default('acp.error_log.not_configured', "PHP is not configured with an <code>error_log</code> file for this request - it is very likely logging to the web server's own error log instead (e.g. Apache's log under XAMPP, WAMP, Uniform Server, or a Linux host), which lives outside this site's folder and cannot be found automatically.") ?>
98 <?php else: ?>
99 <?= t_default('acp.error_log.path_missing', 'The configured path <code>{path}</code> does not exist or is not readable from PHP.', ['path' => h($path)]) ?>
100 <?php endif; ?>
101 <?= t_default('acp.error_log.help', "Paste the correct path below - check your stack's control panel (XAMPP: <code>apache/logs/error.log</code>, WAMP: <code>logs/apache_error.log</code>, Uniform Server: its <code>logs</code> folder) or your <code>php.ini</code>'s <code>error_log</code> directive.") ?>
102 </span>
103 </div>
104<?php endif; ?>
105
106<section class="acp-card">
107 <header class="acp-card-head">
108 <h2><?= t_default('acp.error_log.title', 'Log file') ?></h2>
109 <p><?= t_default('acp.error_log.ini_get_label', "PHP's <code>ini_get('error_log')</code> for this request:") ?> <code><?= h(acp_error_log_ini_path() !== '' ? acp_error_log_ini_path() : t_default('acp.error_log.empty_sapi_default', '(empty - using the SAPI default)')) ?></code></p>
110 </header>
111 <div class="acp-card-body">
112 <form method="post" class="acp-row">
113 <?= acp_csrf_field() ?>
114 <input type="hidden" name="do" value="save_path">
115 <div class="acp-field" style="flex:2;min-width:280px;">
116 <label class="acp-label" for="path"><?= t_default('acp.error_log.path_label', 'Path to the error log') ?></label>
117 <input class="acp-input" id="path" name="path" value="<?= h($configured) ?>" placeholder="<?= h(acp_error_log_ini_path()) ?>">
118 </div>
119 <div class="acp-actions">
120 <button class="acp-btn" type="submit"><i class="fa fa-check"></i> <?= t_default('acp.error_log.save', 'Save') ?></button>
121 <?php if ($configured !== ''): ?>
122 <form method="post" style="display:inline">
123 <?= acp_csrf_field() ?>
124 <input type="hidden" name="do" value="reset_path">
125 <button class="acp-btn acp-btn--ghost" type="submit"><?= t_default('acp.error_log.use_auto_detected', 'Use auto-detected path') ?></button>
126 </form>
127 <?php endif; ?>
128 </div>
129 </form>
130 </div>
131</section>
132
133<?php if ($hasFile): ?>
134 <section class="acp-card">
135 <header class="acp-card-head">
136 <h2><?= t_default('acp.error_log.latest_entries', 'Latest entries') ?></h2>
137 <p>
138 <code><?= h($path) ?></code>
139 · <?= h(number_format(@filesize($path) ?: 0)) ?> <?= t_default('acp.error_log.bytes', 'bytes') ?>
140 · <?= t_default('acp.error_log.last_modified', 'last modified') ?> <?= h(date('Y-m-d H:i:s', @filemtime($path) ?: time())) ?>
141 </p>
142 </header>
143 <div class="acp-card-body">
144 <div class="acp-row" style="margin-bottom:12px">
145 <?php foreach (array('100', '300', '1000') as $n): ?>
146 <a class="acp-btn <?= $linesWant === (int) $n ? '' : 'acp-btn--ghost' ?> acp-btn--sm" href="<?= h(acp_url($module, array('lines' => $n))) ?>"><?= t_default('acp.error_log.last_n', 'Last {n}', ['n' => $n]) ?></a>
147 <?php endforeach; ?>
148 <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url($module, array('lines' => (string) $linesWant))) ?>"><i class="fa fa-refresh"></i> <?= t_default('acp.error_log.refresh', 'Refresh') ?></a>
149 </div>
150 <?php if (!$rows): ?>
151 <?php acp_empty(t_default('acp.error_log.empty', 'The log file is empty.'), 'fa-file-text-o'); ?>
152 <?php else: ?>
153 <div class="acp-table-wrap" style="max-height:70vh;overflow:auto">
154 <table class="acp-table">
155 <tbody>
156 <?php foreach ($rows as $line): $sev = acp_error_log_severity($line); ?>
157 <tr>
158 <td style="font-family:monospace;font-size:12.5px;white-space:pre-wrap;overflow-wrap:anywhere<?= $sev === 'red' ? ';color:#c0392b' : ($sev === 'amber' ? ';color:#b8860b' : '') ?>"><?= h($line) ?></td>
159 </tr>
160 <?php endforeach; ?>
161 </tbody>
162 </table>
163 </div>
164 <?php endif; ?>
165 </div>
166 </section>
167<?php endif; ?>
168