Failed Conditions
Push — psr2 ( 64159a )
by Andreas
07:54 queued 04:15
created

inc/changelog.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
 * @param string $line changelog line
23
 * @return array|bool parsed line or false
24
 */
25
function parseChangelogLine($line) {
26
    $line = rtrim($line, "\n");
27
    $tmp = explode("\t", $line);
28
    if ($tmp!==false && count($tmp)>1) {
29
        $info = array();
30
        $info['date']  = (int)$tmp[0]; // unix timestamp
31
        $info['ip']    = $tmp[1]; // IPv4 address (127.0.0.1)
32
        $info['type']  = $tmp[2]; // log line type
33
        $info['id']    = $tmp[3]; // page id
34
        $info['user']  = $tmp[4]; // user name
35
        $info['sum']   = $tmp[5]; // edit summary (or action reason)
36
        $info['extra'] = $tmp[6]; // extra data (varies by line type)
37
        if(isset($tmp[7]) && $tmp[7] !== '') { //last item has line-end||
38
            $info['sizechange'] = (int) $tmp[7];
39
        } else {
40
            $info['sizechange'] = null;
41
        }
42
        return $info;
43
    } else {
44
        return false;
45
    }
46
}
47
48
/**
49
 * Add's an entry to the changelog and saves the metadata for the page
50
 *
51
 * @param int    $date      Timestamp of the change
52
 * @param String $id        Name of the affected page
53
 * @param String $type      Type of the change see DOKU_CHANGE_TYPE_*
54
 * @param String $summary   Summary of the change
55
 * @param mixed  $extra     In case of a revert the revision (timestmp) of the reverted page
56
 * @param array  $flags     Additional flags in a key value array.
57
 *                             Available flags:
58
 *                             - ExternalEdit - mark as an external edit.
59
 * @param null|int $sizechange Change of filesize
60
 *
61
 * @author Andreas Gohr <[email protected]>
62
 * @author Esther Brunner <[email protected]>
63
 * @author Ben Coburn <[email protected]>
64
 */
65
function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null, $sizechange = null){
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'        => utf8_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);
106
        $meta    = array();
107
        if ($wasCreated && empty($oldmeta['persistent']['date']['created'])){
108
            // newly created
109
            $meta['date']['created'] = $created;
110
            if ($user){
111
                $meta['creator'] = $INFO['userinfo']['name'];
112
                $meta['user']    = $user;
113
            }
114
        } elseif (($wasCreated || $wasReverted) && !empty($oldmeta['persistent']['date']['created'])) {
115
            // re-created / restored
116
            $meta['date']['created']  = $oldmeta['persistent']['date']['created'];
117
            $meta['date']['modified'] = $created; // use the files ctime here
118
            $meta['creator'] = $oldmeta['persistent']['creator'];
119
            if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name'];
120
        } elseif (!$minor) {   // non-minor modification
121
            $meta['date']['modified'] = $date;
122
            if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name'];
123
        }
124
        $meta['last_change'] = $logline;
125
        p_set_metadata($id, $meta);
126
    }
127
128
    // add changelog lines
129
    $logline = implode("\t", $logline)."\n";
130
    io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog
131
    io_saveFile($conf['changelog'],$logline,true); //global changelog cache
132
}
133
134
/**
135
 * Add's an entry to the media changelog
136
 *
137
 * @author Michael Hamann <[email protected]>
138
 * @author Andreas Gohr <[email protected]>
139
 * @author Esther Brunner <[email protected]>
140
 * @author Ben Coburn <[email protected]>
141
 *
142
 * @param int    $date      Timestamp of the change
143
 * @param String $id        Name of the affected page
144
 * @param String $type      Type of the change see DOKU_CHANGE_TYPE_*
145
 * @param String $summary   Summary of the change
146
 * @param mixed  $extra     In case of a revert the revision (timestmp) of the reverted page
147
 * @param array  $flags     Additional flags in a key value array.
148
 *                             Available flags:
149
 *                             - (none, so far)
150
 * @param null|int $sizechange Change of filesize
151
 */
152
function addMediaLogEntry(
153
    $date,
154
    $id,
155
    $type=DOKU_CHANGE_TYPE_EDIT,
156
    $summary='',
157
    $extra='',
158
    $flags=null,
0 ignored issues
show
The parameter $flags is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
159
    $sizechange = null)
