1 <?php
  2 
  3 /**
  4  * Log file class.
  5  *
  6  * @author gharlan
  7  *
  8  * @package redaxo\core
  9  */
 10 class rex_log_file implements Iterator
 11 {
 12     /** @var string */
 13     private $path;
 14 
 15     /** @var resource */
 16     private $file;
 17 
 18     /** @var resource */
 19     private $file2;
 20 
 21     /** @var bool */
 22     private $second = false;
 23 
 24     /** @var int */
 25     private $pos;
 26 
 27     /** @var int */
 28     private $key;
 29 
 30     /** @var string */
 31     private $currentLine;
 32 
 33     /** @var string */
 34     private $buffer;
 35 
 36     /** @var int */
 37     private $bufferPos;
 38 
 39     /**
 40      * Constructor.
 41      *
 42      * @param string   $path        File path
 43      * @param int|null $maxFileSize Maximum file size
 44      */
 45     public function __construct($path, $maxFileSize = null)
 46     {
 47         $this->path = $path;
 48         if (!file_exists($path)) {
 49             rex_file::put($path, '');
 50         }
 51         if ($maxFileSize && filesize($path) > $maxFileSize) {
 52             rename($path, $path . '.2');
 53         }
 54         $this->file = fopen($path, 'a+');
 55     }
 56 
 57     /**
 58      * Adds a log entry.
 59      *
 60      * @param array $data Log data
 61      */
 62     public function add(array $data)
 63     {
 64         fseek($this->file, 0, SEEK_END);
 65         fwrite($this->file, new rex_log_entry(time(), $data) . "\n");
 66     }
 67 
 68     /**
 69      * @return rex_log_entry
 70      */
 71     public function current()
 72     {
 73         return rex_log_entry::createFromString($this->currentLine);
 74     }
 75 
 76     /**
 77      * Reads the log file backwards line by line (each call reads one line).
 78      */
 79     public function next()
 80     {
 81         static $bufferSize = 500;
 82 
 83         if ($this->pos < 0) {
 84             // position is before file start -> look for next file
 85             $path2 = $this->path . '.2';
 86             if ($this->second || !$this->file2 && !file_exists($path2)) {
 87                 // already in file2 or file2 does not exist -> mark currentLine as invalid
 88                 $this->currentLine = null;
 89                 $this->key = null;
 90                 return;
 91             }
 92             // switch to file2 and reset position
 93             if (!$this->file2) {
 94                 $this->file2 = fopen($path2, 'r');
 95             }
 96             $this->second = true;
 97             $this->pos = null;
 98         }
 99 
100         // get current file
101         $file = $this->second ? $this->file2 : $this->file;
102 
103         if (null === $this->pos) {
104             // position is not set -> set start position to start of last buffer
105             fseek($file, 0, SEEK_END);
106             $this->pos = (int) (ftell($file) / $bufferSize) * $bufferSize;
107         }
108 
109         $line = '';
110         // while position is not before file start
111         while ($this->pos >= 0) {
112             if ($this->bufferPos < 0) {
113                 // read next buffer
114                 fseek($file, $this->pos);
115                 $this->buffer = fread($file, $bufferSize);
116                 $this->bufferPos = strlen($this->buffer) - 1;
117             }
118             // read buffer backwards char by char
119             for (; $this->bufferPos >= 0; --$this->bufferPos) {
120                 $char = $this->buffer[$this->bufferPos];
121                 if ("\n" === $char) {
122                     // line start reached -> prepare bufferPos/pos and jump outside of while-loop
123                     --$this->bufferPos;
124                     if ($this->bufferPos < 0) {
125                         $this->pos -= $bufferSize;
126                     }
127                     break 2;
128                 }
129                 if ("\r" !== $char) {
130                     // build line; \r is ignored
131                     $line = $char . $line;
132                 }
133             }
134             $this->pos -= $bufferSize;
135         }
136         if (!$line = trim($line)) {
137             // empty lines are skipped -> read next line
138             $this->next();
139             return;
140         }
141         // found a non-empty line
142         ++$this->key;
143         $this->currentLine = $line;
144     }
145 
146     /**
147      * {@inheritdoc}
148      */
149     public function key()
150     {
151         return $this->key;
152     }
153 
154     /**
155      * {@inheritdoc}
156      */
157     public function valid()
158     {
159         return !empty($this->currentLine);
160     }
161 
162     /**
163      * {@inheritdoc}
164      */
165     public function rewind()
166     {
167         $this->second = false;
168         $this->pos = null;
169         $this->key = -1;
170         $this->bufferPos = -1;
171         $this->next();
172     }
173 
174     /**
175      * Deletes a log file and its rotations.
176      *
177      * @param string $path File path
178      *
179      * @return bool
180      */
181     public static function delete($path)
182     {
183         return rex_file::delete($path) && rex_file::delete($path . '.2');
184     }
185 }
186 
187 /**
188  * Log entry class.
189  *
190  * @author gharlan
191  *
192  * @package redaxo\core
193  */
194 class rex_log_entry
195 {
196     /** @var int */
197     private $timestamp;
198 
199     /** @var array */
200     private $data;
201 
202     /**
203      * Constructor.
204      *
205      * @param int   $timestamp Timestamp
206      * @param array $data      Log data
207      */
208     public function __construct($timestamp, array $data)
209     {
210         $this->timestamp = $timestamp;
211         $this->data = $data;
212     }
213 
214     /**
215      * Creates a log entry from string.
216      *
217      * @param string $string Log line
218      *
219      * @return rex_log_entry
220      */
221     public static function createFromString($string)
222     {
223         $data = [];
224         foreach (explode('|', $string) as $part) {
225             $data[] = str_replace('\n', "\n", trim($part));
226         }
227 
228         $timestamp = strtotime(array_shift($data));
229 
230         return new self($timestamp, $data);
231     }
232 
233     /**
234      * Returns the timestamp.
235      *
236      * @param string $format See {@link rex_formatter::strftime}
237      *
238      * @return int|string Unix timestamp or formatted string if $format is given
239      */
240     public function getTimestamp($format = null)
241     {
242         if (null === $format) {
243             return $this->timestamp;
244         }
245         return rex_formatter::strftime($this->timestamp, $format);
246     }
247 
248     /**
249      * Returns the log data.
250      *
251      * @return array
252      */
253     public function getData()
254     {
255         return $this->data;
256     }
257 
258     /**
259      * @return string
260      */
261     public function __toString()
262     {
263         $data = implode(' | ', array_map('trim', $this->data));
264         $data = str_replace(["\r", "\n"], ['', '\n'], $data);
265         return date('Y-m-d H:i:s', $this->timestamp) . ' | ' . $data;
266     }
267 }
268