diff --git a/composer.json b/composer.json index 1d7d56c..6266a6e 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": "^8.2", + "php": "^8.4", "php-di/php-di": "dev-master", "ext-json": "*", "mcnd/lean": "dev-master" diff --git a/meta/definitions/software.php b/meta/definitions/software.php index a3df986..a9c4299 100644 --- a/meta/definitions/software.php +++ b/meta/definitions/software.php @@ -2,6 +2,8 @@ use Ulmus\ConnectionAdapter; +use function DI\{ get, env }; + define('LEAN_API_PROJECT_PATH', $path = dirname(__DIR__, 2)); return [ @@ -56,6 +58,11 @@ return [ return $adapter; }, + 'app.middlewares' => \DI\add([ + Lean\Api\Middleware\CORS::class, + ]), + + Lean\Api\Lib\RenderCORS::class => DI\create(Lean\Api\Lib\RenderCORS::class)->constructor(get(\Psr\Http\Message\ServerRequestInterface::class), env('LEAN_API_CORS_ORIGIN', '*')), Lean\ApplicationStrategy\NotFoundDecoratorInterface::class => DI\autowire(Lean\Api\ApplicationStrategy\NotFoundDecorator::class), Lean\ApplicationStrategy\MethodNotAllowedInterface::class => DI\autowire(Lean\Api\ApplicationStrategy\MethodNotAllowedDecorator::class), Lean\Api\Factory\MessageFactoryInterface::class => DI\autowire(Lean\Api\Lib\Message::class), diff --git a/src/ApplicationStrategy/MethodNotAllowedDecorator.php b/src/ApplicationStrategy/MethodNotAllowedDecorator.php index 61a662a..5c01676 100644 --- a/src/ApplicationStrategy/MethodNotAllowedDecorator.php +++ b/src/ApplicationStrategy/MethodNotAllowedDecorator.php @@ -3,6 +3,7 @@ namespace Lean\Api\ApplicationStrategy; use DI\Attribute\Inject; +use Lean\Api\Lib\RenderCORS; use Lean\ApplicationStrategy; use Lean\Factory\HttpFactoryInterface; use Picea\Picea; @@ -13,17 +14,17 @@ use Psr\Http\Server\RequestHandlerInterface; class MethodNotAllowedDecorator implements \Lean\ApplicationStrategy\MethodNotAllowedInterface { + public function __construct( + protected RenderCORS $cors, + ) {} + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + if ($request->getMethod() === 'OPTIONS') { + return $this->cors->fromResponse($handler->handle($request), true); + } - if ($request->getMethod() === 'OPTIONS') { - header('Access-Control-Allow-Origin: *'); - header('Access-Control-Allow-Methods: *'); - header('Access-Control-Allow-Headers: *'); - header('Access-Control-Allow-Credentials: true'); - exit(0); - } - - return $handler->handle($request);; + return $handler->handle($request); } } \ No newline at end of file diff --git a/src/Attribute/Action.php b/src/Attribute/Action.php new file mode 100644 index 0000000..008e597 --- /dev/null +++ b/src/Attribute/Action.php @@ -0,0 +1,11 @@ +$field = trim($context->$field, $this->characters); + + return ActionTypeEnum::ValueModifier; + } +} \ No newline at end of file diff --git a/src/Attribute/Action/ValueAppend.php b/src/Attribute/Action/ValueAppend.php new file mode 100644 index 0000000..daf6db6 --- /dev/null +++ b/src/Attribute/Action/ValueAppend.php @@ -0,0 +1,18 @@ +$key); + array_push($entity->$key, $context->$field); + + return ActionTypeEnum::Setter; + } +} \ No newline at end of file diff --git a/src/Attribute/Action/ValuePrepend.php b/src/Attribute/Action/ValuePrepend.php new file mode 100644 index 0000000..420d50a --- /dev/null +++ b/src/Attribute/Action/ValuePrepend.php @@ -0,0 +1,17 @@ +$key, $context->$field); + + return ActionTypeEnum::Setter; + } +} \ No newline at end of file diff --git a/src/Attribute/Action/ValueSet.php b/src/Attribute/Action/ValueSet.php new file mode 100644 index 0000000..9938294 --- /dev/null +++ b/src/Attribute/Action/ValueSet.php @@ -0,0 +1,17 @@ +$key = $context->$field; + + return ActionTypeEnum::Setter; + } +} \ No newline at end of file diff --git a/src/Attribute/ContextFieldActionEnum.php b/src/Attribute/ContextFieldActionEnum.php new file mode 100644 index 0000000..b90347f --- /dev/null +++ b/src/Attribute/ContextFieldActionEnum.php @@ -0,0 +1,11 @@ +object ; $var = $apiField->field ?: $key; + $attr = $this->reflectedProperties($context)[$var] ?? false; - if ( property_exists($context, $var) && ( new \ReflectionProperty($context, $var) )->isInitialized($context) ) { + $contextField = $attr ? $attr->getAttribute(ContextField::class) : null; + + if ( property_exists($context, $var) && new \ReflectionProperty($context, $var)->isInitialized($context) ) { if ($apiField->setterMethod) { # Use a setter method call_user_func([ $entity, $apiField->setterMethod ], $context->{$var}); } - else { - # Direct property set - $entity->$key = $context->{$var}; + elseif($attr) { + $actions = []; + + foreach($attr->getAttributes(Action::class) as $item) { + $actions[] = $item->object->action($entity, $key, $context, $var); + } + + if (! in_array(ActionTypeEnum::Setter, $actions)) { + new ValueSet()->action($entity, $key, $context, $var); + } + } } - elseif (! $entity->isLoaded()) { + elseif (! $entity->isLoaded() && $contextField) { # Applies 'default' value assigned to ContextField property - $attr = $this->reflectedProperties($context)[$var] ?? null; - - if ($attr) { - $contextField = $attr->getAttribute(ContextField::class); - - if ($contextField) { - if (isset($contextField->object->default)) { - - $entity->$key = $contextField->object->default; - } + if (isset($contextField->object->default)) { + if (is_callable($contextField->object->default)) { + $entity->$key = call_user_func_array($contextField->object->default, [ $context, ]); + } + else { + $entity->$key = $contextField->object->default; } } } diff --git a/src/Lib/FormContext.php b/src/Lib/FormContext.php index 6d33dc1..0daee28 100644 --- a/src/Lib/FormContext.php +++ b/src/Lib/FormContext.php @@ -10,42 +10,41 @@ use Ulmus\Entity\EntityInterface; class FormContext extends \Picea\Ui\Method\FormContext { public function __construct( - protected LanguageHandler $languageHandler, - protected MessageFactoryInterface $message, - ServerRequestInterface $request, + protected LanguageHandler $formLanguageHandler, + protected MessageFactoryInterface $formMessage, + ServerRequestInterface $formContextRequest, ? string $formName = null - ) - { - parent::__construct($request, $formName); + ) { + parent::__construct($formContextRequest, $formName); } # public function initializeEntity(EntityInterface $entity) : void {} public function lang(string $key, array $variables = []) { - return $this->languageHandler->languageFromKey($key, $variables); + return $this->formLanguageHandler->languageFromKey($key, $variables); } public function pushSuccessMessage($key, $variables = []) : void { - $this->pushMessage($this->message::generateSuccess( + $this->pushMessage($this->formMessage::generateSuccess( $this->lang($key, $variables) )); } public function pushWarningMessage($key, $variables = []) : void { - $this->pushMessage($this->message::generateWarning( + $this->pushMessage($this->formMessage::generateWarning( $this->lang($key, $variables) )); } public function pushErrorMessage($key, $variables = []) : void { - $this->pushMessage($this->message::generateError( + $this->pushMessage($this->formMessage::generateError( $a = $this->lang($key, $variables) )); } public function pushInfoMessage($key, $variables = []) : void { - $this->pushMessage($this->message::generateInfo( + $this->pushMessage($this->formMessage::generateInfo( $this->lang($key, $variables) )); } diff --git a/src/Lib/RenderCORS.php b/src/Lib/RenderCORS.php new file mode 100644 index 0000000..788f10b --- /dev/null +++ b/src/Lib/RenderCORS.php @@ -0,0 +1,93 @@ +origin)) { + $this->origin = explode(',', $this->origin); + } + } + + public function renderHeader(ServerRequestInterface $request) : void + { + $matched = $this->matchOrigin($this->request->getServerParams()['HTTP_ORIGIN'] ?? null, $this->origin); + + if (! $matched) { + return; + } + + $render = fn(string|array $e) => is_string($e) ? $e : sprintf('%s', implode(', ', $e)); + + header('Access-Control-Allow-Origin: ' . $matched); + header(sprintf('Access-Control-Allow-Methods: %s', $render($this->methods))); + header(sprintf('Access-Control-Allow-Headers: %s', $render($this->headers))); + header('Access-Control-Allow-Credentials: ' . ($this->allowCredentials ? 'true' : 'false')); + } + + public function fromResponse(ResponseInterface $response, bool $emptyResponse = false) : ResponseInterface + { + $matched = $this->matchOrigin($this->request->getServerParams()['HTTP_ORIGIN'] ?? null, $this->origin); + + if (! $matched) { + return $response; + } + + $render = fn(string|array $e) => is_string($e) ? $e : sprintf('%s', implode(', ', $e)); + + $response = $response + ->withHeader('Access-Control-Allow-Origin', $matched) + ->withHeader('Access-Control-Allow-Methods', $render($this->methods)) + ->withHeader('Access-Control-Allow-Headers', $render($this->headers)) + ->withHeader('Access-Control-Allow-Credentials', $this->allowCredentials ? 'true' : 'false'); + + if ($emptyResponse) { + $response = $response->withBody(Stream::fromMemory())->withStatus(200); + } + + return $response; + } + + function matchOrigin(?string $origin, string|array $allowList): ?string + { + if ($origin === null || $origin === '') { + return null; + } + + foreach ((array) $allowList as $rule) { + if ($rule === '*') { + return '*'; + } + + // Regex rule: "/.../" + if (is_string($rule) && strlen($rule) >= 2 && $rule[0] === '/' && substr($rule, -1) === '/') { + if (@preg_match($rule, $origin) === 1) { + return $origin; + } + + continue; + } + + // Exact match + if (is_string($rule) && hash_equals($rule, $origin)) { + return $origin; + } + } + + return null; + } + +} + diff --git a/src/Middleware/CORS.php b/src/Middleware/CORS.php index 3ee74b0..8e429f5 100644 --- a/src/Middleware/CORS.php +++ b/src/Middleware/CORS.php @@ -2,23 +2,20 @@ namespace Lean\Api\Middleware; -use Laminas\Diactoros\Response\JsonResponse; -use Lean\Factory\HttpFactory; -use Picea\Ui\Method\FormHandler; +use Lean\Api\Lib\RenderCORS; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Lean\Api\{Factory\DebugFormFactoryInterface, Form, Entity}; -class CORS implements MiddlewareInterface { +class CORS implements MiddlewareInterface +{ + public function __construct( + protected RenderCORS $cors, + ) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { - if ($request->getMethod() === "option") { - die("an option :)"); - } - - return $handler->handle($request); + return $this->cors->fromResponse($handler->handle($request)); } } diff --git a/view/lean-api/form_descriptor.phtml b/view/lean-api/form_descriptor.phtml index 85e0201..b46aa90 100644 --- a/view/lean-api/form_descriptor.phtml +++ b/view/lean-api/form_descriptor.phtml @@ -13,47 +13,50 @@