xref: /dokuwiki/inc/ChangeLog/ChangeLogTrait.php (revision a3a0fc589f758b9416b6fd0f98b1e743b8625de2)
11d11f1d3SSatoshi Sahara<?php
21d11f1d3SSatoshi Sahara
31d11f1d3SSatoshi Saharanamespace dokuwiki\ChangeLog;
41d11f1d3SSatoshi Sahara
51d11f1d3SSatoshi Sahara/**
61d11f1d3SSatoshi Sahara * Provides methods for handling of changelog
71d11f1d3SSatoshi Sahara */
81d11f1d3SSatoshi Saharatrait ChangeLogTrait
91d11f1d3SSatoshi Sahara{
101d11f1d3SSatoshi Sahara    /**
111d11f1d3SSatoshi Sahara     * Adds an entry to the changelog file
121d11f1d3SSatoshi Sahara     *
131d11f1d3SSatoshi Sahara     * @return array added logline as revision info
141d11f1d3SSatoshi Sahara     */
151d11f1d3SSatoshi Sahara    abstract public function addLogEntry(array $info, $timestamp = null);
161d11f1d3SSatoshi Sahara
171d11f1d3SSatoshi Sahara    /**
181d11f1d3SSatoshi Sahara     * Parses a changelog line into it's components
191d11f1d3SSatoshi Sahara     *
201d11f1d3SSatoshi Sahara     * @author Ben Coburn <btcoburn@silicodon.net>
211d11f1d3SSatoshi Sahara     *
221d11f1d3SSatoshi Sahara     * @param string $line changelog line
231d11f1d3SSatoshi Sahara     * @return array|bool parsed line or false
241d11f1d3SSatoshi Sahara     */
251d11f1d3SSatoshi Sahara    public static function parseLogLine($line)
261d11f1d3SSatoshi Sahara    {
271d11f1d3SSatoshi Sahara        $info = explode("\t", rtrim($line, "\n"));
281d11f1d3SSatoshi Sahara        if ($info !== false && count($info) > 1) {
291d11f1d3SSatoshi Sahara            return $entry = array(
301d11f1d3SSatoshi Sahara                'date'  => (int)$info[0], // unix timestamp
311d11f1d3SSatoshi Sahara                'ip'    => $info[1], // IPv4 address (127.0.0.1)
321d11f1d3SSatoshi Sahara                'type'  => $info[2], // log line type
331d11f1d3SSatoshi Sahara                'id'    => $info[3], // page id
341d11f1d3SSatoshi Sahara                'user'  => $info[4], // user name
351d11f1d3SSatoshi Sahara                'sum'   => $info[5], // edit summary (or action reason)
361d11f1d3SSatoshi Sahara                'extra' => $info[6], // extra data (varies by line type)
371d11f1d3SSatoshi Sahara                'sizechange' => (isset($info[7]) && $info[7] !== '') ? (int)$info[7] : null, //
381d11f1d3SSatoshi Sahara            );
391d11f1d3SSatoshi Sahara        } else {
401d11f1d3SSatoshi Sahara            return false;
411d11f1d3SSatoshi Sahara        }
421d11f1d3SSatoshi Sahara    }
431d11f1d3SSatoshi Sahara
441d11f1d3SSatoshi Sahara    /**
451d11f1d3SSatoshi Sahara     * Build a changelog line from it's components
461d11f1d3SSatoshi Sahara     *
471d11f1d3SSatoshi Sahara     * @param array $info Revision info structure
481d11f1d3SSatoshi Sahara     * @param int $timestamp logline date (optional)
491d11f1d3SSatoshi Sahara     * @return string changelog line
501d11f1d3SSatoshi Sahara     */
511d11f1d3SSatoshi Sahara    public static function buildLogLine(array &$info, $timestamp = null)
521d11f1d3SSatoshi Sahara    {
531d11f1d3SSatoshi Sahara        $strip = ["\t", "\n"];
541d11f1d3SSatoshi Sahara        $entry = array(
551d11f1d3SSatoshi Sahara            'date'  => $timestamp ?? $info['date'],
561d11f1d3SSatoshi Sahara            'ip'    => $info['ip'],
571d11f1d3SSatoshi Sahara            'type'  => str_replace($strip, '', $info['type']),
581d11f1d3SSatoshi Sahara            'id'    => $info['id'],
591d11f1d3SSatoshi Sahara            'user'  => $info['user'],
601d11f1d3SSatoshi Sahara            'sum'   => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $info['sum']), 0, 255),
611d11f1d3SSatoshi Sahara            'extra' => str_replace($strip, '', $info['extra']),
621d11f1d3SSatoshi Sahara            'sizechange' => $info['sizechange'],
631d11f1d3SSatoshi Sahara        );
641d11f1d3SSatoshi Sahara        $info = $entry;
651d11f1d3SSatoshi Sahara        return $line = implode("\t", $entry) ."\n";
661d11f1d3SSatoshi Sahara    }
671d11f1d3SSatoshi Sahara
681d11f1d3SSatoshi Sahara    /**
691d11f1d3SSatoshi Sahara     * Returns path to changelog
701d11f1d3SSatoshi Sahara     *
711d11f1d3SSatoshi Sahara     * @return string path to file
721d11f1d3SSatoshi Sahara     */
731d11f1d3SSatoshi Sahara    abstract protected function getChangelogFilename();
741d11f1d3SSatoshi Sahara
75*a3a0fc58SSatoshi Sahara    /**
76*a3a0fc58SSatoshi Sahara     * Checks if the ID has old revisons
77*a3a0fc58SSatoshi Sahara     * @return boolean
78*a3a0fc58SSatoshi Sahara     */
79*a3a0fc58SSatoshi Sahara    public function hasRevisions()
80*a3a0fc58SSatoshi Sahara    {
81*a3a0fc58SSatoshi Sahara        $logfile = $this->getChangelogFilename();
82*a3a0fc58SSatoshi Sahara        return file_exists($logfile);
83*a3a0fc58SSatoshi Sahara    }
84*a3a0fc58SSatoshi Sahara
85*a3a0fc58SSatoshi Sahara
86*a3a0fc58SSatoshi Sahara    /** @var int */
87*a3a0fc58SSatoshi Sahara    protected $chunk_size;
881d11f1d3SSatoshi Sahara
891d11f1d3SSatoshi Sahara    /**
901d11f1d3SSatoshi Sahara     * Set chunk size for file reading
911d11f1d3SSatoshi Sahara     * Chunk size zero let read whole file at once
921d11f1d3SSatoshi Sahara     *
931d11f1d3SSatoshi Sahara     * @param int $chunk_size maximum block size read from file
941d11f1d3SSatoshi Sahara     */
951d11f1d3SSatoshi Sahara    public function setChunkSize($chunk_size)
961d11f1d3SSatoshi Sahara    {
971d11f1d3SSatoshi Sahara        if (!is_numeric($chunk_size)) $chunk_size = 0;
981d11f1d3SSatoshi Sahara
991d11f1d3SSatoshi Sahara        $this->chunk_size = (int)max($chunk_size, 0);
1001d11f1d3SSatoshi Sahara    }
1011d11f1d3SSatoshi Sahara
1021d11f1d3SSatoshi Sahara    /**
1031d11f1d3SSatoshi Sahara     * Returns lines from changelog.
1041d11f1d3SSatoshi Sahara     * If file larger than $chuncksize, only chunck is read that could contain $rev.
1051d11f1d3SSatoshi Sahara     *
1061d11f1d3SSatoshi Sahara     * When reference timestamp $rev is outside time range of changelog, readloglines() will return
1071d11f1d3SSatoshi Sahara     * lines in first or last chunk, but they obviously does not contain $rev.
1081d11f1d3SSatoshi Sahara     *
1091d11f1d3SSatoshi Sahara     * @param int $rev revision timestamp
1101d11f1d3SSatoshi Sahara     * @return array|false
1111d11f1d3SSatoshi Sahara     *     if success returns array(fp, array(changeloglines), $head, $tail, $eof)
1121d11f1d3SSatoshi Sahara     *     where fp only defined for chuck reading, needs closing.
1131d11f1d3SSatoshi Sahara     *     otherwise false
1141d11f1d3SSatoshi Sahara     */
1151d11f1d3SSatoshi Sahara    protected function readloglines($rev)
1161d11f1d3SSatoshi Sahara    {
1171d11f1d3SSatoshi Sahara        $file = $this->getChangelogFilename();
1181d11f1d3SSatoshi Sahara
1191d11f1d3SSatoshi Sahara        if (!file_exists($file)) {
1201d11f1d3SSatoshi Sahara            return false;
1211d11f1d3SSatoshi Sahara        }
1221d11f1d3SSatoshi Sahara
1231d11f1d3SSatoshi Sahara        $fp = null;
1241d11f1d3SSatoshi Sahara        $head = 0;
1251d11f1d3SSatoshi Sahara        $tail = 0;
1261d11f1d3SSatoshi Sahara        $eof = 0;
1271d11f1d3SSatoshi Sahara
1281d11f1d3SSatoshi Sahara        if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
1291d11f1d3SSatoshi Sahara            // read whole file
1301d11f1d3SSatoshi Sahara            $lines = file($file);
1311d11f1d3SSatoshi Sahara            if ($lines === false) {
1321d11f1d3SSatoshi Sahara                return false;
1331d11f1d3SSatoshi Sahara            }
1341d11f1d3SSatoshi Sahara        } else {
1351d11f1d3SSatoshi Sahara            // read by chunk
1361d11f1d3SSatoshi Sahara            $fp = fopen($file, 'rb'); // "file pointer"
1371d11f1d3SSatoshi Sahara            if ($fp === false) {
1381d11f1d3SSatoshi Sahara                return false;
1391d11f1d3SSatoshi Sahara            }
1401d11f1d3SSatoshi Sahara            $head = 0;
1411d11f1d3SSatoshi Sahara            fseek($fp, 0, SEEK_END);
1421d11f1d3SSatoshi Sahara            $eof = ftell($fp);
1431d11f1d3SSatoshi Sahara            $tail = $eof;
1441d11f1d3SSatoshi Sahara
1451d11f1d3SSatoshi Sahara            // find chunk
1461d11f1d3SSatoshi Sahara            while ($tail - $head > $this->chunk_size) {
1471d11f1d3SSatoshi Sahara                $finger = $head + intval(($tail - $head) / 2);
1481d11f1d3SSatoshi Sahara                $finger = $this->getNewlinepointer($fp, $finger);
1491d11f1d3SSatoshi Sahara                $tmp = fgets($fp);
1501d11f1d3SSatoshi Sahara                if ($finger == $head || $finger == $tail) {
1511d11f1d3SSatoshi Sahara                    break;
1521d11f1d3SSatoshi Sahara                }
1531d11f1d3SSatoshi Sahara                $info = $this->parseLogLine($tmp);
1541d11f1d3SSatoshi Sahara                $finger_rev = $info['date'];
1551d11f1d3SSatoshi Sahara
1561d11f1d3SSatoshi Sahara                if ($finger_rev > $rev) {
1571d11f1d3SSatoshi Sahara                    $tail = $finger;
1581d11f1d3SSatoshi Sahara                } else {
1591d11f1d3SSatoshi Sahara                    $head = $finger;
1601d11f1d3SSatoshi Sahara                }
1611d11f1d3SSatoshi Sahara            }
1621d11f1d3SSatoshi Sahara
1631d11f1d3SSatoshi Sahara            if ($tail - $head < 1) {
1641d11f1d3SSatoshi Sahara                // cound not find chunk, assume requested rev is missing
1651d11f1d3SSatoshi Sahara                fclose($fp);
1661d11f1d3SSatoshi Sahara                return false;
1671d11f1d3SSatoshi Sahara            }
1681d11f1d3SSatoshi Sahara
1691d11f1d3SSatoshi Sahara            $lines = $this->readChunk($fp, $head, $tail);
1701d11f1d3SSatoshi Sahara        }
1711d11f1d3SSatoshi Sahara        return array(
1721d11f1d3SSatoshi Sahara            $fp,
1731d11f1d3SSatoshi Sahara            $lines,
1741d11f1d3SSatoshi Sahara            $head,
1751d11f1d3SSatoshi Sahara            $tail,
1761d11f1d3SSatoshi Sahara            $eof,
1771d11f1d3SSatoshi Sahara        );
1781d11f1d3SSatoshi Sahara    }
1791d11f1d3SSatoshi Sahara
1801d11f1d3SSatoshi Sahara    /**
1811d11f1d3SSatoshi Sahara     * Read chunk and return array with lines of given chunck.
1821d11f1d3SSatoshi Sahara     * Has no check if $head and $tail are really at a new line
1831d11f1d3SSatoshi Sahara     *
1841d11f1d3SSatoshi Sahara     * @param resource $fp resource filepointer
1851d11f1d3SSatoshi Sahara     * @param int $head start point chunck
1861d11f1d3SSatoshi Sahara     * @param int $tail end point chunck
1871d11f1d3SSatoshi Sahara     * @return array lines read from chunck
1881d11f1d3SSatoshi Sahara     */
1891d11f1d3SSatoshi Sahara    protected function readChunk($fp, $head, $tail)
1901d11f1d3SSatoshi Sahara    {
1911d11f1d3SSatoshi Sahara        $chunk = '';
1921d11f1d3SSatoshi Sahara        $chunk_size = max($tail - $head, 0); // found chunk size
1931d11f1d3SSatoshi Sahara        $got = 0;
1941d11f1d3SSatoshi Sahara        fseek($fp, $head);
1951d11f1d3SSatoshi Sahara        while ($got < $chunk_size && !feof($fp)) {
1961d11f1d3SSatoshi Sahara            $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0));
1971d11f1d3SSatoshi Sahara            if ($tmp === false) { //error state
1981d11f1d3SSatoshi Sahara                break;
1991d11f1d3SSatoshi Sahara            }
2001d11f1d3SSatoshi Sahara            $got += strlen($tmp);
2011d11f1d3SSatoshi Sahara            $chunk .= $tmp;
2021d11f1d3SSatoshi Sahara        }
2031d11f1d3SSatoshi Sahara        $lines = explode("\n", $chunk);
2041d11f1d3SSatoshi Sahara        array_pop($lines); // remove trailing newline
2051d11f1d3SSatoshi Sahara        return $lines;
2061d11f1d3SSatoshi Sahara    }
2071d11f1d3SSatoshi Sahara
2081d11f1d3SSatoshi Sahara    /**
2091d11f1d3SSatoshi Sahara     * Set pointer to first new line after $finger and return its position
2101d11f1d3SSatoshi Sahara     *
2111d11f1d3SSatoshi Sahara     * @param resource $fp filepointer
2121d11f1d3SSatoshi Sahara     * @param int $finger a pointer
2131d11f1d3SSatoshi Sahara     * @return int pointer
2141d11f1d3SSatoshi Sahara     */
2151d11f1d3SSatoshi Sahara    protected function getNewlinepointer($fp, $finger)
2161d11f1d3SSatoshi Sahara    {
2171d11f1d3SSatoshi Sahara        fseek($fp, $finger);
2181d11f1d3SSatoshi Sahara        $nl = $finger;
2191d11f1d3SSatoshi Sahara        if ($finger > 0) {
2201d11f1d3SSatoshi Sahara            fgets($fp); // slip the finger forward to a new line
2211d11f1d3SSatoshi Sahara            $nl = ftell($fp);
2221d11f1d3SSatoshi Sahara        }
2231d11f1d3SSatoshi Sahara        return $nl;
2241d11f1d3SSatoshi Sahara    }
2251d11f1d3SSatoshi Sahara
2261d11f1d3SSatoshi Sahara    /**
2271d11f1d3SSatoshi Sahara     * Returns the next lines of the changelog  of the chunck before head or after tail
2281d11f1d3SSatoshi Sahara     *
2291d11f1d3SSatoshi Sahara     * @param resource $fp filepointer
2301d11f1d3SSatoshi Sahara     * @param int $head position head of last chunk
2311d11f1d3SSatoshi Sahara     * @param int $tail position tail of last chunk
2321d11f1d3SSatoshi Sahara     * @param int $direction positive forward, negative backward
2331d11f1d3SSatoshi Sahara     * @return array with entries:
2341d11f1d3SSatoshi Sahara     *    - $lines: changelog lines of readed chunk
2351d11f1d3SSatoshi Sahara     *    - $head: head of chunk
2361d11f1d3SSatoshi Sahara     *    - $tail: tail of chunk
2371d11f1d3SSatoshi Sahara     */
2381d11f1d3SSatoshi Sahara    protected function readAdjacentChunk($fp, $head, $tail, $direction)
2391d11f1d3SSatoshi Sahara    {
2401d11f1d3SSatoshi Sahara        if (!$fp) return array(array(), $head, $tail);
2411d11f1d3SSatoshi Sahara
2421d11f1d3SSatoshi Sahara        if ($direction > 0) {
2431d11f1d3SSatoshi Sahara            //read forward
2441d11f1d3SSatoshi Sahara            $head = $tail;
2451d11f1d3SSatoshi Sahara            $tail = $head + intval($this->chunk_size * (2 / 3));
2461d11f1d3SSatoshi Sahara            $tail = $this->getNewlinepointer($fp, $tail);
2471d11f1d3SSatoshi Sahara        } else {
2481d11f1d3SSatoshi Sahara            //read backward
2491d11f1d3SSatoshi Sahara            $tail = $head;
2501d11f1d3SSatoshi Sahara            $head = max($tail - $this->chunk_size, 0);
2511d11f1d3SSatoshi Sahara            while (true) {
2521d11f1d3SSatoshi Sahara                $nl = $this->getNewlinepointer($fp, $head);
2531d11f1d3SSatoshi Sahara                // was the chunk big enough? if not, take another bite
2541d11f1d3SSatoshi Sahara                if ($nl > 0 && $tail <= $nl) {
2551d11f1d3SSatoshi Sahara                    $head = max($head - $this->chunk_size, 0);
2561d11f1d3SSatoshi Sahara                } else {
2571d11f1d3SSatoshi Sahara                    $head = $nl;
2581d11f1d3SSatoshi Sahara                    break;
2591d11f1d3SSatoshi Sahara                }
2601d11f1d3SSatoshi Sahara            }
2611d11f1d3SSatoshi Sahara        }
2621d11f1d3SSatoshi Sahara
2631d11f1d3SSatoshi Sahara        //load next chunck
2641d11f1d3SSatoshi Sahara        $lines = $this->readChunk($fp, $head, $tail);
2651d11f1d3SSatoshi Sahara        return array($lines, $head, $tail);
2661d11f1d3SSatoshi Sahara    }
2671d11f1d3SSatoshi Sahara
2681d11f1d3SSatoshi Sahara}
269