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 * Returns path to changelog 70 * 71 * @return string path to file 72 */ 73 abstract protected function getChangelogFilename(); 74 75 /** 76 * Checks if the ID has old revisons 77 * @return boolean 78 */ 79 public function hasRevisions() 80 { 81 $logfile = $this->getChangelogFilename(); 82 return file_exists($logfile); 83 } 84 85 86 /** @var int */ 87 protected $chunk_size; 88 89 /** 90 * Set chunk size for file reading 91 * Chunk size zero let read whole file at once 92 * 93 * @param int $chunk_size maximum block size read from file 94 */ 95 public function setChunkSize($chunk_size) 96 { 97 if (!is_numeric($chunk_size)) $chunk_size = 0; 98 99 $this->chunk_size = (int)max($chunk_size, 0); 100 } 101 102 /** 103 * Returns lines from changelog. 104 * If file larger than $chuncksize, only chunck is read that could contain $rev. 105 * 106 * When reference timestamp $rev is outside time range of changelog, readloglines() will return 107 * lines in first or last chunk, but they obviously does not contain $rev. 108 * 109 * @param int $rev revision timestamp 110 * @return array|false 111 * if success returns array(fp, array(changeloglines), $head, $tail, $eof) 112 * where fp only defined for chuck reading, needs closing. 113 * otherwise false 114 */ 115 protected function readloglines($rev) 116 { 117 $file = $this->getChangelogFilename(); 118 119 if (!file_exists($file)) { 120 return false; 121 } 122 123 $fp = null; 124 $head = 0; 125 $tail = 0; 126 $eof = 0; 127 128 if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) { 129 // read whole file 130 $lines = file($file); 131 if ($lines === false) { 132 return false; 133 } 134 } else { 135 // read by chunk 136 $fp = fopen($file, 'rb'); // "file pointer" 137 if ($fp === false) { 138 return false; 139 } 140 $head = 0; 141 fseek($fp, 0, SEEK_END); 142 $eof = ftell($fp); 143 $tail = $eof; 144 145 // find chunk 146 while ($tail - $head > $this->chunk_size) { 147 $finger = $head + intval(($tail - $head) / 2); 148 $finger = $this->getNewlinepointer($fp, $finger); 149 $tmp = fgets($fp); 150 if ($finger == $head || $finger == $tail) { 151 break; 152 } 153 $info = $this->parseLogLine($tmp); 154 $finger_rev = $info['date']; 155 156 if ($finger_rev > $rev) { 157 $tail = $finger; 158 } else { 159 $head = $finger; 160 } 161 } 162 163 if ($tail - $head < 1) { 164 // cound not find chunk, assume requested rev is missing 165 fclose($fp); 166 return false; 167 } 168 169 $lines = $this->readChunk($fp, $head, $tail); 170 } 171 return array( 172 $fp, 173 $lines, 174 $head, 175 $tail, 176 $eof, 177 ); 178 } 179 180 /** 181 * Read chunk and return array with lines of given chunck. 182 * Has no check if $head and $tail are really at a new line 183 * 184 * @param resource $fp resource filepointer 185 * @param int $head start point chunck 186 * @param int $tail end point chunck 187 * @return array lines read from chunck 188 */ 189 protected function readChunk($fp, $head, $tail) 190 { 191 $chunk = ''; 192 $chunk_size = max($tail - $head, 0); // found chunk size 193 $got = 0; 194 fseek($fp, $head); 195 while ($got < $chunk_size && !feof($fp)) { 196 $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0)); 197 if ($tmp === false) { //error state 198 break; 199 } 200 $got += strlen($tmp); 201 $chunk .= $tmp; 202 } 203 $lines = explode("\n", $chunk); 204 array_pop($lines); // remove trailing newline 205 return $lines; 206 } 207 208 /** 209 * Set pointer to first new line after $finger and return its position 210 * 211 * @param resource $fp filepointer 212 * @param int $finger a pointer 213 * @return int pointer 214 */ 215 protected function getNewlinepointer($fp, $finger) 216 { 217 fseek($fp, $finger); 218 $nl = $finger; 219 if ($finger > 0) { 220 fgets($fp); // slip the finger forward to a new line 221 $nl = ftell($fp); 222 } 223 return $nl; 224 } 225 226 /** 227 * Returns the next lines of the changelog of the chunck before head or after tail 228 * 229 * @param resource $fp filepointer 230 * @param int $head position head of last chunk 231 * @param int $tail position tail of last chunk 232 * @param int $direction positive forward, negative backward 233 * @return array with entries: 234 * - $lines: changelog lines of readed chunk 235 * - $head: head of chunk 236 * - $tail: tail of chunk 237 */ 238 protected function readAdjacentChunk($fp, $head, $tail, $direction) 239 { 240 if (!$fp) return array(array(), $head, $tail); 241 242 if ($direction > 0) { 243 //read forward 244 $head = $tail; 245 $tail = $head + intval($this->chunk_size * (2 / 3)); 246 $tail = $this->getNewlinepointer($fp, $tail); 247 } else { 248 //read backward 249 $tail = $head; 250 $head = max($tail - $this->chunk_size, 0); 251 while (true) { 252 $nl = $this->getNewlinepointer($fp, $head); 253 // was the chunk big enough? if not, take another bite 254 if ($nl > 0 && $tail <= $nl) { 255 $head = max($head - $this->chunk_size, 0); 256 } else { 257 $head = $nl; 258 break; 259 } 260 } 261 } 262 263 //load next chunck 264 $lines = $this->readChunk($fp, $head, $tail); 265 return array($lines, $head, $tail); 266 } 267 268} 269