From 27b0a4961788b0fd82dec8dfbce015fddec5b0b5 Mon Sep 17 00:00:00 2001 From: Yanik Kumar Date: Mon, 14 Jul 2025 10:19:29 +0530 Subject: [PATCH 1/2] Add configuration system with config() helper --- config/app.php | 38 ++++ config/cache.php | 22 ++ config/database.php | 39 ++++ config/mail.php | 22 ++ config/security.php | 21 ++ config/view.php | 18 ++ .../02/020e6b9cf5eab4bfb4e8dd65df61a25a.php | 112 ++++++++++ .../05/054bf4937f1119805a3984800c4be401.php | 138 +++++++++++++ .../5a/5a1471f9fd39d77bc2ab8ab23fd414f8.php | 193 ++++++++++++++++++ 9 files changed, 603 insertions(+) create mode 100644 config/app.php create mode 100644 config/cache.php create mode 100644 config/database.php create mode 100644 config/mail.php create mode 100644 config/security.php create mode 100644 config/view.php create mode 100644 public/false/02/020e6b9cf5eab4bfb4e8dd65df61a25a.php create mode 100644 public/false/05/054bf4937f1119805a3984800c4be401.php create mode 100644 public/false/5a/5a1471f9fd39d77bc2ab8ab23fd414f8.php diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..6f0da5a --- /dev/null +++ b/config/app.php @@ -0,0 +1,38 @@ + env('APP_NAME', 'SproutPHP'), + 'env' => env('APP_ENV', 'local'), + 'debug' => env('APP_DEBUG', true), + 'url' => env('APP_URL', 'http://localhost'), + 'timezone' => env('APP_TIMEZONE', 'UTC'), + 'locale' => env('APP_LOCALE', 'en'), + 'key' => env('APP_KEY', 'your-secret-key-here'), + + // Framework settings + 'version' => '1.0.0', + 'framework' => 'SproutPHP', + 'repo' => env('SPROUT_REPO', 'SproutPHP/framework'), + 'user_agent' => env('SPROUT_USER_AGENT', 'sproutphp-app'), + + // Global middleware + 'global_middleware' => [ + \App\Middlewares\VerifyCsrfToken::class, + \App\Middlewares\XssProtection::class, + ], + + // Session settings + 'session' => [ + 'driver' => env('SESSION_DRIVER', 'file'), + 'lifetime' => env('SESSION_LIFETIME', 120), + 'path' => env('SESSION_PATH', '/storage/sessions'), + ], + + // Logging + 'log' => [ + 'driver' => env('LOG_DRIVER', 'file'), + 'level' => env('LOG_LEVEL', 'debug'), + 'path' => env('LOG_PATH', '/storage/logs'), + ], +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..87001e4 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,22 @@ + env('CACHE_DRIVER', 'file'), + + 'stores' => [ + 'file' => [ + 'driver' => 'file', + 'path' => env('CACHE_PATH', '/storage/cache'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'port' => env('REDIS_PORT', 6379), + 'password' => env('REDIS_PASSWORD'), + 'database' => env('REDIS_DB', 0), + ], + ], + + 'prefix' => env('CACHE_PREFIX', 'sprout_'), +]; \ No newline at end of file diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..1c40782 --- /dev/null +++ b/config/database.php @@ -0,0 +1,39 @@ + env('DB_CONNECTION', 'mysql'), + + 'connections' => [ + 'mysql' => [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', 3306), + 'database' => env('DB_NAME', 'sprout'), + 'username' => env('DB_USER', 'root'), + 'password' => env('DB_PASS', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => env('DB_PREFIX', ''), + 'strict' => true, + 'engine' => null, + ], + + 'sqlite' => [ + 'driver' => 'sqlite', + 'database' => env('DB_DATABASE', '/storage/database.sqlite'), + 'prefix' => env('DB_PREFIX', ''), + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', 5432), + 'database' => env('DB_NAME', 'sprout'), + 'username' => env('DB_USER', 'postgres'), + 'password' => env('DB_PASS', ''), + 'charset' => 'utf8', + 'prefix' => env('DB_PREFIX', ''), + 'schema' => 'public', + ], + ] +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..f6c42dc --- /dev/null +++ b/config/mail.php @@ -0,0 +1,22 @@ + env('MAIL_DRIVER', 'smtp'), + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'noreply@sproutphp.com'), + 'name' => env('MAIL_FROM_NAME', 'SproutPHP'), + ], + 'drivers' => [ + 'smtp' => [ + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + ], + + 'sendmail' => [ + 'path'=>'/usr/sbin/sendmail -bs', + ] + ], +]; diff --git a/config/security.php b/config/security.php new file mode 100644 index 0000000..3e6af68 --- /dev/null +++ b/config/security.php @@ -0,0 +1,21 @@ + [ + 'enabled' => env('CSRF_ENABLED', true), + 'token_name' => env('CSRF_TOKEN_NAME', '_token'), + 'expire' => env('CSRF_EXPIRE', 3600), + ], + + 'xss' => [ + 'enabled' => env('XSS_PROTECTION', true), + 'mode' => env('XSS_MODE', 'block'), + ], + + 'cors' => [ + 'enabled' => env('CORS_ENABLED', false), + 'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '*')), + 'allowed_methods' => explode(',', env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,DELETE')), + 'allowed_headers' => explode(',', env('CORS_ALLOWED_HEADERS', 'Content-Type,Authorization')), + ], +]; \ No newline at end of file diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..f0e661b --- /dev/null +++ b/config/view.php @@ -0,0 +1,18 @@ + env('VIEW_ENGINE', 'twig'), + + 'twig' => [ + 'cache' => env('TWIG_CACHE', false), + 'debug' => env('TWIG_DEBUG', true), + 'auto_reload' => env('TWIG_AUTO_RELOAD', true), + 'strict_variables' => env('TWIG_STRICT_VARIABLES', false), + ], + + 'paths' => [ + 'views' => env('VIEW_PATH', '/app/Views'), + 'components' => env('COMPONENT_PATH', '/app/Views/components'), + 'layouts' => env('LAYOUT_PATH', '/app/Views/layouts'), + ], +]; diff --git a/public/false/02/020e6b9cf5eab4bfb4e8dd65df61a25a.php b/public/false/02/020e6b9cf5eab4bfb4e8dd65df61a25a.php new file mode 100644 index 0000000..8539fd7 --- /dev/null +++ b/public/false/02/020e6b9cf5eab4bfb4e8dd65df61a25a.php @@ -0,0 +1,112 @@ + + */ + private array $macros = []; + + public function __construct(Environment $env) + { + parent::__construct($env); + + $this->source = $this->getSourceContext(); + + $this->parent = false; + + $this->blocks = [ + ]; + } + + protected function doDisplay(array $context, array $blocks = []): iterable + { + $macros = $this->macros; + // line 2 + yield "
+\t +
+"; + yield from []; + } + + /** + * @codeCoverageIgnore + */ + public function getTemplateName(): string + { + return "components/navbar.twig"; + } + + /** + * @codeCoverageIgnore + */ + public function isTraitable(): bool + { + return false; + } + + /** + * @codeCoverageIgnore + */ + public function getDebugInfo(): array + { + return array ( 49 => 7, 42 => 2,); + } + + public function getSourceContext(): Source + { + return new Source("{# SproutPHP Component: navbar.twig #} +
+\t +
+", "components/navbar.twig", "D:\\01293\\SproutPHP\\app\\Views\\components\\navbar.twig"); + } +} diff --git a/public/false/05/054bf4937f1119805a3984800c4be401.php b/public/false/05/054bf4937f1119805a3984800c4be401.php new file mode 100644 index 0000000..6497cba --- /dev/null +++ b/public/false/05/054bf4937f1119805a3984800c4be401.php @@ -0,0 +1,138 @@ + + */ + private array $macros = []; + + public function __construct(Environment $env) + { + parent::__construct($env); + + $this->source = $this->getSourceContext(); + + $this->blocks = [ + 'title' => [$this, 'block_title'], + 'content' => [$this, 'block_content'], + ]; + } + + protected function doGetParent(array $context): bool|string|Template|TemplateWrapper + { + // line 1 + return "layouts/base.twig"; + } + + protected function doDisplay(array $context, array $blocks = []): iterable + { + $macros = $this->macros; + $this->parent = $this->load("layouts/base.twig", 1); + yield from $this->parent->unwrap()->yield($context, array_merge($this->blocks, $blocks)); + } + + // line 3 + /** + * @return iterable + */ + public function block_title(array $context, array $blocks = []): iterable + { + $macros = $this->macros; + yield "Home — SproutPHP"; + yield from []; + } + + // line 5 + /** + * @return iterable + */ + public function block_content(array $context, array $blocks = []): iterable + { + $macros = $this->macros; + // line 6 + yield "

"; + yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(($context["title"] ?? null), "html", null, true); + yield "

+

Welcome to SproutPHP, the seed-to-plant minimal PHP framework 🌱

+

HTMX-ready, JS-optional, and developer-happy.

+ + + +
+"; + yield from []; + } + + /** + * @codeCoverageIgnore + */ + public function getTemplateName(): string + { + return "home.twig"; + } + + /** + * @codeCoverageIgnore + */ + public function isTraitable(): bool + { + return false; + } + + /** + * @codeCoverageIgnore + */ + public function getDebugInfo(): array + { + return array ( 70 => 6, 63 => 5, 52 => 3, 41 => 1,); + } + + public function getSourceContext(): Source + { + return new Source("{% extends \"layouts/base.twig\" %} + +{% block title %}Home — SproutPHP{% endblock %} + +{% block content %} +

{{ title }}

+

Welcome to SproutPHP, the seed-to-plant minimal PHP framework 🌱

+

HTMX-ready, JS-optional, and developer-happy.

+ + + +
+{% endblock %} +", "home.twig", "D:\\01293\\SproutPHP\\app\\Views\\home.twig"); + } +} diff --git a/public/false/5a/5a1471f9fd39d77bc2ab8ab23fd414f8.php b/public/false/5a/5a1471f9fd39d77bc2ab8ab23fd414f8.php new file mode 100644 index 0000000..ec6a540 --- /dev/null +++ b/public/false/5a/5a1471f9fd39d77bc2ab8ab23fd414f8.php @@ -0,0 +1,193 @@ + + */ + private array $macros = []; + + public function __construct(Environment $env) + { + parent::__construct($env); + + $this->source = $this->getSourceContext(); + + $this->parent = false; + + $this->blocks = [ + 'title' => [$this, 'block_title'], + 'content' => [$this, 'block_content'], + ]; + } + + protected function doDisplay(array $context, array $blocks = []): iterable + { + $macros = $this->macros; + // line 1 + yield " + +\t +\t\t +\t\t +\t\t +\t\t\t"; + // line 7 + yield from $this->unwrap()->yieldBlock('title', $context, $blocks); + // line 10 + yield "\t\t +\t\t +\t\tenv->getRuntime('Twig\Runtime\EscaperRuntime')->escape(assets("css/sprout.min.css"), "html", null, true); + yield "\"> +\t\t +\t +\t +\t\t
+\t\t\t"; + // line 17 + yield from $this->load("components/navbar.twig", 17)->unwrap()->yield($context); + // line 18 + yield "\t\t\t"; + yield from $this->unwrap()->yieldBlock('content', $context, $blocks); + // line 19 + yield "\t\t
+ +\t\t"; + // line 21 + if ((config("app.env", "local") == "local")) { + // line 22 + yield "\t\t\t
+\t\t\t\t⚡ HTMX Active +\t\t\t\t
+\t\t\t\t📃 +\t\t\t\tHTMX Docs ↗ +\t\t\t
+\t\t"; + } + // line 29 + yield " +\t\t"; + // line 30 + if ((($tmp = ($context["app_debug"] ?? null)) && $tmp instanceof Markup ? (string) $tmp : $tmp)) { + // line 31 + yield "\t\t\t"; + yield (is_scalar($tmp = ($context["debugbar"] ?? null)) ? new Markup($tmp, $this->env->getCharset()) : $tmp); + yield " +\t\t"; + } + // line 33 + yield "\t + +"; + yield from []; + } + + // line 7 + /** + * @return iterable + */ + public function block_title(array $context, array $blocks = []): iterable + { + $macros = $this->macros; + // line 8 + yield "\t\t\t\t"; + yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(config("app.name", "SproutPHP"), "html", null, true); + yield " +\t\t\t"; + yield from []; + } + + // line 18 + /** + * @return iterable + */ + public function block_content(array $context, array $blocks = []): iterable + { + $macros = $this->macros; + yield from []; + } + + /** + * @codeCoverageIgnore + */ + public function getTemplateName(): string + { + return "layouts/base.twig"; + } + + /** + * @codeCoverageIgnore + */ + public function isTraitable(): bool + { + return false; + } + + /** + * @codeCoverageIgnore + */ + public function getDebugInfo(): array + { + return array ( 122 => 18, 114 => 8, 107 => 7, 100 => 33, 94 => 31, 92 => 30, 89 => 29, 80 => 22, 78 => 21, 74 => 19, 71 => 18, 69 => 17, 62 => 13, 58 => 12, 54 => 10, 52 => 7, 44 => 1,); + } + + public function getSourceContext(): Source + { + return new Source(" + +\t +\t\t +\t\t +\t\t +\t\t\t{% block title %} +\t\t\t\t{{ config('app.name', 'SproutPHP') }} +\t\t\t{% endblock %} +\t\t +\t\t +\t\t +\t\t +\t +\t +\t\t
+\t\t\t{% include 'components/navbar.twig' %} +\t\t\t{% block content %}{% endblock %} +\t\t
+ +\t\t{% if config('app.env', 'local') == 'local' %} +\t\t\t
+\t\t\t\t⚡ HTMX Active +\t\t\t\t
+\t\t\t\t📃 +\t\t\t\tHTMX Docs ↗ +\t\t\t
+\t\t{% endif %} + +\t\t{% if app_debug %} +\t\t\t{{ debugbar|raw }} +\t\t{% endif %} +\t + +", "layouts/base.twig", "D:\\01293\\SproutPHP\\app\\Views\\layouts\\base.twig"); + } +} From 2135c8a71a30f1ff940e5cb5f216f0a4c3f51fbe Mon Sep 17 00:00:00 2001 From: Yanik Kumar Date: Mon, 14 Jul 2025 10:27:10 +0530 Subject: [PATCH 2/2] Configuration feature with all fixes --- .env.example | 34 +++++ CONFIGURATION.md | 155 ++++++++++++++++++++ app/Controllers/HomeController.php | 3 +- app/Middlewares/XssProtection.php | 37 ++++- app/Views/layouts/base.twig | 4 +- config/security.php | 8 +- core/Bootstrap/bootstrap.php | 20 ++- core/Database/DB.php | 22 ++- core/Error/ErrorHandler.php | 2 +- core/Http/Middleware/MiddlewareKernel.php | 16 +- core/Http/Middleware/MiddlewareRegistry.php | 10 ++ core/Routing/Router.php | 2 +- core/Support/helpers.php | 38 ++++- core/View/View.php | 15 +- routes/web.php | 37 ++++- 15 files changed, 373 insertions(+), 30 deletions(-) create mode 100644 CONFIGURATION.md diff --git a/.env.example b/.env.example index c87c8f4..ce89773 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,44 @@ +# App +APP_NAME=SproutPHP APP_ENV=local APP_DEBUG=true APP_URL=http://localhost:9090 +APP_TIMEZONE=UTC +APP_LOCALE=en +APP_KEY=your-secret-key-here + +# Repo Info For Versioning SPROUT_REPO=SproutPHP/framework SPROUT_USER_AGENT=sproutphp-app +# Database +DB_CONNECTION=mysql DB_HOST=localhost +DB_PORT=3306 DB_NAME=sprout DB_USER=root DB_PASS= + +# Mail +MAIL_DRIVER=smtp +MAIL_HOST=smtp.mailgun.org +MAIL_PORT=587 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_ENCRYPTION=tls +MAIL_FROM_ADDRESS=noreply@sproutphp.com +MAIL_FROM_NAME=SproutPHP + +# Cache +CACHE_DRIVER=file +CACHE_PATH=/storage/cache + +# Security +CSRF_ENABLED=true +XSS_PROTECTION=true +CORS_ENABLED=false + +# View +VIEW_ENGINE=twig +TWIG_CACHE=false +TWIG_DEBUG=true \ No newline at end of file diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..609a1ba --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,155 @@ +# SproutPHP Configuration Guide + +This document outlines all available configuration options for SproutPHP framework. + +## Environment Variables (.env file) + +### Application Configuration +```env +APP_NAME=SproutPHP +APP_ENV=local +APP_DEBUG=true +APP_URL=http://localhost:9090 +APP_TIMEZONE=UTC +APP_LOCALE=en +APP_KEY=your-secret-key-here +``` + +### Database Configuration +```env +DB_CONNECTION=mysql +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=sprout +DB_USER=root +DB_PASS= +DB_PREFIX= +``` + +### Security Configuration +```env +# CSRF Protection +CSRF_ENABLED=true +CSRF_TOKEN_NAME=_token +CSRF_EXPIRE=3600 + +# XSS Protection +XSS_PROTECTION=true +XSS_MODE=block # 'block', 'sanitize', or '0' to disable + +# Content Security Policy +CSP_ENABLED=true +CSP_REPORT_ONLY=false +CSP_REPORT_URI= + +# CORS +CORS_ENABLED=false +CORS_ALLOWED_ORIGINS=* +CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE +CORS_ALLOWED_HEADERS=Content-Type,Authorization +``` + +### Mail Configuration +```env +MAIL_DRIVER=smtp +MAIL_HOST=smtp.mailgun.org +MAIL_PORT=587 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_ENCRYPTION=tls +MAIL_FROM_ADDRESS=noreply@sproutphp.com +MAIL_FROM_NAME=SproutPHP +``` + +### Cache Configuration +```env +CACHE_DRIVER=file +CACHE_PATH=/storage/cache +``` + +### Session Configuration +```env +SESSION_DRIVER=file +SESSION_LIFETIME=120 +SESSION_PATH=/storage/sessions +``` + +### Logging Configuration +```env +LOG_DRIVER=file +LOG_LEVEL=debug +LOG_PATH=/storage/logs +``` + +### View Configuration +```env +VIEW_ENGINE=twig +TWIG_CACHE=false +TWIG_DEBUG=true +TWIG_AUTO_RELOAD=true +TWIG_STRICT_VARIABLES=false +``` + +### Framework Configuration +```env +SPROUT_REPO=SproutPHP/framework +SPROUT_USER_AGENT=sproutphp-app +``` + +## Configuration Usage + +### In PHP Code +```php +// Get app name +$appName = config('app.name'); + +// Get database host +$dbHost = config('database.connections.mysql.host'); + +// Check if XSS protection is enabled +$xssEnabled = config('security.xss.enabled'); + +// Get CSP mode +$cspMode = config('security.csp.report_only'); +``` + +### In Twig Templates +```twig +{# Get app name #} +

{{ config('app.name') }}

+ +{# Check environment #} +{% if config('app.env') == 'local' %} +
Development Mode
+{% endif %} +``` + +## Security Features + +### XSS Protection +The framework automatically adds XSS protection headers based on your configuration: + +- **Development**: Relaxed CSP policy allowing inline styles and external images +- **Production**: Strict CSP policy for maximum security + +### CSRF Protection +CSRF tokens are automatically generated and validated for state-changing requests (POST, PUT, PATCH, DELETE). + +### Content Security Policy +CSP headers are automatically set based on your environment: +- **Local/Debug**: Allows `unsafe-inline` for styles and external images +- **Production**: Strict policy with no unsafe directives + +## Environment-Specific Behavior + +### Local Environment +- Debug information displayed +- Relaxed security policies +- Detailed error messages +- HTMX debug indicator + +### Production Environment +- No debug information +- Strict security policies +- Generic error pages +- Optimized performance settings \ No newline at end of file diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php index 390018b..bf152d8 100644 --- a/app/Controllers/HomeController.php +++ b/app/Controllers/HomeController.php @@ -7,7 +7,8 @@ class HomeController public function index() { $release = getLatestRelease(); + $appName = config('app.name', 'SproutPHP'); return "

A minimilistic php-framework designed for go-to developer without the need for javascript or heavy modules. 🌳

- SproutPHP latest release: $release"; + {$appName} latest release: $release"; } } diff --git a/app/Middlewares/XssProtection.php b/app/Middlewares/XssProtection.php index 2e83d27..98f84a4 100644 --- a/app/Middlewares/XssProtection.php +++ b/app/Middlewares/XssProtection.php @@ -11,17 +11,40 @@ public function handle(Request $request, callable $next) { $response = $next($request); - header("X-XSS-Protection: 1; mode=block"); + // Get security configuration + $xssEnabled = config('security.xss.enabled', true); + $xssMode = config('security.xss.mode', 'block'); + + if ($xssEnabled) { + header("X-XSS-Protection: 1; mode={$xssMode}"); + } + header("X-Content-Type-Options: nosniff"); - // Set relaxed ContentSecurityPolicy in development, strict in production - if ((function_exists('env') && env('APP_ENV') === 'local') || (function_exists('env') && env('APP_DEBUG') === 'true')) { - header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https://img.shields.io; object-src 'none';"); - } else { - // Production: strict ContentSecurityPolicy - header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none';"); + // Set Content Security Policy based on environment + $cspEnabled = config('security.csp.enabled', true); + if ($cspEnabled) { + $cspPolicy = $this->getCspPolicy(); + header("Content-Security-Policy: {$cspPolicy}"); } return $response; } + + private function getCspPolicy(): string + { + $env = config('app.env', 'local'); + $debug = config('app.debug', false); + + // Base CSP policy + $basePolicy = "default-src 'self'; script-src 'self'; object-src 'none';"; + + if ($env === 'local' || $debug) { + // Development: relaxed policy + return $basePolicy . " style-src 'self' 'unsafe-inline'; img-src 'self' https://img.shields.io;"; + } else { + // Production: strict policy + return $basePolicy . " style-src 'self'; img-src 'self';"; + } + } } \ No newline at end of file diff --git a/app/Views/layouts/base.twig b/app/Views/layouts/base.twig index d169885..550398f 100644 --- a/app/Views/layouts/base.twig +++ b/app/Views/layouts/base.twig @@ -5,7 +5,7 @@ {% block title %} - SproutPHP + {{ config('app.name', 'SproutPHP') }} {% endblock %} @@ -18,7 +18,7 @@ {% block content %}{% endblock %} - {% if env('APP_ENV') == 'local' %} + {% if config('app.env', 'local') == 'local' %}
⚡ HTMX Active
diff --git a/config/security.php b/config/security.php index 3e6af68..1c7d135 100644 --- a/config/security.php +++ b/config/security.php @@ -9,7 +9,13 @@ 'xss' => [ 'enabled' => env('XSS_PROTECTION', true), - 'mode' => env('XSS_MODE', 'block'), + 'mode' => env('XSS_MODE', 'block'), // 'block', 'sanitize', or '0' to disable + ], + + 'csp' => [ + 'enabled' => env('CSP_ENABLED', true), + 'report_only' => env('CSP_REPORT_ONLY', false), + 'report_uri' => env('CSP_REPORT_URI', null), ], 'cors' => [ diff --git a/core/Bootstrap/bootstrap.php b/core/Bootstrap/bootstrap.php index 23fd459..7c0ae5a 100644 --- a/core/Bootstrap/bootstrap.php +++ b/core/Bootstrap/bootstrap.php @@ -23,13 +23,29 @@ */ LoadRoutes::boot(); +/** + * Test config loading + */ +if (!function_exists('config')) { + throw new \Exception("Config helper function not found. Check if helpers.php is loaded."); +} + /** * Gloabl Middlewares */ -$globalMiddleware = [ +$globalMiddleware = config('app.global_middleware', [ VerifyCsrfToken::class, XssProtection::class, -]; +]); + +// Debug middleware loading +if (config('app.debug', false)) { + foreach ($globalMiddleware as $middleware) { + if (!class_exists($middleware)) { + throw new \Exception("Middleware class '$middleware' not found. Check autoloading."); + } + } +} $kernel = new MiddlewareKernel($globalMiddleware); $response = $kernel->handle($request, function ($request) use ($router) { diff --git a/core/Database/DB.php b/core/Database/DB.php index 223cf05..65962a6 100644 --- a/core/Database/DB.php +++ b/core/Database/DB.php @@ -14,14 +14,24 @@ public static function connection() { if (self::$pdo) return self::$pdo; - $host = env('DB_HOST', 'localhost'); - $db = env('DB_NAME', 'sprout'); - $user = env('DB_USER', 'root'); - $pass = env('DB_PASS', ''); - $dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4"; + $connection = config('database.default', 'mysql'); + $config = config("database.connections.$connection"); + + if (!$config) { + die("❌ Database connection '$connection' not found in config."); + } + + $host = $config['host'] ?? 'localhost'; + $port = $config['port'] ?? 3306; + $database = $config['database'] ?? 'sprout'; + $username = $config['username'] ?? 'root'; + $password = $config['password'] ?? ''; + $charset = $config['charset'] ?? 'utf8mb4'; + + $dsn = "mysql:host=$host;port=$port;dbname=$database;charset=$charset"; try { - self::$pdo = new PDO($dsn, $user, $pass); + self::$pdo = new PDO($dsn, $username, $password); self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return self::$pdo; } catch (PDOException $ex) { diff --git a/core/Error/ErrorHandler.php b/core/Error/ErrorHandler.php index 8672129..5d07f37 100644 --- a/core/Error/ErrorHandler.php +++ b/core/Error/ErrorHandler.php @@ -36,7 +36,7 @@ public static function handleFatal() public static function render($message, $file, $line, $trace = null, $code = 500) { - if (env('APP_ENV') === 'local' && env('APP_DEBUG') === 'true') { + if (config('app.env', 'local') === 'local' && config('app.debug', false)) { echo "
"; echo "

🍂 SproutPHP Error

"; echo "Message: $message
"; diff --git a/core/Http/Middleware/MiddlewareKernel.php b/core/Http/Middleware/MiddlewareKernel.php index 8bf2781..6a0bf28 100644 --- a/core/Http/Middleware/MiddlewareKernel.php +++ b/core/Http/Middleware/MiddlewareKernel.php @@ -17,7 +17,21 @@ public function handle(Request $request, callable $coreHandler) { $handler = array_reduce( array_reverse($this->middlewares), - fn($next, $middleware) => fn($req) => (new $middleware())->handle($req, $next), + function($next, $middleware) { + return function($req) use ($next, $middleware) { + // Validate middleware class + if (!is_string($middleware) || !class_exists($middleware)) { + throw new \Exception("Invalid middleware class: " . (is_string($middleware) ? $middleware : gettype($middleware))); + } + + // Check if middleware implements the interface + if (!is_subclass_of($middleware, MiddlewareInterface::class)) { + throw new \Exception("Middleware class '$middleware' must implement MiddlewareInterface"); + } + + return (new $middleware())->handle($req, $next); + }; + }, $coreHandler ); diff --git a/core/Http/Middleware/MiddlewareRegistry.php b/core/Http/Middleware/MiddlewareRegistry.php index 33e6741..f38cadd 100644 --- a/core/Http/Middleware/MiddlewareRegistry.php +++ b/core/Http/Middleware/MiddlewareRegistry.php @@ -10,4 +10,14 @@ class MiddlewareRegistry 'route-test-mw' => \App\Middlewares\RouteTestMiddleware::class, // Register your middlewares here ]; + + public static function get($alias) + { + return self::$map[$alias] ?? null; + } + + public static function register($alias, $class) + { + self::$map[$alias] = $class; + } } \ No newline at end of file diff --git a/core/Routing/Router.php b/core/Routing/Router.php index d852fb3..3222ef3 100644 --- a/core/Routing/Router.php +++ b/core/Routing/Router.php @@ -50,7 +50,7 @@ public function dispatch(Request $request) if (!isset($this->routes[$method][$uri])) { http_response_code(404); - if (env('APP_ENV') === 'local') { + if (config('app.env', 'local') === 'local') { echo "
"; echo "

404: Route not found

"; echo "
"; diff --git a/core/Support/helpers.php b/core/Support/helpers.php index 5f4f987..48b89e1 100644 --- a/core/Support/helpers.php +++ b/core/Support/helpers.php @@ -111,8 +111,8 @@ function abort(int $code, string $message = '') if (!function_exists('getLatestRelease')) { function getLatestRelease() { - $repo = env('SPROUT_REPO', 'SproutPHP/framework'); - $userAgent = env('SPROUT_USER_AGENT', 'sproutphp-app'); + $repo = config('app.repo', 'SproutPHP/framework'); + $userAgent = config('app.user_agent', 'sproutphp-app'); $url = "https://api.github.com/repos/$repo/releases"; $opts = [ "http" => [ @@ -164,3 +164,37 @@ function csrf_field() return ''; } } + +/** + * Config Helper + */ +if (!function_exists('config')) { + function config($key, $default = null) { + static $configs = []; + + $segments = explode('.', $key); + $file = $segments[0]; + + if (!isset($configs[$file])) { + $configPath = __DIR__ . '/../../config/' . $file . '.php'; + if (file_exists($configPath)) { + $configs[$file] = require $configPath; + } else { + return $default; + } + } + + $value = $configs[$file]; + array_shift($segments); // Remove the file name from segments + + foreach ($segments as $segment) { + if (is_array($value) && array_key_exists($segment, $value)) { + $value = $value[$segment]; + } else { + return $default; + } + } + + return $value; + } +} \ No newline at end of file diff --git a/core/View/View.php b/core/View/View.php index e9c9405..5889d9e 100644 --- a/core/View/View.php +++ b/core/View/View.php @@ -13,10 +13,15 @@ class View public static function init() { - $loader = new FilesystemLoader(__DIR__ . '/../../app/Views'); + $viewsPath = __DIR__ . '/../../app/Views'; + $loader = new FilesystemLoader($viewsPath); + + $twigConfig = config('view.twig', []); self::$twig = new Environment($loader, [ - 'cache' => false, - 'debug' => true, + 'cache' => $twigConfig['cache'] ?? false, + 'debug' => $twigConfig['debug'] ?? true, + 'auto_reload' => $twigConfig['auto_reload'] ?? true, + 'strict_variables' => $twigConfig['strict_variables'] ?? false, ]); // Register global helper functions like assets() debug() if it exists as Twig doesn't have direct access to PHP global functions by default @@ -51,7 +56,7 @@ public static function render($template, $data = []) } // Check if this is an AJAX/HTMX request - if (Debugbar::isAjaxRequest() && env('APP_DEBUG') === 'true') { + if (Debugbar::isAjaxRequest() && config('app.debug', false)) { // Reset debugbar for this request Debugbar::resetForRequest(); @@ -66,7 +71,7 @@ public static function render($template, $data = []) } // Regular request handling - if (env('APP_DEBUG') === 'true') { + if (config('app.debug', false)) { $data['debugbar'] = Debugbar::render(); $data['app_debug'] = true; } diff --git a/routes/web.php b/routes/web.php index 76005f7..46bfa9f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -22,8 +22,43 @@ Route::delete('/delete', function () { return 'DELETE received'; }); +Route::get('/security-test', function () { + echo "

Security Configuration Test

"; + echo "

XSS Protection: " . (config('security.xss.enabled') ? 'Enabled' : 'Disabled') . "

"; + echo "

XSS Mode: " . config('security.xss.mode') . "

"; + echo "

CSP Enabled: " . (config('security.csp.enabled') ? 'Enabled' : 'Disabled') . "

"; + echo "

CSP Report Only: " . (config('security.csp.report_only') ? 'Yes' : 'No') . "

"; + echo "

Environment: " . config('app.env') . "

"; + echo "

Debug Mode: " . (config('app.debug') ? 'Enabled' : 'Disabled') . "

"; + + // Test inline styles (should work in local, blocked in production) + echo "

This text should be red in local environment

"; + + // Test external image (should work in local, blocked in production) + echo "GitHub Stars"; +}); + +Route::get('/debug-config', function () { + echo "

Config Debug

"; + echo "

Config function exists: " . (function_exists('config') ? 'Yes' : 'No') . "

"; + echo "

App config loaded: " . (config('app.name') ? 'Yes' : 'No') . "

"; + echo "

Global middleware count: " . count(config('app.global_middleware', [])) . "

"; + foreach (config('app.global_middleware', []) as $middleware) { + echo "

Middleware: $middleware (exists: " . (class_exists($middleware) ? 'Yes' : 'No') . ")

"; + } +}); + +Route::get('/config-test', function () { + echo "

Configuration Test

"; + echo "

App Name: " . config('app.name') . "

"; + echo "

Environment: " . config('app.env') . "

"; + echo "

Debug: " . (config('app.debug') ? 'true' : 'false') . "

"; + echo "

Database Host: " . config('database.connections.mysql.host') . "

"; + echo "

Twig Cache: " . (config('view.twig.cache') ? 'true' : 'false') . "

"; +}); + Route::get('/envtest', function () { - debug(env('APP_ENV', 'default_env')); + debug(config('app.env', 'default_env')); }); Route::get('/crash', function () { $a = 10 / 0; // Division by zero