160
{
161
    global $conf;
162
    /** @var Input $INPUT */
163
    global $INPUT;
164
165
    $id = cleanid($id);
166
167
    if(!$date) $date = time(); //use current time if none supplied
168
    $remote = clientIP(true);
169
    $user   = $INPUT->server->str('REMOTE_USER');
170
    if($sizechange === null) {
171
        $sizechange = '';
172
    } else {
173
        $sizechange = (int) $sizechange;
174
    }
175
176
    $strip = array("\t", "\n");
177
    $logline = array(
178
        'date'       => $date,
179
        'ip'         => $remote,
180
        'type'       => str_replace($strip, '', $type),
181
        'id'         => $id,
182
        'user'       => $user,
183
        'sum'        => utf8_substr(str_replace($strip, '', $summary), 0, 255),
184
        'extra'      => str_replace($strip, '', $extra),
185
        'sizechange' => $sizechange
186
    );
187
188
    // add changelog lines
189
    $logline = implode("\t", $logline)."\n";
190
    io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache
191
    io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog
192
}
193
194
/**
195
 * returns an array of recently changed files using the
196
 * changelog
197
 *
198
 * The following constants can be used to control which changes are
199
 * included. Add them together as needed.
200
 *
201
 * RECENTS_SKIP_DELETED   - don't include deleted pages
202
 * RECENTS_SKIP_MINORS    - don't include minor changes
203
 * RECENTS_SKIP_SUBSPACES - don't include subspaces
204
 * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
205
 * RECENTS_MEDIA_PAGES_MIXED  - return both media changes and page changes
206
 *
207
 * @param int    $first   number of first entry returned (for paginating
208
 * @param int    $num     return $num entries
209
 * @param string $ns      restrict to given namespace
210
 * @param int    $flags   see above
211
 * @return array recently changed files
212
 *
213
 * @author Ben Coburn <[email protected]>
214
 * @author Kate Arzamastseva <[email protected]>
215
 */
216
function getRecents($first,$num,$ns='',$flags=0){
217
    global $conf;
218
    $recent = array();
219
    $count  = 0;
220
221
    if(!$num)
222
        return $recent;
223
224
    // read all recent changes. (kept short)
225
    if ($flags & RECENTS_MEDIA_CHANGES) {
226
        $lines = @file($conf['media_changelog']);
227
    } else {
228
        $lines = @file($conf['changelog']);
229
    }
230
    $lines_position = count($lines)-1;
231
    $media_lines_position = 0;
232
    $media_lines = array();
233
234
    if ($flags & RECENTS_MEDIA_PAGES_MIXED) {
235
        $media_lines = @file($conf['media_changelog']);
236
        $media_lines_position = count($media_lines)-1;
237
    }
238
239
    $seen = array(); // caches seen lines, _handleRecent() skips them
240
241
    // handle lines
242
    while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) {
243
        if (empty($rec) && $lines_position >= 0) {
244
            $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen);
245
            if (!$rec) {
246
                $lines_position --;
247
                continue;
248
            }
249
        }
250
        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) {
251
            $media_rec = _handleRecent(
252
                @$media_lines[$media_lines_position],
253
                $ns,
254
                $flags | RECENTS_MEDIA_CHANGES,
255
                $seen
256
            );
257
            if (!$media_rec) {
258
                $media_lines_position --;
259
                continue;
260
            }
261
        }
262
        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) {
263
            $media_lines_position--;
264
            $x = $media_rec;
265
            $x['media'] = true;
266
            $media_rec = false;
267
        } else {
268
            $lines_position--;
269
            $x = $rec;
270
            if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true;
271
            $rec = false;
272
        }
273
        if(--$first >= 0) continue; // skip first entries
274
        $recent[] = $x;
275
        $count++;
276
        // break when we have enough entries
277
        if($count >= $num){ break; }
278
    }
279
    return $recent;
280
}
281
282
/**
283
 * returns an array of files changed since a given time using the
284
 * changelog
285
 *
286
 * The following constants can be used to control which changes are
287
 * included. Add them together as needed.
288
 *
289
 * RECENTS_SKIP_DELETED   - don't include deleted pages
290
 * RECENTS_SKIP_MINORS    - don't include minor changes
291
 * RECENTS_SKIP_SUBSPACES - don't include subspaces
292
 * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
293
 *
294
 * @param int    $from    date of the oldest entry to return
295
 * @param int    $to      date of the newest entry to return (for pagination, optional)
296
 * @param string $ns      restrict to given namespace (optional)
297
 * @param int    $flags   see above (optional)
298
 * @return array of files
299
 *
300
 * @author Michael Hamann <[email protected]>
301
 * @author Ben Coburn <[email protected]>
302
 */
