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
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    $id = cleanid($id);
163
164    if(!$date) $date = time(); //use current time if none supplied
165    $remote = clientIP(true);
166    $user   = $INPUT->server->str('REMOTE_USER');
167    if($sizechange === null) {
168        $sizechange = '';
169    } else {
170        $sizechange = (int) $sizechange;
171    }
172
173    $strip = array("\t", "\n");
174    $logline = array(
175        'date'       => $date,
176        'ip'         => $remote,
177        'type'       => str_replace($strip, '', $type),
178        'id'         => $id,
179        'user'       => $user,
180        'sum'        => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255),
181        'extra'      => str_replace($strip, '', $extra),
182        'sizechange' => $sizechange
183    );
184
185    // add changelog lines
186    $logline = implode("\t", $logline)."\n";
187    io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache
188    io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog
189}
190
191/**
192 * returns an array of recently changed files using the
193 * changelog
194 *
195 * The following constants can be used to control which changes are
196 * included. Add them together as needed.
197 *
198 * RECENTS_SKIP_DELETED   - don't include deleted pages
199 * RECENTS_SKIP_MINORS    - don't include minor changes
200 * RECENTS_ONLY_CREATION  - only include new created pages and media
201 * RECENTS_SKIP_SUBSPACES - don't include subspaces
202 * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
203 * RECENTS_MEDIA_PAGES_MIXED  - return both media changes and page changes
204 *
205 * @param int    $first   number of first entry returned (for paginating
206 * @param int    $num     return $num entries
207 * @param string $ns      restrict to given namespace
208 * @param int    $flags   see above
209 * @return array recently changed files
210 *
211 * @author Ben Coburn <btcoburn@silicodon.net>
212 * @author Kate Arzamastseva <pshns@ukr.net>
213 */
214function getRecents($first,$num,$ns='',$flags=0){
215    global $conf;
216    $recent = array();
217    $count  = 0;
218
219    if(!$num)
220        return $recent;
221
222    // read all recent changes. (kept short)
223    if ($flags & RECENTS_MEDIA_CHANGES) {
224        $lines = @file($conf['media_changelog']) ?: [];
225    } else {
226        $lines = @file($conf['changelog']) ?: [];
227    }
228    if (!is_array($lines)) {
229        $lines = array();
230    }
231    $lines_position = count($lines)-1;
232    $media_lines_position = 0;
233    $media_lines = array();
234
235    if ($flags & RECENTS_MEDIA_PAGES_MIXED) {
236        $media_lines = @file($conf['media_changelog']) ?: [];
237        if (!is_array($media_lines)) {
238            $media_lines = array();
239        }
240        $media_lines_position = count($media_lines)-1;
241    }
242
243    $seen = array(); // caches seen lines, _handleRecent() skips them
244
245    // handle lines
246    while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) {
247        if (empty($rec) && $lines_position >= 0) {
248            $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen);
249            if (!$rec) {
250                $lines_position --;
251                continue;
252            }
253        }
254        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) {
255            $media_rec = _handleRecent(
256                @$media_lines[$media_lines_position],
257                $ns,
258                $flags | RECENTS_MEDIA_CHANGES,
259                $seen
260            );
261            if (!$media_rec) {
262                $media_lines_position --;
263                continue;
264            }
265        }
266        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) {
267            $media_lines_position--;
268            $x = $media_rec;
269            $x['media'] = true;
270            $media_rec = false;
271        } else {
272            $lines_position--;
273            $x = $rec;
274            if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true;
275            $rec = false;
276        }
277        if(--$first >= 0) continue; // skip first entries
278        $recent[] = $x;
279        $count++;
280        // break when we have enough entries
281        if($count >= $num){ break; }
282    }
283    return $recent;
284}
285
286/**
287 * returns an array of files changed since a given time using the
288 * changelog
289 *
290 * The following constants can be used to control which changes are
291 * included. Add them together as needed.
292 *
293 * RECENTS_SKIP_DELETED   - don't include deleted pages
294 * RECENTS_SKIP_MINORS    - don't include minor changes
295 * RECENTS_ONLY_CREATION  - only include new created pages and media
296 * RECENTS_SKIP_SUBSPACES - don't include subspaces
297 * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
298 *
299 * @param int    $from    date of the oldest entry to return
300 * @param int    $to      date of the newest entry to return (for pagination, optional)
301 * @param string $ns      restrict to given namespace (optional)
302 * @param int    $flags   see above (optional)
303 * @return array of files
304 *
305 * @author Michael Hamann <michael@content-space.de>
306 * @author Ben Coburn <btcoburn@silicodon.net>
307 */
308function getRecentsSince($from,$to=null,$ns='',$flags=0){
309    global $conf;
310    $recent = array();
311
312    if($to && $to < $from)
313        return $recent;
314
315    // read all recent changes. (kept short)
316    if ($flags & RECENTS_MEDIA_CHANGES) {
317        $lines = @file($conf['media_changelog']);
318    } else {
319        $lines = @file($conf['changelog']);
320    }
321    if(!$lines) return $recent;
322
323    // we start searching at the end of the list
324    $lines = array_reverse($lines);
325
326    // handle lines
327    $seen = array(); // caches seen lines, _handleRecent() skips them
328
329    foreach($lines as $line){
330        $rec = _handleRecent($line, $ns, $flags, $seen);
331        if($rec !== false) {
332            if ($rec['date'] >= $from) {
333                if (!$to || $rec['date'] <= $to) {
334                    $recent[] = $rec;
335                }
336            } else {
337                break;
338            }
339        }
340    }
341
342    return array_reverse($recent);
343}
344
345/**
346 * Internal function used by getRecents
347 *
348 * don't call directly
349 *
350 * @see getRecents()
351 * @author Andreas Gohr <andi@splitbrain.org>
352 * @author Ben Coburn <btcoburn@silicodon.net>
353 *
354 * @param string $line   changelog line
355 * @param string $ns     restrict to given namespace
356 * @param int    $flags  flags to control which changes are included
357 * @param array  $seen   listing of seen pages
358 * @return array|bool    false or array with info about a change
359 */
360function _handleRecent($line,$ns,$flags,&$seen){
361    if(empty($line)) return false;   //skip empty lines
362
363    // split the line into parts
364    $recent = parseChangelogLine($line);
365    if ($recent===false) { return false; }
366
367    // skip seen ones
368    if(isset($seen[$recent['id']])) return false;
369
370    // skip changes, of only new items are requested
371    if($recent['type']!==DOKU_CHANGE_TYPE_CREATE && ($flags & RECENTS_ONLY_CREATION)) return false;
372
373    // skip minors
374    if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
375
376    // remember in seen to skip additional sights
377    $seen[$recent['id']] = 1;
378
379    // check if it's a hidden page
380    if(isHiddenPage($recent['id'])) return false;
381
382    // filter namespace
383    if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
384
385    // exclude subnamespaces
386    if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
387
388    // check ACL
389    if ($flags & RECENTS_MEDIA_CHANGES) {
390        $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*');
391    } else {
392        $recent['perms'] = auth_quickaclcheck($recent['id']);
393    }
394    if ($recent['perms'] < AUTH_READ) return false;
395
396    // check existance
397    if($flags & RECENTS_SKIP_DELETED){
398        $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
399        if(!file_exists($fn)) return false;
400    }
401
402    return $recent;
403}
404