1<?php 2 3/** 4 * Class helper_plugin_loglog_logging 5 */ 6class helper_plugin_loglog_logging extends DokuWiki_Plugin 7{ 8 protected $file = ''; 9 10 public function __construct() 11 { 12 global $conf; 13 if(defined('DOKU_UNITTEST')) { 14 $this->file = DOKU_PLUGIN . 'loglog/_test/loglog.log'; 15 } else { 16 $this->file = $conf['cachedir'] . '/loglog.log'; 17 } 18 } 19 20 /** 21 * Build a log entry from passed data and write a single line to log file 22 * 23 * @param string $msg 24 * @param null $user 25 * @param array $data 26 */ 27 public function writeLine($msg, $user = null, $data = []) 28 { 29 global $conf, $INPUT; 30 31 if (is_null($user)) $user = $INPUT->server->str('REMOTE_USER'); 32 if (!$user) $user = $_REQUEST['u']; 33 if (!$user) return; 34 35 $t = time(); 36 $ip = clientIP(true); 37 $data = !empty($data) ? json_encode($data) : ''; 38 39 $line = join("\t", [$t, strftime($conf['dformat'], $t), $ip, $user, $msg, $data]); 40 41 io_saveFile($this->file, "$line\n", true); 42 } 43 44 /** 45 * Return logfile lines limited to specified $min - $max range 46 * 47 * @param int $min 48 * @param int $max 49 * @return array 50 */ 51 public function readLines($min, $max) 52 { 53 $lines = []; 54 $candidateLines = $this->readChunks($min, $max); 55 foreach ($candidateLines as $line) { 56 if (empty($line)) continue; // Filter empty lines 57 $parsedLine = $this->loglineToArray($line); 58 if ($parsedLine['dt'] >= $min && $parsedLine['dt'] <= $max) { 59 $lines[] = $parsedLine; 60 } 61 } 62 return $lines; 63 } 64 65 /** 66 * Read log lines backwards. Start and end timestamps are used to evaluate 67 * only the chunks being read, NOT single lines. This method will return 68 * too many lines, the dates have to be checked by the caller again. 69 * 70 * @param int $min start time (in seconds) 71 * @param int $max end time (in seconds) 72 * @return array 73 */ 74 protected function readChunks($min, $max) 75 { 76 $data = array(); 77 $lines = array(); 78 $chunk_size = 8192; 79 80 if (!@file_exists($this->file)) return $data; 81 $fp = fopen($this->file, 'rb'); 82 if ($fp === false) return $data; 83 84 //seek to end 85 fseek($fp, 0, SEEK_END); 86 $pos = ftell($fp); 87 $chunk = ''; 88 89 while ($pos) { 90 91 // how much to read? Set pointer 92 if ($pos > $chunk_size) { 93 $pos -= $chunk_size; 94 $read = $chunk_size; 95 } else { 96 $read = $pos; 97 $pos = 0; 98 } 99 fseek($fp, $pos); 100 101 $tmp = fread($fp, $read); 102 if ($tmp === false) break; 103 $chunk = $tmp . $chunk; 104 105 // now split the chunk 106 $cparts = explode("\n", $chunk); 107 108 // keep the first part in chunk (may be incomplete) 109 if ($pos) $chunk = array_shift($cparts); 110 111 // no more parts available, read on 112 if (!count($cparts)) continue; 113 114 // get date of first line: 115 list($cdate) = explode("\t", $cparts[0]); 116 117 if ($cdate > $max) continue; // haven't reached wanted area, yet 118 119 // put all the lines from the chunk on the stack 120 $lines = array_merge($cparts, $lines); 121 122 if ($cdate < $min) break; // we have enough 123 } 124 fclose($fp); 125 126 return $lines; 127 } 128 129 /** 130 * Convert log line to array 131 * 132 * @param string $line 133 * @return array 134 */ 135 protected function loglineToArray($line) 136 { 137 list($dt, $junk, $ip, $user, $msg, $data) = explode("\t", $line, 6); 138 return [ 139 'dt' => $dt, // timestamp 140 'ip' => $ip, 141 'user' => $user, 142 'msg' => $msg, 143 'data' => $data, // JSON encoded additional data 144 ]; 145 } 146 147 /** 148 * Returns the number of lines where the given needle has been found in message 149 * 150 * @param array $lines 151 * @param string $msgNeedle 152 * @return mixed 153 */ 154 public function countMatchingLines(array $lines, string $msgNeedle) 155 { 156 return array_reduce( 157 $lines, 158 function ($carry, $line) use ($msgNeedle) { 159 $carry = $carry + (int)(strpos($line['msg'], $msgNeedle) !== false); 160 return $carry; 161 }, 162 0 163 ); 164 } 165 166} 167