diff options
author | daniel <dkinzler@wikimedia.org> | 2021-12-13 00:04:04 +0100 |
---|---|---|
committer | daniel <dkinzler@wikimedia.org> | 2022-01-18 21:51:35 +0100 |
commit | da99bcd6aa195cb2e0aea3a30188143080700976 (patch) | |
tree | 1260dfd26d7c2218f23ac53153d45a371abe3084 /includes/Settings | |
parent | 22ece356e49cb6bd1a884a4d4a59ec0f00f5dcc5 (diff) | |
download | mediawikicore-da99bcd6aa195cb2e0aea3a30188143080700976.tar.gz mediawikicore-da99bcd6aa195cb2e0aea3a30188143080700976.zip |
SettingsBuilder: load settings recursively
Allows settings files to include other settings files.
Change-Id: Ieab7def1ada8b255e60c58927850a58e18309f6e
Diffstat (limited to 'includes/Settings')
-rw-r--r-- | includes/Settings/Cache/CachedSource.php | 12 | ||||
-rw-r--r-- | includes/Settings/SettingsBuilder.php | 93 | ||||
-rw-r--r-- | includes/Settings/Source/FileSource.php | 6 | ||||
-rw-r--r-- | includes/Settings/Source/PhpSettingsSource.php | 6 | ||||
-rw-r--r-- | includes/Settings/Source/SettingsFileUtils.php | 35 | ||||
-rw-r--r-- | includes/Settings/Source/SettingsIncludeLocator.php | 37 |
6 files changed, 173 insertions, 16 deletions
diff --git a/includes/Settings/Cache/CachedSource.php b/includes/Settings/Cache/CachedSource.php index c840636440ad..7e1cf55f29d1 100644 --- a/includes/Settings/Cache/CachedSource.php +++ b/includes/Settings/Cache/CachedSource.php @@ -3,6 +3,7 @@ namespace MediaWiki\Settings\Cache; use BagOStuff; +use MediaWiki\Settings\Source\SettingsIncludeLocator; use MediaWiki\Settings\Source\SettingsSource; /** @@ -11,7 +12,7 @@ use MediaWiki\Settings\Source\SettingsSource; * @since 1.38 * @todo mark as stable before the 1.38 release */ -class CachedSource implements SettingsSource { +class CachedSource implements SettingsSource, SettingsIncludeLocator { /** @var BagOStuff */ private $cache; @@ -115,4 +116,13 @@ class CachedSource implements SettingsSource { 'generation' => $finish - $start, ]; } + + public function locateInclude( string $location ): string { + if ( $this->source instanceof SettingsIncludeLocator ) { + return $this->source->locateInclude( $location ); + } else { + // Just return the location as-is + return $location; + } + } } diff --git a/includes/Settings/SettingsBuilder.php b/includes/Settings/SettingsBuilder.php index bab084e4fcc3..004d65308fd9 100644 --- a/includes/Settings/SettingsBuilder.php +++ b/includes/Settings/SettingsBuilder.php @@ -13,6 +13,8 @@ use MediaWiki\Settings\Config\ConfigSchemaAggregator; use MediaWiki\Settings\Config\PhpIniSink; use MediaWiki\Settings\Source\ArraySource; use MediaWiki\Settings\Source\FileSource; +use MediaWiki\Settings\Source\SettingsFileUtils; +use MediaWiki\Settings\Source\SettingsIncludeLocator; use MediaWiki\Settings\Source\SettingsSource; use StatusValue; @@ -33,7 +35,7 @@ class SettingsBuilder { /** @var ConfigBuilder */ private $configSink; - /** @var array */ + /** @var SettingsSource[] */ private $currentBatch; /** @var ConfigSchemaAggregator */ @@ -86,11 +88,7 @@ class SettingsBuilder { public function load( SettingsSource $source ): self { $this->assertNotFinished(); - if ( $this->cache !== null && $source instanceof CacheableSource ) { - $source = new CachedSource( $this->cache, $source ); - } - - $this->currentBatch[] = $source; + $this->currentBatch[] = $this->wrapSource( $source ); return $this; } @@ -117,12 +115,32 @@ class SettingsBuilder { * @return $this */ public function loadFile( string $path ): self { - // Qualify the path if it isn't already absolute - if ( !preg_match( '!^[a-zA-Z]:\\\\!', $path ) && $path[0] != DIRECTORY_SEPARATOR ) { - $path = $this->baseDir . DIRECTORY_SEPARATOR . $path; + return $this->load( $this->makeSource( $path ) ); + } + + /** + * @param SettingsSource $source + * + * @return SettingsSource + */ + private function wrapSource( SettingsSource $source ): SettingsSource { + if ( $this->cache !== null && $source instanceof CacheableSource ) { + $source = new CachedSource( $this->cache, $source ); } + return $source; + } + + /** + * @param string $location + * @return SettingsSource + */ + private function makeSource( $location ): SettingsSource { + // NOTE: Currently, files are the only kind of location, but we could add others. + // The set of supported source locations will be hard-coded here. + // Custom SettingsSource would have to be instantiated directly and passed to load(). + $path = SettingsFileUtils::resolveRelativeLocation( $location, $this->baseDir ); - return $this->load( new FileSource( $path ) ); + return $this->wrapSource( new FileSource( $path ) ); } /** @@ -174,9 +192,13 @@ class SettingsBuilder { public function apply(): self { $this->assertNotFinished(); - foreach ( $this->currentBatch as $source ) { - $settings = $source->load(); - $this->configSchema->addSchemas( $settings['config-schema'] ?? [], (string)$source ); + $allSettings = $this->loadRecursive( $this->currentBatch ); + + foreach ( $allSettings as $settings ) { + $this->configSchema->addSchemas( + $settings['config-schema'] ?? [], + $settings['source-name'] + ); $this->applySettings( $settings ); } $this->reset(); @@ -184,6 +206,51 @@ class SettingsBuilder { } /** + * Loads all sources in the current batch, recursively resolving includes. + * + * @param SettingsSource[] $batch The batch of sources to load + * @param string[] $stack The current stack of includes, for cycle detection + * + * @return array[] an array of settings arrays + */ + private function loadRecursive( array $batch, array $stack = [] ): array { + $allSettings = []; + + // Depth-first traversal of settings sources. + foreach ( $batch as $source ) { + $sourceName = (string)$source; + + if ( in_array( $sourceName, $stack ) ) { + throw new SettingsBuilderException( + 'Recursive include chain detected: ' . implode( ', ', $stack ) + ); + } + + $settings = $source->load(); + $settings['source-name'] = $sourceName; + + $allSettings[] = $settings; + + $nextBatch = []; + foreach ( $settings['includes'] ?? [] as $location ) { + // Try to resolve the include relative to the source, + // if the source supports that. + if ( $source instanceof SettingsIncludeLocator ) { + $location = $source->locateInclude( $location ); + } + + $nextBatch[] = $this->makeSource( $location ); + } + + $nextStack = array_merge( $stack, [ $settings['source-name'] ] ); + $nextSettings = $this->loadRecursive( $nextBatch, $nextStack ); + $allSettings = array_merge( $allSettings, $nextSettings ); + } + + return $allSettings; + } + + /** * Apply the settings array. * * @param array $settings diff --git a/includes/Settings/Source/FileSource.php b/includes/Settings/Source/FileSource.php index e1aafc138383..7e05dbc59037 100644 --- a/includes/Settings/Source/FileSource.php +++ b/includes/Settings/Source/FileSource.php @@ -15,7 +15,7 @@ use Wikimedia\AtEase\AtEase; * * @since 1.38 */ -class FileSource implements CacheableSource { +class FileSource implements CacheableSource, SettingsIncludeLocator { private const BUILT_IN_FORMATS = [ JsonFormat::class, YamlFormat::class, @@ -210,4 +210,8 @@ class FileSource implements CacheableSource { ); } } + + public function locateInclude( string $location ): string { + return SettingsFileUtils::resolveRelativeLocation( $location, dirname( $this->path ) ); + } } diff --git a/includes/Settings/Source/PhpSettingsSource.php b/includes/Settings/Source/PhpSettingsSource.php index ea26b676ae30..c63d1cdca750 100644 --- a/includes/Settings/Source/PhpSettingsSource.php +++ b/includes/Settings/Source/PhpSettingsSource.php @@ -10,7 +10,7 @@ use Wikimedia\AtEase\AtEase; * * @since 1.38 */ -class PhpSettingsSource implements SettingsSource { +class PhpSettingsSource implements SettingsSource, SettingsIncludeLocator { /** * Path to the PHP file. * @var string @@ -82,4 +82,8 @@ class PhpSettingsSource implements SettingsSource { return $this->path; } + public function locateInclude( string $location ): string { + return SettingsFileUtils::resolveRelativeLocation( $location, dirname( $this->path ) ); + } + } diff --git a/includes/Settings/Source/SettingsFileUtils.php b/includes/Settings/Source/SettingsFileUtils.php new file mode 100644 index 000000000000..f8305075d5fe --- /dev/null +++ b/includes/Settings/Source/SettingsFileUtils.php @@ -0,0 +1,35 @@ +<?php + +namespace MediaWiki\Settings\Source; + +/** + * A collection of static utility methods for use with + * settings files. + * + * @since 1.38 + * @internal since the behavior may change to accommodate more types of source locations. + */ +class SettingsFileUtils { + + /** + * Resolves a relative settings source location. This method will attempt to interpret + * $path as relative to $base if possible. This behaves similar to relative URLs are + * made absolute using a base URL. + * + * The current implementation is based on file paths, but this may be expanded in the future + * to support other kinds of locations. + * + * @param string $path + * @param string $base + * + * @return string + */ + public static function resolveRelativeLocation( string $path, string $base ): string { + // Qualify the path if it isn't already absolute + if ( !preg_match( '!^[a-zA-Z]:\\\\!', $path ) && $path[0] != DIRECTORY_SEPARATOR ) { + $path = $base . DIRECTORY_SEPARATOR . $path; + } + + return $path; + } +} diff --git a/includes/Settings/Source/SettingsIncludeLocator.php b/includes/Settings/Source/SettingsIncludeLocator.php new file mode 100644 index 000000000000..1e34fad2c3db --- /dev/null +++ b/includes/Settings/Source/SettingsIncludeLocator.php @@ -0,0 +1,37 @@ +<?php + +namespace MediaWiki\Settings\Source; + +use MediaWiki\Settings\SettingsBuilderException; + +/** + * Implementations of SettingsSource may additionally implement SettingsIncludeLocator + * as well, to provide support for relative include locations. For instance, a + * SettingsSource that loads a file may provide support for includes to be + * specified relative to the location of that file. + * + * @since 1.38 + * @todo mark as stable before the 1.38 release + */ +interface SettingsIncludeLocator { + + /** + * This method defines how a relative reference to the location of + * another settings source is interpreted. + * + * It tries to make $location absolute by interpreting it as + * relative to the location of the SettingsSource it originates from. + * + * Implementation are "best effort". If a location cannot be made + * absolute, it may be returned as-is. Implementations are also free + * to throw a SettingsBuilderException to indicate that the given + * include location is not supported in this context. + * + * @param string $location + * + * @return string + * @throws SettingsBuilderException if the given location cannot be used + * as an include by the current source. + */ + public function locateInclude( string $location ): string; +} |