1<?php
2
3/**
4 * Class helper_plugin_loglog_logging
5 */
6class helper_plugin_loglog_logging extends DokuWiki_Plugin
7{
8    protected $file = '';
9
10    public function __construct()
11    {
12        global $conf;
13        if(defined('DOKU_UNITTEST')) {
14            $this->file = DOKU_PLUGIN . 'loglog/_test/loglog.log';
15        } else {
16            $this->file = $conf['cachedir'] . '/loglog.log';
17        }
18    }
19
20    /**
21     * Build a log entry from passed data and write a single line to log file
22     *
23     * @param string $msg
24     * @param null $user
25     * @param array $data
26     */
27    public function writeLine($msg, $user = null, $data = [])
28    {
29        global $conf, $INPUT;
30
31        if (is_null($user)) $user = $INPUT->server->str('REMOTE_USER');
32        if (!$user) $user = $_REQUEST['u'];
33        if (!$user) return;
34
35        $t = time();
36        $ip = clientIP(true);
37        $data = !empty($data) ? json_encode($data) : '';
38
39        $line = join("\t", [$t, strftime($conf['dformat'], $t), $ip, $user, $msg, $data]);
40
41        io_saveFile($this->file, "$line\n", true);
42    }
43
44    /**
45     * Return logfile lines limited to specified $min - $max range
46     *
47     * @param int $min
48     * @param int $max
49     * @return array
50     */
51    public function readLines($min, $max)
52    {
53        $lines = [];
54        $candidateLines = $this->readChunks($min, $max);
55        foreach ($candidateLines as $line) {
56            if (empty($line)) continue; // Filter empty lines
57            $parsedLine = $this->loglineToArray($line);
58            if ($parsedLine['dt'] >= $min && $parsedLine['dt'] <= $max) {
59                $lines[] = $parsedLine;
60            }
61        }
62        return $lines;
63    }
64
65    /**
66     * Read log lines backwards. Start and end timestamps are used to evaluate
67     * only the chunks being read, NOT single lines. This method will return
68     * too many lines, the dates have to be checked by the caller again.
69     *
70     * @param int $min start time (in seconds)
71     * @param int $max end time (in seconds)
72     * @return array
73     */
74    protected function readChunks($min, $max)
75    {
76        $data = array();
77        $lines = array();
78        $chunk_size = 8192;
79
80        if (!@file_exists($this->file)) return $data;
81        $fp = fopen($this->file, 'rb');
82        if ($fp === false) return $data;
83
84        //seek to end
85        fseek($fp, 0, SEEK_END);
86        $pos = ftell($fp);
87        $chunk = '';
88
89        while ($pos) {
90
91            // how much to read? Set pointer
92            if ($pos > $chunk_size) {
93                $pos -= $chunk_size;
94                $read = $chunk_size;
95            } else {
96                $read = $pos;
97                $pos = 0;
98            }
99            fseek($fp, $pos);
100
101            $tmp = fread($fp, $read);
102            if ($tmp === false) break;
103            $chunk = $tmp . $chunk;
104
105            // now split the chunk
106            $cparts = explode("\n", $chunk);
107
108            // keep the first part in chunk (may be incomplete)
109            if ($pos) $chunk = array_shift($cparts);
110
111            // no more parts available, read on
112            if (!count($cparts)) continue;
113
114            // get date of first line:
115            list($cdate) = explode("\t", $cparts[0]);
116
117            if ($cdate > $max) continue; // haven't reached wanted area, yet
118
119            // put all the lines from the chunk on the stack
120            $lines = array_merge($cparts, $lines);
121
122            if ($cdate < $min) break; // we have enough
123        }
124        fclose($fp);
125
126        return $lines;
127    }
128
129    /**
130     * Convert log line to array
131     *
132     * @param string $line
133     * @return array
134     */
135    protected function loglineToArray($line)
136    {
137        list($dt, $junk, $ip, $user, $msg, $data) = explode("\t", $line, 6);
138        return [
139            'dt' => $dt, // timestamp
140            'ip' => $ip,
141            'user' => $user,
142            'msg' => $msg,
143            'data' => $data, // JSON encoded additional data
144        ];
145    }
146
147    /**
148     * Returns the number of lines where the given needle has been found in message
149     *
150     * @param array $lines
151     * @param string $msgNeedle
152     * @return mixed
153     */
154    public function countMatchingLines(array $lines, string $msgNeedle)
155    {
156        return array_reduce(
157            $lines,
158            function ($carry, $line) use ($msgNeedle) {
159                $carry = $carry + (int)(strpos($line['msg'], $msgNeedle) !== false);
160                return $carry;
161            },
162            0
163        );
164    }
165
166}
167