Merge remote-tracking branch 'refs/remotes/origin/3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / acp / page / ExceptionLogViewPage.class.php
1 <?php
2 namespace wcf\acp\page;
3 use wcf\page\AbstractPage;
4 use wcf\page\MultipleLinkPage;
5 use wcf\system\event\EventHandler;
6 use wcf\system\exception\IllegalLinkException;
7 use wcf\system\Regex;
8 use wcf\system\WCF;
9 use wcf\util\DirectoryUtil;
10 use wcf\util\JSON;
11 use wcf\util\StringUtil;
12
13 /**
14 * Shows the exception log.
15 *
16 * @author Tim Duesterhus
17 * @copyright 2001-2018 WoltLab GmbH
18 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
19 * @package WoltLabSuite\Core\Acp\Page
20 */
21 class ExceptionLogViewPage extends MultipleLinkPage {
22 /**
23 * @inheritDoc
24 */
25 public $activeMenuItem = 'wcf.acp.menu.link.log.exception';
26
27 /**
28 * @inheritDoc
29 */
30 public $neededPermissions = ['admin.management.canViewLog'];
31
32 /**
33 * @inheritDoc
34 */
35 public $itemsPerPage = 10;
36
37 /**
38 * given exceptionID
39 * @var string
40 */
41 public $exceptionID = '';
42
43 /**
44 * active logfile
45 * @var string
46 */
47 public $logFile = '';
48
49 /**
50 * available logfiles
51 * @var string[]
52 */
53 public $logFiles = [];
54
55 /**
56 * exceptions shown
57 * @var array
58 */
59 public $exceptions = [];
60
61 /**
62 * @inheritDoc
63 */
64 public function readParameters() {
65 parent::readParameters();
66
67 if (isset($_REQUEST['exceptionID'])) $this->exceptionID = StringUtil::trim($_REQUEST['exceptionID']);
68 if (isset($_REQUEST['logFile'])) $this->logFile = StringUtil::trim($_REQUEST['logFile']);
69 }
70
71 /**
72 * @inheritDoc
73 */
74 public function readData() {
75 AbstractPage::readData();
76
77 $fileNameRegex = new Regex('(?:^|/)\d{4}-\d{2}-\d{2}\.txt$');
78 $this->logFiles = DirectoryUtil::getInstance(WCF_DIR.'log/')->getFiles(SORT_DESC, $fileNameRegex);
79
80 if ($this->exceptionID) {
81 // search the appropriate file
82 foreach ($this->logFiles as $logFile) {
83 $contents = file_get_contents($logFile);
84
85 if (mb_strpos($contents, '<<<<<<<<'.$this->exceptionID.'<<<<') !== false) {
86 $fileNameRegex->match($logFile);
87 $matches = $fileNameRegex->getMatches();
88 $this->logFile = $matches[0];
89 break;
90 }
91
92 unset($contents);
93 }
94
95 if (!isset($contents)) {
96 $this->logFile = '';
97 return;
98 }
99 }
100 else if ($this->logFile) {
101 if (!$fileNameRegex->match(basename($this->logFile))) throw new IllegalLinkException();
102 if (!file_exists(WCF_DIR.'log/'.$this->logFile)) throw new IllegalLinkException();
103
104 $contents = file_get_contents(WCF_DIR.'log/'.$this->logFile);
105 }
106 else {
107 return;
108 }
109
110 // unify newlines
111 $contents = StringUtil::unifyNewlines($contents);
112
113 // split contents
114 $split = new Regex('(?:^|\n<<<<\n\n)(?:<<<<<<<<([a-f0-9]{40})<<<<\n|$)');
115 $contents = $split->split($contents, Regex::SPLIT_NON_EMPTY_ONLY | Regex::CAPTURE_SPLIT_DELIMITER);
116
117 // even items become keys, odd items become values
118 try {
119 $this->exceptions = call_user_func_array('array_merge', array_map(
120 function($v) {
121 return [$v[0] => $v[1]];
122 },
123 array_chunk($contents, 2)
124 ));
125 }
126 catch (\Exception $e) {
127 // logfile contents are pretty malformed, abort
128 return;
129 }
130
131 if ($this->exceptionID) $this->searchPage($this->exceptionID);
132 $this->calculateNumberOfPages();
133
134 $i = 0;
135 $exceptionRegex = new Regex("(?P<date>[MTWFS][a-z]{2}, \d{1,2} [JFMASOND][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4})\s*\n".
136 "Message: (?P<message>.*?)\s*\n".
137 "PHP version: (?P<phpVersion>.*?)\s*\n".
138 "WoltLab Suite version: (?P<wcfVersion>.*?)\s*\n".
139 "Request URI: (?P<requestURI>.*?)\s*\n".
140 "Referrer: (?P<referrer>.*?)\s*\n".
141 "User Agent: (?P<userAgent>.*?)\s*\n".
142 "Peak Memory Usage: (?<peakMemory>\d+)/(?<maxMemory>\d+)\s*\n".
143 "(?<chain>======\n".
144 ".*)", Regex::DOT_ALL);
145 $chainRegex = new Regex("======\n".
146 "Error Class: (?P<class>.*?)\s*\n".
147 "Error Message: (?P<message>.*?)\s*\n".
148 "Error Code: (?P<code>\d+)\s*\n".
149 "File: (?P<file>.*?) \((?P<line>\d+)\)\s*\n".
150 "Extra Information: (?P<information>(?:-|[a-zA-Z0-9+/]+={0,2}))\s*\n".
151 "Stack Trace: (?P<stack>\[[^\n]+\])", Regex::DOT_ALL);
152
153 $isPhp7 = version_compare(PHP_VERSION, '7.0.0') >= 0;
154 foreach ($this->exceptions as $key => $val) {
155 $i++;
156 if ($i < $this->startIndex || $i > $this->endIndex) {
157 unset($this->exceptions[$key]);
158 continue;
159 }
160
161 if (!$exceptionRegex->match($val)) {
162 unset($this->exceptions[$key]);
163 continue;
164 }
165 $matches = $exceptionRegex->getMatches();
166 $chainRegex->match($matches['chain'], true, Regex::ORDER_MATCH_BY_SET);
167
168 $chainMatches = array_map(function ($item) use ($isPhp7) {
169 if ($item['information'] === '-') $item['information'] = null;
170 else {
171 if ($isPhp7) {
172 $item['information'] = unserialize(base64_decode($item['information']), ['allowed_classes' => false]);
173 }
174 else {
175 $item['information'] = unserialize(base64_decode($item['information']));
176 }
177 }
178
179 $item['stack'] = JSON::decode($item['stack']);
180
181 return $item;
182 }, $chainRegex->getMatches());
183
184 $matches['chain'] = $chainMatches;
185 $this->exceptions[$key] = $matches;
186 }
187 }
188
189 /**
190 * @inheritDoc
191 */
192 public function countItems() {
193 // call countItems event
194 EventHandler::getInstance()->fireAction($this, 'countItems');
195
196 return count($this->exceptions);
197 }
198
199 /**
200 * Switches to the page containing the exception with the given ID.
201 *
202 * @param string $exceptionID
203 */
204 public function searchPage($exceptionID) {
205 $i = 1;
206
207 foreach ($this->exceptions as $key => $val) {
208 if ($key == $exceptionID) break;
209 $i++;
210 }
211
212 $this->pageNo = ceil($i / $this->itemsPerPage);
213 }
214
215 /**
216 * @inheritDoc
217 */
218 public function assignVariables() {
219 parent::assignVariables();
220
221 WCF::getTPL()->assign([
222 'exceptionID' => $this->exceptionID,
223 'logFiles' => array_flip(array_map('basename', $this->logFiles)),
224 'logFile' => $this->logFile,
225 'exceptions' => $this->exceptions
226 ]);
227 }
228 }