├── Configuration ├── Routing │ └── config.yaml ├── Icons.php ├── JavaScriptModules.php ├── ExpressionLanguage.php ├── Backend │ └── Modules.php └── RequestMiddlewares.php ├── Resources ├── Public │ ├── Css │ │ └── swagger-custom.css │ ├── ESM │ │ └── swagger-init.js │ └── Icons │ │ └── Extension.svg └── Private │ ├── Serializer │ └── Metadata │ │ ├── TYPO3.CMS.Extbase.Domain.Model.FileReference.yml │ │ ├── TYPO3.CMS.Extbase.Domain.Model.AbstractFileFolder.yml │ │ ├── TYPO3.CMS.Core.Resource.Folder.yml │ │ ├── TYPO3.CMS.Core.Resource.FileReference.yml │ │ ├── SourceBroker.T3api.Response.MainEndpointResponse.yml │ │ ├── SourceBroker.T3api.Response.AbstractCollectionResponse.yml │ │ ├── Throwable.yml │ │ ├── TYPO3.CMS.Core.Resource.File.yml │ │ ├── TYPO3.CMS.Extbase.Persistence.ObjectStorage.yml │ │ ├── TYPO3.CMS.Extbase.Persistence.Generic.LazyObjectStorage.yml │ │ ├── TYPO3.CMS.Extbase.DomainObject.AbstractDomainObject.yml │ │ ├── TYPO3.CMS.Core.Resource.AbstractFile.yml │ │ ├── TYPO3.CMS.Core.Resource.ResourceStorage.yml │ │ └── SourceBroker.T3api.Response.HydraCollectionResponse.yml │ ├── .htaccess │ ├── Templates │ └── Administration │ │ └── Documentation.html │ └── Language │ ├── locallang_modadministration.xlf │ └── locallang.xlf ├── Classes ├── Domain │ ├── Model │ │ ├── ItemOperation.php │ │ ├── CollectionOperationFactory.php │ │ ├── AbstractOperationResourceSettings.php │ │ ├── CollectionOperation.php │ │ ├── ApiFilterStrategy.php │ │ ├── OperationInterface.php │ │ ├── PersistenceSettings.php │ │ ├── UploadSettings.php │ │ ├── ApiFilter.php │ │ └── AbstractOperation.php │ └── Repository │ │ └── ApiResourceRepository.php ├── Annotation │ ├── Serializer │ │ ├── Type │ │ │ ├── TypeInterface.php │ │ │ ├── Rte.php │ │ │ ├── Typolink.php │ │ │ ├── PasswordHash.php │ │ │ ├── RecordUri.php │ │ │ ├── Image.php │ │ │ └── CurrentFeUser.php │ │ ├── Exclude.php │ │ ├── VirtualProperty.php │ │ ├── ReadOnlyProperty.php │ │ ├── Groups.php │ │ ├── MaxDepth.php │ │ └── SerializedName.php │ ├── ORM │ │ └── Cascade.php │ ├── ApiResource.php │ └── ApiFilter.php ├── Utility │ ├── ParameterUtility.php │ └── FileUtility.php ├── Exception │ ├── ExceptionInterface.php │ ├── OpenApiSupportingExceptionInterface.php │ ├── RouteNotFoundException.php │ ├── MethodNotAllowedException.php │ ├── ResourceNotFoundException.php │ ├── OperationNotAllowedException.php │ ├── AbstractException.php │ └── ValidationException.php ├── Processor │ ├── ProcessorInterface.php │ └── CorsProcessor.php ├── Dispatcher │ └── HeadlessDispatcher.php ├── ExpressionLanguage │ ├── ConditionFunctionsProvider.php │ ├── T3apiCoreProvider.php │ ├── ConditionProvider.php │ ├── T3apiCoreFunctionsProvider.php │ └── Resolver.php ├── Provider │ └── ApiResourcePath │ │ ├── ApiResourcePathProvider.php │ │ └── LoadedExtensionsDomainModelApiResourcePathProvider.php ├── Filter │ ├── OpenApiSupportingFilterInterface.php │ ├── FilterInterface.php │ ├── UidFilter.php │ ├── BooleanFilter.php │ ├── NumericFilter.php │ ├── OrderFilter.php │ ├── ContainFilter.php │ └── SearchFilter.php ├── Service │ ├── UrlService.php │ ├── ExpressionLanguageService.php │ ├── ValidationService.php │ ├── FileReferenceService.php │ ├── ReflectionService.php │ ├── StorageService.php │ ├── CorsService.php │ ├── FilesystemService.php │ ├── PropertyInfoService.php │ ├── SiteService.php │ └── RouteService.php ├── Serializer │ ├── ContextBuilder │ │ ├── ContextBuilderInterface.php │ │ ├── AbstractContextBuilder.php │ │ ├── SerializationContextBuilder.php │ │ └── DeserializationContextBuilder.php │ ├── Handler │ │ ├── DeserializeHandlerInterface.php │ │ ├── SerializeHandlerInterface.php │ │ ├── CurrentFeUserHandler.php │ │ ├── TypolinkHandler.php │ │ ├── RteHandler.php │ │ ├── PasswordHashHandler.php │ │ ├── RecordUriHandler.php │ │ ├── ObjectStorageHandler.php │ │ ├── AbstractHandler.php │ │ └── ImageHandler.php │ ├── Construction │ │ ├── ExtbaseObjectConstructor.php │ │ ├── InitializedObjectConstructor.php │ │ └── ObjectConstructorChain.php │ ├── Subscriber │ │ ├── ResourceTypeSubscriber.php │ │ ├── GenerateMetadataSubscriber.php │ │ ├── CurrentFeUserSubscriber.php │ │ ├── ThrowableSubscriber.php │ │ ├── FileReferenceSubscriber.php │ │ └── AbstractEntitySubscriber.php │ └── Accessor │ │ └── AccessorStrategy.php ├── OperationHandler │ ├── OperationHandlerInterface.php │ ├── ItemMethodNotAllowedOperationHandler.php │ ├── CollectionMethodNotAllowedOperationHandler.php │ ├── AbstractCollectionOperationHandler.php │ ├── ItemGetOperationHandler.php │ ├── ItemDeleteOperationHandler.php │ ├── CollectionPostOperationHandler.php │ ├── AbstractItemOperationHandler.php │ ├── ItemPatchOperationHandler.php │ ├── CollectionGetOperationHandler.php │ ├── ItemPutOperationHandler.php │ ├── AbstractOperationHandler.php │ └── FileUploadOperationHandler.php ├── Event │ ├── AfterProcessOperationEvent.php │ ├── AfterDeserializeOperationEvent.php │ ├── BeforeFilterAccessGrantedEvent.php │ ├── BeforeOperationAccessGrantedEvent.php │ ├── AfterCreateContextForOperationEvent.php │ └── BeforeOperationAccessGrantedPostDenormalizeEvent.php ├── Response │ ├── MainEndpointResponse.php │ └── AbstractCollectionResponse.php ├── Hook │ └── EnrichHashBase.php ├── Security │ ├── FilterAccessChecker.php │ ├── OperationAccessChecker.php │ └── AbstractAccessChecker.php ├── EventListener │ ├── EnrichPageCacheIdentifierParametersEventListener.php │ ├── AddHydraCollectionResponseSerializationGroupEventListener.php │ └── EnrichSerializationContextEventListener.php ├── Middleware │ ├── T3apiRequestResolver.php │ └── T3apiRequestLanguageResolver.php ├── Configuration │ ├── CorsOptions.php │ └── Configuration.php ├── Routing │ └── Enhancer │ │ └── ResourceEnhancer.php ├── Factory │ └── ApiResourceFactory.php ├── Controller │ └── OpenApiController.php └── ViewHelpers │ └── InlineViewHelper.php ├── ext_emconf.php ├── README.rst └── composer.json /Configuration/Routing/config.yaml: -------------------------------------------------------------------------------- 1 | routeEnhancers: 2 | T3api: 3 | type: T3apiResourceEnhancer 4 | -------------------------------------------------------------------------------- /Resources/Public/Css/swagger-custom.css: -------------------------------------------------------------------------------- 1 | /* Hides server selection as it is useless for now - we display always only one server */ 2 | #t3api-swagger-ui .scheme-container, 3 | #t3api-swagger-ui .info .link 4 | { 5 | display: none; 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Extbase.Domain.Model.FileReference.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Extbase\Domain\Model\FileReference: 2 | properties: 3 | uidLocal: 4 | exclude: true 5 | configurationManager: 6 | exclude: true 7 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ItemOperation.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, 6 | 'source' => 'EXT:t3api/Resources/Public/Icons/Extension.svg', 7 | ], 8 | ]; 9 | -------------------------------------------------------------------------------- /Resources/Private/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache < 2.3 2 | 3 | Order allow,deny 4 | Deny from all 5 | Satisfy All 6 | 7 | 8 | # Apache >= 2.3 9 | 10 | Require all denied 11 | 12 | -------------------------------------------------------------------------------- /Configuration/JavaScriptModules.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'core', 6 | 'backend', 7 | ], 8 | 'imports' => [ 9 | '@sourcebroker/t3api/' => 'EXT:t3api/Resources/Public/ESM/', 10 | ], 11 | ]; 12 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Extbase.Domain.Model.AbstractFileFolder.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Extbase\Domain\Model\AbstractFileFolder: 2 | properties: 3 | originalResource: 4 | type: \TYPO3\CMS\Core\Resource\ResourceInterface 5 | exclude: true 6 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Core.Resource.Folder.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Core\Resource\Folder: 2 | properties: 3 | fileAndFolderNameFilters: 4 | exclude: true 5 | storage: 6 | type: TYPO3\CMS\Core\Resource\ResourceStorage 7 | exclude: true 8 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/Type/TypeInterface.php: -------------------------------------------------------------------------------- 1 | [ 7 | \SourceBroker\T3api\ExpressionLanguage\ConditionProvider::class, 8 | \SourceBroker\T3api\ExpressionLanguage\T3apiCoreProvider::class, 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/Exclude.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/ReadOnlyProperty.php: -------------------------------------------------------------------------------- 1 | 15 | * @Required 16 | */ 17 | public $groups; 18 | } 19 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/MaxDepth.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function getAll(): iterable; 15 | } 16 | -------------------------------------------------------------------------------- /Classes/Annotation/ORM/Cascade.php: -------------------------------------------------------------------------------- 1 | values = (array)$values['value']; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Classes/ExpressionLanguage/T3apiCoreProvider.php: -------------------------------------------------------------------------------- 1 | expressionLanguageProviders = [ 14 | T3apiCoreFunctionsProvider::class, 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Classes/Filter/OpenApiSupportingFilterInterface.php: -------------------------------------------------------------------------------- 1 | expressionLanguageProviders = [ 16 | ConditionFunctionsProvider::class, 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Classes/Serializer/ContextBuilder/ContextBuilderInterface.php: -------------------------------------------------------------------------------- 1 | getExpressionLanguage(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Resources/Public/ESM/swagger-init.js: -------------------------------------------------------------------------------- 1 | let checkExist = setInterval(function() { 2 | if (window.SwaggerUIBundle) { 3 | clearInterval(checkExist); 4 | const element = document.getElementById('t3api-swagger-ui'); 5 | window.ui = SwaggerUIBundle({ 6 | url: element.dataset.specUrl, 7 | dom_id: '#t3api-swagger-ui', 8 | deepLinking: true, 9 | presets: [ 10 | SwaggerUIBundle.presets.apis, 11 | SwaggerUIStandalonePreset 12 | ], 13 | plugins: [ 14 | SwaggerUIBundle.plugins.DownloadUrl 15 | ], 16 | }); 17 | } 18 | }, 100); 19 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/DeserializeHandlerInterface.php: -------------------------------------------------------------------------------- 1 | title = self::translate('exception.route_not_found.title'); 14 | parent::__construct(self::translate('exception.route_not_found.description'), $code); 15 | } 16 | 17 | public function getStatusCode(): int 18 | { 19 | return Response::HTTP_NOT_FOUND; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/Type/RecordUri.php: -------------------------------------------------------------------------------- 1 | identifier]; 24 | } 25 | 26 | public function getName(): string 27 | { 28 | return RecordUriHandler::TYPE; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Core.Resource.ResourceStorage.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Core\Resource\ResourceStorage: 2 | properties: 3 | driver: 4 | exclude: true 5 | fileProcessingService: 6 | exclude: true 7 | userPermissions: 8 | exclude: true 9 | signalSlotDispatcher: 10 | exclude: true 11 | fileAndFolderNameFilters: 12 | exclude: true 13 | isOnline: 14 | exclude: true 15 | isDefault: 16 | exclude: true 17 | processingFolder: 18 | type: TYPO3\CMS\Core\Resource\Folder 19 | exclude: true 20 | processingFolders: 21 | type: array 22 | exclude: true 23 | -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'T3api', 6 | 'description' => 'REST API for your TYPO3 project. Config with annotations, build in filtering, pagination, typolinks, image processing, serialization contexts, responses in Hydra/JSON-LD format.', 7 | 'category' => 'plugin', 8 | 'author' => 'Inscript Team', 9 | 'author_email' => 'office@inscript.dev', 10 | 'state' => 'stable', 11 | 'version' => '4.1.3', 12 | 'constraints' => [ 13 | 'depends' => [ 14 | 'php' => '8.1.0-8.3.99', 15 | 'typo3' => '12.4.00-13.4.99', 16 | ], 17 | 'conflicts' => [], 18 | 'suggests' => [], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /Classes/OperationHandler/OperationHandlerInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Web API 8 | 9 | 10 | Check your REST API documentation 11 | 12 | 13 | This module provides an easy way to check available endpoints of your REST API and test it using SwaggerUI 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Configuration/Backend/Modules.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'parent' => 'tools', 6 | 'position' => ['before' => '*'], 7 | 'access' => 'group,user', 8 | 'iconIdentifier' => 'ext-t3api', 9 | 'labels' => 'LLL:EXT:t3api/Resources/Private/Language/locallang_modadministration.xlf:mlang_tabs_tab', 10 | 'inheritNavigationComponentFromMainModule' => false, 11 | 'path' => '/module/t3api', 12 | 'routes' => [ 13 | '_default' => [ 14 | 'target' => \SourceBroker\T3api\Controller\AdministrationController::class . '::documentationAction', 15 | ], 16 | 'open_api_resources' => [ 17 | 'target' => \SourceBroker\T3api\Controller\OpenApiController::class . '::resourcesAction', 18 | ], 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /Classes/OperationHandler/ItemMethodNotAllowedOperationHandler.php: -------------------------------------------------------------------------------- 1 | name); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Classes/Event/AfterProcessOperationEvent.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 21 | $this->result = $result; 22 | } 23 | 24 | public function getOperation(): OperationInterface 25 | { 26 | return $this->operation; 27 | } 28 | 29 | public function getResult() 30 | { 31 | return $this->result; 32 | } 33 | 34 | public function setResult($result): void 35 | { 36 | $this->result = $result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Classes/OperationHandler/CollectionMethodNotAllowedOperationHandler.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'sourcebroker/t3api/prepare-api-request' => [ 8 | 'target' => \SourceBroker\T3api\Middleware\T3apiRequestLanguageResolver::class, 9 | 'after' => [ 10 | 'typo3/cms-frontend/site', 11 | ], 12 | 'before' => [ 13 | 'typo3/cms-frontend/tsfe', 14 | ], 15 | ], 16 | 'sourcebroker/t3api/process-api-request' => [ 17 | 'target' => \SourceBroker\T3api\Middleware\T3apiRequestResolver::class, 18 | 'after' => [ 19 | 'typo3/cms-frontend/prepare-tsfe-rendering', 20 | ], 21 | 'before' => [ 22 | 'typo3/cms-frontend/shortcut-and-mountpoint-redirect', 23 | ], 24 | ], 25 | ], 26 | ]; 27 | -------------------------------------------------------------------------------- /Classes/Event/AfterDeserializeOperationEvent.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 19 | $this->object = $object; 20 | } 21 | 22 | public function getOperation(): OperationInterface 23 | { 24 | return $this->operation; 25 | } 26 | 27 | public function getObject(): AbstractDomainObject 28 | { 29 | return $this->object; 30 | } 31 | 32 | public function setObject($object): void 33 | { 34 | $this->object = $object; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Classes/Response/MainEndpointResponse.php: -------------------------------------------------------------------------------- 1 | apiResourceRepository->getAll() as $apiResource) { 22 | if (!$apiResource->getMainCollectionOperation() instanceof CollectionOperation) { 23 | continue; 24 | } 25 | 26 | $resources[$apiResource->getEntity()] = $apiResource->getMainCollectionOperation()->getRoute()->getPath(); 27 | } 28 | 29 | return $resources; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Classes/Service/ValidationService.php: -------------------------------------------------------------------------------- 1 | validatorResolver->getBaseValidatorConjunction(get_class($obj)); 22 | $validationResults = $validator->validate($obj); 23 | 24 | if ($validationResults->hasErrors()) { 25 | throw new ValidationException($validationResults, 1581461085077); 26 | } 27 | 28 | return $validationResults; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/Type/Image.php: -------------------------------------------------------------------------------- 1 | width, $this->height, $this->maxWidth, $this->maxHeight, $this->cropVariant]; 43 | } 44 | 45 | public function getName(): string 46 | { 47 | return ImageHandler::TYPE; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Classes/Event/BeforeFilterAccessGrantedEvent.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 20 | $this->expressionLanguageVariables = $expressionLanguageVariables; 21 | } 22 | 23 | public function getFilter(): ApiFilter 24 | { 25 | return $this->filter; 26 | } 27 | 28 | public function getExpressionLanguageVariables(): array 29 | { 30 | return $this->expressionLanguageVariables; 31 | } 32 | 33 | public function setExpressionLanguageVariable(string $name, $value): void 34 | { 35 | $this->expressionLanguageVariables[$name] = $value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Classes/Hook/EnrichHashBase.php: -------------------------------------------------------------------------------- 1 | = v13 \SourceBroker\T3api\EventListener\EnrichPageCacheIdentifierParametersEventListener is doing the same job. 10 | class EnrichHashBase 11 | { 12 | /** 13 | * We add random value to hash base to protect against full page cache which causes at least two known issues in 14 | * production environment: 15 | * 1. Extbase framework configuration is not loaded thus tables mapping is unknown and queries to not existing database tables may be done. 16 | * 2. Links generated using link handler are not build correctly. 17 | */ 18 | public function init(array &$params): void 19 | { 20 | if (RouteService::routeHasT3ApiResourceEnhancerQueryParam()) { 21 | $params['hashParameters']['t3api_hash_base_random'] = microtime(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Classes/Service/FileReferenceService.php: -------------------------------------------------------------------------------- 1 | getPublicUrl() === null || $originalResource->getPublicUrl() === '') { 15 | trigger_error( 16 | sprintf( 17 | 'Could not get public URL for file UID:%d. It is probably missing in filesystem.', 18 | $originalResource->getProperty('uid') 19 | ), 20 | E_USER_WARNING 21 | ); 22 | return null; 23 | } 24 | 25 | return UrlService::forceAbsoluteUrl( 26 | $originalResource->getPublicUrl(), 27 | $context->getAttribute('TYPO3_SITE_URL') 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Domain/Model/CollectionOperation.php: -------------------------------------------------------------------------------- 1 | pagination = Pagination::create($params['attributes'] ?? [], $apiResource->getPagination()); 20 | } 21 | 22 | public function addFilter(ApiFilter $apiFilter): void 23 | { 24 | $this->filters[] = $apiFilter; 25 | } 26 | 27 | /** 28 | * @return ApiFilter[] 29 | */ 30 | public function getFilters(): array 31 | { 32 | return $this->filters; 33 | } 34 | 35 | public function getPagination(): Pagination 36 | { 37 | return $this->pagination; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Resources/Public/Icons/Extension.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | { } 8 | . ... 9 | ..... 10 | ... . 11 | ..... 12 | . ... 13 | 14 | 15 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ApiFilterStrategy.php: -------------------------------------------------------------------------------- 1 | name = !empty($strategy) ? $strategy : ''; 17 | } elseif (is_array($strategy)) { 18 | $this->name = $strategy['name'] ?? ''; 19 | $this->condition = $strategy['condition'] ?? ''; 20 | } else { 21 | throw new \InvalidArgumentException( 22 | sprintf('%s::$strategy has to be either string or array', self::class), 23 | 1587649745 24 | ); 25 | } 26 | } 27 | 28 | public function getName(): ?string 29 | { 30 | return $this->name; 31 | } 32 | 33 | public function getCondition(): ?string 34 | { 35 | return $this->condition; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Classes/Security/FilterAccessChecker.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch($event); 19 | $expressionLanguageVariables = $event->getExpressionLanguageVariables(); 20 | 21 | if (empty($filter->getStrategy()->getCondition())) { 22 | return true; 23 | } 24 | 25 | $variables = array_merge($expressionLanguageVariables, ['t3apiFilter' => $filter]); 26 | 27 | return $this->getExpressionLanguageResolver($variables)->evaluate($filter->getStrategy()->getCondition()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Classes/Serializer/ContextBuilder/AbstractContextBuilder.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch( 23 | new AfterCreateContextForOperationEvent( 24 | $operation, 25 | $request, 26 | $context 27 | ) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Event/BeforeOperationAccessGrantedEvent.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 20 | $this->expressionLanguageVariables = $expressionLanguageVariables; 21 | } 22 | 23 | public function getOperation(): OperationInterface 24 | { 25 | return $this->operation; 26 | } 27 | 28 | public function getExpressionLanguageVariables(): array 29 | { 30 | return $this->expressionLanguageVariables; 31 | } 32 | 33 | public function setExpressionLanguageVariable(string $name, $value): void 34 | { 35 | $this->expressionLanguageVariables[$name] = $value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Classes/Filter/UidFilter.php: -------------------------------------------------------------------------------- 1 | getQuerySettings()->setRespectSysLanguage(false); 21 | $languageAspect = new LanguageAspect( 22 | $query->getQuerySettings()->getLanguageAspect()->getId(), 23 | $query->getQuerySettings()->getLanguageAspect()->getContentId(), 24 | LanguageAspect::OVERLAYS_ON 25 | ); 26 | $query->getQuerySettings()->setLanguageAspect($languageAspect); 27 | 28 | return parent::filterProperty($property, $values, $query, $apiFilter); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Event/AfterCreateContextForOperationEvent.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 25 | $this->request = $request; 26 | $this->context = $context; 27 | } 28 | 29 | public function getOperation(): OperationInterface 30 | { 31 | return $this->operation; 32 | } 33 | 34 | public function getRequest(): Request 35 | { 36 | return $this->request; 37 | } 38 | 39 | public function getContext(): Context 40 | { 41 | return $this->context; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/ExpressionLanguage/T3apiCoreFunctionsProvider.php: -------------------------------------------------------------------------------- 1 | getForceAbsoluteUrlFunction(), 20 | ]; 21 | } 22 | 23 | protected function getForceAbsoluteUrlFunction(): ExpressionFunction 24 | { 25 | return new ExpressionFunction( 26 | 'force_absolute_url', 27 | static function (): void {}, 28 | static function ($existingVariables, string $url, string $fallbackHost): string { 29 | return UrlService::forceAbsoluteUrl($url, $fallbackHost); 30 | } 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Classes/Serializer/Construction/InitializedObjectConstructor.php: -------------------------------------------------------------------------------- 1 | hasAttribute('target') && $context->getDepth() === 1) { 29 | return $context->getAttribute('target'); 30 | } 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Classes/Event/BeforeOperationAccessGrantedPostDenormalizeEvent.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 20 | $this->expressionLanguageVariables = $expressionLanguageVariables; 21 | } 22 | 23 | public function getOperation(): OperationInterface 24 | { 25 | return $this->operation; 26 | } 27 | 28 | public function getExpressionLanguageVariables(): array 29 | { 30 | return $this->expressionLanguageVariables; 31 | } 32 | 33 | public function setExpressionLanguageVariable(string $name, $value): void 34 | { 35 | $this->expressionLanguageVariables[$name] = $value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Classes/Exception/MethodNotAllowedException.php: -------------------------------------------------------------------------------- 1 | title = self::translate('exception.method_not_allowed.title'); 15 | 16 | try { 17 | $className = (new \ReflectionClass($operation))->getShortName(); 18 | } catch (\ReflectionException $exception) { 19 | $className = self::class; 20 | } 21 | 22 | parent::__construct( 23 | self::translate( 24 | 'exception.method_not_allowed.description', 25 | [ 26 | $operation->getMethod(), 27 | $className, 28 | ] 29 | ), 30 | $code 31 | ); 32 | } 33 | 34 | public function getStatusCode(): int 35 | { 36 | return Response::HTTP_METHOD_NOT_ALLOWED; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/CurrentFeUserHandler.php: -------------------------------------------------------------------------------- 1 | persistenceManager->getObjectByIdentifier($data, $type['params'][0]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Classes/Exception/ResourceNotFoundException.php: -------------------------------------------------------------------------------- 1 | statusCode(SymfonyResponse::HTTP_NOT_FOUND) 17 | ->description(self::translate('exception.resource_not_found.title')); 18 | } 19 | 20 | public function __construct(string $resourceType, int $uid, int $code) 21 | { 22 | $this->title = self::translate('exception.resource_not_found.title'); 23 | parent::__construct( 24 | self::translate('exception.resource_not_found.description', [$resourceType, $uid]), 25 | $code 26 | ); 27 | } 28 | 29 | public function getStatusCode(): int 30 | { 31 | return Response::HTTP_NOT_FOUND; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Classes/OperationHandler/AbstractCollectionOperationHandler.php: -------------------------------------------------------------------------------- 1 | operationAccessChecker->isGranted($operation)) { 28 | throw new OperationNotAllowedException($operation, 1574416639472); 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Classes/Service/ReflectionService.php: -------------------------------------------------------------------------------- 1 | contentObjectRenderer->typoLink_URL([ 35 | 'parameter' => $typolinkParameter, 36 | ]); 37 | 38 | return UrlService::forceAbsoluteUrl($url, $context->getAttribute('TYPO3_SITE_URL')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Classes/Filter/BooleanFilter.php: -------------------------------------------------------------------------------- 1 | name($apiFilter->getParameterName()) 24 | ->schema(Schema::boolean()), 25 | ]; 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function filterProperty( 32 | string $property, 33 | $values, 34 | QueryInterface $query, 35 | ApiFilter $apiFilter 36 | ): ?ConstraintInterface { 37 | return $query->equals($property, ParameterUtility::toBoolean(((array)$values)[0])); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Classes/Filter/NumericFilter.php: -------------------------------------------------------------------------------- 1 | name($apiFilter->getParameterName()) 24 | ->schema(Schema::integer()), 25 | ]; 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | * @throws InvalidQueryException 31 | */ 32 | public function filterProperty( 33 | string $property, 34 | $values, 35 | QueryInterface $query, 36 | ApiFilter $apiFilter 37 | ): ?ConstraintInterface { 38 | return $query->in($property, array_map('intval', (array)$values)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Classes/Domain/Model/OperationInterface.php: -------------------------------------------------------------------------------- 1 | getDescendantPageIdsRecursive( 30 | $startPid, 31 | $recursionDepth, 32 | ); 33 | 34 | if ($pids !== '') { 35 | $recursiveStoragePids[] = $pids; 36 | } 37 | } 38 | 39 | return array_unique(array_merge($storagePids, ...$recursiveStoragePids)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Service/CorsService.php: -------------------------------------------------------------------------------- 1 | isWildcard($options->allowOrigin)) { 21 | return true; 22 | } 23 | 24 | if ($options->originRegex) { 25 | foreach ($options->allowOrigin as $originRegexp) { 26 | if (preg_match('{' . $originRegexp . '}i', $origin)) { 27 | return true; 28 | } 29 | } 30 | } elseif (in_array($origin, $options->allowOrigin, true)) { 31 | return true; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | public function isWildcard($option): bool 38 | { 39 | return $option === true 40 | || (is_array($option) && in_array('*', $option, true)) 41 | || (is_string($option) && $option === '*'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/Exception/OperationNotAllowedException.php: -------------------------------------------------------------------------------- 1 | statusCode(SymfonyResponse::HTTP_NOT_FOUND) 18 | ->description(self::translate('exception.resource_not_found.title')); 19 | } 20 | 21 | public function __construct(OperationInterface $operation, int $code) 22 | { 23 | $this->title = self::translate('exception.operation_not_allowed.title'); 24 | 25 | parent::__construct( 26 | self::translate( 27 | 'exception.operation_not_allowed.description', 28 | [$operation->getPath()] 29 | ), 30 | $code 31 | ); 32 | } 33 | 34 | public function getStatusCode(): int 35 | { 36 | return Response::HTTP_FORBIDDEN; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Classes/OperationHandler/ItemGetOperationHandler.php: -------------------------------------------------------------------------------- 1 | isMethodGet(); 20 | } 21 | 22 | /** 23 | * @noinspection ReferencingObjectsInspection 24 | * @throws OperationNotAllowedException 25 | * @throws ResourceNotFoundException 26 | */ 27 | public function handle( 28 | OperationInterface $operation, 29 | Request $request, 30 | array $route, 31 | ?ResponseInterface &$response 32 | ): AbstractDomainObject { 33 | /** @var ItemOperation $operation */ 34 | return parent::handle($operation, $request, $route, $response); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Classes/Service/FilesystemService.php: -------------------------------------------------------------------------------- 1 | clearAllActive($directory); 27 | } 28 | 29 | if ($keepOriginalDirectory) { 30 | GeneralUtility::mkdir($directory); 31 | } 32 | 33 | clearstatcache(); 34 | $result = GeneralUtility::rmdir($temporaryDirectory, true); 35 | } 36 | } 37 | 38 | return $result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Classes/Serializer/ContextBuilder/SerializationContextBuilder.php: -------------------------------------------------------------------------------- 1 | enableMaxDepthChecks() 21 | ->setSerializeNull(true); 22 | } 23 | 24 | /** 25 | * @return SerializationContext 26 | */ 27 | public function createFromOperation( 28 | OperationInterface $operation, 29 | Request $request 30 | ): Context { 31 | $context = $this->create(); 32 | 33 | $attributes = $operation->getNormalizationContext() ?? []; 34 | 35 | foreach ($attributes as $attributeName => $attributeValue) { 36 | $context->setAttribute($attributeName, $attributeValue); 37 | } 38 | 39 | $this->dispatchAfterCreateContextForOperationEvent( 40 | $operation, 41 | $request, 42 | $context 43 | ); 44 | return $context; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Classes/Serializer/Subscriber/ResourceTypeSubscriber.php: -------------------------------------------------------------------------------- 1 | Events::POST_SERIALIZE, 21 | 'method' => 'onPostSerialize', 22 | ], 23 | ]; 24 | } 25 | 26 | public function onPostSerialize(ObjectEvent $event): void 27 | { 28 | if (!$event->getObject() instanceof AbstractDomainObject) { 29 | return; 30 | } 31 | 32 | $entity = $event->getObject(); 33 | /** @var SerializationVisitorInterface $visitor */ 34 | $visitor = $event->getVisitor(); 35 | 36 | $type = get_class($entity); 37 | $visitor->visitProperty( 38 | new StaticPropertyMetadata(AbstractDomainObject::class, '@type', $type), 39 | $type 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/RteHandler.php: -------------------------------------------------------------------------------- 1 | getContentObjectRenderer() 29 | ->parseFunc($text, [], '< lib.parseFunc_RTE'); 30 | } 31 | 32 | protected function getContentObjectRenderer(): ContentObjectRenderer 33 | { 34 | static $contentObjectRenderer; 35 | 36 | if (!$contentObjectRenderer instanceof ContentObjectRenderer) { 37 | $contentObjectRenderer 38 | = GeneralUtility::makeInstance(ContentObjectRenderer::class); 39 | } 40 | 41 | return $contentObjectRenderer; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/EventListener/EnrichPageCacheIdentifierParametersEventListener.php: -------------------------------------------------------------------------------- 1 | setPageCacheIdentifierParameters([ 25 | ...$beforePageCacheIdentifierIsHashedEvent->getPageCacheIdentifierParameters(), 26 | 't3api_hash_base_random' => microtime(), 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Classes/EventListener/AddHydraCollectionResponseSerializationGroupEventListener.php: -------------------------------------------------------------------------------- 1 | getOperation(); 17 | $context = $createContextForOperationEvent->getContext(); 18 | 19 | $collectionResponseClass = Configuration::getCollectionResponseClass(); 20 | if ( 21 | ( 22 | $collectionResponseClass === HydraCollectionResponse::class 23 | || is_subclass_of($collectionResponseClass, HydraCollectionResponse::class) 24 | ) 25 | && $createContextForOperationEvent->getOperation() instanceof CollectionOperation 26 | && $context->hasAttribute('groups') 27 | && $operation->isMethodGet() 28 | ) { 29 | $context->setGroups(array_merge( 30 | $context->getAttribute('groups'), 31 | ['__hydra_collection_response'] 32 | )); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Classes/Domain/Model/PersistenceSettings.php: -------------------------------------------------------------------------------- 1 | storagePids = GeneralUtility::intExplode(',', $attributes['storagePid']); 25 | } 26 | $persistenceSettings->recursionLevel = (int)($attributes['recursive'] ?? $persistenceSettings->recursionLevel); 27 | 28 | return $persistenceSettings; 29 | } 30 | 31 | /** 32 | * @return int[] 33 | */ 34 | public function getStoragePids(): array 35 | { 36 | return $this->storagePids; 37 | } 38 | 39 | public function getRecursionLevel(): int 40 | { 41 | return $this->recursionLevel; 42 | } 43 | 44 | public function getMainStoragePid(): int 45 | { 46 | if (empty($this->storagePids)) { 47 | return 0; 48 | } 49 | 50 | return $this->storagePids[0]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Classes/Annotation/ApiResource.php: -------------------------------------------------------------------------------- 1 | itemOperations = $values['itemOperations'] ?? $this->itemOperations; 32 | $this->collectionOperations = $values['collectionOperations'] ?? $this->collectionOperations; 33 | $this->attributes = array_merge( 34 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['pagination'] ?? [], 35 | $values['attributes'] ?? [] 36 | ); 37 | } 38 | 39 | public function getItemOperations(): array 40 | { 41 | return $this->itemOperations; 42 | } 43 | 44 | public function getCollectionOperations(): array 45 | { 46 | return $this->collectionOperations; 47 | } 48 | 49 | public function getAttributes(): array 50 | { 51 | return $this->attributes; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/Type/CurrentFeUser.php: -------------------------------------------------------------------------------- 1 | feUserClass = $options['value']; 43 | } 44 | 45 | public function getParams(): array 46 | { 47 | return [$this->feUserClass]; 48 | } 49 | 50 | public function getName(): string 51 | { 52 | return CurrentFeUserHandler::TYPE; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/PasswordHashHandler.php: -------------------------------------------------------------------------------- 1 | passwordHashFactory->getDefaultHashInstance('FE')->getHashedPassword($data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Classes/Serializer/Subscriber/GenerateMetadataSubscriber.php: -------------------------------------------------------------------------------- 1 | Events::PRE_SERIALIZE, 20 | 'method' => 'onPreSerialize', 21 | ], 22 | [ 23 | 'event' => Events::PRE_DESERIALIZE, 24 | 'method' => 'onPreDeserialize', 25 | ], 26 | ]; 27 | } 28 | 29 | /** 30 | * @throws \ReflectionException 31 | */ 32 | public function onPreSerialize(ObjectEvent $event): void 33 | { 34 | if (class_exists($event->getType()['name'])) { 35 | SerializerMetadataService::generateAutoloadForClass($event->getType()['name']); 36 | } 37 | } 38 | 39 | /** 40 | * @throws \ReflectionException 41 | */ 42 | public function onPreDeserialize(PreDeserializeEvent $event): void 43 | { 44 | if (class_exists($event->getType()['name'])) { 45 | SerializerMetadataService::generateAutoloadForClass($event->getType()['name']); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Classes/OperationHandler/ItemDeleteOperationHandler.php: -------------------------------------------------------------------------------- 1 | isMethodDelete(); 21 | } 22 | 23 | /** 24 | * @return mixed|null 25 | * @noinspection ReferencingObjectsInspection 26 | * @throws OperationNotAllowedException 27 | * @throws ResourceNotFoundException 28 | */ 29 | public function handle(OperationInterface $operation, Request $request, array $route, ?ResponseInterface &$response) 30 | { 31 | /** @var ItemOperation $operation */ 32 | $repository = $this->getRepositoryForOperation($operation); 33 | $object = parent::handle($operation, $request, $route, $response); 34 | $repository->remove($object); 35 | GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); 36 | $object = null; 37 | 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Classes/Service/PropertyInfoService.php: -------------------------------------------------------------------------------- 1 | getProperty($propertyName); 21 | $annotations = $annotationReader->getPropertyAnnotations($propertyReflection); 22 | $cascadeAnnotations = array_filter( 23 | $annotations, 24 | static function ($annotation): bool { 25 | return $annotation instanceof Cascade; 26 | } 27 | ); 28 | 29 | /** @var Cascade $cascadeAnnotation */ 30 | foreach ($cascadeAnnotations as $cascadeAnnotation) { 31 | if (in_array('persist', $cascadeAnnotation->values, true)) { 32 | return true; 33 | } 34 | } 35 | } catch (\Exception $exception) { 36 | throw new \RuntimeException( 37 | 'It was not possible to check if property allows cascade persistence due to exception', 38 | 1584949881062, 39 | $exception 40 | ); 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Resource not found 8 | 9 | 10 | Could not find resource type `%s` with uid %s 11 | 12 | 13 | Route not found 14 | 15 | 16 | Could not find route 17 | 18 | 19 | Validation error 20 | 21 | 22 | An error occurred during object validation 23 | 24 | 25 | Method not allowed 26 | 27 | 28 | Method `%s` is not allowed for %s 29 | 30 | 31 | Operation not allowed 32 | 33 | 34 | You are not allowed to access operation `%s` 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Classes/Middleware/T3apiRequestResolver.php: -------------------------------------------------------------------------------- 1 | bootstrap = $bootstrap; 22 | } 23 | 24 | /** 25 | * @throws \Throwable 26 | */ 27 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 28 | { 29 | if (RouteService::routeHasT3ApiResourceEnhancerQueryParam($request)) { 30 | return $this->bootstrap->process($this->cleanupRequest($request)); 31 | } 32 | 33 | return $handler->handle($request); 34 | } 35 | 36 | /** 37 | * Removes `t3apiResource` query parameter as it may break further functionality. 38 | * This parameter is needed only to reach a handler - further processing should not rely on it. 39 | */ 40 | private function cleanupRequest(ServerRequestInterface $request): ServerRequestInterface 41 | { 42 | $cleanedQueryParams = $request->getQueryParams(); 43 | unset($cleanedQueryParams[ResourceEnhancer::PARAMETER_NAME]); 44 | 45 | return $request->withQueryParams($cleanedQueryParams); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Classes/Configuration/CorsOptions.php: -------------------------------------------------------------------------------- 1 | allowCredentials = isset($options['allowCredentials']) ? (bool)$options['allowCredentials'] : $this->allowCredentials; 26 | $this->allowOrigin = isset($options['allowOrigin']) ? (array)$options['allowOrigin'] : $this->allowOrigin; 27 | $this->allowHeaders = isset($options['allowHeaders']) ? (array)$options['allowHeaders'] : $this->allowHeaders; 28 | $this->allowHeaders = array_merge( 29 | $this->allowHeaders, 30 | (isset($options['simpleHeaders']) ? (array)$options['simpleHeaders'] : []) 31 | ); 32 | $this->allowHeaders = array_map('strtolower', $this->allowHeaders); 33 | $this->allowMethods = isset($options['allowMethods']) ? 34 | array_map('strtoupper', (array)$options['allowMethods']) : $this->allowMethods; 35 | $this->exposeHeaders = isset($options['exposeHeaders']) ? (array)$options['exposeHeaders'] : $this->exposeHeaders; 36 | $this->maxAge = isset($options['maxAge']) ? (int)$options['maxAge'] : $this->maxAge; 37 | $this->originRegex = isset($options['originRegex']) ? (bool)$options['originRegex'] : $this->originRegex; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Classes/EventListener/EnrichSerializationContextEventListener.php: -------------------------------------------------------------------------------- 1 | GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY'), 16 | 'TYPO3_PORT' => GeneralUtility::getIndpEnv('TYPO3_PORT'), 17 | 'TYPO3_REQUEST_HOST' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST'), 18 | 'TYPO3_REQUEST_URL' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'), 19 | 'TYPO3_REQUEST_SCRIPT' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_SCRIPT'), 20 | 'TYPO3_REQUEST_DIR' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR'), 21 | 'TYPO3_SITE_URL' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), 22 | 'TYPO3_SITE_PATH' => GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), 23 | 'TYPO3_SITE_SCRIPT' => GeneralUtility::getIndpEnv('TYPO3_SITE_SCRIPT'), 24 | 'TYPO3_DOCUMENT_ROOT' => GeneralUtility::getIndpEnv('TYPO3_DOCUMENT_ROOT'), 25 | 'TYPO3_SSL' => GeneralUtility::getIndpEnv('TYPO3_SSL'), 26 | 'TYPO3_PROXY' => GeneralUtility::getIndpEnv('TYPO3_PROXY'), 27 | ]; 28 | 29 | foreach ($attributes as $name => $value) { 30 | $createContextForOperationEvent 31 | ->getContext() 32 | ->setAttribute($name, $value); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Classes/Utility/FileUtility.php: -------------------------------------------------------------------------------- 1 | getPathName(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Classes/Middleware/T3apiRequestLanguageResolver.php: -------------------------------------------------------------------------------- 1 | getAttribute('language'); 20 | $t3apiHeaderLanguageUid = $this->getT3apiLanguageUid($request); 21 | 22 | if ($t3apiHeaderLanguageUid !== null 23 | && RouteService::routeHasT3ApiResourceEnhancerQueryParam($request) 24 | && ($language instanceof SiteLanguage && $language->getLanguageId() !== $t3apiHeaderLanguageUid) 25 | ) { 26 | $request->withAttribute('t3apiHeaderLanguageRequest', true); 27 | $request = $request->withAttribute( 28 | 'language', 29 | $request->getAttribute('site')->getLanguageById($t3apiHeaderLanguageUid) 30 | ); 31 | } 32 | return $handler->handle($request); 33 | } 34 | 35 | protected function getT3apiLanguageUid(ServerRequestInterface $request): ?int 36 | { 37 | $languageHeader = $request->getHeader($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['languageHeader']); 38 | return !empty($languageHeader) ? (int)array_shift($languageHeader) : null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Classes/Serializer/ContextBuilder/DeserializationContextBuilder.php: -------------------------------------------------------------------------------- 1 | enableMaxDepthChecks(); 21 | } 22 | 23 | /** 24 | * @param null $targetObject 25 | * @return DeserializationContext 26 | */ 27 | public function createFromOperation(OperationInterface $operation, Request $request, mixed $targetObject = null): Context 28 | { 29 | $context = $this->create(); 30 | 31 | // There is a fallback to `normalizationContext` because of backward compatibility. Until version 1.2.x 32 | // `denormalizationContext` did not exist and same attributes were used for both contexts 33 | $attributes = $operation->getDenormalizationContext() ?? $operation->getNormalizationContext() ?? []; 34 | 35 | if ($targetObject !== null) { 36 | $attributes['target'] = $targetObject; 37 | } 38 | 39 | foreach ($attributes as $attributeName => $attributeValue) { 40 | $context->setAttribute($attributeName, $attributeValue); 41 | } 42 | 43 | $this->dispatchAfterCreateContextForOperationEvent( 44 | $operation, 45 | $request, 46 | $context 47 | ); 48 | return $context; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Routing/Enhancer/ResourceEnhancer.php: -------------------------------------------------------------------------------- 1 | configuration = $configuration; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function enhanceForMatching(RouteCollection $collection): void 33 | { 34 | /** @var Route $variant */ 35 | $variant = clone $collection->get('default'); 36 | $variant->setPath($this->getBasePath() . sprintf('/{%s?}', self::PARAMETER_NAME)); 37 | $variant->setRequirement(self::PARAMETER_NAME, '.*'); 38 | $collection->add('enhancer_' . $this->getBasePath() . spl_object_hash($variant), $variant); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | * // @todo Think if it ever could be needed 44 | */ 45 | public function enhanceForGeneration(RouteCollection $collection, array $parameters): void {} 46 | 47 | protected function getBasePath(): string 48 | { 49 | static $basePath; 50 | 51 | return $basePath ?? $basePath = RouteService::getApiBasePath(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Classes/OperationHandler/CollectionPostOperationHandler.php: -------------------------------------------------------------------------------- 1 | isMethodPost(); 22 | } 23 | 24 | /** 25 | * @return mixed|AbstractDomainObject|void 26 | * @throws ValidationException 27 | * @throws OperationNotAllowedException 28 | */ 29 | public function handle(OperationInterface $operation, Request $request, array $route, ?ResponseInterface &$response) 30 | { 31 | /** @var CollectionOperation $operation */ 32 | parent::handle($operation, $request, $route, $response); 33 | $repository = $this->getRepositoryForOperation($operation); 34 | 35 | $object = $this->deserializeOperation($operation, $request); 36 | $this->validationService->validateObject($object); 37 | $repository->add($object); 38 | GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); 39 | 40 | $response = $response ? $response->withStatus(201) : $response; 41 | 42 | return $object; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/RecordUriHandler.php: -------------------------------------------------------------------------------- 1 | getObject(); 40 | 41 | if (!$entity instanceof AbstractDomainObject) { 42 | throw new \InvalidArgumentException( 43 | sprintf('Object has to extend %s to build URI', AbstractDomainObject::class), 44 | 1562229270419 45 | ); 46 | } 47 | 48 | $url = $this->contentObjectRenderer->typoLink_URL([ 49 | 'parameter' => sprintf('t3://record?identifier=%s&uid=%s', $type['params'][0], $entity->getUid()), 50 | ]); 51 | return UrlService::forceAbsoluteUrl( 52 | $url, 53 | $context->getAttribute('TYPO3_SITE_URL') 54 | ); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Classes/OperationHandler/AbstractItemOperationHandler.php: -------------------------------------------------------------------------------- 1 | getRepositoryForOperation($operation); 31 | 32 | /** @var AbstractDomainObject|null $object */ 33 | $object = $repository->findByUid((int)$route['id']); 34 | 35 | if (!$object instanceof AbstractDomainObject) { 36 | throw new ResourceNotFoundException( 37 | $operation->getApiResource()->getEntity(), 38 | (int)$route['id'], 39 | 1581461016515 40 | ); 41 | } 42 | 43 | if (!$this->operationAccessChecker->isGranted($operation, ['object' => $object])) { 44 | throw new OperationNotAllowedException($operation, 1574411504130); 45 | } 46 | 47 | return $object; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Classes/Security/OperationAccessChecker.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch($event); 20 | $expressionLanguageVariables = $event->getExpressionLanguageVariables(); 21 | 22 | if ($operation->getSecurity() === '') { 23 | return true; 24 | } 25 | 26 | $variables = array_merge($expressionLanguageVariables, ['t3apiOperation' => $operation]); 27 | 28 | return $this->getExpressionLanguageResolver($variables)->evaluate($operation->getSecurity()); 29 | } 30 | 31 | public function isGrantedPostDenormalize( 32 | OperationInterface $operation, 33 | array $expressionLanguageVariables = [] 34 | ): bool { 35 | $event = new BeforeOperationAccessGrantedPostDenormalizeEvent( 36 | $operation, 37 | $expressionLanguageVariables 38 | ); 39 | $this->eventDispatcher->dispatch($event); 40 | $expressionLanguageVariables = $event->getExpressionLanguageVariables(); 41 | 42 | if ($operation->getSecurityPostDenormalize() === '') { 43 | return true; 44 | } 45 | 46 | $variables = array_merge($expressionLanguageVariables, ['t3apiOperation' => $operation]); 47 | 48 | return $this->getExpressionLanguageResolver($variables)->evaluate($operation->getSecurityPostDenormalize()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Factory/ApiResourceFactory.php: -------------------------------------------------------------------------------- 1 | annotationReader = new AnnotationReader(); 20 | } 21 | 22 | public function createApiResourceFromFqcn(string $fqcn): ?ApiResource 23 | { 24 | /** @var ApiResourceAnnotation $apiResourceAnnotation */ 25 | $apiResourceAnnotation = $this->annotationReader->getClassAnnotation( 26 | new \ReflectionClass($fqcn), 27 | ApiResourceAnnotation::class 28 | ); 29 | 30 | if (!$apiResourceAnnotation instanceof ApiResourceAnnotation) { 31 | return null; 32 | } 33 | 34 | $apiResource = new ApiResource($fqcn, $apiResourceAnnotation); 35 | 36 | $this->addFiltersToApiResource($apiResource); 37 | 38 | return $apiResource; 39 | } 40 | 41 | protected function addFiltersToApiResource(ApiResource $apiResource): void 42 | { 43 | $filterAnnotations = array_filter( 44 | $this->annotationReader->getClassAnnotations(new \ReflectionClass($apiResource->getEntity())), 45 | static function ($annotation): bool { 46 | return $annotation instanceof ApiFilterAnnotation; 47 | } 48 | ); 49 | 50 | foreach ($filterAnnotations as $filterAnnotation) { 51 | foreach (ApiFilter::createFromAnnotations($filterAnnotation) as $apiFilter) { 52 | $apiResource->addFilter($apiFilter); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Classes/Response/AbstractCollectionResponse.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 29 | $this->request = $request; 30 | $this->query = $query; 31 | } 32 | 33 | public function getMembers(): array 34 | { 35 | if ($this->membersCache === null) { 36 | $this->membersCache = $this->applyPagination()->execute()->toArray(); 37 | } 38 | 39 | return $this->membersCache; 40 | } 41 | 42 | public function getTotalItems(): int 43 | { 44 | if ($this->totalItemsCache === null) { 45 | $this->totalItemsCache = $this->query->execute()->count(); 46 | } 47 | 48 | return $this->totalItemsCache; 49 | } 50 | 51 | protected function applyPagination(): QueryInterface 52 | { 53 | $pagination = $this->operation->getPagination()->setParametersFromRequest($this->request); 54 | 55 | if (!$pagination->isEnabled()) { 56 | return $this->query; 57 | } 58 | 59 | return (clone $this->query) 60 | ->setLimit($pagination->getNumberOfItemsPerPage()) 61 | ->setOffset($pagination->getOffset()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Classes/Processor/CorsProcessor.php: -------------------------------------------------------------------------------- 1 | isCorsRequest($request) 19 | || $this->isPreflightRequest($request) 20 | ) { 21 | return; 22 | } 23 | 24 | $options = $this->corsService->getOptions(); 25 | 26 | $requestOrigin = $request->headers->get('Origin'); 27 | 28 | if (!$this->corsService->isAllowedOrigin($requestOrigin, $options)) { 29 | $response = $response->withoutHeader('Access-Control-Allow-Origin'); 30 | } 31 | 32 | $response = $response->withHeader( 33 | 'Access-Control-Allow-Origin', 34 | $requestOrigin 35 | ); 36 | 37 | if ($options->allowCredentials) { 38 | $response = $response->withHeader('Access-Control-Allow-Credentials', 'true'); 39 | } 40 | 41 | if ($options->exposeHeaders !== []) { 42 | $response = $response->withHeader( 43 | 'Access-Control-Expose-Headers', 44 | strtolower(implode(', ', $options->exposeHeaders)) 45 | ); 46 | } 47 | } 48 | 49 | protected function isCorsRequest(Request $request): bool 50 | { 51 | return $request->headers->has('Origin') 52 | && $request->headers->get('Origin') 53 | !== $request->getSchemeAndHttpHost(); 54 | } 55 | 56 | protected function isPreflightRequest(Request $request): bool 57 | { 58 | return $request->getMethod() === Request::METHOD_OPTIONS; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/OperationHandler/ItemPatchOperationHandler.php: -------------------------------------------------------------------------------- 1 | isMethodPatch(); 24 | } 25 | 26 | /** 27 | * @noinspection ReferencingObjectsInspection 28 | * @throws UnknownObjectException 29 | * @throws OperationNotAllowedException 30 | * @throws ValidationException 31 | * @throws ResourceNotFoundException 32 | */ 33 | public function handle( 34 | OperationInterface $operation, 35 | Request $request, 36 | array $route, 37 | ?ResponseInterface &$response 38 | ): AbstractDomainObject { 39 | /** @var ItemOperation $operation */ 40 | $repository = $this->getRepositoryForOperation($operation); 41 | $object = parent::handle($operation, $request, $route, $response); 42 | $this->deserializeOperation($operation, $request, $object); 43 | $this->validationService->validateObject($object); 44 | $repository->update($object); 45 | GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); 46 | 47 | return $object; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Classes/Controller/OpenApiController.php: -------------------------------------------------------------------------------- 1 | getQueryParams()['site'] ?? null; 38 | $site = $this->siteFinder->getSiteByIdentifier($siteIdentifier); 39 | 40 | $imitateSiteRequest = $request->withAttribute('site', $site); 41 | $GLOBALS['TYPO3_REQUEST'] = $imitateSiteRequest; 42 | $output = OpenApiBuilder::build($this->apiResourceRepository->getAll())->toJson(); 43 | $GLOBALS['TYPO3_REQUEST'] = $request; 44 | 45 | $response = new Response(); 46 | $response = $response->withHeader('Content-Type', 'application/json'); 47 | $response->getBody()->write($output); 48 | 49 | return $response; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Classes/Filter/OrderFilter.php: -------------------------------------------------------------------------------- 1 | 'order', 20 | ]; 21 | 22 | /** 23 | * @return Parameter[] 24 | */ 25 | public static function getOpenApiParameters(ApiFilter $apiFilter): array 26 | { 27 | return [ 28 | Parameter::create() 29 | ->name($apiFilter->getParameterName() . '[' . $apiFilter->getProperty() . ']') 30 | ->in(Parameter::IN_QUERY) 31 | ->schema(Schema::string()->enum('asc', 'desc')), 32 | ]; 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function filterProperty( 39 | string $property, 40 | $values, 41 | QueryInterface $query, 42 | ApiFilter $apiFilter 43 | ): ?ConstraintInterface { 44 | if (!isset($values[$property])) { 45 | return null; 46 | } 47 | 48 | $defaultDirection = $apiFilter->getStrategy()->getName(); 49 | $direction = strtoupper($values[$property] !== '' ? $values[$property] : $defaultDirection); 50 | 51 | if ($direction === '') { 52 | return null; 53 | } 54 | 55 | if (!in_array($direction, [QueryInterface::ORDER_ASCENDING, QueryInterface::ORDER_DESCENDING], true)) { 56 | throw new \InvalidArgumentException(sprintf('Unknown order direction `%s`', $direction), 1560890654236); 57 | } 58 | 59 | $query->setOrderings(array_merge($query->getOrderings(), [$property => $direction])); 60 | 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Classes/OperationHandler/CollectionGetOperationHandler.php: -------------------------------------------------------------------------------- 1 | isMethodGet(); 20 | } 21 | 22 | /** @noinspection ReferencingObjectsInspection */ 23 | public function handle(OperationInterface $operation, Request $request, array $route, ?ResponseInterface &$response) 24 | { 25 | /** @var CollectionOperation $operation */ 26 | parent::handle($operation, $request, $route, $response); 27 | $collectionResponseClass = Configuration::getCollectionResponseClass(); 28 | $repository = $this->getRepositoryForOperation($operation); 29 | 30 | if (!is_subclass_of($collectionResponseClass, AbstractCollectionResponse::class)) { 31 | throw new \InvalidArgumentException( 32 | sprintf( 33 | 'Collection response class (`%s`) has to be an instance of `%s`', 34 | $collectionResponseClass, 35 | AbstractCollectionResponse::class 36 | ) 37 | ); 38 | } 39 | 40 | /** @var AbstractCollectionResponse $responseObject */ 41 | $responseObject = GeneralUtility::makeInstance( 42 | $collectionResponseClass, 43 | $operation, 44 | $request, 45 | $repository->findFiltered($operation->getFilters(), $request) 46 | ); 47 | 48 | return $responseObject; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Serializer/Construction/ObjectConstructorChain.php: -------------------------------------------------------------------------------- 1 | constructors = $constructors; 31 | } 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | public function construct( 37 | DeserializationVisitorInterface $visitor, 38 | ClassMetadata $metadata, 39 | $data, 40 | array $type, 41 | DeserializationContext $context 42 | ): ?object { 43 | foreach ($this->getConstructorsInstances() as $constructor) { 44 | $object = $constructor->construct($visitor, $metadata, $data, $type, $context); 45 | 46 | if ($object !== null) { 47 | return $object; 48 | } 49 | } 50 | 51 | throw new \RuntimeException(sprintf('Could not construct object `%s`', $metadata->name), 1577822761813); 52 | } 53 | 54 | /** 55 | * @return ObjectConstructorInterface[] 56 | */ 57 | protected function getConstructorsInstances(): array 58 | { 59 | if ($this->constructorsInstances === null) { 60 | $this->constructorsInstances = []; 61 | foreach ($this->constructors as $constructor) { 62 | $this->constructorsInstances[] = GeneralUtility::makeInstance($constructor); 63 | } 64 | } 65 | 66 | return $this->constructorsInstances; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Classes/Serializer/Subscriber/CurrentFeUserSubscriber.php: -------------------------------------------------------------------------------- 1 | Events::PRE_SERIALIZE, 23 | 'method' => 'onPreSerialize', 24 | ], 25 | [ 26 | 'event' => Events::PRE_DESERIALIZE, 27 | 'method' => 'onPreDeserialize', 28 | ], 29 | ]; 30 | } 31 | 32 | public function onPreSerialize(PreSerializeEvent $event): void 33 | { 34 | if ($event->getType()['name'] !== CurrentFeUserHandler::TYPE) { 35 | return; 36 | } 37 | 38 | $event->setType($event->getType()['params'][0]); 39 | } 40 | 41 | public function onPreDeserialize(PreDeserializeEvent $event): void 42 | { 43 | $className = $event->getType()['name']; 44 | 45 | if (!class_exists($className)) { 46 | return; 47 | } 48 | 49 | /** @var ClassHierarchyMetadata|MergeableClassMetadata|null $metadata */ 50 | $metadata = $event->getContext()->getMetadataFactory()->getMetadataForClass($event->getType()['name']); 51 | 52 | if (!$metadata instanceof ClassMetadata) { 53 | return; 54 | } 55 | 56 | $data = $event->getData(); 57 | foreach ($metadata->propertyMetadata as $propertyName => $propertyMetadata) { 58 | if ($propertyMetadata->type === null || $propertyMetadata->type['name'] !== CurrentFeUserHandler::TYPE) { 59 | continue; 60 | } 61 | 62 | $data[$propertyName] = $GLOBALS['TSFE']->fe_user->user['uid'] ?? null; 63 | } 64 | 65 | $event->setData($data); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Classes/Annotation/ApiFilter.php: -------------------------------------------------------------------------------- 1 | filterClass = $filterClass; 47 | $this->properties = $options['properties'] ?? $this->properties; 48 | $this->strategy = $options['strategy'] ?? $this->strategy; 49 | $this->arguments = $options['arguments'] ?? $this->arguments; 50 | } 51 | 52 | public function getProperties(): array 53 | { 54 | $properties = []; 55 | 56 | foreach ($this->properties as $propertyName => $strategy) { 57 | if (is_numeric($propertyName)) { 58 | $propertyName = $strategy; 59 | $strategy = $this->strategy; 60 | } 61 | 62 | $properties[$propertyName] = $strategy; 63 | } 64 | 65 | return $properties; 66 | } 67 | 68 | public function getFilterClass(): string 69 | { 70 | return $this->filterClass; 71 | } 72 | 73 | public function getArguments(): array 74 | { 75 | return $this->arguments; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Classes/OperationHandler/ItemPutOperationHandler.php: -------------------------------------------------------------------------------- 1 | isMethodPut(); 23 | } 24 | 25 | /** 26 | * @noinspection ReferencingObjectsInspection 27 | * @throws OperationNotAllowedException 28 | * @throws ResourceNotFoundException 29 | * @throws ValidationException 30 | */ 31 | public function handle( 32 | OperationInterface $operation, 33 | Request $request, 34 | array $route, 35 | ?ResponseInterface &$response 36 | ): AbstractDomainObject { 37 | /** @var ItemOperation $operation */ 38 | $repository = $this->getRepositoryForOperation($operation); 39 | $object = parent::handle($operation, $request, $route, $response); 40 | 41 | $entityClass = $operation->getApiResource()->getEntity(); 42 | /** @var AbstractDomainObject $newObject */ 43 | $newObject = new $entityClass(); 44 | 45 | foreach ($newObject->_getProperties() as $propertyName => $propertyValue) { 46 | if ($propertyName === 'uid') { 47 | continue; 48 | } 49 | $object->_setProperty($propertyName, $propertyValue); 50 | } 51 | 52 | $this->deserializeOperation($operation, $request, $object); 53 | $this->validationService->validateObject($object); 54 | $repository->add($object); 55 | GeneralUtility::makeInstance(PersistenceManager::class)->persistAll(); 56 | 57 | return $object; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Classes/Exception/AbstractException.php: -------------------------------------------------------------------------------- 1 | content( 32 | MediaType::json()->schema( 33 | Schema::object()->properties(...static::getOpenApiResponseSchemaProperties()) 34 | ) 35 | ); 36 | } 37 | 38 | /** 39 | * @return SchemaContract[] 40 | */ 41 | protected static function getOpenApiResponseSchemaProperties(): array 42 | { 43 | return [ 44 | Schema::string('hydra:title'), 45 | Schema::string('hydra:description'), 46 | Schema::integer('hydra:code'), 47 | ]; 48 | } 49 | 50 | public function getStatusCode(): int 51 | { 52 | return Response::HTTP_INTERNAL_SERVER_ERROR; 53 | } 54 | 55 | /** 56 | * @VirtualProperty("hydra:title") 57 | */ 58 | public function getTitle(): string 59 | { 60 | return $this->title ?? Response::$statusTexts[$this->getStatusCode()] ?? ''; 61 | } 62 | 63 | /** 64 | * @VirtualProperty("hydra:code") 65 | */ 66 | public function getExceptionCode(): int 67 | { 68 | return $this->getCode(); 69 | } 70 | 71 | /** 72 | * @VirtualProperty("hydra:description") 73 | */ 74 | public function getDescription(): string 75 | { 76 | return $this->message ?? ''; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/ObjectStorageHandler.php: -------------------------------------------------------------------------------- 1 | stopVisiting($objectStorage); 36 | $result = $visitor->visitArray($objectStorage->toArray(), $type); 37 | $context->startVisiting($objectStorage); 38 | 39 | return $result; 40 | } 41 | 42 | /** 43 | * @param mixed $data 44 | */ 45 | public function deserialize( 46 | DeserializationVisitorInterface $visitor, 47 | $data, 48 | array $type, 49 | DeserializationContext $context 50 | ): ObjectStorage { 51 | $objectStorage = new ObjectStorage(); 52 | 53 | if (empty($data)) { 54 | return $objectStorage; 55 | } 56 | 57 | if (!is_array($data)) { 58 | throw new \InvalidArgumentException( 59 | sprintf( 60 | 'Data of type `%s` can not be converted to %s in path `%s`', 61 | gettype($data), 62 | ObjectStorage::class, 63 | implode('.', $context->getCurrentPath()) 64 | ), 65 | 1570805126535 66 | ); 67 | } 68 | 69 | $type['name'] = 'array'; 70 | 71 | $items = $visitor->visitArray($data, $type); 72 | 73 | foreach ($items as $item) { 74 | $objectStorage->attach($item); 75 | } 76 | 77 | return $objectStorage; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | TYPO3 Extension t3api 2 | ===================== 3 | 4 | .. image:: https://poser.pugx.org/sourcebroker/t3api/v/stable 5 | :target: https://extensions.typo3.org/extension/t3api/ 6 | 7 | .. image:: https://img.shields.io/github/actions/workflow/status/sourcebroker/t3api/TYPO3_12.yml?label=Tests%20TYPO3%2012&logo=github 8 | :target: https://github.com/sourcebroker/t3api/actions/workflows/TYPO3_12.yml 9 | 10 | .. image:: https://img.shields.io/github/actions/workflow/status/sourcebroker/t3api/TYPO3_13.yml?label=Tests%20TYPO3%2013&logo=github 11 | :target: https://github.com/sourcebroker/t3api/actions/workflows/TYPO3_13.yml 12 | 13 | Features 14 | -------- 15 | 16 | - Support for Extbase models with GET, POST, PATCH, PUT, DELETE operations. 17 | - Configuration with classes, properties and methods annotations. 18 | - Build-in filters: boolean, numeric, order, range and text (partial, match against and exact strategies). 19 | - Build-in pagination. 20 | - Support for typolinks. 21 | - Support for image processing. 22 | - Support for file uploads (FAL). 23 | - Configurable routing. 24 | - Responses in `Hydra `_ /`JSON-LD `_ format. 25 | - Serialization contexts - customizable output depending on routing. 26 | - Easy customizable serialization handlers and subscribers. 27 | - Backend module with Swagger for documentation and real testing. 28 | 29 | Documentation 30 | ------------- 31 | 32 | Read the docs at https://docs.typo3.org/p/sourcebroker/t3api/master/en-us/ 33 | 34 | Take a look and test 35 | -------------------- 36 | 37 | After cloning repo you can run ``ddev restart && ddev composer install`` and then ``ddev ci 13`` to install local integration test instance. 38 | Local instance is available at https://13.t3api.ddev.site/ (login to backend with ``admin`` / ``Password1!`` credentials). 39 | 40 | At frontend part you can at once test REST API responses for ext news: 41 | 42 | * https://13.t3api.ddev.site/_api/news/news 43 | * https://13.t3api.ddev.site/_api/news/news/1 44 | * https://13.t3api.ddev.site/_api/news/categories 45 | * etc 46 | 47 | You can also run Postman test with ``ddev composer ci:tests:postman`` command or full test suite with ``ddev composer ci``. 48 | Postman is doing full CRUD test with category and news (with image). 49 | 50 | Development 51 | ----------- 52 | 53 | If you want to help with development take a look at https://docs.typo3.org/p/sourcebroker/t3api/main/en-us/Miscellaneous/Development/Index.html 54 | -------------------------------------------------------------------------------- /Classes/OperationHandler/AbstractOperationHandler.php: -------------------------------------------------------------------------------- 1 | serializerService->deserialize( 43 | $request->getContent(), 44 | $operation->getApiResource()->getEntity(), 45 | $this->deserializationContextBuilder->createFromOperation($operation, $request, $targetObject) 46 | ); 47 | 48 | if (!$this->operationAccessChecker->isGrantedPostDenormalize($operation, ['object' => $object])) { 49 | throw new OperationNotAllowedException($operation, 1574782843388); 50 | } 51 | 52 | $afterDeserializeOperationEvent = new AfterDeserializeOperationEvent( 53 | $operation, 54 | $object 55 | ); 56 | $this->eventDispatcher->dispatch($afterDeserializeOperationEvent); 57 | 58 | return $afterDeserializeOperationEvent->getObject(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/Serializer/Subscriber/ThrowableSubscriber.php: -------------------------------------------------------------------------------- 1 | Events::POST_SERIALIZE, 21 | 'method' => 'onPostSerialize', 22 | ], 23 | ]; 24 | } 25 | 26 | public function onPostSerialize(ObjectEvent $event): void 27 | { 28 | if (!$event->getObject() instanceof \Throwable) { 29 | return; 30 | } 31 | 32 | /** @var \Throwable $object */ 33 | $object = $event->getObject(); 34 | 35 | /** @var JsonSerializationVisitor $visitor */ 36 | $visitor = $event->getVisitor(); 37 | 38 | $this->addDescription($object, $visitor); 39 | $this->addDebug($object, $visitor); 40 | } 41 | 42 | protected function addDescription(\Throwable $throwable, JsonSerializationVisitor $visitor): void 43 | { 44 | $visitor->visitProperty( 45 | new StaticPropertyMetadata(get_class($throwable), 'hydra:description', $throwable->getMessage()), 46 | $throwable->getMessage() 47 | ); 48 | } 49 | 50 | protected function addDebug(\Throwable $throwable, JsonSerializationVisitor $visitor): void 51 | { 52 | if (!SerializerService::isDebugMode()) { 53 | return; 54 | } 55 | 56 | $debug = [ 57 | [ 58 | 'file' => $throwable->getFile(), 59 | 'line' => $throwable->getLine(), 60 | 'function' => null, 61 | 'class' => null, 62 | ], 63 | ]; 64 | 65 | foreach ($throwable->getTrace() as $trace) { 66 | $debug[] = [ 67 | 'file' => $trace['file'] ?? '', 68 | 'line' => $trace['line'] ?? '', 69 | 'function' => $trace['function'] ?? '', 70 | 'class' => $trace['class'] ?? '', 71 | ]; 72 | } 73 | 74 | $visitor->visitProperty( 75 | new StaticPropertyMetadata(get_class($throwable), 'hydra:debug', $debug), 76 | $debug 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Classes/OperationHandler/FileUploadOperationHandler.php: -------------------------------------------------------------------------------- 1 | isMethodPost() 29 | && is_subclass_of($operation->getApiResource()->getEntity(), File::class, true); 30 | } 31 | 32 | public function __construct( 33 | FileUploadService $fileUploadService, 34 | SerializerService $serializerService, 35 | ValidationService $validationService, 36 | OperationAccessChecker $operationAccessChecker, 37 | DeserializationContextBuilder $deserializationContextBuilder, 38 | EventDispatcherInterface $eventDispatcher 39 | ) { 40 | parent::__construct( 41 | $serializerService, 42 | $validationService, 43 | $operationAccessChecker, 44 | $deserializationContextBuilder, 45 | $eventDispatcher 46 | ); 47 | $this->fileUploadService = $fileUploadService; 48 | } 49 | 50 | /** 51 | * @return mixed|\TYPO3\CMS\Core\Resource\File|void 52 | * @throws Exception 53 | * @throws OperationNotAllowedException 54 | */ 55 | public function handle(OperationInterface $operation, Request $request, array $route, ?ResponseInterface &$response) 56 | { 57 | /** @var CollectionOperation $operation */ 58 | parent::handle($operation, $request, $route, $response); 59 | 60 | $object = $this->fileUploadService->process($operation, $request); 61 | 62 | $response = $response ? $response->withStatus(201) : $response; 63 | 64 | return $object; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Classes/Serializer/Subscriber/FileReferenceSubscriber.php: -------------------------------------------------------------------------------- 1 | Events::PRE_SERIALIZE, 27 | 'method' => 'onPreSerialize', 28 | ], 29 | [ 30 | 'event' => Events::PRE_DESERIALIZE, 31 | 'method' => 'onPreDeserialize', 32 | ], 33 | ]; 34 | } 35 | 36 | /** 37 | * Changes type to the custom one to make it possible to handle data with serializer handler 38 | */ 39 | public function onPreSerialize(PreSerializeEvent $event): void 40 | { 41 | $this->changeTypeToHandleAllFileReferenceExtendingClasses($event); 42 | } 43 | 44 | /** 45 | * Changes type to the custom one to make it possible to handle data with serializer handler 46 | */ 47 | public function onPreDeserialize(PreDeserializeEvent $event): void 48 | { 49 | $this->changeTypeToHandleAllFileReferenceExtendingClasses($event); 50 | } 51 | 52 | protected function changeTypeToHandleAllFileReferenceExtendingClasses(Event $event): void 53 | { 54 | if ( 55 | ( 56 | is_subclass_of($event->getType()['name'], Folder::class) 57 | || is_subclass_of($event->getType()['name'], File::class) 58 | || is_subclass_of($event->getType()['name'], FileReference::class) 59 | ) 60 | && $event->getContext()->getDepth() > 1 61 | ) { 62 | /** @var PreDeserializeEvent|PreSerializeEvent $event */ 63 | $event->setType( 64 | FileReferenceHandler::TYPE, 65 | [ 66 | 'targetType' => $event->getType()['name'], 67 | ] 68 | ); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Classes/Domain/Model/UploadSettings.php: -------------------------------------------------------------------------------- 1 | folder = $attributes['folder'] ?? $uploadSettings->folder; 37 | $uploadSettings->allowedFileExtensions = $attributes['allowedFileExtensions'] ?? 38 | GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']); 39 | $uploadSettings->conflictMode = $attributes['conflictMode'] ?? $uploadSettings->conflictMode; 40 | $uploadSettings->filenameHashAlgorithm = $attributes['filenameHashAlgorithm'] ?? $uploadSettings->filenameHashAlgorithm; 41 | $uploadSettings->contentHashAlgorithm = $attributes['contentHashAlgorithm'] ?? $uploadSettings->contentHashAlgorithm; 42 | $uploadSettings->filenameMask = $attributes['filenameMask'] ?? $uploadSettings->filenameMask; 43 | 44 | return $uploadSettings; 45 | } 46 | 47 | public function getFolder(): string 48 | { 49 | return $this->folder; 50 | } 51 | 52 | /** 53 | * @return string[] 54 | */ 55 | public function getAllowedFileExtensions(): array 56 | { 57 | return $this->allowedFileExtensions; 58 | } 59 | 60 | public function getConflictMode(): string 61 | { 62 | return $this->conflictMode; 63 | } 64 | 65 | public function getFilenameHashAlgorithm(): string 66 | { 67 | return $this->filenameHashAlgorithm; 68 | } 69 | 70 | public function getContentHashAlgorithm(): string 71 | { 72 | return $this->contentHashAlgorithm; 73 | } 74 | 75 | public function getFilenameMask(): string 76 | { 77 | return $this->filenameMask; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Classes/ExpressionLanguage/Resolver.php: -------------------------------------------------------------------------------- 1 | getExpressionLanguageProviders()[$context] ?? []; 23 | array_unshift($providers, DefaultProvider::class); 24 | $providers = array_unique($providers); 25 | $functionProviders = []; 26 | $generalVariables = []; 27 | foreach ($providers as $provider) { 28 | /** @var ProviderInterface $providerInstance */ 29 | $providerInstance = GeneralUtility::makeInstance($provider); 30 | $functionProviders[] = $providerInstance->getExpressionLanguageProviders(); 31 | $generalVariables[] = $providerInstance->getExpressionLanguageVariables(); 32 | } 33 | $functionProviders = array_merge(...$functionProviders); 34 | $generalVariables = array_replace_recursive(...$generalVariables); 35 | $this->expressionLanguageVariables = array_replace_recursive($generalVariables, $variables); 36 | foreach ($functionProviders as $functionProvider) { 37 | /** @var ExpressionFunctionProviderInterface[] $functionProviderInstances */ 38 | $functionProviderInstances[] = GeneralUtility::makeInstance($functionProvider); 39 | } 40 | $this->expressionLanguage = new ExpressionLanguage(null, $functionProviderInstances); 41 | } 42 | 43 | /** 44 | * @internal 45 | */ 46 | public function getExpressionLanguage(): ExpressionLanguage 47 | { 48 | return $this->expressionLanguage; 49 | } 50 | 51 | public function evaluate(string $expression, array $contextVariables = []): mixed 52 | { 53 | return $this->expressionLanguage->evaluate( 54 | $expression, 55 | array_replace($this->expressionLanguageVariables, $contextVariables) 56 | ); 57 | } 58 | 59 | public function compile(string $condition): string 60 | { 61 | return $this->expressionLanguage->compile($condition, array_keys($this->expressionLanguageVariables)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Classes/Configuration/Configuration.php: -------------------------------------------------------------------------------- 1 | $class, 39 | 'priority' => is_numeric($priority) ? $priority : 50, 40 | ]; 41 | }, 42 | array_keys($items), 43 | $items 44 | ); 45 | 46 | usort( 47 | $items, 48 | static function (array $itemA, array $itemB): int { 49 | return $itemB['priority'] <=> $itemA['priority']; 50 | } 51 | ); 52 | 53 | return array_column($items, 'className'); 54 | } 55 | 56 | /** 57 | * @return \Generator|ApiResourcePathProvider[] 58 | */ 59 | public static function getApiResourcePathProviders(): \Generator 60 | { 61 | $apiResourcePathProvidersClasses = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['apiResourcePathProviders']; 62 | 63 | foreach ($apiResourcePathProvidersClasses as $apiResourcePathProviderClass) { 64 | $apiResourcePathProvider = GeneralUtility::makeInstance($apiResourcePathProviderClass); 65 | 66 | if (!$apiResourcePathProvider instanceof ApiResourcePathProvider) { 67 | throw new \InvalidArgumentException( 68 | sprintf( 69 | 'API resource path provider `%s` has to be an instance of `%s`', 70 | $apiResourcePathProviderClass, 71 | ApiResourcePathProvider::class 72 | ), 73 | 1609066405400 74 | ); 75 | } 76 | 77 | yield $apiResourcePathProvider; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/InlineViewHelper.php: -------------------------------------------------------------------------------- 1 | headlessDispatcher = GeneralUtility::makeInstance(HeadlessDispatcher::class); 27 | } 28 | 29 | public function initializeArguments(): void 30 | { 31 | $this->registerArgument('route', 'string', 'API endpoint route', true); 32 | $this->registerArgument('params', 'array', 'Request parameters', false, []); 33 | $this->registerArgument('itemsPerPage', 'int', 'Items per page number'); 34 | $this->registerArgument('page', 'int', 'Pagination page number'); 35 | } 36 | 37 | /** 38 | * @throws RouteNotFoundException|\SourceBroker\T3api\Exception\RouteNotFoundException 39 | */ 40 | public function render(): string 41 | { 42 | $request = Request::create($this->getRequestUri(), 'GET', $this->getRequestParameters()); 43 | $requestContext = (new RequestContext())->fromRequest($request); 44 | 45 | return $this->headlessDispatcher->processOperationByRequest($requestContext, $request); 46 | } 47 | 48 | protected function getRequestUri(): string 49 | { 50 | return implode( 51 | '/', 52 | [ 53 | GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST'), 54 | RouteService::getFullApiBasePath(), 55 | $this->arguments['route'], 56 | ] 57 | ); 58 | } 59 | 60 | protected function getRequestParameters(): array 61 | { 62 | $params = $this->arguments['params']; 63 | 64 | if ($this->hasArgument('itemsPerPage')) { 65 | // todo add support for `items_per_page_parameter_name` specific for selected route 66 | $params[$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['pagination']['items_per_page_parameter_name']] = 67 | $this->arguments['itemsPerPage']; 68 | } 69 | 70 | if ($this->hasArgument('page')) { 71 | // todo add support for `page_parameter_name` specific for selected route 72 | $params[$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['pagination']['page_parameter_name']] = 73 | $this->arguments['page']; 74 | } 75 | 76 | return $params; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/AbstractHandler.php: -------------------------------------------------------------------------------- 1 | GraphNavigatorInterface::DIRECTION_SERIALIZATION, 33 | 'type' => $supportedType, 34 | 'format' => 'json', 35 | 'method' => 'serialize', 36 | ]; 37 | } 38 | 39 | if (is_subclass_of(static::class, DeserializeHandlerInterface::class)) { 40 | $methods[] = [ 41 | 'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION, 42 | 'type' => $supportedType, 43 | 'format' => 'json', 44 | 'method' => 'deserialize', 45 | ]; 46 | } 47 | 48 | return $methods; 49 | }, 50 | static::$supportedTypes 51 | ) 52 | ); 53 | } 54 | 55 | protected function cloneDeserializationContext( 56 | DeserializationContext $context, 57 | array $attributes = [] 58 | ): DeserializationContext { 59 | try { 60 | $reflection = new \ReflectionClass(Context::class); 61 | $property = $reflection->getProperty('attributes'); 62 | $property->setAccessible(true); 63 | $contextAttributes = $property->getValue($context); 64 | $deserializationContext = DeserializationContext::create(); 65 | foreach (array_merge($contextAttributes, $attributes) as $attributeName => $attributeValue) { 66 | $deserializationContext->setAttribute($attributeName, $attributeValue); 67 | } 68 | 69 | return $deserializationContext; 70 | } catch (\ReflectionException $e) { 71 | throw new \RuntimeException('Could not clone deserialization object', 1589868671607, $e); 72 | } 73 | } 74 | 75 | protected function getDecodedParams(array $params): array 76 | { 77 | return array_map(SerializerMetadataService::class . '::decodeFromSingleHandlerParam', $params); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Classes/Exception/ValidationException.php: -------------------------------------------------------------------------------- 1 | statusCode(SymfonyResponse::HTTP_BAD_REQUEST) 26 | ->description(self::translate('exception.validation.title')); 27 | } 28 | 29 | protected static function getOpenApiResponseSchemaProperties(): array 30 | { 31 | return array_merge( 32 | parent::getOpenApiResponseSchemaProperties(), 33 | [ 34 | Schema::array('violations')->items( 35 | Schema::object()->properties( 36 | Schema::string('propertyPath'), 37 | Schema::string('message'), 38 | Schema::integer('code') 39 | ) 40 | ), 41 | ] 42 | ); 43 | } 44 | 45 | public function __construct(Result $validationResult, int $code) 46 | { 47 | $this->validationResult = $validationResult; 48 | $this->title = self::translate('exception.validation.title'); 49 | parent::__construct(self::translate('exception.validation.description'), $code); 50 | } 51 | 52 | /** 53 | * @VirtualProperty("violations") 54 | */ 55 | public function getViolations(): array 56 | { 57 | return $this->getViolationsRecursive($this->validationResult); 58 | } 59 | 60 | /** 61 | * @see https://stackoverflow.com/a/3290198/1588346 62 | */ 63 | public function getStatusCode(): int 64 | { 65 | return Response::HTTP_BAD_REQUEST; 66 | } 67 | 68 | protected function getViolationsRecursive(Result $result, array $propertyPath = [], array &$violations = []): array 69 | { 70 | foreach ($result->getErrors() as $error) { 71 | $violations[] = [ 72 | 'propertyPath' => implode('.', $propertyPath), 73 | 'message' => $error->getMessage(), 74 | 'code' => $error->getCode(), 75 | ]; 76 | } 77 | 78 | if (!empty($result->getSubResults())) { 79 | foreach ($result->getSubResults() as $subPropertyName => $subResult) { 80 | $this->getViolationsRecursive( 81 | $subResult, 82 | array_merge($propertyPath, [$subPropertyName]), 83 | $violations 84 | ); 85 | } 86 | } 87 | 88 | return $violations; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Classes/Security/AbstractAccessChecker.php: -------------------------------------------------------------------------------- 1 | hasAspect('backend.user')) { 29 | /** @var UserAspect $backendUserAspect */ 30 | $backendUserAspect = $context->getAspect('backend.user'); 31 | $backend = new \stdClass(); 32 | $backend->user = new \stdClass(); 33 | $backend->user->isAdmin = $backendUserAspect->get('isAdmin'); 34 | $backend->user->isLoggedIn = $backendUserAspect->get('isLoggedIn'); 35 | $backend->user->userId = $backendUserAspect->get('id'); 36 | $backend->user->userGroupList = implode(',', $backendUserAspect->get('groupIds')); 37 | } 38 | 39 | if ($context->hasAspect('frontend.user')) { 40 | /** @var UserAspect $frontendUserAspect */ 41 | $frontendUserAspect = $context->getAspect('frontend.user'); 42 | $frontend = new \stdClass(); 43 | $frontend->user = new \stdClass(); 44 | $frontend->user->isLoggedIn = $frontendUserAspect->get('isLoggedIn'); 45 | $frontend->user->userId = $frontendUserAspect->get('id'); 46 | $frontend->user->userGroupList = implode(',', $frontendUserAspect->get('groupIds')); 47 | } 48 | } catch (AspectNotFoundException $e) { 49 | // this catch exists only to avoid IDE complaints - such error can not be thrown since `getAspect` 50 | // usages are wrapped with `hasAspect` conditions 51 | } catch (AspectPropertyNotFoundException $e) { 52 | } 53 | 54 | $variables = array_merge( 55 | [ 56 | 'backend' => $backend ?? null, 57 | 'frontend' => $frontend ?? null, 58 | ], 59 | $additionalExpressionLanguageVariables 60 | ); 61 | 62 | $expressionLanguageResolver = GeneralUtility::makeInstance( 63 | Resolver::class, 64 | 't3api', 65 | $variables 66 | ); 67 | } 68 | 69 | return $expressionLanguageResolver; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Classes/Filter/ContainFilter.php: -------------------------------------------------------------------------------- 1 | name($apiFilter->getParameterName()) 27 | ->schema(Schema::string()), 28 | ]; 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | public function filterProperty( 35 | string $property, 36 | $values, 37 | QueryInterface $query, 38 | ApiFilter $apiFilter 39 | ): ?ConstraintInterface { 40 | $ids = $this->findContainingIds( 41 | $property, 42 | $values, 43 | $query, 44 | $apiFilter 45 | ); 46 | 47 | return $query->in('uid', $ids + [0]); 48 | } 49 | 50 | /** 51 | * @return int[] 52 | * @throws UnexpectedTypeException 53 | */ 54 | protected function findContainingIds( 55 | string $property, 56 | array $values, 57 | QueryInterface $query, 58 | ApiFilter $apiFilter 59 | ): array { 60 | $tableName = $this->getTableName($query); 61 | $conditions = []; 62 | $rootAlias = 'o'; 63 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName); 64 | 65 | if ($this->isPropertyNested($property)) { 66 | [$tableAlias, $propertyName] = $this->addJoinsForNestedProperty( 67 | $property, 68 | $rootAlias, 69 | $query, 70 | $queryBuilder 71 | ); 72 | } else { 73 | $tableAlias = $rootAlias; 74 | $propertyName = $property; 75 | } 76 | 77 | foreach ($values as $value) { 78 | $conditions[] = sprintf( 79 | 'FIND_IN_SET(%s, %s) > 0', 80 | $queryBuilder->createNamedParameter($value), 81 | $queryBuilder->quoteIdentifier( 82 | $tableAlias . '.' . GeneralUtility::makeInstance(DataMapper::class) 83 | ->convertPropertyNameToColumnName($propertyName, $apiFilter->getFilterClass()) 84 | ) 85 | ); 86 | } 87 | 88 | return $queryBuilder 89 | ->select($rootAlias . '.uid') 90 | ->from($tableName, $rootAlias) 91 | ->andWhere($queryBuilder->expr()->or(...$conditions)) 92 | ->executeQuery() 93 | ->fetchFirstColumn(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Classes/Service/SiteService.php: -------------------------------------------------------------------------------- 1 | getAllSites(); 43 | } 44 | 45 | public static function hasT3apiRouteEnhancer(Site $site): bool 46 | { 47 | return !empty(self::getT3apiRouteEnhancer($site)); 48 | } 49 | 50 | public static function getT3apiRouteEnhancer(Site $site): ?array 51 | { 52 | foreach ($site->getConfiguration()['routeEnhancers'] ?? [] as $routeEnhancer) { 53 | if ($routeEnhancer['type'] === ResourceEnhancer::ENHANCER_NAME) { 54 | return $routeEnhancer; 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | 61 | /** 62 | * @throws SiteNotFoundException 63 | */ 64 | public static function getByIdentifier(string $identifier): Site 65 | { 66 | return GeneralUtility::makeInstance(SiteFinder::class) 67 | ->getSiteByIdentifier($identifier); 68 | } 69 | 70 | protected static function getResolvedByTypo3(): ?SiteInterface 71 | { 72 | if (!class_exists(SiteMatcher::class)) { 73 | return null; 74 | } 75 | 76 | $routeResult = GeneralUtility::makeInstance(SiteMatcher::class) 77 | ->matchRequest(ServerRequestFactory::fromGlobals()); 78 | 79 | return $routeResult instanceof SiteRouteResult ? $routeResult->getSite() : null; 80 | } 81 | 82 | protected static function getFirstMatchingCurrentUrl(): ?Site 83 | { 84 | foreach (self::getAll() as $site) { 85 | if (rtrim(trim((string)$site->getBase()), '/') 86 | === GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST')) { 87 | return $site; 88 | } 89 | } 90 | 91 | return null; 92 | } 93 | 94 | protected static function getFirstWithWildcardDomain(): ?Site 95 | { 96 | foreach (self::getAll() as $site) { 97 | if (trim((string)$site->getBase()) === '/') { 98 | return $site; 99 | } 100 | } 101 | 102 | return null; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/ApiResourceRepository.php: -------------------------------------------------------------------------------- 1 | cache = $cacheManager->getCache('t3api'); 26 | } 27 | 28 | /** 29 | * @return ApiResource[] 30 | */ 31 | public function getAll(): array 32 | { 33 | $cacheIdentifier = $this->buildCacheIdentifier(); 34 | 35 | $apiResources = $this->cache->get($cacheIdentifier); 36 | 37 | if ($apiResources !== false) { 38 | return $apiResources; 39 | } 40 | 41 | $apiResources = []; 42 | 43 | foreach ($this->getAllDomainModels() as $fqcn) { 44 | $apiResources[] = $this->apiResourceFactory->createApiResourceFromFqcn($fqcn); 45 | } 46 | 47 | $apiResources = array_filter($apiResources); 48 | 49 | $this->cache->set($cacheIdentifier, $apiResources); 50 | 51 | return $apiResources; 52 | } 53 | 54 | public function getByEntity(string|object $entity): ?ApiResource 55 | { 56 | $className = is_string($entity) ? $entity : get_class($entity); 57 | 58 | foreach ($this->getAll() as $apiResource) { 59 | if ($apiResource->getEntity() === $className) { 60 | return $apiResource; 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | 67 | /** 68 | * @return iterable 69 | */ 70 | protected function getAllDomainModels(): iterable 71 | { 72 | foreach ($this->getAllDomainModelClassNames() as $className) { 73 | if (is_subclass_of($className, AbstractDomainObject::class)) { 74 | yield $className; 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * @return iterable 81 | */ 82 | protected function getAllDomainModelClassNames(): iterable 83 | { 84 | foreach (Configuration::getApiResourcePathProviders() as $apiResourcePathProvider) { 85 | foreach ($apiResourcePathProvider->getAll() as $domainModelClassFile) { 86 | $className = $this->reflectionService->getClassNameFromFile($domainModelClassFile); 87 | if ($className !== null && $className !== '') { 88 | yield $className; 89 | } 90 | } 91 | } 92 | } 93 | 94 | protected function buildCacheIdentifier(): string 95 | { 96 | return 'ApiResourceRepository_getAll' 97 | . preg_replace('~[^\pL\d]+~u', '_', RouteService::getFullApiBasePath()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Classes/Serializer/Accessor/AccessorStrategy.php: -------------------------------------------------------------------------------- 1 | evaluator = SerializerService::getExpressionEvaluator(); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getValue(object $object, PropertyMetadata $metadata, SerializationContext $context) 30 | { 31 | try { 32 | if ($metadata instanceof ExpressionPropertyMetadata) { 33 | $variables = ['object' => $object, 'context' => $context, 'property_metadata' => $metadata]; 34 | 35 | return $this->evaluator->evaluate((string)($metadata->expression), $variables); 36 | } 37 | 38 | if ($metadata->getter === null) { 39 | return ObjectAccess::getProperty($object, $metadata->name); 40 | } 41 | 42 | $callback = [$object, $metadata->getter]; 43 | if (is_callable($callback)) { 44 | return $callback(); 45 | } 46 | } catch (\Exception $exception) { 47 | $exclusionForExceptions = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializer']['exclusionForExceptionsInAccessorStrategyGetValue']; 48 | foreach ($exclusionForExceptions as $objectClass => $exceptionClasses) { 49 | if ($object instanceof $objectClass) { 50 | if (in_array('*', $exceptionClasses, true)) { 51 | trigger_error($exception->getMessage(), E_USER_WARNING); 52 | return null; 53 | } 54 | foreach ($exceptionClasses as $exceptionClass) { 55 | if ($exception instanceof $exceptionClass) { 56 | trigger_error($exception->getMessage(), E_USER_WARNING); 57 | return null; 58 | } 59 | } 60 | } 61 | } 62 | throw $exception; 63 | } 64 | 65 | return null; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function setValue(object $object, $value, PropertyMetadata $metadata, DeserializationContext $context): void 72 | { 73 | if ($metadata->readOnly === true) { 74 | throw new LogicException(sprintf('Property `%s` on `%s` is read only.', $metadata->name, $metadata->class)); 75 | } 76 | 77 | if ($metadata->setter === null) { 78 | ObjectAccess::setProperty($object, $metadata->name, $value); 79 | 80 | return; 81 | } 82 | 83 | $callback = [$object, $metadata->setter]; 84 | if (is_callable($callback)) { 85 | $callback($value); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Classes/Service/RouteService.php: -------------------------------------------------------------------------------- 1 | getBase(), '/') 34 | . '/' . ltrim(self::getFullApiBasePath(), '/'); 35 | } 36 | 37 | public static function routeHasT3ApiResourceEnhancerQueryParam(?ServerRequestInterface $request = null): bool 38 | { 39 | $request = $request ?? self::getRequest(); 40 | return $request instanceof ServerRequest && is_array($request->getQueryParams()) 41 | && array_key_exists(ResourceEnhancer::PARAMETER_NAME, $request->getQueryParams()); 42 | } 43 | 44 | protected static function getApiRouteEnhancer(): array 45 | { 46 | static $apiRouteEnhancer; 47 | 48 | if (!empty($apiRouteEnhancer)) { 49 | return $apiRouteEnhancer; 50 | } 51 | 52 | $routeEnhancer = SiteService::getT3apiRouteEnhancer(SiteService::getCurrent()); 53 | 54 | if ($routeEnhancer !== null) { 55 | return $routeEnhancer; 56 | } 57 | 58 | throw new \RuntimeException( 59 | sprintf( 60 | 'Route enhancer `%s` is not defined. You need to add it to your site configuration first. See example configuration in PHP doc of %s.', 61 | ResourceEnhancer::ENHANCER_NAME, 62 | ResourceEnhancer::class 63 | ), 64 | 1565853631761 65 | ); 66 | } 67 | 68 | /** 69 | * We support for two cases:language set in X-Locale header 70 | * 1) when request has X-Locale header with language (t3apiHeaderLanguageRequest)) 71 | * 2) when request has no X-Locale header and url itself stores language information 72 | */ 73 | protected static function getLanguageBasePath(): string 74 | { 75 | $request = self::getRequest(); 76 | /** @var SiteLanguage $requestLanguage */ 77 | $requestLanguage = $request?->getAttribute('language'); 78 | if ($requestLanguage instanceof SiteLanguage 79 | && $request?->getAttribute('t3apiHeaderLanguageRequest') !== true) { 80 | $languagePrefix = $requestLanguage->getBase()->getPath(); 81 | } else { 82 | $languagePrefix = SiteService::getCurrent()->getDefaultLanguage()->getBase()->getPath(); 83 | } 84 | 85 | if (str_starts_with($request?->getUri()->getPath(), $languagePrefix)) { 86 | return $languagePrefix; 87 | } 88 | 89 | return ''; 90 | } 91 | 92 | protected static function getRequest(): ?ServerRequestInterface 93 | { 94 | return $GLOBALS['TYPO3_REQUEST'] ?? null; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ApiFilter.php: -------------------------------------------------------------------------------- 1 | filterClass = $filterClass; 23 | $this->property = $property; 24 | $this->strategy = new ApiFilterStrategy($strategy); 25 | $this->arguments = $arguments; 26 | } 27 | 28 | /** 29 | * @return self[] 30 | */ 31 | public static function createFromAnnotations(ApiFilterAnnotation $apiFilterAnnotation): array 32 | { 33 | $filterClass = $apiFilterAnnotation->getFilterClass(); 34 | 35 | $arguments = $apiFilterAnnotation->getArguments(); 36 | if (property_exists($filterClass, 'defaultArguments')) { 37 | if (!is_array($filterClass::$defaultArguments)) { 38 | throw new \InvalidArgumentException( 39 | sprintf('%s::$defaultArguments has to be an array', $filterClass), 40 | 1582290496996 41 | ); 42 | } 43 | $arguments = array_merge($filterClass::$defaultArguments, $arguments); 44 | } 45 | 46 | // In case when properties are not determined we still want to register filter. 47 | // Needed e.g. in `\SourceBroker\T3api\Filter\DistanceFilter` which is not based on single property 48 | // and properties are determined inside of arguments. 49 | if ($apiFilterAnnotation->getProperties() === []) { 50 | return [new static($apiFilterAnnotation->getFilterClass(), '', '', $arguments)]; 51 | } 52 | 53 | $instances = []; 54 | foreach ($apiFilterAnnotation->getProperties() as $property => $strategy) { 55 | $instances[] = new static( 56 | $apiFilterAnnotation->getFilterClass(), 57 | $property, 58 | $strategy, 59 | $arguments 60 | ); 61 | } 62 | 63 | return $instances; 64 | } 65 | 66 | public function getFilterClass(): string 67 | { 68 | return $this->filterClass; 69 | } 70 | 71 | public function getStrategy(): ApiFilterStrategy 72 | { 73 | return $this->strategy; 74 | } 75 | 76 | public function getProperty(): string 77 | { 78 | return $this->property; 79 | } 80 | 81 | public function getArguments(): array 82 | { 83 | return $this->arguments; 84 | } 85 | 86 | /** 87 | * @return mixed 88 | */ 89 | public function getArgument(string $argumentName) 90 | { 91 | return $this->getArguments()[$argumentName] ?? null; 92 | } 93 | 94 | public function getParameterName(): string 95 | { 96 | if ($this->isOrderFilter()) { 97 | $plainParameterName = $this->getArgument('orderParameterName'); 98 | } else { 99 | $plainParameterName = $this->getArgument('parameterName') ?? $this->getProperty(); 100 | } 101 | 102 | // PHP automatically replaces some characters in variable names, which also affects GET parameters 103 | // https://www.php.net/variables.external#language.variables.external.dot-in-names 104 | return str_replace('.', '_', $plainParameterName); 105 | } 106 | 107 | public function isOrderFilter(): bool 108 | { 109 | return is_a($this->filterClass, OrderFilter::class, true); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Classes/Serializer/Subscriber/AbstractEntitySubscriber.php: -------------------------------------------------------------------------------- 1 | Events::POST_SERIALIZE, 29 | 'method' => 'onPostSerialize', 30 | ], 31 | [ 32 | 'event' => Events::PRE_DESERIALIZE, 33 | 'method' => 'onPreDeserialize', 34 | ], 35 | ]; 36 | } 37 | 38 | public function onPostSerialize(ObjectEvent $event): void 39 | { 40 | if (!$event->getObject() instanceof AbstractDomainObject) { 41 | return; 42 | } 43 | 44 | /** @var AbstractDomainObject $entity */ 45 | $entity = $event->getObject(); 46 | 47 | /** @var JsonSerializationVisitor $visitor */ 48 | $visitor = $event->getVisitor(); 49 | 50 | $this->addForceEntityProperties($entity, $visitor); 51 | $this->addIri($entity, $visitor); 52 | } 53 | 54 | public function onPreDeserialize(PreDeserializeEvent $event): void 55 | { 56 | // Changes type to the custom one to make it possible to handle data with serializer handler 57 | if ( 58 | !isset($event->getType()['params']['_skipDomainObjectTransport']) 59 | && is_subclass_of($event->getType()['name'], AbstractDomainObject::class) 60 | && $event->getContext()->getDepth() > 1 61 | ) { 62 | $event->setType( 63 | AbstractDomainObjectHandler::TYPE, 64 | [ 65 | 'targetType' => $event->getType()['name'], 66 | ] 67 | ); 68 | } 69 | } 70 | 71 | protected function addForceEntityProperties(AbstractDomainObject $entity, JsonSerializationVisitor $visitor): void 72 | { 73 | foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['forceEntityProperties'] as $property) { 74 | $value = ObjectAccess::getProperty($entity, $property); 75 | $visitor->visitProperty( 76 | new StaticPropertyMetadata(AbstractDomainObject::class, $property, $value), 77 | $value 78 | ); 79 | } 80 | } 81 | 82 | protected function addIri(AbstractDomainObject $entity, JsonSerializationVisitor $visitor): void 83 | { 84 | $apiResource = $this->apiResourceRepository->getByEntity($entity); 85 | if ($apiResource instanceof ApiResource && $apiResource->getMainItemOperation() instanceof ItemOperation) { 86 | // @todo should be generated with symfony router 87 | $iri = str_replace( 88 | '{id}', 89 | (string)$entity->getUid(), 90 | $apiResource->getMainItemOperation()->getRoute()->getPath() 91 | ); 92 | $visitor->visitProperty( 93 | new StaticPropertyMetadata(AbstractDomainObject::class, '@id', $iri), 94 | $iri 95 | ); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/ImageHandler.php: -------------------------------------------------------------------------------- 1 | processSingleImage($fileReference, $type, $context); 49 | }, 50 | $fileReference instanceof \Traversable ? iterator_to_array($fileReference) : $fileReference 51 | ) 52 | ); 53 | } 54 | 55 | return $this->processSingleImage($fileReference, $type, $context); 56 | } 57 | 58 | protected function processSingleImage( 59 | FileReference|int $fileReference, 60 | array $type, 61 | SerializationContext $context 62 | ): ?string { 63 | $processedFileUrl = null; 64 | try { 65 | if (is_int($fileReference)) { 66 | $fileResource = $this->resourceFactory->getFileReferenceObject($fileReference); 67 | } else { 68 | $fileResource = $fileReference->getOriginalResource(); 69 | } 70 | 71 | $file = $fileResource->getOriginalFile(); 72 | $processedFile = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, [ 73 | 'width' => $type['params'][0] ?? '', 74 | 'height' => $type['params'][1] ?? '', 75 | 'maxWidth' => $type['params'][2] ?? '', 76 | 'maxHeight' => $type['params'][3] ?? '', 77 | 'crop' => $this->getCropArea($fileResource, $type['params'][4] ?? 'default'), 78 | ]); 79 | $processedFileUrl = $this->fileReferenceService->getUrlFromResource($processedFile, $context); 80 | } catch (\Exception $e) { 81 | trigger_error( 82 | $e->getMessage(), 83 | E_USER_WARNING 84 | ); 85 | } 86 | return $processedFileUrl; 87 | } 88 | 89 | protected function getCropArea($fileResource, string $cropVariant): ?Area 90 | { 91 | if ($fileResource->hasProperty('crop') && $fileResource->getProperty('crop')) { 92 | $cropString = $fileResource->getProperty('crop'); 93 | $cropVariantCollection = CropVariantCollection::create((string)$cropString); 94 | $cropArea = $cropVariantCollection->getCropArea($cropVariant); 95 | return $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($fileResource); 96 | } 97 | 98 | return null; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sourcebroker/t3api", 3 | "description": "REST API for your TYPO3 project. Config with annotations, build in filtering, pagination, typolinks, image processing, serialization contexts, responses in Hydra/JSON-LD format.", 4 | "license": [ 5 | "GPL-2.0-or-later" 6 | ], 7 | "type": "typo3-cms-extension", 8 | "authors": [ 9 | { 10 | "name": "Inscript Team", 11 | "role": "Developer" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.1.0", 16 | "ext-json": "*", 17 | "ext-pdo": "*", 18 | "ext-tokenizer": "*", 19 | "doctrine/annotations": "^1.13.3 || ^2.0", 20 | "goldspecdigital/oooas": "^2.4", 21 | "jms/serializer": "^3.1", 22 | "phpdocumentor/reflection-docblock": "^5.2", 23 | "symfony/cache": "^6.4 || ^7.0", 24 | "symfony/expression-language": "^6.4 || ^7.0", 25 | "symfony/http-foundation": "^6.4 || ^7.0", 26 | "symfony/mime": "^6.4 || ^7.0", 27 | "symfony/property-info": "^6.4 || ^7.0", 28 | "symfony/psr-http-message-bridge": "^6.4 || ^7.0", 29 | "symfony/routing": "^6.4 || ^7.0", 30 | "typo3/cms-core": "^12.4.16 || ^13.3", 31 | "typo3/cms-extbase": "^12.4.16 || ^13.3", 32 | "typo3/cms-frontend": "^12.4.16 || ^13.3" 33 | }, 34 | "require-dev": { 35 | "ergebnis/composer-normalize": "^2.47.0", 36 | "friendsofphp/php-cs-fixer": "^3.75.0", 37 | "phpstan/extension-installer": "^1.2.0", 38 | "phpstan/phpstan": "^1.12.26", 39 | "saschaegerer/phpstan-typo3": "~1.10.2", 40 | "seld/jsonlint": "^1.11.0", 41 | "symfony/yaml": "^6.0 || ^7.0", 42 | "typo3/cms-install": "^12.4.16 || ^13.3", 43 | "typo3/coding-standards": "^0.8", 44 | "typo3/testing-framework": "^8.2.1" 45 | }, 46 | "replace": { 47 | "typo3-ter/t3api": "self.version" 48 | }, 49 | "autoload": { 50 | "psr-4": { 51 | "SourceBroker\\T3api\\": "Classes" 52 | } 53 | }, 54 | "autoload-dev": { 55 | "psr-4": { 56 | "SourceBroker\\T3api\\Tests\\": "Tests" 57 | } 58 | }, 59 | "config": { 60 | "allow-plugins": { 61 | "ergebnis/composer-normalize": true, 62 | "phpstan/extension-installer": true, 63 | "typo3/class-alias-loader": true, 64 | "typo3/cms-composer-installers": true 65 | }, 66 | "bin-dir": ".Build/bin", 67 | "vendor-dir": ".Build/vendor" 68 | }, 69 | "extra": { 70 | "typo3/cms": { 71 | "extension-key": "t3api", 72 | "web-dir": ".Build/public" 73 | } 74 | }, 75 | "scripts": { 76 | "ci": [ 77 | "@ci:composer:normalize", 78 | "@ci:yaml:lint", 79 | "@ci:json:lint", 80 | "@ci:php:lint", 81 | "@ci:php:cs-fixer", 82 | "@ci:php:stan", 83 | "@ci:tests:unit", 84 | "@ci:tests:functional", 85 | "@ci:tests:postman" 86 | ], 87 | "ci:composer:normalize": "@composer normalize --dry-run", 88 | "ci:json:lint": "find ./composer.json ./Resources ./Configuration -name '*.json' | xargs -r php .Build/bin/jsonlint -q", 89 | "ci:php:cs-fixer": "PHP_CS_FIXER_IGNORE_ENV=1 .Build/bin/php-cs-fixer fix --config .php-cs-fixer.php -v --dry-run --using-cache no --diff", 90 | "ci:php:lint": "find . -name '*.php' -not -path './.Build/*' -not -path './.cache/*' -not -path './.ddev/*' -not -path './.test/*' -not -path './.Documentation/*' -not -path './.Documentation-GENERATED-temp/*' -print0 | xargs -0 -n 1 -P 4 php -l > /dev/null", 91 | "ci:php:stan": ".Build/bin/phpstan --no-progress", 92 | "ci:tests:create-directories": "mkdir -p .Build/public/typo3temp/var/tests", 93 | "ci:tests:functional": [ 94 | "@ci:tests:create-directories", 95 | "typo3DatabaseHost=db typo3DatabaseUsername=root typo3DatabasePassword=root typo3DatabaseName=db .Build/bin/phpunit -c ./Build/phpunit/FunctionalTests.xml" 96 | ], 97 | "ci:tests:postman": "bash ./Build/postman/run.sh", 98 | "ci:tests:unit": ".Build/bin/phpunit -c ./Build/phpunit/UnitTests.xml", 99 | "ci:yaml:lint": "find ./Resources ./Configuration -regextype egrep -regex '.*.ya?ml$' | xargs -r php .Build/bin/yaml-lint", 100 | "fix": [ 101 | "@fix:php:cs-fixer", 102 | "@fix:composer:normalize" 103 | ], 104 | "fix:composer:normalize": "@composer normalize", 105 | "fix:php:cs-fixer": "PHP_CS_FIXER_IGNORE_ENV=1 .Build/bin/php-cs-fixer fix --config .php-cs-fixer.php" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Classes/Domain/Model/AbstractOperation.php: -------------------------------------------------------------------------------- 1 | key = $key; 38 | $this->apiResource = $apiResource; 39 | $this->method = strtoupper($params['method'] ?? $this->method); 40 | $this->path = $params['path'] ?? $this->path; 41 | $this->security = $params['security'] ?? $this->security; 42 | $this->securityPostDenormalize = $params['security_post_denormalize'] ?? $this->securityPostDenormalize; 43 | $this->normalizationContext = isset($params['normalizationContext']) 44 | ? array_replace_recursive([], $params['normalizationContext']) 45 | : $this->normalizationContext; 46 | $this->denormalizationContext = isset($params['denormalizationContext']) 47 | ? array_replace_recursive([], $params['denormalizationContext']) 48 | : $this->denormalizationContext; 49 | $this->route = new Route( 50 | RouteService::getFullApiBasePath() . $this->path, 51 | $params['defaults'] ?? [], 52 | $params['requirements'] ?? [], 53 | [], 54 | null, 55 | [], 56 | [$this->method, Request::METHOD_OPTIONS] 57 | ); 58 | $this->persistenceSettings = PersistenceSettings::create( 59 | $params['attributes']['persistence'] ?? [], 60 | $apiResource->getPersistenceSettings() 61 | ); 62 | $this->uploadSettings = UploadSettings::create( 63 | $params['attributes']['upload'] ?? [], 64 | $apiResource->getUploadSettings() 65 | ); 66 | } 67 | 68 | public function getKey(): string 69 | { 70 | return $this->key; 71 | } 72 | 73 | public function getMethod(): string 74 | { 75 | return $this->method; 76 | } 77 | 78 | public function getPath(): string 79 | { 80 | return $this->path; 81 | } 82 | 83 | public function getSecurity(): string 84 | { 85 | return $this->security; 86 | } 87 | 88 | public function getSecurityPostDenormalize(): string 89 | { 90 | return $this->securityPostDenormalize; 91 | } 92 | 93 | public function getRoute(): Route 94 | { 95 | return $this->route; 96 | } 97 | 98 | public function getApiResource(): ApiResource 99 | { 100 | return $this->apiResource; 101 | } 102 | 103 | public function getNormalizationContext(): ?array 104 | { 105 | return $this->normalizationContext; 106 | } 107 | 108 | public function getDenormalizationContext(): ?array 109 | { 110 | return $this->denormalizationContext; 111 | } 112 | 113 | public function isMethodGet(): bool 114 | { 115 | return $this->method === 'GET'; 116 | } 117 | 118 | public function isMethodPut(): bool 119 | { 120 | return $this->method === 'PUT'; 121 | } 122 | 123 | public function isMethodPatch(): bool 124 | { 125 | return $this->method === 'PATCH'; 126 | } 127 | 128 | public function isMethodPost(): bool 129 | { 130 | return $this->method === 'POST'; 131 | } 132 | 133 | public function isMethodDelete(): bool 134 | { 135 | return $this->method === 'DELETE'; 136 | } 137 | 138 | public function getPersistenceSettings(): PersistenceSettings 139 | { 140 | return $this->persistenceSettings; 141 | } 142 | 143 | public function getUploadSettings(): UploadSettings 144 | { 145 | return $this->uploadSettings; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Classes/Filter/SearchFilter.php: -------------------------------------------------------------------------------- 1 | name($apiFilter->getParameterName()) 28 | ->schema(Schema::string()), 29 | ]; 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | * @throws InvalidQueryException 35 | * @throws UnexpectedTypeException 36 | */ 37 | public function filterProperty( 38 | string $property, 39 | $values, 40 | QueryInterface $query, 41 | ApiFilter $apiFilter 42 | ): ?ConstraintInterface { 43 | $values = (array)$values; 44 | 45 | switch ($apiFilter->getStrategy()->getName()) { 46 | case 'partial': 47 | return $query->logicalOr( 48 | ...array_map( 49 | static function ($value) use ($query, $property) { 50 | return $query->like($property, '%' . $value . '%'); 51 | }, 52 | $values 53 | ) 54 | ); 55 | case 'matchAgainst': 56 | $ids = $this->matchAgainstFindIds( 57 | $property, 58 | $values, 59 | $query, 60 | $apiFilter 61 | ); 62 | 63 | return $query->in('uid', $ids + [0]); 64 | case 'exact': 65 | default: 66 | return $query->in($property, $values); 67 | } 68 | } 69 | 70 | /** 71 | * @return int[] 72 | * @throws UnexpectedTypeException 73 | */ 74 | protected function matchAgainstFindIds( 75 | string $property, 76 | array $values, 77 | QueryInterface $query, 78 | ApiFilter $apiFilter 79 | ): array { 80 | $tableName = $this->getTableName($query); 81 | $conditions = []; 82 | $binds = []; 83 | $rootAlias = 'o'; 84 | $queryExpansion = (bool)$apiFilter->getArgument('withQueryExpansion'); 85 | 86 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 87 | ->getQueryBuilderForTable($tableName); 88 | 89 | if ($this->isPropertyNested($property)) { 90 | [$tableAlias, $propertyName] = $this->addJoinsForNestedProperty( 91 | $property, 92 | $rootAlias, 93 | $query, 94 | $queryBuilder 95 | ); 96 | } else { 97 | $tableAlias = $rootAlias; 98 | $propertyName = $property; 99 | } 100 | 101 | foreach ($values as $i => $value) { 102 | $key = ':text_ma_' . ((int)$i); 103 | $conditions[] = sprintf( 104 | 'MATCH(%s) AGAINST (%s IN NATURAL LANGUAGE MODE %s)', 105 | $queryBuilder->quoteIdentifier( 106 | $tableAlias . '.' . GeneralUtility::makeInstance(DataMapper::class) 107 | ->convertPropertyNameToColumnName($propertyName, $apiFilter->getFilterClass()) 108 | ), 109 | $key, 110 | $queryExpansion ? ' WITH QUERY EXPANSION ' : '' 111 | ); 112 | $binds[ltrim($key, ':')] = $value; 113 | } 114 | 115 | return $queryBuilder 116 | ->select($rootAlias . '.uid') 117 | ->from($tableName, $rootAlias) 118 | ->andWhere($queryBuilder->expr()->or(...$conditions)) 119 | ->setParameters($binds) 120 | ->executeQuery() 121 | ->fetchFirstColumn(); 122 | } 123 | } 124 | --------------------------------------------------------------------------------