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