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

PageChangelog   A

Complexity

Total Complexity 2

Size/Duplication

Total Lines 20
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 10
c 0
b 0
f 0
wmc 2
lcom 1
cbo 1

2 Methods

Rating   Name   Duplication   Size   Complexity  
A getChangelogFilename() 0 3 1
A getFilename() 0 3 1
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
Unused Code introduced by
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;
0 ignored issues
show
Bug introduced by
The variable $media_rec does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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)
0 ignored issues
show
Bug Best Practice introduced by
The expression $to of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $to of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
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