303
function getRecentsSince($from,$to=null,$ns='',$flags=0){
304
    global $conf;
305
    $recent = array();
306
307
    if($to && $to < $from)
308
        return $recent;
309
310
    // read all recent changes. (kept short)
311
    if ($flags & RECENTS_MEDIA_CHANGES) {
312
        $lines = @file($conf['media_changelog']);
313
    } else {
314
        $lines = @file($conf['changelog']);
315
    }
316
    if(!$lines) return $recent;
317
318
    // we start searching at the end of the list
319
    $lines = array_reverse($lines);
320
321
    // handle lines
322
    $seen = array(); // caches seen lines, _handleRecent() skips them
323
324
    foreach($lines as $line){
325
        $rec = _handleRecent($line, $ns, $flags, $seen);
326
        if($rec !== false) {
327
            if ($rec['date'] >= $from) {
328
                if (!$to || $rec['date'] <= $to) {
329
                    $recent[] = $rec;
330
                }
331
            } else {
332
                break;
333
            }
334
        }
335
    }
336
337
    return array_reverse($recent);
338
}
339
340
/**
341
 * Internal function used by getRecents
342
 *
343
 * don't call directly
344
 *
345
 * @see getRecents()
346
 * @author Andreas Gohr <[email protected]>
347
 * @author Ben Coburn <[email protected]>
348
 *
349
 * @param string $line   changelog line
350
 * @param string $ns     restrict to given namespace
351
 * @param int    $flags  flags to control which changes are included
352
 * @param array  $seen   listing of seen pages
353
 * @return array|bool    false or array with info about a change
354
 */
355
function _handleRecent($line,$ns,$flags,&$seen){
356
    if(empty($line)) return false;   //skip empty lines
357
358
    // split the line into parts
359
    $recent = parseChangelogLine($line);
360
    if ($recent===false) { return false; }
361
362
    // skip seen ones
363
    if(isset($seen[$recent['id']])) return false;
364
365
    // skip minors
366
    if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
367
368
    // remember in seen to skip additional sights
369
    $seen[$recent['id']] = 1;
370
371
    // check if it's a hidden page
372
    if(isHiddenPage($recent['id'])) return false;
373
374
    // filter namespace
375
    if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
376
377
    // exclude subnamespaces
378
    if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
379
380
    // check ACL
381
    if ($flags & RECENTS_MEDIA_CHANGES) {
382
        $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*');
383
    } else {
384
        $recent['perms'] = auth_quickaclcheck($recent['id']);
385
    }
386
    if ($recent['perms'] < AUTH_READ) return false;
387
388
    // check existance
389
    if($flags & RECENTS_SKIP_DELETED){
390
        $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
391
        if(!file_exists($fn)) return false;
392
    }
393
394
    return $recent;
395
}
396
397
/**
398
 * Class ChangeLog
399
 * methods for handling of changelog of pages or media files
400
 */
