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