Skip to content

Commit

Permalink
Merge pull request #80 from tomaj/rewritten-api-presenter
Browse files Browse the repository at this point in the history
Rewritten API presenter
  • Loading branch information
tomaj authored May 12, 2020
2 parents 847dc35 + c83095c commit fbf5661
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 73 deletions.
124 changes: 56 additions & 68 deletions src/Presenters/ApiPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,33 @@
namespace Tomaj\NetteApi\Presenters;

use Exception;
use Nette\Application\IPresenter;
use Nette\Application\IResponse;
use Nette\Application\Request;
use Nette\Application\Responses\JsonResponse;
use Nette\Application\UI\Presenter;
use Nette\DI\Container;
use Nette\Http\Response;
use Tomaj\NetteApi\Api;
use Tomaj\NetteApi\ApiDecider;
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
use Tomaj\NetteApi\Handlers\ApiHandlerInterface;
use Tomaj\NetteApi\Api;
use Tomaj\NetteApi\Logger\ApiLoggerInterface;
use Tomaj\NetteApi\Misc\IpDetectorInterface;
use Tomaj\NetteApi\Params\ParamsProcessor;
use Tomaj\NetteApi\RateLimit\RateLimitInterface;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tracy\Debugger;

/**
* @property-read Container $context
*/
class ApiPresenter extends Presenter
final class ApiPresenter implements IPresenter
{
/** @var ApiDecider @inject */
public $apiDecider;

/** @var Response @inject */
public $response;

/** @var Container @inject */
public $context;

/**
* CORS header settings
*
Expand All @@ -42,12 +46,6 @@ class ApiPresenter extends Presenter
*/
protected $corsHeader = '*';

public function startup(): void
{
parent::startup();
$this->autoCanonicalize = false;
}

/**
* Set cors header
*
Expand All @@ -60,29 +58,38 @@ public function setCorsHeader(string $corsHeader): void
$this->corsHeader = $corsHeader;
}

public function renderDefault(): void
public function run(Request $request): IResponse
{
$start = microtime(true);

$this->sendCorsHeaders();

$api = $this->getApi();
$api = $this->getApi($request);
$handler = $api->getHandler();
$authorization = $api->getAuthorization();
$rateLimit = $api->getRateLimit();

if ($this->checkAuth($authorization) === false) {
return;
$authResponse = $this->checkAuth($authorization);
if ($authResponse !== null) {
return $authResponse;
}

if ($this->checkRateLimit($rateLimit) === false) {
return;
$rateLimitResponse = $this->checkRateLimit($rateLimit);
if ($rateLimitResponse !== null) {
return $rateLimitResponse;
}

$params = $this->processInputParams($handler);
if ($params === null) {
return;
$paramsProcessor = new ParamsProcessor($handler->params());
if ($paramsProcessor->isError()) {
$this->response->setCode(Response::S400_BAD_REQUEST);
if (Debugger::isEnabled()) {
$response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]);
} else {
$response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']);
}
return $response;
}
$params = $paramsProcessor->getValues();

try {
$response = $handler->handle($params);
Expand Down Expand Up @@ -115,75 +122,56 @@ public function renderDefault(): void
if ($this->context->findByType(ApiLoggerInterface::class)) {
/** @var ApiLoggerInterface $apiLogger */
$apiLogger = $this->context->getByType(ApiLoggerInterface::class);
$this->logRequest($apiLogger, $code, $end - $start);
$this->logRequest($request, $apiLogger, $code, $end - $start);
}

// output to nette
$this->getHttpResponse()->setCode($code);
$this->sendResponse($response);
$this->response->setCode($code);
return $response;
}

private function getApi(): Api
private function getApi(Request $request): Api
{
return $this->apiDecider->getApi(
$this->getRequest()->getMethod(),
(int) $this->params['version'],
$this->params['package'],
$this->params['apiAction']
$request->getMethod(),
(int) $request->getParameter('version'),
$request->getParameter('package'),
$request->getParameter('apiAction')
);
}

private function checkAuth(ApiAuthorizationInterface $authorization): bool
private function checkAuth(ApiAuthorizationInterface $authorization): ?IResponse
{
if (!$authorization->authorized()) {
$this->getHttpResponse()->setCode(Response::S403_FORBIDDEN);
$this->sendResponse(new JsonResponse(['status' => 'error', 'message' => $authorization->getErrorMessage()]));
return false;
$this->response->setCode(Response::S403_FORBIDDEN);
return new JsonResponse(['status' => 'error', 'message' => $authorization->getErrorMessage()]);
}
return true;
return null;
}

private function checkRateLimit(RateLimitInterface $rateLimit): bool
private function checkRateLimit(RateLimitInterface $rateLimit): ?IResponse
{
$rateLimitResponse = $rateLimit->check();
if (!$rateLimitResponse) {
return true;
return null;
}

$limit = $rateLimitResponse->getLimit();
$remaining = $rateLimitResponse->getRemaining();
$retryAfter = $rateLimitResponse->getRetryAfter();

$this->getHttpResponse()->addHeader('X-RateLimit-Limit', (string)$limit);
$this->getHttpResponse()->addHeader('X-RateLimit-Remaining', (string)$remaining);
$this->response->addHeader('X-RateLimit-Limit', (string)$limit);
$this->response->addHeader('X-RateLimit-Remaining', (string)$remaining);

if ($remaining === 0) {
$this->getHttpResponse()->setCode(Response::S429_TOO_MANY_REQUESTS);
$this->getHttpResponse()->addHeader('Retry-After', (string)$retryAfter);
$response = $rateLimitResponse->getErrorResponse() ?: new JsonResponse(['status' => 'error', 'message' => 'Too many requests. Retry after ' . $retryAfter . ' seconds.']);
$this->sendResponse($response);
return false;
}
return true;
}

