Creating repository for dokuwiki modifications for sudaraka.org
[sudaraka-org:dokuwiki-mods.git] / inc / changelog.php
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 // 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');
16
17 /**
18  * parses a changelog line into it's components
19  *
20  * @author Ben Coburn <btcoburn@silicodon.net>
21  */
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; }
35 }
36
37 /**
38  * Add's an entry to the changelog and saves the metadata for the page
39  *
40  * @param int    $date      Timestamp of the change
41  * @param String $id        Name of the affected page
42  * @param String $type      Type of the change see DOKU_CHANGE_TYPE_*
43  * @param String $summary   Summary of the change
44  * @param mixed  $extra     In case of a revert the revision (timestmp) of the reverted page
45  * @param array  $flags     Additional flags in a key value array.
46  *                             Availible flags:
47  *                             - ExternalEdit - mark as an external edit.
48  *
49  * @author Andreas Gohr <andi@splitbrain.org>
50  * @author Esther Brunner <wikidesign@gmail.com>
51  * @author Ben Coburn <btcoburn@silicodon.net>
52  */
53 function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null){
54     global $conf, $INFO;
55
56     // check for special flags as keys
57     if (!is_array($flags)) { $flags = array(); }
58     $flagExternalEdit = isset($flags['ExternalEdit']);
59
60     $id = cleanid($id);
61     $file = wikiFN($id);
62     $created = @filectime($file);
63     $minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT);
64     $wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE);
65
66     if(!$date) $date = time(); //use current time if none supplied
67     $remote = (!$flagExternalEdit)?clientIP(true):'127.0.0.1';
68     $user   = (!$flagExternalEdit)?$_SERVER['REMOTE_USER']:'';
69
70     $strip = array("\t", "\n");
71     $logline = array(
72             'date'  => $date,
73             'ip'    => $remote,
74             'type'  => str_replace($strip, '', $type),
75             'id'    => $id,
76             'user'  => $user,
77             'sum'   => utf8_substr(str_replace($strip, '', $summary),0,255),
78             'extra' => str_replace($strip, '', $extra)
79             );
80
81     // update metadata
82     if (!$wasRemoved) {
83         $oldmeta = p_read_metadata($id);
84         $meta    = array();
85         if (!$INFO['exists'] && empty($oldmeta['persistent']['date']['created'])){ // newly created
86             $meta['date']['created'] = $created;
87             if ($user){
88                 $meta['creator'] = $INFO['userinfo']['name'];
89                 $meta['user']    = $user;
90             }
91         } elseif (!$INFO['exists'] && !empty($oldmeta['persistent']['date']['created'])) { // re-created / restored
92             $meta['date']['created']  = $oldmeta['persistent']['date']['created'];
93             $meta['date']['modified'] = $created; // use the files ctime here
94             $meta['creator'] = $oldmeta['persistent']['creator'];
95             if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name'];
96         } elseif (!$minor) {   // non-minor modification
97             $meta['date']['modified'] = $date;
98             if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name'];
99         }
100         $meta['last_change'] = $logline;
101         p_set_metadata($id, $meta);
102     }
103
104     // add changelog lines
105     $logline = implode("\t", $logline)."\n";
106     io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog
107     io_saveFile($conf['changelog'],$logline,true); //global changelog cache
108 }
109
110 /**
111  * Add's an entry to the media changelog
112  *
113  * @author Michael Hamann <michael@content-space.de>
114  * @author Andreas Gohr <andi@splitbrain.org>
115  * @author Esther Brunner <wikidesign@gmail.com>
116  * @author Ben Coburn <btcoburn@silicodon.net>
117  */
118 function addMediaLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null){
119     global $conf;
120
121     $id = cleanid($id);
122
123     if(!$date) $date = time(); //use current time if none supplied
124     $remote = clientIP(true);
125     $user   = $_SERVER['REMOTE_USER'];
126
127     $strip = array("\t", "\n");
128     $logline = array(
129             'date'  => $date,
130             'ip'    => $remote,
131             'type'  => str_replace($strip, '', $type),
132             'id'    => $id,
133             'user'  => $user,
134             'sum'   => utf8_substr(str_replace($strip, '', $summary),0,255),
135             'extra' => str_replace($strip, '', $extra)
136             );
137
138     // add changelog lines
139     $logline = implode("\t", $logline)."\n";
140     io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache
141     io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog
142 }
143
144 /**
145  * returns an array of recently changed files using the
146  * changelog
147  *
148  * The following constants can be used to control which changes are
149  * included. Add them together as needed.
150  *
151  * RECENTS_SKIP_DELETED   - don't include deleted pages
152  * RECENTS_SKIP_MINORS    - don't include minor changes
153  * RECENTS_SKIP_SUBSPACES - don't include subspaces
154  * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
155  * RECENTS_MEDIA_PAGES_MIXED  - return both media changes and page changes
156  *
157  * @param int    $first   number of first entry returned (for paginating
158  * @param int    $num     return $num entries
159  * @param string $ns      restrict to given namespace
160  * @param bool   $flags   see above
161  *
162  * @author Ben Coburn <btcoburn@silicodon.net>
163  * @author Kate Arzamastseva <pshns@ukr.net>
164  */
165 function getRecents($first,$num,$ns='',$flags=0){
166     global $conf;
167     $recent = array();
168     $count  = 0;
169
170     if(!$num)
171         return $recent;
172
173     // read all recent changes. (kept short)
174     if ($flags & RECENTS_MEDIA_CHANGES) {
175         $lines = @file($conf['media_changelog']);
176     } else {
177         $lines = @file($conf['changelog']);
178     }
179     $lines_position = count($lines)-1;
180
181     if ($flags & RECENTS_MEDIA_PAGES_MIXED) {
182         $media_lines = @file($conf['media_changelog']);
183         $media_lines_position = count($media_lines)-1;
184     }
185
186     $seen = array(); // caches seen lines, _handleRecent() skips them
187
188     // handle lines
189     while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) {
190         if (empty($rec) && $lines_position >= 0) {
191             $rec = _handleRecent(@$lines[$lines_position], $ns, $flags & ~RECENTS_MEDIA_CHANGES, $seen);
192             if (!$rec) {
193                 $lines_position --;
194                 continue;
195             }
196         }
197         if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) {
198             $media_rec = _handleRecent(@$media_lines[$media_lines_position], $ns, $flags | RECENTS_MEDIA_CHANGES, $seen);
199             if (!$media_rec) {
200                 $media_lines_position --;
201                 continue;
202             }
203         }
204         if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) {
205             $media_lines_position--;
206             $x = $media_rec;
207             $x['media'] = true;
208             $media_rec = false;
209         } else {
210             $lines_position--;
211             $x = $rec;
212             if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true;
213             $rec = false;
214         }
215         if(--$first >= 0) continue; // skip first entries
216         $recent[] = $x;
217         $count++;
218         // break when we have enough entries
219         if($count >= $num){ break; }
220     }
221     return $recent;
222 }
223
224 /**
225  * returns an array of files changed since a given time using the
226  * changelog
227  *
228  * The following constants can be used to control which changes are
229  * included. Add them together as needed.
230  *
231  * RECENTS_SKIP_DELETED   - don't include deleted pages
232  * RECENTS_SKIP_MINORS    - don't include minor changes
233  * RECENTS_SKIP_SUBSPACES - don't include subspaces
234  * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
235  *
236  * @param int    $from    date of the oldest entry to return
237  * @param int    $to      date of the newest entry to return (for pagination, optional)
238  * @param string $ns      restrict to given namespace (optional)
239  * @param bool   $flags   see above (optional)
240  *
241  * @author Michael Hamann <michael@content-space.de>
242  * @author Ben Coburn <btcoburn@silicodon.net>
243  */
244 function getRecentsSince($from,$to=null,$ns='',$flags=0){
245     global $conf;
246     $recent = array();
247
248     if($to && $to < $from)
249         return $recent;
250
251     // read all recent changes. (kept short)
252     if ($flags & RECENTS_MEDIA_CHANGES) {
253         $lines = @file($conf['media_changelog']);
254     } else {
255         $lines = @file($conf['changelog']);
256     }
257
258     // we start searching at the end of the list
259     $lines = array_reverse($lines);
260
261     // handle lines
262     $seen = array(); // caches seen lines, _handleRecent() skips them
263
264     foreach($lines as $line){
265         $rec = _handleRecent($line, $ns, $flags, $seen);
266         if($rec !== false) {
267             if ($rec['date'] >= $from) {
268                 if (!$to || $rec['date'] <= $to) {
269                     $recent[] = $rec;
270                 }
271             } else {
272                 break;
273             }
274         }
275     }
276
277     return array_reverse($recent);
278 }
279
280 /**
281  * Internal function used by getRecents
282  *
283  * don't call directly
284  *
285  * @see getRecents()
286  * @author Andreas Gohr <andi@splitbrain.org>
287  * @author Ben Coburn <btcoburn@silicodon.net>
288  */
289 function _handleRecent($line,$ns,$flags,&$seen){
290     if(empty($line)) return false;   //skip empty lines
291
292     // split the line into parts
293     $recent = parseChangelogLine($line);
294     if ($recent===false) { return false; }
295
296     // skip seen ones
297     if(isset($seen[$recent['id']])) return false;
298
299     // skip minors
300     if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
301
302     // remember in seen to skip additional sights
303     $seen[$recent['id']] = 1;
304
305     // check if it's a hidden page
306     if(isHiddenPage($recent['id'])) return false;
307
308     // filter namespace
309     if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
310
311     // exclude subnamespaces
312     if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
313
314     // check ACL
315     if ($flags & RECENTS_MEDIA_CHANGES) {
316         $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*');
317     } else {
318         $recent['perms'] = auth_quickaclcheck($recent['id']);
319     }
320     if ($recent['perms'] < AUTH_READ) return false;
321
322     // check existance
323     $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
324     if((!@file_exists($fn)) && ($flags & RECENTS_SKIP_DELETED)) return false;
325
326     return $recent;
327 }
328
329 /**
330  * Get the changelog information for a specific page id
331  * and revision (timestamp). Adjacent changelog lines
332  * are optimistically parsed and cached to speed up
333  * consecutive calls to getRevisionInfo. For large
334  * changelog files, only the chunk containing the
335  * requested changelog line is read.
336  *
337  * @author Ben Coburn <btcoburn@silicodon.net>
338  * @author Kate Arzamastseva <pshns@ukr.net>
339  */
340 function getRevisionInfo($id, $rev, $chunk_size=8192, $media=false) {
341     global $cache_revinfo;
342     $cache =& $cache_revinfo;
343     if (!isset($cache[$id])) { $cache[$id] = array(); }
344     $rev = max($rev, 0);
345
346     // check if it's already in the memory cache
347     if (isset($cache[$id]) && isset($cache[$id][$rev])) {
348         return $cache[$id][$rev];
349     }
350
351     if ($media) {
352         $file = mediaMetaFN($id, '.changes');
353     } else {
354         $file = metaFN($id, '.changes');
355     }
356     if (!@file_exists($file)) { return false; }
357     if (filesize($file)<$chunk_size || $chunk_size==0) {
358         // read whole file
359         $lines = file($file);
360         if ($lines===false) { return false; }
361     } else {
362         // read by chunk
363         $fp = fopen($file, 'rb'); // "file pointer"
364         if ($fp===false) { return false; }
365         $head = 0;
366         fseek($fp, 0, SEEK_END);
367         $tail = ftell($fp);
368         $finger = 0;
369         $finger_rev = 0;
370
371         // find chunk
372         while ($tail-$head>$chunk_size) {
373             $finger = $head+floor(($tail-$head)/2.0);
374             fseek($fp, $finger);
375             fgets($fp); // slip the finger forward to a new line
376             $finger = ftell($fp);
377             $tmp = fgets($fp); // then read at that location
378             $tmp = parseChangelogLine($tmp);
379             $finger_rev = $tmp['date'];
380             if ($finger==$head || $finger==$tail) { break; }
381             if ($finger_rev>$rev) {
382                 $tail = $finger;
383             } else {
384                 $head = $finger;
385             }
386         }
387
388         if ($tail-$head<1) {
389             // cound not find chunk, assume requested rev is missing
390             fclose($fp);
391             return false;
392         }
393
394         // read chunk
395         $chunk = '';
396         $chunk_size = max($tail-$head, 0); // found chunk size
397         $got = 0;
398         fseek($fp, $head);
399         while ($got<$chunk_size && !feof($fp)) {
400             $tmp = @fread($fp, max($chunk_size-$got, 0));
401             if ($tmp===false) { break; } //error state
402             $got += strlen($tmp);
403             $chunk .= $tmp;
404         }
405         $lines = explode("\n", $chunk);
406         array_pop($lines); // remove trailing newline
407         fclose($fp);
408     }
409
410     // parse and cache changelog lines
411     foreach ($lines as $value) {
412         $tmp = parseChangelogLine($value);
413         if ($tmp!==false) {
414             $cache[$id][$tmp['date']] = $tmp;
415         }
416     }
417     if (!isset($cache[$id][$rev])) { return false; }
418     return $cache[$id][$rev];
419 }
420
421 /**
422  * Return a list of page revisions numbers
423  * Does not guarantee that the revision exists in the attic,
424  * only that a line with the date exists in the changelog.
425  * By default the current revision is skipped.
426  *
427  * id:    the page of interest
428  * first: skip the first n changelog lines
429  * num:   number of revisions to return
430  *
431  * The current revision is automatically skipped when the page exists.
432  * See $INFO['meta']['last_change'] for the current revision.
433  *
434  * For efficiency, the log lines are parsed and cached for later
435  * calls to getRevisionInfo. Large changelog files are read
436  * backwards in chunks until the requested number of changelog
437  * lines are recieved.
438  *
439  * @author Ben Coburn <btcoburn@silicodon.net>
440  * @author Kate Arzamastseva <pshns@ukr.net>
441  */
442 function getRevisions($id, $first, $num, $chunk_size=8192, $media=false) {
443     global $cache_revinfo;
444     $cache =& $cache_revinfo;
445     if (!isset($cache[$id])) { $cache[$id] = array(); }
446
447     $revs = array();
448     $lines = array();
449     $count  = 0;
450     if ($media) {
451         $file = mediaMetaFN($id, '.changes');
452     } else {
453         $file = metaFN($id, '.changes');
454     }
455     $num = max($num, 0);
456     $chunk_size = max($chunk_size, 0);
457     if ($first<0) {
458         $first = 0;
459     } else if (!$media && @file_exists(wikiFN($id)) || $media && @file_exists(mediaFN($id))) {
460         // skip current revision if the page exists
461         $first = max($first+1, 0);
462     }
463
464     if (!@file_exists($file)) { return $revs; }
465     if (filesize($file)<$chunk_size || $chunk_size==0) {
466         // read whole file
467         $lines = file($file);
468         if ($lines===false) { return $revs; }
469     } else {
470         // read chunks backwards
471         $fp = fopen($file, 'rb'); // "file pointer"
472         if ($fp===false) { return $revs; }
473         fseek($fp, 0, SEEK_END);
474         $tail = ftell($fp);
475
476         // chunk backwards
477         $finger = max($tail-$chunk_size, 0);
478         while ($count<$num+$first) {
479             fseek($fp, $finger);
480             $nl = $finger;
481             if ($finger>0) {
482                 fgets($fp); // slip the finger forward to a new line
483                 $nl = ftell($fp);
484             }
485
486             // was the chunk big enough? if not, take another bite
487             if($nl > 0 && $tail <= $nl){
488                 $finger = max($finger-$chunk_size, 0);
489                 continue;
490             }else{
491                 $finger = $nl;
492             }
493
494             // read chunk
495             $chunk = '';
496             $read_size = max($tail-$finger, 0); // found chunk size
497             $got = 0;
498             while ($got<$read_size && !feof($fp)) {
499                 $tmp = @fread($fp, max($read_size-$got, 0));
500                 if ($tmp===false) { break; } //error state
501                 $got += strlen($tmp);
502                 $chunk .= $tmp;
503             }
504             $tmp = explode("\n", $chunk);
505             array_pop($tmp); // remove trailing newline
506
507             // combine with previous chunk
508             $count += count($tmp);
509             $lines = array_merge($tmp, $lines);
510
511             // next chunk
512             if ($finger==0) { break; } // already read all the lines
513             else {
514                 $tail = $finger;
515                 $finger = max($tail-$chunk_size, 0);
516             }
517         }
518         fclose($fp);
519     }
520
521     // skip parsing extra lines
522     $num = max(min(count($lines)-$first, $num), 0);
523     if      ($first>0 && $num>0)  { $lines = array_slice($lines, max(count($lines)-$first-$num, 0), $num); }
524     else if ($first>0 && $num==0) { $lines = array_slice($lines, 0, max(count($lines)-$first, 0)); }
525     else if ($first==0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$num, 0)); }
526
527     // handle lines in reverse order
528     for ($i = count($lines)-1; $i >= 0; $i--) {
529         $tmp = parseChangelogLine($lines[$i]);
530         if ($tmp!==false) {
531             $cache[$id][$tmp['date']] = $tmp;
532             $revs[] = $tmp['date'];
533         }
534     }
535
536     return $revs;
537 }
538
539