• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 13218345875

08 Feb 2025 06:43PM UTC coverage: 84.559% (+0.001%) from 84.558%
13218345875

push

github

web-flow
fix: set headers for CORS (#9437)

* fix: set everything in the before filter for CORS

* fix append vary header

* cors: apply response headers in the after filter only if they are not already set

* cs fix

18 of 19 new or added lines in 2 files covered. (94.74%)

1 existing line in 1 file now uncovered.

20842 of 24648 relevant lines covered (84.56%)

191.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.18
/system/HTTP/Cors.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\Exceptions\ConfigException;
17
use Config\Cors as CorsConfig;
18

19
/**
20
 * Cross-Origin Resource Sharing (CORS)
21
 *
22
 * @see \CodeIgniter\HTTP\CorsTest
23
 */
24
class Cors
25
{
26
    /**
27
     * @var array{
28
     *     allowedOrigins: list<string>,
29
     *     allowedOriginsPatterns: list<string>,
30
     *     supportsCredentials: bool,
31
     *     allowedHeaders: list<string>,
32
     *     exposedHeaders: list<string>,
33
     *     allowedMethods: list<string>,
34
     *     maxAge: int,
35
     * }
36
     */
37
    private array $config = [
38
        'allowedOrigins'         => [],
39
        'allowedOriginsPatterns' => [],
40
        'supportsCredentials'    => false,
41
        'allowedHeaders'         => [],
42
        'exposedHeaders'         => [],
43
        'allowedMethods'         => [],
44
        'maxAge'                 => 7200,
45
    ];
46

47
    /**
48
     * @param array{
49
     *     allowedOrigins?: list<string>,
50
     *     allowedOriginsPatterns?: list<string>,
51
     *     supportsCredentials?: bool,
52
     *     allowedHeaders?: list<string>,
53
     *     exposedHeaders?: list<string>,
54
     *     allowedMethods?: list<string>,
55
     *     maxAge?: int,
56
     * }|CorsConfig|null $config
57
     */
58
    public function __construct($config = null)
59
    {
60
        $config ??= config(CorsConfig::class);
48✔
61
        if ($config instanceof CorsConfig) {
48✔
62
            $config = $config->default;
×
63
        }
64
        $this->config = array_merge($this->config, $config);
48✔
65
    }
66

67
    /**
68
     * Creates a new instance by config name.
69
     */
70
    public static function factory(string $configName = 'default'): self
71
    {
72
        $config = config(CorsConfig::class)->{$configName};
×
73

74
        return new self($config);
×
75
    }
76

77
    /**
78
     * Whether if the request is a preflight request.
79
     */
80
    public function isPreflightRequest(IncomingRequest $request): bool
81
    {
82
        return $request->is('OPTIONS')
30✔
83
            && $request->hasHeader('Access-Control-Request-Method');
30✔
84
    }
85

86
    /**
87
     * Handles the preflight request, and returns the response.
88
     */
89
    public function handlePreflightRequest(RequestInterface $request, ResponseInterface $response): ResponseInterface
90
    {
91
        $response->setStatusCode(204);
17✔
92

93
        $this->setAllowOrigin($request, $response);
17✔
94

95
        if ($response->hasHeader('Access-Control-Allow-Origin')) {
16✔
96
            $this->setAllowHeaders($response);
14✔
97
            $this->setAllowMethods($response);
13✔
98
            $this->setAllowMaxAge($response);
13✔
99
            $this->setAllowCredentials($response);
13✔
100
        }
101

102
        return $response;
15✔
103
    }
104

105
    private function checkWildcard(string $name, int $count): void
106
    {
107
        if (in_array('*', $this->config[$name], true) && $count > 1) {
43✔
108
            throw new ConfigException(
1✔
109
                "If wildcard is specified, you must set `'{$name}' => ['*']`."
1✔
110
                . ' But using wildcard is not recommended.',
1✔
111
            );
1✔
112
        }
113
    }
114

115
    private function checkWildcardAndCredentials(string $name, string $header): void
116
    {
117
        if (
118
            $this->config[$name] === ['*']
42✔
119
            && $this->config['supportsCredentials']
42✔
120
        ) {
121
            throw new ConfigException(
3✔
122
                'When responding to a credentialed request, '
3✔
123
                . 'the server must not specify the "*" wildcard for the '
3✔
124
                . $header . ' response-header value.',
3✔
125
            );
3✔
126
        }
127
    }
128

129
    private function setAllowOrigin(RequestInterface $request, ResponseInterface $response): void
130
    {
131
        $originCount        = count($this->config['allowedOrigins']);
43✔
132
        $originPatternCount = count($this->config['allowedOriginsPatterns']);
43✔
133

134
        $this->checkWildcard('allowedOrigins', $originCount);
43✔
135
        $this->checkWildcardAndCredentials('allowedOrigins', 'Access-Control-Allow-Origin');
42✔
136

137
        // Single Origin.
138
        if ($originCount === 1 && $originPatternCount === 0) {
40✔
139
            $response->setHeader('Access-Control-Allow-Origin', $this->config['allowedOrigins'][0]);
28✔
140

141
            return;
28✔
142
        }
143

144
        // Multiple Origins.
145
        if (! $request->hasHeader('Origin')) {
12✔
146
            return;
1✔
147
        }
148

149
        $origin = $request->getHeaderLine('Origin');
11✔
150

151
        if ($originCount > 1 && in_array($origin, $this->config['allowedOrigins'], true)) {
11✔
152
            $response->setHeader('Access-Control-Allow-Origin', $origin);
5✔
153
            $response->appendHeader('Vary', 'Origin');
5✔
154

155
            return;
5✔
156
        }
157

158
        if ($originPatternCount > 0) {
6✔
159
            foreach ($this->config['allowedOriginsPatterns'] as $pattern) {
4✔
160
                $regex = '#\A' . $pattern . '\z#';
4✔
161

162
                if (preg_match($regex, $origin)) {
4✔
163
                    $response->setHeader('Access-Control-Allow-Origin', $origin);
3✔
164
                    $response->appendHeader('Vary', 'Origin');
3✔
165

166
                    return;
3✔
167
                }
168
            }
169
        }
170
    }
171

172
    private function setAllowHeaders(ResponseInterface $response): void
173
    {
174
        $this->checkWildcard('allowedHeaders', count($this->config['allowedHeaders']));
14✔
175
        $this->checkWildcardAndCredentials('allowedHeaders', 'Access-Control-Allow-Headers');
14✔
176

177
        $response->setHeader(
13✔
178
            'Access-Control-Allow-Headers',
13✔
179
            implode(', ', $this->config['allowedHeaders']),
13✔
180
        );
13✔
181
    }
182

183
    private function setAllowMethods(ResponseInterface $response): void
184
    {
185
        $this->checkWildcard('allowedMethods', count($this->config['allowedMethods']));
13✔
186
        $this->checkWildcardAndCredentials('allowedMethods', 'Access-Control-Allow-Methods');
13✔
187

188
        $response->setHeader(
13✔
189
            'Access-Control-Allow-Methods',
13✔
190
            implode(', ', $this->config['allowedMethods']),
13✔
191
        );
13✔
192
    }
193

194
    private function setAllowMaxAge(ResponseInterface $response): void
195
    {
196
        $response->setHeader('Access-Control-Max-Age', (string) $this->config['maxAge']);
13✔
197
    }
198

199
    private function setAllowCredentials(ResponseInterface $response): void
200
    {
201
        if ($this->config['supportsCredentials']) {
35✔
202
            $response->setHeader('Access-Control-Allow-Credentials', 'true');
4✔
203
        }
204
    }
205

206
    /**
207
     * Adds CORS headers to the Response.
208
     */
209
    public function addResponseHeaders(RequestInterface $request, ResponseInterface $response): ResponseInterface
210
    {
211
        $this->setAllowOrigin($request, $response);
26✔
212

213
        if ($response->hasHeader('Access-Control-Allow-Origin')) {
24✔
214
            $this->setAllowCredentials($response);
22✔
215
            $this->setExposeHeaders($response);
22✔
216
        }
217

218
        return $response;
24✔
219
    }
220

221
    private function setExposeHeaders(ResponseInterface $response): void
222
    {
223
        if ($this->config['exposedHeaders'] !== []) {
22✔
224
            $response->setHeader(
2✔
225
                'Access-Control-Expose-Headers',
2✔
226
                implode(', ', $this->config['exposedHeaders']),
2✔
227
            );
2✔
228
        }
229
    }
230

231
    /**
232
     * Check if response headers were set
233
     */
234
    public function hasResponseHeaders(RequestInterface $request, ResponseInterface $response): bool
235
    {
236
        if (! $response->hasHeader('Access-Control-Allow-Origin')) {
18✔
237
            return false;
4✔
238
        }
239

240
        if ($this->config['supportsCredentials']
14✔
241
            && ! $response->hasHeader('Access-Control-Allow-Credentials')) {
14✔
NEW
242
            return false;
×
243
        }
244

245
        return ! ($this->config['exposedHeaders'] !== [] && (! $response->hasHeader('Access-Control-Expose-Headers') || ! str_contains(
14✔
246
            $response->getHeaderLine('Access-Control-Expose-Headers'),
14✔
247
            implode(', ', $this->config['exposedHeaders']),
14✔
248
        )));
14✔
249
    }
250
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc