Completed
Pull Request — master (#61)
by Tomas
02:14
created

ApiPresenter::checkAuth()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

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