3 use Wikimedia\TestingAccessWrapper
;
12 class ApiBaseTest
extends ApiTestCase
{
14 * This covers a variety of stub methods that return a fixed value.
16 * @param string|array $method Name of method, or [ name, params... ]
17 * @param string $value Expected value
19 * @dataProvider provideStubMethods
21 public function testStubMethods( $expected, $method, $args = [] ) {
22 // Some of these are protected
23 $mock = TestingAccessWrapper
::newFromObject( new MockApi() );
24 $result = call_user_func_array( [ $mock, $method ], $args );
25 $this->assertSame( $expected, $result );
28 public function provideStubMethods() {
30 [ null, 'getModuleManager' ],
31 [ null, 'getCustomPrinter' ],
32 [ [], 'getHelpUrls' ],
33 // @todo This is actually overriden by MockApi
34 // [ [], 'getAllowedParams' ],
35 [ true, 'shouldCheckMaxLag' ],
36 [ true, 'isReadMode' ],
37 [ false, 'isWriteMode' ],
38 [ false, 'mustBePosted' ],
39 [ false, 'isDeprecated' ],
40 [ false, 'isInternal' ],
41 [ false, 'needsToken' ],
42 [ null, 'getWebUITokenSalt', [ [] ] ],
43 [ null, 'getConditionalRequestData', [ 'etag' ] ],
44 [ null, 'dynamicParameterDocumentation' ],
48 public function testRequireOnlyOneParameterDefault() {
49 $mock = new MockApi();
50 $mock->requireOnlyOneParameter(
51 [ "filename" => "foo.txt", "enablechunks" => false ],
52 "filename", "enablechunks"
54 $this->assertTrue( true );
58 * @expectedException ApiUsageException
60 public function testRequireOnlyOneParameterZero() {
61 $mock = new MockApi();
62 $mock->requireOnlyOneParameter(
63 [ "filename" => "foo.txt", "enablechunks" => 0 ],
64 "filename", "enablechunks"
69 * @expectedException ApiUsageException
71 public function testRequireOnlyOneParameterTrue() {
72 $mock = new MockApi();
73 $mock->requireOnlyOneParameter(
74 [ "filename" => "foo.txt", "enablechunks" => true ],
75 "filename", "enablechunks"
79 public function testRequireOnlyOneParameterMissing() {
80 $this->setExpectedException( ApiUsageException
::class,
81 'One of the parameters "foo" and "bar" is required.' );
82 $mock = new MockApi();
83 $mock->requireOnlyOneParameter(
84 [ "filename" => "foo.txt", "enablechunks" => false ],
88 public function testRequireMaxOneParameterZero() {
89 $mock = new MockApi();
90 $mock->requireMaxOneParameter(
91 [ 'foo' => 'bar', 'baz' => 'quz' ],
93 $this->assertTrue( true );
96 public function testRequireMaxOneParameterOne() {
97 $mock = new MockApi();
98 $mock->requireMaxOneParameter(
99 [ 'foo' => 'bar', 'baz' => 'quz' ],
101 $this->assertTrue( true );
104 public function testRequireMaxOneParameterTwo() {
105 $this->setExpectedException( ApiUsageException
::class,
106 'The parameters "foo" and "baz" can not be used together.' );
107 $mock = new MockApi();
108 $mock->requireMaxOneParameter(
109 [ 'foo' => 'bar', 'baz' => 'quz' ],
113 public function testRequireAtLeastOneParameterZero() {
114 $this->setExpectedException( ApiUsageException
::class,
115 'At least one of the parameters "foo" and "bar" is required.' );
116 $mock = new MockApi();
117 $mock->requireAtLeastOneParameter(
118 [ 'a' => 'b', 'c' => 'd' ],
122 public function testRequireAtLeastOneParameterOne() {
123 $mock = new MockApi();
124 $mock->requireAtLeastOneParameter(
125 [ 'a' => 'b', 'c' => 'd' ],
127 $this->assertTrue( true );
130 public function testRequireAtLeastOneParameterTwo() {
131 $mock = new MockApi();
132 $mock->requireAtLeastOneParameter(
133 [ 'a' => 'b', 'c' => 'd' ],
135 $this->assertTrue( true );
138 public function testGetTitleOrPageIdBadParams() {
139 $this->setExpectedException( ApiUsageException
::class,
140 'The parameters "title" and "pageid" can not be used together.' );
141 $mock = new MockApi();
142 $mock->getTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
145 public function testGetTitleOrPageIdTitle() {
146 $mock = new MockApi();
147 $result = $mock->getTitleOrPageId( [ 'title' => 'Foo' ] );
148 $this->assertInstanceOf( WikiPage
::class, $result );
149 $this->assertSame( 'Foo', $result->getTitle()->getPrefixedText() );
152 public function testGetTitleOrPageIdInvalidTitle() {
153 $this->setExpectedException( ApiUsageException
::class,
155 $mock = new MockApi();
156 $mock->getTitleOrPageId( [ 'title' => '|' ] );
159 public function testGetTitleOrPageIdSpecialTitle() {
160 $this->setExpectedException( ApiUsageException
::class,
161 "Namespace doesn't allow actual pages." );
162 $mock = new MockApi();
163 $mock->getTitleOrPageId( [ 'title' => 'Special:RandomPage' ] );
166 public function testGetTitleOrPageIdPageId() {
167 $page = $this->getExistingTestPage();
168 $result = ( new MockApi() )->getTitleOrPageId(
169 [ 'pageid' => $page->getId() ] );
170 $this->assertInstanceOf( WikiPage
::class, $result );
172 $page->getTitle()->getPrefixedText(),
173 $result->getTitle()->getPrefixedText()
177 public function testGetTitleOrPageIdInvalidPageId() {
178 $this->setExpectedException( ApiUsageException
::class,
179 'There is no page with ID 2147483648.' );
180 $mock = new MockApi();
181 $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] );
184 public function testGetTitleFromTitleOrPageIdBadParams() {
185 $this->setExpectedException( ApiUsageException
::class,
186 'The parameters "title" and "pageid" can not be used together.' );
187 $mock = new MockApi();
188 $mock->getTitleFromTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
191 public function testGetTitleFromTitleOrPageIdTitle() {
192 $mock = new MockApi();
193 $result = $mock->getTitleFromTitleOrPageId( [ 'title' => 'Foo' ] );
194 $this->assertInstanceOf( Title
::class, $result );
195 $this->assertSame( 'Foo', $result->getPrefixedText() );
198 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
199 $this->setExpectedException( ApiUsageException
::class,
201 $mock = new MockApi();
202 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
205 public function testGetTitleFromTitleOrPageIdPageId() {
206 $page = $this->getExistingTestPage();
207 $result = ( new MockApi() )->getTitleFromTitleOrPageId(
208 [ 'pageid' => $page->getId() ] );
209 $this->assertInstanceOf( Title
::class, $result );
210 $this->assertSame( $page->getTitle()->getPrefixedText(), $result->getPrefixedText() );
213 public function testGetTitleFromTitleOrPageIdInvalidPageId() {
214 $this->setExpectedException( ApiUsageException
::class,
215 'There is no page with ID 298401643.' );
216 $mock = new MockApi();
217 $mock->getTitleFromTitleOrPageId( [ 'pageid' => 298401643 ] );
220 public function testGetParameter() {
221 $mock = $this->getMockBuilder( MockApi
::class )
222 ->setMethods( [ 'getAllowedParams' ] )
224 $mock->method( 'getAllowedParams' )->willReturn( [
226 ApiBase
::PARAM_TYPE
=> [ 'value' ],
229 ApiBase
::PARAM_TYPE
=> [ 'value' ],
232 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
234 $context = new DerivativeContext( $mock );
235 $context->setRequest( new FauxRequest( [ 'foo' => 'bad', 'bar' => 'value' ] ) );
236 $wrapper->mMainModule
= new ApiMain( $context );
238 // Even though 'foo' is bad, getParameter( 'bar' ) must not fail
239 $this->assertSame( 'value', $wrapper->getParameter( 'bar' ) );
241 // But getParameter( 'foo' ) must throw.
243 $wrapper->getParameter( 'foo' );
244 $this->fail( 'Expected exception not thrown' );
245 } catch ( ApiUsageException
$ex ) {
246 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
249 // And extractRequestParams() must throw too.
251 $mock->extractRequestParams();
252 $this->fail( 'Expected exception not thrown' );
253 } catch ( ApiUsageException
$ex ) {
254 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
259 * @param string|null $input
260 * @param array $paramSettings
261 * @param mixed $expected
262 * @param array $options Key-value pairs:
263 * 'parseLimits': true|false
264 * 'apihighlimits': true|false
265 * 'internalmode': true|false
266 * 'prefix': true|false
267 * @param string[] $warnings
269 private function doGetParameterFromSettings(
270 $input, $paramSettings, $expected, $warnings, $options = []
272 $mock = new MockApi();
273 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
274 if ( $options['prefix'] ) {
275 $wrapper->mModulePrefix
= 'my';
276 $paramName = 'Param';
278 $paramName = 'myParam';
281 $context = new DerivativeContext( $mock );
282 $context->setRequest( new FauxRequest(
283 $input !== null ?
[ 'myParam' => $input ] : [] ) );
284 $wrapper->mMainModule
= new ApiMain( $context );
286 $parseLimits = $options['parseLimits'] ??
true;
288 if ( !empty( $options['apihighlimits'] ) ) {
289 $context->setUser( self
::$users['sysop']->getUser() );
292 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
293 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->mMainModule
);
294 $mainWrapper->mInternalMode
= false;
297 // If we're testing tags, set up some tags
298 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
299 $paramSettings[ApiBase
::PARAM_TYPE
] === 'tags'
301 ChangeTags
::defineTag( 'tag1' );
302 ChangeTags
::defineTag( 'tag2' );
305 if ( $expected instanceof Exception
) {
307 $wrapper->getParameterFromSettings( $paramName, $paramSettings,
309 $this->fail( 'No exception thrown' );
310 } catch ( Exception
$ex ) {
311 $this->assertEquals( $expected, $ex );
314 $result = $wrapper->getParameterFromSettings( $paramName,
315 $paramSettings, $parseLimits );
316 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
317 $paramSettings[ApiBase
::PARAM_TYPE
] === 'timestamp' &&
320 // Allow one second of fuzziness. Make sure the formats are
322 $this->assertRegExp( '/^\d{14}$/', $result );
323 $this->assertLessThanOrEqual( 1,
324 abs( wfTimestamp( TS_UNIX
, $result ) - time() ),
325 "Result $result differs from expected $expected by " .
326 'more than one second' );
328 $this->assertSame( $expected, $result );
330 $actualWarnings = array_map( function ( $warn ) {
331 return $warn instanceof Message
332 ?
array_merge( [ $warn->getKey() ], $warn->getParams() )
334 }, $mock->warnings
);
335 $this->assertSame( $warnings, $actualWarnings );
338 if ( !empty( $paramSettings[ApiBase
::PARAM_SENSITIVE
] ) ||
339 ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
340 $paramSettings[ApiBase
::PARAM_TYPE
] === 'password' )
342 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->getMain() );
343 $this->assertSame( [ 'myParam' ],
344 $mainWrapper->getSensitiveParams() );
349 * @dataProvider provideGetParameterFromSettings
350 * @see self::doGetParameterFromSettings()
352 public function testGetParameterFromSettings_noprefix(
353 $input, $paramSettings, $expected, $warnings, $options = []
355 $options['prefix'] = false;
356 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
360 * @dataProvider provideGetParameterFromSettings
361 * @see self::doGetParameterFromSettings()
363 public function testGetParameterFromSettings_prefix(
364 $input, $paramSettings, $expected, $warnings, $options = []
366 $options['prefix'] = true;
367 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
370 public static function provideGetParameterFromSettings() {
372 [ 'apiwarn-badutf8', 'myParam' ],
377 for ( $i = 0; $i < 32; $i++
) {
379 $enc .= ( $i === 9 ||
$i === 10 ||
$i === 13 )
385 'Basic param' => [ 'bar', null, 'bar', [] ],
386 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
387 'String param' => [ 'bar', '', 'bar', [] ],
388 'String param, defaulted' => [ null, '', '', [] ],
389 'String param, empty' => [ '', 'default', '', [] ],
390 'String param, required, empty' => [
392 [ ApiBase
::PARAM_DFLT
=> 'default', ApiBase
::PARAM_REQUIRED
=> true ],
393 ApiUsageException
::newWithMessage( null,
394 [ 'apierror-missingparam', 'myParam' ] ),
397 'Multi-valued parameter' => [
399 [ ApiBase
::PARAM_ISMULTI
=> true ],
403 'Multi-valued parameter, alternative separator' => [
405 [ ApiBase
::PARAM_ISMULTI
=> true ],
409 'Multi-valued parameter, other C0 controls' => [
411 [ ApiBase
::PARAM_ISMULTI
=> true ],
415 'Multi-valued parameter, other C0 controls (2)' => [
417 [ ApiBase
::PARAM_ISMULTI
=> true ],
418 [ substr( $enc, 0, -3 ), '' ],
421 'Multi-valued parameter with limits' => [
424 ApiBase
::PARAM_ISMULTI
=> true,
425 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 3,
430 'Multi-valued parameter with exceeded limits' => [
433 ApiBase
::PARAM_ISMULTI
=> true,
434 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
436 ApiUsageException
::newWithMessage(
437 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
441 'Multi-valued parameter with exceeded limits for non-bot' => [
444 ApiBase
::PARAM_ISMULTI
=> true,
445 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
446 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
448 ApiUsageException
::newWithMessage(
449 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
453 'Multi-valued parameter with non-exceeded limits for bot' => [
456 ApiBase
::PARAM_ISMULTI
=> true,
457 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
458 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
462 [ 'apihighlimits' => true ],
464 'Multi-valued parameter with prohibited duplicates' => [
466 [ ApiBase
::PARAM_ISMULTI
=> true ],
467 // Note that the keys are not sequential! This matches
468 // array_unique, but might be unexpected.
469 [ 0 => 'a', 1 => 'b', 3 => 'c' ],
472 'Multi-valued parameter with allowed duplicates' => [
475 ApiBase
::PARAM_ISMULTI
=> true,
476 ApiBase
::PARAM_ALLOW_DUPLICATES
=> true,
481 'Empty boolean param' => [
483 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
487 'Boolean param 0' => [
489 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
493 'Boolean param false' => [
495 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
499 'Boolean multi-param' => [
502 ApiBase
::PARAM_TYPE
=> 'boolean',
503 ApiBase
::PARAM_ISMULTI
=> true,
506 'Internal error in ApiBase::getParameterFromSettings: ' .
507 'Multi-values not supported for myParam'
511 'Empty boolean param with non-false default' => [
514 ApiBase
::PARAM_TYPE
=> 'boolean',
515 ApiBase
::PARAM_DFLT
=> true,
518 'Internal error in ApiBase::getParameterFromSettings: ' .
519 "Boolean param myParam's default is set to '1'. " .
520 'Boolean parameters must default to false.' ),
523 'Deprecated parameter' => [
525 [ ApiBase
::PARAM_DEPRECATED
=> true ],
527 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
529 'Deprecated parameter value' => [
531 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ] ],
533 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
535 'Multiple deprecated parameter values' => [
537 [ ApiBase
::PARAM_DEPRECATED_VALUES
=>
538 [ 'b' => true, 'd' => true ],
539 ApiBase
::PARAM_ISMULTI
=> true ],
540 [ 'a', 'b', 'c', 'd' ],
542 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
543 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
546 'Deprecated parameter value with custom warning' => [
548 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => 'my-msg' ] ],
552 '"*" when wildcard not allowed' => [
554 [ ApiBase
::PARAM_ISMULTI
=> true,
555 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ] ],
557 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
558 [ 'list' => [ '*' ], 'type' => 'comma' ], 1 ] ],
563 ApiBase
::PARAM_ISMULTI
=> true,
564 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
565 ApiBase
::PARAM_ALL
=> true,
570 'Wildcard "*" with multiples not allowed' => [
573 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
574 ApiBase
::PARAM_ALL
=> true,
576 ApiUsageException
::newWithMessage( null,
577 [ 'apierror-unrecognizedvalue', 'myParam', '*' ],
581 'Wildcard "*" with unrestricted type' => [
584 ApiBase
::PARAM_ISMULTI
=> true,
585 ApiBase
::PARAM_ALL
=> true,
593 ApiBase
::PARAM_ISMULTI
=> true,
594 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
595 ApiBase
::PARAM_ALL
=> 'x',
600 'Wildcard conflicting with allowed value' => [
603 ApiBase
::PARAM_ISMULTI
=> true,
604 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
605 ApiBase
::PARAM_ALL
=> 'a',
608 'Internal error in ApiBase::getParameterFromSettings: ' .
609 'For param myParam, PARAM_ALL collides with a possible ' .
613 'Namespace with wildcard' => [
616 ApiBase
::PARAM_ISMULTI
=> true,
617 ApiBase
::PARAM_TYPE
=> 'namespace',
619 MWNamespace
::getValidNamespaces(),
622 // PARAM_ALL is ignored with namespace types.
623 'Namespace with wildcard suppressed' => [
626 ApiBase
::PARAM_ISMULTI
=> true,
627 ApiBase
::PARAM_TYPE
=> 'namespace',
628 ApiBase
::PARAM_ALL
=> false,
630 MWNamespace
::getValidNamespaces(),
633 'Namespace with wildcard "x"' => [
636 ApiBase
::PARAM_ISMULTI
=> true,
637 ApiBase
::PARAM_TYPE
=> 'namespace',
638 ApiBase
::PARAM_ALL
=> 'x',
641 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
642 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
645 'dDy+G?e?txnr.1:(@[Ru',
646 [ ApiBase
::PARAM_TYPE
=> 'password' ],
647 'dDy+G?e?txnr.1:(@[Ru',
650 'Sensitive field' => [
651 'I am fond of pineapples',
652 [ ApiBase
::PARAM_SENSITIVE
=> true ],
653 'I am fond of pineapples',
656 'Upload with default' => [
659 ApiBase
::PARAM_TYPE
=> 'upload',
660 ApiBase
::PARAM_DFLT
=> '',
663 'Internal error in ApiBase::getParameterFromSettings: ' .
664 "File upload param myParam's default is set to ''. " .
665 'File upload parameters may not have a default.' ),
668 'Multiple upload' => [
671 ApiBase
::PARAM_TYPE
=> 'upload',
672 ApiBase
::PARAM_ISMULTI
=> true,
675 'Internal error in ApiBase::getParameterFromSettings: ' .
676 'Multi-values not supported for myParam' ),
679 // @todo Test actual upload
682 [ ApiBase
::PARAM_TYPE
=> 'namespace' ],
683 ApiUsageException
::newWithMessage( null,
684 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
688 'Extra namespace -1' => [
691 ApiBase
::PARAM_TYPE
=> 'namespace',
692 ApiBase
::PARAM_EXTRA_NAMESPACES
=> [ '-1' ],
697 // @todo Test with PARAM_SUBMODULE_MAP unset, need
698 // getModuleManager() to return something real
699 'Nonexistent module' => [
702 ApiBase
::PARAM_TYPE
=> 'submodule',
703 ApiBase
::PARAM_SUBMODULE_MAP
=>
704 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
706 ApiUsageException
::newWithMessage(
709 'apierror-unrecognizedvalue',
717 '\\x1f with multiples not allowed' => [
720 ApiUsageException
::newWithMessage( null,
721 'apierror-badvalue-notmultivalue',
722 'badvalue_notmultivalue' ),
725 'Integer with unenforced min' => [
728 ApiBase
::PARAM_TYPE
=> 'integer',
729 ApiBase
::PARAM_MIN
=> -1,
732 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
735 'Integer with enforced min' => [
738 ApiBase
::PARAM_TYPE
=> 'integer',
739 ApiBase
::PARAM_MIN
=> -1,
740 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
742 ApiUsageException
::newWithMessage( null,
743 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
744 '-1', '-2' ], 'integeroutofrange',
745 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
748 'Integer with unenforced max (internal mode)' => [
751 ApiBase
::PARAM_TYPE
=> 'integer',
752 ApiBase
::PARAM_MAX
=> 7,
757 'Integer with enforced max (internal mode)' => [
760 ApiBase
::PARAM_TYPE
=> 'integer',
761 ApiBase
::PARAM_MAX
=> 7,
762 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
767 'Integer with unenforced max (non-internal mode)' => [
770 ApiBase
::PARAM_TYPE
=> 'integer',
771 ApiBase
::PARAM_MAX
=> 7,
774 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
775 [ 'internalmode' => false ],
777 'Integer with enforced max (non-internal mode)' => [
780 ApiBase
::PARAM_TYPE
=> 'integer',
781 ApiBase
::PARAM_MAX
=> 7,
782 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
784 ApiUsageException
::newWithMessage(
786 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
788 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
791 [ 'internalmode' => false ],
793 'Array of integers' => [
796 ApiBase
::PARAM_ISMULTI
=> true,
797 ApiBase
::PARAM_TYPE
=> 'integer',
802 'Array of integers with unenforced min/max (internal mode)' => [
805 ApiBase
::PARAM_ISMULTI
=> true,
806 ApiBase
::PARAM_TYPE
=> 'integer',
807 ApiBase
::PARAM_MIN
=> 0,
808 ApiBase
::PARAM_MAX
=> 100,
811 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
813 'Array of integers with enforced min/max (internal mode)' => [
816 ApiBase
::PARAM_ISMULTI
=> true,
817 ApiBase
::PARAM_TYPE
=> 'integer',
818 ApiBase
::PARAM_MIN
=> 0,
819 ApiBase
::PARAM_MAX
=> 100,
820 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
822 ApiUsageException
::newWithMessage(
824 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
826 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
830 'Array of integers with unenforced min/max (non-internal mode)' => [
833 ApiBase
::PARAM_ISMULTI
=> true,
834 ApiBase
::PARAM_TYPE
=> 'integer',
835 ApiBase
::PARAM_MIN
=> 0,
836 ApiBase
::PARAM_MAX
=> 100,
840 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
841 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
843 [ 'internalmode' => false ],
845 'Array of integers with enforced min/max (non-internal mode)' => [
848 ApiBase
::PARAM_ISMULTI
=> true,
849 ApiBase
::PARAM_TYPE
=> 'integer',
850 ApiBase
::PARAM_MIN
=> 0,
851 ApiBase
::PARAM_MAX
=> 100,
852 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
854 ApiUsageException
::newWithMessage(
856 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
858 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
861 [ 'internalmode' => false ],
863 'Limit with parseLimits false' => [
865 [ ApiBase
::PARAM_TYPE
=> 'limit' ],
868 [ 'parseLimits' => false ],
870 'Limit with no max' => [
873 ApiBase
::PARAM_TYPE
=> 'limit',
874 ApiBase
::PARAM_MAX2
=> 10,
875 ApiBase
::PARAM_ISMULTI
=> true,
878 'Internal error in ApiBase::getParameterFromSettings: ' .
879 'MAX1 or MAX2 are not defined for the limit myParam' ),
882 'Limit with no max2' => [
885 ApiBase
::PARAM_TYPE
=> 'limit',
886 ApiBase
::PARAM_MAX
=> 10,
887 ApiBase
::PARAM_ISMULTI
=> true,
890 'Internal error in ApiBase::getParameterFromSettings: ' .
891 'MAX1 or MAX2 are not defined for the limit myParam' ),
894 'Limit with multi-value' => [
897 ApiBase
::PARAM_TYPE
=> 'limit',
898 ApiBase
::PARAM_MAX
=> 10,
899 ApiBase
::PARAM_MAX2
=> 10,
900 ApiBase
::PARAM_ISMULTI
=> true,
903 'Internal error in ApiBase::getParameterFromSettings: ' .
904 'Multi-values not supported for myParam' ),
910 ApiBase
::PARAM_TYPE
=> 'limit',
911 ApiBase
::PARAM_MAX
=> 100,
912 ApiBase
::PARAM_MAX2
=> 100,
920 ApiBase
::PARAM_TYPE
=> 'limit',
921 ApiBase
::PARAM_MAX
=> 100,
922 ApiBase
::PARAM_MAX2
=> 101,
927 'Limit max for apihighlimits' => [
930 ApiBase
::PARAM_TYPE
=> 'limit',
931 ApiBase
::PARAM_MAX
=> 100,
932 ApiBase
::PARAM_MAX2
=> 101,
936 [ 'apihighlimits' => true ],
938 'Limit too large (internal mode)' => [
941 ApiBase
::PARAM_TYPE
=> 'limit',
942 ApiBase
::PARAM_MAX
=> 100,
943 ApiBase
::PARAM_MAX2
=> 101,
948 'Limit okay for apihighlimits (internal mode)' => [
951 ApiBase
::PARAM_TYPE
=> 'limit',
952 ApiBase
::PARAM_MAX
=> 100,
953 ApiBase
::PARAM_MAX2
=> 101,
957 [ 'apihighlimits' => true ],
959 'Limit too large for apihighlimits (internal mode)' => [
962 ApiBase
::PARAM_TYPE
=> 'limit',
963 ApiBase
::PARAM_MAX
=> 100,
964 ApiBase
::PARAM_MAX2
=> 101,
968 [ 'apihighlimits' => true ],
970 'Limit too large (non-internal mode)' => [
973 ApiBase
::PARAM_TYPE
=> 'limit',
974 ApiBase
::PARAM_MAX
=> 100,
975 ApiBase
::PARAM_MAX2
=> 101,
978 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
979 [ 'internalmode' => false ],
981 'Limit okay for apihighlimits (non-internal mode)' => [
984 ApiBase
::PARAM_TYPE
=> 'limit',
985 ApiBase
::PARAM_MAX
=> 100,
986 ApiBase
::PARAM_MAX2
=> 101,
990 [ 'internalmode' => false, 'apihighlimits' => true ],
992 'Limit too large for apihighlimits (non-internal mode)' => [
995 ApiBase
::PARAM_TYPE
=> 'limit',
996 ApiBase
::PARAM_MAX
=> 100,
997 ApiBase
::PARAM_MAX2
=> 101,
1000 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
1001 [ 'internalmode' => false, 'apihighlimits' => true ],
1003 'Limit too small' => [
1006 ApiBase
::PARAM_TYPE
=> 'limit',
1007 ApiBase
::PARAM_MIN
=> -1,
1008 ApiBase
::PARAM_MAX
=> 100,
1009 ApiBase
::PARAM_MAX2
=> 100,
1012 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
1016 wfTimestamp( TS_UNIX
, '20211221122112' ),
1017 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1023 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1026 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
1028 'Timestamp empty' => [
1030 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1032 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
1034 // wfTimestamp() interprets this as Unix time
1037 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1041 'Timestamp now' => [
1043 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1047 'Invalid timestamp' => [
1049 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1050 ApiUsageException
::newWithMessage(
1052 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
1053 'badtimestamp_myParam'
1057 'Timestamp array' => [
1060 ApiBase
::PARAM_TYPE
=> 'timestamp',
1061 ApiBase
::PARAM_ISMULTI
=> 1,
1063 [ wfTimestamp( TS_MW
, 100 ), wfTimestamp( TS_MW
, 101 ) ],
1068 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1072 'User prefixed with "User:"' => [
1074 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1078 'Invalid username "|"' => [
1080 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1081 ApiUsageException
::newWithMessage( null,
1082 [ 'apierror-baduser', 'myParam', '|' ],
1083 'baduser_myParam' ),
1086 'Invalid username "300.300.300.300"' => [
1088 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1089 ApiUsageException
::newWithMessage( null,
1090 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1091 'baduser_myParam' ),
1094 'IP range as username' => [
1096 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1100 'IPv6 as username' => [
1102 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1106 'Obsolete cloaked usemod IP address as username' => [
1108 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1112 'Invalid username containing IP address' => [
1113 'This is [not] valid 1.2.3.xxx, ha!',
1114 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1115 ApiUsageException
::newWithMessage(
1117 [ 'apierror-baduser', 'myParam', 'This is [not] valid 1.2.3.xxx, ha!' ],
1122 'External username' => [
1124 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1128 'Array of usernames' => [
1131 ApiBase
::PARAM_TYPE
=> 'user',
1132 ApiBase
::PARAM_ISMULTI
=> true,
1139 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1143 'Array of one tag' => [
1146 ApiBase
::PARAM_TYPE
=> 'tags',
1147 ApiBase
::PARAM_ISMULTI
=> true,
1152 'Array of tags' => [
1155 ApiBase
::PARAM_TYPE
=> 'tags',
1156 ApiBase
::PARAM_ISMULTI
=> true,
1163 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1164 new ApiUsageException( null,
1165 Status
::newFatal( 'tags-apply-not-allowed-one',
1166 'invalid tag', 1 ) ),
1169 'Unrecognized type' => [
1171 [ ApiBase
::PARAM_TYPE
=> 'nonexistenttype' ],
1173 'Internal error in ApiBase::getParameterFromSettings: ' .
1174 "Param myParam's type is unknown - nonexistenttype" ),
1177 'Too many bytes' => [
1180 ApiBase
::PARAM_MAX_BYTES
=> 0,
1181 ApiBase
::PARAM_MAX_CHARS
=> 0,
1183 ApiUsageException
::newWithMessage( null,
1184 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1187 'Too many chars' => [
1190 ApiBase
::PARAM_MAX_BYTES
=> 4,
1191 ApiBase
::PARAM_MAX_CHARS
=> 1,
1193 ApiUsageException
::newWithMessage( null,
1194 [ 'apierror-maxchars', 'myParam', 1 ] ),
1197 'Omitted required param' => [
1199 [ ApiBase
::PARAM_REQUIRED
=> true ],
1200 ApiUsageException
::newWithMessage( null,
1201 [ 'apierror-missingparam', 'myParam' ] ),
1204 'Empty multi-value' => [
1206 [ ApiBase
::PARAM_ISMULTI
=> true ],
1210 'Multi-value \x1f' => [
1212 [ ApiBase
::PARAM_ISMULTI
=> true ],
1216 'Allowed non-multi-value with "|"' => [
1218 [ ApiBase
::PARAM_TYPE
=> [ 'a|b' ] ],
1222 'Prohibited multi-value' => [
1224 [ ApiBase
::PARAM_TYPE
=> [ 'a', 'b' ] ],
1225 ApiUsageException
::newWithMessage( null,
1227 'apierror-multival-only-one-of',
1229 Message
::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1238 // The following really just test PHP's string-to-int conversion.
1246 [ "\t1", 1, '\t1' ],
1247 [ "\r1", 1, '\r1' ],
1248 [ "\f1", 0, '\f1', 'badutf-8' ],
1249 [ "\n1", 1, '\n1' ],
1250 [ "\v1", 0, '\v1', 'badutf-8' ],
1251 [ "\e1", 0, '\e1', 'badutf-8' ],
1252 [ "\x001", 0, '\x001', 'badutf-8' ],
1255 foreach ( $integerTests as $test ) {
1256 $desc = $test[2] ??
$test[0];
1257 $warnings = isset( $test[3] ) ?
1258 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1259 $returnArray["\"$desc\" as integer"] = [
1261 [ ApiBase
::PARAM_TYPE
=> 'integer' ],
1267 return $returnArray;
1270 public function testErrorArrayToStatus() {
1271 $mock = new MockApi();
1273 // Sanity check empty array
1274 $expect = Status
::newGood();
1275 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
1277 // No blocked $user, so no special block handling
1278 $expect = Status
::newGood();
1279 $expect->fatal( 'blockedtext' );
1280 $expect->fatal( 'autoblockedtext' );
1281 $expect->fatal( 'systemblockedtext' );
1282 $expect->fatal( 'mainpage' );
1283 $expect->fatal( 'parentheses', 'foobar' );
1284 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1286 [ 'autoblockedtext' ],
1287 [ 'systemblockedtext' ],
1289 [ 'parentheses', 'foobar' ],
1292 // Has a blocked $user, so special block handling
1293 $user = $this->getMutableTestUser()->getUser();
1294 $block = new \
Block( [
1295 'address' => $user->getName(),
1296 'user' => $user->getID(),
1297 'by' => $this->getTestSysop()->getUser()->getId(),
1298 'reason' => __METHOD__
,
1299 'expiry' => time() +
100500,
1302 $blockinfo = [ 'blockinfo' => ApiQueryUserInfo
::getBlockInfo( $block ) ];
1304 $expect = Status
::newGood();
1305 $expect->fatal( ApiMessage
::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1306 $expect->fatal( ApiMessage
::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1307 $expect->fatal( ApiMessage
::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1308 $expect->fatal( 'mainpage' );
1309 $expect->fatal( 'parentheses', 'foobar' );
1310 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1312 [ 'autoblockedtext' ],
1313 [ 'systemblockedtext' ],
1315 [ 'parentheses', 'foobar' ],
1319 public function testDieStatus() {
1320 $mock = new MockApi();
1322 $status = StatusValue
::newGood();
1323 $status->error( 'foo' );
1324 $status->warning( 'bar' );
1326 $mock->dieStatus( $status );
1327 $this->fail( 'Expected exception not thrown' );
1328 } catch ( ApiUsageException
$ex ) {
1329 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1330 $this->assertFalse( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1333 $status = StatusValue
::newGood();
1334 $status->warning( 'foo' );
1335 $status->warning( 'bar' );
1337 $mock->dieStatus( $status );
1338 $this->fail( 'Expected exception not thrown' );
1339 } catch ( ApiUsageException
$ex ) {
1340 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1341 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1344 $status = StatusValue
::newGood();
1345 $status->setOk( false );
1347 $mock->dieStatus( $status );
1348 $this->fail( 'Expected exception not thrown' );
1349 } catch ( ApiUsageException
$ex ) {
1350 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'unknownerror-nocode' ),
1351 'Exception has "unknownerror-nocode"' );
1356 * @covers ApiBase::extractRequestParams
1358 public function testExtractRequestParams() {
1359 $request = new FauxRequest( [
1360 'xxexists' => 'exists!',
1361 'xxmulti' => 'a|b|c|d|{bad}',
1363 'xxtemplate-a' => 'A!',
1364 'xxtemplate-b' => 'B1|B2|B3',
1365 'xxtemplate-c' => '',
1366 'xxrecursivetemplate-b-B1' => 'X',
1367 'xxrecursivetemplate-b-B3' => 'Y',
1368 'xxrecursivetemplate-b-B4' => '?',
1369 'xxemptytemplate-' => 'nope',
1372 'errorformat' => 'raw',
1374 $context = new DerivativeContext( RequestContext
::getMain() );
1375 $context->setRequest( $request );
1376 $main = new ApiMain( $context );
1378 $mock = $this->getMockBuilder( ApiBase
::class )
1379 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1380 ->setMethods( [ 'getAllowedParams' ] )
1381 ->getMockForAbstractClass();
1382 $mock->method( 'getAllowedParams' )->willReturn( [
1383 'notexists' => null,
1386 ApiBase
::PARAM_ISMULTI
=> true,
1389 ApiBase
::PARAM_ISMULTI
=> true,
1392 ApiBase
::PARAM_ISMULTI
=> true,
1393 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'multi' ],
1395 'recursivetemplate-{m}-{t}' => [
1396 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 't' => 'template-{m}', 'm' => 'multi' ],
1398 'emptytemplate-{m}' => [
1399 ApiBase
::PARAM_ISMULTI
=> true,
1400 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'empty' ],
1402 'badtemplate-{e}' => [
1403 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'exists' ],
1405 'badtemplate2-{e}' => [
1406 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'badtemplate2-{e}' ],
1408 'badtemplate3-{x}' => [
1409 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'x' => 'foo' ],
1413 $this->assertEquals( [
1414 'notexists' => null,
1415 'exists' => 'exists!',
1416 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1418 'template-a' => [ 'A!' ],
1419 'template-b' => [ 'B1', 'B2', 'B3' ],
1421 'template-d' => null,
1422 'recursivetemplate-a-A!' => null,
1423 'recursivetemplate-b-B1' => 'X',
1424 'recursivetemplate-b-B2' => null,
1425 'recursivetemplate-b-B3' => 'Y',
1426 ], $mock->extractRequestParams() );
1428 $used = TestingAccessWrapper
::newFromObject( $main )->getParamsUsed();
1430 $this->assertEquals( [
1435 'xxrecursivetemplate-a-A!',
1436 'xxrecursivetemplate-b-B1',
1437 'xxrecursivetemplate-b-B2',
1438 'xxrecursivetemplate-b-B3',
1445 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1446 $this->assertCount( 1, $warnings );
1447 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );