Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   1  <?php
   2  
   3  namespace GuzzleHttp\Cookie;
   4  
   5  use Psr\Http\Message\RequestInterface;
   6  use Psr\Http\Message\ResponseInterface;
   7  
   8  /**
   9   * Cookie jar that stores cookies as an array
  10   */
  11  class CookieJar implements CookieJarInterface
  12  {
  13      /**
  14       * @var SetCookie[] Loaded cookie data
  15       */
  16      private $cookies = [];
  17  
  18      /**
  19       * @var bool
  20       */
  21      private $strictMode;
  22  
  23      /**
  24       * @param bool  $strictMode  Set to true to throw exceptions when invalid
  25       *                           cookies are added to the cookie jar.
  26       * @param array $cookieArray Array of SetCookie objects or a hash of
  27       *                           arrays that can be used with the SetCookie
  28       *                           constructor
  29       */
  30      public function __construct(bool $strictMode = false, array $cookieArray = [])
  31      {
  32          $this->strictMode = $strictMode;
  33  
  34          foreach ($cookieArray as $cookie) {
  35              if (!($cookie instanceof SetCookie)) {
  36                  $cookie = new SetCookie($cookie);
  37              }
  38              $this->setCookie($cookie);
  39          }
  40      }
  41  
  42      /**
  43       * Create a new Cookie jar from an associative array and domain.
  44       *
  45       * @param array  $cookies Cookies to create the jar from
  46       * @param string $domain  Domain to set the cookies to
  47       */
  48      public static function fromArray(array $cookies, string $domain): self
  49      {
  50          $cookieJar = new self();
  51          foreach ($cookies as $name => $value) {
  52              $cookieJar->setCookie(new SetCookie([
  53                  'Domain'  => $domain,
  54                  'Name'    => $name,
  55                  'Value'   => $value,
  56                  'Discard' => true
  57              ]));
  58          }
  59  
  60          return $cookieJar;
  61      }
  62  
  63      /**
  64       * Evaluate if this cookie should be persisted to storage
  65       * that survives between requests.
  66       *
  67       * @param SetCookie $cookie              Being evaluated.
  68       * @param bool      $allowSessionCookies If we should persist session cookies
  69       */
  70      public static function shouldPersist(SetCookie $cookie, bool $allowSessionCookies = false): bool
  71      {
  72          if ($cookie->getExpires() || $allowSessionCookies) {
  73              if (!$cookie->getDiscard()) {
  74                  return true;
  75              }
  76          }
  77  
  78          return false;
  79      }
  80  
  81      /**
  82       * Finds and returns the cookie based on the name
  83       *
  84       * @param string $name cookie name to search for
  85       *
  86       * @return SetCookie|null cookie that was found or null if not found
  87       */
  88      public function getCookieByName(string $name): ?SetCookie
  89      {
  90          foreach ($this->cookies as $cookie) {
  91              if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) {
  92                  return $cookie;
  93              }
  94          }
  95  
  96          return null;
  97      }
  98  
  99      /**
 100       * @inheritDoc
 101       */
 102      public function toArray(): array
 103      {
 104          return \array_map(static function (SetCookie $cookie): array {
 105              return $cookie->toArray();
 106          }, $this->getIterator()->getArrayCopy());
 107      }
 108  
 109      /**
 110       * @inheritDoc
 111       */
 112      public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void
 113      {
 114          if (!$domain) {
 115              $this->cookies = [];
 116              return;
 117          } elseif (!$path) {
 118              $this->cookies = \array_filter(
 119                  $this->cookies,
 120                  static function (SetCookie $cookie) use ($domain): bool {
 121                      return !$cookie->matchesDomain($domain);
 122                  }
 123              );
 124          } elseif (!$name) {
 125              $this->cookies = \array_filter(
 126                  $this->cookies,
 127                  static function (SetCookie $cookie) use ($path, $domain): bool {
 128                      return !($cookie->matchesPath($path) &&
 129                          $cookie->matchesDomain($domain));
 130                  }
 131              );
 132          } else {
 133              $this->cookies = \array_filter(
 134                  $this->cookies,
 135                  static function (SetCookie $cookie) use ($path, $domain, $name) {
 136                      return !($cookie->getName() == $name &&
 137                          $cookie->matchesPath($path) &&
 138                          $cookie->matchesDomain($domain));
 139                  }
 140              );
 141          }
 142      }
 143  
 144      /**
 145       * @inheritDoc
 146       */
 147      public function clearSessionCookies(): void
 148      {
 149          $this->cookies = \array_filter(
 150              $this->cookies,
 151              static function (SetCookie $cookie): bool {
 152                  return !$cookie->getDiscard() && $cookie->getExpires();
 153              }
 154          );
 155      }
 156  
 157      /**
 158       * @inheritDoc
 159       */
 160      public function setCookie(SetCookie $cookie): bool
 161      {
 162          // If the name string is empty (but not 0), ignore the set-cookie
 163          // string entirely.
 164          $name = $cookie->getName();
 165          if (!$name && $name !== '0') {
 166              return false;
 167          }
 168  
 169          // Only allow cookies with set and valid domain, name, value
 170          $result = $cookie->validate();
 171          if ($result !== true) {
 172              if ($this->strictMode) {
 173                  throw new \RuntimeException('Invalid cookie: ' . $result);
 174              }
 175              $this->removeCookieIfEmpty($cookie);
 176              return false;
 177          }
 178  
 179          // Resolve conflicts with previously set cookies
 180          foreach ($this->cookies as $i => $c) {
 181              // Two cookies are identical, when their path, and domain are
 182              // identical.
 183              if ($c->getPath() != $cookie->getPath() ||
 184                  $c->getDomain() != $cookie->getDomain() ||
 185                  $c->getName() != $cookie->getName()
 186              ) {
 187                  continue;
 188              }
 189  
 190              // The previously set cookie is a discard cookie and this one is
 191              // not so allow the new cookie to be set
 192              if (!$cookie->getDiscard() && $c->getDiscard()) {
 193                  unset($this->cookies[$i]);
 194                  continue;
 195              }
 196  
 197              // If the new cookie's expiration is further into the future, then
 198              // replace the old cookie
 199              if ($cookie->getExpires() > $c->getExpires()) {
 200                  unset($this->cookies[$i]);
 201                  continue;
 202              }
 203  
 204              // If the value has changed, we better change it
 205              if ($cookie->getValue() !== $c->getValue()) {
 206                  unset($this->cookies[$i]);
 207                  continue;
 208              }
 209  
 210              // The cookie exists, so no need to continue
 211              return false;
 212          }
 213  
 214          $this->cookies[] = $cookie;
 215  
 216          return true;
 217      }
 218  
 219      public function count(): int
 220      {
 221          return \count($this->cookies);
 222      }
 223  
 224      /**
 225       * @return \ArrayIterator<int, SetCookie>
 226       */
 227      public function getIterator(): \ArrayIterator
 228      {
 229          return new \ArrayIterator(\array_values($this->cookies));
 230      }
 231  
 232      public function extractCookies(RequestInterface $request, ResponseInterface $response): void
 233      {
 234          if ($cookieHeader = $response->getHeader('Set-Cookie')) {
 235              foreach ($cookieHeader as $cookie) {
 236                  $sc = SetCookie::fromString($cookie);
 237                  if (!$sc->getDomain()) {
 238                      $sc->setDomain($request->getUri()->getHost());
 239                  }
 240                  if (0 !== \strpos($sc->getPath(), '/')) {
 241                      $sc->setPath($this->getCookiePathFromRequest($request));
 242                  }
 243                  if (!$sc->matchesDomain($request->getUri()->getHost())) {
 244                      continue;
 245                  }
 246                  // Note: At this point `$sc->getDomain()` being a public suffix should
 247                  // be rejected, but we don't want to pull in the full PSL dependency.
 248                  $this->setCookie($sc);
 249              }
 250          }
 251      }
 252  
 253      /**
 254       * Computes cookie path following RFC 6265 section 5.1.4
 255       *
 256       * @link https://tools.ietf.org/html/rfc6265#section-5.1.4
 257       */
 258      private function getCookiePathFromRequest(RequestInterface $request): string
 259      {
 260          $uriPath = $request->getUri()->getPath();
 261          if ('' === $uriPath) {
 262              return '/';
 263          }
 264          if (0 !== \strpos($uriPath, '/')) {
 265              return '/';
 266          }
 267          if ('/' === $uriPath) {
 268              return '/';
 269          }
 270          $lastSlashPos = \strrpos($uriPath, '/');
 271          if (0 === $lastSlashPos || false === $lastSlashPos) {
 272              return '/';
 273          }
 274  
 275          return \substr($uriPath, 0, $lastSlashPos);
 276      }
 277  
 278      public function withCookieHeader(RequestInterface $request): RequestInterface
 279      {
 280          $values = [];
 281          $uri = $request->getUri();
 282          $scheme = $uri->getScheme();
 283          $host = $uri->getHost();
 284          $path = $uri->getPath() ?: '/';
 285  
 286          foreach ($this->cookies as $cookie) {
 287              if ($cookie->matchesPath($path) &&
 288                  $cookie->matchesDomain($host) &&
 289                  !$cookie->isExpired() &&
 290                  (!$cookie->getSecure() || $scheme === 'https')
 291              ) {
 292                  $values[] = $cookie->getName() . '='
 293                      . $cookie->getValue();
 294              }
 295          }
 296  
 297          return $values
 298              ? $request->withHeader('Cookie', \implode('; ', $values))
 299              : $request;
 300      }
 301  
 302      /**
 303       * If a cookie already exists and the server asks to set it again with a
 304       * null value, the cookie must be deleted.
 305       */
 306      private function removeCookieIfEmpty(SetCookie $cookie): void
 307      {
 308          $cookieValue = $cookie->getValue();
 309          if ($cookieValue === null || $cookieValue === '') {
 310              $this->clear(
 311                  $cookie->getDomain(),
 312                  $cookie->getPath(),
 313                  $cookie->getName()
 314              );
 315          }
 316      }
 317  }