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