diff --git a/Documentation/Filtering/Index.rst b/Documentation/Filtering/Index.rst index 0cb14518..b2a6eb79 100644 --- a/Documentation/Filtering/Index.rst +++ b/Documentation/Filtering/Index.rst @@ -108,6 +108,8 @@ Filters gives possibility to customize SQL query used to receive results in ``GE { } +``persistence.storagePid`` limits records of collection operations only. Item operations (``/{id}``) load a record from any page, see :ref:`storage pages `. + It is possible to configure filters only for whole resource. That means filters are available for all ``GET`` collection operations within this resource. It is not possible (yet) to configure filters only for specific operations. There are few build-in filters. See the next pages. diff --git a/Documentation/GettingStarted/Index.rst b/Documentation/GettingStarted/Index.rst index 2b2982d9..f3d6fabb 100644 --- a/Documentation/GettingStarted/Index.rst +++ b/Documentation/GettingStarted/Index.rst @@ -7,20 +7,35 @@ Getting started What does it do ================ -T3api extension provides easy configurable and customizable REST API for your Extbase models. -It allows to configure whole API functionality with annotations for classes, properties and methods. +T3api provides a configurable and customizable REST API for Extbase models. API resources, their operations, filters +and access rules are configured with annotations on the model class (or on a class extending a model of another +extension, see :ref:`integration`), output of the models with annotations or YAML serializer metadata. -Most of configuration options is based on `API Platform `_ to make it easier to use for -developers experienced in this awesome framework. +Most configuration options follow `API Platform `__, so they are familiar to developers who +know this framework. -T3api comes with partial support of `JSON-LD `__ and `Hydra `__, -which allows to build smart frontend applications with auto-discoverability capabilities. +Responses use `JSON-LD `__ and `Hydra `__ (partial support), which +allows frontend applications to discover resources, filters and pagination from the responses. Installation ============ -T3api supports TYPO3 12.4, 13 and 14 (PHP 8.1 or higher). Run +Supported versions: + +.. list-table:: + :header-rows: 1 + + * - TYPO3 + - PHP + * - 12.4 + - 8.1 - 8.4 + * - 13.4 + - 8.2 - 8.5 + * - 14 + - 8.2 - 8.5 + +Run .. code-block:: bash @@ -119,3 +134,25 @@ After flushing caches the items are available under ``https://example.com/_api/i * | Single news: | `https://14.t3api.ddev.site/_api/news/news/1 `__ | + +.. _getting-started_backend-module: + +Backend module +============== + +Backend module **Web API** shows the API of a site in `Swagger UI `__ - all +resources and operations with their parameters, filters and response schemas, generated as OpenAPI specification from +the configuration of the API resources. Select the site in the module header. The site needs the +:ref:`route enhancer `, otherwise the module shows an error message. + +Backend users need access to the module (admins have it by default). + +.. important:: + + "Try it out" in Swagger UI sends real requests to the API of the selected site - ``POST``, ``PATCH``, ``PUT`` and + ``DELETE`` change and delete records. The browser sends the requests with its cookies of the API domain, so + :ref:`security ` expressions see the frontend or backend user logged in on that domain. + +The backend Content Security Policy of the module allows requests to the domains of all sites with the t3api route +enhancer. Swagger UI does not use the online validator of swagger.io, so the URL of the specification is not sent +outside of the TYPO3 instance. diff --git a/Documentation/Index.rst b/Documentation/Index.rst index f46830aa..5a53bb10 100644 --- a/Documentation/Index.rst +++ b/Documentation/Index.rst @@ -37,6 +37,24 @@ If you find an error or something is missing, please: `Report a Problem `__. +T3api provides a REST API for Extbase models. API resources, their operations and filters are configured with +annotations on the model class (or on a class extending a model of another extension, see :ref:`integration`), output +with serializer metadata in annotations or :ref:`YAML `. Configuration options follow +`API Platform `__ and responses use `JSON-LD `__ / +`Hydra `__ format. + +Overview +======== + +- :ref:`Operations ` - ``GET``, ``POST``, ``PATCH``, ``PUT`` and ``DELETE`` for Extbase models, :ref:`slug fields ` generated according to TCA. +- :ref:`Filtering ` and :ref:`pagination ` of collections, :ref:`ordered UIDs filter ` for collections in externally resolved order. +- :ref:`Serialization ` - output per operation with :ref:`context groups `, metadata in annotations or :ref:`YAML `, typolinks, processed images and file references. +- :ref:`Security ` - access to operations and filters controlled with expressions. +- :ref:`Response cache ` - opt-in cache of ``GET`` responses with tag-based invalidation. +- :ref:`Multilanguage `, :ref:`file upload `, :ref:`cascade persistence ` of relations and :ref:`CORS `. +- :ref:`Backend module ` with Swagger UI - OpenAPI documentation of the API and a client to test it. +- :ref:`Customization ` and :ref:`events ` to change the behaviour. + **Table of Contents** .. toctree:: diff --git a/Documentation/Miscellaneous/Changelog/Index.rst b/Documentation/Miscellaneous/Changelog/Index.rst index 45668f59..f138301a 100644 --- a/Documentation/Miscellaneous/Changelog/Index.rst +++ b/Documentation/Miscellaneous/Changelog/Index.rst @@ -50,6 +50,7 @@ Changelog - Use event request when enriching page cache identifier. [`commit `__] - Delegate to ``Typo3ConditionFunctionsProvider`` instead of extending it. [`commit `__] - Add PHPStan 2 compatibility, rector, PHPUnit attributes. [`commit `__] +- Documentation: ``storagePid`` limits only collection operations, item operations need ``security``. See :ref:`storage pages `. - Documentation: real examples for TYPO3 14 testing instance, new pages for pagination, security, serialization, integration and file upload. 4.1.3 diff --git a/Documentation/Security/Index.rst b/Documentation/Security/Index.rst index 3723f5d9..2c6dc48a 100644 --- a/Documentation/Security/Index.rst +++ b/Documentation/Security/Index.rst @@ -62,6 +62,58 @@ If an expression returns ``false``, the operation is not processed and the respo ``object`` exists only in item operations and in ``security_post_denormalize``. Collection operations (``GET`` and ``POST`` on a collection) do not have it in ``security`` - for collection ``GET`` use filters or your own repository constraints to limit records, for ``POST`` use ``security_post_denormalize``. +.. _security_storage-pid: + +Storage pages +============== + +``storagePid`` in ``persistence`` attributes limits records of collection ``GET`` operations and is the page where new records are stored. It does **not** limit item operations: ``GET``, ``PATCH``, ``PUT`` and ``DELETE`` on ``/{id}`` load the record by its UID from any page, the same as ``findByUid()`` in Extbase. Hidden records, start and end time and frontend user groups are still respected. + +When records must be available only on the storage pages of the resource (e.g. one resource per site or client), check the page of the record in ``security`` of every item operation. ``security`` gets the record as stored in the database, also for ``PATCH`` and ``PUT``. + +Do not allow to write ``pid``: when an operation sets ``denormalizationContext`` groups, properties without groups (``pid`` too) are not deserialized. A new record is then stored on the first page from ``storagePid``, and ``PATCH`` / ``PUT`` cannot change the page of a record. + +.. code-block:: php + + /** + * @T3api\ApiResource ( + * attributes={ + * "persistence"={"storagePid"="3"} + * }, + * collectionOperations={ + * "get"={"path"="/products"}, + * "post"={ + * "method"="POST", + * "path"="/products", + * "denormalizationContext"={"groups"={"api_write_product"}}, + * }, + * }, + * itemOperations={ + * "get"={ + * "path"="/products/{id}", + * "security"="object.getPid() in t3apiOperation.getPersistenceSettings().getStoragePids()", + * }, + * "patch"={ + * "method"="PATCH", + * "path"="/products/{id}", + * "security"="object.getPid() in t3apiOperation.getPersistenceSettings().getStoragePids()", + * "denormalizationContext"={"groups"={"api_write_product"}}, + * }, + * "delete"={ + * "method"="DELETE", + * "path"="/products/{id}", + * "security"="object.getPid() in t3apiOperation.getPersistenceSettings().getStoragePids()", + * }, + * }, + * ) + */ + +``t3apiOperation.getPersistenceSettings().getStoragePids()`` contains the pages from ``storagePid`` only, without subpages of ``recursive`` - list the pages in the expression when you use ``recursive``. + +.. important:: + + ``security_post_denormalize`` alone does not limit item operations to the storage pages, as it checks the record with the data from the request. If ``pid`` is writable, check it there in addition to ``security`` and allow ``null``: ``pid`` of a new record (``POST``) and of a replaced record (``PUT``) is ``null`` until the record is stored, unless the request sends it, e.g. ``object.getPid() === null or object.getPid() in [3]``. + Filter security ================ diff --git a/README.md b/README.md index d134c91d..891a4802 100644 --- a/README.md +++ b/README.md @@ -3,51 +3,111 @@ [![Latest Stable Version](https://poser.pugx.org/sourcebroker/t3api/v/stable)](https://extensions.typo3.org/extension/t3api/) [![Tests](https://img.shields.io/github/actions/workflow/status/sourcebroker/t3api/tests.yml?branch=main&label=Tests&logo=github)](https://github.com/sourcebroker/t3api/actions/workflows/tests.yml) -Supports TYPO3 12.4, 13.4 and 14 on PHP 8.1 - 8.5. +REST API for Extbase models. Resources, operations and filters are configured with annotations on the model class +(or on a class extending a model of another extension), output with serializer metadata in annotations or YAML. +Configuration options follow [API Platform](https://api-platform.com), responses use [JSON-LD](https://json-ld.org/) / +[Hydra](https://www.hydra-cg.com/) format. + +| TYPO3 | PHP | +|-------|-----------| +| 12.4 | 8.1 - 8.4 | +| 13.4 | 8.2 - 8.5 | +| 14 | 8.2 - 8.5 | ## Features -- Support for Extbase models with GET, POST, PATCH, PUT, DELETE operations. -- Configuration with classes, properties and methods annotations. -- Built-in filters: boolean, numeric, uid, contain, order, range, distance and text (partial, match against and exact strategies). -- Ordered UIDs filter and query modifiers - return collections in externally resolved order (e.g. from search engine). -- Built-in pagination. -- Opt-in response caching with automatic tag-based invalidation. -- Support for typolinks. -- Support for image processing. -- Support for file uploads (FAL). -- Configurable routing. -- Responses in [Hydra](https://www.hydra-cg.com/) / [JSON-LD](https://json-ld.org/) format. -- Serialization contexts - customizable output depending on routing. -- Easy customizable serialization handlers and subscribers. -- Backend module with Swagger for documentation and real testing. +**Resources and operations** + +- `GET`, `POST`, `PATCH`, `PUT` and `DELETE` operations for Extbase models. +- Configurable routing and base path of the API. +- Cascade persistence of relations, including inline (IRRE) relations. +- Slug fields generated according to TCA configuration. +- Multilanguage support (language prefix in URL or `X-Locale` header). + +**Collections** + +- Built-in filters: boolean, numeric, uid, contain, order, range, distance and search (partial, match against and exact strategies). +- Ordered UIDs filter and query modifiers - collections in externally resolved order (e.g. from a search engine). +- Pagination. + +**Output** + +- Serialization contexts (groups) - different output for every operation. +- Serializer metadata in annotations or YAML, custom handlers and subscribers. +- Typolinks, processed images and file references. +- File upload to FAL. + +**Access and HTTP** + +- Access control of operations and filters with expressions (`security`, `security_post_denormalize`). +- CORS configuration. +- Opt-in response cache with automatic tag-based invalidation. + +**Developer tools** + +- OpenAPI specification and backend module with Swagger UI for documentation and testing of the API. +- Events to customize processing of operations. + +## Quick start + +```bash +composer require sourcebroker/t3api +``` + +Add the route enhancer to the `config.yaml` of your site: + +```yaml +imports: + - { resource: "EXT:t3api/Configuration/Routing/config.yaml" } +``` + +Make an API resource from an Extbase model: + +```php +use SourceBroker\T3api\Annotation\ApiResource; + +/** + * @ApiResource( + * collectionOperations={ + * "get"={"path"="/items"}, + * }, + * itemOperations={ + * "get"={"path"="/items/{id}"}, + * }, + * ) + */ +class Item extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity +{ +} +``` + +After flushing caches the items are available under `https://example.com/_api/items` and +`https://example.com/_api/items/1`. ## Documentation -Read the docs at https://docs.typo3.org/p/sourcebroker/t3api/main/en-us/ +https://docs.typo3.org/p/sourcebroker/t3api/main/en-us/ -## Take a look and test +## Local test instance -After cloning repo you can run: +After cloning the repository run: ```bash ddev restart && ddev composer install ddev ci 14 ``` -to install local integration test instance. Local instance is available at https://14.t3api.ddev.site/ -(login to backend with `admin` / `Password1!` credentials). - -At frontend part you can at once test REST API responses for ext news: +It installs a TYPO3 14 instance at https://14.t3api.ddev.site/ (backend login `admin` / `Password1!`) with +`EXT:news` records exposed as API resources, e.g.: +- https://14.t3api.ddev.site/_api/ - https://14.t3api.ddev.site/_api/news/news - https://14.t3api.ddev.site/_api/news/news/1 - https://14.t3api.ddev.site/_api/news/categories -- etc -You can also run Postman test with `ddev composer ci:tests:postman` command or full test suite with `ddev composer ci`. -Postman is doing full CRUD test with category and news (with image). +Run the Postman tests (full CRUD of news and categories, including image upload) with +`ddev composer ci:tests:postman`, or the full test suite with `ddev composer ci`. ## Development -If you want to help with development take a look at https://docs.typo3.org/p/sourcebroker/t3api/main/en-us/Miscellaneous/Development/Index.html +See https://docs.typo3.org/p/sourcebroker/t3api/main/en-us/Miscellaneous/Development/Index.html diff --git a/Tests/Functional/Dispatcher/StoragePidDispatcherTest.php b/Tests/Functional/Dispatcher/StoragePidDispatcherTest.php new file mode 100644 index 00000000..46bbe765 --- /dev/null +++ b/Tests/Functional/Dispatcher/StoragePidDispatcherTest.php @@ -0,0 +1,120 @@ +getConnectionPool()->getConnectionForTable(self::TABLE); + $connection->insert(self::TABLE, ['uid' => 1, 'pid' => 3, 'title' => 'On storage page']); + $connection->insert(self::TABLE, ['uid' => 2, 'pid' => 9, 'title' => 'On other page']); + } + + #[Test] + public function collectionContainsOnlyRecordsOfStoragePage(): void + { + $response = $this->dispatch('GET', '/stored-categories'); + + self::assertSame([1], array_column(json_decode($response['body'], true)['hydra:member'], 'uid')); + } + + #[Test] + public function itemOperationWithoutSecurityLoadsRecordOfOtherPage(): void + { + $response = $this->dispatch('GET', '/unrestricted-stored-categories/2'); + + self::assertSame(200, $response['status']); + self::assertSame('On other page', json_decode($response['body'], true)['title']); + } + + #[Test] + public function securityAllowsRecordOfStoragePage(): void + { + self::assertSame(200, $this->dispatch('GET', '/stored-categories/1')['status']); + } + + #[Test] + public function securityDeniesRecordOfOtherPage(): void + { + self::assertSame(403, $this->dispatch('GET', '/stored-categories/2')['status']); + self::assertSame(403, $this->dispatch('PATCH', '/stored-categories/2', '{"title": "Changed", "pid": 3}')['status']); + self::assertSame(403, $this->dispatch('PUT', '/stored-categories/2', '{"title": "Changed"}')['status']); + self::assertSame(403, $this->dispatch('DELETE', '/stored-categories/2')['status']); + + self::assertSame(['pid' => 9, 'title' => 'On other page', 'deleted' => 0], $this->fetchRecord(2)); + } + + #[Test] + public function pidIsNotWritableWithDenormalizationGroups(): void + { + $response = $this->dispatch('PATCH', '/stored-categories/1', '{"title": "Changed", "pid": 9}'); + + self::assertSame(200, $response['status']); + self::assertSame(['pid' => 3, 'title' => 'Changed', 'deleted' => 0], $this->fetchRecord(1)); + } + + #[Test] + public function putOfRecordOfStoragePageIsAllowed(): void + { + self::assertSame(200, $this->dispatch('PUT', '/stored-categories/1', '{"title": "Replaced"}')['status']); + self::assertSame(['pid' => 3, 'title' => 'Replaced', 'deleted' => 0], $this->fetchRecord(1)); + } + + #[Test] + public function newRecordIsStoredOnStoragePage(): void + { + $response = $this->dispatch('POST', '/stored-categories', '{"title": "New", "pid": 9}'); + + self::assertSame(201, $response['status']); + self::assertSame(['pid' => 3, 'title' => 'New', 'deleted' => 0], $this->fetchRecord(json_decode($response['body'], true)['uid'])); + } + + private function fetchRecord(int $uid): array + { + return $this->getConnectionPool()->getConnectionForTable(self::TABLE) + ->select(['pid', 'title', 'deleted'], self::TABLE, ['uid' => $uid])->fetchAssociative(); + } + + /** + * @return array{status: int, body: string} + */ + private function dispatch(string $method, string $path, ?string $body = null): array + { + $request = SymfonyRequest::create( + 'https://example.com/_api' . $path, + $method, + [], + [], + [], + $body !== null ? ['CONTENT_TYPE' => 'application/json'] : [], + $body + ); + $response = new Response(); + try { + $body = $this->get(Bootstrap::class) + ->processOperationByRequest((new RequestContext())->fromRequest($request), $request, $response); + } catch (ExceptionInterface $exception) { + return ['status' => $exception->getStatusCode(), 'body' => '']; + } + + return ['status' => $response->getStatusCode(), 'body' => (string)$body]; + } +} diff --git a/Tests/Functional/Fixtures/Extensions/t3api_functional_test/Classes/Domain/Model/StoredCategory.php b/Tests/Functional/Fixtures/Extensions/t3api_functional_test/Classes/Domain/Model/StoredCategory.php new file mode 100644 index 00000000..c6bf1188 --- /dev/null +++ b/Tests/Functional/Fixtures/Extensions/t3api_functional_test/Classes/Domain/Model/StoredCategory.php @@ -0,0 +1,55 @@ + [ 'tableName' => 'tx_functionaltest_domain_model_tag', ], + StoredCategory::class => [ + 'tableName' => 'tx_functionaltest_domain_model_category', + ], FileReference::class => [ 'tableName' => 'sys_file_reference', ],