Commit b0ee2fef authored by Oliver Bartsch's avatar Oliver Bartsch
Browse files

Kickstart REST API

parent dfc64af9
<?php
declare(strict_types=1);
namespace T3o\Ter\Controller\Api;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use T3o\Ter\Rest\Response\ApiResponseFactory;
use T3o\Ter\Rest\RouteArgument\DeepObjectRouteArgumentInterface;
use T3o\Ter\Rest\RouteConfiguration;
use T3o\Ter\Rest\RouteResultArguments;
/**
* Receive the request, initialize the response using the ApiResponseFactory,
* validate the route arguments and finally call the operationId of the
* specified request handler.
*
* @see ApiResponseFactory
* @see RouteResultArguments
*/
abstract class AbstractApiController implements RequestHandlerInterface
{
protected ApiResponseFactory $apiResponseFactory;
protected RouteConfiguration $routeConfiguration;
protected ServerRequestInterface $request;
protected RouteResultArguments $routeResultArguments;
public function __construct(ApiResponseFactory $apiResponseFactory, RouteConfiguration $routeConfiguration)
{
$this->apiResponseFactory = $apiResponseFactory;
$this->routeConfiguration = $routeConfiguration;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$this->request = $request;
if (!($request->getAttribute('routing') instanceof RouteResultArguments)) {
return $this->apiResponseFactory
->createErrorResponseForRequest($request,1600994285, 'Route arguments are invalid');
}
$this->routeResultArguments = $request->getAttribute('routing');
$handler = $this->routeResultArguments->getOperationId();
if (!method_exists($this, $handler)) {
return $this->apiResponseFactory
->createErrorResponseForRequest($request, 1601289381, 'Action couldn\'t be determined');
}
$invalidArguments = $this->validateRouteArguments($this->routeResultArguments->getRouteArguments());
if ($invalidArguments !== []) {
return $this->apiResponseFactory->createResponseForRequest(
$request,
['status' => 400, 'code' => 1601294359, 'message' => 'One ore more arguments are invalid', 'invalidArguments' => $invalidArguments],
400
);
}
return $this->$handler();
}
protected function validateRouteArguments(array $routeArguments): array
{
$invalidArguments = [];
foreach ($routeArguments as $routeArgument) {
if ($routeArgument instanceof DeepObjectRouteArgumentInterface) {
$subProperties = $this->validateRouteArguments($routeArgument->getProperties());
if ($subProperties !== []) {
$invalidArguments[$routeArgument->getName()] = $subProperties;
}
}
if (!$routeArgument->isValid()) {
$invalidArguments[] = $routeArgument;
}
}
return $invalidArguments;
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace T3o\Ter\Controller\Api;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Psr\Http\Message\ResponseInterface;
/**
* Request handler for `/extension` endpoint
*/
final class ExtensionController extends AbstractApiController
{
public function getExtensions(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function checkExtensionKey(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function registerExtensionKey(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function updateExtensionKey(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function deleteExtensionKey(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function getExtensionVersions(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function transferExtensionKey(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function checkExtensionVersion(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function uploadExtenionVersion(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function deleteExtensionVersion(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function updateReviewState(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace T3o\Ter\Controller\Api;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Psr\Http\Message\ResponseInterface;
/**
* Request handler for `/oauth` endpoint
*/
final class OauthController extends AbstractApiController
{
public function generateAccessToken(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
public function invalidateAccessToken(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest($this->request, ['handler' => __METHOD__]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace T3o\Ter\Controller\Api;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Psr\Http\Message\ResponseInterface;
/**
* Request handler for `/ping` endpoint
*/
final class PingController extends AbstractApiController
{
public function ping(): ResponseInterface
{
return $this->apiResponseFactory->createResponseForRequest(
$this->request,
['endpoints' => array_keys($this->routeConfiguration->getSchema()['paths'] ?? [])]
);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
namespace T3o\Ter\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use T3o\Ter\Rest\RouteHandler;
use T3o\Ter\Rest\RouteConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This middleware forwards the incoming request to the routeHandler
* if the request matches an existing openApi specification.
*
* @see RouteHandler
*/
final class RestRouteDispatcher implements MiddlewareInterface
{
private const API_REQUEST_PATH = '/api/';
private const SCHEMA_PATH = 'EXT:ter/resources/schema/';
protected RouteHandler $routeHandler;
protected RouteConfiguration $routeConfiguration;
public function __construct(RouteHandler $routeHandler, RouteConfiguration $routeConfiguration)
{
$this->routeHandler = $routeHandler;
$this->routeConfiguration = $routeConfiguration;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->isApiRequest($request->getUri()->getPath())) {
// @todo Might need need some further TSFE initialization here
return $this->routeHandler->handle($request);
}
return $handler->handle($request);
}
protected function isApiRequest(string $path): bool
{
if (!preg_match('/^\/api\/v\d{1}\/.*/', $path)) {
return false;
}
[$version] = explode('/', str_replace(self::API_REQUEST_PATH, '', $path));
$resource = GeneralUtility::getFileAbsFileName(self::SCHEMA_PATH . $version . '.json');
if (!is_readable($resource)) {
return false;
}
$schema = json_decode((string)file_get_contents($resource), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($schema) || $schema === []) {
return false;
}
$this->routeConfiguration
->setBase(self::API_REQUEST_PATH . $version)
->setSchema($schema)
->createRouteCollection();
return true;
}
}
<?php
declare(strict_types=1);
namespace T3o\Ter\Rest;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Psr\Container\ContainerInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Routing\RouteResultInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Create a request handler for the resolved route
*/
final class RequestHandlerFactory
{
protected ContainerInterface $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function createRequestHandlerForRouteResult(RouteResultInterface $routeResult)
{
if (!$routeResult->offsetExists('requestHandler')) {
throw new \InvalidArgumentException('No handler to process the request given.', 1600995362);
}
$requestHandlerClassName = (string)$routeResult->offsetGet('requestHandler');
$requestHandler = $this->container->has($requestHandlerClassName)
? $this->container->get($requestHandlerClassName)
: GeneralUtility::makeInstance($requestHandlerClassName);
if (!$requestHandler instanceof RequestHandlerInterface) {
throw new \InvalidArgumentException('No qualified handler to process the request found.', 1600994201);
}
return $requestHandler;
}
}
<?php
declare(strict_types=1);
namespace T3o\Ter\Rest\Response;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Create a response object for the accepted type and the given data
*/
final class ApiResponseFactory
{
private const DEFAULT_RESPONSE_TYPE = 'application/json';
protected $availableResponseTypes;
public function __construct(ExtensionConfiguration $extensionConfiguration)
{
$this->availableResponseTypes = $extensionConfiguration->get('ter', 'routing/responseTypes') ?? [];
}
public function createResponseForRequest(
ServerRequestInterface $request,
$data = null,
int $status = 200,
bool $rateLimit = true
): ResponseInterface {
$response = $this
->initializeResponse($request)
->withStatus($status);
if ($data !== null) {
$this->addDataForResponseType($response, $data);
}
if ($rateLimit) {
$this->addRateLimitHeader($response);
}
return $response;
}
public function createErrorResponseForRequest(
ServerRequestInterface $request,
int $code,
string $message = '',
int $status = 500
): ResponseInterface {
return $this->createResponseForRequest(
$request,
[
'status' => $status,
'code' => $code,
'message' => $message
],
$status,
false
);
}
public function createResponseForType(string $type): ResponseInterface
{
if ($type === '') {
throw new \InvalidArgumentException('Response type cannot be empty', 1601025839);
}
if (!isset($this->availableResponseTypes[$type])) {
throw new \OutOfRangeException(sprintf('No Response handler found for %s', $type), 1601025964);
}
$className = $this->availableResponseTypes[$type];
return GeneralUtility::makeInstance($className);
}
protected function initializeResponse(ServerRequestInterface $request): ResponseInterface
{
$types = $request->getHeader('accept')
?: $request->getHeader('content-type')
?: [self::DEFAULT_RESPONSE_TYPE];
$type = (string)reset($types);
return $this->createResponseForType($type);
}
protected function addDataForResponseType(ResponseInterface $response, $data): void
{
if ($response instanceof JsonResponse && is_array($data)) {
$response->setPayload($data);
}
}
protected function addRateLimitHeader(ResponseInterface $response): void
{
// @todo Implement rate limit functionality.
// @todo Maybe use existing packages like https://github.com/sunspikes/php-ratelimiter
$rateLimitHeader = [
'x-ratelimit-limit' => 100,
'x-ratelimit-remaining' => 99,
'x-ratelimit-reset' => time() + 86400
];
foreach ($rateLimitHeader as $header => $value) {
$response->withHeader($header, (string)$value);
}
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace T3o\Ter\Rest\RouteArgument;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use T3o\Ter\Rest\Validaton\RequiredValidator;
use T3o\Ter\Rest\Validaton\ValidationErrorInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Can be used by specific argument implementations
*/
abstract class AbstractRouteArgument implements RouteArgumentInterface, \JsonSerializable
{
protected string $name;
protected array $configuration;
protected array $validators = [];
protected array $validationErrors = [];
public function __construct(string $name, array $configuration)
{
$this->name = $name;
$this->configuration = $configuration;
if ((bool)($this->configuration['required'] ?? false)) {
$this->validators[RequiredValidator::class] = [];
}
}
public function getName(): string
{
return $this->name;
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function isValid(): bool
{
foreach ($this->validators as $class => $options) {
GeneralUtility::makeInstance($class, ...$options)->validate($this);
}
return $this->validationErrors === [];
}
public function addValidationError(ValidationErrorInterface $validationError): void
{
$this->validationErrors[] = $validationError;
}
public function getValidationErrors(): array
{
return $this->validationErrors;
}
public function jsonSerialize(): array
{
$validationErrorMessages = [];
foreach ($this->validationErrors as $error) {
$validationErrorMessages[] = (string)$error;
}
return [
'name' => $this->getName(),
'value' => $this->getValue(),
'validationErrors' => $validationErrorMessages
];
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace T3o\Ter\Rest\RouteArgument;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
/**
* Interface to be implemented by deepObject (containing sub properties) route arguments.
*/
interface DeepObjectRouteArgumentInterface