1<?php 2 3namespace GuzzleHttp\Cookie; 4 5use Psr\Http\Message\RequestInterface; 6use Psr\Http\Message\ResponseInterface; 7 8/** 9 * Cookie jar that stores cookies as an array 10 */ 11class 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 public function toArray(): array 100 { 101 return \array_map(static function (SetCookie $cookie): array { 102 return $cookie->toArray(); 103 }, $this->getIterator()->getArrayCopy()); 104 } 105 106 public function clear(string $domain = null, string $path = null, string $name = null): void 107 { 108 if (!$domain) { 109 $this->cookies = []; 110 111 return; 112 } elseif (!$path) { 113 $this->cookies = \array_filter( 114 $this->cookies, 115 static function (SetCookie $cookie) use ($domain): bool { 116 return !$cookie->matchesDomain($domain); 117 } 118 ); 119 } elseif (!$name) { 120 $this->cookies = \array_filter( 121 $this->cookies, 122 static function (SetCookie $cookie) use ($path, $domain): bool { 123 return !($cookie->matchesPath($path) 124 && $cookie->matchesDomain($domain)); 125 } 126 ); 127 } else { 128 $this->cookies = \array_filter( 129 $this->cookies, 130 static function (SetCookie $cookie) use ($path, $domain, $name) { 131 return !($cookie->getName() == $name 132 && $cookie->matchesPath($path) 133 && $cookie->matchesDomain($domain)); 134 } 135 ); 136 } 137 } 138 139 public function clearSessionCookies(): void 140 { 141 $this->cookies = \array_filter( 142 $this->cookies, 143 static function (SetCookie $cookie): bool { 144 return !$cookie->getDiscard() && $cookie->getExpires(); 145 } 146 ); 147 } 148 149 public function setCookie(SetCookie $cookie): bool 150 { 151 // If the name string is empty (but not 0), ignore the set-cookie 152 // string entirely. 153 $name = $cookie->getName(); 154 if (!$name && $name !== '0') { 155 return false; 156 } 157 158 // Only allow cookies with set and valid domain, name, value 159 $result = $cookie->validate(); 160 if ($result !== true) { 161 if ($this->strictMode) { 162 throw new \RuntimeException('Invalid cookie: '.$result); 163 } 164 $this->removeCookieIfEmpty($cookie); 165 166 return false; 167 } 168 169 // Resolve conflicts with previously set cookies 170 foreach ($this->cookies as $i => $c) { 171 // Two cookies are identical, when their path, and domain are 172 // identical. 173 if ($c->getPath() != $cookie->getPath() 174 || $c->getDomain() != $cookie->getDomain() 175 || $c->getName() != $cookie->getName() 176 ) { 177 continue; 178 } 179 180 // The previously set cookie is a discard cookie and this one is 181 // not so allow the new cookie to be set 182 if (!$cookie->getDiscard() && $c->getDiscard()) { 183 unset($this->cookies[$i]); 184 continue; 185 } 186 187 // If the new cookie's expiration is further into the future, then 188 // replace the old cookie 189 if ($cookie->getExpires() > $c->getExpires()) { 190 unset($this->cookies[$i]); 191 continue; 192 } 193 194 // If the value has changed, we better change it 195 if ($cookie->getValue() !== $c->getValue()) { 196 unset($this->cookies[$i]); 197 continue; 198 } 199 200 // The cookie exists, so no need to continue 201 return false; 202 } 203 204 $this->cookies[] = $cookie; 205 206 return true; 207 } 208 209 public function count(): int 210 { 211 return \count($this->cookies); 212 } 213 214 /** 215 * @return \ArrayIterator<int, SetCookie> 216 */ 217 public function getIterator(): \ArrayIterator 218 { 219 return new \ArrayIterator(\array_values($this->cookies)); 220 } 221 222 public function extractCookies(RequestInterface $request, ResponseInterface $response): void 223 { 224 if ($cookieHeader = $response->getHeader('Set-Cookie')) { 225 foreach ($cookieHeader as $cookie) { 226 $sc = SetCookie::fromString($cookie); 227 if (!$sc->getDomain()) { 228 $sc->setDomain($request->getUri()->getHost()); 229 } 230 if (0 !== \strpos($sc->getPath(), '/')) { 231 $sc->setPath($this->getCookiePathFromRequest($request)); 232 } 233 if (!$sc->matchesDomain($request->getUri()->getHost())) { 234 continue; 235 } 236 // Note: At this point `$sc->getDomain()` being a public suffix should 237 // be rejected, but we don't want to pull in the full PSL dependency. 238 $this->setCookie($sc); 239 } 240 } 241 } 242 243 /** 244 * Computes cookie path following RFC 6265 section 5.1.4 245 * 246 * @see https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4 247 */ 248 private function getCookiePathFromRequest(RequestInterface $request): string 249 { 250 $uriPath = $request->getUri()->getPath(); 251 if ('' === $uriPath) { 252 return '/'; 253 } 254 if (0 !== \strpos($uriPath, '/')) { 255 return '/'; 256 } 257 if ('/' === $uriPath) { 258 return '/'; 259 } 260 $lastSlashPos = \strrpos($uriPath, '/'); 261 if (0 === $lastSlashPos || false === $lastSlashPos) { 262 return '/'; 263 } 264 265 return \substr($uriPath, 0, $lastSlashPos); 266 } 267 268 public function withCookieHeader(RequestInterface $request): RequestInterface 269 { 270 $values = []; 271 $uri = $request->getUri(); 272 $scheme = $uri->getScheme(); 273 $host = $uri->getHost(); 274 $path = $uri->getPath() ?: '/'; 275 276 foreach ($this->cookies as $cookie) { 277 if ($cookie->matchesPath($path) 278 && $cookie->matchesDomain($host) 279 && !$cookie->isExpired() 280 && (!$cookie->getSecure() || $scheme === 'https') 281 ) { 282 $values[] = $cookie->getName().'=' 283 .$cookie->getValue(); 284 } 285 } 286 287 return $values 288 ? $request->withHeader('Cookie', \implode('; ', $values)) 289 : $request; 290 } 291 292 /** 293 * If a cookie already exists and the server asks to set it again with a 294 * null value, the cookie must be deleted. 295 */ 296 private function removeCookieIfEmpty(SetCookie $cookie): void 297 { 298 $cookieValue = $cookie->getValue(); 299 if ($cookieValue === null || $cookieValue === '') { 300 $this->clear( 301 $cookie->getDomain(), 302 $cookie->getPath(), 303 $cookie->getName() 304 ); 305 } 306 } 307} 308