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