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 // FIXME: fails under postgres
179 $this->markTestSkippedIfDbType( 'postgres' );
181 $this->setExpectedException( ApiUsageException
::class,
182 'There is no page with ID 2147483648.' );
183 $mock = new MockApi();
184 $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] );
187 public function testGetTitleFromTitleOrPageIdBadParams() {
188 $this->setExpectedException( ApiUsageException
::class,
189 'The parameters "title" and "pageid" can not be used together.' );
190 $mock = new MockApi();
191 $mock->getTitleFromTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] );
194 public function testGetTitleFromTitleOrPageIdTitle() {
195 $mock = new MockApi();
196 $result = $mock->getTitleFromTitleOrPageId( [ 'title' => 'Foo' ] );
197 $this->assertInstanceOf( Title
::class, $result );
198 $this->assertSame( 'Foo', $result->getPrefixedText() );
201 public function testGetTitleFromTitleOrPageIdInvalidTitle() {
202 $this->setExpectedException( ApiUsageException
::class,
204 $mock = new MockApi();
205 $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] );
208 public function testGetTitleFromTitleOrPageIdPageId() {
209 $page = $this->getExistingTestPage();
210 $result = ( new MockApi() )->getTitleFromTitleOrPageId(
211 [ 'pageid' => $page->getId() ] );
212 $this->assertInstanceOf( Title
::class, $result );
213 $this->assertSame( $page->getTitle()->getPrefixedText(), $result->getPrefixedText() );
216 public function testGetTitleFromTitleOrPageIdInvalidPageId() {
217 $this->setExpectedException( ApiUsageException
::class,
218 'There is no page with ID 298401643.' );
219 $mock = new MockApi();
220 $mock->getTitleFromTitleOrPageId( [ 'pageid' => 298401643 ] );
223 public function testGetParameter() {
224 $mock = $this->getMockBuilder( MockApi
::class )
225 ->setMethods( [ 'getAllowedParams' ] )
227 $mock->method( 'getAllowedParams' )->willReturn( [
229 ApiBase
::PARAM_TYPE
=> [ 'value' ],
232 ApiBase
::PARAM_TYPE
=> [ 'value' ],
235 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
237 $context = new DerivativeContext( $mock );
238 $context->setRequest( new FauxRequest( [ 'foo' => 'bad', 'bar' => 'value' ] ) );
239 $wrapper->mMainModule
= new ApiMain( $context );
241 // Even though 'foo' is bad, getParameter( 'bar' ) must not fail
242 $this->assertSame( 'value', $wrapper->getParameter( 'bar' ) );
244 // But getParameter( 'foo' ) must throw.
246 $wrapper->getParameter( 'foo' );
247 $this->fail( 'Expected exception not thrown' );
248 } catch ( ApiUsageException
$ex ) {
249 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
252 // And extractRequestParams() must throw too.
254 $mock->extractRequestParams();
255 $this->fail( 'Expected exception not thrown' );
256 } catch ( ApiUsageException
$ex ) {
257 $this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
262 * @param string|null $input
263 * @param array $paramSettings
264 * @param mixed $expected
265 * @param array $options Key-value pairs:
266 * 'parseLimits': true|false
267 * 'apihighlimits': true|false
268 * 'internalmode': true|false
269 * 'prefix': true|false
270 * @param string[] $warnings
272 private function doGetParameterFromSettings(
273 $input, $paramSettings, $expected, $warnings, $options = []
275 $mock = new MockApi();
276 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
277 if ( $options['prefix'] ) {
278 $wrapper->mModulePrefix
= 'my';
279 $paramName = 'Param';
281 $paramName = 'myParam';
284 $context = new DerivativeContext( $mock );
285 $context->setRequest( new FauxRequest(
286 $input !== null ?
[ 'myParam' => $input ] : [] ) );
287 $wrapper->mMainModule
= new ApiMain( $context );
289 $parseLimits = $options['parseLimits'] ??
true;
291 if ( !empty( $options['apihighlimits'] ) ) {
292 $context->setUser( self
::$users['sysop']->getUser() );
295 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
296 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->mMainModule
);
297 $mainWrapper->mInternalMode
= false;
300 // If we're testing tags, set up some tags
301 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
302 $paramSettings[ApiBase
::PARAM_TYPE
] === 'tags'
304 ChangeTags
::defineTag( 'tag1' );
305 ChangeTags
::defineTag( 'tag2' );
308 if ( $expected instanceof Exception
) {
310 $wrapper->getParameterFromSettings( $paramName, $paramSettings,
312 $this->fail( 'No exception thrown' );
313 } catch ( Exception
$ex ) {
314 $this->assertEquals( $expected, $ex );
317 $result = $wrapper->getParameterFromSettings( $paramName,
318 $paramSettings, $parseLimits );
319 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
320 $paramSettings[ApiBase
::PARAM_TYPE
] === 'timestamp' &&
323 // Allow one second of fuzziness. Make sure the formats are
325 $this->assertRegExp( '/^\d{14}$/', $result );
326 $this->assertLessThanOrEqual( 1,
327 abs( wfTimestamp( TS_UNIX
, $result ) - time() ),
328 "Result $result differs from expected $expected by " .
329 'more than one second' );
331 $this->assertSame( $expected, $result );
333 $actualWarnings = array_map( function ( $warn ) {
334 return $warn instanceof Message
335 ?
array_merge( [ $warn->getKey() ], $warn->getParams() )
337 }, $mock->warnings
);
338 $this->assertSame( $warnings, $actualWarnings );
341 if ( !empty( $paramSettings[ApiBase
::PARAM_SENSITIVE
] ) ||
342 ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
343 $paramSettings[ApiBase
::PARAM_TYPE
] === 'password' )
345 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->getMain() );
346 $this->assertSame( [ 'myParam' ],
347 $mainWrapper->getSensitiveParams() );
352 * @dataProvider provideGetParameterFromSettings
353 * @see self::doGetParameterFromSettings()
355 public function testGetParameterFromSettings_noprefix(
356 $input, $paramSettings, $expected, $warnings, $options = []
358 $options['prefix'] = false;
359 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
363 * @dataProvider provideGetParameterFromSettings
364 * @see self::doGetParameterFromSettings()
366 public function testGetParameterFromSettings_prefix(
367 $input, $paramSettings, $expected, $warnings, $options = []
369 $options['prefix'] = true;
370 $this->doGetParameterFromSettings( $input, $paramSettings, $expected, $warnings, $options );
373 public static function provideGetParameterFromSettings() {
375 [ 'apiwarn-badutf8', 'myParam' ],
380 for ( $i = 0; $i < 32; $i++
) {
382 $enc .= ( $i === 9 ||
$i === 10 ||
$i === 13 )
388 'Basic param' => [ 'bar', null, 'bar', [] ],
389 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
390 'String param' => [ 'bar', '', 'bar', [] ],
391 'String param, defaulted' => [ null, '', '', [] ],
392 'String param, empty' => [ '', 'default', '', [] ],
393 'String param, required, empty' => [
395 [ ApiBase
::PARAM_DFLT
=> 'default', ApiBase
::PARAM_REQUIRED
=> true ],
396 ApiUsageException
::newWithMessage( null,
397 [ 'apierror-missingparam', 'myParam' ] ),
400 'Multi-valued parameter' => [
402 [ ApiBase
::PARAM_ISMULTI
=> true ],
406 'Multi-valued parameter, alternative separator' => [
408 [ ApiBase
::PARAM_ISMULTI
=> true ],
412 'Multi-valued parameter, other C0 controls' => [
414 [ ApiBase
::PARAM_ISMULTI
=> true ],
418 'Multi-valued parameter, other C0 controls (2)' => [
420 [ ApiBase
::PARAM_ISMULTI
=> true ],
421 [ substr( $enc, 0, -3 ), '' ],
424 'Multi-valued parameter with limits' => [
427 ApiBase
::PARAM_ISMULTI
=> true,
428 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 3,
433 'Multi-valued parameter with exceeded limits' => [
436 ApiBase
::PARAM_ISMULTI
=> true,
437 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
439 ApiUsageException
::newWithMessage(
440 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
444 'Multi-valued parameter with exceeded limits for non-bot' => [
447 ApiBase
::PARAM_ISMULTI
=> true,
448 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
449 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
451 ApiUsageException
::newWithMessage(
452 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
456 'Multi-valued parameter with non-exceeded limits for bot' => [
459 ApiBase
::PARAM_ISMULTI
=> true,
460 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
461 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
465 [ 'apihighlimits' => true ],
467 'Multi-valued parameter with prohibited duplicates' => [
469 [ ApiBase
::PARAM_ISMULTI
=> true ],
470 // Note that the keys are not sequential! This matches
471 // array_unique, but might be unexpected.
472 [ 0 => 'a', 1 => 'b', 3 => 'c' ],
475 'Multi-valued parameter with allowed duplicates' => [
478 ApiBase
::PARAM_ISMULTI
=> true,
479 ApiBase
::PARAM_ALLOW_DUPLICATES
=> true,
484 'Empty boolean param' => [
486 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
490 'Boolean param 0' => [
492 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
496 'Boolean param false' => [
498 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
502 'Boolean multi-param' => [
505 ApiBase
::PARAM_TYPE
=> 'boolean',
506 ApiBase
::PARAM_ISMULTI
=> true,
509 'Internal error in ApiBase::getParameterFromSettings: ' .
510 'Multi-values not supported for myParam'
514 'Empty boolean param with non-false default' => [
517 ApiBase
::PARAM_TYPE
=> 'boolean',
518 ApiBase
::PARAM_DFLT
=> true,
521 'Internal error in ApiBase::getParameterFromSettings: ' .
522 "Boolean param myParam's default is set to '1'. " .
523 'Boolean parameters must default to false.' ),
526 'Deprecated parameter' => [
528 [ ApiBase
::PARAM_DEPRECATED
=> true ],
530 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
532 'Deprecated parameter with default, unspecified' => [
534 [ ApiBase
::PARAM_DEPRECATED
=> true, ApiBase
::PARAM_DFLT
=> 'foo' ],
538 'Deprecated parameter with default, specified' => [
540 [ ApiBase
::PARAM_DEPRECATED
=> true, ApiBase
::PARAM_DFLT
=> 'foo' ],
542 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
544 'Deprecated parameter value' => [
546 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ] ],
548 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
550 'Deprecated parameter value as default, unspecified' => [
552 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ], ApiBase
::PARAM_DFLT
=> 'a' ],
556 'Deprecated parameter value as default, specified' => [
558 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ], ApiBase
::PARAM_DFLT
=> 'a' ],
560 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
562 'Multiple deprecated parameter values' => [
564 [ ApiBase
::PARAM_DEPRECATED_VALUES
=>
565 [ 'b' => true, 'd' => true ],
566 ApiBase
::PARAM_ISMULTI
=> true ],
567 [ 'a', 'b', 'c', 'd' ],
569 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
570 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
573 'Deprecated parameter value with custom warning' => [
575 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => 'my-msg' ] ],
579 '"*" when wildcard not allowed' => [
581 [ ApiBase
::PARAM_ISMULTI
=> true,
582 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ] ],
584 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
585 [ 'list' => [ '*' ], 'type' => 'comma' ], 1 ] ],
590 ApiBase
::PARAM_ISMULTI
=> true,
591 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
592 ApiBase
::PARAM_ALL
=> true,
597 'Wildcard "*" with multiples not allowed' => [
600 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
601 ApiBase
::PARAM_ALL
=> true,
603 ApiUsageException
::newWithMessage( null,
604 [ 'apierror-unrecognizedvalue', 'myParam', '*' ],
608 'Wildcard "*" with unrestricted type' => [
611 ApiBase
::PARAM_ISMULTI
=> true,
612 ApiBase
::PARAM_ALL
=> true,
620 ApiBase
::PARAM_ISMULTI
=> true,
621 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
622 ApiBase
::PARAM_ALL
=> 'x',
627 'Wildcard conflicting with allowed value' => [
630 ApiBase
::PARAM_ISMULTI
=> true,
631 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
632 ApiBase
::PARAM_ALL
=> 'a',
635 'Internal error in ApiBase::getParameterFromSettings: ' .
636 'For param myParam, PARAM_ALL collides with a possible ' .
640 'Namespace with wildcard' => [
643 ApiBase
::PARAM_ISMULTI
=> true,
644 ApiBase
::PARAM_TYPE
=> 'namespace',
646 MWNamespace
::getValidNamespaces(),
649 // PARAM_ALL is ignored with namespace types.
650 'Namespace with wildcard suppressed' => [
653 ApiBase
::PARAM_ISMULTI
=> true,
654 ApiBase
::PARAM_TYPE
=> 'namespace',
655 ApiBase
::PARAM_ALL
=> false,
657 MWNamespace
::getValidNamespaces(),
660 'Namespace with wildcard "x"' => [
663 ApiBase
::PARAM_ISMULTI
=> true,
664 ApiBase
::PARAM_TYPE
=> 'namespace',
665 ApiBase
::PARAM_ALL
=> 'x',
668 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
669 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
672 'dDy+G?e?txnr.1:(@[Ru',
673 [ ApiBase
::PARAM_TYPE
=> 'password' ],
674 'dDy+G?e?txnr.1:(@[Ru',
677 'Sensitive field' => [
678 'I am fond of pineapples',
679 [ ApiBase
::PARAM_SENSITIVE
=> true ],
680 'I am fond of pineapples',
683 'Upload with default' => [
686 ApiBase
::PARAM_TYPE
=> 'upload',
687 ApiBase
::PARAM_DFLT
=> '',
690 'Internal error in ApiBase::getParameterFromSettings: ' .
691 "File upload param myParam's default is set to ''. " .
692 'File upload parameters may not have a default.' ),
695 'Multiple upload' => [
698 ApiBase
::PARAM_TYPE
=> 'upload',
699 ApiBase
::PARAM_ISMULTI
=> true,
702 'Internal error in ApiBase::getParameterFromSettings: ' .
703 'Multi-values not supported for myParam' ),
706 // @todo Test actual upload
709 [ ApiBase
::PARAM_TYPE
=> 'namespace' ],
710 ApiUsageException
::newWithMessage( null,
711 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
715 'Extra namespace -1' => [
718 ApiBase
::PARAM_TYPE
=> 'namespace',
719 ApiBase
::PARAM_EXTRA_NAMESPACES
=> [ '-1' ],
724 // @todo Test with PARAM_SUBMODULE_MAP unset, need
725 // getModuleManager() to return something real
726 'Nonexistent module' => [
729 ApiBase
::PARAM_TYPE
=> 'submodule',
730 ApiBase
::PARAM_SUBMODULE_MAP
=>
731 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
733 ApiUsageException
::newWithMessage(
736 'apierror-unrecognizedvalue',
744 '\\x1f with multiples not allowed' => [
747 ApiUsageException
::newWithMessage( null,
748 'apierror-badvalue-notmultivalue',
749 'badvalue_notmultivalue' ),
752 'Integer with unenforced min' => [
755 ApiBase
::PARAM_TYPE
=> 'integer',
756 ApiBase
::PARAM_MIN
=> -1,
759 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
762 'Integer with enforced min' => [
765 ApiBase
::PARAM_TYPE
=> 'integer',
766 ApiBase
::PARAM_MIN
=> -1,
767 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
769 ApiUsageException
::newWithMessage( null,
770 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
771 '-1', '-2' ], 'integeroutofrange',
772 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
775 'Integer with unenforced max (internal mode)' => [
778 ApiBase
::PARAM_TYPE
=> 'integer',
779 ApiBase
::PARAM_MAX
=> 7,
784 'Integer with enforced max (internal mode)' => [
787 ApiBase
::PARAM_TYPE
=> 'integer',
788 ApiBase
::PARAM_MAX
=> 7,
789 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
794 'Integer with unenforced max (non-internal mode)' => [
797 ApiBase
::PARAM_TYPE
=> 'integer',
798 ApiBase
::PARAM_MAX
=> 7,
801 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
802 [ 'internalmode' => false ],
804 'Integer with enforced max (non-internal mode)' => [
807 ApiBase
::PARAM_TYPE
=> 'integer',
808 ApiBase
::PARAM_MAX
=> 7,
809 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
811 ApiUsageException
::newWithMessage(
813 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
815 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
818 [ 'internalmode' => false ],
820 'Array of integers' => [
823 ApiBase
::PARAM_ISMULTI
=> true,
824 ApiBase
::PARAM_TYPE
=> 'integer',
829 'Array of integers with unenforced min/max (internal mode)' => [
832 ApiBase
::PARAM_ISMULTI
=> true,
833 ApiBase
::PARAM_TYPE
=> 'integer',
834 ApiBase
::PARAM_MIN
=> 0,
835 ApiBase
::PARAM_MAX
=> 100,
838 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
840 'Array of integers with enforced min/max (internal mode)' => [
843 ApiBase
::PARAM_ISMULTI
=> true,
844 ApiBase
::PARAM_TYPE
=> 'integer',
845 ApiBase
::PARAM_MIN
=> 0,
846 ApiBase
::PARAM_MAX
=> 100,
847 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
849 ApiUsageException
::newWithMessage(
851 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
853 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
857 'Array of integers with unenforced min/max (non-internal mode)' => [
860 ApiBase
::PARAM_ISMULTI
=> true,
861 ApiBase
::PARAM_TYPE
=> 'integer',
862 ApiBase
::PARAM_MIN
=> 0,
863 ApiBase
::PARAM_MAX
=> 100,
867 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
868 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
870 [ 'internalmode' => false ],
872 'Array of integers with enforced min/max (non-internal mode)' => [
875 ApiBase
::PARAM_ISMULTI
=> true,
876 ApiBase
::PARAM_TYPE
=> 'integer',
877 ApiBase
::PARAM_MIN
=> 0,
878 ApiBase
::PARAM_MAX
=> 100,
879 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
881 ApiUsageException
::newWithMessage(
883 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
885 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
888 [ 'internalmode' => false ],
890 'Limit with parseLimits false' => [
892 [ ApiBase
::PARAM_TYPE
=> 'limit' ],
895 [ 'parseLimits' => false ],
897 'Limit with no max' => [
900 ApiBase
::PARAM_TYPE
=> 'limit',
901 ApiBase
::PARAM_MAX2
=> 10,
902 ApiBase
::PARAM_ISMULTI
=> true,
905 'Internal error in ApiBase::getParameterFromSettings: ' .
906 'MAX1 or MAX2 are not defined for the limit myParam' ),
909 'Limit with no max2' => [
912 ApiBase
::PARAM_TYPE
=> 'limit',
913 ApiBase
::PARAM_MAX
=> 10,
914 ApiBase
::PARAM_ISMULTI
=> true,
917 'Internal error in ApiBase::getParameterFromSettings: ' .
918 'MAX1 or MAX2 are not defined for the limit myParam' ),
921 'Limit with multi-value' => [
924 ApiBase
::PARAM_TYPE
=> 'limit',
925 ApiBase
::PARAM_MAX
=> 10,
926 ApiBase
::PARAM_MAX2
=> 10,
927 ApiBase
::PARAM_ISMULTI
=> true,
930 'Internal error in ApiBase::getParameterFromSettings: ' .
931 'Multi-values not supported for myParam' ),
937 ApiBase
::PARAM_TYPE
=> 'limit',
938 ApiBase
::PARAM_MAX
=> 100,
939 ApiBase
::PARAM_MAX2
=> 100,
947 ApiBase
::PARAM_TYPE
=> 'limit',
948 ApiBase
::PARAM_MAX
=> 100,
949 ApiBase
::PARAM_MAX2
=> 101,
954 'Limit max for apihighlimits' => [
957 ApiBase
::PARAM_TYPE
=> 'limit',
958 ApiBase
::PARAM_MAX
=> 100,
959 ApiBase
::PARAM_MAX2
=> 101,
963 [ 'apihighlimits' => true ],
965 'Limit too large (internal mode)' => [
968 ApiBase
::PARAM_TYPE
=> 'limit',
969 ApiBase
::PARAM_MAX
=> 100,
970 ApiBase
::PARAM_MAX2
=> 101,
975 'Limit okay for apihighlimits (internal mode)' => [
978 ApiBase
::PARAM_TYPE
=> 'limit',
979 ApiBase
::PARAM_MAX
=> 100,
980 ApiBase
::PARAM_MAX2
=> 101,
984 [ 'apihighlimits' => true ],
986 'Limit too large for apihighlimits (internal mode)' => [
989 ApiBase
::PARAM_TYPE
=> 'limit',
990 ApiBase
::PARAM_MAX
=> 100,
991 ApiBase
::PARAM_MAX2
=> 101,
995 [ 'apihighlimits' => true ],
997 'Limit too large (non-internal mode)' => [
1000 ApiBase
::PARAM_TYPE
=> 'limit',
1001 ApiBase
::PARAM_MAX
=> 100,
1002 ApiBase
::PARAM_MAX2
=> 101,
1005 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
1006 [ 'internalmode' => false ],
1008 'Limit okay for apihighlimits (non-internal mode)' => [
1011 ApiBase
::PARAM_TYPE
=> 'limit',
1012 ApiBase
::PARAM_MAX
=> 100,
1013 ApiBase
::PARAM_MAX2
=> 101,
1017 [ 'internalmode' => false, 'apihighlimits' => true ],
1019 'Limit too large for apihighlimits (non-internal mode)' => [
1022 ApiBase
::PARAM_TYPE
=> 'limit',
1023 ApiBase
::PARAM_MAX
=> 100,
1024 ApiBase
::PARAM_MAX2
=> 101,
1027 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
1028 [ 'internalmode' => false, 'apihighlimits' => true ],
1030 'Limit too small' => [
1033 ApiBase
::PARAM_TYPE
=> 'limit',
1034 ApiBase
::PARAM_MIN
=> -1,
1035 ApiBase
::PARAM_MAX
=> 100,
1036 ApiBase
::PARAM_MAX2
=> 100,
1039 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
1043 wfTimestamp( TS_UNIX
, '20211221122112' ),
1044 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1050 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1053 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
1055 'Timestamp empty' => [
1057 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1059 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
1061 // wfTimestamp() interprets this as Unix time
1064 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1068 'Timestamp now' => [
1070 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1074 'Invalid timestamp' => [
1076 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1077 ApiUsageException
::newWithMessage(
1079 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
1080 'badtimestamp_myParam'
1084 'Timestamp array' => [
1087 ApiBase
::PARAM_TYPE
=> 'timestamp',
1088 ApiBase
::PARAM_ISMULTI
=> 1,
1090 [ wfTimestamp( TS_MW
, 100 ), wfTimestamp( TS_MW
, 101 ) ],
1095 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1099 'User prefixed with "User:"' => [
1101 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1105 'Invalid username "|"' => [
1107 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1108 ApiUsageException
::newWithMessage( null,
1109 [ 'apierror-baduser', 'myParam', '|' ],
1110 'baduser_myParam' ),
1113 'Invalid username "300.300.300.300"' => [
1115 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1116 ApiUsageException
::newWithMessage( null,
1117 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1118 'baduser_myParam' ),
1121 'IP range as username' => [
1123 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1127 'IPv6 as username' => [
1129 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1133 'Obsolete cloaked usemod IP address as username' => [
1135 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1139 'Invalid username containing IP address' => [
1140 'This is [not] valid 1.2.3.xxx, ha!',
1141 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1142 ApiUsageException
::newWithMessage(
1144 [ 'apierror-baduser', 'myParam', 'This is [not] valid 1.2.3.xxx, ha!' ],
1149 'External username' => [
1151 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1155 'Array of usernames' => [
1158 ApiBase
::PARAM_TYPE
=> 'user',
1159 ApiBase
::PARAM_ISMULTI
=> true,
1166 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1170 'Array of one tag' => [
1173 ApiBase
::PARAM_TYPE
=> 'tags',
1174 ApiBase
::PARAM_ISMULTI
=> true,
1179 'Array of tags' => [
1182 ApiBase
::PARAM_TYPE
=> 'tags',
1183 ApiBase
::PARAM_ISMULTI
=> true,
1190 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1191 new ApiUsageException( null,
1192 Status
::newFatal( 'tags-apply-not-allowed-one',
1193 'invalid tag', 1 ) ),
1196 'Unrecognized type' => [
1198 [ ApiBase
::PARAM_TYPE
=> 'nonexistenttype' ],
1200 'Internal error in ApiBase::getParameterFromSettings: ' .
1201 "Param myParam's type is unknown - nonexistenttype" ),
1204 'Too many bytes' => [
1207 ApiBase
::PARAM_MAX_BYTES
=> 0,
1208 ApiBase
::PARAM_MAX_CHARS
=> 0,
1210 ApiUsageException
::newWithMessage( null,
1211 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1214 'Too many chars' => [
1217 ApiBase
::PARAM_MAX_BYTES
=> 4,
1218 ApiBase
::PARAM_MAX_CHARS
=> 1,
1220 ApiUsageException
::newWithMessage( null,
1221 [ 'apierror-maxchars', 'myParam', 1 ] ),
1224 'Omitted required param' => [
1226 [ ApiBase
::PARAM_REQUIRED
=> true ],
1227 ApiUsageException
::newWithMessage( null,
1228 [ 'apierror-missingparam', 'myParam' ] ),
1231 'Empty multi-value' => [
1233 [ ApiBase
::PARAM_ISMULTI
=> true ],
1237 'Multi-value \x1f' => [
1239 [ ApiBase
::PARAM_ISMULTI
=> true ],
1243 'Allowed non-multi-value with "|"' => [
1245 [ ApiBase
::PARAM_TYPE
=> [ 'a|b' ] ],
1249 'Prohibited multi-value' => [
1251 [ ApiBase
::PARAM_TYPE
=> [ 'a', 'b' ] ],
1252 ApiUsageException
::newWithMessage( null,
1254 'apierror-multival-only-one-of',
1256 Message
::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1265 // The following really just test PHP's string-to-int conversion.
1273 [ "\t1", 1, '\t1' ],
1274 [ "\r1", 1, '\r1' ],
1275 [ "\f1", 0, '\f1', 'badutf-8' ],
1276 [ "\n1", 1, '\n1' ],
1277 [ "\v1", 0, '\v1', 'badutf-8' ],
1278 [ "\e1", 0, '\e1', 'badutf-8' ],
1279 [ "\x001", 0, '\x001', 'badutf-8' ],
1282 foreach ( $integerTests as $test ) {
1283 $desc = $test[2] ??
$test[0];
1284 $warnings = isset( $test[3] ) ?
1285 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1286 $returnArray["\"$desc\" as integer"] = [
1288 [ ApiBase
::PARAM_TYPE
=> 'integer' ],
1294 return $returnArray;
1297 public function testErrorArrayToStatus() {
1298 $mock = new MockApi();
1300 $msg = new Message( 'mainpage' );
1302 // Sanity check empty array
1303 $expect = Status
::newGood();
1304 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
1306 // No blocked $user, so no special block handling
1307 $expect = Status
::newGood();
1308 $expect->fatal( 'blockedtext' );
1309 $expect->fatal( 'autoblockedtext' );
1310 $expect->fatal( 'systemblockedtext' );
1311 $expect->fatal( 'mainpage' );
1312 $expect->fatal( $msg );
1313 $expect->fatal( $msg, 'foobar' );
1314 $expect->fatal( 'parentheses', 'foobar' );
1315 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1317 [ 'autoblockedtext' ],
1318 [ 'systemblockedtext' ],
1322 [ 'parentheses', 'foobar' ],
1325 // Has a blocked $user, so special block handling
1326 $user = $this->getMutableTestUser()->getUser();
1327 $block = new \
Block( [
1328 'address' => $user->getName(),
1329 'user' => $user->getID(),
1330 'by' => $this->getTestSysop()->getUser()->getId(),
1331 'reason' => __METHOD__
,
1332 'expiry' => time() +
100500,
1335 $userInfoTrait = TestingAccessWrapper
::newFromObject(
1336 $this->getMockForTrait( ApiBlockInfoTrait
::class )
1338 $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockInfo( $block ) ];
1340 $expect = Status
::newGood();
1341 $expect->fatal( ApiMessage
::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1342 $expect->fatal( ApiMessage
::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1343 $expect->fatal( ApiMessage
::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1344 $expect->fatal( 'mainpage' );
1345 $expect->fatal( $msg );
1346 $expect->fatal( $msg, 'foobar' );
1347 $expect->fatal( 'parentheses', 'foobar' );
1348 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1350 [ 'autoblockedtext' ],
1351 [ 'systemblockedtext' ],
1355 [ 'parentheses', 'foobar' ],
1359 public function testAddBlockInfoToStatus() {
1360 $mock = new MockApi();
1362 $msg = new Message( 'mainpage' );
1364 // Sanity check empty array
1365 $expect = Status
::newGood();
1366 $test = Status
::newGood();
1367 $mock->addBlockInfoToStatus( $test );
1368 $this->assertEquals( $expect, $test );
1370 // No blocked $user, so no special block handling
1371 $expect = Status
::newGood();
1372 $expect->fatal( 'blockedtext' );
1373 $expect->fatal( 'autoblockedtext' );
1374 $expect->fatal( 'systemblockedtext' );
1375 $expect->fatal( 'mainpage' );
1376 $expect->fatal( $msg );
1377 $expect->fatal( $msg, 'foobar' );
1378 $expect->fatal( 'parentheses', 'foobar' );
1379 $test = clone $expect;
1380 $mock->addBlockInfoToStatus( $test );
1381 $this->assertEquals( $expect, $test );
1383 // Has a blocked $user, so special block handling
1384 $user = $this->getMutableTestUser()->getUser();
1385 $block = new \
Block( [
1386 'address' => $user->getName(),
1387 'user' => $user->getID(),
1388 'by' => $this->getTestSysop()->getUser()->getId(),
1389 'reason' => __METHOD__
,
1390 'expiry' => time() +
100500,
1393 $userInfoTrait = TestingAccessWrapper
::newFromObject(
1394 $this->getObjectForTrait( ApiBlockInfoTrait
::class )
1396 $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockInfo( $block ) ];
1398 $expect = Status
::newGood();
1399 $expect->fatal( ApiMessage
::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1400 $expect->fatal( ApiMessage
::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1401 $expect->fatal( ApiMessage
::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1402 $expect->fatal( 'mainpage' );
1403 $expect->fatal( $msg );
1404 $expect->fatal( $msg, 'foobar' );
1405 $expect->fatal( 'parentheses', 'foobar' );
1406 $test = Status
::newGood();
1407 $test->fatal( 'blockedtext' );
1408 $test->fatal( 'autoblockedtext' );
1409 $test->fatal( 'systemblockedtext' );
1410 $test->fatal( 'mainpage' );
1411 $test->fatal( $msg );
1412 $test->fatal( $msg, 'foobar' );
1413 $test->fatal( 'parentheses', 'foobar' );
1414 $mock->addBlockInfoToStatus( $test, $user );
1415 $this->assertEquals( $expect, $test );
1418 public function testDieStatus() {
1419 $mock = new MockApi();
1421 $status = StatusValue
::newGood();
1422 $status->error( 'foo' );
1423 $status->warning( 'bar' );
1425 $mock->dieStatus( $status );
1426 $this->fail( 'Expected exception not thrown' );
1427 } catch ( ApiUsageException
$ex ) {
1428 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1429 $this->assertFalse( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1432 $status = StatusValue
::newGood();
1433 $status->warning( 'foo' );
1434 $status->warning( 'bar' );
1436 $mock->dieStatus( $status );
1437 $this->fail( 'Expected exception not thrown' );
1438 } catch ( ApiUsageException
$ex ) {
1439 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1440 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1443 $status = StatusValue
::newGood();
1444 $status->setOk( false );
1446 $mock->dieStatus( $status );
1447 $this->fail( 'Expected exception not thrown' );
1448 } catch ( ApiUsageException
$ex ) {
1449 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'unknownerror-nocode' ),
1450 'Exception has "unknownerror-nocode"' );
1455 * @covers ApiBase::extractRequestParams
1457 public function testExtractRequestParams() {
1458 $request = new FauxRequest( [
1459 'xxexists' => 'exists!',
1460 'xxmulti' => 'a|b|c|d|{bad}',
1462 'xxtemplate-a' => 'A!',
1463 'xxtemplate-b' => 'B1|B2|B3',
1464 'xxtemplate-c' => '',
1465 'xxrecursivetemplate-b-B1' => 'X',
1466 'xxrecursivetemplate-b-B3' => 'Y',
1467 'xxrecursivetemplate-b-B4' => '?',
1468 'xxemptytemplate-' => 'nope',
1471 'errorformat' => 'raw',
1473 $context = new DerivativeContext( RequestContext
::getMain() );
1474 $context->setRequest( $request );
1475 $main = new ApiMain( $context );
1477 $mock = $this->getMockBuilder( ApiBase
::class )
1478 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1479 ->setMethods( [ 'getAllowedParams' ] )
1480 ->getMockForAbstractClass();
1481 $mock->method( 'getAllowedParams' )->willReturn( [
1482 'notexists' => null,
1485 ApiBase
::PARAM_ISMULTI
=> true,
1488 ApiBase
::PARAM_ISMULTI
=> true,
1491 ApiBase
::PARAM_ISMULTI
=> true,
1492 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'multi' ],
1494 'recursivetemplate-{m}-{t}' => [
1495 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 't' => 'template-{m}', 'm' => 'multi' ],
1497 'emptytemplate-{m}' => [
1498 ApiBase
::PARAM_ISMULTI
=> true,
1499 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'empty' ],
1501 'badtemplate-{e}' => [
1502 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'exists' ],
1504 'badtemplate2-{e}' => [
1505 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'badtemplate2-{e}' ],
1507 'badtemplate3-{x}' => [
1508 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'x' => 'foo' ],
1512 $this->assertEquals( [
1513 'notexists' => null,
1514 'exists' => 'exists!',
1515 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1517 'template-a' => [ 'A!' ],
1518 'template-b' => [ 'B1', 'B2', 'B3' ],
1520 'template-d' => null,
1521 'recursivetemplate-a-A!' => null,
1522 'recursivetemplate-b-B1' => 'X',
1523 'recursivetemplate-b-B2' => null,
1524 'recursivetemplate-b-B3' => 'Y',
1525 ], $mock->extractRequestParams() );
1527 $used = TestingAccessWrapper
::newFromObject( $main )->getParamsUsed();
1529 $this->assertEquals( [
1534 'xxrecursivetemplate-a-A!',
1535 'xxrecursivetemplate-b-B1',
1536 'xxrecursivetemplate-b-B2',
1537 'xxrecursivetemplate-b-B3',
1544 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1545 $this->assertCount( 1, $warnings );
1546 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );