Completed
Pull Request — master (#80)
by Michal
01:39
created

ApiPresenter::run()   C

Complexity

Conditions 11
Paths 82

Size

Total Lines 71

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 12.7663

Importance

Changes 0
Metric Value
dl 0
loc 71
ccs 34
cts 45
cp 0.7556
rs 6.486
c 0
b 0
f 0
cc 11
nc 82
nop 1
crap 12.7663

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\IPresenter;
9
use Nette\Application\IResponse;
10
use Nette\Application\Request;
11
use Nette\Application\Responses\JsonResponse;
12
use Nette\DI\Container;
13
use Nette\Http\Response;
14
use Tomaj\NetteApi\Api;
15
use Tomaj\NetteApi\ApiDecider;
16
use Tomaj\NetteApi\Authorization\ApiAuthorizationInterface;
17
use Tomaj\NetteApi\Logger\ApiLoggerInterface;
18
use Tomaj\NetteApi\Misc\IpDetectorInterface;
19
use Tomaj\NetteApi\Params\ParamsProcessor;
20
use Tomaj\NetteApi\RateLimit\RateLimitInterface;
21
use Tomaj\NetteApi\Response\JsonApiResponse;
22
use Tracy\Debugger;
23
24
class ApiPresenter implements IPresenter
25
{
26
    /** @var ApiDecider @inject */
27
    public $apiDecider;
28
29
    /** @var Response @inject */
30
    public $response;
31
32
    /** @var Container @inject */
33
    public $context;
34
35
    /**
36
     * CORS header settings
37
     *
38
     * Available values:
39
     *   'auto'  - send back header Access-Control-Allow-Origin with domain that made request
40
     *   '*'     - send header with '*' - this will workf fine if you dont need to send cookies via ajax calls to api
41
     *             with jquery $.ajax with xhrFields: { withCredentials: true } settings
42
     *   'off'   - will not send any CORS header
43
     *   other   - any other value will be send in Access-Control-Allow-Origin header
44
     *
45
     * @var string
46
     */
47
    protected $corsHeader = '*';
48
49
    /**
50
     * Set cors header
51
     *
52
     * See description to property $corsHeader for valid inputs
53
     *
54
     * @param string $corsHeader
55
     */
56
    public function setCorsHeader(string $corsHeader): void
57
    {
58
        $this->corsHeader = $corsHeader;
59
    }
60
61 12
    public function run(Request $request): IResponse
62
    {
63 12
        $start = microtime(true);
64
65 12
        $this->sendCorsHeaders();
66
67 12
        $api = $this->getApi($request);
68 12
        $handler = $api->getHandler();
69 12
        $authorization = $api->getAuthorization();
70 12
        $rateLimit = $api->getRateLimit();
71
72 12
        $authResponse = $this->checkAuth($authorization);
73 12
        if ($authResponse !== null) {
74 3
            return $authResponse;
75
        }
76
77 9
        $rateLimitResponse = $this->checkRateLimit($rateLimit);
78 9
        if ($rateLimitResponse !== null) {
79
            return $rateLimitResponse;
80
        }
81
82 9
        $paramsProcessor = new ParamsProcessor($handler->params());
83 9
        if ($paramsProcessor->isError()) {
84 3
            $this->response->setCode(Response::S400_BAD_REQUEST);
85 3
            if (Debugger::isEnabled()) {
86 3
                $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]);
87
            } else {
88 3
                $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']);
89
            }
90 3
            return $response;
91
        }
92 6
        $params = $paramsProcessor->getValues();
93
94
        try {
95 6
            $response = $handler->handle($params);
96 6
            $outputValid = count($handler->outputs()) === 0; // back compatibility for handlers with no outputs defined
97 6
            $outputValidatorErrors = [];
98 6
            foreach ($handler->outputs() as $output) {
99 3
                $validationResult = $output->validate($response);
100 3
                if ($validationResult->isOk()) {
101 3
                    $outputValid = true;
102 3
                    break;
103
                }
104
                $outputValidatorErrors[] = $validationResult->getErrors();
105
            }
106 6
            if (!$outputValid) {
107
                $response = new JsonApiResponse(500, ['status' => 'error', 'message' => 'Internal server error', 'details' => $outputValidatorErrors]);
108
            }
109 6
            $code = $response->getCode();
110
        } catch (Exception $exception) {
111
            if (Debugger::isEnabled()) {
112
                $response = new JsonApiResponse(500, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]);
113
            } else {
114
                $response = new JsonApiResponse(500, ['status' => 'error', 'message' => 'Internal server error']);
115
            }
116
            $code = $response->getCode();
117
            Debugger::log($exception, Debugger::EXCEPTION);
118
        }
119
120 6
        $end = microtime(true);
