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