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