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