Skip to content

Commit 2f8789f

Browse files
committed
init
0 parents  commit 2f8789f

File tree

6 files changed

+325
-0
lines changed

6 files changed

+325
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.idea
2+
composer.lock
3+
vendor

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# ThinkCors
2+
3+
ThinkPHP跨域扩展
4+
5+
## 安装
6+
7+
```
8+
composer require topthink/think-cors
9+
```
10+
11+
## 配置
12+
13+
配置文件位于 `config/cors.php`
14+
15+
```
16+
[
17+
'paths' => ['api/*'],
18+
...
19+
]
20+
```

composer.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "topthink/think-cors",
3+
"description": "The Cors Library For ThinkPHP",
4+
"license": "Apache-2.0",
5+
"authors": [
6+
{
7+
"name": "yunwuxin",
8+
"email": "[email protected]"
9+
}
10+
],
11+
"require": {
12+
"topthink/framework": "^6.0|^8.0"
13+
},
14+
"autoload": {
15+
"psr-4": {
16+
"think\\cors\\": "src"
17+
}
18+
},
19+
"extra": {
20+
"think": {
21+
"services": [
22+
"think\\cors\\Service"
23+
],
24+
"config": {
25+
"cors": "src/config.php"
26+
}
27+
}
28+
}
29+
}

