Upgrade phpCAS
[piwik-CASLogin.git] / CAS / CAS / CookieJar.php
1 <?php
2
3 /**
4  * Licensed to Jasig under one or more contributor license
5  * agreements. See the NOTICE file distributed with this work for
6  * additional information regarding copyright ownership.
7  *
8  * Jasig licenses this file to you under the Apache License,
9  * Version 2.0 (the "License"); you may not use this file except in
10  * compliance with the License. You may obtain a copy of the License at:
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  *
20  * PHP Version 5
21  *
22  * @file     CAS/CookieJar.php
23  * @category Authentication
24  * @package  PhpCAS
25  * @author   Adam Franco <afranco@middlebury.edu>
26  * @license  http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
27  * @link     https://wiki.jasig.org/display/CASC/phpCAS
28  */
29
30 /**
31  * This class provides access to service cookies and handles parsing of response
32  * headers to pull out cookie values.
33  *
34  * @class    CAS_CookieJar
35  * @category Authentication
36  * @package  PhpCAS
37  * @author   Adam Franco <afranco@middlebury.edu>
38  * @license  http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
39  * @link     https://wiki.jasig.org/display/CASC/phpCAS
40  */
41 class CAS_CookieJar
42 {
43
44     private $_cookies;
45
46     /**
47      * Create a new cookie jar by passing it a reference to an array in which it
48      * should store cookies.
49      *
50      * @param array &$storageArray Array to store cookies
51      *
52      * @return void
53      */
54     public function __construct (array &$storageArray)
55     {
56         $this->_cookies =& $storageArray;
57     }
58
59     /**
60      * Store cookies for a web service request.
61      * Cookie storage is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
62      *
63      * @param string $request_url      The URL that generated the response headers.
64      * @param array  $response_headers An array of the HTTP response header strings.
65      *
66      * @return void
67      *
68      * @access private
69      */
70     public function storeCookies ($request_url, $response_headers)
71     {
72         $urlParts = parse_url($request_url);
73         $defaultDomain = $urlParts['host'];
74
75         $cookies = $this->parseCookieHeaders($response_headers, $defaultDomain);
76
77         // var_dump($cookies);
78         foreach ($cookies as $cookie) {
79             // Enforce the same-origin policy by verifying that the cookie
80             // would match the url that is setting it
81             if (!$this->cookieMatchesTarget($cookie, $urlParts)) {
82                 continue;
83             }
84
85             // store the cookie
86             $this->storeCookie($cookie);
87
88             phpCAS::trace($cookie['name'].' -> '.$cookie['value']);
89         }
90     }
91
92     /**
93      * Retrieve cookies applicable for a web service request.
94      * Cookie applicability is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
95      *
96      * @param string $request_url The url that the cookies will be for.
97      *
98      * @return array An array containing cookies. E.g. array('name' => 'val');
99      *
100      * @access private
101      */
102     public function getCookies ($request_url)
103     {
104         if (!count($this->_cookies)) {
105             return array();
106         }
107
108         // If our request URL can't be parsed, no cookies apply.
109         $target = parse_url($request_url);
110         if ($target === false) {
111             return array();
112         }
113
114         $this->expireCookies();
115
116         $matching_cookies = array();
117         foreach ($this->_cookies as $key => $cookie) {
118             if ($this->cookieMatchesTarget($cookie, $target)) {
119                 $matching_cookies[$cookie['name']] = $cookie['value'];
120             }
121         }
122         return $matching_cookies;
123     }
124
125
126     /**
127      * Parse Cookies without PECL
128      * From the comments in http://php.net/manual/en/function.http-parse-cookie.php
129      *
130      * @param array  $header        array of header lines.
131      * @param string $defaultDomain The domain to use if none is specified in
132      * the cookie.
133      *
134      * @return array of cookies
135      */
136     protected function parseCookieHeaders( $header, $defaultDomain )
137     {
138         phpCAS::traceBegin();
139         $cookies = array();
140         foreach ( $header as $line ) {
141             if ( preg_match('/^Set-Cookie2?: /i', $line)) {
142                 $cookies[] = $this->parseCookieHeader($line, $defaultDomain);
143             }
144         }
145
146         phpCAS::traceEnd($cookies);
147         return $cookies;
148     }
149
150     /**
151      * Parse a single cookie header line.
152      *
153      * Based on RFC2965 http://www.ietf.org/rfc/rfc2965.txt
154      *
155      * @param string $line          The header line.
156      * @param string $defaultDomain The domain to use if none is specified in
157      * the cookie.
158      *
159      * @return array
160      */
161     protected function parseCookieHeader ($line, $defaultDomain)
162     {
163         if (!$defaultDomain) {
164             throw new CAS_InvalidArgumentException('$defaultDomain was not provided.');
165         }
166
167         // Set our default values
168         $cookie = array(
169             'domain' => $defaultDomain,
170             'path' => '/',
171             'secure' => false,
172         );
173
174         $line = preg_replace('/^Set-Cookie2?: /i', '', trim($line));
175
176         // trim any trailing semicolons.
177         $line = trim($line, ';');
178
179         phpCAS::trace("Cookie Line: $line");
180
181         // This implementation makes the assumption that semicolons will not
182         // be present in quoted attribute values. While attribute values that
183         // contain semicolons are allowed by RFC2965, they are hopefully rare
184         // enough to ignore for our purposes. Most browsers make the same
185         // assumption.
186         $attributeStrings = explode(';', $line);
187
188         foreach ( $attributeStrings as $attributeString ) {
189             // split on the first equals sign and use the rest as value
190             $attributeParts = explode('=', $attributeString, 2);
191
192             $attributeName = trim($attributeParts[0]);
193             $attributeNameLC = strtolower($attributeName);
194
195             if (isset($attributeParts[1])) {
196                 $attributeValue = trim($attributeParts[1]);
197                 // Values may be quoted strings.
198                 if (strpos($attributeValue, '"') === 0) {
199                     $attributeValue = trim($attributeValue, '"');
200                     // unescape any escaped quotes:
201                     $attributeValue = str_replace('\"', '"', $attributeValue);
202                 }
203             } else {
204                 $attributeValue = null;
205             }
206
207             switch ($attributeNameLC) {
208             case 'expires':
209                 $cookie['expires'] = strtotime($attributeValue);
210                 break;
211             case 'max-age':
212                 $cookie['max-age'] = (int)$attributeValue;
213                 // Set an expiry time based on the max-age
214                 if ($cookie['max-age']) {
215                     $cookie['expires'] = time() + $cookie['max-age'];
216                 } else {
217                     // If max-age is zero, then the cookie should be removed
218                     // imediately so set an expiry before now.
219                     $cookie['expires'] = time() - 1;
220                 }
221                 break;
222             case 'secure':
223                 $cookie['secure'] = true;
224                 break;
225             case 'domain':
226             case 'path':
227             case 'port':
228             case 'version':
229             case 'comment':
230             case 'commenturl':
231             case 'discard':
232             case 'httponly':
233                 $cookie[$attributeNameLC] = $attributeValue;
234                 break;
235             default:
236                 $cookie['name'] = $attributeName;
237                 $cookie['value'] = $attributeValue;
238             }
239         }
240
241         return $cookie;
242     }
243
244     /**
245      * Add, update, or remove a cookie.
246      *
247      * @param array $cookie A cookie array as created by parseCookieHeaders()
248      *
249      * @return void
250      *
251      * @access protected
252      */
253     protected function storeCookie ($cookie)
254     {
255         // Discard any old versions of this cookie.
256         $this->discardCookie($cookie);
257         $this->_cookies[] = $cookie;
258
259     }
260
261     /**
262      * Discard an existing cookie
263      *
264      * @param array $cookie An cookie
265      *
266      * @return void
267      *
268      * @access protected
269      */
270     protected function discardCookie ($cookie)
271     {
272         if (!isset($cookie['domain'])
273             || !isset($cookie['path'])
274             || !isset($cookie['path'])
275         ) {
276             throw new CAS_InvalidArgumentException('Invalid Cookie array passed.');
277         }
278
279         foreach ($this->_cookies as $key => $old_cookie) {
280             if ( $cookie['domain'] == $old_cookie['domain']
281                 && $cookie['path'] == $old_cookie['path']
282                 && $cookie['name'] == $old_cookie['name']
283             ) {
284                 unset($this->_cookies[$key]);
285             }
286         }
287     }
288
289     /**
290      * Go through our stored cookies and remove any that are expired.
291      *
292      * @return void
293      *
294      * @access protected
295      */
296     protected function expireCookies ()
297     {
298         foreach ($this->_cookies as $key => $cookie) {
299             if (isset($cookie['expires']) && $cookie['expires'] < time()) {
300                 unset($this->_cookies[$key]);
301             }
302         }
303     }
304
305     /**
306      * Answer true if cookie is applicable to a target.
307      *
308      * @param array $cookie An array of cookie attributes.
309      * @param array $target An array of URL attributes as generated by parse_url().
310      *
311      * @return bool
312      *
313      * @access private
314      */
315     protected function cookieMatchesTarget ($cookie, $target)
316     {
317         if (!is_array($target)) {
318             throw new CAS_InvalidArgumentException('$target must be an array of URL attributes as generated by parse_url().');
319         }
320         if (!isset($target['host'])) {
321             throw new CAS_InvalidArgumentException('$target must be an array of URL attributes as generated by parse_url().');
322         }
323
324         // Verify that the scheme matches
325         if ($cookie['secure'] && $target['scheme'] != 'https') {
326             return false;
327         }
328
329         // Verify that the host matches
330         // Match domain and mulit-host cookies
331         if (strpos($cookie['domain'], '.') === 0) {
332             // .host.domain.edu cookies are valid for host.domain.edu
333             if (substr($cookie['domain'], 1) == $target['host']) {
334                 // continue with other checks
335             } else {
336                 // non-exact host-name matches.
337                 // check that the target host a.b.c.edu is within .b.c.edu
338                 $pos = strripos($target['host'], $cookie['domain']);
339                 if (!$pos) {
340                     return false;
341                 }
342                 // verify that the cookie domain is the last part of the host.
343                 if ($pos + strlen($cookie['domain']) != strlen($target['host'])) {
344                     return false;
345                 }
346                 // verify that the host name does not contain interior dots as per
347                 // RFC 2965 section 3.3.2  Rejecting Cookies
348                 // http://www.ietf.org/rfc/rfc2965.txt
349                 $hostname = substr($target['host'], 0, $pos);
350                 if (strpos($hostname, '.') !== false) {
351                     return false;
352                 }
353             }
354         } else {
355             // If the cookie host doesn't begin with '.', the host must case-insensitive
356             // match exactly
357             if (strcasecmp($target['host'], $cookie['domain']) !== 0) {
358                 return false;
359             }
360         }
361
362         // Verify that the port matches
363         if (isset($cookie['ports']) && !in_array($target['port'], $cookie['ports'])) {
364             return false;
365         }
366
367         // Verify that the path matches
368         if (strpos($target['path'], $cookie['path']) !== 0) {
369             return false;
370         }
371
372         return true;
373     }
374
375 }
376
377 ?>