3 use MediaWiki\MediaWikiServices
;
4 use Wikimedia\TestingAccessWrapper
;
13 class ApiBaseTest
extends ApiTestCase
{
15 * This covers a variety of stub methods that return a fixed value.
17 * @param string|array $method Name of method, or [ name, params... ]
18 * @param string $value Expected value
20 * @dataProvider provideStubMethods
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 );
29 public function provideStubMethods() {
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' ],
49 public function testRequireOnlyOneParameterDefault() {
50 $mock = new MockApi();
51 $mock->requireOnlyOneParameter(
52 [ "filename" => "foo.txt", "enablechunks" => false ],
53 "filename", "enablechunks"
55 $this->assertTrue( true );
59 * @expectedException ApiUsageException
61 public function testRequireOnlyOneParameterZero() {
62 $mock = new MockApi();
63 $mock->requireOnlyOneParameter(
64 [ "filename" => "foo.txt", "enablechunks" => 0 ],
65 "filename", "enablechunks"
70 * @expectedException ApiUsageException
72 public function testRequireOnlyOneParameterTrue() {
73 $mock = new MockApi();
74 $mock->requireOnlyOneParameter(
75 [ "filename" => "foo.txt", "enablechunks" => true ],
76 "filename", "enablechunks"
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 ],
89 public function testRequireMaxOneParameterZero() {
90 $mock = new MockApi();
91 $mock->requireMaxOneParameter(
92 [ 'foo' => 'bar', 'baz' => 'quz' ],
94 $this->assertTrue( true );
97 public function testRequireMaxOneParameterOne() {
98 $mock = new MockApi();
99 $mock->requireMaxOneParameter(
100 [ 'foo' => 'bar', 'baz' => 'quz' ],
102 $this->assertTrue( true );
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' ],
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' ],
123 public function testRequireAtLeastOneParameterOne() {
124 $mock = new MockApi();
125 $mock->requireAtLeastOneParameter(
126 [ 'a' => 'b', 'c' => 'd' ],
128 $this->assertTrue( true );
131 public function testRequireAtLeastOneParameterTwo() {
132 $mock = new MockApi();
133 $mock->requireAtLeastOneParameter(
134 [ 'a' => 'b', 'c' => 'd' ],
136 $this->assertTrue( true );
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 ] );
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() );
153 public function testGetTitleOrPageIdInvalidTitle() {
154 $this->setExpectedException( ApiUsageException
::class,
156 $mock = new MockApi();
157 $mock->getTitleOrPageId( [ 'title' => '|' ] );
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' ] );
167 public function testGetTitleOrPageIdPageId() {
168 $page = $this->getExistingTestPage();
169 $result = ( new MockApi() )->getTitleOrPageId(
170 [ 'pageid' => $page->getId() ] );
171 $this->assertInstanceOf( WikiPage
::class, $result );
173 $page->getTitle()->getPrefixedText(),
174 $result->getTitle()->getPrefixedText()
178 public function testGetTitleOrPageIdInvalidPageId() {
179 // FIXME: fails under postgres
180 $this->markTestSkippedIfDbType( 'postgres' );
182 $this->setExpectedException( ApiUsageException
::class,
183 'There is no page with ID 2147483648.' );
184 $mock = new MockApi();
185 $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] );
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 ] );
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() );
202 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
203 $this->setExpectedException( ApiUsageException
::class,
205 $mock = new MockApi();
206 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
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() );
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 ] );
224 public function testGetParameter() {
225 $mock = $this->getMockBuilder( MockApi
::class )
226 ->setMethods( [ 'getAllowedParams' ] )
228 $mock->method( 'getAllowedParams' )->willReturn( [
230 ApiBase
::PARAM_TYPE
=> [ 'value' ],
233 ApiBase
::PARAM_TYPE
=> [ 'value' ],
236 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
238 $context = new DerivativeContext( $mock );
239 $context->setRequest( new FauxRequest( [ 'foo' => 'bad', 'bar' => 'value' ] ) );
240 $wrapper->mMainModule
= new ApiMain( $context );
242 // Even though 'foo' is bad, getParameter( 'bar' ) must not fail
243 $this->assertSame( 'value', $wrapper->getParameter( 'bar' ) );
245 // But getParameter( 'foo' ) must throw.
247 $wrapper->getParameter( 'foo' );
248 $this->fail( 'Expected exception not thrown' );
249 } catch ( ApiUsageException
$ex ) {
250 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
253 // And extractRequestParams() must throw too.
255 $mock->extractRequestParams();
256 $this->fail( 'Expected exception not thrown' );
257 } catch ( ApiUsageException
$ex ) {
258 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
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
273 private function doGetParameterFromSettings(
274 $input, $paramSettings, $expected, $warnings, $options = []
276 $mock = new MockApi();
277 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
278 if ( $options['prefix'] ) {
279 $wrapper->mModulePrefix
= 'my';
280 $paramName = 'Param';
282 $paramName = 'myParam';
285 $context = new DerivativeContext( $mock );
286 $context->setRequest( new FauxRequest(
287 $input !== null ?
[ 'myParam' => $input ] : [] ) );
288 $wrapper->mMainModule
= new ApiMain( $context );
290 $parseLimits = $options['parseLimits'] ??
true;
292 if ( !empty( $options['apihighlimits'] ) ) {
293 $context->setUser( self
::$users['sysop']->getUser() );
296 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
297 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->mMainModule
);
298 $mainWrapper->mInternalMode
= false;
301 // If we're testing tags, set up some tags
302 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
303 $paramSettings[ApiBase
::PARAM_TYPE
] === 'tags'
305 ChangeTags
::defineTag( 'tag1' );
306 ChangeTags
::defineTag( 'tag2' );
309 if ( $expected instanceof Exception
) {
311 $wrapper->getParameterFromSettings( $paramName, $paramSettings,
313 $this->fail( 'No exception thrown' );
314 } catch ( Exception
$ex ) {
315 $this->assertEquals( $expected, $ex );
318 $result = $wrapper->getParameterFromSettings( $paramName,
319 $paramSettings, $parseLimits );
320 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
321 $paramSettings[ApiBase
::PARAM_TYPE
] === 'timestamp' &&
324 // Allow one second of fuzziness. Make sure the formats are
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' );
332 $this->assertSame( $expected, $result );
334 $actualWarnings = array_map( function ( $warn ) {
335 return $warn instanceof Message
336 ?
array_merge( [ $warn->getKey() ], $warn->getParams() )
338 }, $mock->warnings
);
339 $this->assertSame( $warnings, $actualWarnings );
342 if ( !empty( $paramSettings[ApiBase
::PARAM_SENSITIVE
] ) ||
343 ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
344 $paramSettings[ApiBase
::PARAM_TYPE
] === 'password' )
346 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->getMain() );
347 $this->assertSame( [ 'myParam' ],
348 $mainWrapper->getSensitiveParams() );
353 * @dataProvider provideGetParameterFromSettings
354 * @see self::doGetParameterFromSettings()
356 public function testGetParameterFromSettings_noprefix(
357 $input, $paramSettings, $expected, $warnings, $options = []
359 $options['prefix'] = false;
360 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
364 * @dataProvider provideGetParameterFromSettings
365 * @see self::doGetParameterFromSettings()
367 public function testGetParameterFromSettings_prefix(
368 $input, $paramSettings, $expected, $warnings, $options = []
370 $options['prefix'] = true;
371 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
374 public static function provideGetParameterFromSettings() {
376 [ 'apiwarn-badutf8', 'myParam' ],
381 for ( $i = 0; $i < 32; $i++
) {
383 $enc .= ( $i === 9 ||
$i === 10 ||
$i === 13 )
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' => [
396 [ ApiBase
::PARAM_DFLT
=> 'default', ApiBase
::PARAM_REQUIRED
=> true ],
397 ApiUsageException
::newWithMessage( null,
398 [ 'apierror-missingparam', 'myParam' ] ),
401 'Multi-valued parameter' => [
403 [ ApiBase
::PARAM_ISMULTI
=> true ],
407 'Multi-valued parameter, alternative separator' => [
409 [ ApiBase
::PARAM_ISMULTI
=> true ],
413 'Multi-valued parameter, other C0 controls' => [
415 [ ApiBase
::PARAM_ISMULTI
=> true ],
419 'Multi-valued parameter, other C0 controls (2)' => [
421 [ ApiBase
::PARAM_ISMULTI
=> true ],
422 [ substr( $enc, 0, -3 ), '' ],
425 'Multi-valued parameter with limits' => [
428 ApiBase
::PARAM_ISMULTI
=> true,
429 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 3,
434 'Multi-valued parameter with exceeded limits' => [
437 ApiBase
::PARAM_ISMULTI
=> true,
438 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
440 ApiUsageException
::newWithMessage(
441 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
445 'Multi-valued parameter with exceeded limits for non-bot' => [
448 ApiBase
::PARAM_ISMULTI
=> true,
449 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
450 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
452 ApiUsageException
::newWithMessage(
453 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
457 'Multi-valued parameter with non-exceeded limits for bot' => [
460 ApiBase
::PARAM_ISMULTI
=> true,
461 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
462 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
466 [ 'apihighlimits' => true ],
468 'Multi-valued parameter with prohibited duplicates' => [
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' ],
476 'Multi-valued parameter with allowed duplicates' => [
479 ApiBase
::PARAM_ISMULTI
=> true,
480 ApiBase
::PARAM_ALLOW_DUPLICATES
=> true,
485 'Empty boolean param' => [
487 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
491 'Boolean param 0' => [
493 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
497 'Boolean param false' => [
499 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
503 'Boolean multi-param' => [
506 ApiBase
::PARAM_TYPE
=> 'boolean',
507 ApiBase
::PARAM_ISMULTI
=> true,
510 'Internal error in ApiBase::getParameterFromSettings: ' .
511 'Multi-values not supported for myParam'
515 'Empty boolean param with non-false default' => [
518 ApiBase
::PARAM_TYPE
=> 'boolean',
519 ApiBase
::PARAM_DFLT
=> true,
522 'Internal error in ApiBase::getParameterFromSettings: ' .
523 "Boolean param myParam's default is set to '1'. " .
524 'Boolean parameters must default to false.' ),
527 'Deprecated parameter' => [
529 [ ApiBase
::PARAM_DEPRECATED
=> true ],
531 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
533 'Deprecated parameter with default, unspecified' => [
535 [ ApiBase
::PARAM_DEPRECATED
=> true, ApiBase
::PARAM_DFLT
=> 'foo' ],
539 'Deprecated parameter with default, specified' => [
541 [ ApiBase
::PARAM_DEPRECATED
=> true, ApiBase
::PARAM_DFLT
=> 'foo' ],
543 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
545 'Deprecated parameter value' => [
547 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ] ],
549 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
551 'Deprecated parameter value as default, unspecified' => [
553 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ], ApiBase
::PARAM_DFLT
=> 'a' ],
557 'Deprecated parameter value as default, specified' => [
559 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ], ApiBase
::PARAM_DFLT
=> 'a' ],
561 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
563 'Multiple deprecated parameter values' => [
565 [ ApiBase
::PARAM_DEPRECATED_VALUES
=>
566 [ 'b' => true, 'd' => true ],
567 ApiBase
::PARAM_ISMULTI
=> true ],
568 [ 'a', 'b', 'c', 'd' ],
570 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
571 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
574 'Deprecated parameter value with custom warning' => [
576 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => 'my-msg' ] ],
580 '"*" when wildcard not allowed' => [
582 [ ApiBase
::PARAM_ISMULTI
=> true,
583 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ] ],
585 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
586 [ 'list' => [ '*' ], 'type' => 'comma' ], 1 ] ],
591 ApiBase
::PARAM_ISMULTI
=> true,
592 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
593 ApiBase
::PARAM_ALL
=> true,
598 'Wildcard "*" with multiples not allowed' => [
601 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
602 ApiBase
::PARAM_ALL
=> true,
604 ApiUsageException
::newWithMessage( null,
605 [ 'apierror-unrecognizedvalue', 'myParam', '*' ],
609 'Wildcard "*" with unrestricted type' => [
612 ApiBase
::PARAM_ISMULTI
=> true,
613 ApiBase
::PARAM_ALL
=> true,
621 ApiBase
::PARAM_ISMULTI
=> true,
622 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
623 ApiBase
::PARAM_ALL
=> 'x',
628 'Wildcard conflicting with allowed value' => [
631 ApiBase
::PARAM_ISMULTI
=> true,
632 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
633 ApiBase
::PARAM_ALL
=> 'a',
636 'Internal error in ApiBase::getParameterFromSettings: ' .
637 'For param myParam, PARAM_ALL collides with a possible ' .
641 'Namespace with wildcard' => [
644 ApiBase
::PARAM_ISMULTI
=> true,
645 ApiBase
::PARAM_TYPE
=> 'namespace',
647 MediaWikiServices
::getInstance()->getNamespaceInfo()->getValidNamespaces(),
650 // PARAM_ALL is ignored with namespace types.
651 'Namespace with wildcard suppressed' => [
654 ApiBase
::PARAM_ISMULTI
=> true,
655 ApiBase
::PARAM_TYPE
=> 'namespace',
656 ApiBase
::PARAM_ALL
=> false,
658 MediaWikiServices
::getInstance()->getNamespaceInfo()->getValidNamespaces(),
661 'Namespace with wildcard "x"' => [
664 ApiBase
::PARAM_ISMULTI
=> true,
665 ApiBase
::PARAM_TYPE
=> 'namespace',
666 ApiBase
::PARAM_ALL
=> 'x',
669 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
670 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
673 'dDy+G?e?txnr.1:(@[Ru',
674 [ ApiBase
::PARAM_TYPE
=> 'password' ],
675 'dDy+G?e?txnr.1:(@[Ru',
678 'Sensitive field' => [
679 'I am fond of pineapples',
680 [ ApiBase
::PARAM_SENSITIVE
=> true ],
681 'I am fond of pineapples',
684 'Upload with default' => [
687 ApiBase
::PARAM_TYPE
=> 'upload',
688 ApiBase
::PARAM_DFLT
=> '',
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.' ),
696 'Multiple upload' => [
699 ApiBase
::PARAM_TYPE
=> 'upload',
700 ApiBase
::PARAM_ISMULTI
=> true,
703 'Internal error in ApiBase::getParameterFromSettings: ' .
704 'Multi-values not supported for myParam' ),
707 // @todo Test actual upload
710 [ ApiBase
::PARAM_TYPE
=> 'namespace' ],
711 ApiUsageException
::newWithMessage( null,
712 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
716 'Extra namespace -1' => [
719 ApiBase
::PARAM_TYPE
=> 'namespace',
720 ApiBase
::PARAM_EXTRA_NAMESPACES
=> [ '-1' ],
725 // @todo Test with PARAM_SUBMODULE_MAP unset, need
726 // getModuleManager() to return something real
727 'Nonexistent module' => [
730 ApiBase
::PARAM_TYPE
=> 'submodule',
731 ApiBase
::PARAM_SUBMODULE_MAP
=>
732 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
734 ApiUsageException
::newWithMessage(
737 'apierror-unrecognizedvalue',
745 '\\x1f with multiples not allowed' => [
748 ApiUsageException
::newWithMessage( null,
749 'apierror-badvalue-notmultivalue',
750 'badvalue_notmultivalue' ),
753 'Integer with unenforced min' => [
756 ApiBase
::PARAM_TYPE
=> 'integer',
757 ApiBase
::PARAM_MIN
=> -1,
760 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
763 'Integer with enforced min' => [
766 ApiBase
::PARAM_TYPE
=> 'integer',
767 ApiBase
::PARAM_MIN
=> -1,
768 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
770 ApiUsageException
::newWithMessage( null,
771 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
772 '-1', '-2' ], 'integeroutofrange',
773 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
776 'Integer with unenforced max (internal mode)' => [
779 ApiBase
::PARAM_TYPE
=> 'integer',
780 ApiBase
::PARAM_MAX
=> 7,
785 'Integer with enforced max (internal mode)' => [
788 ApiBase
::PARAM_TYPE
=> 'integer',
789 ApiBase
::PARAM_MAX
=> 7,
790 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
795 'Integer with unenforced max (non-internal mode)' => [
798 ApiBase
::PARAM_TYPE
=> 'integer',
799 ApiBase
::PARAM_MAX
=> 7,
802 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
803 [ 'internalmode' => false ],
805 'Integer with enforced max (non-internal mode)' => [
808 ApiBase
::PARAM_TYPE
=> 'integer',
809 ApiBase
::PARAM_MAX
=> 7,
810 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
812 ApiUsageException
::newWithMessage(
814 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
816 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
819 [ 'internalmode' => false ],
821 'Array of integers' => [
824 ApiBase
::PARAM_ISMULTI
=> true,
825 ApiBase
::PARAM_TYPE
=> 'integer',
830 'Array of integers with unenforced min/max (internal mode)' => [
833 ApiBase
::PARAM_ISMULTI
=> true,
834 ApiBase
::PARAM_TYPE
=> 'integer',
835 ApiBase
::PARAM_MIN
=> 0,
836 ApiBase
::PARAM_MAX
=> 100,
839 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
841 'Array of integers with enforced min/max (internal mode)' => [
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,
850 ApiUsageException
::newWithMessage(
852 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
854 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
858 'Array of integers with unenforced min/max (non-internal mode)' => [
861 ApiBase
::PARAM_ISMULTI
=> true,
862 ApiBase
::PARAM_TYPE
=> 'integer',
863 ApiBase
::PARAM_MIN
=> 0,
864 ApiBase
::PARAM_MAX
=> 100,
868 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
869 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
871 [ 'internalmode' => false ],
873 'Array of integers with enforced min/max (non-internal mode)' => [
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,
882 ApiUsageException
::newWithMessage(
884 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
886 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
889 [ 'internalmode' => false ],
891 'Limit with parseLimits false' => [
893 [ ApiBase
::PARAM_TYPE
=> 'limit' ],
896 [ 'parseLimits' => false ],
898 'Limit with no max' => [
901 ApiBase
::PARAM_TYPE
=> 'limit',
902 ApiBase
::PARAM_MAX2
=> 10,
903 ApiBase
::PARAM_ISMULTI
=> true,
906 'Internal error in ApiBase::getParameterFromSettings: ' .
907 'MAX1 or MAX2 are not defined for the limit myParam' ),
910 'Limit with no max2' => [
913 ApiBase
::PARAM_TYPE
=> 'limit',
914 ApiBase
::PARAM_MAX
=> 10,
915 ApiBase
::PARAM_ISMULTI
=> true,
918 'Internal error in ApiBase::getParameterFromSettings: ' .
919 'MAX1 or MAX2 are not defined for the limit myParam' ),
922 'Limit with multi-value' => [
925 ApiBase
::PARAM_TYPE
=> 'limit',
926 ApiBase
::PARAM_MAX
=> 10,
927 ApiBase
::PARAM_MAX2
=> 10,
928 ApiBase
::PARAM_ISMULTI
=> true,
931 'Internal error in ApiBase::getParameterFromSettings: ' .
932 'Multi-values not supported for myParam' ),
938 ApiBase
::PARAM_TYPE
=> 'limit',
939 ApiBase
::PARAM_MAX
=> 100,
940 ApiBase
::PARAM_MAX2
=> 100,
948 ApiBase
::PARAM_TYPE
=> 'limit',
949 ApiBase
::PARAM_MAX
=> 100,
950 ApiBase
::PARAM_MAX2
=> 101,
955 'Limit max for apihighlimits' => [
958 ApiBase
::PARAM_TYPE
=> 'limit',
959 ApiBase
::PARAM_MAX
=> 100,
960 ApiBase
::PARAM_MAX2
=> 101,
964 [ 'apihighlimits' => true ],
966 'Limit too large (internal mode)' => [
969 ApiBase
::PARAM_TYPE
=> 'limit',
970 ApiBase
::PARAM_MAX
=> 100,
971 ApiBase
::PARAM_MAX2
=> 101,
976 'Limit okay for apihighlimits (internal mode)' => [
979 ApiBase
::PARAM_TYPE
=> 'limit',
980 ApiBase
::PARAM_MAX
=> 100,
981 ApiBase
::PARAM_MAX2
=> 101,
985 [ 'apihighlimits' => true ],
987 'Limit too large for apihighlimits (internal mode)' => [
990 ApiBase
::PARAM_TYPE
=> 'limit',
991 ApiBase
::PARAM_MAX
=> 100,
992 ApiBase
::PARAM_MAX2
=> 101,
996 [ 'apihighlimits' => true ],
998 'Limit too large (non-internal mode)' => [
1001 ApiBase
::PARAM_TYPE
=> 'limit',
1002 ApiBase
::PARAM_MAX
=> 100,
1003 ApiBase
::PARAM_MAX2
=> 101,
1006 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
1007 [ 'internalmode' => false ],
1009 'Limit okay for apihighlimits (non-internal mode)' => [
1012 ApiBase
::PARAM_TYPE
=> 'limit',
1013 ApiBase
::PARAM_MAX
=> 100,
1014 ApiBase
::PARAM_MAX2
=> 101,
1018 [ 'internalmode' => false, 'apihighlimits' => true ],
1020 'Limit too large for apihighlimits (non-internal mode)' => [
1023 ApiBase
::PARAM_TYPE
=> 'limit',
1024 ApiBase
::PARAM_MAX
=> 100,
1025 ApiBase
::PARAM_MAX2
=> 101,
1028 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
1029 [ 'internalmode' => false, 'apihighlimits' => true ],
1031 'Limit too small' => [
1034 ApiBase
::PARAM_TYPE
=> 'limit',
1035 ApiBase
::PARAM_MIN
=> -1,
1036 ApiBase
::PARAM_MAX
=> 100,
1037 ApiBase
::PARAM_MAX2
=> 100,
1040 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
1044 wfTimestamp( TS_UNIX
, '20211221122112' ),
1045 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1051 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1054 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
1056 'Timestamp empty' => [
1058 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1060 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
1062 // wfTimestamp() interprets this as Unix time
1065 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1069 'Timestamp now' => [
1071 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1075 'Invalid timestamp' => [
1077 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1078 ApiUsageException
::newWithMessage(
1080 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
1081 'badtimestamp_myParam'
1085 'Timestamp array' => [
1088 ApiBase
::PARAM_TYPE
=> 'timestamp',
1089 ApiBase
::PARAM_ISMULTI
=> 1,
1091 [ wfTimestamp( TS_MW
, 100 ), wfTimestamp( TS_MW
, 101 ) ],
1096 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1100 'User prefixed with "User:"' => [
1102 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1106 'Invalid username "|"' => [
1108 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1109 ApiUsageException
::newWithMessage( null,
1110 [ 'apierror-baduser', 'myParam', '|' ],
1111 'baduser_myParam' ),
1114 'Invalid username "300.300.300.300"' => [
1116 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1117 ApiUsageException
::newWithMessage( null,
1118 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1119 'baduser_myParam' ),
1122 'IP range as username' => [
1124 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1128 'IPv6 as username' => [
1130 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1134 'Obsolete cloaked usemod IP address as username' => [
1136 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1140 'Invalid username containing IP address' => [
1141 'This is [not] valid 1.2.3.xxx, ha!',
1142 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1143 ApiUsageException
::newWithMessage(
1145 [ 'apierror-baduser', 'myParam', 'This is [not] valid 1.2.3.xxx, ha!' ],
1150 'External username' => [
1152 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1156 'Array of usernames' => [
1159 ApiBase
::PARAM_TYPE
=> 'user',
1160 ApiBase
::PARAM_ISMULTI
=> true,
1167 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1171 'Array of one tag' => [
1174 ApiBase
::PARAM_TYPE
=> 'tags',
1175 ApiBase
::PARAM_ISMULTI
=> true,
1180 'Array of tags' => [
1183 ApiBase
::PARAM_TYPE
=> 'tags',
1184 ApiBase
::PARAM_ISMULTI
=> true,
1191 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1192 new ApiUsageException( null,
1193 Status
::newFatal( 'tags-apply-not-allowed-one',
1194 'invalid tag', 1 ) ),
1197 'Unrecognized type' => [
1199 [ ApiBase
::PARAM_TYPE
=> 'nonexistenttype' ],
1201 'Internal error in ApiBase::getParameterFromSettings: ' .
1202 "Param myParam's type is unknown - nonexistenttype" ),
1205 'Too many bytes' => [
1208 ApiBase
::PARAM_MAX_BYTES
=> 0,
1209 ApiBase
::PARAM_MAX_CHARS
=> 0,
1211 ApiUsageException
::newWithMessage( null,
1212 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1215 'Too many chars' => [
1218 ApiBase
::PARAM_MAX_BYTES
=> 4,
1219 ApiBase
::PARAM_MAX_CHARS
=> 1,
1221 ApiUsageException
::newWithMessage( null,
1222 [ 'apierror-maxchars', 'myParam', 1 ] ),
1225 'Omitted required param' => [
1227 [ ApiBase
::PARAM_REQUIRED
=> true ],
1228 ApiUsageException
::newWithMessage( null,
1229 [ 'apierror-missingparam', 'myParam' ] ),
1232 'Empty multi-value' => [
1234 [ ApiBase
::PARAM_ISMULTI
=> true ],
1238 'Multi-value \x1f' => [
1240 [ ApiBase
::PARAM_ISMULTI
=> true ],
1244 'Allowed non-multi-value with "|"' => [
1246 [ ApiBase
::PARAM_TYPE
=> [ 'a|b' ] ],
1250 'Prohibited multi-value' => [
1252 [ ApiBase
::PARAM_TYPE
=> [ 'a', 'b' ] ],
1253 ApiUsageException
::newWithMessage( null,
1255 'apierror-multival-only-one-of',
1257 Message
::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1266 // The following really just test PHP's string-to-int conversion.
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' ],
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"] = [
1289 [ ApiBase
::PARAM_TYPE
=> 'integer' ],
1295 return $returnArray;
1298 public function testErrorArrayToStatus() {
1299 $mock = new MockApi();
1301 $msg = new Message( 'mainpage' );
1303 // Sanity check empty array
1304 $expect = Status
::newGood();
1305 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
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( [
1318 [ 'autoblockedtext' ],
1319 [ 'systemblockedtext' ],
1323 [ 'parentheses', 'foobar' ],
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,
1336 $userInfoTrait = TestingAccessWrapper
::newFromObject(
1337 $this->getMockForTrait( ApiBlockInfoTrait
::class )
1339 $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockInfo( $block ) ];
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( [
1351 [ 'autoblockedtext' ],
1352 [ 'systemblockedtext' ],
1356 [ 'parentheses', 'foobar' ],
1360 public function testAddBlockInfoToStatus() {
1361 $mock = new MockApi();
1363 $msg = new Message( 'mainpage' );
1365 // Sanity check empty array
1366 $expect = Status
::newGood();
1367 $test = Status
::newGood();
1368 $mock->addBlockInfoToStatus( $test );
1369 $this->assertEquals( $expect, $test );
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 );
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,
1394 $userInfoTrait = TestingAccessWrapper
::newFromObject(
1395 $this->getObjectForTrait( ApiBlockInfoTrait
::class )
1397 $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockInfo( $block ) ];
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 );
1419 public function testDieStatus() {
1420 $mock = new MockApi();
1422 $status = StatusValue
::newGood();
1423 $status->error( 'foo' );
1424 $status->warning( 'bar' );
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"' );
1433 $status = StatusValue
::newGood();
1434 $status->warning( 'foo' );
1435 $status->warning( 'bar' );
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"' );
1444 $status = StatusValue
::newGood();
1445 $status->setOk( false );
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"' );
1456 * @covers ApiBase::extractRequestParams
1458 public function testExtractRequestParams() {
1459 $request = new FauxRequest( [
1460 'xxexists' => 'exists!',
1461 'xxmulti' => 'a|b|c|d|{bad}',
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',
1472 'errorformat' => 'raw',
1474 $context = new DerivativeContext( RequestContext
::getMain() );
1475 $context->setRequest( $request );
1476 $main = new ApiMain( $context );
1478 $mock = $this->getMockBuilder( ApiBase
::class )
1479 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1480 ->setMethods( [ 'getAllowedParams' ] )
1481 ->getMockForAbstractClass();
1482 $mock->method( 'getAllowedParams' )->willReturn( [
1483 'notexists' => null,
1486 ApiBase
::PARAM_ISMULTI
=> true,
1489 ApiBase
::PARAM_ISMULTI
=> true,
1492 ApiBase
::PARAM_ISMULTI
=> true,
1493 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'multi' ],
1495 'recursivetemplate-{m}-{t}' => [
1496 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 't' => 'template-{m}', 'm' => 'multi' ],
1498 'emptytemplate-{m}' => [
1499 ApiBase
::PARAM_ISMULTI
=> true,
1500 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'empty' ],
1502 'badtemplate-{e}' => [
1503 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'exists' ],
1505 'badtemplate2-{e}' => [
1506 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'badtemplate2-{e}' ],
1508 'badtemplate3-{x}' => [
1509 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'x' => 'foo' ],
1513 $this->assertEquals( [
1514 'notexists' => null,
1515 'exists' => 'exists!',
1516 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1518 'template-a' => [ 'A!' ],
1519 'template-b' => [ 'B1', 'B2', 'B3' ],
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() );
1528 $used = TestingAccessWrapper
::newFromObject( $main )->getParamsUsed();
1530 $this->assertEquals( [
1535 'xxrecursivetemplate-a-A!',
1536 'xxrecursivetemplate-b-B1',
1537 'xxrecursivetemplate-b-B2',
1538 'xxrecursivetemplate-b-B3',
1545 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1546 $this->assertCount( 1, $warnings );
1547 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );