├── .ddev ├── apache │ ├── 10.conf │ └── 20.conf ├── commands │ ├── host │ │ ├── ci │ │ └── docs │ └── web │ │ ├── .install-12 │ │ ├── .install-13 │ │ ├── cache-flush │ │ ├── data │ │ ├── fix │ │ ├── install │ │ └── next ├── config.yaml ├── docker-compose.web.yaml ├── homeadditions │ └── .config │ │ └── mc │ │ └── ini └── test │ ├── files │ ├── config │ │ └── sites │ │ │ └── main │ │ │ └── config.yaml │ ├── patches │ │ └── typo3-cms-impexp-disable-error-on-sys-file-warning.patch │ └── src │ │ ├── site │ │ ├── Classes │ │ │ ├── EventListener │ │ │ │ ├── AfterCreateContextForOperationEventListener.php │ │ │ │ ├── AfterDeserializeOperationEventListener.php │ │ │ │ ├── AfterProcessOperationEventListener.php │ │ │ │ ├── BeforeFilterAccessGrantedEventListener.php │ │ │ │ ├── BeforeOperationAccessGrantedEventListener.php │ │ │ │ └── BeforeOperationAccessGrantedPostDenormalizeEventListener.php │ │ │ └── Logger │ │ │ │ └── HtmlFileLogger.php │ │ ├── Configuration │ │ │ ├── .htaccess │ │ │ └── Services.yaml │ │ └── composer.json │ │ └── t3apinews │ │ ├── Classes │ │ └── Domain │ │ │ └── Model │ │ │ ├── Category.php │ │ │ ├── File.php │ │ │ ├── FileReference.php │ │ │ ├── News.php │ │ │ └── Tag.php │ │ ├── Configuration │ │ ├── Extbase │ │ │ └── Persistence │ │ │ │ └── Classes.php │ │ ├── TCA │ │ │ └── Overrides │ │ │ │ └── sys_template.php │ │ ├── TsConfig │ │ │ └── Page │ │ │ │ └── mod.tsconfig │ │ └── TypoScript │ │ │ ├── constants.typoscript │ │ │ └── setup.typoscript │ │ ├── Resources │ │ ├── Private │ │ │ ├── .htaccess │ │ │ └── Serializer │ │ │ │ └── GeorgRinger.News │ │ │ │ ├── Domain.Model.FileReference.yml │ │ │ │ ├── TYPO3.CMS.Core.Resource.File.yml_ │ │ │ │ ├── TYPO3.CMS.Core.Resource.FileReference.yml_ │ │ │ │ └── TYPO3.CMS.Extbase.Domain.Model.FileReference.yml_ │ │ └── Public │ │ │ └── Icons │ │ │ └── Extension.svg │ │ ├── composer.json │ │ └── ext_localconf.php │ ├── impexp │ ├── data.xml │ └── data.xml.files │ │ └── c7254f44aa10b6f89e328731672eda5082fd4976 │ ├── index.php │ ├── utils-install.sh │ └── utils.sh ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── release.yml └── workflows │ ├── TYPO3_12.yml │ ├── TYPO3_13.yml │ └── release.yaml ├── .gitignore ├── .php-cs-fixer.php ├── Build ├── phpunit │ ├── FunctionalTests.xml │ ├── FunctionalTestsBootstrap.php │ ├── UnitTests.xml │ └── UnitTestsBootstrap.php └── postman │ ├── .gitignore │ ├── .nvmrc │ ├── package-lock.json │ ├── package.json │ └── run.sh ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Classes ├── Annotation │ ├── ApiFilter.php │ ├── ApiResource.php │ ├── ORM │ │ └── Cascade.php │ └── Serializer │ │ ├── Exclude.php │ │ ├── Groups.php │ │ ├── MaxDepth.php │ │ ├── ReadOnlyProperty.php │ │ ├── SerializedName.php │ │ ├── Type │ │ ├── CurrentFeUser.php │ │ ├── Image.php │ │ ├── PasswordHash.php │ │ ├── RecordUri.php │ │ ├── Rte.php │ │ ├── TypeInterface.php │ │ └── Typolink.php │ │ └── VirtualProperty.php ├── Configuration │ ├── Configuration.php │ └── CorsOptions.php ├── Controller │ ├── AdministrationController.php │ └── OpenApiController.php ├── Dispatcher │ ├── AbstractDispatcher.php │ ├── Bootstrap.php │ └── HeadlessDispatcher.php ├── Domain │ ├── Model │ │ ├── AbstractOperation.php │ │ ├── AbstractOperationResourceSettings.php │ │ ├── ApiFilter.php │ │ ├── ApiFilterStrategy.php │ │ ├── ApiResource.php │ │ ├── CollectionOperation.php │ │ ├── CollectionOperationFactory.php │ │ ├── ItemOperation.php │ │ ├── OperationInterface.php │ │ ├── Pagination.php │ │ ├── PersistenceSettings.php │ │ └── UploadSettings.php │ └── Repository │ │ ├── ApiResourceRepository.php │ │ └── CommonRepository.php ├── Event │ ├── AfterCreateContextForOperationEvent.php │ ├── AfterDeserializeOperationEvent.php │ ├── AfterProcessOperationEvent.php │ ├── BeforeFilterAccessGrantedEvent.php │ ├── BeforeOperationAccessGrantedEvent.php │ └── BeforeOperationAccessGrantedPostDenormalizeEvent.php ├── EventListener │ ├── AddHydraCollectionResponseSerializationGroupEventListener.php │ └── EnrichSerializationContextEventListener.php ├── Exception │ ├── AbstractException.php │ ├── ExceptionInterface.php │ ├── MethodNotAllowedException.php │ ├── OpenApiSupportingExceptionInterface.php │ ├── OperationNotAllowedException.php │ ├── ResourceNotFoundException.php │ ├── RouteNotFoundException.php │ └── ValidationException.php ├── ExpressionLanguage │ ├── ConditionFunctionsProvider.php │ ├── ConditionProvider.php │ ├── Resolver.php │ ├── T3apiCoreFunctionsProvider.php │ └── T3apiCoreProvider.php ├── Factory │ └── ApiResourceFactory.php ├── Filter │ ├── AbstractFilter.php │ ├── BooleanFilter.php │ ├── ContainFilter.php │ ├── DistanceFilter.php │ ├── FilterInterface.php │ ├── NumericFilter.php │ ├── OpenApiSupportingFilterInterface.php │ ├── OrderFilter.php │ ├── RangeFilter.php │ ├── SearchFilter.php │ └── UidFilter.php ├── Hook │ └── EnrichHashBase.php ├── Middleware │ ├── T3apiRequestLanguageResolver.php │ └── T3apiRequestResolver.php ├── OperationHandler │ ├── AbstractCollectionOperationHandler.php │ ├── AbstractItemOperationHandler.php │ ├── AbstractOperationHandler.php │ ├── CollectionGetOperationHandler.php │ ├── CollectionMethodNotAllowedOperationHandler.php │ ├── CollectionPostOperationHandler.php │ ├── FileUploadOperationHandler.php │ ├── ItemDeleteOperationHandler.php │ ├── ItemGetOperationHandler.php │ ├── ItemMethodNotAllowedOperationHandler.php │ ├── ItemPatchOperationHandler.php │ ├── ItemPutOperationHandler.php │ ├── OperationHandlerInterface.php │ └── OptionsOperationHandler.php ├── Processor │ ├── CorsProcessor.php │ └── ProcessorInterface.php ├── Provider │ └── ApiResourcePath │ │ ├── ApiResourcePathProvider.php │ │ └── LoadedExtensionsDomainModelApiResourcePathProvider.php ├── Response │ ├── AbstractCollectionResponse.php │ ├── HydraCollectionResponse.php │ └── MainEndpointResponse.php ├── Routing │ └── Enhancer │ │ └── ResourceEnhancer.php ├── Security │ ├── AbstractAccessChecker.php │ ├── FilterAccessChecker.php │ └── OperationAccessChecker.php ├── Serializer │ ├── Accessor │ │ └── AccessorStrategy.php │ ├── Construction │ │ ├── ExtbaseObjectConstructor.php │ │ ├── InitializedObjectConstructor.php │ │ └── ObjectConstructorChain.php │ ├── ContextBuilder │ │ ├── AbstractContextBuilder.php │ │ ├── ContextBuilderInterface.php │ │ ├── DeserializationContextBuilder.php │ │ └── SerializationContextBuilder.php │ ├── Handler │ │ ├── AbstractDomainObjectHandler.php │ │ ├── AbstractHandler.php │ │ ├── CurrentFeUserHandler.php │ │ ├── DeserializeHandlerInterface.php │ │ ├── FileReferenceHandler.php │ │ ├── ImageHandler.php │ │ ├── ObjectStorageHandler.php │ │ ├── PasswordHashHandler.php │ │ ├── RecordUriHandler.php │ │ ├── RteHandler.php │ │ ├── SerializeHandlerInterface.php │ │ └── TypolinkHandler.php │ └── Subscriber │ │ ├── AbstractEntitySubscriber.php │ │ ├── CurrentFeUserSubscriber.php │ │ ├── FileReferenceSubscriber.php │ │ ├── GenerateMetadataSubscriber.php │ │ ├── ResourceTypeSubscriber.php │ │ └── ThrowableSubscriber.php ├── Service │ ├── CorsService.php │ ├── ExpressionLanguageService.php │ ├── FileReferenceService.php │ ├── FileUploadService.php │ ├── FilesystemService.php │ ├── OpenApiBuilder.php │ ├── PropertyInfoService.php │ ├── ReflectionService.php │ ├── RouteService.php │ ├── SerializerMetadataService.php │ ├── SerializerService.php │ ├── SiteService.php │ ├── StorageService.php │ ├── UrlService.php │ └── ValidationService.php ├── Utility │ ├── FileUtility.php │ └── ParameterUtility.php └── ViewHelpers │ └── InlineViewHelper.php ├── Configuration ├── Backend │ └── Modules.php ├── ExpressionLanguage.php ├── Icons.php ├── JavaScriptModules.php ├── RequestMiddlewares.php ├── Routing │ └── config.yaml └── Services.yaml ├── Documentation ├── Cors │ └── Index.rst ├── Customization │ ├── ApiResourcePath │ │ └── Index.rst │ ├── CollectionResponseSchema │ │ └── Index.rst │ ├── ExpressionLanguage │ │ └── Index.rst │ └── Index.rst ├── Events │ └── Index.rst ├── Filtering │ ├── BuiltinFilters │ │ ├── BooleanFilter │ │ │ └── Index.rst │ │ ├── ContainFilter │ │ │ └── Index.rst │ │ ├── DistanceFilter │ │ │ └── Index.rst │ │ ├── Index.rst │ │ ├── NumericFilter │ │ │ └── Index.rst │ │ ├── OrderFilter │ │ │ └── Index.rst │ │ ├── RangeFilter │ │ │ └── Index.rst │ │ ├── SearchFilter │ │ │ └── Index.rst │ │ └── UidFilter │ │ │ └── Index.rst │ ├── CustomFilters │ │ └── Index.rst │ ├── Index.rst │ ├── SqlInOperator │ │ └── Index.rst │ └── SqlOrOperator │ │ └── Index.rst ├── GettingStarted │ └── Index.rst ├── HandlingCascadePersistence │ └── Index.rst ├── HandlingFileUpload │ └── Index.rst ├── Index.rst ├── Integration │ └── Index.rst ├── Miscellaneous │ ├── Changelog │ │ └── Index.rst │ ├── CommonIssues │ │ └── Index.rst │ ├── Development │ │ ├── CommandsList │ │ │ └── Index.rst │ │ ├── Index.rst │ │ ├── Introduction │ │ │ └── Index.rst │ │ └── TypicalUseCases │ │ │ ├── BugsFixing │ │ │ └── Index.rst │ │ │ └── Index.rst │ └── Index.rst ├── Multilanguage │ └── Index.rst ├── Operations │ ├── CustomizingOperationHandler │ │ └── Index.rst │ └── Index.rst ├── Pagination │ ├── ClientSide │ │ └── Index.rst │ ├── Index.rst │ └── ServerSide │ │ └── Index.rst ├── Security │ └── Index.rst ├── Serialization │ ├── ContextGroups │ │ └── Index.rst │ ├── Customization │ │ └── Index.rst │ ├── Exceptions │ │ └── Index.rst │ ├── Handlers │ │ └── Index.rst │ ├── Index.rst │ ├── Subscribers │ │ └── Index.rst │ └── YamlMetadata │ │ └── Index.rst ├── Sitemap.rst ├── UseCases │ ├── CurrentUserAssignment │ │ └── Index.rst │ ├── CurrentUserEndpoint │ │ └── Index.rst │ └── Index.rst └── guides.xml ├── LICENSE.md ├── README.rst ├── Resources ├── Private │ ├── .htaccess │ ├── Language │ │ ├── locallang.xlf │ │ └── locallang_modadministration.xlf │ ├── Serializer │ │ └── Metadata │ │ │ ├── SourceBroker.T3api.Response.AbstractCollectionResponse.yml │ │ │ ├── SourceBroker.T3api.Response.HydraCollectionResponse.yml │ │ │ ├── SourceBroker.T3api.Response.MainEndpointResponse.yml │ │ │ ├── TYPO3.CMS.Core.Resource.AbstractFile.yml │ │ │ ├── TYPO3.CMS.Core.Resource.File.yml │ │ │ ├── TYPO3.CMS.Core.Resource.FileReference.yml │ │ │ ├── TYPO3.CMS.Core.Resource.Folder.yml │ │ │ ├── TYPO3.CMS.Core.Resource.ResourceStorage.yml │ │ │ ├── TYPO3.CMS.Extbase.Domain.Model.AbstractFileFolder.yml │ │ │ ├── TYPO3.CMS.Extbase.Domain.Model.FileReference.yml │ │ │ ├── TYPO3.CMS.Extbase.DomainObject.AbstractDomainObject.yml │ │ │ ├── TYPO3.CMS.Extbase.Persistence.Generic.LazyObjectStorage.yml │ │ │ ├── TYPO3.CMS.Extbase.Persistence.ObjectStorage.yml │ │ │ └── Throwable.yml │ └── Templates │ │ └── Administration │ │ └── Documentation.html └── Public │ ├── Css │ ├── swagger-custom.css │ └── swagger-ui.css │ ├── ESM │ └── swagger-init.js │ ├── Icons │ └── Extension.svg │ └── JavaScript │ ├── swagger-ui-bundle.js │ └── swagger-ui-standalone-preset.js ├── Tests ├── Functional │ └── Domain │ │ └── Repository │ │ └── ApiResourceRepositoryTest.php ├── Postman │ ├── fixtures │ │ └── test1.jpg │ ├── t3apinews.crud.json │ ├── t3apinews.language.header.json │ └── t3apinews.language.prefix.json └── Unit │ ├── Domain │ └── Model │ │ ├── ApiFilterTest.php │ │ └── PaginationTest.php │ ├── Fixtures │ ├── Annotation │ │ └── Serializer │ │ │ └── Type │ │ │ └── ExampleTypeWithNestedParams.php │ └── Domain │ │ └── Model │ │ ├── AbstractEntry.php │ │ ├── Address.php │ │ ├── Category.php │ │ ├── Company.php │ │ ├── ContactDataTrait.php │ │ ├── Group.php │ │ ├── IdentifiableInterface.php │ │ ├── Person.php │ │ ├── Tag.php │ │ └── TaggableInterface.php │ ├── Service │ └── SerializerMetadataServiceTest.php │ └── Utility │ └── FileUtilityTest.php ├── composer.json ├── ext_emconf.php ├── ext_localconf.php ├── phpstan-baseline.neon └── phpstan.neon /.ddev/apache/10.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName t3api.ddev.site 3 | DocumentRoot /var/www/html/.test 4 | 5 | AllowOverride All 6 | Allow from All 7 | 8 | 9 | RewriteEngine On 10 | RewriteCond %{HTTP:X-Forwarded-Proto} =https 11 | RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d 12 | RewriteRule ^(.+[^/])$ https://%{HTTP_HOST}$1/ [redirect,last] 13 | SetEnvIf X-Forwarded-Proto "https" HTTPS=on 14 | ErrorLog /dev/stdout 15 | CustomLog ${APACHE_LOG_DIR}/access.log combined 16 | Alias "/phpstatus" "/var/www/phpstatus.php" 17 | 18 | 19 | 20 | ServerName t3api.ddev.site 21 | DocumentRoot /var/www/html/.test 22 | 23 | AllowOverride All 24 | Allow from All 25 | 26 | 27 | RewriteEngine On 28 | RewriteCond %{HTTP:X-Forwarded-Proto} =https 29 | RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d 30 | RewriteRule ^(.+[^/])$ https://%{HTTP_HOST}$1/ [redirect,last] 31 | SetEnvIf X-Forwarded-Proto "https" HTTPS=on 32 | ErrorLog /dev/stdout 33 | CustomLog ${APACHE_LOG_DIR}/access.log combined 34 | Alias "/phpstatus" "/var/www/phpstatus.php" 35 | 36 | SSLEngine on 37 | SSLCertificateFile /etc/ssl/certs/master.crt 38 | SSLCertificateKeyFile /etc/ssl/certs/master.key 39 | 40 | -------------------------------------------------------------------------------- /.ddev/apache/20.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName sub.t3api.ddev.site 3 | ServerAlias *.t3api.ddev.site 4 | ServerAlias *.api.t3api.ddev.site 5 | DocumentRoot /var/www/html/.test 6 | 7 | AllowOverride All 8 | Allow from All 9 | 10 | 11 | RewriteEngine On 12 | RewriteCond %{HTTP_HOST} ^([a-z0-9-]+)\.t3api\.ddev\.site$ [OR] 13 | RewriteCond %{HTTP_HOST} ^([a-z0-9-]+)\.api\.t3api\.ddev\.site$ 14 | RewriteRule ^(.*)$ /var/www/html/.test/%1/public/$1 [L] 15 | 16 | RewriteCond %{HTTP:X-Forwarded-Proto} =https 17 | RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d 18 | RewriteRule ^(.+[^/])$ https://%{HTTP_HOST}$1/ [redirect,last] 19 | SetEnvIf X-Forwarded-Proto "https" HTTPS=on 20 | ErrorLog /dev/stdout 21 | CustomLog ${APACHE_LOG_DIR}/access.log combined 22 | Alias "/phpstatus" "/var/www/phpstatus.php" 23 | 24 | 25 | 26 | ServerName sub.t3api.ddev.site 27 | ServerAlias *.t3api.ddev.site 28 | ServerAlias *.api.t3api.ddev.site 29 | DocumentRoot /var/www/html/.test 30 | 31 | AllowOverride All 32 | Allow from All 33 | 34 | 35 | RewriteEngine On 36 | RewriteCond %{HTTP_HOST} ^([a-z0-9-]+)\.t3api\.ddev\.site$ [OR] 37 | RewriteCond %{HTTP_HOST} ^([a-z0-9-]+)\.api\.t3api\.ddev\.site$ 38 | RewriteRule ^(.*)$ /var/www/html/.test/%1/public/$1 [L] 39 | 40 | RewriteCond %{HTTP:X-Forwarded-Proto} =https 41 | RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d 42 | RewriteRule ^(.+[^/])$ https://%{HTTP_HOST}$1/ [redirect,last] 43 | SetEnvIf X-Forwarded-Proto "https" HTTPS=on 44 | ErrorLog /dev/stdout 45 | CustomLog ${APACHE_LOG_DIR}/access.log combined 46 | Alias "/phpstatus" "/var/www/phpstatus.php" 47 | 48 | SSLEngine on 49 | SSLCertificateFile /etc/ssl/certs/master.crt 50 | SSLCertificateKeyFile /etc/ssl/certs/master.key 51 | 52 | -------------------------------------------------------------------------------- /.ddev/commands/host/docs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Description: [ExtDev] Build docs or open docs editing in watch mode. Mode "watch" is default. 4 | ## Usage: "docs [watch|build|test]" 5 | ## Example: "ddev docs" "ddev docs watch" "ddev docs build" "ddev docs test" 6 | 7 | MODE=${1:-watch} 8 | 9 | if [ "$MODE" == "build" ]; then 10 | mkdir -p Documentation-GENERATED-temp 11 | docker run --rm --pull always -v ./:/project/ ghcr.io/typo3-documentation/render-guides:latest --no-progress --config Documentation 12 | elif [ "$MODE" == "watch" ]; then 13 | mkdir -p Documentation-GENERATED-temp 14 | open http://localhost:5173/Documentation-GENERATED-temp/Index.html 15 | docker run --rm -it --pull always \ 16 | -v "./Documentation:/project/Documentation" \ 17 | -v "./Documentation-GENERATED-temp:/project/Documentation-GENERATED-temp" \ 18 | -p 5173:5173 ghcr.io/garvinhicking/typo3-documentation-browsersync:latest 19 | elif [ "$MODE" == "ci" ]; then 20 | mkdir -p Documentation-GENERATED-temp 21 | docker run --rm --pull always -v "$(pwd)":/project -t ghcr.io/typo3-documentation/render-guides:latest --config=Documentation --no-progress --fail-on-log 22 | else 23 | echo "Invalid mode. Please use 'build', 'watch', or 'test'." 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /.ddev/commands/web/.install-12: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Description: [ExtDev] Install TYPO3 12 integration instance. 4 | ## Usage: install 5 | ## Example: "ddev install 12" 6 | 7 | set +x 8 | set -e 9 | 10 | source .ddev/test/utils-install.sh 11 | install_start "12" 12 | 13 | composer req typo3/cms-backend:'^12.4' typo3/cms-core:'^12.4' typo3/cms-extbase:'^12.4' typo3/cms-filelist:'^12.4' \ 14 | typo3/cms-fluid:'^12.4' typo3/cms-frontend:'^12.4' typo3/cms-recycler:'^12.4' typo3/cms-tstemplate:'^12.4' \ 15 | typo3/cms-info:'^12.4' typo3/cms-lowlevel:'^12.4' typo3/cms-rte-ckeditor:'^12.4' typo3/cms-impexp:'^12.4' \ 16 | typo3/cms-install:'^12.4' \ 17 | helhum/typo3-console:'^8.2.1' \ 18 | cweagans/composer-patches:'^1.7.3' georgringer/news:'^12.1' \ 19 | sourcebroker/t3apinews:'^1.0.0' v/site:'^1.0.0' \ 20 | sourcebroker/t3api:'@dev' \ 21 | --no-progress --no-interaction --working-dir "$BASE_PATH" 22 | 23 | install_end 24 | -------------------------------------------------------------------------------- /.ddev/commands/web/.install-13: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Description: [ExtDev] Install TYPO3 13 integration instance. 4 | ## Usage: install 5 | ## Example: "ddev install 13" 6 | 7 | set +x 8 | set -e 9 | 10 | source .ddev/test/utils-install.sh 11 | install_start "13" 12 | 13 | composer req typo3/cms-backend:'^13.3' typo3/cms-core:'^13.3' typo3/cms-extbase:'^13.3' typo3/cms-filelist:'^13.3' \ 14 | typo3/cms-fluid:'^13.3' typo3/cms-frontend:'^13.3' typo3/cms-recycler:'^13.3' typo3/cms-tstemplate:'^13.3' \ 15 | typo3/cms-info:'^13.3' typo3/cms-lowlevel:'^13.3' typo3/cms-rte-ckeditor:'^13.3' typo3/cms-impexp:'^13.3' \ 16 | typo3/cms-install:'^13.3' \ 17 | helhum/typo3-console:'^8.2.1' \ 18 | cweagans/composer-patches:'^1.7.3' georgringer/news:'^12.1' \ 19 | sourcebroker/t3apinews:'^1.0.0' v/site:'^1.0.0' \ 20 | sourcebroker/t3api:'@dev' \ 21 | --no-progress --no-interaction --working-dir "$BASE_PATH" 22 | 23 | install_end 24 | -------------------------------------------------------------------------------- /.ddev/commands/web/cache-flush: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Description: [ExtDev] Flush cache for all available TYPO3 integration instances. 4 | ## Usage: cache-flush 5 | ## Example: "ddev cache-flush" 6 | 7 | source .ddev/test/utils.sh 8 | 9 | mapfile -t versions < <(get_supported_typo3_versions) 10 | for version in "${versions[@]}"; do 11 | TYPO3_PATH=".test/${version}/vendor/bin/typo3" 12 | if [ -f "$TYPO3_PATH" ]; then 13 | message green "Cache flush TYPO3 v${version}..." 14 | /usr/bin/php $TYPO3_PATH cache:flush 15 | else 16 | message red "TYPO3 binary not found for version ${version}" 17 | fi 18 | done 19 | -------------------------------------------------------------------------------- /.ddev/commands/web/fix: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Description: [ExtDev] Run all possible automate fixes. 4 | ## Usage: fix 5 | ## Example: "ddev fix" 6 | 7 | composer fix 8 | -------------------------------------------------------------------------------- /.ddev/commands/web/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Description: [ExtDev] Install defined TYPO3 testing instance. If no param it fallback to first supported TYPO3. If "all" it installs all supported TYPO3. 4 | ## Usage: install 5 | ## Example: "ddev install, ddev install 12, ddev install all" 6 | 7 | source .ddev/test/utils.sh 8 | 9 | TYPO3=${1} 10 | 11 | if [ "$TYPO3" == "all" ]; then 12 | mapfile -t versions < <(get_supported_typo3_versions) 13 | for version in "${versions[@]}"; do 14 | message green "Installing TYPO3 v${version}..." 15 | .ddev/commands/web/.install-$version 16 | done 17 | else 18 | if [ -z "$TYPO3" ]; then 19 | TYPO3=$(get_lowest_supported_typo3_versions) 20 | else 21 | if ! check_typo3_version "$TYPO3"; then 22 | exit 1 23 | fi 24 | fi 25 | ".ddev/commands/web/.install-$TYPO3" 26 | fi 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.ddev/docker-compose.web.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | environment: 4 | - EXTENSION_KEY=t3api 5 | - PACKAGE_NAME=sourcebroker/t3api 6 | 7 | - TYPO3_VERSIONS=12 13 8 | - TYPO3_VERSIONS_12_PHP=8.1 8.2 8.3 8.4 9 | - TYPO3_VERSIONS_13_PHP=8.2 8.3 8.4 10 | 11 | - TYPO3_INSTALL_DB_DRIVER=mysqli 12 | - TYPO3_INSTALL_DB_USER=root 13 | - TYPO3_INSTALL_DB_PASSWORD=root 14 | - TYPO3_INSTALL_DB_HOST=db 15 | - TYPO3_INSTALL_DB_PORT=3306 16 | - TYPO3_INSTALL_DB_UNIX_SOCKET= 17 | - TYPO3_INSTALL_DB_USE_EXISTING=0 18 | - TYPO3_INSTALL_ADMIN_USER=admin 19 | - TYPO3_INSTALL_ADMIN_PASSWORD=Password1! 20 | - TYPO3_INSTALL_SITE_NAME=EXT:t3api 21 | - TYPO3_INSTALL_SITE_SETUP_TYPE=site 22 | - TYPO3_INSTALL_WEB_SERVER_CONFIG=apache 23 | -------------------------------------------------------------------------------- /.ddev/test/files/config/sites/main/config.yaml: -------------------------------------------------------------------------------- 1 | base: / 2 | languages: 3 | - 4 | title: English 5 | enabled: true 6 | languageId: 0 7 | base: / 8 | typo3Language: default 9 | locale: en_US.UTF-8 10 | iso-639-1: en 11 | navigationTitle: English 12 | hreflang: en-us 13 | direction: ltr 14 | flag: us 15 | websiteTitle: '' 16 | - 17 | title: German 18 | enabled: true 19 | base: /de/ 20 | typo3Language: de 21 | locale: de_DE.UTF-8 22 | iso-639-1: de 23 | websiteTitle: '' 24 | navigationTitle: Deutsche 25 | hreflang: de-de 26 | direction: ltr 27 | fallbackType: strict 28 | fallbacks: '' 29 | flag: de 30 | languageId: 1 31 | - 32 | title: Polish 33 | enabled: true 34 | base: /pl/ 35 | typo3Language: pl 36 | locale: pl_PL.UTF-8 37 | iso-639-1: pl 38 | websiteTitle: '' 39 | navigationTitle: Polski 40 | hreflang: pl-pl 41 | direction: ltr 42 | fallbackType: fallback 43 | fallbacks: '0' 44 | flag: pl 45 | languageId: 2 46 | rootPageId: 1 47 | websiteTitle: '' 48 | imports: 49 | - 50 | resource: 'EXT:t3api/Configuration/Routing/config.yaml' 51 | routeEnhancers: 52 | News: 53 | type: Extbase 54 | limitToPages: 55 | - 5 56 | extension: News 57 | plugin: Pi1 58 | routes: 59 | - routePath: '/' 60 | _controller: 'News::list' 61 | - routePath: '/page-{page}' 62 | _controller: 'News::list' 63 | _arguments: 64 | page: 'currentPage' 65 | - routePath: '/{news-title}' 66 | _controller: 'News::detail' 67 | _arguments: 68 | news-title: news 69 | defaultController: 'News::list' 70 | defaults: 71 | page: '0' 72 | aspects: 73 | news-title: 74 | type: PersistedAliasMapper 75 | tableName: tx_news_domain_model_news 76 | routeFieldName: path_segment 77 | page: 78 | type: StaticRangeMapper 79 | start: '1' 80 | end: '100' 81 | -------------------------------------------------------------------------------- /.ddev/test/files/patches/typo3-cms-impexp-disable-error-on-sys-file-warning.patch: -------------------------------------------------------------------------------- 1 | --- Classes/ImportExport.php.orig 2024-06-01 18:06:29.331590962 +0000 2 | +++ Classes/ImportExport.php 2024-06-01 18:07:47.435376487 +0000 3 | @@ -489,7 +489,7 @@ 4 | $this->addError('Updating sys_file records is not supported! They will be imported as new records!'); 5 | } 6 | if ($this->forceAllUids && $table === 'sys_file') { 7 | - $this->addError('Forcing uids of sys_file records is not supported! They will be imported as new records!'); 8 | +# $this->addError('Forcing uids of sys_file records is not supported! They will be imported as new records!'); 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/Classes/EventListener/AfterCreateContextForOperationEventListener.php: -------------------------------------------------------------------------------- 1 | getOperation(); 18 | $logMessage = sprintf( 19 | ' 20 | Operation processed: %s
21 | Method: %s
22 | Path: %s 23 | ', 24 | get_class($operation), 25 | $operation->getMethod(), 26 | $operation->getPath() 27 | ); 28 | 29 | $logContext = [ 30 | 'operation' => $operation, 31 | 'request' => $event->getRequest(), 32 | 'context' => $event->getContext(), 33 | ]; 34 | 35 | $this->logger->warning($logMessage, $logContext); 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/Classes/EventListener/AfterDeserializeOperationEventListener.php: -------------------------------------------------------------------------------- 1 | getOperation(); 17 | $logMessage = sprintf( 18 | ' 19 | Operation processed: %s
20 | Method: %s
21 | Path: %s 22 | ', 23 | get_class($operation), 24 | $operation->getMethod(), 25 | $operation->getPath() 26 | ); 27 | 28 | $logContext = [ 29 | 'operation' => $operation, 30 | 'object' => $event->getObject(), 31 | ]; 32 | 33 | $this->logger->warning($logMessage, $logContext); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/Classes/EventListener/AfterProcessOperationEventListener.php: -------------------------------------------------------------------------------- 1 | getOperation(); 18 | $logMessage = sprintf( 19 | ' 20 | Operation processed: %s
21 | Method: %s
22 | Path: %s 23 | ', 24 | get_class($operation), 25 | $operation->getMethod(), 26 | $operation->getPath() 27 | ); 28 | 29 | $logContext = [ 30 | 'operation' => $operation, 31 | 'result' => $event->getResult(), 32 | ]; 33 | 34 | $this->logger->warning($logMessage, $logContext); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/Classes/EventListener/BeforeFilterAccessGrantedEventListener.php: -------------------------------------------------------------------------------- 1 | getFilter(); 17 | 18 | $logMessage = sprintf( 19 | 'Filter processed: %s
', 20 | $filter->getFilterClass(), 21 | ); 22 | 23 | $logContext = [ 24 | 'filter' => $filter, 25 | 'expressionLanguageVariables' => $event->getExpressionLanguageVariables(), 26 | ]; 27 | 28 | $this->logger->warning($logMessage, $logContext); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/Classes/EventListener/BeforeOperationAccessGrantedEventListener.php: -------------------------------------------------------------------------------- 1 | getOperation(); 17 | $logMessage = sprintf( 18 | ' 19 | Operation processed: %s
20 | Method: %s
21 | Path: %s 22 | ', 23 | get_class($operation), 24 | $operation->getMethod(), 25 | $operation->getPath() 26 | ); 27 | 28 | $logContext = [ 29 | 'operation' => $operation, 30 | 'expressionLanguageVariables' => $event->getExpressionLanguageVariables(), 31 | ]; 32 | 33 | $this->logger->warning($logMessage, $logContext); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/Classes/EventListener/BeforeOperationAccessGrantedPostDenormalizeEventListener.php: -------------------------------------------------------------------------------- 1 | getOperation(); 17 | $logMessage = sprintf( 18 | ' 19 | Operation processed: %s
20 | Method: %s
21 | Path: %s 22 | ', 23 | get_class($operation), 24 | $operation->getMethod(), 25 | $operation->getPath() 26 | ); 27 | 28 | $logContext = [ 29 | 'operation' => $operation, 30 | 'expressionLanguageVariables' => $event->getExpressionLanguageVariables(), 31 | ]; 32 | 33 | $this->logger->warning($logMessage, $logContext); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/Configuration/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all 2 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/Configuration/Services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | V\Site\: 8 | resource: '../Classes/*' 9 | 10 | V\Site\EventListener\AfterProcessOperationEventListener: 11 | tags: 12 | - name: 'event.listener' 13 | 14 | V\Site\EventListener\AfterCreateContextForOperationEventListener: 15 | tags: 16 | - name: 'event.listener' 17 | 18 | V\Site\EventListener\AfterDeserializeOperationEventListener: 19 | tags: 20 | - name: 'event.listener' 21 | 22 | V\Site\EventListener\BeforeFilterAccessGrantedEventListener: 23 | tags: 24 | - name: 'event.listener' 25 | 26 | V\Site\EventListener\BeforeOperationAccessGrantedEventListener: 27 | tags: 28 | - name: 'event.listener' 29 | 30 | V\Site\EventListener\BeforeOperationAccessGrantedPostDenormalizeEventListener: 31 | tags: 32 | - name: 'event.listener' 33 | -------------------------------------------------------------------------------- /.ddev/test/files/src/site/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v/site", 3 | "description": "T3api testing", 4 | "license": "GPL-2.0-or-later", 5 | "type": "typo3-cms-extension", 6 | "authors": [ 7 | { 8 | "name": "", 9 | "email": "no-email@given.com" 10 | } 11 | ], 12 | "suggest": {}, 13 | "conflict": {}, 14 | "extra": { 15 | "typo3/cms": { 16 | "extension-key": "site" 17 | } 18 | }, 19 | "version": "1.0.0", 20 | "autoload": { 21 | "psr-4": { 22 | "V\\Site\\": "Classes/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.ddev/test/files/src/t3apinews/Classes/Domain/Model/File.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'tableName' => 'tx_news_domain_model_news', 13 | ], 14 | Tag::class => [ 15 | 'tableName' => 'tx_news_domain_model_tag', 16 | ], 17 | FileReference::class => [ 18 | 'tableName' => 'sys_file_reference', 19 | ], 20 | Category::class => [ 21 | 'tableName' => 'sys_category', 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /.ddev/test/files/src/t3apinews/Configuration/TCA/Overrides/sys_template.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /.ddev/test/files/src/t3apinews/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sourcebroker/t3apinews", 3 | "license": [ 4 | "GPL-2.0-or-later" 5 | ], 6 | "type": "typo3-cms-extension", 7 | "description": "T3api sample for ext:news", 8 | "authors": [ 9 | { 10 | "name": "SourceBroker Team", 11 | "role": "Developer" 12 | } 13 | ], 14 | "require": { 15 | "typo3/cms-core": "^12.4 || ^13.1", 16 | "sourcebroker/t3api": "@dev", 17 | "georgringer/news": "@dev" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "SourceBroker\\T3apinews\\": "Classes" 22 | } 23 | }, 24 | "replace": { 25 | "typo3-ter/t3apinews": "self.version" 26 | }, 27 | "config": { 28 | "vendor-dir": ".Build/vendor", 29 | "bin-dir": ".Build/bin" 30 | }, 31 | "extra": { 32 | "typo3/cms": { 33 | "extension-key": "t3apinews", 34 | "app-dir": ".Build", 35 | "web-dir": ".Build/public" 36 | } 37 | }, 38 | "version": "1.0.0" 39 | } 40 | -------------------------------------------------------------------------------- /.ddev/test/files/src/t3apinews/ext_localconf.php: -------------------------------------------------------------------------------- 1 | '); 9 | 10 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializerMetadataDirs'] = array_merge( 11 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializerMetadataDirs'] ?? [], 12 | [ 13 | 'GeorgRinger\News' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('t3apinews') . 'Resources/Private/Serializer/GeorgRinger.News', 14 | ] 15 | ); 16 | 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /.ddev/test/impexp/data.xml.files/c7254f44aa10b6f89e328731672eda5082fd4976: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebroker/t3api/fdf013c5687c92c8072c86b25fa8246e14b10f76/.ddev/test/impexp/data.xml.files/c7254f44aa10b6f89e328731672eda5082fd4976 -------------------------------------------------------------------------------- /.ddev/test/index.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | <?php echo($extensionKey); ?> 12 | 13 | 14 |

Run 'ddev install all' to install all testing instances below.

15 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 4 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | # TS/JS-Files 16 | [*.{ts,js}] 17 | indent_size = 2 18 | 19 | # JSON-Files 20 | [*.json] 21 | indent_style = tab 22 | 23 | # ReST-Files 24 | [*.{rst,rst.txt}] 25 | indent_size = 4 26 | max_line_length = 80 27 | 28 | # Markdown-Files 29 | [*.md] 30 | max_line_length = 80 31 | 32 | # YAML-Files 33 | [*.{yaml,yml}] 34 | indent_size = 2 35 | 36 | # NEON-Files 37 | [*.neon] 38 | indent_size = 2 39 | indent_style = tab 40 | 41 | # package.json 42 | [package.json] 43 | indent_size = 2 44 | 45 | # TypoScript 46 | [*.{typoscript,tsconfig}] 47 | indent_size = 2 48 | 49 | # XLF-Files 50 | [*.xlf] 51 | indent_style = tab 52 | 53 | # SQL-Files 54 | [*.sql] 55 | indent_style = tab 56 | indent_size = 2 57 | 58 | # .htaccess 59 | [{_.htaccess,.htaccess}] 60 | indent_style = tab 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community support 4 | url: https://github.com/sourcebroker/t3api/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this extension. 3 | title: "[FEATURE]" 4 | labels: 5 | - enhancement 6 | assignees: 7 | - kszymukowicz 8 | body: 9 | - type: textarea 10 | attributes: 11 | label: Is your feature request related to a problem? 12 | description: A clear description of what the problem is. 13 | placeholder: I'm always frustrated when [...] 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Describe the solution you'd like 19 | description: A clear description of what you want to happen. 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: Describe alternatives you've considered 25 | description: A clear description of any alternative solutions or features you've considered. 26 | - type: textarea 27 | attributes: 28 | label: Additional context 29 | description: Add any other context or screenshots about the feature request here. 30 | - type: checkboxes 31 | id: terms 32 | attributes: 33 | label: Code of Conduct 34 | description: > 35 | By submitting this issue, you agree to follow our 36 | [Code of Conduct](https://github.com/sourcebroker/t3api/blob/main/CODE_OF_CONDUCT.md). 37 | options: 38 | - label: I agree to follow this project's Code of Conduct. 39 | required: true 40 | - type: markdown 41 | attributes: 42 | value: > 43 | :bulb: **Tip:** Have you already looked into https://github.com/sourcebroker/t3api/discussions/categories/ideas? 44 | Maybe your idea has already been discussed there. 45 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - duplicate 5 | - 'good first issue' 6 | - 'help wanted' 7 | - invalid 8 | - question 9 | - wontfix 10 | categories: 11 | - title: ⚡ Breaking 12 | labels: 13 | - breaking 14 | - title: 🚀 Improved 15 | labels: 16 | - enhancement 17 | - title: 🚑 Fixed 18 | labels: 19 | - bug 20 | - title: 👷 Changed 21 | labels: 22 | - maintenance 23 | - title: 📖 Documentation 24 | labels: 25 | - documentation 26 | - title: ⚙️ Dependencies 27 | labels: 28 | - dependencies 29 | - title: Other changes 30 | labels: 31 | - "*" 32 | -------------------------------------------------------------------------------- /.github/workflows/TYPO3_12.yml: -------------------------------------------------------------------------------- 1 | name: TYPO3 12 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | typo3: ["12"] 17 | php: ["8.1", "8.2", "8.3", "8.4"] 18 | composer: ["lowest", "highest"] 19 | env: 20 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Run tests 24 | uses: ddev/github-action-setup-ddev@v1 25 | - run: | 26 | if [ -n "$GH_TOKEN" ] && ! ddev composer config --global --list | grep -q "github-oauth.github.com"; then 27 | echo "Add composer github-oauth.github.com to ddev web container." 28 | ddev composer config --global github-oauth.github.com ${{ env.GH_TOKEN }} 29 | fi 30 | - run: ddev ci ${{ matrix.typo3 }} ${{ matrix.php }} ${{ matrix.composer }} 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/TYPO3_13.yml: -------------------------------------------------------------------------------- 1 | name: TYPO3 13 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | typo3: ["13"] 17 | php: ["8.2", "8.3", "8.4"] 18 | composer: ["lowest", "highest"] 19 | env: 20 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Run tests 24 | uses: ddev/github-action-setup-ddev@v1 25 | - run: | 26 | if [ -n "$GH_TOKEN" ] && ! ddev composer config --global --list | grep -q "github-oauth.github.com"; then 27 | echo "Add composer github-oauth.github.com to ddev web container." 28 | ddev composer config --global github-oauth.github.com ${{ env.GH_TOKEN }} 29 | fi 30 | - run: ddev ci ${{ matrix.typo3 }} ${{ matrix.php }} ${{ matrix.composer }} 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Git global ignore file 2 | # for local exclude patterns please edit .git/info/exclude 3 | # Example file see https://github.com/TYPO3-Documentation/T3DocTeam/blob/master/.gitignore 4 | 5 | # ignore generated documentation 6 | *GENERATED* 7 | 8 | # ignore typical clutter of IDEs and editors (this could be added in .git/info/exclude, 9 | # but we add it here for convenience) 10 | *~ 11 | *.bak 12 | *.idea 13 | *.project 14 | *.swp 15 | .cache 16 | 17 | /composer.lock 18 | /.Build 19 | /.test 20 | /var/ 21 | /.php_cs.cache 22 | /.php-cs-fixer.cache 23 | /composer.json.orig 24 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setParallelConfig(ParallelConfigFactory::detect()) 9 | ->setFinder((new PhpCsFixer\Finder()) 10 | ->in(realpath(__DIR__)) 11 | ->ignoreVCSIgnored(true) 12 | ->notPath('/^Build\/phpunit\/(UnitTestsBootstrap|FunctionalTestsBootstrap).php/') 13 | ->notPath('/^Configuration\//') 14 | ->notPath('/^Documentation\//') 15 | ->notPath('/^Documentation-GENERATED-temp\//') 16 | ->notName('/^ext_(emconf|localconf|tables).php/') 17 | ); 18 | 19 | return $config; 20 | -------------------------------------------------------------------------------- /Build/phpunit/FunctionalTests.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 30 | 31 | 32 | ../../Tests/Functional/ 33 | 34 | 35 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Build/phpunit/FunctionalTestsBootstrap.php: -------------------------------------------------------------------------------- 1 | defineOriginalRootPath(); 28 | $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); 29 | $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); 30 | })(); 31 | -------------------------------------------------------------------------------- /Build/phpunit/UnitTests.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 30 | 31 | 32 | ../../Tests/Unit/ 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Build/postman/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /Build/postman/.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /Build/postman/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "newman", 3 | "version": "1.0.0", 4 | "description": "", 5 | "devDependencies": { 6 | "newman": "^6.1.2" 7 | }, 8 | "author": "", 9 | "license": "" 10 | } 11 | -------------------------------------------------------------------------------- /Build/postman/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source .ddev/test/utils.sh 4 | 5 | set +x 6 | set -e 7 | 8 | POSTMAN_BUILD_PATH=/var/www/html/Build/postman/ 9 | cd ${POSTMAN_BUILD_PATH} || exit 10 | 11 | if [[ ! -e ".nvmrc" ]]; then 12 | echo_red "No file .nvmrc with node version in folder: ${POSTMAN_BUILD_PATH}" && exit 1 13 | fi 14 | 15 | if [[ ! -d "node_modules" ]]; then 16 | npm ci 17 | fi 18 | 19 | TYPO3="" 20 | TEST_FILE="" 21 | 22 | while [[ $# -gt 0 ]]; do 23 | case $1 in 24 | --file) 25 | TEST_FILE="$2" 26 | shift 2 27 | ;; 28 | *) 29 | TYPO3="$1" 30 | shift 31 | ;; 32 | esac 33 | done 34 | 35 | if [ -z "$TYPO3" ]; then 36 | TYPO3=$("../../.Build/bin/typo3" | grep -oP 'TYPO3 CMS \K[0-9]+') 37 | fi 38 | 39 | if ! check_typo3_version "$TYPO3"; then 40 | exit 1 41 | fi 42 | 43 | if [[ ! -d "/var/www/html/.test/$TYPO3" ]]; then 44 | echo_red "Can not test. Install first TYPO3 $TYPO3 with command 'ddev install $TYPO3'" 45 | else 46 | DOMAINS=("https://$TYPO3.$EXTENSION_KEY.ddev.site") 47 | for DOMAIN in "${DOMAINS[@]}"; do 48 | if [[ -n "$TEST_FILE" ]]; then 49 | ./node_modules/.bin/newman run "../../Tests/Postman/$TEST_FILE" --verbose --bail --env-var "baseUrl=$DOMAIN" 50 | else 51 | for TEST_FILE in ../../Tests/Postman/*.json; do 52 | ./node_modules/.bin/newman run "$TEST_FILE" --verbose --bail --env-var "baseUrl=$DOMAIN" 53 | done 54 | fi 55 | done 56 | fi 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project uses following code of conduct: https://typo3.org/community/values/code-of-conduct 4 | 5 | By contributing to this project or engaging with community members, you agree to abide by this code of conduct. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please have a look in the [development section][1] in documentation. 4 | 5 | [1]: https://docs.typo3.org/p/sourcebroker/t3api/main/en-us/Miscellaneous/Development/Index.html 6 | -------------------------------------------------------------------------------- /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/ORM/Cascade.php: -------------------------------------------------------------------------------- 1 | values = (array)$values['value']; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/Exclude.php: -------------------------------------------------------------------------------- 1 | 15 | * @Required 16 | */ 17 | public $groups; 18 | } 19 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/MaxDepth.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/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/Annotation/Serializer/Type/PasswordHash.php: -------------------------------------------------------------------------------- 1 | identifier]; 24 | } 25 | 26 | public function getName(): string 27 | { 28 | return RecordUriHandler::TYPE; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Annotation/Serializer/Type/Rte.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/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/Dispatcher/HeadlessDispatcher.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/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 | -------------------------------------------------------------------------------- /Classes/Domain/Model/CollectionOperationFactory.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/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/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/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/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/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/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/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/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/Exception/ExceptionInterface.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/Exception/OpenApiSupportingExceptionInterface.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/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/Exception/RouteNotFoundException.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/ExpressionLanguage/ConditionFunctionsProvider.php: -------------------------------------------------------------------------------- 1 | expressionLanguageProviders = [ 16 | ConditionFunctionsProvider::class, 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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/ExpressionLanguage/T3apiCoreProvider.php: -------------------------------------------------------------------------------- 1 | expressionLanguageProviders = [ 14 | T3apiCoreFunctionsProvider::class, 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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/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/FilterInterface.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/Filter/OpenApiSupportingFilterInterface.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/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/Hook/EnrichHashBase.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/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/OperationHandler/AbstractCollectionOperationHandler.php: -------------------------------------------------------------------------------- 1 | operationAccessChecker->isGranted($operation)) { 28 | throw new OperationNotAllowedException($operation, 1574416639472); 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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/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/OperationHandler/CollectionMethodNotAllowedOperationHandler.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/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/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/OperationHandler/ItemMethodNotAllowedOperationHandler.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/OperationHandler/OperationHandlerInterface.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/Processor/ProcessorInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function getAll(): iterable; 15 | } 16 | -------------------------------------------------------------------------------- /Classes/Provider/ApiResourcePath/LoadedExtensionsDomainModelApiResourcePathProvider.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/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/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/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/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/Serializer/Construction/ExtbaseObjectConstructor.php: -------------------------------------------------------------------------------- 1 | name); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Classes/Serializer/Construction/InitializedObjectConstructor.php: -------------------------------------------------------------------------------- 1 | hasAttribute('target') && $context->getDepth() === 1) { 29 | return $context->getAttribute('target'); 30 | } 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Classes/Serializer/ContextBuilder/AbstractContextBuilder.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch( 23 | new AfterCreateContextForOperationEvent( 24 | $operation, 25 | $request, 26 | $context 27 | ) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Serializer/ContextBuilder/ContextBuilderInterface.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/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/Handler/CurrentFeUserHandler.php: -------------------------------------------------------------------------------- 1 | persistenceManager->getObjectByIdentifier($data, $type['params'][0]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Classes/Serializer/Handler/DeserializeHandlerInterface.php: -------------------------------------------------------------------------------- 1 | passwordHashFactory->getDefaultHashInstance('FE')->getHashedPassword($data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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/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/Serializer/Handler/SerializeHandlerInterface.php: -------------------------------------------------------------------------------- 1 | contentObjectRenderer->typoLink_URL([ 35 | 'parameter' => $typolinkParameter, 36 | ]); 37 | 38 | return UrlService::forceAbsoluteUrl($url, $context->getAttribute('TYPO3_SITE_URL')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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/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/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/Service/ExpressionLanguageService.php: -------------------------------------------------------------------------------- 1 | getExpressionLanguage(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /Classes/Service/ReflectionService.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/UrlService.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/Utility/FileUtility.php: -------------------------------------------------------------------------------- 1 | getPathName(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Classes/Utility/ParameterUtility.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 | -------------------------------------------------------------------------------- /Configuration/ExpressionLanguage.php: -------------------------------------------------------------------------------- 1 | [ 7 | \SourceBroker\T3api\ExpressionLanguage\ConditionProvider::class, 8 | \SourceBroker\T3api\ExpressionLanguage\T3apiCoreProvider::class, 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /Configuration/Icons.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, 6 | 'source' => 'EXT:t3api/Resources/Public/Icons/Extension.svg', 7 | ], 8 | ]; 9 | -------------------------------------------------------------------------------- /Configuration/JavaScriptModules.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'core', 6 | 'backend', 7 | ], 8 | 'imports' => [ 9 | '@sourcebroker/t3api/' => 'EXT:t3api/Resources/Public/ESM/', 10 | ], 11 | ]; 12 | -------------------------------------------------------------------------------- /Configuration/RequestMiddlewares.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 | -------------------------------------------------------------------------------- /Configuration/Routing/config.yaml: -------------------------------------------------------------------------------- 1 | routeEnhancers: 2 | T3api: 3 | type: T3apiResourceEnhancer 4 | -------------------------------------------------------------------------------- /Documentation/Customization/ApiResourcePath/Index.rst: -------------------------------------------------------------------------------- 1 | .. _customization_api-resource-path: 2 | 3 | Api Resource Path 4 | ================= 5 | 6 | By default t3api will search for API Resource classes in 7 | :folder:`Classes/Domain/Model/*.php` of currently loaded extensions. This behaviour 8 | is defined in :class:`LoadedExtensionsDomainModelApiResourcePathProvider` 9 | and registered in :file:`ext_localconf.php` like this: 10 | 11 | .. code-block:: php 12 | 13 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['apiResourcePathProviders'] = [ 14 | \SourceBroker\T3api\Provider\ApiResourcePath\LoadedExtensionsDomainModelApiResourcePathProvider::class, 15 | ]; 16 | 17 | The same way you can add your own providers for additional patches. 18 | -------------------------------------------------------------------------------- /Documentation/Customization/CollectionResponseSchema/Index.rst: -------------------------------------------------------------------------------- 1 | .. _customization_collection-response-schema: 2 | 3 | Collection response schema 4 | =========================== 5 | 6 | @todo - write docs 7 | -------------------------------------------------------------------------------- /Documentation/Customization/ExpressionLanguage/Index.rst: -------------------------------------------------------------------------------- 1 | .. _customization_expression-language: 2 | 3 | Expression Language 4 | ===================== 5 | 6 | `Symfony expression language `__ is `widely used in TYPO3 core `__. T3api also uses it in two places: 7 | 8 | - :ref:`Serialization (and deserialization) ` 9 | - :ref:`Security (checking access to operations and filters) ` 10 | 11 | T3api utilizes TYPO3 core expression language feature. So, if you would like to extend expression language used in t3api core, you need to register new provider in `the same way as you do it e.g. for TS `__. Just notice that appropriate context has to be specified as array key (in this case it's ``t3api``). Code below shows how to register ``MyCustomProvider`` in ``t3api`` context. 12 | 13 | .. code-block:: php 14 | :caption: typo3conf/ext/my_ext/Configuration/ExpressionLanguage.php 15 | 16 | return [ 17 | 't3api' => [ 18 | \Vendor\MyExt\ExpressionLanguage\MyCustomProvider::class, 19 | ], 20 | ]; 21 | 22 | After registration of custom provider follow TYPO3 documentation how to `add additional variables `__ and `additional functions `__ into expression language. Now you can use your custom variables and functions inside t3api security check or serialization and deserialization expressions. 23 | -------------------------------------------------------------------------------- /Documentation/Customization/Index.rst: -------------------------------------------------------------------------------- 1 | .. _customization: 2 | 3 | ================ 4 | Customization 5 | ================ 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | :hidden: 10 | 11 | ApiResourcePath/Index 12 | ExpressionLanguage/Index 13 | CollectionResponseSchema/Index 14 | -------------------------------------------------------------------------------- /Documentation/Filtering/BuiltinFilters/BooleanFilter/Index.rst: -------------------------------------------------------------------------------- 1 | .. _filtering_filters_boolean-filter: 2 | 3 | BooleanFilter 4 | ============== 5 | 6 | Should be used to filter items by boolean fields. 7 | 8 | Syntax: ``?property=`` 9 | 10 | .. code-block:: php 11 | 12 | use SourceBroker\T3api\Annotation as T3api; 13 | use SourceBroker\T3api\Filter\SearchFilter; 14 | 15 | /** 16 | * @T3api\ApiResource ( 17 | * collectionOperations={ 18 | * "get"={ 19 | * "method"="GET", 20 | * "path"="/news/news", 21 | * }, 22 | * }, 23 | * ) 24 | * 25 | * @T3api\ApiFilter( 26 | * BooleanFilter::class, 27 | * properties={"istopnews"} 28 | * ) 29 | */ 30 | class News extends \GeorgRinger\News\Domain\Model\News 31 | { 32 | } 33 | 34 | .. admonition:: Real examples. Run "ddev restart && ddev ci 13" and try those links below. 35 | 36 | * | Get list of news which are "Top News": 37 | | https://13.t3api.ddev.site/_api/news/news?istopnews=true 38 | | 39 | * | Get list of news which are "Top News" and sort by title 40 | | `https://13.t3api.ddev.site/_api/news/news?istopnews=true&order[title]=asc `__ 41 | -------------------------------------------------------------------------------- /Documentation/Filtering/BuiltinFilters/ContainFilter/Index.rst: -------------------------------------------------------------------------------- 1 | .. _filtering_filters_contain-filter: 2 | 3 | ContainFilter 4 | ============== 5 | 6 | @todo - write docs 7 | -------------------------------------------------------------------------------- /Documentation/Filtering/BuiltinFilters/DistanceFilter/Index.rst: -------------------------------------------------------------------------------- 1 | .. _filtering_filters_distance-filter: 2 | 3 | DistanceFilter 4 | =============== 5 | 6 | Distance filter allows to filter map points points by radius. Map points kept in the database needs to contain latitude and longitude to use this filter. 7 | 8 | Configuration for distance filter looks a little bit different than for other build-in filter. Because distance filter is not based on single field it should not contain ``properties`` definition. Instead of that it is needed to specify which model properties contain latitude and longitude in ``arguments``. Moreover, as ``properties`` is not defined, ``parameterName`` is required. Beside default values in ``arguments``, distance filter accepts also: 9 | 10 | - ``latProperty`` (``string``) - Name of the property which holds latitude 11 | - ``lngProperty`` (``string``) - name of the property which holds longitude 12 | - ``unit`` (ENUM: "mi", "km"; default "km") - Unit of the radius 13 | - ``radius`` (``float/int``; default ``100``) - Radius to filter in; if ``allowClientRadius`` is set to ``true``, then used as default value. 14 | - ``allowClientRadius`` (``bool``; default ``false``) - Set to ``true`` allow to change the radius from GET parameter. 15 | 16 | .. code-block:: php 17 | 18 | use SourceBroker\T3api\Filter\DistanceFilter; 19 | 20 | /** 21 | * @T3api\ApiFilter( 22 | * DistanceFilter::class, 23 | * arguments={ 24 | * "parameterName"="position", 25 | * "latProperty"="gpsLatitude", 26 | * "lngProperty"="gpsLongitude", 27 | * "radius"="100", 28 | * "unit"="km", 29 | * } 30 | * ) 31 | */ 32 | class Item extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity 33 | { 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Documentation/Filtering/BuiltinFilters/Index.rst: -------------------------------------------------------------------------------- 1 | .. _filtering_builtin-filters: 2 | 3 | ================ 4 | Built-in filters 5 | ================ 6 | 7 | There are several build in filters: 8 | 9 | .. toctree:: 10 | :maxdepth: 3 11 | 12 | BooleanFilter/Index 13 | ContainFilter/Index 14 | DistanceFilter/Index 15 | NumericFilter/Index 16 | OrderFilter/Index 17 | RangeFilter/Index 18 | SearchFilter/Index 19 | UidFilter/Index 20 | 21 | -------------------------------------------------------------------------------- /Documentation/Filtering/BuiltinFilters/NumericFilter/Index.rst: -------------------------------------------------------------------------------- 1 | .. _filtering_filters_numeric-filter: 2 | 3 | NumericFilter 4 | ============== 5 | 6 | Should be used to filter items by numeric fields. 7 | 8 | Syntax: ``?property=`` or ``?property[]=&property[]=``. 9 | 10 | .. code-block:: php 11 | 12 | use SourceBroker\T3api\Annotation as T3api; 13 | use SourceBroker\T3api\Filter\NumericFilter; 14 | 15 | /** 16 | * @T3api\ApiResource ( 17 | * collectionOperations={ 18 | * "get"={ 19 | * "path"="/users", 20 | * }, 21 | * }, 22 | * ) 23 | * @T3api\ApiFilter( 24 | * NumericFilter::class, 25 | * properties={"address.number", "height"}, 26 | * ) 27 | */ 28 | class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser 29 | { 30 | } 31 | -------------------------------------------------------------------------------- /Documentation/Filtering/BuiltinFilters/OrderFilter/Index.rst: -------------------------------------------------------------------------------- 1 | .. _filtering_filters_order-filter: 2 | 3 | OrderFilter 4 | ============ 5 | 6 | Allows to change default ordering of collection responses. 7 | 8 | Syntax: ``?order[property]=`` 9 | 10 | It is possible to order by single field (query string :uri:`?order[title]=asc`) or by multiple of them (query string :uri:`?order[title]=asc&order[datetime]=desc`). 11 | 12 | It may happen that conflict of names will occur if ``order`` is also the name of property with enabled another filter. Solution in such cases would be a change of parameter name used by ``OrderFilter``. It can be done using argument ``orderParameterName``, as on example below: 13 | 14 | .. code-block:: php 15 | 16 | use SourceBroker\T3api\Annotation as T3api; 17 | use SourceBroker\T3api\Filter\OrderFilter; 18 | 19 | /** 20 | * @T3api\ApiResource ( 21 | * collectionOperations={ 22 | * "get"={ 23 | * "path"="/news/news", 24 | * }, 25 | * }, 26 | * ) 27 | * 28 | * @T3api\ApiFilter( 29 | * OrderFilter::class, 30 | * properties={"title","datetime"} 31 | * arguments={"orderParameterName": "myOrderParameterName"}, 32 | * ) 33 | */ 34 | class News extends \GeorgRinger\News\Domain\Model\News 35 | { 36 | } 37 | 38 | 39 | .. admonition:: Real examples. Run "ddev restart && ddev ci 13" and try those links below. 40 | 41 | * | Get list of news sorted by titles ascending: 42 | | `https://13.t3api.ddev.site/_api/news/news?order[title]=asc `__ 43 | | 44 | * | Get list of news sorted by date descending and then by title ascending: 45 | | `https://13.t3api.ddev.site/_api/news/news?order[datetime]=desc&order[title]=asc `__ 46 | -------------------------------------------------------------------------------- /Documentation/Filtering/SqlInOperator/Index.rst: -------------------------------------------------------------------------------- 1 | .. _filtering_sql-in-operator: 2 | 3 | SQL "IN" operator 4 | ================== 5 | 6 | When using query params ``?property=`` only items which match exactly such condition are returned. But it is possible to pass multiple values. If you would like to receive all items which ``property`` matches ``value1`` **or** ``value2`` then you can send ``property`` as an array in query string: ``?property[]=&property[]=``. From build-in filters ``NumericFilter`` and ``SearchFilter`` are the filters which support ``IN`` operator. 7 | -------------------------------------------------------------------------------- /Documentation/Filtering/SqlOrOperator/Index.rst: -------------------------------------------------------------------------------- 1 | .. _filtering_sql-or-operator: 2 | 3 | SQL "OR" conjunction 4 | ===================== 5 | 6 | By default ``AND`` conjunction is used between all applied filters but there is a ways to change conjunction to ``OR``. Frontend applications often needs single input field which searches multiple fields. To create such filter it is needed to set same ``parameterName`` for multiple fields. Example code below means that request to URL ``/users?search=john`` will return records where any of the fields (``firstName``, ``middleName``, ``lastName`` or ``address.street``) matches searched text. If we would not determine ``parameterName`` in filter arguments this configuration would work as separate filters for every property. 7 | 8 | .. code-block:: php 9 | 10 | use SourceBroker\T3api\Annotation as T3api; 11 | use SourceBroker\T3api\Filter\SearchFilter; 12 | 13 | /** 14 | * @T3api\ApiResource ( 15 | * collectionOperations={ 16 | * "get"={ 17 | * "path"="/users", 18 | * }, 19 | * }, 20 | * ) 21 | * 22 | * @T3api\ApiFilter( 23 | * SearchFilter::class, 24 | * properties={ 25 | * "firstName": "partial", 26 | * "middleName": "partial", 27 | * "lastName": "partial", 28 | * "address.street": "partial", 29 | * }, 30 | * arguments={ 31 | * "parameterName": "search", 32 | * } 33 | * ) 34 | */ 35 | class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser 36 | { 37 | } 38 | -------------------------------------------------------------------------------- /Documentation/Index.rst: -------------------------------------------------------------------------------- 1 | .. _start: 2 | 3 | ============================================================= 4 | TYPO3 REST API 5 | ============================================================= 6 | 7 | :Version: 8 | |release| 9 | 10 | :Language: 11 | en 12 | 13 | :Authors: 14 | Inscript Team 15 | 16 | :Email: 17 | office@inscript.dev 18 | 19 | :Website: 20 | https://www.inscript.team/ 21 | 22 | :License: 23 | This extension documentation is published under the 24 | `CC BY-NC-SA 4.0 `__ (Creative Commons) 25 | license 26 | 27 | :Rendered: 28 | |today| 29 | 30 | 31 | The content of this document is related to TYPO3 CMS, a GNU/GPL CMS/Framework available from `typo3.org 32 | `_ . 33 | 34 | This documentation is for the TYPO3 extension `t3api `_. 35 | 36 | If you find an error or something is missing, please: `Report a Problem 37 | `__. 38 | 39 | 40 | **Table of Contents** 41 | 42 | .. toctree:: 43 | :maxdepth: 5 44 | :titlesonly: 45 | :glob: 46 | 47 | GettingStarted/Index 48 | Operations/Index 49 | Filtering/Index 50 | Pagination/Index 51 | Security/Index 52 | Serialization/Index 53 | Integration/Index 54 | Customization/Index 55 | Events/Index 56 | Multilanguage/Index 57 | HandlingFileUpload/Index 58 | HandlingCascadePersistence/Index 59 | UseCases/Index 60 | Cors/Index 61 | Miscellaneous/Index 62 | Sitemap 63 | -------------------------------------------------------------------------------- /Documentation/Integration/Index.rst: -------------------------------------------------------------------------------- 1 | .. _integration: 2 | 3 | ============ 4 | Integration 5 | ============ 6 | 7 | @todo - write docs 8 | 9 | Integration with other extensions 10 | ==================================== 11 | 12 | @todo - write docs 13 | @todo - write docs: configure serializer for classes which can not be override :ref:`serialization_yaml_metadata` 14 | 15 | News extension - Example integration 16 | ====================================== 17 | 18 | @todo - write docs t3apinews 19 | 20 | Inline output 21 | ====================================== 22 | 23 | @todo - write docs: If you would like to include your JSON directly in TYPO3 HTML output (e.g. to omit waiting for initial request 24 | to API) you can use xxxViewHelper as follows: 25 | 26 | .. code-block:: html 27 | 28 | 32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Documentation/Miscellaneous/CommonIssues/Index.rst: -------------------------------------------------------------------------------- 1 | .. _common_issues: 2 | 3 | ===================================== 4 | Common Issues 5 | ===================================== 6 | 7 | "&cHash empty" issue 8 | ========================================================== 9 | 10 | Request parameters could not be validated (&cHash empty) 11 | 12 | If you are receiving such TYPO3's error when trying to access endpoints potential reasons could be: 13 | 14 | - t3api version lower than 2.1. 15 | - TYPO3 global configuration or one of the installed extensions is resetting ``cacheHash.excludedParameters`` value. Check if ``t3apiResource`` is inside collection of excluded parameters defined in ``$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters']``. Such value is added by t3api core so you should use merge ``excludedParameters`` instead of override. 16 | -------------------------------------------------------------------------------- /Documentation/Miscellaneous/Development/Index.rst: -------------------------------------------------------------------------------- 1 | .. _development: 2 | 3 | ============ 4 | Development 5 | =========== 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | Introduction/Index 11 | CommandsList/Index 12 | TypicalUseCases/Index 13 | -------------------------------------------------------------------------------- /Documentation/Miscellaneous/Development/Introduction/Index.rst: -------------------------------------------------------------------------------- 1 | .. _development_introduction: 2 | 3 | ============= 4 | Introduction 5 | ============= 6 | 7 | During development of t3api, you will be dealing with two different TYPO3 installations, 8 | which serve different purposes. 9 | 10 | Unit and functional testing installation 11 | ++++++++++++++++++++++++++++++++++++++++ 12 | 13 | * It is a minimal TYPO3 installation, which is used for running unit and functional tests. 14 | * Files are under directory :directory:`.Build` 15 | * You can install it manually with :bash:`ddev composer i` 16 | * It is installed automatically while using command :ref:`_development_commands_list_ddev_ci` 17 | * There can be only one TYPO3 version installed at one time. 18 | 19 | Integration and manual testing installation 20 | +++++++++++++++++++++++++++++++++++++++++++ 21 | 22 | * This installation is a full TYPO3 accessible under :uri:`https://[TYPO3_VERSION].t3api.ddev.site` 23 | and it is used for testing REST API endpoints using Postman tests. It is also used for manual testing. 24 | * Files are under directory :directory:`/.test/[TYPO3_VERSION]` 25 | * You can install it manually using command :ref:`_development_commands_list_ddev_install` 26 | * It is installed automatically while using command :ref:`_development_commands_list_ddev_ci` 27 | * There can be multiple TYPO3 versions integrations installations at one time each under different url. 28 | -------------------------------------------------------------------------------- /Documentation/Miscellaneous/Development/TypicalUseCases/Index.rst: -------------------------------------------------------------------------------- 1 | .. _development_typical_use_cases: 2 | 3 | ================= 4 | Typical Use Cases 5 | ================= 6 | 7 | 8 | .. toctree:: 9 | :maxdepth: 3 10 | 11 | BugsFixing/Index 12 | -------------------------------------------------------------------------------- /Documentation/Miscellaneous/Index.rst: -------------------------------------------------------------------------------- 1 | .. _miscellaneous: 2 | 3 | =============== 4 | Miscellaneous 5 | =============== 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | Changelog/Index 11 | CommonIssues/Index 12 | Development/Index 13 | -------------------------------------------------------------------------------- /Documentation/Pagination/ClientSide/Index.rst: -------------------------------------------------------------------------------- 1 | .. _pagination_client-side: 2 | 3 | =============== 4 | Client side 5 | =============== 6 | 7 | @todo - write docs 8 | -------------------------------------------------------------------------------- /Documentation/Pagination/Index.rst: -------------------------------------------------------------------------------- 1 | .. _pagination: 2 | 3 | =========== 4 | Pagination 5 | =========== 6 | 7 | @todo - write docs 8 | 9 | Global configuration 10 | ====================== 11 | 12 | @todo - write docs 13 | 14 | Resource specific configuration 15 | ================================ 16 | 17 | @todo - write docs 18 | 19 | .. toctree:: 20 | :maxdepth: 3 21 | :hidden: 22 | 23 | ServerSide/Index 24 | ClientSide/Index 25 | -------------------------------------------------------------------------------- /Documentation/Pagination/ServerSide/Index.rst: -------------------------------------------------------------------------------- 1 | .. _pagination_server-side: 2 | 3 | =============== 4 | Server side 5 | =============== 6 | 7 | @todo - write docs 8 | -------------------------------------------------------------------------------- /Documentation/Security/Index.rst: -------------------------------------------------------------------------------- 1 | .. _security: 2 | 3 | ========= 4 | Security 5 | ========= 6 | 7 | - @todo - write docs: general usage of expression language inside of "security" annotation 8 | - @todo - write docs: general usage of expression language inside of "security_post_denormalize" annotation 9 | - @todo - write docs: example usage of frontend user 10 | - @todo - write docs: example usage of frontend user group 11 | - @todo - write docs: example usage of backend user 12 | - @todo - write docs: example usage of backend user group 13 | - @todo - write docs: example usage of `object` in items operations (+ information that `object` doesn't exist in collection operation) 14 | - @todo - write docs: information that most of the conditions from typoscript can be used (https://docs.typo3 .org/m/typo3/reference-typoscript/master/en-us/Conditions/Index.html#description; `tree` is an example object which can not be used) 15 | -------------------------------------------------------------------------------- /Documentation/Serialization/ContextGroups/Index.rst: -------------------------------------------------------------------------------- 1 | .. _serialization_context-groups: 2 | 3 | =============== 4 | Context groups 5 | =============== 6 | 7 | @todo - write docs 8 | 9 | Customization 10 | =============== 11 | 12 | @todo - write docs: describe how to customize serialization context using ``\SourceBroker\T3api\Serializer\ContextBuilder\ContextBuilderInterface::SIGNAL_CUSTOMIZE_SERIALIZER_CONTEXT_ATTRIBUTES`` (e.g. how to conditionally include fields if current user belongs to specified group) 13 | -------------------------------------------------------------------------------- /Documentation/Serialization/Customization/Index.rst: -------------------------------------------------------------------------------- 1 | .. _serialization_expression-language: 2 | 3 | ==================== 4 | Expression language 5 | ==================== 6 | 7 | In some places of serialization and deserialization process Symfony expression language is used. 8 | 9 | @todo add detailed info what variables and functions are supported by default (function `force_absolute_url`, variables in `context`, `object` in AccessorStrategy execution) 10 | 11 | :ref:`Here you can find out how to customize and extend expression language for serialization ` 12 | -------------------------------------------------------------------------------- /Documentation/Serialization/Exceptions/Index.rst: -------------------------------------------------------------------------------- 1 | .. _serialization_exceptions: 2 | 3 | ========== 4 | Exceptions 5 | ========== 6 | 7 | TYPO3 may encounter issues with FileReference or File objects, such as when a file 8 | is missing, inaccessible, or if the relation is broken. These issues can interrupt 9 | the serialization process and a jsonified error will be returned. 10 | To address this, we've introduced a configuration option that allows for the 11 | graceful handling of specific exceptions during the serialization process. 12 | This configuration is defined on a per-class basis, meaning that different 13 | classes can have different sets of exceptions that are handled gracefully. 14 | 15 | Once configured, these exceptions will not interrupt the serialization process 16 | for the respective class. Instead, they will be handled appropriately, 17 | allowing the serialization process to continue uninterrupted. 18 | This ensures that a problem with a single object does not prevent the successful 19 | serialization of other objects. 20 | 21 | 22 | Setting responsible for that is: 23 | 24 | .. code-block:: php 25 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializer']['exclusionForExceptionsInAccessorStrategyGetValue'] 26 | 27 | 28 | Example value: 29 | 30 | .. code-block:: php 31 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializer']['exclusionForExceptionsInAccessorStrategyGetValue'] = [ 32 | \SourceBroker\T3apinews\Domain\Model\FileReference::class => [ 33 | \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException::class, 34 | ], 35 | ]; 36 | 37 | Asterix as "all exceptions" is also supported: 38 | Example: 39 | 40 | .. code-block:: php 41 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializer']['exclusionForExceptionsInAccessorStrategyGetValue'] = [ 42 | \SourceBroker\T3apinews\Domain\Model\FileReference::class => ['*'], 43 | ]; 44 | -------------------------------------------------------------------------------- /Documentation/Serialization/Handlers/Index.rst: -------------------------------------------------------------------------------- 1 | .. _serialization_handlers: 2 | 3 | ========= 4 | Handlers 5 | ========= 6 | 7 | @todo - write docs 8 | 9 | FileReference 10 | =============== 11 | 12 | @todo - write docs 13 | 14 | Image 15 | =============== 16 | 17 | @todo - write docs 18 | 19 | RecordUri 20 | =========== 21 | 22 | @todo - write docs 23 | 24 | Typolink 25 | ========= 26 | 27 | @todo - write docs 28 | 29 | Custom handlers 30 | ================ 31 | 32 | @todo - write docs 33 | -------------------------------------------------------------------------------- /Documentation/Serialization/Index.rst: -------------------------------------------------------------------------------- 1 | .. _serialization: 2 | 3 | Serialization 4 | ============== 5 | 6 | @todo - write docs 7 | 8 | .. toctree:: 9 | :maxdepth: 3 10 | :hidden: 11 | 12 | ContextGroups/Index 13 | Handlers/Index 14 | Subscribers/Index 15 | YamlMetadata/Index 16 | Customization/Index 17 | Exceptions/Index 18 | -------------------------------------------------------------------------------- /Documentation/Serialization/YamlMetadata/Index.rst: -------------------------------------------------------------------------------- 1 | .. _serialization_yaml-metadata: 2 | 3 | ============ 4 | Yaml metadata 5 | ============ 6 | 7 | @todo - write docs: how to use serializerMetadataDirs 8 | -------------------------------------------------------------------------------- /Documentation/Sitemap.rst: -------------------------------------------------------------------------------- 1 | :template: sitemap.html 2 | 3 | .. _sitemap: 4 | 5 | ======= 6 | Sitemap 7 | ======= 8 | -------------------------------------------------------------------------------- /Documentation/UseCases/Index.rst: -------------------------------------------------------------------------------- 1 | .. _use-cases: 2 | 3 | ================ 4 | Use cases 5 | ================ 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | CurrentUserEndpoint/Index 11 | CurrentUserAssignment/Index 12 | -------------------------------------------------------------------------------- /Documentation/guides.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_modadministration.xlf: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/SourceBroker.T3api.Response.AbstractCollectionResponse.yml: -------------------------------------------------------------------------------- 1 | SourceBroker\T3api\Response\AbstractCollectionResponse: 2 | properties: 3 | operation: 4 | exclude: true 5 | query: 6 | exclude: true 7 | request: 8 | exclude: true 9 | membersCache: 10 | exclude: true 11 | totalItemsCache: 12 | exclude: true 13 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/SourceBroker.T3api.Response.HydraCollectionResponse.yml: -------------------------------------------------------------------------------- 1 | SourceBroker\T3api\Response\HydraCollectionResponse: 2 | virtual_properties: 3 | getMembers: 4 | name: members 5 | serialized_name: hydra:member 6 | type: array 7 | groups: ['__hydra_collection_response'] 8 | getTotalItems: 9 | name: totalItems 10 | serialized_name: hydra:totalItems 11 | type: int 12 | groups: ['__hydra_collection_response'] 13 | getView: 14 | name: view 15 | serialized_name: hydra:view 16 | type: array 17 | groups: ['__hydra_collection_response'] 18 | getSearch: 19 | name: search 20 | serialized_name: hydra:search 21 | type: array 22 | groups: ['__hydra_collection_response'] 23 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/SourceBroker.T3api.Response.MainEndpointResponse.yml: -------------------------------------------------------------------------------- 1 | SourceBroker\T3api\Response\MainEndpointResponse: 2 | properties: 3 | apiResourceRepository: 4 | exclude: true 5 | virtual_properties: 6 | getResources: 7 | name: resources 8 | serialized_name: resources 9 | type: array 10 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Core.Resource.AbstractFile.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Core\Resource\AbstractFile: 2 | properties: 3 | storage: 4 | type: TYPO3\CMS\Core\Resource\ResourceStorage 5 | exclude: true 6 | properties: 7 | type: array 8 | identifier: 9 | type: string 10 | name: 11 | type: string 12 | deleted: 13 | type: bool 14 | virtual_properties: 15 | getPublicUrl: 16 | type: string 17 | absolutePublicUrl: 18 | type: string 19 | exp: force_absolute_url(object.getPublicUrl(), context.getAttribute('TYPO3_SITE_URL')) 20 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Core.Resource.File.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Core\Resource\File: 2 | properties: 3 | metaDataLoaded: 4 | exclude: true 5 | metaDataProperties: 6 | exclude: true 7 | metaDataAspect: 8 | exclude: true 9 | indexingInProgress: 10 | exclude: true 11 | virtual_properties: 12 | uid: 13 | exp: object.getProperties()['uid'] 14 | type: integer 15 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Core.Resource.FileReference.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Core\Resource\FileReference: 2 | properties: 3 | propertiesOfFileReference: 4 | exclude: true 5 | uidOfFileReference: 6 | exclude: true 7 | driver: 8 | exclude: true 9 | mergedProperties: 10 | exclude: true 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Extbase.DomainObject.AbstractDomainObject.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject: 2 | properties: 3 | uid: 4 | readOnly: true 5 | type: integer 6 | pid: 7 | type: integer 8 | _localizedUid: 9 | exclude: true 10 | _languageUid: 11 | exclude: true 12 | _versionedUid: 13 | exclude: true 14 | _isClone: 15 | exclude: true 16 | _cleanProperties: 17 | exclude: true 18 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Extbase.Persistence.Generic.LazyObjectStorage.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage: 2 | properties: 3 | warning: 4 | exclude: true 5 | dataMapper: 6 | exclude: true 7 | parentObject: 8 | exclude: true 9 | propertyName: 10 | exclude: true 11 | fieldValue: 12 | exclude: true 13 | isInitialized: 14 | exclude: true 15 | objectManager: 16 | exclude: true 17 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/TYPO3.CMS.Extbase.Persistence.ObjectStorage.yml: -------------------------------------------------------------------------------- 1 | TYPO3\CMS\Extbase\Persistence\ObjectStorage: 2 | properties: 3 | warning: 4 | exclude: true 5 | storage: 6 | exclude: true 7 | isModified: 8 | exclude: true 9 | addedObjectsPositions: 10 | exclude: true 11 | removedObjectsPositions: 12 | exclude: true 13 | positionCounter: 14 | exclude: true 15 | -------------------------------------------------------------------------------- /Resources/Private/Serializer/Metadata/Throwable.yml: -------------------------------------------------------------------------------- 1 | Exception: 2 | properties: 3 | message: 4 | serialized_name: 'hydra:title' 5 | trace: 6 | exclude: true 7 | string: 8 | exclude: true 9 | code: 10 | serialized_name: 'hydra:code' 11 | file: 12 | exclude: true 13 | line: 14 | exclude: true 15 | previous: 16 | exclude: true 17 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Administration/Documentation.html: -------------------------------------------------------------------------------- 1 | {namespace core=TYPO3\CMS\Core\ViewHelpers} 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /Resources/Public/Icons/Extension.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | { } 8 | . ... 9 | ..... 10 | ... . 11 | ..... 12 | . ... 13 | 14 | 15 | -------------------------------------------------------------------------------- /Tests/Postman/fixtures/test1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebroker/t3api/fdf013c5687c92c8072c86b25fa8246e14b10f76/Tests/Postman/fixtures/test1.jpg -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/ApiFilterTest.php: -------------------------------------------------------------------------------- 1 | subject = new ApiFilter('filterClass', 'property', 'strategy', ['argument' => 'argumentValue']); 18 | } 19 | 20 | protected function tearDown(): void 21 | { 22 | parent::tearDown(); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function getFilterClassReturnsInitialValueForString() 29 | { 30 | self::assertSame( 31 | 'filterClass', 32 | $this->subject->getFilterClass() 33 | ); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function getStrategyReturnsInitialValueForApiFilterStrategy() 40 | { 41 | self::assertSame( 42 | 'strategy', 43 | $this->subject->getStrategy()->getName() 44 | ); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function getPropertyReturnsInitialValueForString() 51 | { 52 | self::assertSame( 53 | 'property', 54 | $this->subject->getProperty() 55 | ); 56 | } 57 | 58 | /** 59 | * @test 60 | */ 61 | public function getArgumentsReturnsInitialValueForArray() 62 | { 63 | self::assertSame( 64 | ['argument' => 'argumentValue'], 65 | $this->subject->getArguments() 66 | ); 67 | } 68 | 69 | /** 70 | * @test 71 | */ 72 | public function getArgumentReturnsInitialValueForAccessingSingleItem() 73 | { 74 | self::assertSame( 75 | 'argumentValue', 76 | $this->subject->getArgument('argument') 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/Unit/Fixtures/Annotation/Serializer/Type/ExampleTypeWithNestedParams.php: -------------------------------------------------------------------------------- 1 | value = $options['value']; 22 | $this->config = $options['config'] ?? $this->config; 23 | } 24 | 25 | public function getParams(): array 26 | { 27 | return [ 28 | $this->value, 29 | $this->config, 30 | ]; 31 | } 32 | 33 | public function getName(): string 34 | { 35 | return 'ExampleTypeWithNestedParams'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/Unit/Fixtures/Domain/Model/AbstractEntry.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | protected ObjectStorage $groups; 24 | 25 | protected bool $hidden; 26 | 27 | /** 28 | * @var ObjectStorage 29 | */ 30 | protected ObjectStorage $categories; 31 | 32 | public function getId(): int 33 | { 34 | return $this->id; 35 | } 36 | 37 | /** 38 | * @VirtualProperty 39 | * @return int[] 40 | */ 41 | public function getTagIds(): array 42 | { 43 | return [122, 83, 110]; 44 | } 45 | 46 | /** 47 | * @VirtualProperty("groupIds") 48 | * @return array 49 | */ 50 | public function getIdsOfAssignedGroups(): array 51 | { 52 | return [10, 27, 35]; 53 | } 54 | 55 | /** 56 | * @VirtualProperty 57 | * @return ObjectStorage 58 | */ 59 | public function getTags(): ObjectStorage 60 | { 61 | return new ObjectStorage(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/Unit/Fixtures/Domain/Model/Address.php: -------------------------------------------------------------------------------- 1 | bankAccountNumber; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Unit/Fixtures/Domain/Model/Group.php: -------------------------------------------------------------------------------- 1 | firstName . ' ' . $this->lastName; 32 | } 33 | 34 | /** 35 | * @VirtualProperty("privateAddress") 36 | * @ExampleTypeWithNestedParams( 37 | * "PrivateAddress", 38 | * config={ 39 | * "parameter1": "value1", 40 | * "parameter2": { 41 | * "value2a", 42 | * "value2b", 43 | * }, 44 | * "parameter3": { 45 | * "parameter3a": "value3a", 46 | * "parameter3b": 3, 47 | * }, 48 | * } 49 | * ) 50 | */ 51 | public function getPrivateAddress(): ?Address 52 | { 53 | return $this->address; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/Unit/Fixtures/Domain/Model/Tag.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.0.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 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - '#Unsafe usage of new static\(\)#' 4 | - '#Access to an undefined property Metadata\\PropertyMetadata::\$type.#' 5 | typo3: 6 | requestGetAttributeMapping: 7 | t3apiHeaderLanguageRequest: bool 8 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | parallel: 6 | maximumNumberOfProcesses: 5 7 | 8 | level: 2 9 | 10 | paths: 11 | - Classes 12 | - Configuration 13 | - Tests 14 | --------------------------------------------------------------------------------