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