Completed
Push — issue2613 ( 561ba0...395b0e )
by Henry
07:27 queued 03:39
created

changelog.php ➔ getRecents()   F

Complexity

Conditions 21
Paths 481

Size

Total Lines 66

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
nc 481
nop 4
dl 0
loc 66
rs 0.7208
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
    if (!is_array($lines)) {
221
        $lines = array();
222
    }
223
    $lines_position = count($lines)-1;
224
    $media_lines_position = 0;
225
    $media_lines = array();
226
227
    if ($flags & RECENTS_MEDIA_PAGES_MIXED) {
228
        $media_lines = @file($conf['media_changelog']);
229
        if (!is_array($media_lines)) {
230
            $media_lines = array();
231
        }
232
        $media_lines_position = count($media_lines)-1;
233
    }
234
235
    $seen = array(); // caches seen lines, _handleRecent() skips them
236
237
    // handle lines
238
    while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) {
239
        if (empty($rec) && $lines_position >= 0) {
240
            $rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen);
241
            if (!$rec) {
242
                $lines_position --;
243
                continue;
244
            }
245
        }
246
        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && empty($media_rec) && $media_lines_position >= 0) {
247
            $media_rec = _handleRecent(@$media_lines[$media_lines_position], $ns, $flags | RECENTS_MEDIA_CHANGES, $seen);
248
            if (!$media_rec) {
249
                $media_lines_position --;
250
                continue;
251
            }
252
        }
253
        if (($flags & RECENTS_MEDIA_PAGES_MIXED) && @$media_rec['date'] >= @$rec['date']) {
254
            $media_lines_position--;
255
            $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...
256
            $x['media'] = true;
257
            $media_rec = false;
258
        } else {
259
            $lines_position--;
260
            $x = $rec;
261
            if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true;
262
            $rec = false;
263
        }
264
        if(--$first >= 0) continue; // skip first entries
265
        $recent[] = $x;
266
        $count++;
267
        // break when we have enough entries
268
        if($count >= $num){ break; }
269
    }
270
    return $recent;
271
}
272
273
/**
274
 * returns an array of files changed since a given time using the
275
 * changelog
276
 *
277
 * The following constants can be used to control which changes are
278
 * included. Add them together as needed.
279
 *
280
 * RECENTS_SKIP_DELETED   - don't include deleted pages
281
 * RECENTS_SKIP_MINORS    - don't include minor changes
282
 * RECENTS_SKIP_SUBSPACES - don't include subspaces
283
 * RECENTS_MEDIA_CHANGES  - return media changes instead of page changes
284
 *
285
 * @param int    $from    date of the oldest entry to return
286
 * @param int    $to      date of the newest entry to return (for pagination, optional)
287
 * @param string $ns      restrict to given namespace (optional)
288
 * @param int    $flags   see above (optional)
289
 * @return array of files
290
 *
291
 * @author Michael Hamann <[email protected]>
292
 * @author Ben Coburn <[email protected]>
293
 */
294
function getRecentsSince($from,$to=null,$ns='',$flags=0){
295
    global $conf;
296
    $recent = array();
297
298
    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...
299
        return $recent;
300
301
    // read all recent changes. (kept short)
302
    if ($flags & RECENTS_MEDIA_CHANGES) {
303
        $lines = @file($conf['media_changelog']);
304
    } else {
305
        $lines = @file($conf['changelog']);
306
    }
307
    if(!$lines) return $recent;
308
309
    // we start searching at the end of the list
310
    $lines = array_reverse($lines);
311
312
    // handle lines
313
    $seen = array(); // caches seen lines, _handleRecent() skips them
314
315
    foreach($lines as $line){
316
        $rec = _handleRecent($line, $ns, $flags, $seen);
317
        if($rec !== false) {
318
            if ($rec['date'] >= $from) {
319
                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...
320
                    $recent[] = $rec;
321
                }
322
            } else {
323
                break;
324
            }
325
        }
326
    }
327
328
    return array_reverse($recent);
329
}
330
331
/**
332
 * Internal function used by getRecents
333
 *
334
 * don't call directly
335
 *
336
 * @see getRecents()
337
 * @author Andreas Gohr <[email protected]>
338
 * @author Ben Coburn <[email protected]>
339
 *
340
 * @param string $line   changelog line
341
 * @param string $ns     restrict to given namespace
342
 * @param int    $flags  flags to control which changes are included
343
 * @param array  $seen   listing of seen pages
344
 * @return array|bool    false or array with info about a change
345
 */
