Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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          foreach ($cookies as $cookie) {
  78              // Enforce the same-origin policy by verifying that the cookie
  79              // would match the url that is setting it
  80              if (!$this->cookieMatchesTarget($cookie, $urlParts)) {
  81                  continue;
  82              }
  83  
  84              // store the cookie
  85              $this->storeCookie($cookie);
  86  
  87              phpCAS::trace($cookie['name'].' -> '.$cookie['value']);
  88          }
  89      }
  90  
  91      /**
  92       * Retrieve cookies applicable for a web service request.
  93       * Cookie applicability is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt
  94       *
  95       * @param string $request_url The url that the cookies will be for.
  96       *
  97       * @return array An array containing cookies. E.g. array('name' => 'val');
  98       *
  99       * @access private
 100       */
 101      public function getCookies ($request_url)
 102      {
 103          if (!count($this->_cookies)) {
 104              return array();
 105          }
 106  
 107          // If our request URL can't be parsed, no cookies apply.
 108          $target = parse_url($request_url);
 109          if ($target === false) {
 110              return array();
 111          }
 112  
 113          $this->expireCookies();
 114  
 115          $matching_cookies = array();
 116          foreach ($this->_cookies as $key => $cookie) {
 117              if ($this->cookieMatchesTarget($cookie, $target)) {
 118                  $matching_cookies[$cookie['name']] = $cookie['value'];
 119              }
 120          }
 121          return $matching_cookies;
 122      }
 123  
 124  
 125      /**
 126       * Parse Cookies without PECL
 127       * From the comments in http://php.net/manual/en/function.http-parse-cookie.php
 128       *
 129       * @param array  $header        array of header lines.
 130       * @param string $defaultDomain The domain to use if none is specified in
 131       * the cookie.
 132       *
 133       * @return array of cookies
 134       */
 135      protected function parseCookieHeaders( $header, $defaultDomain )
 136      {
 137          phpCAS::traceBegin();
 138          $cookies = array();
 139          foreach ( $header as $line ) {
 140              if ( preg_match('/^Set-Cookie2?: /i', $line)) {
 141                  $cookies[] = $this->parseCookieHeader($line, $defaultDomain);
 142              }
 143          }
 144  
 145          phpCAS::traceEnd($cookies);
 146          return $cookies;
 147      }
 148  
 149      /**
 150       * Parse a single cookie header line.
 151       *
 152       * Based on RFC2965 http://www.ietf.org/rfc/rfc2965.txt
 153       *
 154       * @param string $line          The header line.
 155       * @param string $defaultDomain The domain to use if none is specified in
 156       * the cookie.
 157       *
 158       * @return array
 159       */
 160      protected function parseCookieHeader ($line, $defaultDomain)
 161      {
 162          if (!$defaultDomain) {
 163              throw new CAS_InvalidArgumentException(
 164                  '$defaultDomain was not provided.'
 165              );
 166          }
 167  
 168          // Set our default values
 169          $cookie = array(
 170              'domain' => $defaultDomain,
 171              'path' => '/',
 172              'secure' => false,
 173          );
 174  
 175          $line = preg_replace('/^Set-Cookie2?: /i', '', trim($line));
 176  
 177          // trim any trailing semicolons.
 178          $line = trim($line, ';');
 179  
 180          phpCAS::trace("Cookie Line: $line");
 181  
 182          // This implementation makes the assumption that semicolons will not
 183          // be present in quoted attribute values. While attribute values that
 184          // contain semicolons are allowed by RFC2965, they are hopefully rare
 185          // enough to ignore for our purposes. Most browsers make the same
 186          // assumption.
 187          $attributeStrings = explode(';', $line);
 188  
 189          foreach ( $attributeStrings as $attributeString ) {
 190              // split on the first equals sign and use the rest as value
 191              $attributeParts = explode('=', $attributeString, 2);
 192  
 193              $attributeName = trim($attributeParts[0]);
 194              $attributeNameLC = strtolower($attributeName);
 195  
 196              if (isset($attributeParts[1])) {
 197                  $attributeValue = trim($attributeParts[1]);
 198                  // Values may be quoted strings.
 199                  if (strpos($attributeValue, '"') === 0) {
 200                      $attributeValue = trim($attributeValue, '"');
 201                      // unescape any escaped quotes:
 202                      $attributeValue = str_replace('\"', '"', $attributeValue);
 203                  }
 204              } else {
 205                  $attributeValue = null;
 206              }
 207  
 208              switch ($attributeNameLC) {
 209              case 'expires':
 210                  $cookie['expires'] = strtotime($attributeValue);
 211                  break;
 212              case 'max-age':
 213                  $cookie['max-age'] = (int)$attributeValue;
 214                  // Set an expiry time based on the max-age
 215                  if ($cookie['max-age']) {
 216                      $cookie['expires'] = time() + $cookie['max-age'];
 217                  } else {
 218                      // If max-age is zero, then the cookie should be removed
 219                      // imediately so set an expiry before now.
 220                      $cookie['expires'] = time() - 1;
 221                  }
 222                  break;
 223              case 'secure':
 224                  $cookie['secure'] = true;
 225                  break;
 226              case 'domain':
 227              case 'path':
 228              case 'port':
 229              case 'version':
 230              case 'comment':
 231              case 'commenturl':
 232              case 'discard':
 233              case 'httponly':
 234                  $cookie[$attributeNameLC] = $attributeValue;
 235                  break;
 236              default:
 237                  $cookie['name'] = $attributeName;
 238                  $cookie['value'] = $attributeValue;
 239              }
 240          }
 241  
 242          return $cookie;
 243      }
 244  
 245      /**
 246       * Add, update, or remove a cookie.
 247       *
 248       * @param array $cookie A cookie array as created by parseCookieHeaders()
 249       *
 250       * @return void
 251       *
 252       * @access protected
 253       */
 254      protected function storeCookie ($cookie)
 255      {
 256          // Discard any old versions of this cookie.
 257          $this->discardCookie($cookie);
 258          $this->_cookies[] = $cookie;
 259  
 260      }
 261  
 262      /**
 263       * Discard an existing cookie
 264       *
 265       * @param array $cookie An cookie
 266       *
 267       * @return void
 268       *
 269       * @access protected
 270       */
 271      protected function discardCookie ($cookie)
 272      {
 273          if (!isset($cookie['domain'])
 274              || !isset($cookie['path'])
 275              || !isset($cookie['path'])
 276          ) {
 277              throw new CAS_InvalidArgumentException('Invalid Cookie array passed.');
 278          }
 279  
 280          foreach ($this->_cookies as $key => $old_cookie) {
 281              if ( $cookie['domain'] == $old_cookie['domain']
 282                  && $cookie['path'] == $old_cookie['path']
 283                  && $cookie['name'] == $old_cookie['name']
 284              ) {
 285                  unset($this->_cookies[$key]);
 286              }
 287          }
 288      }
 289  
 290      /**
 291       * Go through our stored cookies and remove any that are expired.
 292       *
 293       * @return void
 294       *
 295       * @access protected
 296       */
 297      protected function expireCookies ()
 298      {
 299          foreach ($this->_cookies as $key => $cookie) {
 300              if (isset($cookie['expires']) && $cookie['expires'] < time()) {
 301                  unset($this->_cookies[$key]);
 302              }
 303          }
 304      }
 305  
 306      /**
 307       * Answer true if cookie is applicable to a target.
 308       *
 309       * @param array $cookie An array of cookie attributes.
 310       * @param array|false $target An array of URL attributes as generated by parse_url().
 311       *
 312       * @return bool
 313       *
 314       * @access private
 315       */
 316      protected function cookieMatchesTarget ($cookie, $target)
 317      {
 318          if (!is_array($target)) {
 319              throw new CAS_InvalidArgumentException(
 320                  '$target must be an array of URL attributes as generated by parse_url().'
 321              );
 322          }
 323          if (!isset($target['host'])) {
 324              throw new CAS_InvalidArgumentException(
 325                  '$target must be an array of URL attributes as generated by parse_url().'
 326              );
 327          }
 328  
 329          // Verify that the scheme matches
 330          if ($cookie['secure'] && $target['scheme'] != 'https') {
 331              return false;
 332          }
 333  
 334          // Verify that the host matches
 335          // Match domain and mulit-host cookies
 336          if (strpos($cookie['domain'], '.') === 0) {
 337              // .host.domain.edu cookies are valid for host.domain.edu
 338              if (substr($cookie['domain'], 1) == $target['host']) {
 339                  // continue with other checks
 340              } else {
 341                  // non-exact host-name matches.
 342                  // check that the target host a.b.c.edu is within .b.c.edu
 343                  $pos = strripos($target['host'], $cookie['domain']);
 344                  if (!$pos) {
 345                      return false;
 346                  }
 347                  // verify that the cookie domain is the last part of the host.
 348                  if ($pos + strlen($cookie['domain']) != strlen($target['host'])) {
 349                      return false;
 350                  }
 351                  // verify that the host name does not contain interior dots as per
 352                  // RFC 2965 section 3.3.2  Rejecting Cookies
 353                  // http://www.ietf.org/rfc/rfc2965.txt
 354                  $hostname = substr($target['host'], 0, $pos);
 355                  if (strpos($hostname, '.') !== false) {
 356                      return false;
 357                  }
 358              }
 359          } else {
 360              // If the cookie host doesn't begin with '.',
 361              // the host must case-insensitive match exactly
 362              if (strcasecmp($target['host'], $cookie['domain']) !== 0) {
 363                  return false;
 364              }
 365          }
 366  
 367          // Verify that the port matches
 368          if (isset($cookie['ports'])
 369              && !in_array($target['port'], $cookie['ports'])
 370          ) {
 371              return false;
 372          }
 373  
 374          // Verify that the path matches
 375          if (strpos($target['path'], $cookie['path']) !== 0) {
 376              return false;
 377          }
 378  
 379          return true;
 380      }
 381  
 382  }
 383  
 384  ?>