private function processInputParams(ApiHandlerInterface $handler): ?array
{
$paramsProcessor = new ParamsProcessor($handler->params());
if ($paramsProcessor->isError()) {
$this->getHttpResponse()->setCode(Response::S400_BAD_REQUEST);
if (Debugger::isEnabled()) {
$response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]);
} else {
$response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']);
}
$this->sendResponse($response);
return null;
$this->response->setCode(Response::S429_TOO_MANY_REQUESTS);
$this->response->addHeader('Retry-After', (string)$retryAfter);
return $rateLimitResponse->getErrorResponse() ?: new JsonResponse(['status' => 'error', 'message' => 'Too many requests. Retry after ' . $retryAfter . ' seconds.']);
}
return $paramsProcessor->getValues();
return null;
}

private function logRequest(ApiLoggerInterface $logger, int $code, float $elapsed): void
private function logRequest(Request $request, ApiLoggerInterface $logger, int $code, float $elapsed): void
{
$headers = [];
if (function_exists('getallheaders')) {
Expand All @@ -205,7 +193,7 @@ private function logRequest(ApiLoggerInterface $logger, int $code, float $elapse
$ipDetector = $this->context->getByType(IpDetectorInterface::class);
$logger->log(
$code,
$this->getRequest()->getMethod(),
$request->getMethod(),
$requestHeaders,
(string) filter_input(INPUT_SERVER, 'REQUEST_URI'),
$ipDetector ? $ipDetector->getRequestIp() : '',
Expand All @@ -216,24 +204,24 @@ private function logRequest(ApiLoggerInterface $logger, int $code, float $elapse

protected function sendCorsHeaders(): void
{
$this->getHttpResponse()->addHeader('Access-Control-Allow-Methods', 'POST, DELETE, PUT, GET, OPTIONS');
$this->response->addHeader('Access-Control-Allow-Methods', 'POST, DELETE, PUT, GET, OPTIONS');

if ($this->corsHeader === 'auto') {
$domain = $this->getRequestDomain();
if ($domain !== null) {
$this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', $domain);
$this->getHttpResponse()->addHeader('Access-Control-Allow-Credentials', 'true');
$this->response->addHeader('Access-Control-Allow-Origin', $domain);
$this->response->addHeader('Access-Control-Allow-Credentials', 'true');
}
return;
}

if ($this->corsHeader === '*') {
$this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', '*');
$this->response->addHeader('Access-Control-Allow-Origin', '*');
return;
}

if ($this->corsHeader !== 'off') {
$this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', $this->corsHeader);
$this->response->addHeader('Access-Control-Allow-Origin', $this->corsHeader);
}
}

Expand Down
14 changes: 9 additions & 5 deletions tests/Presenters/ApiPresenterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public function testSimpleResponse()

$presenter = new ApiPresenter();
$presenter->apiDecider = $apiDecider;
$presenter->injectPrimary(new Container(), null, null, new HttpRequest(new UrlScript()), new HttpResponse());
$presenter->response = new HttpResponse();
$presenter->context = new Container();

$request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']);
$result = $presenter->run($request);
Expand All @@ -53,7 +54,8 @@ public function testWithAuthorization()

$presenter = new ApiPresenter();
$presenter->apiDecider = $apiDecider;
$presenter->injectPrimary(new Container(), null, null, new HttpRequest(new UrlScript()), new HttpResponse());
$presenter->response = new HttpResponse();
$presenter->context = new Container();

$request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']);
$result = $presenter->run($request);
Expand All @@ -73,11 +75,12 @@ public function testWithParams()

$presenter = new ApiPresenter();
$presenter->apiDecider = $apiDecider;
$presenter->injectPrimary(new Container(), null, null, new HttpRequest(new UrlScript()), new HttpResponse());
$presenter->response = new HttpResponse();
$presenter->context = new Container();

$request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']);
$result = $presenter->run($request);

$this->assertEquals(['status' => 'error', 'message' => 'wrong input'], $result->getPayload());
$this->assertEquals('application/json', $result->getContentType());

Expand All @@ -99,7 +102,8 @@ public function testWithOutputs()

$presenter = new ApiPresenter();
$presenter->apiDecider = $apiDecider;
$presenter->injectPrimary(new Container(), null, null, new HttpRequest(new UrlScript()), new HttpResponse());
$presenter->response = new HttpResponse();
$presenter->context = new Container();

$request = new Request('Api:Api:default', 'GET', ['version' => 1, 'package' => 'test', 'apiAction' => 'api']);
$result = $presenter->run($request);
Expand Down

0 comments on commit fbf5661

Please sign in to comment.