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