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