xref: /dokuwiki/inc/ChangeLog/ChangeLog.php (revision 0c3a5702735953748f68955d51bb478538fb6eda)
1*0c3a5702SAndreas Gohr<?php
2*0c3a5702SAndreas Gohr
3*0c3a5702SAndreas Gohrnamespace dokuwiki\ChangeLog;
4*0c3a5702SAndreas Gohr
5*0c3a5702SAndreas Gohr/**
6*0c3a5702SAndreas Gohr * methods for handling of changelog of pages or media files
7*0c3a5702SAndreas Gohr */
8*0c3a5702SAndreas Gohrabstract class ChangeLog
9*0c3a5702SAndreas Gohr{
10*0c3a5702SAndreas Gohr
11*0c3a5702SAndreas Gohr    /** @var string */
12*0c3a5702SAndreas Gohr    protected $id;
13*0c3a5702SAndreas Gohr    /** @var int */
14*0c3a5702SAndreas Gohr    protected $chunk_size;
15*0c3a5702SAndreas Gohr    /** @var array */
16*0c3a5702SAndreas Gohr    protected $cache;
17*0c3a5702SAndreas Gohr
18*0c3a5702SAndreas Gohr    /**
19*0c3a5702SAndreas Gohr     * Constructor
20*0c3a5702SAndreas Gohr     *
21*0c3a5702SAndreas Gohr     * @param string $id page id
22*0c3a5702SAndreas Gohr     * @param int $chunk_size maximum block size read from file
23*0c3a5702SAndreas Gohr     */
24*0c3a5702SAndreas Gohr    public function __construct($id, $chunk_size = 8192)
25*0c3a5702SAndreas Gohr    {
26*0c3a5702SAndreas Gohr        global $cache_revinfo;
27*0c3a5702SAndreas Gohr
28*0c3a5702SAndreas Gohr        $this->cache =& $cache_revinfo;
29*0c3a5702SAndreas Gohr        if (!isset($this->cache[$id])) {
30*0c3a5702SAndreas Gohr            $this->cache[$id] = array();
31*0c3a5702SAndreas Gohr        }
32*0c3a5702SAndreas Gohr
33*0c3a5702SAndreas Gohr        $this->id = $id;
34*0c3a5702SAndreas Gohr        $this->setChunkSize($chunk_size);
35*0c3a5702SAndreas Gohr
36*0c3a5702SAndreas Gohr    }
37*0c3a5702SAndreas Gohr
38*0c3a5702SAndreas Gohr    /**
39*0c3a5702SAndreas Gohr     * Set chunk size for file reading
40*0c3a5702SAndreas Gohr     * Chunk size zero let read whole file at once
41*0c3a5702SAndreas Gohr     *
42*0c3a5702SAndreas Gohr     * @param int $chunk_size maximum block size read from file
43*0c3a5702SAndreas Gohr     */
44*0c3a5702SAndreas Gohr    public function setChunkSize($chunk_size)
45*0c3a5702SAndreas Gohr    {
46*0c3a5702SAndreas Gohr        if (!is_numeric($chunk_size)) $chunk_size = 0;
47*0c3a5702SAndreas Gohr
48*0c3a5702SAndreas Gohr        $this->chunk_size = (int)max($chunk_size, 0);
49*0c3a5702SAndreas Gohr    }
50*0c3a5702SAndreas Gohr
51*0c3a5702SAndreas Gohr    /**
52*0c3a5702SAndreas Gohr     * Returns path to changelog
53*0c3a5702SAndreas Gohr     *
54*0c3a5702SAndreas Gohr     * @return string path to file
55*0c3a5702SAndreas Gohr     */
56*0c3a5702SAndreas Gohr    abstract protected function getChangelogFilename();
57*0c3a5702SAndreas Gohr
58*0c3a5702SAndreas Gohr    /**
59*0c3a5702SAndreas Gohr     * Returns path to current page/media
60*0c3a5702SAndreas Gohr     *
61*0c3a5702SAndreas Gohr     * @return string path to file
62*0c3a5702SAndreas Gohr     */
63*0c3a5702SAndreas Gohr    abstract protected function getFilename();
64*0c3a5702SAndreas Gohr
65*0c3a5702SAndreas Gohr    /**
66*0c3a5702SAndreas Gohr     * Get the changelog information for a specific page id and revision (timestamp)
67*0c3a5702SAndreas Gohr     *
68*0c3a5702SAndreas Gohr     * Adjacent changelog lines are optimistically parsed and cached to speed up
69*0c3a5702SAndreas Gohr     * consecutive calls to getRevisionInfo. For large changelog files, only the chunk
70*0c3a5702SAndreas Gohr     * containing the requested changelog line is read.
71*0c3a5702SAndreas Gohr     *
72*0c3a5702SAndreas Gohr     * @param int $rev revision timestamp
73*0c3a5702SAndreas Gohr     * @return bool|array false or array with entries:
74*0c3a5702SAndreas Gohr     *      - date:  unix timestamp
75*0c3a5702SAndreas Gohr     *      - ip:    IPv4 address (127.0.0.1)
76*0c3a5702SAndreas Gohr     *      - type:  log line type
77*0c3a5702SAndreas Gohr     *      - id:    page id
78*0c3a5702SAndreas Gohr     *      - user:  user name
79*0c3a5702SAndreas Gohr     *      - sum:   edit summary (or action reason)
80*0c3a5702SAndreas Gohr     *      - extra: extra data (varies by line type)
81*0c3a5702SAndreas Gohr     *
82*0c3a5702SAndreas Gohr     * @author Ben Coburn <btcoburn@silicodon.net>
83*0c3a5702SAndreas Gohr     * @author Kate Arzamastseva <pshns@ukr.net>
84*0c3a5702SAndreas Gohr     */
85*0c3a5702SAndreas Gohr    public function getRevisionInfo($rev)
86*0c3a5702SAndreas Gohr    {
87*0c3a5702SAndreas Gohr        $rev = max($rev, 0);
88*0c3a5702SAndreas Gohr
89*0c3a5702SAndreas Gohr        // check if it's already in the memory cache
90*0c3a5702SAndreas Gohr        if (isset($this->cache[$this->id]) && isset($this->cache[$this->id][$rev])) {
91*0c3a5702SAndreas Gohr            return $this->cache[$this->id][$rev];
92*0c3a5702SAndreas Gohr        }
93*0c3a5702SAndreas Gohr
94*0c3a5702SAndreas Gohr        //read lines from changelog
95*0c3a5702SAndreas Gohr        list($fp, $lines) = $this->readloglines($rev);
96*0c3a5702SAndreas Gohr        if ($fp) {
97*0c3a5702SAndreas Gohr            fclose($fp);
98*0c3a5702SAndreas Gohr        }
99*0c3a5702SAndreas Gohr        if (empty($lines)) return false;
100*0c3a5702SAndreas Gohr
101*0c3a5702SAndreas Gohr        // parse and cache changelog lines
102*0c3a5702SAndreas Gohr        foreach ($lines as $value) {
103*0c3a5702SAndreas Gohr            $tmp = parseChangelogLine($value);
104*0c3a5702SAndreas Gohr            if ($tmp !== false) {
105*0c3a5702SAndreas Gohr                $this->cache[$this->id][$tmp['date']] = $tmp;
106*0c3a5702SAndreas Gohr            }
107*0c3a5702SAndreas Gohr        }
108*0c3a5702SAndreas Gohr        if (!isset($this->cache[$this->id][$rev])) {
109*0c3a5702SAndreas Gohr            return false;
110*0c3a5702SAndreas Gohr        }
111*0c3a5702SAndreas Gohr        return $this->cache[$this->id][$rev];
112*0c3a5702SAndreas Gohr    }
113*0c3a5702SAndreas Gohr
114*0c3a5702SAndreas Gohr    /**
115*0c3a5702SAndreas Gohr     * Return a list of page revisions numbers
116*0c3a5702SAndreas Gohr     *
117*0c3a5702SAndreas Gohr     * Does not guarantee that the revision exists in the attic,
118*0c3a5702SAndreas Gohr     * only that a line with the date exists in the changelog.
119*0c3a5702SAndreas Gohr     * By default the current revision is skipped.
120*0c3a5702SAndreas Gohr     *
121*0c3a5702SAndreas Gohr     * The current revision is automatically skipped when the page exists.
122*0c3a5702SAndreas Gohr     * See $INFO['meta']['last_change'] for the current revision.
123*0c3a5702SAndreas Gohr     * A negative $first let read the current revision too.
124*0c3a5702SAndreas Gohr     *
125*0c3a5702SAndreas Gohr     * For efficiency, the log lines are parsed and cached for later
126*0c3a5702SAndreas Gohr     * calls to getRevisionInfo. Large changelog files are read
127*0c3a5702SAndreas Gohr     * backwards in chunks until the requested number of changelog
128*0c3a5702SAndreas Gohr     * lines are recieved.
129*0c3a5702SAndreas Gohr     *
130*0c3a5702SAndreas Gohr     * @param int $first skip the first n changelog lines
131*0c3a5702SAndreas Gohr     * @param int $num number of revisions to return
132*0c3a5702SAndreas Gohr     * @return array with the revision timestamps
133*0c3a5702SAndreas Gohr     *
134*0c3a5702SAndreas Gohr     * @author Ben Coburn <btcoburn@silicodon.net>
135*0c3a5702SAndreas Gohr     * @author Kate Arzamastseva <pshns@ukr.net>
136*0c3a5702SAndreas Gohr     */
137*0c3a5702SAndreas Gohr    public function getRevisions($first, $num)
138*0c3a5702SAndreas Gohr    {
139*0c3a5702SAndreas Gohr        $revs = array();
140*0c3a5702SAndreas Gohr        $lines = array();
141*0c3a5702SAndreas Gohr        $count = 0;
142*0c3a5702SAndreas Gohr
143*0c3a5702SAndreas Gohr        $num = max($num, 0);
144*0c3a5702SAndreas Gohr        if ($num == 0) {
145*0c3a5702SAndreas Gohr            return $revs;
146*0c3a5702SAndreas Gohr        }
147*0c3a5702SAndreas Gohr
148*0c3a5702SAndreas Gohr        if ($first < 0) {
149*0c3a5702SAndreas Gohr            $first = 0;
150*0c3a5702SAndreas Gohr        } else {
151*0c3a5702SAndreas Gohr            if (file_exists($this->getFilename())) {
152*0c3a5702SAndreas Gohr                // skip current revision if the page exists
153*0c3a5702SAndreas Gohr                $first = max($first + 1, 0);
154*0c3a5702SAndreas Gohr            }
155*0c3a5702SAndreas Gohr        }
156*0c3a5702SAndreas Gohr
157*0c3a5702SAndreas Gohr        $file = $this->getChangelogFilename();
158*0c3a5702SAndreas Gohr
159*0c3a5702SAndreas Gohr        if (!file_exists($file)) {
160*0c3a5702SAndreas Gohr            return $revs;
161*0c3a5702SAndreas Gohr        }
162*0c3a5702SAndreas Gohr        if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
163*0c3a5702SAndreas Gohr            // read whole file
164*0c3a5702SAndreas Gohr            $lines = file($file);
165*0c3a5702SAndreas Gohr            if ($lines === false) {
166*0c3a5702SAndreas Gohr                return $revs;
167*0c3a5702SAndreas Gohr            }
168*0c3a5702SAndreas Gohr        } else {
169*0c3a5702SAndreas Gohr            // read chunks backwards
170*0c3a5702SAndreas Gohr            $fp = fopen($file, 'rb'); // "file pointer"
171*0c3a5702SAndreas Gohr            if ($fp === false) {
172*0c3a5702SAndreas Gohr                return $revs;
173*0c3a5702SAndreas Gohr            }
174*0c3a5702SAndreas Gohr            fseek($fp, 0, SEEK_END);
175*0c3a5702SAndreas Gohr            $tail = ftell($fp);
176*0c3a5702SAndreas Gohr
177*0c3a5702SAndreas Gohr            // chunk backwards
178*0c3a5702SAndreas Gohr            $finger = max($tail - $this->chunk_size, 0);
179*0c3a5702SAndreas Gohr            while ($count < $num + $first) {
180*0c3a5702SAndreas Gohr                $nl = $this->getNewlinepointer($fp, $finger);
181*0c3a5702SAndreas Gohr
182*0c3a5702SAndreas Gohr                // was the chunk big enough? if not, take another bite
183*0c3a5702SAndreas Gohr                if ($nl > 0 && $tail <= $nl) {
184*0c3a5702SAndreas Gohr                    $finger = max($finger - $this->chunk_size, 0);
185*0c3a5702SAndreas Gohr                    continue;
186*0c3a5702SAndreas Gohr                } else {
187*0c3a5702SAndreas Gohr                    $finger = $nl;
188*0c3a5702SAndreas Gohr                }
189*0c3a5702SAndreas Gohr
190*0c3a5702SAndreas Gohr                // read chunk
191*0c3a5702SAndreas Gohr                $chunk = '';
192*0c3a5702SAndreas Gohr                $read_size = max($tail - $finger, 0); // found chunk size
193*0c3a5702SAndreas Gohr                $got = 0;
194*0c3a5702SAndreas Gohr                while ($got < $read_size && !feof($fp)) {
195*0c3a5702SAndreas Gohr                    $tmp = @fread($fp, max(min($this->chunk_size, $read_size - $got), 0));
196*0c3a5702SAndreas Gohr                    if ($tmp === false) {
197*0c3a5702SAndreas Gohr                        break;
198*0c3a5702SAndreas Gohr                    } //error state
199*0c3a5702SAndreas Gohr                    $got += strlen($tmp);
200*0c3a5702SAndreas Gohr                    $chunk .= $tmp;
201*0c3a5702SAndreas Gohr                }
202*0c3a5702SAndreas Gohr                $tmp = explode("\n", $chunk);
203*0c3a5702SAndreas Gohr                array_pop($tmp); // remove trailing newline
204*0c3a5702SAndreas Gohr
205*0c3a5702SAndreas Gohr                // combine with previous chunk
206*0c3a5702SAndreas Gohr                $count += count($tmp);
207*0c3a5702SAndreas Gohr                $lines = array_merge($tmp, $lines);
208*0c3a5702SAndreas Gohr
209*0c3a5702SAndreas Gohr                // next chunk
210*0c3a5702SAndreas Gohr                if ($finger == 0) {
211*0c3a5702SAndreas Gohr                    break;
212*0c3a5702SAndreas Gohr                } // already read all the lines
213*0c3a5702SAndreas Gohr                else {
214*0c3a5702SAndreas Gohr                    $tail = $finger;
215*0c3a5702SAndreas Gohr                    $finger = max($tail - $this->chunk_size, 0);
216*0c3a5702SAndreas Gohr                }
217*0c3a5702SAndreas Gohr            }
218*0c3a5702SAndreas Gohr            fclose($fp);
219*0c3a5702SAndreas Gohr        }
220*0c3a5702SAndreas Gohr
221*0c3a5702SAndreas Gohr        // skip parsing extra lines
222*0c3a5702SAndreas Gohr        $num = max(min(count($lines) - $first, $num), 0);
223*0c3a5702SAndreas Gohr        if ($first > 0 && $num > 0) {
224*0c3a5702SAndreas Gohr            $lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num);
225*0c3a5702SAndreas Gohr        } else {
226*0c3a5702SAndreas Gohr            if ($first > 0 && $num == 0) {
227*0c3a5702SAndreas Gohr                $lines = array_slice($lines, 0, max(count($lines) - $first, 0));
228*0c3a5702SAndreas Gohr            } elseif ($first == 0 && $num > 0) {
229*0c3a5702SAndreas Gohr                $lines = array_slice($lines, max(count($lines) - $num, 0));
230*0c3a5702SAndreas Gohr            }
231*0c3a5702SAndreas Gohr        }
232*0c3a5702SAndreas Gohr
233*0c3a5702SAndreas Gohr        // handle lines in reverse order
234*0c3a5702SAndreas Gohr        for ($i = count($lines) - 1; $i >= 0; $i--) {
235*0c3a5702SAndreas Gohr            $tmp = parseChangelogLine($lines[$i]);
236*0c3a5702SAndreas Gohr            if ($tmp !== false) {
237*0c3a5702SAndreas Gohr                $this->cache[$this->id][$tmp['date']] = $tmp;
238*0c3a5702SAndreas Gohr                $revs[] = $tmp['date'];
239*0c3a5702SAndreas Gohr            }
240*0c3a5702SAndreas Gohr        }
241*0c3a5702SAndreas Gohr
242*0c3a5702SAndreas Gohr        return $revs;
243*0c3a5702SAndreas Gohr    }
244*0c3a5702SAndreas Gohr
245*0c3a5702SAndreas Gohr    /**
246*0c3a5702SAndreas Gohr     * Get the nth revision left or right handside  for a specific page id and revision (timestamp)
247*0c3a5702SAndreas Gohr     *
248*0c3a5702SAndreas Gohr     * For large changelog files, only the chunk containing the
249*0c3a5702SAndreas Gohr     * reference revision $rev is read and sometimes a next chunck.
250*0c3a5702SAndreas Gohr     *
251*0c3a5702SAndreas Gohr     * Adjacent changelog lines are optimistically parsed and cached to speed up
252*0c3a5702SAndreas Gohr     * consecutive calls to getRevisionInfo.
253*0c3a5702SAndreas Gohr     *
254*0c3a5702SAndreas Gohr     * @param int $rev revision timestamp used as startdate (doesn't need to be revisionnumber)
255*0c3a5702SAndreas Gohr     * @param int $direction give position of returned revision with respect to $rev; positive=next, negative=prev
256*0c3a5702SAndreas Gohr     * @return bool|int
257*0c3a5702SAndreas Gohr     *      timestamp of the requested revision
258*0c3a5702SAndreas Gohr     *      otherwise false
259*0c3a5702SAndreas Gohr     */
260*0c3a5702SAndreas Gohr    public function getRelativeRevision($rev, $direction)
261*0c3a5702SAndreas Gohr    {
262*0c3a5702SAndreas Gohr        $rev = max($rev, 0);
263*0c3a5702SAndreas Gohr        $direction = (int)$direction;
264*0c3a5702SAndreas Gohr
265*0c3a5702SAndreas Gohr        //no direction given or last rev, so no follow-up
266*0c3a5702SAndreas Gohr        if (!$direction || ($direction > 0 && $this->isCurrentRevision($rev))) {
267*0c3a5702SAndreas Gohr            return false;
268*0c3a5702SAndreas Gohr        }
269*0c3a5702SAndreas Gohr
270*0c3a5702SAndreas Gohr        //get lines from changelog
271*0c3a5702SAndreas Gohr        list($fp, $lines, $head, $tail, $eof) = $this->readloglines($rev);
272*0c3a5702SAndreas Gohr        if (empty($lines)) return false;
273*0c3a5702SAndreas Gohr
274*0c3a5702SAndreas Gohr        // look for revisions later/earlier then $rev, when founded count till the wanted revision is reached
275*0c3a5702SAndreas Gohr        // also parse and cache changelog lines for getRevisionInfo().
276*0c3a5702SAndreas Gohr        $revcounter = 0;
277*0c3a5702SAndreas Gohr        $relativerev = false;
278*0c3a5702SAndreas Gohr        $checkotherchunck = true; //always runs once
279*0c3a5702SAndreas Gohr        while (!$relativerev && $checkotherchunck) {
280*0c3a5702SAndreas Gohr            $tmp = array();
281*0c3a5702SAndreas Gohr            //parse in normal or reverse order
282*0c3a5702SAndreas Gohr            $count = count($lines);
283*0c3a5702SAndreas Gohr            if ($direction > 0) {
284*0c3a5702SAndreas Gohr                $start = 0;
285*0c3a5702SAndreas Gohr                $step = 1;
286*0c3a5702SAndreas Gohr            } else {
287*0c3a5702SAndreas Gohr                $start = $count - 1;
288*0c3a5702SAndreas Gohr                $step = -1;
289*0c3a5702SAndreas Gohr            }
290*0c3a5702SAndreas Gohr            for ($i = $start; $i >= 0 && $i < $count; $i = $i + $step) {
291*0c3a5702SAndreas Gohr                $tmp = parseChangelogLine($lines[$i]);
292*0c3a5702SAndreas Gohr                if ($tmp !== false) {
293*0c3a5702SAndreas Gohr                    $this->cache[$this->id][$tmp['date']] = $tmp;
294*0c3a5702SAndreas Gohr                    //look for revs older/earlier then reference $rev and select $direction-th one
295*0c3a5702SAndreas Gohr                    if (($direction > 0 && $tmp['date'] > $rev) || ($direction < 0 && $tmp['date'] < $rev)) {
296*0c3a5702SAndreas Gohr                        $revcounter++;
297*0c3a5702SAndreas Gohr                        if ($revcounter == abs($direction)) {
298*0c3a5702SAndreas Gohr                            $relativerev = $tmp['date'];
299*0c3a5702SAndreas Gohr                        }
300*0c3a5702SAndreas Gohr                    }
301*0c3a5702SAndreas Gohr                }
302*0c3a5702SAndreas Gohr            }
303*0c3a5702SAndreas Gohr
304*0c3a5702SAndreas Gohr            //true when $rev is found, but not the wanted follow-up.
305*0c3a5702SAndreas Gohr            $checkotherchunck = $fp
306*0c3a5702SAndreas Gohr                && ($tmp['date'] == $rev || ($revcounter > 0 && !$relativerev))
307*0c3a5702SAndreas Gohr                && !(($tail == $eof && $direction > 0) || ($head == 0 && $direction < 0));
308*0c3a5702SAndreas Gohr
309*0c3a5702SAndreas Gohr            if ($checkotherchunck) {
310*0c3a5702SAndreas Gohr                list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, $direction);
311*0c3a5702SAndreas Gohr
312*0c3a5702SAndreas Gohr                if (empty($lines)) break;
313*0c3a5702SAndreas Gohr            }
314*0c3a5702SAndreas Gohr        }
315*0c3a5702SAndreas Gohr        if ($fp) {
316*0c3a5702SAndreas Gohr            fclose($fp);
317*0c3a5702SAndreas Gohr        }
318*0c3a5702SAndreas Gohr
319*0c3a5702SAndreas Gohr        return $relativerev;
320*0c3a5702SAndreas Gohr    }
321*0c3a5702SAndreas Gohr
322*0c3a5702SAndreas Gohr    /**
323*0c3a5702SAndreas Gohr     * Returns revisions around rev1 and rev2
324*0c3a5702SAndreas Gohr     * When available it returns $max entries for each revision
325*0c3a5702SAndreas Gohr     *
326*0c3a5702SAndreas Gohr     * @param int $rev1 oldest revision timestamp
327*0c3a5702SAndreas Gohr     * @param int $rev2 newest revision timestamp (0 looks up last revision)
328*0c3a5702SAndreas Gohr     * @param int $max maximum number of revisions returned
329*0c3a5702SAndreas Gohr     * @return array with two arrays with revisions surrounding rev1 respectively rev2
330*0c3a5702SAndreas Gohr     */
331*0c3a5702SAndreas Gohr    public function getRevisionsAround($rev1, $rev2, $max = 50)
332*0c3a5702SAndreas Gohr    {
333*0c3a5702SAndreas Gohr        $max = floor(abs($max) / 2) * 2 + 1;
334*0c3a5702SAndreas Gohr        $rev1 = max($rev1, 0);
335*0c3a5702SAndreas Gohr        $rev2 = max($rev2, 0);
336*0c3a5702SAndreas Gohr
337*0c3a5702SAndreas Gohr        if ($rev2) {
338*0c3a5702SAndreas Gohr            if ($rev2 < $rev1) {
339*0c3a5702SAndreas Gohr                $rev = $rev2;
340*0c3a5702SAndreas Gohr                $rev2 = $rev1;
341*0c3a5702SAndreas Gohr                $rev1 = $rev;
342*0c3a5702SAndreas Gohr            }
343*0c3a5702SAndreas Gohr        } else {
344*0c3a5702SAndreas Gohr            //empty right side means a removed page. Look up last revision.
345*0c3a5702SAndreas Gohr            $revs = $this->getRevisions(-1, 1);
346*0c3a5702SAndreas Gohr            $rev2 = $revs[0];
347*0c3a5702SAndreas Gohr        }
348*0c3a5702SAndreas Gohr        //collect revisions around rev2
349*0c3a5702SAndreas Gohr        list($revs2, $allrevs, $fp, $lines, $head, $tail) = $this->retrieveRevisionsAround($rev2, $max);
350*0c3a5702SAndreas Gohr
351*0c3a5702SAndreas Gohr        if (empty($revs2)) return array(array(), array());
352*0c3a5702SAndreas Gohr
353*0c3a5702SAndreas Gohr        //collect revisions around rev1
354*0c3a5702SAndreas Gohr        $index = array_search($rev1, $allrevs);
355*0c3a5702SAndreas Gohr        if ($index === false) {
356*0c3a5702SAndreas Gohr            //no overlapping revisions
357*0c3a5702SAndreas Gohr            list($revs1, , , , ,) = $this->retrieveRevisionsAround($rev1, $max);
358*0c3a5702SAndreas Gohr            if (empty($revs1)) $revs1 = array();
359*0c3a5702SAndreas Gohr        } else {
360*0c3a5702SAndreas Gohr            //revisions overlaps, reuse revisions around rev2
361*0c3a5702SAndreas Gohr            $revs1 = $allrevs;
362*0c3a5702SAndreas Gohr            while ($head > 0) {
363*0c3a5702SAndreas Gohr                for ($i = count($lines) - 1; $i >= 0; $i--) {
364*0c3a5702SAndreas Gohr                    $tmp = parseChangelogLine($lines[$i]);
365*0c3a5702SAndreas Gohr                    if ($tmp !== false) {
366*0c3a5702SAndreas Gohr                        $this->cache[$this->id][$tmp['date']] = $tmp;
367*0c3a5702SAndreas Gohr                        $revs1[] = $tmp['date'];
368*0c3a5702SAndreas Gohr                        $index++;
369*0c3a5702SAndreas Gohr
370*0c3a5702SAndreas Gohr                        if ($index > floor($max / 2)) break 2;
371*0c3a5702SAndreas Gohr                    }
372*0c3a5702SAndreas Gohr                }
373*0c3a5702SAndreas Gohr
374*0c3a5702SAndreas Gohr                list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
375*0c3a5702SAndreas Gohr            }
376*0c3a5702SAndreas Gohr            sort($revs1);
377*0c3a5702SAndreas Gohr            //return wanted selection
378*0c3a5702SAndreas Gohr            $revs1 = array_slice($revs1, max($index - floor($max / 2), 0), $max);
379*0c3a5702SAndreas Gohr        }
380*0c3a5702SAndreas Gohr
381*0c3a5702SAndreas Gohr        return array(array_reverse($revs1), array_reverse($revs2));
382*0c3a5702SAndreas Gohr    }
383*0c3a5702SAndreas Gohr
384*0c3a5702SAndreas Gohr    /**
385*0c3a5702SAndreas Gohr     * Returns lines from changelog.
386*0c3a5702SAndreas Gohr     * If file larger than $chuncksize, only chunck is read that could contain $rev.
387*0c3a5702SAndreas Gohr     *
388*0c3a5702SAndreas Gohr     * @param int $rev revision timestamp
389*0c3a5702SAndreas Gohr     * @return array|false
390*0c3a5702SAndreas Gohr     *     if success returns array(fp, array(changeloglines), $head, $tail, $eof)
391*0c3a5702SAndreas Gohr     *     where fp only defined for chuck reading, needs closing.
392*0c3a5702SAndreas Gohr     *     otherwise false
393*0c3a5702SAndreas Gohr     */
394*0c3a5702SAndreas Gohr    protected function readloglines($rev)
395*0c3a5702SAndreas Gohr    {
396*0c3a5702SAndreas Gohr        $file = $this->getChangelogFilename();
397*0c3a5702SAndreas Gohr
398*0c3a5702SAndreas Gohr        if (!file_exists($file)) {
399*0c3a5702SAndreas Gohr            return false;
400*0c3a5702SAndreas Gohr        }
401*0c3a5702SAndreas Gohr
402*0c3a5702SAndreas Gohr        $fp = null;
403*0c3a5702SAndreas Gohr        $head = 0;
404*0c3a5702SAndreas Gohr        $tail = 0;
405*0c3a5702SAndreas Gohr        $eof = 0;
406*0c3a5702SAndreas Gohr
407*0c3a5702SAndreas Gohr        if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
408*0c3a5702SAndreas Gohr            // read whole file
409*0c3a5702SAndreas Gohr            $lines = file($file);
410*0c3a5702SAndreas Gohr            if ($lines === false) {
411*0c3a5702SAndreas Gohr                return false;
412*0c3a5702SAndreas Gohr            }
413*0c3a5702SAndreas Gohr        } else {
414*0c3a5702SAndreas Gohr            // read by chunk
415*0c3a5702SAndreas Gohr            $fp = fopen($file, 'rb'); // "file pointer"
416*0c3a5702SAndreas Gohr            if ($fp === false) {
417*0c3a5702SAndreas Gohr                return false;
418*0c3a5702SAndreas Gohr            }
419*0c3a5702SAndreas Gohr            $head = 0;
420*0c3a5702SAndreas Gohr            fseek($fp, 0, SEEK_END);
421*0c3a5702SAndreas Gohr            $eof = ftell($fp);
422*0c3a5702SAndreas Gohr            $tail = $eof;
423*0c3a5702SAndreas Gohr
424*0c3a5702SAndreas Gohr            // find chunk
425*0c3a5702SAndreas Gohr            while ($tail - $head > $this->chunk_size) {
426*0c3a5702SAndreas Gohr                $finger = $head + floor(($tail - $head) / 2.0);
427*0c3a5702SAndreas Gohr                $finger = $this->getNewlinepointer($fp, $finger);
428*0c3a5702SAndreas Gohr                $tmp = fgets($fp);
429*0c3a5702SAndreas Gohr                if ($finger == $head || $finger == $tail) {
430*0c3a5702SAndreas Gohr                    break;
431*0c3a5702SAndreas Gohr                }
432*0c3a5702SAndreas Gohr                $tmp = parseChangelogLine($tmp);
433*0c3a5702SAndreas Gohr                $finger_rev = $tmp['date'];
434*0c3a5702SAndreas Gohr
435*0c3a5702SAndreas Gohr                if ($finger_rev > $rev) {
436*0c3a5702SAndreas Gohr                    $tail = $finger;
437*0c3a5702SAndreas Gohr                } else {
438*0c3a5702SAndreas Gohr                    $head = $finger;
439*0c3a5702SAndreas Gohr                }
440*0c3a5702SAndreas Gohr            }
441*0c3a5702SAndreas Gohr
442*0c3a5702SAndreas Gohr            if ($tail - $head < 1) {
443*0c3a5702SAndreas Gohr                // cound not find chunk, assume requested rev is missing
444*0c3a5702SAndreas Gohr                fclose($fp);
445*0c3a5702SAndreas Gohr                return false;
446*0c3a5702SAndreas Gohr            }
447*0c3a5702SAndreas Gohr
448*0c3a5702SAndreas Gohr            $lines = $this->readChunk($fp, $head, $tail);
449*0c3a5702SAndreas Gohr        }
450*0c3a5702SAndreas Gohr        return array(
451*0c3a5702SAndreas Gohr            $fp,
452*0c3a5702SAndreas Gohr            $lines,
453*0c3a5702SAndreas Gohr            $head,
454*0c3a5702SAndreas Gohr            $tail,
455*0c3a5702SAndreas Gohr            $eof,
456*0c3a5702SAndreas Gohr        );
457*0c3a5702SAndreas Gohr    }
458*0c3a5702SAndreas Gohr
459*0c3a5702SAndreas Gohr    /**
460*0c3a5702SAndreas Gohr     * Read chunk and return array with lines of given chunck.
461*0c3a5702SAndreas Gohr     * Has no check if $head and $tail are really at a new line
462*0c3a5702SAndreas Gohr     *
463*0c3a5702SAndreas Gohr     * @param resource $fp resource filepointer
464*0c3a5702SAndreas Gohr     * @param int $head start point chunck
465*0c3a5702SAndreas Gohr     * @param int $tail end point chunck
466*0c3a5702SAndreas Gohr     * @return array lines read from chunck
467*0c3a5702SAndreas Gohr     */
468*0c3a5702SAndreas Gohr    protected function readChunk($fp, $head, $tail)
469*0c3a5702SAndreas Gohr    {
470*0c3a5702SAndreas Gohr        $chunk = '';
471*0c3a5702SAndreas Gohr        $chunk_size = max($tail - $head, 0); // found chunk size
472*0c3a5702SAndreas Gohr        $got = 0;
473*0c3a5702SAndreas Gohr        fseek($fp, $head);
474*0c3a5702SAndreas Gohr        while ($got < $chunk_size && !feof($fp)) {
475*0c3a5702SAndreas Gohr            $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0));
476*0c3a5702SAndreas Gohr            if ($tmp === false) { //error state
477*0c3a5702SAndreas Gohr                break;
478*0c3a5702SAndreas Gohr            }
479*0c3a5702SAndreas Gohr            $got += strlen($tmp);
480*0c3a5702SAndreas Gohr            $chunk .= $tmp;
481*0c3a5702SAndreas Gohr        }
482*0c3a5702SAndreas Gohr        $lines = explode("\n", $chunk);
483*0c3a5702SAndreas Gohr        array_pop($lines); // remove trailing newline
484*0c3a5702SAndreas Gohr        return $lines;
485*0c3a5702SAndreas Gohr    }
486*0c3a5702SAndreas Gohr
487*0c3a5702SAndreas Gohr    /**
488*0c3a5702SAndreas Gohr     * Set pointer to first new line after $finger and return its position
489*0c3a5702SAndreas Gohr     *
490*0c3a5702SAndreas Gohr     * @param resource $fp filepointer
491*0c3a5702SAndreas Gohr     * @param int $finger a pointer
492*0c3a5702SAndreas Gohr     * @return int pointer
493*0c3a5702SAndreas Gohr     */
494*0c3a5702SAndreas Gohr    protected function getNewlinepointer($fp, $finger)
495*0c3a5702SAndreas Gohr    {
496*0c3a5702SAndreas Gohr        fseek($fp, $finger);
497*0c3a5702SAndreas Gohr        $nl = $finger;
498*0c3a5702SAndreas Gohr        if ($finger > 0) {
499*0c3a5702SAndreas Gohr            fgets($fp); // slip the finger forward to a new line
500*0c3a5702SAndreas Gohr            $nl = ftell($fp);
501*0c3a5702SAndreas Gohr        }
502*0c3a5702SAndreas Gohr        return $nl;
503*0c3a5702SAndreas Gohr    }
504*0c3a5702SAndreas Gohr
505*0c3a5702SAndreas Gohr    /**
506*0c3a5702SAndreas Gohr     * Check whether given revision is the current page
507*0c3a5702SAndreas Gohr     *
508*0c3a5702SAndreas Gohr     * @param int $rev timestamp of current page
509*0c3a5702SAndreas Gohr     * @return bool true if $rev is current revision, otherwise false
510*0c3a5702SAndreas Gohr     */
511*0c3a5702SAndreas Gohr    public function isCurrentRevision($rev)
512*0c3a5702SAndreas Gohr    {
513*0c3a5702SAndreas Gohr        return $rev == @filemtime($this->getFilename());
514*0c3a5702SAndreas Gohr    }
515*0c3a5702SAndreas Gohr
516*0c3a5702SAndreas Gohr    /**
517*0c3a5702SAndreas Gohr     * Return an existing revision for a specific date which is
518*0c3a5702SAndreas Gohr     * the current one or younger or equal then the date
519*0c3a5702SAndreas Gohr     *
520*0c3a5702SAndreas Gohr     * @param number $date_at timestamp
521*0c3a5702SAndreas Gohr     * @return string revision ('' for current)
522*0c3a5702SAndreas Gohr     */
523*0c3a5702SAndreas Gohr    public function getLastRevisionAt($date_at)
524*0c3a5702SAndreas Gohr    {
525*0c3a5702SAndreas Gohr        //requested date_at(timestamp) younger or equal then modified_time($this->id) => load current
526*0c3a5702SAndreas Gohr        if (file_exists($this->getFilename()) && $date_at >= @filemtime($this->getFilename())) {
527*0c3a5702SAndreas Gohr            return '';
528*0c3a5702SAndreas Gohr        } else {
529*0c3a5702SAndreas Gohr            if ($rev = $this->getRelativeRevision($date_at + 1, -1)) { //+1 to get also the requested date revision
530*0c3a5702SAndreas Gohr                return $rev;
531*0c3a5702SAndreas Gohr            } else {
532*0c3a5702SAndreas Gohr                return false;
533*0c3a5702SAndreas Gohr            }
534*0c3a5702SAndreas Gohr        }
535*0c3a5702SAndreas Gohr    }
536*0c3a5702SAndreas Gohr
537*0c3a5702SAndreas Gohr    /**
538*0c3a5702SAndreas Gohr     * Returns the next lines of the changelog  of the chunck before head or after tail
539*0c3a5702SAndreas Gohr     *
540*0c3a5702SAndreas Gohr     * @param resource $fp filepointer
541*0c3a5702SAndreas Gohr     * @param int $head position head of last chunk
542*0c3a5702SAndreas Gohr     * @param int $tail position tail of last chunk
543*0c3a5702SAndreas Gohr     * @param int $direction positive forward, negative backward
544*0c3a5702SAndreas Gohr     * @return array with entries:
545*0c3a5702SAndreas Gohr     *    - $lines: changelog lines of readed chunk
546*0c3a5702SAndreas Gohr     *    - $head: head of chunk
547*0c3a5702SAndreas Gohr     *    - $tail: tail of chunk
548*0c3a5702SAndreas Gohr     */
549*0c3a5702SAndreas Gohr    protected function readAdjacentChunk($fp, $head, $tail, $direction)
550*0c3a5702SAndreas Gohr    {
551*0c3a5702SAndreas Gohr        if (!$fp) return array(array(), $head, $tail);
552*0c3a5702SAndreas Gohr
553*0c3a5702SAndreas Gohr        if ($direction > 0) {
554*0c3a5702SAndreas Gohr            //read forward
555*0c3a5702SAndreas Gohr            $head = $tail;
556*0c3a5702SAndreas Gohr            $tail = $head + floor($this->chunk_size * (2 / 3));
557*0c3a5702SAndreas Gohr            $tail = $this->getNewlinepointer($fp, $tail);
558*0c3a5702SAndreas Gohr        } else {
559*0c3a5702SAndreas Gohr            //read backward
560*0c3a5702SAndreas Gohr            $tail = $head;
561*0c3a5702SAndreas Gohr            $head = max($tail - $this->chunk_size, 0);
562*0c3a5702SAndreas Gohr            while (true) {
563*0c3a5702SAndreas Gohr                $nl = $this->getNewlinepointer($fp, $head);
564*0c3a5702SAndreas Gohr                // was the chunk big enough? if not, take another bite
565*0c3a5702SAndreas Gohr                if ($nl > 0 && $tail <= $nl) {
566*0c3a5702SAndreas Gohr                    $head = max($head - $this->chunk_size, 0);
567*0c3a5702SAndreas Gohr                } else {
568*0c3a5702SAndreas Gohr                    $head = $nl;
569*0c3a5702SAndreas Gohr                    break;
570*0c3a5702SAndreas Gohr                }
571*0c3a5702SAndreas Gohr            }
572*0c3a5702SAndreas Gohr        }
573*0c3a5702SAndreas Gohr
574*0c3a5702SAndreas Gohr        //load next chunck
575*0c3a5702SAndreas Gohr        $lines = $this->readChunk($fp, $head, $tail);
576*0c3a5702SAndreas Gohr        return array($lines, $head, $tail);
577*0c3a5702SAndreas Gohr    }
578*0c3a5702SAndreas Gohr
579*0c3a5702SAndreas Gohr    /**
580*0c3a5702SAndreas Gohr     * Collect the $max revisions near to the timestamp $rev
581*0c3a5702SAndreas Gohr     *
582*0c3a5702SAndreas Gohr     * @param int $rev revision timestamp
583*0c3a5702SAndreas Gohr     * @param int $max maximum number of revisions to be returned
584*0c3a5702SAndreas Gohr     * @return bool|array
585*0c3a5702SAndreas Gohr     *     return array with entries:
586*0c3a5702SAndreas Gohr     *       - $requestedrevs: array of with $max revision timestamps
587*0c3a5702SAndreas Gohr     *       - $revs: all parsed revision timestamps
588*0c3a5702SAndreas Gohr     *       - $fp: filepointer only defined for chuck reading, needs closing.
589*0c3a5702SAndreas Gohr     *       - $lines: non-parsed changelog lines before the parsed revisions
590*0c3a5702SAndreas Gohr     *       - $head: position of first readed changelogline
591*0c3a5702SAndreas Gohr     *       - $lasttail: position of end of last readed changelogline
592*0c3a5702SAndreas Gohr     *     otherwise false
593*0c3a5702SAndreas Gohr     */
594*0c3a5702SAndreas Gohr    protected function retrieveRevisionsAround($rev, $max)
595*0c3a5702SAndreas Gohr    {
596*0c3a5702SAndreas Gohr        //get lines from changelog
597*0c3a5702SAndreas Gohr        list($fp, $lines, $starthead, $starttail, /* $eof */) = $this->readloglines($rev);
598*0c3a5702SAndreas Gohr        if (empty($lines)) return false;
599*0c3a5702SAndreas Gohr
600*0c3a5702SAndreas Gohr        //parse chunk containing $rev, and read forward more chunks until $max/2 is reached
601*0c3a5702SAndreas Gohr        $head = $starthead;
602*0c3a5702SAndreas Gohr        $tail = $starttail;
603*0c3a5702SAndreas Gohr        $revs = array();
604*0c3a5702SAndreas Gohr        $aftercount = $beforecount = 0;
605*0c3a5702SAndreas Gohr        while (count($lines) > 0) {
606*0c3a5702SAndreas Gohr            foreach ($lines as $line) {
607*0c3a5702SAndreas Gohr                $tmp = parseChangelogLine($line);
608*0c3a5702SAndreas Gohr                if ($tmp !== false) {
609*0c3a5702SAndreas Gohr                    $this->cache[$this->id][$tmp['date']] = $tmp;
610*0c3a5702SAndreas Gohr                    $revs[] = $tmp['date'];
611*0c3a5702SAndreas Gohr                    if ($tmp['date'] >= $rev) {
612*0c3a5702SAndreas Gohr                        //count revs after reference $rev
613*0c3a5702SAndreas Gohr                        $aftercount++;
614*0c3a5702SAndreas Gohr                        if ($aftercount == 1) $beforecount = count($revs);
615*0c3a5702SAndreas Gohr                    }
616*0c3a5702SAndreas Gohr                    //enough revs after reference $rev?
617*0c3a5702SAndreas Gohr                    if ($aftercount > floor($max / 2)) break 2;
618*0c3a5702SAndreas Gohr                }
619*0c3a5702SAndreas Gohr            }
620*0c3a5702SAndreas Gohr            //retrieve next chunk
621*0c3a5702SAndreas Gohr            list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, 1);
622*0c3a5702SAndreas Gohr        }
623*0c3a5702SAndreas Gohr        if ($aftercount == 0) return false;
624*0c3a5702SAndreas Gohr
625*0c3a5702SAndreas Gohr        $lasttail = $tail;
626*0c3a5702SAndreas Gohr
627*0c3a5702SAndreas Gohr        //read additional chuncks backward until $max/2 is reached and total number of revs is equal to $max
628*0c3a5702SAndreas Gohr        $lines = array();
629*0c3a5702SAndreas Gohr        $i = 0;
630*0c3a5702SAndreas Gohr        if ($aftercount > 0) {
631*0c3a5702SAndreas Gohr            $head = $starthead;
632*0c3a5702SAndreas Gohr            $tail = $starttail;
633*0c3a5702SAndreas Gohr            while ($head > 0) {
634*0c3a5702SAndreas Gohr                list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
635*0c3a5702SAndreas Gohr
636*0c3a5702SAndreas Gohr                for ($i = count($lines) - 1; $i >= 0; $i--) {
637*0c3a5702SAndreas Gohr                    $tmp = parseChangelogLine($lines[$i]);
638*0c3a5702SAndreas Gohr                    if ($tmp !== false) {
639*0c3a5702SAndreas Gohr                        $this->cache[$this->id][$tmp['date']] = $tmp;
640*0c3a5702SAndreas Gohr                        $revs[] = $tmp['date'];
641*0c3a5702SAndreas Gohr                        $beforecount++;
642*0c3a5702SAndreas Gohr                        //enough revs before reference $rev?
643*0c3a5702SAndreas Gohr                        if ($beforecount > max(floor($max / 2), $max - $aftercount)) break 2;
644*0c3a5702SAndreas Gohr                    }
645*0c3a5702SAndreas Gohr                }
646*0c3a5702SAndreas Gohr            }
647*0c3a5702SAndreas Gohr        }
648*0c3a5702SAndreas Gohr        sort($revs);
649*0c3a5702SAndreas Gohr
650*0c3a5702SAndreas Gohr        //keep only non-parsed lines
651*0c3a5702SAndreas Gohr        $lines = array_slice($lines, 0, $i);
652*0c3a5702SAndreas Gohr        //trunk desired selection
653*0c3a5702SAndreas Gohr        $requestedrevs = array_slice($revs, -$max, $max);
654*0c3a5702SAndreas Gohr
655*0c3a5702SAndreas Gohr        return array($requestedrevs, $revs, $fp, $lines, $head, $lasttail);
656*0c3a5702SAndreas Gohr    }
657*0c3a5702SAndreas Gohr}
658