From: Gergő Tisza Date: Fri, 28 Jul 2017 17:41:13 +0000 (+0000) Subject: Make API multivalue limits configurable X-Git-Tag: 1.31.0-rc.0~2313^2 X-Git-Url: http://git.cyclocoop.org/?a=commitdiff_plain;h=5e7640338736c63faf5178b8f4d11b29a47da304;p=lhc%2Fweb%2Fwiklou.git Make API multivalue limits configurable Adds two new parameter settings, ApiBase::PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 for configuring the maximum number of values that can be contained in a multivalue field (for unprivileged and apihighlimits users, respectively). When present, these replace the default 50/500. Change-Id: Ic1b1bcc7ff556b7762c8d2375d910cc4fcb43087 --- diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 2012e7d8cf..80aeff5478 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -204,6 +204,19 @@ abstract class ApiBase extends ContextSource { */ 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 = '*'; @@ -1024,6 +1037,12 @@ abstract class ApiBase extends ContextSource { $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; @@ -1148,7 +1167,9 @@ abstract class ApiBase extends ContextSource { $value, $multi, is_array( $type ) ? $type : null, - $allowAll ? $allSpecifier : null + $allowAll ? $allSpecifier : null, + $multiLimit1, + $multiLimit2 ); } @@ -1350,21 +1371,25 @@ abstract class ApiBase extends ContextSource { * 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 diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 12e778bfb6..318555a93b 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -485,7 +485,9 @@ class ApiHelp extends ApiBase { $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 ); @@ -669,13 +671,25 @@ class ApiHelp extends ApiBase { 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 ); diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 480575ca26..2fa20a96ea 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -371,11 +371,15 @@ class ApiParamInfo extends ApiBase { $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] ) ) { diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 3d4a100419..9fbc0127fe 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -1593,6 +1593,7 @@ "api-help-param-upload": "Must be posted as a file upload using multipart/form-data.", "api-help-param-multi-separate": "Separate values with | 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 $1.", "api-help-param-default": "Default: $1", "api-help-param-default-empty": "Default: (empty)", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 4336c29349..c878a539e6 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -1483,7 +1483,8 @@ "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}}", diff --git a/tests/phpunit/structure/ApiDocumentationTest.php b/tests/phpunit/structure/ApiDocumentationTest.php deleted file mode 100644 index 83585af52e..0000000000 --- a/tests/phpunit/structure/ApiDocumentationTest.php +++ /dev/null @@ -1,185 +0,0 @@ - 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; - } -} diff --git a/tests/phpunit/structure/ApiStructureTest.php b/tests/phpunit/structure/ApiStructureTest.php new file mode 100644 index 0000000000..7912f97902 --- /dev/null +++ b/tests/phpunit/structure/ApiStructureTest.php @@ -0,0 +1,238 @@ + 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; + } +}