diff view puts revision back into $REV
[dokuwiki/radio.git] / inc / changelog.php
blobdebbc2834f8548cf48fc48c84e3dcb34caddc793
1 <?php
2 /**
3 * Changelog handling functions
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author Andreas Gohr <andi@splitbrain.org>
7 */
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');
17 /**
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) {
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; }
37 /**
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){
45 global $conf, $INFO;
47 // check for special flags as keys
48 if (!is_array($flags)) { $flags = array(); }
49 $flagExternalEdit = isset($flags['ExternalEdit']);
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);
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");
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)
72 // update metadata
73 if (!$wasRemoved) {
74 $oldmeta = p_read_metadata($id);
75 $meta = array();
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
98 /**
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){
107 global $conf, $INFO;
109 $id = cleanid($id);
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");
116 $logline = array(
117 'date' => $date,
118 'ip' => $remote,
119 'type' => str_replace($strip, '', $type),
120 'id' => $id,
121 'user' => $user,
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
133 * changelog
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){
151 global $conf;
152 $recent = array();
153 $count = 0;
155 if(!$num)
156 return $recent;
158 // read all recent changes. (kept short)
159 if ($flags & RECENTS_MEDIA_CHANGES) {
160 $lines = @file($conf['media_changelog']);
161 } else {
162 $lines = @file($conf['changelog']);
166 // handle lines
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);
170 if($rec !== false) {
171 if(--$first >= 0) continue; // skip first entries
172 $recent[] = $rec;
173 $count++;
174 // break when we have enough entries
175 if($count >= $num){ break; }
179 return $recent;
183 * returns an array of files changed since a given time using the
184 * changelog
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){
203 global $conf;
204 $recent = array();
206 if($to && $to < $from)
207 return $recent;
209 // read all recent changes. (kept short)
210 if ($flags & RECENTS_MEDIA_CHANGES) {
211 $lines = @file($conf['media_changelog']);
212 } else {
213 $lines = @file($conf['changelog']);
216 // we start searching at the end of the list
217 $lines = array_reverse($lines);
219 // handle lines
220 $seen = array(); // caches seen lines, _handleRecent() skips them
222 foreach($lines as $line){
223 $rec = _handleRecent($line, $ns, $flags, $seen);
224 if($rec !== false) {
225 if ($rec['date'] >= $from) {
226 if (!$to || $rec['date'] <= $to) {
227 $recent[] = $rec;
229 } else {
230 break;
235 return array_reverse($recent);
239 * Internal function used by getRecents
241 * don't call directly
243 * @see getRecents()
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; }
254 // skip seen ones
255 if(isset($seen[$recent['id']])) return false;
257 // skip minors
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;
266 // filter namespace
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;
272 // check ACL
273 $recent['perms'] = auth_quickaclcheck($recent['id']);
274 if ($recent['perms'] < AUTH_READ) return false;
276 // check existance
277 $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
278 if((!@file_exists($fn)) && ($flags & RECENTS_SKIP_DELETED)) return false;
280 return $recent;
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(); }
297 $rev = max($rev, 0);
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) {
307 // read whole file
308 $lines = file($file);
309 if ($lines===false) { return false; }
310 } else {
311 // read by chunk
312 $fp = fopen($file, 'rb'); // "file pointer"
313 if ($fp===false) { return false; }
314 $head = 0;
315 fseek($fp, 0, SEEK_END);
316 $tail = ftell($fp);
317 $finger = 0;
318 $finger_rev = 0;
320 // find chunk
321 while ($tail-$head>$chunk_size) {
322 $finger = $head+floor(($tail-$head)/2.0);
323 fseek($fp, $finger);
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) {
331 $tail = $finger;
332 } else {
333 $head = $finger;
337 if ($tail-$head<1) {
338 // cound not find chunk, assume requested rev is missing
339 fclose($fp);
340 return false;
343 // read chunk
344 $chunk = '';
345 $chunk_size = max($tail-$head, 0); // found chunk size
346 $got = 0;
347 fseek($fp, $head);
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);
352 $chunk .= $tmp;
354 $lines = explode("\n", $chunk);
355 array_pop($lines); // remove trailing newline
356 fclose($fp);
359 // parse and cache changelog lines
360 foreach ($lines as $value) {
361 $tmp = parseChangelogLine($value);
362 if ($tmp!==false) {
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 untill 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(); }
395 $revs = array();
396 $lines = array();
397 $count = 0;
398 $file = metaFN($id, '.changes');
399 $num = max($num, 0);
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) {
409 // read whole file
410 $lines = file($file);
411 if ($lines===false) { return $revs; }
412 } else {
413 // read chunks backwards
414 $fp = fopen($file, 'rb'); // "file pointer"
415 if ($fp===false) { return $revs; }
416 fseek($fp, 0, SEEK_END);
417 $tail = ftell($fp);
419 // chunk backwards
420 $finger = max($tail-$chunk_size, 0);
421 while ($count<$num+$first) {
422 fseek($fp, $finger);
423 if ($finger>0) {
424 fgets($fp); // slip the finger forward to a new line
425 $finger = ftell($fp);
428 // read chunk
429 if ($tail<=$finger) { break; }
430 $chunk = '';
431 $read_size = max($tail-$finger, 0); // found chunk size
432 $got = 0;
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);
437 $chunk .= $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);
446 // next chunk
447 if ($finger==0) { break; } // already read all the lines
448 else {
449 $tail = $finger;
450 $finger = max($tail-$chunk_size, 0);
453 fclose($fp);
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]);
465 if ($tmp!==false) {
466 $cache[$id][$tmp['date']] = $tmp;
467 $revs[] = $tmp['date'];
471 return $revs;