/src/http-message/src/Cookie/CookieJar.php

https://github.com/zanehy/hyperf · PHP · 312 lines · 214 code · 32 blank · 66 comment · 35 complexity · c7eb8758c933d373462e4678db76882a MD5 · raw file

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