401
abstract class ChangeLog {
402
403
    /** @var string */
404
    protected $id;
405
    /** @var int */
406
    protected $chunk_size;
407
    /** @var array */
408
    protected $cache;
409
410
    /**
411
     * Constructor
412
     *
413
     * @param string $id         page id
414
     * @param int $chunk_size maximum block size read from file
415
     */
416
    public function __construct($id, $chunk_size = 8192) {
417
        global $cache_revinfo;
418
419
        $this->cache =& $cache_revinfo;
420
        if(!isset($this->cache[$id])) {
421
            $this->cache[$id] = array();
422
        }
423
424
        $this->id = $id;
425
        $this->setChunkSize($chunk_size);
426
427
    }
428
429
    /**
430
     * Set chunk size for file reading
431
     * Chunk size zero let read whole file at once
432
     *
433
     * @param int $chunk_size maximum block size read from file
434
     */
435
    public function setChunkSize($chunk_size) {
436
        if(!is_numeric($chunk_size)) $chunk_size = 0;
437
438
        $this->chunk_size = (int) max($chunk_size, 0);
439
    }
440
441
    /**
442
     * Returns path to changelog
443
     *
444
     * @return string path to file
445
     */
446
    abstract protected function getChangelogFilename();
447
448
    /**
449
     * Returns path to current page/media
450
     *
451
     * @return string path to file
452
     */
453
    abstract protected function getFilename();
454
455
    /**
456
     * Get the changelog information for a specific page id and revision (timestamp)
457
     *
458
     * Adjacent changelog lines are optimistically parsed and cached to speed up
459
     * consecutive calls to getRevisionInfo. For large changelog files, only the chunk
460
     * containing the requested changelog line is read.
461
     *
462
     * @param int $rev        revision timestamp
463
     * @return bool|array false or array with entries:
464
     *      - date:  unix timestamp
465
     *      - ip:    IPv4 address (127.0.0.1)
466
     *      - type:  log line type
467
     *      - id:    page id
468
     *      - user:  user name
469
     *      - sum:   edit summary (or action reason)
470
     *      - extra: extra data (varies by line type)
471
     *
472
     * @author Ben Coburn <[email protected]>
473
     * @author Kate Arzamastseva <[email protected]>
474
     */
475
    public function getRevisionInfo($rev) {
476
        $rev = max($rev, 0);
477
478
        // check if it's already in the memory cache
479
        if(isset($this->cache[$this->id]) && isset($this->cache[$this->id][$rev])) {
480
            return $this->cache[$this->id][$rev];
481
        }
482
483
        //read lines from changelog
484
        list($fp, $lines) = $this->readloglines($rev);
485
        if($fp) {
486
            fclose($fp);
487
        }
488
        if(empty($lines)) return false;
489
490
        // parse and cache changelog lines
491
        foreach($lines as $value) {
492
            $tmp = parseChangelogLine($value);
493
            if($tmp !== false) {
494
                $this->cache[$this->id][$tmp['date']] = $tmp;
495
            }
496
        }
497
        if(!isset($this->cache[$this->id][$rev])) {
498
            return false;
499
        }
500
        return $this->cache[$this->id][$rev];
501
    }
502
503
    /**
504
     * Return a list of page revisions numbers
505
     *
506
     * Does not guarantee that the revision exists in the attic,
507
     * only that a line with the date exists in the changelog.
508
     * By default the current revision is skipped.
509
     *
510
     * The current revision is automatically skipped when the page exists.
511
     * See $INFO['meta']['last_change'] for the current revision.
512
     * A negative $first let read the current revision too.
513
     *
514
     * For efficiency, the log lines are parsed and cached for later
515
     * calls to getRevisionInfo. Large changelog files are read
516
     * backwards in chunks until the requested number of changelog
517
     * lines are recieved.
518
     *
519
     * @param int $first      skip the first n changelog lines
520
     * @param int $num        number of revisions to return
521
     * @return array with the revision timestamps
522
     *
523
     * @author Ben Coburn <[email protected]>
524
     * @author Kate Arzamastseva <[email protected]>
525
     */
526
    public function getRevisions($first, $num) {
527
        $revs = array();
528
        $lines = array();
529
        $count = 0;
530
531
        $num = max($num, 0);
532
        if($num == 0) {
533
            return $revs;
534
        }
535
536
        if($first < 0) {
537
            $first = 0;
538
        } else if(file_exists($this->getFilename())) {
539
            // skip current revision if the page exists
540
            $first = max($first + 1, 0);
541
        }
542
543
        $file = $this->getChangelogFilename();
544
545
        if(!file_exists($file)) {
546
            return $revs;
547
        }
548
        if(filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
549
            // read whole file
550
            $lines = file($file);
551
            if($lines === false) {
552
                return $revs;
553
            }
554
        } else {
555
            // read chunks backwards
556
            $fp = fopen($file, 'rb'); // "file pointer"
557
            if($fp === false) {
558
                return $revs;
559
            }
560
            fseek($fp, 0, SEEK_END);
561
            $tail = ftell($fp);
562
563
            // chunk backwards
564
            $finger = max($tail - $this->chunk_size, 0);
565
            while($count < $num + $first) {
566
                $nl = $this->getNewlinepointer($fp, $finger);
567
568
                // was the chunk big enough? if not, take another bite
569
                if($nl > 0 && $tail <= $nl) {
570
                    $finger = max($finger - $this->chunk_size, 0);
571
                    continue;
572
                } else {
573
                    $finger = $nl;
574
                }
575
576
                // read chunk
577
                $chunk = '';
578
                $read_size = max($tail - $finger, 0); // found chunk size
579
                $got = 0;
580
                while($got < $read_size && !feof($fp)) {
581
                    $tmp = @fread($fp, max(min($this->chunk_size, $read_size - $got), 0));
582
                    if($tmp === false) {
583
                        break;
584
                    } //error state
585
                    $got += strlen($tmp);
586
                    $chunk .= $tmp;
587
                }
588
                $tmp = explode("\n", $chunk);
589
                array_pop($tmp); // remove trailing newline
590
591
                // combine with previous chunk
592
                $count += count($tmp);
593
                $lines = array_merge($tmp, $lines);
594
595
                // next chunk
596
                if($finger == 0) {
597
                    break;
598
                } // already read all the lines
599
                else {
600
                    $tail = $finger;
601
                    $finger = max($tail - $this->chunk_size, 0);
602
                }
603
            }
604
            fclose($fp);
605
        }
606
607
        // skip parsing extra lines
608
        $num = max(min(count($lines) - $first, $num), 0);
609
        if     ($first > 0 && $num > 0)  { $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num); }
610
        else if($first > 0 && $num == 0) { $lines = array_slice($lines, 0, max(count($lines) - $first, 0)); }
611
        else if($first == 0 && $num > 0) { $lines = array_slice($lines, max(count($lines) - $num, 0)); }
612
613
        // handle lines in reverse order
614
        for($i = count($lines) - 1; $i >= 0; $i--) {
615
            $tmp = parseChangelogLine($lines[$i]);
616
            if($tmp !== false) {
617
                $this->cache[$this->id][$tmp['date']] = $tmp;
618
                $revs[] = $tmp['date'];
619
            }
620
        }
621
622
        return $revs;
623
    }
