Merge "Fix 'Tags' padding to keep it farther from the edge and document the source...
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiBaseTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 /**
6 * @group API
7 * @group Database
8 * @group medium
9 *
10 * @covers ApiBase
11 */
12 class ApiBaseTest extends ApiTestCase {
13 /**
14 * This covers a variety of stub methods that return a fixed value.
15 *
16 * @param string|array $method Name of method, or [ name, params... ]
17 * @param string $value Expected value
18 *
19 * @dataProvider provideStubMethods
20 */
21 public function testStubMethods( $expected, $method, $args = [] ) {
22 // Some of these are protected
23 $mock = TestingAccessWrapper::newFromObject( new MockApi() );
24 $result = call_user_func_array( [ $mock, $method ], $args );
25 $this->assertSame( $expected, $result );
26 }
27
28 public function provideStubMethods() {
29 return [
30 [ null, 'getModuleManager' ],
31 [ null, 'getCustomPrinter' ],
32 [ [], 'getHelpUrls' ],
33 // @todo This is actually overriden by MockApi
34 // [ [], 'getAllowedParams' ],
35 [ true, 'shouldCheckMaxLag' ],
36 [ true, 'isReadMode' ],
37 [ false, 'isWriteMode' ],
38 [ false, 'mustBePosted' ],
39 [ false, 'isDeprecated' ],
40 [ false, 'isInternal' ],
41 [ false, 'needsToken' ],
42 [ null, 'getWebUITokenSalt', [ [] ] ],
43 [ null, 'getConditionalRequestData', [ 'etag' ] ],
44 [ null, 'dynamicParameterDocumentation' ],
45 ];
46 }
47
48 public function testRequireOnlyOneParameterDefault() {
49 $mock = new MockApi();
50 $mock->requireOnlyOneParameter(
51 [ "filename" => "foo.txt", "enablechunks" => false ],
52 "filename", "enablechunks"
53 );
54 $this->assertTrue( true );
55 }
56
57 /**
58 * @expectedException ApiUsageException
59 */
60 public function testRequireOnlyOneParameterZero() {
61 $mock = new MockApi();
62 $mock->requireOnlyOneParameter(
63 [ "filename" => "foo.txt", "enablechunks" => 0 ],
64 "filename", "enablechunks"
65 );
66 }
67
68 /**
69 * @expectedException ApiUsageException
70 */
71 public function testRequireOnlyOneParameterTrue() {
72 $mock = new MockApi();
73 $mock->requireOnlyOneParameter(
74 [ "filename" => "foo.txt", "enablechunks" => true ],
75 "filename", "enablechunks"
76 );
77 }
78
79 public function testRequireOnlyOneParameterMissing() {
80 $this->setExpectedException( ApiUsageException::class,
81 'One of the parameters "foo" and "bar" is required.' );
82 $mock = new MockApi();
83 $mock->requireOnlyOneParameter(
84 [ "filename" => "foo.txt", "enablechunks" => false ],
85 "foo", "bar" );
86 }
87
88 public function testRequireMaxOneParameterZero() {
89 $mock = new MockApi();
90 $mock->requireMaxOneParameter(
91 [ 'foo' => 'bar', 'baz' => 'quz' ],
92 'squirrel' );
93 $this->assertTrue( true );
94 }
95
96 public function testRequireMaxOneParameterOne() {
97 $mock = new MockApi();
98 $mock->requireMaxOneParameter(
99 [ 'foo' => 'bar', 'baz' => 'quz' ],
100 'foo', 'squirrel' );
101 $this->assertTrue( true );
102 }
103
104 public function testRequireMaxOneParameterTwo() {
105 $this->setExpectedException( ApiUsageException::class,
106 'The parameters "foo" and "baz" can not be used together.' );
107 $mock = new MockApi();
108 $mock->requireMaxOneParameter(
109 [ 'foo' => 'bar', 'baz' => 'quz' ],
110 'foo', 'baz' );
111 }
112
113 public function testRequireAtLeastOneParameterZero() {
114 $this->setExpectedException( ApiUsageException::class,
115 'At least one of the parameters "foo" and "bar" is required.' );
116 $mock = new MockApi();
117 $mock->requireAtLeastOneParameter(
118 [ 'a' => 'b', 'c' => 'd' ],
119 'foo', 'bar' );
120 }
121
122 public function testRequireAtLeastOneParameterOne() {
123 $mock = new MockApi();
124 $mock->requireAtLeastOneParameter(
125 [ 'a' => 'b', 'c' => 'd' ],
126 'foo', 'a' );
127 $this->assertTrue( true );
128 }
129
130 public function testRequireAtLeastOneParameterTwo() {
131 $mock = new MockApi();
132 $mock->requireAtLeastOneParameter(
133 [ 'a' => 'b', 'c' => 'd' ],
134 'a', 'c' );
135 $this->assertTrue( true );
136 }
137
138 public function testGetTitleOrPageIdBadParams() {
139 $this->setExpectedException( ApiUsageException::class,
140 'The parameters "title" and "pageid" can not be used together.' );
141 $mock = new MockApi();
142 $mock->getTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
143 }
144
145 public function testGetTitleOrPageIdTitle() {
146 $mock = new MockApi();
147 $result = $mock->getTitleOrPageId( [ 'title' => 'Foo' ] );
148 $this->assertInstanceOf( WikiPage::class, $result );
149 $this->assertSame( 'Foo', $result->getTitle()->getPrefixedText() );
150 }
151
152 public function testGetTitleOrPageIdInvalidTitle() {
153 $this->setExpectedException( ApiUsageException::class,
154 'Bad title "|".' );
155 $mock = new MockApi();
156 $mock->getTitleOrPageId( [ 'title' => '|' ] );
157 }
158
159 public function testGetTitleOrPageIdSpecialTitle() {
160 $this->setExpectedException( ApiUsageException::class,
161 "Namespace doesn't allow actual pages." );
162 $mock = new MockApi();
163 $mock->getTitleOrPageId( [ 'title' => 'Special:RandomPage' ] );
164 }
165
166 public function testGetTitleOrPageIdPageId() {
167 $page = $this->getExistingTestPage();
168 $result = ( new MockApi() )->getTitleOrPageId(
169 [ 'pageid' => $page->getId() ] );
170 $this->assertInstanceOf( WikiPage::class, $result );
171 $this->assertSame(
172 $page->getTitle()->getPrefixedText(),
173 $result->getTitle()->getPrefixedText()
174 );
175 }
176
177 public function testGetTitleOrPageIdInvalidPageId() {
178 $this->setExpectedException( ApiUsageException::class,
179 'There is no page with ID 2147483648.' );
180 $mock = new MockApi();
181 $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] );
182 }
183
184 public function testGetTitleFromTitleOrPageIdBadParams() {
185 $this->setExpectedException( ApiUsageException::class,
186 'The parameters "title" and "pageid" can not be used together.' );
187 $mock = new MockApi();
188 $mock->getTitleFromTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
189 }
190
191 public function testGetTitleFromTitleOrPageIdTitle() {
192 $mock = new MockApi();
193 $result = $mock->getTitleFromTitleOrPageId( [ 'title' => 'Foo' ] );
194 $this->assertInstanceOf( Title::class, $result );
195 $this->assertSame( 'Foo', $result->getPrefixedText() );
196 }
197
198 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
199 $this->setExpectedException( ApiUsageException::class,
200 'Bad title "|".' );
201 $mock = new MockApi();
202 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
203 }
204
205 public function testGetTitleFromTitleOrPageIdPageId() {
206 $page = $this->getExistingTestPage();
207 $result = ( new MockApi() )->getTitleFromTitleOrPageId(
208 [ 'pageid' => $page->getId() ] );
209 $this->assertInstanceOf( Title::class, $result );
210 $this->assertSame( $page->getTitle()->getPrefixedText(), $result->getPrefixedText() );
211 }
212
213 public function testGetTitleFromTitleOrPageIdInvalidPageId() {
214 $this->setExpectedException( ApiUsageException::class,
215 'There is no page with ID 298401643.' );
216 $mock = new MockApi();
217 $mock->getTitleFromTitleOrPageId( [ 'pageid' => 298401643 ] );
218 }
219
220 public function testGetParameter() {
221 $mock = $this->getMockBuilder( MockApi::class )
222 ->setMethods( [ 'getAllowedParams' ] )
223 ->getMock();
224 $mock->method( 'getAllowedParams' )->willReturn( [
225 'foo' => [
226 ApiBase::PARAM_TYPE => [ 'value' ],
227 ],
228 'bar' => [
229 ApiBase::PARAM_TYPE => [ 'value' ],
230 ],
231 ] );
232 $wrapper = TestingAccessWrapper::newFromObject( $mock );
233
234 $context = new DerivativeContext( $mock );
235 $context->setRequest( new FauxRequest( [ 'foo' => 'bad', 'bar' => 'value' ] ) );
236 $wrapper->mMainModule = new ApiMain( $context );
237
238 // Even though 'foo' is bad, getParameter( 'bar' ) must not fail
239 $this->assertSame( 'value', $wrapper->getParameter( 'bar' ) );
240
241 // But getParameter( 'foo' ) must throw.
242 try {
243 $wrapper->getParameter( 'foo' );
244 $this->fail( 'Expected exception not thrown' );
245 } catch ( ApiUsageException $ex ) {
246 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
247 }
248
249 // And extractRequestParams() must throw too.
250 try {
251 $mock->extractRequestParams();
252 $this->fail( 'Expected exception not thrown' );
253 } catch ( ApiUsageException $ex ) {
254 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
255 }
256 }
257
258 /**
259 * @dataProvider provideGetParameterFromSettings
260 * @param string|null $input
261 * @param array $paramSettings
262 * @param mixed $expected
263 * @param array $options Key-value pairs:
264 * 'parseLimits': true|false
265 * 'apihighlimits': true|false
266 * 'internalmode': true|false
267 * @param string[] $warnings
268 */
269 public function testGetParameterFromSettings(
270 $input, $paramSettings, $expected, $warnings, $options = []
271 ) {
272 $mock = new MockApi();
273 $wrapper = TestingAccessWrapper::newFromObject( $mock );
274
275 $context = new DerivativeContext( $mock );
276 $context->setRequest( new FauxRequest(
277 $input !== null ? [ 'myParam' => $input ] : [] ) );
278 $wrapper->mMainModule = new ApiMain( $context );
279
280 $parseLimits = $options['parseLimits'] ?? true;
281
282 if ( !empty( $options['apihighlimits'] ) ) {
283 $context->setUser( self::$users['sysop']->getUser() );
284 }
285
286 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
287 $mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->mMainModule );
288 $mainWrapper->mInternalMode = false;
289 }
290
291 // If we're testing tags, set up some tags
292 if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
293 $paramSettings[ApiBase::PARAM_TYPE] === 'tags'
294 ) {
295 ChangeTags::defineTag( 'tag1' );
296 ChangeTags::defineTag( 'tag2' );
297 }
298
299 if ( $expected instanceof Exception ) {
300 try {
301 $wrapper->getParameterFromSettings( 'myParam', $paramSettings,
302 $parseLimits );
303 $this->fail( 'No exception thrown' );
304 } catch ( Exception $ex ) {
305 $this->assertEquals( $expected, $ex );
306 }
307 } else {
308 $result = $wrapper->getParameterFromSettings( 'myParam',
309 $paramSettings, $parseLimits );
310 if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
311 $paramSettings[ApiBase::PARAM_TYPE] === 'timestamp' &&
312 $expected === 'now'
313 ) {
314 // Allow one second of fuzziness. Make sure the formats are
315 // correct!
316 $this->assertRegExp( '/^\d{14}$/', $result );
317 $this->assertLessThanOrEqual( 1,
318 abs( wfTimestamp( TS_UNIX, $result ) - time() ),
319 "Result $result differs from expected $expected by " .
320 'more than one second' );
321 } else {
322 $this->assertSame( $expected, $result );
323 }
324 $actualWarnings = array_map( function ( $warn ) {
325 return $warn instanceof Message
326 ? array_merge( [ $warn->getKey() ], $warn->getParams() )
327 : $warn;
328 }, $mock->warnings );
329 $this->assertSame( $warnings, $actualWarnings );
330 }
331
332 if ( !empty( $paramSettings[ApiBase::PARAM_SENSITIVE] ) ||
333 ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
334 $paramSettings[ApiBase::PARAM_TYPE] === 'password' )
335 ) {
336 $mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->getMain() );
337 $this->assertSame( [ 'myParam' ],
338 $mainWrapper->getSensitiveParams() );
339 }
340 }
341
342 public static function provideGetParameterFromSettings() {
343 $warnings = [
344 [ 'apiwarn-badutf8', 'myParam' ],
345 ];
346
347 $c0 = '';
348 $enc = '';
349 for ( $i = 0; $i < 32; $i++ ) {
350 $c0 .= chr( $i );
351 $enc .= ( $i === 9 || $i === 10 || $i === 13 )
352 ? chr( $i )
353 : '�';
354 }
355
356 $returnArray = [
357 'Basic param' => [ 'bar', null, 'bar', [] ],
358 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
359 'String param' => [ 'bar', '', 'bar', [] ],
360 'String param, defaulted' => [ null, '', '', [] ],
361 'String param, empty' => [ '', 'default', '', [] ],
362 'String param, required, empty' => [
363 '',
364 [ ApiBase::PARAM_DFLT => 'default', ApiBase::PARAM_REQUIRED => true ],
365 ApiUsageException::newWithMessage( null,
366 [ 'apierror-missingparam', 'myParam' ] ),
367 []
368 ],
369 'Multi-valued parameter' => [
370 'a|b|c',
371 [ ApiBase::PARAM_ISMULTI => true ],
372 [ 'a', 'b', 'c' ],
373 []
374 ],
375 'Multi-valued parameter, alternative separator' => [
376 "\x1fa|b\x1fc|d",
377 [ ApiBase::PARAM_ISMULTI => true ],
378 [ 'a|b', 'c|d' ],
379 []
380 ],
381 'Multi-valued parameter, other C0 controls' => [
382 $c0,
383 [ ApiBase::PARAM_ISMULTI => true ],
384 [ $enc ],
385 $warnings
386 ],
387 'Multi-valued parameter, other C0 controls (2)' => [
388 "\x1f" . $c0,
389 [ ApiBase::PARAM_ISMULTI => true ],
390 [ substr( $enc, 0, -3 ), '' ],
391 $warnings
392 ],
393 'Multi-valued parameter with limits' => [
394 'a|b|c',
395 [
396 ApiBase::PARAM_ISMULTI => true,
397 ApiBase::PARAM_ISMULTI_LIMIT1 => 3,
398 ],
399 [ 'a', 'b', 'c' ],
400 [],
401 ],
402 'Multi-valued parameter with exceeded limits' => [
403 'a|b|c',
404 [
405 ApiBase::PARAM_ISMULTI => true,
406 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
407 ],
408 ApiUsageException::newWithMessage(
409 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
410 ),
411 []
412 ],
413 'Multi-valued parameter with exceeded limits for non-bot' => [
414 'a|b|c',
415 [
416 ApiBase::PARAM_ISMULTI => true,
417 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
418 ApiBase::PARAM_ISMULTI_LIMIT2 => 3,
419 ],
420 ApiUsageException::newWithMessage(
421 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
422 ),
423 []
424 ],
425 'Multi-valued parameter with non-exceeded limits for bot' => [
426 'a|b|c',
427 [
428 ApiBase::PARAM_ISMULTI => true,
429 ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
430 ApiBase::PARAM_ISMULTI_LIMIT2 => 3,
431 ],
432 [ 'a', 'b', 'c' ],
433 [],
434 [ 'apihighlimits' => true ],
435 ],
436 'Multi-valued parameter with prohibited duplicates' => [
437 'a|b|a|c',
438 [ ApiBase::PARAM_ISMULTI => true ],
439 // Note that the keys are not sequential! This matches
440 // array_unique, but might be unexpected.
441 [ 0 => 'a', 1 => 'b', 3 => 'c' ],
442 [],
443 ],
444 'Multi-valued parameter with allowed duplicates' => [
445 'a|a',
446 [
447 ApiBase::PARAM_ISMULTI => true,
448 ApiBase::PARAM_ALLOW_DUPLICATES => true,
449 ],
450 [ 'a', 'a' ],
451 [],
452 ],
453 'Empty boolean param' => [
454 '',
455 [ ApiBase::PARAM_TYPE => 'boolean' ],
456 true,
457 [],
458 ],
459 'Boolean param 0' => [
460 '0',
461 [ ApiBase::PARAM_TYPE => 'boolean' ],
462 true,
463 [],
464 ],
465 'Boolean param false' => [
466 'false',
467 [ ApiBase::PARAM_TYPE => 'boolean' ],
468 true,
469 [],
470 ],
471 'Boolean multi-param' => [
472 'true|false',
473 [
474 ApiBase::PARAM_TYPE => 'boolean',
475 ApiBase::PARAM_ISMULTI => true,
476 ],
477 new MWException(
478 'Internal error in ApiBase::getParameterFromSettings: ' .
479 'Multi-values not supported for myParam'
480 ),
481 [],
482 ],
483 'Empty boolean param with non-false default' => [
484 '',
485 [
486 ApiBase::PARAM_TYPE => 'boolean',
487 ApiBase::PARAM_DFLT => true,
488 ],
489 new MWException(
490 'Internal error in ApiBase::getParameterFromSettings: ' .
491 "Boolean param myParam's default is set to '1'. " .
492 'Boolean parameters must default to false.' ),
493 [],
494 ],
495 'Deprecated parameter' => [
496 'foo',
497 [ ApiBase::PARAM_DEPRECATED => true ],
498 'foo',
499 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
500 ],
501 'Deprecated parameter value' => [
502 'a',
503 [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ] ],
504 'a',
505 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
506 ],
507 'Multiple deprecated parameter values' => [
508 'a|b|c|d',
509 [ ApiBase::PARAM_DEPRECATED_VALUES =>
510 [ 'b' => true, 'd' => true ],
511 ApiBase::PARAM_ISMULTI => true ],
512 [ 'a', 'b', 'c', 'd' ],
513 [
514 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
515 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
516 ],
517 ],
518 'Deprecated parameter value with custom warning' => [
519 'a',
520 [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => 'my-msg' ] ],
521 'a',
522 [ 'my-msg' ],
523 ],
524 '"*" when wildcard not allowed' => [
525 '*',
526 [ ApiBase::PARAM_ISMULTI => true,
527 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ] ],
528 [],
529 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
530 [ 'list' => [ '&#42;' ], 'type' => 'comma' ], 1 ] ],
531 ],
532 'Wildcard "*"' => [
533 '*',
534 [
535 ApiBase::PARAM_ISMULTI => true,
536 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
537 ApiBase::PARAM_ALL => true,
538 ],
539 [ 'a', 'b', 'c' ],
540 [],
541 ],
542 'Wildcard "*" with multiples not allowed' => [
543 '*',
544 [
545 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
546 ApiBase::PARAM_ALL => true,
547 ],
548 ApiUsageException::newWithMessage( null,
549 [ 'apierror-unrecognizedvalue', 'myParam', '&#42;' ],
550 'unknown_myParam' ),
551 [],
552 ],
553 'Wildcard "*" with unrestricted type' => [
554 '*',
555 [
556 ApiBase::PARAM_ISMULTI => true,
557 ApiBase::PARAM_ALL => true,
558 ],
559 [ '*' ],
560 [],
561 ],
562 'Wildcard "x"' => [
563 'x',
564 [
565 ApiBase::PARAM_ISMULTI => true,
566 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
567 ApiBase::PARAM_ALL => 'x',
568 ],
569 [ 'a', 'b', 'c' ],
570 [],
571 ],
572 'Wildcard conflicting with allowed value' => [
573 'a',
574 [
575 ApiBase::PARAM_ISMULTI => true,
576 ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
577 ApiBase::PARAM_ALL => 'a',
578 ],
579 new MWException(
580 'Internal error in ApiBase::getParameterFromSettings: ' .
581 'For param myParam, PARAM_ALL collides with a possible ' .
582 'value' ),
583 [],
584 ],
585 'Namespace with wildcard' => [
586 '*',
587 [
588 ApiBase::PARAM_ISMULTI => true,
589 ApiBase::PARAM_TYPE => 'namespace',
590 ],
591 MWNamespace::getValidNamespaces(),
592 [],
593 ],
594 // PARAM_ALL is ignored with namespace types.
595 'Namespace with wildcard suppressed' => [
596 '*',
597 [
598 ApiBase::PARAM_ISMULTI => true,
599 ApiBase::PARAM_TYPE => 'namespace',
600 ApiBase::PARAM_ALL => false,
601 ],
602 MWNamespace::getValidNamespaces(),
603 [],
604 ],
605 'Namespace with wildcard "x"' => [
606 'x',
607 [
608 ApiBase::PARAM_ISMULTI => true,
609 ApiBase::PARAM_TYPE => 'namespace',
610 ApiBase::PARAM_ALL => 'x',
611 ],
612 [],
613 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
614 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
615 ],
616 'Password' => [
617 'dDy+G?e?txnr.1:(@[Ru',
618 [ ApiBase::PARAM_TYPE => 'password' ],
619 'dDy+G?e?txnr.1:(@[Ru',
620 [],
621 ],
622 'Sensitive field' => [
623 'I am fond of pineapples',
624 [ ApiBase::PARAM_SENSITIVE => true ],
625 'I am fond of pineapples',
626 [],
627 ],
628 'Upload with default' => [
629 '',
630 [
631 ApiBase::PARAM_TYPE => 'upload',
632 ApiBase::PARAM_DFLT => '',
633 ],
634 new MWException(
635 'Internal error in ApiBase::getParameterFromSettings: ' .
636 "File upload param myParam's default is set to ''. " .
637 'File upload parameters may not have a default.' ),
638 [],
639 ],
640 'Multiple upload' => [
641 '',
642 [
643 ApiBase::PARAM_TYPE => 'upload',
644 ApiBase::PARAM_ISMULTI => true,
645 ],
646 new MWException(
647 'Internal error in ApiBase::getParameterFromSettings: ' .
648 'Multi-values not supported for myParam' ),
649 [],
650 ],
651 // @todo Test actual upload
652 'Namespace -1' => [
653 '-1',
654 [ ApiBase::PARAM_TYPE => 'namespace' ],
655 ApiUsageException::newWithMessage( null,
656 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
657 'unknown_myParam' ),
658 [],
659 ],
660 'Extra namespace -1' => [
661 '-1',
662 [
663 ApiBase::PARAM_TYPE => 'namespace',
664 ApiBase::PARAM_EXTRA_NAMESPACES => [ '-1' ],
665 ],
666 '-1',
667 [],
668 ],
669 // @todo Test with PARAM_SUBMODULE_MAP unset, need
670 // getModuleManager() to return something real
671 'Nonexistent module' => [
672 'not-a-module-name',
673 [
674 ApiBase::PARAM_TYPE => 'submodule',
675 ApiBase::PARAM_SUBMODULE_MAP =>
676 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
677 ],
678 ApiUsageException::newWithMessage(
679 null,
680 [
681 'apierror-unrecognizedvalue',
682 'myParam',
683 'not-a-module-name',
684 ],
685 'unknown_myParam'
686 ),
687 [],
688 ],
689 '\\x1f with multiples not allowed' => [
690 "\x1f",
691 [],
692 ApiUsageException::newWithMessage( null,
693 'apierror-badvalue-notmultivalue',
694 'badvalue_notmultivalue' ),
695 [],
696 ],
697 'Integer with unenforced min' => [
698 '-2',
699 [
700 ApiBase::PARAM_TYPE => 'integer',
701 ApiBase::PARAM_MIN => -1,
702 ],
703 -1,
704 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
705 -2 ] ],
706 ],
707 'Integer with enforced min' => [
708 '-2',
709 [
710 ApiBase::PARAM_TYPE => 'integer',
711 ApiBase::PARAM_MIN => -1,
712 ApiBase::PARAM_RANGE_ENFORCE => true,
713 ],
714 ApiUsageException::newWithMessage( null,
715 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
716 '-1', '-2' ], 'integeroutofrange',
717 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
718 [],
719 ],
720 'Integer with unenforced max (internal mode)' => [
721 '8',
722 [
723 ApiBase::PARAM_TYPE => 'integer',
724 ApiBase::PARAM_MAX => 7,
725 ],
726 8,
727 [],
728 ],
729 'Integer with enforced max (internal mode)' => [
730 '8',
731 [
732 ApiBase::PARAM_TYPE => 'integer',
733 ApiBase::PARAM_MAX => 7,
734 ApiBase::PARAM_RANGE_ENFORCE => true,
735 ],
736 8,
737 [],
738 ],
739 'Integer with unenforced max (non-internal mode)' => [
740 '8',
741 [
742 ApiBase::PARAM_TYPE => 'integer',
743 ApiBase::PARAM_MAX => 7,
744 ],
745 7,
746 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
747 [ 'internalmode' => false ],
748 ],
749 'Integer with enforced max (non-internal mode)' => [
750 '8',
751 [
752 ApiBase::PARAM_TYPE => 'integer',
753 ApiBase::PARAM_MAX => 7,
754 ApiBase::PARAM_RANGE_ENFORCE => true,
755 ],
756 ApiUsageException::newWithMessage(
757 null,
758 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
759 'integeroutofrange',
760 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
761 ),
762 [],
763 [ 'internalmode' => false ],
764 ],
765 'Array of integers' => [
766 '3|12|966|-1',
767 [
768 ApiBase::PARAM_ISMULTI => true,
769 ApiBase::PARAM_TYPE => 'integer',
770 ],
771 [ 3, 12, 966, -1 ],
772 [],
773 ],
774 'Array of integers with unenforced min/max (internal mode)' => [
775 '3|12|966|-1',
776 [
777 ApiBase::PARAM_ISMULTI => true,
778 ApiBase::PARAM_TYPE => 'integer',
779 ApiBase::PARAM_MIN => 0,
780 ApiBase::PARAM_MAX => 100,
781 ],
782 [ 3, 12, 966, 0 ],
783 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
784 ],
785 'Array of integers with enforced min/max (internal mode)' => [
786 '3|12|966|-1',
787 [
788 ApiBase::PARAM_ISMULTI => true,
789 ApiBase::PARAM_TYPE => 'integer',
790 ApiBase::PARAM_MIN => 0,
791 ApiBase::PARAM_MAX => 100,
792 ApiBase::PARAM_RANGE_ENFORCE => true,
793 ],
794 ApiUsageException::newWithMessage(
795 null,
796 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
797 'integeroutofrange',
798 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
799 ),
800 [],
801 ],
802 'Array of integers with unenforced min/max (non-internal mode)' => [
803 '3|12|966|-1',
804 [
805 ApiBase::PARAM_ISMULTI => true,
806 ApiBase::PARAM_TYPE => 'integer',
807 ApiBase::PARAM_MIN => 0,
808 ApiBase::PARAM_MAX => 100,
809 ],
810 [ 3, 12, 100, 0 ],
811 [
812 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
813 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
814 ],
815 [ 'internalmode' => false ],
816 ],
817 'Array of integers with enforced min/max (non-internal mode)' => [
818 '3|12|966|-1',
819 [
820 ApiBase::PARAM_ISMULTI => true,
821 ApiBase::PARAM_TYPE => 'integer',
822 ApiBase::PARAM_MIN => 0,
823 ApiBase::PARAM_MAX => 100,
824 ApiBase::PARAM_RANGE_ENFORCE => true,
825 ],
826 ApiUsageException::newWithMessage(
827 null,
828 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
829 'integeroutofrange',
830 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
831 ),
832 [],
833 [ 'internalmode' => false ],
834 ],
835 'Limit with parseLimits false' => [
836 '100',
837 [ ApiBase::PARAM_TYPE => 'limit' ],
838 '100',
839 [],
840 [ 'parseLimits' => false ],
841 ],
842 'Limit with no max' => [
843 '100',
844 [
845 ApiBase::PARAM_TYPE => 'limit',
846 ApiBase::PARAM_MAX2 => 10,
847 ApiBase::PARAM_ISMULTI => true,
848 ],
849 new MWException(
850 'Internal error in ApiBase::getParameterFromSettings: ' .
851 'MAX1 or MAX2 are not defined for the limit myParam' ),
852 [],
853 ],
854 'Limit with no max2' => [
855 '100',
856 [
857 ApiBase::PARAM_TYPE => 'limit',
858 ApiBase::PARAM_MAX => 10,
859 ApiBase::PARAM_ISMULTI => true,
860 ],
861 new MWException(
862 'Internal error in ApiBase::getParameterFromSettings: ' .
863 'MAX1 or MAX2 are not defined for the limit myParam' ),
864 [],
865 ],
866 'Limit with multi-value' => [
867 '100',
868 [
869 ApiBase::PARAM_TYPE => 'limit',
870 ApiBase::PARAM_MAX => 10,
871 ApiBase::PARAM_MAX2 => 10,
872 ApiBase::PARAM_ISMULTI => true,
873 ],
874 new MWException(
875 'Internal error in ApiBase::getParameterFromSettings: ' .
876 'Multi-values not supported for myParam' ),
877 [],
878 ],
879 'Valid limit' => [
880 '100',
881 [
882 ApiBase::PARAM_TYPE => 'limit',
883 ApiBase::PARAM_MAX => 100,
884 ApiBase::PARAM_MAX2 => 100,
885 ],
886 100,
887 [],
888 ],
889 'Limit max' => [
890 'max',
891 [
892 ApiBase::PARAM_TYPE => 'limit',
893 ApiBase::PARAM_MAX => 100,
894 ApiBase::PARAM_MAX2 => 101,
895 ],
896 100,
897 [],
898 ],
899 'Limit max for apihighlimits' => [
900 'max',
901 [
902 ApiBase::PARAM_TYPE => 'limit',
903 ApiBase::PARAM_MAX => 100,
904 ApiBase::PARAM_MAX2 => 101,
905 ],
906 101,
907 [],
908 [ 'apihighlimits' => true ],
909 ],
910 'Limit too large (internal mode)' => [
911 '101',
912 [
913 ApiBase::PARAM_TYPE => 'limit',
914 ApiBase::PARAM_MAX => 100,
915 ApiBase::PARAM_MAX2 => 101,
916 ],
917 101,
918 [],
919 ],
920 'Limit okay for apihighlimits (internal mode)' => [
921 '101',
922 [
923 ApiBase::PARAM_TYPE => 'limit',
924 ApiBase::PARAM_MAX => 100,
925 ApiBase::PARAM_MAX2 => 101,
926 ],
927 101,
928 [],
929 [ 'apihighlimits' => true ],
930 ],
931 'Limit too large for apihighlimits (internal mode)' => [
932 '102',
933 [
934 ApiBase::PARAM_TYPE => 'limit',
935 ApiBase::PARAM_MAX => 100,
936 ApiBase::PARAM_MAX2 => 101,
937 ],
938 102,
939 [],
940 [ 'apihighlimits' => true ],
941 ],
942 'Limit too large (non-internal mode)' => [
943 '101',
944 [
945 ApiBase::PARAM_TYPE => 'limit',
946 ApiBase::PARAM_MAX => 100,
947 ApiBase::PARAM_MAX2 => 101,
948 ],
949 100,
950 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
951 [ 'internalmode' => false ],
952 ],
953 'Limit okay for apihighlimits (non-internal mode)' => [
954 '101',
955 [
956 ApiBase::PARAM_TYPE => 'limit',
957 ApiBase::PARAM_MAX => 100,
958 ApiBase::PARAM_MAX2 => 101,
959 ],
960 101,
961 [],
962 [ 'internalmode' => false, 'apihighlimits' => true ],
963 ],
964 'Limit too large for apihighlimits (non-internal mode)' => [
965 '102',
966 [
967 ApiBase::PARAM_TYPE => 'limit',
968 ApiBase::PARAM_MAX => 100,
969 ApiBase::PARAM_MAX2 => 101,
970 ],
971 101,
972 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
973 [ 'internalmode' => false, 'apihighlimits' => true ],
974 ],
975 'Limit too small' => [
976 '-2',
977 [
978 ApiBase::PARAM_TYPE => 'limit',
979 ApiBase::PARAM_MIN => -1,
980 ApiBase::PARAM_MAX => 100,
981 ApiBase::PARAM_MAX2 => 100,
982 ],
983 -1,
984 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
985 -2 ] ],
986 ],
987 'Timestamp' => [
988 wfTimestamp( TS_UNIX, '20211221122112' ),
989 [ ApiBase::PARAM_TYPE => 'timestamp' ],
990 '20211221122112',
991 [],
992 ],
993 'Timestamp 0' => [
994 '0',
995 [ ApiBase::PARAM_TYPE => 'timestamp' ],
996 // Magic keyword
997 'now',
998 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
999 ],
1000 'Timestamp empty' => [
1001 '',
1002 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1003 'now',
1004 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
1005 ],
1006 // wfTimestamp() interprets this as Unix time
1007 'Timestamp 00' => [
1008 '00',
1009 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1010 '19700101000000',
1011 [],
1012 ],
1013 'Timestamp now' => [
1014 'now',
1015 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1016 'now',
1017 [],
1018 ],
1019 'Invalid timestamp' => [
1020 'a potato',
1021 [ ApiBase::PARAM_TYPE => 'timestamp' ],
1022 ApiUsageException::newWithMessage(
1023 null,
1024 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
1025 'badtimestamp_myParam'
1026 ),
1027 [],
1028 ],
1029 'Timestamp array' => [
1030 '100|101',
1031 [
1032 ApiBase::PARAM_TYPE => 'timestamp',
1033 ApiBase::PARAM_ISMULTI => 1,
1034 ],
1035 [ wfTimestamp( TS_MW, 100 ), wfTimestamp( TS_MW, 101 ) ],
1036 [],
1037 ],
1038 'User' => [
1039 'foo_bar',
1040 [ ApiBase::PARAM_TYPE => 'user' ],
1041 'Foo bar',
1042 [],
1043 ],
1044 'User prefixed with "User:"' => [
1045 'User:foo_bar',
1046 [ ApiBase::PARAM_TYPE => 'user' ],
1047 'Foo bar',
1048 [],
1049 ],
1050 'Invalid username "|"' => [
1051 '|',
1052 [ ApiBase::PARAM_TYPE => 'user' ],
1053 ApiUsageException::newWithMessage( null,
1054 [ 'apierror-baduser', 'myParam', '&#124;' ],
1055 'baduser_myParam' ),
1056 [],
1057 ],
1058 'Invalid username "300.300.300.300"' => [
1059 '300.300.300.300',
1060 [ ApiBase::PARAM_TYPE => 'user' ],
1061 ApiUsageException::newWithMessage( null,
1062 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1063 'baduser_myParam' ),
1064 [],
1065 ],
1066 'IP range as username' => [
1067 '10.0.0.0/8',
1068 [ ApiBase::PARAM_TYPE => 'user' ],
1069 '10.0.0.0/8',
1070 [],
1071 ],
1072 'IPv6 as username' => [
1073 '::1',
1074 [ ApiBase::PARAM_TYPE => 'user' ],
1075 '0:0:0:0:0:0:0:1',
1076 [],
1077 ],
1078 'Obsolete cloaked usemod IP address as username' => [
1079 '1.2.3.xxx',
1080 [ ApiBase::PARAM_TYPE => 'user' ],
1081 '1.2.3.xxx',
1082 [],
1083 ],
1084 'Invalid username containing IP address' => [
1085 'This is [not] valid 1.2.3.xxx, ha!',
1086 [ ApiBase::PARAM_TYPE => 'user' ],
1087 ApiUsageException::newWithMessage(
1088 null,
1089 [ 'apierror-baduser', 'myParam', 'This is &#91;not&#93; valid 1.2.3.xxx, ha!' ],
1090 'baduser_myParam'
1091 ),
1092 [],
1093 ],
1094 'External username' => [
1095 'M>Foo bar',
1096 [ ApiBase::PARAM_TYPE => 'user' ],
1097 'M>Foo bar',
1098 [],
1099 ],
1100 'Array of usernames' => [
1101 'foo|bar',
1102 [
1103 ApiBase::PARAM_TYPE => 'user',
1104 ApiBase::PARAM_ISMULTI => true,
1105 ],
1106 [ 'Foo', 'Bar' ],
1107 [],
1108 ],
1109 'tag' => [
1110 'tag1',
1111 [ ApiBase::PARAM_TYPE => 'tags' ],
1112 [ 'tag1' ],
1113 [],
1114 ],
1115 'Array of one tag' => [
1116 'tag1',
1117 [
1118 ApiBase::PARAM_TYPE => 'tags',
1119 ApiBase::PARAM_ISMULTI => true,
1120 ],
1121 [ 'tag1' ],
1122 [],
1123 ],
1124 'Array of tags' => [
1125 'tag1|tag2',
1126 [
1127 ApiBase::PARAM_TYPE => 'tags',
1128 ApiBase::PARAM_ISMULTI => true,
1129 ],
1130 [ 'tag1', 'tag2' ],
1131 [],
1132 ],
1133 'Invalid tag' => [
1134 'invalid tag',
1135 [ ApiBase::PARAM_TYPE => 'tags' ],
1136 new ApiUsageException( null,
1137 Status::newFatal( 'tags-apply-not-allowed-one',
1138 'invalid tag', 1 ) ),
1139 [],
1140 ],
1141 'Unrecognized type' => [
1142 'foo',
1143 [ ApiBase::PARAM_TYPE => 'nonexistenttype' ],
1144 new MWException(
1145 'Internal error in ApiBase::getParameterFromSettings: ' .
1146 "Param myParam's type is unknown - nonexistenttype" ),
1147 [],
1148 ],
1149 'Too many bytes' => [
1150 '1',
1151 [
1152 ApiBase::PARAM_MAX_BYTES => 0,
1153 ApiBase::PARAM_MAX_CHARS => 0,
1154 ],
1155 ApiUsageException::newWithMessage( null,
1156 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1157 [],
1158 ],
1159 'Too many chars' => [
1160 '§§',
1161 [
1162 ApiBase::PARAM_MAX_BYTES => 4,
1163 ApiBase::PARAM_MAX_CHARS => 1,
1164 ],
1165 ApiUsageException::newWithMessage( null,
1166 [ 'apierror-maxchars', 'myParam', 1 ] ),
1167 [],
1168 ],
1169 'Omitted required param' => [
1170 null,
1171 [ ApiBase::PARAM_REQUIRED => true ],
1172 ApiUsageException::newWithMessage( null,
1173 [ 'apierror-missingparam', 'myParam' ] ),
1174 [],
1175 ],
1176 'Empty multi-value' => [
1177 '',
1178 [ ApiBase::PARAM_ISMULTI => true ],
1179 [],
1180 [],
1181 ],
1182 'Multi-value \x1f' => [
1183 "\x1f",
1184 [ ApiBase::PARAM_ISMULTI => true ],
1185 [],
1186 [],
1187 ],
1188 'Allowed non-multi-value with "|"' => [
1189 'a|b',
1190 [ ApiBase::PARAM_TYPE => [ 'a|b' ] ],
1191 'a|b',
1192 [],
1193 ],
1194 'Prohibited multi-value' => [
1195 'a|b',
1196 [ ApiBase::PARAM_TYPE => [ 'a', 'b' ] ],
1197 ApiUsageException::newWithMessage( null,
1198 [
1199 'apierror-multival-only-one-of',
1200 'myParam',
1201 Message::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1202 2
1203 ],
1204 'multival_myParam'
1205 ),
1206 [],
1207 ],
1208 ];
1209
1210 // The following really just test PHP's string-to-int conversion.
1211 $integerTests = [
1212 [ '+1', 1 ],
1213 [ '-1', -1 ],
1214 [ '1.5', 1 ],
1215 [ '-1.5', -1 ],
1216 [ '1abc', 1 ],
1217 [ ' 1', 1 ],
1218 [ "\t1", 1, '\t1' ],
1219 [ "\r1", 1, '\r1' ],
1220 [ "\f1", 0, '\f1', 'badutf-8' ],
1221 [ "\n1", 1, '\n1' ],
1222 [ "\v1", 0, '\v1', 'badutf-8' ],
1223 [ "\e1", 0, '\e1', 'badutf-8' ],
1224 [ "\x001", 0, '\x001', 'badutf-8' ],
1225 ];
1226
1227 foreach ( $integerTests as $test ) {
1228 $desc = $test[2] ?? $test[0];
1229 $warnings = isset( $test[3] ) ?
1230 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1231 $returnArray["\"$desc\" as integer"] = [
1232 $test[0],
1233 [ ApiBase::PARAM_TYPE => 'integer' ],
1234 $test[1],
1235 $warnings,
1236 ];
1237 }
1238
1239 return $returnArray;
1240 }
1241
1242 public function testErrorArrayToStatus() {
1243 $mock = new MockApi();
1244
1245 // Sanity check empty array
1246 $expect = Status::newGood();
1247 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
1248
1249 // No blocked $user, so no special block handling
1250 $expect = Status::newGood();
1251 $expect->fatal( 'blockedtext' );
1252 $expect->fatal( 'autoblockedtext' );
1253 $expect->fatal( 'systemblockedtext' );
1254 $expect->fatal( 'mainpage' );
1255 $expect->fatal( 'parentheses', 'foobar' );
1256 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1257 [ 'blockedtext' ],
1258 [ 'autoblockedtext' ],
1259 [ 'systemblockedtext' ],
1260 'mainpage',
1261 [ 'parentheses', 'foobar' ],
1262 ] ) );
1263
1264 // Has a blocked $user, so special block handling
1265 $user = $this->getMutableTestUser()->getUser();
1266 $block = new \Block( [
1267 'address' => $user->getName(),
1268 'user' => $user->getID(),
1269 'by' => $this->getTestSysop()->getUser()->getId(),
1270 'reason' => __METHOD__,
1271 'expiry' => time() + 100500,
1272 ] );
1273 $block->insert();
1274 $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ];
1275
1276 $expect = Status::newGood();
1277 $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1278 $expect->fatal( ApiMessage::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1279 $expect->fatal( ApiMessage::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1280 $expect->fatal( 'mainpage' );
1281 $expect->fatal( 'parentheses', 'foobar' );
1282 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1283 [ 'blockedtext' ],
1284 [ 'autoblockedtext' ],
1285 [ 'systemblockedtext' ],
1286 'mainpage',
1287 [ 'parentheses', 'foobar' ],
1288 ], $user ) );
1289 }
1290
1291 public function testDieStatus() {
1292 $mock = new MockApi();
1293
1294 $status = StatusValue::newGood();
1295 $status->error( 'foo' );
1296 $status->warning( 'bar' );
1297 try {
1298 $mock->dieStatus( $status );
1299 $this->fail( 'Expected exception not thrown' );
1300 } catch ( ApiUsageException $ex ) {
1301 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1302 $this->assertFalse( ApiTestCase::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1303 }
1304
1305 $status = StatusValue::newGood();
1306 $status->warning( 'foo' );
1307 $status->warning( 'bar' );
1308 try {
1309 $mock->dieStatus( $status );
1310 $this->fail( 'Expected exception not thrown' );
1311 } catch ( ApiUsageException $ex ) {
1312 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1313 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1314 }
1315
1316 $status = StatusValue::newGood();
1317 $status->setOk( false );
1318 try {
1319 $mock->dieStatus( $status );
1320 $this->fail( 'Expected exception not thrown' );
1321 } catch ( ApiUsageException $ex ) {
1322 $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'unknownerror-nocode' ),
1323 'Exception has "unknownerror-nocode"' );
1324 }
1325 }
1326
1327 /**
1328 * @covers ApiBase::extractRequestParams
1329 */
1330 public function testExtractRequestParams() {
1331 $request = new FauxRequest( [
1332 'xxexists' => 'exists!',
1333 'xxmulti' => 'a|b|c|d|{bad}',
1334 'xxempty' => '',
1335 'xxtemplate-a' => 'A!',
1336 'xxtemplate-b' => 'B1|B2|B3',
1337 'xxtemplate-c' => '',
1338 'xxrecursivetemplate-b-B1' => 'X',
1339 'xxrecursivetemplate-b-B3' => 'Y',
1340 'xxrecursivetemplate-b-B4' => '?',
1341 'xxemptytemplate-' => 'nope',
1342 'foo' => 'a|b|c',
1343 'xxfoo' => 'a|b|c',
1344 'errorformat' => 'raw',
1345 ] );
1346 $context = new DerivativeContext( RequestContext::getMain() );
1347 $context->setRequest( $request );
1348 $main = new ApiMain( $context );
1349
1350 $mock = $this->getMockBuilder( ApiBase::class )
1351 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1352 ->setMethods( [ 'getAllowedParams' ] )
1353 ->getMockForAbstractClass();
1354 $mock->method( 'getAllowedParams' )->willReturn( [
1355 'notexists' => null,
1356 'exists' => null,
1357 'multi' => [
1358 ApiBase::PARAM_ISMULTI => true,
1359 ],
1360 'empty' => [
1361 ApiBase::PARAM_ISMULTI => true,
1362 ],
1363 'template-{m}' => [
1364 ApiBase::PARAM_ISMULTI => true,
1365 ApiBase::PARAM_TEMPLATE_VARS => [ 'm' => 'multi' ],
1366 ],
1367 'recursivetemplate-{m}-{t}' => [
1368 ApiBase::PARAM_TEMPLATE_VARS => [ 't' => 'template-{m}', 'm' => 'multi' ],
1369 ],
1370 'emptytemplate-{m}' => [
1371 ApiBase::PARAM_ISMULTI => true,
1372 ApiBase::PARAM_TEMPLATE_VARS => [ 'm' => 'empty' ],
1373 ],
1374 'badtemplate-{e}' => [
1375 ApiBase::PARAM_TEMPLATE_VARS => [ 'e' => 'exists' ],
1376 ],
1377 'badtemplate2-{e}' => [
1378 ApiBase::PARAM_TEMPLATE_VARS => [ 'e' => 'badtemplate2-{e}' ],
1379 ],
1380 'badtemplate3-{x}' => [
1381 ApiBase::PARAM_TEMPLATE_VARS => [ 'x' => 'foo' ],
1382 ],
1383 ] );
1384
1385 $this->assertEquals( [
1386 'notexists' => null,
1387 'exists' => 'exists!',
1388 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1389 'empty' => [],
1390 'template-a' => [ 'A!' ],
1391 'template-b' => [ 'B1', 'B2', 'B3' ],
1392 'template-c' => [],
1393 'template-d' => null,
1394 'recursivetemplate-a-A!' => null,
1395 'recursivetemplate-b-B1' => 'X',
1396 'recursivetemplate-b-B2' => null,
1397 'recursivetemplate-b-B3' => 'Y',
1398 ], $mock->extractRequestParams() );
1399
1400 $used = TestingAccessWrapper::newFromObject( $main )->getParamsUsed();
1401 sort( $used );
1402 $this->assertEquals( [
1403 'xxempty',
1404 'xxexists',
1405 'xxmulti',
1406 'xxnotexists',
1407 'xxrecursivetemplate-a-A!',
1408 'xxrecursivetemplate-b-B1',
1409 'xxrecursivetemplate-b-B2',
1410 'xxrecursivetemplate-b-B3',
1411 'xxtemplate-a',
1412 'xxtemplate-b',
1413 'xxtemplate-c',
1414 'xxtemplate-d',
1415 ], $used );
1416
1417 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1418 $this->assertCount( 1, $warnings );
1419 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );
1420 }
1421
1422 }