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