624
625
    /**
626
     * Get the nth revision left or right handside  for a specific page id and revision (timestamp)
627
     *
628
     * For large changelog files, only the chunk containing the
629
     * reference revision $rev is read and sometimes a next chunck.
630
     *
631
     * Adjacent changelog lines are optimistically parsed and cached to speed up
632
     * consecutive calls to getRevisionInfo.
633
     *
634
     * @param int $rev        revision timestamp used as startdate (doesn't need to be revisionnumber)
635
     * @param int $direction  give position of returned revision with respect to $rev; positive=next, negative=prev
636
     * @return bool|int
637
     *      timestamp of the requested revision
638
     *      otherwise false
639
     */
640
    public function getRelativeRevision($rev, $direction) {
641
        $rev = max($rev, 0);
642
        $direction = (int) $direction;
643
644
        //no direction given or last rev, so no follow-up
645
        if(!$direction || ($direction > 0 && $this->isCurrentRevision($rev))) {
646
            return false;
647
        }
648
649
        //get lines from changelog
650
        list($fp, $lines, $head, $tail, $eof) = $this->readloglines($rev);
651
        if(empty($lines)) return false;
652
653
        // look for revisions later/earlier then $rev, when founded count till the wanted revision is reached
654
        // also parse and cache changelog lines for getRevisionInfo().
655
        $revcounter = 0;
656
        $relativerev = false;
657
        $checkotherchunck = true; //always runs once
658
        while(!$relativerev && $checkotherchunck) {
659
            $tmp = array();
660
            //parse in normal or reverse order
661
            $count = count($lines);
662
            if($direction > 0) {
663
                $start = 0;
664
                $step = 1;
665
            } else {
666
                $start = $count - 1;
667
                $step = -1;
668
            }
669
            for($i = $start; $i >= 0 && $i < $count; $i = $i + $step) {
670
                $tmp = parseChangelogLine($lines[$i]);
671
                if($tmp !== false) {
672
                    $this->cache[$this->id][$tmp['date']] = $tmp;
673
                    //look for revs older/earlier then reference $rev and select $direction-th one
674
                    if(($direction > 0 && $tmp['date'] > $rev) || ($direction < 0 && $tmp['date'] < $rev)) {
675
                        $revcounter++;
676
                        if($revcounter == abs($direction)) {
677
                            $relativerev = $tmp['date'];
678
                        }
679
                    }
680
                }
681
            }
682
683
            //true when $rev is found, but not the wanted follow-up.
684
            $checkotherchunck = $fp
685
                && ($tmp['date'] == $rev || ($revcounter > 0 && !$relativerev))
686
                && !(($tail == $eof && $direction > 0) || ($head == 0 && $direction < 0));
687
688
            if($checkotherchunck) {
689
                list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, $direction);
690
691
                if(empty($lines)) break;
692
            }
693
        }
