1818use FloatPHP \Interfaces \Classes \LoggerInterface ;
1919
2020/**
21- * Built-in logger class.
21+ * Built-in secure file-based class.
2222 */
2323class 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