1: | <?php |
2: | |
3: | declare(strict_types=1); |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | namespace Nexus\Cookie; |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | final class CookieValidator |
23: | { |
24: | public const string HOST_PREFIX = '__Host-'; |
25: | public const string SECURE_PREFIX = '__Secure-'; |
26: | |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | |
34: | public const string RESERVED_CHARS_LIST = "()<>@,;:\\\"/[]?={} \t\r\n\f\v"; |
35: | |
36: | |
37: | |
38: | |
39: | |
40: | |
41: | public const array ALLOWED_SAMESITE_VALUES = [ |
42: | CookieInterface::SAMESITE_NONE, |
43: | CookieInterface::SAMESITE_LAX, |
44: | CookieInterface::SAMESITE_STRICT, |
45: | ]; |
46: | |
47: | |
48: | |
49: | |
50: | |
51: | |
52: | public static function validateName(string $name, bool $raw): void |
53: | { |
54: | if ($raw && strpbrk($name, self::RESERVED_CHARS_LIST) !== false) { |
55: | throw new \InvalidArgumentException(\sprintf('Cookie name "%s" contains invalid characters.', $name)); |
56: | } |
57: | |
58: | if ('' === $name) { |
59: | throw new \InvalidArgumentException('Cookie name cannot be empty.'); |
60: | } |
61: | } |
62: | |
63: | |
64: | |
65: | |
66: | public static function validatePrefix(string $prefix, bool $secure, string $path, string $domain): void |
67: | { |
68: | if (str_starts_with($prefix, self::SECURE_PREFIX) && ! $secure) { |
69: | throw new \InvalidArgumentException(\sprintf( |
70: | 'Cookie prefix "%s" must be set with a "Secure" attribute from a secure page.', |
71: | $prefix, |
72: | )); |
73: | } |
74: | |
75: | if (str_starts_with($prefix, self::HOST_PREFIX) && (! $secure || '' !== $domain || '/' !== $path)) { |
76: | throw new \InvalidArgumentException(\sprintf( |
77: | 'Cookie prefix "%s" must be set with a "Secure" attribute from a secure page, with an empty domain, and path of "/".', |
78: | $prefix, |
79: | )); |
80: | } |
81: | } |
82: | |
83: | |
84: | |
85: | |
86: | public static function validatePartitioned(bool $partitioned, bool $secure, string $sameSite, string $prefix): void |
87: | { |
88: | if ($partitioned && ! $secure) { |
89: | throw new \InvalidArgumentException( |
90: | 'Partitioned cookies must be set with a "Secure" attribute.', |
91: | ); |
92: | } |
93: | |
94: | if ($partitioned && CookieInterface::SAMESITE_NONE !== $sameSite) { |
95: | throw new \InvalidArgumentException(\sprintf( |
96: | 'Partitioned cookies must have a SameSite value of "%s".', |
97: | CookieInterface::SAMESITE_NONE, |
98: | )); |
99: | } |
100: | |
101: | if ($partitioned && ! str_starts_with($prefix, self::HOST_PREFIX)) { |
102: | throw new \InvalidArgumentException(\sprintf( |
103: | 'Partitioned cookies must have a prefix of "%s".', |
104: | self::HOST_PREFIX, |
105: | )); |
106: | } |
107: | } |
108: | |
109: | |
110: | |
111: | |
112: | |
113: | |
114: | |
115: | |
116: | public static function validateSameSite(string $sameSite, bool $secure): void |
117: | { |
118: | if (! \in_array($sameSite, self::ALLOWED_SAMESITE_VALUES, true)) { |
119: | throw new \InvalidArgumentException(\sprintf( |
120: | 'Invalid SameSite value "%s". Allowed values are: "%s".', |
121: | $sameSite, |
122: | implode('", "', self::ALLOWED_SAMESITE_VALUES), |
123: | )); |
124: | } |
125: | |
126: | if (CookieInterface::SAMESITE_NONE === $sameSite && ! $secure) { |
127: | throw new \InvalidArgumentException('SameSite=None must be set with a "Secure" attribute.'); |
128: | } |
129: | } |
130: | } |
131: | |