Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.29% |
162 / 170 |
|
81.82% |
9 / 11 |
CRAP | |
0.00% |
0 / 1 |
ApiParamValidator | |
95.29% |
162 / 170 |
|
81.82% |
9 / 11 |
58 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
knownTypes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
mapDeprecatedSettingsMessages | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
normalizeSettings | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
checkSettingsMessage | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
checkSettings | |
92.93% |
92 / 99 |
|
0.00% |
0 / 1 |
34.41 | |||
convertValidationException | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
getValue | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
validateValue | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getParamInfo | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getHelpInfo | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Api\Validator; |
4 | |
5 | use Exception; |
6 | use MediaWiki\Api\ApiBase; |
7 | use MediaWiki\Api\ApiMain; |
8 | use MediaWiki\Api\ApiMessage; |
9 | use MediaWiki\Api\ApiUsageException; |
10 | use MediaWiki\Message\Message; |
11 | use MediaWiki\ParamValidator\TypeDef\NamespaceDef; |
12 | use MediaWiki\ParamValidator\TypeDef\TagsDef; |
13 | use MediaWiki\ParamValidator\TypeDef\TitleDef; |
14 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
15 | use Wikimedia\Message\DataMessageValue; |
16 | use Wikimedia\Message\MessageValue; |
17 | use Wikimedia\ObjectFactory\ObjectFactory; |
18 | use Wikimedia\ParamValidator\ParamValidator; |
19 | use Wikimedia\ParamValidator\TypeDef\EnumDef; |
20 | use Wikimedia\ParamValidator\TypeDef\ExpiryDef; |
21 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
22 | use Wikimedia\ParamValidator\TypeDef\LimitDef; |
23 | use Wikimedia\ParamValidator\TypeDef\PasswordDef; |
24 | use Wikimedia\ParamValidator\TypeDef\PresenceBooleanDef; |
25 | use Wikimedia\ParamValidator\TypeDef\StringDef; |
26 | use Wikimedia\ParamValidator\TypeDef\TimestampDef; |
27 | use Wikimedia\ParamValidator\TypeDef\UploadDef; |
28 | use Wikimedia\ParamValidator\ValidationException; |
29 | use Wikimedia\RequestTimeout\TimeoutException; |
30 | |
31 | /** |
32 | * This wraps a bunch of the API-specific parameter validation logic. |
33 | * |
34 | * It's intended to be used in ApiMain by composition. |
35 | * |
36 | * @since 1.35 |
37 | * @ingroup API |
38 | */ |
39 | class ApiParamValidator { |
40 | |
41 | /** @var ParamValidator */ |
42 | private $paramValidator; |
43 | |
44 | /** Type defs for ParamValidator */ |
45 | private const TYPE_DEFS = [ |
46 | 'boolean' => [ 'class' => PresenceBooleanDef::class ], |
47 | 'enum' => [ 'class' => EnumDef::class ], |
48 | 'expiry' => [ 'class' => ExpiryDef::class ], |
49 | 'integer' => [ 'class' => IntegerDef::class ], |
50 | 'limit' => [ 'class' => LimitDef::class ], |
51 | 'namespace' => [ |
52 | 'class' => NamespaceDef::class, |
53 | 'services' => [ 'NamespaceInfo' ], |
54 | ], |
55 | 'NULL' => [ |
56 | 'class' => StringDef::class, |
57 | 'args' => [ [ |
58 | StringDef::OPT_ALLOW_EMPTY => true, |
59 | ] ], |
60 | ], |
61 | 'password' => [ 'class' => PasswordDef::class ], |
62 | // Unlike 'string', the 'raw' type will not be subject to Unicode |
63 | // NFC normalization. |
64 | 'raw' => [ 'class' => StringDef::class ], |
65 | 'string' => [ 'class' => StringDef::class ], |
66 | 'submodule' => [ 'class' => SubmoduleDef::class ], |
67 | 'tags' => [ |
68 | 'class' => TagsDef::class, |
69 | 'services' => [ 'ChangeTagsStore' ], |
70 | ], |
71 | 'text' => [ 'class' => StringDef::class ], |
72 | 'timestamp' => [ |
73 | 'class' => TimestampDef::class, |
74 | 'args' => [ [ |
75 | 'defaultFormat' => TS_MW, |
76 | ] ], |
77 | ], |
78 | 'title' => [ |
79 | 'class' => TitleDef::class, |
80 | 'services' => [ 'TitleFactory' ], |
81 | ], |
82 | 'user' => [ |
83 | 'class' => UserDef::class, |
84 | 'services' => [ 'UserIdentityLookup', 'TitleParser', 'UserNameUtils' ] |
85 | ], |
86 | 'upload' => [ 'class' => UploadDef::class ], |
87 | ]; |
88 | |
89 | /** |
90 | * @internal |
91 | * @param ApiMain $main |
92 | * @param ObjectFactory $objectFactory |
93 | */ |
94 | public function __construct( ApiMain $main, ObjectFactory $objectFactory ) { |
95 | $this->paramValidator = new ParamValidator( |
96 | new ApiParamValidatorCallbacks( $main ), |
97 | $objectFactory, |
98 | [ |
99 | 'typeDefs' => self::TYPE_DEFS, |
100 | 'ismultiLimits' => [ ApiBase::LIMIT_SML1, ApiBase::LIMIT_SML2 ], |
101 | ] |
102 | ); |
103 | } |
104 | |
105 | /** |
106 | * List known type names |
107 | * @return string[] |
108 | */ |
109 | public function knownTypes(): array { |
110 | return $this->paramValidator->knownTypes(); |
111 | } |
112 | |
113 | /** |
114 | * Map deprecated styles for messages for ParamValidator |
115 | * @param array $settings |
116 | * @return array |
117 | */ |
118 | private function mapDeprecatedSettingsMessages( array $settings ): array { |
119 | if ( isset( $settings[EnumDef::PARAM_DEPRECATED_VALUES] ) ) { |
120 | foreach ( $settings[EnumDef::PARAM_DEPRECATED_VALUES] as &$v ) { |
121 | if ( $v === null || $v === true || $v instanceof MessageValue ) { |
122 | continue; |
123 | } |
124 | |
125 | // Convert the message specification to a DataMessageValue. Flag in the data |
126 | // that it was so converted, so ApiParamValidatorCallbacks::recordCondition() can |
127 | // take that into account. |
128 | $msg = ApiMessage::create( $v ); |
129 | $v = DataMessageValue::new( |
130 | $msg->getKey(), |
131 | $msg->getParams(), |
132 | 'bogus', |
133 | [ '💩' => 'back-compat' ] |
134 | ); |
135 | } |
136 | unset( $v ); |
137 | } |
138 | |
139 | return $settings; |
140 | } |
141 | |
142 | /** |
143 | * Adjust certain settings where ParamValidator differs from historical Action API behavior |
144 | * @param array|mixed $settings |
145 | * @return array |
146 | */ |
147 | public function normalizeSettings( $settings ): array { |
148 | if ( is_array( $settings ) ) { |
149 | if ( !isset( $settings[ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES] ) ) { |
150 | $settings[ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES] = true; |
151 | } |
152 | |
153 | if ( !isset( $settings[IntegerDef::PARAM_IGNORE_RANGE] ) ) { |
154 | $settings[IntegerDef::PARAM_IGNORE_RANGE] = empty( $settings[ApiBase::PARAM_RANGE_ENFORCE] ); |
155 | } |
156 | |
157 | $settings = $this->mapDeprecatedSettingsMessages( $settings ); |
158 | } |
159 | |
160 | return $this->paramValidator->normalizeSettings( $settings ); |
161 | } |
162 | |
163 | /** |
164 | * Check an API settings message |
165 | * @param ApiBase $module |
166 | * @param string $key |
167 | * @param string|array|Message $value Message definition, see Message::newFromSpecifier() |
168 | * @param array &$ret |
169 | */ |
170 | private function checkSettingsMessage( ApiBase $module, string $key, $value, array &$ret ): void { |
171 | try { |
172 | $msg = Message::newFromSpecifier( $value ); |
173 | $ret['messages'][] = MessageValue::newFromSpecifier( $msg ); |
174 | } catch ( TimeoutException $e ) { |
175 | throw $e; |
176 | } catch ( Exception $e ) { |
177 | $ret['issues'][] = "Message specification for $key is not valid"; |
178 | } |
179 | } |
180 | |
181 | /** |
182 | * Check settings for the Action API. |
183 | * @param ApiBase $module |
184 | * @param array $params All module params to test |
185 | * @param string $name Parameter to test |
186 | * @param array $options Options array |
187 | * @return array As for ParamValidator::checkSettings() |
188 | */ |
189 | public function checkSettings( |
190 | ApiBase $module, array $params, string $name, array $options |
191 | ): array { |
192 | $options['module'] = $module; |
193 | $settings = $params[$name]; |
194 | if ( is_array( $settings ) ) { |
195 | $settings = $this->mapDeprecatedSettingsMessages( $settings ); |
196 | } |
197 | $ret = $this->paramValidator->checkSettings( |
198 | $module->encodeParamName( $name ), $settings, $options |
199 | ); |
200 | |
201 | $ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [ |
202 | ApiBase::PARAM_RANGE_ENFORCE, ApiBase::PARAM_HELP_MSG, ApiBase::PARAM_HELP_MSG_APPEND, |
203 | ApiBase::PARAM_HELP_MSG_INFO, ApiBase::PARAM_HELP_MSG_PER_VALUE, ApiBase::PARAM_TEMPLATE_VARS, |
204 | ] ); |
205 | |
206 | if ( !is_array( $settings ) ) { |
207 | $settings = []; |
208 | } |
209 | |
210 | if ( !is_bool( $settings[ApiBase::PARAM_RANGE_ENFORCE] ?? false ) ) { |
211 | $ret['issues'][ApiBase::PARAM_RANGE_ENFORCE] = 'PARAM_RANGE_ENFORCE must be boolean, got ' |
212 | . gettype( $settings[ApiBase::PARAM_RANGE_ENFORCE] ); |
213 | } |
214 | |
215 | $path = $module->getModulePath(); |
216 | $this->checkSettingsMessage( |
217 | $module, 'PARAM_HELP_MSG', $settings[ApiBase::PARAM_HELP_MSG] ?? "apihelp-$path-param-$name", $ret |
218 | ); |
219 | |
220 | if ( isset( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) { |
221 | if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) { |
222 | $ret['issues'][ApiBase::PARAM_HELP_MSG_APPEND] = 'PARAM_HELP_MSG_APPEND must be an array, got ' |
223 | . gettype( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ); |
224 | } else { |
225 | foreach ( $settings[ApiBase::PARAM_HELP_MSG_APPEND] as $k => $v ) { |
226 | $this->checkSettingsMessage( $module, "PARAM_HELP_MSG_APPEND[$k]", $v, $ret ); |
227 | } |
228 | } |
229 | } |
230 | |
231 | if ( isset( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) { |
232 | if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) { |
233 | $ret['issues'][ApiBase::PARAM_HELP_MSG_INFO] = 'PARAM_HELP_MSG_INFO must be an array, got ' |
234 | . gettype( $settings[ApiBase::PARAM_HELP_MSG_INFO] ); |
235 | } else { |
236 | foreach ( $settings[ApiBase::PARAM_HELP_MSG_INFO] as $k => $v ) { |
237 | if ( !is_array( $v ) ) { |
238 | $ret['issues'][] = "PARAM_HELP_MSG_INFO[$k] must be an array, got " . gettype( $v ); |
239 | } elseif ( !is_string( $v[0] ) ) { |
240 | $ret['issues'][] = "PARAM_HELP_MSG_INFO[$k][0] must be a string, got " . gettype( $v[0] ); |
241 | } else { |
242 | $v[0] = "apihelp-{$path}-paraminfo-{$v[0]}"; |
243 | $this->checkSettingsMessage( $module, "PARAM_HELP_MSG_INFO[$k]", $v, $ret ); |
244 | } |
245 | } |
246 | } |
247 | } |
248 | |
249 | if ( isset( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) { |
250 | if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) { |
251 | $ret['issues'][ApiBase::PARAM_HELP_MSG_PER_VALUE] = 'PARAM_HELP_MSG_PER_VALUE must be an array,' |
252 | . ' got ' . gettype( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ); |
253 | } elseif ( !is_array( $settings[ParamValidator::PARAM_TYPE] ?? '' ) ) { |
254 | $ret['issues'][ApiBase::PARAM_HELP_MSG_PER_VALUE] = 'PARAM_HELP_MSG_PER_VALUE can only be used ' |
255 | . 'with PARAM_TYPE as an array'; |
256 | } else { |
257 | $values = array_map( 'strval', $settings[ParamValidator::PARAM_TYPE] ); |
258 | foreach ( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] as $k => $v ) { |
259 | if ( !in_array( (string)$k, $values, true ) ) { |
260 | // Or should this be allowed? |
261 | $ret['issues'][] = "PARAM_HELP_MSG_PER_VALUE contains \"$k\", which is not in PARAM_TYPE."; |
262 | } |
263 | $this->checkSettingsMessage( $module, "PARAM_HELP_MSG_PER_VALUE[$k]", $v, $ret ); |
264 | } |
265 | foreach ( $settings[ParamValidator::PARAM_TYPE] as $p ) { |
266 | if ( array_key_exists( $p, $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) { |
267 | continue; |
268 | } |
269 | $path = $module->getModulePath(); |
270 | $this->checkSettingsMessage( |
271 | $module, |
272 | "PARAM_HELP_MSG_PER_VALUE[$p]", |
273 | "apihelp-$path-paramvalue-$name-$p", |
274 | $ret |
275 | ); |
276 | } |
277 | } |
278 | } |
279 | |
280 | if ( isset( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) ) { |
281 | if ( !is_array( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) ) { |
282 | $ret['issues'][ApiBase::PARAM_TEMPLATE_VARS] = 'PARAM_TEMPLATE_VARS must be an array,' |
283 | . ' got ' . gettype( $settings[ApiBase::PARAM_TEMPLATE_VARS] ); |
284 | } elseif ( $settings[ApiBase::PARAM_TEMPLATE_VARS] === [] ) { |
285 | $ret['issues'][ApiBase::PARAM_TEMPLATE_VARS] = 'PARAM_TEMPLATE_VARS cannot be the empty array'; |
286 | } else { |
287 | foreach ( $settings[ApiBase::PARAM_TEMPLATE_VARS] as $key => $target ) { |
288 | if ( !preg_match( '/^[^{}]+$/', $key ) ) { |
289 | $ret['issues'][] = "PARAM_TEMPLATE_VARS keys may not contain '{' or '}', got \"$key\""; |
290 | } elseif ( !str_contains( $name, '{' . $key . '}' ) ) { |
291 | $ret['issues'][] = "Parameter name must contain PARAM_TEMPLATE_VARS key {{$key}}"; |
292 | } |
293 | if ( !is_string( $target ) && !is_int( $target ) ) { |
294 | $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] has invalid target type " . gettype( $target ); |
295 | } elseif ( !isset( $params[$target] ) ) { |
296 | $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] target parameter \"$target\" does not exist"; |
297 | } else { |
298 | $settings2 = $params[$target]; |
299 | if ( empty( $settings2[ParamValidator::PARAM_ISMULTI] ) ) { |
300 | $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] target parameter \"$target\" must have " |
301 | . 'PARAM_ISMULTI = true'; |
302 | } |
303 | if ( isset( $settings2[ApiBase::PARAM_TEMPLATE_VARS] ) ) { |
304 | if ( $target === $name ) { |
305 | $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] cannot target the parameter itself"; |
306 | } |
307 | if ( array_diff( |
308 | $settings2[ApiBase::PARAM_TEMPLATE_VARS], |
309 | $settings[ApiBase::PARAM_TEMPLATE_VARS] |
310 | ) ) { |
311 | $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key]: Target's " |
312 | . 'PARAM_TEMPLATE_VARS must be a subset of the original'; |
313 | } |
314 | } |
315 | } |
316 | } |
317 | |
318 | $keys = implode( '|', array_map( |
319 | static function ( $key ) { |
320 | return preg_quote( $key, '/' ); |
321 | }, |
322 | array_keys( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) |
323 | ) ); |
324 | if ( !preg_match( '/^(?>[^{}]+|\{(?:' . $keys . ')\})+$/', $name ) ) { |
325 | $ret['issues'][] = "Parameter name may not contain '{' or '}' other than ' |
326 | . 'as defined by PARAM_TEMPLATE_VARS"; |
327 | } |
328 | } |
329 | } elseif ( !preg_match( '/^[^{}]+$/', $name ) ) { |
330 | $ret['issues'][] = "Parameter name may not contain '{' or '}' without PARAM_TEMPLATE_VARS"; |
331 | } |
332 | |
333 | return $ret; |
334 | } |
335 | |
336 | /** |
337 | * Convert a ValidationException to an ApiUsageException |
338 | * @param ApiBase $module |
339 | * @param ValidationException $ex |
340 | * @throws ApiUsageException always |
341 | * @return never |
342 | */ |
343 | private function convertValidationException( ApiBase $module, ValidationException $ex ) { |
344 | $mv = $ex->getFailureMessage(); |
345 | throw ApiUsageException::newWithMessage( |
346 | $module, |
347 | $mv, |
348 | $mv->getCode(), |
349 | $mv->getData(), |
350 | 0, |
351 | $ex |
352 | ); |
353 | } |
354 | |
355 | /** |
356 | * Get and validate a value |
357 | * @param ApiBase $module |
358 | * @param string $name Parameter name, unprefixed |
359 | * @param array|mixed $settings Default value or an array of settings |
360 | * using PARAM_* constants. |
361 | * @param array $options Options array |
362 | * @return mixed Validated parameter value |
363 | * @throws ApiUsageException if the value is invalid |
364 | */ |
365 | public function getValue( ApiBase $module, string $name, $settings, array $options = [] ) { |
366 | $options['module'] = $module; |
367 | $name = $module->encodeParamName( $name ); |
368 | $settings = $this->normalizeSettings( $settings ); |
369 | try { |
370 | return $this->paramValidator->getValue( $name, $settings, $options ); |
371 | } catch ( ValidationException $ex ) { |
372 | $this->convertValidationException( $module, $ex ); |
373 | } |
374 | } |
375 | |
376 | /** |
377 | * Validate a parameter value using a settings array |
378 | * |
379 | * @param ApiBase $module |
380 | * @param string $name Parameter name, unprefixed |
381 | * @param mixed $value Parameter value |
382 | * @param array|mixed $settings Default value or an array of settings |
383 | * using PARAM_* constants. |
384 | * @param array $options Options array |
385 | * @return mixed Validated parameter value(s) |
386 | * @throws ApiUsageException if the value is invalid |
387 | */ |
388 | public function validateValue( |
389 | ApiBase $module, string $name, $value, $settings, array $options = [] |
390 | ) { |
391 | $options['module'] = $module; |
392 | $name = $module->encodeParamName( $name ); |
393 | $settings = $this->normalizeSettings( $settings ); |
394 | try { |
395 | return $this->paramValidator->validateValue( $name, $value, $settings, $options ); |
396 | } catch ( ValidationException $ex ) { |
397 | $this->convertValidationException( $module, $ex ); |
398 | } |
399 | } |
400 | |
401 | /** |
402 | * Describe parameter settings in a machine-readable format. |
403 | * |
404 | * @param ApiBase $module |
405 | * @param string $name Parameter name. |
406 | * @param array|mixed $settings Default value or an array of settings |
407 | * using PARAM_* constants. |
408 | * @param array $options Options array. |
409 | * @return array |
410 | */ |
411 | public function getParamInfo( ApiBase $module, string $name, $settings, array $options ): array { |
412 | $options['module'] = $module; |
413 | $name = $module->encodeParamName( $name ); |
414 | return $this->paramValidator->getParamInfo( $name, $settings, $options ); |
415 | } |
416 | |
417 | /** |
418 | * Describe parameter settings in human-readable format |
419 | * |
420 | * @param ApiBase $module |
421 | * @param string $name Parameter name being described. |
422 | * @param array|mixed $settings Default value or an array of settings |
423 | * using PARAM_* constants. |
424 | * @param array $options Options array. |
425 | * @return Message[] |
426 | */ |
427 | public function getHelpInfo( ApiBase $module, string $name, $settings, array $options ): array { |
428 | $options['module'] = $module; |
429 | $name = $module->encodeParamName( $name ); |
430 | |
431 | $ret = $this->paramValidator->getHelpInfo( $name, $settings, $options ); |
432 | foreach ( $ret as &$m ) { |
433 | $k = $m->getKey(); |
434 | $m = Message::newFromSpecifier( $m ); |
435 | if ( str_starts_with( $k, 'paramvalidator-help-' ) ) { |
436 | $m = new Message( |
437 | [ 'api-help-param-' . substr( $k, 20 ), $k ], |
438 | $m->getParams() |
439 | ); |
440 | } |
441 | } |
442 | '@phan-var Message[] $ret'; // The above loop converts it |
443 | |
444 | return $ret; |
445 | } |
446 | } |