1<?php 2/** 3 * Changelog handling functions 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9// Constants for known core changelog line types. 10// Use these in place of string literals for more readable code. 11define('DOKU_CHANGE_TYPE_CREATE', 'C'); 12define('DOKU_CHANGE_TYPE_EDIT', 'E'); 13define('DOKU_CHANGE_TYPE_MINOR_EDIT', 'e'); 14define('DOKU_CHANGE_TYPE_DELETE', 'D'); 15define('DOKU_CHANGE_TYPE_REVERT', 'R'); 16 17/** 18 * parses a changelog line into it's components 19 * 20 * @author Ben Coburn <btcoburn@silicodon.net> 21 */ 22function parseChangelogLine($line) { 23 $tmp = explode("\t", $line); 24 if ($tmp!==false && count($tmp)>1) { 25 $info = array(); 26 $info['date'] = (int)$tmp[0]; // unix timestamp 27 $info['ip'] = $tmp[1]; // IPv4 address (127.0.0.1) 28 $info['type'] = $tmp[2]; // log line type 29 $info['id'] = $tmp[3]; // page id 30 $info['user'] = $tmp[4]; // user name 31 $info['sum'] = $tmp[5]; // edit summary (or action reason) 32 $info['extra'] = rtrim($tmp[6], "\n"); // extra data (varies by line type) 33 return $info; 34 } else { return false; } 35} 36 37/** 38 * Add's an entry to the changelog and saves the metadata for the page 39 * 40 * @param int $date Timestamp of the change 41 * @param String $id Name of the affected page 42 * @param String $type Type of the change see DOKU_CHANGE_TYPE_* 43 * @param String $summary Summary of the change 44 * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page 45 * @param array $flags Additional flags in a key value array. 46 * Availible flags: 47 * - ExternalEdit - mark as an external edit. 48 * 49 * @author Andreas Gohr <andi@splitbrain.org> 50 * @author Esther Brunner <wikidesign@gmail.com> 51 * @author Ben Coburn <btcoburn@silicodon.net> 52 */ 53function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null){ 54 global $conf, $INFO; 55 /** @var Input $INPUT */ 56 global $INPUT; 57 58 // check for special flags as keys 59 if (!is_array($flags)) { $flags = array(); } 60 $flagExternalEdit = isset($flags['ExternalEdit']); 61 62 $id = cleanid($id); 63 $file = wikiFN($id); 64 $created = @filectime($file); 65 $minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT); 66 $wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE); 67 68 if(!$date) $date = time(); //use current time if none supplied 69 $remote = (!$flagExternalEdit)?clientIP(true):'127.0.0.1'; 70 $user = (!$flagExternalEdit)?$INPUT->server->str('REMOTE_USER'):''; 71 72 $strip = array("\t", "\n"); 73 $logline = array( 74 'date' => $date, 75 'ip' => $remote, 76 'type' => str_replace($strip, '', $type), 77 'id' => $id, 78 'user' => $user, 79 'sum' => utf8_substr(str_replace($strip, '', $summary),0,255), 80 'extra' => str_replace($strip, '', $extra) 81 ); 82 83 // update metadata 84 if (!$wasRemoved) { 85 $oldmeta = p_read_metadata($id); 86 $meta = array(); 87 if (!$INFO['exists'] && empty($oldmeta['persistent']['date']['created'])){ // newly created 88 $meta['date']['created'] = $created; 89 if ($user){ 90 $meta['creator'] = $INFO['userinfo']['name']; 91 $meta['user'] = $user; 92 } 93 } elseif (!$INFO['exists'] && !empty($oldmeta['persistent']['date']['created'])) { // re-created / restored 94 $meta['date']['created'] = $oldmeta['persistent']['date']['created']; 95 $meta['date']['modified'] = $created; // use the files ctime here 96 $meta['creator'] = $oldmeta['persistent']['creator']; 97 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 98 } elseif (!$minor) { // non-minor modification 99 $meta['date']['modified'] = $date; 100 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 101 } 102 $meta['last_change'] = $logline; 103 p_set_metadata($id, $meta); 104 } 105 106 // add changelog lines 107 $logline = implode("\t", $logline)."\n"; 108 io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog 109 io_saveFile($conf['changelog'],$logline,true); //global changelog cache 110} 111 112/** 113 * Add's an entry to the media changelog 114 * 115 * @author Michael Hamann <michael@content-space.de> 116 * @author Andreas Gohr <andi@splitbrain.org> 117 * @author Esther Brunner <wikidesign@gmail.com> 118 * @author Ben Coburn <btcoburn@silicodon.net> 119 */ 120function addMediaLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null){ 121 global $conf; 122 /** @var Input $INPUT */ 123 global $INPUT; 124 125 $id = cleanid($id); 126 127 if(!$date) $date = time(); //use current time if none supplied 128 $remote = clientIP(true); 129 $user = $INPUT->server->str('REMOTE_USER'); 130 131 $strip = array("\t", "\n"); 132 $logline = array( 133 'date' => $date, 134 'ip' => $remote, 135 'type' => str_replace($strip, '', $type), 136 'id' => $id, 137 'user' => $user, 138 'sum' => utf8_substr(str_replace($strip, '', $summary),0,255), 139 'extra' => str_replace($strip, '', $extra) 140 ); 141 142 // add changelog lines 143 $logline = implode("\t", $logline)."\n"; 144 io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache 145 io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog 146} 147 148/** 149 * returns an array of recently changed files using the 150 * changelog 151 * 152 * The following constants can be used to control which changes are 153 * included. Add them together as needed. 154 * 155 * RECENTS_SKIP_DELETED - don't include deleted pages 156 * RECENTS_SKIP_MINORS - don't include minor changes 157 * RECENTS_SKIP_SUBSPACES - don't include subspaces 158 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 159 * RECENTS_MEDIA_PAGES_MIXED - return both media changes and page changes 160 * 161 * @param int $first number of first entry returned (for paginating 162 * @param int $num return $num entries 163 * @param string $ns restrict to given namespace 164 * @param int $flags see above 165 * @return array recently changed files 166 * 167 * @author Ben Coburn <btcoburn@silicodon.net> 168 * @author Kate Arzamastseva <pshns@ukr.net> 169 */ 170function getRecents($first,$num,$ns='',$flags=0){ 171 global $conf; 172 $recent = array(); 173 $count = 0; 174 175 if(!$num) 176 return $recent; 177 178 // read all recent changes. (kept short) 179 if ($flags & RECENTS_MEDIA_CHANGES) { 180 $lines = @file($conf['media_changelog']); 181 } else { 182 $lines = @file($conf['changelog']); 183 } 184 $lines_position = count($lines)-1; 185 $media_lines_position = 0; 186 $media_lines = array(); 187 188 if ($flags & RECENTS_MEDIA_PAGES_MIXED) { 189 $media_lines = @file($conf['media_changelog']); 190 $media_lines_position = count($media_lines)-1; 191 } 192 193 $seen = array(); // caches seen lines, _handleRecent() skips them 194 195 // handle lines 196 while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) { 197 if (empty($rec) && $lines_position >= 0) { 198 $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen); 199 if (!$rec) { 200 $lines_position --; 201 continue; 202 } 203 } 204 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) { 205 $media_rec = _handleRecent(@$media_lines[$media_lines_position], $ns, $flags | RECENTS_MEDIA_CHANGES, $seen); 206 if (!$media_rec) { 207 $media_lines_position --; 208 continue; 209 } 210 } 211 if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) { 212 $media_lines_position--; 213 $x = $media_rec; 214 $x['media'] = true; 215 $media_rec = false; 216 } else { 217 $lines_position--; 218 $x = $rec; 219 if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true; 220 $rec = false; 221 } 222 if(--$first >= 0) continue; // skip first entries 223 $recent[] = $x; 224 $count++; 225 // break when we have enough entries 226 if($count >= $num){ break; } 227 } 228 return $recent; 229} 230 231/** 232 * returns an array of files changed since a given time using the 233 * changelog 234 * 235 * The following constants can be used to control which changes are 236 * included. Add them together as needed. 237 * 238 * RECENTS_SKIP_DELETED - don't include deleted pages 239 * RECENTS_SKIP_MINORS - don't include minor changes 240 * RECENTS_SKIP_SUBSPACES - don't include subspaces 241 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 242 * 243 * @param int $from date of the oldest entry to return 244 * @param int $to date of the newest entry to return (for pagination, optional) 245 * @param string $ns restrict to given namespace (optional) 246 * @param int $flags see above (optional) 247 * @return array of files 248 * 249 * @author Michael Hamann <michael@content-space.de> 250 * @author Ben Coburn <btcoburn@silicodon.net> 251 */ 252function getRecentsSince($from,$to=null,$ns='',$flags=0){ 253 global $conf; 254 $recent = array(); 255 256 if($to && $to < $from) 257 return $recent; 258 259 // read all recent changes. (kept short) 260 if ($flags & RECENTS_MEDIA_CHANGES) { 261 $lines = @file($conf['media_changelog']); 262 } else { 263 $lines = @file($conf['changelog']); 264 } 265 if(!$lines) return $recent; 266 267 // we start searching at the end of the list 268 $lines = array_reverse($lines); 269 270 // handle lines 271 $seen = array(); // caches seen lines, _handleRecent() skips them 272 273 foreach($lines as $line){ 274 $rec = _handleRecent($line, $ns, $flags, $seen); 275 if($rec !== false) { 276 if ($rec['date'] >= $from) { 277 if (!$to || $rec['date'] <= $to) { 278 $recent[] = $rec; 279 } 280 } else { 281 break; 282 } 283 } 284 } 285 286 return array_reverse($recent); 287} 288 289/** 290 * Internal function used by getRecents 291 * 292 * don't call directly 293 * 294 * @see getRecents() 295 * @author Andreas Gohr <andi@splitbrain.org> 296 * @author Ben Coburn <btcoburn@silicodon.net> 297 */ 298function _handleRecent($line,$ns,$flags,&$seen){ 299 if(empty($line)) return false; //skip empty lines 300 301 // split the line into parts 302 $recent = parseChangelogLine($line); 303 if ($recent===false) { return false; } 304 305 // skip seen ones 306 if(isset($seen[$recent['id']])) return false; 307 308 // skip minors 309 if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false; 310 311 // remember in seen to skip additional sights 312 $seen[$recent['id']] = 1; 313 314 // check if it's a hidden page 315 if(isHiddenPage($recent['id'])) return false; 316 317 // filter namespace 318 if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false; 319 320 // exclude subnamespaces 321 if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false; 322 323 // check ACL 324 if ($flags & RECENTS_MEDIA_CHANGES) { 325 $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*'); 326 } else { 327 $recent['perms'] = auth_quickaclcheck($recent['id']); 328 } 329 if ($recent['perms'] < AUTH_READ) return false; 330 331 // check existance 332 if($flags & RECENTS_SKIP_DELETED){ 333 $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id'])); 334 if(!@file_exists($fn)) return false; 335 } 336 337 return $recent; 338} 339 340/** 341 * Class ChangeLog 342 * methods for handling of changelog of pages or media files 343 */ 344abstract class ChangeLog { 345 346 /** @var string */ 347 protected $id; 348 /** @var int */ 349 protected $chunk_size; 350 /** @var array */ 351 protected $cache; 352 353 /** 354 * Constructor 355 * 356 * @param string $id page id 357 * @param int $chunk_size maximum block size read from file 358 */ 359 public function __construct($id, $chunk_size = 8192) { 360 global $cache_revinfo; 361 362 $this->cache =& $cache_revinfo; 363 if(!isset($this->cache[$id])) { 364 $this->cache[$id] = array(); 365 } 366 367 $this->id = $id; 368 $this->setChunkSize($chunk_size); 369 370 } 371 372 /** 373 * Set chunk size for file reading 374 * Chunk size zero let read whole file at once 375 * 376 * @param int $chunk_size maximum block size read from file 377 */ 378 public function setChunkSize($chunk_size) { 379 if(!is_numeric($chunk_size)) $chunk_size = 0; 380 381 $this->chunk_size = (int) max($chunk_size, 0); 382 } 383 384 /** 385 * Returns path to changelog 386 * 387 * @return string path to file 388 */ 389 abstract protected function getChangelogFilename(); 390 391 /** 392 * Returns path to current page/media 393 * 394 * @return string path to file 395 */ 396 abstract protected function getFilename(); 397 398 /** 399 * Get the changelog information for a specific page id and revision (timestamp) 400 * 401 * Adjacent changelog lines are optimistically parsed and cached to speed up 402 * consecutive calls to getRevisionInfo. For large changelog files, only the chunk 403 * containing the requested changelog line is read. 404 * 405 * @param int $rev revision timestamp 406 * @return bool|array false or array with entries: 407 * - date: unix timestamp 408 * - ip: IPv4 address (127.0.0.1) 409 * - type: log line type 410 * - id: page id 411 * - user: user name 412 * - sum: edit summary (or action reason) 413 * - extra: extra data (varies by line type) 414 * 415 * @author Ben Coburn <btcoburn@silicodon.net> 416 * @author Kate Arzamastseva <pshns@ukr.net> 417 */ 418 public function getRevisionInfo($rev) { 419 $rev = max($rev, 0); 420 421 // check if it's already in the memory cache 422 if(isset($this->cache[$this->id]) && isset($this->cache[$this->id][$rev])) { 423 return $this->cache[$this->id][$rev]; 424 } 425 426 //read lines from changelog 427 list($fp, $lines) = $this->readloglines($rev); 428 if($fp) { 429 fclose($fp); 430 } 431 if(empty($lines)) return false; 432 433 // parse and cache changelog lines 434 foreach($lines as $value) { 435 $tmp = parseChangelogLine($value); 436 if($tmp !== false) { 437 $this->cache[$this->id][$tmp['date']] = $tmp; 438 } 439 } 440 if(!isset($this->cache[$this->id][$rev])) { 441 return false; 442 } 443 return $this->cache[$this->id][$rev]; 444 } 445 446 /** 447 * Return a list of page revisions numbers 448 * 449 * Does not guarantee that the revision exists in the attic, 450 * only that a line with the date exists in the changelog. 451 * By default the current revision is skipped. 452 * 453 * The current revision is automatically skipped when the page exists. 454 * See $INFO['meta']['last_change'] for the current revision. 455 * A negative $first let read the current revision too. 456 * 457 * For efficiency, the log lines are parsed and cached for later 458 * calls to getRevisionInfo. Large changelog files are read 459 * backwards in chunks until the requested number of changelog 460 * lines are recieved. 461 * 462 * @param int $first skip the first n changelog lines 463 * @param int $num number of revisions to return 464 * @return array with the revision timestamps 465 * 466 * @author Ben Coburn <btcoburn@silicodon.net> 467 * @author Kate Arzamastseva <pshns@ukr.net> 468 */ 469 public function getRevisions($first, $num) { 470 $revs = array(); 471 $lines = array(); 472 $count = 0; 473 474 $num = max($num, 0); 475 if($num == 0) { 476 return $revs; 477 } 478 479 if($first < 0) { 480 $first = 0; 481 } else if(@file_exists($this->getFilename())) { 482 // skip current revision if the page exists 483 $first = max($first + 1, 0); 484 } 485 486 $file = $this->getChangelogFilename(); 487 488 if(!@file_exists($file)) { 489 return $revs; 490 } 491 if(filesize($file) < $this->chunk_size || $this->chunk_size == 0) { 492 // read whole file 493 $lines = file($file); 494 if($lines === false) { 495 return $revs; 496 } 497 } else { 498 // read chunks backwards 499 $fp = fopen($file, 'rb'); // "file pointer" 500 if($fp === false) { 501 return $revs; 502 } 503 fseek($fp, 0, SEEK_END); 504 $tail = ftell($fp); 505 506 // chunk backwards 507 $finger = max($tail - $this->chunk_size, 0); 508 while($count < $num + $first) { 509 $nl = $this->getNewlinepointer($fp, $finger); 510 511 // was the chunk big enough? if not, take another bite 512 if($nl > 0 && $tail <= $nl) { 513 $finger = max($finger - $this->chunk_size, 0); 514 continue; 515 } else { 516 $finger = $nl; 517 } 518 519 // read chunk 520 $chunk = ''; 521 $read_size = max($tail - $finger, 0); // found chunk size 522 $got = 0; 523 while($got < $read_size && !feof($fp)) { 524 $tmp = @fread($fp, max(min($this->chunk_size, $read_size - $got), 0)); 525 if($tmp === false) { 526 break; 527 } //error state 528 $got += strlen($tmp); 529 $chunk .= $tmp; 530 } 531 $tmp = explode("\n", $chunk); 532 array_pop($tmp); // remove trailing newline 533 534 // combine with previous chunk 535 $count += count($tmp); 536 $lines = array_merge($tmp, $lines); 537 538 // next chunk 539 if($finger == 0) { 540 break; 541 } // already read all the lines 542 else { 543 $tail = $finger; 544 $finger = max($tail - $this->chunk_size, 0); 545 } 546 } 547 fclose($fp); 548 } 549 550 // skip parsing extra lines 551 $num = max(min(count($lines) - $first, $num), 0); 552 if ($first > 0 && $num > 0) { $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num); } 553 else if($first > 0 && $num == 0) { $lines = array_slice($lines, 0, max(count($lines) - $first, 0)); } 554 else if($first == 0 && $num > 0) { $lines = array_slice($lines, max(count($lines) - $num, 0)); } 555 556 // handle lines in reverse order 557 for($i = count($lines) - 1; $i >= 0; $i--) { 558 $tmp = parseChangelogLine($lines[$i]); 559 if($tmp !== false) { 560 $this->cache[$this->id][$tmp['date']] = $tmp; 561 $revs[] = $tmp['date']; 562 } 563 } 564 565 return $revs; 566 } 567 568 /** 569 * Get the nth revision left or right handside for a specific page id and revision (timestamp) 570 * 571 * For large changelog files, only the chunk containing the 572 * reference revision $rev is read and sometimes a next chunck. 573 * 574 * Adjacent changelog lines are optimistically parsed and cached to speed up 575 * consecutive calls to getRevisionInfo. 576 * 577 * @param int $rev revision timestamp used as startdate (doesn't need to be revisionnumber) 578 * @param int $direction give position of returned revision with respect to $rev; positive=next, negative=prev 579 * @return bool|int 580 * timestamp of the requested revision 581 * otherwise false 582 */ 583 public function getRelativeRevision($rev, $direction) { 584 $rev = max($rev, 0); 585 $direction = (int) $direction; 586 587 //no direction given or last rev, so no follow-up 588 if(!$direction || ($direction > 0 && $this->isCurrentRevision($rev))) { 589 return false; 590 } 591 592 //get lines from changelog 593 list($fp, $lines, $head, $tail, $eof) = $this->readloglines($rev); 594 if(empty($lines)) return false; 595 596 // look for revisions later/earlier then $rev, when founded count till the wanted revision is reached 597 // also parse and cache changelog lines for getRevisionInfo(). 598 $revcounter = 0; 599 $relativerev = false; 600 $checkotherchunck = true; //always runs once 601 while(!$relativerev && $checkotherchunck) { 602 $tmp = array(); 603 //parse in normal or reverse order 604 $count = count($lines); 605 if($direction > 0) { 606 $start = 0; 607 $step = 1; 608 } else { 609 $start = $count - 1; 610 $step = -1; 611 } 612 for($i = $start; $i >= 0 && $i < $count; $i = $i + $step) { 613 $tmp = parseChangelogLine($lines[$i]); 614 if($tmp !== false) { 615 $this->cache[$this->id][$tmp['date']] = $tmp; 616 //look for revs older/earlier then reference $rev and select $direction-th one 617 if(($direction > 0 && $tmp['date'] > $rev) || ($direction < 0 && $tmp['date'] < $rev)) { 618 $revcounter++; 619 if($revcounter == abs($direction)) { 620 $relativerev = $tmp['date']; 621 } 622 } 623 } 624 } 625 626 //true when $rev is found, but not the wanted follow-up. 627 $checkotherchunck = $fp 628 && ($tmp['date'] == $rev || ($revcounter > 0 && !$relativerev)) 629 && !(($tail == $eof && $direction > 0) || ($head == 0 && $direction < 0)); 630 631 if($checkotherchunck) { 632 list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, $direction); 633 634 if(empty($lines)) break; 635 } 636 } 637 if($fp) { 638 fclose($fp); 639 } 640 641 return $relativerev; 642 } 643 644 /** 645 * Returns revisions around rev1 and rev2 646 * When available it returns $max entries for each revision 647 * 648 * @param int $rev1 oldest revision timestamp 649 * @param int $rev2 newest revision timestamp (0 looks up last revision) 650 * @param int $max maximum number of revisions returned 651 * @return array with two arrays with revisions surrounding rev1 respectively rev2 652 */ 653 public function getRevisionsAround($rev1, $rev2, $max = 50) { 654 $max = floor(abs($max) / 2)*2 + 1; 655 $rev1 = max($rev1, 0); 656 $rev2 = max($rev2, 0); 657 658 if($rev2) { 659 if($rev2 < $rev1) { 660 $rev = $rev2; 661 $rev2 = $rev1; 662 $rev1 = $rev; 663 } 664 } else { 665 //empty right side means a removed page. Look up last revision. 666 $revs = $this->getRevisions(-1, 1); 667 $rev2 = $revs[0]; 668 } 669 //collect revisions around rev2 670 list($revs2, $allrevs, $fp, $lines, $head, $tail) = $this->retrieveRevisionsAround($rev2, $max); 671 672 if(empty($revs2)) return array(array(), array()); 673 674 //collect revisions around rev1 675 $index = array_search($rev1, $allrevs); 676 if($index === false) { 677 //no overlapping revisions 678 list($revs1,,,,,) = $this->retrieveRevisionsAround($rev1, $max); 679 if(empty($revs1)) $revs1 = array(); 680 } else { 681 //revisions overlaps, reuse revisions around rev2 682 $revs1 = $allrevs; 683 while($head > 0) { 684 for($i = count($lines) - 1; $i >= 0; $i--) { 685 $tmp = parseChangelogLine($lines[$i]); 686 if($tmp !== false) { 687 $this->cache[$this->id][$tmp['date']] = $tmp; 688 $revs1[] = $tmp['date']; 689 $index++; 690 691 if($index > floor($max / 2)) break 2; 692 } 693 } 694 695 list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1); 696 } 697 sort($revs1); 698 //return wanted selection 699 $revs1 = array_slice($revs1, max($index - floor($max/2), 0), $max); 700 } 701 702 return array(array_reverse($revs1), array_reverse($revs2)); 703 } 704 705 /** 706 * Returns lines from changelog. 707 * If file larger than $chuncksize, only chunck is read that could contain $rev. 708 * 709 * @param int $rev revision timestamp 710 * @return array(fp, array(changeloglines), $head, $tail, $eof)|bool 711 * returns false when not succeed. fp only defined for chuck reading, needs closing. 712 */ 713 protected function readloglines($rev) { 714 $file = $this->getChangelogFilename(); 715 716 if(!@file_exists($file)) { 717 return false; 718 } 719 720 $fp = null; 721 $head = 0; 722 $tail = 0; 723 $eof = 0; 724 725 if(filesize($file) < $this->chunk_size || $this->chunk_size == 0) { 726 // read whole file 727 $lines = file($file); 728 if($lines === false) { 729 return false; 730 } 731 } else { 732 // read by chunk 733 $fp = fopen($file, 'rb'); // "file pointer" 734 if($fp === false) { 735 return false; 736 } 737 $head = 0; 738 fseek($fp, 0, SEEK_END); 739 $eof = ftell($fp); 740 $tail = $eof; 741 742 // find chunk 743 while($tail - $head > $this->chunk_size) { 744 $finger = $head + floor(($tail - $head) / 2.0); 745 $finger = $this->getNewlinepointer($fp, $finger); 746 $tmp = fgets($fp); 747 if($finger == $head || $finger == $tail) { 748 break; 749 } 750 $tmp = parseChangelogLine($tmp); 751 $finger_rev = $tmp['date']; 752 753 if($finger_rev > $rev) { 754 $tail = $finger; 755 } else { 756 $head = $finger; 757 } 758 } 759 760 if($tail - $head < 1) { 761 // cound not find chunk, assume requested rev is missing 762 fclose($fp); 763 return false; 764 } 765 766 $lines = $this->readChunk($fp, $head, $tail); 767 } 768 return array( 769 $fp, 770 $lines, 771 $head, 772 $tail, 773 $eof 774 ); 775 } 776 777 /** 778 * Read chunk and return array with lines of given chunck. 779 * Has no check if $head and $tail are really at a new line 780 * 781 * @param $fp resource filepointer 782 * @param $head int start point chunck 783 * @param $tail int end point chunck 784 * @return array lines read from chunck 785 */ 786 protected function readChunk($fp, $head, $tail) { 787 $chunk = ''; 788 $chunk_size = max($tail - $head, 0); // found chunk size 789 $got = 0; 790 fseek($fp, $head); 791 while($got < $chunk_size && !feof($fp)) { 792 $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0)); 793 if($tmp === false) { //error state 794 break; 795 } 796 $got += strlen($tmp); 797 $chunk .= $tmp; 798 } 799 $lines = explode("\n", $chunk); 800 array_pop($lines); // remove trailing newline 801 return $lines; 802 } 803 804 /** 805 * Set pointer to first new line after $finger and return its position 806 * 807 * @param resource $fp filepointer 808 * @param $finger int a pointer 809 * @return int pointer 810 */ 811 protected function getNewlinepointer($fp, $finger) { 812 fseek($fp, $finger); 813 $nl = $finger; 814 if($finger > 0) { 815 fgets($fp); // slip the finger forward to a new line 816 $nl = ftell($fp); 817 } 818 return $nl; 819 } 820 821 /** 822 * Check whether given revision is the current page 823 * 824 * @param int $rev timestamp of current page 825 * @return bool true if $rev is current revision, otherwise false 826 */ 827 public function isCurrentRevision($rev) { 828 return $rev == @filemtime($this->getFilename()); 829 } 830 831 /** 832 * Returns the next lines of the changelog of the chunck before head or after tail 833 * 834 * @param resource $fp filepointer 835 * @param int $head position head of last chunk 836 * @param int $tail position tail of last chunk 837 * @param int $direction positive forward, negative backward 838 * @return array with entries: 839 * - $lines: changelog lines of readed chunk 840 * - $head: head of chunk 841 * - $tail: tail of chunk 842 */ 843 protected function readAdjacentChunk($fp, $head, $tail, $direction) { 844 if(!$fp) return array(array(), $head, $tail); 845 846 if($direction > 0) { 847 //read forward 848 $head = $tail; 849 $tail = $head + floor($this->chunk_size * (2 / 3)); 850 $tail = $this->getNewlinepointer($fp, $tail); 851 } else { 852 //read backward 853 $tail = $head; 854 $head = max($tail - $this->chunk_size, 0); 855 while(true) { 856 $nl = $this->getNewlinepointer($fp, $head); 857 // was the chunk big enough? if not, take another bite 858 if($nl > 0 && $tail <= $nl) { 859 $head = max($head - $this->chunk_size, 0); 860 } else { 861 $head = $nl; 862 break; 863 } 864 } 865 } 866 867 //load next chunck 868 $lines = $this->readChunk($fp, $head, $tail); 869 return array($lines, $head, $tail); 870 } 871 872 /** 873 * Collect the $max revisions near to the timestamp $rev 874 * 875 * @param int $rev revision timestamp 876 * @param int $max maximum number of revisions to be returned 877 * @return bool|array 878 * return array with entries: 879 * - $requestedrevs: array of with $max revision timestamps 880 * - $revs: all parsed revision timestamps 881 * - $fp: filepointer only defined for chuck reading, needs closing. 882 * - $lines: non-parsed changelog lines before the parsed revisions 883 * - $head: position of first readed changelogline 884 * - $lasttail: position of end of last readed changelogline 885 * otherwise false 886 */ 887 protected function retrieveRevisionsAround($rev, $max) { 888 //get lines from changelog 889 list($fp, $lines, $starthead, $starttail, $eof) = $this->readloglines($rev); 890 if(empty($lines)) return false; 891 892 //parse chunk containing $rev, and read forward more chunks until $max/2 is reached 893 $head = $starthead; 894 $tail = $starttail; 895 $revs = array(); 896 $aftercount = $beforecount = 0; 897 while(count($lines) > 0) { 898 foreach($lines as $line) { 899 $tmp = parseChangelogLine($line); 900 if($tmp !== false) { 901 $this->cache[$this->id][$tmp['date']] = $tmp; 902 $revs[] = $tmp['date']; 903 if($tmp['date'] >= $rev) { 904 //count revs after reference $rev 905 $aftercount++; 906 if($aftercount == 1) $beforecount = count($revs); 907 } 908 //enough revs after reference $rev? 909 if($aftercount > floor($max / 2)) break 2; 910 } 911 } 912 //retrieve next chunk 913 list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, 1); 914 } 915 if($aftercount == 0) return false; 916 917 $lasttail = $tail; 918 919 //read additional chuncks backward until $max/2 is reached and total number of revs is equal to $max 920 $lines = array(); 921 $i = 0; 922 if($aftercount > 0) { 923 $head = $starthead; 924 $tail = $starttail; 925 while($head > 0) { 926 list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1); 927 928 for($i = count($lines) - 1; $i >= 0; $i--) { 929 $tmp = parseChangelogLine($lines[$i]); 930 if($tmp !== false) { 931 $this->cache[$this->id][$tmp['date']] = $tmp; 932 $revs[] = $tmp['date']; 933 $beforecount++; 934 //enough revs before reference $rev? 935 if($beforecount > max(floor($max / 2), $max - $aftercount)) break 2; 936 } 937 } 938 } 939 } 940 sort($revs); 941 942 //keep only non-parsed lines 943 $lines = array_slice($lines, 0, $i); 944 //trunk desired selection 945 $requestedrevs = array_slice($revs, -$max, $max); 946 947 return array($requestedrevs, $revs, $fp, $lines, $head, $lasttail); 948 } 949} 950 951/** 952 * Class PageChangelog handles changelog of a wiki page 953 */ 954class PageChangelog extends ChangeLog { 955 956 /** 957 * Returns path to changelog 958 * 959 * @return string path to file 960 */ 961 protected function getChangelogFilename() { 962 return metaFN($this->id, '.changes'); 963 } 964 965 /** 966 * Returns path to current page/media 967 * 968 * @return string path to file 969 */ 970 protected function getFilename() { 971 return wikiFN($this->id); 972 } 973} 974 975/** 976 * Class MediaChangelog handles changelog of a media file 977 */ 978class MediaChangelog extends ChangeLog { 979 980 /** 981 * Returns path to changelog 982 * 983 * @return string path to file 984 */ 985 protected function getChangelogFilename() { 986 return mediaMetaFN($this->id, '.changes'); 987 } 988 989 /** 990 * Returns path to current page/media 991 * 992 * @return string path to file 993 */ 994 protected function getFilename() { 995 return mediaFN($this->id); 996 } 997} 998 999/** 1000 * Get the changelog information for a specific page id 1001 * and revision (timestamp). Adjacent changelog lines 1002 * are optimistically parsed and cached to speed up 1003 * consecutive calls to getRevisionInfo. For large 1004 * changelog files, only the chunk containing the 1005 * requested changelog line is read. 1006 * 1007 * @deprecated 2013-11-20 1008 * 1009 * @author Ben Coburn <btcoburn@silicodon.net> 1010 * @author Kate Arzamastseva <pshns@ukr.net> 1011 */ 1012function getRevisionInfo($id, $rev, $chunk_size = 8192, $media = false) { 1013 dbg_deprecated('class PageChangeLog or class MediaChanglog'); 1014 if($media) { 1015 $changelog = new MediaChangeLog($id, $chunk_size); 1016 } else { 1017 $changelog = new PageChangeLog($id, $chunk_size); 1018 } 1019 return $changelog->getRevisionInfo($rev); 1020} 1021 1022/** 1023 * Return a list of page revisions numbers 1024 * Does not guarantee that the revision exists in the attic, 1025 * only that a line with the date exists in the changelog. 1026 * By default the current revision is skipped. 1027 * 1028 * id: the page of interest 1029 * first: skip the first n changelog lines 1030 * num: number of revisions to return 1031 * 1032 * The current revision is automatically skipped when the page exists. 1033 * See $INFO['meta']['last_change'] for the current revision. 1034 * 1035 * For efficiency, the log lines are parsed and cached for later 1036 * calls to getRevisionInfo. Large changelog files are read 1037 * backwards in chunks until the requested number of changelog 1038 * lines are recieved. 1039 * 1040 * @deprecated 2013-11-20 1041 * 1042 * @author Ben Coburn <btcoburn@silicodon.net> 1043 * @author Kate Arzamastseva <pshns@ukr.net> 1044 */ 1045function getRevisions($id, $first, $num, $chunk_size = 8192, $media = false) { 1046 dbg_deprecated('class PageChangeLog or class MediaChanglog'); 1047 if($media) { 1048 $changelog = new MediaChangeLog($id, $chunk_size); 1049 } else { 1050 $changelog = new PageChangeLog($id, $chunk_size); 1051 } 1052 return $changelog->getRevisions($first, $num); 1053} 1054