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