vendor/symfony/http-foundation/Cookie.php line 19

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpFoundation;
  11. /**
  12. * Represents a cookie.
  13. *
  14. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  15. */
  16. class Cookie
  17. {
  18. public const SAMESITE_NONE = 'none';
  19. public const SAMESITE_LAX = 'lax';
  20. public const SAMESITE_STRICT = 'strict';
  21. protected $name;
  22. protected $value;
  23. protected $domain;
  24. protected $expire;
  25. protected $path;
  26. protected $secure;
  27. protected $httpOnly;
  28. private $raw;
  29. private $sameSite;
  30. private $secureDefault = false;
  31. private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f";
  32. private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"];
  33. private const RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C'];
  34. // same list as above minus "=", which PHP allows in the path and domain attributes
  35. private const RESERVED_ATTR_CHARS_LIST = ",; \t\r\n\v\f";
  36. /**
  37. * Creates cookie from raw header string.
  38. *
  39. * @return static
  40. */
  41. public static function fromString(string $cookie, bool $decode = false)
  42. {
  43. $data = [
  44. 'expires' => 0,
  45. 'path' => '/',
  46. 'domain' => null,
  47. 'secure' => false,
  48. 'httponly' => false,
  49. 'raw' => !$decode,
  50. 'samesite' => null,
  51. ];
  52. $parts = HeaderUtils::split($cookie, ';=');
  53. $part = array_shift($parts);
  54. $name = $decode ? urldecode($part[0]) : $part[0];
  55. $value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null;
  56. $data = HeaderUtils::combine($parts) + $data;
  57. $data['expires'] = self::expiresTimestamp($data['expires']);
  58. if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) {
  59. $data['expires'] = time() + (int) $data['max-age'];
  60. }
  61. return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
  62. }
  63. public static function create(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self
  64. {
  65. return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite);
  66. }
  67. /**
  68. * @param string $name The name of the cookie
  69. * @param string|null $value The value of the cookie
  70. * @param int|string|\DateTimeInterface $expire The time the cookie expires
  71. * @param string|null $path The path on the server in which the cookie will be available on
  72. * @param string|null $domain The domain that the cookie is available to
  73. * @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS
  74. * @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol
  75. * @param bool $raw Whether the cookie value should be sent with no url encoding
  76. * @param string|null $sameSite Whether the cookie will be available for cross-site requests
  77. *
  78. * @throws \InvalidArgumentException
  79. */
  80. public function __construct(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax')
  81. {
  82. // from PHP source code
  83. if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) {
  84. throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
  85. }
  86. if (empty($name)) {
  87. throw new \InvalidArgumentException('The cookie name cannot be empty.');
  88. }
  89. self::validateAttribute('path', $path);
  90. self::validateAttribute('domain', $domain);
  91. $this->name = $name;
  92. $this->value = $value;
  93. $this->domain = $domain;
  94. $this->expire = self::expiresTimestamp($expire);
  95. $this->path = empty($path) ? '/' : $path;
  96. $this->secure = $secure;
  97. $this->httpOnly = $httpOnly;
  98. $this->raw = $raw;
  99. $this->sameSite = $this->withSameSite($sameSite)->sameSite;
  100. }
  101. /**
  102. * Creates a cookie copy with a new value.
  103. *
  104. * @return static
  105. */
  106. public function withValue(?string $value): self
  107. {
  108. $cookie = clone $this;
  109. $cookie->value = $value;
  110. return $cookie;
  111. }
  112. /**
  113. * Creates a cookie copy with a new domain that the cookie is available to.
  114. *
  115. * @return static
  116. */
  117. public function withDomain(?string $domain): self
  118. {
  119. self::validateAttribute('domain', $domain);
  120. $cookie = clone $this;
  121. $cookie->domain = $domain;
  122. return $cookie;
  123. }
  124. /**
  125. * Creates a cookie copy with a new time the cookie expires.
  126. *
  127. * @param int|string|\DateTimeInterface $expire
  128. *
  129. * @return static
  130. */
  131. public function withExpires($expire = 0): self
  132. {
  133. $cookie = clone $this;
  134. $cookie->expire = self::expiresTimestamp($expire);
  135. return $cookie;
  136. }
  137. /**
  138. * Converts expires formats to a unix timestamp.
  139. *
  140. * @param int|string|\DateTimeInterface $expire
  141. */
  142. private static function expiresTimestamp($expire = 0): int
  143. {
  144. // convert expiration time to a Unix timestamp
  145. if ($expire instanceof \DateTimeInterface) {
  146. $expire = $expire->format('U');
  147. } elseif (!is_numeric($expire)) {
  148. $expire = strtotime($expire);
  149. if (false === $expire) {
  150. throw new \InvalidArgumentException('The cookie expiration time is not valid.');
  151. }
  152. }
  153. return 0 < $expire ? (int) $expire : 0;
  154. }
  155. /**
  156. * Rejects the characters that PHP's setcookie() also rejects in the path and domain attributes.
  157. */
  158. private static function validateAttribute(string $attribute, ?string $value): void
  159. {
  160. if (null !== $value && false !== strpbrk($value, self::RESERVED_ATTR_CHARS_LIST)) {
  161. throw new \InvalidArgumentException(\sprintf('The cookie %s "%s" contains invalid characters.', $attribute, $value));
  162. }
  163. }
  164. /**
  165. * Creates a cookie copy with a new path on the server in which the cookie will be available on.
  166. *
  167. * @return static
  168. */
  169. public function withPath(string $path): self
  170. {
  171. self::validateAttribute('path', $path);
  172. $cookie = clone $this;
  173. $cookie->path = '' === $path ? '/' : $path;
  174. return $cookie;
  175. }
  176. /**
  177. * Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client.
  178. *
  179. * @return static
  180. */
  181. public function withSecure(bool $secure = true): self
  182. {
  183. $cookie = clone $this;
  184. $cookie->secure = $secure;
  185. return $cookie;
  186. }
  187. /**
  188. * Creates a cookie copy that be accessible only through the HTTP protocol.
  189. *
  190. * @return static
  191. */
  192. public function withHttpOnly(bool $httpOnly = true): self
  193. {
  194. $cookie = clone $this;
  195. $cookie->httpOnly = $httpOnly;
  196. return $cookie;
  197. }
  198. /**
  199. * Creates a cookie copy that uses no url encoding.
  200. *
  201. * @return static
  202. */
  203. public function withRaw(bool $raw = true): self
  204. {
  205. if ($raw && false !== strpbrk($this->name, self::RESERVED_CHARS_LIST)) {
  206. throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $this->name));
  207. }
  208. $cookie = clone $this;
  209. $cookie->raw = $raw;
  210. return $cookie;
  211. }
  212. /**
  213. * Creates a cookie copy with SameSite attribute.
  214. *
  215. * @return static
  216. */
  217. public function withSameSite(?string $sameSite): self
  218. {
  219. if ('' === $sameSite) {
  220. $sameSite = null;
  221. } elseif (null !== $sameSite) {
  222. $sameSite = strtolower($sameSite);
  223. }
  224. if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) {
  225. throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
  226. }
  227. $cookie = clone $this;
  228. $cookie->sameSite = $sameSite;
  229. return $cookie;
  230. }
  231. /**
  232. * Returns the cookie as a string.
  233. *
  234. * @return string
  235. */
  236. public function __toString()
  237. {
  238. if ($this->isRaw()) {
  239. $str = $this->getName();
  240. } else {
  241. $str = str_replace(self::RESERVED_CHARS_FROM, self::RESERVED_CHARS_TO, $this->getName());
  242. }
  243. $str .= '=';
  244. if ('' === (string) $this->getValue()) {
  245. $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0';
  246. } else {
  247. $str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
  248. if (0 !== $this->getExpiresTime()) {
  249. $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge();
  250. }
  251. }
  252. if ($this->getPath()) {
  253. $str .= '; path='.$this->getPath();
  254. }
  255. if ($this->getDomain()) {
  256. $str .= '; domain='.$this->getDomain();
  257. }
  258. if (true === $this->isSecure()) {
  259. $str .= '; secure';
  260. }
  261. if (true === $this->isHttpOnly()) {
  262. $str .= '; httponly';
  263. }
  264. if (null !== $this->getSameSite()) {
  265. $str .= '; samesite='.$this->getSameSite();
  266. }
  267. return $str;
  268. }
  269. /**
  270. * Gets the name of the cookie.
  271. *
  272. * @return string
  273. */
  274. public function getName()
  275. {
  276. return $this->name;
  277. }
  278. /**
  279. * Gets the value of the cookie.
  280. *
  281. * @return string|null
  282. */
  283. public function getValue()
  284. {
  285. return $this->value;
  286. }
  287. /**
  288. * Gets the domain that the cookie is available to.
  289. *
  290. * @return string|null
  291. */
  292. public function getDomain()
  293. {
  294. return $this->domain;
  295. }
  296. /**
  297. * Gets the time the cookie expires.
  298. *
  299. * @return int
  300. */
  301. public function getExpiresTime()
  302. {
  303. return $this->expire;
  304. }
  305. /**
  306. * Gets the max-age attribute.
  307. *
  308. * @return int
  309. */
  310. public function getMaxAge()
  311. {
  312. $maxAge = $this->expire - time();
  313. return 0 >= $maxAge ? 0 : $maxAge;
  314. }
  315. /**
  316. * Gets the path on the server in which the cookie will be available on.
  317. *
  318. * @return string
  319. */
  320. public function getPath()
  321. {
  322. return $this->path;
  323. }
  324. /**
  325. * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client.
  326. *
  327. * @return bool
  328. */
  329. public function isSecure()
  330. {
  331. return $this->secure ?? $this->secureDefault;
  332. }
  333. /**
  334. * Checks whether the cookie will be made accessible only through the HTTP protocol.
  335. *
  336. * @return bool
  337. */
  338. public function isHttpOnly()
  339. {
  340. return $this->httpOnly;
  341. }
  342. /**
  343. * Whether this cookie is about to be cleared.
  344. *
  345. * @return bool
  346. */
  347. public function isCleared()
  348. {
  349. return 0 !== $this->expire && $this->expire < time();
  350. }
  351. /**
  352. * Checks if the cookie value should be sent with no url encoding.
  353. *
  354. * @return bool
  355. */
  356. public function isRaw()
  357. {
  358. return $this->raw;
  359. }
  360. /**
  361. * Gets the SameSite attribute.
  362. *
  363. * @return string|null
  364. */
  365. public function getSameSite()
  366. {
  367. return $this->sameSite;
  368. }
  369. /**
  370. * @param bool $default The default value of the "secure" flag when it is set to null
  371. */
  372. public function setSecureDefault(bool $default): void
  373. {
  374. $this->secureDefault = $default;
  375. }
  376. }