1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.2.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Http;
16:
17: use Psr\Http\Message\MessageInterface;
18:
19: /**
20: * A builder object that assists in defining Cross Origin Request related
21: * headers.
22: *
23: * Each of the methods in this object provide a fluent interface. Once you've
24: * set all the headers you want to use, the `build()` method can be used to return
25: * a modified Response.
26: *
27: * It is most convenient to get this object via `Request::cors()`.
28: *
29: * @see \Cake\Http\Response::cors()
30: */
31: class CorsBuilder
32: {
33: /**
34: * The response object this builder is attached to.
35: *
36: * @var \Psr\Http\Message\MessageInterface
37: */
38: protected $_response;
39:
40: /**
41: * The request's Origin header value
42: *
43: * @var string
44: */
45: protected $_origin;
46:
47: /**
48: * Whether or not the request was over SSL.
49: *
50: * @var bool
51: */
52: protected $_isSsl;
53:
54: /**
55: * The headers that have been queued so far.
56: *
57: * @var array
58: */
59: protected $_headers = [];
60:
61: /**
62: * Constructor.
63: *
64: * @param \Psr\Http\Message\MessageInterface $response The response object to add headers onto.
65: * @param string $origin The request's Origin header.
66: * @param bool $isSsl Whether or not the request was over SSL.
67: */
68: public function __construct(MessageInterface $response, $origin, $isSsl = false)
69: {
70: $this->_origin = $origin;
71: $this->_isSsl = $isSsl;
72: $this->_response = $response;
73: }
74:
75: /**
76: * Apply the queued headers to the response.
77: *
78: * If the builder has no Origin, or if there are no allowed domains,
79: * or if the allowed domains do not match the Origin header no headers will be applied.
80: *
81: * @return \Psr\Http\Message\MessageInterface A new instance of the response with new headers.
82: */
83: public function build()
84: {
85: $response = $this->_response;
86: if (empty($this->_origin)) {
87: return $response;
88: }
89:
90: if (isset($this->_headers['Access-Control-Allow-Origin'])) {
91: foreach ($this->_headers as $key => $value) {
92: $response = $response->withHeader($key, $value);
93: }
94: }
95:
96: return $response;
97: }
98:
99: /**
100: * Set the list of allowed domains.
101: *
102: * Accepts a string or an array of domains that have CORS enabled.
103: * You can use `*.example.com` wildcards to accept subdomains, or `*` to allow all domains
104: *
105: * @param string|string[] $domains The allowed domains
106: * @return $this
107: */
108: public function allowOrigin($domains)
109: {
110: $allowed = $this->_normalizeDomains((array)$domains);
111: foreach ($allowed as $domain) {
112: if (!preg_match($domain['preg'], $this->_origin)) {
113: continue;
114: }
115: $value = $domain['original'] === '*' ? '*' : $this->_origin;
116: $this->_headers['Access-Control-Allow-Origin'] = $value;
117: break;
118: }
119:
120: return $this;
121: }
122:
123: /**
124: * Normalize the origin to regular expressions and put in an array format
125: *
126: * @param string[] $domains Domain names to normalize.
127: * @return array
128: */
129: protected function _normalizeDomains($domains)
130: {
131: $result = [];
132: foreach ($domains as $domain) {
133: if ($domain === '*') {
134: $result[] = ['preg' => '@.@', 'original' => '*'];
135: continue;
136: }
137:
138: $original = $preg = $domain;
139: if (strpos($domain, '://') === false) {
140: $preg = ($this->_isSsl ? 'https://' : 'http://') . $domain;
141: }
142: $preg = '@^' . str_replace('\*', '.*', preg_quote($preg, '@')) . '$@';
143: $result[] = compact('original', 'preg');
144: }
145:
146: return $result;
147: }
148:
149: /**
150: * Set the list of allowed HTTP Methods.
151: *
152: * @param string[] $methods The allowed HTTP methods
153: * @return $this
154: */
155: public function allowMethods(array $methods)
156: {
157: $this->_headers['Access-Control-Allow-Methods'] = implode(', ', $methods);
158:
159: return $this;
160: }
161:
162: /**
163: * Enable cookies to be sent in CORS requests.
164: *
165: * @return $this
166: */
167: public function allowCredentials()
168: {
169: $this->_headers['Access-Control-Allow-Credentials'] = 'true';
170:
171: return $this;
172: }
173:
174: /**
175: * Whitelist headers that can be sent in CORS requests.
176: *
177: * @param string[] $headers The list of headers to accept in CORS requests.
178: * @return $this
179: */
180: public function allowHeaders(array $headers)
181: {
182: $this->_headers['Access-Control-Allow-Headers'] = implode(', ', $headers);
183:
184: return $this;
185: }
186:
187: /**
188: * Define the headers a client library/browser can expose to scripting
189: *
190: * @param string[] $headers The list of headers to expose CORS responses
191: * @return $this
192: */
193: public function exposeHeaders(array $headers)
194: {
195: $this->_headers['Access-Control-Expose-Headers'] = implode(', ', $headers);
196:
197: return $this;
198: }
199:
200: /**
201: * Define the max-age preflight OPTIONS requests are valid for.
202: *
203: * @param int $age The max-age for OPTIONS requests in seconds
204: * @return $this
205: */
206: public function maxAge($age)
207: {
208: $this->_headers['Access-Control-Max-Age'] = $age;
209:
210: return $this;
211: }
212: }
213: