xref: /dokuwiki/inc/changelog.php (revision 7d559c7f2292c82b8d5072fad951b8e33cdced05)
1*7d559c7fSBen Coburn<?php
2*7d559c7fSBen Coburn/**
3*7d559c7fSBen Coburn * Changelog handling functions
4*7d559c7fSBen Coburn *
5*7d559c7fSBen Coburn * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6*7d559c7fSBen Coburn * @author     Andreas Gohr <andi@splitbrain.org>
7*7d559c7fSBen Coburn */
8*7d559c7fSBen Coburn
9*7d559c7fSBen Coburn/**
10*7d559c7fSBen Coburn * parses a changelog line into it's components
11*7d559c7fSBen Coburn *
12*7d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
13*7d559c7fSBen Coburn */
14*7d559c7fSBen Coburnfunction parseChangelogLine($line) {
15*7d559c7fSBen Coburn  $tmp = explode("\t", $line);
16*7d559c7fSBen Coburn    if ($tmp!==false && count($tmp)>1) {
17*7d559c7fSBen Coburn      $info = array();
18*7d559c7fSBen Coburn      $info['date']  = $tmp[0]; // unix timestamp
19*7d559c7fSBen Coburn      $info['ip']    = $tmp[1]; // IPv4 address (127.0.0.1)
20*7d559c7fSBen Coburn      $info['type']  = $tmp[2]; // log line type
21*7d559c7fSBen Coburn      $info['id']    = $tmp[3]; // page id
22*7d559c7fSBen Coburn      $info['user']  = $tmp[4]; // user name
23*7d559c7fSBen Coburn      $info['sum']   = $tmp[5]; // edit summary (or action reason)
24*7d559c7fSBen Coburn      $info['extra'] = rtrim($tmp[6], "\n"); // extra data (varies by line type)
25*7d559c7fSBen Coburn      return $info;
26*7d559c7fSBen Coburn  } else { return false; }
27*7d559c7fSBen Coburn}
28*7d559c7fSBen Coburn
29*7d559c7fSBen Coburn/**
30*7d559c7fSBen Coburn * Add's an entry to the changelog and saves the metadata for the page
31*7d559c7fSBen Coburn *
32*7d559c7fSBen Coburn * @author Andreas Gohr <andi@splitbrain.org>
33*7d559c7fSBen Coburn * @author Esther Brunner <wikidesign@gmail.com>
34*7d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
35*7d559c7fSBen Coburn */
36*7d559c7fSBen Coburnfunction addLogEntry($date, $id, $type='E', $summary='', $extra=''){
37*7d559c7fSBen Coburn  global $conf, $INFO;
38*7d559c7fSBen Coburn
39*7d559c7fSBen Coburn  $id = cleanid($id);
40*7d559c7fSBen Coburn  $file = wikiFN($id);
41*7d559c7fSBen Coburn  $created = @filectime($file);
42*7d559c7fSBen Coburn  $minor = ($type==='e');
43*7d559c7fSBen Coburn  $wasRemoved = ($type==='D');
44*7d559c7fSBen Coburn
45*7d559c7fSBen Coburn  if(!$date) $date = time(); //use current time if none supplied
46*7d559c7fSBen Coburn  $remote = $_SERVER['REMOTE_ADDR'];
47*7d559c7fSBen Coburn  $user   = $_SERVER['REMOTE_USER'];
48*7d559c7fSBen Coburn
49*7d559c7fSBen Coburn  $strip = array("\t", "\n");
50*7d559c7fSBen Coburn  $logline = array(
51*7d559c7fSBen Coburn    'date'  => $date,
52*7d559c7fSBen Coburn    'ip'    => $remote,
53*7d559c7fSBen Coburn    'type'  => str_replace($strip, '', $type),
54*7d559c7fSBen Coburn    'id'    => $id,
55*7d559c7fSBen Coburn    'user'  => $user,
56*7d559c7fSBen Coburn    'sum'   => str_replace($strip, '', $summary),
57*7d559c7fSBen Coburn    'extra' => str_replace($strip, '', $extra)
58*7d559c7fSBen Coburn  );
59*7d559c7fSBen Coburn
60*7d559c7fSBen Coburn  // update metadata
61*7d559c7fSBen Coburn  if (!$wasRemoved) {
62*7d559c7fSBen Coburn    $meta = array();
63*7d559c7fSBen Coburn    if (!$INFO['exists']){ // newly created
64*7d559c7fSBen Coburn      $meta['date']['created'] = $created;
65*7d559c7fSBen Coburn      if ($user) $meta['creator'] = $INFO['userinfo']['name'];
66*7d559c7fSBen Coburn    } elseif (!$minor) {   // non-minor modification
67*7d559c7fSBen Coburn      $meta['date']['modified'] = $date;
68*7d559c7fSBen Coburn      if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name'];
69*7d559c7fSBen Coburn    }
70*7d559c7fSBen Coburn    $meta['last_change'] = $logline;
71*7d559c7fSBen Coburn    p_set_metadata($id, $meta, true);
72*7d559c7fSBen Coburn  }
73*7d559c7fSBen Coburn
74*7d559c7fSBen Coburn  // add changelog lines
75*7d559c7fSBen Coburn  $logline = implode("\t", $logline)."\n";
76*7d559c7fSBen Coburn  io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog
77*7d559c7fSBen Coburn  io_saveFile($conf['changelog'],$logline,true); //global changelog cache
78*7d559c7fSBen Coburn}
79*7d559c7fSBen Coburn
80*7d559c7fSBen Coburn/**
81*7d559c7fSBen Coburn * returns an array of recently changed files using the
82*7d559c7fSBen Coburn * changelog
83*7d559c7fSBen Coburn *
84*7d559c7fSBen Coburn * The following constants can be used to control which changes are
85*7d559c7fSBen Coburn * included. Add them together as needed.
86*7d559c7fSBen Coburn *
87*7d559c7fSBen Coburn * RECENTS_SKIP_DELETED   - don't include deleted pages
88*7d559c7fSBen Coburn * RECENTS_SKIP_MINORS    - don't include minor changes
89*7d559c7fSBen Coburn * RECENTS_SKIP_SUBSPACES - don't include subspaces
90*7d559c7fSBen Coburn *
91*7d559c7fSBen Coburn * @param int    $first   number of first entry returned (for paginating
92*7d559c7fSBen Coburn * @param int    $num     return $num entries
93*7d559c7fSBen Coburn * @param string $ns      restrict to given namespace
94*7d559c7fSBen Coburn * @param bool   $flags   see above
95*7d559c7fSBen Coburn *
96*7d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
97*7d559c7fSBen Coburn */
98*7d559c7fSBen Coburnfunction getRecents($first,$num,$ns='',$flags=0){
99*7d559c7fSBen Coburn  global $conf;
100*7d559c7fSBen Coburn  $recent = array();
101*7d559c7fSBen Coburn  $count  = 0;
102*7d559c7fSBen Coburn
103*7d559c7fSBen Coburn  if(!$num)
104*7d559c7fSBen Coburn    return $recent;
105*7d559c7fSBen Coburn
106*7d559c7fSBen Coburn  // read all recent changes. (kept short)
107*7d559c7fSBen Coburn  $lines = file($conf['changelog']);
108*7d559c7fSBen Coburn
109*7d559c7fSBen Coburn  // handle lines
110*7d559c7fSBen Coburn  for($i = count($lines)-1; $i >= 0; $i--){
111*7d559c7fSBen Coburn    $rec = _handleRecent($lines[$i], $ns, $flags);
112*7d559c7fSBen Coburn    if($rec !== false) {
113*7d559c7fSBen Coburn      if(--$first >= 0) continue; // skip first entries
114*7d559c7fSBen Coburn      $recent[] = $rec;
115*7d559c7fSBen Coburn      $count++;
116*7d559c7fSBen Coburn      // break when we have enough entries
117*7d559c7fSBen Coburn      if($count >= $num){ break; }
118*7d559c7fSBen Coburn    }
119*7d559c7fSBen Coburn  }
120*7d559c7fSBen Coburn
121*7d559c7fSBen Coburn  return $recent;
122*7d559c7fSBen Coburn}
123*7d559c7fSBen Coburn
124*7d559c7fSBen Coburn/**
125*7d559c7fSBen Coburn * Internal function used by getRecents
126*7d559c7fSBen Coburn *
127*7d559c7fSBen Coburn * don't call directly
128*7d559c7fSBen Coburn *
129*7d559c7fSBen Coburn * @see getRecents()
130*7d559c7fSBen Coburn * @author Andreas Gohr <andi@splitbrain.org>
131*7d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
132*7d559c7fSBen Coburn */
133*7d559c7fSBen Coburnfunction _handleRecent($line,$ns,$flags){
134*7d559c7fSBen Coburn  static $seen  = array();         //caches seen pages and skip them
135*7d559c7fSBen Coburn  if(empty($line)) return false;   //skip empty lines
136*7d559c7fSBen Coburn
137*7d559c7fSBen Coburn  // split the line into parts
138*7d559c7fSBen Coburn  $recent = parseChangelogLine($line);
139*7d559c7fSBen Coburn  if ($recent===false) { return false; }
140*7d559c7fSBen Coburn
141*7d559c7fSBen Coburn  // skip seen ones
142*7d559c7fSBen Coburn  if(isset($seen[$recent['id']])) return false;
143*7d559c7fSBen Coburn
144*7d559c7fSBen Coburn  // skip minors
145*7d559c7fSBen Coburn  if($recent['type']==='e' && ($flags & RECENTS_SKIP_MINORS)) return false;
146*7d559c7fSBen Coburn
147*7d559c7fSBen Coburn  // remember in seen to skip additional sights
148*7d559c7fSBen Coburn  $seen[$recent['id']] = 1;
149*7d559c7fSBen Coburn
150*7d559c7fSBen Coburn  // check if it's a hidden page
151*7d559c7fSBen Coburn  if(isHiddenPage($recent['id'])) return false;
152*7d559c7fSBen Coburn
153*7d559c7fSBen Coburn  // filter namespace
154*7d559c7fSBen Coburn  if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
155*7d559c7fSBen Coburn
156*7d559c7fSBen Coburn  // exclude subnamespaces
157*7d559c7fSBen Coburn  if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
158*7d559c7fSBen Coburn
159*7d559c7fSBen Coburn  // check ACL
160*7d559c7fSBen Coburn  if (auth_quickaclcheck($recent['id']) < AUTH_READ) return false;
161*7d559c7fSBen Coburn
162*7d559c7fSBen Coburn  // check existance
163*7d559c7fSBen Coburn  if((!@file_exists(wikiFN($recent['id']))) && ($flags & RECENTS_SKIP_DELETED)) return false;
164*7d559c7fSBen Coburn
165*7d559c7fSBen Coburn  return $recent;
166*7d559c7fSBen Coburn}
167*7d559c7fSBen Coburn
168*7d559c7fSBen Coburn/**
169*7d559c7fSBen Coburn * Get the changelog information for a specific page id
170*7d559c7fSBen Coburn * and revision (timestamp). Adjacent changelog lines
171*7d559c7fSBen Coburn * are optimistically parsed and cached to speed up
172*7d559c7fSBen Coburn * consecutive calls to getRevisionInfo. For large
173*7d559c7fSBen Coburn * changelog files, only the chunk containing the
174*7d559c7fSBen Coburn * requested changelog line is read.
175*7d559c7fSBen Coburn *
176*7d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
177*7d559c7fSBen Coburn */
178*7d559c7fSBen Coburnfunction getRevisionInfo($id, $rev, $chunk_size=8192) {
179*7d559c7fSBen Coburn  global $cache_revinfo;
180*7d559c7fSBen Coburn  $cache =& $cache_revinfo;
181*7d559c7fSBen Coburn  if (!isset($cache[$id])) { $cache[$id] = array(); }
182*7d559c7fSBen Coburn  $rev = max($rev, 0);
183*7d559c7fSBen Coburn
184*7d559c7fSBen Coburn  // check if it's already in the memory cache
185*7d559c7fSBen Coburn  if (isset($cache[$id]) && isset($cache[$id][$rev])) {
186*7d559c7fSBen Coburn    return $cache[$id][$rev];
187*7d559c7fSBen Coburn  }
188*7d559c7fSBen Coburn
189*7d559c7fSBen Coburn  $file = metaFN($id, '.changes');
190*7d559c7fSBen Coburn  if (!@file_exists($file)) { return false; }
191*7d559c7fSBen Coburn  if (filesize($file)<$chunk_size || $chunk_size==0) {
192*7d559c7fSBen Coburn    // read whole file
193*7d559c7fSBen Coburn    $lines = file($file);
194*7d559c7fSBen Coburn    if ($lines===false) { return false; }
195*7d559c7fSBen Coburn  } else {
196*7d559c7fSBen Coburn    // read by chunk
197*7d559c7fSBen Coburn    $fp = fopen($file, 'rb'); // "file pointer"
198*7d559c7fSBen Coburn    if ($fp===false) { return false; }
199*7d559c7fSBen Coburn    $head = 0;
200*7d559c7fSBen Coburn    fseek($fp, 0, SEEK_END);
201*7d559c7fSBen Coburn    $tail = ftell($fp);
202*7d559c7fSBen Coburn    $finger = 0;
203*7d559c7fSBen Coburn    $finger_rev = 0;
204*7d559c7fSBen Coburn
205*7d559c7fSBen Coburn    // find chunk
206*7d559c7fSBen Coburn    while ($tail-$head>$chunk_size) {
207*7d559c7fSBen Coburn      $finger = $head+floor(($tail-$head)/2.0);
208*7d559c7fSBen Coburn      fseek($fp, $finger);
209*7d559c7fSBen Coburn      fgets($fp); // slip the finger forward to a new line
210*7d559c7fSBen Coburn      $finger = ftell($fp);
211*7d559c7fSBen Coburn      $tmp = fgets($fp); // then read at that location
212*7d559c7fSBen Coburn      $tmp = parseChangelogLine($tmp);
213*7d559c7fSBen Coburn      $finger_rev = $tmp['date'];
214*7d559c7fSBen Coburn      if ($finger==$head || $finger==$tail) { break; }
215*7d559c7fSBen Coburn      if ($finger_rev>$rev) {
216*7d559c7fSBen Coburn        $tail = $finger;
217*7d559c7fSBen Coburn      } else {
218*7d559c7fSBen Coburn        $head = $finger;
219*7d559c7fSBen Coburn      }
220*7d559c7fSBen Coburn    }
221*7d559c7fSBen Coburn
222*7d559c7fSBen Coburn    if ($tail-$head<1) {
223*7d559c7fSBen Coburn      // cound not find chunk, assume requested rev is missing
224*7d559c7fSBen Coburn      fclose($fp);
225*7d559c7fSBen Coburn      return false;
226*7d559c7fSBen Coburn    }
227*7d559c7fSBen Coburn
228*7d559c7fSBen Coburn    // read chunk
229*7d559c7fSBen Coburn    $chunk = '';
230*7d559c7fSBen Coburn    $chunk_size = max($tail-$head, 0); // found chunk size
231*7d559c7fSBen Coburn    $got = 0;
232*7d559c7fSBen Coburn    fseek($fp, $head);
233*7d559c7fSBen Coburn    while ($got<$chunk_size && !feof($fp)) {
234*7d559c7fSBen Coburn      $tmp = fread($fp, max($chunk_size-$got, 0));
235*7d559c7fSBen Coburn      if ($tmp===false) { break; } //error state
236*7d559c7fSBen Coburn      $got += strlen($tmp);
237*7d559c7fSBen Coburn      $chunk .= $tmp;
238*7d559c7fSBen Coburn    }
239*7d559c7fSBen Coburn    $lines = explode("\n", $chunk);
240*7d559c7fSBen Coburn    array_pop($lines); // remove trailing newline
241*7d559c7fSBen Coburn    fclose($fp);
242*7d559c7fSBen Coburn  }
243*7d559c7fSBen Coburn
244*7d559c7fSBen Coburn  // parse and cache changelog lines
245*7d559c7fSBen Coburn  foreach ($lines as $value) {
246*7d559c7fSBen Coburn    $tmp = parseChangelogLine($value);
247*7d559c7fSBen Coburn    if ($tmp!==false) {
248*7d559c7fSBen Coburn      $cache[$id][$tmp['date']] = $tmp;
249*7d559c7fSBen Coburn    }
250*7d559c7fSBen Coburn  }
251*7d559c7fSBen Coburn  if (!isset($cache[$id][$rev])) { return false; }
252*7d559c7fSBen Coburn  return $cache[$id][$rev];
253*7d559c7fSBen Coburn}
254*7d559c7fSBen Coburn
255*7d559c7fSBen Coburn/**
256*7d559c7fSBen Coburn * Return a list of page revisions numbers
257*7d559c7fSBen Coburn * Does not guarantee that the revision exists in the attic,
258*7d559c7fSBen Coburn * only that a line with the date exists in the changelog.
259*7d559c7fSBen Coburn * By default the current revision is skipped.
260*7d559c7fSBen Coburn *
261*7d559c7fSBen Coburn * id:    the page of interest
262*7d559c7fSBen Coburn * first: skip the first n changelog lines
263*7d559c7fSBen Coburn * num:   number of revisions to return
264*7d559c7fSBen Coburn *
265*7d559c7fSBen Coburn * The current revision is automatically skipped when the page exists.
266*7d559c7fSBen Coburn * See $INFO['meta']['last_change'] for the current revision.
267*7d559c7fSBen Coburn *
268*7d559c7fSBen Coburn * For efficiency, the log lines are parsed and cached for later
269*7d559c7fSBen Coburn * calls to getRevisionInfo. Large changelog files are read
270*7d559c7fSBen Coburn * backwards in chunks untill the requested number of changelog
271*7d559c7fSBen Coburn * lines are recieved.
272*7d559c7fSBen Coburn *
273*7d559c7fSBen Coburn * @author Ben Coburn <btcoburn@silicodon.net>
274*7d559c7fSBen Coburn */
275*7d559c7fSBen Coburnfunction getRevisions($id, $first, $num, $chunk_size=8192) {
276*7d559c7fSBen Coburn  global $cache_revinfo;
277*7d559c7fSBen Coburn  $cache =& $cache_revinfo;
278*7d559c7fSBen Coburn  if (!isset($cache[$id])) { $cache[$id] = array(); }
279*7d559c7fSBen Coburn
280*7d559c7fSBen Coburn  $revs = array();
281*7d559c7fSBen Coburn  $lines = array();
282*7d559c7fSBen Coburn  $count  = 0;
283*7d559c7fSBen Coburn  $file = metaFN($id, '.changes');
284*7d559c7fSBen Coburn  $num = max($num, 0);
285*7d559c7fSBen Coburn  $chunk_size = max($chunk_size, 0);
286*7d559c7fSBen Coburn  if ($first<0) { $first = 0; }
287*7d559c7fSBen Coburn  else if (@file_exists(wikiFN($id))) {
288*7d559c7fSBen Coburn     // skip current revision if the page exists
289*7d559c7fSBen Coburn    $first = max($first+1, 0);
290*7d559c7fSBen Coburn  }
291*7d559c7fSBen Coburn
292*7d559c7fSBen Coburn  if (!@file_exists($file)) { return $revs; }
293*7d559c7fSBen Coburn  if (filesize($file)<$chunk_size || $chunk_size==0) {
294*7d559c7fSBen Coburn    // read whole file
295*7d559c7fSBen Coburn    $lines = file($file);
296*7d559c7fSBen Coburn    if ($lines===false) { return $revs; }
297*7d559c7fSBen Coburn  } else {
298*7d559c7fSBen Coburn    // read chunks backwards
299*7d559c7fSBen Coburn    $fp = fopen($file, 'rb'); // "file pointer"
300*7d559c7fSBen Coburn    if ($fp===false) { return $revs; }
301*7d559c7fSBen Coburn    fseek($fp, 0, SEEK_END);
302*7d559c7fSBen Coburn    $tail = ftell($fp);
303*7d559c7fSBen Coburn
304*7d559c7fSBen Coburn    // chunk backwards
305*7d559c7fSBen Coburn    $finger = max($tail-$chunk_size, 0);
306*7d559c7fSBen Coburn    while ($count<$num+$first) {
307*7d559c7fSBen Coburn      fseek($fp, $finger);
308*7d559c7fSBen Coburn      if ($finger>0) {
309*7d559c7fSBen Coburn        fgets($fp); // slip the finger forward to a new line
310*7d559c7fSBen Coburn        $finger = ftell($fp);
311*7d559c7fSBen Coburn      }
312*7d559c7fSBen Coburn
313*7d559c7fSBen Coburn      // read chunk
314*7d559c7fSBen Coburn      if ($tail<=$finger) { break; }
315*7d559c7fSBen Coburn      $chunk = '';
316*7d559c7fSBen Coburn      $read_size = max($tail-$finger, 0); // found chunk size
317*7d559c7fSBen Coburn      $got = 0;
318*7d559c7fSBen Coburn      while ($got<$read_size && !feof($fp)) {
319*7d559c7fSBen Coburn        $tmp = fread($fp, max($read_size-$got, 0));
320*7d559c7fSBen Coburn        if ($tmp===false) { break; } //error state
321*7d559c7fSBen Coburn        $got += strlen($tmp);
322*7d559c7fSBen Coburn        $chunk .= $tmp;
323*7d559c7fSBen Coburn      }
324*7d559c7fSBen Coburn      $tmp = explode("\n", $chunk);
325*7d559c7fSBen Coburn      array_pop($tmp); // remove trailing newline
326*7d559c7fSBen Coburn
327*7d559c7fSBen Coburn      // combine with previous chunk
328*7d559c7fSBen Coburn      $count += count($tmp);
329*7d559c7fSBen Coburn      $lines = array_merge($tmp, $lines);
330*7d559c7fSBen Coburn
331*7d559c7fSBen Coburn      // next chunk
332*7d559c7fSBen Coburn      if ($finger==0) { break; } // already read all the lines
333*7d559c7fSBen Coburn      else {
334*7d559c7fSBen Coburn        $tail = $finger;
335*7d559c7fSBen Coburn        $finger = max($tail-$chunk_size, 0);
336*7d559c7fSBen Coburn      }
337*7d559c7fSBen Coburn    }
338*7d559c7fSBen Coburn    fclose($fp);
339*7d559c7fSBen Coburn  }
340*7d559c7fSBen Coburn
341*7d559c7fSBen Coburn  // skip parsing extra lines
342*7d559c7fSBen Coburn  $num = max(min(count($lines)-$first, $num), 0);
343*7d559c7fSBen Coburn  if      ($first>0 && $num>0)  { $lines = array_slice($lines, max(count($lines)-$first-$num, 0), $num); }
344*7d559c7fSBen Coburn  else if ($first>0 && $num==0) { $lines = array_slice($lines, 0, max(count($lines)-$first, 0)); }
345*7d559c7fSBen Coburn  else if ($first==0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$num, 0)); }
346*7d559c7fSBen Coburn
347*7d559c7fSBen Coburn  // handle lines in reverse order
348*7d559c7fSBen Coburn  for ($i = count($lines)-1; $i >= 0; $i--) {
349*7d559c7fSBen Coburn    $tmp = parseChangelogLine($lines[$i]);
350*7d559c7fSBen Coburn    if ($tmp!==false) {
351*7d559c7fSBen Coburn      $cache[$id][$tmp['date']] = $tmp;
352*7d559c7fSBen Coburn      $revs[] = $tmp['date'];
353*7d559c7fSBen Coburn    }
354*7d559c7fSBen Coburn  }
355*7d559c7fSBen Coburn
356*7d559c7fSBen Coburn  return $revs;
357*7d559c7fSBen Coburn}
358*7d559c7fSBen Coburn
359*7d559c7fSBen Coburn
360