694
        if($fp) {
695
            fclose($fp);
696
        }
697
698
        return $relativerev;
699
    }
700
701
    /**
702
     * Returns revisions around rev1 and rev2
703
     * When available it returns $max entries for each revision
704
     *
705
     * @param int $rev1 oldest revision timestamp
706
     * @param int $rev2 newest revision timestamp (0 looks up last revision)
707
     * @param int $max maximum number of revisions returned
708
     * @return array with two arrays with revisions surrounding rev1 respectively rev2
709
     */
710
    public function getRevisionsAround($rev1, $rev2, $max = 50) {
711
        $max = floor(abs($max) / 2)*2 + 1;
712
        $rev1 = max($rev1, 0);
713
        $rev2 = max($rev2, 0);
714
715
        if($rev2) {
716
            if($rev2 < $rev1) {
717
                $rev = $rev2;
718
                $rev2 = $rev1;
719
                $rev1 = $rev;
720
            }
721
        } else {
722
            //empty right side means a removed page. Look up last revision.
723
            $revs = $this->getRevisions(-1, 1);
724
            $rev2 = $revs[0];
725
        }
726
        //collect revisions around rev2
727
        list($revs2, $allrevs, $fp, $lines, $head, $tail) = $this->retrieveRevisionsAround($rev2, $max);
728
729
        if(empty($revs2)) return array(array(), array());
730
731
        //collect revisions around rev1
732
        $index = array_search($rev1, $allrevs);
733
        if($index === false) {
734
            //no overlapping revisions
735
            list($revs1,,,,,) = $this->retrieveRevisionsAround($rev1, $max);
736
            if(empty($revs1)) $revs1 = array();
737
        } else {
738
            //revisions overlaps, reuse revisions around rev2
739
            $revs1 = $allrevs;
740
            while($head > 0) {
741
                for($i = count($lines) - 1; $i >= 0; $i--) {
742
                    $tmp = parseChangelogLine($lines[$i]);
743
                    if($tmp !== false) {
744
                        $this->cache[$this->id][$tmp['date']] = $tmp;
745
                        $revs1[] = $tmp['date'];
746
                        $index++;
747
748
                        if($index > floor($max / 2)) break 2;
749
                    }
750
                }
751
752
                list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
753
            }
754
            sort($revs1);
755
            //return wanted selection
756
            $revs1 = array_slice($revs1, max($index - floor($max/2), 0), $max);
757
        }
758
759
        return array(array_reverse($revs1), array_reverse($revs2));
760
    }
761
762
    /**
763
     * Returns lines from changelog.
764
     * If file larger than $chuncksize, only chunck is read that could contain $rev.
765
     *
766
     * @param int $rev   revision timestamp
767
     * @return array|false
768
     *     if success returns array(fp, array(changeloglines), $head, $tail, $eof)
769
     *     where fp only defined for chuck reading, needs closing.
770
     *     otherwise false
771
     */
772
    protected function readloglines($rev) {
773
        $file = $this->getChangelogFilename();
774
775
        if(!file_exists($file)) {
776
            return false;
777
        }
778
779
        $fp = null;
780
        $head = 0;
781
        $tail = 0;
782
        $eof = 0;
783
784
        if(filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
785
            // read whole file
786
            $lines = file($file);
787
            if($lines === false) {
788
                return false;
789
            }
790
        } else {
791
            // read by chunk
792
            $fp = fopen($file, 'rb'); // "file pointer"
793
            if($fp === false) {
794
                return false;
795
            }
796
            $head = 0;
797
            fseek($fp, 0, SEEK_END);
798
            $eof = ftell($fp);
799
            $tail = $eof;
800
801
            // find chunk
802
            while($tail - $head > $this->chunk_size) {
803
                $finger = $head + floor(($tail - $head) / 2.0);
804
                $finger = $this->getNewlinepointer($fp, $finger);
805
                $tmp = fgets($fp);
806
                if($finger == $head || $finger == $tail) {
807
                    break;
808
                }
809
                $tmp = parseChangelogLine($tmp);
810
                $finger_rev = $tmp['date'];
811
812
                if($finger_rev > $rev) {
813
                    $tail = $finger;
814
                } else {
815
                    $head = $finger;
816
                }
817
            }
818
819
            if($tail - $head < 1) {
820
                // cound not find chunk, assume requested rev is missing
821
                fclose($fp);
822
                return false;
823
            }
824
825
            $lines = $this->readChunk($fp, $head, $tail);
826
        }
827
        return array(
828
            $fp,
829
            $lines,
830
            $head,
831
            $tail,
832
            $eof
833
        );
834
    }
