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 $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