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 * Get the changelog information for a specific page id 342 * and revision (timestamp). Adjacent changelog lines 343 * are optimistically parsed and cached to speed up 344 * consecutive calls to getRevisionInfo. For large 345 * changelog files, only the chunk containing the 346 * requested changelog line is read. 347 * 348 * @author Ben Coburn <btcoburn@silicodon.net> 349 * @author Kate Arzamastseva <pshns@ukr.net> 350 */ 351function getRevisionInfo($id, $rev, $chunk_size=8192, $media=false) { 352 global $cache_revinfo; 353 $cache =& $cache_revinfo; 354 if (!isset($cache[$id])) { $cache[$id] = array(); } 355 $rev = max($rev, 0); 356 357 // check if it's already in the memory cache 358 if (isset($cache[$id]) && isset($cache[$id][$rev])) { 359 return $cache[$id][$rev]; 360 } 361 362 if ($media) { 363 $file = mediaMetaFN($id, '.changes'); 364 } else { 365 $file = metaFN($id, '.changes'); 366 } 367 if (!@file_exists($file)) { return false; } 368 if (filesize($file)<$chunk_size || $chunk_size==0) { 369 // read whole file 370 $lines = file($file); 371 if ($lines===false) { return false; } 372 } else { 373 // read by chunk 374 $fp = fopen($file, 'rb'); // "file pointer" 375 if ($fp===false) { return false; } 376 $head = 0; 377 fseek($fp, 0, SEEK_END); 378 $tail = ftell($fp); 379 $finger = 0; 380 $finger_rev = 0; 381 382 // find chunk 383 while ($tail-$head>$chunk_size) { 384 $finger = $head+floor(($tail-$head)/2.0); 385 fseek($fp, $finger); 386 fgets($fp); // slip the finger forward to a new line 387 $finger = ftell($fp); 388 $tmp = fgets($fp); // then read at that location 389 $tmp = parseChangelogLine($tmp); 390 $finger_rev = $tmp['date']; 391 if ($finger==$head || $finger==$tail) { break; } 392 if ($finger_rev>$rev) { 393 $tail = $finger; 394 } else { 395 $head = $finger; 396 } 397 } 398 399 if ($tail-$head<1) { 400 // cound not find chunk, assume requested rev is missing 401 fclose($fp); 402 return false; 403 } 404 405 // read chunk 406 $chunk = ''; 407 $chunk_size = max($tail-$head, 0); // found chunk size 408 $got = 0; 409 fseek($fp, $head); 410 while ($got<$chunk_size && !feof($fp)) { 411 $tmp = @fread($fp, max($chunk_size-$got, 0)); 412 if ($tmp===false) { break; } //error state 413 $got += strlen($tmp); 414 $chunk .= $tmp; 415 } 416 $lines = explode("\n", $chunk); 417 array_pop($lines); // remove trailing newline 418 fclose($fp); 419 } 420 421 // parse and cache changelog lines 422 foreach ($lines as $value) { 423 $tmp = parseChangelogLine($value); 424 if ($tmp!==false) { 425 $cache[$id][$tmp['date']] = $tmp; 426 } 427 } 428 if (!isset($cache[$id][$rev])) { return false; } 429 return $cache[$id][$rev]; 430} 431 432/** 433 * Return a list of page revisions numbers 434 * Does not guarantee that the revision exists in the attic, 435 * only that a line with the date exists in the changelog. 436 * By default the current revision is skipped. 437 * 438 * id: the page of interest 439 * first: skip the first n changelog lines 440 * num: number of revisions to return 441 * 442 * The current revision is automatically skipped when the page exists. 443 * See $INFO['meta']['last_change'] for the current revision. 444 * 445 * For efficiency, the log lines are parsed and cached for later 446 * calls to getRevisionInfo. Large changelog files are read 447 * backwards in chunks until the requested number of changelog 448 * lines are recieved. 449 * 450 * @author Ben Coburn <btcoburn@silicodon.net> 451 * @author Kate Arzamastseva <pshns@ukr.net> 452 */ 453function getRevisions($id, $first, $num, $chunk_size=8192, $media=false) { 454 global $cache_revinfo; 455 $cache =& $cache_revinfo; 456 if (!isset($cache[$id])) { $cache[$id] = array(); } 457 458 $revs = array(); 459 $lines = array(); 460 $count = 0; 461 if ($media) { 462 $file = mediaMetaFN($id, '.changes'); 463 } else { 464 $file = metaFN($id, '.changes'); 465 } 466 $num = max($num, 0); 467 if ($num == 0) { return $revs; } 468 469 $chunk_size = max($chunk_size, 0); 470 if ($first<0) { 471 $first = 0; 472 } else if (!$media && @file_exists(wikiFN($id)) || $media && @file_exists(mediaFN($id))) { 473 // skip current revision if the page exists 474 $first = max($first+1, 0); 475 } 476 477 if (!@file_exists($file)) { return $revs; } 478 if (filesize($file)<$chunk_size || $chunk_size==0) { 479 // read whole file 480 $lines = file($file); 481 if ($lines===false) { return $revs; } 482 } else { 483 // read chunks backwards 484 $fp = fopen($file, 'rb'); // "file pointer" 485 if ($fp===false) { return $revs; } 486 fseek($fp, 0, SEEK_END); 487 $tail = ftell($fp); 488 489 // chunk backwards 490 $finger = max($tail-$chunk_size, 0); 491 while ($count<$num+$first) { 492 fseek($fp, $finger); 493 $nl = $finger; 494 if ($finger>0) { 495 fgets($fp); // slip the finger forward to a new line 496 $nl = ftell($fp); 497 } 498 499 // was the chunk big enough? if not, take another bite 500 if($nl > 0 && $tail <= $nl){ 501 $finger = max($finger-$chunk_size, 0); 502 continue; 503 }else{ 504 $finger = $nl; 505 } 506 507 // read chunk 508 $chunk = ''; 509 $read_size = max($tail-$finger, 0); // found chunk size 510 $got = 0; 511 while ($got<$read_size && !feof($fp)) { 512 $tmp = @fread($fp, max($read_size-$got, 0)); 513 if ($tmp===false) { break; } //error state 514 $got += strlen($tmp); 515 $chunk .= $tmp; 516 } 517 $tmp = explode("\n", $chunk); 518 array_pop($tmp); // remove trailing newline 519 520 // combine with previous chunk 521 $count += count($tmp); 522 $lines = array_merge($tmp, $lines); 523 524 // next chunk 525 if ($finger==0) { break; } // already read all the lines 526 else { 527 $tail = $finger; 528 $finger = max($tail-$chunk_size, 0); 529 } 530 } 531 fclose($fp); 532 } 533 534 // skip parsing extra lines 535 $num = max(min(count($lines)-$first, $num), 0); 536 if ($first>0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$first-$num, 0), $num); } 537 else if ($first>0 && $num==0) { $lines = array_slice($lines, 0, max(count($lines)-$first, 0)); } 538 else if ($first==0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$num, 0)); } 539 540 // handle lines in reverse order 541 for ($i = count($lines)-1; $i >= 0; $i--) { 542 $tmp = parseChangelogLine($lines[$i]); 543 if ($tmp!==false) { 544 $cache[$id][$tmp['date']] = $tmp; 545 $revs[] = $tmp['date']; 546 } 547 } 548 549 return $revs; 550} 551 552 553