346
function _handleRecent($line,$ns,$flags,&$seen){
347
    if(empty($line)) return false;   //skip empty lines
348
349
    // split the line into parts
350
    $recent = parseChangelogLine($line);
351
    if ($recent===false) { return false; }
352
353
    // skip seen ones
354
    if(isset($seen[$recent['id']])) return false;
355
356
    // skip minors
357
    if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
358
359
    // remember in seen to skip additional sights
360
    $seen[$recent['id']] = 1;
361
362
    // check if it's a hidden page
363
    if(isHiddenPage($recent['id'])) return false;
364
365
    // filter namespace
366
    if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
367
368
    // exclude subnamespaces
369
    if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
370
371
    // check ACL
372
    if ($flags & RECENTS_MEDIA_CHANGES) {
373
        $recent['perms'] = auth_quickaclcheck(getNS($recent['id']).':*');
374
    } else {
375
        $recent['perms'] = auth_quickaclcheck($recent['id']);
376
    }
377
    if ($recent['perms'] < AUTH_READ) return false;
378
379
    // check existance
380
    if($flags & RECENTS_SKIP_DELETED){
381
        $fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
382
        if(!file_exists($fn)) return false;
383
    }
384
385
    return $recent;
386
}
387
388
/**
389
 * Class ChangeLog
390
 * methods for handling of changelog of pages or media files
391
 */
