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