1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace SimpleSAML\Assert; |
6
|
|
|
|
7
|
|
|
use DateTimeImmutable; // Requires ext-date |
8
|
|
|
use InvalidArgumentException; |
9
|
|
|
|
10
|
|
|
use function array_map; |
11
|
|
|
use function base64_decode; |
12
|
|
|
use function base64_encode; |
13
|
|
|
use function call_user_func_array; |
14
|
|
|
use function filter_var; |
15
|
|
|
use function implode; |
16
|
|
|
use function in_array; |
17
|
|
|
use function reset; |
18
|
|
|
use function sprintf; |
19
|
|
|
|
20
|
|
|
/** |
|
|
|
|
21
|
|
|
* @package simplesamlphp/assert |
22
|
|
|
*/ |
|
|
|
|
23
|
|
|
rait CustomAssertionTrait |
|
|
|
|
24
|
|
|
{ |
25
|
|
|
private static string $duration_regex = '/^(-?)P(?=.)((\d+)Y)?((\d+)M)?((\d+)D)?(T(?=.)((\d+)H)?((\d+)M)?(\d*(\.\d+)?S)?)?$/i'; |
26
|
|
|
|
27
|
|
|
private static string $qname_regex = '/^[a-zA-Z_][\w.-]*:[a-zA-Z_][\w.-]*$/'; |
28
|
|
|
|
29
|
|
|
private static string $ncname_regex = '/^[a-zA-Z_][\w.-]*$/'; |
30
|
|
|
|
31
|
|
|
private static string $base64_regex = '/^(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?$/i'; |
32
|
|
|
|
33
|
|
|
private static string $uri_same_document_regex = '#^(?:\#([A-Za-z][A-Za-z0-9+\-.]*:(?:\/\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})*)(?::[0-9]*)?(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?|(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|)(?:\?(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?(?:\#(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?|(?:\/\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})*)(?::[0-9]*)?(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?|(?:[A-Za-z0-9\-._~!$&\'()*+,;=@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|)(?:\?(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?(?:\#(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?))$#'; |
34
|
|
|
|
35
|
|
|
private static string $urn_regex = '/\A(?i:urn:(?!urn:)(?<nid>[a-z0-9][a-z0-9-]{1,31}):(?<nss>(?:[-a-z0-9()+,.:=@;$_!*\'&~\/]|%[0-9a-f]{2})+)(?:\?\+(?<rcomponent>.*?))?(?:\?=(?<qcomponent>.*?))?(?:#(?<fcomponent>.*?))?)\z/'; |
36
|
|
|
|
37
|
|
|
private static string $uri_regex = '#[A-Za-z][A-Za-z0-9+\-.]*:(?:\/\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})*)(?::[0-9]*)?(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|\/(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?|(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*|)(?:\?(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?(?:\#(?:[A-Za-z0-9\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?#'; |
38
|
|
|
|
39
|
|
|
private static string $hostname_regex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/'; |
40
|
|
|
|
41
|
|
|
/*********************************************************************************** |
42
|
|
|
* NOTE: Custom assertions may be added below this line. * |
43
|
|
|
* They SHOULD be marked as `private` to ensure the call is forced * |
44
|
|
|
* through __callStatic(). * |
45
|
|
|
* Assertions marked `public` are called directly and will * |
46
|
|
|
* not handle any custom exception passed to it. * |
47
|
|
|
***********************************************************************************/ |
48
|
|
|
|
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @param string $value |
52
|
|
|
*/ |
53
|
|
|
private static function validDuration(string $value, string $message = ''): void |
54
|
|
|
{ |
55
|
|
|
if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$duration_regex]]) === false) { |
56
|
|
|
throw new InvalidArgumentException(sprintf( |
57
|
|
|
$message ?: '\'%s\' is not a valid xs:duration', |
58
|
|
|
$value |
59
|
|
|
)); |
60
|
|
|
} |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Note: This test is not bullet-proof but prevents a string containing illegal characters |
66
|
|
|
* from being passed and ensures the string roughly follows the correct format for a Base64 encoded string |
67
|
|
|
* |
68
|
|
|
* @param string $value |
69
|
|
|
* @param string $message |
70
|
|
|
*/ |
71
|
|
|
private static function stringPlausibleBase64(string $value, string $message = ''): void |
72
|
|
|
{ |
73
|
|
|
$result = true; |
74
|
|
|
|
75
|
|
|
if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$base64_regex]]) === false) { |
76
|
|
|
$result = false; |
77
|
|
|
} elseif (strlen($value) % 4 !== 0) { |
78
|
|
|
$result = false; |
79
|
|
|
} else { |
80
|
|
|
$decoded = base64_decode($value, true); |
81
|
|
|
if ($decoded === false) { |
82
|
|
|
$result = false; |
83
|
|
|
} elseif (base64_encode($decoded) !== $value) { |
84
|
|
|
$result = false; |
85
|
|
|
} |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
if ($result === false) { |
89
|
|
|
throw new InvalidArgumentException(sprintf( |
90
|
|
|
$message ?: '\'%s\' is not a valid Base64 encoded string', |
91
|
|
|
$value |
92
|
|
|
)); |
93
|
|
|
} |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @param string $value |
99
|
|
|
* @param string $message |
100
|
|
|
*/ |
101
|
|
|
private static function validDateTime(string $value, string $message = ''): void |
102
|
|
|
{ |
103
|
|
|
if ( |
104
|
|
|
DateTimeImmutable::createFromFormat(DateTimeImmutable::ISO8601, $value) === false && |
105
|
|
|
DateTimeImmutable::createFromFormat(DateTimeImmutable::RFC3339_EXTENDED, $value) === false |
106
|
|
|
) { |
107
|
|
|
throw new InvalidArgumentException(sprintf( |
108
|
|
|
$message ?: '\'%s\' is not a valid DateTime', |
109
|
|
|
$value |
110
|
|
|
)); |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* @param string $value |
117
|
|
|
* @param string $message |
118
|
|
|
*/ |
119
|
|
|
private static function validDateTimeZulu(string $value, string $message = ''): void |
120
|
|
|
{ |
121
|
|
|
$dateTime1 = DateTimeImmutable::createFromFormat(DateTimeImmutable::ISO8601, $value); |
122
|
|
|
$dateTime2 = DateTimeImmutable::createFromFormat(DateTimeImmutable::RFC3339_EXTENDED, $value); |
123
|
|
|
|
124
|
|
|
$dateTime = $dateTime1 ?: $dateTime2; |
125
|
|
|
if ($dateTime === false) { |
126
|
|
|
throw new InvalidArgumentException(sprintf( |
127
|
|
|
$message ?: '\'%s\' is not a valid DateTime', |
128
|
|
|
$value |
129
|
|
|
)); |
130
|
|
|
} elseif ($dateTime->getTimezone()->getName() !== 'Z') { |
131
|
|
|
throw new InvalidArgumentException(sprintf( |
132
|
|
|
$message ?: '\'%s\' is not a DateTime expressed in the UTC timezone using the \'Z\' timezone identifier.', |
133
|
|
|
$value |
134
|
|
|
)); |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* @param mixed $value |
141
|
|
|
* @param array $values |
142
|
|
|
* @param string $message |
143
|
|
|
*/ |
144
|
|
|
private static function notInArray($value, array $values, string $message = ''): void |
145
|
|
|
{ |
146
|
|
|
if (in_array($value, $values, true)) { |
147
|
|
|
$callable = /** @param mixed $val */function ($val) { |
148
|
|
|
return self::valueToString($val); |
149
|
|
|
}; |
150
|
|
|
|
151
|
|
|
throw new InvalidArgumentException(sprintf( |
152
|
|
|
$message ?: 'Expected none of: %2$s. Got: %s', |
153
|
|
|
self::valueToString($value), |
154
|
|
|
implode(', ', array_map($callable, $values)), |
155
|
|
|
)); |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* @param string $value |
162
|
|
|
* @param string $message |
163
|
|
|
*/ |
164
|
|
|
private static function validURN(string $value, string $message = ''): void |
165
|
|
|
{ |
166
|
|
|
if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$urn_regex]]) === false) { |
167
|
|
|
throw new InvalidArgumentException(sprintf( |
168
|
|
|
$message ?: '\'%s\' is not a valid RFC8141 compliant URN', |
169
|
|
|
$value |
170
|
|
|
)); |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @param string $value |
177
|
|
|
* @param string $message |
178
|
|
|
*/ |
179
|
|
|
private static function validURL(string $value, string $message = ''): void |
180
|
|
|
{ |
181
|
|
|
if (filter_var($value, FILTER_VALIDATE_URL) === false) { |
182
|
|
|
throw new InvalidArgumentException(sprintf( |
183
|
|
|
$message ?: '\'%s\' is not a valid RFC2396 compliant URL', |
184
|
|
|
$value |
185
|
|
|
)); |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @param string $value |
192
|
|
|
* @param string $message |
193
|
|
|
*/ |
194
|
|
|
private static function validURI(string $value, string $message = ''): void |
195
|
|
|
{ |
196
|
|
|
if ( |
197
|
|
|
filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$uri_regex]]) === false && |
198
|
|
|
// We're very lenient here to accept DNS hostnames without a scheme |
199
|
|
|
filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$hostname_regex]]) === false && |
200
|
|
|
filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$uri_same_document_regex]]) === false |
201
|
|
|
) { |
202
|
|
|
throw new InvalidArgumentException(sprintf( |
203
|
|
|
$message ?: '\'%s\' is not a valid RFC3986 compliant URI', |
204
|
|
|
$value |
205
|
|
|
)); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @param string $value |
212
|
|
|
* @param string $message |
213
|
|
|
*/ |
214
|
|
|
private static function validNCName(string $value, string $message = ''): void |
215
|
|
|
{ |
216
|
|
|
if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$ncname_regex]]) === false) { |
217
|
|
|
throw new InvalidArgumentException(sprintf( |
218
|
|
|
$message ?: '\'%s\' is not a valid non-colonized name (NCName)', |
219
|
|
|
$value |
220
|
|
|
)); |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* @param string $value |
227
|
|
|
* @param string $message |
228
|
|
|
*/ |
229
|
|
|
private static function validQName(string $value, string $message = ''): void |
230
|
|
|
{ |
231
|
|
|
if ( |
232
|
|
|
filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$qname_regex]]) === false && |
233
|
|
|
filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$ncname_regex]]) === false |
234
|
|
|
) { |
235
|
|
|
throw new InvalidArgumentException(sprintf( |
236
|
|
|
$message ?: '\'%s\' is not a valid qualified name (QName)', |
237
|
|
|
$value |
238
|
|
|
)); |
239
|
|
|
} |
240
|
|
|
} |
241
|
|
|
} |
242
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.