392
abstract class ChangeLog {
393
394
    /** @var string */
395
    protected $id;
396
    /** @var int */
397
    protected $chunk_size;
398
    /** @var array */
399
    protected $cache;
400
401
    /**
402
     * Constructor
403
     *
404
     * @param string $id         page id
405
     * @param int $chunk_size maximum block size read from file
406
     */
407
    public function __construct($id, $chunk_size = 8192) {
408
        global $cache_revinfo;
409
410
        $this->cache =& $cache_revinfo;
411
        if(!isset($this->cache[$id])) {
412
            $this->cache[$id] = array();
413
        }
414
415
        $this->id = $id;
416
        $this->setChunkSize($chunk_size);
417
418
    }
419
420
    /**
421
     * Set chunk size for file reading
422
     * Chunk size zero let read whole file at once
423
     *
424
     * @param int $chunk_size maximum block size read from file
425
     */
426
    public function setChunkSize($chunk_size) {
427
        if(!is_numeric($chunk_size)) $chunk_size = 0;
428
429
        $this->chunk_size = (int) max($chunk_size, 0);
430
    }
431
432
    /**
433
     * Returns path to changelog
434
     *
435
     * @return string path to file
436
     */
437
    abstract protected function getChangelogFilename();
438
439
    /**
440
     * Returns path to current page/media
441
     *
442
     * @return string path to file
443
     */
444
    abstract protected function getFilename();
445
446
    /**
447
     * Get the changelog information for a specific page id and revision (timestamp)
448
     *
449
     * Adjacent changelog lines are optimistically parsed and cached to speed up
450
     * consecutive calls to getRevisionInfo. For large changelog files, only the chunk
451
     * containing the requested changelog line is read.
452
     *
453
     * @param int $rev        revision timestamp
454
     * @return bool|array false or array with entries:
455
     *      - date:  unix timestamp
456
     *      - ip:    IPv4 address (127.0.0.1)
457
     *      - type:  log line type
458
     *      - id:    page id
459
     *      - user:  user name
460
     *      - sum:   edit summary (or action reason)
461
     *      - extra: extra data (varies by line type)
462
     *
463
     * @author Ben Coburn <[email protected]>
464
     * @author Kate Arzamastseva <[email protected]>
465
     */
466
    public function getRevisionInfo($rev) {
467
        $rev = max($rev, 0);
468
469
        // check if it's already in the memory cache
470
        if(isset($this->cache[$this->id]) && isset($this->cache[$this->id][$rev])) {
471
            return $this->cache[$this->id][$rev];
472
        }
473
474
        //read lines from changelog
475
        list($fp, $lines) = $this->readloglines($rev);
476
        if($fp) {
477
            fclose($fp);
478
        }
479
        if(empty($lines)) return false;
480
481
        // parse and cache changelog lines
482
        foreach($lines as $value) {
483
            $tmp = parseChangelogLine($value);
484
            if($tmp !== false) {
485
                $this->cache[$this->id][$tmp['date']] = $tmp;
486
            }
487
        }
488
        if(!isset($this->cache[$this->id][$rev])) {
489
            return false;
490
        }
491
        return $this->cache[$this->id][$rev];
492
    }
493
494
    /**
495
     * Return a list of page revisions numbers
496
     *
497
     * Does not guarantee that the revision exists in the attic,
498
     * only that a line with the date exists in the changelog.
499
     * By default the current revision is skipped.
500
     *
501
     * The current revision is automatically skipped when the page exists.
502
     * See $INFO['meta']['last_change'] for the current revision.
503
     * A negative $first let read the current revision too.
504
     *
505
     * For efficiency, the log lines are parsed and cached for later
506
     * calls to getRevisionInfo. Large changelog files are read
507
     * backwards in chunks until the requested number of changelog
508
     * lines are recieved.
509
     *
510
     * @param int $first      skip the first n changelog lines
511
     * @param int $num        number of revisions to return
512
     * @return array with the revision timestamps
513
     *
514
     * @author Ben Coburn <[email protected]>
515
     * @author Kate Arzamastseva <[email protected]>
516
     */
517
    public function getRevisions($first, $num) {
518
        $revs = array();
519
        $lines = array();
520
        $count = 0;
521
522
        $num = max($num, 0);
523
        if($num == 0) {
524
            return $revs;
525
        }
526
527
        if($first < 0) {
528
            $first = 0;
529
        } else if(file_exists($this->getFilename())) {
530
            // skip current revision if the page exists
531
            $first = max($first + 1, 0);
532
        }
533
534
        $file = $this->getChangelogFilename();
535
536
        if(!file_exists($file)) {
537
            return $revs;
538
        }
539
        if(filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
540
            // read whole file
541
            $lines = file($file);
542
            if($lines === false) {
543
                return $revs;
544
            }
545
        } else {
546
            // read chunks backwards
547
            $fp = fopen($file, 'rb'); // "file pointer"
548
            if($fp === false) {
549
                return $revs;
550
            }
551
            fseek($fp, 0, SEEK_END);
552
            $tail = ftell($fp);
553
554
            // chunk backwards
555
            $finger = max($tail - $this->chunk_size, 0);
556
            while($count < $num + $first) {
557
                $nl = $this->getNewlinepointer($fp, $finger);
558
559
                // was the chunk big enough? if not, take another bite
560
                if($nl > 0 && $tail <= $nl) {
561
                    $finger = max($finger - $this->chunk_size, 0);
562
                    continue;
563
                } else {
564
                    $finger = $nl;
565
                }
566
567
                // read chunk
568
                $chunk = '';
569
                $read_size = max($tail - $finger, 0); // found chunk size
570
                $got = 0;
571
                while($got < $read_size && !feof($fp)) {
572
                    $tmp = @fread($fp, max(min($this->chunk_size, $read_size - $got), 0));
573
                    if($tmp === false) {
574
                        break;
575
                    } //error state
576
                    $got += strlen($tmp);
577
                    $chunk .= $tmp;
578
                }
579
                $tmp = explode("\n", $chunk);
580
                array_pop($tmp); // remove trailing newline
581
582
                // combine with previous chunk
583
                $count += count($tmp);
584
                $lines = array_merge($tmp, $lines);
585
586
                // next chunk
587
                if($finger == 0) {
588
                    break;
589
                } // already read all the lines
590
                else {
591
                    $tail = $finger;
592
                    $finger = max($tail - $this->chunk_size, 0);
593
                }
594
            }
595
            fclose($fp);
596
        }
597
598
        // skip parsing extra lines
599
        $num = max(min(count($lines) - $first, $num), 0);
600
        if     ($first > 0 && $num > 0)  { $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num); }
601
        else if($first > 0 && $num == 0) { $lines = array_slice($lines, 0, max(count($lines) - $first, 0)); }
602
        else if($first == 0 && $num > 0) { $lines = array_slice($lines, max(count($lines) - $num, 0)); }
603
604
        // handle lines in reverse order
605
        for($i = count($lines) - 1; $i >= 0; $i--) {
606
            $tmp = parseChangelogLine($lines[$i]);
607
            if($tmp !== false) {
608
                $this->cache[$this->id][$tmp['date']] = $tmp;
609
                $revs[] = $tmp['date'];
610
            }
611
        }
612
613
        return $revs;
614
    }
615
616
    /**
617
     * Get the nth revision left or right handside  for a specific page id and revision (timestamp)
618
     *
619
     * For large changelog files, only the chunk containing the
620
     * reference revision $rev is read and sometimes a next chunck.
621
     *
622
     * Adjacent changelog lines are optimistically parsed and cached to speed up
623
     * consecutive calls to getRevisionInfo.
624
     *
625
     * @param int $rev        revision timestamp used as startdate (doesn't need to be revisionnumber)
626
     * @param int $direction  give position of returned revision with respect to $rev; positive=next, negative=prev
627
     * @return bool|int
628
     *      timestamp of the requested revision
629
     *      otherwise false
630
     */
