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 56 // check for special flags as keys 57 if (!is_array($flags)) { $flags = array(); } 58 $flagExternalEdit = isset($flags['ExternalEdit']); 59 60 $id = cleanid($id); 61 $file = wikiFN($id); 62 $created = @filectime($file); 63 $minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT); 64 $wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE); 65 66 if(!$date) $date = time(); //use current time if none supplied 67 $remote = (!$flagExternalEdit)?clientIP(true):'127.0.0.1'; 68 $user = (!$flagExternalEdit)?$_SERVER['REMOTE_USER']:''; 69 70 $strip = array("\t", "\n"); 71 $logline = array( 72 'date' => $date, 73 'ip' => $remote, 74 'type' => str_replace($strip, '', $type), 75 'id' => $id, 76 'user' => $user, 77 'sum' => str_replace($strip, '', $summary), 78 'extra' => str_replace($strip, '', $extra) 79 ); 80 81 // update metadata 82 if (!$wasRemoved) { 83 $oldmeta = p_read_metadata($id); 84 $meta = array(); 85 if (!$INFO['exists'] && empty($oldmeta['persistent']['date']['created'])){ // newly created 86 $meta['date']['created'] = $created; 87 if ($user) $meta['creator'] = $INFO['userinfo']['name']; 88 } elseif (!$INFO['exists'] && !empty($oldmeta['persistent']['date']['created'])) { // re-created / restored 89 $meta['date']['created'] = $oldmeta['persistent']['date']['created']; 90 $meta['date']['modified'] = $created; // use the files ctime here 91 $meta['creator'] = $oldmeta['persistent']['creator']; 92 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 93 } elseif (!$minor) { // non-minor modification 94 $meta['date']['modified'] = $date; 95 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 96 } 97 $meta['last_change'] = $logline; 98 p_set_metadata($id, $meta); 99 } 100 101 // add changelog lines 102 $logline = implode("\t", $logline)."\n"; 103 io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog 104 io_saveFile($conf['changelog'],$logline,true); //global changelog cache 105} 106 107/** 108 * Add's an entry to the media changelog 109 * 110 * @author Michael Hamann <michael@content-space.de> 111 * @author Andreas Gohr <andi@splitbrain.org> 112 * @author Esther Brunner <wikidesign@gmail.com> 113 * @author Ben Coburn <btcoburn@silicodon.net> 114 */ 115function addMediaLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null){ 116 global $conf; 117 118 $id = cleanid($id); 119 120 if(!$date) $date = time(); //use current time if none supplied 121 $remote = clientIP(true); 122 $user = $_SERVER['REMOTE_USER']; 123 124 $strip = array("\t", "\n"); 125 $logline = array( 126 'date' => $date, 127 'ip' => $remote, 128 'type' => str_replace($strip, '', $type), 129 'id' => $id, 130 'user' => $user, 131 'sum' => str_replace($strip, '', $summary), 132 'extra' => str_replace($strip, '', $extra) 133 ); 134 135 // add changelog lines 136 $logline = implode("\t", $logline)."\n"; 137 io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache 138} 139 140/** 141 * returns an array of recently changed files using the 142 * changelog 143 * 144 * The following constants can be used to control which changes are 145 * included. Add them together as needed. 146 * 147 * RECENTS_SKIP_DELETED - don't include deleted pages 148 * RECENTS_SKIP_MINORS - don't include minor changes 149 * RECENTS_SKIP_SUBSPACES - don't include subspaces 150 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 151 * 152 * @param int $first number of first entry returned (for paginating 153 * @param int $num return $num entries 154 * @param string $ns restrict to given namespace 155 * @param bool $flags see above 156 * 157 * @author Ben Coburn <btcoburn@silicodon.net> 158 */ 159function getRecents($first,$num,$ns='',$flags=0){ 160 global $conf; 161 $recent = array(); 162 $count = 0; 163 164 if(!$num) 165 return $recent; 166 167 // read all recent changes. (kept short) 168 if ($flags & RECENTS_MEDIA_CHANGES) { 169 $lines = @file($conf['media_changelog']); 170 } else { 171 $lines = @file($conf['changelog']); 172 } 173 174 // handle lines 175 $seen = array(); // caches seen lines, _handleRecent() skips them 176 for($i = count($lines)-1; $i >= 0; $i--){ 177 $rec = _handleRecent($lines[$i], $ns, $flags, $seen); 178 if($rec !== false) { 179 if(--$first >= 0) continue; // skip first entries 180 $recent[] = $rec; 181 $count++; 182 // break when we have enough entries 183 if($count >= $num){ break; } 184 } 185 } 186 187 return $recent; 188} 189 190/** 191 * returns an array of files changed since a given time using the 192 * changelog 193 * 194 * The following constants can be used to control which changes are 195 * included. Add them together as needed. 196 * 197 * RECENTS_SKIP_DELETED - don't include deleted pages 198 * RECENTS_SKIP_MINORS - don't include minor changes 199 * RECENTS_SKIP_SUBSPACES - don't include subspaces 200 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes 201 * 202 * @param int $from date of the oldest entry to return 203 * @param int $to date of the newest entry to return (for pagination, optional) 204 * @param string $ns restrict to given namespace (optional) 205 * @param bool $flags see above (optional) 206 * 207 * @author Michael Hamann <michael@content-space.de> 208 * @author Ben Coburn <btcoburn@silicodon.net> 209 */ 210function getRecentsSince($from,$to=null,$ns='',$flags=0){ 211 global $conf; 212 $recent = array(); 213 214 if($to && $to < $from) 215 return $recent; 216 217 // read all recent changes. (kept short) 218 if ($flags & RECENTS_MEDIA_CHANGES) { 219 $lines = @file($conf['media_changelog']); 220 } else { 221 $lines = @file($conf['changelog']); 222 } 223 224 // we start searching at the end of the list 225 $lines = array_reverse($lines); 226 227 // handle lines 228 $seen = array(); // caches seen lines, _handleRecent() skips them 229 230 foreach($lines as $line){ 231 $rec = _handleRecent($line, $ns, $flags, $seen); 232 if($rec !== false) { 233 if ($rec['date'] >= $from) { 234 if (!$to || $rec['date'] <= $to) { 235 $recent[] = $rec; 236 } 237 } else { 238 break; 239 } 240 } 241 } 242 243 return array_reverse($recent); 244} 245 246/** 247 * Internal function used by getRecents 248 * 249 * don't call directly 250 * 251 * @see getRecents() 252 * @author Andreas Gohr <andi@splitbrain.org> 253 * @author Ben Coburn <btcoburn@silicodon.net> 254 */ 255function _handleRecent($line,$ns,$flags,&$seen){ 256 if(empty($line)) return false; //skip empty lines 257 258 // split the line into parts 259 $recent = parseChangelogLine($line); 260 if ($recent===false) { return false; } 261 262 // skip seen ones 263 if(isset($seen[$recent['id']])) return false; 264 265 // skip minors 266 if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false; 267 268 // remember in seen to skip additional sights 269 $seen[$recent['id']] = 1; 270 271 // check if it's a hidden page 272 if(isHiddenPage($recent['id'])) return false; 273 274 // filter namespace 275 if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false; 276 277 // exclude subnamespaces 278 if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false; 279 280 // check ACL 281 $recent['perms'] = auth_quickaclcheck($recent['id']); 282 if ($recent['perms'] < AUTH_READ) return false; 283 284 // check existance 285 $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id'])); 286 if((!@file_exists($fn)) && ($flags & RECENTS_SKIP_DELETED)) return false; 287 288 return $recent; 289} 290 291/** 292 * Get the changelog information for a specific page id 293 * and revision (timestamp). Adjacent changelog lines 294 * are optimistically parsed and cached to speed up 295 * consecutive calls to getRevisionInfo. For large 296 * changelog files, only the chunk containing the 297 * requested changelog line is read. 298 * 299 * @author Ben Coburn <btcoburn@silicodon.net> 300 */ 301function getRevisionInfo($id, $rev, $chunk_size=8192) { 302 global $cache_revinfo; 303 $cache =& $cache_revinfo; 304 if (!isset($cache[$id])) { $cache[$id] = array(); } 305 $rev = max($rev, 0); 306 307 // check if it's already in the memory cache 308 if (isset($cache[$id]) && isset($cache[$id][$rev])) { 309 return $cache[$id][$rev]; 310 } 311 312 $file = metaFN($id, '.changes'); 313 if (!@file_exists($file)) { return false; } 314 if (filesize($file)<$chunk_size || $chunk_size==0) { 315 // read whole file 316 $lines = file($file); 317 if ($lines===false) { return false; } 318 } else { 319 // read by chunk 320 $fp = fopen($file, 'rb'); // "file pointer" 321 if ($fp===false) { return false; } 322 $head = 0; 323 fseek($fp, 0, SEEK_END); 324 $tail = ftell($fp); 325 $finger = 0; 326 $finger_rev = 0; 327 328 // find chunk 329 while ($tail-$head>$chunk_size) { 330 $finger = $head+floor(($tail-$head)/2.0); 331 fseek($fp, $finger); 332 fgets($fp); // slip the finger forward to a new line 333 $finger = ftell($fp); 334 $tmp = fgets($fp); // then read at that location 335 $tmp = parseChangelogLine($tmp); 336 $finger_rev = $tmp['date']; 337 if ($finger==$head || $finger==$tail) { break; } 338 if ($finger_rev>$rev) { 339 $tail = $finger; 340 } else { 341 $head = $finger; 342 } 343 } 344 345 if ($tail-$head<1) { 346 // cound not find chunk, assume requested rev is missing 347 fclose($fp); 348 return false; 349 } 350 351 // read chunk 352 $chunk = ''; 353 $chunk_size = max($tail-$head, 0); // found chunk size 354 $got = 0; 355 fseek($fp, $head); 356 while ($got<$chunk_size && !feof($fp)) { 357 $tmp = @fread($fp, max($chunk_size-$got, 0)); 358 if ($tmp===false) { break; } //error state 359 $got += strlen($tmp); 360 $chunk .= $tmp; 361 } 362 $lines = explode("\n", $chunk); 363 array_pop($lines); // remove trailing newline 364 fclose($fp); 365 } 366 367 // parse and cache changelog lines 368 foreach ($lines as $value) { 369 $tmp = parseChangelogLine($value); 370 if ($tmp!==false) { 371 $cache[$id][$tmp['date']] = $tmp; 372 } 373 } 374 if (!isset($cache[$id][$rev])) { return false; } 375 return $cache[$id][$rev]; 376} 377 378/** 379 * Return a list of page revisions numbers 380 * Does not guarantee that the revision exists in the attic, 381 * only that a line with the date exists in the changelog. 382 * By default the current revision is skipped. 383 * 384 * id: the page of interest 385 * first: skip the first n changelog lines 386 * num: number of revisions to return 387 * 388 * The current revision is automatically skipped when the page exists. 389 * See $INFO['meta']['last_change'] for the current revision. 390 * 391 * For efficiency, the log lines are parsed and cached for later 392 * calls to getRevisionInfo. Large changelog files are read 393 * backwards in chunks until the requested number of changelog 394 * lines are recieved. 395 * 396 * @author Ben Coburn <btcoburn@silicodon.net> 397 */ 398function getRevisions($id, $first, $num, $chunk_size=8192) { 399 global $cache_revinfo; 400 $cache =& $cache_revinfo; 401 if (!isset($cache[$id])) { $cache[$id] = array(); } 402 403 $revs = array(); 404 $lines = array(); 405 $count = 0; 406 $file = metaFN($id, '.changes'); 407 $num = max($num, 0); 408 $chunk_size = max($chunk_size, 0); 409 if ($first<0) { $first = 0; } 410 else if (@file_exists(wikiFN($id))) { 411 // skip current revision if the page exists 412 $first = max($first+1, 0); 413 } 414 415 if (!@file_exists($file)) { return $revs; } 416 if (filesize($file)<$chunk_size || $chunk_size==0) { 417 // read whole file 418 $lines = file($file); 419 if ($lines===false) { return $revs; } 420 } else { 421 // read chunks backwards 422 $fp = fopen($file, 'rb'); // "file pointer" 423 if ($fp===false) { return $revs; } 424 fseek($fp, 0, SEEK_END); 425 $tail = ftell($fp); 426 427 // chunk backwards 428 $finger = max($tail-$chunk_size, 0); 429 while ($count<$num+$first) { 430 fseek($fp, $finger); 431 if ($finger>0) { 432 fgets($fp); // slip the finger forward to a new line 433 $finger = ftell($fp); 434 } 435 436 // read chunk 437 if ($tail<=$finger) { break; } 438 $chunk = ''; 439 $read_size = max($tail-$finger, 0); // found chunk size 440 $got = 0; 441 while ($got<$read_size && !feof($fp)) { 442 $tmp = @fread($fp, max($read_size-$got, 0)); 443 if ($tmp===false) { break; } //error state 444 $got += strlen($tmp); 445 $chunk .= $tmp; 446 } 447 $tmp = explode("\n", $chunk); 448 array_pop($tmp); // remove trailing newline 449 450 // combine with previous chunk 451 $count += count($tmp); 452 $lines = array_merge($tmp, $lines); 453 454 // next chunk 455 if ($finger==0) { break; } // already read all the lines 456 else { 457 $tail = $finger; 458 $finger = max($tail-$chunk_size, 0); 459 } 460 } 461 fclose($fp); 462 } 463 464 // skip parsing extra lines 465 $num = max(min(count($lines)-$first, $num), 0); 466 if ($first>0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$first-$num, 0), $num); } 467 else if ($first>0 && $num==0) { $lines = array_slice($lines, 0, max(count($lines)-$first, 0)); } 468 else if ($first==0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$num, 0)); } 469 470 // handle lines in reverse order 471 for ($i = count($lines)-1; $i >= 0; $i--) { 472 $tmp = parseChangelogLine($lines[$i]); 473 if ($tmp!==false) { 474 $cache[$id][$tmp['date']] = $tmp; 475 $revs[] = $tmp['date']; 476 } 477 } 478 479 return $revs; 480} 481 482 483