ChangeLogTrait.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php
  2. namespace dokuwiki\ChangeLog;
  3. use dokuwiki\Utf8\PhpString;
  4. /**
  5. * Provides methods for handling of changelog
  6. */
  7. trait ChangeLogTrait
  8. {
  9. /**
  10. * Adds an entry to the changelog file
  11. *
  12. * @return array added log line as revision info
  13. */
  14. abstract public function addLogEntry(array $info, $timestamp = null);
  15. /**
  16. * Parses a changelog line into it's components
  17. *
  18. * @author Ben Coburn <btcoburn@silicodon.net>
  19. *
  20. * @param string $line changelog line
  21. * @return array|bool parsed line or false
  22. */
  23. public static function parseLogLine($line)
  24. {
  25. $info = explode("\t", rtrim($line, "\n"));
  26. if ($info !== false && count($info) > 1) {
  27. return [
  28. 'date' => (int)$info[0], // unix timestamp
  29. 'ip' => $info[1], // IPv4 address (127.0.0.1)
  30. 'type' => $info[2], // log line type
  31. 'id' => $info[3], // page id
  32. 'user' => $info[4], // user name
  33. 'sum' => $info[5], // edit summary (or action reason)
  34. 'extra' => $info[6], // extra data (varies by line type)
  35. 'sizechange' => (isset($info[7]) && $info[7] !== '') ? (int)$info[7] : null, //
  36. ];
  37. } else {
  38. return false;
  39. }
  40. }
  41. /**
  42. * Build a changelog line from it's components
  43. *
  44. * @param array $info Revision info structure
  45. * @param int $timestamp log line date (optional)
  46. * @return string changelog line
  47. */
  48. public static function buildLogLine(array &$info, $timestamp = null)
  49. {
  50. $strip = ["\t", "\n"];
  51. $entry = array(
  52. 'date' => $timestamp ?? $info['date'],
  53. 'ip' => $info['ip'],
  54. 'type' => str_replace($strip, '', $info['type']),
  55. 'id' => $info['id'],
  56. 'user' => $info['user'],
  57. 'sum' => PhpString::substr(str_replace($strip, '', $info['sum']), 0, 255),
  58. 'extra' => str_replace($strip, '', $info['extra']),
  59. 'sizechange' => $info['sizechange'],
  60. );
  61. $info = $entry;
  62. return implode("\t", $entry) ."\n";
  63. }
  64. /**
  65. * Returns path to changelog
  66. *
  67. * @return string path to file
  68. */
  69. abstract protected function getChangelogFilename();
  70. /**
  71. * Checks if the ID has old revisions
  72. * @return boolean
  73. */
  74. public function hasRevisions()
  75. {
  76. $logfile = $this->getChangelogFilename();
  77. return file_exists($logfile);
  78. }
  79. /** @var int */
  80. protected $chunk_size;
  81. /**
  82. * Set chunk size for file reading
  83. * Chunk size zero let read whole file at once
  84. *
  85. * @param int $chunk_size maximum block size read from file
  86. */
  87. public function setChunkSize($chunk_size)
  88. {
  89. if (!is_numeric($chunk_size)) $chunk_size = 0;
  90. $this->chunk_size = (int)max($chunk_size, 0);
  91. }
  92. /**
  93. * Returns lines from changelog.
  94. * If file larger than $chunk_size, only chunk is read that could contain $rev.
  95. *
  96. * When reference timestamp $rev is outside time range of changelog, readloglines() will return
  97. * lines in first or last chunk, but they obviously does not contain $rev.
  98. *
  99. * @param int $rev revision timestamp
  100. * @return array|false
  101. * if success returns array(fp, array(changeloglines), $head, $tail, $eof)
  102. * where fp only defined for chuck reading, needs closing.
  103. * otherwise false
  104. */
  105. protected function readloglines($rev)
  106. {
  107. $file = $this->getChangelogFilename();
  108. if (!file_exists($file)) {
  109. return false;
  110. }
  111. $fp = null;
  112. $head = 0;
  113. $tail = 0;
  114. $eof = 0;
  115. if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
  116. // read whole file
  117. $lines = file($file);
  118. if ($lines === false) {
  119. return false;
  120. }
  121. } else {
  122. // read by chunk
  123. $fp = fopen($file, 'rb'); // "file pointer"
  124. if ($fp === false) {
  125. return false;
  126. }
  127. fseek($fp, 0, SEEK_END);
  128. $eof = ftell($fp);
  129. $tail = $eof;
  130. // find chunk
  131. while ($tail - $head > $this->chunk_size) {
  132. $finger = $head + intval(($tail - $head) / 2);
  133. $finger = $this->getNewlinepointer($fp, $finger);
  134. $tmp = fgets($fp);
  135. if ($finger == $head || $finger == $tail) {
  136. break;
  137. }
  138. $info = $this->parseLogLine($tmp);
  139. $finger_rev = $info['date'];
  140. if ($finger_rev > $rev) {
  141. $tail = $finger;
  142. } else {
  143. $head = $finger;
  144. }
  145. }
  146. if ($tail - $head < 1) {
  147. // could not find chunk, assume requested rev is missing
  148. fclose($fp);
  149. return false;
  150. }
  151. $lines = $this->readChunk($fp, $head, $tail);
  152. }
  153. return array(
  154. $fp,
  155. $lines,
  156. $head,
  157. $tail,
  158. $eof,
  159. );
  160. }
  161. /**
  162. * Read chunk and return array with lines of given chunk.
  163. * Has no check if $head and $tail are really at a new line
  164. *
  165. * @param resource $fp resource file pointer
  166. * @param int $head start point chunk
  167. * @param int $tail end point chunk
  168. * @return array lines read from chunk
  169. */
  170. protected function readChunk($fp, $head, $tail)
  171. {
  172. $chunk = '';
  173. $chunk_size = max($tail - $head, 0); // found chunk size
  174. $got = 0;
  175. fseek($fp, $head);
  176. while ($got < $chunk_size && !feof($fp)) {
  177. $tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0));
  178. if ($tmp === false) { //error state
  179. break;
  180. }
  181. $got += strlen($tmp);
  182. $chunk .= $tmp;
  183. }
  184. $lines = explode("\n", $chunk);
  185. array_pop($lines); // remove trailing newline
  186. return $lines;
  187. }
  188. /**
  189. * Set pointer to first new line after $finger and return its position
  190. *
  191. * @param resource $fp file pointer
  192. * @param int $finger a pointer
  193. * @return int pointer
  194. */
  195. protected function getNewlinepointer($fp, $finger)
  196. {
  197. fseek($fp, $finger);
  198. $nl = $finger;
  199. if ($finger > 0) {
  200. fgets($fp); // slip the finger forward to a new line
  201. $nl = ftell($fp);
  202. }
  203. return $nl;
  204. }
  205. /**
  206. * Returns the next lines of the changelog of the chunk before head or after tail
  207. *
  208. * @param resource $fp file pointer
  209. * @param int $head position head of last chunk
  210. * @param int $tail position tail of last chunk
  211. * @param int $direction positive forward, negative backward
  212. * @return array with entries:
  213. * - $lines: changelog lines of read chunk
  214. * - $head: head of chunk
  215. * - $tail: tail of chunk
  216. */
  217. protected function readAdjacentChunk($fp, $head, $tail, $direction)
  218. {
  219. if (!$fp) return array(array(), $head, $tail);
  220. if ($direction > 0) {
  221. //read forward
  222. $head = $tail;
  223. $tail = $head + intval($this->chunk_size * (2 / 3));
  224. $tail = $this->getNewlinepointer($fp, $tail);
  225. } else {
  226. //read backward
  227. $tail = $head;
  228. $head = max($tail - $this->chunk_size, 0);
  229. while (true) {
  230. $nl = $this->getNewlinepointer($fp, $head);
  231. // was the chunk big enough? if not, take another bite
  232. if ($nl > 0 && $tail <= $nl) {
  233. $head = max($head - $this->chunk_size, 0);
  234. } else {
  235. $head = $nl;
  236. break;
  237. }
  238. }
  239. }
  240. //load next chunk
  241. $lines = $this->readChunk($fp, $head, $tail);
  242. return array($lines, $head, $tail);
  243. }
  244. }