3 use MediaWiki\Block\DatabaseBlock
;
4 use MediaWiki\MediaWikiServices
;
5 use Wikimedia\TestingAccessWrapper
;
14 class ApiBaseTest
extends ApiTestCase
{
16 * This covers a variety of stub methods that return a fixed value.
18 * @param string|array $method Name of method, or [ name, params... ]
19 * @param string $value Expected value
21 * @dataProvider provideStubMethods
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 );
30 public function provideStubMethods() {
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' ],
50 public function testRequireOnlyOneParameterDefault() {
51 $mock = new MockApi();
52 $mock->requireOnlyOneParameter(
53 [ "filename" => "foo.txt", "enablechunks" => false ],
54 "filename", "enablechunks"
56 $this->assertTrue( true );
60 * @expectedException ApiUsageException
62 public function testRequireOnlyOneParameterZero() {
63 $mock = new MockApi();
64 $mock->requireOnlyOneParameter(
65 [ "filename" => "foo.txt", "enablechunks" => 0 ],
66 "filename", "enablechunks"
71 * @expectedException ApiUsageException
73 public function testRequireOnlyOneParameterTrue() {
74 $mock = new MockApi();
75 $mock->requireOnlyOneParameter(
76 [ "filename" => "foo.txt", "enablechunks" => true ],
77 "filename", "enablechunks"
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 ],
90 public function testRequireMaxOneParameterZero() {
91 $mock = new MockApi();
92 $mock->requireMaxOneParameter(
93 [ 'foo' => 'bar', 'baz' => 'quz' ],
95 $this->assertTrue( true );
98 public function testRequireMaxOneParameterOne() {
99 $mock = new MockApi();
100 $mock->requireMaxOneParameter(
101 [ 'foo' => 'bar', 'baz' => 'quz' ],
103 $this->assertTrue( true );
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' ],
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' ],
124 public function testRequireAtLeastOneParameterOne() {
125 $mock = new MockApi();
126 $mock->requireAtLeastOneParameter(
127 [ 'a' => 'b', 'c' => 'd' ],
129 $this->assertTrue( true );
132 public function testRequireAtLeastOneParameterTwo() {
133 $mock = new MockApi();
134 $mock->requireAtLeastOneParameter(
135 [ 'a' => 'b', 'c' => 'd' ],
137 $this->assertTrue( true );
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 ] );
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() );
154 public function testGetTitleOrPageIdInvalidTitle() {
155 $this->setExpectedException( ApiUsageException
::class,
157 $mock = new MockApi();
158 $mock->getTitleOrPageId( [ 'title' => '|' ] );
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' ] );
168 public function testGetTitleOrPageIdPageId() {
169 $page = $this->getExistingTestPage();
170 $result = ( new MockApi() )->getTitleOrPageId(
171 [ 'pageid' => $page->getId() ] );
172 $this->assertInstanceOf( WikiPage
::class, $result );
174 $page->getTitle()->getPrefixedText(),
175 $result->getTitle()->getPrefixedText()
179 public function testGetTitleOrPageIdInvalidPageId() {
180 // FIXME: fails under postgres
181 $this->markTestSkippedIfDbType( 'postgres' );
183 $this->setExpectedException( ApiUsageException
::class,
184 'There is no page with ID 2147483648.' );
185 $mock = new MockApi();
186 $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] );
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 ] );
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() );
203 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
204 $this->setExpectedException( ApiUsageException
::class,
206 $mock = new MockApi();
207 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
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() );
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 ] );
225 public function testGetParameter() {
226 $mock = $this->getMockBuilder( MockApi
::class )
227 ->setMethods( [ 'getAllowedParams' ] )
229 $mock->method( 'getAllowedParams' )->willReturn( [
231 ApiBase
::PARAM_TYPE
=> [ 'value' ],
234 ApiBase
::PARAM_TYPE
=> [ 'value' ],
237 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
239 $context = new DerivativeContext( $mock );
240 $context->setRequest( new FauxRequest( [ 'foo' => 'bad', 'bar' => 'value' ] ) );
241 $wrapper->mMainModule
= new ApiMain( $context );
243 // Even though 'foo' is bad, getParameter( 'bar' ) must not fail
244 $this->assertSame( 'value', $wrapper->getParameter( 'bar' ) );
246 // But getParameter( 'foo' ) must throw.
248 $wrapper->getParameter( 'foo' );
249 $this->fail( 'Expected exception not thrown' );
250 } catch ( ApiUsageException
$ex ) {
251 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
254 // And extractRequestParams() must throw too.
256 $mock->extractRequestParams();
257 $this->fail( 'Expected exception not thrown' );
258 } catch ( ApiUsageException
$ex ) {
259 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
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
274 private function doGetParameterFromSettings(
275 $input, $paramSettings, $expected, $warnings, $options = []
277 $mock = new MockApi();
278 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
279 if ( $options['prefix'] ) {
280 $wrapper->mModulePrefix
= 'my';
281 $paramName = 'Param';
283 $paramName = 'myParam';
286 $context = new DerivativeContext( $mock );
287 $context->setRequest( new FauxRequest(
288 $input !== null ?
[ 'myParam' => $input ] : [] ) );
289 $wrapper->mMainModule
= new ApiMain( $context );
291 $parseLimits = $options['parseLimits'] ??
true;
293 if ( !empty( $options['apihighlimits'] ) ) {
294 $context->setUser( self
::$users['sysop']->getUser() );
297 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
298 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->mMainModule
);
299 $mainWrapper->mInternalMode
= false;
302 // If we're testing tags, set up some tags
303 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
304 $paramSettings[ApiBase
::PARAM_TYPE
] === 'tags'
306 ChangeTags
::defineTag( 'tag1' );
307 ChangeTags
::defineTag( 'tag2' );
310 if ( $expected instanceof Exception
) {
312 $wrapper->getParameterFromSettings( $paramName, $paramSettings,
314 $this->fail( 'No exception thrown' );
315 } catch ( Exception
$ex ) {
316 $this->assertEquals( $expected, $ex );
319 $result = $wrapper->getParameterFromSettings( $paramName,
320 $paramSettings, $parseLimits );
321 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
322 $paramSettings[ApiBase
::PARAM_TYPE
] === 'timestamp' &&
325 // Allow one second of fuzziness. Make sure the formats are
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' );
333 $this->assertSame( $expected, $result );
335 $actualWarnings = array_map( function ( $warn ) {
336 return $warn instanceof Message
337 ?
array_merge( [ $warn->getKey() ], $warn->getParams() )
339 }, $mock->warnings
);
340 $this->assertSame( $warnings, $actualWarnings );
343 if ( !empty( $paramSettings[ApiBase
::PARAM_SENSITIVE
] ) ||
344 ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
345 $paramSettings[ApiBase
::PARAM_TYPE
] === 'password' )
347 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->getMain() );
348 $this->assertSame( [ 'myParam' ],
349 $mainWrapper->getSensitiveParams() );
354 * @dataProvider provideGetParameterFromSettings
355 * @see self::doGetParameterFromSettings()
357 public function testGetParameterFromSettings_noprefix(
358 $input, $paramSettings, $expected, $warnings, $options = []
360 $options['prefix'] = false;
361 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
365 * @dataProvider provideGetParameterFromSettings
366 * @see self::doGetParameterFromSettings()
368 public function testGetParameterFromSettings_prefix(
369 $input, $paramSettings, $expected, $warnings, $options = []
371 $options['prefix'] = true;
372 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
375 public static function provideGetParameterFromSettings() {
377 [ 'apiwarn-badutf8', 'myParam' ],
382 for ( $i = 0; $i < 32; $i++
) {
384 $enc .= ( $i === 9 ||
$i === 10 ||
$i === 13 )
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' => [
397 [ ApiBase
::PARAM_DFLT
=> 'default', ApiBase
::PARAM_REQUIRED
=> true ],
398 ApiUsageException
::newWithMessage( null,
399 [ 'apierror-missingparam', 'myParam' ] ),
402 'Multi-valued parameter' => [
404 [ ApiBase
::PARAM_ISMULTI
=> true ],
408 'Multi-valued parameter, alternative separator' => [
410 [ ApiBase
::PARAM_ISMULTI
=> true ],
414 'Multi-valued parameter, other C0 controls' => [
416 [ ApiBase
::PARAM_ISMULTI
=> true ],
420 'Multi-valued parameter, other C0 controls (2)' => [
422 [ ApiBase
::PARAM_ISMULTI
=> true ],
423 [ substr( $enc, 0, -3 ), '' ],
426 'Multi-valued parameter with limits' => [
429 ApiBase
::PARAM_ISMULTI
=> true,
430 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 3,
435 'Multi-valued parameter with exceeded limits' => [
438 ApiBase
::PARAM_ISMULTI
=> true,
439 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
441 ApiUsageException
::newWithMessage(
442 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
446 'Multi-valued parameter with exceeded limits for non-bot' => [
449 ApiBase
::PARAM_ISMULTI
=> true,
450 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
451 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
453 ApiUsageException
::newWithMessage(
454 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
458 'Multi-valued parameter with non-exceeded limits for bot' => [
461 ApiBase
::PARAM_ISMULTI
=> true,
462 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
463 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
467 [ 'apihighlimits' => true ],
469 'Multi-valued parameter with prohibited duplicates' => [
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' ],
477 'Multi-valued parameter with allowed duplicates' => [
480 ApiBase
::PARAM_ISMULTI
=> true,
481 ApiBase
::PARAM_ALLOW_DUPLICATES
=> true,
486 'Empty boolean param' => [
488 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
492 'Boolean param 0' => [
494 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
498 'Boolean param false' => [
500 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
504 'Boolean multi-param' => [
507 ApiBase
::PARAM_TYPE
=> 'boolean',
508 ApiBase
::PARAM_ISMULTI
=> true,
511 'Internal error in ApiBase::getParameterFromSettings: ' .
512 'Multi-values not supported for myParam'
516 'Empty boolean param with non-false default' => [
519 ApiBase
::PARAM_TYPE
=> 'boolean',
520 ApiBase
::PARAM_DFLT
=> true,
523 'Internal error in ApiBase::getParameterFromSettings: ' .
524 "Boolean param myParam's default is set to '1'. " .
525 'Boolean parameters must default to false.' ),
528 'Deprecated parameter' => [
530 [ ApiBase
::PARAM_DEPRECATED
=> true ],
532 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
534 'Deprecated parameter with default, unspecified' => [
536 [ ApiBase
::PARAM_DEPRECATED
=> true, ApiBase
::PARAM_DFLT
=> 'foo' ],
540 'Deprecated parameter with default, specified' => [
542 [ ApiBase
::PARAM_DEPRECATED
=> true, ApiBase
::PARAM_DFLT
=> 'foo' ],
544 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
546 'Deprecated parameter value' => [
548 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ] ],
550 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
552 'Deprecated parameter value as default, unspecified' => [
554 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ], ApiBase
::PARAM_DFLT
=> 'a' ],
558 'Deprecated parameter value as default, specified' => [
560 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ], ApiBase
::PARAM_DFLT
=> 'a' ],
562 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
564 'Multiple deprecated parameter values' => [
566 [ ApiBase
::PARAM_DEPRECATED_VALUES
=>
567 [ 'b' => true, 'd' => true ],
568 ApiBase
::PARAM_ISMULTI
=> true ],
569 [ 'a', 'b', 'c', 'd' ],
571 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
572 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
575 'Deprecated parameter value with custom warning' => [
577 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => 'my-msg' ] ],
581 '"*" when wildcard not allowed' => [
583 [ ApiBase
::PARAM_ISMULTI
=> true,
584 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ] ],
586 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
587 [ 'list' => [ '*' ], 'type' => 'comma' ], 1 ] ],
592 ApiBase
::PARAM_ISMULTI
=> true,
593 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
594 ApiBase
::PARAM_ALL
=> true,
599 'Wildcard "*" with multiples not allowed' => [
602 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
603 ApiBase
::PARAM_ALL
=> true,
605 ApiUsageException
::newWithMessage( null,
606 [ 'apierror-unrecognizedvalue', 'myParam', '*' ],
610 'Wildcard "*" with unrestricted type' => [
613 ApiBase
::PARAM_ISMULTI
=> true,
614 ApiBase
::PARAM_ALL
=> true,
622 ApiBase
::PARAM_ISMULTI
=> true,
623 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
624 ApiBase
::PARAM_ALL
=> 'x',
629 'Wildcard conflicting with allowed value' => [
632 ApiBase
::PARAM_ISMULTI
=> true,
633 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
634 ApiBase
::PARAM_ALL
=> 'a',
637 'Internal error in ApiBase::getParameterFromSettings: ' .
638 'For param myParam, PARAM_ALL collides with a possible ' .
642 'Namespace with wildcard' => [
645 ApiBase
::PARAM_ISMULTI
=> true,
646 ApiBase
::PARAM_TYPE
=> 'namespace',
648 MediaWikiServices
::getInstance()->getNamespaceInfo()->getValidNamespaces(),
651 // PARAM_ALL is ignored with namespace types.
652 'Namespace with wildcard suppressed' => [
655 ApiBase
::PARAM_ISMULTI
=> true,
656 ApiBase
::PARAM_TYPE
=> 'namespace',
657 ApiBase
::PARAM_ALL
=> false,
659 MediaWikiServices
::getInstance()->getNamespaceInfo()->getValidNamespaces(),
662 'Namespace with wildcard "x"' => [
665 ApiBase
::PARAM_ISMULTI
=> true,
666 ApiBase
::PARAM_TYPE
=> 'namespace',
667 ApiBase
::PARAM_ALL
=> 'x',
670 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
671 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
674 'dDy+G?e?txnr.1:(@[Ru',
675 [ ApiBase
::PARAM_TYPE
=> 'password' ],
676 'dDy+G?e?txnr.1:(@[Ru',
679 'Sensitive field' => [
680 'I am fond of pineapples',
681 [ ApiBase
::PARAM_SENSITIVE
=> true ],
682 'I am fond of pineapples',
685 'Upload with default' => [
688 ApiBase
::PARAM_TYPE
=> 'upload',
689 ApiBase
::PARAM_DFLT
=> '',
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.' ),
697 'Multiple upload' => [
700 ApiBase
::PARAM_TYPE
=> 'upload',
701 ApiBase
::PARAM_ISMULTI
=> true,
704 'Internal error in ApiBase::getParameterFromSettings: ' .
705 'Multi-values not supported for myParam' ),
708 // @todo Test actual upload
711 [ ApiBase
::PARAM_TYPE
=> 'namespace' ],
712 ApiUsageException
::newWithMessage( null,
713 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
717 'Extra namespace -1' => [
720 ApiBase
::PARAM_TYPE
=> 'namespace',
721 ApiBase
::PARAM_EXTRA_NAMESPACES
=> [ '-1' ],
726 // @todo Test with PARAM_SUBMODULE_MAP unset, need
727 // getModuleManager() to return something real
728 'Nonexistent module' => [
731 ApiBase
::PARAM_TYPE
=> 'submodule',
732 ApiBase
::PARAM_SUBMODULE_MAP
=>
733 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
735 ApiUsageException
::newWithMessage(
738 'apierror-unrecognizedvalue',
746 '\\x1f with multiples not allowed' => [
749 ApiUsageException
::newWithMessage( null,
750 'apierror-badvalue-notmultivalue',
751 'badvalue_notmultivalue' ),
754 'Integer with unenforced min' => [
757 ApiBase
::PARAM_TYPE
=> 'integer',
758 ApiBase
::PARAM_MIN
=> -1,
761 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
764 'Integer with enforced min' => [
767 ApiBase
::PARAM_TYPE
=> 'integer',
768 ApiBase
::PARAM_MIN
=> -1,
769 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
771 ApiUsageException
::newWithMessage( null,
772 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
773 '-1', '-2' ], 'integeroutofrange',
774 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
777 'Integer with unenforced max (internal mode)' => [
780 ApiBase
::PARAM_TYPE
=> 'integer',
781 ApiBase
::PARAM_MAX
=> 7,
786 'Integer with enforced max (internal mode)' => [
789 ApiBase
::PARAM_TYPE
=> 'integer',
790 ApiBase
::PARAM_MAX
=> 7,
791 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
796 'Integer with unenforced max (non-internal mode)' => [
799 ApiBase
::PARAM_TYPE
=> 'integer',
800 ApiBase
::PARAM_MAX
=> 7,
803 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
804 [ 'internalmode' => false ],
806 'Integer with enforced max (non-internal mode)' => [
809 ApiBase
::PARAM_TYPE
=> 'integer',
810 ApiBase
::PARAM_MAX
=> 7,
811 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
813 ApiUsageException
::newWithMessage(
815 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
817 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
820 [ 'internalmode' => false ],
822 'Array of integers' => [
825 ApiBase
::PARAM_ISMULTI
=> true,
826 ApiBase
::PARAM_TYPE
=> 'integer',
831 'Array of integers with unenforced min/max (internal mode)' => [
834 ApiBase
::PARAM_ISMULTI
=> true,
835 ApiBase
::PARAM_TYPE
=> 'integer',
836 ApiBase
::PARAM_MIN
=> 0,
837 ApiBase
::PARAM_MAX
=> 100,
840 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
842 'Array of integers with enforced min/max (internal mode)' => [
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,
851 ApiUsageException
::newWithMessage(
853 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
855 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
859 'Array of integers with unenforced min/max (non-internal mode)' => [
862 ApiBase
::PARAM_ISMULTI
=> true,
863 ApiBase
::PARAM_TYPE
=> 'integer',
864 ApiBase
::PARAM_MIN
=> 0,
865 ApiBase
::PARAM_MAX
=> 100,
869 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
870 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
872 [ 'internalmode' => false ],
874 'Array of integers with enforced min/max (non-internal mode)' => [
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,
883 ApiUsageException
::newWithMessage(
885 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
887 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
890 [ 'internalmode' => false ],
892 'Limit with parseLimits false' => [
894 [ ApiBase
::PARAM_TYPE
=> 'limit' ],
897 [ 'parseLimits' => false ],
899 'Limit with no max' => [
902 ApiBase
::PARAM_TYPE
=> 'limit',
903 ApiBase
::PARAM_MAX2
=> 10,
904 ApiBase
::PARAM_ISMULTI
=> true,
907 'Internal error in ApiBase::getParameterFromSettings: ' .
908 'MAX1 or MAX2 are not defined for the limit myParam' ),
911 'Limit with no max2' => [
914 ApiBase
::PARAM_TYPE
=> 'limit',
915 ApiBase
::PARAM_MAX
=> 10,
916 ApiBase
::PARAM_ISMULTI
=> true,
919 'Internal error in ApiBase::getParameterFromSettings: ' .
920 'MAX1 or MAX2 are not defined for the limit myParam' ),
923 'Limit with multi-value' => [
926 ApiBase
::PARAM_TYPE
=> 'limit',
927 ApiBase
::PARAM_MAX
=> 10,
928 ApiBase
::PARAM_MAX2
=> 10,
929 ApiBase
::PARAM_ISMULTI
=> true,
932 'Internal error in ApiBase::getParameterFromSettings: ' .
933 'Multi-values not supported for myParam' ),
939 ApiBase
::PARAM_TYPE
=> 'limit',
940 ApiBase
::PARAM_MAX
=> 100,
941 ApiBase
::PARAM_MAX2
=> 100,
949 ApiBase
::PARAM_TYPE
=> 'limit',
950 ApiBase
::PARAM_MAX
=> 100,
951 ApiBase
::PARAM_MAX2
=> 101,
956 'Limit max for apihighlimits' => [
959 ApiBase
::PARAM_TYPE
=> 'limit',
960 ApiBase
::PARAM_MAX
=> 100,
961 ApiBase
::PARAM_MAX2
=> 101,
965 [ 'apihighlimits' => true ],
967 'Limit too large (internal mode)' => [
970 ApiBase
::PARAM_TYPE
=> 'limit',
971 ApiBase
::PARAM_MAX
=> 100,
972 ApiBase
::PARAM_MAX2
=> 101,
977 'Limit okay for apihighlimits (internal mode)' => [
980 ApiBase
::PARAM_TYPE
=> 'limit',
981 ApiBase
::PARAM_MAX
=> 100,
982 ApiBase
::PARAM_MAX2
=> 101,
986 [ 'apihighlimits' => true ],
988 'Limit too large for apihighlimits (internal mode)' => [
991 ApiBase
::PARAM_TYPE
=> 'limit',
992 ApiBase
::PARAM_MAX
=> 100,
993 ApiBase
::PARAM_MAX2
=> 101,
997 [ 'apihighlimits' => true ],
999 'Limit too large (non-internal mode)' => [
1002 ApiBase
::PARAM_TYPE
=> 'limit',
1003 ApiBase
::PARAM_MAX
=> 100,
1004 ApiBase
::PARAM_MAX2
=> 101,
1007 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
1008 [ 'internalmode' => false ],
1010 'Limit okay for apihighlimits (non-internal mode)' => [
1013 ApiBase
::PARAM_TYPE
=> 'limit',
1014 ApiBase
::PARAM_MAX
=> 100,
1015 ApiBase
::PARAM_MAX2
=> 101,
1019 [ 'internalmode' => false, 'apihighlimits' => true ],
1021 'Limit too large for apihighlimits (non-internal mode)' => [
1024 ApiBase
::PARAM_TYPE
=> 'limit',
1025 ApiBase
::PARAM_MAX
=> 100,
1026 ApiBase
::PARAM_MAX2
=> 101,
1029 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
1030 [ 'internalmode' => false, 'apihighlimits' => true ],
1032 'Limit too small' => [
1035 ApiBase
::PARAM_TYPE
=> 'limit',
1036 ApiBase
::PARAM_MIN
=> -1,
1037 ApiBase
::PARAM_MAX
=> 100,
1038 ApiBase
::PARAM_MAX2
=> 100,
1041 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
1045 wfTimestamp( TS_UNIX
, '20211221122112' ),
1046 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1052 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1055 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
1057 'Timestamp empty' => [
1059 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1061 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
1063 // wfTimestamp() interprets this as Unix time
1066 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1070 'Timestamp now' => [
1072 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1076 'Invalid timestamp' => [
1078 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1079 ApiUsageException
::newWithMessage(
1081 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
1082 'badtimestamp_myParam'
1086 'Timestamp array' => [
1089 ApiBase
::PARAM_TYPE
=> 'timestamp',
1090 ApiBase
::PARAM_ISMULTI
=> 1,
1092 [ wfTimestamp( TS_MW
, 100 ), wfTimestamp( TS_MW
, 101 ) ],
1097 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1101 'User prefixed with "User:"' => [
1103 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1107 'Invalid username "|"' => [
1109 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1110 ApiUsageException
::newWithMessage( null,
1111 [ 'apierror-baduser', 'myParam', '|' ],
1112 'baduser_myParam' ),
1115 'Invalid username "300.300.300.300"' => [
1117 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1118 ApiUsageException
::newWithMessage( null,
1119 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1120 'baduser_myParam' ),
1123 'IP range as username' => [
1125 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1129 'IPv6 as username' => [
1131 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1135 'Obsolete cloaked usemod IP address as username' => [
1137 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1141 'Invalid username containing IP address' => [
1142 'This is [not] valid 1.2.3.xxx, ha!',
1143 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1144 ApiUsageException
::newWithMessage(
1146 [ 'apierror-baduser', 'myParam', 'This is [not] valid 1.2.3.xxx, ha!' ],
1151 'External username' => [
1153 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1157 'Array of usernames' => [
1160 ApiBase
::PARAM_TYPE
=> 'user',
1161 ApiBase
::PARAM_ISMULTI
=> true,
1168 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1172 'Array of one tag' => [
1175 ApiBase
::PARAM_TYPE
=> 'tags',
1176 ApiBase
::PARAM_ISMULTI
=> true,
1181 'Array of tags' => [
1184 ApiBase
::PARAM_TYPE
=> 'tags',
1185 ApiBase
::PARAM_ISMULTI
=> true,
1192 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1193 new ApiUsageException( null,
1194 Status
::newFatal( 'tags-apply-not-allowed-one',
1195 'invalid tag', 1 ) ),
1198 'Unrecognized type' => [
1200 [ ApiBase
::PARAM_TYPE
=> 'nonexistenttype' ],
1202 'Internal error in ApiBase::getParameterFromSettings: ' .
1203 "Param myParam's type is unknown - nonexistenttype" ),
1206 'Too many bytes' => [
1209 ApiBase
::PARAM_MAX_BYTES
=> 0,
1210 ApiBase
::PARAM_MAX_CHARS
=> 0,
1212 ApiUsageException
::newWithMessage( null,
1213 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1216 'Too many chars' => [
1219 ApiBase
::PARAM_MAX_BYTES
=> 4,
1220 ApiBase
::PARAM_MAX_CHARS
=> 1,
1222 ApiUsageException
::newWithMessage( null,
1223 [ 'apierror-maxchars', 'myParam', 1 ] ),
1226 'Omitted required param' => [
1228 [ ApiBase
::PARAM_REQUIRED
=> true ],
1229 ApiUsageException
::newWithMessage( null,
1230 [ 'apierror-missingparam', 'myParam' ] ),
1233 'Empty multi-value' => [
1235 [ ApiBase
::PARAM_ISMULTI
=> true ],
1239 'Multi-value \x1f' => [
1241 [ ApiBase
::PARAM_ISMULTI
=> true ],
1245 'Allowed non-multi-value with "|"' => [
1247 [ ApiBase
::PARAM_TYPE
=> [ 'a|b' ] ],
1251 'Prohibited multi-value' => [
1253 [ ApiBase
::PARAM_TYPE
=> [ 'a', 'b' ] ],
1254 ApiUsageException
::newWithMessage( null,
1256 'apierror-multival-only-one-of',
1258 Message
::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1267 // The following really just test PHP's string-to-int conversion.
1275 [ "\t1", 1, '\t1' ],
1276 [ "\r1", 1, '\r1' ],
1277 [ "\f1", 0, '\f1', 'badutf-8' ],
1278 [ "\n1", 1, '\n1' ],
1279 [ "\v1", 0, '\v1', 'badutf-8' ],
1280 [ "\e1", 0, '\e1', 'badutf-8' ],
1281 [ "\x001", 0, '\x001', 'badutf-8' ],
1284 foreach ( $integerTests as $test ) {
1285 $desc = $test[2] ??
$test[0];
1286 $warnings = isset( $test[3] ) ?
1287 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1288 $returnArray["\"$desc\" as integer"] = [
1290 [ ApiBase
::PARAM_TYPE
=> 'integer' ],
1296 return $returnArray;
1299 public function testErrorArrayToStatus() {
1300 $mock = new MockApi();
1302 $msg = new Message( 'mainpage' );
1304 // Sanity check empty array
1305 $expect = Status
::newGood();
1306 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
1308 // No blocked $user, so no special block handling
1309 $expect = Status
::newGood();
1310 $expect->fatal( 'blockedtext' );
1311 $expect->fatal( 'autoblockedtext' );
1312 $expect->fatal( 'systemblockedtext' );
1313 $expect->fatal( 'mainpage' );
1314 $expect->fatal( $msg );
1315 $expect->fatal( $msg, 'foobar' );
1316 $expect->fatal( 'parentheses', 'foobar' );
1317 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1319 [ 'autoblockedtext' ],
1320 [ 'systemblockedtext' ],
1324 [ 'parentheses', 'foobar' ],
1327 // Has a blocked $user, so special block handling
1328 $user = $this->getMutableTestUser()->getUser();
1329 $block = new DatabaseBlock( [
1330 'address' => $user->getName(),
1331 'user' => $user->getID(),
1332 'by' => $this->getTestSysop()->getUser()->getId(),
1333 'reason' => __METHOD__
,
1334 'expiry' => time() +
100500,
1337 $userInfoTrait = TestingAccessWrapper
::newFromObject(
1338 $this->getMockForTrait( ApiBlockInfoTrait
::class )
1340 $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockDetails( $block ) ];
1342 $expect = Status
::newGood();
1343 $expect->fatal( ApiMessage
::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1344 $expect->fatal( ApiMessage
::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1345 $expect->fatal( ApiMessage
::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1346 $expect->fatal( 'mainpage' );
1347 $expect->fatal( $msg );
1348 $expect->fatal( $msg, 'foobar' );
1349 $expect->fatal( 'parentheses', 'foobar' );
1350 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1352 [ 'autoblockedtext' ],
1353 [ 'systemblockedtext' ],
1357 [ 'parentheses', 'foobar' ],
1361 public function testAddBlockInfoToStatus() {
1362 $mock = new MockApi();
1364 $msg = new Message( 'mainpage' );
1366 // Sanity check empty array
1367 $expect = Status
::newGood();
1368 $test = Status
::newGood();
1369 $mock->addBlockInfoToStatus( $test );
1370 $this->assertEquals( $expect, $test );
1372 // No blocked $user, so no special block handling
1373 $expect = Status
::newGood();
1374 $expect->fatal( 'blockedtext' );
1375 $expect->fatal( 'autoblockedtext' );
1376 $expect->fatal( 'systemblockedtext' );
1377 $expect->fatal( 'mainpage' );
1378 $expect->fatal( $msg );
1379 $expect->fatal( $msg, 'foobar' );
1380 $expect->fatal( 'parentheses', 'foobar' );
1381 $test = clone $expect;
1382 $mock->addBlockInfoToStatus( $test );
1383 $this->assertEquals( $expect, $test );
1385 // Has a blocked $user, so special block handling
1386 $user = $this->getMutableTestUser()->getUser();
1387 $block = new DatabaseBlock( [
1388 'address' => $user->getName(),
1389 'user' => $user->getID(),
1390 'by' => $this->getTestSysop()->getUser()->getId(),
1391 'reason' => __METHOD__
,
1392 'expiry' => time() +
100500,
1395 $userInfoTrait = TestingAccessWrapper
::newFromObject(
1396 $this->getObjectForTrait( ApiBlockInfoTrait
::class )
1398 $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockDetails( $block ) ];
1400 $expect = Status
::newGood();
1401 $expect->fatal( ApiMessage
::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1402 $expect->fatal( ApiMessage
::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1403 $expect->fatal( ApiMessage
::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1404 $expect->fatal( 'mainpage' );
1405 $expect->fatal( $msg );
1406 $expect->fatal( $msg, 'foobar' );
1407 $expect->fatal( 'parentheses', 'foobar' );
1408 $test = Status
::newGood();
1409 $test->fatal( 'blockedtext' );
1410 $test->fatal( 'autoblockedtext' );
1411 $test->fatal( 'systemblockedtext' );
1412 $test->fatal( 'mainpage' );
1413 $test->fatal( $msg );
1414 $test->fatal( $msg, 'foobar' );
1415 $test->fatal( 'parentheses', 'foobar' );
1416 $mock->addBlockInfoToStatus( $test, $user );
1417 $this->assertEquals( $expect, $test );
1420 public function testDieStatus() {
1421 $mock = new MockApi();
1423 $status = StatusValue
::newGood();
1424 $status->error( 'foo' );
1425 $status->warning( 'bar' );
1427 $mock->dieStatus( $status );
1428 $this->fail( 'Expected exception not thrown' );
1429 } catch ( ApiUsageException
$ex ) {
1430 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1431 $this->assertFalse( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1434 $status = StatusValue
::newGood();
1435 $status->warning( 'foo' );
1436 $status->warning( 'bar' );
1438 $mock->dieStatus( $status );
1439 $this->fail( 'Expected exception not thrown' );
1440 } catch ( ApiUsageException
$ex ) {
1441 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1442 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1445 $status = StatusValue
::newGood();
1446 $status->setOK( false );
1448 $mock->dieStatus( $status );
1449 $this->fail( 'Expected exception not thrown' );
1450 } catch ( ApiUsageException
$ex ) {
1451 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'unknownerror-nocode' ),
1452 'Exception has "unknownerror-nocode"' );
1457 * @covers ApiBase::extractRequestParams
1459 public function testExtractRequestParams() {
1460 $request = new FauxRequest( [
1461 'xxexists' => 'exists!',
1462 'xxmulti' => 'a|b|c|d|{bad}',
1464 'xxtemplate-a' => 'A!',
1465 'xxtemplate-b' => 'B1|B2|B3',
1466 'xxtemplate-c' => '',
1467 'xxrecursivetemplate-b-B1' => 'X',
1468 'xxrecursivetemplate-b-B3' => 'Y',
1469 'xxrecursivetemplate-b-B4' => '?',
1470 'xxemptytemplate-' => 'nope',
1473 'errorformat' => 'raw',
1475 $context = new DerivativeContext( RequestContext
::getMain() );
1476 $context->setRequest( $request );
1477 $main = new ApiMain( $context );
1479 $mock = $this->getMockBuilder( ApiBase
::class )
1480 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1481 ->setMethods( [ 'getAllowedParams' ] )
1482 ->getMockForAbstractClass();
1483 $mock->method( 'getAllowedParams' )->willReturn( [
1484 'notexists' => null,
1487 ApiBase
::PARAM_ISMULTI
=> true,
1490 ApiBase
::PARAM_ISMULTI
=> true,
1493 ApiBase
::PARAM_ISMULTI
=> true,
1494 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'multi' ],
1496 'recursivetemplate-{m}-{t}' => [
1497 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 't' => 'template-{m}', 'm' => 'multi' ],
1499 'emptytemplate-{m}' => [
1500 ApiBase
::PARAM_ISMULTI
=> true,
1501 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'empty' ],
1503 'badtemplate-{e}' => [
1504 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'exists' ],
1506 'badtemplate2-{e}' => [
1507 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'badtemplate2-{e}' ],
1509 'badtemplate3-{x}' => [
1510 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'x' => 'foo' ],
1514 $this->assertEquals( [
1515 'notexists' => null,
1516 'exists' => 'exists!',
1517 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1519 'template-a' => [ 'A!' ],
1520 'template-b' => [ 'B1', 'B2', 'B3' ],
1522 'template-d' => null,
1523 'recursivetemplate-a-A!' => null,
1524 'recursivetemplate-b-B1' => 'X',
1525 'recursivetemplate-b-B2' => null,
1526 'recursivetemplate-b-B3' => 'Y',
1527 ], $mock->extractRequestParams() );
1529 $used = TestingAccessWrapper
::newFromObject( $main )->getParamsUsed();
1531 $this->assertEquals( [
1536 'xxrecursivetemplate-a-A!',
1537 'xxrecursivetemplate-b-B1',
1538 'xxrecursivetemplate-b-B2',
1539 'xxrecursivetemplate-b-B3',
1546 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1547 $this->assertCount( 1, $warnings );
1548 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );