From fd463c4e933762f985c6cf3c2fc6f76617bb6e62 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 19 Oct 2025 09:18:29 -0700 Subject: [PATCH 01/18] Fixed links in readme. Signed-off-by: Joshua Parker --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f69fe2e..497edeb 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ composer require codefyphp/codefy ## 📘 Documentation Documentation is still a work in progress. Between the [Qubus Components](https://docs.qubusphp.com/) documentation -and [CodefyPHP's](https://codefyphp.com/documentation/) documentation, that should help you get started. If you have questions or -need help, feel free to ask for help in the [forums](https://codefyphp.com/community/). +and [CodefyPHP's](https://codefyphp.com/docs/) documentation, that should help you get started. If you have questions or +need help, feel free to ask for help in the [forums](https://forum.codefyphp.com/). ## 🙌 Sponsors From 83b15f965679f94f3165041b2fd27269a6ea0f5e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 27 Oct 2025 07:51:41 -0700 Subject: [PATCH 02/18] Added gravatar package. Signed-off-by: Joshua Parker --- composer.json | 1 + src/Helpers/core.php | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/composer.json b/composer.json index e3b592a..261da9e 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "codefyphp/domain-driven-core": "^3", "composer/class-map-generator": "^1", "dragonmantank/cron-expression": "^3", + "forxer/gravatar": "^5.0", "melbahja/seo": "^2", "middlewares/cache": "^3", "middlewares/minifier": "^2", diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 13ddfb8..5be348f 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -23,6 +23,8 @@ use Codefy\QueryBus\Query; use Codefy\QueryBus\Resolvers\NativeQueryHandlerResolver; use Codefy\QueryBus\UnresolvableQueryHandlerException; +use Gravatar\Image; +use Gravatar\Profile; use Qubus\Exception\Exception; use Qubus\Expressive\Connection; use Qubus\Expressive\QueryBuilder; @@ -427,3 +429,25 @@ function queue(ShouldQueue $queue): NodeQueue { return new NodeQueue($queue); } + +/** + * Return a new Gravatar Image instance. + * + * @param string|null $email + * @return Image + */ +function gravatar(?string $email = null): Image +{ + return new Image($email); +} + +/** + * Return a new Gravatar Profile instance. + * + * @param string|null $email + * @return Profile + */ +function gravatar_profile(?string $email = null): Profile +{ + return new Profile($email); +} From 628cc97fbfcbec91aa96eaeb26ce08d4e0df6ea6 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 09:41:54 -0800 Subject: [PATCH 03/18] Added Exception Middleware. Signed-off-by: Joshua Parker --- src/Http/Middleware/ExceptionMiddleware.php | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Http/Middleware/ExceptionMiddleware.php diff --git a/src/Http/Middleware/ExceptionMiddleware.php b/src/Http/Middleware/ExceptionMiddleware.php new file mode 100644 index 0000000..1f84918 --- /dev/null +++ b/src/Http/Middleware/ExceptionMiddleware.php @@ -0,0 +1,49 @@ +handle($request); + } catch (HttpException $e) { + //$response = new Response($this->app->flash->error($e->getMessage()), $e->getStatusCode(), $e->getHeaders()); + return RedirectResponseFactory::create( + uri: $e->getUri() ?? $request->getServerParams()['HTTP_REFERER'] ?? '/', + status: $e->getStatusCode(), + headers: $e->getHeaders() + )->withBody(new Stream($this->app->flash->error($e->getMessage()))); + } catch (Throwable $t) { + //$response = new Response($this->app->flash->error('Internal Error'), 500, []); + return RedirectResponseFactory::create( + uri: $request->getServerParams()['HTTP_REFERER'] ?? '/', + status: 500, + headers: [] + )->withBody(new Stream($this->app->flash->error('Internal Error'))); + } + + return $response; + } +} From 35054de8a59bcebe8f5866e3c6a8a8c5ecf84f33 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 11:34:49 -0800 Subject: [PATCH 04/18] Updated exception middleware to log exceptions. Signed-off-by: Joshua Parker --- src/Http/Middleware/ExceptionMiddleware.php | 38 +++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Http/Middleware/ExceptionMiddleware.php b/src/Http/Middleware/ExceptionMiddleware.php index 1f84918..9658e67 100644 --- a/src/Http/Middleware/ExceptionMiddleware.php +++ b/src/Http/Middleware/ExceptionMiddleware.php @@ -10,9 +10,11 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Qubus\Error\Handlers\Psr3ErrorHandler; +use Qubus\Exception\Data\TypeException; use Qubus\Exception\Http\HttpException; use Qubus\Http\Factories\RedirectResponseFactory; -use Qubus\Http\Response; +use ReflectionException; use Throwable; class ExceptionMiddleware implements MiddlewareInterface @@ -29,14 +31,34 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface try { $response = $handler->handle($request); } catch (HttpException $e) { - //$response = new Response($this->app->flash->error($e->getMessage()), $e->getStatusCode(), $e->getHeaders()); + $this->logException( + $e, + [ + 'uri' => $e->getUri(), + 'code' => $e->getStatusCode(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'method' => $request->getMethod(), + 'message' => $e->getMessage(), + ] + ); + return RedirectResponseFactory::create( uri: $e->getUri() ?? $request->getServerParams()['HTTP_REFERER'] ?? '/', status: $e->getStatusCode(), headers: $e->getHeaders() )->withBody(new Stream($this->app->flash->error($e->getMessage()))); } catch (Throwable $t) { - //$response = new Response($this->app->flash->error('Internal Error'), 500, []); + $this->logException( + $t, + [ + 'message' => $t->getMessage(), + 'file' => $t->getFile(), + 'line' => $t->getLine(), + 'previous message' => $t->getPrevious()->getMessage() + ] + ); + return RedirectResponseFactory::create( uri: $request->getServerParams()['HTTP_REFERER'] ?? '/', status: 500, @@ -46,4 +68,14 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $response; } + + /** + * @throws ReflectionException + * @throws TypeException + */ + protected function logException(Throwable $t, array $context = []): void + { + $psrLogger = new Psr3ErrorHandler($this->app->getLogger()); + $psrLogger->handle($t, $context); + } } From d5457f556b1909d5c4bf6402a115198d2846fd69 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 12:16:08 -0800 Subject: [PATCH 05/18] Fixed exception middleware to catch any and all exceptions. Signed-off-by: Joshua Parker --- src/Http/Middleware/ExceptionMiddleware.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Http/Middleware/ExceptionMiddleware.php b/src/Http/Middleware/ExceptionMiddleware.php index 9658e67..70bf4ea 100644 --- a/src/Http/Middleware/ExceptionMiddleware.php +++ b/src/Http/Middleware/ExceptionMiddleware.php @@ -48,6 +48,22 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface status: $e->getStatusCode(), headers: $e->getHeaders() )->withBody(new Stream($this->app->flash->error($e->getMessage()))); + } catch (\Exception $e) { + $this->logException( + $e, + [ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'previous message' => $e->getPrevious()->getMessage() + ] + ); + + return RedirectResponseFactory::create( + uri: $request->getServerParams()['HTTP_REFERER'] ?? '/', + status: 500, + headers: [] + )->withBody(new Stream($this->app->flash->error('Internal Error'))); } catch (Throwable $t) { $this->logException( $t, From 72d54029e28172de5c92bccf9456a2591acf404b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 14:07:56 -0800 Subject: [PATCH 06/18] Fixed issues with Exception middleware. Signed-off-by: Joshua Parker --- src/Http/Middleware/ExceptionMiddleware.php | 29 +++++---------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/Http/Middleware/ExceptionMiddleware.php b/src/Http/Middleware/ExceptionMiddleware.php index 70bf4ea..1fc6521 100644 --- a/src/Http/Middleware/ExceptionMiddleware.php +++ b/src/Http/Middleware/ExceptionMiddleware.php @@ -5,7 +5,6 @@ namespace Codefy\Framework\Http\Middleware; use Codefy\Framework\Application; -use Laminas\Diactoros\Stream; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -13,6 +12,7 @@ use Qubus\Error\Handlers\Psr3ErrorHandler; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Http\HttpException; +use Qubus\Exception\Http\Psr7Exception; use Qubus\Http\Factories\RedirectResponseFactory; use ReflectionException; use Throwable; @@ -30,7 +30,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { try { $response = $handler->handle($request); - } catch (HttpException $e) { + } catch (HttpException | Psr7Exception $e) { + $this->app->flash->error($e->getMessage()); + $this->logException( $e, [ @@ -45,41 +47,24 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return RedirectResponseFactory::create( uri: $e->getUri() ?? $request->getServerParams()['HTTP_REFERER'] ?? '/', - status: $e->getStatusCode(), headers: $e->getHeaders() - )->withBody(new Stream($this->app->flash->error($e->getMessage()))); - } catch (\Exception $e) { - $this->logException( - $e, - [ - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'previous message' => $e->getPrevious()->getMessage() - ] ); - - return RedirectResponseFactory::create( - uri: $request->getServerParams()['HTTP_REFERER'] ?? '/', - status: 500, - headers: [] - )->withBody(new Stream($this->app->flash->error('Internal Error'))); } catch (Throwable $t) { + $this->app->flash->error('Internal Error'); + $this->logException( $t, [ 'message' => $t->getMessage(), 'file' => $t->getFile(), 'line' => $t->getLine(), - 'previous message' => $t->getPrevious()->getMessage() ] ); return RedirectResponseFactory::create( uri: $request->getServerParams()['HTTP_REFERER'] ?? '/', - status: 500, headers: [] - )->withBody(new Stream($this->app->flash->error('Internal Error'))); + ); } return $response; From d719a032f038c08ff53b6dceed49ff3ee3c04cb6 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 14:08:46 -0800 Subject: [PATCH 07/18] Added throwable function. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 5be348f..3fc2725 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -29,6 +29,7 @@ use Qubus\Expressive\Connection; use Qubus\Expressive\QueryBuilder; use ReflectionException; +use RuntimeException; use function dirname; use function error_log; @@ -451,3 +452,24 @@ function gravatar_profile(?string $email = null): Profile { return new Profile($email); } + +/** + * Throw the given exception if the given condition is true. + * + * @param mixed $condition + * @param string $exception + * @param ...$parameters + * @return mixed + */ +function throw_if(mixed $condition, string $exception = RuntimeException::class, ...$parameters): mixed +{ + if ($condition) { + if (is_string($exception) && class_exists($exception)) { + $exception = new $exception(...$parameters); + } + + throw is_string($exception) ? new RuntimeException($exception) : $exception; + } + + return $condition; +} From 9ecc062da8799c77b74c341790263488cb6a23be Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 14:49:33 -0800 Subject: [PATCH 08/18] Updated throw_if exception parameter. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 3fc2725..9169079 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -461,7 +461,7 @@ function gravatar_profile(?string $email = null): Profile * @param ...$parameters * @return mixed */ -function throw_if(mixed $condition, string $exception = RuntimeException::class, ...$parameters): mixed +function throw_if(mixed $condition, string $exception = '\RuntimeException', ...$parameters): mixed { if ($condition) { if (is_string($exception) && class_exists($exception)) { From 483b2c86d1c38f66093a91c05d5ce06b689db3e0 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 15:38:35 -0800 Subject: [PATCH 09/18] Reverted change of throw_if exception parameter. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 9169079..3fc2725 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -461,7 +461,7 @@ function gravatar_profile(?string $email = null): Profile * @param ...$parameters * @return mixed */ -function throw_if(mixed $condition, string $exception = '\RuntimeException', ...$parameters): mixed +function throw_if(mixed $condition, string $exception = RuntimeException::class, ...$parameters): mixed { if ($condition) { if (is_string($exception) && class_exists($exception)) { From 3843ff9fca9251ac7377c01f1cfc0bff11325cde Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 22:16:39 -0800 Subject: [PATCH 10/18] Relocated ddd commands. Signed-off-by: Joshua Parker --- src/Console/Commands/{ => Domain}/UlidCommand.php | 2 +- src/Console/Commands/{ => Domain}/UuidCommand.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Console/Commands/{ => Domain}/UlidCommand.php (94%) rename src/Console/Commands/{ => Domain}/UuidCommand.php (94%) diff --git a/src/Console/Commands/UlidCommand.php b/src/Console/Commands/Domain/UlidCommand.php similarity index 94% rename from src/Console/Commands/UlidCommand.php rename to src/Console/Commands/Domain/UlidCommand.php index 1d69698..da8049d 100644 --- a/src/Console/Commands/UlidCommand.php +++ b/src/Console/Commands/Domain/UlidCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Codefy\Framework\Console\Commands; +namespace Codefy\Framework\Console\Commands\Domain; use Codefy\Framework\Application; use Codefy\Framework\Console\ConsoleCommand; diff --git a/src/Console/Commands/UuidCommand.php b/src/Console/Commands/Domain/UuidCommand.php similarity index 94% rename from src/Console/Commands/UuidCommand.php rename to src/Console/Commands/Domain/UuidCommand.php index ae01ad3..d683964 100644 --- a/src/Console/Commands/UuidCommand.php +++ b/src/Console/Commands/Domain/UuidCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Codefy\Framework\Console\Commands; +namespace Codefy\Framework\Console\Commands\Domain; use Codefy\Framework\Application; use Codefy\Framework\Console\ConsoleCommand; From 21ead11f86b6ae430ae333ab0484c6015156e0d6 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 22:17:40 -0800 Subject: [PATCH 11/18] Exception middleware simplified. Signed-off-by: Joshua Parker --- src/Http/Middleware/ExceptionMiddleware.php | 25 ++++----------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/Http/Middleware/ExceptionMiddleware.php b/src/Http/Middleware/ExceptionMiddleware.php index 1fc6521..0431069 100644 --- a/src/Http/Middleware/ExceptionMiddleware.php +++ b/src/Http/Middleware/ExceptionMiddleware.php @@ -25,6 +25,8 @@ public function __construct(protected Application $app) /** * @inheritDoc + * @throws TypeException + * @throws ReflectionException */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { @@ -33,37 +35,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } catch (HttpException | Psr7Exception $e) { $this->app->flash->error($e->getMessage()); - $this->logException( - $e, - [ - 'uri' => $e->getUri(), - 'code' => $e->getStatusCode(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'method' => $request->getMethod(), - 'message' => $e->getMessage(), - ] - ); + $this->logException($e); return RedirectResponseFactory::create( uri: $e->getUri() ?? $request->getServerParams()['HTTP_REFERER'] ?? '/', - headers: $e->getHeaders() ); } catch (Throwable $t) { $this->app->flash->error('Internal Error'); - $this->logException( - $t, - [ - 'message' => $t->getMessage(), - 'file' => $t->getFile(), - 'line' => $t->getLine(), - ] - ); + $this->logException($t); return RedirectResponseFactory::create( uri: $request->getServerParams()['HTTP_REFERER'] ?? '/', - headers: [] ); } From 845e18b1199c7fbef3ca87a4f0e70189abad513e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 12 Nov 2025 22:35:25 -0800 Subject: [PATCH 12/18] New default middlewares class. Signed-off-by: Joshua Parker --- src/Support/DefaultMiddlewares.php | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/Support/DefaultMiddlewares.php diff --git a/src/Support/DefaultMiddlewares.php b/src/Support/DefaultMiddlewares.php new file mode 100644 index 0000000..8b4391f --- /dev/null +++ b/src/Support/DefaultMiddlewares.php @@ -0,0 +1,40 @@ +collection = $collection ?: [ + 'api' => \Codefy\Framework\Http\Middleware\ApiMiddleware::class, + 'security.headers' => \Codefy\Framework\Http\Middleware\SecureHeaders\ContentSecurityPolicyMiddleware::class, + 'content.cache' => \Codefy\Framework\Http\Middleware\ContentCacheMiddleware::class, + 'cors' => \Codefy\Framework\Http\Middleware\CorsMiddleware::class, + 'csrf.token' => \Codefy\Framework\Http\Middleware\Csrf\CsrfTokenMiddleware::class, + 'csrf.protection' => \Codefy\Framework\Http\Middleware\Csrf\CsrfProtectionMiddleware::class, + 'css.minify' => \Codefy\Framework\Http\Middleware\CssMinifierMiddleware::class, + 'honeypot' => \Codefy\Framework\Http\Middleware\Spam\HoneyPotMiddleware::class, + 'html.minify' => \Codefy\Framework\Http\Middleware\HtmlMinifierMiddleware::class, + 'http.cache' => \Codefy\Framework\Http\Middleware\Cache\CacheMiddleware::class, + 'http.cache.clear.data' => \Codefy\Framework\Http\Middleware\Cache\ClearSiteDataMiddleware::class, + 'http.cache.expires' => \Codefy\Framework\Http\Middleware\Cache\CacheExpiresMiddleware::class, + 'http.cache.prevention' => \Codefy\Framework\Http\Middleware\Cache\CachePreventionMiddleware::class, + 'js.minify' => \Codefy\Framework\Http\Middleware\JsMinifierMiddleware::class, + 'rate.limiter' => \Codefy\Framework\Http\Middleware\ThrottleMiddleware::class, + 'referrer.spam' => \Codefy\Framework\Http\Middleware\Spam\ReferrerSpamMiddleware::class, + 'user.authenticate' => \Codefy\Framework\Http\Middleware\Auth\AuthenticationMiddleware::class, + 'user.session' => \Codefy\Framework\Http\Middleware\Auth\UserSessionMiddleware::class, + 'user.authorization' => \Codefy\Framework\Http\Middleware\Auth\UserAuthorizationMiddleware::class, + 'user.session.expire' => \Codefy\Framework\Http\Middleware\Auth\ExpireUserSessionMiddleware::class, + 'php.debugbar' => \Codefy\Framework\Http\Middleware\DebugBarMiddleware::class, + 'http.exception' => \Codefy\Framework\Http\Middleware\ExceptionMiddleware::class, + ]; + } +} From 3d2f53319407d622a8c5cd66e7c55775d21f8146 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 13 Nov 2025 07:36:12 -0800 Subject: [PATCH 13/18] Added configuration for middleware. Signed-off-by: Joshua Parker --- src/Configuration/Middleware.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/Configuration/Middleware.php diff --git a/src/Configuration/Middleware.php b/src/Configuration/Middleware.php new file mode 100644 index 0000000..3c614cc --- /dev/null +++ b/src/Configuration/Middleware.php @@ -0,0 +1,30 @@ +merge(self::$customAliases); + } +} From 56c3014aed8c26cf4178e6771435b30c803608b4 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 13 Nov 2025 16:24:49 -0800 Subject: [PATCH 14/18] Rearranged a couple of commands. Signed-off-by: Joshua Parker --- src/Support/DefaultCommands.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Support/DefaultCommands.php b/src/Support/DefaultCommands.php index fa97703..ed7e3af 100644 --- a/src/Support/DefaultCommands.php +++ b/src/Support/DefaultCommands.php @@ -27,8 +27,8 @@ public function __construct(array $collection = []) \Codefy\Framework\Console\Commands\MigrateRedoCommand::class, \Codefy\Framework\Console\Commands\ScheduleListCommand::class, \Codefy\Framework\Console\Commands\ServeCommand::class, - \Codefy\Framework\Console\Commands\UuidCommand::class, - \Codefy\Framework\Console\Commands\UlidCommand::class, + \Codefy\Framework\Console\Commands\Domain\UuidCommand::class, + \Codefy\Framework\Console\Commands\Domain\UlidCommand::class, \Codefy\Framework\Console\Commands\FlushPipelineCommand::class, \Codefy\Framework\Console\Commands\VendorPublishCommand::class, \Codefy\Framework\Console\Commands\GenerateEncryptionKeyCommand::class, From 01ba890d23d2bda06ffba866074d816b37978c37 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 13 Nov 2025 16:31:15 -0800 Subject: [PATCH 15/18] Updated docblock Signed-off-by: Joshua Parker --- src/Console/ConsoleKernel.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Console/ConsoleKernel.php b/src/Console/ConsoleKernel.php index eca4301..3967ba8 100644 --- a/src/Console/ConsoleKernel.php +++ b/src/Console/ConsoleKernel.php @@ -197,6 +197,7 @@ protected function load(array $commands = []): void * * @throws CommandNotFoundException * @throws Exception + * @throws Throwable */ public function call(string $command, array $parameters = [], bool|OutputInterface|null $outputBuffer = null): int { From 18be9f844e274ecedfafa44c20ebd2c8b2a032d1 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 14 Nov 2025 20:46:11 -0800 Subject: [PATCH 16/18] New view helper. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 3fc2725..c11dda3 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -25,9 +25,12 @@ use Codefy\QueryBus\UnresolvableQueryHandlerException; use Gravatar\Image; use Gravatar\Profile; +use Psr\Http\Message\ResponseInterface; use Qubus\Exception\Exception; use Qubus\Expressive\Connection; use Qubus\Expressive\QueryBuilder; +use Qubus\Http\Factories\HtmlResponseFactory; +use Qubus\View\Renderer; use ReflectionException; use RuntimeException; @@ -473,3 +476,17 @@ function throw_if(mixed $condition, string $exception = RuntimeException::class, return $condition; } + +/** + * @param array|string $template + * @param array $data + * @return ResponseInterface + * @throws \Exception + */ +function view(array|string $template, array $data = []): ResponseInterface +{ + /** @var Renderer $template */ + $template = Codefy::$PHP->make(name: Renderer::class); + + return HtmlResponseFactory::create($template->render($template, $data)); +} From ace0de9cff71aed939e72c5c10fcaea85a36367e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 14 Nov 2025 20:49:03 -0800 Subject: [PATCH 17/18] CSRF middleware to not use cache. Signed-off-by: Joshua Parker --- .../Csrf/CsrfProtectionMiddleware.php | 44 +++++------ .../Middleware/Csrf/CsrfTokenMiddleware.php | 77 +++++++++--------- .../Middleware/Csrf/InvalidTokenException.php | 11 +++ .../Csrf/TokenMismatchException.php | 11 +++ .../Middleware/Csrf/Traits/CsrfTokenAware.php | 79 ++++++++++++++----- 5 files changed, 141 insertions(+), 81 deletions(-) create mode 100644 src/Http/Middleware/Csrf/InvalidTokenException.php create mode 100644 src/Http/Middleware/Csrf/TokenMismatchException.php diff --git a/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php b/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php index c85eb85..b6bba3f 100644 --- a/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php +++ b/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php @@ -6,13 +6,12 @@ use Codefy\Framework\Http\Middleware\Csrf\Traits\CsrfTokenAware; use Codefy\Framework\Support\RequestMethod; +use Exception; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Qubus\Config\ConfigContainer; -use Qubus\Exception\Exception; -use Qubus\Http\Factories\JsonResponseFactory; use Qubus\Http\Session\SessionService; use function hash_equals; @@ -30,23 +29,20 @@ public function __construct(protected ConfigContainer $configContainer, protecte /** * @inheritDoc + * @throws TokenMismatchException + * @throws Exception */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - try { - $response = $handler->handle($request); - - if (true === $this->needsProtection($request) && false === $this->tokensMatch($request)) { - return JsonResponseFactory::create( - data: 'Bad CSRF Token.', - status: $this->configContainer->getConfigKey(key: 'csrf.error_status_code') - ); - } - - return $response; - } catch (\Exception $e) { - return $handler->handle($request); + if (true === $this->needsProtection($request) && false === $this->tokensMatch($request)) { + throw new TokenMismatchException( + uri: $request->getServerParams()['HTTP_REFERER'], + message: 'Bad CSRF token.', + code: 412 + ); } + + return $handler->handle($request); } /** @@ -74,23 +70,25 @@ private function tokensMatch(ServerRequestInterface $request): bool /** * @throws Exception - * @throws \Exception */ private function fetchToken(ServerRequestInterface $request): string { - /** @var CsrfSession $csrf */ - $csrf = $request->getAttribute(CsrfTokenMiddleware::CSRF_SESSION_ATTRIBUTE); + $token = $request->getAttribute(CsrfTokenMiddleware::CSRF_SESSION_ATTRIBUTE); // Ensure the token stored previously by the CsrfTokenMiddleware is present and has a valid format. if ( - is_string($csrf->csrfToken()) && - ctype_alnum($csrf->csrfToken()) && - strlen($csrf->csrfToken()) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length') + is_string($token) && + ctype_alnum($token) && + strlen($token) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length') ) { - return $csrf->csrfToken(); + return $token; } - return ''; + throw new InvalidTokenException( + uri: $request->getServerParams()['HTTP_REFERER'], + message: 'Missing or invalid CSRF token.', + code: 403 + ); } /** diff --git a/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php index f01df9b..5d8b5c3 100644 --- a/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php +++ b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -5,13 +5,16 @@ namespace Codefy\Framework\Http\Middleware\Csrf; use Codefy\Framework\Http\Middleware\Csrf\Traits\CsrfTokenAware; +use Defuse\Crypto\Exception\BadFormatException; +use Defuse\Crypto\Exception\EnvironmentIsBrokenException; +use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Qubus\Config\ConfigContainer; use Qubus\Exception\Exception; -use Qubus\Http\Session\SessionService; +use Qubus\Http\Cookies\Factory\HttpCookieFactory; use function sprintf; @@ -25,7 +28,7 @@ class CsrfTokenMiddleware implements MiddlewareInterface private ?string $token = null; - public function __construct(protected ConfigContainer $configContainer, protected SessionService $sessionService) + public function __construct(protected ConfigContainer $configContainer, public readonly HttpCookieFactory $cookie,) { self::$current = $this; } @@ -52,47 +55,41 @@ public function getFieldAttr(): string /** * @inheritDoc + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + * @throws Exception + * @throws BadFormatException + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - try { - $this->sessionService::$options = [ - 'cookie-name' => $this->configContainer->getConfigKey(key: 'csrf.cookie_name', default: 'CSRFSESSID'), - 'cookie-lifetime' => (int) $this->configContainer->getConfigKey(key: 'csrf.lifetime', default: 86400), - ]; - - $session = $this->sessionService->makeSession($request); - - $this->token = $this->prepareToken(session: $session); - - if ( - $request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header')) - && $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')) !== '' - ) { - $this->token = $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')); - } - - /** - * If true, the application will do a header check, if not, - * it will expect data submitted via an HTML form tag. - */ - if ($this->configContainer->getConfigKey(key: 'csrf.request_header') === true) { - $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token); - } - - /** @var CsrfSession $csrf */ - $csrf = $session->get(CsrfSession::class); - $csrf - ->withCsrfToken($this->token); - - $response = $handler->handle( - $request - ->withAttribute(self::CSRF_SESSION_ATTRIBUTE, $csrf) - ); - - return $this->sessionService->commitSession($response, $session); - } catch (\Exception $e) { - return $handler->handle($request); + + // Retrieve an existing token from the cookie or generate a new one. + $this->token = $this->prepareToken($request); + + if ( + $request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header')) + && $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')) !== '' + ) { + $this->token = $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')); + } + + /** + * If true, the application will do a header check, if not, + * it will expect data submitted via an HTML form tag. + */ + if ($this->configContainer->getConfigKey(key: 'csrf.request_header') === true) { + $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token); } + + $response = $handler->handle( + $request + ->withAttribute(self::CSRF_SESSION_ATTRIBUTE, $this->token) + ); + + // Attach/Refresh the token cookie for the "next" request call. + return $this->createCookie($response, $this->token); } } diff --git a/src/Http/Middleware/Csrf/InvalidTokenException.php b/src/Http/Middleware/Csrf/InvalidTokenException.php new file mode 100644 index 0000000..7224c83 --- /dev/null +++ b/src/Http/Middleware/Csrf/InvalidTokenException.php @@ -0,0 +1,11 @@ +clientSessionId(); + // Try to retrieve an existing token from the cookie request. + $token = $this->getTokenFromCookie($request->getCookieParams()); // If token isn't present in the session, we generate a new token. if ($token === '') { $token = $this->generateToken(); } - return hash_hmac( - algo: $this->configContainer->getConfigKey(key: 'csrf.hash_algo', default: 'sha256'), - data: $token, - key: $this->configContainer->getConfigKey(key: 'csrf.salt') - ); + + return $token; + } + + /** + * Get the token from the request cookie if it's present. + * Decrypt the cookie token value using the app crypto key. + * + * Return null if the cookie is missing or if the decryption fails. + * + * @param array $cookies + * @return string|null + * @throws Exception + * @throws BadFormatException + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException + */ + private function getTokenFromCookie(array $cookies): ?string + { + $key = Key::loadFromAsciiSafeString($this->configContainer->getConfigKey(key: 'app.crypto_key')); + $name = $this->configContainer->getConfigKey(key: 'csrf.cookie_name', default: 'CSRFSESSID'); + $value = $cookies[$name] ?? ''; + + return Crypto::decrypt(ciphertext: $value, key: $key); } /** + * Create CSRF cookie to store the encrypted token value. + * + * Encrypt the value for better security (in case of XSS attack). + * + * @param ResponseInterface $response + * @param string $token + * @return ResponseInterface * @throws Exception + * @throws EnvironmentIsBrokenException + * @throws BadFormatException */ - protected function hashEquals(string $knownString, string $userString): bool + private function createCookie(ResponseInterface $response, string $token): ResponseInterface { - return hash_equals( - $knownString, - hash_hmac( - algo: $this->configContainer->getConfigKey(key: 'csrf.hash_algo', default: 'sha256'), - data: $userString, - key: $this->configContainer->getConfigKey(key: 'csrf.salt') + $key = Key::loadFromAsciiSafeString($this->configContainer->getConfigKey(key: 'app.crypto_key')); + $name = $this->configContainer->getConfigKey(key: 'csrf.cookie_name', default: 'CSRFSESSID'); + $value = Crypto::encrypt(plaintext: $token, key: $key); + $expires = (int) $this->configContainer->getConfigKey(key: 'csrf.lifetime', default: 86400); + + return CookiesResponse::set( + response: $response, + setCookieCollection: $this->cookie->make( + name: $name, + value: $value, + maxAge: $expires ) ); } From 9fa1643f9767712080ae218f1476f34d28fc3169 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 14 Nov 2025 20:53:43 -0800 Subject: [PATCH 18/18] Revert "CSRF middleware to not use cache." This reverts commit ace0de9cff71aed939e72c5c10fcaea85a36367e. --- .../Csrf/CsrfProtectionMiddleware.php | 44 ++++++----- .../Middleware/Csrf/CsrfTokenMiddleware.php | 77 +++++++++--------- .../Middleware/Csrf/InvalidTokenException.php | 11 --- .../Csrf/TokenMismatchException.php | 11 --- .../Middleware/Csrf/Traits/CsrfTokenAware.php | 79 +++++-------------- 5 files changed, 81 insertions(+), 141 deletions(-) delete mode 100644 src/Http/Middleware/Csrf/InvalidTokenException.php delete mode 100644 src/Http/Middleware/Csrf/TokenMismatchException.php diff --git a/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php b/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php index b6bba3f..c85eb85 100644 --- a/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php +++ b/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php @@ -6,12 +6,13 @@ use Codefy\Framework\Http\Middleware\Csrf\Traits\CsrfTokenAware; use Codefy\Framework\Support\RequestMethod; -use Exception; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Qubus\Config\ConfigContainer; +use Qubus\Exception\Exception; +use Qubus\Http\Factories\JsonResponseFactory; use Qubus\Http\Session\SessionService; use function hash_equals; @@ -29,20 +30,23 @@ public function __construct(protected ConfigContainer $configContainer, protecte /** * @inheritDoc - * @throws TokenMismatchException - * @throws Exception */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (true === $this->needsProtection($request) && false === $this->tokensMatch($request)) { - throw new TokenMismatchException( - uri: $request->getServerParams()['HTTP_REFERER'], - message: 'Bad CSRF token.', - code: 412 - ); + try { + $response = $handler->handle($request); + + if (true === $this->needsProtection($request) && false === $this->tokensMatch($request)) { + return JsonResponseFactory::create( + data: 'Bad CSRF Token.', + status: $this->configContainer->getConfigKey(key: 'csrf.error_status_code') + ); + } + + return $response; + } catch (\Exception $e) { + return $handler->handle($request); } - - return $handler->handle($request); } /** @@ -70,25 +74,23 @@ private function tokensMatch(ServerRequestInterface $request): bool /** * @throws Exception + * @throws \Exception */ private function fetchToken(ServerRequestInterface $request): string { - $token = $request->getAttribute(CsrfTokenMiddleware::CSRF_SESSION_ATTRIBUTE); + /** @var CsrfSession $csrf */ + $csrf = $request->getAttribute(CsrfTokenMiddleware::CSRF_SESSION_ATTRIBUTE); // Ensure the token stored previously by the CsrfTokenMiddleware is present and has a valid format. if ( - is_string($token) && - ctype_alnum($token) && - strlen($token) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length') + is_string($csrf->csrfToken()) && + ctype_alnum($csrf->csrfToken()) && + strlen($csrf->csrfToken()) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length') ) { - return $token; + return $csrf->csrfToken(); } - throw new InvalidTokenException( - uri: $request->getServerParams()['HTTP_REFERER'], - message: 'Missing or invalid CSRF token.', - code: 403 - ); + return ''; } /** diff --git a/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php index 5d8b5c3..f01df9b 100644 --- a/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php +++ b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -5,16 +5,13 @@ namespace Codefy\Framework\Http\Middleware\Csrf; use Codefy\Framework\Http\Middleware\Csrf\Traits\CsrfTokenAware; -use Defuse\Crypto\Exception\BadFormatException; -use Defuse\Crypto\Exception\EnvironmentIsBrokenException; -use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Qubus\Config\ConfigContainer; use Qubus\Exception\Exception; -use Qubus\Http\Cookies\Factory\HttpCookieFactory; +use Qubus\Http\Session\SessionService; use function sprintf; @@ -28,7 +25,7 @@ class CsrfTokenMiddleware implements MiddlewareInterface private ?string $token = null; - public function __construct(protected ConfigContainer $configContainer, public readonly HttpCookieFactory $cookie,) + public function __construct(protected ConfigContainer $configContainer, protected SessionService $sessionService) { self::$current = $this; } @@ -55,41 +52,47 @@ public function getFieldAttr(): string /** * @inheritDoc - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface - * @throws Exception - * @throws BadFormatException - * @throws EnvironmentIsBrokenException - * @throws WrongKeyOrModifiedCiphertextException */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - - // Retrieve an existing token from the cookie or generate a new one. - $this->token = $this->prepareToken($request); - - if ( - $request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header')) - && $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')) !== '' - ) { - $this->token = $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')); - } - - /** - * If true, the application will do a header check, if not, - * it will expect data submitted via an HTML form tag. - */ - if ($this->configContainer->getConfigKey(key: 'csrf.request_header') === true) { - $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token); + try { + $this->sessionService::$options = [ + 'cookie-name' => $this->configContainer->getConfigKey(key: 'csrf.cookie_name', default: 'CSRFSESSID'), + 'cookie-lifetime' => (int) $this->configContainer->getConfigKey(key: 'csrf.lifetime', default: 86400), + ]; + + $session = $this->sessionService->makeSession($request); + + $this->token = $this->prepareToken(session: $session); + + if ( + $request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header')) + && $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')) !== '' + ) { + $this->token = $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')); + } + + /** + * If true, the application will do a header check, if not, + * it will expect data submitted via an HTML form tag. + */ + if ($this->configContainer->getConfigKey(key: 'csrf.request_header') === true) { + $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token); + } + + /** @var CsrfSession $csrf */ + $csrf = $session->get(CsrfSession::class); + $csrf + ->withCsrfToken($this->token); + + $response = $handler->handle( + $request + ->withAttribute(self::CSRF_SESSION_ATTRIBUTE, $csrf) + ); + + return $this->sessionService->commitSession($response, $session); + } catch (\Exception $e) { + return $handler->handle($request); } - - $response = $handler->handle( - $request - ->withAttribute(self::CSRF_SESSION_ATTRIBUTE, $this->token) - ); - - // Attach/Refresh the token cookie for the "next" request call. - return $this->createCookie($response, $this->token); } } diff --git a/src/Http/Middleware/Csrf/InvalidTokenException.php b/src/Http/Middleware/Csrf/InvalidTokenException.php deleted file mode 100644 index 7224c83..0000000 --- a/src/Http/Middleware/Csrf/InvalidTokenException.php +++ /dev/null @@ -1,11 +0,0 @@ -getTokenFromCookie($request->getCookieParams()); + // Try to retrieve an existing token from the session. + $token = $session->clientSessionId(); // If token isn't present in the session, we generate a new token. if ($token === '') { $token = $this->generateToken(); } - - return $token; - } - - /** - * Get the token from the request cookie if it's present. - * Decrypt the cookie token value using the app crypto key. - * - * Return null if the cookie is missing or if the decryption fails. - * - * @param array $cookies - * @return string|null - * @throws Exception - * @throws BadFormatException - * @throws EnvironmentIsBrokenException - * @throws WrongKeyOrModifiedCiphertextException - */ - private function getTokenFromCookie(array $cookies): ?string - { - $key = Key::loadFromAsciiSafeString($this->configContainer->getConfigKey(key: 'app.crypto_key')); - $name = $this->configContainer->getConfigKey(key: 'csrf.cookie_name', default: 'CSRFSESSID'); - $value = $cookies[$name] ?? ''; - - return Crypto::decrypt(ciphertext: $value, key: $key); + return hash_hmac( + algo: $this->configContainer->getConfigKey(key: 'csrf.hash_algo', default: 'sha256'), + data: $token, + key: $this->configContainer->getConfigKey(key: 'csrf.salt') + ); } /** - * Create CSRF cookie to store the encrypted token value. - * - * Encrypt the value for better security (in case of XSS attack). - * - * @param ResponseInterface $response - * @param string $token - * @return ResponseInterface * @throws Exception - * @throws EnvironmentIsBrokenException - * @throws BadFormatException */ - private function createCookie(ResponseInterface $response, string $token): ResponseInterface + protected function hashEquals(string $knownString, string $userString): bool { - $key = Key::loadFromAsciiSafeString($this->configContainer->getConfigKey(key: 'app.crypto_key')); - $name = $this->configContainer->getConfigKey(key: 'csrf.cookie_name', default: 'CSRFSESSID'); - $value = Crypto::encrypt(plaintext: $token, key: $key); - $expires = (int) $this->configContainer->getConfigKey(key: 'csrf.lifetime', default: 86400); - - return CookiesResponse::set( - response: $response, - setCookieCollection: $this->cookie->make( - name: $name, - value: $value, - maxAge: $expires + return hash_equals( + $knownString, + hash_hmac( + algo: $this->configContainer->getConfigKey(key: 'csrf.hash_algo', default: 'sha256'), + data: $userString, + key: $this->configContainer->getConfigKey(key: 'csrf.salt') ) ); }