Completed
Push — master ( 2948b6...bd2bef )
by Midori
131:55 queued 130:47
created

Api::setPrefix()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
c 2
b 0
f 0
nc 7
nop 1
dl 0
loc 26
ccs 0
cts 15
cp 0
crap 30
rs 9.4888
1
<?php
2
3
declare(strict_types=1);
4
5
namespace midorikocak\nano;
6
7
use Exception;
8
9
use function array_key_exists;
10
use function array_map;
11
use function array_shift;
12
use function array_values;
13
use function base64_decode;
14
use function count;
15
use function explode;
16
use function header;
17
use function http_response_code;
18
use function is_callable;
19
use function is_string;
20
use function json_encode;
21
use function parse_url;
22
use function preg_match;
23
use function preg_match_all;
24
use function preg_replace;
25
use function strncasecmp;
26
use function strtolower;
27
use function substr;
28
use function trim;
29
30
use const JSON_THROW_ON_ERROR;
31
use const PHP_URL_PATH;
32
33
class Api
34
{
35
    private array $endpoints = [];
36
    private array $wildcards = [];
37
38
    private string $origin = '*';
39
    private int $responseCode = 404;
40
41 12
    public function __construct($origin = '*')
42
    {
43 12
        $this->origin = $origin;
44 12
    }
45
46
    public function setPrefix(string $prefix): void
47
    {
48
        $prefixEndpoints = [];
49
        $prefix = trim($prefix, '/');
50
51
        foreach ($this->endpoints as $methodName => $item) {
52
            if (!isset($prefixEndpoints[$methodName])) {
53
                $prefixEndpoints[$methodName] = [];
54
            }
55
56
            foreach ($item as $key => $value) {
57
                $key = $key === '' ? $key : '/' . $key;
58
                $prefixEndpoints[$methodName][$prefix . $key] = $value;
59
            }
60
        }
61
62
        $this->endpoints = $prefixEndpoints;
63
64
        $prefixWildcards = array_map(
65
            static function ($item) use ($prefix) {
66
                return $prefix . '/' . $item;
67
            },
68
            $this->wildcards
69
        );
70
71
        $this->wildcards = $prefixWildcards;
72
    }
73
74
    private function isOptions(): bool
75
    {
76
        if ((($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') && $this->checkOrigin()) {
77
            header('Access-Control-Max-Age: 1728000');
78
            header('Content-Length: 0');
79
            header('Content-Type: text/plain');
80
            http_response_code(200);
81
            return true;
82
        }
83
84
        header('Access-Control-Max-Age: 3600');
85
        header(
86
            'Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With'
87
        );
88
        return false;
89
    }
90
91
    private function checkOrigin(): bool
92
    {
93
        if ($this->origin !== '*' && !$_SERVER['HTTP_ORIGIN'] === $this->origin) {
0 ignored issues
show
introduced by
The condition ! $_SERVER['HTTP_ORIGIN'] === $this->origin is always false.
Loading history...
94
            header('HTTP/1.1 403 Access Forbidden');
95
            header('Content-Type: text/plain');
96
            return false;
97
        }
98
        return true;
99
    }
100
101 12
    public function __destruct()
102
    {
103 12
        header("Access-Control-Allow-Origin: $this->origin");
104 12
        header('Content-Type: application/json; charset=UTF-8');
105 12
        header('Access-Control-Allow-Methods: OPTIONS, POST, GET, PUT, DELETE');
106 12
        header('Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept');
107
108 12
        $method = strtolower($_SERVER['REQUEST_METHOD'] ?? 'GET');
109 12
        if ($method === 'options') {
110
            $this->isOptions();
111
        } else {
112 12
            $uri = trim(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH), '/');
113
114
            // Ignore uri that starts .php file extension
115
            //$uri = preg_replace('/^(.*?\.php\/{0,1})/', '', $uri);
116 12
            if (!isset($this->endpoints[$method])) {
117 6
                $this->endpoints[$method] = [];
118
            }
119 12
            $compared = $this->compareAgainstWildcards($uri);
120 12
            if (!empty($compared)) {
121
                try {
122
                    if (!array_key_exists($compared['pattern'], $this->endpoints[$method])) {
123
                        throw new Exception('Not found');
124
                    }
125
                    $fn = $this->endpoints[$method][$compared['pattern']];
126
                    $this->responseCode = 200;
127
                    $fn(...$compared['values']);
128
                } catch (Exception $e) {
129
                    echo json_encode($e->getMessage(), JSON_THROW_ON_ERROR, 512);
130
                    $this->responseCode = 404;
131
                }
132 12
            } elseif (isset($this->endpoints[$method][$uri])) {
133
                try {
134 3
                    $fn = $this->endpoints[$method][$uri];
135 3
                    $this->responseCode = 200;
136 3
                    $fn();
137
                } catch (Exception $e) {
138
                    echo json_encode($e->getMessage(), JSON_THROW_ON_ERROR, 512);
139
                    $this->responseCode = 404;
140
                }
141
            }
142
        }
143
144 12
        if ($this->responseCode && ((string) http_response_code() === '200')) {
145 3
            http_response_code($this->responseCode);
146
        }
147 12
    }
148
149 12
    private function compareAgainstWildcards($uri): array
150
    {
151 12
        foreach ($this->wildcards as $wildcard) {
152 6
            $compareUri = $this->compareUri($uri, $wildcard);
153 6
            if (!empty($compareUri)) {
154
                return $compareUri;
155
            }
156
        }
157 12
        return [];
158
    }
159
160 6
    private function compareUri($uri, $pattern): array
161
    {
162
        // does url have brackets?
163 6
        $hasBrackets = preg_match_all('/{(.+)}/', $pattern, $vars);
164 6
        if ($hasBrackets) {
165 3
            $newPattern = preg_replace('/{.+?}/m', '([^/{}]+)', $pattern);
166 3
            $passesNewPattern = preg_match('~^' . $newPattern . '$~', $uri, $values);
167 3
            array_shift($values);
168 3
            if ($passesNewPattern) {
169
                return [
170
                    'pattern' => $pattern,
171
                    'uri' => $uri,
172
                    'vars' => $vars,
173
                    'values' => array_values($values),
174
                ];
175
            }
176
        }
177 6
        return [];
178
    }
179
180 6
    private function hasBrackets($uri): bool
181
    {
182 6
        return preg_match('/{(.*?)}/', $uri) !== false;
183
    }
184
185
    public function getResponseCode(int $code): void
186
    {
187
        $this->responseCode = $code;
188
    }
189
190 6
    public function auth(callable $fn, callable $login)
191
    {
192 6
        if (isset($_SERVER['HTTP_AUTHORIZATION']) && strncasecmp($_SERVER['HTTP_AUTHORIZATION'], 'basic ', 6) === 0) {
193
            $exploded = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)), 2);
194
195
            if (count($exploded) === 2) {
196
                [$un, $pw] = $exploded;
197
            }
198
            if ($login($un ?? '', $pw ?? '')) {
199
                $fn();
200
            } else {
201
                $this->responseCode = 401;
202
            }
203
        }
204 6
    }
205
206 6
    public function __call($name, $arguments)
207
    {
208 6
        if (!isset($this->endpoints[$name])) {
209 6
            $this->endpoints[$name] = [];
210
        }
211
212 6
        if (count($arguments) !== 2) {
213
            return;
214
        }
215
216 6
        if (is_string($arguments[0]) && is_callable($arguments[1])) {
217 6
            $endpoint = parse_url(trim($arguments[0], '/'), PHP_URL_PATH);
218 6
            if ($this->hasBrackets($arguments[0])) {
219 6
                $this->wildcards[] = $endpoint;
220
            }
221 6
            $this->endpoints[$name][$endpoint] = $arguments[1];
222
        }
223 6
    }
224
}
225