* */ public static function parseLogLine($line) { $info = sexplode("\t", rtrim($line, "\n"), 8); if ($info[3]) { // we need at least the page id to consider it a valid line return [ 'date' => (int)$info[0], // unix timestamp 'ip' => $info[1], // IP address (127.0.0.1) 'type' => $info[2], // log line type 'id' => $info[3], // page id 'user' => $info[4], // user name 'sum' => $info[5], // edit summary (or action reason) 'extra' => $info[6], // extra data (varies by line type) 'sizechange' => ($info[7] != '') ? (int)$info[7] : null, // size difference in bytes ]; } else { return false; } } /** * Build a changelog line from its components * * @param array $info Revision info structure * @param int $timestamp log line date (optional) * @return string changelog line */ public static function buildLogLine(array &$info, $timestamp = null) { $strip = ["\t", "\n"]; $entry = [ 'date' => $timestamp ?? $info['date'], 'ip' => $info['ip'], 'type' => str_replace($strip, '', $info['type']), 'id' => $info['id'], 'user' => $info['user'], 'sum' => PhpString::substr(str_replace($strip, '', $info['sum'] ?? ''), 0, 255), 'extra' => str_replace($strip, '', $info['extra']), 'sizechange' => $info['sizechange'] ]; $info = $entry; return implode("\t", $entry) . "\n"; } /** * Returns path to changelog * * @return string path to file */ abstract protected function getChangelogFilename(); /** * Checks if the ID has old revisions * @return boolean */ public function hasRevisions() { $logfile = $this->getChangelogFilename(); return file_exists($logfile); } /** @var int */ protected $chunk_size; /** * Set chunk size for file reading * Chunk size zero let read whole file at once * * @param int $chunk_size maximum block size read from file */ public function setChunkSize($chunk_size) { if (!is_numeric($chunk_size)) $chunk_size = 0; $this->chunk_size = max($chunk_size, 0); } /** * Returns lines from changelog. * If file larger than $chunk_size, only chunk is read that could contain $rev. * * When reference timestamp $rev is outside time range of changelog, readloglines() will return * lines in first or last chunk, but they obviously does not contain $rev. * * @param int $rev revision timestamp * @return array|false * if success returns array(fp, array(changeloglines), $head, $tail, $eof) * where fp only defined for chuck reading, needs closing. * otherwise false */ protected function readloglines($rev) { $file = $this->getChangelogFilename(); if (!file_exists($file)) { return false; } $fp = null; $head = 0; $tail = 0; $eof = 0; if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) { // read whole file $lines = file($file); if ($lines === false) { return false; } } else { // read by chunk $fp = fopen($file, 'rb'); // "file pointer" if ($fp === false) { return false; } fseek($fp, 0, SEEK_END); $eof = ftell($fp); $tail = $eof; // find chunk while ($tail - $head > $this->chunk_size) { $finger = $head + (int)(($tail - $head) / 2); $finger = $this->getNewlinepointer($fp, $finger); $tmp = fgets($fp); if ($finger == $head || $finger == $tail) { break; } $info = $this->parseLogLine($tmp); $finger_rev = $info['date']; if ($finger_rev > $rev) { $tail = $finger; } else { $head = $finger; } } if ($tail - $head < 1) { // could not find chunk, assume requested rev is missing fclose($fp); return false; } $lines = $this->readChunk($fp, $head, $tail); } return [$fp, $lines, $head, $tail, $eof]; } /** * Read chunk and return array with lines of given chunk. * Has no check if $head and $tail are really at a new line * * @param resource $fp resource file pointer * @param int $head start point chunk * @param int $tail end point chunk * @return array lines read from chunk */ protected function readChunk($fp, $head, $tail) { $chunk = ''; $chunk_size = max($tail - $head, 0); // found chunk size $got = 0; fseek($fp, $head); while ($got < $chunk_size && !feof($fp)) { $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0)); if ($tmp === false) { //error state break; } $got += strlen($tmp); $chunk .= $tmp; } $lines = explode("\n", $chunk); array_pop($lines); // remove trailing newline return $lines; } /** * Set pointer to first new line after $finger and return its position * * @param resource $fp file pointer * @param int $finger a pointer * @return int pointer */ protected function getNewlinepointer($fp, $finger) { fseek($fp, $finger); $nl = $finger; if ($finger > 0) { fgets($fp); // slip the finger forward to a new line $nl = ftell($fp); } return $nl; } /** * Returns the next lines of the changelog of the chunk before head or after tail * * @param resource $fp file pointer * @param int $head position head of last chunk * @param int $tail position tail of last chunk * @param int $direction positive forward, negative backward * @return array with entries: * - $lines: changelog lines of read chunk * - $head: head of chunk * - $tail: tail of chunk */ protected function readAdjacentChunk($fp, $head, $tail, $direction) { if (!$fp) return [[], $head, $tail]; if ($direction > 0) { //read forward $head = $tail; $tail = $head + (int)($this->chunk_size * (2 / 3)); $tail = $this->getNewlinepointer($fp, $tail); } else { //read backward $tail = $head; $head = max($tail - $this->chunk_size, 0); while (true) { $nl = $this->getNewlinepointer($fp, $head); // was the chunk big enough? if not, take another bite if ($nl > 0 && $tail <= $nl) { $head = max($head - $this->chunk_size, 0); } else { $head = $nl; break; } } } //load next chunk $lines = $this->readChunk($fp, $head, $tail); return [$lines, $head, $tail]; } }