CookieJar.php 9.0 KB

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