Completed
Push — master ( 98e0f8...788820 )
by Tomas
29s queued 12s
created

ApiPresenter::renderDefault()   C

Complexity

Conditions 10
Paths 81

Size

Total Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 14.6268

Importance

Changes 0
Metric Value
dl 0
loc 62
ccs 25
cts 39
cp 0.641
rs 6.9624
c 0
b 0
f 0
cc 10
nc 81
nop 0
crap 14.6268

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Tomaj\NetteApi\Presenters;
6
7
use Exception;
8
use Nette\Application\Responses\JsonResponse;
9
use Nette\Application\UI\Presenter;
10
use Nette\DI\Container;
11
use Nette\Http\Response;
12
use Tomaj\NetteApi\ApiDecider;
13
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
14
use Tomaj\NetteApi\Handlers\ApiHandlerInterface;
15
use Tomaj\NetteApi\Api;
16
use Tomaj\NetteApi\Logger\ApiLoggerInterface;
17
use Tomaj\NetteApi\Misc\IpDetectorInterface;
18
use Tomaj\NetteApi\Params\ParamsProcessor;
19
use Tomaj\NetteApi\RateLimit\RateLimitInterface;
20
use Tomaj\NetteApi\Response\JsonApiResponse;
21
use Tracy\Debugger;
22
23
/**
24
 * @property-read Container $context
25
 */
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
46
    {
47 12
        parent::startup();
48 12
        $this->autoCanonicalize = false;
49 12
    }
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
59
    {
60
        $this->corsHeader = $corsHeader;
61
    }
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
127
    {
128 12
        return $this->apiDecider->getApi(
129 12
            $this->getRequest()->getMethod(),
130 12
            (int) $this->params['version'],
131 12
            $this->params['package'],
132 12
            $this->params['apiAction']
133
        );
134
    }
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
187
    {
188
        $headers = [];
189
        if (function_exists('getallheaders')) {
190
            $headers = getallheaders();
191
        } else {
192
            foreach ($_SERVER as $name => $value) {
193
                if (substr($name, 0, 5) === 'HTTP_') {
194
                    $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
195
                    $headers[$key] = $value;
196
                }
197
            }
198
        }
199
200
        $requestHeaders = '';
201
        foreach ($headers as $key => $value) {
202
            $requestHeaders .= "$key: $value\n";
203
        }
204
205
        $ipDetector = $this->context->getByType(IpDetectorInterface::class);
206
        $logger->log(
207
            $code,
208
            $this->getRequest()->getMethod(),
209
            $requestHeaders,
210
            (string) filter_input(INPUT_SERVER, 'REQUEST_URI'),
211
            $ipDetector ? $ipDetector->getRequestIp() : '',
212
            (string) filter_input(INPUT_SERVER, 'HTTP_USER_AGENT'),
213
            (int) ($elapsed) * 1000
214
        );
215
    }
216
217 12
    protected function sendCorsHeaders(): void
218
    {
219 12
        $this->getHttpResponse()->addHeader('Access-Control-Allow-Methods', 'POST, DELETE, PUT, GET, OPTIONS');
220
221 12
        if ($this->corsHeader === 'auto') {
222
            $domain = $this->getRequestDomain();
223
            if ($domain !== null) {
224
                $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', $domain);
225
                $this->getHttpResponse()->addHeader('Access-Control-Allow-Credentials', 'true');
226
            }
227
            return;
228
        }
229
230 12
        if ($this->corsHeader === '*') {
231 12
            $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', '*');
232 12
            return;
233
        }
234
235
        if ($this->corsHeader !== 'off') {
236
            $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', $this->corsHeader);
237
        }
238
    }
239
240
    private function getRequestDomain(): ?string
241
    {
242
        if (!filter_input(INPUT_SERVER, 'HTTP_REFERER')) {
243
            return null;
244
        }
245
        $refererParsedUrl = parse_url(filter_input(INPUT_SERVER, 'HTTP_REFERER'));
246
        if (!(isset($refererParsedUrl['scheme']) && isset($refererParsedUrl['host']))) {
247
            return null;
248
        }
249
        $url = $refererParsedUrl['scheme'] . '://' . $refererParsedUrl['host'];
250
        if (isset($refererParsedUrl['port']) && $refererParsedUrl['port'] !== 80) {
251
            $url .= ':' . $refererParsedUrl['port'];
252
        }
253
        return $url;
254
    }
255
}
256