121
122 6
        if ($this->context->findByType(ApiLoggerInterface::class)) {
123
            /** @var ApiLoggerInterface $apiLogger */
124
            $apiLogger = $this->context->getByType(ApiLoggerInterface::class);
125
            $this->logRequest($request, $apiLogger, $code, $end - $start);
126
        }
127
128
        // output to nette
129 6
        $this->response->setCode($code);
130 6
        return $response;
131
    }
132
133 12
    private function getApi(Request $request): Api
134
    {
135 12
        return $this->apiDecider->getApi(
136 12
            $request->getMethod(),
137 12
            (int) $request->getParameter('version'),
138 12
            $request->getParameter('package'),
139 12
            $request->getParameter('apiAction')
140
        );
141
    }
142
143 12
    private function checkAuth(ApiAuthorizationInterface $authorization): ?IResponse
144
    {
145 12
        if (!$authorization->authorized()) {
146 3
            $this->response->setCode(Response::S403_FORBIDDEN);
147 3
            return new JsonResponse(['status' => 'error', 'message' => $authorization->getErrorMessage()]);
148
149
        }
150 9
        return null;
151
    }
152
153 9
    private function checkRateLimit(RateLimitInterface $rateLimit): ?IResponse
154
    {
155 9
        $rateLimitResponse = $rateLimit->check();
156 9
        if (!$rateLimitResponse) {
157 9
            return null;
158
        }
159
160
        $limit = $rateLimitResponse->getLimit();
161
        $remaining = $rateLimitResponse->getRemaining();
162
        $retryAfter = $rateLimitResponse->getRetryAfter();
163
164
        $this->response->addHeader('X-RateLimit-Limit', (string)$limit);
165
        $this->response->addHeader('X-RateLimit-Remaining', (string)$remaining);
166
167
        if ($remaining === 0) {
168
            $this->response->setCode(Response::S429_TOO_MANY_REQUESTS);
169
            $this->response->addHeader('Retry-After', (string)$retryAfter);
170
            return $rateLimitResponse->getErrorResponse() ?: new JsonResponse(['status' => 'error', 'message' => 'Too many requests. Retry after ' . $retryAfter . ' seconds.']);
171
        }
172
        return null;
173
    }
174
175
    private function logRequest(Request $request, ApiLoggerInterface $logger, int $code, float $elapsed): void
176
    {
177
        $headers = [];
178
        if (function_exists('getallheaders')) {
179
            $headers = getallheaders();
180
        } else {
181
            foreach ($_SERVER as $name => $value) {
182
                if (substr($name, 0, 5) === 'HTTP_') {
183
                    $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
184
                    $headers[$key] = $value;
185
                }
186
            }
187
        }
188
189
        $requestHeaders = '';
190
        foreach ($headers as $key => $value) {
191
            $requestHeaders .= "$key: $value\n";
192
        }
193
194
        $ipDetector = $this->context->getByType(IpDetectorInterface::class);
195
        $logger->log(
196
            $code,
197
            $request->getMethod(),
198
            $requestHeaders,
199
            (string) filter_input(INPUT_SERVER, 'REQUEST_URI'),
200
            $ipDetector ? $ipDetector->getRequestIp() : '',
201
            (string) filter_input(INPUT_SERVER, 'HTTP_USER_AGENT'),
202
            (int) ($elapsed) * 1000
203
        );
204
    }
205
206 12
    protected function sendCorsHeaders(): void
207
    {
208 12
        $this->response->addHeader('Access-Control-Allow-Methods', 'POST, DELETE, PUT, GET, OPTIONS');
209
210 12
        if ($this->corsHeader === 'auto') {
211
            $domain = $this->getRequestDomain();
212
            if ($domain !== null) {
213
                $this->response->addHeader('Access-Control-Allow-Origin', $domain);
214
                $this->response->addHeader('Access-Control-Allow-Credentials', 'true');
215
            }
216
            return;
217
        }
218
219 12
        if ($this->corsHeader === '*') {
220 12
            $this->response->addHeader('Access-Control-Allow-Origin', '*');
221 12
            return;
222
        }
223
224
        if ($this->corsHeader !== 'off') {
225
            $this->response->addHeader('Access-Control-Allow-Origin', $this->corsHeader);
226
        }
227
    }
228
229
    private function getRequestDomain(): ?string
230
    {
231
        if (!filter_input(INPUT_SERVER, 'HTTP_REFERER')) {
232
            return null;
233
        }
234
        $refererParsedUrl = parse_url(filter_input(INPUT_SERVER, 'HTTP_REFERER'));
235
        if (!(isset($refererParsedUrl['scheme']) && isset($refererParsedUrl['host']))) {
236
            return null;
237
        }
238
        $url = $refererParsedUrl['scheme'] . '://' . $refererParsedUrl['host'];
239
        if (isset($refererParsedUrl['port']) && $refererParsedUrl['port'] !== 80) {
240
            $url .= ':' . $refererParsedUrl['port'];
241
        }
242
        return $url;
243
    }
244
}
245