Completed
Pull Request — master (#61)
by Michal
04:05
created

ApiPresenter::getApi()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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 9
    public function startup(): void
43
    {
44 9
        parent::startup();
45 9
        $this->autoCanonicalize = false;
46 9
    }
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 9
    public function renderDefault(): void
61
    {
62 9
        $start = microtime(true);
63
64 9
        $this->sendCorsHeaders();
65
66 9
        $api = $this->getApi();
67 9
        $handler = $api->getHandler();
68 9
        $authorization = $api->getAuthorization();
69
70 9
        if ($this->checkAuth($authorization) === false) {
71
            return;
72
        }
73
74 6
        $params = $this->processInputParams($handler);
75 3
        if ($params === null) {
76
            return;
77
        }
78
79
        try {
80 3
            $response = $handler->handle($params);
81 3
            $code = $response->getCode();
82
        } catch (Exception $exception) {
83
            if (Debugger::isEnabled()) {
84
                $response = new JsonApiResponse(500, ['status' => 'error', 'message' => 'Internal server error', 'detail' => $exception->getMessage()]);
85
            } else {
86
                $response = new JsonApiResponse(500, ['status' => 'error', 'message' => 'Internal server error']);
87
            }
88
            $code = $response->getCode();
89
            Debugger::log($exception, Debugger::EXCEPTION);
90
        }
91
92 3
        $end = microtime(true);
93
94 3
        if ($this->context->findByType(ApiLoggerInterface::class)) {
95
            /** @var ApiLoggerInterface $apiLogger */
96
            $apiLogger = $this->context->getByType(ApiLoggerInterface::class);
97
            $this->logRequest($apiLogger, $code, $end - $start);
98
        }
99
100
        // output to nette
101 3
        $this->getHttpResponse()->setCode($code);
102 3
        $this->sendResponse($response);
103
    }
104
105 9
    private function getApi(): Api
106
    {
107 9
        return $this->apiDecider->getApi(
108 9
            $this->getRequest()->getMethod(),
109 9
            $this->params['version'],
110 9
            $this->params['package'],
111 9
            $this->params['apiAction']
112
        );
113
    }
114
115 9
    private function checkAuth(ApiAuthorizationInterface $authorization): bool
116
    {
117 9
        if (!$authorization->authorized()) {
118 3
            $this->getHttpResponse()->setCode(Response::S403_FORBIDDEN);
119 3
            $this->sendResponse(new JsonResponse(['status' => 'error', 'message' => $authorization->getErrorMessage()]));
120
            return false;
121
        }
122 6
        return true;
123
    }
124
125 6
    private function processInputParams(ApiHandlerInterface $handler): ?array
126
    {
127 6
        $paramsProcessor = new ParamsProcessor($handler->params());
128 6
        if ($paramsProcessor->isError()) {
129 3
            $this->getHttpResponse()->setCode(Response::S400_BAD_REQUEST);
130 3
            if (Debugger::isEnabled()) {
131
                $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input', 'detail' => $paramsProcessor->getErrors()]);
132
            } else {
133 3
                $response = new JsonResponse(['status' => 'error', 'message' => 'wrong input']);
134
            }
135 3
            $this->sendResponse($response);
136
            return null;
137
        }
138 3
        return $paramsProcessor->getValues();
139
    }
140
141
    private function logRequest(ApiLoggerInterface $logger, int $code, float $elapsed): void
142
    {
143
        $headers = [];
144
        if (function_exists('getallheaders')) {
145
            $headers = getallheaders();
146
        } else {
147
            foreach ($_SERVER as $name => $value) {
148
                if (substr($name, 0, 5) === 'HTTP_') {
149
                    $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
150
                    $headers[$key] = $value;
151
                }
152
            }
153
        }
154
155
        $requestHeaders = '';
156
        foreach ($headers as $key => $value) {
157
            $requestHeaders .= "$key: $value\n";
158
        }
159
160
        $ipDetector = $this->context->getByType(IpDetectorInterface::class);
161
        $logger->log(
162
            $code,
163
            $this->getRequest()->getMethod(),
164
            $requestHeaders,
165
            filter_input(INPUT_SERVER, 'REQUEST_URI'),
166
            $ipDetector->getRequestIp(),
167
            filter_input(INPUT_SERVER, 'HTTP_USER_AGENT'),
168
            (int) ($elapsed) * 1000
169
        );
170
    }
171
172 9
    protected function sendCorsHeaders(): void
173
    {
174 9
        $this->getHttpResponse()->addHeader('Access-Control-Allow-Methods', 'POST, DELETE, PUT, GET, OPTIONS');
175
176 9
        if ($this->corsHeader === 'auto') {
177
            $domain = $this->getRequestDomain();
178
            if ($domain !== null) {
179
                $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', $domain);
180
                $this->getHttpResponse()->addHeader('Access-Control-Allow-Credentials', 'true');
181
            }
182
            return;
183
        }
184
185 9
        if ($this->corsHeader === '*') {
186 9
            $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', '*');
187 9
            return;
188
        }
189
190
        if ($this->corsHeader !== 'off') {
191
            $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', $this->corsHeader);
192
        }
193
    }
194
195
    private function getRequestDomain(): ?string
196
    {
197
        if (!filter_input(INPUT_SERVER, 'HTTP_REFERER')) {
198
            return null;
199
        }
200
        $refererParsedUrl = parse_url(filter_input(INPUT_SERVER, 'HTTP_REFERER'));
201
        if (!(isset($refererParsedUrl['scheme']) && isset($refererParsedUrl['host']))) {
202
            return null;
203
        }
204
        $url = $refererParsedUrl['scheme'] . '://' . $refererParsedUrl['host'];
205
        if (isset($refererParsedUrl['port']) && $refererParsedUrl['port'] !== 80) {
206
            $url .= ':' . $refererParsedUrl['port'];
207
        }
208
        return $url;
209
    }
210
}
211