CookieCollection.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * Redistributions of files must retain the above copyright notice.
  8. *
  9. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  10. * @link http://cakephp.org CakePHP(tm) Project
  11. * @since 3.5.0
  12. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  13. */
  14. namespace Cake\Http\Cookie;
  15. use ArrayIterator;
  16. use Cake\Http\Client\Response as ClientResponse;
  17. use Countable;
  18. use DateTime;
  19. use InvalidArgumentException;
  20. use IteratorAggregate;
  21. use Psr\Http\Message\RequestInterface;
  22. use Psr\Http\Message\ResponseInterface;
  23. /**
  24. * Cookie Collection
  25. *
  26. * Provides an immutable collection of cookies objects. Adding or removing
  27. * to a collection returns a *new* collection that you must retain.
  28. */
  29. class CookieCollection implements IteratorAggregate, Countable
  30. {
  31. /**
  32. * Cookie objects
  33. *
  34. * @var Cookie[]
  35. */
  36. protected $cookies = [];
  37. /**
  38. * Constructor
  39. *
  40. * @param array $cookies Array of cookie objects
  41. */
  42. public function __construct(array $cookies = [])
  43. {
  44. $this->checkCookies($cookies);
  45. foreach ($cookies as $cookie) {
  46. $this->cookies[$cookie->getId()] = $cookie;
  47. }
  48. }
  49. /**
  50. * Create a Cookie Collection from an array of Set-Cookie Headers
  51. *
  52. * @param array $header The array of set-cookie header values.
  53. * @return static
  54. */
  55. public static function createFromHeader(array $header)
  56. {
  57. $cookies = static::parseSetCookieHeader($header);
  58. return new CookieCollection($cookies);
  59. }
  60. /**
  61. * Get the number of cookies in the collection.
  62. *
  63. * @return int
  64. */
  65. public function count()
  66. {
  67. return count($this->cookies);
  68. }
  69. /**
  70. * Add a cookie and get an updated collection.
  71. *
  72. * Cookies are stored by id. This means that there can be duplicate
  73. * cookies if a cookie collection is used for cookies across multiple
  74. * domains. This can impact how get(), has() and remove() behave.
  75. *
  76. * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie instance to add.
  77. * @return static
  78. */
  79. public function add(CookieInterface $cookie)
  80. {
  81. $new = clone $this;
  82. $new->cookies[$cookie->getId()] = $cookie;
  83. return $new;
  84. }
  85. /**
  86. * Get the first cookie by name.
  87. *
  88. * @param string $name The name of the cookie.
  89. * @return \Cake\Http\Cookie\CookieInterface|null
  90. */
  91. public function get($name)
  92. {
  93. $key = mb_strtolower($name);
  94. foreach ($this->cookies as $cookie) {
  95. if (mb_strtolower($cookie->getName()) === $key) {
  96. return $cookie;
  97. }
  98. }
  99. return null;
  100. }
  101. /**
  102. * Check if a cookie with the given name exists
  103. *
  104. * @param string $name The cookie name to check.
  105. * @return bool True if the cookie exists, otherwise false.
  106. */
  107. public function has($name)
  108. {
  109. $key = mb_strtolower($name);
  110. foreach ($this->cookies as $cookie) {
  111. if (mb_strtolower($cookie->getName()) === $key) {
  112. return true;
  113. }
  114. }
  115. return false;
  116. }
  117. /**
  118. * Create a new collection with all cookies matching $name removed.
  119. *
  120. * If the cookie is not in the collection, this method will do nothing.
  121. *
  122. * @param string $name The name of the cookie to remove.
  123. * @return static
  124. */
  125. public function remove($name)
  126. {
  127. $new = clone $this;
  128. $key = mb_strtolower($name);
  129. foreach ($new->cookies as $i => $cookie) {
  130. if (mb_strtolower($cookie->getName()) === $key) {
  131. unset($new->cookies[$i]);
  132. }
  133. }
  134. return $new;
  135. }
  136. /**
  137. * Checks if only valid cookie objects are in the array
  138. *
  139. * @param array $cookies Array of cookie objects
  140. * @return void
  141. * @throws \InvalidArgumentException
  142. */
  143. protected function checkCookies(array $cookies)
  144. {
  145. foreach ($cookies as $index => $cookie) {
  146. if (!$cookie instanceof CookieInterface) {
  147. throw new InvalidArgumentException(
  148. sprintf(
  149. 'Expected `%s[]` as $cookies but instead got `%s` at index %d',
  150. static::class,
  151. is_object($cookie) ? get_class($cookie) : gettype($cookie),
  152. $index
  153. )
  154. );
  155. }
  156. }
  157. }
  158. /**
  159. * Gets the iterator
  160. *
  161. * @return \ArrayIterator
  162. */
  163. public function getIterator()
  164. {
  165. return new ArrayIterator($this->cookies);
  166. }
  167. /**
  168. * Add cookies that match the path/domain/expiration to the request.
  169. *
  170. * This allows CookieCollections to be used as a 'cookie jar' in an HTTP client
  171. * situation. Cookies that match the request's domain + path that are not expired
  172. * when this method is called will be applied to the request.
  173. *
  174. * @param \Psr\Http\Message\RequestInterface $request The request to update.
  175. * @param array $extraCookies Associative array of additional cookies to add into the request. This
  176. * is useful when you have cookie data from outside the collection you want to send.
  177. * @return \Psr\Http\Message\RequestInterface An updated request.
  178. */
  179. public function addToRequest(RequestInterface $request, array $extraCookies = [])
  180. {
  181. $uri = $request->getUri();
  182. $cookies = $this->findMatchingCookies(
  183. $uri->getScheme(),
  184. $uri->getHost(),
  185. $uri->getPath()
  186. );
  187. $cookies = array_merge($cookies, $extraCookies);
  188. $cookiePairs = [];
  189. foreach ($cookies as $key => $value) {
  190. $cookiePairs[] = sprintf("%s=%s", rawurlencode($key), rawurlencode($value));
  191. }
  192. return $request->withHeader('Cookie', implode('; ', $cookiePairs));
  193. }
  194. /**
  195. * Find cookies matching the scheme, host, and path
  196. *
  197. * @param string $scheme The http scheme to match
  198. * @param string $host The host to match.
  199. * @param string $path The path to match
  200. * @return array An array of cookie name/value pairs
  201. */
  202. protected function findMatchingCookies($scheme, $host, $path)
  203. {
  204. $out = [];
  205. foreach ($this->cookies as $cookie) {
  206. if ($scheme === 'http' && $cookie->isSecure()) {
  207. continue;
  208. }
  209. if (strpos($path, $cookie->getPath()) !== 0) {
  210. continue;
  211. }
  212. $domain = $cookie->getDomain();
  213. $leadingDot = substr($domain, 0, 1) === '.';
  214. if ($leadingDot) {
  215. $domain = ltrim($domain, '.');
  216. }
  217. $expires = $cookie->getExpiry();
  218. if ($expires && time() > $expires) {
  219. continue;
  220. }
  221. $pattern = '/' . preg_quote($domain, '/') . '$/';
  222. if (!preg_match($pattern, $host)) {
  223. continue;
  224. }
  225. $out[$cookie->getName()] = $cookie->getValue();
  226. }
  227. return $out;
  228. }
  229. /**
  230. * Create a new collection that includes cookies from the response.
  231. *
  232. * @param \Psr\Http\Message\ResponseInterface $response Response to extract cookies from.
  233. * @param \Psr\Http\Message\RequestInterface $request Request to get cookie context from.
  234. * @return static
  235. */
  236. public function addFromResponse(ResponseInterface $response, RequestInterface $request)
  237. {
  238. $uri = $request->getUri();
  239. $host = $uri->getHost();
  240. $path = $uri->getPath() ?: '/';
  241. $cookies = static::parseSetCookieHeader($response->getHeader('Set-Cookie'));
  242. $cookies = $this->setRequestDefaults($cookies, $host, $path);
  243. $new = clone $this;
  244. foreach ($cookies as $cookie) {
  245. $new->cookies[$cookie->getId()] = $cookie;
  246. }
  247. $new->removeExpiredCookies($host, $path);
  248. return $new;
  249. }
  250. /**
  251. * Apply path and host to the set of cookies if they are not set.
  252. *
  253. * @param array $cookies An array of cookies to update.
  254. * @param string $host The host to set.
  255. * @param string $path The path to set.
  256. * @return array An array of updated cookies.
  257. */
  258. protected function setRequestDefaults(array $cookies, $host, $path)
  259. {
  260. $out = [];
  261. foreach ($cookies as $name => $cookie) {
  262. if (!$cookie->getDomain()) {
  263. $cookie = $cookie->withDomain($host);
  264. }
  265. if (!$cookie->getPath()) {
  266. $cookie = $cookie->withPath($path);
  267. }
  268. $out[] = $cookie;
  269. }
  270. return $out;
  271. }
  272. /**
  273. * Parse Set-Cookie headers into array
  274. *
  275. * @param array $values List of Set-Cookie Header values.
  276. * @return \Cake\Http\Cookie\Cookie[] An array of cookie objects
  277. */
  278. protected static function parseSetCookieHeader($values)
  279. {
  280. $cookies = [];
  281. foreach ($values as $value) {
  282. $value = rtrim($value, ';');
  283. $parts = preg_split('/\;[ \t]*/', $value);
  284. $name = false;
  285. $cookie = [
  286. 'value' => '',
  287. 'path' => '',
  288. 'domain' => '',
  289. 'secure' => false,
  290. 'httponly' => false,
  291. 'expires' => null
  292. ];
  293. foreach ($parts as $i => $part) {
  294. if (strpos($part, '=') !== false) {
  295. list($key, $value) = explode('=', $part, 2);
  296. } else {
  297. $key = $part;
  298. $value = true;
  299. }
  300. if ($i === 0) {
  301. $name = $key;
  302. $cookie['value'] = urldecode($value);
  303. continue;
  304. }
  305. $key = strtolower($key);
  306. if (!strlen($cookie[$key])) {
  307. $cookie[$key] = $value;
  308. }
  309. }
  310. $expires = null;
  311. if ($cookie['expires']) {
  312. $expires = new DateTime();
  313. $expires->setTimestamp(strtotime($cookie['expires']));
  314. }
  315. $cookies[] = new Cookie(
  316. $name,
  317. $cookie['value'],
  318. $expires,
  319. $cookie['path'],
  320. $cookie['domain'],
  321. $cookie['secure'],
  322. $cookie['httponly']
  323. );
  324. }
  325. return $cookies;
  326. }
  327. /**
  328. * Remove expired cookies from the collection.
  329. *
  330. * @param string $host The host to check for expired cookies on.
  331. * @param string $path The path to check for expired cookies on.
  332. * @return void
  333. */
  334. protected function removeExpiredCookies($host, $path)
  335. {
  336. $time = time();
  337. $hostPattern = '/' . preg_quote($host, '/') . '$/';
  338. foreach ($this->cookies as $i => $cookie) {
  339. $expires = $cookie->getExpiry();
  340. $expired = ($expires > 0 && $expires < $time);
  341. $pathMatches = strpos($path, $cookie->getPath()) === 0;
  342. $hostMatches = preg_match($hostPattern, $cookie->getDomain());
  343. if ($pathMatches && $hostMatches && $expired) {
  344. unset($this->cookies[$i]);
  345. }
  346. }
  347. }
  348. }