You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

304 lines
8.3 KiB

  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. const SAMESITE_NONE = 'none';
  19. const SAMESITE_LAX = 'lax';
  20. 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 static $reservedCharsList = "=,; \t\r\n\v\f";
  31. private static $reservedCharsFrom = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"];
  32. private static $reservedCharsTo = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C'];
  33. /**
  34. * Creates cookie from raw header string.
  35. *
  36. * @param string $cookie
  37. * @param bool $decode
  38. *
  39. * @return static
  40. */
  41. public static function fromString($cookie, $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. foreach (explode(';', $cookie) as $part) {
  53. if (false === strpos($part, '=')) {
  54. $key = trim($part);
  55. $value = true;
  56. } else {
  57. list($key, $value) = explode('=', trim($part), 2);
  58. $key = trim($key);
  59. $value = trim($value);
  60. }
  61. if (!isset($data['name'])) {
  62. $data['name'] = $decode ? urldecode($key) : $key;
  63. $data['value'] = true === $value ? null : ($decode ? urldecode($value) : $value);
  64. continue;
  65. }
  66. switch ($key = strtolower($key)) {
  67. case 'name':
  68. case 'value':
  69. break;
  70. case 'max-age':
  71. $data['expires'] = time() + (int) $value;
  72. break;
  73. default:
  74. $data[$key] = $value;
  75. break;
  76. }
  77. }
  78. return new static($data['name'], $data['value'], $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
  79. }
  80. /**
  81. * @param string $name The name of the cookie
  82. * @param string|null $value The value of the cookie
  83. * @param int|string|\DateTimeInterface $expire The time the cookie expires
  84. * @param string $path The path on the server in which the cookie will be available on
  85. * @param string|null $domain The domain that the cookie is available to
  86. * @param bool $secure Whether the cookie should only be transmitted over a secure HTTPS connection from the client
  87. * @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol
  88. * @param bool $raw Whether the cookie value should be sent with no url encoding
  89. * @param string|null $sameSite Whether the cookie will be available for cross-site requests
  90. *
  91. * @throws \InvalidArgumentException
  92. */
  93. public function __construct($name, $value = null, $expire = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true, $raw = false, $sameSite = null)
  94. {
  95. // from PHP source code
  96. if ($raw && false !== strpbrk($name, self::$reservedCharsList)) {
  97. throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
  98. }
  99. if (empty($name)) {
  100. throw new \InvalidArgumentException('The cookie name cannot be empty.');
  101. }
  102. // convert expiration time to a Unix timestamp
  103. if ($expire instanceof \DateTimeInterface) {
  104. $expire = $expire->format('U');
  105. } elseif (!is_numeric($expire)) {
  106. $expire = strtotime($expire);
  107. if (false === $expire) {
  108. throw new \InvalidArgumentException('The cookie expiration time is not valid.');
  109. }
  110. }
  111. $this->name = $name;
  112. $this->value = $value;
  113. $this->domain = $domain;
  114. $this->expire = 0 < $expire ? (int) $expire : 0;
  115. $this->path = empty($path) ? '/' : $path;
  116. $this->secure = (bool) $secure;
  117. $this->httpOnly = (bool) $httpOnly;
  118. $this->raw = (bool) $raw;
  119. if (null !== $sameSite) {
  120. $sameSite = strtolower($sameSite);
  121. }
  122. if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) {
  123. throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
  124. }
  125. $this->sameSite = $sameSite;
  126. }
  127. /**
  128. * Returns the cookie as a string.
  129. *
  130. * @return string The cookie
  131. */
  132. public function __toString()
  133. {
  134. if ($this->isRaw()) {
  135. $str = $this->getName();
  136. } else {
  137. $str = str_replace(self::$reservedCharsFrom, self::$reservedCharsTo, $this->getName());
  138. }
  139. $str .= '=';
  140. if ('' === (string) $this->getValue()) {
  141. $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0';
  142. } else {
  143. $str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
  144. if (0 !== $this->getExpiresTime()) {
  145. $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge();
  146. }
  147. }
  148. if ($this->getPath()) {
  149. $str .= '; path='.$this->getPath();
  150. }
  151. if ($this->getDomain()) {
  152. $str .= '; domain='.$this->getDomain();
  153. }
  154. if (true === $this->isSecure()) {
  155. $str .= '; secure';
  156. }
  157. if (true === $this->isHttpOnly()) {
  158. $str .= '; httponly';
  159. }
  160. if (null !== $this->getSameSite()) {
  161. $str .= '; samesite='.$this->getSameSite();
  162. }
  163. return $str;
  164. }
  165. /**
  166. * Gets the name of the cookie.
  167. *
  168. * @return string
  169. */
  170. public function getName()
  171. {
  172. return $this->name;
  173. }
  174. /**
  175. * Gets the value of the cookie.
  176. *
  177. * @return string|null
  178. */
  179. public function getValue()
  180. {
  181. return $this->value;
  182. }
  183. /**
  184. * Gets the domain that the cookie is available to.
  185. *
  186. * @return string|null
  187. */
  188. public function getDomain()
  189. {
  190. return $this->domain;
  191. }
  192. /**
  193. * Gets the time the cookie expires.
  194. *
  195. * @return int
  196. */
  197. public function getExpiresTime()
  198. {
  199. return $this->expire;
  200. }
  201. /**
  202. * Gets the max-age attribute.
  203. *
  204. * @return int
  205. */
  206. public function getMaxAge()
  207. {
  208. $maxAge = $this->expire - time();
  209. return 0 >= $maxAge ? 0 : $maxAge;
  210. }
  211. /**
  212. * Gets the path on the server in which the cookie will be available on.
  213. *
  214. * @return string
  215. */
  216. public function getPath()
  217. {
  218. return $this->path;
  219. }
  220. /**
  221. * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client.
  222. *
  223. * @return bool
  224. */
  225. public function isSecure()
  226. {
  227. return $this->secure;
  228. }
  229. /**
  230. * Checks whether the cookie will be made accessible only through the HTTP protocol.
  231. *
  232. * @return bool
  233. */
  234. public function isHttpOnly()
  235. {
  236. return $this->httpOnly;
  237. }
  238. /**
  239. * Whether this cookie is about to be cleared.
  240. *
  241. * @return bool
  242. */
  243. public function isCleared()
  244. {
  245. return 0 !== $this->expire && $this->expire < time();
  246. }
  247. /**
  248. * Checks if the cookie value should be sent with no url encoding.
  249. *
  250. * @return bool
  251. */
  252. public function isRaw()
  253. {
  254. return $this->raw;
  255. }
  256. /**
  257. * Gets the SameSite attribute.
  258. *
  259. * @return string|null
  260. */
  261. public function getSameSite()
  262. {
  263. return $this->sameSite;
  264. }
  265. }