--- /dev/null
+<?php
+
+/**
+ * Licensed to Jasig under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for
+ * additional information regarding copyright ownership.
+ *
+ * Jasig licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * PHP Version 5
+ *
+ * @file CAS/CookieJar.php
+ * @category Authentication
+ * @package PhpCAS
+ * @author Adam Franco <afranco@middlebury.edu>
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
+ * @link https://wiki.jasig.org/display/CASC/phpCAS
+ */
+
+/**
+ * This class provides access to service cookies and handles parsing of response
+ * headers to pull out cookie values.
+ *
+ * @class CAS_CookieJar
+ * @category Authentication
+ * @package PhpCAS
+ * @author Adam Franco <afranco@middlebury.edu>
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
+ * @link https://wiki.jasig.org/display/CASC/phpCAS
+ */
+class CAS_CookieJar
+{
+
+ private $_cookies;
+
+ /**
+ * Create a new cookie jar by passing it a reference to an array in which it
+ * should store cookies.
+ *
+ * @param array &$storageArray Array to store cookies
+ *
+ * @return void
+ */
+ public function __construct (array &$storageArray)
+ {
+ $this->_cookies =& $storageArray;
+ }
+
+ /**
+ * Store cookies for a web service request.
+ * Cookie storage is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
+ *
+ * @param string $request_url The URL that generated the response headers.
+ * @param array $response_headers An array of the HTTP response header strings.
+ *
+ * @return void
+ *
+ * @access private
+ */
+ public function storeCookies ($request_url, $response_headers)
+ {
+ $urlParts = parse_url($request_url);
+ $defaultDomain = $urlParts['host'];
+
+ $cookies = $this->parseCookieHeaders($response_headers, $defaultDomain);
+
+ // var_dump($cookies);
+ foreach ($cookies as $cookie) {
+ // Enforce the same-origin policy by verifying that the cookie
+ // would match the url that is setting it
+ if (!$this->cookieMatchesTarget($cookie, $urlParts)) {
+ continue;
+ }
+
+ // store the cookie
+ $this->storeCookie($cookie);
+
+ phpCAS::trace($cookie['name'].' -> '.$cookie['value']);
+ }
+ }
+
+ /**
+ * Retrieve cookies applicable for a web service request.
+ * Cookie applicability is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
+ *
+ * @param string $request_url The url that the cookies will be for.
+ *
+ * @return array An array containing cookies. E.g. array('name' => 'val');
+ *
+ * @access private
+ */
+ public function getCookies ($request_url)
+ {
+ if (!count($this->_cookies)) {
+ return array();
+ }
+
+ // If our request URL can't be parsed, no cookies apply.
+ $target = parse_url($request_url);
+ if ($target === false) {
+ return array();
+ }
+
+ $this->expireCookies();
+
+ $matching_cookies = array();
+ foreach ($this->_cookies as $key => $cookie) {
+ if ($this->cookieMatchesTarget($cookie, $target)) {
+ $matching_cookies[$cookie['name']] = $cookie['value'];
+ }
+ }
+ return $matching_cookies;
+ }
+
+
+ /**
+ * Parse Cookies without PECL
+ * From the comments in http://php.net/manual/en/function.http-parse-cookie.php
+ *
+ * @param array $header array of header lines.
+ * @param string $defaultDomain The domain to use if none is specified in
+ * the cookie.
+ *
+ * @return array of cookies
+ */
+ protected function parseCookieHeaders( $header, $defaultDomain )
+ {
+ phpCAS::traceBegin();
+ $cookies = array();
+ foreach ( $header as $line ) {
+ if ( preg_match('/^Set-Cookie2?: /i', $line)) {
+ $cookies[] = $this->parseCookieHeader($line, $defaultDomain);
+ }
+ }
+
+ phpCAS::traceEnd($cookies);
+ return $cookies;
+ }
+
+ /**
+ * Parse a single cookie header line.
+ *
+ * Based on RFC2965 http://www.ietf.org/rfc/rfc2965.txt
+ *
+ * @param string $line The header line.
+ * @param string $defaultDomain The domain to use if none is specified in
+ * the cookie.
+ *
+ * @return array
+ */
+ protected function parseCookieHeader ($line, $defaultDomain)
+ {
+ if (!$defaultDomain) {
+ throw new CAS_InvalidArgumentException('$defaultDomain was not provided.');
+ }
+
+ // Set our default values
+ $cookie = array(
+ 'domain' => $defaultDomain,
+ 'path' => '/',
+ 'secure' => false,
+ );
+
+ $line = preg_replace('/^Set-Cookie2?: /i', '', trim($line));
+
+ // trim any trailing semicolons.
+ $line = trim($line, ';');
+
+ phpCAS::trace("Cookie Line: $line");
+
+ // This implementation makes the assumption that semicolons will not
+ // be present in quoted attribute values. While attribute values that
+ // contain semicolons are allowed by RFC2965, they are hopefully rare
+ // enough to ignore for our purposes. Most browsers make the same
+ // assumption.
+ $attributeStrings = explode(';', $line);
+
+ foreach ( $attributeStrings as $attributeString ) {
+ // split on the first equals sign and use the rest as value
+ $attributeParts = explode('=', $attributeString, 2);
+
+ $attributeName = trim($attributeParts[0]);
+ $attributeNameLC = strtolower($attributeName);
+
+ if (isset($attributeParts[1])) {
+ $attributeValue = trim($attributeParts[1]);
+ // Values may be quoted strings.
+ if (strpos($attributeValue, '"') === 0) {
+ $attributeValue = trim($attributeValue, '"');
+ // unescape any escaped quotes:
+ $attributeValue = str_replace('\"', '"', $attributeValue);
+ }
+ } else {
+ $attributeValue = null;
+ }
+
+ switch ($attributeNameLC) {
+ case 'expires':
+ $cookie['expires'] = strtotime($attributeValue);
+ break;
+ case 'max-age':
+ $cookie['max-age'] = (int)$attributeValue;
+ // Set an expiry time based on the max-age
+ if ($cookie['max-age']) {
+ $cookie['expires'] = time() + $cookie['max-age'];
+ } else {
+ // If max-age is zero, then the cookie should be removed
+ // imediately so set an expiry before now.
+ $cookie['expires'] = time() - 1;
+ }
+ break;
+ case 'secure':
+ $cookie['secure'] = true;
+ break;
+ case 'domain':
+ case 'path':
+ case 'port':
+ case 'version':
+ case 'comment':
+ case 'commenturl':
+ case 'discard':
+ case 'httponly':
+ $cookie[$attributeNameLC] = $attributeValue;
+ break;
+ default:
+ $cookie['name'] = $attributeName;
+ $cookie['value'] = $attributeValue;
+ }
+ }
+
+ return $cookie;
+ }
+
+ /**
+ * Add, update, or remove a cookie.
+ *
+ * @param array $cookie A cookie array as created by parseCookieHeaders()
+ *
+ * @return void
+ *
+ * @access protected
+ */
+ protected function storeCookie ($cookie)
+ {
+ // Discard any old versions of this cookie.
+ $this->discardCookie($cookie);
+ $this->_cookies[] = $cookie;
+
+ }
+
+ /**
+ * Discard an existing cookie
+ *
+ * @param array $cookie An cookie
+ *
+ * @return void
+ *
+ * @access protected
+ */
+ protected function discardCookie ($cookie)
+ {
+ if (!isset($cookie['domain'])
+ || !isset($cookie['path'])
+ || !isset($cookie['path'])
+ ) {
+ throw new CAS_InvalidArgumentException('Invalid Cookie array passed.');
+ }
+
+ foreach ($this->_cookies as $key => $old_cookie) {
+ if ( $cookie['domain'] == $old_cookie['domain']
+ && $cookie['path'] == $old_cookie['path']
+ && $cookie['name'] == $old_cookie['name']
+ ) {
+ unset($this->_cookies[$key]);
+ }
+ }
+ }
+
+ /**
+ * Go through our stored cookies and remove any that are expired.
+ *
+ * @return void
+ *
+ * @access protected
+ */
+ protected function expireCookies ()
+ {
+ foreach ($this->_cookies as $key => $cookie) {
+ if (isset($cookie['expires']) && $cookie['expires'] < time()) {
+ unset($this->_cookies[$key]);
+ }
+ }
+ }
+
+ /**
+ * Answer true if cookie is applicable to a target.
+ *
+ * @param array $cookie An array of cookie attributes.
+ * @param array $target An array of URL attributes as generated by parse_url().
+ *
+ * @return bool
+ *
+ * @access private
+ */
+ protected function cookieMatchesTarget ($cookie, $target)
+ {
+ if (!is_array($target)) {
+ throw new CAS_InvalidArgumentException('$target must be an array of URL attributes as generated by parse_url().');
+ }
+ if (!isset($target['host'])) {
+ throw new CAS_InvalidArgumentException('$target must be an array of URL attributes as generated by parse_url().');
+ }
+
+ // Verify that the scheme matches
+ if ($cookie['secure'] && $target['scheme'] != 'https') {
+ return false;
+ }
+
+ // Verify that the host matches
+ // Match domain and mulit-host cookies
+ if (strpos($cookie['domain'], '.') === 0) {
+ // .host.domain.edu cookies are valid for host.domain.edu
+ if (substr($cookie['domain'], 1) == $target['host']) {
+ // continue with other checks
+ } else {
+ // non-exact host-name matches.
+ // check that the target host a.b.c.edu is within .b.c.edu
+ $pos = strripos($target['host'], $cookie['domain']);
+ if (!$pos) {
+ return false;
+ }
+ // verify that the cookie domain is the last part of the host.
+ if ($pos + strlen($cookie['domain']) != strlen($target['host'])) {
+ return false;
+ }
+ // verify that the host name does not contain interior dots as per
+ // RFC 2965 section 3.3.2 Rejecting Cookies
+ // http://www.ietf.org/rfc/rfc2965.txt
+ $hostname = substr($target['host'], 0, $pos);
+ if (strpos($hostname, '.') !== false) {
+ return false;
+ }
+ }
+ } else {
+ // If the cookie host doesn't begin with '.', the host must case-insensitive
+ // match exactly
+ if (strcasecmp($target['host'], $cookie['domain']) !== 0) {
+ return false;
+ }
+ }
+
+ // Verify that the port matches
+ if (isset($cookie['ports']) && !in_array($target['port'], $cookie['ports'])) {
+ return false;
+ }
+
+ // Verify that the path matches
+ if (strpos($target['path'], $cookie['path']) !== 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+}
+
+?>