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