*/
const PARAM_DEPRECATED_VALUES = 20;
+ /**
+ * (integer) Maximum number of values, for normal users. Must be used with PARAM_ISMULTI.
+ * @since 1.30
+ */
+ const PARAM_ISMULTI_LIMIT1 = 21;
+
+ /**
+ * (integer) Maximum number of values, for users with the apihighimits right.
+ * Must be used with PARAM_ISMULTI.
+ * @since 1.30
+ */
+ const PARAM_ISMULTI_LIMIT2 = 22;
+
/**@}*/
const ALL_DEFAULT_STRING = '*';
$multi = isset( $paramSettings[self::PARAM_ISMULTI] )
? $paramSettings[self::PARAM_ISMULTI]
: false;
+ $multiLimit1 = isset( $paramSettings[self::PARAM_ISMULTI_LIMIT1] )
+ ? $paramSettings[self::PARAM_ISMULTI_LIMIT1]
+ : null;
+ $multiLimit2 = isset( $paramSettings[self::PARAM_ISMULTI_LIMIT2] )
+ ? $paramSettings[self::PARAM_ISMULTI_LIMIT2]
+ : null;
$type = isset( $paramSettings[self::PARAM_TYPE] )
? $paramSettings[self::PARAM_TYPE]
: null;
$value,
$multi,
is_array( $type ) ? $type : null,
- $allowAll ? $allSpecifier : null
+ $allowAll ? $allSpecifier : null,
+ $multiLimit1,
+ $multiLimit2
);
}
* null, all values are accepted.
* @param string|null $allSpecifier String to use to specify all allowed values, or null
* if this behavior should not be allowed
+ * @param int|null $limit1 Maximum number of values, for normal users.
+ * @param int|null $limit2 Maximum number of values, for users with the apihighlimits right.
* @return string|string[] (allowMultiple ? an_array_of_values : a_single_value)
*/
protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues,
- $allSpecifier = null
+ $allSpecifier = null, $limit1 = null, $limit2 = null
) {
if ( ( trim( $value ) === '' || trim( $value ) === "\x1f" ) && $allowMultiple ) {
return [];
}
+ $limit1 = $limit1 ?: self::LIMIT_SML1;
+ $limit2 = $limit2 ?: self::LIMIT_SML2;
// This is a bit awkward, but we want to avoid calling canApiHighLimits()
// because it unstubs $wgUser
- $valuesList = $this->explodeMultiValue( $value, self::LIMIT_SML2 + 1 );
- $sizeLimit = count( $valuesList ) > self::LIMIT_SML1 && $this->mMainModule->canApiHighLimits()
- ? self::LIMIT_SML2
- : self::LIMIT_SML1;
+ $valuesList = $this->explodeMultiValue( $value, $limit2 + 1 );
+ $sizeLimit = count( $valuesList ) > $limit1 && $this->mMainModule->canApiHighLimits()
+ ? $limit2
+ : $limit1;
if ( $allowMultiple && is_array( $allowedValues ) && $allSpecifier &&
count( $valuesList ) === 1 && $valuesList[0] === $allSpecifier
$type = $settings[ApiBase::PARAM_TYPE];
$multi = !empty( $settings[ApiBase::PARAM_ISMULTI] );
$hintPipeSeparated = true;
- $count = ApiBase::LIMIT_SML2 + 1;
+ $count = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] )
+ ? $settings[ApiBase::PARAM_ISMULTI_LIMIT2] + 1
+ : ApiBase::LIMIT_SML2 + 1;
if ( is_array( $type ) ) {
$count = count( $type );
if ( $multi ) {
$extra = [];
+ $lowcount = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT1] )
+ ? $settings[ApiBase::PARAM_ISMULTI_LIMIT1]
+ : ApiBase::LIMIT_SML1;
+ $highcount = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] )
+ ? $settings[ApiBase::PARAM_ISMULTI_LIMIT2]
+ : ApiBase::LIMIT_SML2;
+
if ( $hintPipeSeparated ) {
$extra[] = $context->msg( 'api-help-param-multi-separate' )->parse();
}
- if ( $count > ApiBase::LIMIT_SML1 ) {
- $extra[] = $context->msg( 'api-help-param-multi-max' )
- ->numParams( ApiBase::LIMIT_SML1, ApiBase::LIMIT_SML2 )
- ->parse();
+ if ( $count > $lowcount ) {
+ if ( $lowcount === $highcount ) {
+ $msg = $context->msg( 'api-help-param-multi-max-simple' )
+ ->numParams( $lowcount );
+ } else {
+ $msg = $context->msg( 'api-help-param-multi-max' )
+ ->numParams( $lowcount, $highcount );
+ }
+ $extra[] = $msg->parse();
}
if ( $extra ) {
$info[] = implode( ' ', $extra );
$item['multi'] = !empty( $settings[ApiBase::PARAM_ISMULTI] );
if ( $item['multi'] ) {
- $item['limit'] = $this->getMain()->canApiHighLimits() ?
- ApiBase::LIMIT_SML2 :
- ApiBase::LIMIT_SML1;
- $item['lowlimit'] = ApiBase::LIMIT_SML1;
- $item['highlimit'] = ApiBase::LIMIT_SML2;
+ $item['lowlimit'] = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT1] )
+ ? $settings[ApiBase::PARAM_ISMULTI_LIMIT1]
+ : ApiBase::LIMIT_SML1;
+ $item['highlimit'] = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] )
+ ? $settings[ApiBase::PARAM_ISMULTI_LIMIT2]
+ : ApiBase::LIMIT_SML2;
+ $item['limit'] = $this->getMain()->canApiHighLimits()
+ ? $item['highlimit']
+ : $item['lowlimit'];
}
if ( !empty( $settings[ApiBase::PARAM_ALLOW_DUPLICATES] ) ) {
"api-help-param-upload": "Must be posted as a file upload using multipart/form-data.",
"api-help-param-multi-separate": "Separate values with <kbd>|</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]].",
"api-help-param-multi-max": "Maximum number of values is {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} for bots).",
+ "api-help-param-multi-max-simple": "Maximum number of values is {{PLURAL:$1|$1}}.",
"api-help-param-multi-all": "To specify all values, use <kbd>$1</kbd>.",
"api-help-param-default": "Default: $1",
"api-help-param-default-empty": "Default: <span class=\"apihelp-empty\">(empty)</span>",
"api-help-param-integer-minmax": "Used to display an integer parameter with a maximum and minimum values\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Minimum value\n* $3 - Maximum value\n\nSee also:\n* {{msg-mw|api-help-param-integer-min}}\n* {{msg-mw|api-help-param-integer-max}}",
"api-help-param-upload": "{{technical}} Used to indicate that an 'upload'-type parameter must be posted as a file upload using multipart/form-data",
"api-help-param-multi-separate": "Used to indicate how to separate multiple values. Not used with {{msg-mw|api-help-param-list}}.",
- "api-help-param-multi-max": "Used to indicate the maximum number of values accepted for a multi-valued parameter.\n\nParameters:\n* $1 - Maximum value without the apihighlimits right\n* $2 - Maximum value with the apihighlimits right",
+ "api-help-param-multi-max": "Used to indicate the maximum number of values accepted for a multi-valued parameter when that value is influenced by the user having apihighlimits right (otherwise {{msg-mw|api-help-param-multi-max-simple}} is used).\n\nParameters:\n* $1 - Maximum value without the apihighlimits right\n* $2 - Maximum value with the apihighlimits right",
+ "api-help-param-multi-max-simple": "Used to indicate the maximum number of values accepted for a multi-valued parameter when that value is not influenced by the user having apihighlimits right (otherwise {{msg-mw|api-help-param-multi-max}} is used).\n\nParameters:\n* $1 - Maximum value",
"api-help-param-multi-all": "Used to indicate what string can be used to specify all possible values of a multi-valued parameter. \n\nParameters:\n* $1 - String to specify all possible values of the parameter",
"api-help-param-default": "Used to display the default value for an API parameter\n\nParameters:\n* $1 - Default value\n\nSee also:\n* {{msg-mw|api-help-param-default-empty}}\n{{Identical|Default}}",
"api-help-param-default-empty": "Used to display the default value for an API parameter when that default is an empty value\n\nSee also:\n* {{msg-mw|api-help-param-default}}",
+++ /dev/null
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * Checks that all API modules, core and extensions, have documentation i18n messages
- *
- * It won't catch everything since i18n messages can vary based on the wiki
- * configuration, but it should catch many cases for forgotten i18n.
- *
- * @group API
- */
-class ApiDocumentationTest extends MediaWikiTestCase {
-
- /** @var ApiMain */
- private static $main;
-
- /** @var array Sets of globals to test. Each array element is input to HashConfig */
- private static $testGlobals = [
- [
- 'MiserMode' => false,
- 'AllowCategorizedRecentChanges' => false,
- ],
- [
- 'MiserMode' => true,
- 'AllowCategorizedRecentChanges' => true,
- ],
- ];
-
- /**
- * Initialize/fetch the ApiMain instance for testing
- * @return ApiMain
- */
- private static function getMain() {
- if ( !self::$main ) {
- self::$main = new ApiMain( RequestContext::getMain() );
- self::$main->getContext()->setLanguage( 'en' );
- self::$main->getContext()->setTitle(
- Title::makeTitle( NS_SPECIAL, 'Badtitle/dummy title for ApiDocumentationTest' )
- );
- }
- return self::$main;
- }
-
- /**
- * Test a message
- * @param Message $msg
- * @param string $what Which message is being checked
- */
- private function checkMessage( $msg, $what ) {
- $msg = ApiBase::makeMessage( $msg, self::getMain()->getContext() );
- $this->assertInstanceOf( 'Message', $msg, "$what message" );
- $this->assertTrue( $msg->exists(), "$what message {$msg->getKey()} exists" );
- }
-
- /**
- * @dataProvider provideDocumentationExists
- * @param string $path Module path
- * @param array $globals Globals to set
- */
- public function testDocumentationExists( $path, array $globals ) {
- $main = self::getMain();
-
- // Set configuration variables
- $main->getContext()->setConfig( new MultiConfig( [
- new HashConfig( $globals ),
- RequestContext::getMain()->getConfig(),
- ] ) );
- foreach ( $globals as $k => $v ) {
- $this->setMwGlobals( "wg$k", $v );
- }
-
- // Fetch module.
- $module = TestingAccessWrapper::newFromObject( $main->getModuleFromPath( $path ) );
-
- // Test messages for flags.
- foreach ( $module->getHelpFlags() as $flag ) {
- $this->checkMessage( "api-help-flag-$flag", "Flag $flag" );
- }
-
- // Module description messages.
- $this->checkMessage( $module->getSummaryMessage(), 'Module summary' );
- $this->checkMessage( $module->getExtendedDescription(), 'Module help top text' );
-
- // Parameters. Lots of messages in here.
- $params = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
- $tags = [];
- foreach ( $params as $name => $settings ) {
- if ( !is_array( $settings ) ) {
- $settings = [];
- }
-
- // Basic description message
- if ( isset( $settings[ApiBase::PARAM_HELP_MSG] ) ) {
- $msg = $settings[ApiBase::PARAM_HELP_MSG];
- } else {
- $msg = "apihelp-{$path}-param-{$name}";
- }
- $this->checkMessage( $msg, "Parameter $name description" );
-
- // If param-per-value is in use, each value's message
- if ( isset( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
- $this->assertInternalType( 'array', $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE],
- "Parameter $name PARAM_HELP_MSG_PER_VALUE is array" );
- $this->assertInternalType( 'array', $settings[ApiBase::PARAM_TYPE],
- "Parameter $name PARAM_TYPE is array for msg-per-value mode" );
- $valueMsgs = $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE];
- foreach ( $settings[ApiBase::PARAM_TYPE] as $value ) {
- if ( isset( $valueMsgs[$value] ) ) {
- $msg = $valueMsgs[$value];
- } else {
- $msg = "apihelp-{$path}-paramvalue-{$name}-{$value}";
- }
- $this->checkMessage( $msg, "Parameter $name value $value" );
- }
- }
-
- // Appended messages (e.g. "disabled in miser mode")
- if ( isset( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
- $this->assertInternalType( 'array', $settings[ApiBase::PARAM_HELP_MSG_APPEND],
- "Parameter $name PARAM_HELP_MSG_APPEND is array" );
- foreach ( $settings[ApiBase::PARAM_HELP_MSG_APPEND] as $i => $msg ) {
- $this->checkMessage( $msg, "Parameter $name HELP_MSG_APPEND #$i" );
- }
- }
-
- // Info tags (e.g. "only usable in mode 1") are typically shared by
- // several parameters, so accumulate them and test them later.
- if ( !empty( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) {
- foreach ( $settings[ApiBase::PARAM_HELP_MSG_INFO] as $i ) {
- $tags[array_shift( $i )] = 1;
- }
- }
- }
-
- // Info tags (e.g. "only usable in mode 1") accumulated above
- foreach ( $tags as $tag => $dummy ) {
- $this->checkMessage( "apihelp-{$path}-paraminfo-{$tag}", "HELP_MSG_INFO tag $tag" );
- }
-
- // Messages for examples.
- foreach ( $module->getExamplesMessages() as $qs => $msg ) {
- $this->assertStringStartsNotWith( 'api.php?', $qs,
- "Query string must not begin with 'api.php?'" );
- $this->checkMessage( $msg, "Example $qs" );
- }
- }
-
- public static function provideDocumentationExists() {
- $main = self::getMain();
- $paths = self::getSubModulePaths( $main->getModuleManager() );
- array_unshift( $paths, $main->getModulePath() );
-
- $ret = [];
- foreach ( $paths as $path ) {
- foreach ( self::$testGlobals as $globals ) {
- $g = [];
- foreach ( $globals as $k => $v ) {
- $g[] = "$k=" . var_export( $v, 1 );
- }
- $k = "Module $path with " . implode( ', ', $g );
- $ret[$k] = [ $path, $globals ];
- }
- }
- return $ret;
- }
-
- /**
- * Return paths of all submodules in an ApiModuleManager, recursively
- * @param ApiModuleManager $manager
- * @return string[]
- */
- protected static function getSubModulePaths( ApiModuleManager $manager ) {
- $paths = [];
- foreach ( $manager->getNames() as $name ) {
- $module = $manager->getModule( $name );
- $paths[] = $module->getModulePath();
- $subManager = $module->getModuleManager();
- if ( $subManager ) {
- $paths = array_merge( $paths, self::getSubModulePaths( $subManager ) );
- }
- }
- return $paths;
- }
-}
--- /dev/null
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * Checks that all API modules, core and extensions, conform to the conventions:
+ * - have documentation i18n messages (the test won't catch everything since
+ * i18n messages can vary based on the wiki configuration, but it should
+ * catch many cases for forgotten i18n)
+ * - do not have inconsistencies in the parameter definitions
+ *
+ * @group API
+ */
+class ApiStructureTest extends MediaWikiTestCase {
+
+ /** @var ApiMain */
+ private static $main;
+
+ /** @var array Sets of globals to test. Each array element is input to HashConfig */
+ private static $testGlobals = [
+ [
+ 'MiserMode' => false,
+ 'AllowCategorizedRecentChanges' => false,
+ ],
+ [
+ 'MiserMode' => true,
+ 'AllowCategorizedRecentChanges' => true,
+ ],
+ ];
+
+ /**
+ * Initialize/fetch the ApiMain instance for testing
+ * @return ApiMain
+ */
+ private static function getMain() {
+ if ( !self::$main ) {
+ self::$main = new ApiMain( RequestContext::getMain() );
+ self::$main->getContext()->setLanguage( 'en' );
+ self::$main->getContext()->setTitle(
+ Title::makeTitle( NS_SPECIAL, 'Badtitle/dummy title for ApiStructureTest' )
+ );
+ }
+ return self::$main;
+ }
+
+ /**
+ * Test a message
+ * @param Message $msg
+ * @param string $what Which message is being checked
+ */
+ private function checkMessage( $msg, $what ) {
+ $msg = ApiBase::makeMessage( $msg, self::getMain()->getContext() );
+ $this->assertInstanceOf( 'Message', $msg, "$what message" );
+ $this->assertTrue( $msg->exists(), "$what message {$msg->getKey()} exists" );
+ }
+
+ /**
+ * @dataProvider provideDocumentationExists
+ * @param string $path Module path
+ * @param array $globals Globals to set
+ */
+ public function testDocumentationExists( $path, array $globals ) {
+ $main = self::getMain();
+
+ // Set configuration variables
+ $main->getContext()->setConfig( new MultiConfig( [
+ new HashConfig( $globals ),
+ RequestContext::getMain()->getConfig(),
+ ] ) );
+ foreach ( $globals as $k => $v ) {
+ $this->setMwGlobals( "wg$k", $v );
+ }
+
+ // Fetch module.
+ $module = TestingAccessWrapper::newFromObject( $main->getModuleFromPath( $path ) );
+
+ // Test messages for flags.
+ foreach ( $module->getHelpFlags() as $flag ) {
+ $this->checkMessage( "api-help-flag-$flag", "Flag $flag" );
+ }
+
+ // Module description messages.
+ $this->checkMessage( $module->getSummaryMessage(), 'Module summary' );
+ $this->checkMessage( $module->getExtendedDescription(), 'Module help top text' );
+
+ // Parameters. Lots of messages in here.
+ $params = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
+ $tags = [];
+ foreach ( $params as $name => $settings ) {
+ if ( !is_array( $settings ) ) {
+ $settings = [];
+ }
+
+ // Basic description message
+ if ( isset( $settings[ApiBase::PARAM_HELP_MSG] ) ) {
+ $msg = $settings[ApiBase::PARAM_HELP_MSG];
+ } else {
+ $msg = "apihelp-{$path}-param-{$name}";
+ }
+ $this->checkMessage( $msg, "Parameter $name description" );
+
+ // If param-per-value is in use, each value's message
+ if ( isset( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
+ $this->assertInternalType( 'array', $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE],
+ "Parameter $name PARAM_HELP_MSG_PER_VALUE is array" );
+ $this->assertInternalType( 'array', $settings[ApiBase::PARAM_TYPE],
+ "Parameter $name PARAM_TYPE is array for msg-per-value mode" );
+ $valueMsgs = $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE];
+ foreach ( $settings[ApiBase::PARAM_TYPE] as $value ) {
+ if ( isset( $valueMsgs[$value] ) ) {
+ $msg = $valueMsgs[$value];
+ } else {
+ $msg = "apihelp-{$path}-paramvalue-{$name}-{$value}";
+ }
+ $this->checkMessage( $msg, "Parameter $name value $value" );
+ }
+ }
+
+ // Appended messages (e.g. "disabled in miser mode")
+ if ( isset( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
+ $this->assertInternalType( 'array', $settings[ApiBase::PARAM_HELP_MSG_APPEND],
+ "Parameter $name PARAM_HELP_MSG_APPEND is array" );
+ foreach ( $settings[ApiBase::PARAM_HELP_MSG_APPEND] as $i => $msg ) {
+ $this->checkMessage( $msg, "Parameter $name HELP_MSG_APPEND #$i" );
+ }
+ }
+
+ // Info tags (e.g. "only usable in mode 1") are typically shared by
+ // several parameters, so accumulate them and test them later.
+ if ( !empty( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) {
+ foreach ( $settings[ApiBase::PARAM_HELP_MSG_INFO] as $i ) {
+ $tags[array_shift( $i )] = 1;
+ }
+ }
+ }
+
+ // Info tags (e.g. "only usable in mode 1") accumulated above
+ foreach ( $tags as $tag => $dummy ) {
+ $this->checkMessage( "apihelp-{$path}-paraminfo-{$tag}", "HELP_MSG_INFO tag $tag" );
+ }
+
+ // Messages for examples.
+ foreach ( $module->getExamplesMessages() as $qs => $msg ) {
+ $this->assertStringStartsNotWith( 'api.php?', $qs,
+ "Query string must not begin with 'api.php?'" );
+ $this->checkMessage( $msg, "Example $qs" );
+ }
+ }
+
+ public static function provideDocumentationExists() {
+ $main = self::getMain();
+ $paths = self::getSubModulePaths( $main->getModuleManager() );
+ array_unshift( $paths, $main->getModulePath() );
+
+ $ret = [];
+ foreach ( $paths as $path ) {
+ foreach ( self::$testGlobals as $globals ) {
+ $g = [];
+ foreach ( $globals as $k => $v ) {
+ $g[] = "$k=" . var_export( $v, 1 );
+ }
+ $k = "Module $path with " . implode( ', ', $g );
+ $ret[$k] = [ $path, $globals ];
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * @dataProvider provideParameterConsistency
+ * @param string $path
+ */
+ public function testParameterConsistency( $path ) {
+ $main = self::getMain();
+ $module = TestingAccessWrapper::newFromObject( $main->getModuleFromPath( $path ) );
+
+ $paramsPlain = $module->getFinalParams();
+ $paramsForHelp = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
+
+ // avoid warnings about empty tests when no parameter needs to be checked
+ $this->assertTrue( true );
+
+ foreach ( [ $paramsPlain, $paramsForHelp ] as $params ) {
+ foreach ( $params as $param => $config ) {
+ if (
+ isset( $config[ApiBase::PARAM_ISMULTI_LIMIT1] )
+ || isset( $config[ApiBase::PARAM_ISMULTI_LIMIT2] )
+ ) {
+ $this->assertTrue( !empty( $config[ApiBase::PARAM_ISMULTI] ), $param
+ . ': PARAM_ISMULTI_LIMIT* only makes sense when PARAM_ISMULTI is true' );
+ $this->assertTrue( isset( $config[ApiBase::PARAM_ISMULTI_LIMIT1] )
+ && isset( $config[ApiBase::PARAM_ISMULTI_LIMIT2] ), $param
+ . ': PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 must be used together' );
+ $this->assertType( 'int', $config[ApiBase::PARAM_ISMULTI_LIMIT1], $param
+ . 'PARAM_ISMULTI_LIMIT1 must be an integer' );
+ $this->assertType( 'int', $config[ApiBase::PARAM_ISMULTI_LIMIT2], $param
+ . 'PARAM_ISMULTI_LIMIT2 must be an integer' );
+ $this->assertGreaterThanOrEqual( $config[ApiBase::PARAM_ISMULTI_LIMIT1],
+ $config[ApiBase::PARAM_ISMULTI_LIMIT2], $param
+ . 'PARAM_ISMULTI limit cannot be smaller for users with apihighlimits rights' );
+ }
+ }
+ }
+ }
+
+ /**
+ * @return array List of API module paths to test
+ */
+ public static function provideParameterConsistency() {
+ $main = self::getMain();
+ $paths = self::getSubModulePaths( $main->getModuleManager() );
+ array_unshift( $paths, $main->getModulePath() );
+
+ $ret = [];
+ foreach ( $paths as $path ) {
+ $ret[] = [ $path ];
+ }
+ return $ret;
+ }
+
+ /**
+ * Return paths of all submodules in an ApiModuleManager, recursively
+ * @param ApiModuleManager $manager
+ * @return string[]
+ */
+ protected static function getSubModulePaths( ApiModuleManager $manager ) {
+ $paths = [];
+ foreach ( $manager->getNames() as $name ) {
+ $module = $manager->getModule( $name );
+ $paths[] = $module->getModulePath();
+ $subManager = $module->getModuleManager();
+ if ( $subManager ) {
+ $paths = array_merge( $paths, self::getSubModulePaths( $subManager ) );
+ }
+ }
+ return $paths;
+ }
+}