1 | <?php |
||
26 | class ApiPresenter extends Presenter |
||
27 | { |
||
28 | /** @var ApiDecider @inject */ |
||
29 | public $apiDecider; |
||
30 | |||
31 | /** |
||
32 | * CORS header settings |
||
33 | * |
||
34 | * Available values: |
||
35 | * 'auto' - send back header Access-Control-Allow-Origin with domain that made request |
||
36 | * '*' - send header with '*' - this will workf fine if you dont need to send cookies via ajax calls to api |
||
37 | * with jquery $.ajax with xhrFields: { withCredentials: true } settings |
||
38 | * 'off' - will not send any CORS header |
||
39 | * other - any other value will be send in Access-Control-Allow-Origin header |
||
40 | * |
||
41 | * @var string |
||
42 | */ |
||
43 | protected $corsHeader = '*'; |
||
44 | |||
45 | 12 | public function startup(): void |
|
50 | |||
51 | /** |
||
52 | * Set cors header |
||
53 | * |
||
54 | * See description to property $corsHeader for valid inputs |
||
55 | * |
||
56 | * @param string $corsHeader |
||
57 | */ |
||
58 | public function setCorsHeader(string $corsHeader): void |
||
62 | |||
63 | 12 | public function renderDefault(): void |
|
64 | { |
||
65 | 12 | $start = microtime(true); |
|
66 | |||
67 | 12 | $this->sendCorsHeaders(); |
|
68 | |||
69 | 12 | $api = $this->getApi(); |
|
70 | 12 | $handler = $api->getHandler(); |
|
71 | 12 | $authorization = $api->getAuthorization(); |
|
72 | 12 | $rateLimit = $api->getRateLimit(); |
|
73 | |||
74 | 12 | if ($this->checkAuth($authorization) === false) { |
|
75 | return; |
||
76 | } |
||
77 | |||
78 | 9 | if ($this->checkRateLimit($rateLimit) === false) { |
|
79 | return; |
||
80 | } |
||
81 | |||
82 | 9 | $params = $this->processInputParams($handler); |
|
83 | 6 | if ($params === null) { |
|
84 | return; |
||
85 | } |
||
86 | |||
87 | try { |
||
88 | 6 | $response = $handler->handle($params); |
|
89 | 6 | $outputValid = count($handler->outputs()) === 0; // back compatibility for handlers with no outputs defined |
|
90 | 6 | $outputValidatorErrors = []; |
|
91 | 6 | foreach ($handler->outputs() as $output) { |
|
92 | 3 | $validationResult = $output->validate($response); |
|
93 | 3 | if ($validationResult->isOk()) { |
|
94 | 3 | $outputValid = true; |
|
95 | 3 | break; |
|
96 | } |
||
97 | $outputValidatorErrors[] = $validationResult->getErrors(); |
||
98 | } |
||
99 | 6 | if (!$outputValid) { |
|
100 | $response = new JsonApiResponse(500, ['status' => 'error', 'message' => 'Internal server error', 'details' => $outputValidatorErrors]); |
||
101 | } |
||
102 | 6 | $code = $response->getCode(); |
|
103 | } catch (Exception $exception) { |
||
104 | if (Debugger::isEnabled()) { |
||
105 | $response = new JsonApiResponse(500, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]); |
||
106 | } else { |
||
107 | $response = new JsonApiResponse(500, ['status' => 'error', 'message' => 'Internal server error']); |
||
108 | } |
||
109 | $code = $response->getCode(); |
||
110 | Debugger::log($exception, Debugger::EXCEPTION); |
||
111 | } |
||
112 | |||
113 | 6 | $end = microtime(true); |
|
114 | |||
115 | 6 | if ($this->context->findByType(ApiLoggerInterface::class)) { |
|
116 | /** @var ApiLoggerInterface $apiLogger */ |
||
117 | $apiLogger = $this->context->getByType(ApiLoggerInterface::class); |
||
118 | $this->logRequest($apiLogger, $code, $end - $start); |
||
119 | } |
||
120 | |||
121 | // output to nette |
||
122 | 6 | $this->getHttpResponse()->setCode($code); |
|
123 | 6 | $this->sendResponse($response); |
|
124 | } |
||
125 | |||
126 | 12 | private function getApi(): Api |
|
135 | |||
136 | 12 | private function checkAuth(ApiAuthorizationInterface $authorization): bool |
|
137 | { |
||
138 | 12 | if (!$authorization->authorized()) { |
|
139 | 3 | $this->getHttpResponse()->setCode(Response::S403_FORBIDDEN); |
|
140 | 3 | $this->sendResponse(new JsonResponse(['status' => 'error', 'message' => $authorization->getErrorMessage()])); |
|
141 | return false; |
||
142 | } |
||
143 | 9 | return true; |
|
144 | } |
||
145 | |||
146 | 9 | private function checkRateLimit(RateLimitInterface $rateLimit): bool |
|
147 | { |
||
148 | 9 | $rateLimitResponse = $rateLimit->check(); |
|
149 | 9 | if (!$rateLimitResponse) { |
|
150 | 9 | return true; |
|
151 | } |
||
152 | |||
153 | $limit = $rateLimitResponse->getLimit(); |
||
154 | $remaining = $rateLimitResponse->getRemaining(); |
||
155 | $retryAfter = $rateLimitResponse->getRetryAfter(); |
||
156 | |||
157 | $this->getHttpResponse()->addHeader('X-RateLimit-Limit', (string)$limit); |
||
158 | $this->getHttpResponse()->addHeader('X-RateLimit-Remaining', (string)$remaining); |
||
159 | |||
160 | if ($remaining === 0) { |
||
161 | $this->getHttpResponse()->setCode(Response::S429_TOO_MANY_REQUESTS); |
||
162 | $this->getHttpResponse()->addHeader('Retry-After', (string)$retryAfter); |
||
163 | $response = $rateLimitResponse->getErrorResponse() ?: new JsonResponse(['status' => 'error', 'message' => 'Too many requests. Retry after ' . $retryAfter . ' seconds.']); |
||
164 | $this->sendResponse($response); |
||
165 | return false; |
||
166 | } |
||
167 | return true; |
||
168 | } |
||
169 | |||
170 | 9 | private function processInputParams(ApiHandlerInterface $handler): ?array |
|
171 | { |
||
172 | 9 | $paramsProcessor = new ParamsProcessor($handler->params()); |
|
173 | 9 | if ($paramsProcessor->isError()) { |
|
174 | 3 | $this->getHttpResponse()->setCode(Response::S400_BAD_REQUEST); |
|
175 | 3 | if (Debugger::isEnabled()) { |
|
176 | 3 | $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]); |
|
177 | } else { |
||
178 | 3 | $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']); |
|
179 | } |
||
180 | 3 | $this->sendResponse($response); |
|
181 | return null; |
||
182 | } |
||
183 | 6 | return $paramsProcessor->getValues(); |
|
184 | } |
||
185 | |||
186 | private function logRequest(ApiLoggerInterface $logger, int $code, float $elapsed): void |
||
216 | |||
217 | 12 | protected function sendCorsHeaders(): void |
|
239 | |||
240 | private function getRequestDomain(): ?string |
||
255 | } |
||
256 |