Mereged updates from DokuWiki 38
[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 int    $flags   see above
161  * @return array recently changed files
162  *
163  * @author Ben Coburn <btcoburn@silicodon.net>
164  * @author Kate Arzamastseva <pshns@ukr.net>
165  */
166 function getRecents($first,$num,$ns='',$flags=0){
167     global $conf;
168     $recent = array();
169     $count  = 0;
170
171     if(!$num)
172         return $recent;
173
174     // read all recent changes. (kept short)
175     if ($flags & RECENTS_MEDIA_CHANGES) {
176         $lines = @file($conf['media_changelog']);
177     } else {
178         $lines = @file($conf['changelog']);
179     }
180     $lines_position = count($lines)-1;
181     $media_lines_position = 0;
182     $media_lines = array();
183
184     if ($flags & RECENTS_MEDIA_PAGES_MIXED) {
185         $media_lines = @file($conf['media_changelog']);
186         $media_lines_position = count($media_lines)-1;
187     }
188
189     $seen = array(); // caches seen lines, _handleRecent() skips them
190
191     // handle lines
192     while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) {
193         if (empty($rec) && $lines_position >= 0) {
194             $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen);
195             if (!$rec) {
196                 $lines_position --;
197                 continue;
198             }
199         }
200         if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) {
201             $media_rec = _handleRecent(@$media_lines[$media_lines_position], $ns, $flags | RECENTS_MEDIA_CHANGES, $seen);
202             if (!$media_rec) {
203                 $media_lines_position --;
204                 continue;
205             }
206         }
207         if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) {
208             $media_lines_position--;
209             $x = $media_rec;
210             $x['media'] = true;
211             $media_rec = false;
212         } else {
213             $lines_position--;
214             $x = $rec;
215             if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true;
216             $rec = false;
217         }
218         if(--$first >= 0) continue; // skip first entries
219         $recent[] = $x;
220         $count++;
221         // break when we have enough entries
222         if($count >= $num){ break; }
223     }
224     return $recent;
225 }
226
227 /**
228  * returns an array of files changed since a given time using the
229  * changelog
230  *
231  * The following constants can be used to control which changes are
232  * included. Add them together as needed.
233  *
234  * RECENTS_SKIP_DELETED   - don't include deleted pages
235  * RECENTS_SKIP_MINORS    - don't include minor changes
236  * RECENTS_SKIP_SUBSPACES - don't include subspaces
237  * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
238  *
239  * @param int    $from    date of the oldest entry to return
240  * @param int    $to      date of the newest entry to return (for pagination, optional)
241  * @param string $ns      restrict to given namespace (optional)
242  * @param int    $flags   see above (optional)
243  * @return array of files
244  *
245  * @author Michael Hamann <michael@content-space.de>
246  * @author Ben Coburn <btcoburn@silicodon.net>
247  */
248 function getRecentsSince($from,$to=null,$ns='',$flags=0){
249     global $conf;
250     $recent = array();
251
252     if($to && $to < $from)
253         return $recent;
254
255     // read all recent changes. (kept short)
256     if ($flags & RECENTS_MEDIA_CHANGES) {
257         $lines = @file($conf['media_changelog']);
258     } else {
259         $lines = @file($conf['changelog']);
260     }
261
262     // we start searching at the end of the list
263     $lines = array_reverse($lines);
264
265     // handle lines
266     $seen = array(); // caches seen lines, _handleRecent() skips them
267
268     foreach($lines as $line){
269         $rec = _handleRecent($line, $ns, $flags, $seen);
270         if($rec !== false) {
271             if ($rec['date'] >= $from) {
272                 if (!$to || $rec['date'] <= $to) {
273                     $recent[] = $rec;
274                 }
275             } else {
276                 break;
277             }
278         }
279     }
280
281     return array_reverse($recent);
282 }
283
284 /**
285  * Internal function used by getRecents
286  *
287  * don't call directly
288  *
289  * @see getRecents()
290  * @author Andreas Gohr <andi@splitbrain.org>
291  * @author Ben Coburn <btcoburn@silicodon.net>
292  */
293 function _handleRecent($line,$ns,$flags,&$seen){
294     if(empty($line)) return false;   //skip empty lines
295
296     // split the line into parts
297     $recent = parseChangelogLine($line);
298     if ($recent===false) { return false; }
299
300     // skip seen ones
301     if(isset($seen[$recent['id']])) return false;
302
303     // skip minors
304     if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
305
306     // remember in seen to skip additional sights
307     $seen[$recent['id']] = 1;
308
309     // check if it's a hidden page
310     if(isHiddenPage($recent['id'])) return false;
311
312     // filter namespace
313     if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
314
315     // exclude subnamespaces
316     if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
317
318     // check ACL
319     if ($flags & RECENTS_MEDIA_CHANGES) {
320         $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*');
321     } else {
322         $recent['perms'] = auth_quickaclcheck($recent['id']);
323     }
324     if ($recent['perms'] < AUTH_READ) return false;
325
326     // check existance
327     if($flags & RECENTS_SKIP_DELETED){
328         $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
329         if(!@file_exists($fn)) return false;
330     }
331
332     return $recent;
333 }
334
335 /**
336  * Get the changelog information for a specific page id
337  * and revision (timestamp). Adjacent changelog lines
338  * are optimistically parsed and cached to speed up
339  * consecutive calls to getRevisionInfo. For large
340  * changelog files, only the chunk containing the
341  * requested changelog line is read.
342  *
343  * @author Ben Coburn <btcoburn@silicodon.net>
344  * @author Kate Arzamastseva <pshns@ukr.net>
345  */
346 function getRevisionInfo($id, $rev, $chunk_size=8192, $media=false) {
347     global $cache_revinfo;
348     $cache =& $cache_revinfo;
349     if (!isset($cache[$id])) { $cache[$id] = array(); }
350     $rev = max($rev, 0);
351
352     // check if it's already in the memory cache
353     if (isset($cache[$id]) && isset($cache[$id][$rev])) {
354         return $cache[$id][$rev];
355     }
356
357     if ($media) {
358         $file = mediaMetaFN($id, '.changes');
359     } else {
360         $file = metaFN($id, '.changes');
361     }
362     if (!@file_exists($file)) { return false; }
363     if (filesize($file)<$chunk_size || $chunk_size==0) {
364         // read whole file
365         $lines = file($file);
366         if ($lines===false) { return false; }
367     } else {
368         // read by chunk
369         $fp = fopen($file, 'rb'); // "file pointer"
370         if ($fp===false) { return false; }
371         $head = 0;
372         fseek($fp, 0, SEEK_END);
373         $tail = ftell($fp);
374         $finger = 0;
375         $finger_rev = 0;
376
377         // find chunk
378         while ($tail-$head>$chunk_size) {
379             $finger = $head+floor(($tail-$head)/2.0);
380             fseek($fp, $finger);
381             fgets($fp); // slip the finger forward to a new line
382             $finger = ftell($fp);
383             $tmp = fgets($fp); // then read at that location
384             $tmp = parseChangelogLine($tmp);
385             $finger_rev = $tmp['date'];
386             if ($finger==$head || $finger==$tail) { break; }
387             if ($finger_rev>$rev) {
388                 $tail = $finger;
389             } else {
390                 $head = $finger;
391             }
392         }
393
394         if ($tail-$head<1) {
395             // cound not find chunk, assume requested rev is missing
396             fclose($fp);
397             return false;
398         }
399
400         // read chunk
401         $chunk = '';
402         $chunk_size = max($tail-$head, 0); // found chunk size
403         $got = 0;
404         fseek($fp, $head);
405         while ($got<$chunk_size && !feof($fp)) {
406             $tmp = @fread($fp, max($chunk_size-$got, 0));
407             if ($tmp===false) { break; } //error state
408             $got += strlen($tmp);
409             $chunk .= $tmp;
410         }
411         $lines = explode("\n", $chunk);
412         array_pop($lines); // remove trailing newline
413         fclose($fp);
414     }
415
416     // parse and cache changelog lines
417     foreach ($lines as $value) {
418         $tmp = parseChangelogLine($value);
419         if ($tmp!==false) {
420             $cache[$id][$tmp['date']] = $tmp;
421         }
422     }
423     if (!isset($cache[$id][$rev])) { return false; }
424     return $cache[$id][$rev];
425 }
426
427 /**
428  * Return a list of page revisions numbers
429  * Does not guarantee that the revision exists in the attic,
430  * only that a line with the date exists in the changelog.
431  * By default the current revision is skipped.
432  *
433  * id:    the page of interest
434  * first: skip the first n changelog lines
435  * num:   number of revisions to return
436  *
437  * The current revision is automatically skipped when the page exists.
438  * See $INFO['meta']['last_change'] for the current revision.
439  *
440  * For efficiency, the log lines are parsed and cached for later
441  * calls to getRevisionInfo. Large changelog files are read
442  * backwards in chunks until the requested number of changelog
443  * lines are recieved.
444  *
445  * @author Ben Coburn <btcoburn@silicodon.net>
446  * @author Kate Arzamastseva <pshns@ukr.net>
447  */
448 function getRevisions($id, $first, $num, $chunk_size=8192, $media=false) {
449     global $cache_revinfo;
450     $cache =& $cache_revinfo;
451     if (!isset($cache[$id])) { $cache[$id] = array(); }
452
453     $revs = array();
454     $lines = array();
455     $count  = 0;
456     if ($media) {
457         $file = mediaMetaFN($id, '.changes');
458     } else {
459         $file = metaFN($id, '.changes');
460     }
461     $num = max($num, 0);
462     $chunk_size = max($chunk_size, 0);
463     if ($first<0) {
464         $first = 0;
465     } else if (!$media && @file_exists(wikiFN($id)) || $media && @file_exists(mediaFN($id))) {
466         // skip current revision if the page exists
467         $first = max($first+1, 0);
468     }
469
470     if (!@file_exists($file)) { return $revs; }
471     if (filesize($file)<$chunk_size || $chunk_size==0) {
472         // read whole file
473         $lines = file($file);
474         if ($lines===false) { return $revs; }
475     } else {
476         // read chunks backwards
477         $fp = fopen($file, 'rb'); // "file pointer"
478         if ($fp===false) { return $revs; }
479         fseek($fp, 0, SEEK_END);
480         $tail = ftell($fp);
481
482         // chunk backwards
483         $finger = max($tail-$chunk_size, 0);
484         while ($count<$num+$first) {
485             fseek($fp, $finger);
486             $nl = $finger;
487             if ($finger>0) {
488                 fgets($fp); // slip the finger forward to a new line
489                 $nl = ftell($fp);
490             }
491
492             // was the chunk big enough? if not, take another bite
493             if($nl > 0 && $tail <= $nl){
494                 $finger = max($finger-$chunk_size, 0);
495                 continue;
496             }else{
497                 $finger = $nl;
498             }
499
500             // read chunk
501             $chunk = '';
502             $read_size = max($tail-$finger, 0); // found chunk size
503             $got = 0;
504             while ($got<$read_size && !feof($fp)) {
505                 $tmp = @fread($fp, max($read_size-$got, 0));
506                 if ($tmp===false) { break; } //error state
507                 $got += strlen($tmp);
508                 $chunk .= $tmp;
509             }
510             $tmp = explode("\n", $chunk);
511             array_pop($tmp); // remove trailing newline
512
513             // combine with previous chunk
514             $count += count($tmp);
515             $lines = array_merge($tmp, $lines);
516
517             // next chunk
518             if ($finger==0) { break; } // already read all the lines
519             else {
520                 $tail = $finger;
521                 $finger = max($tail-$chunk_size, 0);
522             }
523         }
524         fclose($fp);
525     }
526
527     // skip parsing extra lines
528     $num = max(min(count($lines)-$first, $num), 0);
529     if      ($first>0 && $num>0)  { $lines = array_slice($lines, max(count($lines)-$first-$num, 0), $num); }
530     else if ($first>0 && $num==0) { $lines = array_slice($lines, 0, max(count($lines)-$first, 0)); }
531     else if ($first==0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$num, 0)); }
532
533     // handle lines in reverse order
534     for ($i = count($lines)-1; $i >= 0; $i--) {
535         $tmp = parseChangelogLine($lines[$i]);
536         if ($tmp!==false) {
537             $cache[$id][$tmp['date']] = $tmp;
538             $revs[] = $tmp['date'];
539         }
540     }
541
542     return $revs;
543 }
544
545