10c3a5702SAndreas Gohr<?php 20c3a5702SAndreas Gohr 30c3a5702SAndreas Gohrnamespace dokuwiki\ChangeLog; 40c3a5702SAndreas Gohr 566f4cdd4SSatoshi Saharause dokuwiki\Logger; 666f4cdd4SSatoshi Sahara 70c3a5702SAndreas Gohr/** 81d11f1d3SSatoshi Sahara * ChangeLog Prototype; methods for handling changelog 90c3a5702SAndreas Gohr */ 100c3a5702SAndreas Gohrabstract class ChangeLog 110c3a5702SAndreas Gohr{ 121d11f1d3SSatoshi Sahara use ChangeLogTrait; 131d11f1d3SSatoshi Sahara 140c3a5702SAndreas Gohr /** @var string */ 150c3a5702SAndreas Gohr protected $id; 1679a2d784SGerrit Uitslag /** @var false|int */ 17bd17ac90SSatoshi Sahara protected $currentRevision; 180c3a5702SAndreas Gohr /** @var array */ 190603e565SAndreas Gohr protected $cache = []; 200c3a5702SAndreas Gohr 210c3a5702SAndreas Gohr /** 220c3a5702SAndreas Gohr * Constructor 230c3a5702SAndreas Gohr * 240c3a5702SAndreas Gohr * @param string $id page id 250c3a5702SAndreas Gohr * @param int $chunk_size maximum block size read from file 260c3a5702SAndreas Gohr */ 270c3a5702SAndreas Gohr public function __construct($id, $chunk_size = 8192) 280c3a5702SAndreas Gohr { 290c3a5702SAndreas Gohr global $cache_revinfo; 300c3a5702SAndreas Gohr 310c3a5702SAndreas Gohr $this->cache =& $cache_revinfo; 320c3a5702SAndreas Gohr if (!isset($this->cache[$id])) { 330603e565SAndreas Gohr $this->cache[$id] = []; 340c3a5702SAndreas Gohr } 350c3a5702SAndreas Gohr 360c3a5702SAndreas Gohr $this->id = $id; 370c3a5702SAndreas Gohr $this->setChunkSize($chunk_size); 380c3a5702SAndreas Gohr } 390c3a5702SAndreas Gohr 400c3a5702SAndreas Gohr /** 410c3a5702SAndreas Gohr * Returns path to current page/media 420c3a5702SAndreas Gohr * 4383bec475SAndreas Gohr * @param string|int $rev empty string or revision timestamp 440c3a5702SAndreas Gohr * @return string path to file 450c3a5702SAndreas Gohr */ 4683bec475SAndreas Gohr abstract protected function getFilename($rev = ''); 470c3a5702SAndreas Gohr 48df7627d6SSatoshi Sahara /** 49a835c93aSGerrit Uitslag * Returns mode 50a835c93aSGerrit Uitslag * 51a835c93aSGerrit Uitslag * @return string RevisionInfo::MODE_MEDIA or RevisionInfo::MODE_PAGE 52a835c93aSGerrit Uitslag */ 53a835c93aSGerrit Uitslag abstract protected function getMode(); 54a835c93aSGerrit Uitslag 55a835c93aSGerrit Uitslag /** 56*01e8d739SAndreas Gohr * Returns path to the global changelog file (the cross-page recent-changes feed) 57*01e8d739SAndreas Gohr * 58*01e8d739SAndreas Gohr * @return string path to file 59*01e8d739SAndreas Gohr */ 60*01e8d739SAndreas Gohr abstract protected function getGlobalChangelogFilename(); 61*01e8d739SAndreas Gohr 62*01e8d739SAndreas Gohr /** 63df7627d6SSatoshi Sahara * Check whether given revision is the current page 64df7627d6SSatoshi Sahara * 65df7627d6SSatoshi Sahara * @param int $rev timestamp of current page 66df7627d6SSatoshi Sahara * @return bool true if $rev is current revision, otherwise false 67df7627d6SSatoshi Sahara */ 68df7627d6SSatoshi Sahara public function isCurrentRevision($rev) 69df7627d6SSatoshi Sahara { 70df7627d6SSatoshi Sahara return $rev == $this->currentRevision(); 71df7627d6SSatoshi Sahara } 72df7627d6SSatoshi Sahara 73df7627d6SSatoshi Sahara /** 74df7627d6SSatoshi Sahara * Checks if the revision is last revision 75df7627d6SSatoshi Sahara * 76df7627d6SSatoshi Sahara * @param int $rev revision timestamp 77df7627d6SSatoshi Sahara * @return bool true if $rev is last revision, otherwise false 78df7627d6SSatoshi Sahara */ 79df7627d6SSatoshi Sahara public function isLastRevision($rev = null) 80df7627d6SSatoshi Sahara { 81df7627d6SSatoshi Sahara return $rev === $this->lastRevision(); 82df7627d6SSatoshi Sahara } 83df7627d6SSatoshi Sahara 84df7627d6SSatoshi Sahara /** 85eeda7adaSGerrit Uitslag * Return the current revision identifier 8605282e9fSSatoshi Sahara * 8705282e9fSSatoshi Sahara * The "current" revision means current version of the page or media file. It is either 8805282e9fSSatoshi Sahara * identical with or newer than the "last" revision, that depends on whether the file 8905282e9fSSatoshi Sahara * has modified, created or deleted outside of DokuWiki. 9005282e9fSSatoshi Sahara * The value of identifier can be determined by timestamp as far as the file exists, 9105282e9fSSatoshi Sahara * otherwise it must be assigned larger than any other revisions to keep them sortable. 9205282e9fSSatoshi Sahara * 9305282e9fSSatoshi Sahara * @return int|false revision timestamp 94df7627d6SSatoshi Sahara */ 95df7627d6SSatoshi Sahara public function currentRevision() 96df7627d6SSatoshi Sahara { 97df7627d6SSatoshi Sahara if (!isset($this->currentRevision)) { 98df7627d6SSatoshi Sahara // set ChangeLog::currentRevision property 99df7627d6SSatoshi Sahara $this->getCurrentRevisionInfo(); 100df7627d6SSatoshi Sahara } 101df7627d6SSatoshi Sahara return $this->currentRevision; 102df7627d6SSatoshi Sahara } 103df7627d6SSatoshi Sahara 104df7627d6SSatoshi Sahara /** 105eeda7adaSGerrit Uitslag * Return the last revision identifier, date value of the last entry of the changelog 106d154755dSSatoshi Sahara * 10705282e9fSSatoshi Sahara * @return int|false revision timestamp 108df7627d6SSatoshi Sahara */ 109df7627d6SSatoshi Sahara public function lastRevision() 110df7627d6SSatoshi Sahara { 111df7627d6SSatoshi Sahara $revs = $this->getRevisions(-1, 1); 112df7627d6SSatoshi Sahara return empty($revs) ? false : $revs[0]; 113df7627d6SSatoshi Sahara } 114df7627d6SSatoshi Sahara 1150c3a5702SAndreas Gohr /** 116a835c93aSGerrit Uitslag * Parses a changelog line into its components and save revision info to the cache pool 117b82f2411SSatoshi Sahara * 118a835c93aSGerrit Uitslag * @param string $value changelog line 119a835c93aSGerrit Uitslag * @return array|bool parsed line or false 120b82f2411SSatoshi Sahara */ 121a835c93aSGerrit Uitslag protected function parseAndCacheLogLine($value) 122b82f2411SSatoshi Sahara { 123a835c93aSGerrit Uitslag $info = static::parseLogLine($value); 124a835c93aSGerrit Uitslag if (is_array($info)) { 125a835c93aSGerrit Uitslag $info['mode'] = $this->getMode(); 1260603e565SAndreas Gohr $this->cache[$this->id][$info['date']] ??= $info; 127a835c93aSGerrit Uitslag return $info; 128a835c93aSGerrit Uitslag } 129a835c93aSGerrit Uitslag return false; 130b82f2411SSatoshi Sahara } 131b82f2411SSatoshi Sahara 132b82f2411SSatoshi Sahara /** 133d154755dSSatoshi Sahara * Get the changelog information for a specific revision (timestamp) 1340c3a5702SAndreas Gohr * 1350c3a5702SAndreas Gohr * Adjacent changelog lines are optimistically parsed and cached to speed up 1360c3a5702SAndreas Gohr * consecutive calls to getRevisionInfo. For large changelog files, only the chunk 1370c3a5702SAndreas Gohr * containing the requested changelog line is read. 1380c3a5702SAndreas Gohr * 1390c3a5702SAndreas Gohr * @param int $rev revision timestamp 14086216bf0SGerrit Uitslag * @param bool $retrieveCurrentRevInfo allows to skip for getting other revision info in the 14186216bf0SGerrit Uitslag * getCurrentRevisionInfo() where $currentRevision is not yet determined 1420c3a5702SAndreas Gohr * @return bool|array false or array with entries: 1430c3a5702SAndreas Gohr * - date: unix timestamp 1440c3a5702SAndreas Gohr * - ip: IPv4 address (127.0.0.1) 1450c3a5702SAndreas Gohr * - type: log line type 1460c3a5702SAndreas Gohr * - id: page id 1470c3a5702SAndreas Gohr * - user: user name 1480c3a5702SAndreas Gohr * - sum: edit summary (or action reason) 1490c3a5702SAndreas Gohr * - extra: extra data (varies by line type) 150bd17ac90SSatoshi Sahara * - sizechange: change of filesize 151a835c93aSGerrit Uitslag * additional: 152a835c93aSGerrit Uitslag * - mode: page or media 1530c3a5702SAndreas Gohr * 1540c3a5702SAndreas Gohr * @author Ben Coburn <btcoburn@silicodon.net> 1550c3a5702SAndreas Gohr * @author Kate Arzamastseva <pshns@ukr.net> 1560c3a5702SAndreas Gohr */ 15786216bf0SGerrit Uitslag public function getRevisionInfo($rev, $retrieveCurrentRevInfo = true) 1580c3a5702SAndreas Gohr { 159a3984ddfSSatoshi Sahara $rev = max(0, $rev); 160a3984ddfSSatoshi Sahara if (!$rev) return false; 1610c3a5702SAndreas Gohr 16286216bf0SGerrit Uitslag //ensure the external edits are cached as well 16386216bf0SGerrit Uitslag if (!isset($this->currentRevision) && $retrieveCurrentRevInfo) { 16486216bf0SGerrit Uitslag $this->getCurrentRevisionInfo(); 16586216bf0SGerrit Uitslag } 16686216bf0SGerrit Uitslag 1670c3a5702SAndreas Gohr // check if it's already in the memory cache 16885160059SGerrit Uitslag if (isset($this->cache[$this->id][$rev])) { 1690c3a5702SAndreas Gohr return $this->cache[$this->id][$rev]; 1700c3a5702SAndreas Gohr } 1710c3a5702SAndreas Gohr 1720c3a5702SAndreas Gohr //read lines from changelog 173d41f5a8fSAndreas Gohr $result = $this->readloglines($rev); 174d41f5a8fSAndreas Gohr if ($result === false) return false; 175d41f5a8fSAndreas Gohr [$fp, $lines] = $result; 1760c3a5702SAndreas Gohr if ($fp) { 1770c3a5702SAndreas Gohr fclose($fp); 1780c3a5702SAndreas Gohr } 1790c3a5702SAndreas Gohr if (empty($lines)) return false; 1800c3a5702SAndreas Gohr 1810c3a5702SAndreas Gohr // parse and cache changelog lines 182a835c93aSGerrit Uitslag foreach ($lines as $line) { 183a835c93aSGerrit Uitslag $this->parseAndCacheLogLine($line); 1840c3a5702SAndreas Gohr } 18585160059SGerrit Uitslag 18685160059SGerrit Uitslag return $this->cache[$this->id][$rev] ?? false; 1870c3a5702SAndreas Gohr } 1880c3a5702SAndreas Gohr 1890c3a5702SAndreas Gohr /** 1900c3a5702SAndreas Gohr * Return a list of page revisions numbers 1910c3a5702SAndreas Gohr * 1920c3a5702SAndreas Gohr * Does not guarantee that the revision exists in the attic, 1930c3a5702SAndreas Gohr * only that a line with the date exists in the changelog. 1940c3a5702SAndreas Gohr * By default the current revision is skipped. 1950c3a5702SAndreas Gohr * 1960c3a5702SAndreas Gohr * The current revision is automatically skipped when the page exists. 1970c3a5702SAndreas Gohr * See $INFO['meta']['last_change'] for the current revision. 1980c3a5702SAndreas Gohr * A negative $first let read the current revision too. 1990c3a5702SAndreas Gohr * 2000c3a5702SAndreas Gohr * For efficiency, the log lines are parsed and cached for later 2010c3a5702SAndreas Gohr * calls to getRevisionInfo. Large changelog files are read 2020c3a5702SAndreas Gohr * backwards in chunks until the requested number of changelog 203eeda7adaSGerrit Uitslag * lines are received. 2040c3a5702SAndreas Gohr * 2050c3a5702SAndreas Gohr * @param int $first skip the first n changelog lines 2060c3a5702SAndreas Gohr * @param int $num number of revisions to return 2070c3a5702SAndreas Gohr * @return array with the revision timestamps 2080c3a5702SAndreas Gohr * 2090c3a5702SAndreas Gohr * @author Ben Coburn <btcoburn@silicodon.net> 2100c3a5702SAndreas Gohr * @author Kate Arzamastseva <pshns@ukr.net> 2110c3a5702SAndreas Gohr */ 2120c3a5702SAndreas Gohr public function getRevisions($first, $num) 2130c3a5702SAndreas Gohr { 2140603e565SAndreas Gohr $revs = []; 2150603e565SAndreas Gohr $lines = []; 2160c3a5702SAndreas Gohr $count = 0; 2170c3a5702SAndreas Gohr 218d154755dSSatoshi Sahara $logfile = $this->getChangelogFilename(); 219d154755dSSatoshi Sahara if (!file_exists($logfile)) return $revs; 220d154755dSSatoshi Sahara 2210c3a5702SAndreas Gohr $num = max($num, 0); 2220c3a5702SAndreas Gohr if ($num == 0) { 2230c3a5702SAndreas Gohr return $revs; 2240c3a5702SAndreas Gohr } 2250c3a5702SAndreas Gohr 2260c3a5702SAndreas Gohr if ($first < 0) { 2270c3a5702SAndreas Gohr $first = 0; 2280c3a5702SAndreas Gohr } else { 229df7627d6SSatoshi Sahara $fileLastMod = $this->getFilename(); 230df7627d6SSatoshi Sahara if (file_exists($fileLastMod) && $this->isLastRevision(filemtime($fileLastMod))) { 231df7627d6SSatoshi Sahara // skip last revision if the page exists 2320c3a5702SAndreas Gohr $first = max($first + 1, 0); 2330c3a5702SAndreas Gohr } 2340c3a5702SAndreas Gohr } 2350c3a5702SAndreas Gohr 236d154755dSSatoshi Sahara if (filesize($logfile) < $this->chunk_size || $this->chunk_size == 0) { 2370c3a5702SAndreas Gohr // read whole file 238d154755dSSatoshi Sahara $lines = file($logfile); 2390c3a5702SAndreas Gohr if ($lines === false) { 2400c3a5702SAndreas Gohr return $revs; 2410c3a5702SAndreas Gohr } 2420c3a5702SAndreas Gohr } else { 2430c3a5702SAndreas Gohr // read chunks backwards 244d154755dSSatoshi Sahara $fp = fopen($logfile, 'rb'); // "file pointer" 2450c3a5702SAndreas Gohr if ($fp === false) { 2460c3a5702SAndreas Gohr return $revs; 2470c3a5702SAndreas Gohr } 2480c3a5702SAndreas Gohr fseek($fp, 0, SEEK_END); 2490c3a5702SAndreas Gohr $tail = ftell($fp); 2500c3a5702SAndreas Gohr 2510c3a5702SAndreas Gohr // chunk backwards 2520c3a5702SAndreas Gohr $finger = max($tail - $this->chunk_size, 0); 2530c3a5702SAndreas Gohr while ($count < $num + $first) { 2540c3a5702SAndreas Gohr $nl = $this->getNewlinepointer($fp, $finger); 2550c3a5702SAndreas Gohr 2560c3a5702SAndreas Gohr // was the chunk big enough? if not, take another bite 2570c3a5702SAndreas Gohr if ($nl > 0 && $tail <= $nl) { 2580c3a5702SAndreas Gohr $finger = max($finger - $this->chunk_size, 0); 2590c3a5702SAndreas Gohr continue; 2600c3a5702SAndreas Gohr } else { 2610c3a5702SAndreas Gohr $finger = $nl; 2620c3a5702SAndreas Gohr } 2630c3a5702SAndreas Gohr 2640c3a5702SAndreas Gohr // read chunk 2650c3a5702SAndreas Gohr $chunk = ''; 2660c3a5702SAndreas Gohr $read_size = max($tail - $finger, 0); // found chunk size 2670c3a5702SAndreas Gohr $got = 0; 2680c3a5702SAndreas Gohr while ($got < $read_size && !feof($fp)) { 2690c3a5702SAndreas Gohr $tmp = @fread($fp, max(min($this->chunk_size, $read_size - $got), 0)); 2700c3a5702SAndreas Gohr if ($tmp === false) { 2710c3a5702SAndreas Gohr break; 2720c3a5702SAndreas Gohr } //error state 2730c3a5702SAndreas Gohr $got += strlen($tmp); 2740c3a5702SAndreas Gohr $chunk .= $tmp; 2750c3a5702SAndreas Gohr } 2760c3a5702SAndreas Gohr $tmp = explode("\n", $chunk); 2770c3a5702SAndreas Gohr array_pop($tmp); // remove trailing newline 2780c3a5702SAndreas Gohr 2790c3a5702SAndreas Gohr // combine with previous chunk 2800c3a5702SAndreas Gohr $count += count($tmp); 2810603e565SAndreas Gohr $lines = [...$tmp, ...$lines]; 2820c3a5702SAndreas Gohr 2830c3a5702SAndreas Gohr // next chunk 2840c3a5702SAndreas Gohr if ($finger == 0) { 2850c3a5702SAndreas Gohr break; 286e24a74c0SAndreas Gohr } else { // already read all the lines 2870c3a5702SAndreas Gohr $tail = $finger; 2880c3a5702SAndreas Gohr $finger = max($tail - $this->chunk_size, 0); 2890c3a5702SAndreas Gohr } 2900c3a5702SAndreas Gohr } 2910c3a5702SAndreas Gohr fclose($fp); 2920c3a5702SAndreas Gohr } 2930c3a5702SAndreas Gohr 2940c3a5702SAndreas Gohr // skip parsing extra lines 2950c3a5702SAndreas Gohr $num = max(min(count($lines) - $first, $num), 0); 2960c3a5702SAndreas Gohr if ($first > 0 && $num > 0) { 2970c3a5702SAndreas Gohr $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num); 298df7627d6SSatoshi Sahara } elseif ($first > 0 && $num == 0) { 2990c3a5702SAndreas Gohr $lines = array_slice($lines, 0, max(count($lines) - $first, 0)); 3000c3a5702SAndreas Gohr } elseif ($first == 0 && $num > 0) { 3010c3a5702SAndreas Gohr $lines = array_slice($lines, max(count($lines) - $num, 0)); 3020c3a5702SAndreas Gohr } 3030c3a5702SAndreas Gohr 3040c3a5702SAndreas Gohr // handle lines in reverse order 3050c3a5702SAndreas Gohr for ($i = count($lines) - 1; $i >= 0; $i--) { 306a835c93aSGerrit Uitslag $info = $this->parseAndCacheLogLine($lines[$i]); 307a835c93aSGerrit Uitslag if (is_array($info)) { 308bd17ac90SSatoshi Sahara $revs[] = $info['date']; 3090c3a5702SAndreas Gohr } 3100c3a5702SAndreas Gohr } 3110c3a5702SAndreas Gohr 3120c3a5702SAndreas Gohr return $revs; 3130c3a5702SAndreas Gohr } 3140c3a5702SAndreas Gohr 3150c3a5702SAndreas Gohr /** 316eeda7adaSGerrit Uitslag * Get the nth revision left or right-hand side for a specific page id and revision (timestamp) 3170c3a5702SAndreas Gohr * 3180c3a5702SAndreas Gohr * For large changelog files, only the chunk containing the 319eeda7adaSGerrit Uitslag * reference revision $rev is read and sometimes a next chunk. 3200c3a5702SAndreas Gohr * 3210c3a5702SAndreas Gohr * Adjacent changelog lines are optimistically parsed and cached to speed up 3220c3a5702SAndreas Gohr * consecutive calls to getRevisionInfo. 3230c3a5702SAndreas Gohr * 324d154755dSSatoshi Sahara * @param int $rev revision timestamp used as start date 325d154755dSSatoshi Sahara * (doesn't need to be exact revision number) 326d154755dSSatoshi Sahara * @param int $direction give position of returned revision with respect to $rev; 327d154755dSSatoshi Sahara positive=next, negative=prev 3280c3a5702SAndreas Gohr * @return bool|int 3290c3a5702SAndreas Gohr * timestamp of the requested revision 3300c3a5702SAndreas Gohr * otherwise false 3310c3a5702SAndreas Gohr */ 3320c3a5702SAndreas Gohr public function getRelativeRevision($rev, $direction) 3330c3a5702SAndreas Gohr { 3340c3a5702SAndreas Gohr $rev = max($rev, 0); 3350c3a5702SAndreas Gohr $direction = (int)$direction; 3360c3a5702SAndreas Gohr 3370c3a5702SAndreas Gohr //no direction given or last rev, so no follow-up 3380c3a5702SAndreas Gohr if (!$direction || ($direction > 0 && $this->isCurrentRevision($rev))) { 3390c3a5702SAndreas Gohr return false; 3400c3a5702SAndreas Gohr } 3410c3a5702SAndreas Gohr 3420c3a5702SAndreas Gohr //get lines from changelog 343d41f5a8fSAndreas Gohr $result = $this->readloglines($rev); 344d41f5a8fSAndreas Gohr if ($result === false) return false; 345d41f5a8fSAndreas Gohr [$fp, $lines, $head, $tail, $eof] = $result; 3460c3a5702SAndreas Gohr if (empty($lines)) return false; 3470c3a5702SAndreas Gohr 3485d9428a0SSatoshi Sahara // look for revisions later/earlier than $rev, when founded count till the wanted revision is reached 3490c3a5702SAndreas Gohr // also parse and cache changelog lines for getRevisionInfo(). 350eeda7adaSGerrit Uitslag $revCounter = 0; 351eeda7adaSGerrit Uitslag $relativeRev = false; 352eeda7adaSGerrit Uitslag $checkOtherChunk = true; //always runs once 353eeda7adaSGerrit Uitslag while (!$relativeRev && $checkOtherChunk) { 3540603e565SAndreas Gohr $info = []; 3550c3a5702SAndreas Gohr //parse in normal or reverse order 3560c3a5702SAndreas Gohr $count = count($lines); 3570c3a5702SAndreas Gohr if ($direction > 0) { 3580c3a5702SAndreas Gohr $start = 0; 3590c3a5702SAndreas Gohr $step = 1; 3600c3a5702SAndreas Gohr } else { 3610c3a5702SAndreas Gohr $start = $count - 1; 3620c3a5702SAndreas Gohr $step = -1; 3630c3a5702SAndreas Gohr } 3640603e565SAndreas Gohr for ($i = $start; $i >= 0 && $i < $count; $i += $step) { 365a835c93aSGerrit Uitslag $info = $this->parseAndCacheLogLine($lines[$i]); 366a835c93aSGerrit Uitslag if (is_array($info)) { 3670c3a5702SAndreas Gohr //look for revs older/earlier then reference $rev and select $direction-th one 368bd17ac90SSatoshi Sahara if (($direction > 0 && $info['date'] > $rev) || ($direction < 0 && $info['date'] < $rev)) { 369eeda7adaSGerrit Uitslag $revCounter++; 370eeda7adaSGerrit Uitslag if ($revCounter == abs($direction)) { 371eeda7adaSGerrit Uitslag $relativeRev = $info['date']; 3720c3a5702SAndreas Gohr } 3730c3a5702SAndreas Gohr } 3740c3a5702SAndreas Gohr } 3750c3a5702SAndreas Gohr } 3760c3a5702SAndreas Gohr 3770c3a5702SAndreas Gohr //true when $rev is found, but not the wanted follow-up. 378eeda7adaSGerrit Uitslag $checkOtherChunk = $fp 379eeda7adaSGerrit Uitslag && ($info['date'] == $rev || ($revCounter > 0 && !$relativeRev)) 3800603e565SAndreas Gohr && (!($tail == $eof && $direction > 0) && !($head == 0 && $direction < 0)); 3810c3a5702SAndreas Gohr 382eeda7adaSGerrit Uitslag if ($checkOtherChunk) { 3830603e565SAndreas Gohr [$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, $direction); 3840c3a5702SAndreas Gohr 3850c3a5702SAndreas Gohr if (empty($lines)) break; 3860c3a5702SAndreas Gohr } 3870c3a5702SAndreas Gohr } 3880c3a5702SAndreas Gohr if ($fp) { 3890c3a5702SAndreas Gohr fclose($fp); 3900c3a5702SAndreas Gohr } 3910c3a5702SAndreas Gohr 392eeda7adaSGerrit Uitslag return $relativeRev; 3930c3a5702SAndreas Gohr } 3940c3a5702SAndreas Gohr 3950c3a5702SAndreas Gohr /** 3960c3a5702SAndreas Gohr * Returns revisions around rev1 and rev2 3970c3a5702SAndreas Gohr * When available it returns $max entries for each revision 3980c3a5702SAndreas Gohr * 3990c3a5702SAndreas Gohr * @param int $rev1 oldest revision timestamp 4000c3a5702SAndreas Gohr * @param int $rev2 newest revision timestamp (0 looks up last revision) 4010c3a5702SAndreas Gohr * @param int $max maximum number of revisions returned 4020c3a5702SAndreas Gohr * @return array with two arrays with revisions surrounding rev1 respectively rev2 4030c3a5702SAndreas Gohr */ 4040c3a5702SAndreas Gohr public function getRevisionsAround($rev1, $rev2, $max = 50) 4050c3a5702SAndreas Gohr { 4060603e565SAndreas Gohr $max = (int) (abs($max) / 2) * 2 + 1; 4070c3a5702SAndreas Gohr $rev1 = max($rev1, 0); 4080c3a5702SAndreas Gohr $rev2 = max($rev2, 0); 4090c3a5702SAndreas Gohr 4100c3a5702SAndreas Gohr if ($rev2) { 4110c3a5702SAndreas Gohr if ($rev2 < $rev1) { 4120c3a5702SAndreas Gohr $rev = $rev2; 4130c3a5702SAndreas Gohr $rev2 = $rev1; 4140c3a5702SAndreas Gohr $rev1 = $rev; 4150c3a5702SAndreas Gohr } 4160c3a5702SAndreas Gohr } else { 4170c3a5702SAndreas Gohr //empty right side means a removed page. Look up last revision. 418bd17ac90SSatoshi Sahara $rev2 = $this->currentRevision(); 4190c3a5702SAndreas Gohr } 4200c3a5702SAndreas Gohr //collect revisions around rev2 421d41f5a8fSAndreas Gohr $result2 = $this->retrieveRevisionsAround($rev2, $max); 422d41f5a8fSAndreas Gohr if ($result2 === false) return [[], []]; 423d41f5a8fSAndreas Gohr [$revs2, $allRevs, $fp, $lines, $head, $tail] = $result2; 4240c3a5702SAndreas Gohr 4250603e565SAndreas Gohr if (empty($revs2)) return [[], []]; 4260c3a5702SAndreas Gohr 4270c3a5702SAndreas Gohr //collect revisions around rev1 4280f8604a9SAndreas Gohr $index = array_search($rev1, $allRevs); 4290c3a5702SAndreas Gohr if ($index === false) { 4300c3a5702SAndreas Gohr //no overlapping revisions 431d41f5a8fSAndreas Gohr $result1 = $this->retrieveRevisionsAround($rev1, $max); 432d41f5a8fSAndreas Gohr if ($result1 === false) { 433d41f5a8fSAndreas Gohr $revs1 = []; 434d41f5a8fSAndreas Gohr } else { 435d41f5a8fSAndreas Gohr [$revs1, , , , , ] = $result1; 4360603e565SAndreas Gohr if (empty($revs1)) $revs1 = []; 437d41f5a8fSAndreas Gohr } 4380c3a5702SAndreas Gohr } else { 4390c3a5702SAndreas Gohr //revisions overlaps, reuse revisions around rev2 440eeda7adaSGerrit Uitslag $lastRev = array_pop($allRevs); //keep last entry that could be external edit 441eeda7adaSGerrit Uitslag $revs1 = $allRevs; 4420c3a5702SAndreas Gohr while ($head > 0) { 4430c3a5702SAndreas Gohr for ($i = count($lines) - 1; $i >= 0; $i--) { 444a835c93aSGerrit Uitslag $info = $this->parseAndCacheLogLine($lines[$i]); 445a835c93aSGerrit Uitslag if (is_array($info)) { 446bd17ac90SSatoshi Sahara $revs1[] = $info['date']; 4470c3a5702SAndreas Gohr $index++; 4480c3a5702SAndreas Gohr 44985160059SGerrit Uitslag if ($index > (int) ($max / 2)) { 45085160059SGerrit Uitslag break 2; 45185160059SGerrit Uitslag } 4520c3a5702SAndreas Gohr } 4530c3a5702SAndreas Gohr } 4540c3a5702SAndreas Gohr 4550603e565SAndreas Gohr [$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, -1); 4560c3a5702SAndreas Gohr } 4570c3a5702SAndreas Gohr sort($revs1); 458eeda7adaSGerrit Uitslag $revs1[] = $lastRev; //push back last entry 4595d9428a0SSatoshi Sahara 4600c3a5702SAndreas Gohr //return wanted selection 4610603e565SAndreas Gohr $revs1 = array_slice($revs1, max($index - (int) ($max / 2), 0), $max); 4620c3a5702SAndreas Gohr } 4630c3a5702SAndreas Gohr 4640603e565SAndreas Gohr return [array_reverse($revs1), array_reverse($revs2)]; 4650c3a5702SAndreas Gohr } 4660c3a5702SAndreas Gohr 4670c3a5702SAndreas Gohr /** 4680c3a5702SAndreas Gohr * Return an existing revision for a specific date which is 4690c3a5702SAndreas Gohr * the current one or younger or equal then the date 4700c3a5702SAndreas Gohr * 4710c3a5702SAndreas Gohr * @param number $date_at timestamp 4720c3a5702SAndreas Gohr * @return string revision ('' for current) 4730c3a5702SAndreas Gohr */ 4740c3a5702SAndreas Gohr public function getLastRevisionAt($date_at) 4750c3a5702SAndreas Gohr { 476d154755dSSatoshi Sahara $fileLastMod = $this->getFilename(); 4770c3a5702SAndreas Gohr //requested date_at(timestamp) younger or equal then modified_time($this->id) => load current 478d154755dSSatoshi Sahara if (file_exists($fileLastMod) && $date_at >= @filemtime($fileLastMod)) { 4790c3a5702SAndreas Gohr return ''; 4800603e565SAndreas Gohr } elseif ($rev = $this->getRelativeRevision($date_at + 1, -1)) { 4810603e565SAndreas Gohr //+1 to get also the requested date revision 4820c3a5702SAndreas Gohr return $rev; 4830c3a5702SAndreas Gohr } else { 4840c3a5702SAndreas Gohr return false; 4850c3a5702SAndreas Gohr } 4860c3a5702SAndreas Gohr } 4870c3a5702SAndreas Gohr 4880c3a5702SAndreas Gohr /** 4890c3a5702SAndreas Gohr * Collect the $max revisions near to the timestamp $rev 4900c3a5702SAndreas Gohr * 491bd17ac90SSatoshi Sahara * Ideally, half of retrieved timestamps are older than $rev, another half are newer. 492eeda7adaSGerrit Uitslag * The returned array $requestedRevs may not contain the reference timestamp $rev 493bd17ac90SSatoshi Sahara * when it does not match any revision value recorded in changelog. 494bd17ac90SSatoshi Sahara * 4950c3a5702SAndreas Gohr * @param int $rev revision timestamp 4960c3a5702SAndreas Gohr * @param int $max maximum number of revisions to be returned 4970c3a5702SAndreas Gohr * @return bool|array 4980c3a5702SAndreas Gohr * return array with entries: 499eeda7adaSGerrit Uitslag * - $requestedRevs: array of with $max revision timestamps 5000c3a5702SAndreas Gohr * - $revs: all parsed revision timestamps 5010c3a5702SAndreas Gohr * - $fp: file pointer only defined for chuck reading, needs closing. 5020c3a5702SAndreas Gohr * - $lines: non-parsed changelog lines before the parsed revisions 503eeda7adaSGerrit Uitslag * - $head: position of first read changelog line 504eeda7adaSGerrit Uitslag * - $lastTail: position of end of last read changelog line 5050c3a5702SAndreas Gohr * otherwise false 5060c3a5702SAndreas Gohr */ 5070c3a5702SAndreas Gohr protected function retrieveRevisionsAround($rev, $max) 5080c3a5702SAndreas Gohr { 5090603e565SAndreas Gohr $revs = []; 5100603e565SAndreas Gohr $afterCount = 0; 5110603e565SAndreas Gohr $beforeCount = 0; 512a3984ddfSSatoshi Sahara 5130c3a5702SAndreas Gohr //get lines from changelog 514d41f5a8fSAndreas Gohr $result = $this->readloglines($rev); 515d41f5a8fSAndreas Gohr if ($result === false) return false; 516d41f5a8fSAndreas Gohr [$fp, $lines, $startHead, $startTail, $eof] = $result; 517bd17ac90SSatoshi Sahara if (empty($lines)) return false; 5180c3a5702SAndreas Gohr 519bd17ac90SSatoshi Sahara //parse changelog lines in chunk, and read forward more chunks until $max/2 is reached 520eeda7adaSGerrit Uitslag $head = $startHead; 521eeda7adaSGerrit Uitslag $tail = $startTail; 5220c3a5702SAndreas Gohr while (count($lines) > 0) { 5230c3a5702SAndreas Gohr foreach ($lines as $line) { 524a835c93aSGerrit Uitslag $info = $this->parseAndCacheLogLine($line); 525a835c93aSGerrit Uitslag if (is_array($info)) { 526bd17ac90SSatoshi Sahara $revs[] = $info['date']; 527bd17ac90SSatoshi Sahara if ($info['date'] >= $rev) { 5280c3a5702SAndreas Gohr //count revs after reference $rev 529eeda7adaSGerrit Uitslag $afterCount++; 53085160059SGerrit Uitslag if ($afterCount == 1) { 53185160059SGerrit Uitslag $beforeCount = count($revs); 53285160059SGerrit Uitslag } 5330c3a5702SAndreas Gohr } 5340c3a5702SAndreas Gohr //enough revs after reference $rev? 53585160059SGerrit Uitslag if ($afterCount > (int) ($max / 2)) { 53685160059SGerrit Uitslag break 2; 53785160059SGerrit Uitslag } 5380c3a5702SAndreas Gohr } 5390c3a5702SAndreas Gohr } 5400c3a5702SAndreas Gohr //retrieve next chunk 5410603e565SAndreas Gohr [$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, 1); 5420c3a5702SAndreas Gohr } 543eeda7adaSGerrit Uitslag $lastTail = $tail; 5440c3a5702SAndreas Gohr 545bd17ac90SSatoshi Sahara // add a possible revision of external edit, create or deletion 5467d34963bSAndreas Gohr if ( 5477d34963bSAndreas Gohr $lastTail == $eof && $afterCount <= (int) ($max / 2) && 548df7627d6SSatoshi Sahara count($revs) && !$this->isCurrentRevision($revs[count($revs) - 1]) 549df7627d6SSatoshi Sahara ) { 550bd17ac90SSatoshi Sahara $revs[] = $this->currentRevision; 551eeda7adaSGerrit Uitslag $afterCount++; 552bd17ac90SSatoshi Sahara } 553bd17ac90SSatoshi Sahara 554eeda7adaSGerrit Uitslag if ($afterCount == 0) { 555bd17ac90SSatoshi Sahara //given timestamp $rev is newer than the most recent line in chunk 556bd17ac90SSatoshi Sahara return false; //FIXME: or proceed to collect older revisions? 557bd17ac90SSatoshi Sahara } 558bd17ac90SSatoshi Sahara 559bd17ac90SSatoshi Sahara //read more chunks backward until $max/2 is reached and total number of revs is equal to $max 5600603e565SAndreas Gohr $lines = []; 5610c3a5702SAndreas Gohr $i = 0; 562eeda7adaSGerrit Uitslag $head = $startHead; 563eeda7adaSGerrit Uitslag $tail = $startTail; 5640c3a5702SAndreas Gohr while ($head > 0) { 5650603e565SAndreas Gohr [$lines, $head, $tail] = $this->readAdjacentChunk($fp, $head, $tail, -1); 5660c3a5702SAndreas Gohr 5670c3a5702SAndreas Gohr for ($i = count($lines) - 1; $i >= 0; $i--) { 568a835c93aSGerrit Uitslag $info = $this->parseAndCacheLogLine($lines[$i]); 569a835c93aSGerrit Uitslag if (is_array($info)) { 570bd17ac90SSatoshi Sahara $revs[] = $info['date']; 571eeda7adaSGerrit Uitslag $beforeCount++; 5720c3a5702SAndreas Gohr //enough revs before reference $rev? 57385160059SGerrit Uitslag if ($beforeCount > max((int) ($max / 2), $max - $afterCount)) { 57485160059SGerrit Uitslag break 2; 57585160059SGerrit Uitslag } 5760c3a5702SAndreas Gohr } 5770c3a5702SAndreas Gohr } 5780c3a5702SAndreas Gohr } 5790c3a5702SAndreas Gohr //keep only non-parsed lines 5800c3a5702SAndreas Gohr $lines = array_slice($lines, 0, $i); 581bd17ac90SSatoshi Sahara 582bd17ac90SSatoshi Sahara sort($revs); 583bd17ac90SSatoshi Sahara 5840c3a5702SAndreas Gohr //trunk desired selection 585eeda7adaSGerrit Uitslag $requestedRevs = array_slice($revs, -$max, $max); 5860c3a5702SAndreas Gohr 5870603e565SAndreas Gohr return [$requestedRevs, $revs, $fp, $lines, $head, $lastTail]; 5880c3a5702SAndreas Gohr } 589a3984ddfSSatoshi Sahara 590a3984ddfSSatoshi Sahara /** 591bd17ac90SSatoshi Sahara * Get the current revision information, considering external edit, create or deletion 592bd17ac90SSatoshi Sahara * 593a19054e9SSatoshi Sahara * When the file has not modified since its last revision, the information of the last 59405282e9fSSatoshi Sahara * change that had already recorded in the changelog is returned as current change info. 595a19054e9SSatoshi Sahara * Otherwise, the change information since the last revision caused outside DokuWiki 59605282e9fSSatoshi Sahara * should be returned, which is referred as "external revision". 597bd17ac90SSatoshi Sahara * 598*01e8d739SAndreas Gohr * External revisions are persisted to the changelog (and, for non-delete cases, copied 599*01e8d739SAndreas Gohr * to the attic) on first detection so subsequent reads see one canonical entry instead 600*01e8d739SAndreas Gohr * of recomputing a synthesized one. If persistence fails (e.g. the data dir is not 601*01e8d739SAndreas Gohr * writable in the current process context), the in-memory synthesized entry is still 602*01e8d739SAndreas Gohr * returned so the read path keeps working. 603bd17ac90SSatoshi Sahara * 604bd17ac90SSatoshi Sahara * @return bool|array false when page had never existed or array with entries: 605bd17ac90SSatoshi Sahara * - date: revision identifier (timestamp or last revision +1) 606bd17ac90SSatoshi Sahara * - ip: IPv4 address (127.0.0.1) 607bd17ac90SSatoshi Sahara * - type: log line type 608bd17ac90SSatoshi Sahara * - id: id of page or media 609bd17ac90SSatoshi Sahara * - user: user name 610bd17ac90SSatoshi Sahara * - sum: edit summary (or action reason) 611bd17ac90SSatoshi Sahara * - extra: extra data (varies by line type) 612bd17ac90SSatoshi Sahara * - sizechange: change of filesize 613dbf582ddSSatoshi Sahara * - timestamp: unix timestamp or false (key set only for external edit occurred) 614a835c93aSGerrit Uitslag * additional: 615a835c93aSGerrit Uitslag * - mode: page or media 616bd17ac90SSatoshi Sahara * 617bd17ac90SSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 618bd17ac90SSatoshi Sahara */ 619bd17ac90SSatoshi Sahara public function getCurrentRevisionInfo() 620bd17ac90SSatoshi Sahara { 621bd17ac90SSatoshi Sahara global $lang; 622bd17ac90SSatoshi Sahara 62385160059SGerrit Uitslag if (isset($this->currentRevision)) { 62485160059SGerrit Uitslag return $this->getRevisionInfo($this->currentRevision); 62585160059SGerrit Uitslag } 626bd17ac90SSatoshi Sahara 627eeda7adaSGerrit Uitslag // get revision id from the item file timestamp and changelog 628dbf582ddSSatoshi Sahara $fileLastMod = $this->getFilename(); 629dbf582ddSSatoshi Sahara $fileRev = @filemtime($fileLastMod); // false when the file not exist 630df7627d6SSatoshi Sahara $lastRev = $this->lastRevision(); // false when no changelog 631bd17ac90SSatoshi Sahara 632df7627d6SSatoshi Sahara if (!$fileRev && !$lastRev) { // has never existed 633df7627d6SSatoshi Sahara $this->currentRevision = false; 634bd17ac90SSatoshi Sahara return false; 635bd17ac90SSatoshi Sahara } elseif ($fileRev === $lastRev) { // not external edit 636bd17ac90SSatoshi Sahara $this->currentRevision = $lastRev; 6375ec96136SSatoshi Sahara return $this->getRevisionInfo($lastRev); 638bd17ac90SSatoshi Sahara } 639bd17ac90SSatoshi Sahara 640bd17ac90SSatoshi Sahara if (!$fileRev && $lastRev) { // item file does not exist 641bd17ac90SSatoshi Sahara // check consistency against changelog 64286216bf0SGerrit Uitslag $revInfo = $this->getRevisionInfo($lastRev, false); 643bd17ac90SSatoshi Sahara if ($revInfo['type'] == DOKU_CHANGE_TYPE_DELETE) { 644bd17ac90SSatoshi Sahara $this->currentRevision = $lastRev; 64554d95e36SGerrit Uitslag return $revInfo; 646bd17ac90SSatoshi Sahara } 647bd17ac90SSatoshi Sahara 64866f4cdd4SSatoshi Sahara // externally deleted, set revision date as late as possible 649bd17ac90SSatoshi Sahara $revInfo = [ 650*01e8d739SAndreas Gohr 'date' => max($lastRev + 1, time() - 1), // 1 sec before now or last revision +1 651bd17ac90SSatoshi Sahara 'ip' => '127.0.0.1', 652bd17ac90SSatoshi Sahara 'type' => DOKU_CHANGE_TYPE_DELETE, 653bd17ac90SSatoshi Sahara 'id' => $this->id, 654bd17ac90SSatoshi Sahara 'user' => '', 655df7627d6SSatoshi Sahara 'sum' => $lang['deleted'] . ' - ' . $lang['external_edit'] . ' (' . $lang['unknowndate'] . ')', 656bd17ac90SSatoshi Sahara 'extra' => '', 6576e695190SAndreas Gohr 'sizechange' => -io_getSizeFile($this->getFilename($lastRev)), 658dbf582ddSSatoshi Sahara 'timestamp' => false, 659a835c93aSGerrit Uitslag 'mode' => $this->getMode() 660bd17ac90SSatoshi Sahara ]; 6610b5bb6b4SGerrit Uitslag } else { // item file exists, with timestamp $fileRev 66254d95e36SGerrit Uitslag // here, file timestamp $fileRev is different with last revision timestamp $lastRev in changelog 663df7627d6SSatoshi Sahara $isJustCreated = $lastRev === false || ( 664e39c2efbSSatoshi Sahara $fileRev > $lastRev && 66586216bf0SGerrit Uitslag $this->getRevisionInfo($lastRev, false)['type'] == DOKU_CHANGE_TYPE_DELETE 666e39c2efbSSatoshi Sahara ); 667bd17ac90SSatoshi Sahara $filesize_new = filesize($this->getFilename()); 6686e695190SAndreas Gohr $filesize_old = $isJustCreated ? 0 : io_getSizeFile($this->getFilename($lastRev)); 669bd17ac90SSatoshi Sahara $sizechange = $filesize_new - $filesize_old; 670bd17ac90SSatoshi Sahara 671dbf582ddSSatoshi Sahara if ($isJustCreated) { 6728ff5c11aSSatoshi Sahara $timestamp = $fileRev; 673bd17ac90SSatoshi Sahara $sum = $lang['created'] . ' - ' . $lang['external_edit']; 674bd17ac90SSatoshi Sahara } elseif ($fileRev > $lastRev) { 6758ff5c11aSSatoshi Sahara $timestamp = $fileRev; 676bd17ac90SSatoshi Sahara $sum = $lang['external_edit']; 677bd17ac90SSatoshi Sahara } else { 678eeda7adaSGerrit Uitslag // $fileRev is older than $lastRev, that is erroneous/incorrect occurrence. 67966f4cdd4SSatoshi Sahara $msg = "Warning: current file modification time is older than last revision date"; 68010f359adSAndreas Gohr $details = 'File revision: ' . $fileRev . ' ' . dformat($fileRev, "%Y-%m-%d %H:%M:%S") . "\n" 68110f359adSAndreas Gohr . 'Last revision: ' . $lastRev . ' ' . dformat($lastRev, "%Y-%m-%d %H:%M:%S"); 68266f4cdd4SSatoshi Sahara Logger::error($msg, $details, $this->getFilename()); 68366f4cdd4SSatoshi Sahara $timestamp = false; 684df7627d6SSatoshi Sahara $sum = $lang['external_edit'] . ' (' . $lang['unknowndate'] . ')'; 685bd17ac90SSatoshi Sahara } 686bd17ac90SSatoshi Sahara 687bd17ac90SSatoshi Sahara // externally created or edited 688bd17ac90SSatoshi Sahara $revInfo = [ 68966f4cdd4SSatoshi Sahara 'date' => $timestamp ?: $lastRev + 1, 690bd17ac90SSatoshi Sahara 'ip' => '127.0.0.1', 691bd17ac90SSatoshi Sahara 'type' => $isJustCreated ? DOKU_CHANGE_TYPE_CREATE : DOKU_CHANGE_TYPE_EDIT, 692bd17ac90SSatoshi Sahara 'id' => $this->id, 693bd17ac90SSatoshi Sahara 'user' => '', 694bd17ac90SSatoshi Sahara 'sum' => $sum, 695bd17ac90SSatoshi Sahara 'extra' => '', 696bd17ac90SSatoshi Sahara 'sizechange' => $sizechange, 697bd17ac90SSatoshi Sahara 'timestamp' => $timestamp, 698a835c93aSGerrit Uitslag 'mode' => $this->getMode() 699bd17ac90SSatoshi Sahara ]; 700bd17ac90SSatoshi Sahara } 701bd17ac90SSatoshi Sahara 702*01e8d739SAndreas Gohr // persist the synthesized entry so subsequent reads see it as a real changelog entry 703*01e8d739SAndreas Gohr $this->persistCurrentRevisionInfo($revInfo); 704*01e8d739SAndreas Gohr 705bd17ac90SSatoshi Sahara // cache current revision information of external edition 706bd17ac90SSatoshi Sahara $this->currentRevision = $revInfo['date']; 707bd17ac90SSatoshi Sahara $this->cache[$this->id][$this->currentRevision] = $revInfo; 708bd17ac90SSatoshi Sahara return $this->getRevisionInfo($this->currentRevision); 709bd17ac90SSatoshi Sahara } 710312e7095SSatoshi Sahara 711312e7095SSatoshi Sahara /** 712*01e8d739SAndreas Gohr * Adds an entry to the changelog 713*01e8d739SAndreas Gohr * 714*01e8d739SAndreas Gohr * Locks the local changelog file for the duration of the write so concurrent writers 715*01e8d739SAndreas Gohr * serialize through the same key. Subclasses provide the actual append logic via 716*01e8d739SAndreas Gohr * writeLogEntry() so persistCurrentRevisionInfo() can append while already holding 717*01e8d739SAndreas Gohr * the lock without re-entering it. 718*01e8d739SAndreas Gohr * 719*01e8d739SAndreas Gohr * Best-effort: if writeLogEntry() throws, surfaces the error via msg() and still 720*01e8d739SAndreas Gohr * returns the info dict so existing callers (saveWikiText etc.) keep working. 721*01e8d739SAndreas Gohr * 722*01e8d739SAndreas Gohr * @param array $info Revision info structure of a page or media file 723*01e8d739SAndreas Gohr * @param int $timestamp log line date (optional) 724*01e8d739SAndreas Gohr * @return array revision info of added log line 725*01e8d739SAndreas Gohr */ 726*01e8d739SAndreas Gohr public function addLogEntry(array $info, $timestamp = null) 727*01e8d739SAndreas Gohr { 728*01e8d739SAndreas Gohr $logfile = $this->getChangelogFilename(); 729*01e8d739SAndreas Gohr io_lock($logfile); 730*01e8d739SAndreas Gohr try { 731*01e8d739SAndreas Gohr return $this->writeLogEntry($info, $timestamp); 732*01e8d739SAndreas Gohr } catch (\RuntimeException $e) { 733*01e8d739SAndreas Gohr msg($e->getMessage(), -1); 734*01e8d739SAndreas Gohr $info['mode'] = $this->getMode(); 735*01e8d739SAndreas Gohr return $info; 736*01e8d739SAndreas Gohr } finally { 737*01e8d739SAndreas Gohr io_unlock($logfile); 738*01e8d739SAndreas Gohr } 739*01e8d739SAndreas Gohr } 740*01e8d739SAndreas Gohr 741*01e8d739SAndreas Gohr /** 742*01e8d739SAndreas Gohr * Append a log entry to the changelog files and update the in-memory cache. 743*01e8d739SAndreas Gohr * 744*01e8d739SAndreas Gohr * The caller MUST hold io_lock() on the local changelog file. The global changelog 745*01e8d739SAndreas Gohr * is a different file with its own lock (acquired briefly here). This is what 746*01e8d739SAndreas Gohr * addLogEntry() runs under its lock, and what persistCurrentRevisionInfo() calls 747*01e8d739SAndreas Gohr * directly while it's holding the same lock for the detect-and-write critical section. 748*01e8d739SAndreas Gohr * 749*01e8d739SAndreas Gohr * @param array $info Revision info structure 750*01e8d739SAndreas Gohr * @param int $timestamp log line date (optional) 751*01e8d739SAndreas Gohr * @return array revision info of added log line 752*01e8d739SAndreas Gohr * @throws \RuntimeException if the local changelog write fails 753*01e8d739SAndreas Gohr */ 754*01e8d739SAndreas Gohr protected function writeLogEntry(array $info, $timestamp = null) 755*01e8d739SAndreas Gohr { 756*01e8d739SAndreas Gohr global $conf; 757*01e8d739SAndreas Gohr 758*01e8d739SAndreas Gohr if (isset($timestamp)) unset($this->cache[$this->id][$info['date']]); 759*01e8d739SAndreas Gohr 760*01e8d739SAndreas Gohr $logline = static::buildLogLine($info, $timestamp); 761*01e8d739SAndreas Gohr 762*01e8d739SAndreas Gohr // append to local changelog without re-locking (caller holds the lock) 763*01e8d739SAndreas Gohr $localFile = $this->getChangelogFilename(); 764*01e8d739SAndreas Gohr io_makeFileDir($localFile); 765*01e8d739SAndreas Gohr $fileexists = file_exists($localFile); 766*01e8d739SAndreas Gohr $fh = @fopen($localFile, 'ab'); 767*01e8d739SAndreas Gohr if (!$fh || @fwrite($fh, $logline) === false) { 768*01e8d739SAndreas Gohr if ($fh) @fclose($fh); 769*01e8d739SAndreas Gohr throw new \RuntimeException("Writing $localFile failed"); 770*01e8d739SAndreas Gohr } 771*01e8d739SAndreas Gohr fclose($fh); 772*01e8d739SAndreas Gohr if (!$fileexists && !empty($conf['fperm'])) chmod($localFile, $conf['fperm']); 773*01e8d739SAndreas Gohr 774*01e8d739SAndreas Gohr // global changelog has its own lock and msg() reporting via io_saveFile() 775*01e8d739SAndreas Gohr io_saveFile($this->getGlobalChangelogFilename(), $logline, true); 776*01e8d739SAndreas Gohr 777*01e8d739SAndreas Gohr $this->currentRevision = $info['date']; 778*01e8d739SAndreas Gohr $info['mode'] = $this->getMode(); 779*01e8d739SAndreas Gohr $this->cache[$this->id][$this->currentRevision] = $info; 780*01e8d739SAndreas Gohr return $info; 781*01e8d739SAndreas Gohr } 782*01e8d739SAndreas Gohr 783*01e8d739SAndreas Gohr /** 784*01e8d739SAndreas Gohr * Persist a synthesized external-revision entry to the changelog 785*01e8d739SAndreas Gohr * 786*01e8d739SAndreas Gohr * Holds the local changelog lock around the entire detect-and-write critical section 787*01e8d739SAndreas Gohr * (idempotency check, attic copy, log append) so it serializes against any other 788*01e8d739SAndreas Gohr * writer that goes through addLogEntry(). The append uses writeLogEntry() to avoid 789*01e8d739SAndreas Gohr * re-entering the lock we already hold. 790*01e8d739SAndreas Gohr * 791*01e8d739SAndreas Gohr * Returns false (without raising) when the attic write fails or another request 792*01e8d739SAndreas Gohr * already persisted the entry. The caller falls back to the in-memory synthesized 793*01e8d739SAndreas Gohr * entry. 794*01e8d739SAndreas Gohr * 795*01e8d739SAndreas Gohr * @param array $revInfo synthesized revision info 796*01e8d739SAndreas Gohr * @return bool true if newly persisted, false otherwise 797*01e8d739SAndreas Gohr */ 798*01e8d739SAndreas Gohr protected function persistCurrentRevisionInfo(array $revInfo) 799*01e8d739SAndreas Gohr { 800*01e8d739SAndreas Gohr // only the synthesized branches carry the 'timestamp' key 801*01e8d739SAndreas Gohr if (!array_key_exists('timestamp', $revInfo)) return false; 802*01e8d739SAndreas Gohr 803*01e8d739SAndreas Gohr $logfile = $this->getChangelogFilename(); 804*01e8d739SAndreas Gohr io_lock($logfile); 805*01e8d739SAndreas Gohr try { 806*01e8d739SAndreas Gohr // re-read lastRev under the lock — another request may have just persisted 807*01e8d739SAndreas Gohr $lastRev = $this->lastRevision(); 808*01e8d739SAndreas Gohr if ($lastRev !== false && $lastRev >= $revInfo['date']) { 809*01e8d739SAndreas Gohr return false; 810*01e8d739SAndreas Gohr } 811*01e8d739SAndreas Gohr 812*01e8d739SAndreas Gohr if ($revInfo['type'] !== DOKU_CHANGE_TYPE_DELETE) { 813*01e8d739SAndreas Gohr if (!$this->saveExternalAttic($revInfo)) return false; 814*01e8d739SAndreas Gohr } 815*01e8d739SAndreas Gohr 816*01e8d739SAndreas Gohr $this->writeLogEntry($revInfo); 817*01e8d739SAndreas Gohr return true; 818*01e8d739SAndreas Gohr } catch (\RuntimeException $e) { 819*01e8d739SAndreas Gohr // silent fallback to in-memory synthesis 820*01e8d739SAndreas Gohr return false; 821*01e8d739SAndreas Gohr } finally { 822*01e8d739SAndreas Gohr io_unlock($logfile); 823*01e8d739SAndreas Gohr } 824*01e8d739SAndreas Gohr } 825*01e8d739SAndreas Gohr 826*01e8d739SAndreas Gohr /** 827*01e8d739SAndreas Gohr * Save the externally-modified file to the attic before the synthesized log entry is 828*01e8d739SAndreas Gohr * persisted. For deletions there is no file to copy and implementations should return 829*01e8d739SAndreas Gohr * true (the persist flow skips this call for the delete branch). 830*01e8d739SAndreas Gohr * 831*01e8d739SAndreas Gohr * @param array $revInfo synthesized revision info with 'date' set 832*01e8d739SAndreas Gohr * @return bool true on success or no-op, false to abort persistence 833*01e8d739SAndreas Gohr */ 834*01e8d739SAndreas Gohr abstract protected function saveExternalAttic(array $revInfo); 835*01e8d739SAndreas Gohr 836*01e8d739SAndreas Gohr /** 837312e7095SSatoshi Sahara * Mechanism to trace no-actual external current revision 838312e7095SSatoshi Sahara * @param int $rev 839312e7095SSatoshi Sahara */ 840312e7095SSatoshi Sahara public function traceCurrentRevision($rev) 841312e7095SSatoshi Sahara { 842312e7095SSatoshi Sahara if ($rev > $this->lastRevision()) { 843312e7095SSatoshi Sahara $rev = $this->currentRevision(); 844312e7095SSatoshi Sahara } 845312e7095SSatoshi Sahara return $rev; 846312e7095SSatoshi Sahara } 8470c3a5702SAndreas Gohr} 848