├── 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 |
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 |
--------------------------------------------------------------------------------