Completed
Pull Request — master (#61)
by Michal
02:15
created

ApiPresenter::renderDefault()   B

Complexity

Conditions 9
Paths 80

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 12.8138

Importance

Changes 0
Metric Value
dl 0
loc 57
c 0
b 0
f 0
ccs 23
cts 36
cp 0.6389
rs 7.3826
cc 9
nc 80
nop 0
crap 12.8138

How to fix   Long Method   

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