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