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