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