631
    public function getRelativeRevision($rev, $direction) {
632
        $rev = max($rev, 0);
633
        $direction = (int) $direction;
634
635
        //no direction given or last rev, so no follow-up
636
        if(!$direction || ($direction > 0 && $this->isCurrentRevision($rev))) {
637
            return false;
638
        }
639
640
        //get lines from changelog
641
        list($fp, $lines, $head, $tail, $eof) = $this->readloglines($rev);
642
        if(empty($lines)) return false;
643
644
        // look for revisions later/earlier then $rev, when founded count till the wanted revision is reached
645
        // also parse and cache changelog lines for getRevisionInfo().
646
        $revcounter = 0;
647
        $relativerev = false;
648
        $checkotherchunck = true; //always runs once
649
        while(!$relativerev && $checkotherchunck) {
650
            $tmp = array();
651
            //parse in normal or reverse order
652
            $count = count($lines);
653
            if($direction > 0) {
654
                $start = 0;
655
                $step = 1;
656
            } else {
657
                $start = $count - 1;
658
                $step = -1;
659
            }
660
            for($i = $start; $i >= 0 && $i < $count; $i = $i + $step) {
661
                $tmp = parseChangelogLine($lines[$i]);
662
                if($tmp !== false) {
663
                    $this->cache[$this->id][$tmp['date']] = $tmp;
664
                    //look for revs older/earlier then reference $rev and select $direction-th one
665
                    if(($direction > 0 && $tmp['date'] > $rev) || ($direction < 0 && $tmp['date'] < $rev)) {
666
                        $revcounter++;
667
                        if($revcounter == abs($direction)) {
668
                            $relativerev = $tmp['date'];
669
                        }
670
                    }
671
                }
672
            }
673
674
            //true when $rev is found, but not the wanted follow-up.
675
            $checkotherchunck = $fp
676
                && ($tmp['date'] == $rev || ($revcounter > 0 && !$relativerev))
677
                && !(($tail == $eof && $direction > 0) || ($head == 0 && $direction < 0));
678
679
            if($checkotherchunck) {
680
                list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, $direction);
681
682
                if(empty($lines)) break;
683
            }
684
        }
685
        if($fp) {
686
            fclose($fp);
687
        }
688
689
        return $relativerev;
690
    }
691
692
    /**
693
     * Returns revisions around rev1 and rev2
694
     * When available it returns $max entries for each revision
695
     *
696
     * @param int $rev1 oldest revision timestamp
697
     * @param int $rev2 newest revision timestamp (0 looks up last revision)
698
     * @param int $max maximum number of revisions returned
699
     * @return array with two arrays with revisions surrounding rev1 respectively rev2
700
     */
701
    public function getRevisionsAround($rev1, $rev2, $max = 50) {
702
        $max = floor(abs($max) / 2)*2 + 1;
703
        $rev1 = max($rev1, 0);
704
        $rev2 = max($rev2, 0);
705
706
        if($rev2) {
707
            if($rev2 < $rev1) {
708
                $rev = $rev2;
709
                $rev2 = $rev1;
710
                $rev1 = $rev;
711
            }
712
        } else {
713
            //empty right side means a removed page. Look up last revision.
714
            $revs = $this->getRevisions(-1, 1);
715
            $rev2 = $revs[0];
716
        }
717
        //collect revisions around rev2
718
        list($revs2, $allrevs, $fp, $lines, $head, $tail) = $this->retrieveRevisionsAround($rev2, $max);
719
720
        if(empty($revs2)) return array(array(), array());
721
722
        //collect revisions around rev1
723
        $index = array_search($rev1, $allrevs);
724
        if($index === false) {
725
            //no overlapping revisions
726
            list($revs1,,,,,) = $this->retrieveRevisionsAround($rev1, $max);
727
            if(empty($revs1)) $revs1 = array();
728
        } else {
729
            //revisions overlaps, reuse revisions around rev2
730
            $revs1 = $allrevs;
731
            while($head > 0) {
732
                for($i = count($lines) - 1; $i >= 0; $i--) {
733
                    $tmp = parseChangelogLine($lines[$i]);
734
                    if($tmp !== false) {
735
                        $this->cache[$this->id][$tmp['date']] = $tmp;
736
                        $revs1[] = $tmp['date'];
737
                        $index++;
738
739
                        if($index > floor($max / 2)) break 2;
740
                    }
741
                }
742
743
                list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
744
            }
745
            sort($revs1);
746
            //return wanted selection
747
            $revs1 = array_slice($revs1, max($index - floor($max/2), 0), $max);
748
        }
749
750
        return array(array_reverse($revs1), array_reverse($revs2));
751
    }
752
753
    /**
754
     * Checks if the ID has old revisons
755
     * @return boolean
756
     */
757
    public function hasRevisions() {
758
        $file = $this->getChangelogFilename();
759
        return file_exists($file);
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