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