Failed Conditions
Push — master ( cbaf27...ca549e )
by Andreas
08:53 queued 04:43
created

changelog.php ➔ getRecents()   D

Complexity

Conditions 19
Paths 161

Size

Total Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
nc 161
nop 4
dl 0
loc 60
rs 4.0083
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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