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