835
836
    /**
837
     * Read chunk and return array with lines of given chunck.
838
     * Has no check if $head and $tail are really at a new line
839
     *
840
     * @param resource $fp    resource filepointer
841
     * @param int      $head  start point chunck
842
     * @param int      $tail  end point chunck
843
     * @return array lines read from chunck
844
     */
845
    protected function readChunk($fp, $head, $tail) {
846
        $chunk = '';
847
        $chunk_size = max($tail - $head, 0); // found chunk size
848
        $got = 0;
849
        fseek($fp, $head);
850
        while($got < $chunk_size && !feof($fp)) {
851
            $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0));
852
            if($tmp === false) { //error state
853
                break;
854
            }
855
            $got += strlen($tmp);
856
            $chunk .= $tmp;
857
        }
858
        $lines = explode("\n", $chunk);
859
        array_pop($lines); // remove trailing newline
860
        return $lines;
861
    }
862
863
    /**
864
     * Set pointer to first new line after $finger and return its position
865
     *
866
     * @param resource $fp      filepointer
867
     * @param int      $finger  a pointer
868
     * @return int pointer
869
     */
870
    protected function getNewlinepointer($fp, $finger) {
871
        fseek($fp, $finger);
872
        $nl = $finger;
873
        if($finger > 0) {
874
            fgets($fp); // slip the finger forward to a new line
875
            $nl = ftell($fp);
876
        }
877
        return $nl;
878
    }
879
880
    /**
881
     * Check whether given revision is the current page
882
     *
883
     * @param int $rev   timestamp of current page
884
     * @return bool true if $rev is current revision, otherwise false
885
     */
886
    public function isCurrentRevision($rev) {
887
        return $rev == @filemtime($this->getFilename());
888
    }
889
890
    /**
891
    * Return an existing revision for a specific date which is
892
    * the current one or younger or equal then the date
893
    *
894
    * @param number $date_at timestamp
895
    * @return string revision ('' for current)
896
    */
897
    function getLastRevisionAt($date_at){
898
        //requested date_at(timestamp) younger or equal then modified_time($this->id) => load current
899
        if(file_exists($this->getFilename()) && $date_at >= @filemtime($this->getFilename())) {
900
            return '';
901
        } else if ($rev = $this->getRelativeRevision($date_at+1, -1)) { //+1 to get also the requested date revision
902
            return $rev;
903
        } else {
904
            return false;
905
        }
906
    }
907
908
    /**
909
     * Returns the next lines of the changelog  of the chunck before head or after tail
910
     *
911
     * @param resource $fp filepointer
912
     * @param int $head position head of last chunk
913
     * @param int $tail position tail of last chunk
914
     * @param int $direction positive forward, negative backward
915
     * @return array with entries:
916
     *    - $lines: changelog lines of readed chunk
917
     *    - $head: head of chunk
918
     *    - $tail: tail of chunk
919
     */
920
    protected function readAdjacentChunk($fp, $head, $tail, $direction) {
921
        if(!$fp) return array(array(), $head, $tail);
922
923
        if($direction > 0) {
924
            //read forward
925
            $head = $tail;
926
            $tail = $head + floor($this->chunk_size * (2 / 3));
927
            $tail = $this->getNewlinepointer($fp, $tail);
928
        } else {
929
            //read backward
930
            $tail = $head;
931
            $head = max($tail - $this->chunk_size, 0);
932
            while(true) {
933
                $nl = $this->getNewlinepointer($fp, $head);
934
                // was the chunk big enough? if not, take another bite
935
                if($nl > 0 && $tail <= $nl) {
936
                    $head = max($head - $this->chunk_size, 0);
937
                } else {
938
                    $head = $nl;
939
                    break;
940
                }
941
            }
942
        }
943
944
        //load next chunck
945
        $lines = $this->readChunk($fp, $head, $tail);
946
        return array($lines, $head, $tail);
947
    }
