aboutsummaryrefslogtreecommitdiffstats
path: root/includes/Settings
diff options
context:
space:
mode:
authordaniel <dkinzler@wikimedia.org>2021-12-13 00:04:04 +0100
committerdaniel <dkinzler@wikimedia.org>2022-01-18 21:51:35 +0100
commitda99bcd6aa195cb2e0aea3a30188143080700976 (patch)
tree1260dfd26d7c2218f23ac53153d45a371abe3084 /includes/Settings
parent22ece356e49cb6bd1a884a4d4a59ec0f00f5dcc5 (diff)
downloadmediawikicore-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.php12
-rw-r--r--includes/Settings/SettingsBuilder.php93
-rw-r--r--includes/Settings/Source/FileSource.php6
-rw-r--r--includes/Settings/Source/PhpSettingsSource.php6
-rw-r--r--includes/Settings/Source/SettingsFileUtils.php35
-rw-r--r--includes/Settings/Source/SettingsIncludeLocator.php37
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;
+}