Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
324 / 324 |
|
100.00% |
16 / 16 |
CRAP | |
100.00% |
1 / 1 |
ParamValidator | |
100.00% |
324 / 324 |
|
100.00% |
16 / 16 |
111 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
knownTypes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addTypeDefs | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
addTypeDef | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
overrideTypeDef | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
hasTypeDef | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTypeDef | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
normalizeSettingsInternal | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
normalizeSettings | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
checkSettings | |
100.00% |
76 / 76 |
|
100.00% |
1 / 1 |
26 | |||
getValue | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
6 | |||
validateValue | |
100.00% |
80 / 80 |
|
100.00% |
1 / 1 |
26 | |||
getParamInfo | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
11 | |||
getHelpInfo | |
100.00% |
47 / 47 |
|
100.00% |
1 / 1 |
16 | |||
explodeMultiValue | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
implodeMultiValue | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | |
3 | namespace Wikimedia\ParamValidator; |
4 | |
5 | use DomainException; |
6 | use InvalidArgumentException; |
7 | use Wikimedia\Assert\Assert; |
8 | use Wikimedia\Message\DataMessageValue; |
9 | use Wikimedia\Message\MessageValue; |
10 | use Wikimedia\Message\ParamType; |
11 | use Wikimedia\Message\ScalarParam; |
12 | use Wikimedia\ObjectFactory\ObjectFactory; |
13 | |
14 | /** |
15 | * Service for formatting and validating API parameters |
16 | * |
17 | * A settings array is simply an array with keys being the relevant PARAM_* |
18 | * constants from this class, TypeDef, and its subclasses. |
19 | * |
20 | * As a general overview of the architecture here: |
21 | * - ParamValidator handles some general validation of the parameter, |
22 | * then hands off to a TypeDef subclass to validate the specific representation |
23 | * based on the parameter's type. |
24 | * - TypeDef subclasses handle conversion between the string representation |
25 | * submitted by the client and the output PHP data types, validating that the |
26 | * strings are valid representations of the intended type as they do so. |
27 | * - ValidationException is used to report fatal errors in the validation back |
28 | * to the caller, since the return value represents the successful result of |
29 | * the validation and might be any type or class. |
30 | * - The Callbacks interface allows ParamValidator to reach out and fetch data |
31 | * it needs to perform the validation. Currently that includes: |
32 | * - Fetching the value of the parameter being validated (largely since a generic |
33 | * caller cannot know whether it needs to fetch a string from $_GET/$_POST or |
34 | * an array from $_FILES). |
35 | * - Reporting of non-fatal warnings back to the caller. |
36 | * - Fetching the "high limits" flag when necessary, to avoid the need for loading |
37 | * the user unnecessarily. |
38 | * |
39 | * @since 1.34 |
40 | * @unstable |
41 | */ |
42 | class ParamValidator { |
43 | |
44 | // region Constants for parameter settings arrays |
45 | /** @name Constants for parameter settings arrays |
46 | * These constants are keys in the settings array that define how the |
47 | * parameters coming in from the request are to be interpreted. |
48 | * |
49 | * If a constant is associated with a failure code, the failure code |
50 | * and data are described. ValidationExceptions are typically thrown, but |
51 | * those indicated as "non-fatal" are instead passed to |
52 | * Callbacks::recordCondition(). |
53 | * |
54 | * Additional constants may be defined by TypeDef subclasses, or by other |
55 | * libraries for controlling things like auto-generated parameter documentation. |
56 | * For purposes of namespacing the constants, the values of all constants |
57 | * defined by this library begin with 'param-'. |
58 | * |
59 | * @{ |
60 | */ |
61 | |
62 | /** |
63 | * (mixed) Default value of the parameter. If omitted, null is the default. |
64 | * |
65 | * TypeDef::validate() will be informed when the default value was used by the presence of |
66 | * 'is-default' in $options. |
67 | */ |
68 | public const PARAM_DEFAULT = 'param-default'; |
69 | |
70 | /** |
71 | * (string|array) Type of the parameter. |
72 | * Must be a registered type or an array of enumerated values (in which case the "enum" |
73 | * type must be registered). If omitted, the default is the PHP type of the default value |
74 | * (see PARAM_DEFAULT). |
75 | */ |
76 | public const PARAM_TYPE = 'param-type'; |
77 | |
78 | /** |
79 | * (bool) Indicate that the parameter is required. |
80 | * |
81 | * Failure codes: |
82 | * - 'missingparam': The parameter is omitted/empty (and no default was set). No data. |
83 | */ |
84 | public const PARAM_REQUIRED = 'param-required'; |
85 | |
86 | /** |
87 | * (bool) Indicate that the parameter is multi-valued. |
88 | * |
89 | * A multi-valued parameter may be submitted in one of several formats. All |
90 | * of the following result in a value of `[ 'a', 'b', 'c' ]`. |
91 | * - "a|b|c", i.e. pipe-separated. |
92 | * - "\x1Fa\x1Fb\x1Fc", i.e. separated by U+001F, with a signalling U+001F at the start. |
93 | * - As a string[], e.g. from a query string like "foo[]=a&foo[]=b&foo[]=c". |
94 | * |
95 | * Each of the multiple values is passed individually to the TypeDef. |
96 | * $options will contain a 'values-list' key holding the entire list. |
97 | * |
98 | * By default duplicates are removed from the resulting parameter list. Use |
99 | * PARAM_ALLOW_DUPLICATES to override that behavior. |
100 | * |
101 | * Failure codes: |
102 | * - 'toomanyvalues': More values were supplied than are allowed. See |
103 | * PARAM_ISMULTI_LIMIT1, PARAM_ISMULTI_LIMIT2, and constructor option |
104 | * 'ismultiLimits'. Data: |
105 | * - 'limit': The limit currently in effect. |
106 | * - 'lowlimit': The limit when high limits are not allowed. |
107 | * - 'highlimit': The limit when high limits are allowed. |
108 | * - 'unrecognizedvalues': Non-fatal. Invalid values were passed and |
109 | * PARAM_IGNORE_UNRECOGNIZED_VALUES was set. Data: |
110 | * - 'values': The unrecognized values. |
111 | */ |
112 | public const PARAM_ISMULTI = 'param-ismulti'; |
113 | |
114 | /** |
115 | * (int) Maximum number of multi-valued parameter values allowed |
116 | * |
117 | * @see PARAM_ISMULTI |
118 | */ |
119 | public const PARAM_ISMULTI_LIMIT1 = 'param-ismulti-limit1'; |
120 | |
121 | /** |
122 | * (int) Maximum number of multi-valued parameter values allowed for users |
123 | * allowed high limits. |
124 | * |
125 | * @see PARAM_ISMULTI |
126 | */ |
127 | public const PARAM_ISMULTI_LIMIT2 = 'param-ismulti-limit2'; |
128 | |
129 | /** |
130 | * (bool|string) Whether a magic "all values" value exists for multi-valued |
131 | * enumerated types, and if so what that value is. |
132 | * |
133 | * When PARAM_TYPE has a defined set of values and PARAM_ISMULTI is true, |
134 | * this allows for an asterisk ('*') to be passed in place of a pipe-separated list of |
135 | * every possible value. If a string is set, it will be used in place of the asterisk. |
136 | */ |
137 | public const PARAM_ALL = 'param-all'; |
138 | |
139 | /** |
140 | * (bool) Allow the same value to be set more than once when PARAM_ISMULTI is true? |
141 | * |
142 | * If not truthy, the set of values will be passed through |
143 | * `array_values( array_unique() )`. The default is falsey. |
144 | */ |
145 | public const PARAM_ALLOW_DUPLICATES = 'param-allow-duplicates'; |
146 | |
147 | /** |
148 | * (bool) Indicate that the parameter's value should not be logged. |
149 | * |
150 | * Failure codes: (non-fatal) |
151 | * - 'param-sensitive': Always recorded when the parameter is used. |
152 | */ |
153 | public const PARAM_SENSITIVE = 'param-sensitive'; |
154 | |
155 | /** |
156 | * (bool) Indicate that a deprecated parameter was used. |
157 | * |
158 | * Failure codes: (non-fatal) |
159 | * - 'param-deprecated': Always recorded when the parameter is used. |
160 | */ |
161 | public const PARAM_DEPRECATED = 'param-deprecated'; |
162 | |
163 | /** |
164 | * (bool) Whether to downgrade "badvalue" errors to non-fatal when validating multi-valued |
165 | * parameters. |
166 | * @see PARAM_ISMULTI |
167 | */ |
168 | public const PARAM_IGNORE_UNRECOGNIZED_VALUES = 'param-ignore-unrecognized-values'; |
169 | |
170 | /** @} */ |
171 | // endregion -- end of Constants for parameter settings arrays |
172 | |
173 | /** Magic "all values" value when PARAM_ALL is true. */ |
174 | public const ALL_DEFAULT_STRING = '*'; |
175 | |
176 | /** A list of standard type names and types that may be passed as `$typeDefs` to __construct(). */ |
177 | public static $STANDARD_TYPES = [ |
178 | 'boolean' => [ 'class' => TypeDef\BooleanDef::class ], |
179 | 'checkbox' => [ 'class' => TypeDef\PresenceBooleanDef::class ], |
180 | 'integer' => [ 'class' => TypeDef\IntegerDef::class ], |
181 | 'limit' => [ 'class' => TypeDef\LimitDef::class ], |
182 | 'float' => [ 'class' => TypeDef\FloatDef::class ], |
183 | 'double' => [ 'class' => TypeDef\FloatDef::class ], |
184 | 'string' => [ 'class' => TypeDef\StringDef::class ], |
185 | 'password' => [ 'class' => TypeDef\PasswordDef::class ], |
186 | 'NULL' => [ |
187 | 'class' => TypeDef\StringDef::class, |
188 | 'args' => [ [ |
189 | 'allowEmptyWhenRequired' => true, |
190 | ] ], |
191 | ], |
192 | 'timestamp' => [ 'class' => TypeDef\TimestampDef::class ], |
193 | 'upload' => [ 'class' => TypeDef\UploadDef::class ], |
194 | 'enum' => [ 'class' => TypeDef\EnumDef::class ], |
195 | 'expiry' => [ 'class' => TypeDef\ExpiryDef::class ], |
196 | ]; |
197 | |
198 | /** @var Callbacks */ |
199 | private $callbacks; |
200 | |
201 | /** @var ObjectFactory */ |
202 | private $objectFactory; |
203 | |
204 | /** @var (TypeDef|array)[] Map parameter type names to TypeDef objects or ObjectFactory specs */ |
205 | private $typeDefs = []; |
206 | |
207 | /** @var int Default values for PARAM_ISMULTI_LIMIT1 */ |
208 | private $ismultiLimit1; |
209 | |
210 | /** @var int Default values for PARAM_ISMULTI_LIMIT2 */ |
211 | private $ismultiLimit2; |
212 | |
213 | /** |
214 | * @param Callbacks $callbacks |
215 | * @param ObjectFactory $objectFactory To turn specs into TypeDef objects |
216 | * @param array $options Associative array of additional settings |
217 | * - 'typeDefs': (array) As for addTypeDefs(). If omitted, self::$STANDARD_TYPES will be used. |
218 | * Pass an empty array if you want to start with no registered types. |
219 | * - 'ismultiLimits': (int[]) Two ints, being the default values for PARAM_ISMULTI_LIMIT1 and |
220 | * PARAM_ISMULTI_LIMIT2. If not given, defaults to `[ 50, 500 ]`. |
221 | */ |
222 | public function __construct( |
223 | Callbacks $callbacks, |
224 | ObjectFactory $objectFactory, |
225 | array $options = [] |
226 | ) { |
227 | $this->callbacks = $callbacks; |
228 | $this->objectFactory = $objectFactory; |
229 | |
230 | $this->addTypeDefs( $options['typeDefs'] ?? self::$STANDARD_TYPES ); |
231 | $this->ismultiLimit1 = $options['ismultiLimits'][0] ?? 50; |
232 | $this->ismultiLimit2 = $options['ismultiLimits'][1] ?? 500; |
233 | } |
234 | |
235 | /** |
236 | * List known type names |
237 | * @return string[] |
238 | */ |
239 | public function knownTypes() { |
240 | return array_keys( $this->typeDefs ); |
241 | } |
242 | |
243 | /** |
244 | * Register multiple type handlers |
245 | * |
246 | * @see addTypeDef() |
247 | * @param array $typeDefs Associative array mapping `$name` to `$typeDef`. |
248 | */ |
249 | public function addTypeDefs( array $typeDefs ) { |
250 | foreach ( $typeDefs as $name => $def ) { |
251 | $this->addTypeDef( $name, $def ); |
252 | } |
253 | } |
254 | |
255 | /** |
256 | * Register a type handler |
257 | * |
258 | * To allow code to omit PARAM_TYPE in settings arrays to derive the type |
259 | * from PARAM_DEFAULT, it is strongly recommended that the following types be |
260 | * registered: "boolean", "integer", "double", "string", "NULL", and "enum". |
261 | * |
262 | * When using ObjectFactory specs, the following extra arguments are passed: |
263 | * - The Callbacks object for this ParamValidator instance. |
264 | * |
265 | * @param string $name Type name |
266 | * @param TypeDef|array $typeDef Type handler or ObjectFactory spec to create one. |
267 | */ |
268 | public function addTypeDef( $name, $typeDef ) { |
269 | Assert::parameterType( |
270 | [ TypeDef::class, 'array' ], |
271 | $typeDef, |
272 | '$typeDef' |
273 | ); |
274 | |
275 | if ( isset( $this->typeDefs[$name] ) ) { |
276 | throw new InvalidArgumentException( "Type '$name' is already registered" ); |
277 | } |
278 | $this->typeDefs[$name] = $typeDef; |
279 | } |
280 | |
281 | /** |
282 | * Register a type handler, overriding any existing handler |
283 | * @see addTypeDef |
284 | * @param string $name Type name |
285 | * @param TypeDef|array|null $typeDef As for addTypeDef, or null to unregister a type. |
286 | */ |
287 | public function overrideTypeDef( $name, $typeDef ) { |
288 | Assert::parameterType( |
289 | [ TypeDef::class, 'array', 'null' ], |
290 | $typeDef, |
291 | '$typeDef' |
292 | ); |
293 | |
294 | if ( $typeDef === null ) { |
295 | unset( $this->typeDefs[$name] ); |
296 | } else { |
297 | $this->typeDefs[$name] = $typeDef; |
298 | } |
299 | } |
300 | |
301 | /** |
302 | * Test if a type is registered |
303 | * @param string $name Type name |
304 | * @return bool |
305 | */ |
306 | public function hasTypeDef( $name ) { |
307 | return isset( $this->typeDefs[$name] ); |
308 | } |
309 | |
310 | /** |
311 | * Get the TypeDef for a type |
312 | * @param string|array $type Any array is considered equivalent to the string "enum". |
313 | * @return TypeDef|null |
314 | */ |
315 | public function getTypeDef( $type ) { |
316 | if ( is_array( $type ) ) { |
317 | $type = 'enum'; |
318 | } |
319 | |
320 | if ( !isset( $this->typeDefs[$type] ) ) { |
321 | return null; |
322 | } |
323 | |
324 | $def = $this->typeDefs[$type]; |
325 | if ( !$def instanceof TypeDef ) { |
326 | $def = $this->objectFactory->createObject( $def, [ |
327 | 'extraArgs' => [ $this->callbacks ], |
328 | 'assertClass' => TypeDef::class, |
329 | ] ); |
330 | $this->typeDefs[$type] = $def; |
331 | } |
332 | |
333 | return $def; |
334 | } |
335 | |
336 | /** |
337 | * Logic shared by normalizeSettings() and checkSettings() |
338 | * @param array|mixed $settings |
339 | * @return array |
340 | */ |
341 | private function normalizeSettingsInternal( $settings ) { |
342 | // Shorthand |
343 | if ( !is_array( $settings ) ) { |
344 | $settings = [ |
345 | self::PARAM_DEFAULT => $settings, |
346 | ]; |
347 | } |
348 | |
349 | // When type is not given, determine it from the type of the PARAM_DEFAULT |
350 | if ( !isset( $settings[self::PARAM_TYPE] ) ) { |
351 | $settings[self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ?? null ); |
352 | } |
353 | |
354 | return $settings; |
355 | } |
356 | |
357 | /** |
358 | * Normalize a parameter settings array |
359 | * @param array|mixed $settings Default value or an array of settings |
360 | * using PARAM_* constants. |
361 | * @return array |
362 | */ |
363 | public function normalizeSettings( $settings ) { |
364 | $settings = $this->normalizeSettingsInternal( $settings ); |
365 | |
366 | $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); |
367 | if ( $typeDef ) { |
368 | $settings = $typeDef->normalizeSettings( $settings ); |
369 | } |
370 | |
371 | return $settings; |
372 | } |
373 | |
374 | /** |
375 | * Validate a parameter settings array |
376 | * |
377 | * This is intended for validation of parameter settings during unit or |
378 | * integration testing, and should implement strict checks. |
379 | * |
380 | * The rest of the code should generally be more permissive. |
381 | * |
382 | * @param string $name Parameter name |
383 | * @param array|mixed $settings Default value or an array of settings |
384 | * using PARAM_* constants. |
385 | * @param array $options Options array, passed through to the TypeDef and Callbacks. |
386 | * @return array |
387 | * - 'issues': (string[]) Errors detected in $settings, as English text. If the settings |
388 | * are valid, this will be the empty array. |
389 | * - 'allowedKeys': (string[]) ParamValidator keys that are allowed in `$settings`. |
390 | * - 'messages': (MessageValue[]) Messages to be checked for existence. |
391 | */ |
392 | public function checkSettings( string $name, $settings, array $options ): array { |
393 | $settings = $this->normalizeSettingsInternal( $settings ); |
394 | $issues = []; |
395 | $allowedKeys = [ |
396 | self::PARAM_TYPE, self::PARAM_DEFAULT, self::PARAM_REQUIRED, self::PARAM_ISMULTI, |
397 | self::PARAM_SENSITIVE, self::PARAM_DEPRECATED, self::PARAM_IGNORE_UNRECOGNIZED_VALUES, |
398 | ]; |
399 | $messages = []; |
400 | |
401 | $type = $settings[self::PARAM_TYPE]; |
402 | $typeDef = null; |
403 | if ( !is_string( $type ) && !is_array( $type ) ) { |
404 | $issues[self::PARAM_TYPE] = 'PARAM_TYPE must be a string or array, got ' . gettype( $type ); |
405 | } else { |
406 | $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); |
407 | if ( !$typeDef ) { |
408 | if ( is_array( $type ) ) { |
409 | $type = 'enum'; |
410 | } |
411 | $issues[self::PARAM_TYPE] = "Unknown/unregistered PARAM_TYPE \"$type\""; |
412 | } |
413 | } |
414 | |
415 | if ( isset( $settings[self::PARAM_DEFAULT] ) ) { |
416 | try { |
417 | $this->validateValue( |
418 | $name, $settings[self::PARAM_DEFAULT], $settings, [ 'is-default' => true ] + $options |
419 | ); |
420 | } catch ( ValidationException $ex ) { |
421 | $issues[self::PARAM_DEFAULT] = 'Value for PARAM_DEFAULT does not validate (code ' |
422 | . $ex->getFailureMessage()->getCode() . ')'; |
423 | } |
424 | } |
425 | |
426 | if ( !is_bool( $settings[self::PARAM_REQUIRED] ?? false ) ) { |
427 | $issues[self::PARAM_REQUIRED] = 'PARAM_REQUIRED must be boolean, got ' |
428 | . gettype( $settings[self::PARAM_REQUIRED] ); |
429 | } |
430 | |
431 | if ( !is_bool( $settings[self::PARAM_ISMULTI] ?? false ) ) { |
432 | $issues[self::PARAM_ISMULTI] = 'PARAM_ISMULTI must be boolean, got ' |
433 | . gettype( $settings[self::PARAM_ISMULTI] ); |
434 | } |
435 | |
436 | if ( !empty( $settings[self::PARAM_ISMULTI] ) ) { |
437 | $allowedKeys = array_merge( $allowedKeys, [ |
438 | self::PARAM_ISMULTI_LIMIT1, self::PARAM_ISMULTI_LIMIT2, |
439 | self::PARAM_ALL, self::PARAM_ALLOW_DUPLICATES |
440 | ] ); |
441 | |
442 | $limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1; |
443 | $limit2 = $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2; |
444 | if ( !is_int( $limit1 ) ) { |
445 | $issues[self::PARAM_ISMULTI_LIMIT1] = 'PARAM_ISMULTI_LIMIT1 must be an integer, got ' |
446 | . gettype( $settings[self::PARAM_ISMULTI_LIMIT1] ); |
447 | } elseif ( $limit1 <= 0 ) { |
448 | $issues[self::PARAM_ISMULTI_LIMIT1] = |
449 | "PARAM_ISMULTI_LIMIT1 must be greater than 0, got $limit1"; |
450 | } |
451 | if ( !is_int( $limit2 ) ) { |
452 | $issues[self::PARAM_ISMULTI_LIMIT2] = 'PARAM_ISMULTI_LIMIT2 must be an integer, got ' |
453 | . gettype( $settings[self::PARAM_ISMULTI_LIMIT2] ); |
454 | } elseif ( $limit2 < $limit1 ) { |
455 | $issues[self::PARAM_ISMULTI_LIMIT2] = |
456 | 'PARAM_ISMULTI_LIMIT2 must be greater than or equal to PARAM_ISMULTI_LIMIT1, but ' |
457 | . "$limit2 < $limit1"; |
458 | } |
459 | |
460 | $all = $settings[self::PARAM_ALL] ?? false; |
461 | if ( !is_string( $all ) && !is_bool( $all ) ) { |
462 | $issues[self::PARAM_ALL] = 'PARAM_ALL must be a string or boolean, got ' . gettype( $all ); |
463 | } elseif ( $all !== false && $typeDef ) { |
464 | if ( $all === true ) { |
465 | $all = self::ALL_DEFAULT_STRING; |
466 | } |
467 | $values = $typeDef->getEnumValues( $name, $settings, $options ); |
468 | if ( !is_array( $values ) ) { |
469 | $issues[self::PARAM_ALL] = 'PARAM_ALL cannot be used with non-enumerated types'; |
470 | } elseif ( in_array( $all, $values, true ) ) { |
471 | $issues[self::PARAM_ALL] = 'Value for PARAM_ALL conflicts with an enumerated value'; |
472 | } |
473 | } |
474 | |
475 | if ( !is_bool( $settings[self::PARAM_ALLOW_DUPLICATES] ?? false ) ) { |
476 | $issues[self::PARAM_ALLOW_DUPLICATES] = 'PARAM_ALLOW_DUPLICATES must be boolean, got ' |
477 | . gettype( $settings[self::PARAM_ALLOW_DUPLICATES] ); |
478 | } |
479 | } |
480 | |
481 | if ( !is_bool( $settings[self::PARAM_SENSITIVE] ?? false ) ) { |
482 | $issues[self::PARAM_SENSITIVE] = 'PARAM_SENSITIVE must be boolean, got ' |
483 | . gettype( $settings[self::PARAM_SENSITIVE] ); |
484 | } |
485 | |
486 | if ( !is_bool( $settings[self::PARAM_DEPRECATED] ?? false ) ) { |
487 | $issues[self::PARAM_DEPRECATED] = 'PARAM_DEPRECATED must be boolean, got ' |
488 | . gettype( $settings[self::PARAM_DEPRECATED] ); |
489 | } |
490 | |
491 | if ( !is_bool( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] ?? false ) ) { |
492 | $issues[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] = 'PARAM_IGNORE_UNRECOGNIZED_VALUES must be ' |
493 | . 'boolean, got ' . gettype( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] ); |
494 | } |
495 | |
496 | $ret = [ 'issues' => $issues, 'allowedKeys' => $allowedKeys, 'messages' => $messages ]; |
497 | if ( $typeDef ) { |
498 | $ret = $typeDef->checkSettings( $name, $settings, $options, $ret ); |
499 | } |
500 | |
501 | return $ret; |
502 | } |
503 | |
504 | /** |
505 | * Fetch and validate a parameter value using a settings array |
506 | * |
507 | * @param string $name Parameter name |
508 | * @param array|mixed $settings Default value or an array of settings |
509 | * using PARAM_* constants. |
510 | * @param array $options Options array, passed through to the TypeDef and Callbacks. |
511 | * - An additional option, 'is-default', will be set when the value comes from PARAM_DEFAULT. |
512 | * @return mixed Validated parameter value |
513 | * @throws ValidationException if the value is invalid |
514 | */ |
515 | public function getValue( $name, $settings, array $options = [] ) { |
516 | $settings = $this->normalizeSettings( $settings ); |
517 | |
518 | $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); |
519 | if ( !$typeDef ) { |
520 | throw new DomainException( |
521 | "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}" |
522 | ); |
523 | } |
524 | |
525 | $value = $typeDef->getValue( $name, $settings, $options ); |
526 | |
527 | if ( $value !== null ) { |
528 | if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) { |
529 | $strValue = $typeDef->stringifyValue( $name, $value, $settings, $options ); |
530 | $this->callbacks->recordCondition( |
531 | DataMessageValue::new( 'paramvalidator-param-sensitive', [], 'param-sensitive' ) |
532 | ->plaintextParams( $name, $strValue ), |
533 | $name, $value, $settings, $options |
534 | ); |
535 | } |
536 | |
537 | // Set a warning if a deprecated parameter has been passed |
538 | if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) { |
539 | $strValue = $typeDef->stringifyValue( $name, $value, $settings, $options ); |
540 | $this->callbacks->recordCondition( |
541 | DataMessageValue::new( 'paramvalidator-param-deprecated', [], 'param-deprecated' ) |
542 | ->plaintextParams( $name, $strValue ), |
543 | $name, $value, $settings, $options |
544 | ); |
545 | } |
546 | } elseif ( isset( $settings[self::PARAM_DEFAULT] ) ) { |
547 | $value = $settings[self::PARAM_DEFAULT]; |
548 | $options['is-default'] = true; |
549 | } |
550 | |
551 | return $this->validateValue( $name, $value, $settings, $options ); |
552 | } |
553 | |
554 | /** |
555 | * Validate a parameter value using a settings array |
556 | * |
557 | * @param string $name Parameter name |
558 | * @param null|mixed $value Parameter value |
559 | * @param array|mixed $settings Default value or an array of settings |
560 | * using PARAM_* constants. |
561 | * @param array $options Options array, passed through to the TypeDef and Callbacks. |
562 | * - An additional option, 'values-list', will be set when processing the |
563 | * values of a multi-valued parameter. |
564 | * @return mixed Validated parameter value(s) |
565 | * @throws ValidationException if the value is invalid |
566 | */ |
567 | public function validateValue( $name, $value, $settings, array $options = [] ) { |
568 | $settings = $this->normalizeSettings( $settings ); |
569 | |
570 | $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); |
571 | if ( !$typeDef ) { |
572 | throw new DomainException( |
573 | "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}" |
574 | ); |
575 | } |
576 | |
577 | if ( $value === null ) { |
578 | if ( !empty( $settings[self::PARAM_REQUIRED] ) ) { |
579 | throw new ValidationException( |
580 | DataMessageValue::new( 'paramvalidator-missingparam', [], 'missingparam' ) |
581 | ->plaintextParams( $name ), |
582 | $name, $value, $settings |
583 | ); |
584 | } |
585 | return null; |
586 | } |
587 | |
588 | // Non-multi |
589 | if ( empty( $settings[self::PARAM_ISMULTI] ) ) { |
590 | if ( is_string( $value ) && substr( $value, 0, 1 ) === "\x1f" ) { |
591 | throw new ValidationException( |
592 | DataMessageValue::new( 'paramvalidator-notmulti', [], 'badvalue' ) |
593 | ->plaintextParams( $name, $value ), |
594 | $name, $value, $settings |
595 | ); |
596 | } |
597 | |
598 | // T326764: If the type of the actual param value is different from |
599 | // the type that is defined via getParamSettings(), throw an exception |
600 | // because this is a type to value mismatch. |
601 | if ( is_array( $value ) && !$typeDef->supportsArrays() ) { |
602 | throw new ValidationException( |
603 | DataMessageValue::new( 'paramvalidator-notmulti', [], 'badvalue' ) |
604 | ->plaintextParams( $name, gettype( $value ) ), |
605 | $name, $value, $settings |
606 | ); |
607 | } |
608 | |
609 | return $typeDef->validate( $name, $value, $settings, $options ); |
610 | } |
611 | |
612 | // Split the multi-value and validate each parameter |
613 | $limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1; |
614 | $limit2 = max( $limit1, $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 ); |
615 | $valuesList = is_array( $value ) ? $value : self::explodeMultiValue( $value, $limit2 + 1 ); |
616 | |
617 | // Handle PARAM_ALL |
618 | $enumValues = $typeDef->getEnumValues( $name, $settings, $options ); |
619 | if ( is_array( $enumValues ) && isset( $settings[self::PARAM_ALL] ) && |
620 | count( $valuesList ) === 1 |
621 | ) { |
622 | $allValue = is_string( $settings[self::PARAM_ALL] ) |
623 | ? $settings[self::PARAM_ALL] |
624 | : self::ALL_DEFAULT_STRING; |
625 | if ( $valuesList[0] === $allValue ) { |
626 | return $enumValues; |
627 | } |
628 | } |
629 | |
630 | // Avoid checking useHighLimits() unless it's actually necessary |
631 | $sizeLimit = ( |
632 | $limit2 > $limit1 && count( $valuesList ) > $limit1 && |
633 | $this->callbacks->useHighLimits( $options ) |
634 | ) ? $limit2 : $limit1; |
635 | if ( count( $valuesList ) > $sizeLimit ) { |
636 | throw new ValidationException( |
637 | DataMessageValue::new( 'paramvalidator-toomanyvalues', [], 'toomanyvalues', [ |
638 | 'parameter' => $name, |
639 | 'limit' => $sizeLimit, |
640 | 'lowlimit' => $limit1, |
641 | 'highlimit' => $limit2, |
642 | ] )->plaintextParams( $name )->numParams( $sizeLimit ), |
643 | $name, $valuesList, $settings |
644 | ); |
645 | } |
646 | |
647 | $options['values-list'] = $valuesList; |
648 | $validValues = []; |
649 | $invalidValues = []; |
650 | foreach ( $valuesList as $v ) { |
651 | try { |
652 | $validValues[] = $typeDef->validate( $name, $v, $settings, $options ); |
653 | } catch ( ValidationException $ex ) { |
654 | if ( $ex->getFailureMessage()->getCode() !== 'badvalue' || |
655 | empty( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] ) |
656 | ) { |
657 | throw $ex; |
658 | } |
659 | $invalidValues[] = $v; |
660 | } |
661 | } |
662 | if ( $invalidValues ) { |
663 | if ( is_array( $value ) ) { |
664 | $value = self::implodeMultiValue( $value ); |
665 | } |
666 | $this->callbacks->recordCondition( |
667 | DataMessageValue::new( 'paramvalidator-unrecognizedvalues', [], 'unrecognizedvalues', [ |
668 | 'values' => $invalidValues, |
669 | ] ) |
670 | ->plaintextParams( $name, $value ) |
671 | ->commaListParams( array_map( static function ( $v ) { |
672 | return new ScalarParam( ParamType::PLAINTEXT, $v ); |
673 | }, $invalidValues ) ) |
674 | ->numParams( count( $invalidValues ) ), |
675 | $name, $value, $settings, $options |
676 | ); |
677 | } |
678 | |
679 | // Throw out duplicates if requested |
680 | if ( empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) { |
681 | $validValues = array_values( array_unique( $validValues ) ); |
682 | } |
683 | |
684 | return $validValues; |
685 | } |
686 | |
687 | /** |
688 | * Describe parameter settings in a machine-readable format. |
689 | * |
690 | * @param string $name Parameter name. |
691 | * @param array|mixed $settings Default value or an array of settings |
692 | * using PARAM_* constants. |
693 | * @param array $options Options array. |
694 | * @return array |
695 | */ |
696 | public function getParamInfo( $name, $settings, array $options ) { |
697 | $settings = $this->normalizeSettings( $settings ); |
698 | $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); |
699 | $info = []; |
700 | |
701 | $info['type'] = $settings[self::PARAM_TYPE]; |
702 | $info['required'] = !empty( $settings[self::PARAM_REQUIRED] ); |
703 | if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) { |
704 | $info['deprecated'] = true; |
705 | } |
706 | if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) { |
707 | $info['sensitive'] = true; |
708 | } |
709 | if ( isset( $settings[self::PARAM_DEFAULT] ) ) { |
710 | $info['default'] = $settings[self::PARAM_DEFAULT]; |
711 | } |
712 | $info['multi'] = !empty( $settings[self::PARAM_ISMULTI] ); |
713 | if ( $info['multi'] ) { |
714 | $info['lowlimit'] = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1; |
715 | $info['highlimit'] = max( |
716 | $info['lowlimit'], $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 |
717 | ); |
718 | $info['limit'] = |
719 | $info['highlimit'] > $info['lowlimit'] && $this->callbacks->useHighLimits( $options ) |
720 | ? $info['highlimit'] |
721 | : $info['lowlimit']; |
722 | |
723 | if ( !empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) { |
724 | $info['allowsduplicates'] = true; |
725 | } |
726 | |
727 | $allSpecifier = $settings[self::PARAM_ALL] ?? false; |
728 | if ( $allSpecifier !== false ) { |
729 | if ( !is_string( $allSpecifier ) ) { |
730 | $allSpecifier = self::ALL_DEFAULT_STRING; |
731 | } |
732 | $info['allspecifier'] = $allSpecifier; |
733 | } |
734 | } |
735 | |
736 | if ( $typeDef ) { |
737 | $info = array_merge( $info, $typeDef->getParamInfo( $name, $settings, $options ) ); |
738 | } |
739 | |
740 | // Filter out nulls (strictly) |
741 | return array_filter( $info, static function ( $v ) { |
742 | return $v !== null; |
743 | } ); |
744 | } |
745 | |
746 | /** |
747 | * Describe parameter settings in human-readable format |
748 | * |
749 | * @param string $name Parameter name being described. |
750 | * @param array|mixed $settings Default value or an array of settings |
751 | * using PARAM_* constants. |
752 | * @param array $options Options array. |
753 | * @return MessageValue[] |
754 | */ |
755 | public function getHelpInfo( $name, $settings, array $options ) { |
756 | $settings = $this->normalizeSettings( $settings ); |
757 | $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); |
758 | |
759 | // Define ordering. Some are overwritten below, some expected from the TypeDef |
760 | $info = [ |
761 | self::PARAM_DEPRECATED => null, |
762 | self::PARAM_REQUIRED => null, |
763 | self::PARAM_SENSITIVE => null, |
764 | self::PARAM_TYPE => null, |
765 | self::PARAM_ISMULTI => null, |
766 | self::PARAM_ISMULTI_LIMIT1 => null, |
767 | self::PARAM_ALL => null, |
768 | self::PARAM_DEFAULT => null, |
769 | ]; |
770 | |
771 | if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) { |
772 | $info[self::PARAM_DEPRECATED] = MessageValue::new( 'paramvalidator-help-deprecated' ); |
773 | } |
774 | |
775 | if ( !empty( $settings[self::PARAM_REQUIRED] ) ) { |
776 | $info[self::PARAM_REQUIRED] = MessageValue::new( 'paramvalidator-help-required' ); |
777 | } |
778 | |
779 | if ( !empty( $settings[self::PARAM_ISMULTI] ) ) { |
780 | $info[self::PARAM_ISMULTI] = MessageValue::new( 'paramvalidator-help-multi-separate' ); |
781 | |
782 | $lowcount = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1; |
783 | $highcount = max( $lowcount, $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 ); |
784 | $values = $typeDef ? $typeDef->getEnumValues( $name, $settings, $options ) : null; |
785 | if ( |
786 | // Only mention the limits if they're likely to matter. |
787 | $values === null || count( $values ) > $lowcount || |
788 | !empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) |
789 | ) { |
790 | if ( $highcount > $lowcount ) { |
791 | $info[self::PARAM_ISMULTI_LIMIT1] = MessageValue::new( 'paramvalidator-help-multi-max' ) |
792 | ->numParams( $lowcount, $highcount ); |
793 | } else { |
794 | $info[self::PARAM_ISMULTI_LIMIT1] = MessageValue::new( 'paramvalidator-help-multi-max-simple' ) |
795 | ->numParams( $lowcount ); |
796 | } |
797 | } |
798 | |
799 | $allSpecifier = $settings[self::PARAM_ALL] ?? false; |
800 | if ( $allSpecifier !== false ) { |
801 | if ( !is_string( $allSpecifier ) ) { |
802 | $allSpecifier = self::ALL_DEFAULT_STRING; |
803 | } |
804 | $info[self::PARAM_ALL] = MessageValue::new( 'paramvalidator-help-multi-all' ) |
805 | ->plaintextParams( $allSpecifier ); |
806 | } |
807 | } |
808 | |
809 | if ( isset( $settings[self::PARAM_DEFAULT] ) && $typeDef ) { |
810 | $value = $typeDef->stringifyValue( $name, $settings[self::PARAM_DEFAULT], $settings, $options ); |
811 | if ( $value === '' ) { |
812 | $info[self::PARAM_DEFAULT] = MessageValue::new( 'paramvalidator-help-default-empty' ); |
813 | } elseif ( $value !== null ) { |
814 | $info[self::PARAM_DEFAULT] = MessageValue::new( 'paramvalidator-help-default' ) |
815 | ->plaintextParams( $value ); |
816 | } |
817 | } |
818 | |
819 | if ( $typeDef ) { |
820 | $info = array_merge( $info, $typeDef->getHelpInfo( $name, $settings, $options ) ); |
821 | } |
822 | |
823 | // Put the default at the very end (the TypeDef may have added extra messages) |
824 | $default = $info[self::PARAM_DEFAULT]; |
825 | unset( $info[self::PARAM_DEFAULT] ); |
826 | $info[self::PARAM_DEFAULT] = $default; |
827 | |
828 | // Filter out nulls |
829 | return array_filter( $info ); |
830 | } |
831 | |
832 | /** |
833 | * Split a multi-valued parameter string, like explode() |
834 | * |
835 | * Note that, unlike explode(), this will return an empty array when given |
836 | * an empty string. |
837 | * |
838 | * @param string $value |
839 | * @param int $limit |
840 | * @return string[] |
841 | */ |
842 | public static function explodeMultiValue( $value, $limit ) { |
843 | if ( $value === '' || $value === "\x1f" ) { |
844 | return []; |
845 | } |
846 | |
847 | if ( substr( $value, 0, 1 ) === "\x1f" ) { |
848 | $sep = "\x1f"; |
849 | $value = substr( $value, 1 ); |
850 | } else { |
851 | $sep = '|'; |
852 | } |
853 | |
854 | return explode( $sep, $value, $limit ); |
855 | } |
856 | |
857 | /** |
858 | * Implode an array as a multi-valued parameter string, like implode() |
859 | * |
860 | * @param array $value |
861 | * @return string |
862 | */ |
863 | public static function implodeMultiValue( array $value ) { |
864 | if ( $value === [ '' ] ) { |
865 | // There's no value that actually returns a single empty string. |
866 | // Best we can do is this that returns two, which will be deduplicated to one. |
867 | return '|'; |
868 | } |
869 | |
870 | foreach ( $value as $v ) { |
871 | if ( strpos( $v, '|' ) !== false ) { |
872 | return "\x1f" . implode( "\x1f", $value ); |
873 | } |
874 | } |
875 | return implode( '|', $value ); |
876 | } |
877 | |
878 | } |