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