1*0c3a5702SAndreas Gohr<?php 2*0c3a5702SAndreas Gohr 3*0c3a5702SAndreas Gohrnamespace dokuwiki\ChangeLog; 4*0c3a5702SAndreas Gohr 5*0c3a5702SAndreas Gohr/** 6*0c3a5702SAndreas Gohr * methods for handling of changelog of pages or media files 7*0c3a5702SAndreas Gohr */ 8*0c3a5702SAndreas Gohrabstract class ChangeLog 9*0c3a5702SAndreas Gohr{ 10*0c3a5702SAndreas Gohr 11*0c3a5702SAndreas Gohr /** @var string */ 12*0c3a5702SAndreas Gohr protected $id; 13*0c3a5702SAndreas Gohr /** @var int */ 14*0c3a5702SAndreas Gohr protected $chunk_size; 15*0c3a5702SAndreas Gohr /** @var array */ 16*0c3a5702SAndreas Gohr protected $cache; 17*0c3a5702SAndreas Gohr 18*0c3a5702SAndreas Gohr /** 19*0c3a5702SAndreas Gohr * Constructor 20*0c3a5702SAndreas Gohr * 21*0c3a5702SAndreas Gohr * @param string $id page id 22*0c3a5702SAndreas Gohr * @param int $chunk_size maximum block size read from file 23*0c3a5702SAndreas Gohr */ 24*0c3a5702SAndreas Gohr public function __construct($id, $chunk_size = 8192) 25*0c3a5702SAndreas Gohr { 26*0c3a5702SAndreas Gohr global $cache_revinfo; 27*0c3a5702SAndreas Gohr 28*0c3a5702SAndreas Gohr $this->cache =& $cache_revinfo; 29*0c3a5702SAndreas Gohr if (!isset($this->cache[$id])) { 30*0c3a5702SAndreas Gohr $this->cache[$id] = array(); 31*0c3a5702SAndreas Gohr } 32*0c3a5702SAndreas Gohr 33*0c3a5702SAndreas Gohr $this->id = $id; 34*0c3a5702SAndreas Gohr $this->setChunkSize($chunk_size); 35*0c3a5702SAndreas Gohr 36*0c3a5702SAndreas Gohr } 37*0c3a5702SAndreas Gohr 38*0c3a5702SAndreas Gohr /** 39*0c3a5702SAndreas Gohr * Set chunk size for file reading 40*0c3a5702SAndreas Gohr * Chunk size zero let read whole file at once 41*0c3a5702SAndreas Gohr * 42*0c3a5702SAndreas Gohr * @param int $chunk_size maximum block size read from file 43*0c3a5702SAndreas Gohr */ 44*0c3a5702SAndreas Gohr public function setChunkSize($chunk_size) 45*0c3a5702SAndreas Gohr { 46*0c3a5702SAndreas Gohr if (!is_numeric($chunk_size)) $chunk_size = 0; 47*0c3a5702SAndreas Gohr 48*0c3a5702SAndreas Gohr $this->chunk_size = (int)max($chunk_size, 0); 49*0c3a5702SAndreas Gohr } 50*0c3a5702SAndreas Gohr 51*0c3a5702SAndreas Gohr /** 52*0c3a5702SAndreas Gohr * Returns path to changelog 53*0c3a5702SAndreas Gohr * 54*0c3a5702SAndreas Gohr * @return string path to file 55*0c3a5702SAndreas Gohr */ 56*0c3a5702SAndreas Gohr abstract protected function getChangelogFilename(); 57*0c3a5702SAndreas Gohr 58*0c3a5702SAndreas Gohr /** 59*0c3a5702SAndreas Gohr * Returns path to current page/media 60*0c3a5702SAndreas Gohr * 61*0c3a5702SAndreas Gohr * @return string path to file 62*0c3a5702SAndreas Gohr */ 63*0c3a5702SAndreas Gohr abstract protected function getFilename(); 64*0c3a5702SAndreas Gohr 65*0c3a5702SAndreas Gohr /** 66*0c3a5702SAndreas Gohr * Get the changelog information for a specific page id and revision (timestamp) 67*0c3a5702SAndreas Gohr * 68*0c3a5702SAndreas Gohr * Adjacent changelog lines are optimistically parsed and cached to speed up 69*0c3a5702SAndreas Gohr * consecutive calls to getRevisionInfo. For large changelog files, only the chunk 70*0c3a5702SAndreas Gohr * containing the requested changelog line is read. 71*0c3a5702SAndreas Gohr * 72*0c3a5702SAndreas Gohr * @param int $rev revision timestamp 73*0c3a5702SAndreas Gohr * @return bool|array false or array with entries: 74*0c3a5702SAndreas Gohr * - date: unix timestamp 75*0c3a5702SAndreas Gohr * - ip: IPv4 address (127.0.0.1) 76*0c3a5702SAndreas Gohr * - type: log line type 77*0c3a5702SAndreas Gohr * - id: page id 78*0c3a5702SAndreas Gohr * - user: user name 79*0c3a5702SAndreas Gohr * - sum: edit summary (or action reason) 80*0c3a5702SAndreas Gohr * - extra: extra data (varies by line type) 81*0c3a5702SAndreas Gohr * 82*0c3a5702SAndreas Gohr * @author Ben Coburn <btcoburn@silicodon.net> 83*0c3a5702SAndreas Gohr * @author Kate Arzamastseva <pshns@ukr.net> 84*0c3a5702SAndreas Gohr */ 85*0c3a5702SAndreas Gohr public function getRevisionInfo($rev) 86*0c3a5702SAndreas Gohr { 87*0c3a5702SAndreas Gohr $rev = max($rev, 0); 88*0c3a5702SAndreas Gohr 89*0c3a5702SAndreas Gohr // check if it's already in the memory cache 90*0c3a5702SAndreas Gohr if (isset($this->cache[$this->id]) && isset($this->cache[$this->id][$rev])) { 91*0c3a5702SAndreas Gohr return $this->cache[$this->id][$rev]; 92*0c3a5702SAndreas Gohr } 93*0c3a5702SAndreas Gohr 94*0c3a5702SAndreas Gohr //read lines from changelog 95*0c3a5702SAndreas Gohr list($fp, $lines) = $this->readloglines($rev); 96*0c3a5702SAndreas Gohr if ($fp) { 97*0c3a5702SAndreas Gohr fclose($fp); 98*0c3a5702SAndreas Gohr } 99*0c3a5702SAndreas Gohr if (empty($lines)) return false; 100*0c3a5702SAndreas Gohr 101*0c3a5702SAndreas Gohr // parse and cache changelog lines 102*0c3a5702SAndreas Gohr foreach ($lines as $value) { 103*0c3a5702SAndreas Gohr $tmp = parseChangelogLine($value); 104*0c3a5702SAndreas Gohr if ($tmp !== false) { 105*0c3a5702SAndreas Gohr $this->cache[$this->id][$tmp['date']] = $tmp; 106*0c3a5702SAndreas Gohr } 107*0c3a5702SAndreas Gohr } 108*0c3a5702SAndreas Gohr if (!isset($this->cache[$this->id][$rev])) { 109*0c3a5702SAndreas Gohr return false; 110*0c3a5702SAndreas Gohr } 111*0c3a5702SAndreas Gohr return $this->cache[$this->id][$rev]; 112*0c3a5702SAndreas Gohr } 113*0c3a5702SAndreas Gohr 114*0c3a5702SAndreas Gohr /** 115*0c3a5702SAndreas Gohr * Return a list of page revisions numbers 116*0c3a5702SAndreas Gohr * 117*0c3a5702SAndreas Gohr * Does not guarantee that the revision exists in the attic, 118*0c3a5702SAndreas Gohr * only that a line with the date exists in the changelog. 119*0c3a5702SAndreas Gohr * By default the current revision is skipped. 120*0c3a5702SAndreas Gohr * 121*0c3a5702SAndreas Gohr * The current revision is automatically skipped when the page exists. 122*0c3a5702SAndreas Gohr * See $INFO['meta']['last_change'] for the current revision. 123*0c3a5702SAndreas Gohr * A negative $first let read the current revision too. 124*0c3a5702SAndreas Gohr * 125*0c3a5702SAndreas Gohr * For efficiency, the log lines are parsed and cached for later 126*0c3a5702SAndreas Gohr * calls to getRevisionInfo. Large changelog files are read 127*0c3a5702SAndreas Gohr * backwards in chunks until the requested number of changelog 128*0c3a5702SAndreas Gohr * lines are recieved. 129*0c3a5702SAndreas Gohr * 130*0c3a5702SAndreas Gohr * @param int $first skip the first n changelog lines 131*0c3a5702SAndreas Gohr * @param int $num number of revisions to return 132*0c3a5702SAndreas Gohr * @return array with the revision timestamps 133*0c3a5702SAndreas Gohr * 134*0c3a5702SAndreas Gohr * @author Ben Coburn <btcoburn@silicodon.net> 135*0c3a5702SAndreas Gohr * @author Kate Arzamastseva <pshns@ukr.net> 136*0c3a5702SAndreas Gohr */ 137*0c3a5702SAndreas Gohr public function getRevisions($first, $num) 138*0c3a5702SAndreas Gohr { 139*0c3a5702SAndreas Gohr $revs = array(); 140*0c3a5702SAndreas Gohr $lines = array(); 141*0c3a5702SAndreas Gohr $count = 0; 142*0c3a5702SAndreas Gohr 143*0c3a5702SAndreas Gohr $num = max($num, 0); 144*0c3a5702SAndreas Gohr if ($num == 0) { 145*0c3a5702SAndreas Gohr return $revs; 146*0c3a5702SAndreas Gohr } 147*0c3a5702SAndreas Gohr 148*0c3a5702SAndreas Gohr if ($first < 0) { 149*0c3a5702SAndreas Gohr $first = 0; 150*0c3a5702SAndreas Gohr } else { 151*0c3a5702SAndreas Gohr if (file_exists($this->getFilename())) { 152*0c3a5702SAndreas Gohr // skip current revision if the page exists 153*0c3a5702SAndreas Gohr $first = max($first + 1, 0); 154*0c3a5702SAndreas Gohr } 155*0c3a5702SAndreas Gohr } 156*0c3a5702SAndreas Gohr 157*0c3a5702SAndreas Gohr $file = $this->getChangelogFilename(); 158*0c3a5702SAndreas Gohr 159*0c3a5702SAndreas Gohr if (!file_exists($file)) { 160*0c3a5702SAndreas Gohr return $revs; 161*0c3a5702SAndreas Gohr } 162*0c3a5702SAndreas Gohr if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) { 163*0c3a5702SAndreas Gohr // read whole file 164*0c3a5702SAndreas Gohr $lines = file($file); 165*0c3a5702SAndreas Gohr if ($lines === false) { 166*0c3a5702SAndreas Gohr return $revs; 167*0c3a5702SAndreas Gohr } 168*0c3a5702SAndreas Gohr } else { 169*0c3a5702SAndreas Gohr // read chunks backwards 170*0c3a5702SAndreas Gohr $fp = fopen($file, 'rb'); // "file pointer" 171*0c3a5702SAndreas Gohr if ($fp === false) { 172*0c3a5702SAndreas Gohr return $revs; 173*0c3a5702SAndreas Gohr } 174*0c3a5702SAndreas Gohr fseek($fp, 0, SEEK_END); 175*0c3a5702SAndreas Gohr $tail = ftell($fp); 176*0c3a5702SAndreas Gohr 177*0c3a5702SAndreas Gohr // chunk backwards 178*0c3a5702SAndreas Gohr $finger = max($tail - $this->chunk_size, 0); 179*0c3a5702SAndreas Gohr while ($count < $num + $first) { 180*0c3a5702SAndreas Gohr $nl = $this->getNewlinepointer($fp, $finger); 181*0c3a5702SAndreas Gohr 182*0c3a5702SAndreas Gohr // was the chunk big enough? if not, take another bite 183*0c3a5702SAndreas Gohr if ($nl > 0 && $tail <= $nl) { 184*0c3a5702SAndreas Gohr $finger = max($finger - $this->chunk_size, 0); 185*0c3a5702SAndreas Gohr continue; 186*0c3a5702SAndreas Gohr } else { 187*0c3a5702SAndreas Gohr $finger = $nl; 188*0c3a5702SAndreas Gohr } 189*0c3a5702SAndreas Gohr 190*0c3a5702SAndreas Gohr // read chunk 191*0c3a5702SAndreas Gohr $chunk = ''; 192*0c3a5702SAndreas Gohr $read_size = max($tail - $finger, 0); // found chunk size 193*0c3a5702SAndreas Gohr $got = 0; 194*0c3a5702SAndreas Gohr while ($got < $read_size && !feof($fp)) { 195*0c3a5702SAndreas Gohr $tmp = @fread($fp, max(min($this->chunk_size, $read_size - $got), 0)); 196*0c3a5702SAndreas Gohr if ($tmp === false) { 197*0c3a5702SAndreas Gohr break; 198*0c3a5702SAndreas Gohr } //error state 199*0c3a5702SAndreas Gohr $got += strlen($tmp); 200*0c3a5702SAndreas Gohr $chunk .= $tmp; 201*0c3a5702SAndreas Gohr } 202*0c3a5702SAndreas Gohr $tmp = explode("\n", $chunk); 203*0c3a5702SAndreas Gohr array_pop($tmp); // remove trailing newline 204*0c3a5702SAndreas Gohr 205*0c3a5702SAndreas Gohr // combine with previous chunk 206*0c3a5702SAndreas Gohr $count += count($tmp); 207*0c3a5702SAndreas Gohr $lines = array_merge($tmp, $lines); 208*0c3a5702SAndreas Gohr 209*0c3a5702SAndreas Gohr // next chunk 210*0c3a5702SAndreas Gohr if ($finger == 0) { 211*0c3a5702SAndreas Gohr break; 212*0c3a5702SAndreas Gohr } // already read all the lines 213*0c3a5702SAndreas Gohr else { 214*0c3a5702SAndreas Gohr $tail = $finger; 215*0c3a5702SAndreas Gohr $finger = max($tail - $this->chunk_size, 0); 216*0c3a5702SAndreas Gohr } 217*0c3a5702SAndreas Gohr } 218*0c3a5702SAndreas Gohr fclose($fp); 219*0c3a5702SAndreas Gohr } 220*0c3a5702SAndreas Gohr 221*0c3a5702SAndreas Gohr // skip parsing extra lines 222*0c3a5702SAndreas Gohr $num = max(min(count($lines) - $first, $num), 0); 223*0c3a5702SAndreas Gohr if ($first > 0 && $num > 0) { 224*0c3a5702SAndreas Gohr $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num); 225*0c3a5702SAndreas Gohr } else { 226*0c3a5702SAndreas Gohr if ($first > 0 && $num == 0) { 227*0c3a5702SAndreas Gohr $lines = array_slice($lines, 0, max(count($lines) - $first, 0)); 228*0c3a5702SAndreas Gohr } elseif ($first == 0 && $num > 0) { 229*0c3a5702SAndreas Gohr $lines = array_slice($lines, max(count($lines) - $num, 0)); 230*0c3a5702SAndreas Gohr } 231*0c3a5702SAndreas Gohr } 232*0c3a5702SAndreas Gohr 233*0c3a5702SAndreas Gohr // handle lines in reverse order 234*0c3a5702SAndreas Gohr for ($i = count($lines) - 1; $i >= 0; $i--) { 235*0c3a5702SAndreas Gohr $tmp = parseChangelogLine($lines[$i]); 236*0c3a5702SAndreas Gohr if ($tmp !== false) { 237*0c3a5702SAndreas Gohr $this->cache[$this->id][$tmp['date']] = $tmp; 238*0c3a5702SAndreas Gohr $revs[] = $tmp['date']; 239*0c3a5702SAndreas Gohr } 240*0c3a5702SAndreas Gohr } 241*0c3a5702SAndreas Gohr 242*0c3a5702SAndreas Gohr return $revs; 243*0c3a5702SAndreas Gohr } 244*0c3a5702SAndreas Gohr 245*0c3a5702SAndreas Gohr /** 246*0c3a5702SAndreas Gohr * Get the nth revision left or right handside for a specific page id and revision (timestamp) 247*0c3a5702SAndreas Gohr * 248*0c3a5702SAndreas Gohr * For large changelog files, only the chunk containing the 249*0c3a5702SAndreas Gohr * reference revision $rev is read and sometimes a next chunck. 250*0c3a5702SAndreas Gohr * 251*0c3a5702SAndreas Gohr * Adjacent changelog lines are optimistically parsed and cached to speed up 252*0c3a5702SAndreas Gohr * consecutive calls to getRevisionInfo. 253*0c3a5702SAndreas Gohr * 254*0c3a5702SAndreas Gohr * @param int $rev revision timestamp used as startdate (doesn't need to be revisionnumber) 255*0c3a5702SAndreas Gohr * @param int $direction give position of returned revision with respect to $rev; positive=next, negative=prev 256*0c3a5702SAndreas Gohr * @return bool|int 257*0c3a5702SAndreas Gohr * timestamp of the requested revision 258*0c3a5702SAndreas Gohr * otherwise false 259*0c3a5702SAndreas Gohr */ 260*0c3a5702SAndreas Gohr public function getRelativeRevision($rev, $direction) 261*0c3a5702SAndreas Gohr { 262*0c3a5702SAndreas Gohr $rev = max($rev, 0); 263*0c3a5702SAndreas Gohr $direction = (int)$direction; 264*0c3a5702SAndreas Gohr 265*0c3a5702SAndreas Gohr //no direction given or last rev, so no follow-up 266*0c3a5702SAndreas Gohr if (!$direction || ($direction > 0 && $this->isCurrentRevision($rev))) { 267*0c3a5702SAndreas Gohr return false; 268*0c3a5702SAndreas Gohr } 269*0c3a5702SAndreas Gohr 270*0c3a5702SAndreas Gohr //get lines from changelog 271*0c3a5702SAndreas Gohr list($fp, $lines, $head, $tail, $eof) = $this->readloglines($rev); 272*0c3a5702SAndreas Gohr if (empty($lines)) return false; 273*0c3a5702SAndreas Gohr 274*0c3a5702SAndreas Gohr // look for revisions later/earlier then $rev, when founded count till the wanted revision is reached 275*0c3a5702SAndreas Gohr // also parse and cache changelog lines for getRevisionInfo(). 276*0c3a5702SAndreas Gohr $revcounter = 0; 277*0c3a5702SAndreas Gohr $relativerev = false; 278*0c3a5702SAndreas Gohr $checkotherchunck = true; //always runs once 279*0c3a5702SAndreas Gohr while (!$relativerev && $checkotherchunck) { 280*0c3a5702SAndreas Gohr $tmp = array(); 281*0c3a5702SAndreas Gohr //parse in normal or reverse order 282*0c3a5702SAndreas Gohr $count = count($lines); 283*0c3a5702SAndreas Gohr if ($direction > 0) { 284*0c3a5702SAndreas Gohr $start = 0; 285*0c3a5702SAndreas Gohr $step = 1; 286*0c3a5702SAndreas Gohr } else { 287*0c3a5702SAndreas Gohr $start = $count - 1; 288*0c3a5702SAndreas Gohr $step = -1; 289*0c3a5702SAndreas Gohr } 290*0c3a5702SAndreas Gohr for ($i = $start; $i >= 0 && $i < $count; $i = $i + $step) { 291*0c3a5702SAndreas Gohr $tmp = parseChangelogLine($lines[$i]); 292*0c3a5702SAndreas Gohr if ($tmp !== false) { 293*0c3a5702SAndreas Gohr $this->cache[$this->id][$tmp['date']] = $tmp; 294*0c3a5702SAndreas Gohr //look for revs older/earlier then reference $rev and select $direction-th one 295*0c3a5702SAndreas Gohr if (($direction > 0 && $tmp['date'] > $rev) || ($direction < 0 && $tmp['date'] < $rev)) { 296*0c3a5702SAndreas Gohr $revcounter++; 297*0c3a5702SAndreas Gohr if ($revcounter == abs($direction)) { 298*0c3a5702SAndreas Gohr $relativerev = $tmp['date']; 299*0c3a5702SAndreas Gohr } 300*0c3a5702SAndreas Gohr } 301*0c3a5702SAndreas Gohr } 302*0c3a5702SAndreas Gohr } 303*0c3a5702SAndreas Gohr 304*0c3a5702SAndreas Gohr //true when $rev is found, but not the wanted follow-up. 305*0c3a5702SAndreas Gohr $checkotherchunck = $fp 306*0c3a5702SAndreas Gohr && ($tmp['date'] == $rev || ($revcounter > 0 && !$relativerev)) 307*0c3a5702SAndreas Gohr && !(($tail == $eof && $direction > 0) || ($head == 0 && $direction < 0)); 308*0c3a5702SAndreas Gohr 309*0c3a5702SAndreas Gohr if ($checkotherchunck) { 310*0c3a5702SAndreas Gohr list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, $direction); 311*0c3a5702SAndreas Gohr 312*0c3a5702SAndreas Gohr if (empty($lines)) break; 313*0c3a5702SAndreas Gohr } 314*0c3a5702SAndreas Gohr } 315*0c3a5702SAndreas Gohr if ($fp) { 316*0c3a5702SAndreas Gohr fclose($fp); 317*0c3a5702SAndreas Gohr } 318*0c3a5702SAndreas Gohr 319*0c3a5702SAndreas Gohr return $relativerev; 320*0c3a5702SAndreas Gohr } 321*0c3a5702SAndreas Gohr 322*0c3a5702SAndreas Gohr /** 323*0c3a5702SAndreas Gohr * Returns revisions around rev1 and rev2 324*0c3a5702SAndreas Gohr * When available it returns $max entries for each revision 325*0c3a5702SAndreas Gohr * 326*0c3a5702SAndreas Gohr * @param int $rev1 oldest revision timestamp 327*0c3a5702SAndreas Gohr * @param int $rev2 newest revision timestamp (0 looks up last revision) 328*0c3a5702SAndreas Gohr * @param int $max maximum number of revisions returned 329*0c3a5702SAndreas Gohr * @return array with two arrays with revisions surrounding rev1 respectively rev2 330*0c3a5702SAndreas Gohr */ 331*0c3a5702SAndreas Gohr public function getRevisionsAround($rev1, $rev2, $max = 50) 332*0c3a5702SAndreas Gohr { 333*0c3a5702SAndreas Gohr $max = floor(abs($max) / 2) * 2 + 1; 334*0c3a5702SAndreas Gohr $rev1 = max($rev1, 0); 335*0c3a5702SAndreas Gohr $rev2 = max($rev2, 0); 336*0c3a5702SAndreas Gohr 337*0c3a5702SAndreas Gohr if ($rev2) { 338*0c3a5702SAndreas Gohr if ($rev2 < $rev1) { 339*0c3a5702SAndreas Gohr $rev = $rev2; 340*0c3a5702SAndreas Gohr $rev2 = $rev1; 341*0c3a5702SAndreas Gohr $rev1 = $rev; 342*0c3a5702SAndreas Gohr } 343*0c3a5702SAndreas Gohr } else { 344*0c3a5702SAndreas Gohr //empty right side means a removed page. Look up last revision. 345*0c3a5702SAndreas Gohr $revs = $this->getRevisions(-1, 1); 346*0c3a5702SAndreas Gohr $rev2 = $revs[0]; 347*0c3a5702SAndreas Gohr } 348*0c3a5702SAndreas Gohr //collect revisions around rev2 349*0c3a5702SAndreas Gohr list($revs2, $allrevs, $fp, $lines, $head, $tail) = $this->retrieveRevisionsAround($rev2, $max); 350*0c3a5702SAndreas Gohr 351*0c3a5702SAndreas Gohr if (empty($revs2)) return array(array(), array()); 352*0c3a5702SAndreas Gohr 353*0c3a5702SAndreas Gohr //collect revisions around rev1 354*0c3a5702SAndreas Gohr $index = array_search($rev1, $allrevs); 355*0c3a5702SAndreas Gohr if ($index === false) { 356*0c3a5702SAndreas Gohr //no overlapping revisions 357*0c3a5702SAndreas Gohr list($revs1, , , , ,) = $this->retrieveRevisionsAround($rev1, $max); 358*0c3a5702SAndreas Gohr if (empty($revs1)) $revs1 = array(); 359*0c3a5702SAndreas Gohr } else { 360*0c3a5702SAndreas Gohr //revisions overlaps, reuse revisions around rev2 361*0c3a5702SAndreas Gohr $revs1 = $allrevs; 362*0c3a5702SAndreas Gohr while ($head > 0) { 363*0c3a5702SAndreas Gohr for ($i = count($lines) - 1; $i >= 0; $i--) { 364*0c3a5702SAndreas Gohr $tmp = parseChangelogLine($lines[$i]); 365*0c3a5702SAndreas Gohr if ($tmp !== false) { 366*0c3a5702SAndreas Gohr $this->cache[$this->id][$tmp['date']] = $tmp; 367*0c3a5702SAndreas Gohr $revs1[] = $tmp['date']; 368*0c3a5702SAndreas Gohr $index++; 369*0c3a5702SAndreas Gohr 370*0c3a5702SAndreas Gohr if ($index > floor($max / 2)) break 2; 371*0c3a5702SAndreas Gohr } 372*0c3a5702SAndreas Gohr } 373*0c3a5702SAndreas Gohr 374*0c3a5702SAndreas Gohr list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1); 375*0c3a5702SAndreas Gohr } 376*0c3a5702SAndreas Gohr sort($revs1); 377*0c3a5702SAndreas Gohr //return wanted selection 378*0c3a5702SAndreas Gohr $revs1 = array_slice($revs1, max($index - floor($max / 2), 0), $max); 379*0c3a5702SAndreas Gohr } 380*0c3a5702SAndreas Gohr 381*0c3a5702SAndreas Gohr return array(array_reverse($revs1), array_reverse($revs2)); 382*0c3a5702SAndreas Gohr } 383*0c3a5702SAndreas Gohr 384*0c3a5702SAndreas Gohr /** 385*0c3a5702SAndreas Gohr * Returns lines from changelog. 386*0c3a5702SAndreas Gohr * If file larger than $chuncksize, only chunck is read that could contain $rev. 387*0c3a5702SAndreas Gohr * 388*0c3a5702SAndreas Gohr * @param int $rev revision timestamp 389*0c3a5702SAndreas Gohr * @return array|false 390*0c3a5702SAndreas Gohr * if success returns array(fp, array(changeloglines), $head, $tail, $eof) 391*0c3a5702SAndreas Gohr * where fp only defined for chuck reading, needs closing. 392*0c3a5702SAndreas Gohr * otherwise false 393*0c3a5702SAndreas Gohr */ 394*0c3a5702SAndreas Gohr protected function readloglines($rev) 395*0c3a5702SAndreas Gohr { 396*0c3a5702SAndreas Gohr $file = $this->getChangelogFilename(); 397*0c3a5702SAndreas Gohr 398*0c3a5702SAndreas Gohr if (!file_exists($file)) { 399*0c3a5702SAndreas Gohr return false; 400*0c3a5702SAndreas Gohr } 401*0c3a5702SAndreas Gohr 402*0c3a5702SAndreas Gohr $fp = null; 403*0c3a5702SAndreas Gohr $head = 0; 404*0c3a5702SAndreas Gohr $tail = 0; 405*0c3a5702SAndreas Gohr $eof = 0; 406*0c3a5702SAndreas Gohr 407*0c3a5702SAndreas Gohr if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) { 408*0c3a5702SAndreas Gohr // read whole file 409*0c3a5702SAndreas Gohr $lines = file($file); 410*0c3a5702SAndreas Gohr if ($lines === false) { 411*0c3a5702SAndreas Gohr return false; 412*0c3a5702SAndreas Gohr } 413*0c3a5702SAndreas Gohr } else { 414*0c3a5702SAndreas Gohr // read by chunk 415*0c3a5702SAndreas Gohr $fp = fopen($file, 'rb'); // "file pointer" 416*0c3a5702SAndreas Gohr if ($fp === false) { 417*0c3a5702SAndreas Gohr return false; 418*0c3a5702SAndreas Gohr } 419*0c3a5702SAndreas Gohr $head = 0; 420*0c3a5702SAndreas Gohr fseek($fp, 0, SEEK_END); 421*0c3a5702SAndreas Gohr $eof = ftell($fp); 422*0c3a5702SAndreas Gohr $tail = $eof; 423*0c3a5702SAndreas Gohr 424*0c3a5702SAndreas Gohr // find chunk 425*0c3a5702SAndreas Gohr while ($tail - $head > $this->chunk_size) { 426*0c3a5702SAndreas Gohr $finger = $head + floor(($tail - $head) / 2.0); 427*0c3a5702SAndreas Gohr $finger = $this->getNewlinepointer($fp, $finger); 428*0c3a5702SAndreas Gohr $tmp = fgets($fp); 429*0c3a5702SAndreas Gohr if ($finger == $head || $finger == $tail) { 430*0c3a5702SAndreas Gohr break; 431*0c3a5702SAndreas Gohr } 432*0c3a5702SAndreas Gohr $tmp = parseChangelogLine($tmp); 433*0c3a5702SAndreas Gohr $finger_rev = $tmp['date']; 434*0c3a5702SAndreas Gohr 435*0c3a5702SAndreas Gohr if ($finger_rev > $rev) { 436*0c3a5702SAndreas Gohr $tail = $finger; 437*0c3a5702SAndreas Gohr } else { 438*0c3a5702SAndreas Gohr $head = $finger; 439*0c3a5702SAndreas Gohr } 440*0c3a5702SAndreas Gohr } 441*0c3a5702SAndreas Gohr 442*0c3a5702SAndreas Gohr if ($tail - $head < 1) { 443*0c3a5702SAndreas Gohr // cound not find chunk, assume requested rev is missing 444*0c3a5702SAndreas Gohr fclose($fp); 445*0c3a5702SAndreas Gohr return false; 446*0c3a5702SAndreas Gohr } 447*0c3a5702SAndreas Gohr 448*0c3a5702SAndreas Gohr $lines = $this->readChunk($fp, $head, $tail); 449*0c3a5702SAndreas Gohr } 450*0c3a5702SAndreas Gohr return array( 451*0c3a5702SAndreas Gohr $fp, 452*0c3a5702SAndreas Gohr $lines, 453*0c3a5702SAndreas Gohr $head, 454*0c3a5702SAndreas Gohr $tail, 455*0c3a5702SAndreas Gohr $eof, 456*0c3a5702SAndreas Gohr ); 457*0c3a5702SAndreas Gohr } 458*0c3a5702SAndreas Gohr 459*0c3a5702SAndreas Gohr /** 460*0c3a5702SAndreas Gohr * Read chunk and return array with lines of given chunck. 461*0c3a5702SAndreas Gohr * Has no check if $head and $tail are really at a new line 462*0c3a5702SAndreas Gohr * 463*0c3a5702SAndreas Gohr * @param resource $fp resource filepointer 464*0c3a5702SAndreas Gohr * @param int $head start point chunck 465*0c3a5702SAndreas Gohr * @param int $tail end point chunck 466*0c3a5702SAndreas Gohr * @return array lines read from chunck 467*0c3a5702SAndreas Gohr */ 468*0c3a5702SAndreas Gohr protected function readChunk($fp, $head, $tail) 469*0c3a5702SAndreas Gohr { 470*0c3a5702SAndreas Gohr $chunk = ''; 471*0c3a5702SAndreas Gohr $chunk_size = max($tail - $head, 0); // found chunk size 472*0c3a5702SAndreas Gohr $got = 0; 473*0c3a5702SAndreas Gohr fseek($fp, $head); 474*0c3a5702SAndreas Gohr while ($got < $chunk_size && !feof($fp)) { 475*0c3a5702SAndreas Gohr $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0)); 476*0c3a5702SAndreas Gohr if ($tmp === false) { //error state 477*0c3a5702SAndreas Gohr break; 478*0c3a5702SAndreas Gohr } 479*0c3a5702SAndreas Gohr $got += strlen($tmp); 480*0c3a5702SAndreas Gohr $chunk .= $tmp; 481*0c3a5702SAndreas Gohr } 482*0c3a5702SAndreas Gohr $lines = explode("\n", $chunk); 483*0c3a5702SAndreas Gohr array_pop($lines); // remove trailing newline 484*0c3a5702SAndreas Gohr return $lines; 485*0c3a5702SAndreas Gohr } 486*0c3a5702SAndreas Gohr 487*0c3a5702SAndreas Gohr /** 488*0c3a5702SAndreas Gohr * Set pointer to first new line after $finger and return its position 489*0c3a5702SAndreas Gohr * 490*0c3a5702SAndreas Gohr * @param resource $fp filepointer 491*0c3a5702SAndreas Gohr * @param int $finger a pointer 492*0c3a5702SAndreas Gohr * @return int pointer 493*0c3a5702SAndreas Gohr */ 494*0c3a5702SAndreas Gohr protected function getNewlinepointer($fp, $finger) 495*0c3a5702SAndreas Gohr { 496*0c3a5702SAndreas Gohr fseek($fp, $finger); 497*0c3a5702SAndreas Gohr $nl = $finger; 498*0c3a5702SAndreas Gohr if ($finger > 0) { 499*0c3a5702SAndreas Gohr fgets($fp); // slip the finger forward to a new line 500*0c3a5702SAndreas Gohr $nl = ftell($fp); 501*0c3a5702SAndreas Gohr } 502*0c3a5702SAndreas Gohr return $nl; 503*0c3a5702SAndreas Gohr } 504*0c3a5702SAndreas Gohr 505*0c3a5702SAndreas Gohr /** 506*0c3a5702SAndreas Gohr * Check whether given revision is the current page 507*0c3a5702SAndreas Gohr * 508*0c3a5702SAndreas Gohr * @param int $rev timestamp of current page 509*0c3a5702SAndreas Gohr * @return bool true if $rev is current revision, otherwise false 510*0c3a5702SAndreas Gohr */ 511*0c3a5702SAndreas Gohr public function isCurrentRevision($rev) 512*0c3a5702SAndreas Gohr { 513*0c3a5702SAndreas Gohr return $rev == @filemtime($this->getFilename()); 514*0c3a5702SAndreas Gohr } 515*0c3a5702SAndreas Gohr 516*0c3a5702SAndreas Gohr /** 517*0c3a5702SAndreas Gohr * Return an existing revision for a specific date which is 518*0c3a5702SAndreas Gohr * the current one or younger or equal then the date 519*0c3a5702SAndreas Gohr * 520*0c3a5702SAndreas Gohr * @param number $date_at timestamp 521*0c3a5702SAndreas Gohr * @return string revision ('' for current) 522*0c3a5702SAndreas Gohr */ 523*0c3a5702SAndreas Gohr public function getLastRevisionAt($date_at) 524*0c3a5702SAndreas Gohr { 525*0c3a5702SAndreas Gohr //requested date_at(timestamp) younger or equal then modified_time($this->id) => load current 526*0c3a5702SAndreas Gohr if (file_exists($this->getFilename()) && $date_at >= @filemtime($this->getFilename())) { 527*0c3a5702SAndreas Gohr return ''; 528*0c3a5702SAndreas Gohr } else { 529*0c3a5702SAndreas Gohr if ($rev = $this->getRelativeRevision($date_at + 1, -1)) { //+1 to get also the requested date revision 530*0c3a5702SAndreas Gohr return $rev; 531*0c3a5702SAndreas Gohr } else { 532*0c3a5702SAndreas Gohr return false; 533*0c3a5702SAndreas Gohr } 534*0c3a5702SAndreas Gohr } 535*0c3a5702SAndreas Gohr } 536*0c3a5702SAndreas Gohr 537*0c3a5702SAndreas Gohr /** 538*0c3a5702SAndreas Gohr * Returns the next lines of the changelog of the chunck before head or after tail 539*0c3a5702SAndreas Gohr * 540*0c3a5702SAndreas Gohr * @param resource $fp filepointer 541*0c3a5702SAndreas Gohr * @param int $head position head of last chunk 542*0c3a5702SAndreas Gohr * @param int $tail position tail of last chunk 543*0c3a5702SAndreas Gohr * @param int $direction positive forward, negative backward 544*0c3a5702SAndreas Gohr * @return array with entries: 545*0c3a5702SAndreas Gohr * - $lines: changelog lines of readed chunk 546*0c3a5702SAndreas Gohr * - $head: head of chunk 547*0c3a5702SAndreas Gohr * - $tail: tail of chunk 548*0c3a5702SAndreas Gohr */ 549*0c3a5702SAndreas Gohr protected function readAdjacentChunk($fp, $head, $tail, $direction) 550*0c3a5702SAndreas Gohr { 551*0c3a5702SAndreas Gohr if (!$fp) return array(array(), $head, $tail); 552*0c3a5702SAndreas Gohr 553*0c3a5702SAndreas Gohr if ($direction > 0) { 554*0c3a5702SAndreas Gohr //read forward 555*0c3a5702SAndreas Gohr $head = $tail; 556*0c3a5702SAndreas Gohr $tail = $head + floor($this->chunk_size * (2 / 3)); 557*0c3a5702SAndreas Gohr $tail = $this->getNewlinepointer($fp, $tail); 558*0c3a5702SAndreas Gohr } else { 559*0c3a5702SAndreas Gohr //read backward 560*0c3a5702SAndreas Gohr $tail = $head; 561*0c3a5702SAndreas Gohr $head = max($tail - $this->chunk_size, 0); 562*0c3a5702SAndreas Gohr while (true) { 563*0c3a5702SAndreas Gohr $nl = $this->getNewlinepointer($fp, $head); 564*0c3a5702SAndreas Gohr // was the chunk big enough? if not, take another bite 565*0c3a5702SAndreas Gohr if ($nl > 0 && $tail <= $nl) { 566*0c3a5702SAndreas Gohr $head = max($head - $this->chunk_size, 0); 567*0c3a5702SAndreas Gohr } else { 568*0c3a5702SAndreas Gohr $head = $nl; 569*0c3a5702SAndreas Gohr break; 570*0c3a5702SAndreas Gohr } 571*0c3a5702SAndreas Gohr } 572*0c3a5702SAndreas Gohr } 573*0c3a5702SAndreas Gohr 574*0c3a5702SAndreas Gohr //load next chunck 575*0c3a5702SAndreas Gohr $lines = $this->readChunk($fp, $head, $tail); 576*0c3a5702SAndreas Gohr return array($lines, $head, $tail); 577*0c3a5702SAndreas Gohr } 578*0c3a5702SAndreas Gohr 579*0c3a5702SAndreas Gohr /** 580*0c3a5702SAndreas Gohr * Collect the $max revisions near to the timestamp $rev 581*0c3a5702SAndreas Gohr * 582*0c3a5702SAndreas Gohr * @param int $rev revision timestamp 583*0c3a5702SAndreas Gohr * @param int $max maximum number of revisions to be returned 584*0c3a5702SAndreas Gohr * @return bool|array 585*0c3a5702SAndreas Gohr * return array with entries: 586*0c3a5702SAndreas Gohr * - $requestedrevs: array of with $max revision timestamps 587*0c3a5702SAndreas Gohr * - $revs: all parsed revision timestamps 588*0c3a5702SAndreas Gohr * - $fp: filepointer only defined for chuck reading, needs closing. 589*0c3a5702SAndreas Gohr * - $lines: non-parsed changelog lines before the parsed revisions 590*0c3a5702SAndreas Gohr * - $head: position of first readed changelogline 591*0c3a5702SAndreas Gohr * - $lasttail: position of end of last readed changelogline 592*0c3a5702SAndreas Gohr * otherwise false 593*0c3a5702SAndreas Gohr */ 594*0c3a5702SAndreas Gohr protected function retrieveRevisionsAround($rev, $max) 595*0c3a5702SAndreas Gohr { 596*0c3a5702SAndreas Gohr //get lines from changelog 597*0c3a5702SAndreas Gohr list($fp, $lines, $starthead, $starttail, /* $eof */) = $this->readloglines($rev); 598*0c3a5702SAndreas Gohr if (empty($lines)) return false; 599*0c3a5702SAndreas Gohr 600*0c3a5702SAndreas Gohr //parse chunk containing $rev, and read forward more chunks until $max/2 is reached 601*0c3a5702SAndreas Gohr $head = $starthead; 602*0c3a5702SAndreas Gohr $tail = $starttail; 603*0c3a5702SAndreas Gohr $revs = array(); 604*0c3a5702SAndreas Gohr $aftercount = $beforecount = 0; 605*0c3a5702SAndreas Gohr while (count($lines) > 0) { 606*0c3a5702SAndreas Gohr foreach ($lines as $line) { 607*0c3a5702SAndreas Gohr $tmp = parseChangelogLine($line); 608*0c3a5702SAndreas Gohr if ($tmp !== false) { 609*0c3a5702SAndreas Gohr $this->cache[$this->id][$tmp['date']] = $tmp; 610*0c3a5702SAndreas Gohr $revs[] = $tmp['date']; 611*0c3a5702SAndreas Gohr if ($tmp['date'] >= $rev) { 612*0c3a5702SAndreas Gohr //count revs after reference $rev 613*0c3a5702SAndreas Gohr $aftercount++; 614*0c3a5702SAndreas Gohr if ($aftercount == 1) $beforecount = count($revs); 615*0c3a5702SAndreas Gohr } 616*0c3a5702SAndreas Gohr //enough revs after reference $rev? 617*0c3a5702SAndreas Gohr if ($aftercount > floor($max / 2)) break 2; 618*0c3a5702SAndreas Gohr } 619*0c3a5702SAndreas Gohr } 620*0c3a5702SAndreas Gohr //retrieve next chunk 621*0c3a5702SAndreas Gohr list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, 1); 622*0c3a5702SAndreas Gohr } 623*0c3a5702SAndreas Gohr if ($aftercount == 0) return false; 624*0c3a5702SAndreas Gohr 625*0c3a5702SAndreas Gohr $lasttail = $tail; 626*0c3a5702SAndreas Gohr 627*0c3a5702SAndreas Gohr //read additional chuncks backward until $max/2 is reached and total number of revs is equal to $max 628*0c3a5702SAndreas Gohr $lines = array(); 629*0c3a5702SAndreas Gohr $i = 0; 630*0c3a5702SAndreas Gohr if ($aftercount > 0) { 631*0c3a5702SAndreas Gohr $head = $starthead; 632*0c3a5702SAndreas Gohr $tail = $starttail; 633*0c3a5702SAndreas Gohr while ($head > 0) { 634*0c3a5702SAndreas Gohr list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1); 635*0c3a5702SAndreas Gohr 636*0c3a5702SAndreas Gohr for ($i = count($lines) - 1; $i >= 0; $i--) { 637*0c3a5702SAndreas Gohr $tmp = parseChangelogLine($lines[$i]); 638*0c3a5702SAndreas Gohr if ($tmp !== false) { 639*0c3a5702SAndreas Gohr $this->cache[$this->id][$tmp['date']] = $tmp; 640*0c3a5702SAndreas Gohr $revs[] = $tmp['date']; 641*0c3a5702SAndreas Gohr $beforecount++; 642*0c3a5702SAndreas Gohr //enough revs before reference $rev? 643*0c3a5702SAndreas Gohr if ($beforecount > max(floor($max / 2), $max - $aftercount)) break 2; 644*0c3a5702SAndreas Gohr } 645*0c3a5702SAndreas Gohr } 646*0c3a5702SAndreas Gohr } 647*0c3a5702SAndreas Gohr } 648*0c3a5702SAndreas Gohr sort($revs); 649*0c3a5702SAndreas Gohr 650*0c3a5702SAndreas Gohr //keep only non-parsed lines 651*0c3a5702SAndreas Gohr $lines = array_slice($lines, 0, $i); 652*0c3a5702SAndreas Gohr //trunk desired selection 653*0c3a5702SAndreas Gohr $requestedrevs = array_slice($revs, -$max, $max); 654*0c3a5702SAndreas Gohr 655*0c3a5702SAndreas Gohr return array($requestedrevs, $revs, $fp, $lines, $head, $lasttail); 656*0c3a5702SAndreas Gohr } 657*0c3a5702SAndreas Gohr} 658