Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is 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 7
  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              case 'samesite':
 235                  $cookie[$attributeNameLC] = $attributeValue;
 236                  break;
 237              default:
 238                  $cookie['name'] = $attributeName;
 239                  $cookie['value'] = $attributeValue;
 240              }
 241          }
 242  
 243          return $cookie;
 244      }
 245  
 246      /**
 247       * Add, update, or remove a cookie.
 248       *
 249       * @param array $cookie A cookie array as created by parseCookieHeaders()
 250       *
 251       * @return void
 252       *
 253       * @access protected
 254       */
 255      protected function storeCookie ($cookie)
 256      {
 257          // Discard any old versions of this cookie.
 258          $this->discardCookie($cookie);
 259          $this->_cookies[] = $cookie;
 260  
 261      }
 262  
 263      /**
 264       * Discard an existing cookie
 265       *
 266       * @param array $cookie An cookie
 267       *
 268       * @return void
 269       *
 270       * @access protected
 271       */
 272      protected function discardCookie ($cookie)
 273      {
 274          if (!isset($cookie['domain'])
 275              || !isset($cookie['path'])
 276              || !isset($cookie['path'])
 277          ) {
 278              throw new CAS_InvalidArgumentException('Invalid Cookie array passed.');
 279          }
 280  
 281          foreach ($this->_cookies as $key => $old_cookie) {
 282              if ( $cookie['domain'] == $old_cookie['domain']
 283                  && $cookie['path'] == $old_cookie['path']
 284                  && $cookie['name'] == $old_cookie['name']
 285              ) {
 286                  unset($this->_cookies[$key]);
 287              }
 288          }
 289      }
 290  
 291      /**
 292       * Go through our stored cookies and remove any that are expired.
 293       *
 294       * @return void
 295       *
 296       * @access protected
 297       */
 298      protected function expireCookies ()
 299      {
 300          foreach ($this->_cookies as $key => $cookie) {
 301              if (isset($cookie['expires']) && $cookie['expires'] < time()) {
 302                  unset($this->_cookies[$key]);
 303              }
 304          }
 305      }
 306  
 307      /**
 308       * Answer true if cookie is applicable to a target.
 309       *
 310       * @param array $cookie An array of cookie attributes.
 311       * @param array|false $target An array of URL attributes as generated by parse_url().
 312       *
 313       * @return bool
 314       *
 315       * @access private
 316       */
 317      protected function cookieMatchesTarget ($cookie, $target)
 318      {
 319          if (!is_array($target)) {
 320              throw new CAS_InvalidArgumentException(
 321                  '$target must be an array of URL attributes as generated by parse_url().'
 322              );
 323          }
 324          if (!isset($target['host'])) {
 325              throw new CAS_InvalidArgumentException(
 326                  '$target must be an array of URL attributes as generated by parse_url().'
 327              );
 328          }
 329  
 330          // Verify that the scheme matches
 331          if ($cookie['secure'] && $target['scheme'] != 'https') {
 332              return false;
 333          }
 334  
 335          // Verify that the host matches
 336          // Match domain and mulit-host cookies
 337          if (strpos($cookie['domain'], '.') === 0) {
 338              // .host.domain.edu cookies are valid for host.domain.edu
 339              if (substr($cookie['domain'], 1) == $target['host']) {
 340                  // continue with other checks
 341              } else {
 342                  // non-exact host-name matches.
 343                  // check that the target host a.b.c.edu is within .b.c.edu
 344                  $pos = strripos($target['host'], $cookie['domain']);
 345                  if (!$pos) {
 346                      return false;
 347                  }
 348                  // verify that the cookie domain is the last part of the host.
 349                  if ($pos + strlen($cookie['domain']) != strlen($target['host'])) {
 350                      return false;
 351                  }
 352                  // verify that the host name does not contain interior dots as per
 353                  // RFC 2965 section 3.3.2  Rejecting Cookies
 354                  // http://www.ietf.org/rfc/rfc2965.txt
 355                  $hostname = substr($target['host'], 0, $pos);
 356                  if (strpos($hostname, '.') !== false) {
 357                      return false;
 358                  }
 359              }
 360          } else {
 361              // If the cookie host doesn't begin with '.',
 362              // the host must case-insensitive match exactly
 363              if (strcasecmp($target['host'], $cookie['domain']) !== 0) {
 364                  return false;
 365              }
 366          }
 367  
 368          // Verify that the port matches
 369          if (isset($cookie['ports'])
 370              && !in_array($target['port'], $cookie['ports'])
 371          ) {
 372              return false;
 373          }
 374  
 375          // Verify that the path matches
 376          if (strpos($target['path'], $cookie['path']) !== 0) {
 377              return false;
 378          }
 379  
 380          return true;
 381      }
 382  
 383  }
 384  
 385  ?>