1 | <?php
|
---|
2 | /**
|
---|
3 | * Changelog handling functions
|
---|
4 | *
|
---|
5 | * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
---|
6 | * @author Andreas Gohr <[email protected]>
|
---|
7 | */
|
---|
8 |
|
---|
9 | // Constants for known core changelog line types.
|
---|
10 | // Use these in place of string literals for more readable code.
|
---|
11 | define('DOKU_CHANGE_TYPE_CREATE', 'C');
|
---|
12 | define('DOKU_CHANGE_TYPE_EDIT', 'E');
|
---|
13 | define('DOKU_CHANGE_TYPE_MINOR_EDIT', 'e');
|
---|
14 | define('DOKU_CHANGE_TYPE_DELETE', 'D');
|
---|
15 | define('DOKU_CHANGE_TYPE_REVERT', 'R');
|
---|
16 |
|
---|
17 | /**
|
---|
18 | * parses a changelog line into it's components
|
---|
19 | *
|
---|
20 | * @author Ben Coburn <[email protected]>
|
---|
21 | */
|
---|
22 | function parseChangelogLine($line) {
|
---|
23 | $tmp = explode("\t", $line);
|
---|
24 | if ($tmp!==false && count($tmp)>1) {
|
---|
25 | $info = array();
|
---|
26 | $info['date'] = (int)$tmp[0]; // unix timestamp
|
---|
27 | $info['ip'] = $tmp[1]; // IPv4 address (127.0.0.1)
|
---|
28 | $info['type'] = $tmp[2]; // log line type
|
---|
29 | $info['id'] = $tmp[3]; // page id
|
---|
30 | $info['user'] = $tmp[4]; // user name
|
---|
31 | $info['sum'] = $tmp[5]; // edit summary (or action reason)
|
---|
32 | $info['extra'] = rtrim($tmp[6], "\n"); // extra data (varies by line type)
|
---|
33 | return $info;
|
---|
34 | } else { return false; }
|
---|
35 | }
|
---|
36 |
|
---|
37 | /**
|
---|
38 | * Add's an entry to the changelog and saves the metadata for the page
|
---|
39 | *
|
---|
40 | * @param int $date Timestamp of the change
|
---|
41 | * @param String $id Name of the affected page
|
---|
42 | * @param String $type Type of the change see DOKU_CHANGE_TYPE_*
|
---|
43 | * @param String $summary Summary of the change
|
---|
44 | * @param mixed $extra In case of a revert the revision (timestmp) of the reverted page
|
---|
45 | * @param array $flags Additional flags in a key value array.
|
---|
46 | * Availible flags:
|
---|
47 | * - ExternalEdit - mark as an external edit.
|
---|
48 | *
|
---|
49 | * @author Andreas Gohr <[email protected]>
|
---|
50 | * @author Esther Brunner <[email protected]>
|
---|
51 | * @author Ben Coburn <[email protected]>
|
---|
52 | */
|
---|
53 | function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null){
|
---|
54 | global $conf, $INFO;
|
---|
55 |
|
---|
56 | // check for special flags as keys
|
---|
57 | if (!is_array($flags)) { $flags = array(); }
|
---|
58 | $flagExternalEdit = isset($flags['ExternalEdit']);
|
---|
59 |
|
---|
60 | $id = cleanid($id);
|
---|
61 | $file = wikiFN($id);
|
---|
62 | $created = @filectime($file);
|
---|
63 | $minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT);
|
---|
64 | $wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE);
|
---|
65 |
|
---|
66 | if(!$date) $date = time(); //use current time if none supplied
|
---|
67 | $remote = (!$flagExternalEdit)?clientIP(true):'127.0.0.1';
|
---|
68 | $user = (!$flagExternalEdit)?$_SERVER['REMOTE_USER']:'';
|
---|
69 |
|
---|
70 | $strip = array("\t", "\n");
|
---|
71 | $logline = array(
|
---|
72 | 'date' => $date,
|
---|
73 | 'ip' => $remote,
|
---|
74 | 'type' => str_replace($strip, '', $type),
|
---|
75 | 'id' => $id,
|
---|
76 | 'user' => $user,
|
---|
77 | 'sum' => str_replace($strip, '', $summary),
|
---|
78 | 'extra' => str_replace($strip, '', $extra)
|
---|
79 | );
|
---|
80 |
|
---|
81 | // update metadata
|
---|
82 | if (!$wasRemoved) {
|
---|
83 | $oldmeta = p_read_metadata($id);
|
---|
84 | $meta = array();
|
---|
85 | if (!$INFO['exists'] && empty($oldmeta['persistent']['date']['created'])){ // newly created
|
---|
86 | $meta['date']['created'] = $created;
|
---|
87 | if ($user){
|
---|
88 | $meta['creator'] = $INFO['userinfo']['name'];
|
---|
89 | $meta['user'] = $user;
|
---|
90 | }
|
---|
91 | } elseif (!$INFO['exists'] && !empty($oldmeta['persistent']['date']['created'])) { // re-created / restored
|
---|
92 | $meta['date']['created'] = $oldmeta['persistent']['date']['created'];
|
---|
93 | $meta['date']['modified'] = $created; // use the files ctime here
|
---|
94 | $meta['creator'] = $oldmeta['persistent']['creator'];
|
---|
95 | if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name'];
|
---|
96 | } elseif (!$minor) { // non-minor modification
|
---|
97 | $meta['date']['modified'] = $date;
|
---|
98 | if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name'];
|
---|
99 | }
|
---|
100 | $meta['last_change'] = $logline;
|
---|
101 | p_set_metadata($id, $meta);
|
---|
102 | }
|
---|
103 |
|
---|
104 | // add changelog lines
|
---|
105 | $logline = implode("\t", $logline)."\n";
|
---|
106 | io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog
|
---|
107 | io_saveFile($conf['changelog'],$logline,true); //global changelog cache
|
---|
108 | }
|
---|
109 |
|
---|
110 | /**
|
---|
111 | * Add's an entry to the media changelog
|
---|
112 | *
|
---|
113 | * @author Michael Hamann <[email protected]>
|
---|
114 | * @author Andreas Gohr <[email protected]>
|
---|
115 | * @author Esther Brunner <[email protected]>
|
---|
116 | * @author Ben Coburn <[email protected]>
|
---|
117 | */
|
---|
118 | function addMediaLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null){
|
---|
119 | global $conf;
|
---|
120 |
|
---|
121 | $id = cleanid($id);
|
---|
122 |
|
---|
123 | if(!$date) $date = time(); //use current time if none supplied
|
---|
124 | $remote = clientIP(true);
|
---|
125 | $user = $_SERVER['REMOTE_USER'];
|
---|
126 |
|
---|
127 | $strip = array("\t", "\n");
|
---|
128 | $logline = array(
|
---|
129 | 'date' => $date,
|
---|
130 | 'ip' => $remote,
|
---|
131 | 'type' => str_replace($strip, '', $type),
|
---|
132 | 'id' => $id,
|
---|
133 | 'user' => $user,
|
---|
134 | 'sum' => str_replace($strip, '', $summary),
|
---|
135 | 'extra' => str_replace($strip, '', $extra)
|
---|
136 | );
|
---|
137 |
|
---|
138 | // add changelog lines
|
---|
139 | $logline = implode("\t", $logline)."\n";
|
---|
140 | io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache
|
---|
141 | }
|
---|
142 |
|
---|
143 | /**
|
---|
144 | * returns an array of recently changed files using the
|
---|
145 | * changelog
|
---|
146 | *
|
---|
147 | * The following constants can be used to control which changes are
|
---|
148 | * included. Add them together as needed.
|
---|
149 | *
|
---|
150 | * RECENTS_SKIP_DELETED - don't include deleted pages
|
---|
151 | * RECENTS_SKIP_MINORS - don't include minor changes
|
---|
152 | * RECENTS_SKIP_SUBSPACES - don't include subspaces
|
---|
153 | * RECENTS_MEDIA_CHANGES - return media changes instead of 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 bool $flags see above
|
---|
159 | *
|
---|
160 | * @author Ben Coburn <[email protected]>
|
---|
161 | */
|
---|
162 | function getRecents($first,$num,$ns='',$flags=0){
|
---|
163 | global $conf;
|
---|
164 | $recent = array();
|
---|
165 | $count = 0;
|
---|
166 |
|
---|
167 | if(!$num)
|
---|
168 | return $recent;
|
---|
169 |
|
---|
170 | // read all recent changes. (kept short)
|
---|
171 | if ($flags & RECENTS_MEDIA_CHANGES) {
|
---|
172 | $lines = @file($conf['media_changelog']);
|
---|
173 | } else {
|
---|
174 | $lines = @file($conf['changelog']);
|
---|
175 | }
|
---|
176 |
|
---|
177 | // handle lines
|
---|
178 | $seen = array(); // caches seen lines, _handleRecent() skips them
|
---|
179 | for($i = count($lines)-1; $i >= 0; $i--){
|
---|
180 | $rec = _handleRecent($lines[$i], $ns, $flags, $seen);
|
---|
181 | if($rec !== false) {
|
---|
182 | if(--$first >= 0) continue; // skip first entries
|
---|
183 | $recent[] = $rec;
|
---|
184 | $count++;
|
---|
185 | // break when we have enough entries
|
---|
186 | if($count >= $num){ break; }
|
---|
187 | }
|
---|
188 | }
|
---|
189 |
|
---|
190 | return $recent;
|
---|
191 | }
|
---|
192 |
|
---|
193 | /**
|
---|
194 | * returns an array of files changed since a given time using the
|
---|
195 | * changelog
|
---|
196 | *
|
---|
197 | * The following constants can be used to control which changes are
|
---|
198 | * included. Add them together as needed.
|
---|
199 | *
|
---|
200 | * RECENTS_SKIP_DELETED - don't include deleted pages
|
---|
201 | * RECENTS_SKIP_MINORS - don't include minor changes
|
---|
202 | * RECENTS_SKIP_SUBSPACES - don't include subspaces
|
---|
203 | * RECENTS_MEDIA_CHANGES - return media changes instead of page changes
|
---|
204 | *
|
---|
205 | * @param int $from date of the oldest entry to return
|
---|
206 | * @param int $to date of the newest entry to return (for pagination, optional)
|
---|
207 | * @param string $ns restrict to given namespace (optional)
|
---|
208 | * @param bool $flags see above (optional)
|
---|
209 | *
|
---|
210 | * @author Michael Hamann <[email protected]>
|
---|
211 | * @author Ben Coburn <[email protected]>
|
---|
212 | */
|
---|
213 | function getRecentsSince($from,$to=null,$ns='',$flags=0){
|
---|
214 | global $conf;
|
---|
215 | $recent = array();
|
---|
216 |
|
---|
217 | if($to && $to < $from)
|
---|
218 | return $recent;
|
---|
219 |
|
---|
220 | // read all recent changes. (kept short)
|
---|
221 | if ($flags & RECENTS_MEDIA_CHANGES) {
|
---|
222 | $lines = @file($conf['media_changelog']);
|
---|
223 | } else {
|
---|
224 | $lines = @file($conf['changelog']);
|
---|
225 | }
|
---|
226 |
|
---|
227 | // we start searching at the end of the list
|
---|
228 | $lines = array_reverse($lines);
|
---|
229 |
|
---|
230 | // handle lines
|
---|
231 | $seen = array(); // caches seen lines, _handleRecent() skips them
|
---|
232 |
|
---|
233 | foreach($lines as $line){
|
---|
234 | $rec = _handleRecent($line, $ns, $flags, $seen);
|
---|
235 | if($rec !== false) {
|
---|
236 | if ($rec['date'] >= $from) {
|
---|
237 | if (!$to || $rec['date'] <= $to) {
|
---|
238 | $recent[] = $rec;
|
---|
239 | }
|
---|
240 | } else {
|
---|
241 | break;
|
---|
242 | }
|
---|
243 | }
|
---|
244 | }
|
---|
245 |
|
---|
246 | return array_reverse($recent);
|
---|
247 | }
|
---|
248 |
|
---|
249 | /**
|
---|
250 | * Internal function used by getRecents
|
---|
251 | *
|
---|
252 | * don't call directly
|
---|
253 | *
|
---|
254 | * @see getRecents()
|
---|
255 | * @author Andreas Gohr <[email protected]>
|
---|
256 | * @author Ben Coburn <[email protected]>
|
---|
257 | */
|
---|
258 | function _handleRecent($line,$ns,$flags,&$seen){
|
---|
259 | if(empty($line)) return false; //skip empty lines
|
---|
260 |
|
---|
261 | // split the line into parts
|
---|
262 | $recent = parseChangelogLine($line);
|
---|
263 | if ($recent===false) { return false; }
|
---|
264 |
|
---|
265 | // skip seen ones
|
---|
266 | if(isset($seen[$recent['id']])) return false;
|
---|
267 |
|
---|
268 | // skip minors
|
---|
269 | if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
|
---|
270 |
|
---|
271 | // remember in seen to skip additional sights
|
---|
272 | $seen[$recent['id']] = 1;
|
---|
273 |
|
---|
274 | // check if it's a hidden page
|
---|
275 | if(isHiddenPage($recent['id'])) return false;
|
---|
276 |
|
---|
277 | // filter namespace
|
---|
278 | if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
|
---|
279 |
|
---|
280 | // exclude subnamespaces
|
---|
281 | if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
|
---|
282 |
|
---|
283 | // check ACL
|
---|
284 | $recent['perms'] = auth_quickaclcheck($recent['id']);
|
---|
285 | if ($recent['perms'] < AUTH_READ) return false;
|
---|
286 |
|
---|
287 | // check existance
|
---|
288 | $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
|
---|
289 | if((!@file_exists($fn)) && ($flags & RECENTS_SKIP_DELETED)) return false;
|
---|
290 |
|
---|
291 | return $recent;
|
---|
292 | }
|
---|
293 |
|
---|
294 | /**
|
---|
295 | * Get the changelog information for a specific page id
|
---|
296 | * and revision (timestamp). Adjacent changelog lines
|
---|
297 | * are optimistically parsed and cached to speed up
|
---|
298 | * consecutive calls to getRevisionInfo. For large
|
---|
299 | * changelog files, only the chunk containing the
|
---|
300 | * requested changelog line is read.
|
---|
301 | *
|
---|
302 | * @author Ben Coburn <[email protected]>
|
---|
303 | */
|
---|
304 | function getRevisionInfo($id, $rev, $chunk_size=8192) {
|
---|
305 | global $cache_revinfo;
|
---|
306 | $cache =& $cache_revinfo;
|
---|
307 | if (!isset($cache[$id])) { $cache[$id] = array(); }
|
---|
308 | $rev = max($rev, 0);
|
---|
309 |
|
---|
310 | // check if it's already in the memory cache
|
---|
311 | if (isset($cache[$id]) && isset($cache[$id][$rev])) {
|
---|
312 | return $cache[$id][$rev];
|
---|
313 | }
|
---|
314 |
|
---|
315 | $file = metaFN($id, '.changes');
|
---|
316 | if (!@file_exists($file)) { return false; }
|
---|
317 | if (filesize($file)<$chunk_size || $chunk_size==0) {
|
---|
318 | // read whole file
|
---|
319 | $lines = file($file);
|
---|
320 | if ($lines===false) { return false; }
|
---|
321 | } else {
|
---|
322 | // read by chunk
|
---|
323 | $fp = fopen($file, 'rb'); // "file pointer"
|
---|
324 | if ($fp===false) { return false; }
|
---|
325 | $head = 0;
|
---|
326 | fseek($fp, 0, SEEK_END);
|
---|
327 | $tail = ftell($fp);
|
---|
328 | $finger = 0;
|
---|
329 | $finger_rev = 0;
|
---|
330 |
|
---|
331 | // find chunk
|
---|
332 | while ($tail-$head>$chunk_size) {
|
---|
333 | $finger = $head+floor(($tail-$head)/2.0);
|
---|
334 | fseek($fp, $finger);
|
---|
335 | fgets($fp); // slip the finger forward to a new line
|
---|
336 | $finger = ftell($fp);
|
---|
337 | $tmp = fgets($fp); // then read at that location
|
---|
338 | $tmp = parseChangelogLine($tmp);
|
---|
339 | $finger_rev = $tmp['date'];
|
---|
340 | if ($finger==$head || $finger==$tail) { break; }
|
---|
341 | if ($finger_rev>$rev) {
|
---|
342 | $tail = $finger;
|
---|
343 | } else {
|
---|
344 | $head = $finger;
|
---|
345 | }
|
---|
346 | }
|
---|
347 |
|
---|
348 | if ($tail-$head<1) {
|
---|
349 | // cound not find chunk, assume requested rev is missing
|
---|
350 | fclose($fp);
|
---|
351 | return false;
|
---|
352 | }
|
---|
353 |
|
---|
354 | // read chunk
|
---|
355 | $chunk = '';
|
---|
356 | $chunk_size = max($tail-$head, 0); // found chunk size
|
---|
357 | $got = 0;
|
---|
358 | fseek($fp, $head);
|
---|
359 | while ($got<$chunk_size && !feof($fp)) {
|
---|
360 | $tmp = @fread($fp, max($chunk_size-$got, 0));
|
---|
361 | if ($tmp===false) { break; } //error state
|
---|
362 | $got += strlen($tmp);
|
---|
363 | $chunk .= $tmp;
|
---|
364 | }
|
---|
365 | $lines = explode("\n", $chunk);
|
---|
366 | array_pop($lines); // remove trailing newline
|
---|
367 | fclose($fp);
|
---|
368 | }
|
---|
369 |
|
---|
370 | // parse and cache changelog lines
|
---|
371 | foreach ($lines as $value) {
|
---|
372 | $tmp = parseChangelogLine($value);
|
---|
373 | if ($tmp!==false) {
|
---|
374 | $cache[$id][$tmp['date']] = $tmp;
|
---|
375 | }
|
---|
376 | }
|
---|
377 | if (!isset($cache[$id][$rev])) { return false; }
|
---|
378 | return $cache[$id][$rev];
|
---|
379 | }
|
---|
380 |
|
---|
381 | /**
|
---|
382 | * Return a list of page revisions numbers
|
---|
383 | * Does not guarantee that the revision exists in the attic,
|
---|
384 | * only that a line with the date exists in the changelog.
|
---|
385 | * By default the current revision is skipped.
|
---|
386 | *
|
---|
387 | * id: the page of interest
|
---|
388 | * first: skip the first n changelog lines
|
---|
389 | * num: number of revisions to return
|
---|
390 | *
|
---|
391 | * The current revision is automatically skipped when the page exists.
|
---|
392 | * See $INFO['meta']['last_change'] for the current revision.
|
---|
393 | *
|
---|
394 | * For efficiency, the log lines are parsed and cached for later
|
---|
395 | * calls to getRevisionInfo. Large changelog files are read
|
---|
396 | * backwards in chunks until the requested number of changelog
|
---|
397 | * lines are recieved.
|
---|
398 | *
|
---|
399 | * @author Ben Coburn <[email protected]>
|
---|
400 | */
|
---|
401 | function getRevisions($id, $first, $num, $chunk_size=8192) {
|
---|
402 | global $cache_revinfo;
|
---|
403 | $cache =& $cache_revinfo;
|
---|
404 | if (!isset($cache[$id])) { $cache[$id] = array(); }
|
---|
405 |
|
---|
406 | $revs = array();
|
---|
407 | $lines = array();
|
---|
408 | $count = 0;
|
---|
409 | $file = metaFN($id, '.changes');
|
---|
410 | $num = max($num, 0);
|
---|
411 | $chunk_size = max($chunk_size, 0);
|
---|
412 | if ($first<0) { $first = 0; }
|
---|
413 | else if (@file_exists(wikiFN($id))) {
|
---|
414 | // skip current revision if the page exists
|
---|
415 | $first = max($first+1, 0);
|
---|
416 | }
|
---|
417 |
|
---|
418 | if (!@file_exists($file)) { return $revs; }
|
---|
419 | if (filesize($file)<$chunk_size || $chunk_size==0) {
|
---|
420 | // read whole file
|
---|
421 | $lines = file($file);
|
---|
422 | if ($lines===false) { return $revs; }
|
---|
423 | } else {
|
---|
424 | // read chunks backwards
|
---|
425 | $fp = fopen($file, 'rb'); // "file pointer"
|
---|
426 | if ($fp===false) { return $revs; }
|
---|
427 | fseek($fp, 0, SEEK_END);
|
---|
428 | $tail = ftell($fp);
|
---|
429 |
|
---|
430 | // chunk backwards
|
---|
431 | $finger = max($tail-$chunk_size, 0);
|
---|
432 | while ($count<$num+$first) {
|
---|
433 | fseek($fp, $finger);
|
---|
434 | if ($finger>0) {
|
---|
435 | fgets($fp); // slip the finger forward to a new line
|
---|
436 | $finger = ftell($fp);
|
---|
437 | }
|
---|
438 |
|
---|
439 | // read chunk
|
---|
440 | if ($tail<=$finger) { break; }
|
---|
441 | $chunk = '';
|
---|
442 | $read_size = max($tail-$finger, 0); // found chunk size
|
---|
443 | $got = 0;
|
---|
444 | while ($got<$read_size && !feof($fp)) {
|
---|
445 | $tmp = @fread($fp, max($read_size-$got, 0));
|
---|
446 | if ($tmp===false) { break; } //error state
|
---|
447 | $got += strlen($tmp);
|
---|
448 | $chunk .= $tmp;
|
---|
449 | }
|
---|
450 | $tmp = explode("\n", $chunk);
|
---|
451 | array_pop($tmp); // remove trailing newline
|
---|
452 |
|
---|
453 | // combine with previous chunk
|
---|
454 | $count += count($tmp);
|
---|
455 | $lines = array_merge($tmp, $lines);
|
---|
456 |
|
---|
457 | // next chunk
|
---|
458 | if ($finger==0) { break; } // already read all the lines
|
---|
459 | else {
|
---|
460 | $tail = $finger;
|
---|
461 | $finger = max($tail-$chunk_size, 0);
|
---|
462 | }
|
---|
463 | }
|
---|
464 | fclose($fp);
|
---|
465 | }
|
---|
466 |
|
---|
467 | // skip parsing extra lines
|
---|
468 | $num = max(min(count($lines)-$first, $num), 0);
|
---|
469 | if ($first>0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$first-$num, 0), $num); }
|
---|
470 | else if ($first>0 && $num==0) { $lines = array_slice($lines, 0, max(count($lines)-$first, 0)); }
|
---|
471 | else if ($first==0 && $num>0) { $lines = array_slice($lines, max(count($lines)-$num, 0)); }
|
---|
472 |
|
---|
473 | // handle lines in reverse order
|
---|
474 | for ($i = count($lines)-1; $i >= 0; $i--) {
|
---|
475 | $tmp = parseChangelogLine($lines[$i]);
|
---|
476 | if ($tmp!==false) {
|
---|
477 | $cache[$id][$tmp['date']] = $tmp;
|
---|
478 | $revs[] = $tmp['date'];
|
---|
479 | }
|
---|
480 | }
|
---|
481 |
|
---|
482 | return $revs;
|
---|
483 | }
|
---|
484 |
|
---|
485 |
|
---|