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 * @dataProvider provideGetParameterFromSettings
260 * @param string|null $input
261 * @param array $paramSettings
262 * @param mixed $expected
263 * @param array $options Key-value pairs:
264 * 'parseLimits': true|false
265 * 'apihighlimits': true|false
266 * 'internalmode': true|false
267 * @param string[] $warnings
269 public function testGetParameterFromSettings(
270 $input, $paramSettings, $expected, $warnings, $options = []
272 $mock = new MockApi();
273 $wrapper = TestingAccessWrapper
::newFromObject( $mock );
275 $context = new DerivativeContext( $mock );
276 $context->setRequest( new FauxRequest(
277 $input !== null ?
[ 'myParam' => $input ] : [] ) );
278 $wrapper->mMainModule
= new ApiMain( $context );
280 $parseLimits = $options['parseLimits'] ??
true;
282 if ( !empty( $options['apihighlimits'] ) ) {
283 $context->setUser( self
::$users['sysop']->getUser() );
286 if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
287 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->mMainModule
);
288 $mainWrapper->mInternalMode
= false;
291 // If we're testing tags, set up some tags
292 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
293 $paramSettings[ApiBase
::PARAM_TYPE
] === 'tags'
295 ChangeTags
::defineTag( 'tag1' );
296 ChangeTags
::defineTag( 'tag2' );
299 if ( $expected instanceof Exception
) {
301 $wrapper->getParameterFromSettings( 'myParam', $paramSettings,
303 $this->fail( 'No exception thrown' );
304 } catch ( Exception
$ex ) {
305 $this->assertEquals( $expected, $ex );
308 $result = $wrapper->getParameterFromSettings( 'myParam',
309 $paramSettings, $parseLimits );
310 if ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
311 $paramSettings[ApiBase
::PARAM_TYPE
] === 'timestamp' &&
314 // Allow one second of fuzziness. Make sure the formats are
316 $this->assertRegExp( '/^\d{14}$/', $result );
317 $this->assertLessThanOrEqual( 1,
318 abs( wfTimestamp( TS_UNIX
, $result ) - time() ),
319 "Result $result differs from expected $expected by " .
320 'more than one second' );
322 $this->assertSame( $expected, $result );
324 $actualWarnings = array_map( function ( $warn ) {
325 return $warn instanceof Message
326 ?
array_merge( [ $warn->getKey() ], $warn->getParams() )
328 }, $mock->warnings
);
329 $this->assertSame( $warnings, $actualWarnings );
332 if ( !empty( $paramSettings[ApiBase
::PARAM_SENSITIVE
] ) ||
333 ( isset( $paramSettings[ApiBase
::PARAM_TYPE
] ) &&
334 $paramSettings[ApiBase
::PARAM_TYPE
] === 'password' )
336 $mainWrapper = TestingAccessWrapper
::newFromObject( $wrapper->getMain() );
337 $this->assertSame( [ 'myParam' ],
338 $mainWrapper->getSensitiveParams() );
342 public static function provideGetParameterFromSettings() {
344 [ 'apiwarn-badutf8', 'myParam' ],
349 for ( $i = 0; $i < 32; $i++
) {
351 $enc .= ( $i === 9 ||
$i === 10 ||
$i === 13 )
357 'Basic param' => [ 'bar', null, 'bar', [] ],
358 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
359 'String param' => [ 'bar', '', 'bar', [] ],
360 'String param, defaulted' => [ null, '', '', [] ],
361 'String param, empty' => [ '', 'default', '', [] ],
362 'String param, required, empty' => [
364 [ ApiBase
::PARAM_DFLT
=> 'default', ApiBase
::PARAM_REQUIRED
=> true ],
365 ApiUsageException
::newWithMessage( null,
366 [ 'apierror-missingparam', 'myParam' ] ),
369 'Multi-valued parameter' => [
371 [ ApiBase
::PARAM_ISMULTI
=> true ],
375 'Multi-valued parameter, alternative separator' => [
377 [ ApiBase
::PARAM_ISMULTI
=> true ],
381 'Multi-valued parameter, other C0 controls' => [
383 [ ApiBase
::PARAM_ISMULTI
=> true ],
387 'Multi-valued parameter, other C0 controls (2)' => [
389 [ ApiBase
::PARAM_ISMULTI
=> true ],
390 [ substr( $enc, 0, -3 ), '' ],
393 'Multi-valued parameter with limits' => [
396 ApiBase
::PARAM_ISMULTI
=> true,
397 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 3,
402 'Multi-valued parameter with exceeded limits' => [
405 ApiBase
::PARAM_ISMULTI
=> true,
406 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
408 ApiUsageException
::newWithMessage(
409 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
413 'Multi-valued parameter with exceeded limits for non-bot' => [
416 ApiBase
::PARAM_ISMULTI
=> true,
417 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
418 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
420 ApiUsageException
::newWithMessage(
421 null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
425 'Multi-valued parameter with non-exceeded limits for bot' => [
428 ApiBase
::PARAM_ISMULTI
=> true,
429 ApiBase
::PARAM_ISMULTI_LIMIT1
=> 2,
430 ApiBase
::PARAM_ISMULTI_LIMIT2
=> 3,
434 [ 'apihighlimits' => true ],
436 'Multi-valued parameter with prohibited duplicates' => [
438 [ ApiBase
::PARAM_ISMULTI
=> true ],
439 // Note that the keys are not sequential! This matches
440 // array_unique, but might be unexpected.
441 [ 0 => 'a', 1 => 'b', 3 => 'c' ],
444 'Multi-valued parameter with allowed duplicates' => [
447 ApiBase
::PARAM_ISMULTI
=> true,
448 ApiBase
::PARAM_ALLOW_DUPLICATES
=> true,
453 'Empty boolean param' => [
455 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
459 'Boolean param 0' => [
461 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
465 'Boolean param false' => [
467 [ ApiBase
::PARAM_TYPE
=> 'boolean' ],
471 'Boolean multi-param' => [
474 ApiBase
::PARAM_TYPE
=> 'boolean',
475 ApiBase
::PARAM_ISMULTI
=> true,
478 'Internal error in ApiBase::getParameterFromSettings: ' .
479 'Multi-values not supported for myParam'
483 'Empty boolean param with non-false default' => [
486 ApiBase
::PARAM_TYPE
=> 'boolean',
487 ApiBase
::PARAM_DFLT
=> true,
490 'Internal error in ApiBase::getParameterFromSettings: ' .
491 "Boolean param myParam's default is set to '1'. " .
492 'Boolean parameters must default to false.' ),
495 'Deprecated parameter' => [
497 [ ApiBase
::PARAM_DEPRECATED
=> true ],
499 [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
501 'Deprecated parameter value' => [
503 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => true ] ],
505 [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
507 'Multiple deprecated parameter values' => [
509 [ ApiBase
::PARAM_DEPRECATED_VALUES
=>
510 [ 'b' => true, 'd' => true ],
511 ApiBase
::PARAM_ISMULTI
=> true ],
512 [ 'a', 'b', 'c', 'd' ],
514 [ 'apiwarn-deprecation-parameter', 'myParam=b' ],
515 [ 'apiwarn-deprecation-parameter', 'myParam=d' ],
518 'Deprecated parameter value with custom warning' => [
520 [ ApiBase
::PARAM_DEPRECATED_VALUES
=> [ 'a' => 'my-msg' ] ],
524 '"*" when wildcard not allowed' => [
526 [ ApiBase
::PARAM_ISMULTI
=> true,
527 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ] ],
529 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
530 [ 'list' => [ '*' ], 'type' => 'comma' ], 1 ] ],
535 ApiBase
::PARAM_ISMULTI
=> true,
536 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
537 ApiBase
::PARAM_ALL
=> true,
542 'Wildcard "*" with multiples not allowed' => [
545 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
546 ApiBase
::PARAM_ALL
=> true,
548 ApiUsageException
::newWithMessage( null,
549 [ 'apierror-unrecognizedvalue', 'myParam', '*' ],
553 'Wildcard "*" with unrestricted type' => [
556 ApiBase
::PARAM_ISMULTI
=> true,
557 ApiBase
::PARAM_ALL
=> true,
565 ApiBase
::PARAM_ISMULTI
=> true,
566 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
567 ApiBase
::PARAM_ALL
=> 'x',
572 'Wildcard conflicting with allowed value' => [
575 ApiBase
::PARAM_ISMULTI
=> true,
576 ApiBase
::PARAM_TYPE
=> [ 'a', 'b', 'c' ],
577 ApiBase
::PARAM_ALL
=> 'a',
580 'Internal error in ApiBase::getParameterFromSettings: ' .
581 'For param myParam, PARAM_ALL collides with a possible ' .
585 'Namespace with wildcard' => [
588 ApiBase
::PARAM_ISMULTI
=> true,
589 ApiBase
::PARAM_TYPE
=> 'namespace',
591 MWNamespace
::getValidNamespaces(),
594 // PARAM_ALL is ignored with namespace types.
595 'Namespace with wildcard suppressed' => [
598 ApiBase
::PARAM_ISMULTI
=> true,
599 ApiBase
::PARAM_TYPE
=> 'namespace',
600 ApiBase
::PARAM_ALL
=> false,
602 MWNamespace
::getValidNamespaces(),
605 'Namespace with wildcard "x"' => [
608 ApiBase
::PARAM_ISMULTI
=> true,
609 ApiBase
::PARAM_TYPE
=> 'namespace',
610 ApiBase
::PARAM_ALL
=> 'x',
613 [ [ 'apiwarn-unrecognizedvalues', 'myParam',
614 [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
617 'dDy+G?e?txnr.1:(@[Ru',
618 [ ApiBase
::PARAM_TYPE
=> 'password' ],
619 'dDy+G?e?txnr.1:(@[Ru',
622 'Sensitive field' => [
623 'I am fond of pineapples',
624 [ ApiBase
::PARAM_SENSITIVE
=> true ],
625 'I am fond of pineapples',
628 'Upload with default' => [
631 ApiBase
::PARAM_TYPE
=> 'upload',
632 ApiBase
::PARAM_DFLT
=> '',
635 'Internal error in ApiBase::getParameterFromSettings: ' .
636 "File upload param myParam's default is set to ''. " .
637 'File upload parameters may not have a default.' ),
640 'Multiple upload' => [
643 ApiBase
::PARAM_TYPE
=> 'upload',
644 ApiBase
::PARAM_ISMULTI
=> true,
647 'Internal error in ApiBase::getParameterFromSettings: ' .
648 'Multi-values not supported for myParam' ),
651 // @todo Test actual upload
654 [ ApiBase
::PARAM_TYPE
=> 'namespace' ],
655 ApiUsageException
::newWithMessage( null,
656 [ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
660 'Extra namespace -1' => [
663 ApiBase
::PARAM_TYPE
=> 'namespace',
664 ApiBase
::PARAM_EXTRA_NAMESPACES
=> [ '-1' ],
669 // @todo Test with PARAM_SUBMODULE_MAP unset, need
670 // getModuleManager() to return something real
671 'Nonexistent module' => [
674 ApiBase
::PARAM_TYPE
=> 'submodule',
675 ApiBase
::PARAM_SUBMODULE_MAP
=>
676 [ 'foo' => 'foo', 'bar' => 'foo+bar' ],
678 ApiUsageException
::newWithMessage(
681 'apierror-unrecognizedvalue',
689 '\\x1f with multiples not allowed' => [
692 ApiUsageException
::newWithMessage( null,
693 'apierror-badvalue-notmultivalue',
694 'badvalue_notmultivalue' ),
697 'Integer with unenforced min' => [
700 ApiBase
::PARAM_TYPE
=> 'integer',
701 ApiBase
::PARAM_MIN
=> -1,
704 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
707 'Integer with enforced min' => [
710 ApiBase
::PARAM_TYPE
=> 'integer',
711 ApiBase
::PARAM_MIN
=> -1,
712 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
714 ApiUsageException
::newWithMessage( null,
715 [ 'apierror-integeroutofrange-belowminimum', 'myParam',
716 '-1', '-2' ], 'integeroutofrange',
717 [ 'min' => -1, 'max' => null, 'botMax' => null ] ),
720 'Integer with unenforced max (internal mode)' => [
723 ApiBase
::PARAM_TYPE
=> 'integer',
724 ApiBase
::PARAM_MAX
=> 7,
729 'Integer with enforced max (internal mode)' => [
732 ApiBase
::PARAM_TYPE
=> 'integer',
733 ApiBase
::PARAM_MAX
=> 7,
734 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
739 'Integer with unenforced max (non-internal mode)' => [
742 ApiBase
::PARAM_TYPE
=> 'integer',
743 ApiBase
::PARAM_MAX
=> 7,
746 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
747 [ 'internalmode' => false ],
749 'Integer with enforced max (non-internal mode)' => [
752 ApiBase
::PARAM_TYPE
=> 'integer',
753 ApiBase
::PARAM_MAX
=> 7,
754 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
756 ApiUsageException
::newWithMessage(
758 [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
760 [ 'min' => null, 'max' => 7, 'botMax' => 7 ]
763 [ 'internalmode' => false ],
765 'Array of integers' => [
768 ApiBase
::PARAM_ISMULTI
=> true,
769 ApiBase
::PARAM_TYPE
=> 'integer',
774 'Array of integers with unenforced min/max (internal mode)' => [
777 ApiBase
::PARAM_ISMULTI
=> true,
778 ApiBase
::PARAM_TYPE
=> 'integer',
779 ApiBase
::PARAM_MIN
=> 0,
780 ApiBase
::PARAM_MAX
=> 100,
783 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
785 'Array of integers with enforced min/max (internal mode)' => [
788 ApiBase
::PARAM_ISMULTI
=> true,
789 ApiBase
::PARAM_TYPE
=> 'integer',
790 ApiBase
::PARAM_MIN
=> 0,
791 ApiBase
::PARAM_MAX
=> 100,
792 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
794 ApiUsageException
::newWithMessage(
796 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
798 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
802 'Array of integers with unenforced min/max (non-internal mode)' => [
805 ApiBase
::PARAM_ISMULTI
=> true,
806 ApiBase
::PARAM_TYPE
=> 'integer',
807 ApiBase
::PARAM_MIN
=> 0,
808 ApiBase
::PARAM_MAX
=> 100,
812 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
813 [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
815 [ 'internalmode' => false ],
817 'Array of integers with enforced min/max (non-internal mode)' => [
820 ApiBase
::PARAM_ISMULTI
=> true,
821 ApiBase
::PARAM_TYPE
=> 'integer',
822 ApiBase
::PARAM_MIN
=> 0,
823 ApiBase
::PARAM_MAX
=> 100,
824 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
826 ApiUsageException
::newWithMessage(
828 [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
830 [ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
833 [ 'internalmode' => false ],
835 'Limit with parseLimits false' => [
837 [ ApiBase
::PARAM_TYPE
=> 'limit' ],
840 [ 'parseLimits' => false ],
842 'Limit with no max' => [
845 ApiBase
::PARAM_TYPE
=> 'limit',
846 ApiBase
::PARAM_MAX2
=> 10,
847 ApiBase
::PARAM_ISMULTI
=> true,
850 'Internal error in ApiBase::getParameterFromSettings: ' .
851 'MAX1 or MAX2 are not defined for the limit myParam' ),
854 'Limit with no max2' => [
857 ApiBase
::PARAM_TYPE
=> 'limit',
858 ApiBase
::PARAM_MAX
=> 10,
859 ApiBase
::PARAM_ISMULTI
=> true,
862 'Internal error in ApiBase::getParameterFromSettings: ' .
863 'MAX1 or MAX2 are not defined for the limit myParam' ),
866 'Limit with multi-value' => [
869 ApiBase
::PARAM_TYPE
=> 'limit',
870 ApiBase
::PARAM_MAX
=> 10,
871 ApiBase
::PARAM_MAX2
=> 10,
872 ApiBase
::PARAM_ISMULTI
=> true,
875 'Internal error in ApiBase::getParameterFromSettings: ' .
876 'Multi-values not supported for myParam' ),
882 ApiBase
::PARAM_TYPE
=> 'limit',
883 ApiBase
::PARAM_MAX
=> 100,
884 ApiBase
::PARAM_MAX2
=> 100,
892 ApiBase
::PARAM_TYPE
=> 'limit',
893 ApiBase
::PARAM_MAX
=> 100,
894 ApiBase
::PARAM_MAX2
=> 101,
899 'Limit max for apihighlimits' => [
902 ApiBase
::PARAM_TYPE
=> 'limit',
903 ApiBase
::PARAM_MAX
=> 100,
904 ApiBase
::PARAM_MAX2
=> 101,
908 [ 'apihighlimits' => true ],
910 'Limit too large (internal mode)' => [
913 ApiBase
::PARAM_TYPE
=> 'limit',
914 ApiBase
::PARAM_MAX
=> 100,
915 ApiBase
::PARAM_MAX2
=> 101,
920 'Limit okay for apihighlimits (internal mode)' => [
923 ApiBase
::PARAM_TYPE
=> 'limit',
924 ApiBase
::PARAM_MAX
=> 100,
925 ApiBase
::PARAM_MAX2
=> 101,
929 [ 'apihighlimits' => true ],
931 'Limit too large for apihighlimits (internal mode)' => [
934 ApiBase
::PARAM_TYPE
=> 'limit',
935 ApiBase
::PARAM_MAX
=> 100,
936 ApiBase
::PARAM_MAX2
=> 101,
940 [ 'apihighlimits' => true ],
942 'Limit too large (non-internal mode)' => [
945 ApiBase
::PARAM_TYPE
=> 'limit',
946 ApiBase
::PARAM_MAX
=> 100,
947 ApiBase
::PARAM_MAX2
=> 101,
950 [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
951 [ 'internalmode' => false ],
953 'Limit okay for apihighlimits (non-internal mode)' => [
956 ApiBase
::PARAM_TYPE
=> 'limit',
957 ApiBase
::PARAM_MAX
=> 100,
958 ApiBase
::PARAM_MAX2
=> 101,
962 [ 'internalmode' => false, 'apihighlimits' => true ],
964 'Limit too large for apihighlimits (non-internal mode)' => [
967 ApiBase
::PARAM_TYPE
=> 'limit',
968 ApiBase
::PARAM_MAX
=> 100,
969 ApiBase
::PARAM_MAX2
=> 101,
972 [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
973 [ 'internalmode' => false, 'apihighlimits' => true ],
975 'Limit too small' => [
978 ApiBase
::PARAM_TYPE
=> 'limit',
979 ApiBase
::PARAM_MIN
=> -1,
980 ApiBase
::PARAM_MAX
=> 100,
981 ApiBase
::PARAM_MAX2
=> 100,
984 [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
988 wfTimestamp( TS_UNIX
, '20211221122112' ),
989 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
995 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
998 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
1000 'Timestamp empty' => [
1002 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1004 [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
1006 // wfTimestamp() interprets this as Unix time
1009 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1013 'Timestamp now' => [
1015 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1019 'Invalid timestamp' => [
1021 [ ApiBase
::PARAM_TYPE
=> 'timestamp' ],
1022 ApiUsageException
::newWithMessage(
1024 [ 'apierror-badtimestamp', 'myParam', 'a potato' ],
1025 'badtimestamp_myParam'
1029 'Timestamp array' => [
1032 ApiBase
::PARAM_TYPE
=> 'timestamp',
1033 ApiBase
::PARAM_ISMULTI
=> 1,
1035 [ wfTimestamp( TS_MW
, 100 ), wfTimestamp( TS_MW
, 101 ) ],
1040 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1044 'User prefixed with "User:"' => [
1046 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1050 'Invalid username "|"' => [
1052 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1053 ApiUsageException
::newWithMessage( null,
1054 [ 'apierror-baduser', 'myParam', '|' ],
1055 'baduser_myParam' ),
1058 'Invalid username "300.300.300.300"' => [
1060 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1061 ApiUsageException
::newWithMessage( null,
1062 [ 'apierror-baduser', 'myParam', '300.300.300.300' ],
1063 'baduser_myParam' ),
1066 'IP range as username' => [
1068 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1072 'IPv6 as username' => [
1074 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1078 'Obsolete cloaked usemod IP address as username' => [
1080 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1084 'Invalid username containing IP address' => [
1085 'This is [not] valid 1.2.3.xxx, ha!',
1086 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1087 ApiUsageException
::newWithMessage(
1089 [ 'apierror-baduser', 'myParam', 'This is [not] valid 1.2.3.xxx, ha!' ],
1094 'External username' => [
1096 [ ApiBase
::PARAM_TYPE
=> 'user' ],
1100 'Array of usernames' => [
1103 ApiBase
::PARAM_TYPE
=> 'user',
1104 ApiBase
::PARAM_ISMULTI
=> true,
1111 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1115 'Array of one tag' => [
1118 ApiBase
::PARAM_TYPE
=> 'tags',
1119 ApiBase
::PARAM_ISMULTI
=> true,
1124 'Array of tags' => [
1127 ApiBase
::PARAM_TYPE
=> 'tags',
1128 ApiBase
::PARAM_ISMULTI
=> true,
1135 [ ApiBase
::PARAM_TYPE
=> 'tags' ],
1136 new ApiUsageException( null,
1137 Status
::newFatal( 'tags-apply-not-allowed-one',
1138 'invalid tag', 1 ) ),
1141 'Unrecognized type' => [
1143 [ ApiBase
::PARAM_TYPE
=> 'nonexistenttype' ],
1145 'Internal error in ApiBase::getParameterFromSettings: ' .
1146 "Param myParam's type is unknown - nonexistenttype" ),
1149 'Too many bytes' => [
1152 ApiBase
::PARAM_MAX_BYTES
=> 0,
1153 ApiBase
::PARAM_MAX_CHARS
=> 0,
1155 ApiUsageException
::newWithMessage( null,
1156 [ 'apierror-maxbytes', 'myParam', 0 ] ),
1159 'Too many chars' => [
1162 ApiBase
::PARAM_MAX_BYTES
=> 4,
1163 ApiBase
::PARAM_MAX_CHARS
=> 1,
1165 ApiUsageException
::newWithMessage( null,
1166 [ 'apierror-maxchars', 'myParam', 1 ] ),
1169 'Omitted required param' => [
1171 [ ApiBase
::PARAM_REQUIRED
=> true ],
1172 ApiUsageException
::newWithMessage( null,
1173 [ 'apierror-missingparam', 'myParam' ] ),
1176 'Empty multi-value' => [
1178 [ ApiBase
::PARAM_ISMULTI
=> true ],
1182 'Multi-value \x1f' => [
1184 [ ApiBase
::PARAM_ISMULTI
=> true ],
1188 'Allowed non-multi-value with "|"' => [
1190 [ ApiBase
::PARAM_TYPE
=> [ 'a|b' ] ],
1194 'Prohibited multi-value' => [
1196 [ ApiBase
::PARAM_TYPE
=> [ 'a', 'b' ] ],
1197 ApiUsageException
::newWithMessage( null,
1199 'apierror-multival-only-one-of',
1201 Message
::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
1210 // The following really just test PHP's string-to-int conversion.
1218 [ "\t1", 1, '\t1' ],
1219 [ "\r1", 1, '\r1' ],
1220 [ "\f1", 0, '\f1', 'badutf-8' ],
1221 [ "\n1", 1, '\n1' ],
1222 [ "\v1", 0, '\v1', 'badutf-8' ],
1223 [ "\e1", 0, '\e1', 'badutf-8' ],
1224 [ "\x001", 0, '\x001', 'badutf-8' ],
1227 foreach ( $integerTests as $test ) {
1228 $desc = $test[2] ??
$test[0];
1229 $warnings = isset( $test[3] ) ?
1230 [ [ 'apiwarn-badutf8', 'myParam' ] ] : [];
1231 $returnArray["\"$desc\" as integer"] = [
1233 [ ApiBase
::PARAM_TYPE
=> 'integer' ],
1239 return $returnArray;
1242 public function testErrorArrayToStatus() {
1243 $mock = new MockApi();
1245 // Sanity check empty array
1246 $expect = Status
::newGood();
1247 $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
1249 // No blocked $user, so no special block handling
1250 $expect = Status
::newGood();
1251 $expect->fatal( 'blockedtext' );
1252 $expect->fatal( 'autoblockedtext' );
1253 $expect->fatal( 'systemblockedtext' );
1254 $expect->fatal( 'mainpage' );
1255 $expect->fatal( 'parentheses', 'foobar' );
1256 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1258 [ 'autoblockedtext' ],
1259 [ 'systemblockedtext' ],
1261 [ 'parentheses', 'foobar' ],
1264 // Has a blocked $user, so special block handling
1265 $user = $this->getMutableTestUser()->getUser();
1266 $block = new \
Block( [
1267 'address' => $user->getName(),
1268 'user' => $user->getID(),
1269 'by' => $this->getTestSysop()->getUser()->getId(),
1270 'reason' => __METHOD__
,
1271 'expiry' => time() +
100500,
1274 $blockinfo = [ 'blockinfo' => ApiQueryUserInfo
::getBlockInfo( $block ) ];
1276 $expect = Status
::newGood();
1277 $expect->fatal( ApiMessage
::create( 'apierror-blocked', 'blocked', $blockinfo ) );
1278 $expect->fatal( ApiMessage
::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
1279 $expect->fatal( ApiMessage
::create( 'apierror-systemblocked', 'blocked', $blockinfo ) );
1280 $expect->fatal( 'mainpage' );
1281 $expect->fatal( 'parentheses', 'foobar' );
1282 $this->assertEquals( $expect, $mock->errorArrayToStatus( [
1284 [ 'autoblockedtext' ],
1285 [ 'systemblockedtext' ],
1287 [ 'parentheses', 'foobar' ],
1291 public function testDieStatus() {
1292 $mock = new MockApi();
1294 $status = StatusValue
::newGood();
1295 $status->error( 'foo' );
1296 $status->warning( 'bar' );
1298 $mock->dieStatus( $status );
1299 $this->fail( 'Expected exception not thrown' );
1300 } catch ( ApiUsageException
$ex ) {
1301 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1302 $this->assertFalse( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1305 $status = StatusValue
::newGood();
1306 $status->warning( 'foo' );
1307 $status->warning( 'bar' );
1309 $mock->dieStatus( $status );
1310 $this->fail( 'Expected exception not thrown' );
1311 } catch ( ApiUsageException
$ex ) {
1312 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' );
1313 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' );
1316 $status = StatusValue
::newGood();
1317 $status->setOk( false );
1319 $mock->dieStatus( $status );
1320 $this->fail( 'Expected exception not thrown' );
1321 } catch ( ApiUsageException
$ex ) {
1322 $this->assertTrue( ApiTestCase
::apiExceptionHasCode( $ex, 'unknownerror-nocode' ),
1323 'Exception has "unknownerror-nocode"' );
1328 * @covers ApiBase::extractRequestParams
1330 public function testExtractRequestParams() {
1331 $request = new FauxRequest( [
1332 'xxexists' => 'exists!',
1333 'xxmulti' => 'a|b|c|d|{bad}',
1335 'xxtemplate-a' => 'A!',
1336 'xxtemplate-b' => 'B1|B2|B3',
1337 'xxtemplate-c' => '',
1338 'xxrecursivetemplate-b-B1' => 'X',
1339 'xxrecursivetemplate-b-B3' => 'Y',
1340 'xxrecursivetemplate-b-B4' => '?',
1341 'xxemptytemplate-' => 'nope',
1344 'errorformat' => 'raw',
1346 $context = new DerivativeContext( RequestContext
::getMain() );
1347 $context->setRequest( $request );
1348 $main = new ApiMain( $context );
1350 $mock = $this->getMockBuilder( ApiBase
::class )
1351 ->setConstructorArgs( [ $main, 'test', 'xx' ] )
1352 ->setMethods( [ 'getAllowedParams' ] )
1353 ->getMockForAbstractClass();
1354 $mock->method( 'getAllowedParams' )->willReturn( [
1355 'notexists' => null,
1358 ApiBase
::PARAM_ISMULTI
=> true,
1361 ApiBase
::PARAM_ISMULTI
=> true,
1364 ApiBase
::PARAM_ISMULTI
=> true,
1365 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'multi' ],
1367 'recursivetemplate-{m}-{t}' => [
1368 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 't' => 'template-{m}', 'm' => 'multi' ],
1370 'emptytemplate-{m}' => [
1371 ApiBase
::PARAM_ISMULTI
=> true,
1372 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'm' => 'empty' ],
1374 'badtemplate-{e}' => [
1375 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'exists' ],
1377 'badtemplate2-{e}' => [
1378 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'e' => 'badtemplate2-{e}' ],
1380 'badtemplate3-{x}' => [
1381 ApiBase
::PARAM_TEMPLATE_VARS
=> [ 'x' => 'foo' ],
1385 $this->assertEquals( [
1386 'notexists' => null,
1387 'exists' => 'exists!',
1388 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
1390 'template-a' => [ 'A!' ],
1391 'template-b' => [ 'B1', 'B2', 'B3' ],
1393 'template-d' => null,
1394 'recursivetemplate-a-A!' => null,
1395 'recursivetemplate-b-B1' => 'X',
1396 'recursivetemplate-b-B2' => null,
1397 'recursivetemplate-b-B3' => 'Y',
1398 ], $mock->extractRequestParams() );
1400 $used = TestingAccessWrapper
::newFromObject( $main )->getParamsUsed();
1402 $this->assertEquals( [
1407 'xxrecursivetemplate-a-A!',
1408 'xxrecursivetemplate-b-B1',
1409 'xxrecursivetemplate-b-B2',
1410 'xxrecursivetemplate-b-B3',
1417 $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
1418 $this->assertCount( 1, $warnings );
1419 $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );