Ext: sg_rest

License: GNU GPL, Version 2

Repository: https://gitlab.sgalinski.de/typo3/sg_rest

TYPO3 version: >=10.4

How To Call REST Functions?

In Chrome With Postman

In this case you can install the Chrome extension "Postman" (https://www.getpostman.com/). With this extension you can dispatch REST calls to a specific URL with POST parameters, which is required for our REST implementation.

REST Registration

Target

After this registration process you can call the following REST function:

Calls an action of the entity, or returns an entity with the given uid.

URL: https://www.website-base.dev/?type=1595576052&request=<apiKey>/<entityName>/<actionOrUid>

Required POST data:

authToken = <aAuthTokenFromAUser> // See "REST Authentication" for this

OR

bearerToken = <bearerToken> // See "REST Authentication" for this

Tasks

1) Call this in the extensions "ext_localconf.php":

$class = 'SGalinski\SgRest\Service\RegistrationService';
$restRegistrationService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($class);
$restRegistrationService->registerAccessGroup(
	<apiKey>, // Example: "news"
	'Vendor.<extension_key>', // Example: "Vendor.sg_news"
	<accessGroupName>, // Example: "News" It's the name of the api, which is shown in the user TCA. See: "REST Authentication"
	[
		<entityName> => [ // Example: "news"
			'read' => 'uid, title' // This allows that the API can read the fields "uid" and "title" from the entity "news"
		]
	]
);

2) Create a controller which is the endpoint of the registration:

namespace Vendor\ExtensionName\Controller\Rest\<apiKeyWithCamelcase>; // Example: "...\Controller\Rest\News"

use SGalinski\SgRest\Controller\AbstractRestController;
use SGalinski\SgRest\Service\PaginationService;
use SGalinski\SgRest\Domain\Model\FrontendUser;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings;

class <entityNameWithCamelcase>Controller extends AbstractRestController { // Example: "NewsController"
	/**
	 * Class name for the model mapping
	 *
	 * @var string
	 */
	protected $className = <entityNameWithNameSpace>; // Example: "Vendor\ExtensionName\Domain\Model\News"

	/**
	 * @var EntityRepository
	 */
	protected $entityRepository;

	/**
	 * Injects the repository. Is lot faster to use the inject method than the inject annotation!
	 *
	 * @param EntityRepository $entityRepository
	 * @return void
	 */
	public function injectEntityRepository(EntityRepository $entityRepository) {
		/** @var $querySettings Typo3QuerySettings */
		$querySettings = GeneralUtility::makeInstance('TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings');
		$querySettings->setRespectStoragePage(FALSE);
		$entityRepository->setDefaultQuerySettings($querySettings);

		$this->entityRepository = $entityRepository;
	}

	/**
	 * Action to return a entity in json format.
	 *
	 * @param Entity $entity
	 * @return void
	 * @throws \Exception
	 */
	public function getAction(Entity $entity) {
		if (!$entity) {
			throw new \InvalidArgumentException('You´re not allowed to access this entity!', 403);
		}

		$this->returnData($this->dataResolveService->getArrayFromObject($entity));
	}

	/**
	 * Get list request for entities.
	 *
	 * @param int $page
	 * @param int $limit
	 * @throws \Exception
	 * @return void
	 */
	public function getListAction($page = 1, $limit = 10) {
		/** @var FrontendUser $authenticatedUser */
		$authenticatedUser = $this->authenticationService->getAuthenticatedUser();

		if (!$authenticatedUser) {
			throw new \InvalidArgumentException('You´re not allowed to access this entity list!', 403);
		}

		$response = ['amountOfAllEntries' => 0, 'prev' => NULL, 'next' => NULL, 'data' => []];
        $amountOfAllEntities = $this->entityRepository->countAll();

        /** @var PaginationService $paginationService */
        $class = 'SGalinski\SgRest\Service\PaginationService';

        $paginationService = GeneralUtility::makeInstance($class, $page, $limit, 10, $amountOfAllEntities, $this->apiKey);
        $paginationSettings = $paginationService->getPaginationSettings();
        $response['prev'] = $paginationService->getPreviousPageUrl(<entityName>); // Example: "news"
        $response['next'] = $paginationService->getNextPageUrl(<entityName>); // Example: "news"
        $response['amountOfAllEntries'] = $amountOfAllEntities;

        $entries = $this->entityRepository->findAllWithPagination($limit, $paginationSettings['offset']);
        foreach ($entries as $entry) {
            $response['data'][] = $this->dataResolveService->getArrayFromObject($entry);
        }

		$this->returnData($response);
	}
}

3) Create the function "findAllWithPagination" in your entity repository:

	/**
	 * Find all entries with some pagination information.
	 *
	 * @param int $limit
	 * @param int $offset
	 * @return array|\TYPO3\CMS\Extbase\Persistence\QueryResultInterface
	 */
	public function findAllWithPagination($limit, $offset) {
		return $this->createQuery()->setOrderings(['crdate' => QueryInterface::ORDER_ASCENDING])
			->setOffset((int) $offset)
			->setLimit((int) $limit)
			->execute();
	}

Additional Registration Configuration

If you want to use POST/PUT/PATCH/DELETE request, then you need to add the additional configuration "httpPermissions".

$class = 'SGalinski\SgRest\Service\RegistrationService';
$restRegistrationService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($class);
$restRegistrationService->registerAccessGroup(
	<apiKey>, // Example: "news"
	'Vendor.<extension_key>', // Example: "Vendor.sg_news"
	<accessGroupName>, // Example: "News" It's the name of the api, which is shown in the user TCA. See: "REST Authentication"
	[
		<entityName> => [ // Example: "news"
			'read' => 'uid, title' // This allows that the API can read the fields "uid" and "title" from the entity "news",
			'httpPermissions' => [
				'deleteForVerbs' => TRUE,
				'putWithIdentifier' => TRUE,
				'patchWithIdentifier' => TRUE,
				'postWithIdentifier' => TRUE,
			],
		]
	]
);

Hints

  1. Always use the function "$this->returnData" in your controller functions, to return the data.
  2. Always use the function "$this->dataResolveService->getArrayFromObject($entity)" to get the data from your registered entity. This respects the allowed fields, which are configured in "$restRegistrationService->registerAccessGroup()".
  3. Caching should be used in each api function.
  4. Try not to use Extbase calls, so the performance would be much better.

REST Authentication

Our REST solution works with the authorization of frontend users. These got two new TCA fields for this.

Field "Authentication Token"

This field must be set to a unique ID. So it can be mapped with the given "authToken" parameter, which must be set in each REST function call.

Field "Access groups"

All available registered access groups are listed. Each of them are linked to a REST api. The user just haves access to the api, if the group is set here.

Field "Test mode"

With this field you can verify an authenticated user as a test user. The authentication API offers a function isAuthenticatedUserInTestMode to check if the current authenticated user is in test-mode.

Bearer Token instead of Authentication Token

As an alternative to the authentication token, our REST solution also offers authentication via a Bearer Token / JWT. Upon successful authentication, the user gets a token which then must be sent with every request. When the token is valid, the server returns the executes the desired request. Since this type of token can get verified without the need for a database connection, it is especially interesting, when used in a micro-service environment, in which the authentication service / server would otherwise act as a bottleneck.

Using the BearerAuthenticationService

To actually use the Bearer Token for Authentication, we need to switch from using the BasicAuthenticationService to using the BearerAuthenticationService. The classes using a AuthenticationService get it by Constructor Dependency Injection of the AuthenticationServiceInterface, which means that any AuthenticationService that implements this Interface can be injected here. By default, the alias configured in the Services.yaml for this Interface is the BasicAuthenticationService. To switch to using the BearerAuthenticationService, you simply need to change this alias in your own Services.yaml:

Default:

SGalinski\SgRest\Service\Authentication\AuthenticationServiceInterface: '@SGalinski\SgRest\Service\Authentication\BasicAuthenticationService'

To use the BearerAuthenticationService:

SGalinski\SgRest\Service\Authentication\AuthenticationServiceInterface: '@SGalinski\SgRest\Service\Authentication\BearerAuthenticationService'

How to get a Bearer Token

To get a Bearer Token, you need to execute an authentication request and transmit valid user credentials to the following url:

/?type=1595576052&tx_sgrest[request]=authentication/authentication/getBearerToken&logintype=login

Since we use the standard authentication process for fe_users which based on the middleware TYPO3\CMS\Frontend\Middleware\FrontendUserAuthenticator, the request must be executed as POST and contain the form data params user and pass.

When the user authentication is successful, and the user is allowed to use at least one REST endpoint, a Bearer Token is returned:

{
    "bearerToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoyLCJleHAiOjE2MDgwMjQzMTd9.exQ3UkeBBd2P_1o4woP0uBn6koxvTV63aVT13JTkWRw"
}

Implementing your own AuthenticationService

You can also implement your own AuthenticationService. For this, you need to build a class which either implements the AuthenticationServiceInterface or extends the AbtstractAuthenticationService.

namespace Vendor\Extension\Service\Authentication;

use TYPO3\CMS\Core\SingletonInterface;
use SGalinski\SgRest\Service\AbstractAuthenticationService;

class CustomAuthenticationService extends AbstractAuthenticationService implements SingletonInterface {

	/**
	 * @param array $requestHeaders
	 * @return bool
	 * @throws Exception
	 */
	public function verifyRequest(array $requestHeaders): bool {
        // verify Request
        //return bool;
	}

	/**
	 * Verify if the authenticated user has access to the given apikey.
	 *
	 * @param $apiKey
	 * @return bool
	 */
	public function verifyUserAccess($apiKey): bool {
        // verify user access
        //return bool;
	}
}

Additional Configuration

A Better URL For The Call

With API Subdomain

Old: https://www.website-base.dev/?type=1595576052&request=<apiKey>/<entityName>/<actionOrUid>
New: https://api.website-base.dev/<apiKey>/<entityName>/<actionOrUid>

If you want to call the REST api with a URL like above, then you just need to add the following code to the projects .htaccess

# Api redirects
RewriteCond %{HTTP_HOST} ^api.(?:website-base.dev?)$ [NC]
RewriteRule ^(.+)$ index.php?type=1595576052&request=%{REQUEST_URI} [QSA,NC,L]

Without API Subdomain

Old: https://www.website-base.dev/?type=1595576052&request=<apiKey>/<entityName>/<actionOrUid>
New: https://www.website-base.dev/api/v1/<apiKey>/<entityName>/<actionOrUid>

If you want to call the REST api with a URL like above, then you just need to add the following code to the projects .htaccess

# Api redirects
RewriteRule ^api/v1/(.*) /index.php?eID=kboJobsRestApi&request=$1 [QSA]

Also see the next section "Customize the REST url pattern" for more on this

Customize the REST url pattern

If your REST url is not a subdomain you maybe have a url segment after the host or something completely different. The default url pattern is like HOST/APIKEY/ENTITY and if your host does not contain the identifier for your API, you need to adjust the REST url pattern. Otherwise your pagination service will generate invalid next and previous URLs.

You don't need to add a url scheme. HTTPS is required and will be always used. The url pattern uses handlebars for the markers. The following markers exist:

  • {{HOST}}
  • {{APIKEY}}
  • {{ENTITY}}

So the default pattern is: {{HOST}}/{{APIKEY}}/{{ENTITY}}

If you want to adjust the URL from https://api.yourdomain.com/apikey/entity to https://www.yourdomain.com/api/apikey/entity for instance. You just need to set a new url pattern to the pagination service.

	/** @var PaginationService $paginationService */
	$paginationService = GeneralUtility::makeInstance(
		PaginationService::class, $page, $limit, 10, $amountOfEntries, $apiKey
	);
	$paginationService->setUrlPattern('{{HOST}}/api/{{APIKEY}}/{{ENTITY}}');

Route Enhancer

Unfortunately the URL pattern of this extension isn't compatible with the Route Enhancer at the moment, since the path mapped to the Rest Controller and Action by the PathUtility is provided within the request parameter:

[...]?sg_rest[request]=news/news/getList

The Route Enhancer simply isn't able to map this properly.

Please use the .htaccess option explained above to beautify your REST urls in the meantime!