diff --git a/actions/class.TestAction.php b/actions/class.TestAction.php new file mode 100644 index 0000000000..e89ec5de3d --- /dev/null +++ b/actions/class.TestAction.php @@ -0,0 +1,56 @@ +getGetParameter('uri'); + $listOfUris = $this->getGetParameter('listOfUris'); + $subQuery = $this->getGetParameter('subQuery'); + + $subQueryUri = $subQuery['uri'] ?? ''; + $subQueryValue = $subQuery['value'] ?? 0; + + $this->setSuccessJsonResponse(__METHOD__); + } + + /** + * @ParamConverter("query", converter="oat.tao.param_converter.query") + */ + public function withParamConverter(Query $query): void + { + $uri = $query->getUri(); + $listOfUris = $query->getListOfUris(); + + $subQueryUri = $query->getSubQuery()->getUri(); + $subQueryValue = $query->getSubQuery()->getValue(); + + $this->setSuccessJsonResponse(__METHOD__); + } +} diff --git a/composer.json b/composer.json index cb8d297571..6eb3621804 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,11 @@ "flow/jsonpath": "~0.5", "jtl-software/opsgenie-client": "2.0.2", "league/openapi-psr7-validator": "0.9", - "league/csv": "^9.6" + "league/csv": "^9.6", + "symfony/serializer": "~5.2.0", + "symfony/http-foundation": "^5.3", + "symfony/psr-http-message-bridge": "^2.1", + "symfony/property-access": "~5.0.0" }, "autoload": { "psr-4": { diff --git a/manifest.php b/manifest.php index 3b3fd77163..f9fc29b3a6 100755 --- a/manifest.php +++ b/manifest.php @@ -24,66 +24,69 @@ declare(strict_types=1); +use oat\tao\model\user\TaoRoles; use oat\tao\controller\api\Users; -use oat\tao\controller\Middleware\MiddlewareConfig; -use oat\tao\model\accessControl\AccessControlServiceProvider; -use oat\tao\model\Csv\CsvServiceProvider; -use oat\tao\model\import\ServiceProvider\ImportServiceProvider; -use oat\tao\model\metadata\ServiceProvider\MetadataServiceProvider; -use oat\tao\model\Observer\ServiceProvider\ObserverServiceProvider; -use oat\tao\model\resources\ResourcesServiceProvider; -use oat\tao\model\featureFlag\FeatureFlagServiceProvider; -use oat\tao\helpers\form\ServiceProvider\FormServiceProvider; -use oat\tao\install\services\SetupSettingsStorage; -use oat\tao\model\accessControl\func\AccessRule; -use oat\tao\model\export\ServiceProvider\MetadataServiceProvider as ExportMetadataServiceProvider; -use oat\tao\model\Lists\ServiceProvider\ListsServiceProvider; use oat\tao\model\routing\ApiRoute; +use oat\tao\scripts\update\Updater; +use oat\tao\scripts\install\AddLogFs; use oat\tao\model\routing\LegacyRoute; -use oat\tao\model\routing\ServiceProvider\RoutingServiceProvider; -use oat\tao\model\user\TaoRoles; -use oat\tao\model\user\UserSettingsServiceProvider; +use oat\tao\model\Csv\CsvServiceProvider; use oat\tao\model\LanguageServiceProvider; -use oat\tao\scripts\install\AddArchiveService; -use oat\tao\scripts\install\AddLogFs; +use oat\tao\scripts\install\RegisterEvents; +use oat\tao\scripts\install\SetServiceState; +use oat\tao\scripts\install\SetUpQueueTasks; use oat\tao\scripts\install\AddTmpFsHandlers; +use oat\tao\scripts\install\AddArchiveService; use oat\tao\scripts\install\CreateRdsListStore; -use oat\tao\scripts\install\CreateWebhookEventLogTable; -use oat\tao\scripts\install\InstallNotificationTable; -use oat\tao\scripts\install\RegisterActionService; -use oat\tao\scripts\install\RegisterActionAccessControl; -use oat\tao\scripts\install\RegisterClassMetadataServices; -use oat\tao\scripts\install\RegisterClassPropertiesChangedEvent; -use oat\tao\scripts\install\RegisterClassPropertiesChangedEventListener; -use oat\tao\scripts\install\RegisterClassPropertyRemovedEvent; -use oat\tao\scripts\install\RegisterClassPropertyRemovedListener; -use oat\tao\scripts\install\RegisterDataAccessControlChangedEvent; -use oat\tao\scripts\install\RegisterDataAccessControlChangedListener; -use oat\tao\scripts\install\RegisterEvents; -use oat\tao\scripts\install\RegisterResourceEvents; -use oat\tao\scripts\install\RegisterResourceRelationService; -use oat\tao\scripts\install\RegisterResourceWatcherService; use oat\tao\scripts\install\RegisterRtlLocales; -use oat\tao\scripts\install\RegisterSearchServices; -use oat\tao\scripts\install\RegisterSessionCookieService; -use oat\tao\scripts\install\RegisterSignatureGenerator; -use oat\tao\scripts\install\RegisterTaoUpdateEventListener; -use oat\tao\scripts\install\RegisterTaskQueueServices; -use oat\tao\scripts\install\RegisterUserLockoutsEventListeners; +use oat\tao\model\accessControl\func\AccessRule; use oat\tao\scripts\install\RegisterUserService; -use oat\tao\scripts\install\RegisterValidationRules; -use oat\tao\scripts\install\RegisterValueCollectionServices; -use oat\tao\scripts\install\SetClientLoggerConfig; use oat\tao\scripts\install\SetContainerService; use oat\tao\scripts\install\SetDefaultCSPHeader; +use oat\tao\install\services\SetupSettingsStorage; +use oat\tao\scripts\install\RegisterActionService; +use oat\tao\scripts\install\SetClientLoggerConfig; +use oat\tao\scripts\install\SetServiceFileStorage; +use oat\tao\controller\Middleware\MiddlewareConfig; +use oat\tao\model\user\UserSettingsServiceProvider; +use oat\tao\scripts\install\RegisterResourceEvents; +use oat\tao\scripts\install\RegisterSearchServices; use oat\tao\scripts\install\SetImageAligmentConfig; use oat\tao\scripts\install\SetLocaleNumbersConfig; -use oat\tao\scripts\install\SetServiceFileStorage; -use oat\tao\scripts\install\SetServiceState; +use oat\tao\scripts\install\RegisterValidationRules; use oat\tao\scripts\install\SetupMaintenanceService; -use oat\tao\scripts\install\SetUpQueueTasks; -use oat\tao\scripts\update\Updater; +use oat\tao\model\resources\ResourcesServiceProvider; +use oat\tao\scripts\install\InstallNotificationTable; +use oat\tao\scripts\install\RegisterTaskQueueServices; +use oat\tao\model\Serializer\SerializerServiceProvider; +use oat\tao\scripts\install\CreateWebhookEventLogTable; +use oat\tao\scripts\install\RegisterSignatureGenerator; +use oat\tao\scripts\install\RegisterActionAccessControl; +use oat\tao\model\featureFlag\FeatureFlagServiceProvider; +use oat\tao\scripts\install\RegisterSessionCookieService; +use oat\tao\scripts\install\RegisterClassMetadataServices; +use oat\tao\scripts\install\RegisterResourceWatcherService; +use oat\tao\scripts\install\RegisterTaoUpdateEventListener; +use oat\tao\scripts\install\RegisterResourceRelationService; +use oat\tao\scripts\install\RegisterValueCollectionServices; +use oat\tao\helpers\form\ServiceProvider\FormServiceProvider; +use oat\tao\model\Lists\ServiceProvider\ListsServiceProvider; +use oat\tao\model\accessControl\AccessControlServiceProvider; +use oat\tao\scripts\install\RegisterClassPropertyRemovedEvent; +use oat\tao\model\import\ServiceProvider\ImportServiceProvider; +use oat\tao\model\ParamConverter\ParamConverterServiceProvider; +use oat\tao\scripts\install\RegisterUserLockoutsEventListeners; +use oat\tao\scripts\install\RegisterClassPropertiesChangedEvent; +use oat\tao\model\routing\ServiceProvider\RoutingServiceProvider; +use oat\tao\scripts\install\RegisterClassPropertyRemovedListener; +use oat\tao\scripts\install\RegisterDataAccessControlChangedEvent; +use oat\tao\model\Observer\ServiceProvider\ObserverServiceProvider; +use oat\tao\model\metadata\ServiceProvider\MetadataServiceProvider; +use oat\tao\scripts\install\RegisterDataAccessControlChangedListener; +use oat\tao\scripts\install\RegisterClassPropertiesChangedEventListener; use oat\tao\model\StatisticalMetadata\StatisticalMetadataServiceProvider; +use oat\tao\model\HttpFoundation\ServiceProvider\HttpFoundationServiceProvider; +use oat\tao\model\export\ServiceProvider\MetadataServiceProvider as ExportMetadataServiceProvider; $extpath = __DIR__ . DIRECTORY_SEPARATOR; @@ -95,7 +98,7 @@ 'author' => 'Open Assessment Technologies, CRP Henri Tudor', 'models' => [ 'http://www.tao.lu/Ontologies/TAO.rdf', - 'http://www.tao.lu/middleware/wfEngine.rdf' + 'http://www.tao.lu/middleware/wfEngine.rdf', ], 'install' => [ 'rdf' => [ @@ -109,7 +112,7 @@ __DIR__ . '/models/ontology/widgetdefinitions.rdf', __DIR__ . '/models/ontology/requiredaction.rdf', __DIR__ . '/models/ontology/auth/basicauth.rdf', - __DIR__ . '/models/ontology/userlocks.rdf' + __DIR__ . '/models/ontology/userlocks.rdf', ], 'checks' => [ ['type' => 'CheckPHPRuntime', 'value' => ['id' => 'tao_php_runtime', 'min' => '5.4']], @@ -129,8 +132,8 @@ ['type' => 'CheckPHPINIValue', 'value' => ['id' => 'tao_ini_suhosin_request_max_varname_length', 'name' => 'suhosin.request.max_varname_length', 'value' => '128', 'dependsOn' => ['tao_extension_suhosin']]], ['type' => 'CheckFileSystemComponent', 'value' => ['id' => 'fs_generis_common_conf', 'location' => 'config', 'rights' => 'rw', 'recursive' => true]], ['type' => 'CheckFileSystemComponent', 'value' => ['id' => 'fs_tao_client_locales', 'location' => 'tao/views/locales', 'rights' => 'rw']], - ['type' => 'CheckCustom', 'value' => ['id' => 'tao_custom_not_nginx', 'name' => 'not_nginx', 'extension' => 'tao', "optional" => true, 'dependsOn' => ['tao_extension_curl']]], - ['type' => 'CheckCustom', 'value' => ['id' => 'tao_custom_allowoverride', 'name' => 'allow_override', 'extension' => 'tao', "optional" => true, 'dependsOn' => ['tao_custom_not_nginx']]], + ['type' => 'CheckCustom', 'value' => ['id' => 'tao_custom_not_nginx', 'name' => 'not_nginx', 'extension' => 'tao', 'optional' => true, 'dependsOn' => ['tao_extension_curl']]], + ['type' => 'CheckCustom', 'value' => ['id' => 'tao_custom_allowoverride', 'name' => 'allow_override', 'extension' => 'tao', 'optional' => true, 'dependsOn' => ['tao_custom_not_nginx']]], ['type' => 'CheckCustom', 'value' => ['id' => 'tao_custom_mod_rewrite', 'name' => 'mod_rewrite', 'extension' => 'tao', 'dependsOn' => ['tao_custom_allowoverride']]], ['type' => 'CheckCustom', 'value' => ['id' => 'tao_custom_database_drivers', 'name' => 'database_drivers', 'extension' => 'tao']], ], @@ -177,13 +180,13 @@ RegisterActionAccessControl::class, RegisterRtlLocales::class, RegisterSearchServices::class, - SetImageAligmentConfig::class + SetImageAligmentConfig::class, ], ], 'update' => Updater::class, 'optimizableClasses' => [ 'http://www.tao.lu/Ontologies/TAO.rdf#Languages', - 'http://www.tao.lu/Ontologies/TAO.rdf#LanguageUsages' + 'http://www.tao.lu/Ontologies/TAO.rdf#LanguageUsages', ], 'managementRole' => TaoRoles::TAO_MANAGER, 'acl' => [ @@ -298,6 +301,9 @@ AccessControlServiceProvider::class, MetadataServiceProvider::class, ObserverServiceProvider::class, + SerializerServiceProvider::class, + HttpFoundationServiceProvider::class, + ParamConverterServiceProvider::class, ], 'middlewares' => [ MiddlewareConfig::class, diff --git a/migrations/Version202109300806032234_tao.php b/migrations/Version202109300806032234_tao.php new file mode 100644 index 0000000000..881353a6fc --- /dev/null +++ b/migrations/Version202109300806032234_tao.php @@ -0,0 +1,56 @@ +getEventManager(); + $eventManager->attach(ParamConverterEvent::class, [ParamConverterListener::class, 'handleEvent']); + $this->getServiceLocator()->register(EventManager::SERVICE_ID, $eventManager); + } + + public function down(Schema $schema): void + { + $eventManager = $this->getEventManager(); + $eventManager->detach(ParamConverterEvent::class, [ParamConverterListener::class, 'handleEvent']); + $this->getServiceLocator()->register(EventManager::SERVICE_ID, $eventManager); + } + + private function getEventManager(): EventManager + { + return $this->getServiceLocator()->getContainer()->get(EventManager::SERVICE_ID); + } +} diff --git a/models/classes/HttpFoundation/Factory/HttpFoundationFactory.php b/models/classes/HttpFoundation/Factory/HttpFoundationFactory.php new file mode 100644 index 0000000000..a4ba9a5199 --- /dev/null +++ b/models/classes/HttpFoundation/Factory/HttpFoundationFactory.php @@ -0,0 +1,46 @@ +httpFoundationFactory = new SymfonyHttpFoundationFactory(); + } + + public function createRequest(ServerRequestInterface $psrRequest, bool $streamed = false): RequestInterface + { + $request = $this->httpFoundationFactory->createRequest($psrRequest, $streamed); + + return new Request($request); + } +} diff --git a/models/classes/HttpFoundation/Factory/HttpFoundationFactoryInterface.php b/models/classes/HttpFoundation/Factory/HttpFoundationFactoryInterface.php new file mode 100644 index 0000000000..382700a201 --- /dev/null +++ b/models/classes/HttpFoundation/Factory/HttpFoundationFactoryInterface.php @@ -0,0 +1,31 @@ +request = $request; + } + + public function request(): SymfonyRequest + { + return $this->request; + } + + /** + * {@inheritdoc} + */ + public function get(string $key, $default = null) + { + return $this->request()->get($key, $default); + } + + public function getAttributes(): array + { + return $this->request()->attributes->all(); + } + + /** + * {@inheritdoc} + */ + public function getAttribute(string $key, $default = null) + { + return $this->request()->attributes->get($key, $default); + } + + /** + * {@inheritdoc} + */ + public function setAttribute(string $key, $value): void + { + $this->request()->attributes->set($key, $value); + } + + public function getRequestParameters(): array + { + return $this->request()->request->all(); + } + + /** + * {@inheritdoc} + */ + public function getRequestParameter(string $key, $default = null) + { + return $this->request()->request->get($key, $default); + } + + /** + * {@inheritdoc} + */ + public function setRequestParameter(string $key, $value): void + { + $this->request()->request->set($key, $value); + } + + public function getQueryParameters(): array + { + return $this->request()->query->all(); + } + + /** + * {@inheritdoc} + */ + public function getQueryParameter(string $key, $default = null) + { + return $this->request()->query->get($key, $default); + } + + /** + * {@inheritdoc} + */ + public function setQueryParameter(string $key, $value): void + { + $this->request()->query->set($key, $value); + } + + public function getServerParameters(): array + { + return $this->request()->server->all(); + } + + /** + * {@inheritdoc} + */ + public function getServerParameter(string $key, $default = null) + { + return $this->request()->server->get($key, $default); + } + + /** + * {@inheritdoc} + */ + public function setServerParameter(string $key, $value): void + { + $this->request()->server->set($key, $value); + } + + public function getFiles(): array + { + return $this->request()->files->all(); + } + + /** + * {@inheritdoc} + */ + public function getFile(string $key, $default = null) + { + return $this->request()->files->get($key, $default); + } + + /** + * {@inheritdoc} + */ + public function setFile(string $key, $value): void + { + $this->request()->files->set($key, $value); + } + + public function getCookies(): array + { + return $this->request()->cookies->all(); + } + + /** + * {@inheritdoc} + */ + public function getCookie(string $key, $default = null) + { + return $this->request()->cookies->get($key, $default); + } + + /** + * {@inheritdoc} + */ + public function setCookie(string $key, $value): void + { + $this->request()->cookies->set($key, $value); + } + + public function getHeaders(): array + { + return $this->request()->headers->all(); + } + + /** + * {@inheritdoc} + */ + public function getHeader(string $key, string $default = null): ?string + { + return $this->request()->headers->get($key, $default); + } + + /** + * {@inheritdoc} + */ + public function setHeader(string $key, $values, bool $replace = true): void + { + $this->request()->headers->set($key, $values, $replace); + } + + /** + * {@inheritdoc} + */ + public function getContent(bool $asResource = false) + { + return $this->request()->getContent($asResource); + } +} diff --git a/models/classes/HttpFoundation/Request/RequestInterface.php b/models/classes/HttpFoundation/Request/RequestInterface.php new file mode 100644 index 0000000000..ac0a4d130e --- /dev/null +++ b/models/classes/HttpFoundation/Request/RequestInterface.php @@ -0,0 +1,138 @@ +services(); + + $services + ->set(HttpFoundationFactory::class, HttpFoundationFactory::class) + ->public(); + } +} diff --git a/models/classes/ParamConverter/Configuration/ConfigurationAnnotation.php b/models/classes/ParamConverter/Configuration/ConfigurationAnnotation.php new file mode 100644 index 0000000000..3944b528eb --- /dev/null +++ b/models/classes/ParamConverter/Configuration/ConfigurationAnnotation.php @@ -0,0 +1,67 @@ + $value) { + if ($this->setViaSetter($property, $value) || $this->setDirectly($property, $value)) { + continue; + } + + throw new RuntimeException( + sprintf( + 'Unknown property "%s" for annotation "@%s".', + $property, + static::class + ) + ); + } + } + + private function setViaSetter(string $property, $value): bool + { + $isSetterExists = method_exists($this, 'set' . $property); + + if ($isSetterExists) { + $this->{'set' . $property}($value); + } + + return $isSetterExists; + } + + private function setDirectly(string $property, $value): bool + { + $isPropertyExists = property_exists($this, $property); + + if ($isPropertyExists) { + $this->$property = $value; + } + + return $isPropertyExists; + } +} diff --git a/models/classes/ParamConverter/Configuration/Configurator.php b/models/classes/ParamConverter/Configuration/Configurator.php new file mode 100644 index 0000000000..093bc29869 --- /dev/null +++ b/models/classes/ParamConverter/Configuration/Configurator.php @@ -0,0 +1,90 @@ +getParameters() as $parameter) { + $type = $parameter->getType(); + $class = $this->getParamClassByType($type); + + if ( + $class !== null + && ($request instanceof $class || $request->request() instanceof $class) + ) { + continue; + } + + $name = $parameter->getName(); + + if ($type) { + if (!isset($configurations[$name])) { + $configurations[$name] = new ParamConverter($name); + } + + if ($class !== null && $configurations[$name]->getClass() === null) { + $configurations[$name]->setClass($class); + } + + $configurationClass = $configurations[$name]->getClass(); + + if ( + $configurationClass !== null + && $configurations[$name]->getConverter() === null + && defined($configurationClass . '::CONVERTER_ID') + ) { + $configurations[$name]->setConverter( + constant($configurationClass . '::CONVERTER_ID') + ); + } + } + + if (isset($configurations[$name])) { + $isOptional = $parameter->isOptional() + || $parameter->isDefaultValueAvailable() + || ($type && $type->allowsNull()); + + $configurations[$name]->setIsOptional($isOptional); + } + } + } + + private function getParamClassByType(?ReflectionType $type): ?string + { + return $type !== null && !$type->isBuiltin() + ? $type->getName() + : null; + } +} diff --git a/models/classes/ParamConverter/Configuration/ConfiguratorInterface.php b/models/classes/ParamConverter/Configuration/ConfiguratorInterface.php new file mode 100644 index 0000000000..e8c42fca5b --- /dev/null +++ b/models/classes/ParamConverter/Configuration/ConfiguratorInterface.php @@ -0,0 +1,40 @@ +name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function setValue(string $name): void + { + $this->setName($name); + } + + public function getClass(): ?string + { + return $this->class; + } + + public function setClass(?string $class): void + { + $this->class = $class; + } + + public function getOptions(): array + { + return $this->options; + } + + public function setOptions(array $options): void + { + $this->options = $options; + } + + public function isOptional(): bool + { + return $this->isOptional; + } + + public function setIsOptional(bool $isOptional): void + { + $this->isOptional = $isOptional; + } + + public function getConverter(): ?string + { + return $this->converter; + } + + public function setConverter(?string $converter): void + { + $this->converter = $converter; + } +} diff --git a/models/classes/ParamConverter/Context/ObjectFactoryContext.php b/models/classes/ParamConverter/Context/ObjectFactoryContext.php new file mode 100644 index 0000000000..d4988593ff --- /dev/null +++ b/models/classes/ParamConverter/Context/ObjectFactoryContext.php @@ -0,0 +1,88 @@ +getParameter(self::PARAM_CLASS); + } + + public function getData(): array + { + return $this->getParameter(self::PARAM_DATA); + } + + public function getFormat(): string + { + return $this->getParameter(self::PARAM_FORMAT, 'json'); + } + + public function getContext(): array + { + return $this->getParameter(self::PARAM_CONTEXT, []); + } + + protected function getSupportedParameters(): array + { + return [ + self::PARAM_CLASS, + self::PARAM_DATA, + self::PARAM_FORMAT, + self::PARAM_CONTEXT, + ]; + } + + protected function validateParameter(string $parameter, $parameterValue): void + { + if ( + in_array($parameter, [self::PARAM_CLASS, self::PARAM_FORMAT], true) + && is_string($parameterValue) + ) { + return; + } + + if ( + in_array($parameter, [self::PARAM_DATA, self::PARAM_CONTEXT], true) + && is_array($parameterValue) + ) { + return; + } + + throw new InvalidArgumentException( + sprintf( + 'Context parameter %s is not valid.', + $parameter + ) + ); + } +} diff --git a/models/classes/ParamConverter/Context/ObjectFactoryContextInterface.php b/models/classes/ParamConverter/Context/ObjectFactoryContextInterface.php new file mode 100644 index 0000000000..77e1e36e2d --- /dev/null +++ b/models/classes/ParamConverter/Context/ObjectFactoryContextInterface.php @@ -0,0 +1,34 @@ +checkRequiredParameters($parameters); + + parent::__construct($parameters); + } + + public function getRequest(): RequestInterface + { + return $this->getParameter(self::PARAM_REQUEST); + } + + public function getController(): string + { + return $this->getParameter(self::PARAM_CONTROLLER); + } + + public function getMethod(): string + { + return $this->getParameter(self::PARAM_METHOD); + } + + protected function getRequiredParameters(): array + { + return [ + self::PARAM_REQUEST, + self::PARAM_CONTROLLER, + self::PARAM_METHOD, + ]; + } + + protected function getSupportedParameters(): array + { + return [ + self::PARAM_REQUEST, + self::PARAM_CONTROLLER, + self::PARAM_METHOD, + ]; + } + + protected function validateParameter(string $parameter, $parameterValue): void + { + if ($parameter === self::PARAM_REQUEST && $parameterValue instanceof RequestInterface) { + return; + } + + if ( + in_array($parameter, [self::PARAM_CONTROLLER, self::PARAM_METHOD], true) + && is_string($parameterValue) + ) { + return; + } + + throw new InvalidArgumentException( + sprintf( + 'Context parameter %s is not valid.', + $parameter + ) + ); + } + + private function checkRequiredParameters(array $parameters): void + { + $missedParameters = array_diff($this->getRequiredParameters(), array_keys($parameters)); + + if (!empty($missedParameters)) { + throw new InvalidArgumentException( + sprintf( + 'The following required context parameters are missing: %s.', + implode(', ', $missedParameters) + ) + ); + } + } +} diff --git a/models/classes/ParamConverter/Context/ParamConverterListenerContextInterface.php b/models/classes/ParamConverter/Context/ParamConverterListenerContextInterface.php new file mode 100644 index 0000000000..d6e3be3ac6 --- /dev/null +++ b/models/classes/ParamConverter/Context/ParamConverterListenerContextInterface.php @@ -0,0 +1,34 @@ +context = $context; + } + + public function getContext(): ParamConverterListenerContextInterface + { + return $this->context; + } + + public function getName(): string + { + return self::class; + } +} diff --git a/models/classes/ParamConverter/EventListener/ListenerInterface.php b/models/classes/ParamConverter/EventListener/ListenerInterface.php new file mode 100644 index 0000000000..98544b8e7c --- /dev/null +++ b/models/classes/ParamConverter/EventListener/ListenerInterface.php @@ -0,0 +1,30 @@ +configurator = $autoConfigurator; + $this->paramConverterManager = $paramConverterManager; + $this->autoConvert = $autoConvert; + } + + public function handleEvent(Event $event): void + { + if (!$event instanceof ParamConverterEvent) { + return; + } + + $context = $event->getContext(); + $request = $context->getRequest(); + + $configurations = $this->extractConfigurations($request); + + $controller = $context->getController(); + $method = $context->getMethod(); + + // Automatically apply conversion for non-configured objects + if ($this->autoConvert && is_callable([$controller, $method])) { + $this->configurator->configure( + new ReflectionMethod($controller, $method), + $request, + $configurations + ); + } + + $this->paramConverterManager->apply($request, $configurations); + } + + /** + * @return ParamConverter[] + */ + private function extractConfigurations(RequestInterface $request): array + { + $configurations = []; + $requestConfigurations = $request->getAttribute(self::REQUEST_ATTRIBUTE_CONVERTERS, []); + + if (!is_array($requestConfigurations)) { + $requestConfigurations = [$requestConfigurations]; + } + + /** @var ParamConverter $requestConfiguration */ + foreach ($requestConfigurations as $requestConfiguration) { + $configurations[$requestConfiguration->getName()] = $requestConfiguration; + } + + return $configurations; + } +} diff --git a/models/classes/ParamConverter/Factory/ObjectFactory.php b/models/classes/ParamConverter/Factory/ObjectFactory.php new file mode 100644 index 0000000000..bfb6b40d19 --- /dev/null +++ b/models/classes/ParamConverter/Factory/ObjectFactory.php @@ -0,0 +1,89 @@ +serializer = $serializer; + } + + public function create(ObjectFactoryContextInterface $context): object + { + $constructorArgs = []; + $data = $context->getData(); + + $reflectionClass = new ReflectionClass($context->getClass()); + $constructor = $reflectionClass->getConstructor(); + + if ($constructor) { + foreach ($constructor->getParameters() as $constructorParameter) { + $constructorParameterName = $constructorParameter->getName(); + + if (array_key_exists($constructorParameterName, $data)) { + $constructorArgs[$constructorParameterName] = $data[$constructorParameterName]; + unset($data[$constructorParameterName]); + } + } + } + + $instance = $reflectionClass->newInstanceArgs($constructorArgs); + + foreach ($data as $queryParameter => $value) { + if ($reflectionClass->hasMethod('set' . $queryParameter)) { + $reflectionClass + ->getMethod('set' . $queryParameter) + ->invoke($instance, $value); + } elseif ($reflectionClass->hasProperty($queryParameter)) { + $reflectionClass->getProperty($queryParameter)->setValue($instance, $value); + } + } + + return $instance; + } + + public function deserialize(ObjectFactoryContextInterface $context): object + { + $format = $context->getFormat(); + + if ($format !== 'json') { + throw new InvalidArgumentException('Currently, only the "json" format is supported.'); + } + + return $this->serializer->deserialize( + json_encode($context->getData()), + $context->getClass(), + $format, + $context->getContext() + ); + } +} diff --git a/models/classes/ParamConverter/Factory/ObjectFactoryInterface.php b/models/classes/ParamConverter/Factory/ObjectFactoryInterface.php new file mode 100644 index 0000000000..2af73abce5 --- /dev/null +++ b/models/classes/ParamConverter/Factory/ObjectFactoryInterface.php @@ -0,0 +1,32 @@ + */ + private $converters = []; + + /** @var array */ + private $namedConverters = []; + + /** + * {@inheritdoc} + */ + public function apply(RequestInterface $request, array $configurations): void + { + foreach ($configurations as $configuration) { + $this->applyConfiguration($request, $configuration); + } + } + + /** + * {@inheritdoc} + */ + public function add(ParamConverterInterface $converter, string $name = null, ?int $priority = 0): void + { + if ($priority !== null) { + if (!isset($this->converters[$priority])) { + $this->converters[$priority] = []; + } + + $this->converters[$priority][] = $converter; + } + + if ($name !== null) { + $this->namedConverters[$name] = $converter; + } + } + + /** + * {@inheritdoc} + */ + public function all(): array + { + krsort($this->converters); + $converters = []; + + foreach ($this->converters as $all) { + $converters = array_merge($converters, $all); + } + + return $converters; + } + + private function applyConfiguration(RequestInterface $request, ParamConverter $configuration): void + { + $value = $request->getAttribute($configuration->getName()); + $className = $configuration->getClass(); + + // If the value is already an instance of the class we are trying to convert it into + // we should continue as no conversion is required + if ($value instanceof $className) { + return; + } + + if ($converterName = $configuration->getConverter()) { + $this->checkProvidedConverterName($converterName, $configuration->getName()); + /** @var ParamConverterInterface $converter */ + $converter = $this->namedConverters[$converterName]; + + $this->checkConverterSupport($converter, $configuration); + $converter->apply($request, $configuration); + + return; + } + + foreach ($this->all() as $converter) { + if ($converter->supports($configuration)) { + if ($converter->apply($request, $configuration)) { + return; + } + } + } + } + + private function checkProvidedConverterName(string $converterName, string $parameter): void + { + if (!isset($this->namedConverters[$converterName])) { + throw new RuntimeException( + sprintf( + 'No converter named "%s" found for conversion of parameter "%s".', + $converterName, + $parameter + ) + ); + } + } + + private function checkConverterSupport(ParamConverterInterface $converter, ParamConverter $configuration): void + { + if (!$converter->supports($configuration)) { + throw new RuntimeException( + sprintf( + 'Converter "%s" does not support conversion of parameter "%s".', + $configuration->getConverter(), + $configuration->getName() + ) + ); + } + } +} diff --git a/models/classes/ParamConverter/Manager/ParamConverterManagerInterface.php b/models/classes/ParamConverter/Manager/ParamConverterManagerInterface.php new file mode 100644 index 0000000000..90751ca350 --- /dev/null +++ b/models/classes/ParamConverter/Manager/ParamConverterManagerInterface.php @@ -0,0 +1,55 @@ + QueryParamConverter::class, + ]; + + /** @var ServicesConfigurator */ + private $services; + + public function __invoke(ContainerConfigurator $configurator): void + { + $this->services = $configurator->services(); + + $this->provideConverters(); + $this->provideParamConverterManager(); + $this->provideParamConverterListener(); + } + + private function provideConverters(): void + { + $this->services + ->set(ObjectFactory::class, ObjectFactory::class) + ->args( + [ + service(Serializer::class), + ] + ); + + foreach (self::PARAM_CONVERTERS as $paramConverter) { + $this->services + ->set($paramConverter, $paramConverter) + ->args( + [ + service(ObjectFactory::class), + ] + ); + } + } + + private function provideParamConverterManager(): void + { + $this->services->set(ParamConverterManager::class, ParamConverterManager::class); + $paramConverterManager = $this->services->get(ParamConverterManager::class); + + foreach (self::PARAM_CONVERTERS as $name => $paramConverterId) { + $paramConverterManager->call( + 'add', + [ + service($paramConverterId), + $name, + ] + ); + } + } + + private function provideParamConverterListener(): void + { + $this->services->set(Configurator::class, Configurator::class); + + $this->services + ->set(ParamConverterListener::class, ParamConverterListener::class) + ->public() + ->args( + [ + service(Configurator::class), + service(ParamConverterManager::class), + true, + ] + ); + } +} diff --git a/models/classes/ParamConverter/Request/AbstractParamConverter.php b/models/classes/ParamConverter/Request/AbstractParamConverter.php new file mode 100644 index 0000000000..eced57e117 --- /dev/null +++ b/models/classes/ParamConverter/Request/AbstractParamConverter.php @@ -0,0 +1,84 @@ +objectFactory = $objectFactory; + } + + public function apply(RequestInterface $request, ParamConverter $configuration): bool + { + try { + $options = $configuration->getOptions(); + $data = $this->getData($request, $options); + $object = $this->createObject($data, $configuration->getClass(), $options); + + $converted = $request->getAttribute(self::ATTRIBUTE_CONVERTED, []); + $converted[$configuration->getName()] = $object; + + $request->setAttribute(self::ATTRIBUTE_CONVERTED, $converted); + } catch (Throwable $exception) { + return false; + } + + return true; + } + + public function supports(ParamConverter $configuration): bool + { + return $configuration->getClass() !== null; + } + + abstract protected function getData(RequestInterface $request, array $options): array; + + private function createObject(array $data, string $class, array $options): object + { + $rule = $options[ParamConverter::OPTION_CREATION_RULE] ?? null; + $context = new ObjectFactoryContext( + [ + ObjectFactoryContext::PARAM_CLASS => $class, + ObjectFactoryContext::PARAM_DATA => $data, + ] + ); + + if ($rule === ParamConverter::RULE_CREATE) { + $object = $this->objectFactory->create($context); + } else { + $object = $this->objectFactory->deserialize($context); + } + + return $object; + } +} diff --git a/models/classes/ParamConverter/Request/ParamConverterInterface.php b/models/classes/ParamConverter/Request/ParamConverterInterface.php new file mode 100644 index 0000000000..f3258a0f63 --- /dev/null +++ b/models/classes/ParamConverter/Request/ParamConverterInterface.php @@ -0,0 +1,41 @@ +getQueryParameters(); + } +} diff --git a/models/classes/Serializer/Serializer.php b/models/classes/Serializer/Serializer.php new file mode 100644 index 0000000000..b6e649744e --- /dev/null +++ b/models/classes/Serializer/Serializer.php @@ -0,0 +1,52 @@ +serializer = $serializer; + } + + /** + * {@inheritdoc} + */ + public function serialize($data, string $format, array $context = []): string + { + return $this->serializer->serialize($data, $format, $context); + } + + /** + * {@inheritdoc} + */ + public function deserialize($data, string $type, string $format, array $context = []) + { + return $this->serializer->deserialize($data, $type, $format, $context); + } +} diff --git a/models/classes/Serializer/SerializerInterface.php b/models/classes/Serializer/SerializerInterface.php new file mode 100644 index 0000000000..2a940d1335 --- /dev/null +++ b/models/classes/Serializer/SerializerInterface.php @@ -0,0 +1,44 @@ +services(); + + $services->set(ObjectNormalizer::class, ObjectNormalizer::class); + $services->set(JsonEncoder::class, JsonEncoder::class); + + $services + ->set(SymfonySerializerInterface::class, SymfonySerializerAlias::class) + ->args( + [ + [ + service(ObjectNormalizer::class), + ], + [ + service(JsonEncoder::class), + ], + ] + ); + + $services + ->set(Serializer::class, Serializer::class) + ->args( + [ + service(SymfonySerializerInterface::class), + ] + ); + } +} diff --git a/models/classes/TestParamConverter/Query.php b/models/classes/TestParamConverter/Query.php new file mode 100644 index 0000000000..225eadaf3b --- /dev/null +++ b/models/classes/TestParamConverter/Query.php @@ -0,0 +1,44 @@ +uri = $uri; + $this->listOfUris = $listOfUris; + $this->subQuery = new SubQuery(); + } + + public function getUri(): string + { + return $this->uri; + } + + public function getListOfUris(): array + { + return $this->listOfUris; + } + + public function getSubQuery(): SubQuery + { + return $this->subQuery; + } + + public function setSubQuery(SubQuery $subQuery): void + { + $this->subQuery = $subQuery; + } +} diff --git a/models/classes/TestParamConverter/SubQuery.php b/models/classes/TestParamConverter/SubQuery.php new file mode 100644 index 0000000000..1b84c8ba77 --- /dev/null +++ b/models/classes/TestParamConverter/SubQuery.php @@ -0,0 +1,29 @@ +uri; + } + + public function getValue(): int + { + return $this->value; + } + + public function setValue(int $value): void + { + $this->value = $value; + } +} diff --git a/models/classes/routing/ActionEnforcer.php b/models/classes/routing/ActionEnforcer.php index 0f443cbc57..b70b453dcf 100644 --- a/models/classes/routing/ActionEnforcer.php +++ b/models/classes/routing/ActionEnforcer.php @@ -15,40 +15,50 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2014-2021 (original work) Open Assessment Technologies SA; - * - * + * Copyright (c) 2014-2022 (original work) Open Assessment Technologies SA; */ +declare(strict_types=1); + namespace oat\tao\model\routing; use Context; -use GuzzleHttp\Psr7\ServerRequest; -use oat\generis\model\Middleware\MiddlewareRequestHandler; -use oat\tao\model\routing\Contract\ActionFinderInterface; -use oat\tao\model\routing\Service\ActionFinder; -use Psr\Container\ContainerInterface; -use ReflectionException; use IExecutable; +use ReflectionMethod; +use ReflectionException; use ActionEnforcingException; -use oat\tao\model\http\ResponseEmitter; -use oat\oatbox\service\ServiceManagerAwareInterface; -use oat\oatbox\service\ServiceManagerAwareTrait; +use GuzzleHttp\Psr7\Response; +use common_session_SessionManager; +use GuzzleHttp\Psr7\ServerRequest; +use oat\oatbox\event\EventManager; use oat\tao\model\http\Controller; +use oat\oatbox\log\LoggerAwareTrait; +use Psr\Container\ContainerInterface; +use oat\tao\model\event\BeforeAction; +use oat\tao\model\http\ResponseEmitter; use Psr\Http\Message\ResponseInterface; +use oat\tao\model\accessControl\AclProxy; +use oat\oatbox\log\TaoLoggerAwareInterface; use Psr\Http\Message\ServerRequestInterface; -use ReflectionMethod; -use common_session_SessionManager; use tao_models_classes_AccessDeniedException; -use oat\tao\model\accessControl\AclProxy; +use oat\tao\model\action\CommonModuleInterface; +use oat\tao\model\routing\Service\ActionFinder; +use oat\oatbox\service\ServiceManagerAwareTrait; +use Doctrine\Common\Annotations\AnnotationReader; +use oat\oatbox\service\ServiceManagerAwareInterface; use oat\tao\model\accessControl\data\DataAccessControl; use oat\tao\model\accessControl\data\PermissionException; +use oat\tao\model\routing\Contract\ActionFinderInterface; +use oat\generis\model\Middleware\MiddlewareRequestHandler; +use oat\tao\model\HttpFoundation\Request\RequestInterface; use oat\tao\model\accessControl\func\AclProxy as FuncProxy; -use oat\oatbox\event\EventManager; -use oat\tao\model\event\BeforeAction; -use oat\oatbox\log\LoggerAwareTrait; -use oat\oatbox\log\TaoLoggerAwareInterface; -use oat\tao\model\action\CommonModuleInterface; +use oat\tao\model\ParamConverter\Event\ParamConverterEvent; +use oat\tao\model\ParamConverter\Configuration\ParamConverter; +use oat\tao\model\HttpFoundation\Factory\HttpFoundationFactory; +use oat\tao\model\ParamConverter\Request\ParamConverterInterface; +use oat\tao\model\ParamConverter\EventListener\ParamConverterListener; +use oat\tao\model\ParamConverter\Context\ParamConverterListenerContext; +use oat\tao\model\HttpFoundation\Factory\HttpFoundationFactoryInterface; /** * @TODO ActionEnforcer class documentation. @@ -135,6 +145,7 @@ protected function getResponse() if (!$this->response) { $this->response = $this->getContainer()->get(ResponseInterface::class); } + return $this->response; } @@ -242,15 +253,19 @@ public function resolve(ServerRequestInterface $request): ResponseInterface } /** + * @param mixed $controller + * * @throws ReflectionException */ private function resolveParameters(ServerRequestInterface $request, $controller, string $action): array { - // search parameters method - $reflect = new ReflectionMethod($controller, $action); + // Search parameters method + $reflect = new ReflectionMethod($controller, $action); $parameters = $this->getParameters(); + $actionParameters = []; + + $this->applyParamConverters($parameters, $request, $reflect); - $actionParameters = []; foreach ($reflect->getParameters() as $param) { $paramName = $param->getName(); $paramType = $param->getType(); @@ -258,12 +273,19 @@ private function resolveParameters(ServerRequestInterface $request, $controller, if (isset($parameters[$paramName])) { $actionParameters[$paramName] = $parameters[$paramName]; - } elseif($paramTypeName === ServerRequest::class) { + } elseif ($paramTypeName === ServerRequest::class) { $actionParameters[$paramName] = $request; - } elseif (class_exists($paramTypeName) || interface_exists($paramTypeName)) { + } elseif ($paramTypeName !== null && (class_exists($paramTypeName) || interface_exists($paramTypeName))) { $actionParameters[$paramName] = $this->getClassInstance($paramTypeName); } elseif (!$param->isDefaultValueAvailable()) { - $this->logWarning('Missing parameter ' . $paramName . ' for ' . $this->getControllerClass() . '@' . $action); + $this->logWarning( + sprintf( + 'Missing parameter %s for %s@%s', + $paramName, + $this->getControllerClass(), + $action + ) + ); } } @@ -308,6 +330,71 @@ private function getContainer(): ContainerInterface return $this->container; } + private function applyParamConverters( + array &$parameters, + ServerRequestInterface $request, + ReflectionMethod $reflectionMethod + ): void { + $configurations = $this->getParamConverters($reflectionMethod); + + if (empty($configurations)) { + return; + } + + $request = $this->getHttpFoundationFactory()->createRequest($request); + $request->setAttribute(ParamConverterListener::REQUEST_ATTRIBUTE_CONVERTERS, $configurations); + + $this->getEventManager()->trigger( + new ParamConverterEvent( + $this->createParamConverterListenerContext($request, $reflectionMethod) + ) + ); + + $parameters = array_merge( + $parameters, + $request->getAttribute(ParamConverterInterface::ATTRIBUTE_CONVERTED, []) + ); + } + + private function getParamConverters(ReflectionMethod $reflectionMethod): array + { + /** + * Ignore 'requiresRight' annotation as we don't have such annotation class in TAO. + * + * TODO Create annotation class and use it in all places instead of non-existing `requiresRight` + */ + AnnotationReader::addGlobalIgnoredName('requiresRight'); + + // Autoload 'ParamConverter' annotation. + class_exists(ParamConverter::class); + $annotations = (new AnnotationReader())->getMethodAnnotations($reflectionMethod); + + return array_filter($annotations, static function ($annotation) { + return $annotation instanceof ParamConverter; + }); + } + + private function createParamConverterListenerContext( + RequestInterface $request, + ReflectionMethod $reflectionMethod + ): ParamConverterListenerContext { + return new ParamConverterListenerContext([ + ParamConverterListenerContext::PARAM_REQUEST => $request, + ParamConverterListenerContext::PARAM_CONTROLLER => $reflectionMethod->class, + ParamConverterListenerContext::PARAM_METHOD => $reflectionMethod->getName(), + ]); + } + + private function getHttpFoundationFactory(): HttpFoundationFactoryInterface + { + return $this->getContainer()->get(HttpFoundationFactory::class); + } + + private function getEventManager(): EventManager + { + return $this->getContainer()->get(EventManager::SERVICE_ID); + } + private function getMiddlewareRequestHandler(): MiddlewareRequestHandler { return $this->getContainer()->get(MiddlewareRequestHandler::class); diff --git a/scripts/install/RegisterEvents.php b/scripts/install/RegisterEvents.php index 9c617671ac..7d9835a1a2 100644 --- a/scripts/install/RegisterEvents.php +++ b/scripts/install/RegisterEvents.php @@ -15,34 +15,38 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2020 (original work) Open Assessment Technologies SA; - * + * Copyright (c) 2020-2021 (original work) Open Assessment Technologies SA; */ declare(strict_types = 1); namespace oat\tao\scripts\install; +use oat\oatbox\reporting\Report; use oat\oatbox\event\EventManager; use oat\oatbox\extension\InstallAction; -use oat\generis\model\OntologyAwareTrait; +use common_ext_event_ExtensionInstalled; use oat\tao\model\migrations\MigrationsService; +use oat\tao\model\ParamConverter\Event\ParamConverterEvent; +use oat\tao\model\ParamConverter\EventListener\ParamConverterListener; -/** - * Class RegisterEvents - * @package oat\tao\scripts\install - */ class RegisterEvents extends InstallAction { - use OntologyAwareTrait; - public function __invoke($params) { - /** @var EventManager $eventManager */ - $eventManager = $this->getServiceManager()->get(EventManager::SERVICE_ID); - $eventManager->attach(\common_ext_event_ExtensionInstalled::class, [MigrationsService::SERVICE_ID, 'extensionInstalled']); - $this->getServiceManager()->register(EventManager::SERVICE_ID, $eventManager); + $eventManager = $this->getEventManager(); + $eventManager->attach( + common_ext_event_ExtensionInstalled::class, + [MigrationsService::SERVICE_ID, 'extensionInstalled'] + ); + $eventManager->attach(ParamConverterEvent::class, [ParamConverterListener::class, 'handleEvent']); + $this->getServiceLocator()->register(EventManager::SERVICE_ID, $eventManager); - return new \common_report_Report(\common_report_Report::TYPE_SUCCESS, 'Events registered'); + return Report::createSuccess('Events registered'); + } + + private function getEventManager(): EventManager + { + return $this->getServiceLocator()->getContainer()->get(EventManager::SERVICE_ID); } }