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