diff --git a/src/Phaseolies/Error/ErrorHandler.php b/src/Phaseolies/Error/ErrorHandler.php index c837c5cb..b31b8243 100644 --- a/src/Phaseolies/Error/ErrorHandler.php +++ b/src/Phaseolies/Error/ErrorHandler.php @@ -19,56 +19,95 @@ public static function handle(): void self::configureErrorReporting(); self::configureShutdownReporting(); - set_exception_handler(function ($exception) { - self::logException($exception); + set_exception_handler(function (Throwable $exception) { + $loggerException = self::logException($exception); + $activeException = $loggerException ?? $exception; - self::triggerBeforeException($exception); + self::triggerBeforeException($activeException); + self::dispatch($activeException); + }); + } - $handler = ErrorHandlerFactory::getSupportedHandler(); + /** + * Dispatch the exception to the appropriate handler. + * + * @param Throwable $exception + * @return void + */ + protected static function dispatch(Throwable $exception): void + { + $handler = ErrorHandlerFactory::getSupportedHandler(); - if ($handler) { - $handler->handle($exception); - } else { - self::handleFallback($exception); - } - }); + if ($handler) { + $handler->handle($exception); + } else { + self::handleFallback($exception); + } } /** * Log an exception using the application's logger. * * @param Throwable $exception - * @return void + * @return Throwable|null */ - protected static function logException(Throwable $exception): void + protected static function logException(Throwable $exception): ?Throwable { - $logMessage = "Error: " . $exception->getMessage(); - $logMessage .= "\nFile: " . $exception->getFile(); - $logMessage .= "\nLine: " . $exception->getLine(); - $logMessage .= "\nTrace: " . $exception->getTraceAsString(); - - app(LoggerService::class) - ->channel(env('LOG_CHANNEL', 'stack')) - ->error($logMessage); + try { + $logMessage = sprintf( + "Error: %s\nFile: %s\nLine: %d\nTrace: %s", + $exception->getMessage(), + $exception->getFile(), + $exception->getLine(), + $exception->getTraceAsString() + ); + + app(LoggerService::class) + ->channel(env('LOG_CHANNEL', 'stack')) + ->error($logMessage); + + return null; + } catch (Throwable $e) { + return new \RuntimeException( + sprintf( + 'Logger unavailable: %s | Original error: %s in %s on line %d', + $e->getMessage(), + $exception->getMessage(), + $exception->getFile(), + $exception->getLine() + ), + 0, + $e + ); + } } + /** + * Last resort fallback handler when no specific handler is available. + * + * @param Throwable $exception + * @return void + */ protected static function handleFallback(Throwable $exception): void { - abort($exception->getCode() ?: 500, "An error occurred. Please try again later."); + $code = $exception->getCode(); + $httpCode = ($code >= 400 && $code < 600) ? $code : 500; - exit(1); + abort($httpCode, "An error occurred. Please try again later."); } /** - * Default fallback handler if no specific handler supports the context. + * Configure PHP error handler to convert runtime errors to exceptions. * - * @param Throwable $exception * @return void */ protected static function configureErrorReporting(): void { - set_error_handler(function ($severity, $message, $file, $line) { - if (strpos($message, 'fsockopen():') === 0) { + set_error_handler(function (int $severity, string $message, string $file, int $line) { + if (!(error_reporting() & $severity)) { + return false; + } + if (str_starts_with($message, 'fsockopen():')) { return false; } @@ -81,7 +120,7 @@ protected static function configureErrorReporting(): void } /** - * Configure handling of PHP runtime warnings and minor errors. + * Configure shutdown handler for fatal errors that bypass set_error_handler * * @return void */ @@ -89,41 +128,70 @@ protected static function configureShutdownReporting(): void { register_shutdown_function(function () { $error = error_get_last(); - if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { - if (strpos($error['message'], 'fsockopen():') === 0) { - return; - } - if (ob_get_level() > 0) { - ob_end_clean(); - } + if (!$error || !in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { + return; + } + + if (str_starts_with($error['message'], 'fsockopen():')) { + return; + } - throw new \ErrorException("A fatal error occurred: " . $error['message']); + if (ob_get_level() > 0) { + ob_end_clean(); } + + $exception = new \ErrorException( + "A fatal error occurred: " . $error['message'], + 0, + $error['type'], + $error['file'], + $error['line'] + ); + + $loggerException = static::logException($exception); + $activeException = $loggerException ?? $exception; + + static::triggerBeforeException($activeException); + static::dispatch($activeException); }); } /** - * Executes the hook before the main error handling flow begins + * Executes the user-defined hook before the main error handling flow * * @param Throwable $exception * @return void */ protected static function triggerBeforeException(Throwable $exception): void { - $beforeExceptionClass = \App\Http\Exceptions\BeforeExceptionHandler::class; + try { + $beforeExceptionClass = \App\Http\Exceptions\BeforeExceptionHandler::class; + + if (!class_exists($beforeExceptionClass)) { + return; + } - if (class_exists($beforeExceptionClass)) { $before = app($beforeExceptionClass); - if ($before->supports()) { - $response = app(Response::class); - $statusCode = $exception instanceof \Phaseolies\Http\Exceptions\HttpException - ? $exception->getStatusCode() - : 500; - $response->setExceptionError($exception, $statusCode); - $before->handle($exception); + if (!$before->supports()) { + return; } + + $response = app(Response::class); + $statusCode = $exception instanceof \Phaseolies\Http\Exceptions\HttpException + ? $exception->getStatusCode() + : 500; + + $response->setExceptionError($exception, $statusCode); + $before->handle($exception); + } catch (Throwable $e) { + error_log(sprintf( + '[Doppar] BeforeExceptionHandler failed: %s in %s on line %d', + $e->getMessage(), + $e->getFile(), + $e->getLine() + )); } } } diff --git a/src/Phaseolies/Http/Controllers/Controller.php b/src/Phaseolies/Http/Controllers/Controller.php index 02686f76..1ff6bfbf 100644 --- a/src/Phaseolies/Http/Controllers/Controller.php +++ b/src/Phaseolies/Http/Controllers/Controller.php @@ -467,10 +467,13 @@ protected function compileAndCache(string $actual, string $cache): void // Make sure the file has proper permissions @chmod($tempFile, 0644); - // Atomic rename (on Unix systems) - if (!rename($tempFile, $cache)) { + // Atomic rename (on Unix/Mac), fallback copy for Windows + if (!@rename($tempFile, $cache)) { + if (!@copy($tempFile, $cache)) { + @unlink($tempFile); + throw new RuntimeException("Failed to move compiled view to cache: {$cache}"); + } @unlink($tempFile); - throw new RuntimeException("Failed to move compiled view to cache: {$cache}"); } // Verify the cache file was created successfully diff --git a/src/Phaseolies/Logger/Drivers/DailyLogHandler.php b/src/Phaseolies/Logger/Drivers/DailyLogHandler.php index 9148445d..c7c52232 100644 --- a/src/Phaseolies/Logger/Drivers/DailyLogHandler.php +++ b/src/Phaseolies/Logger/Drivers/DailyLogHandler.php @@ -17,16 +17,27 @@ class DailyLogHandler extends AbstractHandler implements LogHandlerInterface */ public function configureHandler(Logger $logger, string $channel): void { - $path = base_path('storage/logs'); + $path = base_path('storage' . DIRECTORY_SEPARATOR . 'logs'); if (!is_dir($path)) { - mkdir($path, 0775, true); + if (!mkdir($path, 0775, true) && !is_dir($path)) { + throw new \RuntimeException('Unable to create log directory: ' . $path); + } } - $logFile = $path . '/' . date('Y_m_d') . '_doppar.log'; + if (!is_writable($path)) { + throw new \RuntimeException('Log directory is not writable: ' . $path); + } + + $logFile = $path . DIRECTORY_SEPARATOR . date('Y_m_d') . '_doppar.log'; if (!is_file($logFile)) { - touch($logFile); + if (touch($logFile) === false) { + throw new \RuntimeException('Unable to create log file: ' . $logFile); + } + + // Ignored on Windows but correct for Mac/Ubuntu + chmod($logFile, 0664); } $this->handleConfiguration($logger, $logFile); diff --git a/src/Phaseolies/Logger/Drivers/DefaultLogHandler.php b/src/Phaseolies/Logger/Drivers/DefaultLogHandler.php index 2e96a700..fc4a4dd3 100644 --- a/src/Phaseolies/Logger/Drivers/DefaultLogHandler.php +++ b/src/Phaseolies/Logger/Drivers/DefaultLogHandler.php @@ -17,16 +17,26 @@ class DefaultLogHandler extends AbstractHandler implements LogHandlerInterface */ public function configureHandler(Logger $logger, string $channel): void { - $path = base_path('storage/logs'); + $path = base_path('storage' . DIRECTORY_SEPARATOR . 'logs'); if (!is_dir($path)) { - mkdir($path, 0775, true); + if (!mkdir($path, 0775, true) && !is_dir($path)) { + throw new \RuntimeException('Unable to create log directory: ' . $path); + } } - $logFile = $path . '/doppar.log'; + if (!is_writable($path)) { + throw new \RuntimeException('Log directory is not writable: ' . $path); + } + + $logFile = $path . DIRECTORY_SEPARATOR . 'doppar.log'; if (!is_file($logFile)) { - touch($logFile); + if (touch($logFile) === false) { + throw new \RuntimeException('Unable to create log file: ' . $logFile); + } + + chmod($logFile, 0664); } $this->handleConfiguration($logger, $logFile); diff --git a/src/Phaseolies/Logger/Drivers/SingleLogHandler.php b/src/Phaseolies/Logger/Drivers/SingleLogHandler.php index 0ad3513a..4da50118 100644 --- a/src/Phaseolies/Logger/Drivers/SingleLogHandler.php +++ b/src/Phaseolies/Logger/Drivers/SingleLogHandler.php @@ -17,16 +17,26 @@ class SingleLogHandler extends AbstractHandler implements LogHandlerInterface */ public function configureHandler(Logger $logger, string $channel): void { - $path = base_path('storage/logs'); + $path = base_path('storage' . DIRECTORY_SEPARATOR . 'logs'); if (!is_dir($path)) { - mkdir($path, 0775, true); + if (!mkdir($path, 0775, true) && !is_dir($path)) { + throw new \RuntimeException('Unable to create log directory: ' . $path); + } } - $logFile = $path . '/doppar.log'; + if (!is_writable($path)) { + throw new \RuntimeException('Log directory is not writable: ' . $path); + } + + $logFile = $path . DIRECTORY_SEPARATOR . 'doppar.log'; if (!is_file($logFile)) { - touch($logFile); + if (touch($logFile) === false) { + throw new \RuntimeException('Unable to create log file: ' . $logFile); + } + + chmod($logFile, 0664); } $this->handleConfiguration($logger, $logFile); diff --git a/src/Phaseolies/Support/Odo/OdoCache.php b/src/Phaseolies/Support/Odo/OdoCache.php index 03ef55d3..d7b4e8e8 100644 --- a/src/Phaseolies/Support/Odo/OdoCache.php +++ b/src/Phaseolies/Support/Odo/OdoCache.php @@ -27,13 +27,17 @@ trait OdoCache */ public function createCacheFolder(): void { - $actual = base_path() . '/' . $this->cacheFolder; + $actual = base_path() . DIRECTORY_SEPARATOR . $this->cacheFolder; if (!is_dir($actual)) { if (!mkdir($actual, 0755, true) && !is_dir($actual)) { throw new RuntimeException('Unable to create view cache folder: ' . $actual); } } + + if (!is_writable($actual)) { + throw new RuntimeException('View cache folder is not writable: ' . $actual); + } } /** @@ -43,13 +47,17 @@ public function createCacheFolder(): void */ public function createPublicSymlinkFolder(): void { - $actual = base_path() . '/' . $this->publicSymlinkFolder; + $actual = base_path() . DIRECTORY_SEPARATOR . $this->publicSymlinkFolder; if (!is_dir($actual)) { if (!mkdir($actual, 0755, true) && !is_dir($actual)) { throw new RuntimeException('Unable to create app/public cache folder: ' . $actual); } } + + if (!is_writable($actual)) { + throw new RuntimeException('App/public folder is not writable: ' . $actual); + } } /** @@ -60,12 +68,13 @@ public function createPublicSymlinkFolder(): void public function clearCache(): bool { $extension = ltrim($this->fileExtension, '.'); - $files = glob($this->cacheFolder . DIRECTORY_SEPARATOR . '*.' . $extension); + + $files = glob(str_replace('\\', '/', $this->cacheFolder) . '/*.' . $extension); $result = true; foreach ($files as $file) { - if (is_file($file)) { - $result = @unlink($file); + if (is_file($file) && !@unlink($file)) { + $result = false; } } @@ -79,7 +88,7 @@ public function clearCache(): bool */ public function setCacheFolder($path): void { - $this->cacheFolder = str_replace('/', DIRECTORY_SEPARATOR, $path); + $this->cacheFolder = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $path), DIRECTORY_SEPARATOR); } /** @@ -89,6 +98,6 @@ public function setCacheFolder($path): void */ public function setSymlinkPathFolder($path): void { - $this->publicSymlinkFolder = str_replace('/', DIRECTORY_SEPARATOR, $path); + $this->publicSymlinkFolder = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $path), DIRECTORY_SEPARATOR); } }