Completed
Pull Request — master (#61)
by Tomas
03:17
created

ApiPresenter   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 183
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 43.3%

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 8
dl 0
loc 183
ccs 42
cts 97
cp 0.433
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A startup() 0 5 1
A setCorsHeader() 0 4 1
B renderDefault() 0 44 6
A getApi() 0 9 1
A checkAuth() 0 9 2
A processInputParams() 0 10 2
A logRequest() 0 30 5
A sendCorsHeaders() 0 22 5
A getRequestDomain() 0 15 6
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::S500_INTERNAL_SERVER_ERROR);
130 3
            $this->sendResponse(new JsonResponse(['status' => 'error', 'message' => 'wrong input']));
131
            return null;
132
        }
133 3
        return $paramsProcessor->getValues();
134
    }
135
136
    private function logRequest(ApiLoggerInterface $logger, int $code, float $elapsed): void
137
    {
138
        $headers = [];
139
        if (function_exists('getallheaders')) {
140
            $headers = getallheaders();
141
        } else {
142
            foreach ($_SERVER as $name => $value) {
143
                if (substr($name, 0, 5) == 'HTTP_') {
144
                    $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
145
                    $headers[$key] = $value;
146
                }
147
            }
148
        }
149
150
        $requestHeaders = '';
151
        foreach ($headers as $key => $value) {
152
            $requestHeaders .= "$key: $value\n";
153
        }
154
155
        $ipDetector = $this->context->getByType(IpDetectorInterface::class);
156
        $logger->log(
157
            $code,
158
            $this->getRequest()->getMethod(),
159
            $requestHeaders,
160
            filter_input(INPUT_SERVER, 'REQUEST_URI'),
161
            $ipDetector->getRequestIp(),
162
            filter_input(INPUT_SERVER, 'HTTP_USER_AGENT'),
163
            (int) ($elapsed) * 1000
164
        );
165
    }
166
167 9
    protected function sendCorsHeaders(): void
168
    {
169 9
        $this->getHttpResponse()->addHeader('Access-Control-Allow-Methods', 'POST, DELETE, PUT, GET, OPTIONS');
170
171 9
        if ($this->corsHeader == 'auto') {
172
            $domain = $this->getRequestDomain();
173
            if ($domain !== null) {
174
                $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', $domain);
175
                $this->getHttpResponse()->addHeader('Access-Control-Allow-Credentials', 'true');
176
            }
177
            return;
178
        }
179
180 9
        if ($this->corsHeader == '*') {
181 9
            $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', '*');
182 9
            return;
183
        }
184
185
        if ($this->corsHeader != 'off') {
186
            $this->getHttpResponse()->addHeader('Access-Control-Allow-Origin', $this->corsHeader);
187
        }
188
    }
189
190
    private function getRequestDomain(): ?string
191
    {
192
        if (!filter_input(INPUT_SERVER, 'HTTP_REFERER')) {
193
            return null;
194
        }
195
        $refererParsedUrl = parse_url(filter_input(INPUT_SERVER, 'HTTP_REFERER'));
196
        if (!(isset($refererParsedUrl['scheme']) && isset($refererParsedUrl['host']))) {
197
            return null;
198
        }
199
        $url = $refererParsedUrl['scheme'] . '://' . $refererParsedUrl['host'];
200
        if (isset($refererParsedUrl['port']) && $refererParsedUrl['port'] !== 80) {
201
            $url .= ':' . $refererParsedUrl['port'];
202
        }
203
        return $url;
204
    }
205
}
206