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