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: /**
35: * The response object this builder is attached to.
36: *
37: * @var \Psr\Http\Message\MessageInterface
38: */
39: protected $_response;
40:
41: /**
42: * The request's Origin header value
43: *
44: * @var string
45: */
46: protected $_origin;
47:
48: /**
49: * Whether or not the request was over SSL.
50: *
51: * @var bool
52: */
53: protected $_isSsl;
54:
55: /**
56: * The headers that have been queued so far.
57: *
58: * @var array
59: */
60: protected $_headers = [];
61:
62: /**
63: * Constructor.
64: *
65: * @param \Psr\Http\Message\MessageInterface $response The response object to add headers onto.
66: * @param string $origin The request's Origin header.
67: * @param bool $isSsl Whether or not the request was over SSL.
68: */
69: public function __construct(MessageInterface $response, $origin, $isSsl = false)
70: {
71: $this->_origin = $origin;
72: $this->_isSsl = $isSsl;
73: $this->_response = $response;
74: }
75:
76: /**
77: * Apply the queued headers to the response.
78: *
79: * If the builder has no Origin, or if there are no allowed domains,
80: * or if the allowed domains do not match the Origin header no headers will be applied.
81: *
82: * @return \Psr\Http\Message\MessageInterface A new instance of the response with new headers.
83: */
84: public function build()
85: {
86: $response = $this->_response;
87: if (empty($this->_origin)) {
88: return $response;
89: }
90:
91: if (isset($this->_headers['Access-Control-Allow-Origin'])) {
92: foreach ($this->_headers as $key => $value) {
93: $response = $response->withHeader($key, $value);
94: }
95: }
96:
97: return $response;
98: }
99:
100: /**
101: * Set the list of allowed domains.
102: *
103: * Accepts a string or an array of domains that have CORS enabled.
104: * You can use `*.example.com` wildcards to accept subdomains, or `*` to allow all domains
105: *
106: * @param string|array $domain The allowed domains
107: * @return $this
108: */
109: public function allowOrigin($domain)
110: {
111: $allowed = $this->_normalizeDomains((array)$domain);
112: foreach ($allowed as $domain) {
113: if (!preg_match($domain['preg'], $this->_origin)) {
114: continue;
115: }
116: $value = $domain['original'] === '*' ? '*' : $this->_origin;
117: $this->_headers['Access-Control-Allow-Origin'] = $value;
118: break;
119: }
120:
121: return $this;
122: }
123:
124: /**
125: * Normalize the origin to regular expressions and put in an array format
126: *
127: * @param array $domains Domain names to normalize.
128: * @return array
129: */
130: protected function _normalizeDomains($domains)
131: {
132: $result = [];
133: foreach ($domains as $domain) {
134: if ($domain === '*') {
135: $result[] = ['preg' => '@.@', 'original' => '*'];
136: continue;
137: }
138:
139: $original = $preg = $domain;
140: if (strpos($domain, '://') === false) {
141: $preg = ($this->_isSsl ? 'https://' : 'http://') . $domain;
142: }
143: $preg = '@^' . str_replace('\*', '.*', preg_quote($preg, '@')) . '$@';
144: $result[] = compact('original', 'preg');
145: }
146:
147: return $result;
148: }
149:
150: /**
151: * Set the list of allowed HTTP Methods.
152: *
153: * @param array $methods The allowed HTTP methods
154: * @return $this
155: */
156: public function allowMethods(array $methods)
157: {
158: $this->_headers['Access-Control-Allow-Methods'] = implode(', ', $methods);
159:
160: return $this;
161: }
162:
163: /**
164: * Enable cookies to be sent in CORS requests.
165: *
166: * @return $this
167: */
168: public function allowCredentials()
169: {
170: $this->_headers['Access-Control-Allow-Credentials'] = 'true';
171:
172: return $this;
173: }
174:
175: /**
176: * Whitelist headers that can be sent in CORS requests.
177: *
178: * @param array $headers The list of headers to accept in CORS requests.
179: * @return $this
180: */
181: public function allowHeaders(array $headers)
182: {
183: $this->_headers['Access-Control-Allow-Headers'] = implode(', ', $headers);
184:
185: return $this;
186: }
187:
188: /**
189: * Define the headers a client library/browser can expose to scripting
190: *
191: * @param array $headers The list of headers to expose CORS responses
192: * @return $this
193: */
194: public function exposeHeaders(array $headers)
195: {
196: $this->_headers['Access-Control-Expose-Headers'] = implode(', ', $headers);
197:
198: return $this;
199: }
200:
201: /**
202: * Define the max-age preflight OPTIONS requests are valid for.
203: *
204: * @param int $age The max-age for OPTIONS requests in seconds
205: * @return $this
206: */
207: public function maxAge($age)
208: {
209: $this->_headers['Access-Control-Max-Age'] = $age;
210:
211: return $this;
212: }
213: }
214: