xref: /dokuwiki/inc/ChangeLog/ChangeLogTrait.php (revision 3d3f60569fc9f67e7ded6b3bf3f228a57af09e9a)
1<?php
2
3namespace dokuwiki\ChangeLog;
4
5/**
6 * Provides methods for handling of changelog
7 */
8trait ChangeLogTrait
9{
10    /**
11     * Adds an entry to the changelog file
12     *
13     * @return array added logline as revision info
14     */
15    abstract public function addLogEntry(array $info, $timestamp = null);
16
17    /**
18     * Parses a changelog line into it's components
19     *
20     * @author Ben Coburn <btcoburn@silicodon.net>
21     *
22     * @param string $line changelog line
23     * @return array|bool parsed line or false
24     */
25    public static function parseLogLine($line)
26    {
27        $info = explode("\t", rtrim($line, "\n"));
28        if ($info !== false && count($info) > 1) {
29            return $entry = array(
30                'date'  => (int)$info[0], // unix timestamp
31                'ip'    => $info[1], // IPv4 address (127.0.0.1)
32                'type'  => $info[2], // log line type
33                'id'    => $info[3], // page id
34                'user'  => $info[4], // user name
35                'sum'   => $info[5], // edit summary (or action reason)
36                'extra' => $info[6], // extra data (varies by line type)
37                'sizechange' => (isset($info[7]) && $info[7] !== '') ? (int)$info[7] : null, //
38            );
39        } else {
40            return false;
41        }
42    }
43
44    /**
45     * Build a changelog line from it's components
46     *
47     * @param array $info Revision info structure
48     * @param int $timestamp logline date (optional)
49     * @return string changelog line
50     */
51    public static function buildLogLine(array &$info, $timestamp = null)
52    {
53        $strip = ["\t", "\n"];
54        $entry = array(
55            'date'  => $timestamp ?? $info['date'],
56            'ip'    => $info['ip'],
57            'type'  => str_replace($strip, '', $info['type']),
58            'id'    => $info['id'],
59            'user'  => $info['user'],
60            'sum'   => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $info['sum']), 0, 255),
61            'extra' => str_replace($strip, '', $info['extra']),
62            'sizechange' => $info['sizechange'],
63        );
64        $info = $entry;
65        return $line = implode("\t", $entry) ."\n";
66    }
67
68
69    /** @var int */
70    protected $chunk_size;
71
72    /**
73     * Returns path to changelog
74     *
75     * @return string path to file
76     */
77    abstract protected function getChangelogFilename();
78
79
80    /**
81     * Set chunk size for file reading
82     * Chunk size zero let read whole file at once
83     *
84     * @param int $chunk_size maximum block size read from file
85     */
86    public function setChunkSize($chunk_size)
87    {
88        if (!is_numeric($chunk_size)) $chunk_size = 0;
89
90        $this->chunk_size = (int)max($chunk_size, 0);
91    }
92
93    /**
94     * Returns lines from changelog.
95     * If file larger than $chuncksize, only chunck is read that could contain $rev.
96     *
97     * When reference timestamp $rev is outside time range of changelog, readloglines() will return
98     * lines in first or last chunk, but they obviously does not contain $rev.
99     *
100     * @param int $rev revision timestamp
101     * @return array|false
102     *     if success returns array(fp, array(changeloglines), $head, $tail, $eof)
103     *     where fp only defined for chuck reading, needs closing.
104     *     otherwise false
105     */
106    protected function readloglines($rev)
107    {
108        $file = $this->getChangelogFilename();
109
110        if (!file_exists($file)) {
111            return false;
112        }
113
114        $fp = null;
115        $head = 0;
116        $tail = 0;
117        $eof = 0;
118
119        if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
120            // read whole file
121            $lines = file($file);
122            if ($lines === false) {
123                return false;
124            }
125        } else {
126            // read by chunk
127            $fp = fopen($file, 'rb'); // "file pointer"
128            if ($fp === false) {
129                return false;
130            }
131            $head = 0;
132            fseek($fp, 0, SEEK_END);
133            $eof = ftell($fp);
134            $tail = $eof;
135
136            // find chunk
137            while ($tail - $head > $this->chunk_size) {
138                $finger = $head + intval(($tail - $head) / 2);
139                $finger = $this->getNewlinepointer($fp, $finger);
140                $tmp = fgets($fp);
141                if ($finger == $head || $finger == $tail) {
142                    break;
143                }
144                $info = $this->parseLogLine($tmp);
145                $finger_rev = $info['date'];
146
147                if ($finger_rev > $rev) {
148                    $tail = $finger;
149                } else {
150                    $head = $finger;
151                }
152            }
153
154            if ($tail - $head < 1) {
155                // cound not find chunk, assume requested rev is missing
156                fclose($fp);
157                return false;
158            }
159
160            $lines = $this->readChunk($fp, $head, $tail);
161        }
162        return array(
163            $fp,
164            $lines,
165            $head,
166            $tail,
167            $eof,
168        );
169    }
170
171    /**
172     * Read chunk and return array with lines of given chunck.
173     * Has no check if $head and $tail are really at a new line
174     *
175     * @param resource $fp resource filepointer
176     * @param int $head start point chunck
177     * @param int $tail end point chunck
178     * @return array lines read from chunck
179     */
180    protected function readChunk($fp, $head, $tail)
181    {
182        $chunk = '';
183        $chunk_size = max($tail - $head, 0); // found chunk size
184        $got = 0;
185        fseek($fp, $head);
186        while ($got < $chunk_size && !feof($fp)) {
187            $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0));
188            if ($tmp === false) { //error state
189                break;
190            }
191            $got += strlen($tmp);
192            $chunk .= $tmp;
193        }
194        $lines = explode("\n", $chunk);
195        array_pop($lines); // remove trailing newline
196        return $lines;
197    }
198
199    /**
200     * Set pointer to first new line after $finger and return its position
201     *
202     * @param resource $fp filepointer
203     * @param int $finger a pointer
204     * @return int pointer
205     */
206    protected function getNewlinepointer($fp, $finger)
207    {
208        fseek($fp, $finger);
209        $nl = $finger;
210        if ($finger > 0) {
211            fgets($fp); // slip the finger forward to a new line
212            $nl = ftell($fp);
213        }
214        return $nl;
215    }
216
217    /**
218     * Returns the next lines of the changelog  of the chunck before head or after tail
219     *
220     * @param resource $fp filepointer
221     * @param int $head position head of last chunk
222     * @param int $tail position tail of last chunk
223     * @param int $direction positive forward, negative backward
224     * @return array with entries:
225     *    - $lines: changelog lines of readed chunk
226     *    - $head: head of chunk
227     *    - $tail: tail of chunk
228     */
229    protected function readAdjacentChunk($fp, $head, $tail, $direction)
230    {
231        if (!$fp) return array(array(), $head, $tail);
232
233        if ($direction > 0) {
234            //read forward
235            $head = $tail;
236            $tail = $head + intval($this->chunk_size * (2 / 3));
237            $tail = $this->getNewlinepointer($fp, $tail);
238        } else {
239            //read backward
240            $tail = $head;
241            $head = max($tail - $this->chunk_size, 0);
242            while (true) {
243                $nl = $this->getNewlinepointer($fp, $head);
244                // was the chunk big enough? if not, take another bite
245                if ($nl > 0 && $tail <= $nl) {
246                    $head = max($head - $this->chunk_size, 0);
247                } else {
248                    $head = $nl;
249                    break;
250                }
251            }
252        }
253
254        //load next chunck
255        $lines = $this->readChunk($fp, $head, $tail);
256        return array($lines, $head, $tail);
257    }
258
259}
260