More adjusted API tests
[dokuwiki.git] / inc / ChangeLog / ChangeLogTrait.php
blobf10c53d8b2e6008bd9983491aa65b2300ac7cfe9
1 <?php
3 namespace dokuwiki\ChangeLog;
5 use dokuwiki\Utf8\PhpString;
7 /**
8 * Provides methods for handling of changelog
9 */
10 trait ChangeLogTrait
12 /**
13 * Adds an entry to the changelog file
15 * @return array added log line as revision info
17 abstract public function addLogEntry(array $info, $timestamp = null);
19 /**
20 * Parses a changelog line into its components
22 * @param string $line changelog line
23 * @return array|bool parsed line or false
24 * @author Ben Coburn <btcoburn@silicodon.net>
27 public static function parseLogLine($line)
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
41 } else {
42 return false;
46 /**
47 * Build a changelog line from its components
49 * @param array $info Revision info structure
50 * @param int $timestamp log line date (optional)
51 * @return string changelog line
53 public static function buildLogLine(array &$info, $timestamp = null)
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']
66 $info = $entry;
67 return implode("\t", $entry) . "\n";
70 /**
71 * Returns path to changelog
73 * @return string path to file
75 abstract protected function getChangelogFilename();
77 /**
78 * Checks if the ID has old revisions
79 * @return boolean
81 public function hasRevisions()
83 $logfile = $this->getChangelogFilename();
84 return file_exists($logfile);
88 /** @var int */
89 protected $chunk_size;
91 /**
92 * Set chunk size for file reading
93 * Chunk size zero let read whole file at once
95 * @param int $chunk_size maximum block size read from file
97 public function setChunkSize($chunk_size)
99 if (!is_numeric($chunk_size)) $chunk_size = 0;
101 $this->chunk_size = max($chunk_size, 0);
105 * Returns lines from changelog.
106 * If file larger than $chunk_size, only chunk is read that could contain $rev.
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.
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
117 protected function readloglines($rev)
119 $file = $this->getChangelogFilename();
121 if (!file_exists($file)) {
122 return false;
125 $fp = null;
126 $head = 0;
127 $tail = 0;
128 $eof = 0;
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;
136 } else {
137 // read by chunk
138 $fp = fopen($file, 'rb'); // "file pointer"
139 if ($fp === false) {
140 return false;
142 fseek($fp, 0, SEEK_END);
143 $eof = ftell($fp);
144 $tail = $eof;
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;
154 $info = $this->parseLogLine($tmp);
155 $finger_rev = $info['date'];
157 if ($finger_rev > $rev) {
158 $tail = $finger;
159 } else {
160 $head = $finger;
164 if ($tail - $head < 1) {
165 // could not find chunk, assume requested rev is missing
166 fclose($fp);
167 return false;
170 $lines = $this->readChunk($fp, $head, $tail);
172 return [$fp, $lines, $head, $tail, $eof];
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
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
184 protected function readChunk($fp, $head, $tail)
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;
195 $got += strlen($tmp);
196 $chunk .= $tmp;
198 $lines = explode("\n", $chunk);
199 array_pop($lines); // remove trailing newline
200 return $lines;
204 * Set pointer to first new line after $finger and return its position
206 * @param resource $fp file pointer
207 * @param int $finger a pointer
208 * @return int pointer
210 protected function getNewlinepointer($fp, $finger)
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);
218 return $nl;
222 * Returns the next lines of the changelog of the chunk before head or after tail
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
233 protected function readAdjacentChunk($fp, $head, $tail, $direction)
235 if (!$fp) return [[], $head, $tail];
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;
258 //load next chunk
259 $lines = $this->readChunk($fp, $head, $tail);
260 return [$lines, $head, $tail];