src/HandleCors.php

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
<?php
2+
3+
namespace think\cors;
4+
5+
use Closure;
6+
use think\Config;
7+
use think\Request;
8+
use think\Response;
9+
10+
class HandleCors
11+
{
12+
/** @var string[] */
13+
protected array $paths = [];
14+
/** @var string[] */
15+
protected array $allowedOrigins = [];
16+
/** @var string[] */
17+
protected array $allowedOriginsPatterns = [];
18+
/** @var string[] */
19+
protected array $allowedMethods = [];
20+
/** @var string[] */
21+
protected array $allowedHeaders = [];
22+
/** @var string[] */
23+
private array $exposedHeaders = [];
24+
protected bool $supportsCredentials = false;
25+
protected ?int $maxAge = 0;
26+
27+
protected bool $allowAllOrigins = false;
28+
protected bool $allowAllMethods = false;
29+
protected bool $allowAllHeaders = false;
30+
31+
public function __construct(Config $config)
32+
{
33+
$options = $config->get('cors', []);
34+
35+
$this->paths = $options['paths'] ?? $this->paths;
36+
$this->allowedOrigins = $options['allowed_origins'] ?? $this->allowedOrigins;
37+
$this->allowedOriginsPatterns = $options['allowed_origins_patterns'] ?? $this->allowedOriginsPatterns;
38+
$this->allowedMethods = $options['allowed_methods'] ?? $this->allowedMethods;
39+
$this->allowedHeaders = $options['allowed_headers'] ?? $this->allowedHeaders;
40+
$this->supportsCredentials = $options['supports_credentials'] ?? $this->supportsCredentials;
41+
42+
$maxAge = $this->maxAge;
43+
if (array_key_exists('max_age', $options)) {
44+
$maxAge = $options['max_age'];
45+
}
46+
$this->maxAge = $maxAge === null ? null : (int) $maxAge;
47+
48+
$exposedHeaders = $options['exposed_headers'] ?? $this->exposedHeaders;
49+
$this->exposedHeaders = $exposedHeaders === false ? [] : $exposedHeaders;
50+
51+
// Normalize case
52+
$this->allowedHeaders = array_map('strtolower', $this->allowedHeaders);
53+
$this->allowedMethods = array_map('strtoupper', $this->allowedMethods);
54+
55+
// Normalize ['*'] to true
56+
$this->allowAllOrigins = in_array('*', $this->allowedOrigins);
57+
$this->allowAllHeaders = in_array('*', $this->allowedHeaders);
58+
$this->allowAllMethods = in_array('*', $this->allowedMethods);
59+
60+
// Transform wildcard pattern
61+
if (!$this->allowAllOrigins) {
62+
foreach ($this->allowedOrigins as $origin) {
63+
if (strpos($origin, '*') !== false) {
64+
$this->allowedOriginsPatterns[] = $this->convertWildcardToPattern($origin);
65+
}
66+
}
67+
}
68+
}
69+
70+
/**
71+
* @param Request $request
72+
* @param Closure $next
73+
* @return Response
74+
*/
75+
public function handle($request, Closure $next)
76+
{
77+
if (!$this->hasMatchingPath($request)) {
78+
return $next($request);
79+
}
80+
81+
if ($this->isPreflightRequest($request)) {
82+
return $this->handlePreflightRequest($request);
83+
}
84+
85+
/** @var Response $response */
86+
$response = $next($request);
87+
88+
return $this->addPreflightRequestHeaders($response, $request);
89+
}
90+
91+
protected function addPreflightRequestHeaders(Response $response, Request $request): Response
92+
{
93+
$this->configureAllowedOrigin($response, $request);
94+
95+
if ($response->getHeader('Access-Control-Allow-Origin')) {
96+
$this->configureAllowCredentials($response, $request);
97+
$this->configureAllowedMethods($response, $request);
98+
$this->configureAllowedHeaders($response, $request);
99+
$this->configureMaxAge($response, $request);
100+
}
101+
102+
return $response;
103+
}
104+
105+
protected function configureAllowedOrigin(Response $response, Request $request): void
106+
{
107+
if ($this->allowAllOrigins === true && !$this->supportsCredentials) {
108+
$response->header(['Access-Control-Allow-Origin' => '*']);
109+
} elseif ($this->isSingleOriginAllowed()) {
110+
$response->header(['Access-Control-Allow-Origin' => array_values($this->allowedOrigins)[0]]);
111+
} else {
112+
if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) {
113+
$response->header(['Access-Control-Allow-Origin' => (string) $request->header('Origin')]);
114+
}
115+
}
116+
}
117+
118+
protected function configureAllowCredentials(Response $response, Request $request): void
119+
{
120+
if ($this->supportsCredentials) {
121+
$response->header(['Access-Control-Allow-Credentials' => 'true']);
122+
}
123+
}
124+
125+
protected function configureAllowedMethods(Response $response, Request $request): void
126+
{
127+
if ($this->allowAllMethods === true) {
128+
$allowMethods = strtoupper((string) $request->header('Access-Control-Request-Method'));
129+
} else {
130+
$allowMethods = implode(', ', $this->allowedMethods);
131+
}
132+
133+
$response->header(['Access-Control-Allow-Methods' => $allowMethods]);
134+
}
135+
136+
protected function configureAllowedHeaders(Response $response, Request $request): void
137+
{
138+
if ($this->allowAllHeaders === true) {
139+
$allowHeaders = (string) $request->header('Access-Control-Request-Headers');
140+
} else {
141+
$allowHeaders = implode(', ', $this->allowedHeaders);
142+
}
143+
$response->header(['Access-Control-Allow-Headers' => $allowHeaders]);
144+
}
145+
146+
protected function configureMaxAge(Response $response, Request $request): void
147+
{
148+
if ($this->maxAge !== null) {
149+
$response->header(['Access-Control-Max-Age' => (string) $this->maxAge]);
150+
}
151+
}
152+
153+
protected function handlePreflightRequest(Request $request)
154+
{
155+
$response = response('', 204);
156+
157+
return $this->addPreflightRequestHeaders($response, $request);
158+
}
159+
160+
protected function isCorsRequest(Request $request)
161+
{
162+
return !!$request->header('Origin');
163+
}
164+
165+
protected function isPreflightRequest(Request $request)
166+
{
167+
return $request->method() === 'OPTIONS' && $request->header('Access-Control-Request-Method');
168+
}
169+
170+
protected function isSingleOriginAllowed(): bool
171+
{
172+
if ($this->allowAllOrigins === true || count($this->allowedOriginsPatterns) > 0) {
173+
return false;
174+
}
175+
176+
return count($this->allowedOrigins) === 1;
177+
}
178+
179+
protected function isOriginAllowed(Request $request): bool
180+
{
181+
if ($this->allowAllOrigins === true) {
182+
return true;
183+
}
184+
185+
$origin = (string) $request->header('Origin');
186+
187+
if (in_array($origin, $this->allowedOrigins)) {
188+
return true;
189+
}
190+
191+
foreach ($this->allowedOriginsPatterns as $pattern) {
192+
if (preg_match($pattern, $origin)) {
193+
return true;
194+
}
195+
}
196+
197+
return false;
198+
}
199+
200+
protected function hasMatchingPath(Request $request)
201+
{
202+
$url = $request->baseUrl();
203+
$url = trim($url, '/');
204+
if ($url === '') {
205+
$url = '/';
206+
}
207+
208+
$paths = $this->getPathsByHost($request->host(true));
209+
210+
foreach ($paths as $path) {
211+
if ($path !== '/') {
212+
$path = trim($path, '/');
213+
}
214+
215+
if ($path === $url) {
216+
return true;
217+
}
218+
219+
$pattern = $this->convertWildcardToPattern($path);
220+
221+
if (preg_match($pattern, $url) === 1) {
222+
return true;
223+
}
224+
}
225+
226+
return false;
227+
}
228+
229+
protected function getPathsByHost($host)
230+
{
231+
$paths = $this->paths;
232+
233+
if (isset($paths[$host])) {
234+
return $paths[$host];
235+
}
236+
237+
return array_filter($paths, function ($path) {
238+
return is_string($path);
239+
});
240+
}
241+
242+
protected function convertWildcardToPattern($pattern)
243+
{
244+
$pattern = preg_quote($pattern, '#');
245+
$pattern = str_replace('\*', '.*', $pattern);
246+
return '#^' . $pattern . '\z#u';
247+
}
248+
}

src/Service.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace think\cors;
4+
5+
class Service extends \think\Service
6+
{
7+
public function boot(): void
8+
{
9+
$this->app->event->listen('HttpRun', function () {
10+
$this->app->middleware->add(HandleCors::class);
11+
});
12+
}
13+
}

src/config.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
return [
4+
'paths' => [],
5+
'allowed_origins' => ['*'],
6+
'allowed_origins_patterns' => [],
7+
'allowed_methods' => ['*'],
8+
'allowed_headers' => ['*'],
9+
'exposed_headers' => [],
10+
'max_age' => 0,
11+
'supports_credentials' => false,
12+
];

0 commit comments

Comments
 (0)