Skip to content

Commit 922f617

Browse files
committed
Update Logger.php
1 parent 1384953 commit 922f617

1 file changed

Lines changed: 237 additions & 21 deletions

File tree

‎src/Filesystem/Logger.php‎

Lines changed: 237 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use FloatPHP\Interfaces\Classes\LoggerInterface;
1919

2020
/**
21-
* Built-in logger class.
21+
* Built-in secure file-based class.
2222
*/
2323
class Logger implements LoggerInterface
2424
{
@@ -35,13 +35,15 @@ class Logger implements LoggerInterface
3535
/**
3636
* Init logger.
3737
*
38-
* @param string $path
39-
* @param string $filename
40-
* @param string $extension
38+
* @param string|null $path
39+
* @param string $file
40+
* @param string $ext
41+
* @throws \InvalidArgumentException When path parameters are invalid
42+
* @throws \RuntimeException When directory creation fails
4143
*/
4244
public function __construct(?string $path = '/', string $file = 'debug', string $ext = 'log')
4345
{
44-
$this->setPath($path);
46+
$this->setPath($path ?? '/');
4547
$this->setFilename($file);
4648
$this->setExtension($ext);
4749
}
@@ -51,14 +53,34 @@ public function __construct(?string $path = '/', string $file = 'debug', string
5153
*
5254
* @access public
5355
* @param string $path
54-
* @return object
56+
* @return self
57+
* @throws \InvalidArgumentException When path is invalid
58+
* @throws \RuntimeException When directory creation fails
5559
*/
5660
public function setPath(string $path) : self
5761
{
62+
// Input validation
63+
if ( empty($path) ) {
64+
throw new \InvalidArgumentException('Log path cannot be empty');
65+
}
66+
67+
// Sanitize path to prevent directory traversal attacks
68+
$path = $this->sanitizePath($path);
69+
70+
// Validate path format
71+
if ( !$this->isValidPath($path) ) {
72+
throw new \InvalidArgumentException("Invalid log path format: {$path}");
73+
}
74+
5875
$this->path = $path;
76+
77+
// Create directory if it doesn't exist
5978
if ( !File::isDir($this->path) ) {
60-
File::addDir($this->path);
79+
if ( !File::addDir($this->path) ) {
80+
throw new \RuntimeException("Failed to create log directory: {$this->path}");
81+
}
6182
}
83+
6284
return $this;
6385
}
6486

@@ -78,10 +100,19 @@ public function getPath() : string
78100
*
79101
* @access public
80102
* @param string $filename
81-
* @return object
103+
* @return self
104+
* @throws \InvalidArgumentException When filename is invalid
82105
*/
83106
public function setFilename(string $filename) : self
84107
{
108+
// Input validation
109+
if ( empty($filename) ) {
110+
throw new \InvalidArgumentException('Log filename cannot be empty');
111+
}
112+
113+
// Sanitize filename
114+
$filename = $this->sanitizeFilename($filename);
115+
85116
$this->filename = $filename;
86117
return $this;
87118
}
@@ -91,10 +122,22 @@ public function setFilename(string $filename) : self
91122
*
92123
* @access public
93124
* @param string $extension
94-
* @return object
125+
* @return self
126+
* @throws \InvalidArgumentException When extension is invalid
95127
*/
96128
public function setExtension(string $extension) : self
97129
{
130+
// Input validation
131+
if ( empty($extension) ) {
132+
throw new \InvalidArgumentException('Log extension cannot be empty');
133+
}
134+
135+
// Remove leading dot if present and validate
136+
$extension = ltrim($extension, '.');
137+
if ( !preg_match('/^[a-zA-Z0-9]+$/', $extension) ) {
138+
throw new \InvalidArgumentException("Invalid log extension format: {$extension}");
139+
}
140+
98141
$this->extension = $extension;
99142
return $this;
100143
}
@@ -106,12 +149,12 @@ public function setExtension(string $extension) : self
106149
* @param mixed $message
107150
* @param bool $isArray
108151
* @return bool
152+
* @throws \InvalidArgumentException When message is invalid
153+
* @throws \RuntimeException When logging fails
109154
*/
110155
public function debug($message, bool $isArray = false) : bool
111156
{
112-
if ( $isArray ) {
113-
$message = print_r($message, true);
114-
}
157+
$message = $this->formatMessage($message, $isArray);
115158
return $this->write('debug', $message);
116159
}
117160

@@ -121,10 +164,12 @@ public function debug($message, bool $isArray = false) : bool
121164
* @access public
122165
* @param string $message
123166
* @return bool
167+
* @throws \InvalidArgumentException When message is invalid
168+
* @throws \RuntimeException When logging fails
124169
*/
125170
public function error(string $message) : bool
126171
{
127-
return $this->write('error', $message);
172+
return $this->write('error', $this->validateMessage($message));
128173
}
129174

130175
/**
@@ -133,10 +178,12 @@ public function error(string $message) : bool
133178
* @access public
134179
* @param string $message
135180
* @return bool
181+
* @throws \InvalidArgumentException When message is invalid
182+
* @throws \RuntimeException When logging fails
136183
*/
137184
public function warning(string $message) : bool
138185
{
139-
return $this->write('warning', $message);
186+
return $this->write('warning', $this->validateMessage($message));
140187
}
141188

142189
/**
@@ -145,10 +192,12 @@ public function warning(string $message) : bool
145192
* @access public
146193
* @param string $message
147194
* @return bool
195+
* @throws \InvalidArgumentException When message is invalid
196+
* @throws \RuntimeException When logging fails
148197
*/
149198
public function info(string $message) : bool
150199
{
151-
return $this->write('info', $message);
200+
return $this->write('info', $this->validateMessage($message));
152201
}
153202

154203
/**
@@ -158,10 +207,12 @@ public function info(string $message) : bool
158207
* @param string $message
159208
* @param string $type
160209
* @return bool
210+
* @throws \InvalidArgumentException When message or type is invalid
211+
* @throws \RuntimeException When logging fails
161212
*/
162213
public function custom(string $message, string $type = 'custom') : bool
163214
{
164-
return $this->write($type, $message);
215+
return $this->write($this->validateLogType($type), $this->validateMessage($message));
165216
}
166217

167218
/**
@@ -186,13 +237,178 @@ public function log(string $message, int $type = 0, ?string $path = null, ?strin
186237
* @param string $status
187238
* @param string $message
188239
* @return bool
240+
* @throws \RuntimeException When file operations fail
189241
*/
190242
protected function write(string $status, string $message) : bool
191243
{
192-
$date = date('[d-m-Y]');
193-
$log = "{$this->path}/{$this->filename}-{$date}.{$this->extension}";
194-
$date = date('[d-m-Y H:i:s]');
195-
$msg = "{$date} : [{$status}] - {$message}" . Stringify::break();
196-
return File::w($log, $msg, true);
244+
try {
245+
// Optimize: Calculate date once
246+
$currentDate = date('[d-m-Y]');
247+
$currentDateTime = date('[d-m-Y H:i:s]');
248+
249+
$log = "{$this->path}/{$this->filename}-{$currentDate}.{$this->extension}";
250+
$msg = "{$currentDateTime} : [{$status}] - {$message}" . Stringify::break();
251+
252+
$result = File::w($log, $msg, true);
253+
if ( !$result ) {
254+
throw new \RuntimeException("Failed to write to log file: {$log}");
255+
}
256+
257+
return $result;
258+
259+
} catch (\Exception $e) {
260+
// In production, you might want to handle this differently
261+
throw new \RuntimeException("Logging failed: " . $e->getMessage(), 0, $e);
262+
}
263+
}
264+
265+
/**
266+
* Sanitize file path to prevent directory traversal attacks.
267+
*
268+
* @access private
269+
* @param string $path
270+
* @return string
271+
*/
272+
private function sanitizePath(string $path) : string
273+
{
274+
// Remove null bytes
275+
$path = str_replace("\0", '', $path);
276+
277+
// Normalize path separators to forward slashes
278+
$path = str_replace('\\', '/', $path);
279+
280+
// Remove dangerous patterns but preserve valid relative paths
281+
$path = preg_replace('/\.\.+\//', '', $path);
282+
283+
// Normalize multiple slashes
284+
$path = preg_replace('#/+#', '/', $path);
285+
286+
// Remove trailing slash except for root and Windows drive root
287+
if ( !preg_match('/^[a-zA-Z]:\/$/i', $path) && $path !== '/' ) {
288+
$path = rtrim($path, '/');
289+
}
290+
291+
// Handle empty path
292+
if ( empty($path) ) {
293+
$path = '/';
294+
}
295+
296+
return $path;
297+
}
298+
299+
/**
300+
* Validate path format.
301+
*
302+
* @access private
303+
* @param string $path
304+
* @return bool
305+
*/
306+
private function isValidPath(string $path) : bool
307+
{
308+
// Check for dangerous patterns
309+
if ( strpos($path, '..') !== false ) {
310+
return false;
311+
}
312+
313+
// Check for null bytes
314+
if ( strpos($path, "\0") !== false ) {
315+
return false;
316+
}
317+
318+
// Allow Windows drive letters (C:, D:, etc.) and standard path characters
319+
// Updated regex to support Windows paths with drive letters and colons
320+
return (bool)preg_match('/^[a-zA-Z]?:?[a-zA-Z0-9\/\\._\-\s]*$/', $path);
321+
}
322+
323+
/**
324+
* Sanitize filename.
325+
*
326+
* @access private
327+
* @param string $filename
328+
* @return string
329+
*/
330+
private function sanitizeFilename(string $filename) : string
331+
{
332+
// Remove dangerous characters
333+
$filename = preg_replace('/[^a-zA-Z0-9\._-]/', '', $filename);
334+
335+
// Remove multiple dots
336+
$filename = preg_replace('/\.+/', '.', $filename);
337+
338+
// Remove leading/trailing dots and dashes
339+
$filename = trim($filename, '.-');
340+
341+
return $filename ?: 'default';
342+
}
343+
344+
/**
345+
* Format message for logging.
346+
*
347+
* @access private
348+
* @param mixed $message
349+
* @param bool $isArray
350+
* @return string
351+
* @throws \InvalidArgumentException When message cannot be formatted
352+
*/
353+
private function formatMessage($message, bool $isArray = false) : string
354+
{
355+
if ( is_resource($message) ) {
356+
throw new \InvalidArgumentException('Cannot log resource type');
357+
}
358+
359+
if ( $isArray || is_array($message) || is_object($message) ) {
360+
return print_r($message, true);
361+
}
362+
363+
return (string)$message;
364+
}
365+
366+
/**
367+
* Validate log message.
368+
*
369+
* @access private
370+
* @param string $message
371+
* @return string
372+
* @throws \InvalidArgumentException When message is invalid
373+
*/
374+
private function validateMessage(string $message) : string
375+
{
376+
if ( empty($message) ) {
377+
throw new \InvalidArgumentException('Log message cannot be empty');
378+
}
379+
380+
// Remove null bytes and control characters (except newlines and tabs)
381+
$message = preg_replace('/[\x00-\x08\x0B-\x0C\x0E-\x1F]/', '', $message);
382+
383+
// Limit message length to prevent log file bloat
384+
if ( strlen($message) > 8192 ) {
385+
$message = substr($message, 0, 8189) . '...';
386+
}
387+
388+
return $message;
389+
}
390+
391+
/**
392+
* Validate log type.
393+
*
394+
* @access private
395+
* @param string $type
396+
* @return string
397+
* @throws \InvalidArgumentException When type is invalid
398+
*/
399+
private function validateLogType(string $type) : string
400+
{
401+
if ( empty($type) ) {
402+
throw new \InvalidArgumentException('Log type cannot be empty');
403+
}
404+
405+
// Sanitize log type
406+
$type = preg_replace('/[^a-zA-Z0-9_-]/', '', $type);
407+
408+
if ( empty($type) ) {
409+
throw new \InvalidArgumentException('Invalid log type format');
410+
}
411+
412+
return strtolower($type);
197413
}
198414
}

0 commit comments

Comments
 (0)