948
949
    /**
950
     * Collect the $max revisions near to the timestamp $rev
951
     *
952
     * @param int $rev revision timestamp
953
     * @param int $max maximum number of revisions to be returned
954
     * @return bool|array
955
     *     return array with entries:
956
     *       - $requestedrevs: array of with $max revision timestamps
957
     *       - $revs: all parsed revision timestamps
958
     *       - $fp: filepointer only defined for chuck reading, needs closing.
959
     *       - $lines: non-parsed changelog lines before the parsed revisions
960
     *       - $head: position of first readed changelogline
961
     *       - $lasttail: position of end of last readed changelogline
962
     *     otherwise false
963
     */
964
    protected function retrieveRevisionsAround($rev, $max) {
965
        //get lines from changelog
966
        list($fp, $lines, $starthead, $starttail, /* $eof */) = $this->readloglines($rev);
967
        if(empty($lines)) return false;
968
969
        //parse chunk containing $rev, and read forward more chunks until $max/2 is reached
970
        $head = $starthead;
971
        $tail = $starttail;
972
        $revs = array();
973
        $aftercount = $beforecount = 0;
974
        while(count($lines) > 0) {
975
            foreach($lines as $line) {
976
                $tmp = parseChangelogLine($line);
977
                if($tmp !== false) {
978
                    $this->cache[$this->id][$tmp['date']] = $tmp;
979
                    $revs[] = $tmp['date'];
980
                    if($tmp['date'] >= $rev) {
981
                        //count revs after reference $rev
982
                        $aftercount++;
983
                        if($aftercount == 1) $beforecount = count($revs);
984
                    }
985
                    //enough revs after reference $rev?
986
                    if($aftercount > floor($max / 2)) break 2;
987
                }
988
            }
989
            //retrieve next chunk
990
            list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, 1);
991
        }
992
        if($aftercount == 0) return false;
993
994
        $lasttail = $tail;
995
996
        //read additional chuncks backward until $max/2 is reached and total number of revs is equal to $max
997
        $lines = array();
998
        $i = 0;
999
        if($aftercount > 0) {
1000
            $head = $starthead;
1001
            $tail = $starttail;
1002
            while($head > 0) {
1003
                list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
1004
1005
                for($i = count($lines) - 1; $i >= 0; $i--) {
1006
                    $tmp = parseChangelogLine($lines[$i]);
1007
                    if($tmp !== false) {
1008
                        $this->cache[$this->id][$tmp['date']] = $tmp;
1009
                        $revs[] = $tmp['date'];
1010
                        $beforecount++;
1011
                        //enough revs before reference $rev?
1012
                        if($beforecount > max(floor($max / 2), $max - $aftercount)) break 2;
1013
                    }
1014
                }
1015
            }
1016
        }
1017
        sort($revs);
1018
1019
        //keep only non-parsed lines
1020
        $lines = array_slice($lines, 0, $i);
1021
        //trunk desired selection
1022
        $requestedrevs = array_slice($revs, -$max, $max);
1023
1024
        return array($requestedrevs, $revs, $fp, $lines, $head, $lasttail);
1025
    }
1026
}
1027
1028
/**
1029
 * Class PageChangelog handles changelog of a wiki page
1030
 */
1031
class PageChangelog extends ChangeLog {
1032
1033
    /**
1034
     * Returns path to changelog
1035
     *
1036
     * @return string path to file
1037
     */
1038
    protected function getChangelogFilename() {
1039
        return metaFN($this->id, '.changes');
1040
    }
1041
1042
    /**
1043
     * Returns path to current page/media
1044
     *
1045
     * @return string path to file
1046
     */
1047
    protected function getFilename() {
1048
        return wikiFN($this->id);
1049
    }
1050
}
1051
1052
/**
1053
 * Class MediaChangelog handles changelog of a media file
1054
 */
1055
class MediaChangelog extends ChangeLog {
1056
1057
    /**
1058
     * Returns path to changelog
1059
     *
1060
     * @return string path to file
1061
     */
1062
    protected function getChangelogFilename() {
1063
        return mediaMetaFN($this->id, '.changes');
1064
    }
1065
1066
    /**
1067
     * Returns path to current page/media
1068
     *
1069
     * @return string path to file
1070
     */
1071
    protected function getFilename() {
1072
        return mediaFN($this->id);
1073
    }
1074
}
1075