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