Completed
Push — master ( 847dc3...fbf566 )
by Tomas
16s queued 12s
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
final 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 9
        return null;
150
    }
151
152 9
    private function checkRateLimit(RateLimitInterface $rateLimit): ?IResponse
153
    {
154 9
        $rateLimitResponse = $rateLimit->check();
155 9
        if (!$rateLimitResponse) {
156 9
            return null;
157
        }
158
159
        $limit = $rateLimitResponse->getLimit();
160
        $remaining = $rateLimitResponse->getRemaining();
161
        $retryAfter = $rateLimitResponse->getRetryAfter();
162
163
        $this->response->addHeader('X-RateLimit-Limit', (string)$limit);
164
        $this->response->addHeader('X-RateLimit-Remaining', (string)$remaining);
165
166
        if ($remaining === 0) {
167
            $this->response->setCode(Response::S429_TOO_MANY_REQUESTS);
168
            $this->response->addHeader('Retry-After', (string)$retryAfter);
169
            return $rateLimitResponse->getErrorResponse() ?: new JsonResponse(['status' => 'error', 'message' => 'Too many requests. Retry after ' . $retryAfter . ' seconds.']);
170
        }
171
        return null;
172
    }
173
174
    private function logRequest(Request $request, ApiLoggerInterface $logger, int $code, float $elapsed): void
175
    {
176
        $headers = [];
177
        if (function_exists('getallheaders')) {
178
            $headers = getallheaders();
179
        } else {
180
            foreach ($_SERVER as $name => $value) {
181
                if (substr($name, 0, 5) === 'HTTP_') {
182
                    $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
183
                    $headers[$key] = $value;
184
                }
185
            }
186
        }
187
188
        $requestHeaders = '';
189
        foreach ($headers as $key => $value) {
190
            $requestHeaders .= "$key: $value\n";
191
        }
192
193
        $ipDetector = $this->context->getByType(IpDetectorInterface::class);
194
        $logger->log(
195
            $code,
196
            $request->getMethod(),
197
            $requestHeaders,
198
            (string) filter_input(INPUT_SERVER, 'REQUEST_URI'),
199
            $ipDetector ? $ipDetector->getRequestIp() : '',
200
            (string) filter_input(INPUT_SERVER, 'HTTP_USER_AGENT'),
201
            (int) ($elapsed) * 1000
202
        );
203
    }
204
205 12
    protected function sendCorsHeaders(): void
206
    {
207 12
        $this->response->addHeader('Access-Control-Allow-Methods', 'POST, DELETE, PUT, GET, OPTIONS');
208
209 12
        if ($this->corsHeader === 'auto') {
210
            $domain = $this->getRequestDomain();
211
            if ($domain !== null) {
212
                $this->response->addHeader('Access-Control-Allow-Origin', $domain);
213
                $this->response->addHeader('Access-Control-Allow-Credentials', 'true');
214
            }
215
            return;
216
        }
217
218 12
        if ($this->corsHeader === '*') {
219 12
            $this->response->addHeader('Access-Control-Allow-Origin', '*');
220 12
            return;
221
        }
222
223
        if ($this->corsHeader !== 'off') {
224
            $this->response->addHeader('Access-Control-Allow-Origin', $this->corsHeader);
225
        }
226
    }
227
228
    private function getRequestDomain(): ?string
229
    {
230
        if (!filter_input(INPUT_SERVER, 'HTTP_REFERER')) {
231
            return null;
232
        }
233
        $refererParsedUrl = parse_url(filter_input(INPUT_SERVER, 'HTTP_REFERER'));
234
        if (!(isset($refererParsedUrl['scheme']) && isset($refererParsedUrl['host']))) {
235
            return null;
236
        }
237
        $url = $refererParsedUrl['scheme'] . '://' . $refererParsedUrl['host'];
238
        if (isset($refererParsedUrl['port']) && $refererParsedUrl['port'] !== 80) {
239
            $url .= ':' . $refererParsedUrl['port'];
240
        }
241
        return $url;
242
    }
243
}
244