3 * Changelog handling functions
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author Andreas Gohr <andi@splitbrain.org>
9 // Constants for known core changelog line types.
10 // Use these in place of string literals for more readable code.
11 define('DOKU_CHANGE_TYPE_CREATE', 'C');
12 define('DOKU_CHANGE_TYPE_EDIT', 'E');
13 define('DOKU_CHANGE_TYPE_MINOR_EDIT', 'e');
14 define('DOKU_CHANGE_TYPE_DELETE', 'D');
15 define('DOKU_CHANGE_TYPE_REVERT', 'R');
18 * parses a changelog line into it's components
20 * @author Ben Coburn <btcoburn@silicodon.net>
22 function parseChangelogLine($line) {
23 $tmp = explode("\t", $line);
24 if ($tmp!==false && count($tmp)>1) {
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)
34 } else { return false; }
38 * Add's an entry to the changelog and saves the metadata for the page
40 * @author Andreas Gohr <andi@splitbrain.org>
41 * @author Esther Brunner <wikidesign@gmail.com>
42 * @author Ben Coburn <btcoburn@silicodon.net>
44 function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT
, $summary='', $extra='', $flags=null){
47 // check for special flags as keys
48 if (!is_array($flags)) { $flags = array(); }
49 $flagExternalEdit = isset($flags['ExternalEdit']);
53 $created = @filectime
($file);
54 $minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT
);
55 $wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE
);
57 if(!$date) $date = time(); //use current time if none supplied
58 $remote = (!$flagExternalEdit)?
clientIP(true):'127.0.0.1';
59 $user = (!$flagExternalEdit)?
$_SERVER['REMOTE_USER']:'';
61 $strip = array("\t", "\n");
65 'type' => str_replace($strip, '', $type),
68 'sum' => str_replace($strip, '', $summary),
69 'extra' => str_replace($strip, '', $extra)
74 $oldmeta = p_read_metadata($id);
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'];
88 $meta['last_change'] = $logline;
89 p_set_metadata($id, $meta, true);
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
99 * Add's an entry to the media changelog
101 * @author Michael Hamann <michael@content-space.de>
102 * @author Andreas Gohr <andi@splitbrain.org>
103 * @author Esther Brunner <wikidesign@gmail.com>
104 * @author Ben Coburn <btcoburn@silicodon.net>
106 function addMediaLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT
, $summary='', $extra='', $flags=null){
111 if(!$date) $date = time(); //use current time if none supplied
112 $remote = clientIP(true);
113 $user = $_SERVER['REMOTE_USER'];
115 $strip = array("\t", "\n");
119 'type' => str_replace($strip, '', $type),
122 'sum' => str_replace($strip, '', $summary),
123 'extra' => str_replace($strip, '', $extra)
126 // add changelog lines
127 $logline = implode("\t", $logline)."\n";
128 io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache
132 * returns an array of recently changed files using the
135 * The following constants can be used to control which changes are
136 * included. Add them together as needed.
138 * RECENTS_SKIP_DELETED - don't include deleted pages
139 * RECENTS_SKIP_MINORS - don't include minor changes
140 * RECENTS_SKIP_SUBSPACES - don't include subspaces
141 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes
143 * @param int $first number of first entry returned (for paginating
144 * @param int $num return $num entries
145 * @param string $ns restrict to given namespace
146 * @param bool $flags see above
148 * @author Ben Coburn <btcoburn@silicodon.net>
150 function getRecents($first,$num,$ns='',$flags=0){
158 // read all recent changes. (kept short)
159 if ($flags & RECENTS_MEDIA_CHANGES
) {
160 $lines = @file
($conf['media_changelog']);
162 $lines = @file
($conf['changelog']);
167 $seen = array(); // caches seen lines, _handleRecent() skips them
168 for($i = count($lines)-1; $i >= 0; $i--){
169 $rec = _handleRecent($lines[$i], $ns, $flags, $seen);
171 if(--$first >= 0) continue; // skip first entries
174 // break when we have enough entries
175 if($count >= $num){ break; }
183 * returns an array of files changed since a given time using the
186 * The following constants can be used to control which changes are
187 * included. Add them together as needed.
189 * RECENTS_SKIP_DELETED - don't include deleted pages
190 * RECENTS_SKIP_MINORS - don't include minor changes
191 * RECENTS_SKIP_SUBSPACES - don't include subspaces
192 * RECENTS_MEDIA_CHANGES - return media changes instead of page changes
194 * @param int $from date of the oldest entry to return
195 * @param int $to date of the newest entry to return (for pagination, optional)
196 * @param string $ns restrict to given namespace (optional)
197 * @param bool $flags see above (optional)
199 * @author Michael Hamann <michael@content-space.de>
200 * @author Ben Coburn <btcoburn@silicodon.net>
202 function getRecentsSince($from,$to=null,$ns='',$flags=0){
206 if($to && $to < $from)
209 // read all recent changes. (kept short)
210 if ($flags & RECENTS_MEDIA_CHANGES
) {
211 $lines = @file
($conf['media_changelog']);
213 $lines = @file
($conf['changelog']);
216 // we start searching at the end of the list
217 $lines = array_reverse($lines);
220 $seen = array(); // caches seen lines, _handleRecent() skips them
222 foreach($lines as $line){
223 $rec = _handleRecent($line, $ns, $flags, $seen);
225 if ($rec['date'] >= $from) {
226 if (!$to ||
$rec['date'] <= $to) {
235 return array_reverse($recent);
239 * Internal function used by getRecents
241 * don't call directly
244 * @author Andreas Gohr <andi@splitbrain.org>
245 * @author Ben Coburn <btcoburn@silicodon.net>
247 function _handleRecent($line,$ns,$flags,&$seen){
248 if(empty($line)) return false; //skip empty lines
250 // split the line into parts
251 $recent = parseChangelogLine($line);
252 if ($recent===false) { return false; }
255 if(isset($seen[$recent['id']])) return false;
258 if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT
&& ($flags & RECENTS_SKIP_MINORS
)) return false;
260 // remember in seen to skip additional sights
261 $seen[$recent['id']] = 1;
263 // check if it's a hidden page
264 if(isHiddenPage($recent['id'])) return false;
267 if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
269 // exclude subnamespaces
270 if (($flags & RECENTS_SKIP_SUBSPACES
) && (getNS($recent['id']) != $ns)) return false;
273 $recent['perms'] = auth_quickaclcheck($recent['id']);
274 if ($recent['perms'] < AUTH_READ
) return false;
277 $fn = (($flags & RECENTS_MEDIA_CHANGES
) ?
mediaFN($recent['id']) : wikiFN($recent['id']));
278 if((!@file_exists
($fn)) && ($flags & RECENTS_SKIP_DELETED
)) return false;
284 * Get the changelog information for a specific page id
285 * and revision (timestamp). Adjacent changelog lines
286 * are optimistically parsed and cached to speed up
287 * consecutive calls to getRevisionInfo. For large
288 * changelog files, only the chunk containing the
289 * requested changelog line is read.
291 * @author Ben Coburn <btcoburn@silicodon.net>
293 function getRevisionInfo($id, $rev, $chunk_size=8192) {
294 global $cache_revinfo;
295 $cache =& $cache_revinfo;
296 if (!isset($cache[$id])) { $cache[$id] = array(); }
299 // check if it's already in the memory cache
300 if (isset($cache[$id]) && isset($cache[$id][$rev])) {
301 return $cache[$id][$rev];
304 $file = metaFN($id, '.changes');
305 if (!@file_exists
($file)) { return false; }
306 if (filesize($file)<$chunk_size ||
$chunk_size==0) {
308 $lines = file($file);
309 if ($lines===false) { return false; }
312 $fp = fopen($file, 'rb'); // "file pointer"
313 if ($fp===false) { return false; }
315 fseek($fp, 0, SEEK_END
);
321 while ($tail-$head>$chunk_size) {
322 $finger = $head+
floor(($tail-$head)/2.0);
324 fgets($fp); // slip the finger forward to a new line
325 $finger = ftell($fp);
326 $tmp = fgets($fp); // then read at that location
327 $tmp = parseChangelogLine($tmp);
328 $finger_rev = $tmp['date'];
329 if ($finger==$head ||
$finger==$tail) { break; }
330 if ($finger_rev>$rev) {
338 // cound not find chunk, assume requested rev is missing
345 $chunk_size = max($tail-$head, 0); // found chunk size
348 while ($got<$chunk_size && !feof($fp)) {
349 $tmp = @fread
($fp, max($chunk_size-$got, 0));
350 if ($tmp===false) { break; } //error state
351 $got +
= strlen($tmp);
354 $lines = explode("\n", $chunk);
355 array_pop($lines); // remove trailing newline
359 // parse and cache changelog lines
360 foreach ($lines as $value) {
361 $tmp = parseChangelogLine($value);
363 $cache[$id][$tmp['date']] = $tmp;
366 if (!isset($cache[$id][$rev])) { return false; }
367 return $cache[$id][$rev];
371 * Return a list of page revisions numbers
372 * Does not guarantee that the revision exists in the attic,
373 * only that a line with the date exists in the changelog.
374 * By default the current revision is skipped.
376 * id: the page of interest
377 * first: skip the first n changelog lines
378 * num: number of revisions to return
380 * The current revision is automatically skipped when the page exists.
381 * See $INFO['meta']['last_change'] for the current revision.
383 * For efficiency, the log lines are parsed and cached for later
384 * calls to getRevisionInfo. Large changelog files are read
385 * backwards in chunks until the requested number of changelog
386 * lines are recieved.
388 * @author Ben Coburn <btcoburn@silicodon.net>
390 function getRevisions($id, $first, $num, $chunk_size=8192) {
391 global $cache_revinfo;
392 $cache =& $cache_revinfo;
393 if (!isset($cache[$id])) { $cache[$id] = array(); }
398 $file = metaFN($id, '.changes');
400 $chunk_size = max($chunk_size, 0);
401 if ($first<0) { $first = 0; }
402 else if (@file_exists
(wikiFN($id))) {
403 // skip current revision if the page exists
404 $first = max($first+
1, 0);
407 if (!@file_exists
($file)) { return $revs; }
408 if (filesize($file)<$chunk_size ||
$chunk_size==0) {
410 $lines = file($file);
411 if ($lines===false) { return $revs; }
413 // read chunks backwards
414 $fp = fopen($file, 'rb'); // "file pointer"
415 if ($fp===false) { return $revs; }
416 fseek($fp, 0, SEEK_END
);
420 $finger = max($tail-$chunk_size, 0);
421 while ($count<$num+
$first) {
424 fgets($fp); // slip the finger forward to a new line
425 $finger = ftell($fp);
429 if ($tail<=$finger) { break; }
431 $read_size = max($tail-$finger, 0); // found chunk size
433 while ($got<$read_size && !feof($fp)) {
434 $tmp = @fread
($fp, max($read_size-$got, 0));
435 if ($tmp===false) { break; } //error state
436 $got +
= strlen($tmp);
439 $tmp = explode("\n", $chunk);
440 array_pop($tmp); // remove trailing newline
442 // combine with previous chunk
443 $count +
= count($tmp);
444 $lines = array_merge($tmp, $lines);
447 if ($finger==0) { break; } // already read all the lines
450 $finger = max($tail-$chunk_size, 0);
456 // skip parsing extra lines
457 $num = max(min(count($lines)-$first, $num), 0);
458 if ($first>0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$first-$num, 0), $num); }
459 else if ($first>0 && $num==0) { $lines = array_slice($lines, 0, max(count($lines)-$first, 0)); }
460 else if ($first==0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$num, 0)); }
462 // handle lines in reverse order
463 for ($i = count($lines)-1; $i >= 0; $i--) {
464 $tmp = parseChangelogLine($lines[$i]);
466 $cache[$id][$tmp['date']] = $tmp;
467 $revs[] = $tmp['date'];