xref: /dokuwiki/inc/changelog.php (revision 7df78a78385819596d6e10562733bb88073fbf5c)
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 *
14 * @param string $line changelog line
15 * @return array|bool parsed line or false
16 */
17function parseChangelogLine($line) {
18    $line = rtrim($line, "\n");
19    $tmp = explode("\t", $line);
20    if ($tmp!==false && count($tmp)>1) {
21        $info = array();
22        $info['date']  = (int)$tmp[0]; // unix timestamp
23        $info['ip']    = $tmp[1]; // IPv4 address (127.0.0.1)
24        $info['type']  = $tmp[2]; // log line type
25        $info['id']    = $tmp[3]; // page id
26        $info['user']  = $tmp[4]; // user name
27        $info['sum']   = $tmp[5]; // edit summary (or action reason)
28        $info['extra'] = $tmp[6]; // extra data (varies by line type)
29        if(isset($tmp[7]) && $tmp[7] !== '') { //last item has line-end||
30            $info['sizechange'] = (int) $tmp[7];
31        } else {
32            $info['sizechange'] = null;
33        }
34        return $info;
35    } else {
36        return false;
37    }
38}
39
40/**
41 * Adds an entry to the changelog and saves the metadata for the page
42 *
43 * @param int    $date      Timestamp of the change (after quick repeated edits not unique (e.g. change checkbox via do plugin)
44 * @param String $id        Name of the affected page
45 * @param String $type      Type of the change see DOKU_CHANGE_TYPE_*
46 * @param String $summary   Summary of the change
47 * @param mixed  $extra     In case of a revert the revision (timestmp) of the reverted page
48 * @param array  $flags     Additional flags in a key value array.
49 *                             Available flags:
50 *                             - ExternalEdit - mark as an external edit.
51 * @param null|int $sizechange Change of filesize
52 *
53 * @author Andreas Gohr <andi@splitbrain.org>
54 * @author Esther Brunner <wikidesign@gmail.com>
55 * @author Ben Coburn <btcoburn@silicodon.net>
56 */
57function addLogEntry(
58    $date,
59    $id,
60    $type=DOKU_CHANGE_TYPE_EDIT,
61    $summary='',
62    $extra='',
63    $flags=null,
64    $sizechange=null)
65{
66    global $conf, $INFO;
67    /** @var Input $INPUT */
68    global $INPUT;
69
70    // check for special flags as keys
71    if (!is_array($flags)) { $flags = array(); }
72    $flagExternalEdit = isset($flags['ExternalEdit']);
73
74    $id = cleanid($id);
75    $file = wikiFN($id);
76    $created = @filectime($file);
77    $minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT);
78    $wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE);
79
80    if(!$date) $date = time(); //use current time if none supplied
81    $remote = (!$flagExternalEdit)?clientIP(true):'127.0.0.1';
82    $user   = (!$flagExternalEdit)?$INPUT->server->str('REMOTE_USER'):'';
83    if($sizechange === null) {
84        $sizechange = '';
85    } else {
86        $sizechange = (int) $sizechange;
87    }
88
89    $strip = array("\t", "\n");
90    $logline = array(
91        'date'       => $date,
92        'ip'         => $remote,
93        'type'       => str_replace($strip, '', $type),
94        'id'         => $id,
95        'user'       => $user,
96        'sum'        => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255),
97        'extra'      => str_replace($strip, '', $extra),
98        'sizechange' => $sizechange
99    );
100
101    $wasCreated = ($type===DOKU_CHANGE_TYPE_CREATE);
102    $wasReverted = ($type===DOKU_CHANGE_TYPE_REVERT);
103    // update metadata
104    if (!$wasRemoved) {
105        $oldmeta = p_read_metadata($id)['persistent'];
106        $meta    = array();
107        if (
108            $wasCreated && (
109                empty($oldmeta['date']['created']) ||
110                $oldmeta['date']['created'] === $created
111            )
112        ){
113            // newly created
114            $meta['date']['created'] = $created;
115            if ($user){
116                $meta['creator'] = isset($INFO) ? $INFO['userinfo']['name'] : null;
117                $meta['user']    = $user;
118            }
119        } elseif (($wasCreated || $wasReverted) && !empty($oldmeta['date']['created'])) {
120            // re-created / restored
121            $meta['date']['created']  = $oldmeta['date']['created'];
122            $meta['date']['modified'] = $created; // use the files ctime here
123            $meta['creator'] = isset($oldmeta['creator']) ? $oldmeta['creator'] : null;
124            if ($user) $meta['contributor'][$user] = isset($INFO) ? $INFO['userinfo']['name'] : null;
125        } elseif (!$minor) {   // non-minor modification
126            $meta['date']['modified'] = $date;
127            if ($user) $meta['contributor'][$user] = isset($INFO) ? $INFO['userinfo']['name'] : null;
128        }
129        $meta['last_change'] = $logline;
130        p_set_metadata($id, $meta);
131    }
132
133    // add changelog lines
134    $logline = implode("\t", $logline)."\n";
135    io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog
136    io_saveFile($conf['changelog'],$logline,true); //global changelog cache
137}
138
139/**
140 * Add's an entry to the media changelog
141 *
142 * @author Michael Hamann <michael@content-space.de>
143 * @author Andreas Gohr <andi@splitbrain.org>
144 * @author Esther Brunner <wikidesign@gmail.com>
145 * @author Ben Coburn <btcoburn@silicodon.net>
146 *
147 * @param int    $date      Timestamp of the change
148 * @param String $id        Name of the affected page
149 * @param String $type      Type of the change see DOKU_CHANGE_TYPE_*
150 * @param String $summary   Summary of the change
151 * @param mixed  $extra     In case of a revert the revision (timestmp) of the reverted page
152 * @param array  $flags     Additional flags in a key value array.
153 *                             Available flags:
154 *                             - (none, so far)
155 * @param null|int $sizechange Change of filesize
156 */
157function addMediaLogEntry(
158    $date,
159    $id,
160    $type=DOKU_CHANGE_TYPE_EDIT,
161    $summary='',
162    $extra='',
163    $flags=null,
164    $sizechange = null)
165{
166    global $conf;
167    /** @var Input $INPUT */
168    global $INPUT;
169
170    // check for special flags as keys
171    if (!is_array($flags)) $flags = array();
172    $flagExternalEdit = isset($flags['ExternalEdit']);
173
174    $id = cleanid($id);
175
176    if (!$date) $date = time(); //use current time if none supplied
177    $remote = (!$flagExternalEdit) ? clientIP(true) : '127.0.0.1';
178    $user   = (!$flagExternalEdit) ? $INPUT->server->str('REMOTE_USER') : '';
179    if ($sizechange === null) {
180        $sizechange = '';
181    } else {
182        $sizechange = (int) $sizechange;
183    }
184
185    $strip = array("\t", "\n");
186    $logline = array(
187        'date'       => $date,
188        'ip'         => $remote,
189        'type'       => str_replace($strip, '', $type),
190        'id'         => $id,
191        'user'       => $user,
192        'sum'        => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255),
193        'extra'      => str_replace($strip, '', $extra),
194        'sizechange' => $sizechange
195    );
196
197    // add changelog lines
198    $logline = implode("\t", $logline)."\n";
199    io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache
200    io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog
201}
202
203/**
204 * returns an array of recently changed files using the
205 * changelog
206 *
207 * The following constants can be used to control which changes are
208 * included. Add them together as needed.
209 *
210 * RECENTS_SKIP_DELETED   - don't include deleted pages
211 * RECENTS_SKIP_MINORS    - don't include minor changes
212 * RECENTS_ONLY_CREATION  - only include new created pages and media
213 * RECENTS_SKIP_SUBSPACES - don't include subspaces
214 * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
215 * RECENTS_MEDIA_PAGES_MIXED  - return both media changes and page changes
216 *
217 * @param int    $first   number of first entry returned (for paginating
218 * @param int    $num     return $num entries
219 * @param string $ns      restrict to given namespace
220 * @param int    $flags   see above
221 * @return array recently changed files
222 *
223 * @author Ben Coburn <btcoburn@silicodon.net>
224 * @author Kate Arzamastseva <pshns@ukr.net>
225 */
226function getRecents($first,$num,$ns='',$flags=0){
227    global $conf;
228    $recent = array();
229    $count  = 0;
230
231    if(!$num)
232        return $recent;
233
234    // read all recent changes. (kept short)
235    if ($flags & RECENTS_MEDIA_CHANGES) {
236        $lines = @file($conf['media_changelog']) ?: [];
237    } else {
238        $lines = @file($conf['changelog']) ?: [];
239    }
240    if (!is_array($lines)) {
241        $lines = array();
242    }
243    $lines_position = count($lines)-1;
244    $media_lines_position = 0;
245    $media_lines = array();
246
247    if ($flags & RECENTS_MEDIA_PAGES_MIXED) {
248        $media_lines = @file($conf['media_changelog']) ?: [];
249        if (!is_array($media_lines)) {
250            $media_lines = array();
251        }
252        $media_lines_position = count($media_lines)-1;
253    }
254
255    $seen = array(); // caches seen lines, _handleRecent() skips them
256
257    // handle lines
258    while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) {
259        if (empty($rec) && $lines_position >= 0) {
260            $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen);
261            if (!$rec) {
262                $lines_position --;
263                continue;
264            }
265        }
266        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) {
267            $media_rec = _handleRecent(
268                @$media_lines[$media_lines_position],
269                $ns,
270                $flags | RECENTS_MEDIA_CHANGES,
271                $seen
272            );
273            if (!$media_rec) {
274                $media_lines_position --;
275                continue;
276            }
277        }
278        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) {
279            $media_lines_position--;
280            $x = $media_rec;
281            $x['media'] = true;
282            $media_rec = false;
283        } else {
284            $lines_position--;
285            $x = $rec;
286            if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true;
287            $rec = false;
288        }
289        if(--$first >= 0) continue; // skip first entries
290        $recent[] = $x;
291        $count++;
292        // break when we have enough entries
293        if($count >= $num){ break; }
294    }
295    return $recent;
296}
297
298/**
299 * returns an array of files changed since a given time using the
300 * changelog
301 *
302 * The following constants can be used to control which changes are
303 * included. Add them together as needed.
304 *
305 * RECENTS_SKIP_DELETED   - don't include deleted pages
306 * RECENTS_SKIP_MINORS    - don't include minor changes
307 * RECENTS_ONLY_CREATION  - only include new created pages and media
308 * RECENTS_SKIP_SUBSPACES - don't include subspaces
309 * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
310 *
311 * @param int    $from    date of the oldest entry to return
312 * @param int    $to      date of the newest entry to return (for pagination, optional)
313 * @param string $ns      restrict to given namespace (optional)
314 * @param int    $flags   see above (optional)
315 * @return array of files
316 *
317 * @author Michael Hamann <michael@content-space.de>
318 * @author Ben Coburn <btcoburn@silicodon.net>
319 */
320function getRecentsSince($from,$to=null,$ns='',$flags=0){
321    global $conf;
322    $recent = array();
323
324    if($to && $to < $from)
325        return $recent;
326
327    // read all recent changes. (kept short)
328    if ($flags & RECENTS_MEDIA_CHANGES) {
329        $lines = @file($conf['media_changelog']);
330    } else {
331        $lines = @file($conf['changelog']);
332    }
333    if(!$lines) return $recent;
334
335    // we start searching at the end of the list
336    $lines = array_reverse($lines);
337
338    // handle lines
339    $seen = array(); // caches seen lines, _handleRecent() skips them
340
341    foreach($lines as $line){
342        $rec = _handleRecent($line, $ns, $flags, $seen);
343        if($rec !== false) {
344            if ($rec['date'] >= $from) {
345                if (!$to || $rec['date'] <= $to) {
346                    $recent[] = $rec;
347                }
348            } else {
349                break;
350            }
351        }
352    }
353
354    return array_reverse($recent);
355}
356
357/**
358 * Internal function used by getRecents
359 *
360 * don't call directly
361 *
362 * @see getRecents()
363 * @author Andreas Gohr <andi@splitbrain.org>
364 * @author Ben Coburn <btcoburn@silicodon.net>
365 *
366 * @param string $line   changelog line
367 * @param string $ns     restrict to given namespace
368 * @param int    $flags  flags to control which changes are included
369 * @param array  $seen   listing of seen pages
370 * @return array|bool    false or array with info about a change
371 */
372function _handleRecent($line,$ns,$flags,&$seen){
373    if(empty($line)) return false;   //skip empty lines
374
375    // split the line into parts
376    $recent = parseChangelogLine($line);
377    if ($recent===false) { return false; }
378
379    // skip seen ones
380    if(isset($seen[$recent['id']])) return false;
381
382    // skip changes, of only new items are requested
383    if($recent['type']!==DOKU_CHANGE_TYPE_CREATE && ($flags & RECENTS_ONLY_CREATION)) return false;
384
385    // skip minors
386    if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
387
388    // remember in seen to skip additional sights
389    $seen[$recent['id']] = 1;
390
391    // check if it's a hidden page
392    if(isHiddenPage($recent['id'])) return false;
393
394    // filter namespace
395    if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
396
397    // exclude subnamespaces
398    if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
399
400    // check ACL
401    if ($flags & RECENTS_MEDIA_CHANGES) {
402        $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*');
403    } else {
404        $recent['perms'] = auth_quickaclcheck($recent['id']);
405    }
406    if ($recent['perms'] < AUTH_READ) return false;
407
408    // check existance
409    if($flags & RECENTS_SKIP_DELETED){
410        $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
411        if(!file_exists($fn)) return false;
412    }
413
414    return $recent;
415}
416