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.
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:
12 * http://www.apache.org/licenses/LICENSE-2.0
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.
22 * @file CAS/CookieJar.php
23 * @category Authentication
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
31 * This class provides access to service cookies and handles parsing of response
32 * headers to pull out cookie values.
34 * @class CAS_CookieJar
35 * @category Authentication
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
47 * Create a new cookie jar by passing it a reference to an array in which it
48 * should store cookies.
50 * @param array &$storageArray Array to store cookies
54 public function __construct (array &$storageArray)
56 $this->_cookies =& $storageArray;
60 * Store cookies for a web service request.
61 * Cookie storage is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
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.
70 public function storeCookies ($request_url, $response_headers)
72 $urlParts = parse_url($request_url);
73 $defaultDomain = $urlParts['host'];
75 $cookies = $this->parseCookieHeaders($response_headers, $defaultDomain);
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)) {
86 $this->storeCookie($cookie);
88 phpCAS::trace($cookie['name'].' -> '.$cookie['value']);
93 * Retrieve cookies applicable for a web service request.
94 * Cookie applicability is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
96 * @param string $request_url The url that the cookies will be for.
98 * @return array An array containing cookies. E.g. array('name' => 'val');
102 public function getCookies ($request_url)
104 if (!count($this->_cookies)) {
108 // If our request URL can't be parsed, no cookies apply.
109 $target = parse_url($request_url);
110 if ($target === false) {
114 $this->expireCookies();
116 $matching_cookies = array();
117 foreach ($this->_cookies as $key => $cookie) {
118 if ($this->cookieMatchesTarget($cookie, $target)) {
119 $matching_cookies[$cookie['name']] = $cookie['value'];
122 return $matching_cookies;
127 * Parse Cookies without PECL
128 * From the comments in http://php.net/manual/en/function.http-parse-cookie.php
130 * @param array $header array of header lines.
131 * @param string $defaultDomain The domain to use if none is specified in
134 * @return array of cookies
136 protected function parseCookieHeaders( $header, $defaultDomain )
138 phpCAS::traceBegin();
140 foreach ( $header as $line ) {
141 if ( preg_match('/^Set-Cookie2?: /i', $line)) {
142 $cookies[] = $this->parseCookieHeader($line, $defaultDomain);
146 phpCAS::traceEnd($cookies);
151 * Parse a single cookie header line.
153 * Based on RFC2965 http://www.ietf.org/rfc/rfc2965.txt
155 * @param string $line The header line.
156 * @param string $defaultDomain The domain to use if none is specified in
161 protected function parseCookieHeader ($line, $defaultDomain)
163 if (!$defaultDomain) {
164 throw new CAS_InvalidArgumentException('$defaultDomain was not provided.');
167 // Set our default values
169 'domain' => $defaultDomain,
174 $line = preg_replace('/^Set-Cookie2?: /i', '', trim($line));
176 // trim any trailing semicolons.
177 $line = trim($line, ';');
179 phpCAS::trace("Cookie Line: $line");
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
186 $attributeStrings = explode(';', $line);
188 foreach ( $attributeStrings as $attributeString ) {
189 // split on the first equals sign and use the rest as value
190 $attributeParts = explode('=', $attributeString, 2);
192 $attributeName = trim($attributeParts[0]);
193 $attributeNameLC = strtolower($attributeName);
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);
204 $attributeValue = null;
207 switch ($attributeNameLC) {
209 $cookie['expires'] = strtotime($attributeValue);
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'];
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;
223 $cookie['secure'] = true;
233 $cookie[$attributeNameLC] = $attributeValue;
236 $cookie['name'] = $attributeName;
237 $cookie['value'] = $attributeValue;
245 * Add, update, or remove a cookie.
247 * @param array $cookie A cookie array as created by parseCookieHeaders()
253 protected function storeCookie ($cookie)
255 // Discard any old versions of this cookie.
256 $this->discardCookie($cookie);
257 $this->_cookies[] = $cookie;
262 * Discard an existing cookie
264 * @param array $cookie An cookie
270 protected function discardCookie ($cookie)
272 if (!isset($cookie['domain'])
273 || !isset($cookie['path'])
274 || !isset($cookie['path'])
276 throw new CAS_InvalidArgumentException('Invalid Cookie array passed.');
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']
284 unset($this->_cookies[$key]);
290 * Go through our stored cookies and remove any that are expired.
296 protected function expireCookies ()
298 foreach ($this->_cookies as $key => $cookie) {
299 if (isset($cookie['expires']) && $cookie['expires'] < time()) {
300 unset($this->_cookies[$key]);
306 * Answer true if cookie is applicable to a target.
308 * @param array $cookie An array of cookie attributes.
309 * @param array $target An array of URL attributes as generated by parse_url().
315 protected function cookieMatchesTarget ($cookie, $target)
317 if (!is_array($target)) {
318 throw new CAS_InvalidArgumentException('$target must be an array of URL attributes as generated by parse_url().');
320 if (!isset($target['host'])) {
321 throw new CAS_InvalidArgumentException('$target must be an array of URL attributes as generated by parse_url().');
324 // Verify that the scheme matches
325 if ($cookie['secure'] && $target['scheme'] != 'https') {
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
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']);
342 // verify that the cookie domain is the last part of the host.
343 if ($pos + strlen($cookie['domain']) != strlen($target['host'])) {
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) {
355 // If the cookie host doesn't begin with '.', the host must case-insensitive
357 if (strcasecmp($target['host'], $cookie['domain']) !== 0) {
362 // Verify that the port matches
363 if (isset($cookie['ports']) && !in_array($target['port'], $cookie['ports'])) {
367 // Verify that the path matches
368 if (strpos($target['path'], $cookie['path']) !== 0) {