From d485c1bd553614d319f4de7d93d96bf90b51c26b Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Tue, 11 Mar 2025 19:37:49 +0100 Subject: [PATCH 1/3] chore: le systeme de vue doit toujours avoir le locator --- src/Debug/ExceptionManager.php | 2 +- src/View/Adapters/AbstractAdapter.php | 24 +++++++++--------------- src/View/Adapters/BladeAdapter.php | 4 ++-- src/View/Adapters/LatteAdapter.php | 4 ++-- src/View/Adapters/NativeAdapter.php | 6 +++--- src/View/Adapters/PlatesAdapter.php | 4 ++-- src/View/Adapters/SmartyAdapter.php | 4 ++-- src/View/Adapters/TwigAdapter.php | 4 ++-- src/View/View.php | 2 +- 9 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/Debug/ExceptionManager.php b/src/Debug/ExceptionManager.php index b6bcd076..1ec3d2a6 100644 --- a/src/Debug/ExceptionManager.php +++ b/src/Debug/ExceptionManager.php @@ -52,7 +52,7 @@ public static function registerHttpErrors(Run $debugger, array $config): Run if (in_array((string) $exception->getCode(), $files, true)) { $view = new View(); - $view->setAdapter(config('view.active_adapter', 'native'), ['view_path_locator' => $config['error_view_path']]) + $view->setAdapter(config('view.active_adapter', 'native'), ['view_path' => $config['error_view_path']]) ->display((string) $exception->getCode()) ->setData(['message' => $exception->getMessage()]) ->render(); diff --git a/src/View/Adapters/AbstractAdapter.php b/src/View/Adapters/AbstractAdapter.php index 2fe49624..96ffbd3d 100644 --- a/src/View/Adapters/AbstractAdapter.php +++ b/src/View/Adapters/AbstractAdapter.php @@ -50,7 +50,7 @@ abstract class AbstractAdapter implements RendererInterface /** * Instance de Locator lorsque nous devons tenter de trouver une vue qui n'est pas à l'emplacement standard. */ - protected ?LocatorInterface $locator = null; + protected LocatorInterface $locator; /** * Le nom de la mise en page utilisée, le cas échéant. @@ -70,25 +70,19 @@ abstract class AbstractAdapter implements RendererInterface /** * {@inheritDoc} * - * @param array $config Configuration actuelle de l'adapter - * @param bool $debug Devrions-nous stocker des informations sur les performances ? + * @param array $config Configuration actuelle de l'adapter + * @param string|null $viewPath Dossier principal dans lequel les vues doivent être cherchées + * @param bool $debug Devrions-nous stocker des informations sur les performances ? */ - public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG) + public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG) { helper('assets'); - if (! empty($viewPathLocator)) { - if (is_string($viewPathLocator)) { - $this->viewPath = rtrim($viewPathLocator, '\\/ ') . DS; - } elseif ($viewPathLocator instanceof LocatorInterface) { - $this->locator = $viewPathLocator; - } + if (is_string($viewPath) && is_dir($viewPath = rtrim($viewPath, '\\/ ') . DS)) { + $this->viewPath = $viewPath; } - if (! $this->locator instanceof LocatorInterface && ! is_dir($this->viewPath)) { - $this->viewPath = ''; - $this->locator = service('locator'); - } + $this->locator = service('locator'); $this->ext = preg_replace('#^\.#', '', $config['extension'] ?? $this->ext); } @@ -264,7 +258,7 @@ protected function getRenderedFile(?array $options, string $view, ?string $ext = $file = Helpers::ensureExt($file, $ext); - if (! is_file($file) && $this->locator instanceof LocatorInterface) { + if (! is_file($file)) { $file = $this->locator->locateFile($view, 'Views', $ext); } diff --git a/src/View/Adapters/BladeAdapter.php b/src/View/Adapters/BladeAdapter.php index d72a04ee..743f6f15 100644 --- a/src/View/Adapters/BladeAdapter.php +++ b/src/View/Adapters/BladeAdapter.php @@ -30,9 +30,9 @@ class BladeAdapter extends AbstractAdapter /** * {@inheritDoc} */ - public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG) + public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG) { - parent::__construct($config, $viewPathLocator, $debug); + parent::__construct($config, $viewPath, $debug); $this->engine = new Blade( $this->viewPath ?: VIEW_PATH, diff --git a/src/View/Adapters/LatteAdapter.php b/src/View/Adapters/LatteAdapter.php index 1d7e7d5c..a41de51c 100644 --- a/src/View/Adapters/LatteAdapter.php +++ b/src/View/Adapters/LatteAdapter.php @@ -31,9 +31,9 @@ class LatteAdapter extends AbstractAdapter /** * {@inheritDoc} */ - public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG) + public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG) { - parent::__construct($config, $viewPathLocator, $debug); + parent::__construct($config, $viewPath, $debug); $this->latte = new Engine(); diff --git a/src/View/Adapters/NativeAdapter.php b/src/View/Adapters/NativeAdapter.php index fa2e7264..f0dd271e 100644 --- a/src/View/Adapters/NativeAdapter.php +++ b/src/View/Adapters/NativeAdapter.php @@ -78,9 +78,9 @@ class NativeAdapter extends AbstractAdapter /** * {@inheritDoc} */ - public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG) + public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG) { - parent::__construct($config, $viewPathLocator, $debug); + parent::__construct($config, $viewPath, $debug); $this->saveData = (bool) ($config['save_data'] ?? true); } @@ -591,7 +591,7 @@ public function required(bool|string $condition): string /** * Génère un champ input caché à utiliser dans les formulaires générés manuellement. */ - public function csrf(?string $id): string + public function csrf(?string $id = null): string { return csrf_field($id); } diff --git a/src/View/Adapters/PlatesAdapter.php b/src/View/Adapters/PlatesAdapter.php index 94bdbf7a..ff5e517d 100644 --- a/src/View/Adapters/PlatesAdapter.php +++ b/src/View/Adapters/PlatesAdapter.php @@ -31,9 +31,9 @@ class PlatesAdapter extends AbstractAdapter /** * {@inheritDoc} */ - public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG) + public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG) { - parent::__construct($config, $viewPathLocator, $debug); + parent::__construct($config, $viewPath, $debug); $this->engine = new Engine(rtrim($this->viewPath, '/\\'), $this->ext); diff --git a/src/View/Adapters/SmartyAdapter.php b/src/View/Adapters/SmartyAdapter.php index 34a3fa45..9b825d56 100644 --- a/src/View/Adapters/SmartyAdapter.php +++ b/src/View/Adapters/SmartyAdapter.php @@ -30,9 +30,9 @@ class SmartyAdapter extends AbstractAdapter /** * {@inheritDoc} */ - public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG) + public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG) { - parent::__construct($config, $viewPathLocator, $debug); + parent::__construct($config, $viewPath, $debug); $this->engine = new Smarty(); diff --git a/src/View/Adapters/TwigAdapter.php b/src/View/Adapters/TwigAdapter.php index fb7f0a6c..c00b1326 100644 --- a/src/View/Adapters/TwigAdapter.php +++ b/src/View/Adapters/TwigAdapter.php @@ -33,9 +33,9 @@ class TwigAdapter extends AbstractAdapter /** * {@inheritDoc} */ - public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG) + public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG) { - parent::__construct($config, $viewPathLocator, $debug); + parent::__construct($config, $viewPath, $debug); $loader = new FilesystemLoader([ $this->viewPath, diff --git a/src/View/View.php b/src/View/View.php index 6d68e608..a03b4c2b 100644 --- a/src/View/View.php +++ b/src/View/View.php @@ -369,7 +369,7 @@ public function setAdapter(string $adapter, array $config = []): static $this->adapter = new self::$validAdapters[$adapter]( $config, - $config['view_path_locator'] ?? service('locator'), + $config['view_path'] ?? null, $debug ); From fb9556f8dac5826c8b23b782dbe07f544f86618b Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Tue, 11 Mar 2025 19:41:29 +0100 Subject: [PATCH 2/3] feat: ajout du middleware de protection CSRF --- src/Debug/ExceptionManager.php | 16 ++ src/Exceptions/TokenMismatchException.php | 16 ++ src/Middlewares/VerifyCsrfToken.php | 173 ++++++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 src/Exceptions/TokenMismatchException.php create mode 100644 src/Middlewares/VerifyCsrfToken.php diff --git a/src/Debug/ExceptionManager.php b/src/Debug/ExceptionManager.php index 1ec3d2a6..0f2be488 100644 --- a/src/Debug/ExceptionManager.php +++ b/src/Debug/ExceptionManager.php @@ -11,6 +11,8 @@ namespace BlitzPHP\Debug; +use BlitzPHP\Exceptions\HttpException; +use BlitzPHP\Exceptions\TokenMismatchException; use BlitzPHP\View\View; use Symfony\Component\Finder\SplFileInfo; use Throwable; @@ -35,6 +37,8 @@ class ExceptionManager public static function registerHttpErrors(Run $debugger, array $config): Run { return $debugger->pushHandler(static function (Throwable $exception, InspectorInterface $inspector, RunInterface $run) use ($config): int { + $exception = self::prepareException($exception); + $exception_code = $exception->getCode(); if ($exception_code >= 400 && $exception_code < 600) { $run->sendHttpCode($exception_code); @@ -166,4 +170,16 @@ private static function setBlacklist(PrettyPageHandler $handler, array $blacklis return $handler; } + + /** + * Prepare exception for rendering. + */ + private static function prepareException(Throwable $e): Throwable + { + if ($e instanceof TokenMismatchException) { + $e = new HttpException($e->getMessage(), 419, $e); + } + + return $e; + } } diff --git a/src/Exceptions/TokenMismatchException.php b/src/Exceptions/TokenMismatchException.php new file mode 100644 index 00000000..02c9190a --- /dev/null +++ b/src/Exceptions/TokenMismatchException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Exceptions; + +class TokenMismatchException extends FrameworkException +{ +} diff --git a/src/Middlewares/VerifyCsrfToken.php b/src/Middlewares/VerifyCsrfToken.php new file mode 100644 index 00000000..4a59b2eb --- /dev/null +++ b/src/Middlewares/VerifyCsrfToken.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Middlewares; + +use BlitzPHP\Contracts\Http\ResponsableInterface; +use BlitzPHP\Contracts\Security\EncrypterInterface; +use BlitzPHP\Exceptions\EncryptionException; +use BlitzPHP\Exceptions\TokenMismatchException; +use BlitzPHP\Http\Request; +use BlitzPHP\Http\Response; +use BlitzPHP\Session\Cookie\Cookie; +use BlitzPHP\Session\Cookie\CookieValuePrefix; +use BlitzPHP\Traits\Support\InteractsWithTime; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class VerifyCsrfToken implements MiddlewareInterface +{ + use InteractsWithTime; + + /** + * Les URI qui doivent être exclus de la vérification CSRF. + */ + protected array $except = []; + + /** + * Indique si le cookie XSRF-TOKEN doit être défini dans la réponse. + */ + protected bool $addHttpCookie = true; + + /** + * Constructeur + */ + public function __construct(protected EncrypterInterface $encrypter) + { + } + + /** + * {@inheritDoc} + * + * @param Request $request + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ($this->isReading($request) || $this->runningUnitTests() || $this->inExceptArray($request) || $this->tokensMatch($request)) { + return tap($handler->handle($request), function ($response) use ($request) { + if ($this->shouldAddXsrfTokenCookie()) { + $this->addCookieToResponse($request, $response); + } + }); + } + + throw new TokenMismatchException('Erreur de jeton CSRF.'); + } + + /** + * Détermine si la requête HTTP utilise un verbe « read ». + */ + protected function isReading(Request $request): bool + { + return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS'], true); + } + + /** + * Détermine si l'application exécute des tests unitaires. + */ + protected function runningUnitTests(): bool + { + return is_cli() && on_test(); + } + + /** + * Détermine si la requête comporte un URI qui doit faire l'objet d'une vérification CSRF. + */ + protected function inExceptArray(Request $request): bool + { + foreach ($this->except as $except) { + if ($except !== '/') { + $except = trim($except, '/'); + } + + if ($request->fullUrlIs($except) || $request->pathIs($except)) { + return true; + } + } + + return false; + } + + /** + * Détermine si les jetons CSRF de session et d'entrée correspondent. + */ + protected function tokensMatch(Request $request): bool + { + $token = $this->getTokenFromRequest($request); + + return is_string($request->session()->token()) + && is_string($token) + && hash_equals($request->session()->token(), $token); + } + + /** + * Récupère le jeton CSRF de la requête. + */ + protected function getTokenFromRequest(Request $request): ?string + { + $token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN'); + + if (! $token && $header = $request->header('X-XSRF-TOKEN')) { + try { + $token = CookieValuePrefix::remove($this->encrypter->decrypt($header)); + } catch (EncryptionException) { + $token = ''; + } + } + + return $token; + } + + /** + * Détermine si le cookie doit être ajouté à la réponse. + */ + public function shouldAddXsrfTokenCookie(): bool + { + return $this->addHttpCookie; + } + + /** + * Ajoute le jeton CSRF aux cookies de la réponse. + * + * @param Response $response + */ + protected function addCookieToResponse(Request $request, $response): ResponseInterface + { + if ($response instanceof ResponsableInterface) { + $response = $response->toResponse($request); + } + + if (! ($response instanceof Response)) { + return $response; + } + + $config = config('cookie'); + + return $response->withCookie(Cookie::create('XSRF-TOKEN', $request->session()->token(), [ + 'expires' => $this->availableAt(config('session.expiration')), + 'path' => $config['path'], + 'domain' => $config['domain'], + 'secure' => $config['secure'], + 'httponly' => false, + 'samesite' => $config['samesite'] ?? null, + ])); + } + + /** + * Détermine si le contenu du cookie doit être sérialisé. + */ + public static function serialized(): bool + { + return EncryptCookies::serialized('XSRF-TOKEN'); + } +} From 7964c4506770a442f21552c025db81fee625282b Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Tue, 11 Mar 2025 19:57:03 +0100 Subject: [PATCH 3/3] fix phpstan --- src/View/Parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/Parser.php b/src/View/Parser.php index 95482f9e..9b47b25e 100644 --- a/src/View/Parser.php +++ b/src/View/Parser.php @@ -98,7 +98,7 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n if (! is_file($file)) { $fileOrig = $file; - $file = ($this->locator ?: service('locator'))->locateFile($view, 'Views'); + $file = $this->locator->locateFile($view, 'Views'); // locateFile will return an empty string if the file cannot be found. if (empty($file)) {