Issues (5)

src/Api.php (1 issue)

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