1 <?php
2 
3 use dokuwiki\Extension\AdminPlugin;
4 use dokuwiki\Form\Form;
5 use dokuwiki\Logger;
6 
7 /**
8  * DokuWiki Plugin logviewer (Admin Component)
9  *
10  * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
11  * @author  Andreas Gohr <andi@splitbrain.org>
12  */
13 class admin_plugin_logviewer extends AdminPlugin
14 {
15     protected const MAX_READ_SIZE = 1_048_576; // 1 MB
16 
17     protected $facilities;
18     protected $facility;
19     protected $date;
20 
21     /** @inheritDoc */
22     public function forAdminOnly()
23     {
24         return true;
25     }
26 
27     /** @inheritDoc */
28     public function handle()
29     {
30         global $INPUT;
31 
32         $this->facilities = $this->getFacilities();
33         $this->facility = $INPUT->str('facility');
34         if (!in_array($this->facility, $this->facilities)) {
35             $this->facility = $this->facilities[0];
36         }
37 
38         $this->date = $INPUT->str('date');
39         if (!preg_match('/^\d\d\d\d-\d\d-\d\d$/', $this->date)) {
40             $this->date = date('Y-m-d');
41         }
42     }
43 
44     /** @inheritDoc */
45     public function html()
46     {
47         echo '<div id="plugin__logviewer">';
48         echo $this->locale_xhtml('intro');
49         $this->displayTabs();
50         $this->displayLog();
51         echo '</div>';
52     }
53 
54     /**
55      * Show the navigational tabs and date picker
56      */
57     protected function displayTabs()
58     {
59         global $ID;
60 
61         $form = new Form(['method' => 'GET']);
62         $form->setHiddenField('do', 'admin');
63         $form->setHiddenField('page', 'logviewer');
64         $form->setHiddenField('facility', $this->facility);
65         $form->addTextInput('date', $this->getLang('date'))
66             ->attr('type', 'date')->val($this->date)->addClass('quickselect');
67         $form->addButton('submit', '>')->attr('type', 'submit');
68         echo $form->toHTML();
69 
70         echo '<ul class="tabs">';
71         foreach ($this->facilities as $facility) {
72             echo '<li>';
73             if ($facility == $this->facility) {
74                 echo '<strong>' . hsc($facility) . '</strong>';
75             } else {
76                 $link = wl(
77                     $ID,
78                     ['do' => 'admin', 'page' => 'logviewer', 'date' => $this->date, 'facility' => $facility]
79                 );
80                 echo '<a href="' . $link . '">' . hsc($facility) . '</a>';
81             }
82             echo '</li>';
83         }
84         echo '</ul>';
85     }
86 
87     /**
88      * Read and output the logfile contents
89      */
90     protected function displayLog()
91     {
92         $logfile = Logger::getInstance($this->facility)->getLogfile($this->date);
93         if (!file_exists($logfile)) {
94             echo $this->locale_xhtml('nolog');
95             return;
96         }
97 
98         try {
99             $lines = $this->getLogLines($logfile);
100             $this->printLogLines($lines);
101         } catch (Exception $e) {
102             msg($e->getMessage(), -1);
103         }
104     }
105 
106     /**
107      * Get the available logging facilities
108      *
109      * @return array
110      */
111     protected function getFacilities()
112     {
113         global $conf;
114 
115         // default facilities first
116         $facilities = [
117             Logger::LOG_ERROR,
118             Logger::LOG_DEPRECATED,
119             Logger::LOG_DEBUG,
120         ];
121 
122         // add all other dirs
123         $dirs = glob($conf['logdir'] . '/*', GLOB_ONLYDIR);
124         foreach ($dirs as $dir) {
125             $facilities[] = basename($dir);
126         }
127         $facilities = array_unique($facilities);
128 
129         return $facilities;
130     }
131 
132     /**
133      * Read the lines of the logfile and return them as array
134      *
135      * @param string $logfilePath
136      * @return array
137      * @throws Exception when reading fails
138      */
139     protected function getLogLines($logfilePath)
140     {
141         global $lang;
142         $size = filesize($logfilePath);
143         $fp = fopen($logfilePath, 'r');
144 
145         if (!$fp) throw new Exception($lang['log_file_failed_to_open']);
146 
147         try {
148             if ($size < self::MAX_READ_SIZE) {
149                 $toread = $size;
150             } else {
151                 $toread = self::MAX_READ_SIZE;
152                 fseek($fp, -$toread, SEEK_END);
153             }
154 
155             $logData = fread($fp, $toread);
156             if (!$logData) throw new Exception($lang['log_file_failed_to_read']);
157 
158             $lines = explode("\n", $logData);
159             unset($logData); // free memory early
160 
161             if ($size >= self::MAX_READ_SIZE) {
162                 array_shift($lines); // Discard the first line
163                 while ($lines !== [] && str_starts_with($lines[0], '  ')) {
164                     array_shift($lines); // Discard indented lines
165                 }
166 
167                 // A message to inform users that previous lines are skipped
168                 array_unshift($lines, "******\t" . "\t" . '[' . $lang['log_file_too_large'] . ']');
169             }
170         } finally {
171             fclose($fp);
172         }
173 
174         return $lines;
175     }
176 
177     /**
178      * Get an array of log lines and print them using appropriate styles
179      *
180      * @param array $lines
181      */
182     protected function printLogLines($lines)
183     {
184         $numberOfLines = count($lines);
185 
186         echo "<dl>";
187         for ($i = 0; $i < $numberOfLines; $i++) {
188             $line = $lines[$i];
189             if (str_starts_with($line, '  ')) {
190                 // lines indented by two spaces are details, aggregate them
191                 echo '<dd>';
192                 while (str_starts_with($line, '  ')) {
193                     echo hsc(substr($line, 2)) . '<br />';
194                     $i++;
195                     $line = $lines[$i] ?? '';
196                 }
197                 echo '</dd>';
198                 --$i; // rewind the counter
199             } else {
200                 // other lines are actual log lines in three parts
201                 [$dt, $file, $msg] = sexplode("\t", $line, 3, '');
202                 echo '<dt>';
203                 echo '<span class="datetime">' . hsc($dt) . '</span>';
204                 echo '<span class="log">';
205                 echo '<span class="msg">' . hsc($msg) . '</span>';
206                 echo '<span class="file">' . hsc($file) . '</span>';
207                 echo '</span>';
208                 echo '</dt>';
209             }
210         }
211         echo "</dl>";
212     }
213 }
214