1<?php 2 3namespace dokuwiki\ChangeLog; 4 5use dokuwiki\Utf8\PhpString; 6 7/** 8 * Provides methods for handling of changelog 9 */ 10trait 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 if (!is_array($info)) { 156 // skip unparsable lines, but still advance the search boundary 157 $head = $finger; 158 continue; 159 } 160 $finger_rev = $info['date']; 161 162 if ($finger_rev > $rev) { 163 $tail = $finger; 164 } else { 165 $head = $finger; 166 } 167 } 168 169 if ($tail - $head < 1) { 170 // could not find chunk, assume requested rev is missing 171 fclose($fp); 172 return false; 173 } 174 175 $lines = $this->readChunk($fp, $head, $tail); 176 } 177 return [$fp, $lines, $head, $tail, $eof]; 178 } 179 180 /** 181 * Read chunk and return array with lines of given chunk. 182 * Has no check if $head and $tail are really at a new line 183 * 184 * @param resource $fp resource file pointer 185 * @param int $head start point chunk 186 * @param int $tail end point chunk 187 * @return array lines read from chunk 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 file pointer 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 chunk before head or after tail 228 * 229 * @param resource $fp file pointer 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 read 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 [[], $head, $tail]; 241 242 if ($direction > 0) { 243 //read forward 244 $head = $tail; 245 $tail = $head + (int)($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 chunk 264 $lines = $this->readChunk($fp, $head, $tail); 265 return [$lines, $head, $tail]; 266 } 267} 268