From edb29a41a209933fb379264b962e975af0c66c8c Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 30 Oct 2023 12:21:14 +0100 Subject: [PATCH 001/111] fixM bug typs --- src/Http/ServerRequest.php | 2 +- src/Router/RouteCollection.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 9855f7d0..50404fe1 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -173,7 +173,7 @@ class ServerRequest implements ServerRequestInterface /** * Negotiator */ - protected Negotiator $negotiator; + protected Negotiator|null $negotiator = null; /** * Créer un nouvel objet de requête. diff --git a/src/Router/RouteCollection.php b/src/Router/RouteCollection.php index 6447566d..e2cfe338 100644 --- a/src/Router/RouteCollection.php +++ b/src/Router/RouteCollection.php @@ -233,7 +233,7 @@ public function __construct(protected LocatorInterface $locator, object $routing $this->defaultController = $routing->default_controller ?: $this->defaultController; $this->defaultMethod = $routing->default_method ?: $this->defaultMethod; $this->translateURIDashes = $routing->translate_uri_dashes ?: $this->translateURIDashes; - $this->override404 = $routing->fallback ?: ($routing->override404 ?: $this->override404); + $this->override404 = $routing->fallback ?: $this->override404; $this->autoRoute = $routing->auto_route ?: $this->autoRoute; $this->routeFiles = $routing->route_files ?: $this->routeFiles; $this->prioritize = $routing->prioritize ?: $this->prioritize; From 86fcbb84f1751173ded341606f8f1ffc2a8fc06f Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 30 Oct 2023 12:23:39 +0100 Subject: [PATCH 002/111] patch: modification de l'initialisation des middlewares --- src/Http/Middleware.php | 9 ++------- src/Middlewares/BaseMiddleware.php | 18 ++++++++++++++---- src/Router/Dispatcher.php | 7 ++++--- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Http/Middleware.php b/src/Http/Middleware.php index 04693378..9adafedd 100644 --- a/src/Http/Middleware.php +++ b/src/Http/Middleware.php @@ -13,8 +13,6 @@ use BlitzPHP\Container\Services; use BlitzPHP\Middlewares\BaseMiddleware; -use BlitzPHP\Middlewares\BodyParser; -use BlitzPHP\Middlewares\Cors; use LogicException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -36,10 +34,7 @@ class Middleware implements RequestHandlerInterface /** * Aliases des middlewares */ - protected array $aliases = [ - 'body-parser' => BodyParser::class, - 'cors' => Cors::class, - ]; + protected array $aliases = []; /** * Contructor @@ -239,7 +234,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface if ($middleware instanceof MiddlewareInterface) { if ($middleware instanceof BaseMiddleware) { - $middleware = $middleware->init($options + ['path' => $this->path])->fill($options); + $middleware = $middleware->fill($options)->init($options + ['path' => $this->path]); } return $middleware->process($request, $this); diff --git a/src/Middlewares/BaseMiddleware.php b/src/Middlewares/BaseMiddleware.php index c16eefbe..fae5effc 100644 --- a/src/Middlewares/BaseMiddleware.php +++ b/src/Middlewares/BaseMiddleware.php @@ -12,9 +12,8 @@ namespace BlitzPHP\Middlewares; use BlitzPHP\Utilities\String\Text; -use Psr\Http\Server\MiddlewareInterface; -abstract class BaseMiddleware implements MiddlewareInterface +abstract class BaseMiddleware { /** * Liste des arguments envoyes au middleware @@ -36,22 +35,33 @@ public function init(array $arguments = []): static $this->path = $arguments['path'] ?: '/'; unset($arguments['path']); - $this->arguments = $arguments; + $this->arguments = array_merge($this->arguments, $arguments); foreach ($this->arguments as $argument => $value) { + if (! is_string($argument)) { + continue; + } + $method = Text::camel('set_' . $argument); if (method_exists($this, $method)) { call_user_func([$this, $method], $value); + } else if (property_exists($this, $argument)) { + $this->{$argument} = $value; } } return $this; } + public function __get($name) + { + return $this->arguments[$name] ?? null; + } + /** * @internal */ - public function fill(array $params): static + final public function fill(array $params): static { foreach ($this->fillable as $key) { if (empty($params)) { diff --git a/src/Router/Dispatcher.php b/src/Router/Dispatcher.php index 683a3a36..9fce3134 100644 --- a/src/Router/Dispatcher.php +++ b/src/Router/Dispatcher.php @@ -262,15 +262,16 @@ protected function handleRequest(?RouteCollectionInterface $routes = null, ?arra // Le bootstrap dans un middleware $this->middleware->alias('blitz', $this->bootApp()); - $this->middleware->append('blitz'); /** * Ajouter des middlewares de routes */ foreach ($routeMiddlewares as $middleware) { - $this->middleware->prepend($middleware); + $this->middleware->append($middleware); } + $this->middleware->append('blitz'); + // Enregistrer notre URI actuel en tant qu'URI précédent dans la session // pour une utilisation plus sûre et plus précise avec la fonction d'assistance `previous_url()`. $this->storePreviousURL(current_url(true)); @@ -732,8 +733,8 @@ protected function initMiddlewareQueue(): void { $this->middleware = new Middleware($this->response, $this->determinePath()); + $this->middleware->append($this->spoofRequestMethod()); $this->middleware->register($this->request); - $this->middleware->prepend($this->spoofRequestMethod()); } protected function outputBufferingStart(): void From fdb5b8691d5126319e7024c701a843b39e42bd77 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 30 Oct 2023 16:20:02 +0100 Subject: [PATCH 003/111] feat: prise en charge des middleware psr7 lors de la generation avec klinge --- src/Cli/Commands/Generators/Middleware.php | 23 +++++++++++++++++++ .../Generators/Views/middleware.tpl.php | 18 ++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Cli/Commands/Generators/Middleware.php b/src/Cli/Commands/Generators/Middleware.php index 31d3e71b..17c3761d 100644 --- a/src/Cli/Commands/Generators/Middleware.php +++ b/src/Cli/Commands/Generators/Middleware.php @@ -55,6 +55,7 @@ class Middleware extends Command '--namespace' => ["Définit l'espace de noms racine. Par défaut\u{a0}: \"APP_NAMESPACE\".", APP_NAMESPACE], '--suffix' => 'Ajouter le titre du composant au nom de la classe (par exemple, User => UserMiddleware).', '--force' => 'Forcer à écraser le fichier existant.', + '--standard' => 'Le standard utilisé pour le middleware. Par défaut: "psr15"', ]; /** @@ -69,4 +70,26 @@ public function execute(array $params) $this->classNameLang = 'CLI.generator.className.middleware'; $this->runGeneration($params); } + + /** + * Préparez les options et effectuez les remplacements nécessaires. + */ + protected function prepare(string $class): string + { + $standard = $this->option('standard', 'psr15'); + + if (! in_array($standard, ['psr15', 'psr7'], true)) { + // @codeCoverageIgnoreStart + $standard = $this->choice(lang('CLI.generator.middlewareStandard'), ['psr15', 'psr7'], 'psr15'); + $this->eol(); + // @codeCoverageIgnoreEnd + } + + return $this->parseTemplate( + $class, + [], + [], + ['standard' => $standard] + ); + } } diff --git a/src/Cli/Commands/Generators/Views/middleware.tpl.php b/src/Cli/Commands/Generators/Views/middleware.tpl.php index 46499216..8a1b5523 100644 --- a/src/Cli/Commands/Generators/Views/middleware.tpl.php +++ b/src/Cli/Commands/Generators/Views/middleware.tpl.php @@ -2,24 +2,40 @@ namespace {namespace}; +use BlitzPHP\Http\Request; use BlitzPHP\Middlewares\BaseMiddleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; + use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class {class} extends BaseMiddleware implements MiddlewareInterface + + +class {class} extends BaseMiddleware + { /** * Traitez une demande de serveur entrante. * * Traite une demande de serveur entrante afin de produire une réponse. * S'il est incapable de produire la réponse lui-même, il peut déléguer au gestionnaire de requêtes fourni le soin de le faire. + * + * @param Request $request */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface + { // - return $handler->process($request); + + return $handler->handle($request); + + return $next($request, $response); + } } From 538ad242871ddd9b0443f4ff45d8c0c483782f4e Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Tue, 31 Oct 2023 16:00:50 +0100 Subject: [PATCH 004/111] feat: ajout du gestionnaire de cookie --- src/Config/Providers.php | 2 ++ src/Container/Services.php | 22 ++++++++++++++++++++++ src/Helpers/common.php | 30 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/Config/Providers.php b/src/Config/Providers.php index 3f0b2d92..20b211b3 100644 --- a/src/Config/Providers.php +++ b/src/Config/Providers.php @@ -33,6 +33,7 @@ private static function interfaces(): array \BlitzPHP\Contracts\Event\EventManagerInterface::class => static fn () => service('event'), \BlitzPHP\Contracts\Router\RouteCollectionInterface::class => static fn () => service('routes'), \BlitzPHP\Contracts\Security\EncrypterInterface::class => static fn () => service('encrypter'), + \BlitzPHP\Contracts\Session\CookieManagerInterface::class => static fn () => service('cookie'), \BlitzPHP\Contracts\Session\SessionInterface::class => static fn () => service('session'), \BlitzPHP\Mail\MailerInterface::class => static fn () => service('mail'), \Psr\Container\ContainerInterface::class => static fn () => service('container'), @@ -59,6 +60,7 @@ private static function classes(): array \BlitzPHP\Cache\ResponseCache::class => static fn () => service('responsecache'), \BlitzPHP\Router\RouteCollection::class => static fn () => service('routes'), \BlitzPHP\Router\Router::class => static fn () => service('router'), + \BlitzPHP\Session\Cookie\CookieManager::class => static fn () => service('cookie'), \BlitzPHP\Session\Store::class => static fn () => service('session'), \BlitzPHP\Filesystem\FilesystemManager::class => static fn () => service('storage'), ]; diff --git a/src/Container/Services.php b/src/Container/Services.php index a9792e17..2769fbbe 100644 --- a/src/Container/Services.php +++ b/src/Container/Services.php @@ -19,6 +19,7 @@ use BlitzPHP\Config\Config; use BlitzPHP\Contracts\Database\ConnectionResolverInterface; use BlitzPHP\Contracts\Security\EncrypterInterface; +use BlitzPHP\Contracts\Session\CookieManagerInterface; use BlitzPHP\Debug\Logger; use BlitzPHP\Debug\Timer; use BlitzPHP\Debug\Toolbar; @@ -38,6 +39,7 @@ use BlitzPHP\Router\Router; use BlitzPHP\Security\Encryption\Encryption; use BlitzPHP\Session\Cookie\Cookie; +use BlitzPHP\Session\Cookie\CookieManager; use BlitzPHP\Session\Handlers\Database as DatabaseSessionHandler; use BlitzPHP\Session\Handlers\Database\MySQL as MySQLSessionHandler; use BlitzPHP\Session\Handlers\Database\Postgre as PostgreSessionHandler; @@ -149,6 +151,26 @@ public static function container(bool $shared = true): Container return static::$instances[Container::class] = new Container(); } + /** + * Gestionnaire de cookies + */ + public static function cookie(bool $shared = true): CookieManagerInterface + { + if (true === $shared && isset(static::$instances[CookieManager::class])) { + return static::$instances[CookieManager::class]; + } + + $config = (object) static::config()->get('cookie'); + + return static::$instances[CookieManager::class] = (new CookieManager())->setDefaultPathAndDomain( + $config->path ?: '/', + $config->domain ?: '', + $config->secure ?: false, + $config->httponly ?: true, + $config->samesite ?: 'Lax' + ); + } + /** * Émetteur de réponse au client */ diff --git a/src/Helpers/common.php b/src/Helpers/common.php index 0bdda43c..bddbe1ad 100644 --- a/src/Helpers/common.php +++ b/src/Helpers/common.php @@ -13,6 +13,8 @@ use BlitzPHP\Config\Config; use BlitzPHP\Container\Services; use BlitzPHP\Contracts\Database\ConnectionInterface; +use BlitzPHP\Contracts\Session\CookieInterface; +use BlitzPHP\Contracts\Session\CookieManagerInterface; use BlitzPHP\Exceptions\PageNotFoundException; use BlitzPHP\Http\Redirection; use BlitzPHP\Http\ServerRequest; @@ -274,6 +276,34 @@ function cache(?string $key = null, $value = null) } } +if (! function_exists('cookie')) { + /** + * Une méthode pratique qui donne accès à l'objet cookie. + * Si aucun paramètre n'est fourni, renverra l'objet, + * sinon, tentera de renvoyer la valeur du cookie. + * + * Exemples: + * cookie()->make('foo', 'bar'); ou cookie('foo', 'bar'); + * $foo = cookie('bar') + * + * @return CookieManagerInterface|CookieInterface|null + */ + function cookie(?string $name = null, array|string|null $value = null, int $minutes = 0, array $options = []) + { + $cookie = Services::cookie(); + + if (null === $name) { + return $cookie; + } + + if (null === $value) { + return $cookie->get($name); + } + + return $cookie->make($name, $value, $minutes, $options); + } +} + if (! function_exists('session')) { /** * Une méthode pratique pour accéder à l'instance de session, ou un élément qui a été défini dans la session. From 64d39ca5e70953a410579a1752da8fda822d3efc Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Tue, 31 Oct 2023 16:39:23 +0100 Subject: [PATCH 005/111] cs-fix --- src/Cli/Commands/Generators/Middleware.php | 2 +- src/Exceptions/FrameworkException.php | 2 +- src/Helpers/common.php | 8 ++++---- src/Http/ServerRequest.php | 2 +- src/Middlewares/BaseMiddleware.php | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Cli/Commands/Generators/Middleware.php b/src/Cli/Commands/Generators/Middleware.php index 17c3761d..4e6d3bb9 100644 --- a/src/Cli/Commands/Generators/Middleware.php +++ b/src/Cli/Commands/Generators/Middleware.php @@ -77,7 +77,7 @@ public function execute(array $params) protected function prepare(string $class): string { $standard = $this->option('standard', 'psr15'); - + if (! in_array($standard, ['psr15', 'psr7'], true)) { // @codeCoverageIgnoreStart $standard = $this->choice(lang('CLI.generator.middlewareStandard'), ['psr15', 'psr7'], 'psr15'); diff --git a/src/Exceptions/FrameworkException.php b/src/Exceptions/FrameworkException.php index f01337a5..3857f5c5 100644 --- a/src/Exceptions/FrameworkException.php +++ b/src/Exceptions/FrameworkException.php @@ -46,7 +46,7 @@ public static function missingExtension(string $extension) 'The framework needs the following extension(s) installed and loaded: %s.', $extension ); - // @codeCoverageIgnoreEnd + // @codeCoverageIgnoreEnd } else { $message = lang('Core.missingExtension', [$extension]); } diff --git a/src/Helpers/common.php b/src/Helpers/common.php index bddbe1ad..8456095a 100644 --- a/src/Helpers/common.php +++ b/src/Helpers/common.php @@ -278,17 +278,17 @@ function cache(?string $key = null, $value = null) if (! function_exists('cookie')) { /** - * Une méthode pratique qui donne accès à l'objet cookie. + * Une méthode pratique qui donne accès à l'objet cookie. * Si aucun paramètre n'est fourni, renverra l'objet, * sinon, tentera de renvoyer la valeur du cookie. * * Exemples: * cookie()->make('foo', 'bar'); ou cookie('foo', 'bar'); * $foo = cookie('bar') - * - * @return CookieManagerInterface|CookieInterface|null + * + * @return CookieInterface|CookieManagerInterface|null */ - function cookie(?string $name = null, array|string|null $value = null, int $minutes = 0, array $options = []) + function cookie(?string $name = null, null|array|string $value = null, int $minutes = 0, array $options = []) { $cookie = Services::cookie(); diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 50404fe1..13f78d5e 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -173,7 +173,7 @@ class ServerRequest implements ServerRequestInterface /** * Negotiator */ - protected Negotiator|null $negotiator = null; + protected ?Negotiator $negotiator = null; /** * Créer un nouvel objet de requête. diff --git a/src/Middlewares/BaseMiddleware.php b/src/Middlewares/BaseMiddleware.php index fae5effc..757feac0 100644 --- a/src/Middlewares/BaseMiddleware.php +++ b/src/Middlewares/BaseMiddleware.php @@ -45,7 +45,7 @@ public function init(array $arguments = []): static $method = Text::camel('set_' . $argument); if (method_exists($this, $method)) { call_user_func([$this, $method], $value); - } else if (property_exists($this, $argument)) { + } elseif (property_exists($this, $argument)) { $this->{$argument} = $value; } } @@ -55,7 +55,7 @@ public function init(array $arguments = []): static public function __get($name) { - return $this->arguments[$name] ?? null; + return $this->arguments[$name] ?? null; } /** From cefd0c45fef0a83f618f5b55b2c9e2fea856f53b Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 1 Nov 2023 14:38:42 +0100 Subject: [PATCH 006/111] chore: suppression de la generation des entites delocalisation vers le package wolke --- src/Cli/Commands/Generators/Entity.php | 72 ------------------- .../Commands/Generators/Views/entity.tpl.php | 11 --- 2 files changed, 83 deletions(-) delete mode 100644 src/Cli/Commands/Generators/Entity.php delete mode 100644 src/Cli/Commands/Generators/Views/entity.tpl.php diff --git a/src/Cli/Commands/Generators/Entity.php b/src/Cli/Commands/Generators/Entity.php deleted file mode 100644 index 5775a7cb..00000000 --- a/src/Cli/Commands/Generators/Entity.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Cli\Commands\Generators; - -use BlitzPHP\Cli\Console\Command; -use BlitzPHP\Cli\Traits\GeneratorTrait; - -/** - * Génère un fichier squelette d'entité. - */ -class Entity extends Command -{ - use GeneratorTrait; - - /** - * @var string Groupe - */ - protected $group = 'Generateurs'; - - /** - * @var string Nom - */ - protected $name = 'make:entity'; - - /** - * @var string Description - */ - protected $description = 'Génère un nouveau fichier d\'entité.'; - - /** - * @var string - */ - protected $service = 'Service de génération de code'; - - /** - * @var array Arguments - */ - protected $arguments = [ - 'name' => 'Le nom de la classe d\'entité.', - ]; - - /** - * @var array Options - */ - protected $options = [ - '--namespace' => ["Définit l'espace de noms racine. Par défaut\u{a0}: \"APP_NAMESPACE\".", APP_NAMESPACE], - '--suffix' => 'Ajouter le titre du composant au nom de la classe (par exemple, User => UserEntity).', - '--force' => 'Forcer à écraser le fichier existant.', - ]; - - /** - * {@inheritDoc} - */ - public function execute(array $params) - { - $this->component = 'Entity'; - $this->directory = 'Entities'; - $this->template = 'entity.tpl.php'; - - $this->classNameLang = 'CLI.generator.className.entity'; - $this->runGeneration($params); - } -} diff --git a/src/Cli/Commands/Generators/Views/entity.tpl.php b/src/Cli/Commands/Generators/Views/entity.tpl.php deleted file mode 100644 index 61023695..00000000 --- a/src/Cli/Commands/Generators/Views/entity.tpl.php +++ /dev/null @@ -1,11 +0,0 @@ -<@php - -namespace {namespace}; - -use BlitzPHP\Models\BaseEntity; - -class {class} extends BaseEntity -{ - protected $dates = ['created_at', 'updated_at', 'deleted_at']; - protected $casts = []; -} From e117451396a5a3320c76af52544ef68e9ccb59b4 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 2 Nov 2023 13:01:24 +0100 Subject: [PATCH 007/111] chore: mise a jour des workflow github --- .editorconfig | 19 +++ .github/ISSUE_TEMPLATE/bug.yml | 112 ++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 13 ++ .github/PULL_REQUEST_TEMPLATE.md | 24 ++-- .github/dependabot.yml | 8 +- .github/workflows/dependabot-auto-merge.yml | 32 +++++ .../fix-php-code-style-issues-cs-fixer.yml | 26 ++++ .github/workflows/update-changelog.yml | 31 +++++ CHANGELOG.md | 4 + 9 files changed, 255 insertions(+), 14 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml create mode 100644 .github/workflows/fix-php-code-style-issues-cs-fixer.yml create mode 100644 .github/workflows/update-changelog.yml create mode 100644 CHANGELOG.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7fa7ce89 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = tab +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..dbec92b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,112 @@ +name: Rapport d'erreur +description: Signaler un problème ou un bug pour nous aider à améliorer BlitzPHP +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Nous sommes désolés d'apprendre que vous avez un problème. + + Pouvez-vous nous aider à le résoudre en fournissant les détails suivants. + + Avant de commencer, **veuillez vous assurer qu'il n'existe aucun problème existant, qu'il soit encore ouvert ou clos, lié à votre rapport**. + Si tel est le cas, votre rapport sera fermé rapidement. + + --- + + - type: dropdown + id: php-version + attributes: + label: Version PHP + description: Quelle version de PHP utilisez-vous ? S'il vous plaît soyez aussi précis que possible + multiple: true + options: + - '8.1' + - '8.2' + validations: + required: true + + - type: input + id: blitzphp-version + attributes: + label: Version de BlitzPHP + description: | + ex. 0.9.5 + Si vous n'utilisez pas la [dernière version](https://github.com/blitz-php/framework/releases), + veuillez vérifiez si le problème se produit avec la dernière version. + validations: + required: true + + - type: dropdown + id: operating-systems + attributes: + label: Avec quels systèmes d'exploitation cela se produit-il ? + description: Vous pouvez en sélectionner plusieurs. + multiple: true + options: + - macOS + - Windows + - Linux + validations: + required: true + + - type: dropdown + id: server + attributes: + label: Quel serveur avez-vous utilisé? + options: + - apache + - cli + - cli-server (PHP built-in webserver) + - cgi-fcgi + - fpm-fcgi + - phpdbg + validations: + required: true + + - type: input + id: database + attributes: + label: Base de données + description: ex. MySQL 5.6, MariaDB 10.2, PostgreSQL 9.6 + validations: + required: false + + - type: textarea + id: what-happened + attributes: + label: Ce qui s'est passé? + description: À quoi vous attendiez-vous ? + placeholder: Je ne peux pas actuellement faire X chose parce que quand je le fais, cela casse X chose. + validations: + required: true + + - type: textarea + id: how-to-reproduce + attributes: + label: Comment reproduire le bug + description: Comment cela s'est-il produit, veuillez ajouter toutes les valeurs de configuration utilisées et fournir un ensemble d'étapes fiables si possible. + placeholder: Quand je fais X, je vois Y. + validations: + required: true + + - type: textarea + attributes: + label: Resultat attendu + description: Que pensez-vous qu'il se serait passé à la place de ce bug signalé ? + validations: + required: true + + - type: textarea + id: notes + attributes: + label: Autres choses ? + description: | + Utilisez ce champ pour fournir toute autre note qui, selon vous, pourrait être pertinente au problème. + + Liens? Les références? Tout ce qui nous donnera plus de contexte sur le problème que vous rencontrez ! + + Astuce : Vous pouvez joindre des images ou des fichiers journaux en cliquant sur cette zone pour la mettre en surbrillance, puis en y faisant glisser les fichiers. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..db4dcc21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,13 @@ +blank_issues_enabled: false +contact_links: + - name: Poser une question + url: https://github.com/blitz-php/framework/discussions/new?category=q-a + about: Demandez de l'aide à la communauté + + - name: Demander une fonctionnalité + url: https://github.com/blitz-php/framework/discussions/new?category=ideas + about: Partagez des idées de nouvelles fonctionnalités + + - name: Signaler un problème de sécurité + url: https://github.com/blitz-php/framework/security/policy + about: Découvrez comment nous signaler les bugs sensibles diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 98aa948b..7ab2c24b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,19 +1,19 @@ **Description** -Explain what you have changed, and why. +Expliquez ce que vous avez changé et pourquoi. -**Checklist:** -- [ ] Securely signed commits -- [ ] Component(s) with PHPDoc blocks, only if necessary or adds value -- [ ] Unit testing, with >80% coverage -- [ ] User guide updated -- [ ] Conforms to style guide +**Liste de contrôle:** +- [ ] Des commits signés en toute sécurité +- [ ] Composant(s) avec blocs PHPDoc, uniquement si nécessaire ou ajoute de la valeur +- [ ] Tests unitaires, avec une couverture > 80 % +- [ ] Guide de l'utilisateur mis à jour +- [ ] Conforme au guide de style diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 742a2e71..3ea173af 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,3 +1,6 @@ +# Veuillez consulter la documentation pour toutes les options de configuration : +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + version: 2 updates: @@ -5,13 +8,14 @@ updates: directory: '/' schedule: interval: 'daily' - time: "04:00" open-pull-requests-limit: 10 - package-ecosystem: 'github-actions' directory: '/' schedule: - interval: 'daily' + interval: 'weekly' + labels: + - "dependencies" ignore: - dependency-name: '*' update-types: diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 00000000..d3a09f3f --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,32 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.6.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/fix-php-code-style-issues-cs-fixer.yml b/.github/workflows/fix-php-code-style-issues-cs-fixer.yml new file mode 100644 index 00000000..584be23d --- /dev/null +++ b/.github/workflows/fix-php-code-style-issues-cs-fixer.yml @@ -0,0 +1,26 @@ +name: Check & fix styling + +on: [push] + +permissions: + contents: write + +jobs: + php-cs-fixer: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Run PHP CS Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php-cs-fixer.dist.php --allow-risky=yes + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: Fix styling \ No newline at end of file diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 00000000..47d29170 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,31 @@ +name: "Update Changelog" + +on: + release: + types: [released] + +permissions: + contents: write + +jobs: + update: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: main + + - name: Update Changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ github.event.release.name }} + release-notes: ${{ github.event.release.body }} + + - name: Commit updated CHANGELOG + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: main + commit_message: Update CHANGELOG + file_pattern: CHANGELOG.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..4e21d21d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Journal des modifications + +Toutes les modifications notables apportées à `:package_name` seront documentées dans ce fichier. + From bde5a5f481b6551ac2433a9a6e95b9ac9ccc9deb Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 2 Nov 2023 16:20:04 +0100 Subject: [PATCH 008/111] fix: getEnv de la ServerRequest renvoi null --- src/Http/ServerRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 13f78d5e..ba183b77 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -1354,7 +1354,7 @@ public function withProtocolVersion(string $version): static public function getEnv(string $key, ?string $default = null): ?string { $key = strtoupper($key); - if (! array_key_exists($key, $this->_environment)) { + if (! array_key_exists($key, $this->_environment) || null === $this->_environment[$key]) { $this->_environment[$key] = env($key); } From 1391ded398f8c2c4025065c84e33008b451f748a Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 2 Nov 2023 17:41:39 +0100 Subject: [PATCH 009/111] test: RouteCollection --- .../Router/RouterCollection.spec.php | 502 ++++++++++++++++-- src/Router/RouteCollection.php | 21 +- 2 files changed, 471 insertions(+), 52 deletions(-) diff --git a/spec/system/framework/Router/RouterCollection.spec.php b/spec/system/framework/Router/RouterCollection.spec.php index 8b264394..a1820fdd 100644 --- a/spec/system/framework/Router/RouterCollection.spec.php +++ b/spec/system/framework/Router/RouterCollection.spec.php @@ -14,7 +14,7 @@ use BlitzPHP\Router\RouteCollection; use Spec\BlitzPHP\App\Controllers\HomeController; -function getCollector(array $config = [], array $files = []): RouteCollection +function getCollector(string $verb = 'get', array $config = [], array $files = []): RouteCollection { $defaults = ['App' => APP_PATH]; $config = array_merge($config, $defaults); @@ -26,7 +26,7 @@ function getCollector(array $config = [], array $files = []): RouteCollection $routing = (object) config('routing'); $routing->default_namespace = '\\'; - return (new RouteCollection($loader, $routing))->setHTTPVerb('get'); + return (new RouteCollection($loader, $routing))->setHTTPVerb($verb); } function setRequestMethod(string $method): void @@ -34,13 +34,13 @@ function setRequestMethod(string $method): void Services::set(Request::class, Services::request()->withMethod($method)); } -describe('RouteCollection', static function () { - beforeEach(static function () { +describe('RouteCollection', function () { + beforeEach(function () { // Services::reset(false); }); - describe('Ajout de route', static function () { - it('Test de base', static function () { + describe('Ajout de route', function () { + it('Test de base', function () { $routes = getCollector(); $routes->add('home', '\my\controller'); @@ -49,7 +49,7 @@ function setRequestMethod(string $method): void ]); }); - it('Test de base avec un callback', static function () { + it('Test de base avec un callback', function () { $routes = getCollector(); $routes->add('home', [HomeController::class, 'index']); @@ -58,7 +58,7 @@ function setRequestMethod(string $method): void ]); }); - it('Test de base avec un callback et des parametres', static function () { + it('Test de base avec un callback et des parametres', function () { $routes = getCollector(); $routes->add('product/(:num)/(:num)', [[HomeController::class, 'index'], '$2/$1']); @@ -67,7 +67,7 @@ function setRequestMethod(string $method): void ]); }); - it('Test de base avec un callback avec des parametres sans la chaine de definition', static function () { + it('Test de base avec un callback avec des parametres sans la chaine de definition', function () { $routes = getCollector(); $routes->add('product/(:num)/(:num)', [HomeController::class, 'index']); @@ -76,7 +76,7 @@ function setRequestMethod(string $method): void ]); }); - it("Ajout du namespace par défaut quand il n'a pas été défini", static function () { + it("Ajout du namespace par défaut quand il n'a pas été défini", function () { $routes = getCollector(); $routes->add('home', 'controller'); @@ -85,7 +85,7 @@ function setRequestMethod(string $method): void ]); }); - it("Ignorer le namespace par défaut lorsqu'il existe", static function () { + it("Ignorer le namespace par défaut lorsqu'il existe", function () { $routes = getCollector(); $routes->add('home', 'my\controller'); @@ -94,7 +94,7 @@ function setRequestMethod(string $method): void ]); }); - it('Ajout avec un slash en debut de chaine', static function () { + it('Ajout avec un slash en debut de chaine', function () { $routes = getCollector(); $routes->add('/home', 'controller'); @@ -104,8 +104,8 @@ function setRequestMethod(string $method): void }); }); - describe('Correspondance des verbes HTTP', static function () { - it('Match fonctionne avec la methode HTTP actuel', static function () { + describe('Correspondance des verbes HTTP', function () { + it('Match fonctionne avec la methode HTTP actuel', function () { setRequestMethod('GET'); $routes = getCollector(); @@ -116,7 +116,7 @@ function setRequestMethod(string $method): void ]); }); - it('Match ignore les methodes HTTP invalide', static function () { + it('Match ignore les methodes HTTP invalide', function () { setRequestMethod('GET'); $routes = getCollector(); @@ -125,7 +125,21 @@ function setRequestMethod(string $method): void expect($routes->getRoutes())->toBe([]); }); - it('Add fonctionne avec un tableau de verbes HTTP', static function () { + it('Match supporte plusieurs methodes', function () { + setRequestMethod('GET'); + $routes = getCollector(); + + $routes->match(['get', 'post'], 'here', 'there'); + expect($routes->getRoutes())->toBe(['here' => '\there']); + + setRequestMethod('POST'); + $routes = getCollector(); + + $routes->match(['get', 'post'], 'here', 'there'); + expect($routes->getRoutes())->toBe(['here' => '\there']); + }); + + it('Add fonctionne avec un tableau de verbes HTTP', function () { setRequestMethod('POST'); $routes = getCollector(); @@ -136,7 +150,7 @@ function setRequestMethod(string $method): void ]); }); - it('Add remplace les placeholders par defaut avec les bons regex', static function () { + it('Add remplace les placeholders par defaut avec les bons regex', function () { $routes = getCollector(); $routes->add('home/(:any)', 'controller'); @@ -145,7 +159,7 @@ function setRequestMethod(string $method): void ]); }); - it('Add remplace les placeholders personnalisés avec les bons regex', static function () { + it('Add remplace les placeholders personnalisés avec les bons regex', function () { $routes = getCollector(); $routes->addPlaceholder('smiley', ':-)'); $routes->add('home/(:smiley)', 'controller'); @@ -155,7 +169,7 @@ function setRequestMethod(string $method): void ]); }); - it('Add reconnait le namespace par défaut', static function () { + it('Add reconnait le namespace par défaut', function () { $routes = getCollector(); $routes->setDefaultNamespace('\Spec\BlitzPHP\App\Controllers'); $routes->add('home', 'HomeController'); @@ -166,29 +180,29 @@ function setRequestMethod(string $method): void }); }); - describe('Setters', static function () { - it('Modification du controleur par defaut', static function () { + describe('Setters', function () { + it('Modification du controleur par defaut', function () { $routes = getCollector(); $routes->setDefaultController('kishimoto'); expect($routes->getDefaultController())->toBe('kishimotoController'); }); - it('Modification de la methode par defaut', static function () { + it('Modification de la methode par defaut', function () { $routes = getCollector(); $routes->setDefaultMethod('minatoNavigation'); expect($routes->getDefaultMethod())->toBe('minatoNavigation'); }); - it('TranslateURIDashes', static function () { + it('TranslateURIDashes', function () { $routes = getCollector(); $routes->setTranslateURIDashes(true); expect($routes->shouldTranslateURIDashes())->toBeTruthy(); }); - it('AutoRoute', static function () { + it('AutoRoute', function () { $routes = getCollector(); $routes->setAutoRoute(true); @@ -196,10 +210,10 @@ function setRequestMethod(string $method): void }); }); - describe('Groupement', static function () { - it('Les regroupements de routes fonctionne', static function () { + describe('Groupement', function () { + it('Les regroupements de routes fonctionne', function () { $routes = getCollector(); - $routes->group('admin', static function ($routes): void { + $routes->group('admin', function ($routes): void { $routes->add('users/list', '\UsersController::list'); }); @@ -208,9 +222,9 @@ function setRequestMethod(string $method): void ]); }); - it('Netoyage du nom de groupe', static function () { + it('Netoyage du nom de groupe', function () { $routes = getCollector(); - $routes->group('', + '', + '', + '', + '', + '', + ]; + + + expect(fn() => $view->stylesBundle())->toEcho(''); + + foreach ($expecteds as $expected) { + expect(fn() => $view->scriptsBundle())->toMatchEcho(fn($actual) => str_contains($actual, $expected)); + } + }); + }); +}); diff --git a/spec/system/framework/Views/View.spec.php b/spec/system/framework/Views/View.spec.php index eb9a4759..b77aa0d3 100644 --- a/spec/system/framework/Views/View.spec.php +++ b/spec/system/framework/Views/View.spec.php @@ -16,10 +16,6 @@ use BlitzPHP\View\View; describe('Views / View', function () { - beforeAll(function () { - config()->set('view.decorators', []); - }); - describe('Donnees', function () { it('Peut-on stocker des variable', function () { $view = new View(); diff --git a/src/View/Adapters/BladeAdapter.php b/src/View/Adapters/BladeAdapter.php index ea69f649..8af0926a 100644 --- a/src/View/Adapters/BladeAdapter.php +++ b/src/View/Adapters/BladeAdapter.php @@ -36,7 +36,7 @@ public function __construct(protected array $config, $viewPathLocator = null, pr $this->engine = new Blade( $this->viewPath ?: VIEW_PATH, - $this->config['cache_path'] ?? VIEW_CACHE_PATH . 'blade' . DIRECTORY_SEPARATOR . 'cache' + $this->config['cache_path'] ?? VIEW_CACHE_PATH . 'blade' . DIRECTORY_SEPARATOR ); $this->configure(); @@ -68,6 +68,13 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n */ private function configure(): void { + if (isset($this->config['configure']) && is_callable($this->config['configure'])) { + $newInstance = $this->config['configure']($this->engine); + if ($newInstance instanceof Blade) { + $this->engine = $newInstance; + } + } + $directives = (array) ($this->config['directives'] ?? []); foreach ($directives as $name => $callable) { From 277cd78d28b6d6b086fe1d33bb51b859e774cf38 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 7 Dec 2023 19:37:38 +0100 Subject: [PATCH 084/111] feat : Prise en charge des classes et attributs CSS conditionnels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cette modification introduit de nouvelles méthodes dans la classe NativeAdapter qui permettent de compiler des classes CSS conditionnelles et des styles en ligne. En outre, elle fournit des fonctions permettant d'indiquer l'état coché, sélectionné, désactivé, en lecture seule et requis des éléments HTML en fonction de conditions données. La nouvelle section Directives du fichier de test NativeAdapter.spec.php vérifie l'exactitude de ces nouvelles méthodes. --- .../framework/Views/NativeAdapter.spec.php | 91 +++++++++++++++++++ src/View/Adapters/NativeAdapter.php | 85 +++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/spec/system/framework/Views/NativeAdapter.spec.php b/spec/system/framework/Views/NativeAdapter.spec.php index cdc3df2c..87bcccf5 100644 --- a/spec/system/framework/Views/NativeAdapter.spec.php +++ b/spec/system/framework/Views/NativeAdapter.spec.php @@ -219,4 +219,95 @@ } }); }); + + describe('Directives', function () { + beforeAll(function () { + $this->view = new NativeAdapter($this->config); + }); + + it('class', function () { + expect($this->view->class([]))->toBe(''); + + $isActive = false; + $hasError = true; + + expect($this->view->class([ + 'p-4', + 'font-bold' => $isActive, + 'text-gray-500' => ! $isActive, + 'bg-red' => $hasError, + ]))->toBe('class="p-4 text-gray-500 bg-red"'); + }); + + it('style', function () { + expect($this->view->style([]))->toBe(''); + + $isActive = true; + + expect($this->view->style([ + 'background-color: red', + 'font-weight: bold' => $isActive, + ]))->toBe('style="background-color: red; font-weight: bold;"'); + }); + + it('checked', function () { + expect($this->view->checked('a'))->toBe(''); + expect($this->view->checked('true'))->toBe('checked="checked"'); + expect($this->view->checked('1'))->toBe('checked="checked"'); + expect($this->view->checked(true))->toBe('checked="checked"'); + expect($this->view->checked(1))->toBe('checked="checked"'); + expect($this->view->checked(0))->toBe(''); + expect($this->view->checked('0'))->toBe(''); + expect($this->view->checked('false'))->toBe(''); + expect($this->view->checked(false))->toBe(''); + }); + + it('selected', function () { + expect($this->view->selected('a'))->toBe(''); + expect($this->view->selected('true'))->toBe('selected="selected"'); + expect($this->view->selected('1'))->toBe('selected="selected"'); + expect($this->view->selected(true))->toBe('selected="selected"'); + expect($this->view->selected(1))->toBe('selected="selected"'); + expect($this->view->selected(0))->toBe(''); + expect($this->view->selected('0'))->toBe(''); + expect($this->view->selected('false'))->toBe(''); + expect($this->view->selected(false))->toBe(''); + }); + + it('disabled', function () { + expect($this->view->disabled('a'))->toBe(''); + expect($this->view->disabled('true'))->toBe('disabled'); + expect($this->view->disabled('1'))->toBe('disabled'); + expect($this->view->disabled(true))->toBe('disabled'); + expect($this->view->disabled(1))->toBe('disabled'); + expect($this->view->disabled(0))->toBe(''); + expect($this->view->disabled('0'))->toBe(''); + expect($this->view->disabled('false'))->toBe(''); + expect($this->view->disabled(false))->toBe(''); + }); + + it('required', function () { + expect($this->view->required('a'))->toBe(''); + expect($this->view->required('true'))->toBe('required'); + expect($this->view->required('1'))->toBe('required'); + expect($this->view->required(true))->toBe('required'); + expect($this->view->required(1))->toBe('required'); + expect($this->view->required(0))->toBe(''); + expect($this->view->required('0'))->toBe(''); + expect($this->view->required('false'))->toBe(''); + expect($this->view->required(false))->toBe(''); + }); + + it('readonly', function () { + expect($this->view->readonly('a'))->toBe(''); + expect($this->view->readonly('true'))->toBe('readonly'); + expect($this->view->readonly('1'))->toBe('readonly'); + expect($this->view->readonly(true))->toBe('readonly'); + expect($this->view->readonly(1))->toBe('readonly'); + expect($this->view->readonly(0))->toBe(''); + expect($this->view->readonly('0'))->toBe(''); + expect($this->view->readonly('false'))->toBe(''); + expect($this->view->readonly(false))->toBe(''); + }); + }); }); diff --git a/src/View/Adapters/NativeAdapter.php b/src/View/Adapters/NativeAdapter.php index f2fbae81..8ec087fb 100644 --- a/src/View/Adapters/NativeAdapter.php +++ b/src/View/Adapters/NativeAdapter.php @@ -495,6 +495,91 @@ public function insertIf(string $view, ?array $data = [], ?array $options = null return ''; } + /** + * Compile de manière conditionnelle une chaîne de classe CSS. + */ + public function class(array $classes): string + { + if ($classes === []) { + return ''; + } + + $class = []; + + foreach ($classes as $key => $value) { + if (is_int($key)) { + $class[] = $value; + } elseif (true === $value) { + $class[] = $key; + } + } + + return 'class="' . implode(' ', $class) . '"'; + } + + /** + * Ajoute conditionnellement des styles CSS en ligne à un élément HTML + */ + public function style(array $styles): string + { + if ($styles === []) { + return ''; + } + + $style = []; + + foreach ($styles as $key => $value) { + if (is_int($key)) { + $style[] = $value . ';'; + } elseif (true === $value) { + $style[] = $key . ';'; + } + } + + return 'style="' . implode(' ', $style) . '"'; + } + + /** + * Utiliser pour indiquer facilement si une case à cocher HTML donnée est "cochée". + * Indiquera que la case est cochée si la condition fournie est évaluée à true. + */ + public function checked(bool|string $condition): string + { + return true === filter_var($condition, FILTER_VALIDATE_BOOLEAN) ? 'checked="checked"' : ''; + } + + /** + * Utiliser pour indiquer si une option de sélection donnée doit être "sélectionnée". + */ + public function selected(bool|string $condition): string + { + return true === filter_var($condition, FILTER_VALIDATE_BOOLEAN) ? 'selected="selected"' : ''; + } + + /** + * Utiliser pour indiquer si un élément donné doit être "désactivé". + */ + public function disabled(bool|string $condition): string + { + return true === filter_var($condition, FILTER_VALIDATE_BOOLEAN) ? 'disabled' : ''; + } + + /** + * Utiliser pour indiquer si un élément donné doit être "readonly". + */ + public function readonly(bool|string $condition): string + { + return true === filter_var($condition, FILTER_VALIDATE_BOOLEAN) ? 'readonly' : ''; + } + + /** + * Utiliser pour indiquer si un élément donné doit être "obligatoire". + */ + public function required(bool|string $condition): string + { + return true === filter_var($condition, FILTER_VALIDATE_BOOLEAN) ? 'required' : ''; + } + /** * Ajoute un fichier css de librairie a la vue */ From f71310f956a10a653d33cda30a42ad9e02086393 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 7 Dec 2023 20:10:00 +0100 Subject: [PATCH 085/111] =?UTF-8?q?Refactoriser=20la=20m=C3=A9thode=20exte?= =?UTF-8?q?nd=20de=20NativeAdapter=20pour=20utiliser=20une=20fonction=20se?= =?UTF-8?q?tter=20pour=20la=20propri=C3=A9t=C3=A9=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce commit refactorise la méthode extend de la classe NativeAdapter pour utiliser une fonction setter, setLayout(), au lieu de modifier directement la propriété layout. Cette modification améliore l'encapsulation et garantit que toute modification future du comportement de la propriété layout pourra être effectuée de manière cohérente. --- src/View/Adapters/NativeAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/Adapters/NativeAdapter.php b/src/View/Adapters/NativeAdapter.php index 8ec087fb..eaed4efe 100644 --- a/src/View/Adapters/NativeAdapter.php +++ b/src/View/Adapters/NativeAdapter.php @@ -267,7 +267,7 @@ public function resetData(): self */ public function extend(string $layout) { - $this->layout = $layout; + $this->setLayout($layout); } /** From a57bb59af7e94d8d37e0bd97edab7e0204081e66 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 7 Dec 2023 20:10:53 +0100 Subject: [PATCH 086/111] test(view): NativeAdapter --- .../framework/Views/NativeAdapter.spec.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spec/system/framework/Views/NativeAdapter.spec.php b/spec/system/framework/Views/NativeAdapter.spec.php index 87bcccf5..047230cb 100644 --- a/spec/system/framework/Views/NativeAdapter.spec.php +++ b/spec/system/framework/Views/NativeAdapter.spec.php @@ -220,6 +220,32 @@ }); }); + describe('Methodes speciales', function () { + beforeAll(function () { + $this->view = new NativeAdapter($this->config); + }); + + it('title', function() { + expect($this->view->getData())->not->toContainKey('title'); + expect($this->view->title('My Title'))->toBeAnInstanceOf(NativeAdapter::class); + expect($this->view->getData())->toContainKey('title'); + expect($this->view->getData()['title'])->toBe('My Title'); + }); + + it('meta', function() { + expect($this->view->meta('description'))->toBeEmpty(); + expect($this->view->meta('description', 'BlitzPHP'))->toBeAnInstanceOf(NativeAdapter::class); + expect($this->view->meta('charset', 'utf-8'))->toBeAnInstanceOf(NativeAdapter::class); + expect($this->view->meta('description'))->toBe('BlitzPHP'); + expect($this->view->meta('charset'))->toBe('utf-8'); + }); + + it('except', function () { + expect($this->view->excerpt('methodes speciales'))->toBe('methodes speciales'); + expect($this->view->excerpt('methodes speciales', 8))->toBe('metho...'); + }); + }); + describe('Directives', function () { beforeAll(function () { $this->view = new NativeAdapter($this->config); From f9905b356751d730578d65f10622a349f4c0a715 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 7 Dec 2023 20:11:19 +0100 Subject: [PATCH 087/111] cs-fix --- src/View/Adapters/BladeAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/Adapters/BladeAdapter.php b/src/View/Adapters/BladeAdapter.php index 8af0926a..d72a04ee 100644 --- a/src/View/Adapters/BladeAdapter.php +++ b/src/View/Adapters/BladeAdapter.php @@ -68,7 +68,7 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n */ private function configure(): void { - if (isset($this->config['configure']) && is_callable($this->config['configure'])) { + if (isset($this->config['configure']) && is_callable($this->config['configure'])) { $newInstance = $this->config['configure']($this->engine); if ($newInstance instanceof Blade) { $this->engine = $newInstance; From 3d6ec55f02625a04f45896c474c639717fdb02f5 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Fri, 8 Dec 2023 11:14:00 +0100 Subject: [PATCH 088/111] fix analysis --- phpstan-baseline.php | 5 +++++ src/Config/Config.php | 15 ++++----------- src/Container/Services.php | 12 ++++++------ src/Facades/Container.php | 26 +++++++++++++------------- src/Http/UrlGenerator.php | 18 ++++++------------ src/Router/Router.php | 4 ++-- 6 files changed, 36 insertions(+), 44 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 4d9a2fab..ef85f93c 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -581,6 +581,11 @@ 'count' => 1, 'path' => __DIR__ . '/src/View/Adapters/BladeAdapter.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Class Jenssegers\\\\Blade\\\\Blade not found\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/View/Adapters/BladeAdapter.php', +]; $ignoreErrors[] = [ 'message' => '#^Instantiated class Jenssegers\\\\Blade\\\\Blade not found\\.$#', 'count' => 1, diff --git a/src/Config/Config.php b/src/Config/Config.php index c5051dd3..07b4b071 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -17,7 +17,6 @@ use BlitzPHP\Exceptions\ConfigException; use BlitzPHP\Utilities\Helpers; use BlitzPHP\Utilities\Iterable\Arr; -use InvalidArgumentException; use Nette\Schema\Expect; use Nette\Schema\Schema; use ReflectionClass; @@ -159,9 +158,6 @@ public function load($config, ?string $file = null, ?Schema $schema = null, bool { if (is_array($config)) { foreach ($config as $key => $value) { - if (! is_string($value) || empty($value)) { - continue; - } if (is_string($key)) { $file = $value; $conf = $key; @@ -169,9 +165,9 @@ public function load($config, ?string $file = null, ?Schema $schema = null, bool $file = null; $conf = $value; } - self::load($conf, $file, null, $allow_empty); + $this->load($conf, $file, null, $allow_empty); } - } elseif (is_string($config) && ! isset(self::$loaded[$config])) { + } elseif (! isset(self::$loaded[$config])) { $file ??= self::path($config); $schema ??= self::schema($config); @@ -197,15 +193,12 @@ public function load($config, ?string $file = null, ?Schema $schema = null, bool /** * Affiche l'exception dû à la mauvaise definition d'une configuration * - * @param array|string $accepts_values - * @param string $group (app, data, database, etc.) + * @param string $group (app, data, database, etc.) */ - public static function exceptBadConfigValue(string $config_key, $accepts_values, string $group) + public static function exceptBadConfigValue(string $config_key, array|string $accepts_values, string $group) { if (is_array($accepts_values)) { $accepts_values = '(Accept values: ' . implode('/', $accepts_values) . ')'; - } elseif (! is_string($accepts_values)) { - throw new InvalidArgumentException('Misuse of the method ' . __METHOD__); } throw new ConfigException("The '{$group}.{$config_key} configuration is not set correctly. {$accepts_values} \n Please edit '{" . self::path($group) . "}' file to correct it"); diff --git a/src/Container/Services.php b/src/Container/Services.php index ae87237c..03c51717 100644 --- a/src/Container/Services.php +++ b/src/Container/Services.php @@ -107,7 +107,7 @@ public static function autoloader(bool $shared = true): Autoloader $config = static::config()->get('autoload'); $helpers = array_merge(['url'], ($config['helpers'] ?? [])); - return static::$instances[Autoloader::class] = new Autoloader($config, $helpers); + return static::$instances[Autoloader::class] = new Autoloader(/** @scrutinizer ignore-type */ $config, $helpers); } /** @@ -354,7 +354,7 @@ public static function responsecache(bool $shared = true): ResponseCache return static::$instances[ResponseCache::class]; } - return static::$instances[ResponseCache::class] = new ResponseCache(static::cache(), static::config()->get('cache.cache_query_string')); + return static::$instances[ResponseCache::class] = new ResponseCache(static::cache(), /** @scrutinizer ignore-type */ static::config()->get('cache.cache_query_string')); } /** @@ -415,8 +415,8 @@ public static function session(bool $shared = true): Store } } - Cookie::setDefaults($cookies = static::config()->get('cookie')); - $session = new Store($config, $cookies, Helpers::ipAddress()); + Cookie::setDefaults($cookies = /** @scrutinizer ignore-type */ static::config()->get('cookie')); + $session = new Store((array) $config, (array) $cookies, Helpers::ipAddress()); $session->setLogger(static::logger()); $session->setDatabase($db); @@ -436,7 +436,7 @@ public static function storage(bool $shared = true): FilesystemManager return static::$instances[FilesystemManager::class]; } - return static::$instances[FilesystemManager::class] = new FilesystemManager(static::config()->get('filesystems')); + return static::$instances[FilesystemManager::class] = new FilesystemManager(/** @scrutinizer ignore-type */ static::config()->get('filesystems')); } /** @@ -460,7 +460,7 @@ public static function toolbar(?stdClass $config = null, bool $shared = true): T return static::$instances[Toolbar::class]; } - $config ??= (object) config('toolbar'); + $config ??= (object) static::config()->get('toolbar'); return static::$instances[Toolbar::class] = new Toolbar($config); } diff --git a/src/Facades/Container.php b/src/Facades/Container.php index 1cc9c8ff..18023079 100644 --- a/src/Facades/Container.php +++ b/src/Facades/Container.php @@ -14,19 +14,19 @@ use BlitzPHP\Container\Services; /** - * @method static void add(string $key, \Closure $callback) Defini un element au conteneur sous forme de factory. Si l'element existe déjà, il sera remplacé. - * @method static void addIf(string $key, \Closure $callback) Defini un element au conteneur sous forme de factory. Si l'element existe déjà, il sera ignoré. - * @method static bool bound(string $name) Verifie qu'une entree a été explicitement définie dans le conteneur. - * @method static mixed call(array|callable|string $callable, array $parameters = []) Appelez la fonction donnée en utilisant les paramètres donnés. Les paramètres manquants seront résolus à partir du conteneur. - * @method static string debugEntry(string $name) Obtenir les informations de débogage de l'entrée. - * @method static mixed get(string $name) Renvoie une entrée du conteneur par son nom. - * @method static array getKnownEntryNames() Obtenez des entrées de conteneur définies. - * @method static bool has(string $name) Testez si le conteneur peut fournir quelque chose pour le nom donné. - * @method static object injectOn(object $instance) Injectez toutes les dépendances sur une instance existante. - * @method static void merge(array $keys) Defini plusieurs elements au conteneur sous forme de factory. L'element qui existera déjà sera remplacé par la correspondance du tableau. - * @method static void mergeIf(array $keys) Defini plusieurs elements au conteneur sous forme de factory. L'element qui existera déjà sera ignoré. - * @method static void set(string $name, mixed $value) Définissez un objet ou une valeur dans le conteneur. - * @method static mixed make(string $name, array $parameters = []) Construire une entrée du conteneur par son nom. Cette méthode se comporte comme singleton() sauf qu'elle résout à nouveau l'entrée à chaque fois. Par exemple, si l'entrée est une classe, une nouvelle instance sera créée à chaque fois. Cette méthode fait que le conteneur se comporte comme une factory. + * @method static void add(string $key, \Closure $callback) Defini un element au conteneur sous forme de factory. Si l'element existe déjà, il sera remplacé. + * @method static void addIf(string $key, \Closure $callback) Defini un element au conteneur sous forme de factory. Si l'element existe déjà, il sera ignoré. + * @method static bool bound(string $name) Verifie qu'une entree a été explicitement définie dans le conteneur. + * @method static mixed call(array|callable|string $callable, array $parameters = []) Appelez la fonction donnée en utilisant les paramètres donnés. Les paramètres manquants seront résolus à partir du conteneur. + * @method static string debugEntry(string $name) Obtenir les informations de débogage de l'entrée. + * @method static mixed get(string $name) Renvoie une entrée du conteneur par son nom. + * @method static array getKnownEntryNames() Obtenez des entrées de conteneur définies. + * @method static bool has(string $name) Testez si le conteneur peut fournir quelque chose pour le nom donné. + * @method static object injectOn(object $instance) Injectez toutes les dépendances sur une instance existante. + * @method static mixed make(string $name, array $parameters = []) Construire une entrée du conteneur par son nom. Cette méthode résout à nouveau l'entrée à chaque fois. Par exemple, si l'entrée est une classe, une nouvelle instance sera créée à chaque fois. Cette méthode fait que le conteneur se comporte comme une factory. + * @method static void merge(array $keys) Defini plusieurs elements au conteneur sous forme de factory. L'element qui existera déjà sera remplacé par la correspondance du tableau. + * @method static void mergeIf(array $keys) Defini plusieurs elements au conteneur sous forme de factory. L'element qui existera déjà sera ignoré. + * @method static void set(string $name, mixed $value) Définissez un objet ou une valeur dans le conteneur. * * @see \BlitzPHP\Container\Container */ diff --git a/src/Http/UrlGenerator.php b/src/Http/UrlGenerator.php index f34ed3bb..b0a47dc7 100644 --- a/src/Http/UrlGenerator.php +++ b/src/Http/UrlGenerator.php @@ -137,9 +137,7 @@ public function previous($fallback = false): string */ protected function getPreviousUrlFromSession(): ?string { - $session = $this->getSession(); - - return $session ? $session->previousUrl() : null; + return $this->getSession()?->previousUrl(); } /** @@ -228,7 +226,7 @@ protected function removeIndex(string $root): string { $i = 'index.php'; - return Text::contains($root, $i) ? str_replace('/' . $i, '', $root) : $root; + return Text::contains($root, /** @scrutinizer ignore-type */ $i) ? str_replace('/' . $i, '', $root) : $root; } /** @@ -249,15 +247,11 @@ public function formatScheme(?bool $secure = null): string /** * Get the URL to a named route. - * - * @return false|string */ - public function route(string $name, array $parameters = [], bool $absolute = true) + public function route(string $name, array $parameters = [], bool $absolute = true): string { - $route = $this->routes->reverseRoute($name, ...$parameters); - - if (! $route) { - throw HttpException::invalidRedirectRoute($route); + if (false === $route = $this->routes->reverseRoute($name, ...$parameters)) { + throw HttpException::invalidRedirectRoute($name); } return $absolute ? site_url($route) : $route; @@ -319,7 +313,7 @@ public function formatRoot(string $scheme, ?string $root = null): string $root = $this->cachedRoot; } - $start = Text::startsWith($root, 'http://') ? 'http://' : 'https://'; + $start = Text::startsWith($root, /** @scrutinizer ignore-type */ 'http://') ? 'http://' : 'https://'; return preg_replace('~' . $start . '~', $scheme, $root, 1); } diff --git a/src/Router/Router.php b/src/Router/Router.php index 09f96ced..430d53f0 100644 --- a/src/Router/Router.php +++ b/src/Router/Router.php @@ -197,7 +197,7 @@ public function controllerName() : trim($this->collection->getDefaultNamespace(), '\\') . '\\' . $this->controller; $controller = preg_replace( - ['#(\_)?Controller$#i', '#' . config('app.url_suffix') . '$#i'], + ['#(\_)?Controller$#i', '#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i'], '', ucfirst($controller) ); @@ -377,7 +377,7 @@ protected function checkRoutes(string $uri): bool ); if ($this->collection->shouldUseSupportedLocalesOnly() - && ! in_array($matched['locale'], config('App')->supportedLocales, true)) { + && ! in_array($matched['locale'], config('app.supported_locales'), true)) { // Lancer une exception pour empêcher l'autorouteur, // si activé, essayer de trouver une route throw PageNotFoundException::localeNotSupported($matched['locale']); From 51d9cb7b2a90e34728fbe903633706ad80011cd9 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 10 Dec 2023 14:52:47 +0100 Subject: [PATCH 089/111] [Http] Ajout des classes MiddlewareQueue et MiddlewareRunner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce commit ajoute deux nouvelles classes, MiddlewareQueue et MiddlewareRunner, pour gérer l'exécution des middleware dans la couche HTTP. MiddlewareQueue fournit des méthodes pour ajouter, insérer et gérer des intergiciels dans une file d'attente, tandis que MiddlewareRunner est responsable de l'exécution de la chaîne d'intergiciels. Ces classes améliorent la modularité et la flexibilité du framework BlitzPHP, permettant aux développeurs d'ajouter et de gérer facilement des middlewares pour le traitement des requêtes. --- src/Http/MiddlewareQueue.php | 361 +++++++++++++++++++++++++++ src/Http/MiddlewareRunner.php | 63 +++++ src/Middlewares/BaseMiddleware.php | 9 +- src/Middlewares/ClosureDecorator.php | 80 ++++++ src/Router/Dispatcher.php | 16 +- 5 files changed, 515 insertions(+), 14 deletions(-) create mode 100644 src/Http/MiddlewareQueue.php create mode 100644 src/Http/MiddlewareRunner.php create mode 100644 src/Middlewares/ClosureDecorator.php diff --git a/src/Http/MiddlewareQueue.php b/src/Http/MiddlewareQueue.php new file mode 100644 index 00000000..ab9c7a53 --- /dev/null +++ b/src/Http/MiddlewareQueue.php @@ -0,0 +1,361 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Http; + +use BlitzPHP\Container\Container; +use BlitzPHP\Core\App; +use BlitzPHP\Middlewares\BaseMiddleware; +use BlitzPHP\Middlewares\ClosureDecorator; +use Closure; +use Countable; +use InvalidArgumentException; +use LogicException; +use OutOfBoundsException; +use Psr\Http\Server\MiddlewareInterface; +use SeekableIterator; + +class MiddlewareQueue implements Countable, SeekableIterator +{ + /** + * Middlewares a executer pour la requete courante + * + * @var array + */ + protected array $queue = []; + + /** + * Index du middleware actuellement executer + */ + protected int $position = 0; + + /** + * Aliases des middlewares + */ + protected array $aliases = []; + + /** + * Constructor + * + * @param array $middleware Liste des middlewares initiaux + */ + public function __construct(protected Container $container, array $middleware = [], protected ?Request $request = null, protected ?Response $response = null) + { + $this->queue = $middleware; + $this->request = $request ?: $this->container->get(Request::class); + $this->response = $response ?: $this->container->get(Response::class); + } + + /** + * Ajoute un alias de middleware + */ + public function alias(string $alias, callable|object|string $middleware): self + { + return $this->aliases([$alias => $middleware]); + } + + /** + * Ajoute des alias de middlewares + */ + public function aliases(array $aliases): self + { + $this->aliases = array_merge($this->aliases, $aliases); + + return $this; + } + + /** + * Ajoute un middleware a la chaine d'execution + * + * @param array|callable|object|string $middlewares + */ + public function add(MiddlewareInterface|Closure|array|string $middleware): static + { + if (is_array($middleware)) { + $this->queue = array_merge($this->queue, $middleware); + + return $this; + } + $this->queue[] = $middleware; + + return $this; + } + + /** + * Alias pour MiddlewareQueue::add(). + * + * @see MiddlewareQueue::add() + */ + public function push(MiddlewareInterface|Closure|array|string $middleware): static + { + return $this->add($middleware); + } + + /** + * Ajoute un middleware en bout de chaine + * + * Alias pour MiddlewareQueue::add(). + * + * @see MiddlewareQueue::add() + */ + public function append(MiddlewareInterface|Closure|array|string $middleware): static + { + return $this->add($middleware); + } + + /** + * Ajoute un middleware en debut de chaine + */ + public function prepend(MiddlewareInterface|Closure|array|string $middleware): static + { + if (is_array($middleware)) { + $this->queue = array_merge($middleware, $this->queue); + + return $this; + } + array_unshift($this->queue, $middleware); + + return $this; + } + + /** + * insert un middleware a une position donnee. + * + * Alias pour MiddlewareQueue::add(). + * + * @param int $index La position où le middleware doit être insérer. + * + * @see MiddlewareQueue::add() + */ + public function insert(int $index, MiddlewareInterface|Closure|string $middleware): static + { + return $this->insertAt($index, $middleware); + } + + /** + * Insérez un middleware appelable à un index spécifique. + * + * Si l'index existe déjà, le nouvel appelable sera inséré, + * et l'élément existant sera décalé d'un indice supérieur. + * + * @param int $index La position où le middleware doit être insérer. + */ + public function insertAt(int $index, MiddlewareInterface|Closure|string $middleware): static + { + array_splice($this->queue, $index, 0, [$middleware]); + + return $this; + } + + /** + * Insérez un objet middleware avant la première classe correspondante. + * + * Trouve l'index du premier middleware qui correspond à la classe fournie, + * et insère le middleware fourni avant. + * + * @param string $class Le nom de classe pour insérer le middleware avant. + * + * @throws LogicException Si le middleware à insérer avant n'est pas trouvé. + */ + public function insertBefore(string $class, MiddlewareInterface|Closure|string $middleware): static + { + $found = false; + $i = 0; + + if (array_key_exists($class, $this->aliases) && is_string($this->aliases[$class])) { + $class = $this->aliases[$class]; + } + + foreach ($this->queue as $i => $object) { + if ((is_string($object) && $object === $class) || is_a($object, $class)) { + $found = true; + break; + } + } + + if ($found) { + return $this->insertAt($i, $middleware); + } + + throw new LogicException(sprintf("No middleware matching '%s' could be found.", $class)); + } + + /** + * Insérez un objet middleware après la première classe correspondante. + * + * Trouve l'index du premier middleware qui correspond à la classe fournie, + * et insère le callback fourni après celui-ci. Si la classe n'est pas trouvée, + * cette méthode se comportera comme add(). + * + * @param string $class Le nom de classe pour insérer le middleware après. + */ + public function insertAfter(string $class, MiddlewareInterface|Closure|string $middleware): static + { + $found = false; + $i = 0; + + if (array_key_exists($class, $this->aliases) && is_string($this->aliases[$class])) { + $class = $this->aliases[$class]; + } + + foreach ($this->queue as $i => $object) { + if ((is_string($object) && $object === $class) || is_a($object, $class)) { + $found = true; + break; + } + } + + if ($found) { + return $this->insertAt($i + 1, $middleware); + } + + return $this->add($middleware); + } + + /** + * Obtenir le nombre de couches middleware connectés. + */ + public function count(): int + { + return count($this->queue); + } + + /** + * {@inheritDoc} + */ + public function seek(int $position): void + { + if (!isset($this->queue[$position])) { + throw new OutOfBoundsException(sprintf('Invalid seek position (%s).', $position)); + } + + $this->position = $position; + } + + /** + * {@inheritDoc} + */ + public function rewind(): void + { + $this->position = 0; + } + + /** + * {@inheritDoc} + */ + public function current(): MiddlewareInterface + { + if (!isset($this->queue[$this->position])) { + throw new OutOfBoundsException(sprintf('Position actuelle non valide (%s).', $this->position)); + } + + if ($this->queue[$this->position] instanceof MiddlewareInterface) { + return $this->queue[$this->position]; + } + + return $this->queue[$this->position] = $this->resolve($this->queue[$this->position]); + } + + /** + * {@inheritDoc} + */ + public function key(): int + { + return $this->position; + } + + /** + * Passe la position actuelle au middleware suivant. + */ + public function next(): void + { + ++$this->position; + } + + /** + * Vérifie si la position actuelle est valide. + */ + public function valid(): bool + { + return isset($this->queue[$this->position]); + } + + /** + * Enregistre les middlewares definis dans le gestionnaire des middlewares + * + * @internal + */ + public function register() + { + $config = (object) config('middlewares'); + + $this->aliases($config->aliases); + + foreach ($config->globals as $middleware) { + $this->add($middleware); + } + + if (is_callable($build = $config->build)) { + $this->container->call($build, [ + 'request' => $this->request, + 'middleware' => $this, + ]); + } + } + + /** + * {@internal} + */ + public function response(): Response + { + return $this->response; + } + + /** + * Résoudre le nom middleware à une instance de middleware compatible PSR 15. + * + * @throws \InvalidArgumentException si Middleware introuvable. + */ + protected function resolve(MiddlewareInterface|Closure|string $middleware): MiddlewareInterface + { + if (is_string($middleware)) { + [$middleware, $options] = explode(':', $middleware) + [1 => null]; + + if (isset($this->aliases[$middleware])) { + $middleware = $this->aliases[$middleware]; + } + + if ($this->container->has($middleware)) { + $middleware = $this->container->get($middleware); + } else { + /** @var class-string<\Psr\Http\Server\MiddlewareInterface>|null $className */ + $className = App::className($middleware, 'Middleware', 'Middleware'); + if ($className === null) { + throw new InvalidArgumentException(sprintf( + 'Middleware, `%s` n\'a pas été trouvé.', + $middleware + )); + } + $middleware = new $className(); + } + + if ($middleware instanceof BaseMiddleware) { + $middleware->fill(explode(',', $options))->init($this->request->getPath()); + } + } + + if ($middleware instanceof MiddlewareInterface) { + return $middleware; + } + + return new ClosureDecorator($middleware, $this->response); + } +} diff --git a/src/Http/MiddlewareRunner.php b/src/Http/MiddlewareRunner.php new file mode 100644 index 00000000..037a1dc0 --- /dev/null +++ b/src/Http/MiddlewareRunner.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Http; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * Exécute la file d'attente middleware et fournit le prochain qui permet de parcourir la file d'attente. + */ +class MiddlewareRunner implements RequestHandlerInterface +{ + /** + * La file d'attente middleware à exécuter. + */ + protected MiddlewareQueue $queue; + + /** + * Gestionnaire de Fallback à utiliser si file d'attente middleware ne génère pas de réponse. + */ + protected ?RequestHandlerInterface $fallback = null; + + /** + * {@internal} + */ + public function run(MiddlewareQueue $queue, ServerRequestInterface $request, ?RequestHandlerInterface $fallback = null): ResponseInterface + { + $this->queue = $queue; + $this->fallback = $fallback; + $this->queue->rewind(); + + return $this->handle($request); + } + + /** + * Execution du middleware + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + if ($this->queue->valid()) { + $middleware = $this->queue->current(); + $this->queue->next(); + + return $middleware->process($request, $this); + } + + if ($this->fallback) { + return $this->fallback->handle($request); + } + + return $this->queue->response(); + } +} diff --git a/src/Middlewares/BaseMiddleware.php b/src/Middlewares/BaseMiddleware.php index 757feac0..775b91d3 100644 --- a/src/Middlewares/BaseMiddleware.php +++ b/src/Middlewares/BaseMiddleware.php @@ -30,12 +30,9 @@ abstract class BaseMiddleware */ protected string $path; - public function init(array $arguments = []): static + public function init(string $path): static { - $this->path = $arguments['path'] ?: '/'; - unset($arguments['path']); - - $this->arguments = array_merge($this->arguments, $arguments); + $this->path = $path; foreach ($this->arguments as $argument => $value) { if (! is_string($argument)) { @@ -70,6 +67,8 @@ final public function fill(array $params): static $this->arguments[$key] = array_shift($params); } + $this->arguments += $params; + return $this; } } diff --git a/src/Middlewares/ClosureDecorator.php b/src/Middlewares/ClosureDecorator.php new file mode 100644 index 00000000..44fbfa39 --- /dev/null +++ b/src/Middlewares/ClosureDecorator.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Middlewares; + +use BlitzPHP\Container\Services; +use BlitzPHP\Http\Response; +use BlitzPHP\Utilities\Iterable\Arr; +use Closure; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use ReflectionFunction; +use ReflectionParameter; + +/** + * Décorez les closure comme middleware PSR-15. + * + * Décorer les closure avec les signatures suivantes: + * + * ``` + * function ( + * ServerRequestInterface $request, + * RequestHandlerInterface $handler + * ): ResponseInterface + * + * function ( + * ServerRequestInterface $request, + * ResponseInterface $response, + * Closure $next + * ): ResponseInterface + * ``` + * + * tel qu'il fonctionnera comme PSR-15 middleware. + */ +class ClosureDecorator implements MiddlewareInterface +{ + /** + * Constructor + */ + public function __construct(protected Closure $callable, protected ?Response $response = null) + { + $this->response = $response ?: Services::response(); + } + + /** + * Exécutez le callable lors d'une request de serveur entrante. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $reflector = new ReflectionFunction($this->callable); + + $parameters = collect($reflector->getParameters())->map(fn(ReflectionParameter $p) => $p->getName())->all(); + + if (Arr::contains($parameters, ['request', 'response', 'next'])) { + return ($this->callable)($request, $this->response, [$handler, 'handle']); + } else if (Arr::contains($parameters, ['request', 'handler'])) { + return ($this->callable)($request, $handler); + } else { + return $handler->handle($request); + } + } + + /** + * @internal + */ + public function getCallable(): Closure + { + return $this->callable; + } +} diff --git a/src/Router/Dispatcher.php b/src/Router/Dispatcher.php index ce769f3e..2fe39d80 100644 --- a/src/Router/Dispatcher.php +++ b/src/Router/Dispatcher.php @@ -23,7 +23,8 @@ use BlitzPHP\Exceptions\PageNotFoundException; use BlitzPHP\Exceptions\RedirectException; use BlitzPHP\Exceptions\ValidationException; -use BlitzPHP\Http\Middleware; +use BlitzPHP\Http\MiddlewareQueue; +use BlitzPHP\Http\MiddlewareRunner; use BlitzPHP\Http\Request; use BlitzPHP\Http\Response; use BlitzPHP\Http\Uri; @@ -93,7 +94,7 @@ class Dispatcher protected $router; /** - * @var Middleware + * @var MiddlewareQueue */ private $middleware; @@ -244,9 +245,6 @@ protected function handleRequest(?RouteCollectionInterface $routes = null, ?arra { $routeMiddlewares = $this->dispatchRoutes($routes); - // Le bootstrap dans un middleware - $this->middleware->alias('blitz', $this->bootApp()); - /** * Ajouter des middlewares de routes */ @@ -254,13 +252,13 @@ protected function handleRequest(?RouteCollectionInterface $routes = null, ?arra $this->middleware->append($middleware); } - $this->middleware->append('blitz'); + $this->middleware->append($this->bootApp()); // Enregistrer notre URI actuel en tant qu'URI précédent dans la session // pour une utilisation plus sûre et plus précise avec la fonction d'assistance `previous_url()`. $this->storePreviousURL(current_url(true)); - return $this->middleware->handle($this->request); + return (new MiddlewareRunner())->run($this->middleware, $this->request); } /** @@ -651,10 +649,10 @@ protected function formatResponse(ResponseInterface $response, $returned): Respo */ protected function initMiddlewareQueue(): void { - $this->middleware = new Middleware($this->response, $this->request->getPath()); + $this->middleware = new MiddlewareQueue($this->container, [], $this->request, $this->response); $this->middleware->append($this->spoofRequestMethod()); - $this->middleware->register($this->request); + $this->middleware->register(); } protected function outputBufferingStart(): void From d6d05ba1c0882786280d3e85d6d5f19e9dfcb594 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 10 Dec 2023 14:55:32 +0100 Subject: [PATCH 090/111] =?UTF-8?q?fix:=20Correction=20de=20la=20signature?= =?UTF-8?q?=20de=20la=20fonction=20d'aide=20=C3=A0=20l'environnement=20et?= =?UTF-8?q?=20de=20la=20logique=20de=20r=C3=A9cup=C3=A9ration=20de=20l'env?= =?UTF-8?q?ironnement=20actuel.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La fonction d'aide à l'environnement initialise désormais correctement le paramètre $env comme étant nul par défaut, ce qui permet une utilisation optionnelle. La logique de la fonction garantit que la valeur de l'environnement actuel est correctement récupérée, en tenant compte des cas où elle est vide ou définie sur 'auto'. Cette correction assure un comportement précis de récupération de l'environnement. --- src/Helpers/common.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Helpers/common.php b/src/Helpers/common.php index 1f58c79c..7c890784 100644 --- a/src/Helpers/common.php +++ b/src/Helpers/common.php @@ -425,7 +425,7 @@ function stringify_attributes($attributes, bool $js = false): string * * @return bool|string */ - function environment(null|array|string $env) + function environment(null|array|string $env = null) { $current = env('ENVIRONMENT'); if (empty($current) || $current === 'auto') { From ed2e254cb9740e0068ace4598224467914d9c4db Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 10 Dec 2023 14:58:46 +0100 Subject: [PATCH 091/111] cs-fix --- src/Config/Providers.php | 12 +++++---- src/Http/MiddlewareQueue.php | 38 ++++++++++++++-------------- src/Http/MiddlewareRunner.php | 16 ++++++------ src/Middlewares/ClosureDecorator.php | 11 ++++---- src/Router/Dispatcher.php | 2 +- 5 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/Config/Providers.php b/src/Config/Providers.php index 20b211b3..6e1876a6 100644 --- a/src/Config/Providers.php +++ b/src/Config/Providers.php @@ -51,18 +51,20 @@ private static function classes(): array { return [ \BlitzPHP\Autoloader\Autoloader::class => static fn () => service('autoloader'), - \BlitzPHP\Cache\Cache::class => static fn () => service('cache'), - \BlitzPHP\Translator\Translate::class => static fn () => service('translator'), \BlitzPHP\Autoloader\Locator::class => static fn () => service('locator'), - \BlitzPHP\Mail\Mail::class => static fn () => service('mail'), + \BlitzPHP\Cache\Cache::class => static fn () => service('cache'), + \BlitzPHP\Cache\ResponseCache::class => static fn () => service('responsecache'), + \BlitzPHP\Filesystem\FilesystemManager::class => static fn () => service('storage'), \BlitzPHP\Http\Negotiator::class => static fn () => service('negotiator'), \BlitzPHP\Http\Redirection::class => static fn () => service('redirection'), - \BlitzPHP\Cache\ResponseCache::class => static fn () => service('responsecache'), + \BlitzPHP\Http\Request::class => static fn () => service('request'), + \BlitzPHP\Http\Response::class => static fn () => service('response'), + \BlitzPHP\Mail\Mail::class => static fn () => service('mail'), \BlitzPHP\Router\RouteCollection::class => static fn () => service('routes'), \BlitzPHP\Router\Router::class => static fn () => service('router'), \BlitzPHP\Session\Cookie\CookieManager::class => static fn () => service('cookie'), \BlitzPHP\Session\Store::class => static fn () => service('session'), - \BlitzPHP\Filesystem\FilesystemManager::class => static fn () => service('storage'), + \BlitzPHP\Translator\Translate::class => static fn () => service('translator'), ]; } } diff --git a/src/Http/MiddlewareQueue.php b/src/Http/MiddlewareQueue.php index ab9c7a53..2a7b6709 100644 --- a/src/Http/MiddlewareQueue.php +++ b/src/Http/MiddlewareQueue.php @@ -28,7 +28,7 @@ class MiddlewareQueue implements Countable, SeekableIterator /** * Middlewares a executer pour la requete courante * - * @var array + * @var array */ protected array $queue = []; @@ -42,7 +42,7 @@ class MiddlewareQueue implements Countable, SeekableIterator */ protected array $aliases = []; - /** + /** * Constructor * * @param array $middleware Liste des middlewares initiaux @@ -77,7 +77,7 @@ public function aliases(array $aliases): self * * @param array|callable|object|string $middlewares */ - public function add(MiddlewareInterface|Closure|array|string $middleware): static + public function add(array|Closure|MiddlewareInterface|string $middleware): static { if (is_array($middleware)) { $this->queue = array_merge($this->queue, $middleware); @@ -94,7 +94,7 @@ public function add(MiddlewareInterface|Closure|array|string $middleware): stati * * @see MiddlewareQueue::add() */ - public function push(MiddlewareInterface|Closure|array|string $middleware): static + public function push(array|Closure|MiddlewareInterface|string $middleware): static { return $this->add($middleware); } @@ -106,7 +106,7 @@ public function push(MiddlewareInterface|Closure|array|string $middleware): stat * * @see MiddlewareQueue::add() */ - public function append(MiddlewareInterface|Closure|array|string $middleware): static + public function append(array|Closure|MiddlewareInterface|string $middleware): static { return $this->add($middleware); } @@ -114,7 +114,7 @@ public function append(MiddlewareInterface|Closure|array|string $middleware): st /** * Ajoute un middleware en debut de chaine */ - public function prepend(MiddlewareInterface|Closure|array|string $middleware): static + public function prepend(array|Closure|MiddlewareInterface|string $middleware): static { if (is_array($middleware)) { $this->queue = array_merge($middleware, $this->queue); @@ -135,7 +135,7 @@ public function prepend(MiddlewareInterface|Closure|array|string $middleware): s * * @see MiddlewareQueue::add() */ - public function insert(int $index, MiddlewareInterface|Closure|string $middleware): static + public function insert(int $index, Closure|MiddlewareInterface|string $middleware): static { return $this->insertAt($index, $middleware); } @@ -148,7 +148,7 @@ public function insert(int $index, MiddlewareInterface|Closure|string $middlewar * * @param int $index La position où le middleware doit être insérer. */ - public function insertAt(int $index, MiddlewareInterface|Closure|string $middleware): static + public function insertAt(int $index, Closure|MiddlewareInterface|string $middleware): static { array_splice($this->queue, $index, 0, [$middleware]); @@ -165,7 +165,7 @@ public function insertAt(int $index, MiddlewareInterface|Closure|string $middlew * * @throws LogicException Si le middleware à insérer avant n'est pas trouvé. */ - public function insertBefore(string $class, MiddlewareInterface|Closure|string $middleware): static + public function insertBefore(string $class, Closure|MiddlewareInterface|string $middleware): static { $found = false; $i = 0; @@ -197,7 +197,7 @@ public function insertBefore(string $class, MiddlewareInterface|Closure|string $ * * @param string $class Le nom de classe pour insérer le middleware après. */ - public function insertAfter(string $class, MiddlewareInterface|Closure|string $middleware): static + public function insertAfter(string $class, Closure|MiddlewareInterface|string $middleware): static { $found = false; $i = 0; @@ -233,7 +233,7 @@ public function count(): int */ public function seek(int $position): void { - if (!isset($this->queue[$position])) { + if (! isset($this->queue[$position])) { throw new OutOfBoundsException(sprintf('Invalid seek position (%s).', $position)); } @@ -253,7 +253,7 @@ public function rewind(): void */ public function current(): MiddlewareInterface { - if (!isset($this->queue[$this->position])) { + if (! isset($this->queue[$this->position])) { throw new OutOfBoundsException(sprintf('Position actuelle non valide (%s).', $this->position)); } @@ -277,7 +277,7 @@ public function key(): int */ public function next(): void { - ++$this->position; + $this->position++; } /** @@ -322,12 +322,12 @@ public function response(): Response /** * Résoudre le nom middleware à une instance de middleware compatible PSR 15. * - * @throws \InvalidArgumentException si Middleware introuvable. + * @throws InvalidArgumentException si Middleware introuvable. */ - protected function resolve(MiddlewareInterface|Closure|string $middleware): MiddlewareInterface + protected function resolve(Closure|MiddlewareInterface|string $middleware): MiddlewareInterface { if (is_string($middleware)) { - [$middleware, $options] = explode(':', $middleware) + [1 => null]; + [$middleware, $options] = explode(':', $middleware) + [1 => null]; if (isset($this->aliases[$middleware])) { $middleware = $this->aliases[$middleware]; @@ -347,9 +347,9 @@ protected function resolve(MiddlewareInterface|Closure|string $middleware): Midd $middleware = new $className(); } - if ($middleware instanceof BaseMiddleware) { - $middleware->fill(explode(',', $options))->init($this->request->getPath()); - } + if ($middleware instanceof BaseMiddleware) { + $middleware->fill(explode(',', $options))->init($this->request->getPath()); + } } if ($middleware instanceof MiddlewareInterface) { diff --git a/src/Http/MiddlewareRunner.php b/src/Http/MiddlewareRunner.php index 037a1dc0..2015e7f2 100644 --- a/src/Http/MiddlewareRunner.php +++ b/src/Http/MiddlewareRunner.php @@ -34,7 +34,7 @@ class MiddlewareRunner implements RequestHandlerInterface * {@internal} */ public function run(MiddlewareQueue $queue, ServerRequestInterface $request, ?RequestHandlerInterface $fallback = null): ResponseInterface - { + { $this->queue = $queue; $this->fallback = $fallback; $this->queue->rewind(); @@ -47,17 +47,17 @@ public function run(MiddlewareQueue $queue, ServerRequestInterface $request, ?Re */ public function handle(ServerRequestInterface $request): ResponseInterface { - if ($this->queue->valid()) { - $middleware = $this->queue->current(); - $this->queue->next(); + if ($this->queue->valid()) { + $middleware = $this->queue->current(); + $this->queue->next(); - return $middleware->process($request, $this); - } + return $middleware->process($request, $this); + } - if ($this->fallback) { + if ($this->fallback) { return $this->fallback->handle($request); } - return $this->queue->response(); + return $this->queue->response(); } } diff --git a/src/Middlewares/ClosureDecorator.php b/src/Middlewares/ClosureDecorator.php index 44fbfa39..4b858a84 100644 --- a/src/Middlewares/ClosureDecorator.php +++ b/src/Middlewares/ClosureDecorator.php @@ -49,7 +49,7 @@ class ClosureDecorator implements MiddlewareInterface */ public function __construct(protected Closure $callable, protected ?Response $response = null) { - $this->response = $response ?: Services::response(); + $this->response = $response ?: Services::response(); } /** @@ -59,15 +59,16 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { $reflector = new ReflectionFunction($this->callable); - $parameters = collect($reflector->getParameters())->map(fn(ReflectionParameter $p) => $p->getName())->all(); + $parameters = collect($reflector->getParameters())->map(static fn (ReflectionParameter $p) => $p->getName())->all(); if (Arr::contains($parameters, ['request', 'response', 'next'])) { return ($this->callable)($request, $this->response, [$handler, 'handle']); - } else if (Arr::contains($parameters, ['request', 'handler'])) { + } + if (Arr::contains($parameters, ['request', 'handler'])) { return ($this->callable)($request, $handler); - } else { - return $handler->handle($request); } + + return $handler->handle($request); } /** diff --git a/src/Router/Dispatcher.php b/src/Router/Dispatcher.php index 2fe39d80..c0ea422b 100644 --- a/src/Router/Dispatcher.php +++ b/src/Router/Dispatcher.php @@ -258,7 +258,7 @@ protected function handleRequest(?RouteCollectionInterface $routes = null, ?arra // pour une utilisation plus sûre et plus précise avec la fonction d'assistance `previous_url()`. $this->storePreviousURL(current_url(true)); - return (new MiddlewareRunner())->run($this->middleware, $this->request); + return (new MiddlewareRunner())->run($this->middleware, $this->request); } /** From 9583c26cd8ec546ffd62bf541cb4e33006f28d47 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 10 Dec 2023 18:16:26 +0100 Subject: [PATCH 092/111] test(Middleware): ajout des test des middlewares --- .../app/Middlewares/DumbMiddleware.php | 28 ++ .../app/Middlewares/SampleMiddleware.php | 28 ++ .../framework/Http/MiddlewareQueue.spec.php | 410 ++++++++++++++++++ .../framework/Http/MiddlewareRunner.spec.php | 70 +++ src/Http/Middleware.php | 359 --------------- src/Http/MiddlewareQueue.php | 21 +- 6 files changed, 545 insertions(+), 371 deletions(-) create mode 100644 spec/support/application/app/Middlewares/DumbMiddleware.php create mode 100644 spec/support/application/app/Middlewares/SampleMiddleware.php create mode 100644 spec/system/framework/Http/MiddlewareQueue.spec.php create mode 100644 spec/system/framework/Http/MiddlewareRunner.spec.php delete mode 100644 src/Http/Middleware.php diff --git a/spec/support/application/app/Middlewares/DumbMiddleware.php b/spec/support/application/app/Middlewares/DumbMiddleware.php new file mode 100644 index 00000000..b970992f --- /dev/null +++ b/spec/support/application/app/Middlewares/DumbMiddleware.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Spec\BlitzPHP\App\Middlewares; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * Testing stub for middleware tests. + */ +class DumbMiddleware implements MiddlewareInterface +{ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + return $handler->handle($request); + } +} diff --git a/spec/support/application/app/Middlewares/SampleMiddleware.php b/spec/support/application/app/Middlewares/SampleMiddleware.php new file mode 100644 index 00000000..7ad46dd9 --- /dev/null +++ b/spec/support/application/app/Middlewares/SampleMiddleware.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Spec\BlitzPHP\App\Middlewares; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * Testing stub for middleware tests. + */ +class SampleMiddleware implements MiddlewareInterface +{ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + return $handler->handle($request); + } +} diff --git a/spec/system/framework/Http/MiddlewareQueue.spec.php b/spec/system/framework/Http/MiddlewareQueue.spec.php new file mode 100644 index 00000000..52d0bcf0 --- /dev/null +++ b/spec/system/framework/Http/MiddlewareQueue.spec.php @@ -0,0 +1,410 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Container\Services; +use BlitzPHP\Http\MiddlewareQueue; +use BlitzPHP\Spec\ReflectionHelper; +use Spec\BlitzPHP\App\Middlewares\DumbMiddleware; +use Spec\BlitzPHP\App\Middlewares\SampleMiddleware; + +describe('Http / MiddlewareQueue', function () { + beforeAll(function () { + $this->request = Services::request(); + $this->response = Services::response(); + $this->container = Services::container(); + $this->middleware = fn (array $middlewares = []) => new MiddlewareQueue($this->container, $middlewares, $this->request, $this->response); + }); + + describe('Constructeur', function () { + it('Ajout de middleware via le constructeur', function () { + $cb = function (): void { + }; + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware([$cb]); + + expect($queue)->toHaveLength(1); + expect($queue->current()->getCallable())->toBe($cb); + }); + }); + + describe('Recuperation du middleware courant', function () { + it('Est-ce que la recuperation du middleware courant fonctionne', function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $cb = function (): void { + }; + $queue->add($cb); + + expect($queue->current()->getCallable())->toBe($cb); + }); + + it('current() leve une exception pour une position actuelle invalide', function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + expect(fn() => $queue->current())->toThrow(new OutOfBoundsException('Position actuelle non valide (0).')); + }); + }); + + describe('Ajout de middleware a la pile', function () { + it('add() renvoie l\'instance', function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $cb = function (): void { + }; + + expect($queue->add($cb))->toBe($queue); + }); + + it('Les middlewares sont ajoutés dans le bon ordre', function () { + $one = function (): void { + }; + $two = function (): void { + }; + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + expect($queue)->toHaveLength(0); + + $queue->add($one); + expect($queue)->toHaveLength(1); + + $queue->add($two); + expect($queue)->toHaveLength(2); + + expect($queue->current()->getCallable())->toBe($one); + $queue->next(); + expect($queue->current()->getCallable())->toBe($two); + }); + + it('prepend() renvoie l\'instance', function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $cb = function (): void { + }; + + expect($queue->prepend($cb))->toBe($queue); + }); + + it('Les middlewares sont ajoutés en debut de chaine dans le bon ordre', function () { + $one = function (): void { + }; + $two = function (): void { + }; + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + expect($queue)->toHaveLength(0); + + $queue->append($one); + expect($queue)->toHaveLength(1); + + $queue->prepend($two); + expect($queue)->toHaveLength(2); + + expect($queue->current()->getCallable())->toBe($two); + $queue->next(); + expect($queue->current()->getCallable())->toBe($one); + }); + + it('Ajout de middlewares sous forme de chaine de caractere', function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->push('Sample'); + $queue->prepend(SampleMiddleware::class); + + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + }); + + it('Ajout de middlewares via un tableau', function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $one = function (): void { + }; + + $queue->add([$one]); + $queue->prepend(['Spec\BlitzPHP\App\Middlewares\SampleMiddleware']); + + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + $queue->next(); + expect($queue->current()->getCallable())->toBe($one); + }); + }); + + describe('Insertion', function () { + it('Insertion a une position quelconque', function () { + $one = function (): void { + }; + $two = function (): void { + }; + $three = function (): void { + }; + $four = new SampleMiddleware(); + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->add($one)->add($two)->insertAt(0, $three)->insertAt(2, $four); + expect($queue->current()->getCallable())->toBe($three); + $queue->next(); + expect($queue->current()->getCallable())->toBe($one); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + $queue->next(); + expect($queue->current()->getCallable())->toBe($two); + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->add($one)->add($two)->insertAt(1, $three); + expect($queue->current()->getCallable())->toBe($one); + $queue->next(); + expect($queue->current()->getCallable())->toBe($three); + $queue->next(); + expect($queue->current()->getCallable())->toBe($two); + }); + + it('Insertion a une position hors limite', function () { + $one = function (): void { + }; + $two = function (): void { + }; + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->add($one)->insertAt(98, $two); + + expect($queue)->toHaveLength(2); + expect($queue->current()->getCallable())->toBe($one); + $queue->next(); + expect($queue->current()->getCallable())->toBe($two); + }); + + it('Insertion a une position negative', function () { + $one = function (): void { + }; + $two = function (): void { + }; + $three = new SampleMiddleware(); + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->add($one)->insertAt(-1, $two)->insertAt(-1, $three); + + expect($queue)->toHaveLength(3); + expect($queue->current()->getCallable())->toBe($two); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + $queue->next(); + expect($queue->current()->getCallable())->toBe($one); + }); + + it('Insertion avant une classe', function () { + $one = function (): void { + }; + $two = new SampleMiddleware(); + $three = function (): void { + }; + $four = new DumbMiddleware(); + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->add($one)->add($two)->insertBefore(SampleMiddleware::class, $three)->insertBefore(SampleMiddleware::class, $four); + + expect($queue)->toHaveLength(4); + expect($queue->current()->getCallable())->toBe($one); + $queue->next(); + expect($queue->current()->getCallable())->toBe($three); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(DumbMiddleware::class); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + $two = SampleMiddleware::class; + + $queue->add($one)->add($two)->insertBefore(SampleMiddleware::class, $three); + + expect($queue)->toHaveLength(3); + expect($queue->current()->getCallable())->toBe($one); + $queue->next(); + expect($queue->current()->getCallable())->toBe($three); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + }); + + it('Insertion avant une classe invalide leve une exception', function () { + $one = function (): void { + }; + $two = new SampleMiddleware(); + $three = function (): void { + }; + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + expect(fn() => $queue->add($one)->add($two)->insertBefore('InvalidClassName', $three)) + ->toThrow(new LogicException("No middleware matching 'InvalidClassName' could be found.")); + }); + + it('Insertion avant une classe', function () { + $one = new SampleMiddleware(); + $two = function (): void { + }; + $three = function (): void { + }; + $four = new DumbMiddleware(); + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->add($one)->add($two)->insertAfter(SampleMiddleware::class, $three)->insertAfter(SampleMiddleware::class, $four); + + expect($queue)->toHaveLength(4); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(DumbMiddleware::class); + $queue->next(); + expect($queue->current()->getCallable())->toBe($three); + $queue->next(); + expect($queue->current()->getCallable())->toBe($two); + + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + $one = 'Spec\BlitzPHP\App\Middlewares\SampleMiddleware'; + + $queue->add($one)->add($two)->insertAfter('Spec\BlitzPHP\App\Middlewares\SampleMiddleware', $three); + + expect($queue)->toHaveLength(3); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + $queue->next(); + expect($queue->current()->getCallable())->toEqual($three); + $queue->next(); + expect($queue->current()->getCallable())->toEqual($two); + }); + + it('Insertion apres une classe invalide ne leve pas une exception', function () { + $one = new SampleMiddleware(); + $two = function (): void { + }; + $three = function (): void { + }; + + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->add($one)->add($two)->insertAfter('InvalidClassName', $three); + + expect($queue)->toHaveLength(3); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + $queue->next(); + expect($queue->current()->getCallable())->toBe($two); + $queue->next(); + expect($queue->current()->getCallable())->toBe($three); + }); + }); + + describe('Container', function () { + it("S'assurer que le middleware fourni par le conteneur est le meme objet", function () { + $middleware = new SampleMiddleware(); + $this->container->set(SampleMiddleware::class, $middleware); + $queue = new MiddlewareQueue($this->container, [], $this->request, $this->response); + $queue->add(SampleMiddleware::class); + + expect($queue->current())->toBe($middleware); + }); + + it("S'assurer qu'une exception est levee pour les middlewares inconnu", function () { + $queue = new MiddlewareQueue($this->container, [], $this->request, $this->response); + $queue->add('UnresolvableMiddleware'); + + expect(fn() => $queue->current()) + ->toThrow(new InvalidArgumentException("Middleware, `UnresolvableMiddleware` n'a pas été trouvé.")); + }); + }); + + describe('Alias & register', function () { + it("Definition des alias", function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->alias('sample', SampleMiddleware::class); + expect(ReflectionHelper::getPrivateProperty($queue, 'aliases'))->toBe([ + 'sample' => SampleMiddleware::class + ]); + + $queue->aliases([ + 'dummy' => DumbMiddleware::class, + 'sample' => SampleMiddleware::class, + ]); + + expect(ReflectionHelper::getPrivateProperty($queue, 'aliases'))->toBe([ + 'sample' => SampleMiddleware::class, + 'dummy' => DumbMiddleware::class + ]); + }); + + it("Utilisation des alias dans l'insertion", function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->aliases([ + 'dummy' => DumbMiddleware::class, + 'sample' => SampleMiddleware::class, + ]); + + $one = function (): void { + }; + $two = new SampleMiddleware(); + $three = function (): void { + }; + $four = new DumbMiddleware(); + + $queue->add($two)->insertBefore('sample', $three)->push($four)->insertAfter('dummy', $one); + + expect($queue)->toHaveLength(4); + expect($queue->current()->getCallable())->toBe($three); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(DumbMiddleware::class); + $queue->next(); + expect($queue->current()->getCallable())->toBe($one); + }); + + it("Utilisation des alias dans la recuperation du middleware", function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $queue->aliases(['dummy' => DumbMiddleware::class]); + + $queue->add('dummy'); + + expect($queue)->toHaveLength(1); + expect($queue->current())->toBeAnInstanceOf(DumbMiddleware::class); + }); + }); +}); diff --git a/spec/system/framework/Http/MiddlewareRunner.spec.php b/spec/system/framework/Http/MiddlewareRunner.spec.php new file mode 100644 index 00000000..f8bcdb53 --- /dev/null +++ b/spec/system/framework/Http/MiddlewareRunner.spec.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Container\Services; +use BlitzPHP\Http\MiddlewareQueue; +use BlitzPHP\Http\MiddlewareRunner; +use BlitzPHP\Http\Response; +use Psr\Http\Message\ResponseInterface; + +describe('Http / MiddlewareRunner', function () { + beforeAll(function () { + $this->request = Services::request(); + $this->response = Services::response(); + $this->container = Services::container(); + $this->middleware = fn (array $middlewares = []) => new MiddlewareQueue($this->container, $middlewares, $this->request, $this->response); + + $this->ok = fn ($request, $handler) => $handler->handle($request); + $this->pass = fn ($request, $response, $next) => $next($request, $response, $next); + $this->fail = function ($request, $handler): void { + throw new RuntimeException('A bad thing'); + }; + }); + + beforeEach(function() { + $this->queue = $this->middleware(); + }); + + it("Execution d'un seul middleware", function () { + $this->queue->add($this->ok); + + $runner = new MiddlewareRunner(); + $result = $runner->run($this->queue, $this->request); + + expect($result)->toBeAnInstanceOf(ResponseInterface::class); + }); + + it("Execution de middlewares en sequence", function () { + $log = []; + $one = function ($request, $handler) use (&$log) { + $log[] = 'one'; + + return $handler->handle($request); + }; + $two = function ($request, $handler) use (&$log) { + $log[] = 'two'; + + return $handler->handle($request); + }; + $three = function ($request, $handler) use (&$log) { + $log[] = 'three'; + + return $handler->handle($request); + }; + $this->queue->add($one)->add($two)->add($three); + + $runner = new MiddlewareRunner(); + $result = $runner->run($this->queue, $this->request); + expect($result)->toBeAnInstanceOf(Response::class); + + expect($log)->toBe(['one', 'two', 'three']); + }); +}); diff --git a/src/Http/Middleware.php b/src/Http/Middleware.php deleted file mode 100644 index d0f51b79..00000000 --- a/src/Http/Middleware.php +++ /dev/null @@ -1,359 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Http; - -use BlitzPHP\Container\Services; -use BlitzPHP\Middlewares\BaseMiddleware; -use LogicException; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; - -class Middleware implements RequestHandlerInterface -{ - /** - * Middlewares a executer pour la requete courante - * - * @var array[] - */ - protected array $middlewares = []; - - /** - * Index du middleware actuellement executer - */ - protected int $index = 0; - - /** - * Aliases des middlewares - */ - protected array $aliases = []; - - /** - * Contructor - */ - public function __construct(protected Response $response, protected string $path) - { - } - - /** - * Ajoute un alias de middleware - */ - public function alias(string $alias, callable|object|string $middleware): self - { - return $this->aliases([$alias => $middleware]); - } - - /** - * Ajoute des alias de middlewares - */ - public function aliases(array $aliases): self - { - $this->aliases = array_merge($this->aliases, $aliases); - - return $this; - } - - /** - * Ajoute un middleware a la chaine d'execution - * - * @param array|callable|object|string $middlewares - */ - public function add($middlewares, array $options = []): self - { - if (! is_array($middlewares)) { - $middlewares = [$middlewares]; - } - - foreach ($middlewares as $middleware) { - $this->append($middleware, $options); - } - - return $this; - } - - /** - * Ajoute un middleware en bout de chaine - * - * @param callable|object|string $middleware - */ - public function append($middleware, array $options = []): self - { - [$middleware, $options] = $this->getMiddlewareAndOptions($middleware, $options); - - $middleware = $this->makeMiddleware($middleware); - $this->middlewares[] = compact('middleware', 'options'); - - return $this; - } - - /** - * Ajoute un middleware en debut de chaine - * - * @param callable|object|string $middleware - */ - public function prepend($middleware, array $options = []): self - { - [$middleware, $options] = $this->getMiddlewareAndOptions($middleware, $options); - - $middleware = $this->makeMiddleware($middleware); - array_unshift($this->middlewares, compact('middleware', 'options')); - - return $this; - } - - /** - * insert un middleware a une position donnee - * - * @param callable|object|string $middleware - * - * @alias insertAt - */ - public function insert(int $index, $middleware, array $options = []): self - { - return $this->insertAt($index, $middleware, $options); - } - - /** - * Insérez un middleware appelable à un index spécifique. - * - * Si l'index existe déjà, le nouvel appelable sera inséré, - * et l'élément existant sera décalé d'un indice supérieur. - * - * @param int $index La position où le middleware doit être insérer. - * @param callable|object|string $middleware Le middleware à inserer. - */ - public function insertAt(int $index, $middleware, array $options = []): self - { - [$middleware, $options] = $this->getMiddlewareAndOptions($middleware, $options); - - $middleware = [ - 'middleware' => $this->makeMiddleware($middleware), - 'options' => $options, - ]; - array_splice($this->middlewares, $index, 0, [$middleware]); - - return $this; - } - - /** - * Insérez un objet middleware avant la première classe correspondante. - * - * Trouve l'index du premier middleware qui correspond à la classe fournie, - * et insère l'appelable fourni avant. - * - * @param string $class Le nom de classe pour insérer le middleware avant. - * @param callable|object|string $middleware Le middleware à inserer. - * - * @throws LogicException Si le middleware à insérer avant n'est pas trouvé. - */ - public function insertBefore(string $class, $middleware, array $options = []): self - { - $found = false; - $i = 0; - - if (array_key_exists($class, $this->aliases) && is_string($this->aliases[$class])) { - $class = $this->aliases[$class]; - } - - foreach ($this->middlewares as $i => $object) { - $object = $object['middleware']; - - if ((is_string($object) && $object === $class) || is_a($object, $class)) { - $found = true; - break; - } - } - - if ($found) { - return $this->insertAt($i, $middleware, $options); - } - - throw new LogicException(sprintf("No middleware matching '%s' could be found.", $class)); - } - - /** - * Insérez un objet middleware après la première classe correspondante. - * - * Trouve l'index du premier middleware qui correspond à la classe fournie, - * et insère le callback fourni après celui-ci. Si la classe n'est pas trouvée, - * cette méthode se comportera comme add(). - * - * @param string $class Le nom de classe pour insérer le middleware après. - * @param callable|object|string $middleware Le middleware à inserer. - */ - public function insertAfter(string $class, $middleware, array $options = []): self - { - $found = false; - $i = 0; - - if (array_key_exists($class, $this->aliases) && is_string($this->aliases[$class])) { - $class = $this->aliases[$class]; - } - - foreach ($this->middlewares as $i => $object) { - $object = $object['middleware']; - - if ((is_string($object) && $object === $class) || is_a($object, $class)) { - $found = true; - break; - } - } - - if ($found) { - return $this->insertAt($i + 1, $middleware, $options); - } - - return $this->add($middleware, $options); - } - - /** - * Execution du middleware - */ - public function handle(ServerRequestInterface $request): ResponseInterface - { - if (empty($processing = $this->getMiddleware())) { - return $this->response; - } - - ['middleware' => $middleware, 'options' => $options] = $processing; - - if (empty($middleware)) { - return $this->response; - } - - if (isset($options['except']) && $this->pathApplies($this->path, $options['except'])) { - return $this->handle($request); - } - - unset($options['except']); - - if (is_callable($middleware)) { - return $middleware($request, $this->response, [$this, 'handle']); - } - - if ($middleware instanceof MiddlewareInterface) { - if ($middleware instanceof BaseMiddleware) { - $middleware = $middleware->fill($options)->init($options + ['path' => $this->path]); - } - - return $middleware->process($request, $this); - } - - return $this->response; - } - - /** - * Enregistre les middlewares definis dans le gestionnaire des middlewares - * - * @internal - */ - public function register(Request $request) - { - $config = (object) config('middlewares'); - - $this->aliases($config->aliases); - - foreach ($config->globals as $middleware) { - $this->add($middleware); - } - - if (is_callable($build = $config->build)) { - Services::container()->call($build, [ - 'request' => $request, - 'middleware' => $this, - ]); - } - } - - /** - * Fabrique un middleware - * - * @param callable|object|string $middleware - * - * @return callable|object - */ - private function makeMiddleware($middleware) - { - if (is_string($middleware) && array_key_exists($middleware, $this->aliases)) { - $middleware = $this->aliases[$middleware]; - } - - return is_string($middleware) - ? Services::container()->get($middleware) - : $middleware; - } - - /** - * Recuperation du middleware actuel - */ - private function getMiddleware(): array - { - $middleware = []; - - if (isset($this->middlewares[$this->index])) { - $middleware = $this->middlewares[$this->index]; - } - - $this->index++; - - return $middleware; - } - - /** - * Recupere les options d'un middlewares de type string - * - * @param callable|object|string $middleware - */ - private function getMiddlewareAndOptions($middleware, array $options = []): array - { - if (is_string($middleware)) { - $parts = explode(':', $middleware); - $middleware = array_shift($parts); - if (isset($parts[0]) && is_string($parts[0])) { - $options = array_merge($options, explode(',', $parts[0])); - } - } - - return [$middleware, $options]; - } - - /** - * Check paths for match for URI - */ - private function pathApplies(string $uri, array|string $paths): bool - { - // empty path matches all - if (empty($paths)) { - return true; - } - - // make sure the paths are iterable - if (is_string($paths)) { - $paths = [$paths]; - } - - // treat each paths as pseudo-regex - foreach ($paths as $path) { - // need to escape path separators - $path = str_replace('/', '\/', trim($path, '/ ')); - // need to make pseudo wildcard real - $path = strtolower(str_replace('*', '.*', $path)); - // Does this rule apply here? - if (preg_match('#^' . $path . '$#', $uri, $match) === 1) { - return true; - } - } - - return false; - } -} diff --git a/src/Http/MiddlewareQueue.php b/src/Http/MiddlewareQueue.php index 2a7b6709..dcf1a6ad 100644 --- a/src/Http/MiddlewareQueue.php +++ b/src/Http/MiddlewareQueue.php @@ -57,15 +57,17 @@ public function __construct(protected Container $container, array $middleware = /** * Ajoute un alias de middleware */ - public function alias(string $alias, callable|object|string $middleware): self + public function alias(string $alias, Closure|MiddlewareInterface|string $middleware): static { return $this->aliases([$alias => $middleware]); } /** * Ajoute des alias de middlewares + * + * @param array $aliases */ - public function aliases(array $aliases): self + public function aliases(array $aliases): static { $this->aliases = array_merge($this->aliases, $aliases); @@ -336,16 +338,11 @@ protected function resolve(Closure|MiddlewareInterface|string $middleware): Midd if ($this->container->has($middleware)) { $middleware = $this->container->get($middleware); } else { - /** @var class-string<\Psr\Http\Server\MiddlewareInterface>|null $className */ - $className = App::className($middleware, 'Middleware', 'Middleware'); - if ($className === null) { - throw new InvalidArgumentException(sprintf( - 'Middleware, `%s` n\'a pas été trouvé.', - $middleware - )); - } - $middleware = new $className(); - } + throw new InvalidArgumentException(sprintf( + 'Middleware, `%s` n\'a pas été trouvé.', + $middleware + )); + } if ($middleware instanceof BaseMiddleware) { $middleware->fill(explode(',', $options))->init($this->request->getPath()); From 2626b6e3c6773567d1d91ecd1a5c097234475b22 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 10 Dec 2023 18:48:35 +0100 Subject: [PATCH 093/111] =?UTF-8?q?Refonte=20du=20processus=20d'enregistre?= =?UTF-8?q?ment=20du=20MiddlewareQueue=20afin=20d'accepter=20des=20configu?= =?UTF-8?q?rations=20personnalis=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Améliorer le processus d'enregistrement du MiddlewareQueue afin d'accepter des configurations personnalisées, permettant plus de flexibilité et de contrôle sur la configuration du middleware. Ce changement modifie la méthode `register` dans `MiddlewareQueue` pour accepter un paramètre de configuration de type tableau, et met à jour son utilisation dans la classe `Dispatcher`. Maintenant, les alias personnalisés, les middlewares globaux et une fonction de construction peuvent être fournis via la configuration. Ce changement améliore l'extensibilité et la configurabilité du système de middleware. --- .../application/app/Config/middlewares.php | 2 +- .../framework/Http/MiddlewareQueue.spec.php | 33 +++++++++++++++++++ src/Http/MiddlewareQueue.php | 33 ++++++++++--------- src/Router/Dispatcher.php | 2 +- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/spec/support/application/app/Config/middlewares.php b/spec/support/application/app/Config/middlewares.php index f6cbee98..ca9c8629 100644 --- a/spec/support/application/app/Config/middlewares.php +++ b/spec/support/application/app/Config/middlewares.php @@ -12,5 +12,5 @@ return [ 'aliases' => [], 'globals' => [], - 'build' => static fn (\BlitzPHP\Http\Middleware $middleware) => null, + 'build' => static fn (\BlitzPHP\Http\MiddlewareQueue $queue) => null, ]; diff --git a/spec/system/framework/Http/MiddlewareQueue.spec.php b/spec/system/framework/Http/MiddlewareQueue.spec.php index 52d0bcf0..adf98d3f 100644 --- a/spec/system/framework/Http/MiddlewareQueue.spec.php +++ b/spec/system/framework/Http/MiddlewareQueue.spec.php @@ -406,5 +406,38 @@ expect($queue)->toHaveLength(1); expect($queue->current())->toBeAnInstanceOf(DumbMiddleware::class); }); + + it('register', function () { + /** @var MiddlewareQueue $queue */ + $queue = $this->middleware(); + + $aliases = [ + 'sample' => SampleMiddleware::class, + 'dummy' => DumbMiddleware::class + ]; + + $cb = function (): void { + }; + + $config = [ + 'aliases' => $aliases, + 'globals' => array_keys($aliases), + 'build' => static function (\BlitzPHP\Http\MiddlewareQueue $queue) use ($cb) { + $queue->insertAt(0, $cb); + }, + ]; + + $queue->register($config); + + expect($queue)->toHaveLength(3); + expect(ReflectionHelper::getPrivateProperty($queue, 'aliases'))->toBe($config['aliases']); + expect(ReflectionHelper::getPrivateProperty($queue, 'queue'))->toBe([$cb, ...$config['globals']]); + $queue->seek(2); + expect($queue->current())->toBeAnInstanceOf(DumbMiddleware::class); + $queue->rewind(); + expect($queue->current()->getCallable())->toBe($cb); + $queue->next(); + expect($queue->current())->toBeAnInstanceOf(SampleMiddleware::class); + }); }); }); diff --git a/src/Http/MiddlewareQueue.php b/src/Http/MiddlewareQueue.php index dcf1a6ad..b016c361 100644 --- a/src/Http/MiddlewareQueue.php +++ b/src/Http/MiddlewareQueue.php @@ -12,7 +12,6 @@ namespace BlitzPHP\Http; use BlitzPHP\Container\Container; -use BlitzPHP\Core\App; use BlitzPHP\Middlewares\BaseMiddleware; use BlitzPHP\Middlewares\ClosureDecorator; use Closure; @@ -64,8 +63,8 @@ public function alias(string $alias, Closure|MiddlewareInterface|string $middlew /** * Ajoute des alias de middlewares - * - * @param array $aliases + * + * @param array $aliases */ public function aliases(array $aliases): static { @@ -76,8 +75,6 @@ public function aliases(array $aliases): static /** * Ajoute un middleware a la chaine d'execution - * - * @param array|callable|object|string $middlewares */ public function add(array|Closure|MiddlewareInterface|string $middleware): static { @@ -295,20 +292,24 @@ public function valid(): bool * * @internal */ - public function register() + public function register(array $config) { - $config = (object) config('middlewares'); + $config += [ + 'aliases' => [], + 'globals' => [], + 'build' => static fn () => null, + ]; - $this->aliases($config->aliases); + $this->aliases($config['aliases']); - foreach ($config->globals as $middleware) { + foreach ($config['globals'] as $middleware) { $this->add($middleware); } - if (is_callable($build = $config->build)) { + if (is_callable($build = $config['build'])) { $this->container->call($build, [ - 'request' => $this->request, - 'middleware' => $this, + 'request' => $this->request, + 'queue' => $this, ]); } } @@ -339,10 +340,10 @@ protected function resolve(Closure|MiddlewareInterface|string $middleware): Midd $middleware = $this->container->get($middleware); } else { throw new InvalidArgumentException(sprintf( - 'Middleware, `%s` n\'a pas été trouvé.', - $middleware - )); - } + 'Middleware, `%s` n\'a pas été trouvé.', + $middleware + )); + } if ($middleware instanceof BaseMiddleware) { $middleware->fill(explode(',', $options))->init($this->request->getPath()); diff --git a/src/Router/Dispatcher.php b/src/Router/Dispatcher.php index c0ea422b..7a03c5d4 100644 --- a/src/Router/Dispatcher.php +++ b/src/Router/Dispatcher.php @@ -652,7 +652,7 @@ protected function initMiddlewareQueue(): void $this->middleware = new MiddlewareQueue($this->container, [], $this->request, $this->response); $this->middleware->append($this->spoofRequestMethod()); - $this->middleware->register(); + $this->middleware->register(/** @scrutinizer ignore-type */ config('middlewares')); } protected function outputBufferingStart(): void From ba44ab366d4e543eabe8bcd919408c154ad5b86c Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Sun, 10 Dec 2023 19:01:42 +0100 Subject: [PATCH 094/111] Refonte de l'initialisation de l'intergiciel et de la gestion des erreurs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplifier l'initialisation de l'intergiciel en supprimant la méthode inutilisée "init" et en utilisant directement la méthode "__construct". Introduire également une nouvelle méthode "make" pour créer des instances de middleware et remanier la gestion des erreurs dans le fichier de base des erreurs. --- composer.json | 6 +++++- phpstan-baseline.php | 5 +++++ src/Middlewares/BaseMiddleware.php | 5 +++++ src/Middlewares/BodyParser.php | 11 ++++++----- src/Middlewares/Cors.php | 6 ++---- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 693033ec..dbca357e 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,11 @@ "Composer\\Config::disableProcessTimeout", "bash -c \"XDEBUG_MODE=off phpstan analyse\"" ], - "cs": [ + "phpstan:baseline": [ + "Composer\\Config::disableProcessTimeout", + "bash -c \"XDEBUG_MODE=off phpstan analyse --generate-baseline phpstan-baseline.php\"" + ], + "cs": [ "Composer\\Config::disableProcessTimeout", "php-cs-fixer fix --ansi --verbose --dry-run --diff" ], diff --git a/phpstan-baseline.php b/phpstan-baseline.php index ef85f93c..50f7c86b 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -541,6 +541,11 @@ 'count' => 1, 'path' => __DIR__ . '/src/Mail/Adapters/SymfonyMailer.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Unsafe usage of new static\\(\\)\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Middlewares/BaseMiddleware.php', +]; $ignoreErrors[] = [ 'message' => '#^Property BlitzPHP\\\\Router\\\\AutoRouter\\:\\:\\$methodPos is never read, only written\\.$#', 'count' => 1, diff --git a/src/Middlewares/BaseMiddleware.php b/src/Middlewares/BaseMiddleware.php index 775b91d3..8ea257a7 100644 --- a/src/Middlewares/BaseMiddleware.php +++ b/src/Middlewares/BaseMiddleware.php @@ -50,6 +50,11 @@ public function init(string $path): static return $this; } + public static function make(...$args): static + { + return new static(...$args); + } + public function __get($name) { return $this->arguments[$name] ?? null; diff --git a/src/Middlewares/BodyParser.php b/src/Middlewares/BodyParser.php index 3969a6ac..ae038231 100644 --- a/src/Middlewares/BodyParser.php +++ b/src/Middlewares/BodyParser.php @@ -56,23 +56,24 @@ class BodyParser extends BaseMiddleware implements MiddlewareInterface * La manipulation nécessite plus de soin que JSON. * - `methods` Les méthodes HTTP à analyser. Par défaut, PUT, POST, PATCH DELETE. */ - public function init(array $options = []): static + public function __construct(array $options = []) { $options += ['json' => true, 'xml' => false, 'methods' => null]; if ($options['json']) { $this->addParser( ['application/json', 'text/json'], - Closure::fromCallable([$this, 'decodeJson']) + $this->decodeJson(...) ); } if ($options['xml']) { $this->addParser( ['application/xml', 'text/xml'], - Closure::fromCallable([$this, 'decodeXml']) + $this->decodeXml(...) ); } - - return parent::init($options); + if ($options['methods']) { + $this->setMethods($options['methods']); + } } /** diff --git a/src/Middlewares/Cors.php b/src/Middlewares/Cors.php index 4bf47b9f..67e057b3 100644 --- a/src/Middlewares/Cors.php +++ b/src/Middlewares/Cors.php @@ -22,7 +22,7 @@ */ class Cors extends BaseMiddleware implements MiddlewareInterface { - protected $config = [ + protected array $config = [ 'AllowOrigin' => true, 'AllowCredentials' => true, 'AllowMethods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], @@ -34,11 +34,9 @@ class Cors extends BaseMiddleware implements MiddlewareInterface /** * Constructor */ - public function init(array $config = []): static + public function __construct(array $config = []) { $this->config = array_merge($this->config, $config); - - return parent::init($config); } /** From 7c468be153d40c569c171604d962a433a12e34e3 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 13 Dec 2023 15:54:19 +0100 Subject: [PATCH 095/111] Refactor ApplicationController and MiddlewareQueue Refactor the ApplicationController and MiddlewareQueue classes to improve code clarity and maintainability. In the ApplicationController, the logic for building the view path has been simplified by using string manipulation instead of directory traversal. This change improves code readability and reduces complexity. Additionally, the layout handling has been improved to use a stricter check for an empty layout value. In the MiddlewareQueue, a minor refactoring has been done to enhance code organization. The initialization of middleware options has been wrapped in a conditional statement, resulting in a cleaner and more efficient code structure. These changes improve the overall quality and maintainability of the codebase. --- src/Controllers/ApplicationController.php | 25 ++++++++++++++++------- src/Http/MiddlewareQueue.php | 6 +++++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/Controllers/ApplicationController.php b/src/Controllers/ApplicationController.php index 653ec622..d73fc21e 100644 --- a/src/Controllers/ApplicationController.php +++ b/src/Controllers/ApplicationController.php @@ -24,14 +24,14 @@ class ApplicationController extends BaseController { /** - * @var array Données partagées entre toutes les vue chargées à partir d'un controleur + * Données partagées entre toutes les vue chargées à partir d'un controleur */ - protected $viewDatas = []; + protected array $viewDatas = []; /** - * @var string Layout a utiliser + * Layout a utiliser */ - protected $layout; + protected string $layout = ''; /** * Charge une vue @@ -50,15 +50,26 @@ protected function view(string $view, ?array $data = [], ?array $options = []): ['dirname' => $dirname, 'filename' => $filename] = pathinfo($reflection->getFileName()); $dirname = str_ireplace('Controllers', 'Views', $dirname); $filename = strtolower(str_ireplace('Controller', '', $filename)); - $path = implode(DS, [$dirname, $filename]) . DS; + + $parts = explode('Views', $dirname); + $base = array_shift($parts); + $parts = array_map('strtolower', $parts); + $parts = [$base, ...$parts]; + + $dirname = implode('Views', $parts); + $path = implode(DS, [$dirname, $filename]) . DS; + + if (! is_dir($path)) { + $path = implode(DS, [$dirname]) . DS; + } } $viewer = Services::viewer(); $viewer->setData($data)->setOptions($options); - if (! empty($this->layout) && is_string($this->layout)) { - $viewer->setLayout($this->layout); + if ($this->layout !== '') { + $viewer->layout($this->layout); } if (! empty($this->viewDatas) && is_array($this->viewDatas)) { diff --git a/src/Http/MiddlewareQueue.php b/src/Http/MiddlewareQueue.php index b016c361..b9ebcba8 100644 --- a/src/Http/MiddlewareQueue.php +++ b/src/Http/MiddlewareQueue.php @@ -346,7 +346,11 @@ protected function resolve(Closure|MiddlewareInterface|string $middleware): Midd } if ($middleware instanceof BaseMiddleware) { - $middleware->fill(explode(',', $options))->init($this->request->getPath()); + if (null !== $options) { + $middleware->fill(explode(',', $options)); + } + + $middleware->init($this->request->getPath()); } } From 28baaa580423914eb51eff9cabd430fb52eb4799 Mon Sep 17 00:00:00 2001 From: dimtrovich Date: Wed, 13 Dec 2023 14:55:11 +0000 Subject: [PATCH 096/111] Fix styling --- src/Controllers/ApplicationController.php | 4 ++-- src/Http/MiddlewareQueue.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Controllers/ApplicationController.php b/src/Controllers/ApplicationController.php index d73fc21e..66ad7b4c 100644 --- a/src/Controllers/ApplicationController.php +++ b/src/Controllers/ApplicationController.php @@ -55,10 +55,10 @@ protected function view(string $view, ?array $data = [], ?array $options = []): $base = array_shift($parts); $parts = array_map('strtolower', $parts); $parts = [$base, ...$parts]; - + $dirname = implode('Views', $parts); $path = implode(DS, [$dirname, $filename]) . DS; - + if (! is_dir($path)) { $path = implode(DS, [$dirname]) . DS; } diff --git a/src/Http/MiddlewareQueue.php b/src/Http/MiddlewareQueue.php index b9ebcba8..6814c7bf 100644 --- a/src/Http/MiddlewareQueue.php +++ b/src/Http/MiddlewareQueue.php @@ -347,9 +347,9 @@ protected function resolve(Closure|MiddlewareInterface|string $middleware): Midd if ($middleware instanceof BaseMiddleware) { if (null !== $options) { - $middleware->fill(explode(',', $options)); + $middleware->fill(explode(',', $options)); } - + $middleware->init($this->request->getPath()); } } From 23dfd363554216312c38552109f6c969bdeb7884 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 14 Dec 2023 14:52:34 +0100 Subject: [PATCH 097/111] [Uri] phpstan fix + test --- spec/system/framework/Http/Uri.spec.php | 251 ++++++++++++++++++++++ src/Exceptions/HttpException.php | 20 ++ src/Http/Uri.php | 263 +++++++++++++++--------- 3 files changed, 436 insertions(+), 98 deletions(-) create mode 100644 spec/system/framework/Http/Uri.spec.php diff --git a/spec/system/framework/Http/Uri.spec.php b/spec/system/framework/Http/Uri.spec.php new file mode 100644 index 00000000..1d8f721c --- /dev/null +++ b/spec/system/framework/Http/Uri.spec.php @@ -0,0 +1,251 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Exceptions\HttpException; +use BlitzPHP\Http\Uri; + +describe('Http / URI', function () { + describe('Uri', function () { + it("Teste si le constructeur definie toutes les parties", function () { + $uri = new Uri('http://username:password@hostname:9090/path?arg=value#anchor'); + + expect($uri->getScheme())->toBe('http'); + expect($uri->getUserInfo())->toBe('username'); + expect($uri->getHost())->toBe('hostname'); + expect($uri->getPath())->toBe('/path'); + expect($uri->getQuery())->toBe('arg=value'); + expect($uri->getPort())->toBe(9090); + expect($uri->getFragment())->toBe('anchor'); + expect($uri->getSegments())->toBe(['path']); + + // Mot de passe ignoré par défaut pour des raisons de sécurité. + expect($uri->getAuthority())->toBe('username@hostname:9090'); + }); + + it("Teste si Segments est rempli correctement pour plusieurs segments", function () { + $uri = new Uri('http://hostname/path/to/script'); + + expect($uri->getSegments())->toBe(['path', 'to', 'script']); + expect($uri->getSegment(1))->toBe('path'); + expect($uri->getSegment(2))->toBe('to'); + expect($uri->getSegment(3))->toBe('script'); + expect($uri->getSegment(4))->toBe(''); + + expect($uri->getTotalSegments())->toBe(3); + }); + + it("Teste les segments hors limites", function () { + $uri = new Uri('http://hostname/path/to/script'); + + expect(fn() => $uri->getSegment(5))->toThrow(new HttpException()); + }); + + it("Teste les segments hors limites avec une valeur par defaut", function () { + $uri = new Uri('http://abc.com/a123/b/c'); + + expect(fn() => $uri->getSegment(22, 'something'))->toThrow(new HttpException()); + }); + + it("Teste si Segments est rempli avec les valeurs par defaut", function () { + $uri = new Uri('http://hostname/path/to'); + + expect($uri->getSegments())->toBe(['path', 'to']); + expect($uri->getSegment(1))->toBe('path'); + expect($uri->getSegment(2, 'different'))->toBe('to'); + expect($uri->getSegment(3, 'script'))->toBe('script'); + expect($uri->getSegment(3))->toBe(''); + + expect($uri->getTotalSegments())->toBe(2); + }); + + it("Teste si l'URI peut etre caster en string", function () { + $url = 'http://username:password@hostname:9090/path?arg=value#anchor'; + $uri = new Uri($url); + + $expected = 'http://username@hostname:9090/path?arg=value#anchor'; + + expect((string) $uri)->toBe($expected); + }); + + it("Teste les URI simple", function () { + $urls = [ + [ + 'http://example.com', // url + 'http://example.com', // expectedURL + '', // expectedPath + ], + ['http://example.com/', 'http://example.com/', '/'], + ['http://example.com/one/two', 'http://example.com/one/two', '/one/two'], + ['http://example.com/one/two/', 'http://example.com/one/two/', '/one/two/'], + ['http://example.com/one/two//', 'http://example.com/one/two/', '/one/two/'], + ['http://example.com//one/two//', 'http://example.com/one/two/', '/one/two/'], + ['http://example.com//one//two//', 'http://example.com/one/two/', '/one/two/'], + ['http://example.com///one/two', 'http://example.com/one/two', '/one/two'], + ['http://example.com/one/two///', 'http://example.com/one/two/', '/one/two/'], + ]; + + foreach ($urls as $u) { + [$url, $expectedURL, $expectedPath] = $u; + + $uri = new Uri($url); + + expect((string) $uri)->toBe($expectedURL); + expect($uri->getPath())->toBe($expectedPath); + } + }); + + it('Teste les URL vide', function () { + $url = ''; + $uri = new Uri($url); + + expect((string) $uri)->toBe('http://'); + + $url = '/'; + $uri = new Uri($url); + + expect((string) $uri)->toBe('http://'); + }); + + it('Teste les URL malformés', function () { + $url = 'http://abc:a123'; + + expect(fn() => new Uri($url))->toThrow(new HttpException()); + }); + + it('Teste les schema manquant', function () { + $url = 'http://foo.bar/baz'; + $uri = new Uri($url); + + expect($uri->getScheme())->toBe('http'); + expect($uri->getAuthority())->toBe('foo.bar'); + expect($uri->getPath())->toBe('/baz'); + expect((string) $uri)->toBe($url); + }); + }); + + describe('Getter et setter', function () { + it('setScheme', function () { + $url = 'http://example.com/path'; + $uri = new Uri($url); + + $uri->setScheme('https'); + + expect($uri->getScheme())->toBe('https'); + $expected = 'https://example.com/path'; + expect((string) $uri)->toBe($expected); + }); + + it('withScheme', function () { + $url = 'example.com'; + $uri = new Uri('http://' . $url); + + $new = $uri->withScheme('x'); + + expect((string) $uri)->toBe('http://' . $url); + expect((string) $new)->toBe('x://' . $url); + }); + + it('withScheme avec https', function () { + $url = 'http://example.com/path'; + $uri = new Uri($url); + + $new = $uri->withScheme('https'); + + expect($new->getScheme())->toBe('https'); + expect($uri->getScheme())->toBe('http'); + + $expected = 'https://example.com/path'; + expect((string) $new)->toBe($expected); + $expected = 'http://example.com/path'; + expect((string) $uri)->toBe($expected); + }); + + it('withScheme avec une valeur vide', function () { + $url = 'example.com'; + $uri = new Uri('http://' . $url); + + $new = $uri->withScheme(''); + + expect((string) $new)->toBe($url); + expect((string) $uri)->toBe('http://' . $url); + }); + + it('setUserInfo', function () { + $url = 'http://example.com/path'; + $uri = new Uri($url); + + $uri->setUserInfo('user', 'password'); + + expect($uri->getUserInfo())->toBe('user'); + $expected = 'http://user@example.com/path'; + expect((string) $uri)->toBe($expected); + }); + + it('Teste si UserInfo peut afficher le password', function () { + $url = 'http://example.com/path'; + $uri = new Uri($url); + + $uri->setUserInfo('user', 'password'); + + expect($uri->getUserInfo())->toBe('user'); + $expected = 'http://user@example.com/path'; + expect((string) $uri)->toBe($expected); + + $uri->showPassword(); + expect($uri->getUserInfo())->toBe('user:password'); + $expected = 'http://user:password@example.com/path'; + expect((string) $uri)->toBe($expected); + }); + + it('setHost', function () { + $url = 'http://example.com/path'; + $uri = new Uri($url); + + $uri->setHost('another.com'); + + expect($uri->getHost())->toBe('another.com'); + $expected = 'http://another.com/path'; + expect((string) $uri)->toBe($expected); + }); + + it('setPort', function () { + $url = 'http://example.com/path'; + $uri = new Uri($url); + + $uri->setPort(9000); + + expect($uri->getPort())->toBe(9000); + $expected = 'http://example.com:9000/path'; + expect((string) $uri)->toBe($expected); + }); + + it('setPort avec une valeur invalide', function () { + $url = 'http://example.com/path'; + $uri = new Uri($url); + + $ports = [70000, -1, 0]; + foreach ($ports as $port) { + $errorString = lang('HTTP.invalidPort', [$port]); + expect($errorString)->not->toBeEmpty(); + expect(fn() => $uri->setPort($port)) + ->toThrow(new HttpException($errorString)); + } + }); + + it('setURI capture les mauvais port', function () { + $url = 'http://username:password@hostname:90909/path?arg=value#anchor'; + $uri = new Uri(); + + expect(fn() => $uri->setURI($url)) + ->toThrow(new HttpException()); + }); + }); +}); diff --git a/src/Exceptions/HttpException.php b/src/Exceptions/HttpException.php index 3e73bb30..0fcbf306 100644 --- a/src/Exceptions/HttpException.php +++ b/src/Exceptions/HttpException.php @@ -37,4 +37,24 @@ public static function badRequest(string $message = 'Bad Request') { return new static($message, 400); } + + public static function unableToParseURI(string $uri) + { + return new static(lang('HTTP.cannotParseURI', [$uri])); + } + + public static function uriSegmentOutOfRange(int $segment) + { + return new static(lang('HTTP.segmentOutOfRange', [$segment])); + } + + public static function invalidPort(int $port) + { + return new static(lang('HTTP.invalidPort', [$port])); + } + + public static function malformedQueryString() + { + return new static(lang('HTTP.malformedQueryString')); + } } diff --git a/src/Http/Uri.php b/src/Http/Uri.php index bc862f5b..4a6def5e 100644 --- a/src/Http/Uri.php +++ b/src/Http/Uri.php @@ -11,7 +11,7 @@ namespace BlitzPHP\Http; -use BlitzPHP\Exceptions\FrameworkException; +use BlitzPHP\Exceptions\HttpException; use InvalidArgumentException; use Psr\Http\Message\UriInterface; @@ -43,73 +43,53 @@ class Uri implements UriInterface * Liste des segments d'URI. * * Commence à 1 au lieu de 0 - * - * @var array */ - protected $segments = []; + protected array $segments = []; /** * Schéma - * - * @var string */ - protected $scheme = 'http'; + protected string $scheme = 'http'; /** * Informations utilisateur - * - * @var string */ - protected $user = ''; + protected ?string $user = null; /** * Mot de passe - * - * @var string */ - protected $password = ''; + protected ?string $password = null; /** * Hôte - * - * @var string */ - protected $host = ''; + protected ?string $host = null; /** * Port - * - * @var int */ - protected $port = 80; + protected ?int $port = null; /** * Chemin. - * - * @var string */ - protected $path = ''; + protected ?string $path = null; /** * Le nom de n'importe quel fragment. - * - * @var string */ - protected $fragment = ''; + protected string $fragment = ''; /** * La chaîne de requête. - * - * @var array */ - protected $query = []; + protected array $query = []; /** * Default schemes/ports. - * - * @var array */ - protected $defaultPorts = [ + protected array $defaultPorts = [ 'http' => 80, 'https' => 443, 'ftp' => 21, @@ -119,10 +99,8 @@ class Uri implements UriInterface /** * Indique si les mots de passe doivent être affichés dans les appels userInfo/authority. * La valeur par défaut est false car les URI apparaissent souvent dans les journaux - * - * @var bool */ - protected $showPassword = false; + protected bool $showPassword = false; /** * Constructeur. @@ -132,7 +110,6 @@ class Uri implements UriInterface public function __construct(?string $uri = null) { $this->setURI($uri); - $this->port = $_SERVER['SERVER_PORT'] ?? 80; } /** @@ -144,7 +121,7 @@ public function setURI(?string $uri = null): self $parts = parse_url($uri); if ($parts === false) { - throw new FrameworkException('Impossible de parser l\'URI "' . $uri . '"'); + throw HttpException::unableToParseURI($uri); } $this->applyParts($parts); @@ -176,11 +153,9 @@ public function getAuthority(bool $ignorePort = false): string $authority = $this->getUserInfo() . '@' . $authority; } - if (! empty($this->port) && ! $ignorePort) { - // N'ajoute pas de port s'il s'agit d'un port standard pour ce schéma - if ($this->port !== $this->defaultPorts[$this->scheme]) { - $authority .= ':' . $this->port; - } + // N'ajoute pas de port s'il s'agit d'un port standard pour ce schéma + if (! empty($this->port) && ! $ignorePort && $this->port !== $this->defaultPorts[$this->scheme]) { + $authority .= ':' . $this->port; } $this->showPassword = false; @@ -193,7 +168,7 @@ public function getAuthority(bool $ignorePort = false): string */ public function getUserInfo(): string { - $userInfo = $this->user; + $userInfo = $this->user ?: ''; if ($this->showPassword === true && ! empty($this->password)) { $userInfo .= ':' . $this->password; @@ -218,13 +193,13 @@ public function showPassword(bool $val = true): self */ public function getHost(): string { - return $this->host; + return $this->host ?? ''; } /** * {@inheritDoc} */ - public function getPort(): int + public function getPort(): ?int { return $this->port; } @@ -234,7 +209,7 @@ public function getPort(): int */ public function getPath(): string { - return (null === $this->path) ? '' : $this->path; + return $this->path ?? ''; } /** @@ -276,7 +251,7 @@ public function getQuery(array $options = []): string */ public function getFragment(): string { - return null === $this->fragment ? '' : $this->fragment; + return $this->fragment ?? ''; } /** @@ -292,17 +267,20 @@ public function getSegments(): array * * @return string La valeur du segment. Si aucun segment n'est trouvé, lance InvalidArgumentError */ - public function getSegment(int $number): string + public function getSegment(int $number, string $default = ''): string { + if ($number < 1) { + throw HttpException::uriSegmentOutOfRange($number); + } + if ($number > count($this->segments) + 1) { + throw HttpException::uriSegmentOutOfRange($number); + } + // Le segment doit traiter le tableau comme basé sur 1 pour l'utilisateur // mais nous devons encore gérer un tableau de base zéro. $number--; - if ($number > count($this->segments)) { - throw new FrameworkException('Le segment "' . $number . '" n\'est pas dans l\'interval de segment disponible'); - } - - return $this->segments[$number] ?? ''; + return $this->segments[$number] ?? $default; } /** @@ -313,14 +291,18 @@ public function getSegment(int $number): string */ public function setSegment(int $number, $value) { - // Le segment doit traiter le tableau comme basé sur 1 pour l'utilisateur - // mais nous devons encore gérer un tableau de base zéro. - $number--; + if ($number < 1) { + throw HTTPException::uriSegmentOutOfRange($number); + } if ($number > count($this->segments) + 1) { - throw new FrameworkException('Le segment "' . $number . '" n\'est pas dans l\'interval de segment disponible'); + throw HTTPException::uriSegmentOutOfRange($number); } + // Le segment doit traiter le tableau comme basé sur 1 pour l'utilisateur + // mais nous devons encore gérer un tableau de base zéro. + $number--; + $this->segments[$number] = $value; $this->refreshPath(); @@ -341,10 +323,16 @@ public function getTotalSegments(): int */ public function __toString(): string { + $path = $this->getPath(); + $scheme = $this->getScheme(); + + // Si les hôtes correspondent, il faut supposer que l'URL est relative à l'URL de base. + [$scheme, $path] = $this->changeSchemeAndPath($scheme, $path); + return static::createURIString( - $this->getScheme(), + $scheme, $this->getAuthority(), - $this->getPath(), // Les URI absolus doivent utiliser un "/" pour un chemin vide + $path, // Les URI absolus doivent utiliser un "/" pour un chemin vide $this->getQuery(), $this->getFragment() ); @@ -364,15 +352,17 @@ public static function createURIString(?string $scheme = null, ?string $authorit $uri .= $authority; } - if ($path) { - $uri .= substr($uri, -1, 1) !== '/' ? '/' . ltrim($path, '/') : $path; + if (isset($path) && $path !== '') { + $uri .= substr($uri, -1, 1) !== '/' + ? '/' . ltrim($path, '/') + : ltrim($path, '/'); } - if ($query) { + if ($query !== '' && $query !== null) { $uri .= '?' . $query; } - if ($fragment) { + if ($fragment !== '' && $fragment !== null) { $uri .= '#' . $fragment; } @@ -386,7 +376,11 @@ public function setAuthority(string $str): self { $parts = parse_url($str); - if (empty($parts['host']) && ! empty($parts['path'])) { + if (! isset($parts['path'])) { + $parts['path'] = $this->getPath(); + } + + if (empty($parts['host']) && $parts['path'] !== '') { $parts['host'] = $parts['path']; unset($parts['path']); } @@ -406,10 +400,8 @@ public function setAuthority(string $str): self */ public function setScheme(string $str): self { - $str = strtolower($str); - $str = preg_replace('#:(//)?$#', '', $str); - - $this->scheme = $str; + $str = strtolower($str); + $this->scheme = preg_replace('#:(//)?$#', '', $str); return $this; } @@ -417,9 +409,15 @@ public function setScheme(string $str): self /** * {@inheritDoc} */ - public function withScheme(string $scheme): self + public function withScheme(string $scheme): static { - return $this->setScheme($scheme); + $uri = clone $this; + + $scheme = strtolower($scheme); + + $uri->scheme = preg_replace('#:(//)?$#', '', $scheme); + + return $uri; } /** @@ -439,9 +437,13 @@ public function setUserInfo(string $user, string $pass): self /** * {@inheritDoc} */ - public function withUserInfo(string $user, ?string $password = null): self + public function withUserInfo(string $user, ?string $password = null): static { - return $this->setUserInfo($user, $password); + $new = clone $this; + + $new->setUserInfo($user, $password); + + return $new; } /** @@ -457,9 +459,13 @@ public function setHost(string $str): self /** * {@inheritDoc} */ - public function withHost(string $host): self + public function withHost(string $host): static { - return $this->setHost($host); + $new = clone $this; + + $new->setHost($host); + + return $new; } /** @@ -472,7 +478,7 @@ public function setPort(?int $port = null): self } if ($port <= 0 || $port > 65535) { - throw new FrameworkException('Le port "' . $port . '" est invalide'); + throw HttpException::invalidPort($port); } $this->port = $port; @@ -483,9 +489,13 @@ public function setPort(?int $port = null): self /** * {@inheritDoc} */ - public function withPort(?int $port): self + public function withPort(?int $port): static { - return $this->setPort($port); + $new = clone $this; + + $new->setPort($port); + + return $new; } /** @@ -495,7 +505,9 @@ public function setPath(string $path): self { $this->path = $this->filterPath($path); - $this->segments = explode('/', $this->path); + $tempPath = trim($this->path, '/'); + + $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath); return $this; } @@ -503,19 +515,25 @@ public function setPath(string $path): self /** * {@inheritDoc} */ - public function withPath(string $path): self + public function withPath(string $path): static { - return $this->setPath($path); + $new = clone $this; + + $new->setPath($path); + + return $new; } /** * Définit la partie chemin de l'URI en fonction des segments. */ - public function refreshPath(): self + private function refreshPath(): self { $this->path = $this->filterPath(implode('/', $this->segments)); - $this->segments = explode('/', $this->path); + $tempPath = trim($this->path, '/'); + + $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath); return $this; } @@ -527,7 +545,7 @@ public function refreshPath(): self public function setQuery(string $query): self { if (str_contains($query, '#')) { - throw new FrameworkException('La chaine de requete est mal formée'); + throw HTTPException::malformedQueryString(); } // Ne peut pas avoir de début ? @@ -543,9 +561,13 @@ public function setQuery(string $query): self /** * {@inheritDoc} */ - public function withQuery(string $query): self + public function withQuery(string $query): static { - return $this->setQuery($query); + $new = clone $this; + + $new->setQuery($query); + + return $new; } /** @@ -559,6 +581,19 @@ public function setQueryArray(array $query): self return $this->setQuery($query); } + /** + * Une méthode pratique pour transmettre un tableau d'éléments en tant que requête + * partie de l'URI. + */ + public function withQueryParams(array $query): static + { + $uri = clone $this; + + $uri->setQueryArray($query); + + return $uri; + } + /** * Ajoute un seul nouvel élément à la requête vars. */ @@ -617,9 +652,13 @@ public function setFragment(string $string): self /** * {@inheritDoc} */ - public function withFragment(string $fragment): self + public function withFragment(string $fragment): static { - return $this->setFragment($fragment); + $new = clone $this; + + $new->setFragment($fragment); + + return $new; } /** @@ -636,7 +675,7 @@ protected function filterPath(?string $path = null): string $path = urldecode($path); // Supprimer les segments de points - $path = $this->removeDotSegments($path); + $path = self::removeDotSegments($path); // Correction de certains cas de bord de barre oblique... if (str_starts_with($orig, './')) { @@ -683,20 +722,19 @@ protected function applyParts(array $parts) $this->setScheme('http'); } - if (isset($parts['port'])) { - if (null !== $parts['port']) { - // Les numéros de port valides sont appliqués par les précédents parse_url ou setPort() - $port = $parts['port']; - $this->port = $port; - } + if (isset($parts['port']) && null !== $parts['port']) { + // Les numéros de port valides sont appliqués par les précédents parse_url ou setPort() + $this->port = $parts['port']; } if (isset($parts['pass'])) { $this->password = $parts['pass']; } - if (! empty($parts['path'])) { - $this->segments = explode('/', trim($parts['path'], '/')); + if (isset($parts['path']) && $parts['path'] !== '') { + $tempPath = trim($parts['path'], '/'); + + $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath); } } @@ -730,7 +768,7 @@ public function resolveRelativeURI(string $uri): self if ($relative->getPath() === '') { $transformed->setPath($this->getPath()); - if ($relative->getQuery()) { + if ($relative->getQuery() !== '') { $transformed->setQuery($relative->getQuery()); } else { $transformed->setQuery($this->getQuery()); @@ -762,13 +800,13 @@ public function resolveRelativeURI(string $uri): self */ protected function mergePaths(self $base, self $reference): string { - if (! empty($base->getAuthority()) && empty($base->getPath())) { + if (! empty($base->getAuthority()) && '' === $base->getPath()) { return '/' . ltrim($reference->getPath(), '/ '); } $path = explode('/', $base->getPath()); - if (empty($path[0])) { + if ('' === $path[0]) { unset($path[0]); } @@ -828,4 +866,33 @@ public static function removeDotSegments(string $path): string return $output; } + + /** + * Modifier le chemin (et le schéma) en supposant que les URI ayant le même hôte que baseURL doivent être relatifs à la configuration du projet. + * + * @deprecated Cette methode pourrait etre supprimer + */ + private function changeSchemeAndPath(string $scheme, string $path): array + { + // Vérifier s'il s'agit d'un URI interne + $config = (object) config('app'); + $baseUri = new self($config->base_url); + + if (substr($this->getScheme(), 0, 4) === 'http' && $this->getHost() === $baseUri->getHost()) { + // Vérifier la présence de segments supplémentaires + $basePath = trim($baseUri->getPath(), '/') . '/'; + $trimPath = ltrim($path, '/'); + + if ($basePath !== '/' && ! str_starts_with($trimPath, $basePath)) { + $path = $basePath . $trimPath; + } + + // Vérifier si le protocole HTTPS est forcé + if ($config->force_global_secure_requests) { + $scheme = 'https'; + } + } + + return [$scheme, $path]; + } } From 9fc86f5c954a3a4924aa36fcceab51feb10fc6c2 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Fri, 15 Dec 2023 18:21:10 +0100 Subject: [PATCH 098/111] =?UTF-8?q?Refactorisation=20des=20m=C3=A9thodes?= =?UTF-8?q?=20de=20bundle=20et=20suppression=20du=20code=20inutile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le code diff refactore les méthodes `stylesBundle()` et `scriptsBundle()` dans la classe `NativeAdapter`. Le code inutile pour générer les balises de style et de script est supprimé de ces méthodes. Les méthodes `addLibCss()`, `addCss()`, `addLibJs()`, et `addJs()` sont aussi modifiées en utilisant `array_merge()` au lieu d'itérer sur le tableau `$src`. Ces changements simplifient le code et améliorent sa lisibilité. L'impact de ces changements est que les méthodes bundle utilisent maintenant les fonctions `lib_styles()` et `styles()` pour générer les références de liens pour les styles et les fonctions `lib_scripts()` et `scripts()` pour les scripts. --- src/View/Adapters/NativeAdapter.php | 92 +++++------------------------ 1 file changed, 16 insertions(+), 76 deletions(-) diff --git a/src/View/Adapters/NativeAdapter.php b/src/View/Adapters/NativeAdapter.php index eaed4efe..a31a4de4 100644 --- a/src/View/Adapters/NativeAdapter.php +++ b/src/View/Adapters/NativeAdapter.php @@ -362,18 +362,6 @@ public function show(string $sectionName, bool $preserve = false) return; } - $start = $end = ''; - if ($sectionName === 'css') { - $start = "\n"; - } - if ($sectionName === 'js') { - $start = "\n"; - } - - echo $start; - foreach ($this->sections[$sectionName] as $key => $contents) { echo $contents; @@ -381,8 +369,6 @@ public function show(string $sectionName, bool $preserve = false) unset($this->sections[$sectionName][$key]); } } - - echo $end; } /** @@ -585,11 +571,7 @@ public function required(bool|string $condition): string */ public function addLibCss(string ...$src): self { - foreach ($src as $var) { - if (! in_array($var, $this->_lib_styles, true)) { - $this->_lib_styles[] = $var; - } - } + $this->_lib_styles = array_merge($this->_lib_styles, $src); return $this; } @@ -599,11 +581,7 @@ public function addLibCss(string ...$src): self */ public function addCss(string ...$src): self { - foreach ($src as $var) { - if (! in_array($var, $this->_styles, true)) { - $this->_styles[] = $var; - } - } + $this->_styles = array_merge($this->_styles, $src); return $this; } @@ -611,29 +589,14 @@ public function addCss(string ...$src): self /** * Compile les fichiers de style de l'instance et genere les link:href vers ceux-ci */ - public function stylesBundle(string ...$groups): void - { - $groups = (array) (empty($groups) ? $this->layout ?? 'default' : $groups); - $lib_styles = $styles = []; - - foreach ($groups as $group) { - $lib_styles = array_merge( - $lib_styles, - // (array) config('layout.'.$group.'.lib_styles'), - $this->_lib_styles - ); - $styles = array_merge( - $styles, - // (array) config('layout.'.$group.'.styles'), - $this->_styles - ); + public function stylesBundle(): void + { + if (! empty($this->_lib_styles)) { + lib_styles(array_unique($this->_lib_styles)); } - if (! empty($lib_styles)) { - lib_styles(array_unique($lib_styles)); - } - if (! empty($styles)) { - styles(array_unique($styles)); + if (! empty($this->_styles)) { + styles(array_unique($this->_styles)); } $this->show('css'); @@ -644,11 +607,7 @@ public function stylesBundle(string ...$groups): void */ public function addLibJs(string ...$src): self { - foreach ($src as $var) { - if (! in_array($var, $this->_lib_scripts, true)) { - $this->_lib_scripts[] = $var; - } - } + $this->_lib_scripts = array_merge($this->_lib_scripts, $src); return $this; } @@ -658,11 +617,7 @@ public function addLibJs(string ...$src): self */ public function addJs(string ...$src): self { - foreach ($src as $var) { - if (! in_array($var, $this->_scripts, true)) { - $this->_scripts[] = $var; - } - } + $this->_scripts = array_merge($this->_scripts, $src); return $this; } @@ -670,29 +625,14 @@ public function addJs(string ...$src): self /** * Compile les fichiers de script de l'instance et genere les link:href vers ceux-ci */ - public function scriptsBundle(string ...$groups): void - { - $groups = (array) (empty($groups) ? $this->layout ?? 'default' : $groups); - $lib_scripts = $scripts = []; - - foreach ($groups as $group) { - $lib_scripts = array_merge( - $lib_scripts, - // (array) config('layout.'.$group.'.lib_scripts'), - $this->_lib_scripts - ); - $scripts = array_merge( - $scripts, - // (array) config('layout.'.$group.'.scripts'), - $this->_scripts - ); + public function scriptsBundle(): void + { + if (! empty($this->_lib_scripts)) { + lib_scripts(array_unique($this->_lib_scripts)); } - if (! empty($lib_scripts)) { - lib_scripts(array_unique($lib_scripts)); - } - if (! empty($scripts)) { - scripts(array_unique($scripts)); + if (! empty($this->_scripts)) { + scripts(array_unique($this->_scripts)); } $this->show('js'); From 0c9f2fcbac90159d7aec5f37721b1def475479ea Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Fri, 15 Dec 2023 18:25:18 +0100 Subject: [PATCH 099/111] =?UTF-8?q?feat=20:=20Ajouter=20une=20nouvelle=20m?= =?UTF-8?q?=C3=A9thode=20=C3=A0=20NativeAdapter=20pour=20ins=C3=A9rer=20de?= =?UTF-8?q?s=20vues=20suppl=C3=A9mentaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce commit ajoute une nouvelle méthode `insertFirst` à la classe `NativeAdapter` afin de supporter l'inclusion de vues supplémentaires dans les modèles de layout. La méthode accepte un tableau de chemins de vues, et si l'une des vues existe, elle est rendue et retournée sous forme de chaîne de caractères. Auparavant, il n'existait pas de moyen direct d'inclure des vues supplémentaires dans les modèles de présentation, ce qui limitait la flexibilité du système de vues. Cette nouvelle méthode fournit une solution pratique pour insérer dynamiquement des vues basées sur des conditions ou des exigences spécifiques. Une boucle `insertFirst` itère sur les chemins de vue fournis, vérifie leur existence et renvoie la vue rendue si elle est trouvée. Si aucune des vues n'existe, une `ViewException` est lancée avec un message d'erreur qui liste les chemins de vue tentés. Cette amélioration renforce la modularité et la réutilisation du système de vues et permet des compositions d'agencement plus dynamiques et personnalisables. --- src/View/Adapters/NativeAdapter.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/View/Adapters/NativeAdapter.php b/src/View/Adapters/NativeAdapter.php index a31a4de4..3788bc06 100644 --- a/src/View/Adapters/NativeAdapter.php +++ b/src/View/Adapters/NativeAdapter.php @@ -12,6 +12,7 @@ namespace BlitzPHP\View\Adapters; use BlitzPHP\Debug\Toolbar\Collectors\ViewsCollector; +use BlitzPHP\Exceptions\ViewException; use BlitzPHP\Utilities\Helpers; use RuntimeException; @@ -481,6 +482,27 @@ public function insertIf(string $view, ?array $data = [], ?array $options = null return ''; } + /** + * Utilisé dans les vues de mise en page pour inclure des vues supplémentaires si elle existe. + */ + public function insertFirst(array $views, ?array $data = [], ?array $options = null, ?bool $saveData = null): string + { + foreach ($views as $view) { + $view = Helpers::ensureExt($view, $this->ext); + $view = str_replace(' ', '', $view); + + if ($view[0] !== '/') { + $view = $this->retrievePartialPath($view); + } + + if (is_file($view)) { + return $this->addData($data)->render($view, $options, $saveData); + } + } + + throw ViewException::invalidFile(implode(' OR ', $views)); + } + /** * Compile de manière conditionnelle une chaîne de classe CSS. */ From d0ed987090049c9646fd315fcaa94e6112aaf868 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Fri, 15 Dec 2023 18:52:57 +0100 Subject: [PATCH 100/111] =?UTF-8?q?feat=20:=20Ajout=20de=20la=20g=C3=A9n?= =?UTF-8?q?=C3=A9ration=20de=20jetons=20CSRF=20et=20d'aides=20pour=20les?= =?UTF-8?q?=20champs=20de=20m=C3=A9thode=20de=20formulaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce commit ajoute de nouvelles fonctions d'aide pour générer des jetons CSRF et des champs de méthode de formulaire. La fonction `csrf_token()` retourne la valeur actuelle du jeton CSRF, qui peut être utilisé pour construire des champs de saisie cachés ou comme partie d'appels d'API. Les fonctions `csrf_field()` et `csrf_meta()` génèrent respectivement des champs de saisie cachés et des balises méta, tous deux contenant le jeton CSRF. De plus, la fonction `method_field()` génère un champ de saisie caché permettant de modifier la méthode HTTP utilisée dans les formulaires. Ces fonctions d'aide sont ajoutées au fichier `common.php` existant dans le répertoire `src/Helpers`, et sont également utilisées dans la classe `NativeAdapter` dans le fichier src/View/Adapters/NativeAdapter.php`. --- src/Helpers/common.php | 47 +++++++++++++++++++++++++++++ src/View/Adapters/NativeAdapter.php | 26 +++++++++++++--- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/Helpers/common.php b/src/Helpers/common.php index 7c890784..e9e9d5f9 100644 --- a/src/Helpers/common.php +++ b/src/Helpers/common.php @@ -417,6 +417,53 @@ function stringify_attributes($attributes, bool $js = false): string } } +// ================================= FONCTIONS DE FORMULAIRE ================================= // + +if (! function_exists('csrf_token')) { + /** + * Renvoie la valeur de hachage actuelle pour la protection CSRF. + * Peut être utilisé dans les vues lors de la construction manuelle d'input cachées, ou utilisé dans les variables javascript pour l'utilisation de l'API. + */ + function csrf_token(): string + { + return Services::session()->token(); + } +} + +if (! function_exists('csrf_field')) { + /** + * Génère un champ input caché à utiliser dans les formulaires générés manuellement. + */ + function csrf_field(?string $id = null): string + { + $name = config('security.csrf_token_name', '_token'); + + return ''; + } +} + +if (! function_exists('csrf_meta')) { + /** + * Génère une balise méta à utiliser dans les appels javascript. + */ + function csrf_meta(?string $id = null): string + { + $name = config('security.csrf_header_name', 'X-CSRF-TOKEN'); + + return ''; + } +} + +if (! function_exists('method_field')) { + /** + * Générer un champ de formulaire pour usurper le verbe HTTP utilisé par les formulaires. + */ + function method_field(string $method): string + { + return ''; + } +} + // ================================= FONCTIONS D'ENVIRONNEMENT D'EXECUTION ================================= // if (! function_exists('environment')) { diff --git a/src/View/Adapters/NativeAdapter.php b/src/View/Adapters/NativeAdapter.php index 3788bc06..82a440a8 100644 --- a/src/View/Adapters/NativeAdapter.php +++ b/src/View/Adapters/NativeAdapter.php @@ -588,12 +588,28 @@ public function required(bool|string $condition): string return true === filter_var($condition, FILTER_VALIDATE_BOOLEAN) ? 'required' : ''; } + /** + * Génère un champ input caché à utiliser dans les formulaires générés manuellement. + */ + public function csrf(?string $id): string + { + return csrf_field($id); + } + + /** + * Générer un champ de formulaire pour usurper le verbe HTTP utilisé par les formulaires. + */ + public function method(string $method): string + { + return method_field($method); + } + /** * Ajoute un fichier css de librairie a la vue */ public function addLibCss(string ...$src): self { - $this->_lib_styles = array_merge($this->_lib_styles, $src); + $this->_lib_styles = array_merge($this->_lib_styles, $src); return $this; } @@ -603,7 +619,7 @@ public function addLibCss(string ...$src): self */ public function addCss(string ...$src): self { - $this->_styles = array_merge($this->_styles, $src); + $this->_styles = array_merge($this->_styles, $src); return $this; } @@ -629,7 +645,7 @@ public function stylesBundle(): void */ public function addLibJs(string ...$src): self { - $this->_lib_scripts = array_merge($this->_lib_scripts, $src); + $this->_lib_scripts = array_merge($this->_lib_scripts, $src); return $this; } @@ -639,7 +655,7 @@ public function addLibJs(string ...$src): self */ public function addJs(string ...$src): self { - $this->_scripts = array_merge($this->_scripts, $src); + $this->_scripts = array_merge($this->_scripts, $src); return $this; } @@ -653,7 +669,7 @@ public function scriptsBundle(): void lib_scripts(array_unique($this->_lib_scripts)); } - if (! empty($this->_scripts)) { + if (! empty($this->_scripts)) { scripts(array_unique($this->_scripts)); } From 146142c138ef7d33ea220be1bc7ae942422a6f3c Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 20 Dec 2023 14:43:15 +0100 Subject: [PATCH 101/111] =?UTF-8?q?Refactorisation=20de=20la=20gestion=20d?= =?UTF-8?q?es=20entr=C3=A9es=20et=20am=C3=A9lioration=20de=20la=20lisibili?= =?UTF-8?q?t=C3=A9=20du=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le commit refactorise la gestion des entrées dans le trait `InteractsWithInput` en remplaçant l'utilisation de `$this->data()` par `$this->input()`. Ce changement améliore la lisibilité du code et assure la cohérence des méthodes du trait. Les méthodes `isEmptyString()`, `keys()`, `all()`, `string()`, `boolean()`, `integer()`, `float()`, `datetime()`, `enum()`, et `collect()` sont mises à jour pour utiliser `$théorique()`. sont mises à jour pour utiliser `$this->input()` au lieu de `$this->data()`. --- src/Http/Concerns/InteractsWithInput.php | 22 +- src/Http/ServerRequest.php | 271 ++--------------------- 2 files changed, 35 insertions(+), 258 deletions(-) diff --git a/src/Http/Concerns/InteractsWithInput.php b/src/Http/Concerns/InteractsWithInput.php index dff6733d..7be51c7f 100644 --- a/src/Http/Concerns/InteractsWithInput.php +++ b/src/Http/Concerns/InteractsWithInput.php @@ -224,7 +224,7 @@ public function whenMissing(string $key, callable $callback, ?callable $default */ protected function isEmptyString(string $key): bool { - $value = $this->data($key); + $value = $this->input($key); return ! is_bool($value) && ! is_array($value) && trim((string) $value) === ''; } @@ -234,7 +234,7 @@ protected function isEmptyString(string $key): bool */ public function keys(): array { - return array_keys($this->data()); + return array_keys($this->input()); } /** @@ -244,7 +244,7 @@ public function keys(): array */ public function all($keys = null): array { - $input = array_replace_recursive($this->data(), $this->allFiles()); + $input = array_replace_recursive($this->input(), $this->allFiles()); if (! $keys) { return $input; @@ -262,7 +262,7 @@ public function all($keys = null): array /** * Récupérer un élément d'entrée de la requête. */ - public function data(?string $key = null, mixed $default = null): mixed + public function input(?string $key = null, mixed $default = null): mixed { return Arr::dataGet( $this->data + $this->query, @@ -284,7 +284,7 @@ public function str(string $key, mixed $default = null): Stringable */ public function string(string $key, mixed $default = null): Stringable { - return Text::of($this->data($key, $default)); + return Text::of($this->input($key, $default)); } /** @@ -294,7 +294,7 @@ public function string(string $key, mixed $default = null): Stringable */ public function boolean(?string $key = null, bool $default = false): bool { - return filter_var($this->data($key, $default), FILTER_VALIDATE_BOOLEAN); + return filter_var($this->input($key, $default), FILTER_VALIDATE_BOOLEAN); } /** @@ -302,7 +302,7 @@ public function boolean(?string $key = null, bool $default = false): bool */ public function integer(string $key, int $default = 0): int { - return (int) ($this->data($key, $default)); + return (int) ($this->input($key, $default)); } /** @@ -310,7 +310,7 @@ public function integer(string $key, int $default = 0): int */ public function float(string $key, float $default = 0.0): float { - return (float) ($this->data($key, $default)); + return (float) ($this->input($key, $default)); } /** @@ -323,7 +323,7 @@ public function date(string $key, ?string $format = null, ?string $tz = null): ? } if (null === $format) { - return Date::parse($this->data($key), $tz); + return Date::parse($this->input($key), $tz); } return Date::createFromFormat($format, $this->input($key), $tz); @@ -347,7 +347,7 @@ public function enum(string $key, $enumClass) return null; } - return $enumClass::tryFrom($this->data($key)); + return $enumClass::tryFrom($this->input($key)); } /** @@ -355,7 +355,7 @@ public function enum(string $key, $enumClass) */ public function collect(null|array|string $key = null): Collection { - return collect(is_array($key) ? $this->only($key) : $this->data($key)); + return collect(is_array($key) ? $this->only($key) : $this->input($key)); } /** diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 62da37f1..51571c6c 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -18,7 +18,6 @@ use BlitzPHP\Filesystem\Files\UploadedFile; use BlitzPHP\Session\Cookie\CookieCollection; use BlitzPHP\Session\Store; -use BlitzPHP\Utilities\Helpers; use BlitzPHP\Utilities\Iterable\Arr; use Closure; use GuzzleHttp\Psr7\ServerRequest as Psr7ServerRequest; @@ -155,8 +154,6 @@ class ServerRequest implements ServerRequestInterface /** * Tableau de fichiers. - * - * @var UploadedFile[] */ protected array $uploadedFiles = []; @@ -201,10 +198,10 @@ public function __construct(array $config = []) { $config += [ 'params' => $this->params, - 'query' => $_GET, - 'post' => $_POST, - 'files' => $_FILES, - 'cookies' => $_COOKIE, + 'query' => [], + 'post' => [], + 'files' => [], + 'cookies' => [], 'environment' => [], 'url' => '', 'uri' => null, @@ -240,9 +237,13 @@ protected function _setConfig(array $config): void $uri = $config['uri']; } else { if ($config['url'] !== '') { - $config = $this->processUrlOption($config); - } - $uri = Psr7ServerRequest::getUriFromGlobals(); + $config = $this->processUrlOption($config); + $uri = new Uri(implode('?', [$config['url'], $config['environment']['QUERY_STRING'] ?? ''])); + } else if (isset($config['environment']['REQUEST_URI'])) { + $uri = new Uri($config['environment']['REQUEST_URI']); + } else { + $uri = Psr7ServerRequest::getUriFromGlobals(); + } } if (in_array($uri->getHost(), ['localhost', '127.0.0.1'], true)) { @@ -264,11 +265,18 @@ protected function _setConfig(array $config): void } $this->stream = $stream; - $config['post'] = $this->_processPost($config['post']); - $this->data = $this->_processFiles($config['post'], $config['files']); - $this->query = $config['query']; - $this->params = $config['params']; - $this->session = $config['session']; + $post = $config['post']; + if (!(is_array($post) || is_object($post) || $post === null)) { + throw new InvalidArgumentException(sprintf( + 'La clé `post` doit être un tableau, un objet ou null. On a obtenu `%s` à la place.', + get_debug_type($post) + )); + } + $this->data = $post; + $this->uploadedFiles = $config['files']; + $this->query = $config['query']; + $this->params = $config['params']; + $this->session = $config['session']; } /** @@ -1651,7 +1659,7 @@ protected function validateUploadedFiles(array $uploadedFiles, string $path): vo } if (! $file instanceof UploadedFileInterface) { - throw new InvalidArgumentException("Fichier invalide à '{$path}{$key}'"); + throw new InvalidArgumentException("Fichier invalide à `{$path}{$key}`"); } } } @@ -1831,235 +1839,4 @@ public function getLocale(): string return $locale ?? Services::translator()->getLocale(); } - - /** - * Read data from `php://input`. Useful when interacting with XML or JSON - * request body content. - * - * Getting input with a decoding function: - * - * ``` - * $this->request->input('json_decode'); - * ``` - * - * Getting input using a decoding function, and additional params: - * - * ``` - * $this->request->input('Xml::build', ['return' => 'DOMDocument']); - * ``` - * - * Any additional parameters are applied to the callback in the order they are given. - * - * @param string|null $callback A decoding callback that will convert the string data to another - * representation. Leave empty to access the raw input data. You can also - * supply additional parameters for the decoding callback using var args, see above. - * @param array ...$args The additional arguments - * - * @return string The decoded/processed request data. - */ - public function input($callback = null, ...$args): string - { - $this->stream->rewind(); - $input = $this->stream->getContents(); - if ($callback) { - array_unshift($args, $input); - - return $callback(...$args); - } - - return $input; - } - - /** - * Sets the REQUEST_METHOD environment variable based on the simulated _method - * HTTP override value. The 'ORIGINAL_REQUEST_METHOD' is also preserved, if you - * want the read the non-simulated HTTP method the client used. - * - * @param array $data Array of post data. - * - * @return array - */ - protected function _processPost(array $data) - { - $method = $this->getEnv('REQUEST_METHOD'); - $override = false; - - if ($_POST) { - $data = $_POST; - } elseif ( - in_array($method, ['PUT', 'DELETE', 'PATCH'], true) - && str_starts_with($this->contentType() ?? '', 'application/x-www-form-urlencoded') - ) { - $data = $this->input(); - parse_str($data, $data); - } - if (ini_get('magic_quotes_gpc') === '1') { - $data = Helpers::stripslashesDeep((array) $this->data); - } - - if ($this->hasHeader('X-Http-Method-Override')) { - $data['_method'] = $this->getHeaderLine('X-Http-Method-Override'); - $override = true; - } - $this->_environment['ORIGINAL_REQUEST_METHOD'] = $method; - - if (isset($data['_method'])) { - $this->_environment['REQUEST_METHOD'] = $data['_method']; - unset($data['_method']); - $override = true; - } - - if ($override && ! in_array($this->_environment['REQUEST_METHOD'], ['PUT', 'POST', 'DELETE', 'PATCH'], true)) { - $data = []; - } - - return $data; - } - - /** - * Process the GET parameters and move things into the object. - * - * @param array $query The array to which the parsed keys/values are being added. - * @param string $queryString A query string from the URL if provided - * - * @return array An array containing the parsed query string as keys/values. - */ - protected function _processGet($query, $queryString = '') - { - if (ini_get('magic_quotes_gpc') === '1') { - $q = Helpers::stripslashesDeep($_GET); - } else { - $q = $_GET; - } - $query = array_merge($q, $query); - $url = (string) $this->uri; - - $unsetUrl = '/' . str_replace(['.', ' '], '_', urldecode($url)); - unset($query[$unsetUrl], $query[$this->base . $unsetUrl]); - - if (str_contains($url, '?')) { - [, $querystr] = explode('?', $url); - parse_str($querystr, $queryArgs); - $query += $queryArgs; - } - if (isset($this->params['url'])) { - $query = array_merge($this->params['url'], $query); - } - - return $query; - } - - /** - * Process uploaded files and move things onto the post data. - * - * @param array $post Post data to merge files onto. - * @param array $files Uploaded files to merge in. - * - * @return array merged post + file data. - */ - protected function _processFiles(array $post, array $files): array - { - if (! is_array($files)) { - return $post; - } - - $fileData = []; - - foreach ($files as $key => $value) { - if ($value instanceof UploadedFileInterface) { - $fileData[$key] = $value; - - continue; - } - - if (is_array($value) && isset($value['tmp_name'])) { - $fileData[$key] = $this->_createUploadedFile($value); - - continue; - } - - throw new InvalidArgumentException(sprintf( - 'Invalid value in FILES "%s"', - json_encode($value) - )); - } - - $this->uploadedFiles = $fileData; - - // Make a flat map that can be inserted into $post for BC. - $fileMap = Arr::flatten($fileData); - - foreach ($fileMap as $key => $file) { - $error = $file->getError(); - $tmpName = ''; - - if ($error === UPLOAD_ERR_OK) { - $tmpName = $file->getStream()->getMetadata('uri'); - } - - $post = Arr::insert($post, $key, [ - 'tmp_name' => $tmpName, - 'error' => $error, - 'name' => $file->getClientFilename(), - 'type' => $file->getClientMediaType(), - 'size' => $file->getSize(), - ]); - } - - return $post; - } - - /** - * Create an UploadedFile instance from a $_FILES array. - * - * If the value represents an array of values, this method will - * recursively process the data. - * - * @param array $value $_FILES struct - * - * @return UploadedFile|UploadedFile[] - */ - protected function _createUploadedFile(array $value) - { - if (is_array($value['tmp_name'])) { - return $this->_normalizeNestedFiles($value); - } - - return new UploadedFile( - $value['tmp_name'], - $value['size'], - $value['error'], - $value['name'], - $value['type'] - ); - } - - /** - * Normalize an array of file specifications. - * - * Loops through all nested files and returns a normalized array of - * UploadedFileInterface instances. - * - * @param array $files The file data to normalize & convert. - * - * @return UploadedFile[] - */ - protected function _normalizeNestedFiles(array $files = []): array - { - $normalizedFiles = []; - - foreach (array_keys($files['tmp_name']) as $key) { - $spec = [ - 'tmp_name' => $files['tmp_name'][$key], - 'size' => $files['size'][$key], - 'error' => $files['error'][$key], - 'name' => $files['name'][$key], - 'type' => $files['type'][$key], - ]; - - $normalizedFiles[$key] = $this->_createUploadedFile($spec); - } - - return $normalizedFiles; - } } From 7320b08987f789199fd56fc86084c510aaaf92dc Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 20 Dec 2023 14:47:46 +0100 Subject: [PATCH 102/111] =?UTF-8?q?feat=20:=20Ajouter=20ServerRequestFacto?= =?UTF-8?q?ry.spec.php=20pour=20cr=C3=A9er=20des=20requ=C3=AAtes=20serveur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce commit ajoute le fichier `ServerRequestFactory.spec.php`, qui qui contient des tests pour créer des requêtes serveur en utilisant le `ServerRequestFactory`. Les tests couvrent la création de requêtes avec différentes méthodes HTTP, les cibles des requêtes, les paramètres du serveur, et la lecture des superglobales. De plus, il y a des tests pour analyser les corps encodés en url des formulaires, pour gérer les téléchargements de fichiers et pour surcharger les méthodes de requête. Ces tests assurent le bon fonctionnement de la classe `ServerRequestFactory` lors de la création de requêtes serveur, de la lecture des superglobales, de l'analyse des corps de requêtes et de la gestion des téléchargements de fichiers. Ils améliorent la fiabilité et la robustesse du processus de création de requêtes serveur dans le framework PHP Blitz. --- .../Http/ServerRequestFactory.spec.php | 508 ++++++++++++++++++ .../Http/UploadedFileFactory.spec.php | 207 +++++++ src/Container/Services.php | 3 +- src/Http/ServerRequestFactory.php | 185 +++++++ src/Http/UploadedFileFactory.php | 192 +++++++ 5 files changed, 1094 insertions(+), 1 deletion(-) create mode 100644 spec/system/framework/Http/ServerRequestFactory.spec.php create mode 100644 spec/system/framework/Http/UploadedFileFactory.spec.php create mode 100644 src/Http/ServerRequestFactory.php create mode 100644 src/Http/UploadedFileFactory.php diff --git a/spec/system/framework/Http/ServerRequestFactory.spec.php b/spec/system/framework/Http/ServerRequestFactory.spec.php new file mode 100644 index 00000000..63e75028 --- /dev/null +++ b/spec/system/framework/Http/ServerRequestFactory.spec.php @@ -0,0 +1,508 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Filesystem\Files\UploadedFile; +use BlitzPHP\Http\ServerRequest; +use BlitzPHP\Http\ServerRequestFactory; +use Psr\Http\Message\UploadedFileInterface; + +describe('Http / ServerRequestFactory', function () { + describe('ServerRequestFactoryInterface', function () { + it('Test createServerRequest', function () { + $factory = new ServerRequestFactory(); + $request = $factory->createServerRequest('GET', 'https://blitzphp.com/team', ['foo' => 'bar']); + + expect($request)->toBeAnInstanceOf(ServerRequest::class); + expect($request->getMethod())->toBe('GET'); + expect($request->getRequestTarget())->toBe('/team'); + + $expected = ['foo' => 'bar', 'REQUEST_METHOD' => 'GET']; + expect($request->getServerParams())->toBe($expected); + }); + }); + + describe('Superglobales', function () { + it('Teste que fromGlobals lit les superglobales', function () { + $post = [ + 'title' => 'custom', + ]; + $files = [ + 'image' => [ + 'tmp_name' => __FILE__, + 'error' => 0, + 'name' => 'cats.png', + 'type' => 'image/png', + 'size' => 2112, + ], + ]; + $cookies = ['key' => 'value']; + $query = ['query' => 'string']; + $res = ServerRequestFactory::fromGlobals([], $query, $post, $cookies, $files); + + expect($res->getCookie('key'))->toBe($cookies['key']); + expect($res->getQuery('query'))->toBe($query['query']); + expect($res->getData())->toContainKeys(['title', 'image']); + expect($res->getUploadedFiles())->toHaveLength(1); + + /** @var UploadedFileInterface $expected */ + $expected = $res->getData('image'); + expect($expected)->toBeAnInstanceOf(UploadedFileInterface::class); + expect($expected->getSize())->toBe($files['image']['size']); + expect($expected->getError())->toBe($files['image']['error']); + expect($expected->getClientFilename())->toBe($files['image']['name']); + expect($expected->getClientMediaType())->toBe($files['image']['type']); + }); + + it("Schema", function () { + $server = [ + 'DOCUMENT_ROOT' => '/blitz/repo/webroot', + 'PHP_SELF' => '/index.php', + 'REQUEST_URI' => '/posts/add', + 'HTTP_X_FORWARDED_PROTO' => 'https', + ]; + $request = ServerRequestFactory::fromGlobals($server); + + expect($request->scheme())->toBe('http'); + expect($request->getUri()->getScheme())->toBe('http'); + + $request->setTrustedProxies([]); + // Oui, même le fait de définir une liste vide de proxies fait l'affaire. + expect($request->scheme())->toBe('https'); + expect($request->getUri()->getScheme())->toBe('https'); + }); + }); + + describe('Parsing', function () { + it('test Form Url Encoded Body Parsing', function () { + $data = [ + 'Article' => ['title'], + ]; + $request = ServerRequestFactory::fromGlobals([ + 'REQUEST_METHOD' => 'PUT', + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=UTF-8', + 'BLITZPHP_INPUT' => 'Article[]=title', + ]); + expect($request->getData())->toBe($data); + + $data = ['one' => 1, 'two' => 'three']; + $request = ServerRequestFactory::fromGlobals([ + 'REQUEST_METHOD' => 'PUT', + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=UTF-8', + 'BLITZPHP_INPUT' => 'one=1&two=three', + ]); + expect($request->getData())->toEqual($data); + + $request = ServerRequestFactory::fromGlobals([ + 'REQUEST_METHOD' => 'DELETE', + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=UTF-8', + 'BLITZPHP_INPUT' => 'Article[title]=Testing&action=update', + ]); + $expected = [ + 'Article' => ['title' => 'Testing'], + 'action' => 'update', + ]; + expect($request->getData())->toBe($expected); + + $data = [ + 'Article' => ['title'], + 'Tag' => ['Tag' => [1, 2]], + ]; + $request = ServerRequestFactory::fromGlobals([ + 'REQUEST_METHOD' => 'PATCH', + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=UTF-8', + 'BLITZPHP_INPUT' => 'Article[]=title&Tag[Tag][]=1&Tag[Tag][]=2', + ]); + expect($request->getData())->toEqual($data); + }); + + it('methode reecrite', function () { + $post = ['_method' => 'POST']; + $request = ServerRequestFactory::fromGlobals([], [], $post); + expect($request->getEnv('REQUEST_METHOD'))->toBe('POST'); + + $post = ['_method' => 'DELETE']; + $request = ServerRequestFactory::fromGlobals([], [], $post); + expect($request->getEnv('REQUEST_METHOD'))->toBe('DELETE'); + + $request = ServerRequestFactory::fromGlobals(['HTTP_X_HTTP_METHOD_OVERRIDE' => 'PUT']); + expect($request->getEnv('REQUEST_METHOD'))->toBe('PUT'); + + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_METHOD' => 'POST'], + [], + ['_method' => 'PUT'] + ); + expect($request->getEnv('REQUEST_METHOD'))->toBe('PUT'); + expect($request->getEnv('ORIGINAL_REQUEST_METHOD'))->toBe('POST'); + }); + + it('Recuperation des parametres serveur', function () { + $vars = [ + 'REQUEST_METHOD' => 'PUT', + 'HTTPS' => 'on', + ]; + + $request = ServerRequestFactory::fromGlobals($vars); + $expected = $vars + [ + 'CONTENT_TYPE' => null, + 'HTTP_CONTENT_TYPE' => null, + 'HTTP_X_HTTP_METHOD_OVERRIDE' => null, + 'ORIGINAL_REQUEST_METHOD' => 'PUT', + 'HTTP_HOST' => 'example.com', + ]; + + expect($request->getServerParams())->toBe($expected); + }); + + it('Surcharge de la méthode Corps vide analysé.', function () { + $body = ['_method' => 'GET', 'foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_METHOD' => 'POST'], + [], + $body + ); + expect($request->getParsedBody())->toBeEmpty(); + + $request = ServerRequestFactory::fromGlobals( + [ + 'REQUEST_METHOD' => 'POST', + 'HTTP_X_HTTP_METHOD_OVERRIDE' => 'GET', + ], + [], + ['foo' => 'bar'] + ); + expect($request->getParsedBody())->toBeEmpty(); + }); + }); + + describe('Fichiers', function () { + it("Teste le comportement par défaut de la fusion des fichiers téléchargés.", function () { + $files = [ + 'file' => [ + 'name' => 'file.txt', + 'type' => 'text/plain', + 'tmp_name' => __FILE__, + 'error' => 0, + 'size' => 1234, + ], + ]; + $request = ServerRequestFactory::fromGlobals(null, null, null, null, $files); + + /** @var UploadedFile $expected */ + $expected = $request->getData('file'); + + expect($expected->getSize())->toBe($files['file']['size']); + expect($expected->getError())->toBe($files['file']['error']); + expect($expected->getClientFilename())->toBe($files['file']['name']); + expect($expected->getClientMediaType())->toBe($files['file']['type']); + }); + + it("Test de traitement des fichiers avec des noms de champs `file`.", function () { + $files = [ + 'image_main' => [ + 'name' => ['file' => 'born on.txt'], + 'type' => ['file' => 'text/plain'], + 'tmp_name' => ['file' => __FILE__], + 'error' => ['file' => 0], + 'size' => ['file' => 17178], + ], + 0 => [ + 'name' => ['image' => 'scratch.text'], + 'type' => ['image' => 'text/plain'], + 'tmp_name' => ['image' => __FILE__], + 'error' => ['image' => 0], + 'size' => ['image' => 1490], + ], + 'pictures' => [ + 'name' => [ + 0 => ['file' => 'a-file.png'], + 1 => ['file' => 'a-moose.png'], + ], + 'type' => [ + 0 => ['file' => 'image/png'], + 1 => ['file' => 'image/jpg'], + ], + 'tmp_name' => [ + 0 => ['file' => __FILE__], + 1 => ['file' => __FILE__], + ], + 'error' => [ + 0 => ['file' => 0], + 1 => ['file' => 0], + ], + 'size' => [ + 0 => ['file' => 17188], + 1 => ['file' => 2010], + ], + ], + ]; + + $post = [ + 'pictures' => [ + 0 => ['name' => 'A cat'], + 1 => ['name' => 'A moose'], + ], + 0 => [ + 'name' => 'A dog', + ], + ]; + + $request = ServerRequestFactory::fromGlobals(null, null, $post, null, $files); + $expected = [ + 'image_main' => [ + 'file' => new UploadedFile( + __FILE__, + 17178, + 0, + 'born on.txt', + 'text/plain' + ), + ], + 'pictures' => [ + 0 => [ + 'name' => 'A cat', + 'file' => new UploadedFile( + __FILE__, + 17188, + 0, + 'a-file.png', + 'image/png' + ), + ], + 1 => [ + 'name' => 'A moose', + 'file' => new UploadedFile( + __FILE__, + 2010, + 0, + 'a-moose.png', + 'image/jpg' + ), + ], + ], + 0 => [ + 'name' => 'A dog', + 'image' => new UploadedFile( + __FILE__, + 1490, + 0, + 'scratch.text', + 'text/plain' + ), + ], + ]; + + expect($request->getData())->toEqual($expected); + + $uploads = $request->getUploadedFiles(); + expect($uploads)->toHaveLength(3); + expect($uploads)->toContainKey(0); + expect($uploads[0]['image']->getClientFilename())->toBe('scratch.text'); + + expect($uploads)->toContainKey('pictures'); + expect($uploads['pictures'][0]['file']->getClientFilename())->toBe('a-file.png'); + expect($uploads['pictures'][1]['file']->getClientFilename())->toBe('a-moose.png'); + + expect($uploads)->toContainKey('image_main'); + expect($uploads['image_main']['file']->getClientFilename())->toBe('born on.txt'); + }); + + it("Test de traitement d'un fichier d'entrée ne contenant pas de 's.", function () { + $files = [ + 'birth_cert' => [ + 'name' => 'born on.txt', + 'type' => 'application/octet-stream', + 'tmp_name' => __FILE__, + 'error' => 0, + 'size' => 123, + ], + ]; + + $request = ServerRequestFactory::fromGlobals([], [], [], [], $files); + expect($request->getData()['birth_cert'])->toBeAnInstanceOf(UploadedFileInterface::class); + + $uploads = $request->getUploadedFiles(); + expect($uploads)->toHaveLength(1); + expect($uploads)->toContainKey('birth_cert'); + expect($uploads['birth_cert']->getClientFilename())->toBe('born on.txt'); + expect($uploads['birth_cert']->getError())->toBe(0); + expect($uploads['birth_cert']->getClientMediaType())->toBe('application/octet-stream'); + expect($uploads['birth_cert']->getSize())->toBe(123); + }); + + it("Tester que les fichiers du 0e index fonctionnent.", function () { + $files = [ + 0 => [ + 'name' => 'blitz_sqlserver_patch.patch', + 'type' => 'text/plain', + 'tmp_name' => __FILE__, + 'error' => 0, + 'size' => 6271, + ], + ]; + + $request = ServerRequestFactory::fromGlobals([], [], [], [], $files); + expect($request->getData()[0])->toBeAnInstanceOf(UploadedFileInterface::class); + + $uploads = $request->getUploadedFiles(); + expect($uploads)->toHaveLength(1); + expect($uploads[0]->getClientFilename())->toBe($files[0]['name']); + }); + + it("Teste que les téléchargements de fichiers sont fusionnés avec les données du message sous forme d'objets et non de tableaux.", function () { + $files = [ + 'flat' => [ + 'name' => 'flat.txt', + 'type' => 'text/plain', + 'tmp_name' => __FILE__, + 'error' => 0, + 'size' => 1, + ], + 'nested' => [ + 'name' => ['file' => 'nested.txt'], + 'type' => ['file' => 'text/plain'], + 'tmp_name' => ['file' => __FILE__], + 'error' => ['file' => 0], + 'size' => ['file' => 12], + ], + 0 => [ + 'name' => 'numeric.txt', + 'type' => 'text/plain', + 'tmp_name' => __FILE__, + 'error' => 0, + 'size' => 123, + ], + 1 => [ + 'name' => ['file' => 'numeric-nested.txt'], + 'type' => ['file' => 'text/plain'], + 'tmp_name' => ['file' => __FILE__], + 'error' => ['file' => 0], + 'size' => ['file' => 1234], + ], + 'deep' => [ + 'name' => [ + 0 => ['file' => 'deep-1.txt'], + 1 => ['file' => 'deep-2.txt'], + ], + 'type' => [ + 0 => ['file' => 'text/plain'], + 1 => ['file' => 'text/plain'], + ], + 'tmp_name' => [ + 0 => ['file' => __FILE__], + 1 => ['file' => __FILE__], + ], + 'error' => [ + 0 => ['file' => 0], + 1 => ['file' => 0], + ], + 'size' => [ + 0 => ['file' => 12345], + 1 => ['file' => 123456], + ], + ], + ]; + + $post = [ + 'flat' => ['existing'], + 'nested' => [ + 'name' => 'nested', + 'file' => ['existing'], + ], + 'deep' => [ + 0 => [ + 'name' => 'deep 1', + 'file' => ['existing'], + ], + 1 => [ + 'name' => 'deep 2', + ], + ], + 1 => [ + 'name' => 'numeric nested', + ], + ]; + + $expected = [ + 'flat' => new UploadedFile( + __FILE__, + 1, + 0, + 'flat.txt', + 'text/plain' + ), + 'nested' => [ + 'name' => 'nested', + 'file' => new UploadedFile( + __FILE__, + 12, + 0, + 'nested.txt', + 'text/plain' + ), + ], + 'deep' => [ + 0 => [ + 'name' => 'deep 1', + 'file' => new UploadedFile( + __FILE__, + 12345, + 0, + 'deep-1.txt', + 'text/plain' + ), + ], + 1 => [ + 'name' => 'deep 2', + 'file' => new UploadedFile( + __FILE__, + 123456, + 0, + 'deep-2.txt', + 'text/plain' + ), + ], + ], + 0 => new UploadedFile( + __FILE__, + 123, + 0, + 'numeric.txt', + 'text/plain' + ), + 1 => [ + 'name' => 'numeric nested', + 'file' => new UploadedFile( + __FILE__, + 1234, + 0, + 'numeric-nested.txt', + 'text/plain' + ), + ], + ]; + + $request = ServerRequestFactory::fromGlobals([], [], $post, [], $files); + + expect($request->getData())->toEqual($expected); + }); + + it("Test de passage d'une structure de liste de fichiers invalide.", function () { + expect(fn() => ServerRequestFactory::fromGlobals([], [], [], [], [ + [ + 'invalid' => [ + 'data', + ], + ], + ]))->toThrow(new InvalidArgumentException('Valeur non valide dans la spécification des fichiers')); + }); + }); +}); diff --git a/spec/system/framework/Http/UploadedFileFactory.spec.php b/spec/system/framework/Http/UploadedFileFactory.spec.php new file mode 100644 index 00000000..3ebe2d01 --- /dev/null +++ b/spec/system/framework/Http/UploadedFileFactory.spec.php @@ -0,0 +1,207 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Http\UploadedFileFactory; +use GuzzleHttp\Psr7\Stream; +use GuzzleHttp\Psr7\Utils; +use Psr\Http\Message\UploadedFileInterface; + +describe('Http / UploadedFileFactory', function () { + beforeAll(function () { + $this->filename = TEMP_PATH . 'uploadedfile-factory-file-test.txt'; + $this->factory = new UploadedFileFactory(); + }); + + describe('UploadedFileFactoryInterface', function () { + it('create stream resource', function () { + file_put_contents($this->filename, 'it works'); + $stream = new Stream(Utils::tryFopen($this->filename, 'r')); + + $uploadedFile = $this->factory->createUploadedFile($stream, null, UPLOAD_ERR_OK, 'my-name'); + + expect($uploadedFile->getClientFilename())->toBe('my-name'); + expect($uploadedFile->getStream())->toBe($stream); + + @unlink($this->filename); + }); + }); + + describe('makeUploadedFile', function () { + it('makeUploadedFile', function () { + $files = [ + 'name' => 'file.txt', + 'type' => 'text/plain', + 'tmp_name' => __FILE__, + 'error' => 0, + 'size' => 1234, + ]; + + /** @var UploadedFile $expected */ + $expected = UploadedFileFactory::makeUploadedFile($files); + + expect($expected->getSize())->toBe($files['size']); + expect($expected->getError())->toBe($files['error']); + expect($expected->getClientFilename())->toBe($files['name']); + expect($expected->getClientMediaType())->toBe($files['type']); + }); + + it('makeUploadedFile genere une erreur', function () { + $files = [ + 'name' => 'file.txt', + 'type' => 'text/plain', + 'tmp_name' => __FILE__, + 'error' => 0, + // 'size' => 1234, abscense d'un element + ]; + + expect(fn() => UploadedFileFactory::makeUploadedFile($files)) + ->toThrow(new InvalidArgumentException()); + }); + }); + + describe('normalizeUploadedFile', function () { + it("Création d'un fichier téléchargé à partir de la spécification d'un fichier plat", function () { + $files = [ + 'avatar' => [ + 'tmp_name' => 'phpUxcOty', + 'name' => 'my-avatar.png', + 'size' => 90996, + 'type' => 'image/png', + 'error' => 0, + ], + ]; + + $normalised = UploadedFileFactory::normalizeUploadedFiles($files); + + expect($normalised)->toHaveLength(1); + expect($normalised['avatar'])->toBeAnInstanceOf(UploadedFileInterface::class); + expect($normalised['avatar']->getClientFilename())->toBe('my-avatar.png'); + }); + + it("Traverse les spécifications de fichiers imbriqués pour extraire le fichier téléchargé", function () { + $files = [ + 'my-form' => [ + 'details' => [ + 'avatar' => [ + 'tmp_name' => 'phpUxcOty', + 'name' => 'my-avatar.png', + 'size' => 90996, + 'type' => 'image/png', + 'error' => 0, + ], + ], + ], + ]; + + $normalised = UploadedFileFactory::normalizeUploadedFiles($files); + + expect($normalised)->toHaveLength(1); + expect($normalised['my-form']['details']['avatar']->getClientFilename())->toBe('my-avatar.png'); + }); + + it("Traverse les spécifications de fichiers imbriqués pour extraire le fichier téléchargé", function () { + $files = [ + 'my-form' => [ + 'details' => [ + 'avatars' => [ + 'tmp_name' => [ + 0 => 'abc123', + 1 => 'duck123', + 2 => 'goose123', + ], + 'name' => [ + 0 => 'file1.txt', + 1 => 'file2.txt', + 2 => 'file3.txt', + ], + 'size' => [ + 0 => 100, + 1 => 240, + 2 => 750, + ], + 'type' => [ + 0 => 'plain/txt', + 1 => 'image/jpg', + 2 => 'image/png', + ], + 'error' => [ + 0 => 0, + 1 => 0, + 2 => 0, + ], + ], + ], + ], + ]; + + $normalised = UploadedFileFactory::normalizeUploadedFiles($files); + + expect($normalised['my-form']['details']['avatars'])->toHaveLength(3); + expect($normalised['my-form']['details']['avatars'][0]->getClientFilename())->toBe('file1.txt'); + expect($normalised['my-form']['details']['avatars'][1]->getClientFilename())->toBe('file2.txt'); + expect($normalised['my-form']['details']['avatars'][2]->getClientFilename())->toBe('file3.txt'); + }); + + it("Traverse les spécifications de fichiers imbriqués pour extraire le fichier téléchargé", function () { + $files = [ + 'slide-shows' => [ + 'tmp_name' => [ + // Note: Nesting *under* tmp_name/etc + 0 => [ + 'slides' => [ + 0 => '/tmp/phpYzdqkD', + 1 => '/tmp/phpYzdfgh', + ], + ], + ], + 'error' => [ + 0 => [ + 'slides' => [ + 0 => 0, + 1 => 0, + ], + ], + ], + 'name' => [ + 0 => [ + 'slides' => [ + 0 => 'foo.txt', + 1 => 'bar.txt', + ], + ], + ], + 'size' => [ + 0 => [ + 'slides' => [ + 0 => 123, + 1 => 200, + ], + ], + ], + 'type' => [ + 0 => [ + 'slides' => [ + 0 => 'text/plain', + 1 => 'text/plain', + ], + ], + ], + ], + ]; + + $normalised = UploadedFileFactory::normalizeUploadedFiles($files); + + expect($normalised['slide-shows'][0]['slides'])->toHaveLength(2); + expect($normalised['slide-shows'][0]['slides'][0]->getClientFilename())->toBe('foo.txt'); + expect($normalised['slide-shows'][0]['slides'][1]->getClientFilename())->toBe('bar.txt'); + }); + }); +}); diff --git a/src/Container/Services.php b/src/Container/Services.php index 03c51717..5d6a8a53 100644 --- a/src/Container/Services.php +++ b/src/Container/Services.php @@ -32,6 +32,7 @@ use BlitzPHP\Http\Response; use BlitzPHP\Http\ResponseEmitter; use BlitzPHP\Http\ServerRequest; +use BlitzPHP\Http\ServerRequestFactory; use BlitzPHP\Http\Uri; use BlitzPHP\Http\UrlGenerator; use BlitzPHP\Mail\Mail; @@ -330,7 +331,7 @@ public static function request(bool $shared = true): Request return static::$instances[Request::class]; } - return static::$instances[Request::class] = new Request(); + return static::$instances[Request::class] = ServerRequestFactory::fromGlobals(); } /** diff --git a/src/Http/ServerRequestFactory.php b/src/Http/ServerRequestFactory.php new file mode 100644 index 00000000..1d816daa --- /dev/null +++ b/src/Http/ServerRequestFactory.php @@ -0,0 +1,185 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Http; + +use BlitzPHP\Utilities\Iterable\Arr; +use Psr\Http\Message\ServerRequestFactoryInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Usine permettant de créer des instances de ServerRequest. + * + * Ceci ajoute un comportement spécifique à BlitzPHP pour remplir les attributs basePath et webroot. + * En outre, le chemin de l'Uri est corrigé pour ne contenir que le chemin "virtuel" pour la requête. + * + * @credit CakePHP Cake\Http\ServerRequestFactory + * @credit Laminas\Diactoros + */ +class ServerRequestFactory implements ServerRequestFactoryInterface +{ + /** + * Créer une requête à partir des valeurs superglobales fournies. + * + * Si un argument n'est pas fourni, la valeur superglobale correspondante sera utilisée. + * + * @param array|null $server superglobale $_SERVER + * @param array|null $query superglobale $_GET + * @param array|null $parsedBody superglobale $_POST + * @param array|null $cookies superglobale $_COOKIE + * @param array|null $files superglobale $_FILES + * + * @throws \InvalidArgumentException pour les valeurs de fichier non valides + */ + public static function fromGlobals( + ?array $server = null, + ?array $query = null, + ?array $parsedBody = null, + ?array $cookies = null, + ?array $files = null + ): Request { + $server = self::normalizeServer($server ?? $_SERVER); + + $request = new Request([ + 'environment' => $server, + 'cookies' => $cookies ?? $_COOKIE, + 'query' => $query ?? $_GET, + 'session' => service('session'), + 'input' => $server['BLITZPHP_INPUT'] ?? null, + ]); + + $request = static::processBodyAndRequestMethod($parsedBody ?? $_POST, $request); + // Ceci est nécessaire car `ServerRequest::scheme()` ignore la valeur de `HTTP_X_FORWARDED_PROTO` + // à moins que `trustProxy` soit activé, alors que l'instance `Uri` initialement créée prend + // toujours en compte les valeurs de HTTP_X_FORWARDED_PROTO`. + $uri = $request->getUri()->withScheme($request->scheme()); + $request = $request->withUri($uri, true); + + return static::processFiles($files ?? $_FILES, $request); + } + + /** + * Définit la variable d'environnement REQUEST_METHOD en fonction de la valeur HTTP simulée _method. + * La valeur 'ORIGINAL_REQUEST_METHOD' est également préservée, si vous souhaitez lire la méthode HTTP non simulée utilisée par le client. + * + * Le corps de la requête de type "application/x-www-form-urlencoded" est analysé dans un tableau pour les requêtes PUT/PATCH/DELETE. + * + * @param array $parsedBody Corps analysé. + */ + protected static function processBodyAndRequestMethod(array $parsedBody, Request $request): Request + { + $method = $request->getMethod(); + $override = false; + + if (in_array($method, ['PUT', 'DELETE', 'PATCH'], true) && str_starts_with((string)$request->contentType(), 'application/x-www-form-urlencoded')) { + $data = (string)$request->getBody(); + parse_str($data, $parsedBody); + } + if ($request->hasHeader('X-Http-Method-Override')) { + $parsedBody['_method'] = $request->getHeaderLine('X-Http-Method-Override'); + $override = true; + } + + $request = $request->withEnv('ORIGINAL_REQUEST_METHOD', $method); + if (isset($parsedBody['_method'])) { + $request = $request->withEnv('REQUEST_METHOD', $parsedBody['_method']); + unset($parsedBody['_method']); + $override = true; + } + + if ($override && !in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH'], true)) { + $parsedBody = []; + } + + return $request->withParsedBody($parsedBody); + } + + /** + * Traiter les fichiers téléchargés et déplacer les éléments dans le corps analysé. + * + * @param array $files Tableau de fichiers pour la normalisation et la fusion dans le corps analysé. + */ + protected static function processFiles(array $files, Request $request): Request + { + $files = UploadedFileFactory::normalizeUploadedFiles($files); + $request = $request->withUploadedFiles($files); + + $parsedBody = $request->getParsedBody(); + if (!is_array($parsedBody)) { + return $request; + } + + $parsedBody = Arr::merge($parsedBody, $files); + + return $request->withParsedBody($parsedBody); + } + + /** + * Créer une nouvelle requete de serveur. + * + * Notez que les paramètres du serveur sont pris tels quels - aucune analyse/traitement + * des valeurs données n'est effectué, et, en particulier, aucune tentative n'est faite pour + * déterminer la méthode HTTP ou l'URI, qui doivent être fournis explicitement. + * + * @param string $method La méthode HTTP associée à la requete. + * @param \Psr\Http\Message\UriInterface|string $uri L'URI associé à la requete. + * Si la valeur est une chaîne, la fabrique DOIT créer une instance d'UriInterface basée sur celle-ci. + * @param array $serverParams Tableau de paramètres SAPI permettant d'alimenter l'instance de requete générée. + */ + public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface + { + $serverParams['REQUEST_METHOD'] = $method; + $options = ['environment' => $serverParams]; + + if (is_string($uri)) { + $uri = new Uri($uri); + } + $options['uri'] = $uri; + + return new Request($options); + } + + /** + * Marshaller le tableau $_SERVER + * + * Prétraite et renvoie la superglobale $_SERVER. + * En particulier, il tente de détecter l'en-tête Authorization, qui n'est souvent pas agrégé correctement sous diverses combinaisons SAPI/httpd. + * + * @param null|callable $apacheRequestHeaderCallback Callback qui peut être utilisé pour récupérer les en-têtes de requête Apache. + * La valeur par défaut est `apache_request_headers` sous Apache mod_php. + * @see https://github.com/laminas/laminas-diactoros/blob/3.4.x/src/functions/normalize_server.php + * @return array Soit $server mot pour mot, soit avec un en-tête HTTP_AUTHORIZATION ajouté. + */ + private static function normalizeServer(array $server, ?callable $apacheRequestHeaderCallback = null): array + { + if (null === $apacheRequestHeaderCallback && is_callable('apache_request_headers')) { + $apacheRequestHeaderCallback = 'apache_request_headers'; + } + + // Si la valeur HTTP_AUTHORIZATION est déjà définie, ou si le callback n'est pas appelable, nous renvoyons les parameters server sans changements + if (isset($server['HTTP_AUTHORIZATION']) || ! is_callable($apacheRequestHeaderCallback)) { + return $server; + } + + $apacheRequestHeaders = $apacheRequestHeaderCallback(); + if (isset($apacheRequestHeaders['Authorization'])) { + $server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['Authorization']; + return $server; + } + + if (isset($apacheRequestHeaders['authorization'])) { + $server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['authorization']; + return $server; + } + + return $server; + } +} diff --git a/src/Http/UploadedFileFactory.php b/src/Http/UploadedFileFactory.php new file mode 100644 index 00000000..ab28a06a --- /dev/null +++ b/src/Http/UploadedFileFactory.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Http; + +use BlitzPHP\Filesystem\Files\UploadedFile; +use InvalidArgumentException; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UploadedFileFactoryInterface; +use Psr\Http\Message\UploadedFileInterface; + +/** + * Classe d'usine pour la création d'instances de fichiers téléchargés. + * + * @credit Laminas\Diactoros + */ +class UploadedFileFactory implements UploadedFileFactoryInterface +{ + /** + * Créer un nouveau fichier téléchargé. + * + * Si une taille n'est pas fournie, elle sera déterminée en vérifiant la taille du flux. + * + * @link http://php.net/manual/features.file-upload.post-method.php + * @link http://php.net/manual/features.file-upload.errors.php + * @param \Psr\Http\Message\StreamInterface $stream Le flux sous-jacent représentant le contenu du fichier téléchargé. + * @param int|null $size La taille du fichier en octets. + * @param int $error L'erreur de téléchargement du fichier PHP. + * @param string|null $clientFilename Le nom du fichier tel qu'il est fourni par le client, le cas échéant. + * @param string|null $clientMediaType Le type de média tel qu'il est fourni par le client, le cas échéant. + * @throws \InvalidArgumentException Si la ressource du fichier n'est pas lisible. + */ + public function createUploadedFile( + StreamInterface $stream, + ?int $size = null, + int $error = UPLOAD_ERR_OK, + ?string $clientFilename = null, + ?string $clientMediaType = null + ): UploadedFileInterface { + if ($size === null) { + $size = $stream->getSize() ?? 0; + } + + return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); + } + + /** + * Créer une instance de fichier téléchargé à partir d'un tableau de valeurs. + * + * @param array $spec Une seule entrée $_FILES. + * @throws InvalidArgumentException Si une ou plusieurs des clés tmp_name, size ou error sont manquantes dans $spec. + */ + public static function makeUploadedFile(array $spec): UploadedFile + { + if (! isset($spec['tmp_name']) || ! isset($spec['size']) || ! isset($spec['error'])) { + throw new InvalidArgumentException(sprintf( + '$spec fourni à %s DOIT contenir chacune des clés "tmp_name", "size", et "error" ; une ou plusieurs étaient manquantes', + __FUNCTION__ + )); + } + + return new UploadedFile( + $spec['tmp_name'], + (int) $spec['size'], + $spec['error'], + $spec['name'] ?? null, + $spec['type'] ?? null + ); + } + + /** + * Normaliser les fichiers téléchargés + * + * Transforme chaque valeur en une instance UploadedFile, et s'assure que les tableaux imbriqués sont normalisés. + * + * @see https://github.com/laminas/laminas-diactoros/blob/3.4.x/src/functions/normalize_uploaded_files.php + * @return UploadedFileInterface[] + * @throws InvalidArgumentException Pour les valeurs non reconnues. + */ + public static function normalizeUploadedFiles(array $files): array + { + /** + * Traverse une arborescence imbriquée de spécifications de fichiers téléchargés. + * + * @param string[]|array[] $tmpNameTree + * @param int[]|array[] $sizeTree + * @param int[]|array[] $errorTree + * @param string[]|array[]|null $nameTree + * @param string[]|array[]|null $typeTree + * @return UploadedFile[]|array[] + */ + $recursiveNormalize = static function ( + array $tmpNameTree, + array $sizeTree, + array $errorTree, + ?array $nameTree = null, + ?array $typeTree = null + ) use (&$recursiveNormalize): array { + $normalized = []; + foreach ($tmpNameTree as $key => $value) { + if (is_array($value)) { + // Traverse + $normalized[$key] = $recursiveNormalize( + $tmpNameTree[$key], + $sizeTree[$key], + $errorTree[$key], + $nameTree[$key] ?? null, + $typeTree[$key] ?? null + ); + continue; + } + + $normalized[$key] = static::makeUploadedFile([ + 'tmp_name' => $tmpNameTree[$key], + 'size' => $sizeTree[$key], + 'error' => $errorTree[$key], + 'name' => $nameTree[$key] ?? null, + 'type' => $typeTree[$key] ?? null, + ]); + } + + return $normalized; + }; + + /** + * Normaliser un tableau de spécifications de fichiers. + * + * Boucle sur tous les fichiers imbriqués (déterminés par la réception d'un tableau à la clé `tmp_name` d'une spécification `$_FILES`) et renvoie un tableau normalisé d'instances UploadedFile. + * + * Cette fonction normalise un tableau `$_FILES` représentant un ensemble imbriqué de fichiers téléchargés tels que produits par les SAPI php-fpm, CGI SAPI, ou mod_php SAPI. + * + * @return UploadedFile[] + */ + $normalizeUploadedFileSpecification = static function (array $files = []) use (&$recursiveNormalize): array { + if ( + ! isset($files['tmp_name']) || ! is_array($files['tmp_name']) + || ! isset($files['size']) || ! is_array($files['size']) + || ! isset($files['error']) || ! is_array($files['error']) + ) { + throw new InvalidArgumentException(sprintf( + 'Les fichiers fournis à %s DOIVENT contenir chacune des clés "tmp_name", "size" et "error", + chacune étant représentée sous la forme d\'un tableau ; + une ou plusieurs valeurs manquaient ou n\'étaient pas des tableaux.', + __FUNCTION__ + )); + } + + return $recursiveNormalize( + $files['tmp_name'], + $files['size'], + $files['error'], + $files['name'] ?? null, + $files['type'] ?? null + ); + }; + + $normalized = []; + foreach ($files as $key => $value) { + if ($value instanceof UploadedFileInterface) { + $normalized[$key] = $value; + continue; + } + + if (is_array($value) && isset($value['tmp_name']) && is_array($value['tmp_name'])) { + $normalized[$key] = $normalizeUploadedFileSpecification($value); + continue; + } + + if (is_array($value) && isset($value['tmp_name'])) { + $normalized[$key] = self::makeUploadedFile($value); + continue; + } + + if (is_array($value)) { + $normalized[$key] = self::normalizeUploadedFiles($value); + continue; + } + + throw new InvalidArgumentException('Valeur non valide dans la spécification des fichiers'); + } + + return $normalized; + } +} From d90014611e3bbf06d04e1f3556a0c77fa1c50a64 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 20 Dec 2023 14:49:28 +0100 Subject: [PATCH 103/111] test(Http): ServerRequest --- composer.json | 5 +- phpstan-baseline.php | 5 - spec/system/framework/Http/Response.spec.php | 390 +++++++++++++++++- .../framework/Http/ServerRequest.spec.php | 246 +++++++++++ src/Http/Response.php | 2 +- 5 files changed, 640 insertions(+), 8 deletions(-) create mode 100644 spec/system/framework/Http/ServerRequest.spec.php diff --git a/composer.json b/composer.json index dbca357e..47671da6 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,10 @@ "sa": "@analyze", "style": "@cs:fix", "test": "kahlan", - "test:cov": "kahlan --coverage=3 --reporter=verbose --clover=scrutinizer.xml" + "test:cov": [ + "Composer\\Config::disableProcessTimeout", + "kahlan --coverage=3 --reporter=verbose --clover=scrutinizer.xml" + ] }, "scripts-descriptions": { "analyze": "Lance l'analyse statique du code du framework", diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 50f7c86b..42ea6868 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -126,11 +126,6 @@ 'count' => 1, 'path' => __DIR__ . '/src/Http/Response.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property BlitzPHP\\\\Http\\\\ServerRequest\\:\\:\\$uploadedFiles \\(array\\\\) does not accept array\\\\|Psr\\\\Http\\\\Message\\\\UploadedFileInterface\\>\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/src/Http/ServerRequest.php', -]; $ignoreErrors[] = [ 'message' => '#^Access to constant DEBUG_SERVER on an unknown class PHPMailer\\\\PHPMailer\\\\SMTP\\.$#', 'count' => 1, diff --git a/spec/system/framework/Http/Response.spec.php b/spec/system/framework/Http/Response.spec.php index 9fc12c05..fad10656 100644 --- a/spec/system/framework/Http/Response.spec.php +++ b/spec/system/framework/Http/Response.spec.php @@ -17,7 +17,109 @@ use BlitzPHP\Session\Cookie\Cookie; use BlitzPHP\Spec\ReflectionHelper; -describe('Response', function () { +describe('Http / Response', function () { + describe('Constructeur', function () { + it('Le constructeur fonctionne', function () { + $response = new Response(); + expect((string) $response->getBody())->toBe(''); + expect($response->getCharset())->toBe('UTF-8'); + expect($response->getType())->toBe('text/html'); + expect($response->getHeaderLine('Content-Type'))->toBe('text/html; charset=UTF-8'); + expect($response->getStatusCode())->toBe(200); + + $options = [ + 'body' => 'This is the body', + 'charset' => 'my-custom-charset', + 'type' => 'mp3', + 'status' => 203, + ]; + $response = new Response($options); + expect((string) $response->getBody())->toBe($options['body']); + expect($response->getCharset())->toBe($options['charset']); + expect($response->getType())->toBe('audio/mpeg'); + expect($response->getHeaderLine('Content-Type'))->toBe('audio/mpeg'); + expect($response->getStatusCode())->toBe($options['status']); + }); + }); + + describe('Types', function() { + it('GetType', function() { + $response = new Response(); + + expect($response->getType())->toBe('text/html'); + expect($response->withType('pdf')->getType())->toBe('application/pdf'); + expect($response->withType('custom/stuff')->getType())->toBe('custom/stuff'); + expect($response->withType('json')->getType())->toBe('application/json'); + }); + + it('SetTypeMap', function() { + $response = new Response(); + $response->setTypeMap('ical', 'text/calendar'); + expect($response->withType('ical')->getType())->toBe('text/calendar'); + + $response = new Response(); + $response->setTypeMap('ical', ['text/calendar']); + expect($response->withType('ical')->getType())->toBe('text/calendar'); + }); + + it('WithTypeAlias', function() { + $response = new Response(); + + // Le type de contenu par défaut doit correspondre + expect($response->getHeaderLine('Content-Type'))->toBe('text/html; charset=UTF-8'); + + $new = $response->withType('pdf'); + // Doit être une nouvelle instance + expect($new)->not->toBe($response); + + // L'objet original ne doit pas être modifié + expect($response->getHeaderLine('Content-Type'))->toBe('text/html; charset=UTF-8'); + + expect($new->getHeaderLine('Content-Type'))->toBe('application/pdf'); + + $json = $new->withType('json'); + expect($json->getHeaderLine('Content-Type'))->toBe('application/json'); + expect($json->getType())->toBe('application/json'); + }); + + it('WithTypeFull', function() { + $response = new Response(); + + // Ne doit pas ajouter de jeu de caractères à un type explicite + expect($response->withType('application/json')->getHeaderLine('Content-Type')) + ->toBe('application/json'); + + // Doit permettre des types arbitraires + expect($response->withType('custom/stuff')->getHeaderLine('Content-Type')) + ->toBe('custom/stuff'); + + // Doit autoriser les types de jeux de caractères + expect($response->withType('text/html; charset=UTF-8')->getHeaderLine('Content-Type')) + ->toBe('text/html; charset=UTF-8'); + }); + + it('Un type invalide leve une exception', function() { + $response = new Response(); + + expect(fn() => $response->withType('beans')) + ->toThrow(new InvalidArgumentException('`beans` est un content type invalide.')); + }); + + it('MapType', function() { + $response = new Response(); + + expect($response->mapType('audio/x-wav'))->toBe('wav'); + expect($response->mapType('application/pdf'))->toBe('pdf'); + expect($response->mapType('text/xml'))->toBe('xml'); + expect($response->mapType('*/*'))->toBe('html'); + expect($response->mapType('application/vnd.ms-excel'))->toBe('csv'); + + $expected = ['json', 'xhtml', 'css']; + $result = $response->mapType(['application/json', 'application/xhtml+xml', 'text/css']); + expect($result)->toBe($expected); + }); + }); + describe('Status code', function () { it('Modification du statut code', function () { $response = new Response(); @@ -77,6 +179,42 @@ expect($response->getReasonPhrase())->toBe('My Little Pony'); }); + + it('WithStatus efface le content type', function () { + $response = new Response(); + $new = $response->withType('pdf')->withStatus(204); + + expect($new->hasHeader('Content-Type'))->toBeFalsy(); + expect($new->getType())->toBe(''); + expect($new->getStatusCode())->toBe(204); // Le code d'état doit effacer le type de contenu; + + $response = new Response(); + $new = $response->withStatus(304)->withType('pdf'); + + expect($new->getType())->toBe(''); + expect($new->hasHeader('Content-Type'))->toBeFalsy(); // Le type ne doit pas être conservé en raison du code d'état. + + $response = new Response(); + $new = $response->withHeader('Content-Type', 'application/json')->withStatus(204); + + expect($new->hasHeader('Content-Type'))->toBeFalsy(); // L'en-tête direct doit être dégagé + expect($new->getType())->toBe(''); + }); + + it("WithStatus n'efface pas le content type lorsqu'il passe par withHeader", function () { + $response = new Response(); + $new = $response->withHeader('Content-Type', 'application/json')->withStatus(403); + + expect($new->getHeaderLine('Content-Type'))->toBe('application/json'); + expect($new->getStatusCode())->toBe(403); + + $response = new Response(); + $new = $response->withStatus(403)->withHeader('Content-Type', 'application/json'); + + expect($new->getHeaderLine('Content-Type'))->toBe('application/json'); + expect($new->getStatusCode())->toBe(403); + expect($new->getType())->toBe('application/json'); + }); }); describe('Redirection', function () { @@ -195,6 +333,14 @@ }); it('Download', function () { + $response = new Response(); + $new = $response->withDownload('myfile.mp3'); + expect($response->hasHeader('Content-Disposition'))->toBeFalsy(); // Pas de mutation + + $expected = 'attachment; filename="myfile.mp3"'; + expect($new->getHeaderLine('Content-Disposition'))->toBe($expected); + + $response = new Response(); $actual = $response->download(__FILE__); @@ -229,6 +375,9 @@ it('withLink', function () { $response = new Response(); + + expect($response->hasHeader('Link'))->toBeFalsy(); + $response = $response ->withAddedLink('http://example.com?page=1', ['rel' => 'prev']) ->withAddedLink('http://example.com?page=3', ['rel' => 'next']); @@ -255,6 +404,245 @@ expect($response->getHeaderLine('Last-Modified'))->toBe($result); expect($response->getHeaderLine('Cache-Control'))->toMatch(static fn ($value) => str_contains($value, 'public, max-age=')); + + + $response = new Response(); + $since = $time = time(); + + $new = $response->withCache($since, $time); + expect($response->hasHeader('Date'))->toBeFalsy(); + expect($response->hasHeader('Last-Modified'))->toBeFalsy(); + + expect($new->getHeaderLine('Date'))->toBe(gmdate(DATE_RFC7231, $since)); + expect($new->getHeaderLine('Last-Modified'))->toBe(gmdate(DATE_RFC7231, $since)); + expect($new->getHeaderLine('Expires'))->toBe(gmdate(DATE_RFC7231, $time)); + expect($new->getHeaderLine('Cache-Control'))->toBe('public, max-age=0'); + }); + + it('withDisabledCache', function () { + $response = new Response(); + $expected = [ + 'Content-Type' => ['text/html; charset=UTF-8'], + 'Expires' => ['Mon, 26 Jul 1997 05:00:00 GMT'], + 'Last-Modified' => [gmdate(DATE_RFC7231)], + 'Cache-Control' => ['no-store, no-cache, must-revalidate, post-check=0, pre-check=0'], + ]; + + $new = $response->withDisabledCache(); + expect($response->hasHeader('Expires'))->toBeFalsy(); // Ancienne instance non mutée. + + expect($new->getHeaders())->toBe($expected); + }); + + it('withCharset', function () { + $response = new Response(); + expect($response->getHeaderLine('Content-Type'))->toBe('text/html; charset=UTF-8'); + + $new = $response->withCharset('iso-8859-1'); + // L'ancienne instance n'a pas été modifiée + expect($response->getHeaderLine('Content-Type'))->toMatch(fn($actual) => ! str_contains($actual, 'iso')); + expect($new->getCharset())->toBe('iso-8859-1'); + expect($new->getHeaderLine('Content-Type'))->toBe('text/html; charset=iso-8859-1'); + }); + + it('withLength', function () { + $response = new Response(); + expect($response->hasHeader('Content-Length'))->toBeFalsy(); + + $new = $response->withLength(100); + // L'ancienne instance n'a pas été modifiée + expect($response->hasHeader('Content-Length'))->toBeFalsy(); + expect($new->getHeaderLine('Content-Length'))->toBe('100'); + }); + + it('withExpires', function () { + $response = new Response(); + $now = new DateTime('now', new DateTimeZone('Africa/Douala')); + + $new = $response->withExpires($now); + expect($response->hasHeader('Expires'))->toBeFalsy(); + + $now->setTimeZone(new DateTimeZone('UTC')); + expect($new->getHeaderLine('Expires'))->toBe($now->format(DATE_RFC7231)); + + $now = time(); + $new = $response->withExpires($now); + expect($new->getHeaderLine('Expires'))->toBe(gmdate(DATE_RFC7231)); + + $time = new DateTime('+1 day', new DateTimeZone('UTC')); + $new = $response->withExpires('+1 day'); + expect($new->getHeaderLine('Expires'))->toBe($time->format(DATE_RFC7231)); + }); + + it('withModified', function () { + $response = new Response(); + $now = new DateTime('now', new DateTimeZone('Africa/Douala')); + + $new = $response->withModified($now); + expect($response->hasHeader('Last-Modified'))->toBeFalsy(); + + $now->setTimeZone(new DateTimeZone('UTC')); + expect($new->getHeaderLine('Last-Modified'))->toBe($now->format(DATE_RFC7231)); + + $now = time(); + $new = $response->withModified($now); + expect($new->getHeaderLine('Last-Modified'))->toBe(gmdate(DATE_RFC7231)); + + $now = new DateTimeImmutable(); + $new = $response->withModified($now); + expect($new->getHeaderLine('Last-Modified'))->toBe(gmdate(DATE_RFC7231, $now->getTimestamp())); + + $time = new DateTime('+1 day', new DateTimeZone('UTC')); + $new = $response->withModified('+1 day'); + expect($new->getHeaderLine('Last-Modified'))->toBe($time->format(DATE_RFC7231)); }); + + it('withSharable', function () { + $response = new Response(); + $new = $response->withSharable(true); + + expect($response->hasHeader('Cache-Control'))->toBeFalsy(); + expect($new->getHeaderLine('Cache-Control'))->toBe('public'); + + $new = $response->withSharable(false); + expect($new->getHeaderLine('Cache-Control'))->toBe('private'); + + $new = $response->withSharable(true, 3600); + expect($new->getHeaderLine('Cache-Control'))->toBe('public, max-age=3600'); + + $new = $response->withSharable(false, 3600); + expect($new->getHeaderLine('Cache-Control'))->toBe('private, max-age=3600'); + }); + + it('withMaxAge', function () { + $response = new Response(); + + expect($response->hasHeader('Cache-Control'))->toBeFalsy(); + + $new = $response->withMaxAge(3600); + expect($new->getHeaderLine('Cache-Control'))->toBe('max-age=3600'); + + $new = $response->withMaxAge(3600)->withSharable(false); + expect($new->getHeaderLine('Cache-Control'))->toBe('max-age=3600, private'); + }); + + it('withSharedMaxAge', function () { + $response = new Response(); + $new = $response->withSharedMaxAge(3600); + + expect($response->hasHeader('Cache-Control'))->toBeFalsy(); + expect($new->getHeaderLine('Cache-Control'))->toBe('s-maxage=3600'); + + $new = $response->withSharedMaxAge(3600)->withSharable(true); + expect($new->getHeaderLine('Cache-Control'))->toBe('s-maxage=3600, public'); + }); + + it('withMustRevalidate', function () { + $response = new Response(); + + expect($response->hasHeader('Cache-Control'))->toBeFalsy(); + + $new = $response->withMustRevalidate(true); + expect($response->hasHeader('Cache-Control'))->toBeFalsy(); + expect($new->getHeaderLine('Cache-Control'))->toBe('must-revalidate'); + + $new = $response->withMustRevalidate(false); + expect($new->getHeaderLine('Cache-Control'))->toBeEmpty(); + }); + + it('withVary', function () { + $response = new Response(); + $new = $response->withVary('Accept-encoding'); + + expect($response->hasHeader('Vary'))->toBeFalsy(); + expect($new->getHeaderLine('Vary'))->toBe('Accept-encoding'); + + $new = $response->withVary(['Accept-encoding', 'Accept-language']); + expect($response->hasHeader('Vary'))->toBeFalsy(); + expect($new->getHeaderLine('Vary'))->toBe('Accept-encoding, Accept-language'); + }); + + it('withEtag', function () { + $response = new Response(); + $new = $response->withEtag('something'); + + expect($response->hasHeader('Etag'))->toBeFalsy(); + expect($new->getHeaderLine('Etag'))->toBe('"something"'); + + $new = $response->withEtag('something', true); + expect($new->getHeaderLine('Etag'))->toBe('W/"something"'); + }); + + it('withNotModified', function () { + $response = new Response(['body' => 'something']); + $response = $response->withLength(100) + ->withStatus(200) + ->withHeader('Last-Modified', 'value') + ->withHeader('Content-Language', 'en-EN') + ->withHeader('X-things', 'things') + ->withType('application/json'); + + $new = $response->withNotModified(); + + expect($response->hasHeader('Content-Language'))->toBeTruthy(); + expect($response->hasHeader('Content-Length'))->toBeTruthy(); + + expect($new->hasHeader('Content-Type'))->toBeFalsy(); + expect($new->hasHeader('Content-Length'))->toBeFalsy(); + expect($new->hasHeader('Content-Language'))->toBeFalsy(); + expect($new->hasHeader('Last-Modified'))->toBeFalsy(); + + expect($new->getHeaderLine('X-things'))->toBe('things'); // Les autres headers sont conserves + expect($new->getStatusCode())->toBe(304); + expect($new->getBody()->getContents())->toBe(''); + }); }); + + describe('Autres', function () { + it('Compression', function () { + $response = new Response(); + + if (ini_get('zlib.output_compression') === '1' || !extension_loaded('zlib')) { + expect($response->compress())->toBeFalsy(); + skipIf(true); + } + + $_SERVER['HTTP_ACCEPT_ENCODING'] = ''; + $result = $response->compress(); + expect($result)->toBeFalsy(); + + $_SERVER['HTTP_ACCEPT_ENCODING'] = 'gzip'; + $result = $response->compress(); + expect($result)->toBeTruthy(); + expect(ob_list_handlers())->toContain('ob_gzhandler'); + + ob_get_clean(); + }); + + it('Sortie compressee', function () { + $response = new Response(); + + $_SERVER['HTTP_ACCEPT_ENCODING'] = 'gzip'; + expect($response->outputCompressed())->toBeFalsy(); + + $_SERVER['HTTP_ACCEPT_ENCODING'] = ''; + expect($response->outputCompressed())->toBeFalsy(); + + skipIf(!extension_loaded('zlib')); + + if (ini_get('zlib.output_compression') !== '1') { + ob_start('ob_gzhandler'); + } + $_SERVER['HTTP_ACCEPT_ENCODING'] = 'gzip'; + + expect($response->outputCompressed())->toBeTruthy(); + + $_SERVER['HTTP_ACCEPT_ENCODING'] = ''; + expect($response->outputCompressed())->toBeFalsy(); + + if (ini_get('zlib.output_compression') !== '1') { + ob_get_clean(); + } + }); + }); }); diff --git a/spec/system/framework/Http/ServerRequest.spec.php b/spec/system/framework/Http/ServerRequest.spec.php new file mode 100644 index 00000000..e600ca4d --- /dev/null +++ b/spec/system/framework/Http/ServerRequest.spec.php @@ -0,0 +1,246 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Filesystem\Files\UploadedFile; +use BlitzPHP\Http\ServerRequest; + +describe('Http / ServerRequest', function () { + describe('Detector', function () { + it('Custom detector avec des arguments personnalises', function () { + $request = new ServerRequest(); + $request->addDetector('controller', function ($request, $name) { + return $request->getParam('controller') === $name; + }); + + $request = $request->withParam('controller', 'blitz'); + + expect($request->is('controller', 'blitz'))->toBeTruthy(); + expect($request->is('controller', 'nonExistingController'))->toBeFalsy(); + expect($request->isController('blitz'))->toBeTruthy(); + expect($request->isController('nonExistingController'))->toBeFalsy(); + }); + + it("Header detector", function () { + $request = new ServerRequest(); + $request->addDetector('host', ['header' => ['host' => 'blitzphp.com']]); + + $request = $request->withEnv('HTTP_HOST', 'blitzphp.com'); + expect($request->is('host'))->toBeTruthy(); + + $request = $request->withEnv('HTTP_HOST', 'php.net'); + expect($request->is('host'))->toBeFalsy(); + }); + + it("Extension detector", function () { + $request = new ServerRequest(); + $request = $request->withParam('_ext', 'json'); + + expect($request->is('json'))->toBeTruthy(); + + $request = new ServerRequest(); + $request = $request->withParam('_ext', 'xml'); + + expect($request->is('xml'))->toBeTruthy(); + expect($request->is('json'))->toBeFalsy(); + }); + + it("Accept Header detector", function () { + $request = new ServerRequest(); + $request = $request->withEnv('HTTP_ACCEPT', 'application/json, text/plain, */*'); + expect($request->is('json'))->toBeTruthy(); + + $request = new ServerRequest(); + $request = $request->withEnv('HTTP_ACCEPT', 'text/plain, */*'); + expect($request->is('json'))->toBeFalsy(); + + $request = new ServerRequest(); + $request = $request->withEnv('HTTP_ACCEPT', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'); + expect($request->is('json'))->toBeFalsy(); + expect($request->is('xml'))->toBeFalsy(); + }); + }); + + describe('Constructeur', function () { + it('construction avec les données de la requête', function () { + $data = [ + 'query' => [ + 'one' => 'param', + 'two' => 'banana', + ], + 'url' => 'some/path', + ]; + $request = new ServerRequest($data); + + expect($request->getQuery('one'))->toBe('param'); + expect($request->getQueryParams())->toEqual($data['query']); + expect($request->getRequestTarget())->toEqual('/some/path'); + }); + + it('construction avec une chaine URL', function () { + $request = new ServerRequest([ + 'url' => '/articles/view/1', + 'environment' => ['REQUEST_URI' => '/some/other/path'], + ]); + expect($request->getUri()->getPath())->toBe('/articles/view/1'); + + $request = new ServerRequest(['url' => '/']); + expect($request->getUri()->getPath())->toBe('/'); + }); + + it('Teste que les arguments de la chaîne de requête fournis dans la chaîne de l\'URL sont analysés.', function () { + $request = new ServerRequest(['url' => 'some/path?one=something&two=else']); + $expected = ['one' => 'something', 'two' => 'else']; + + expect($request->getQueryParams())->toEqual($expected); + expect($request->getUri()->getPath())->toBe('/some/path'); + expect($request->getUri()->getQuery())->toBe('one=something&two=else'); + }); + + xit('Tester que les chaînes de requête sont gérées correctement.', function () { + $config = ['environment' => ['REQUEST_URI' => '/tasks/index?ts=123456']]; + $request = new ServerRequest($config); + expect($request->getRequestTarget())->toBe('/tasks/index'); + + $config = ['environment' => ['REQUEST_URI' => '/some/path?url=http://blitzphp.com']]; + $request = new ServerRequest($config); + expect($request->getRequestTarget())->toBe('/some/path'); + + $config = ['environment' => [ + 'REQUEST_URI' => config('app.base_url') . '/other/path?url=http://blitzphp.com', + ]]; + $request = new ServerRequest($config); + expect($request->getRequestTarget())->toBe('/other/path'); + }); + + xit("Tester que l'URL dans le chemin d'accès est traité correctement.", function () { + $config = ['environment' => ['REQUEST_URI' => '/jump/http://blitzphp.com']]; + $request = new ServerRequest($config); + expect($request->getRequestTarget())->toBe('/jump/http://blitzphp.com'); + + $config = ['environment' => [ + 'REQUEST_URI' => config('app.base_url') . '/jump/http://blitzphp.com', + ]]; + $request = new ServerRequest($config); + expect($request->getRequestTarget())->toBe('/jump/http://blitzphp.com'); + + }); + + it('getPath', function () { + $request = new ServerRequest(['url' => '/']); + expect($request->getPath())->toBe('/'); + + $request = new ServerRequest(['url' => 'some/path?one=something&two=else']); + expect($request->getPath())->toBe('/some/path'); + + $request = $request->withRequestTarget('/foo/bar?x=y'); + expect($request->getPath())->toBe('/foo/bar'); + }); + }); + + describe('Parsing', function () { + it("Test d'analyse des données POST dans l'objet.", function () { + $post = [ + 'Article' => ['title'], + ]; + $request = new ServerRequest(compact('post')); + expect($post)->toEqual($request->getData()); + + $post = ['one' => 1, 'two' => 'three']; + $request = new ServerRequest(compact('post')); + expect($post)->toEqual($request->getData()); + + $post = [ + 'Article' => ['title' => 'Testing'], + 'action' => 'update', + ]; + $request = new ServerRequest(compact('post')); + expect($post)->toEqual($request->getData()); + }); + }); + + describe('Uploaded files', function () { + it("Tester que le constructeur utilise les objets fichiers téléchargés s'ils sont présents.", function () { + $file = new UploadedFile( + __FILE__, + 123, + UPLOAD_ERR_OK, + 'test.php', + 'text/plain' + ); + $request = new ServerRequest(['files' => ['avatar' => $file]]); + expect($request->getUploadedFiles())->toBe(['avatar' => $file]); + }); + + it("Liste de fichiers vide.", function () { + $request = new ServerRequest(['files' => []]); + expect($request->getUploadedFiles())->toBeEmpty(); + expect($request->getData())->toBeEmpty(); + }); + + it("Remplacement de fichiers.", function () { + $file = new UploadedFile( + __FILE__, + 123, + UPLOAD_ERR_OK, + 'test.php', + 'text/plain' + ); + $request = new ServerRequest(); + $new = $request->withUploadedFiles(['picture' => $file]); + + expect($request->getUploadedFiles())->toBe([]); + expect($request)->not->toBe($new); + expect($new->getUploadedFiles())->toBe(['picture' => $file]); + }); + + it("Recuperation d'un fichier.", function () { + $file = new UploadedFile( + __FILE__, + 123, + UPLOAD_ERR_OK, + 'test.php', + 'text/plain' + ); + $request = new ServerRequest(); + $new = $request->withUploadedFiles(['picture' => $file]); + + expect($new->getUploadedFile(''))->toBeNull(); + expect($new->getUploadedFile('picture'))->toEqual($file); + + $new = $request->withUploadedFiles([ + 'pictures' => [ + [ + 'image' => $file, + ], + ], + ]); + + expect($new->getUploadedFile('pictures'))->toBeNull(); + expect($new->getUploadedFile('pictures.0'))->toBeAn('array'); + expect($new->getUploadedFile('pictures.1'))->toBeNull(); + expect($new->getUploadedFile('pictures.0.image'))->toEqual($file); + }); + + it("Remplacement de fichiers avec un fichier invalide.", function () { + $request = new ServerRequest(); + + expect(fn() => $request->withUploadedFiles(['avatar' => 'picture'])) + ->toThrow(new InvalidArgumentException('Fichier invalide à `avatar`')); + }); + + it("Remplacement de fichiers avec un fichier invalide imbriquer.", function () { + $request = new ServerRequest(); + + expect(fn() => $request->withUploadedFiles(['user' => ['avatar' => 'not a file']])) + ->toThrow(new InvalidArgumentException('Fichier invalide à `user.avatar`')); + }); + }); +}); diff --git a/src/Http/Response.php b/src/Http/Response.php index 36a5d297..d3122fd1 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -764,7 +764,7 @@ protected function resolveType(string $contentType): string return is_array($mapped) ? current($mapped) : $mapped; } if (! str_contains($contentType, '/')) { - throw new InvalidArgumentException(sprintf('"%s" is an invalid content type.', $contentType)); + throw new InvalidArgumentException(sprintf('`%s` est un content type invalide.', $contentType)); } return $contentType; From bdde37f60db61bc9670e401877f960faa55d0ddc Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 20 Dec 2023 18:06:41 +0100 Subject: [PATCH 104/111] =?UTF-8?q?Refonte=20des=20classes=20Config=20et?= =?UTF-8?q?=20ServerRequest=20pour=20une=20meilleure=20fonctionnalit=C3=A9?= =?UTF-8?q?=20et=20flexibilit=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Améliorer la classe `Config` en modifiant la méthode `ghost` pour retourner l'instance pour le chaînage des méthodes. Ce changement permet une utilisation plus facile et plus fluide lors de la définition de configurations à la volée. Dans la classe `ServerRequest`, optimiser les méthodes `hasHeader` et `getHeader` pour vérifier l'existence d'un header en utilisant directement le tableau d'environnement. Cela améliore l'efficacité et élimine les vérifications supplémentaires inutiles. --- src/Config/Config.php | 4 +++- src/Http/ServerRequest.php | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Config/Config.php b/src/Config/Config.php index 07b4b071..5ac73181 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -144,9 +144,11 @@ public function reset(null|array|string $keys = null): void * Rend disponible un groupe de configuration qui n'existe pas (pas de fichier de configuration) * Ceci est notament utilse pour definir des configurations à la volée */ - public function ghost(array|string $key, ?Schema $schema = null): void + public function ghost(array|string $key, ?Schema $schema = null): static { $this->load($key, null, $schema, true); + + return $this; } /** diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 51571c6c..85cb563e 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -756,6 +756,10 @@ public function getHeaders(): array */ public function hasHeader(string $name): bool { + if (isset($this->_environment[$name])) { + return true; + } + if (isset($this->_environment[$this->normalizeHeaderName($name)])) { return true; } @@ -778,7 +782,11 @@ public function hasHeader(string $name): bool */ public function getHeader(string $name): array { - $name = $this->normalizeHeaderName($name); + if (isset($this->_environment[$name])) { + return (array) $this->_environment[$name]; + } + + $name = $this->normalizeHeaderName($name); if (isset($this->_environment[$name])) { return (array) $this->_environment[$name]; } From 3b7a3fbbe1824887aed08380ffaf4dc17d313581 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 20 Dec 2023 18:17:53 +0100 Subject: [PATCH 105/111] feat: Ajout des classes CorsBuilder et CorsMiddleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La commande présente les classes CorsBuilder et CorsMiddleware, qui fournissent des fonctionnalités pour gérer le partage de ressources entre origines (CORS) dans le cadre de BlitzPHP. CorsBuilder permet l'identification des requêtes CORS, la gestion des requêtes de gestion des demandes de contrôle préalable, la gestion des en-têtes CORS et des réponses. CorsMiddleware intègre CorsBuilder dans le pipeline middleware, permettant une gestion CORS transparente dans les applications Blitz PHP. Cet ajout améliore la capacité du framework à gérer les scénarios CORS garantissant ainsi une communication sécurisée et efficace entre les origines. --- .../Middlewares/TestRequestHandler.php | 34 ++ .../Middlewares/ThrowsExceptionMiddleware.php | 29 ++ .../framework/Middlewares/Cors.spec.php | 325 ++++++++++++++++++ src/Http/CorsBuilder.php | 243 +++++++++++++ src/Middlewares/Cors.php | 244 +++++-------- 5 files changed, 718 insertions(+), 157 deletions(-) create mode 100644 spec/support/Middlewares/TestRequestHandler.php create mode 100644 spec/support/application/app/Middlewares/ThrowsExceptionMiddleware.php create mode 100644 spec/system/framework/Middlewares/Cors.spec.php create mode 100644 src/Http/CorsBuilder.php diff --git a/spec/support/Middlewares/TestRequestHandler.php b/spec/support/Middlewares/TestRequestHandler.php new file mode 100644 index 00000000..7da38f16 --- /dev/null +++ b/spec/support/Middlewares/TestRequestHandler.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Spec\BlitzPHP\Middlewares; + +use BlitzPHP\Http\Response; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class TestRequestHandler implements RequestHandlerInterface +{ + public $callable; + + public function __construct(?callable $callable = null) + { + $this->callable = $callable ?: function ($request) { + return new Response(); + }; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + return ($this->callable)($request); + } +} diff --git a/spec/support/application/app/Middlewares/ThrowsExceptionMiddleware.php b/spec/support/application/app/Middlewares/ThrowsExceptionMiddleware.php new file mode 100644 index 00000000..fa4f119e --- /dev/null +++ b/spec/support/application/app/Middlewares/ThrowsExceptionMiddleware.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Spec\BlitzPHP\App\Middlewares; + +use Exception; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * Testing stub for middleware tests. + */ +class ThrowsExceptionMiddleware implements MiddlewareInterface +{ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + throw new Exception('Sample Message', 403); + } +} diff --git a/spec/system/framework/Middlewares/Cors.spec.php b/spec/system/framework/Middlewares/Cors.spec.php new file mode 100644 index 00000000..6e92dc2f --- /dev/null +++ b/spec/system/framework/Middlewares/Cors.spec.php @@ -0,0 +1,325 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Http\CorsBuilder; +use BlitzPHP\Http\Request; +use BlitzPHP\Http\Response; +use BlitzPHP\Http\ServerRequestFactory; +use BlitzPHP\Middlewares\Cors; +use Spec\BlitzPHP\Middlewares\TestRequestHandler; + +describe('Middleware / Cors', function () { + describe('CorsBuilder', function() { + beforeAll(function () { + $this->request = fn() => new Request(); + $this->response = fn() => new Response(); + $this->config = [ + 'allowedOrigins' => ['*'], + 'allowedOriginsPatterns' => [], + 'supportsCredentials' => false, + 'allowedHeaders' => ['*'], + 'exposedHeaders' => [], + 'allowedMethods' => ['*'], + 'maxAge' => 0, + ]; + }); + + it("Teste si la requete est Cors", function () { + /** @var Request $request */ + $request = $this->request()->withHeader('Origin', 'http://foo-bar.test'); + + $cors = new CorsBuilder($this->config); + expect($cors->isCorsRequest($request))->toBeTruthy(); + + /** @var Request $request */ + $request = $this->request()->withHeader('Foo', 'http://foo-bar.test'); + + $cors = new CorsBuilder($this->config); + expect($cors->isCorsRequest($request))->toBeFalsy(); + }); + + it("Teste si c'est une requete Preflight", function () { + /** @var Request $request */ + $request = $this->request()->withMethod('OPTIONS') + ->withHeader('Access-Control-Request-Method', 'GET'); + + $cors = new CorsBuilder($this->config); + expect($cors->isPreflightRequest($request))->toBeTruthy(); + + /** @var Request $request */ + $request = $this->request()->withMethod('GET') + ->withHeader('Access-Control-Request-Method', 'GET'); + + $cors = new CorsBuilder($this->config); + expect($cors->isPreflightRequest($request))->toBeFalsy(); + }); + + it("Vary Header", function () { + /** @var Response $response */ + $response = $this->response() + ->withHeader('Vary', 'Access-Control-Request-Method'); + + $cors = new CorsBuilder($this->config); + $vary = $cors->varyHeader($response, 'Access-Control-Request-Method'); + + expect($response->getHeaderLine('Vary'))->toBe($vary->getHeaderLine('Vary')); + }); + + it("Gere une requete Preflight", function () { + /** @var Request $request */ + $request = $this->request() + ->withMethod('OPTIONS') + ->withHeader('Origin', 'http://foobar.com') + ->withHeader('Access-Control-Request-Method', 'GET') + ->withHeader('Access-Control-Request-Headers', 'X-CSRF-TOKEN'); + + $cors = new CorsBuilder($this->config); + $expected = $cors->handlePreflightRequest($request); + + expect($expected->getHeaderLine('Access-Control-Allow-Credentials'))->toBeEmpty(); + expect($expected->getHeaderLine('Access-Control-Expose-Headers'))->toBeEmpty(); + expect($expected->getHeaderLine('Access-Control-Allow-Methods'))->toBe('GET'); + expect($expected->hasHeader('Vary'))->toBeTruthy(); + expect($expected->getHeaderLine('Vary'))->toBe('Access-Control-Request-Method, Access-Control-Request-Headers'); + expect($expected->getHeaderLine('Access-Control-Allow-Headers'))->toBe('X-CSRF-TOKEN'); + expect($expected->getHeaderLine('Access-Control-Max-Age'))->toBe('0'); + expect($expected->getStatusCode())->toBe(204); + }); + + it("Gere une requete", function () { + /** @var Request $request */ + $request = $this->request() + ->withMethod('GET') + ->withHeader('Origin', 'http://foobar.com'); + + /** @var Response $response */ + $response = $this->response() + ->withHeader('Access-Control-Allow-Origin', $request->getHeaderLine('Origin')); + + $cors = new CorsBuilder($this->config); + $expected = $cors->addPreflightRequestHeaders($request, $response); + + expect($expected->hasHeader('Access-Control-Allow-Origin'))->toBeTruthy(); + expect($expected->getHeaderLine('Access-Control-Allow-Origin'))->toBe('*'); + }); + + it("Gere une requete Preflight avec des restrictions AllowesHeaders", function () { + /** @var Request $request */ + $request = $this->request() + ->withMethod('OPTIONS') + ->withHeader('Origin', 'http://foobar.com') + ->withHeader('Access-Control-Request-Method', 'GET') + ->withHeader('Access-Control-Request-Headers', 'X-CSRF-TOKEN'); + + $cors = new CorsBuilder(array_merge($this->config, [ + 'allowedHeaders' => ['SAMPLE-RESTRICT-HEADER'], + ])); + $expected = $cors->handlePreflightRequest($request); + + expect($request->getHeaderLine('Access-Control-Request-Headers')) + ->not->toBe($expected->getHeaderLine('Access-Control-Allow-Headers')); + }); + + it("Gere une requete Preflight avec des memes restrictions AllowedHeaders", function () { + /** @var Request $request */ + $request = $this->request() + ->withMethod('OPTIONS') + ->withHeader('Origin', 'http://foobar.com') + ->withHeader('Access-Control-Request-Method', 'GET') + ->withHeader('Access-Control-Request-Headers', 'X-CSRF-TOKEN'); + + $cors = new CorsBuilder(array_merge($this->config, [ + 'allowedHeaders' => ['X-CSRF-TOKEN'], + ])); + $expected = $cors->handlePreflightRequest($request); + + expect($request->getHeaderLine('Access-Control-Request-Headers')) + ->toBe($expected->getHeaderLine('Access-Control-Allow-Headers')); + }); + + it("Gere une requete Preflight avec des restrictions AllowedOrigins", function () { + /** @var Request $request */ + $request = $this->request() + ->withMethod('OPTIONS') + ->withHeader('Origin', 'http://foobar.com') + ->withHeader('Access-Control-Request-Method', 'GET') + ->withHeader('Access-Control-Request-Headers', 'X-CSRF-TOKEN'); + + $cors = new CorsBuilder(array_merge($this->config, [ + 'allowedOrigins' => ['http://foo.com'], + ])); + $expected = $cors->handlePreflightRequest($request); + + expect($request->getHeaderLine('Origin')) + ->not->toBe($expected->getHeaderLine('Access-Control-Allow-Origin')); + }); + + it("Gere une requete Preflight avec des memes restrictions AllowedOrigins", function () { + /** @var Request $request */ + $request = $this->request() + ->withMethod('OPTIONS') + ->withHeader('Origin', 'http://foo.com') + ->withHeader('Access-Control-Request-Method', 'GET') + ->withHeader('Access-Control-Request-Headers', 'X-CSRF-TOKEN'); + + $cors = new CorsBuilder(array_merge($this->config, [ + 'allowedOrigins' => ['http://foo.com'], + ])); + $expected = $cors->handlePreflightRequest($request); + + expect($request->getHeaderLine('Origin')) + ->toBe($expected->getHeaderLine('Access-Control-Allow-Origin')); + }); + + it("Gere une requete Preflight avec ExposeHeaders", function () { + /** @var Request $request */ + $request = $this->request() + ->withMethod('GET') + ->withHeader('Origin', 'http://foo.com') + ->withHeader('Access-Control-Request-Headers', 'X-CSRF-TOKEN'); + + $cors = new CorsBuilder(array_merge($this->config, [ + 'exposedHeaders' => ['X-My-Custom-Header', 'X-Another-Custom-Header'], + ])); + $expected = $cors->addActualRequestHeaders($request, $this->response()); + + expect($expected->getHeaderLine('Access-Control-Expose-Headers')) + ->toBe("X-My-Custom-Header, X-Another-Custom-Header"); + }); + + it("Gere une requete Preflight avec ExposeHeaders non definis", function () { + /** @var Request $request */ + $request = $this->request() + ->withMethod('GET') + ->withHeader('Origin', 'http://foo.com') + ->withHeader('Access-Control-Request-Headers', 'X-CSRF-TOKEN'); + + $cors = new CorsBuilder($this->config); + $expected = $cors->addActualRequestHeaders($request, $this->response()); + + expect($expected->getHeaderLine('Access-Control-Expose-Headers'))->toBeEmpty(); + }); + }); + + describe('CorsMiddleware', function() { + require_once TEST_PATH . '/support/Middlewares/TestRequestHandler.php'; + + beforeAll(function () { + config()->ghost('cors')->set('cors', [ + 'allowedOrigins' => ['http://localhost'], + 'supportsCredentials' => true, + 'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE'], + 'allowedHeaders' => ['x-allowed-header', 'x-other-allowed-header'], + 'exposedHeaders' => [], + 'maxAge' => 86400, // 1 day + ]); + + $this->origin = 'http://localhost'; + + $this->setServer = function(array $server) { + $this->server = array_merge($this->server, $server); + }; + + $this->sendRequest = function (array $config = []) { + $request = ServerRequestFactory::fromGlobals($this->server); + $handler = new TestRequestHandler(); + $middleware = new Cors($config); + + return $middleware->process($request, $handler); + }; + + $this->sendRequestForOrigin = function(string $originUrl, $allowUrl) { + $this->setServer(['HTTP_ORIGIN' => $originUrl]); + + return $this->sendRequest(['allowedOrigins' => $allowUrl])->getHeaderLine('Access-Control-Allow-Origin'); + }; + }); + + beforeEach(function () { + $this->server = [ + 'REQUEST_URI' => '/test', + 'HTTP_ORIGIN' => $this->origin, + ]; + }); + + it("modifie une requete sans origine", function () { + unset($this->server['HTTP_ORIGIN']); + + /** @var Response $response */ + $response = $this->sendRequest(); + + expect($response->getHeaderLine('Access-Control-Allow-Origin'))->toBe('http://localhost'); + + $this->server['HTTP_ORIGIN'] = $this->origin; + }); + + it("modifie une requete ayant la même origine", function () { + $this->setServer([ + 'HTTP_HOST' => 'foo.com', + 'HTTP_ORIGIN' => 'http://foo.com', + ]); + + /** @var Response $response */ + $response = $this->sendRequest([ + 'allowedOrigins' => ['*'] + ]); + + expect($response->getHeaderLine('Access-Control-Allow-Origin'))->toBe('http://foo.com'); + }); + + it("renvoie l'en-tête `Allow Origin` en cas de requete réelle valide.", function () { + /** @var Response $response */ + $response = $this->sendRequest(); + + expect($response->hasHeader('Access-Control-Allow-Origin'))->toBeTruthy(); + expect($response->getHeaderLine('Access-Control-Allow-Origin'))->toBe('http://localhost'); + }); + + it("renvoie l'en-tête `Allow Origin` à la requete `Autoriser toutes les origines`.", function () { + /** @var Response $response */ + $response = $this->sendRequest([ + 'allowedOrigins' => ['*'] + ]); + + expect($response->getStatusCode())->toBe(200); + expect($response->hasHeader('Access-Control-Allow-Origin'))->toBeTruthy(); + expect($response->getHeaderLine('Access-Control-Allow-Origin'))->toBe('http://localhost'); + }); + + it("renvoie l'en-tête Allow Headers sur la demande Allow All Headers.", function () { + $this->setServer([ + 'HTTP_ORIGIN' => 'http://localhost', + 'Access-Control-Request-Method' => 'GET', + 'REQUEST_METHOD' => 'OPTIONS', + 'Access-Control-Request-Headers' => 'Foo, BAR' + ]); + + /** @var Response $response */ + $response = $this->sendRequest([ + 'allowedHeaders' => ['*'] + ]); + + expect($response->getStatusCode())->toBe(204); + expect($response->getHeaderLine('Access-Control-Allow-Headers'))->toBe('Foo, BAR'); + expect($response->getHeaderLine('Vary'))->toBe('Access-Control-Request-Headers, Access-Control-Request-Method'); + }); + + it("définit l'en-tête AllowCredentials lorsque l'indicateur est défini dans une demande réelle valide.", function () { + /** @var Response $response */ + $response = $this->sendRequest([ + 'supportsCredentials' => true + ]); + + expect($response->hasHeader('Access-Control-Allow-Credentials'))->toBeTruthy(); + expect($response->getHeaderLine('Access-Control-Allow-Credentials'))->toBe('true'); + }); + }); +}); diff --git a/src/Http/CorsBuilder.php b/src/Http/CorsBuilder.php new file mode 100644 index 00000000..c3e6d57a --- /dev/null +++ b/src/Http/CorsBuilder.php @@ -0,0 +1,243 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Http; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * @credit CodeIgniter4 Cors Fluent\Cors\ServiceCors + */ +class CorsBuilder +{ + protected array $options = []; + + public function __construct(array $options = []) + { + $this->options = $this->normalizeOptions($options); + } + + protected function normalizeOptions(array $options = []): array + { + $options = array_merge([ + 'allowedOrigins' => [], + 'allowedOriginsPatterns' => [], + 'supportsCredentials' => false, + 'allowedHeaders' => [], + 'exposedHeaders' => [], + 'allowedMethods' => [], + 'maxAge' => 0, + ], $options); + + + // Normalize case + $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']); + + + // normalize ['*'] to true + if (in_array('*', $options['allowedOrigins'])) { + $options['allowedOrigins'] = true; + } + if (in_array('*', $options['allowedHeaders'])) { + $options['allowedHeaders'] = true; + } + if (in_array('*', $options['allowedMethods'])) { + $options['allowedMethods'] = true; + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public function isCorsRequest(ServerRequestInterface $request): bool + { + return $request->hasHeader('Origin') && !$this->isSameHost($request); + } + + /** + * {@inheritdoc} + */ + public function isPreflightRequest(ServerRequestInterface $request): bool + { + return strtoupper($request->getMethod()) === 'OPTIONS' && $request->hasHeader('Access-Control-Request-Method'); + } + + /** + * {@inheritdoc} + */ + public function handlePreflightRequest(ServerRequestInterface $request): ResponseInterface + { + $response = new Response(); + + $response = $response->withStatus(204); + + return $this->addPreflightRequestHeaders($request, $response); + } + + public function addPreflightRequestHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $response = $this->configureAllowedOrigin($request, $response); + + if ($response->hasHeader('Access-Control-Allow-Origin')) { + $response = $this->configureAllowCredentials($request, $response); + $response = $this->configureAllowedMethods($request, $response); + $response = $this->configureAllowedHeaders($request, $response); + $response = $this->configureMaxAge($request, $response); + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function isOriginAllowed(ServerRequestInterface $request): bool + { + if ($this->options['allowedOrigins'] === true) { + return true; + } + + if (! $request->hasHeader('Origin')) { + return false; + } + + $origin = $request->getHeaderLine('Origin'); + + if (in_array($origin, $this->options['allowedOrigins'])) { + return true; + } + + foreach ($this->options['allowedOriginsPatterns'] as $pattern) { + if (preg_match($pattern, $origin)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function addActualRequestHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $response = $this->configureAllowedOrigin($request, $response); + + if ($response->hasHeader('Access-Control-Allow-Origin')) { + $response = $this->configureAllowCredentials($request, $response); + $response = $this->configureExposedHeaders($request, $response); + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function varyHeader(ResponseInterface $response, $header): ResponseInterface + { + if (! $response->hasHeader('Vary')) { + $response = $response->withHeader('Vary', $header); + } elseif (! in_array($header, explode(', ', $response->getHeaderLine('Vary')))) { + $response = $response->withHeader('Vary', $response->getHeaderLine('Vary') . ', ' . $header); + } + + return $response; + } + + protected function configureAllowedOrigin(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if ($this->options['allowedOrigins'] === true && ! $this->options['supportsCredentials']) { + // Safe+cacheable, allow everything + $response = $response->withHeader('Access-Control-Allow-Origin', '*'); + } else if ($this->isSingleOriginAllowed()) { + // Single origins can be safely set + $response = $response->withHeader('Access-Control-Allow-Origin', array_values($this->options['allowedOrigins'])[0]); + } else { + // For dynamic headers, set the requested Origin header when set and allowed + if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) { + $response = $response->withHeader('Access-Control-Allow-Origin', (string) $request->getHeaderLine('Origin')); + } + + $response = $this->varyHeader($response, 'Origin'); + } + + return $response; + } + + protected function isSingleOriginAllowed(): bool + { + if ($this->options['allowedOrigins'] === true || ! empty($this->options['allowedOriginsPatterns'])) { + return false; + } + + return count($this->options['allowedOrigins']) === 1; + } + + protected function configureAllowedMethods(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if ($this->options['allowedMethods'] === true) { + $allowMethods = strtoupper($request->getHeaderLine('Access-Control-Request-Method')); + $response = $this->varyHeader($response, 'Access-Control-Request-Method'); + } else { + $allowMethods = implode(', ', $this->options['allowedMethods']); + } + + return $response->withHeader('Access-Control-Allow-Methods', $allowMethods); + } + + protected function configureAllowedHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if ($this->options['allowedHeaders'] === true) { + $allowHeaders = $request->getHeaderLine('Access-Control-Request-Headers'); + $response = $this->varyHeader($response, 'Access-Control-Request-Headers'); + } else { + $allowHeaders = implode(', ', $this->options['allowedHeaders']); + } + + return $response->withHeader('Access-Control-Allow-Headers', $allowHeaders); + } + + protected function configureAllowCredentials(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if ($this->options['supportsCredentials']) { + $response = $response->withHeader('Access-Control-Allow-Credentials', 'true'); + } + + return $response; + } + + protected function configureExposedHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if ($this->options['exposedHeaders']) { + $response = $response->withHeader('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders'])); + } + + return $response; + } + + protected function configureMaxAge(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if ($this->options['maxAge'] !== null) { + $response = $response->withHeader('Access-Control-Max-Age', (string) $this->options['maxAge']); + } + + return $response; + } + + protected function isSameHost(ServerRequestInterface $request): bool + { + return $request->getHeaderLine('Origin') === config('app.base_url'); + } +} diff --git a/src/Middlewares/Cors.php b/src/Middlewares/Cors.php index 67e057b3..1e370f78 100644 --- a/src/Middlewares/Cors.php +++ b/src/Middlewares/Cors.php @@ -11,207 +11,137 @@ namespace BlitzPHP\Middlewares; -use Psr\Http\Message\ResponseInterface; +use BlitzPHP\Http\CorsBuilder; +use BlitzPHP\Utilities\String\Text; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; /** - * Cors - * Middleware cors pour gerer les requetes d'origine croisees + * Middleware cors pour gerer les requetes d'origine croisees + * + * @credit CodeIgniter4 Cors + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS */ -class Cors extends BaseMiddleware implements MiddlewareInterface +class Cors implements MiddlewareInterface { - protected array $config = [ - 'AllowOrigin' => true, - 'AllowCredentials' => true, - 'AllowMethods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], - 'AllowHeaders' => true, - 'ExposeHeaders' => false, - 'MaxAge' => 86400, // 1 day - ]; - - /** - * Constructor + /** + * -------------------------------------------------------------------------- + * En-têtes HTTP autorisés + * -------------------------------------------------------------------------- + * + * Indique les en-têtes HTTP autorisés. */ - public function __construct(array $config = []) - { - $this->config = array_merge($this->config, $config); - } + public array $allowedHeaders = ['*']; /** - * Modifie le MaxAge + * -------------------------------------------------------------------------- + * Méthodes HTTP autorisées + * -------------------------------------------------------------------------- * - * @param float|int $maxAge + * Indique les méthodes HTTP autorisées. */ - public function setMaxAge($maxAge): self - { - $this->config['MaxAge'] = $maxAge; - - return $this; - } + public array $allowedMethods = ['*']; /** - * Modifie les entetes exposes + * -------------------------------------------------------------------------- + * Origine des requêtes autorisées + * -------------------------------------------------------------------------- * - * @param bool|string|string[] $exposeHeaders + * Indique quelles origines sont autorisées à effectuer des demandes. + * Les motifs sont également acceptés, par exemple *.foo.com */ - public function setExposeHeaders($exposeHeaders): self - { - $this->config['ExposeHeaders'] = $exposeHeaders; - - return $this; - } + public array $allowedOrigins = ['*']; /** - * Modifie les entetes autorises + * -------------------------------------------------------------------------- + * Modèles d'origines autorisés + * -------------------------------------------------------------------------- * - * @param bool|string|string[] $headers + * Les motifs qui peuvent être utilisés avec `preg_match` pour correspondre à l'origine. */ - public function setHeaders($headers): self - { - $this->config['AllowHeaders'] = $headers; - - return $this; - } + public array $allowedOriginsPatterns = []; /** - * Modifie les methodes autorisees + * -------------------------------------------------------------------------- + * En-têtes exposés + * -------------------------------------------------------------------------- * - * @param string|string[] $methods + * En-têtes qui sont autorisés à être exposés au serveur web. */ - public function setMethods($methods): self - { - $this->config['AlloMethods'] = $methods; - - return $this; - } + public array $exposedHeaders = []; /** - * Defini si on doit utiliser les informations d'identifications ou pas + * -------------------------------------------------------------------------- + * Âge maximum + * -------------------------------------------------------------------------- + * + * Indique la durée pendant laquelle les résultats d'une demande de contrôle en amont peuvent être mis en cache. */ - public function setCredentials(bool $credentials): self - { - $this->config['AllowCredentials'] = $credentials; - - return $this; - } + public int $maxAge = 0; /** - * Modifie les origines autorisees + * -------------------------------------------------------------------------- + * Si la réponse peut être exposée ou non lorsque des informations d'identification sont présentes + * -------------------------------------------------------------------------- * - * @param bool|string|string[] $origin + * Indique si la réponse à la demande peut être exposée lorsque l'indicateur d'informations d'identification est vrai. + * Lorsqu'il est utilisé dans le cadre d'une réponse à une demande de contrôle en amont, il indique si la demande proprement dite peut être effectuée en utilisant des informations d'identification. + * Notez que les requêtes GET simples ne sont pas contrôlées au préalable, et donc si une requête est faite pour une ressource avec des informations d'identification, si cet en-tête n'est pas renvoyé avec la ressource, la réponse est ignorée par le navigateur et n'est pas renvoyée au contenu web. */ - public function setOrigin($origin): self - { - $this->config['AllowOrigin'] = $origin; + public bool $supportsCredentials = false; - return $this; - } + protected CorsBuilder $cors; - /** - * Execution du middleware + /** + * Constructor. */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __construct(array $config = []) { - $response = $handler->handle($request); - - if ($request->getHeaderLine('Origin')) { - $response = $response - ->withHeader('Access-Control-Allow-Origin', $this->_allowOrigin($request)) - ->withHeader('Access-Control-Allow-Credentials', $this->_allowCredentials()) - ->withHeader('Access-Control-Max-Age', $this->_maxAge()) - ->withHeader('Access-Control-Expose-Headers', $this->_exposeHeaders()); - - if (strtoupper($request->getMethod()) === 'OPTIONS') { - $response = $response - ->withHeader('Access-Control-Allow-Headers', $this->_allowHeaders($request)) - ->withHeader('Access-Control-Allow-Methods', $this->_allowMethods()) - ->withStatus(200); - } - } - - return $response; + $params = (array) config('cors', []); + $config = array_merge($params, $config); + + foreach ($config as $key => $value) { + $key = Text::camel($key); + + if (property_exists($this, $key)) { + $this->{$key} = $value; + } + } + + $this->cors = new CorsBuilder([ + 'allowedHeaders' => $this->allowedHeaders, + 'allowedMethods' => $this->allowedMethods, + 'allowedOrigins' => $this->allowedOrigins, + 'allowedOriginsPatterns' => $this->allowedOriginsPatterns, + 'exposedHeaders' => $this->exposedHeaders, + 'maxAge' => $this->maxAge, + 'supportsCredentials' => $this->supportsCredentials, + ]); } - /** - * Recupere les origines autorisees + /** + * Execution du middleware */ - private function _allowOrigin(ServerRequestInterface $request) - { - $allowOrigin = $this->config['AllowOrigin']; - $origin = $request->getHeaderLine('Origin'); + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ($this->cors->isPreflightRequest($request)) { + $response = $this->cors->handlePreflightRequest($request); - if ($allowOrigin === true || $allowOrigin === '*') { - return $origin; + return $this->cors->varyHeader($response, 'Access-Control-Request-Method'); } - if (is_array($allowOrigin)) { - $origin = (array) $origin; - - foreach ($origin as $o) { - if (in_array($o, $allowOrigin, true)) { - return $origin; - } - } + $response = $handler->handle($request); - return ''; + if ($request->getMethod() === 'OPTIONS') { + $response = $this->cors->varyHeader($response, 'Access-Control-Request-Method'); } - return (string) $allowOrigin; - } - - /** - * Autorise t-on les identifications ? - */ - private function _allowCredentials(): string - { - return ($this->config['AllowCredentials']) ? 'true' : 'false'; - } - - /** - * Recupere les methodes autorisees - */ - private function _allowMethods(): string - { - return implode(', ', (array) $this->config['AllowMethods']); - } - - /** - * Recupere les entetes autorises - */ - private function _allowHeaders(ServerRequestInterface $request): string - { - $allowHeaders = $this->config['AllowHeaders']; - - if ($allowHeaders === true) { - return $request->getHeaderLine('Access-Control-Request-Headers'); - } - - return implode(', ', (array) $allowHeaders); - } - - /** - * Recupere les entetes exposes par l'application - */ - private function _exposeHeaders(): string - { - $exposeHeaders = $this->config['ExposeHeaders']; - - if (is_string($exposeHeaders) || is_array($exposeHeaders)) { - return implode(', ', (array) $exposeHeaders); + if (! $response->hasHeader('Access-Control-Allow-Origin')) { + $response = $this->cors->addActualRequestHeaders($request, $response); } - return ''; - } - - /** - * Recupere la duree de mise en cache des donnees - */ - private function _maxAge(): string - { - $maxAge = (string) $this->config['MaxAge']; - - return ($maxAge) ?: '0'; - } + return $response; + } } From da6fcc615ad08705e0beeac027683588b720232d Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 20 Dec 2023 18:24:40 +0100 Subject: [PATCH 106/111] cs-fix --- src/Config/Config.php | 2 +- src/Http/CorsBuilder.php | 64 +++---- src/Http/ServerRequest.php | 18 +- src/Http/ServerRequestFactory.php | 131 ++++++------- src/Http/UploadedFileFactory.php | 299 ++++++++++++++++-------------- src/Middlewares/Cors.php | 48 ++--- 6 files changed, 280 insertions(+), 282 deletions(-) diff --git a/src/Config/Config.php b/src/Config/Config.php index 5ac73181..796c5868 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -148,7 +148,7 @@ public function ghost(array|string $key, ?Schema $schema = null): static { $this->load($key, null, $schema, true); - return $this; + return $this; } /** diff --git a/src/Http/CorsBuilder.php b/src/Http/CorsBuilder.php index c3e6d57a..2746e2cf 100644 --- a/src/Http/CorsBuilder.php +++ b/src/Http/CorsBuilder.php @@ -11,8 +11,8 @@ namespace BlitzPHP\Http; -use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; /** * @credit CodeIgniter4 Cors Fluent\Cors\ServiceCors @@ -38,51 +38,40 @@ protected function normalizeOptions(array $options = []): array 'maxAge' => 0, ], $options); + // Normaliser la casse + $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']); - // Normalize case - $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']); - - - // normalize ['*'] to true - if (in_array('*', $options['allowedOrigins'])) { + // normalizer ['*'] en true + if (in_array('*', $options['allowedOrigins'], true)) { $options['allowedOrigins'] = true; } - if (in_array('*', $options['allowedHeaders'])) { + if (in_array('*', $options['allowedHeaders'], true)) { $options['allowedHeaders'] = true; } - if (in_array('*', $options['allowedMethods'])) { + if (in_array('*', $options['allowedMethods'], true)) { $options['allowedMethods'] = true; } return $options; } - /** - * {@inheritdoc} - */ public function isCorsRequest(ServerRequestInterface $request): bool { - return $request->hasHeader('Origin') && !$this->isSameHost($request); + return $request->hasHeader('Origin') && ! $this->isSameHost($request); } - /** - * {@inheritdoc} - */ public function isPreflightRequest(ServerRequestInterface $request): bool { - return strtoupper($request->getMethod()) === 'OPTIONS' && $request->hasHeader('Access-Control-Request-Method'); + return strtoupper($request->getMethod()) === 'OPTIONS' && $request->hasHeader('Access-Control-Request-Method'); } - /** - * {@inheritdoc} - */ public function handlePreflightRequest(ServerRequestInterface $request): ResponseInterface { $response = new Response(); $response = $response->withStatus(204); - return $this->addPreflightRequestHeaders($request, $response); + return $this->addPreflightRequestHeaders($request, $response); } public function addPreflightRequestHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface @@ -99,9 +88,6 @@ public function addPreflightRequestHeaders(ServerRequestInterface $request, Resp return $response; } - /** - * {@inheritdoc} - */ public function isOriginAllowed(ServerRequestInterface $request): bool { if ($this->options['allowedOrigins'] === true) { @@ -114,7 +100,7 @@ public function isOriginAllowed(ServerRequestInterface $request): bool $origin = $request->getHeaderLine('Origin'); - if (in_array($origin, $this->options['allowedOrigins'])) { + if (in_array($origin, $this->options['allowedOrigins'], true)) { return true; } @@ -127,9 +113,6 @@ public function isOriginAllowed(ServerRequestInterface $request): bool return false; } - /** - * {@inheritdoc} - */ public function addActualRequestHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $response = $this->configureAllowedOrigin($request, $response); @@ -142,14 +125,11 @@ public function addActualRequestHeaders(ServerRequestInterface $request, Respons return $response; } - /** - * {@inheritdoc} - */ public function varyHeader(ResponseInterface $response, $header): ResponseInterface { if (! $response->hasHeader('Vary')) { $response = $response->withHeader('Vary', $header); - } elseif (! in_array($header, explode(', ', $response->getHeaderLine('Vary')))) { + } elseif (! in_array($header, explode(', ', $response->getHeaderLine('Vary')), true)) { $response = $response->withHeader('Vary', $response->getHeaderLine('Vary') . ', ' . $header); } @@ -159,13 +139,13 @@ public function varyHeader(ResponseInterface $response, $header): ResponseInterf protected function configureAllowedOrigin(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { if ($this->options['allowedOrigins'] === true && ! $this->options['supportsCredentials']) { - // Safe+cacheable, allow everything + // Sûr+cacheable, tout autoriser $response = $response->withHeader('Access-Control-Allow-Origin', '*'); - } else if ($this->isSingleOriginAllowed()) { - // Single origins can be safely set + } elseif ($this->isSingleOriginAllowed()) { + // Les origines uniques peuvent être définies en toute sécurité $response = $response->withHeader('Access-Control-Allow-Origin', array_values($this->options['allowedOrigins'])[0]); } else { - // For dynamic headers, set the requested Origin header when set and allowed + // Pour les en-têtes dynamiques, définir l'en-tête Origin demandé lorsqu'il est défini et autorisé. if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) { $response = $response->withHeader('Access-Control-Allow-Origin', (string) $request->getHeaderLine('Origin')); } @@ -173,7 +153,7 @@ protected function configureAllowedOrigin(ServerRequestInterface $request, Respo $response = $this->varyHeader($response, 'Origin'); } - return $response; + return $response; } protected function isSingleOriginAllowed(): bool @@ -189,7 +169,7 @@ protected function configureAllowedMethods(ServerRequestInterface $request, Resp { if ($this->options['allowedMethods'] === true) { $allowMethods = strtoupper($request->getHeaderLine('Access-Control-Request-Method')); - $response = $this->varyHeader($response, 'Access-Control-Request-Method'); + $response = $this->varyHeader($response, 'Access-Control-Request-Method'); } else { $allowMethods = implode(', ', $this->options['allowedMethods']); } @@ -201,7 +181,7 @@ protected function configureAllowedHeaders(ServerRequestInterface $request, Resp { if ($this->options['allowedHeaders'] === true) { $allowHeaders = $request->getHeaderLine('Access-Control-Request-Headers'); - $response = $this->varyHeader($response, 'Access-Control-Request-Headers'); + $response = $this->varyHeader($response, 'Access-Control-Request-Headers'); } else { $allowHeaders = implode(', ', $this->options['allowedHeaders']); } @@ -215,7 +195,7 @@ protected function configureAllowCredentials(ServerRequestInterface $request, Re $response = $response->withHeader('Access-Control-Allow-Credentials', 'true'); } - return $response; + return $response; } protected function configureExposedHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface @@ -224,7 +204,7 @@ protected function configureExposedHeaders(ServerRequestInterface $request, Resp $response = $response->withHeader('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders'])); } - return $response; + return $response; } protected function configureMaxAge(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface @@ -233,7 +213,7 @@ protected function configureMaxAge(ServerRequestInterface $request, ResponseInte $response = $response->withHeader('Access-Control-Max-Age', (string) $this->options['maxAge']); } - return $response; + return $response; } protected function isSameHost(ServerRequestInterface $request): bool diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 85cb563e..030a2441 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -237,13 +237,13 @@ protected function _setConfig(array $config): void $uri = $config['uri']; } else { if ($config['url'] !== '') { - $config = $this->processUrlOption($config); - $uri = new Uri(implode('?', [$config['url'], $config['environment']['QUERY_STRING'] ?? ''])); - } else if (isset($config['environment']['REQUEST_URI'])) { - $uri = new Uri($config['environment']['REQUEST_URI']); - } else { - $uri = Psr7ServerRequest::getUriFromGlobals(); - } + $config = $this->processUrlOption($config); + $uri = new Uri(implode('?', [$config['url'], $config['environment']['QUERY_STRING'] ?? ''])); + } elseif (isset($config['environment']['REQUEST_URI'])) { + $uri = new Uri($config['environment']['REQUEST_URI']); + } else { + $uri = Psr7ServerRequest::getUriFromGlobals(); + } } if (in_array($uri->getHost(), ['localhost', '127.0.0.1'], true)) { @@ -266,7 +266,7 @@ protected function _setConfig(array $config): void $this->stream = $stream; $post = $config['post']; - if (!(is_array($post) || is_object($post) || $post === null)) { + if (! (is_array($post) || is_object($post) || $post === null)) { throw new InvalidArgumentException(sprintf( 'La clé `post` doit être un tableau, un objet ou null. On a obtenu `%s` à la place.', get_debug_type($post) @@ -786,7 +786,7 @@ public function getHeader(string $name): array return (array) $this->_environment[$name]; } - $name = $this->normalizeHeaderName($name); + $name = $this->normalizeHeaderName($name); if (isset($this->_environment[$name])) { return (array) $this->_environment[$name]; } diff --git a/src/Http/ServerRequestFactory.php b/src/Http/ServerRequestFactory.php index 1d816daa..ce81b0d6 100644 --- a/src/Http/ServerRequestFactory.php +++ b/src/Http/ServerRequestFactory.php @@ -12,6 +12,7 @@ namespace BlitzPHP\Http; use BlitzPHP\Utilities\Iterable\Arr; +use InvalidArgumentException; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -26,18 +27,18 @@ */ class ServerRequestFactory implements ServerRequestFactoryInterface { - /** + /** * Créer une requête à partir des valeurs superglobales fournies. * * Si un argument n'est pas fourni, la valeur superglobale correspondante sera utilisée. * - * @param array|null $server superglobale $_SERVER - * @param array|null $query superglobale $_GET + * @param array|null $server superglobale $_SERVER + * @param array|null $query superglobale $_GET * @param array|null $parsedBody superglobale $_POST - * @param array|null $cookies superglobale $_COOKIE - * @param array|null $files superglobale $_FILES - * - * @throws \InvalidArgumentException pour les valeurs de fichier non valides + * @param array|null $cookies superglobale $_COOKIE + * @param array|null $files superglobale $_FILES + * + * @throws InvalidArgumentException pour les valeurs de fichier non valides */ public static function fromGlobals( ?array $server = null, @@ -49,18 +50,18 @@ public static function fromGlobals( $server = self::normalizeServer($server ?? $_SERVER); $request = new Request([ - 'environment' => $server, - 'cookies' => $cookies ?? $_COOKIE, - 'query' => $query ?? $_GET, - 'session' => service('session'), - 'input' => $server['BLITZPHP_INPUT'] ?? null, + 'environment' => $server, + 'cookies' => $cookies ?? $_COOKIE, + 'query' => $query ?? $_GET, + 'session' => service('session'), + 'input' => $server['BLITZPHP_INPUT'] ?? null, ]); $request = static::processBodyAndRequestMethod($parsedBody ?? $_POST, $request); // Ceci est nécessaire car `ServerRequest::scheme()` ignore la valeur de `HTTP_X_FORWARDED_PROTO` - // à moins que `trustProxy` soit activé, alors que l'instance `Uri` initialement créée prend - // toujours en compte les valeurs de HTTP_X_FORWARDED_PROTO`. - $uri = $request->getUri()->withScheme($request->scheme()); + // à moins que `trustProxy` soit activé, alors que l'instance `Uri` initialement créée prend + // toujours en compte les valeurs de HTTP_X_FORWARDED_PROTO`. + $uri = $request->getUri()->withScheme($request->scheme()); $request = $request->withUri($uri, true); return static::processFiles($files ?? $_FILES, $request); @@ -68,7 +69,7 @@ public static function fromGlobals( /** * Définit la variable d'environnement REQUEST_METHOD en fonction de la valeur HTTP simulée _method. - * La valeur 'ORIGINAL_REQUEST_METHOD' est également préservée, si vous souhaitez lire la méthode HTTP non simulée utilisée par le client. + * La valeur 'ORIGINAL_REQUEST_METHOD' est également préservée, si vous souhaitez lire la méthode HTTP non simulée utilisée par le client. * * Le corps de la requête de type "application/x-www-form-urlencoded" est analysé dans un tableau pour les requêtes PUT/PATCH/DELETE. * @@ -76,16 +77,16 @@ public static function fromGlobals( */ protected static function processBodyAndRequestMethod(array $parsedBody, Request $request): Request { - $method = $request->getMethod(); + $method = $request->getMethod(); $override = false; - if (in_array($method, ['PUT', 'DELETE', 'PATCH'], true) && str_starts_with((string)$request->contentType(), 'application/x-www-form-urlencoded')) { - $data = (string)$request->getBody(); + if (in_array($method, ['PUT', 'DELETE', 'PATCH'], true) && str_starts_with((string) $request->contentType(), 'application/x-www-form-urlencoded')) { + $data = (string) $request->getBody(); parse_str($data, $parsedBody); } if ($request->hasHeader('X-Http-Method-Override')) { $parsedBody['_method'] = $request->getHeaderLine('X-Http-Method-Override'); - $override = true; + $override = true; } $request = $request->withEnv('ORIGINAL_REQUEST_METHOD', $method); @@ -95,7 +96,7 @@ protected static function processBodyAndRequestMethod(array $parsedBody, Request $override = true; } - if ($override && !in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH'], true)) { + if ($override && ! in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH'], true)) { $parsedBody = []; } @@ -109,11 +110,11 @@ protected static function processBodyAndRequestMethod(array $parsedBody, Request */ protected static function processFiles(array $files, Request $request): Request { - $files = UploadedFileFactory::normalizeUploadedFiles($files); + $files = UploadedFileFactory::normalizeUploadedFiles($files); $request = $request->withUploadedFiles($files); $parsedBody = $request->getParsedBody(); - if (!is_array($parsedBody)) { + if (! is_array($parsedBody)) { return $request; } @@ -129,15 +130,15 @@ protected static function processFiles(array $files, Request $request): Request * des valeurs données n'est effectué, et, en particulier, aucune tentative n'est faite pour * déterminer la méthode HTTP ou l'URI, qui doivent être fournis explicitement. * - * @param string $method La méthode HTTP associée à la requete. - * @param \Psr\Http\Message\UriInterface|string $uri L'URI associé à la requete. - * Si la valeur est une chaîne, la fabrique DOIT créer une instance d'UriInterface basée sur celle-ci. - * @param array $serverParams Tableau de paramètres SAPI permettant d'alimenter l'instance de requete générée. + * @param string $method La méthode HTTP associée à la requete. + * @param \Psr\Http\Message\UriInterface|string $uri L'URI associé à la requete. + * Si la valeur est une chaîne, la fabrique DOIT créer une instance d'UriInterface basée sur celle-ci. + * @param array $serverParams Tableau de paramètres SAPI permettant d'alimenter l'instance de requete générée. */ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface { $serverParams['REQUEST_METHOD'] = $method; - $options = ['environment' => $serverParams]; + $options = ['environment' => $serverParams]; if (is_string($uri)) { $uri = new Uri($uri); @@ -147,39 +148,43 @@ public function createServerRequest(string $method, $uri, array $serverParams = return new Request($options); } - /** - * Marshaller le tableau $_SERVER - * - * Prétraite et renvoie la superglobale $_SERVER. - * En particulier, il tente de détecter l'en-tête Authorization, qui n'est souvent pas agrégé correctement sous diverses combinaisons SAPI/httpd. - * - * @param null|callable $apacheRequestHeaderCallback Callback qui peut être utilisé pour récupérer les en-têtes de requête Apache. - * La valeur par défaut est `apache_request_headers` sous Apache mod_php. - * @see https://github.com/laminas/laminas-diactoros/blob/3.4.x/src/functions/normalize_server.php - * @return array Soit $server mot pour mot, soit avec un en-tête HTTP_AUTHORIZATION ajouté. - */ - private static function normalizeServer(array $server, ?callable $apacheRequestHeaderCallback = null): array - { - if (null === $apacheRequestHeaderCallback && is_callable('apache_request_headers')) { - $apacheRequestHeaderCallback = 'apache_request_headers'; - } - - // Si la valeur HTTP_AUTHORIZATION est déjà définie, ou si le callback n'est pas appelable, nous renvoyons les parameters server sans changements - if (isset($server['HTTP_AUTHORIZATION']) || ! is_callable($apacheRequestHeaderCallback)) { - return $server; - } - - $apacheRequestHeaders = $apacheRequestHeaderCallback(); - if (isset($apacheRequestHeaders['Authorization'])) { - $server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['Authorization']; - return $server; - } - - if (isset($apacheRequestHeaders['authorization'])) { - $server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['authorization']; - return $server; - } - - return $server; - } + /** + * Marshaller le tableau $_SERVER + * + * Prétraite et renvoie la superglobale $_SERVER. + * En particulier, il tente de détecter l'en-tête Authorization, qui n'est souvent pas agrégé correctement sous diverses combinaisons SAPI/httpd. + * + * @param callable|null $apacheRequestHeaderCallback Callback qui peut être utilisé pour récupérer les en-têtes de requête Apache. + * La valeur par défaut est `apache_request_headers` sous Apache mod_php. + * + * @see https://github.com/laminas/laminas-diactoros/blob/3.4.x/src/functions/normalize_server.php + * + * @return array Soit $server mot pour mot, soit avec un en-tête HTTP_AUTHORIZATION ajouté. + */ + private static function normalizeServer(array $server, ?callable $apacheRequestHeaderCallback = null): array + { + if (null === $apacheRequestHeaderCallback && is_callable('apache_request_headers')) { + $apacheRequestHeaderCallback = 'apache_request_headers'; + } + + // Si la valeur HTTP_AUTHORIZATION est déjà définie, ou si le callback n'est pas appelable, nous renvoyons les parameters server sans changements + if (isset($server['HTTP_AUTHORIZATION']) || ! is_callable($apacheRequestHeaderCallback)) { + return $server; + } + + $apacheRequestHeaders = $apacheRequestHeaderCallback(); + if (isset($apacheRequestHeaders['Authorization'])) { + $server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['Authorization']; + + return $server; + } + + if (isset($apacheRequestHeaders['authorization'])) { + $server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['authorization']; + + return $server; + } + + return $server; + } } diff --git a/src/Http/UploadedFileFactory.php b/src/Http/UploadedFileFactory.php index ab28a06a..359e6491 100644 --- a/src/Http/UploadedFileFactory.php +++ b/src/Http/UploadedFileFactory.php @@ -25,18 +25,20 @@ class UploadedFileFactory implements UploadedFileFactoryInterface { /** - * Créer un nouveau fichier téléchargé. + * Créer un nouveau fichier téléchargé. * * Si une taille n'est pas fournie, elle sera déterminée en vérifiant la taille du flux. * - * @link http://php.net/manual/features.file-upload.post-method.php - * @link http://php.net/manual/features.file-upload.errors.php - * @param \Psr\Http\Message\StreamInterface $stream Le flux sous-jacent représentant le contenu du fichier téléchargé. - * @param int|null $size La taille du fichier en octets. - * @param int $error L'erreur de téléchargement du fichier PHP. - * @param string|null $clientFilename Le nom du fichier tel qu'il est fourni par le client, le cas échéant. - * @param string|null $clientMediaType Le type de média tel qu'il est fourni par le client, le cas échéant. - * @throws \InvalidArgumentException Si la ressource du fichier n'est pas lisible. + * @see http://php.net/manual/features.file-upload.post-method.php + * @see http://php.net/manual/features.file-upload.errors.php + * + * @param \Psr\Http\Message\StreamInterface $stream Le flux sous-jacent représentant le contenu du fichier téléchargé. + * @param int|null $size La taille du fichier en octets. + * @param int $error L'erreur de téléchargement du fichier PHP. + * @param string|null $clientFilename Le nom du fichier tel qu'il est fourni par le client, le cas échéant. + * @param string|null $clientMediaType Le type de média tel qu'il est fourni par le client, le cas échéant. + * + * @throws InvalidArgumentException Si la ressource du fichier n'est pas lisible. */ public function createUploadedFile( StreamInterface $stream, @@ -52,141 +54,152 @@ public function createUploadedFile( return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); } - /** - * Créer une instance de fichier téléchargé à partir d'un tableau de valeurs. - * - * @param array $spec Une seule entrée $_FILES. - * @throws InvalidArgumentException Si une ou plusieurs des clés tmp_name, size ou error sont manquantes dans $spec. - */ - public static function makeUploadedFile(array $spec): UploadedFile - { - if (! isset($spec['tmp_name']) || ! isset($spec['size']) || ! isset($spec['error'])) { - throw new InvalidArgumentException(sprintf( - '$spec fourni à %s DOIT contenir chacune des clés "tmp_name", "size", et "error" ; une ou plusieurs étaient manquantes', - __FUNCTION__ - )); - } - - return new UploadedFile( - $spec['tmp_name'], - (int) $spec['size'], - $spec['error'], - $spec['name'] ?? null, - $spec['type'] ?? null - ); - } - - /** - * Normaliser les fichiers téléchargés - * - * Transforme chaque valeur en une instance UploadedFile, et s'assure que les tableaux imbriqués sont normalisés. - * - * @see https://github.com/laminas/laminas-diactoros/blob/3.4.x/src/functions/normalize_uploaded_files.php - * @return UploadedFileInterface[] - * @throws InvalidArgumentException Pour les valeurs non reconnues. - */ + /** + * Créer une instance de fichier téléchargé à partir d'un tableau de valeurs. + * + * @param array $spec Une seule entrée $_FILES. + * + * @throws InvalidArgumentException Si une ou plusieurs des clés tmp_name, size ou error sont manquantes dans $spec. + */ + public static function makeUploadedFile(array $spec): UploadedFile + { + if (! isset($spec['tmp_name']) || ! isset($spec['size']) || ! isset($spec['error'])) { + throw new InvalidArgumentException(sprintf( + '$spec fourni à %s DOIT contenir chacune des clés "tmp_name", "size", et "error" ; une ou plusieurs étaient manquantes', + __FUNCTION__ + )); + } + + return new UploadedFile( + $spec['tmp_name'], + (int) $spec['size'], + $spec['error'], + $spec['name'] ?? null, + $spec['type'] ?? null + ); + } + + /** + * Normaliser les fichiers téléchargés + * + * Transforme chaque valeur en une instance UploadedFile, et s'assure que les tableaux imbriqués sont normalisés. + * + * @see https://github.com/laminas/laminas-diactoros/blob/3.4.x/src/functions/normalize_uploaded_files.php + * + * @return UploadedFileInterface[] + * + * @throws InvalidArgumentException Pour les valeurs non reconnues. + */ public static function normalizeUploadedFiles(array $files): array - { - /** - * Traverse une arborescence imbriquée de spécifications de fichiers téléchargés. - * - * @param string[]|array[] $tmpNameTree - * @param int[]|array[] $sizeTree - * @param int[]|array[] $errorTree - * @param string[]|array[]|null $nameTree - * @param string[]|array[]|null $typeTree - * @return UploadedFile[]|array[] - */ - $recursiveNormalize = static function ( - array $tmpNameTree, - array $sizeTree, - array $errorTree, - ?array $nameTree = null, - ?array $typeTree = null - ) use (&$recursiveNormalize): array { - $normalized = []; - foreach ($tmpNameTree as $key => $value) { - if (is_array($value)) { - // Traverse - $normalized[$key] = $recursiveNormalize( - $tmpNameTree[$key], - $sizeTree[$key], - $errorTree[$key], - $nameTree[$key] ?? null, - $typeTree[$key] ?? null - ); - continue; - } - - $normalized[$key] = static::makeUploadedFile([ - 'tmp_name' => $tmpNameTree[$key], - 'size' => $sizeTree[$key], - 'error' => $errorTree[$key], - 'name' => $nameTree[$key] ?? null, - 'type' => $typeTree[$key] ?? null, - ]); - } - - return $normalized; - }; - - /** - * Normaliser un tableau de spécifications de fichiers. - * - * Boucle sur tous les fichiers imbriqués (déterminés par la réception d'un tableau à la clé `tmp_name` d'une spécification `$_FILES`) et renvoie un tableau normalisé d'instances UploadedFile. - * - * Cette fonction normalise un tableau `$_FILES` représentant un ensemble imbriqué de fichiers téléchargés tels que produits par les SAPI php-fpm, CGI SAPI, ou mod_php SAPI. - * - * @return UploadedFile[] - */ - $normalizeUploadedFileSpecification = static function (array $files = []) use (&$recursiveNormalize): array { - if ( - ! isset($files['tmp_name']) || ! is_array($files['tmp_name']) - || ! isset($files['size']) || ! is_array($files['size']) - || ! isset($files['error']) || ! is_array($files['error']) - ) { - throw new InvalidArgumentException(sprintf( - 'Les fichiers fournis à %s DOIVENT contenir chacune des clés "tmp_name", "size" et "error", + { + /** + * Traverse une arborescence imbriquée de spécifications de fichiers téléchargés. + * + * @param array[]|string[] $tmpNameTree + * @param array[]|int[] $sizeTree + * @param array[]|int[] $errorTree + * @param array[]|string[]|null $nameTree + * @param array[]|string[]|null $typeTree + * + * @return array[]|UploadedFile[] + */ + $recursiveNormalize = static function ( + array $tmpNameTree, + array $sizeTree, + array $errorTree, + ?array $nameTree = null, + ?array $typeTree = null + ) use (&$recursiveNormalize): array { + $normalized = []; + + foreach ($tmpNameTree as $key => $value) { + if (is_array($value)) { + // Traverse + $normalized[$key] = $recursiveNormalize( + $tmpNameTree[$key], + $sizeTree[$key], + $errorTree[$key], + $nameTree[$key] ?? null, + $typeTree[$key] ?? null + ); + + continue; + } + + $normalized[$key] = static::makeUploadedFile([ + 'tmp_name' => $tmpNameTree[$key], + 'size' => $sizeTree[$key], + 'error' => $errorTree[$key], + 'name' => $nameTree[$key] ?? null, + 'type' => $typeTree[$key] ?? null, + ]); + } + + return $normalized; + }; + + /** + * Normaliser un tableau de spécifications de fichiers. + * + * Boucle sur tous les fichiers imbriqués (déterminés par la réception d'un tableau à la clé `tmp_name` d'une spécification `$_FILES`) et renvoie un tableau normalisé d'instances UploadedFile. + * + * Cette fonction normalise un tableau `$_FILES` représentant un ensemble imbriqué de fichiers téléchargés tels que produits par les SAPI php-fpm, CGI SAPI, ou mod_php SAPI. + * + * @return UploadedFile[] + */ + $normalizeUploadedFileSpecification = static function (array $files = []) use (&$recursiveNormalize): array { + if ( + ! isset($files['tmp_name']) || ! is_array($files['tmp_name']) + || ! isset($files['size']) || ! is_array($files['size']) + || ! isset($files['error']) || ! is_array($files['error']) + ) { + throw new InvalidArgumentException(sprintf( + 'Les fichiers fournis à %s DOIVENT contenir chacune des clés "tmp_name", "size" et "error", chacune étant représentée sous la forme d\'un tableau ; une ou plusieurs valeurs manquaient ou n\'étaient pas des tableaux.', - __FUNCTION__ - )); - } - - return $recursiveNormalize( - $files['tmp_name'], - $files['size'], - $files['error'], - $files['name'] ?? null, - $files['type'] ?? null - ); - }; - - $normalized = []; - foreach ($files as $key => $value) { - if ($value instanceof UploadedFileInterface) { - $normalized[$key] = $value; - continue; - } - - if (is_array($value) && isset($value['tmp_name']) && is_array($value['tmp_name'])) { - $normalized[$key] = $normalizeUploadedFileSpecification($value); - continue; - } - - if (is_array($value) && isset($value['tmp_name'])) { - $normalized[$key] = self::makeUploadedFile($value); - continue; - } - - if (is_array($value)) { - $normalized[$key] = self::normalizeUploadedFiles($value); - continue; - } - - throw new InvalidArgumentException('Valeur non valide dans la spécification des fichiers'); - } - - return $normalized; - } + __FUNCTION__ + )); + } + + return $recursiveNormalize( + $files['tmp_name'], + $files['size'], + $files['error'], + $files['name'] ?? null, + $files['type'] ?? null + ); + }; + + $normalized = []; + + foreach ($files as $key => $value) { + if ($value instanceof UploadedFileInterface) { + $normalized[$key] = $value; + + continue; + } + + if (is_array($value) && isset($value['tmp_name']) && is_array($value['tmp_name'])) { + $normalized[$key] = $normalizeUploadedFileSpecification($value); + + continue; + } + + if (is_array($value) && isset($value['tmp_name'])) { + $normalized[$key] = self::makeUploadedFile($value); + + continue; + } + + if (is_array($value)) { + $normalized[$key] = self::normalizeUploadedFiles($value); + + continue; + } + + throw new InvalidArgumentException('Valeur non valide dans la spécification des fichiers'); + } + + return $normalized; + } } diff --git a/src/Middlewares/Cors.php b/src/Middlewares/Cors.php index 1e370f78..b386d20a 100644 --- a/src/Middlewares/Cors.php +++ b/src/Middlewares/Cors.php @@ -13,8 +13,8 @@ use BlitzPHP\Http\CorsBuilder; use BlitzPHP\Utilities\String\Text; -use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -26,7 +26,7 @@ */ class Cors implements MiddlewareInterface { - /** + /** * -------------------------------------------------------------------------- * En-têtes HTTP autorisés * -------------------------------------------------------------------------- @@ -87,28 +87,28 @@ class Cors implements MiddlewareInterface * -------------------------------------------------------------------------- * * Indique si la réponse à la demande peut être exposée lorsque l'indicateur d'informations d'identification est vrai. - * Lorsqu'il est utilisé dans le cadre d'une réponse à une demande de contrôle en amont, il indique si la demande proprement dite peut être effectuée en utilisant des informations d'identification. - * Notez que les requêtes GET simples ne sont pas contrôlées au préalable, et donc si une requête est faite pour une ressource avec des informations d'identification, si cet en-tête n'est pas renvoyé avec la ressource, la réponse est ignorée par le navigateur et n'est pas renvoyée au contenu web. + * Lorsqu'il est utilisé dans le cadre d'une réponse à une demande de contrôle en amont, il indique si la demande proprement dite peut être effectuée en utilisant des informations d'identification. + * Notez que les requêtes GET simples ne sont pas contrôlées au préalable, et donc si une requête est faite pour une ressource avec des informations d'identification, si cet en-tête n'est pas renvoyé avec la ressource, la réponse est ignorée par le navigateur et n'est pas renvoyée au contenu web. */ public bool $supportsCredentials = false; - protected CorsBuilder $cors; + protected CorsBuilder $cors; - /** + /** * Constructor. */ public function __construct(array $config = []) { - $params = (array) config('cors', []); - $config = array_merge($params, $config); + $params = (array) config('cors', []); + $config = array_merge($params, $config); - foreach ($config as $key => $value) { - $key = Text::camel($key); + foreach ($config as $key => $value) { + $key = Text::camel($key); - if (property_exists($this, $key)) { - $this->{$key} = $value; - } - } + if (property_exists($this, $key)) { + $this->{$key} = $value; + } + } $this->cors = new CorsBuilder([ 'allowedHeaders' => $this->allowedHeaders, @@ -121,27 +121,27 @@ public function __construct(array $config = []) ]); } - /** + /** * Execution du middleware */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - if ($this->cors->isPreflightRequest($request)) { + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ($this->cors->isPreflightRequest($request)) { $response = $this->cors->handlePreflightRequest($request); return $this->cors->varyHeader($response, 'Access-Control-Request-Method'); } - $response = $handler->handle($request); + $response = $handler->handle($request); - if ($request->getMethod() === 'OPTIONS') { + if ($request->getMethod() === 'OPTIONS') { $response = $this->cors->varyHeader($response, 'Access-Control-Request-Method'); } - if (! $response->hasHeader('Access-Control-Allow-Origin')) { - $response = $this->cors->addActualRequestHeaders($request, $response); + if (! $response->hasHeader('Access-Control-Allow-Origin')) { + $response = $this->cors->addActualRequestHeaders($request, $response); } - return $response; - } + return $response; + } } From c29f2bd78e0159826b34c5172bfbf22abe5f6ea6 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 21 Dec 2023 17:05:59 +0100 Subject: [PATCH 107/111] =?UTF-8?q?Refonte=20de=20la=20classe=20Facade=20e?= =?UTF-8?q?t=20ajout=20de=20tests=20pour=20diff=C3=A9rents=20sc=C3=A9nario?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La classe Facade a été remaniée pour gérer différents scénarios lors de l'accès aux méthodes. Des tests ont été ajoutés pour s'assurer que la méthode d'accès renvoie l'objet ou la chaîne attendus. Si l'accesseur renvoie une chaîne, il est maintenant résolu en tant que service. Une exception est levée si l'accesseur renvoie une chaîne qui ne peut être résolue ou s'il renvoie un non-objet. --- .../system/framework/Facades/Facades.spec.php | 108 +++++++++++++++++- src/Facades/Facade.php | 14 ++- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/spec/system/framework/Facades/Facades.spec.php b/spec/system/framework/Facades/Facades.spec.php index cda27b25..62f43218 100644 --- a/spec/system/framework/Facades/Facades.spec.php +++ b/spec/system/framework/Facades/Facades.spec.php @@ -11,6 +11,7 @@ use BlitzPHP\Container\Container as ContainerContainer; use BlitzPHP\Facades\Container; +use BlitzPHP\Facades\Facade; use BlitzPHP\Facades\Fs; use BlitzPHP\Facades\Route; use BlitzPHP\Facades\Storage; @@ -20,8 +21,113 @@ use BlitzPHP\Router\RouteBuilder; use BlitzPHP\Spec\ReflectionHelper; use BlitzPHP\View\View as ViewView; +use DI\NotFoundException; describe('Facades', function () { + describe('Facade', function () { + it('Accessor retourne un objet', function () { + $class = new class() extends Facade { + protected static function accessor(): object + { + return new stdClass(); + } + }; + + expect(ReflectionHelper::getPrivateMethodInvoker($class, 'accessor')())->toBeAnInstanceOf(stdClass::class); + }); + + it('Accessor retourne un string', function () { + $class = new class() extends Facade { + protected static function accessor(): string + { + return 'fs'; + } + }; + + expect(ReflectionHelper::getPrivateMethodInvoker($class, 'accessor')())->toBe('fs'); + }); + + it('__call et __callStatic fonctionnent', function () { + $class = new class() extends Facade { + protected static function accessor(): string + { + return 'fs'; + } + }; + + expect(ReflectionHelper::getPrivateMethodInvoker($class, 'accessor')())->toBe('fs'); + expect($class->exists(__FILE__))->toBeTruthy(); + expect($class::exists(__FILE__))->toBeTruthy(); + }); + + it('__callStatic genere une erreur si accessor renvoie une chaine qui ne peut pas etre resourdre par le fournisseur de service', function () { + $class = new class() extends Facade { + protected static function accessor(): string + { + return 'fss'; + } + }; + + expect(ReflectionHelper::getPrivateMethodInvoker($class, 'accessor')())->toBe('fss'); + expect(fn() => $class::exists(__FILE__))->toThrow(new NotFoundException("No entry or class found for 'fss'")); + }); + + it('__callStatic genere une erreur si accessor renvoie une chaine qui peut etre resourdre par le fournisseur de service mais n\'est pas un objet', function () { + Container::set('fss', __FILE__); + $class = new class() extends Facade { + protected static function accessor(): string + { + return 'fss'; + } + }; + + expect(ReflectionHelper::getPrivateMethodInvoker($class, 'accessor')())->toBe('fss'); + expect(fn() => $class::test())->toThrow(new InvalidArgumentException()); + }); + + it('__callStatic fonctionne normalement si accessor renvoie une chaine qui peut etre resourdre par le fournisseur de service', function () { + Container::set('fss', new class() { + public function test() { + return true; + } + }); + $class = new class() extends Facade { + protected static function accessor(): string + { + return 'fss'; + } + }; + + expect(ReflectionHelper::getPrivateMethodInvoker($class, 'accessor')())->toBe('fss'); + expect($class::test())->toBeTruthy(); + expect(fn() => $class::test())->not->toThrow(new InvalidArgumentException()); + }); + + it('__callStatic genere une erreur normale si la methode n\'existe pas ou qu\'il y\'a une incompatibilite de parametre', function () { + Container::set('fss', new class() { + public function test() { + return true; + } + public function hello(string $name) { + return 'Hello ' . $name; + } + }); + $class = new class() extends Facade { + protected static function accessor(): string + { + return 'fss'; + } + }; + + expect(ReflectionHelper::getPrivateMethodInvoker($class, 'accessor')())->toBe('fss'); + expect($class::test())->toBeTruthy(); + expect(fn() => $class::test())->not->toThrow(new InvalidArgumentException()); + expect(fn() => $class::testons())->toThrow(new Error('Call to undefined method class@anonymous::testons()')); + expect(fn() => $class::hello())->toThrow(new ArgumentCountError()); + expect($class->hello('BlitzPHP'))->toBe('Hello BlitzPHP'); + }); + }); + describe('Container', function () { it('Container', function () { $accessor = ReflectionHelper::getPrivateMethodInvoker(Container::class, 'accessor'); @@ -42,7 +148,7 @@ }); it('Execution d\'une methode', function () { - expect(FS::exists(__FILE__))->toBeTruthy(); + expect(Fs::exists(__FILE__))->toBeTruthy(); }); }); diff --git a/src/Facades/Facade.php b/src/Facades/Facade.php index 5e832f0f..e40f8b0d 100644 --- a/src/Facades/Facade.php +++ b/src/Facades/Facade.php @@ -11,13 +11,23 @@ namespace BlitzPHP\Facades; +use InvalidArgumentException; + abstract class Facade { - abstract protected static function accessor(): object; + abstract protected static function accessor(): object|string; public static function __callStatic(string $name, array $arguments = []) { - return static::accessor()->{$name}(...$arguments); + if (is_string($accessor = static::accessor())) { + $accessor = service($accessor); + } + + if ( ! is_object($accessor)) { + throw new InvalidArgumentException(sprintf('La methode `%s::accessor` doit retourner un object ou le nom d\'un service.', static::class)); + } + + return $accessor->{$name}(...$arguments); } public function __call(string $name, array $arguments = []) From 6139c0776dcde21a5937c685ff167ad4fab7f226 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 21 Dec 2023 18:52:12 +0100 Subject: [PATCH 108/111] Refactorisation du code relatif au courrier et correction des erreurs d'espace de noms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le commit refactorise le code relatif au courrier en remplaçant la déclaration d'importation pour `MailerInterface` par la déclaration correcte de l'espace de noms `Contracts`. De plus, il corrige les erreurs d'espace de noms dans les fichiers `ControllerFinder`, `Console`, `EventDiscover`, et Services`. Ces changements assurent l'utilisation correcte des interfaces à travers la base de code. Aucune conséquence ou considération significative ne découle de ces changements. --- .../application/app/Views/WorldDecorator.php | 2 +- .../framework/Config/Providers.spec.php | 2 +- src/Cli/Commands/Routes/ControllerFinder.php | 2 +- src/Cli/Console/Console.php | 2 +- src/Config/Providers.php | 4 +- src/Container/Services.php | 2 +- src/Event/EventDiscover.php | 2 +- src/Mail/Adapters/AbstractAdapter.php | 2 +- src/Mail/Mail.php | 3 +- src/Mail/MailerInterface.php | 174 ------------------ src/Router/RouteCollection.php | 2 +- src/View/Adapters/AbstractAdapter.php | 4 +- src/View/Parser.php | 4 +- src/View/RendererInterface.php | 104 ----------- src/View/View.php | 4 + src/View/ViewDecoratorInterface.php | 24 --- src/View/ViewDecoratorTrait.php | 1 + 17 files changed, 21 insertions(+), 317 deletions(-) delete mode 100644 src/Mail/MailerInterface.php delete mode 100644 src/View/RendererInterface.php delete mode 100644 src/View/ViewDecoratorInterface.php diff --git a/spec/support/application/app/Views/WorldDecorator.php b/spec/support/application/app/Views/WorldDecorator.php index e2ebf885..9319fdbd 100644 --- a/spec/support/application/app/Views/WorldDecorator.php +++ b/spec/support/application/app/Views/WorldDecorator.php @@ -11,7 +11,7 @@ namespace Spec\BlitzPHP\App\Views; -use BlitzPHP\View\ViewDecoratorInterface; +use BlitzPHP\Contracts\View\ViewDecoratorInterface; /** * Cette classe n'est utilisée que pour fournir un point de référence pendant les tests afin de s'assurer que les choses fonctionnent comme prévu. diff --git a/spec/system/framework/Config/Providers.spec.php b/spec/system/framework/Config/Providers.spec.php index aed0d6f4..0834d9ec 100644 --- a/spec/system/framework/Config/Providers.spec.php +++ b/spec/system/framework/Config/Providers.spec.php @@ -27,7 +27,7 @@ expect($classes)->toBeA('array'); expect($definitions)->toContainKeys($classes + $interfaces); - expect($interfaces)->toContain(\BlitzPHP\Autoloader\LocatorInterface::class); + expect($interfaces)->toContain(\BlitzPHP\Contracts\Autoloader\LocatorInterface::class); expect($classes)->toContain(\BlitzPHP\Autoloader\Autoloader::class); }); }); diff --git a/src/Cli/Commands/Routes/ControllerFinder.php b/src/Cli/Commands/Routes/ControllerFinder.php index 4ccfa77c..45529a3d 100644 --- a/src/Cli/Commands/Routes/ControllerFinder.php +++ b/src/Cli/Commands/Routes/ControllerFinder.php @@ -11,8 +11,8 @@ namespace BlitzPHP\Cli\Commands\Routes; -use BlitzPHP\Autoloader\LocatorInterface; use BlitzPHP\Container\Services; +use BlitzPHP\Contracts\Autoloader\LocatorInterface; /** * Recherche tous les contrôleurs dans un namespace pour la liste des routes automatiques. diff --git a/src/Cli/Console/Console.php b/src/Cli/Console/Console.php index 3f31be28..9ea9173a 100644 --- a/src/Cli/Console/Console.php +++ b/src/Cli/Console/Console.php @@ -13,8 +13,8 @@ use Ahc\Cli\Application; use Ahc\Cli\Input\Command as AhcCommand; -use BlitzPHP\Autoloader\LocatorInterface; use BlitzPHP\Container\Services; +use BlitzPHP\Contracts\Autoloader\LocatorInterface; use BlitzPHP\Debug\Logger; use BlitzPHP\Exceptions\CLIException; use BlitzPHP\Traits\SingletonTrait; diff --git a/src/Config/Providers.php b/src/Config/Providers.php index 6e1876a6..ed375620 100644 --- a/src/Config/Providers.php +++ b/src/Config/Providers.php @@ -29,13 +29,13 @@ public static function definitions(): array private static function interfaces(): array { return [ - \BlitzPHP\Autoloader\LocatorInterface::class => static fn () => service('locator'), + \BlitzPHP\Contracts\Autoloader\LocatorInterface::class => static fn () => service('locator'), \BlitzPHP\Contracts\Event\EventManagerInterface::class => static fn () => service('event'), + \BlitzPHP\Contracts\Mail\MailerInterface::class => static fn () => service('mail'), \BlitzPHP\Contracts\Router\RouteCollectionInterface::class => static fn () => service('routes'), \BlitzPHP\Contracts\Security\EncrypterInterface::class => static fn () => service('encrypter'), \BlitzPHP\Contracts\Session\CookieManagerInterface::class => static fn () => service('cookie'), \BlitzPHP\Contracts\Session\SessionInterface::class => static fn () => service('session'), - \BlitzPHP\Mail\MailerInterface::class => static fn () => service('mail'), \Psr\Container\ContainerInterface::class => static fn () => service('container'), \Psr\Http\Message\ResponseInterface::class => static fn () => service('response'), \Psr\Http\Message\ServerRequestInterface::class => static fn () => service('request'), diff --git a/src/Container/Services.php b/src/Container/Services.php index 5d6a8a53..6e03f0b9 100644 --- a/src/Container/Services.php +++ b/src/Container/Services.php @@ -13,10 +13,10 @@ use BlitzPHP\Autoloader\Autoloader; use BlitzPHP\Autoloader\Locator; -use BlitzPHP\Autoloader\LocatorInterface; use BlitzPHP\Cache\Cache; use BlitzPHP\Cache\ResponseCache; use BlitzPHP\Config\Config; +use BlitzPHP\Contracts\Autoloader\LocatorInterface; use BlitzPHP\Contracts\Database\ConnectionResolverInterface; use BlitzPHP\Contracts\Security\EncrypterInterface; use BlitzPHP\Contracts\Session\CookieManagerInterface; diff --git a/src/Event/EventDiscover.php b/src/Event/EventDiscover.php index 9c9ab717..a6552a11 100644 --- a/src/Event/EventDiscover.php +++ b/src/Event/EventDiscover.php @@ -11,8 +11,8 @@ namespace BlitzPHP\Event; -use BlitzPHP\Autoloader\LocatorInterface; use BlitzPHP\Container\Services; +use BlitzPHP\Contracts\Autoloader\LocatorInterface; use BlitzPHP\Contracts\Event\EventListenerInterface; use BlitzPHP\Contracts\Event\EventManagerInterface; diff --git a/src/Mail/Adapters/AbstractAdapter.php b/src/Mail/Adapters/AbstractAdapter.php index 9b445564..e2f835ce 100644 --- a/src/Mail/Adapters/AbstractAdapter.php +++ b/src/Mail/Adapters/AbstractAdapter.php @@ -12,7 +12,7 @@ namespace BlitzPHP\Mail\Adapters; use BadMethodCallException; -use BlitzPHP\Mail\MailerInterface; +use BlitzPHP\Contracts\Mail\MailerInterface; use BlitzPHP\Utilities\String\Text; use InvalidArgumentException; use RuntimeException; diff --git a/src/Mail/Mail.php b/src/Mail/Mail.php index 76988fa2..ea820c10 100644 --- a/src/Mail/Mail.php +++ b/src/Mail/Mail.php @@ -11,6 +11,7 @@ namespace BlitzPHP\Mail; +use BlitzPHP\Contracts\Mail\MailerInterface; use BlitzPHP\Mail\Adapters\AbstractAdapter; use BlitzPHP\Mail\Adapters\PHPMailer; use BlitzPHP\Mail\Adapters\SymfonyMailer; @@ -349,7 +350,7 @@ public function view(string $view, array $data = []): static $view = view($path . $view, $data); if (! empty($this->config['template'])) { - $view->setLayout($this->config['template']); + $view->layout($this->config['template']); } return $this->html($view->get(false)); diff --git a/src/Mail/MailerInterface.php b/src/Mail/MailerInterface.php deleted file mode 100644 index 63217d1c..00000000 --- a/src/Mail/MailerInterface.php +++ /dev/null @@ -1,174 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Mail; - -interface MailerInterface -{ - public const PRIORITY_HIGH = 1; - public const PRIORITY_NORMAL = 3; - public const PRIORITY_LOW = 5; - public const PROTOCOL_GMAIL = 'gmail'; - public const PROTOCOL_MAIL = 'mail'; - public const PROTOCOL_MAILGUN = 'mailgun'; - public const PROTOCOL_MANDRILL = 'mandrill'; - public const PROTOCOL_POSTMARK = 'postmark'; - public const PROTOCOL_QMAIL = 'qmail'; - public const PROTOCOL_SENDGRID = 'sendgrid'; - public const PROTOCOL_SENDMAIL = 'sendmail'; - public const PROTOCOL_SES = 'ses'; - public const PROTOCOL_SMTP = 'smtp'; - public const ENCRYPTION_SSL = 'ssl'; - public const ENCRYPTION_TLS = 'tls'; - public const ENCRYPTION_NONE = 'none'; - public const CHARSET_ASCII = 'us-ascii'; - public const CHARSET_ISO88591 = 'iso-8859-1'; - public const CHARSET_UTF8 = 'utf-8'; - public const CONTENT_TYPE_PLAINTEXT = 'text/plain'; - public const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar'; - public const CONTENT_TYPE_TEXT_HTML = 'text/html'; - public const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative'; - public const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed'; - public const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related'; - public const ENCODING_7BIT = '7bit'; - public const ENCODING_8BIT = '8bit'; - public const ENCODING_BASE64 = 'base64'; - public const ENCODING_BINARY = 'binary'; - public const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; - public const ICAL_METHOD_REQUEST = 'REQUEST'; - public const ICAL_METHOD_PUBLISH = 'PUBLISH'; - public const ICAL_METHOD_REPLY = 'REPLY'; - public const ICAL_METHOD_ADD = 'ADD'; - public const ICAL_METHOD_CANCEL = 'CANCEL'; - public const ICAL_METHOD_REFRESH = 'REFRESH'; - public const ICAL_METHOD_COUNTER = 'COUNTER'; - public const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER'; - - /** - * Ajoute un texte alternatif pour le message en cas de nom prise en charge du html - */ - public function alt(string $content): static; - - /** - * Ajoute des pièces jointes au mail a partir d'un chemin du systeme de fichier. - * N'utilisez jamais un chemin d'accès fourni par l'utilisateur vers un fichier ! - * Renvoie faux si le fichier n'a pas pu être trouvé ou lu. - * Explicitement *ne prend pas* en charge la transmission d'URL ; Mailer n'est pas un client HTTP. - * Si vous avez besoin de le faire, récupérez la ressource vous-même et transmettez-la via un fichier local ou une chaîne. - */ - public function attach(array|string $path, string $name = '', string $type = '', string $encoding = self::ENCODING_BASE64, string $disposition = 'attachment'): static; - - /** - * Ajoutez une chaîne ou une pièce jointe binaire (non-système de fichiers). - * Cette méthode peut être utilisée pour joindre des données ascii ou binaires, - * tel qu'un enregistrement BLOB d'une base de données. - * - * @param mixed $binary - */ - public function attachBinary($binary, string $name, string $type = '', string $encoding = self::ENCODING_BASE64, string $disposition = 'attachment'): static; - - /** - *Ajoute des adresses de copie (BCC) au mail - */ - public function bcc(array|string $address, bool|string $name = '', bool $set = false): static; - - /** - * Ajoute des adresses de copie (CC) au mail - */ - public function cc(array|string $address, bool|string $name = '', bool $set = false): static; - - public function dkim(string $pk, string $passphrase = '', string $selector = '', string $domain = ''): static; - - /** - * Ajouter une pièce jointe intégrée (en ligne) à partir d'un fichier. - * Cela peut inclure des images, des sons et à peu près n'importe quel autre type de document. - * Celles-ci diffèrent des pièces jointes « régulières » en ce sens qu'elles sont destinées à être - * affiché en ligne avec le message, pas seulement en pièce jointe pour le téléchargement. - * Ceci est utilisé dans les messages HTML qui intègrent les images - * le HTML fait référence à l'utilisation de la valeur `$cid` dans les balises `img`, par exemple ``. - * N'utilisez jamais un chemin d'accès fourni par l'utilisateur vers un fichier ! * - */ - public function embedded(string $path, string $cid, string $name = '', string $type = '', string $encoding = self::ENCODING_BASE64, string $disposition = 'inline'): static; - - /** - * Ajoutez une pièce jointe stringifiée intégrée. - * Cela peut inclure des images, des sons et à peu près n'importe quel autre type de document. - * Si votre nom de fichier ne contient pas d'extension, assurez-vous de définir $type sur un type MIME approprié. - * - * @param mixed $binary - */ - public function embeddedBinary($binary, string $cid, string $name = '', string $type = '', string $encoding = self::ENCODING_BASE64, string $disposition = 'inline'): static; - - /** - * Defini l'adresse de l'expéditeur (From) du mail - */ - public function from(string $address, string $name = ''): static; - - /** - * Ajoute des entêtes personnalisées au mail à envoyer - */ - public function header(array|string $name, ?string $value = null): static; - - /** - * Defini le message à envoyer au format html - */ - public function html(string $content): static; - - /** - * Initialise le gestionnaire d'email avec les configurations données - */ - public function init(array $config): static; - - /** - * Renvoie l'identifiant du dernier mail envoyé - */ - public function lastId(): string; - - /** - * Defini le message à envoyer - */ - public function message(string $message): static; - - /** - * Ajoute les adresses de reponse (Reply-To) au mail - */ - public function replyTo(array|string $address, bool|string $name = '', bool $set = false): static; - - /** - * Lance l'envoi du message - * - * @return bool false on error - */ - public function send(): bool; - - /** - * Définissez les fichiers de clé publique et privée et le mot de passe pour la signature S/MIME. - * - * @param string $key_pass Mot de passe pour la clé privée - * @param string $extracerts_filename Chemin facultatif vers le certificat de chaîne - */ - public function sign(string $cert_filename, string $key_filename, string $key_pass, string $extracerts_filename = ''): static; - - /** - * Defini le sujet du mail - */ - public function subject(string $subject): static; - - /** - * Defini le message à envoyer au format texte - */ - public function text(string $content): static; - - /** - * Ajoute l'adresse de destination (To) du mail - */ - public function to(array|string $address, bool|string $name = '', bool $set = false): static; -} diff --git a/src/Router/RouteCollection.php b/src/Router/RouteCollection.php index c99f2f5a..c7333505 100644 --- a/src/Router/RouteCollection.php +++ b/src/Router/RouteCollection.php @@ -11,8 +11,8 @@ namespace BlitzPHP\Router; -use BlitzPHP\Autoloader\LocatorInterface; use BlitzPHP\Container\Services; +use BlitzPHP\Contracts\Autoloader\LocatorInterface; use BlitzPHP\Contracts\Router\RouteCollectionInterface; use BlitzPHP\Exceptions\RouterException; use Closure; diff --git a/src/View/Adapters/AbstractAdapter.php b/src/View/Adapters/AbstractAdapter.php index 70a9d3b3..6139cb77 100644 --- a/src/View/Adapters/AbstractAdapter.php +++ b/src/View/Adapters/AbstractAdapter.php @@ -11,11 +11,11 @@ namespace BlitzPHP\View\Adapters; -use BlitzPHP\Autoloader\LocatorInterface; use BlitzPHP\Container\Services; +use BlitzPHP\Contracts\Autoloader\LocatorInterface; +use BlitzPHP\Contracts\View\RendererInterface; use BlitzPHP\Exceptions\ViewException; use BlitzPHP\Utilities\Helpers; -use BlitzPHP\View\RendererInterface; use BlitzPHP\View\ViewDecoratorTrait; abstract class AbstractAdapter implements RendererInterface diff --git a/src/View/Parser.php b/src/View/Parser.php index f0ae0794..825cae26 100644 --- a/src/View/Parser.php +++ b/src/View/Parser.php @@ -458,7 +458,7 @@ protected function parseConditionals(string $template): string * @param string $leftDelimiter * @param string $rightDelimiter */ - public function setDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): RendererInterface + public function setDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): static { $this->leftDelimiter = $leftDelimiter; $this->rightDelimiter = $rightDelimiter; @@ -472,7 +472,7 @@ public function setDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): Rend * @param string $leftDelimiter * @param string $rightDelimiter */ - public function setConditionalDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): RendererInterface + public function setConditionalDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): static { $this->leftConditionalDelimiter = $leftDelimiter; $this->rightConditionalDelimiter = $rightDelimiter; diff --git a/src/View/RendererInterface.php b/src/View/RendererInterface.php deleted file mode 100644 index fac29698..00000000 --- a/src/View/RendererInterface.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\View; - -/** - * Interface de rendu d'interface - * - * L'interface utilisée pour afficher les vues et/ou les fichiers de thème. - */ -interface RendererInterface -{ - /** - * Constructeur - * - * @param \BlitzPHP\Autoloader\Locator|string|null $viewPathLocator - */ - public function __construct(array $config, $viewPathLocator = null, bool $debug = BLITZ_DEBUG); - - /** - * Définit plusieurs éléments de données de vue à la fois. - */ - public function addData(array $data = [], ?string $context = null): self; - - /** - * Construit la sortie en fonction d'un nom de fichier et de tout données déjà définies. - * - * Options valides : - * - cache Nombre de secondes à mettre en cache pour - * - cache_name Nom à utiliser pour le cache - * - * @param string $view Nom de fichier de la source de la vue - * @param array|null $options Réservé à des utilisations tierces car - * il peut être nécessaire de transmettre des - * informations supplémentaires à d'autres moteurs de modèles. - * @param bool|null $saveData Si vrai, enregistre les données pour les appels suivants, - * si faux, nettoie les données après affichage, - * si null, utilise le paramètre de configuration. - */ - public function render(string $view, ?array $options = null, ?bool $saveData = null): string; - - /** - * Construit la sortie en fonction d'une chaîne et de tout - * données déjà définies. - * - * @param string $view Le contenu de la vue - * @param array $options Réservé à des utilisations tierces depuis - * il peut être nécessaire de transmettre des informations supplémentaires - * vers d'autres moteurs de modèles. - * @param bool $saveData Indique s'il faut enregistrer les données pour les appels suivants - */ - public function renderString(string $view, ?array $options = null, bool $saveData = false): string; - - /** - * Verifie qu'un fichier de vue existe - */ - public function exists(string $view, ?string $ext = null, array $options = []): bool; - - /** - * Définit plusieurs éléments de données de vue à la fois. - * - * @param string $context Le contexte d'échappement pour : html, css, js, url - * Si 'raw', aucun echappement ne se produira - */ - public function setData(array $data = [], ?string $context = null): self; - - /** - * Renvoie les données actuelles qui seront affichées dans la vue. - */ - public function getData(): array; - - /** - * Définit une seule donnée de vue. - * - * @param mixed $value - * @param string $context Le contexte d'échappement pour : html, css, js, url - * Si 'raw', aucun echappement ne se produira - */ - public function setVar(string $name, $value = null, ?string $context = null): self; - - /** - * Supprime toutes les données de vue du système. - */ - public function resetData(): self; - - /** - * Definit le layout a utiliser par les vues - */ - public function setLayout(?string $layout): self; - - /** - * Renvoie les données de performances qui ont pu être collectées - * lors de l'exécution. Utilisé principalement dans la barre d'outils de débogage. - */ - public function getPerformanceData(): array; -} diff --git a/src/View/View.php b/src/View/View.php index 4c713fa3..aae1cc71 100644 --- a/src/View/View.php +++ b/src/View/View.php @@ -12,9 +12,11 @@ namespace BlitzPHP\View; use BlitzPHP\Container\Services; +use BlitzPHP\Contracts\View\RendererInterface; use BlitzPHP\Exceptions\ConfigException; use BlitzPHP\Exceptions\ViewException; use BlitzPHP\Validation\ErrorBag; +use BlitzPHP\View\Adapters\AbstractAdapter; use BlitzPHP\View\Adapters\BladeAdapter; use BlitzPHP\View\Adapters\LatteAdapter; use BlitzPHP\View\Adapters\NativeAdapter; @@ -38,6 +40,8 @@ class View implements Stringable /** * Liste des adapters pris en comptes + * + * @var array> */ public static array $validAdapters = [ 'native' => NativeAdapter::class, diff --git a/src/View/ViewDecoratorInterface.php b/src/View/ViewDecoratorInterface.php deleted file mode 100644 index 46c74b6b..00000000 --- a/src/View/ViewDecoratorInterface.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\View; - -/** - * Les decorateurs de vues sont des classes simples qui ont la possibilité de modifier la sortie des appels view() avant qu'elle ne soit mise en cache. - */ -interface ViewDecoratorInterface -{ - /** - * Prend $html et a la possibilité de le modifier. - * DOIT renvoyer le HTML modifié. - */ - public static function decorate(string $html): string; -} diff --git a/src/View/ViewDecoratorTrait.php b/src/View/ViewDecoratorTrait.php index 8f5ba145..23f76753 100644 --- a/src/View/ViewDecoratorTrait.php +++ b/src/View/ViewDecoratorTrait.php @@ -11,6 +11,7 @@ namespace BlitzPHP\View; +use BlitzPHP\Contracts\View\ViewDecoratorInterface; use BlitzPHP\Exceptions\ViewException; trait ViewDecoratorTrait From bd4e0a0722df5474970d4c1529f6112b7392249d Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 21 Dec 2023 18:55:06 +0100 Subject: [PATCH 109/111] cs-fix --- src/Config/Providers.php | 2 +- src/Facades/Facade.php | 12 ++++++------ src/View/View.php | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Config/Providers.php b/src/Config/Providers.php index ed375620..72198e13 100644 --- a/src/Config/Providers.php +++ b/src/Config/Providers.php @@ -31,7 +31,7 @@ private static function interfaces(): array return [ \BlitzPHP\Contracts\Autoloader\LocatorInterface::class => static fn () => service('locator'), \BlitzPHP\Contracts\Event\EventManagerInterface::class => static fn () => service('event'), - \BlitzPHP\Contracts\Mail\MailerInterface::class => static fn () => service('mail'), + \BlitzPHP\Contracts\Mail\MailerInterface::class => static fn () => service('mail'), \BlitzPHP\Contracts\Router\RouteCollectionInterface::class => static fn () => service('routes'), \BlitzPHP\Contracts\Security\EncrypterInterface::class => static fn () => service('encrypter'), \BlitzPHP\Contracts\Session\CookieManagerInterface::class => static fn () => service('cookie'), diff --git a/src/Facades/Facade.php b/src/Facades/Facade.php index e40f8b0d..63d6d446 100644 --- a/src/Facades/Facade.php +++ b/src/Facades/Facade.php @@ -19,13 +19,13 @@ abstract protected static function accessor(): object|string; public static function __callStatic(string $name, array $arguments = []) { - if (is_string($accessor = static::accessor())) { - $accessor = service($accessor); - } + if (is_string($accessor = static::accessor())) { + $accessor = service($accessor); + } - if ( ! is_object($accessor)) { - throw new InvalidArgumentException(sprintf('La methode `%s::accessor` doit retourner un object ou le nom d\'un service.', static::class)); - } + if (! is_object($accessor)) { + throw new InvalidArgumentException(sprintf('La methode `%s::accessor` doit retourner un object ou le nom d\'un service.', static::class)); + } return $accessor->{$name}(...$arguments); } diff --git a/src/View/View.php b/src/View/View.php index aae1cc71..a2c235cd 100644 --- a/src/View/View.php +++ b/src/View/View.php @@ -40,8 +40,8 @@ class View implements Stringable /** * Liste des adapters pris en comptes - * - * @var array> + * + * @var array> */ public static array $validAdapters = [ 'native' => NativeAdapter::class, From a119e30adf3cb6a980d49d130991df45e70f5c78 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Tue, 26 Dec 2023 11:01:09 +0100 Subject: [PATCH 110/111] =?UTF-8?q?Ajout=20de=20la=20fonction=20url()=20co?= =?UTF-8?q?mme=20aide=20et=20refactorisation=20des=20fonctions=20associ?= =?UTF-8?q?=C3=A9es.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cette modification ajoute la fonction url() comme aide dans le fichier url.php. La fonction url() génère une URL pour l'application en utilisant la classe UrlGenerator. Ce refactor simplifie le code en remplaçant les appels directs à la classe UrlGenerator par la fonction url() dans les fonctions previous_url() et action(). Cela permet d'améliorer la lisibilité et la maintenabilité du code, ainsi que d'encapsuler la fonctionnalité de génération d'URL dans une seule fonction. L'ajout de la fonction url() permet de générer des URL de manière plus propre et plus cohérente dans l'ensemble de la base de code. --- src/Helpers/url.php | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Helpers/url.php b/src/Helpers/url.php index 3a984a90..e6a95317 100644 --- a/src/Helpers/url.php +++ b/src/Helpers/url.php @@ -25,6 +25,25 @@ // ================================= ================================= // +if (! function_exists('url')) { + /** + * Générer une url pour l'application. + * + * @return UrlGenerator|string + */ + function url(?string $path = null, mixed $parameters = [], ?bool $secure = null) + { + /** @var UrlGenerator $generator */ + $generator = service(UrlGenerator::class); + + if (null === $path) { + return $generator; + } + + return $generator->to($path, $parameters, $secure); + } +} + if (! function_exists('site_url')) { /** * Renvoie une URL de site telle que définie par la configuration de l'application. @@ -106,10 +125,7 @@ function current_url(bool $returnObject = false, ?ServerRequest $request = null) */ function previous_url(bool $returnObject = false) { - /** @var UrlGenerator $generator */ - $generator = service(UrlGenerator::class); - - $referer = $generator->previous(); + $referer = url()->previous(); return $returnObject ? Services::uri($referer) : $referer; } @@ -485,6 +501,18 @@ function route(string $method, ...$params) } } +if (! function_exists('action')) { + /** + * Obtenir l'URL d'une action du contrôleur. + * + * @return false|string + */ + function action(array|string $action, array $parameters = []) + { + return url()->action($action, $parameters); + } +} + if (! function_exists('url_is')) { /** * Détermine si le chemin d'URL actuel contient le chemin donné. From c5cc5044c00b176a27f611f2f9beac0392d36d82 Mon Sep 17 00:00:00 2001 From: dimtrovich Date: Thu, 28 Dec 2023 15:58:21 +0000 Subject: [PATCH 111/111] Fix styling --- src/Controllers/RestController.php | 6 ++--- src/Debug/Toolbar/Views/toolbar.tpl.php | 32 ++++++++++++------------- src/Helpers/assets.php | 2 +- src/Helpers/common.php | 8 +++---- src/Helpers/url.php | 10 ++++---- src/Http/ServerRequest.php | 2 +- src/Http/UploadedFileFactory.php | 10 ++++---- src/View/Adapters/LatteAdapter.php | 2 +- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/Controllers/RestController.php b/src/Controllers/RestController.php index a0c6b728..a08d14fc 100644 --- a/src/Controllers/RestController.php +++ b/src/Controllers/RestController.php @@ -118,7 +118,7 @@ public function _remap(string $method, array $params = []) * * Ceci permet aux classes filles de specifier comment elles doivent gerer les exceptions lors de la methode remap * - * @return \Psr\Http\Message\ResponseInterface + * @return ResponseInterface */ protected function manageException(Throwable $ex) { @@ -172,7 +172,7 @@ final protected function respond($data, ?int $status = StatusCode::OK) * @param int|string|null $code Code d'erreur personnalisé, spécifique à l'API * @param array $errors La liste des erreurs rencontrées * - * @return \Psr\Http\Message\ResponseInterface + * @return ResponseInterface */ final protected function respondFail(?string $message = "Une erreur s'est produite", ?int $status = StatusCode::INTERNAL_ERROR, null|int|string $code = null, array $errors = []) { @@ -204,7 +204,7 @@ final protected function respondFail(?string $message = "Une erreur s'est produi * * @param mixed|null $result Les données renvoyées par l'API * - * @return \Psr\Http\Message\ResponseInterface + * @return ResponseInterface */ final protected function respondSuccess(?string $message = 'Resultat', $result = null, ?int $status = StatusCode::OK) { diff --git a/src/Debug/Toolbar/Views/toolbar.tpl.php b/src/Debug/Toolbar/Views/toolbar.tpl.php index f3b521ca..daee1d4d 100644 --- a/src/Debug/Toolbar/Views/toolbar.tpl.php +++ b/src/Debug/Toolbar/Views/toolbar.tpl.php @@ -1,21 +1,21 @@ diff --git a/src/Helpers/assets.php b/src/Helpers/assets.php index d5ce4afe..36e52aa5 100644 --- a/src/Helpers/assets.php +++ b/src/Helpers/assets.php @@ -475,7 +475,7 @@ function videos_url(?string $name, bool $add_version = true): string /** * Obtenez le chemin d'accès à un fichier Mix versionné. * - * @throws \Exception + * @throws Exception */ function mix(string $path, string $manifestDirectory = ''): string { diff --git a/src/Helpers/common.php b/src/Helpers/common.php index e9e9d5f9..a3cec38b 100644 --- a/src/Helpers/common.php +++ b/src/Helpers/common.php @@ -233,7 +233,7 @@ function config(null|array|string $key = null, $default = null) * * @param int|string $level * - * @return \BlitzPHP\Debug\Logger|void + * @return BlitzPHP\Debug\Logger|void */ function logger($level = null, ?string $message = null, array $context = []) { @@ -259,7 +259,7 @@ function logger($level = null, ?string $message = null, array $context = []) * * @param mixed|null $value * - * @return \BlitzPHP\Cache\Cache|bool|mixed + * @return BlitzPHP\Cache\Cache|bool|mixed */ function cache(?string $key = null, $value = null) { @@ -961,7 +961,7 @@ function view_exist(string $name, ?string $ext = null, array $options = []): boo /** * Charge une vue * - * @return \BlitzPHP\View\View + * @return BlitzPHP\View\View */ function view(string $view, array $data = [], array $options = []) { @@ -1024,7 +1024,7 @@ function geo_ip(?string $ip = null): ?array * * @uses GuzzleHttp\Psr7\stream_for * - * @throws \InvalidArgumentException si l'argument $resource n'est pas valide. + * @throws InvalidArgumentException si l'argument $resource n'est pas valide. */ function to_stream($resource = '', array $options = []): Psr\Http\Message\StreamInterface { diff --git a/src/Helpers/url.php b/src/Helpers/url.php index e6a95317..9603959a 100644 --- a/src/Helpers/url.php +++ b/src/Helpers/url.php @@ -29,11 +29,11 @@ /** * Générer une url pour l'application. * - * @return UrlGenerator|string + * @return string|UrlGenerator */ function url(?string $path = null, mixed $parameters = [], ?bool $secure = null) { - /** @var UrlGenerator $generator */ + /** @var UrlGenerator $generator */ $generator = service(UrlGenerator::class); if (null === $path) { @@ -93,7 +93,7 @@ function base_url($relativePath = '', ?string $scheme = null): string * * @param bool $returnObject True pour renvoyer un objet au lieu d'une chaîne * - * @return \BlitzPHP\Http\Uri|string + * @return BlitzPHP\Http\Uri|string */ function current_url(bool $returnObject = false, ?ServerRequest $request = null) { @@ -121,7 +121,7 @@ function current_url(bool $returnObject = false, ?ServerRequest $request = null) * Si ce n'est pas disponible, cependant, nous utiliserons une URL épurée de $_SERVER['HTTP_REFERER'] * qui peut être défini par l'utilisateur, il n'est donc pas fiable et n'est pas défini par certains navigateurs/serveurs. * - * @return \BlitzPHP\Http\Uri|mixed|string + * @return BlitzPHP\Http\Uri|mixed|string */ function previous_url(bool $returnObject = false) { @@ -509,7 +509,7 @@ function route(string $method, ...$params) */ function action(array|string $action, array $parameters = []) { - return url()->action($action, $parameters); + return url()->action($action, $parameters); } } diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 030a2441..43a0698b 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -596,7 +596,7 @@ protected function _environmentDetector(array $detect): bool * * @param string[] $types Les types à vérifier. * - * @see \BlitzPHP\Http\ServerRequest::is() + * @see ServerRequest::is() */ public function isAll(array $types): bool { diff --git a/src/Http/UploadedFileFactory.php b/src/Http/UploadedFileFactory.php index 359e6491..1a293805 100644 --- a/src/Http/UploadedFileFactory.php +++ b/src/Http/UploadedFileFactory.php @@ -32,11 +32,11 @@ class UploadedFileFactory implements UploadedFileFactoryInterface * @see http://php.net/manual/features.file-upload.post-method.php * @see http://php.net/manual/features.file-upload.errors.php * - * @param \Psr\Http\Message\StreamInterface $stream Le flux sous-jacent représentant le contenu du fichier téléchargé. - * @param int|null $size La taille du fichier en octets. - * @param int $error L'erreur de téléchargement du fichier PHP. - * @param string|null $clientFilename Le nom du fichier tel qu'il est fourni par le client, le cas échéant. - * @param string|null $clientMediaType Le type de média tel qu'il est fourni par le client, le cas échéant. + * @param StreamInterface $stream Le flux sous-jacent représentant le contenu du fichier téléchargé. + * @param int|null $size La taille du fichier en octets. + * @param int $error L'erreur de téléchargement du fichier PHP. + * @param string|null $clientFilename Le nom du fichier tel qu'il est fourni par le client, le cas échéant. + * @param string|null $clientMediaType Le type de média tel qu'il est fourni par le client, le cas échéant. * * @throws InvalidArgumentException Si la ressource du fichier n'est pas lisible. */ diff --git a/src/View/Adapters/LatteAdapter.php b/src/View/Adapters/LatteAdapter.php index ae6c496c..b1563b4d 100644 --- a/src/View/Adapters/LatteAdapter.php +++ b/src/View/Adapters/LatteAdapter.php @@ -24,7 +24,7 @@ class LatteAdapter extends AbstractAdapter /** * Instance Latte * - * @var \Latte\Engine + * @var Engine */ private $latte;