xref: /dokuwiki/inc/changelog.php (revision 5aa52fafe8be8e728c0d2c9ff12c999e80766127)
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