11d11f1d3SSatoshi Sahara<?php 21d11f1d3SSatoshi Sahara 31d11f1d3SSatoshi Saharanamespace dokuwiki\ChangeLog; 41d11f1d3SSatoshi Sahara 51d11f1d3SSatoshi Sahara/** 61d11f1d3SSatoshi Sahara * Provides methods for handling of changelog 71d11f1d3SSatoshi Sahara */ 81d11f1d3SSatoshi Saharatrait ChangeLogTrait 91d11f1d3SSatoshi Sahara{ 101d11f1d3SSatoshi Sahara /** 111d11f1d3SSatoshi Sahara * Adds an entry to the changelog file 121d11f1d3SSatoshi Sahara * 131d11f1d3SSatoshi Sahara * @return array added logline as revision info 141d11f1d3SSatoshi Sahara */ 151d11f1d3SSatoshi Sahara abstract public function addLogEntry(array $info, $timestamp = null); 161d11f1d3SSatoshi Sahara 171d11f1d3SSatoshi Sahara /** 181d11f1d3SSatoshi Sahara * Parses a changelog line into it's components 191d11f1d3SSatoshi Sahara * 201d11f1d3SSatoshi Sahara * @author Ben Coburn <btcoburn@silicodon.net> 211d11f1d3SSatoshi Sahara * 221d11f1d3SSatoshi Sahara * @param string $line changelog line 231d11f1d3SSatoshi Sahara * @return array|bool parsed line or false 241d11f1d3SSatoshi Sahara */ 251d11f1d3SSatoshi Sahara public static function parseLogLine($line) 261d11f1d3SSatoshi Sahara { 271d11f1d3SSatoshi Sahara $info = explode("\t", rtrim($line, "\n")); 281d11f1d3SSatoshi Sahara if ($info !== false && count($info) > 1) { 291d11f1d3SSatoshi Sahara return $entry = array( 301d11f1d3SSatoshi Sahara 'date' => (int)$info[0], // unix timestamp 311d11f1d3SSatoshi Sahara 'ip' => $info[1], // IPv4 address (127.0.0.1) 321d11f1d3SSatoshi Sahara 'type' => $info[2], // log line type 331d11f1d3SSatoshi Sahara 'id' => $info[3], // page id 341d11f1d3SSatoshi Sahara 'user' => $info[4], // user name 351d11f1d3SSatoshi Sahara 'sum' => $info[5], // edit summary (or action reason) 361d11f1d3SSatoshi Sahara 'extra' => $info[6], // extra data (varies by line type) 371d11f1d3SSatoshi Sahara 'sizechange' => (isset($info[7]) && $info[7] !== '') ? (int)$info[7] : null, // 381d11f1d3SSatoshi Sahara ); 391d11f1d3SSatoshi Sahara } else { 401d11f1d3SSatoshi Sahara return false; 411d11f1d3SSatoshi Sahara } 421d11f1d3SSatoshi Sahara } 431d11f1d3SSatoshi Sahara 441d11f1d3SSatoshi Sahara /** 451d11f1d3SSatoshi Sahara * Build a changelog line from it's components 461d11f1d3SSatoshi Sahara * 471d11f1d3SSatoshi Sahara * @param array $info Revision info structure 481d11f1d3SSatoshi Sahara * @param int $timestamp logline date (optional) 491d11f1d3SSatoshi Sahara * @return string changelog line 501d11f1d3SSatoshi Sahara */ 511d11f1d3SSatoshi Sahara public static function buildLogLine(array &$info, $timestamp = null) 521d11f1d3SSatoshi Sahara { 531d11f1d3SSatoshi Sahara $strip = ["\t", "\n"]; 541d11f1d3SSatoshi Sahara $entry = array( 551d11f1d3SSatoshi Sahara 'date' => $timestamp ?? $info['date'], 561d11f1d3SSatoshi Sahara 'ip' => $info['ip'], 571d11f1d3SSatoshi Sahara 'type' => str_replace($strip, '', $info['type']), 581d11f1d3SSatoshi Sahara 'id' => $info['id'], 591d11f1d3SSatoshi Sahara 'user' => $info['user'], 601d11f1d3SSatoshi Sahara 'sum' => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $info['sum']), 0, 255), 611d11f1d3SSatoshi Sahara 'extra' => str_replace($strip, '', $info['extra']), 621d11f1d3SSatoshi Sahara 'sizechange' => $info['sizechange'], 631d11f1d3SSatoshi Sahara ); 641d11f1d3SSatoshi Sahara $info = $entry; 651d11f1d3SSatoshi Sahara return $line = implode("\t", $entry) ."\n"; 661d11f1d3SSatoshi Sahara } 671d11f1d3SSatoshi Sahara 681d11f1d3SSatoshi Sahara /** 691d11f1d3SSatoshi Sahara * Returns path to changelog 701d11f1d3SSatoshi Sahara * 711d11f1d3SSatoshi Sahara * @return string path to file 721d11f1d3SSatoshi Sahara */ 731d11f1d3SSatoshi Sahara abstract protected function getChangelogFilename(); 741d11f1d3SSatoshi Sahara 75*a3a0fc58SSatoshi Sahara /** 76*a3a0fc58SSatoshi Sahara * Checks if the ID has old revisons 77*a3a0fc58SSatoshi Sahara * @return boolean 78*a3a0fc58SSatoshi Sahara */ 79*a3a0fc58SSatoshi Sahara public function hasRevisions() 80*a3a0fc58SSatoshi Sahara { 81*a3a0fc58SSatoshi Sahara $logfile = $this->getChangelogFilename(); 82*a3a0fc58SSatoshi Sahara return file_exists($logfile); 83*a3a0fc58SSatoshi Sahara } 84*a3a0fc58SSatoshi Sahara 85*a3a0fc58SSatoshi Sahara 86*a3a0fc58SSatoshi Sahara /** @var int */ 87*a3a0fc58SSatoshi Sahara protected $chunk_size; 881d11f1d3SSatoshi Sahara 891d11f1d3SSatoshi Sahara /** 901d11f1d3SSatoshi Sahara * Set chunk size for file reading 911d11f1d3SSatoshi Sahara * Chunk size zero let read whole file at once 921d11f1d3SSatoshi Sahara * 931d11f1d3SSatoshi Sahara * @param int $chunk_size maximum block size read from file 941d11f1d3SSatoshi Sahara */ 951d11f1d3SSatoshi Sahara public function setChunkSize($chunk_size) 961d11f1d3SSatoshi Sahara { 971d11f1d3SSatoshi Sahara if (!is_numeric($chunk_size)) $chunk_size = 0; 981d11f1d3SSatoshi Sahara 991d11f1d3SSatoshi Sahara $this->chunk_size = (int)max($chunk_size, 0); 1001d11f1d3SSatoshi Sahara } 1011d11f1d3SSatoshi Sahara 1021d11f1d3SSatoshi Sahara /** 1031d11f1d3SSatoshi Sahara * Returns lines from changelog. 1041d11f1d3SSatoshi Sahara * If file larger than $chuncksize, only chunck is read that could contain $rev. 1051d11f1d3SSatoshi Sahara * 1061d11f1d3SSatoshi Sahara * When reference timestamp $rev is outside time range of changelog, readloglines() will return 1071d11f1d3SSatoshi Sahara * lines in first or last chunk, but they obviously does not contain $rev. 1081d11f1d3SSatoshi Sahara * 1091d11f1d3SSatoshi Sahara * @param int $rev revision timestamp 1101d11f1d3SSatoshi Sahara * @return array|false 1111d11f1d3SSatoshi Sahara * if success returns array(fp, array(changeloglines), $head, $tail, $eof) 1121d11f1d3SSatoshi Sahara * where fp only defined for chuck reading, needs closing. 1131d11f1d3SSatoshi Sahara * otherwise false 1141d11f1d3SSatoshi Sahara */ 1151d11f1d3SSatoshi Sahara protected function readloglines($rev) 1161d11f1d3SSatoshi Sahara { 1171d11f1d3SSatoshi Sahara $file = $this->getChangelogFilename(); 1181d11f1d3SSatoshi Sahara 1191d11f1d3SSatoshi Sahara if (!file_exists($file)) { 1201d11f1d3SSatoshi Sahara return false; 1211d11f1d3SSatoshi Sahara } 1221d11f1d3SSatoshi Sahara 1231d11f1d3SSatoshi Sahara $fp = null; 1241d11f1d3SSatoshi Sahara $head = 0; 1251d11f1d3SSatoshi Sahara $tail = 0; 1261d11f1d3SSatoshi Sahara $eof = 0; 1271d11f1d3SSatoshi Sahara 1281d11f1d3SSatoshi Sahara if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) { 1291d11f1d3SSatoshi Sahara // read whole file 1301d11f1d3SSatoshi Sahara $lines = file($file); 1311d11f1d3SSatoshi Sahara if ($lines === false) { 1321d11f1d3SSatoshi Sahara return false; 1331d11f1d3SSatoshi Sahara } 1341d11f1d3SSatoshi Sahara } else { 1351d11f1d3SSatoshi Sahara // read by chunk 1361d11f1d3SSatoshi Sahara $fp = fopen($file, 'rb'); // "file pointer" 1371d11f1d3SSatoshi Sahara if ($fp === false) { 1381d11f1d3SSatoshi Sahara return false; 1391d11f1d3SSatoshi Sahara } 1401d11f1d3SSatoshi Sahara $head = 0; 1411d11f1d3SSatoshi Sahara fseek($fp, 0, SEEK_END); 1421d11f1d3SSatoshi Sahara $eof = ftell($fp); 1431d11f1d3SSatoshi Sahara $tail = $eof; 1441d11f1d3SSatoshi Sahara 1451d11f1d3SSatoshi Sahara // find chunk 1461d11f1d3SSatoshi Sahara while ($tail - $head > $this->chunk_size) { 1471d11f1d3SSatoshi Sahara $finger = $head + intval(($tail - $head) / 2); 1481d11f1d3SSatoshi Sahara $finger = $this->getNewlinepointer($fp, $finger); 1491d11f1d3SSatoshi Sahara $tmp = fgets($fp); 1501d11f1d3SSatoshi Sahara if ($finger == $head || $finger == $tail) { 1511d11f1d3SSatoshi Sahara break; 1521d11f1d3SSatoshi Sahara } 1531d11f1d3SSatoshi Sahara $info = $this->parseLogLine($tmp); 1541d11f1d3SSatoshi Sahara $finger_rev = $info['date']; 1551d11f1d3SSatoshi Sahara 1561d11f1d3SSatoshi Sahara if ($finger_rev > $rev) { 1571d11f1d3SSatoshi Sahara $tail = $finger; 1581d11f1d3SSatoshi Sahara } else { 1591d11f1d3SSatoshi Sahara $head = $finger; 1601d11f1d3SSatoshi Sahara } 1611d11f1d3SSatoshi Sahara } 1621d11f1d3SSatoshi Sahara 1631d11f1d3SSatoshi Sahara if ($tail - $head < 1) { 1641d11f1d3SSatoshi Sahara // cound not find chunk, assume requested rev is missing 1651d11f1d3SSatoshi Sahara fclose($fp); 1661d11f1d3SSatoshi Sahara return false; 1671d11f1d3SSatoshi Sahara } 1681d11f1d3SSatoshi Sahara 1691d11f1d3SSatoshi Sahara $lines = $this->readChunk($fp, $head, $tail); 1701d11f1d3SSatoshi Sahara } 1711d11f1d3SSatoshi Sahara return array( 1721d11f1d3SSatoshi Sahara $fp, 1731d11f1d3SSatoshi Sahara $lines, 1741d11f1d3SSatoshi Sahara $head, 1751d11f1d3SSatoshi Sahara $tail, 1761d11f1d3SSatoshi Sahara $eof, 1771d11f1d3SSatoshi Sahara ); 1781d11f1d3SSatoshi Sahara } 1791d11f1d3SSatoshi Sahara 1801d11f1d3SSatoshi Sahara /** 1811d11f1d3SSatoshi Sahara * Read chunk and return array with lines of given chunck. 1821d11f1d3SSatoshi Sahara * Has no check if $head and $tail are really at a new line 1831d11f1d3SSatoshi Sahara * 1841d11f1d3SSatoshi Sahara * @param resource $fp resource filepointer 1851d11f1d3SSatoshi Sahara * @param int $head start point chunck 1861d11f1d3SSatoshi Sahara * @param int $tail end point chunck 1871d11f1d3SSatoshi Sahara * @return array lines read from chunck 1881d11f1d3SSatoshi Sahara */ 1891d11f1d3SSatoshi Sahara protected function readChunk($fp, $head, $tail) 1901d11f1d3SSatoshi Sahara { 1911d11f1d3SSatoshi Sahara $chunk = ''; 1921d11f1d3SSatoshi Sahara $chunk_size = max($tail - $head, 0); // found chunk size 1931d11f1d3SSatoshi Sahara $got = 0; 1941d11f1d3SSatoshi Sahara fseek($fp, $head); 1951d11f1d3SSatoshi Sahara while ($got < $chunk_size && !feof($fp)) { 1961d11f1d3SSatoshi Sahara $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0)); 1971d11f1d3SSatoshi Sahara if ($tmp === false) { //error state 1981d11f1d3SSatoshi Sahara break; 1991d11f1d3SSatoshi Sahara } 2001d11f1d3SSatoshi Sahara $got += strlen($tmp); 2011d11f1d3SSatoshi Sahara $chunk .= $tmp; 2021d11f1d3SSatoshi Sahara } 2031d11f1d3SSatoshi Sahara $lines = explode("\n", $chunk); 2041d11f1d3SSatoshi Sahara array_pop($lines); // remove trailing newline 2051d11f1d3SSatoshi Sahara return $lines; 2061d11f1d3SSatoshi Sahara } 2071d11f1d3SSatoshi Sahara 2081d11f1d3SSatoshi Sahara /** 2091d11f1d3SSatoshi Sahara * Set pointer to first new line after $finger and return its position 2101d11f1d3SSatoshi Sahara * 2111d11f1d3SSatoshi Sahara * @param resource $fp filepointer 2121d11f1d3SSatoshi Sahara * @param int $finger a pointer 2131d11f1d3SSatoshi Sahara * @return int pointer 2141d11f1d3SSatoshi Sahara */ 2151d11f1d3SSatoshi Sahara protected function getNewlinepointer($fp, $finger) 2161d11f1d3SSatoshi Sahara { 2171d11f1d3SSatoshi Sahara fseek($fp, $finger); 2181d11f1d3SSatoshi Sahara $nl = $finger; 2191d11f1d3SSatoshi Sahara if ($finger > 0) { 2201d11f1d3SSatoshi Sahara fgets($fp); // slip the finger forward to a new line 2211d11f1d3SSatoshi Sahara $nl = ftell($fp); 2221d11f1d3SSatoshi Sahara } 2231d11f1d3SSatoshi Sahara return $nl; 2241d11f1d3SSatoshi Sahara } 2251d11f1d3SSatoshi Sahara 2261d11f1d3SSatoshi Sahara /** 2271d11f1d3SSatoshi Sahara * Returns the next lines of the changelog of the chunck before head or after tail 2281d11f1d3SSatoshi Sahara * 2291d11f1d3SSatoshi Sahara * @param resource $fp filepointer 2301d11f1d3SSatoshi Sahara * @param int $head position head of last chunk 2311d11f1d3SSatoshi Sahara * @param int $tail position tail of last chunk 2321d11f1d3SSatoshi Sahara * @param int $direction positive forward, negative backward 2331d11f1d3SSatoshi Sahara * @return array with entries: 2341d11f1d3SSatoshi Sahara * - $lines: changelog lines of readed chunk 2351d11f1d3SSatoshi Sahara * - $head: head of chunk 2361d11f1d3SSatoshi Sahara * - $tail: tail of chunk 2371d11f1d3SSatoshi Sahara */ 2381d11f1d3SSatoshi Sahara protected function readAdjacentChunk($fp, $head, $tail, $direction) 2391d11f1d3SSatoshi Sahara { 2401d11f1d3SSatoshi Sahara if (!$fp) return array(array(), $head, $tail); 2411d11f1d3SSatoshi Sahara 2421d11f1d3SSatoshi Sahara if ($direction > 0) { 2431d11f1d3SSatoshi Sahara //read forward 2441d11f1d3SSatoshi Sahara $head = $tail; 2451d11f1d3SSatoshi Sahara $tail = $head + intval($this->chunk_size * (2 / 3)); 2461d11f1d3SSatoshi Sahara $tail = $this->getNewlinepointer($fp, $tail); 2471d11f1d3SSatoshi Sahara } else { 2481d11f1d3SSatoshi Sahara //read backward 2491d11f1d3SSatoshi Sahara $tail = $head; 2501d11f1d3SSatoshi Sahara $head = max($tail - $this->chunk_size, 0); 2511d11f1d3SSatoshi Sahara while (true) { 2521d11f1d3SSatoshi Sahara $nl = $this->getNewlinepointer($fp, $head); 2531d11f1d3SSatoshi Sahara // was the chunk big enough? if not, take another bite 2541d11f1d3SSatoshi Sahara if ($nl > 0 && $tail <= $nl) { 2551d11f1d3SSatoshi Sahara $head = max($head - $this->chunk_size, 0); 2561d11f1d3SSatoshi Sahara } else { 2571d11f1d3SSatoshi Sahara $head = $nl; 2581d11f1d3SSatoshi Sahara break; 2591d11f1d3SSatoshi Sahara } 2601d11f1d3SSatoshi Sahara } 2611d11f1d3SSatoshi Sahara } 2621d11f1d3SSatoshi Sahara 2631d11f1d3SSatoshi Sahara //load next chunck 2641d11f1d3SSatoshi Sahara $lines = $this->readChunk($fp, $head, $tail); 2651d11f1d3SSatoshi Sahara return array($lines, $head, $tail); 2661d11f1d3SSatoshi Sahara } 2671d11f1d3SSatoshi Sahara 2681d11f1d3SSatoshi Sahara} 269