diff options
Diffstat (limited to 'includes')
28 files changed, 655 insertions, 141 deletions
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index dd4e556875f2..6099aa2a97d7 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -29,13 +29,119 @@ require_once __DIR__ . '/../autoload.php'; class AutoLoader { - protected static $autoloadLocalClassesLower = null; /** - * @internal Only public for ExtensionRegistry + * A mapping of namespace => file path for MediaWiki core. + * The namespaces should follow the PSR-4 standard for autoloading + * + * @see <https://www.php-fig.org/psr/psr-4/> + * @internal Only public for usage in AutoloadGenerator + */ + public const CORE_NAMESPACES = [ + 'MediaWiki\\' => __DIR__ . '/', + 'MediaWiki\\Actions\\' => __DIR__ . '/actions/', + 'MediaWiki\\Api\\' => __DIR__ . '/api/', + 'MediaWiki\\Auth\\' => __DIR__ . '/auth/', + 'MediaWiki\\Block\\' => __DIR__ . '/block/', + 'MediaWiki\\Cache\\' => __DIR__ . '/cache/', + 'MediaWiki\\ChangeTags\\' => __DIR__ . '/changetags/', + 'MediaWiki\\Config\\' => __DIR__ . '/config/', + 'MediaWiki\\Content\\' => __DIR__ . '/content/', + 'MediaWiki\\DB\\' => __DIR__ . '/db/', + 'MediaWiki\\Deferred\\LinksUpdate\\' => __DIR__ . '/deferred/LinksUpdate/', + 'MediaWiki\\Diff\\' => __DIR__ . '/diff/', + 'MediaWiki\\Edit\\' => __DIR__ . '/edit/', + 'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/', + 'MediaWiki\\FileBackend\\LockManager\\' => __DIR__ . '/filebackend/lockmanager/', + 'MediaWiki\\JobQueue\\' => __DIR__ . '/jobqueue/', + 'MediaWiki\\Json\\' => __DIR__ . '/json/', + 'MediaWiki\\Http\\' => __DIR__ . '/http/', + 'MediaWiki\\Installer\\' => __DIR__ . '/installer/', + 'MediaWiki\\Interwiki\\' => __DIR__ . '/interwiki/', + 'MediaWiki\\Languages\\Data\\' => __DIR__ . '/languages/data/', + 'MediaWiki\\Linker\\' => __DIR__ . '/linker/', + 'MediaWiki\\Logger\\' => __DIR__ . '/debug/logger/', + 'MediaWiki\\Logger\Monolog\\' => __DIR__ . '/debug/logger/monolog/', + 'MediaWiki\\Mail\\' => __DIR__ . '/mail/', + 'MediaWiki\\Page\\' => __DIR__ . '/page/', + 'MediaWiki\\Parser\\' => __DIR__ . '/parser/', + 'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/', + 'MediaWiki\\ResourceLoader\\' => __DIR__ . '/resourceloader/', + 'MediaWiki\\Search\\' => __DIR__ . '/search/', + 'MediaWiki\\Search\\SearchWidgets\\' => __DIR__ . '/search/searchwidgets/', + 'MediaWiki\\Session\\' => __DIR__ . '/session/', + 'MediaWiki\\Shell\\' => __DIR__ . '/shell/', + 'MediaWiki\\Site\\' => __DIR__ . '/site/', + 'MediaWiki\\Sparql\\' => __DIR__ . '/sparql/', + 'MediaWiki\\SpecialPage\\' => __DIR__ . '/specialpage/', + 'MediaWiki\\Tidy\\' => __DIR__ . '/tidy/', + 'MediaWiki\\User\\' => __DIR__ . '/user/', + 'MediaWiki\\Utils\\' => __DIR__ . '/utils/', + 'MediaWiki\\Widget\\' => __DIR__ . '/widget/', + 'Wikimedia\\' => __DIR__ . '/libs/', + 'Wikimedia\\Http\\' => __DIR__ . '/libs/http/', + 'Wikimedia\\Rdbms\\Platform\\' => __DIR__ . '/libs/rdbms/platform/', + 'Wikimedia\\UUID\\' => __DIR__ . '/libs/uuid/', + ]; + + /** + * Cache for lower-case version of the content of $wgAutoloadLocalClasses. + * @var array|null + */ + private static $autoloadLocalClassesLower = null; + + /** * @var string[] Namespace (ends with \) => Path (ends with /) + * @internal Will become private in 1.40. + */ + public static $psr4Namespaces = self::CORE_NAMESPACES; + + /** + * @var string[] Class => File + */ + private static $classFiles = []; + + /** + * Register a directory to load the classes of a given namespace from, + * per PSR4. + * + * @see <https://www.php-fig.org/psr/psr-4/> + * @since 1.39 + * @param string[] $dirs a map of namespace (ends with \) to path (ends with /) + */ + public static function registerNamespaces( array $dirs ): void { + self::$psr4Namespaces += $dirs; + } + + /** + * Register a file to load the given class from. + * @since 1.39 + * + * @param string[] $files a map of qualified class names to file names + */ + public static function registerClasses( array $files ): void { + self::$classFiles += $files; + } + + /** + * Load a file that declares classes, functions, or constants. + * The file will be loaded immediately using require_once in function scope. + * + * @note The file to be loaded MUST NOT set global variables or otherwise + * affect the global state. It MAY however use conditionals to determine + * what to declare and how, e.g. to provide polyfills. + * + * @note The file to be loaded MUST NOT assume that MediaWiki has been + * initialized. In particular, it MUST NOT access configuration variables + * or MediaWikiServices. + * + * @since 1.39 + * + * @param string $file the path of the file to load. */ - public static $psr4Namespaces = []; + public static function loadFile( string $file ): void { + require_once $file; + } /** * Find the file containing the given class. @@ -46,7 +152,13 @@ class AutoLoader { public static function find( $className ): ?string { global $wgAutoloadLocalClasses, $wgAutoloadClasses, $wgAutoloadAttemptLowercase; - $filename = $wgAutoloadLocalClasses[$className] ?? $wgAutoloadClasses[$className] ?? false; + // NOTE: $wgAutoloadClasses is supported for compatibility with old-style extension + // registration files. + + $filename = $wgAutoloadLocalClasses[$className] ?? + self::$classFiles[$className] ?? + $wgAutoloadClasses[$className] ?? + false; if ( !$filename && $wgAutoloadAttemptLowercase ) { // Try a different capitalisation. @@ -62,6 +174,7 @@ class AutoLoader { if ( function_exists( 'wfDebugLog' ) ) { wfDebugLog( 'autoloader', "Class {$className} was loaded using incorrect case" ); } + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable $filename = self::$autoloadLocalClassesLower[$lowerClass]; } } @@ -125,67 +238,73 @@ class AutoLoader { self::$autoloadLocalClassesLower = null; } + ///// Methods used during testing ////////////////////////////////////////////// + private static function assertTesting( $method ) { + if ( !defined( 'MW_PHPUNIT_TEST' ) ) { + throw new LogicException( "$method is not supported outside phpunit tests!" ); + } + } + /** - * Get a mapping of namespace => file path - * The namespaces should follow the PSR-4 standard for autoloading - * - * @see <https://www.php-fig.org/psr/psr-4/> - * @internal Only public for usage in AutoloadGenerator - * @codeCoverageIgnore - * @since 1.31 + * Returns a map of class names to file paths for testing. + * @note Will throw if called outside of phpunit tests! * @return string[] */ - public static function getAutoloadNamespaces() { + public static function getClassFiles(): array { + global $wgAutoloadLocalClasses, $wgAutoloadClasses; + + self::assertTesting( __METHOD__ ); + + // NOTE: ensure the order of preference is the same as used by find(). + return array_merge( + $wgAutoloadClasses, + self::$classFiles, + $wgAutoloadLocalClasses + ); + } + + /** + * Returns a map of namespace names to directories, per PSR4. + * @note Will throw if called outside of phpunit tests! + * @return string[] + */ + public static function getNamespaceDirectories(): array { + self::assertTesting( __METHOD__ ); + return self::$psr4Namespaces; + } + + /** + * Returns an array representing the internal state of Autoloader, + * so it can be remembered and later restored during testing. + * @internal + * @note Will throw if called outside of phpunit tests! + * @return array + */ + public static function getState(): array { + self::assertTesting( __METHOD__ ); return [ - 'MediaWiki\\' => __DIR__ . '/', - 'MediaWiki\\Actions\\' => __DIR__ . '/actions/', - 'MediaWiki\\Api\\' => __DIR__ . '/api/', - 'MediaWiki\\Auth\\' => __DIR__ . '/auth/', - 'MediaWiki\\Block\\' => __DIR__ . '/block/', - 'MediaWiki\\Cache\\' => __DIR__ . '/cache/', - 'MediaWiki\\ChangeTags\\' => __DIR__ . '/changetags/', - 'MediaWiki\\Config\\' => __DIR__ . '/config/', - 'MediaWiki\\Content\\' => __DIR__ . '/content/', - 'MediaWiki\\DB\\' => __DIR__ . '/db/', - 'MediaWiki\\Deferred\\LinksUpdate\\' => __DIR__ . '/deferred/LinksUpdate/', - 'MediaWiki\\Diff\\' => __DIR__ . '/diff/', - 'MediaWiki\\Edit\\' => __DIR__ . '/edit/', - 'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/', - 'MediaWiki\\FileBackend\\LockManager\\' => __DIR__ . '/filebackend/lockmanager/', - 'MediaWiki\\JobQueue\\' => __DIR__ . '/jobqueue/', - 'MediaWiki\\Json\\' => __DIR__ . '/json/', - 'MediaWiki\\Http\\' => __DIR__ . '/http/', - 'MediaWiki\\Installer\\' => __DIR__ . '/installer/', - 'MediaWiki\\Interwiki\\' => __DIR__ . '/interwiki/', - 'MediaWiki\\Languages\\Data\\' => __DIR__ . '/languages/data/', - 'MediaWiki\\Linker\\' => __DIR__ . '/linker/', - 'MediaWiki\\Logger\\' => __DIR__ . '/debug/logger/', - 'MediaWiki\\Logger\Monolog\\' => __DIR__ . '/debug/logger/monolog/', - 'MediaWiki\\Mail\\' => __DIR__ . '/mail/', - 'MediaWiki\\Page\\' => __DIR__ . '/page/', - 'MediaWiki\\Parser\\' => __DIR__ . '/parser/', - 'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/', - 'MediaWiki\\ResourceLoader\\' => __DIR__ . '/resourceloader/', - 'MediaWiki\\Search\\' => __DIR__ . '/search/', - 'MediaWiki\\Search\\SearchWidgets\\' => __DIR__ . '/search/searchwidgets/', - 'MediaWiki\\Session\\' => __DIR__ . '/session/', - 'MediaWiki\\Shell\\' => __DIR__ . '/shell/', - 'MediaWiki\\Site\\' => __DIR__ . '/site/', - 'MediaWiki\\Sparql\\' => __DIR__ . '/sparql/', - 'MediaWiki\\SpecialPage\\' => __DIR__ . '/specialpage/', - 'MediaWiki\\Tidy\\' => __DIR__ . '/tidy/', - 'MediaWiki\\User\\' => __DIR__ . '/user/', - 'MediaWiki\\Utils\\' => __DIR__ . '/utils/', - 'MediaWiki\\Widget\\' => __DIR__ . '/widget/', - 'Wikimedia\\' => __DIR__ . '/libs/', - 'Wikimedia\\Http\\' => __DIR__ . '/libs/http/', - 'Wikimedia\\Rdbms\\Platform\\' => __DIR__ . '/libs/rdbms/platform/', - 'Wikimedia\\UUID\\' => __DIR__ . '/libs/uuid/', + 'classFiles' => self::$classFiles, + 'psr4Namespaces' => self::$psr4Namespaces, ]; } + + /** + * Returns an array representing the internal state of Autoloader, + * so it can be remembered and later restored during testing. + * @internal + * @note Will throw if called outside of phpunit tests! + * + * @param array $state A state array returned by getState(). + */ + public static function restoreState( $state ): void { + self::assertTesting( __METHOD__ ); + + self::$classFiles = $state['classFiles']; + self::$psr4Namespaces = $state['psr4Namespaces']; + } + } -AutoLoader::$psr4Namespaces = AutoLoader::getAutoloadNamespaces(); spl_autoload_register( [ 'AutoLoader', 'autoload' ] ); // Load composer's autoloader if present diff --git a/includes/HtmlHelper.php b/includes/HtmlHelper.php new file mode 100644 index 000000000000..ae67518d9ee9 --- /dev/null +++ b/includes/HtmlHelper.php @@ -0,0 +1,77 @@ +<?php + +namespace MediaWiki; + +use Wikimedia\Assert\Assert; +use Wikimedia\RemexHtml\HTMLData; +use Wikimedia\RemexHtml\Serializer\HtmlFormatter; +use Wikimedia\RemexHtml\Serializer\Serializer; +use Wikimedia\RemexHtml\Serializer\SerializerNode; +use Wikimedia\RemexHtml\Tokenizer\Tokenizer; +use Wikimedia\RemexHtml\TreeBuilder\Dispatcher; +use Wikimedia\RemexHtml\TreeBuilder\TreeBuilder; + +/** + * Static utilities for manipulating HTML strings. + */ +class HtmlHelper { + + /** + * Modify elements of an HTML fragment via a user-provided callback. + * @param string $htmlFragment HTML fragment. Must be valid (ie. coming from the parser, not + * the user). + * @param callable $shouldModifyCallback A callback which takes a single + * RemexHtml\Serializer\SerializerNode argument, and returns true if it should be modified. + * @param callable $modifyCallback A callback which takes a single + * RemexHtml\Serializer\SerializerNode argument and actually performs the modification on it. + * It must return the new node (which can be the original node object). + * @return string + */ + public static function modifyElements( + string $htmlFragment, + callable $shouldModifyCallback, + callable $modifyCallback + ) { + $formatter = new class( $options = [], $shouldModifyCallback, $modifyCallback ) extends HtmlFormatter { + /** @var callable */ + private $shouldModifyCallback; + + /** @var callable */ + private $modifyCallback; + + public function __construct( $options, $shouldModifyCallback, $modifyCallback ) { + parent::__construct( $options ); + $this->shouldModifyCallback = $shouldModifyCallback; + $this->modifyCallback = $modifyCallback; + } + + public function element( SerializerNode $parent, SerializerNode $node, $contents ) { + if ( ( $this->shouldModifyCallback )( $node ) ) { + $node = clone $node; + $node->attrs = clone $node->attrs; + $newNode = ( $this->modifyCallback )( $node ); + Assert::parameterType( SerializerNode::class, $newNode, 'return value' ); + return parent::element( $parent, $newNode, $contents ); + } else { + return parent::element( $parent, $node, $contents ); + } + } + + public function startDocument( $fragmentNamespace, $fragmentName ) { + return ''; + } + }; + $serializer = new Serializer( $formatter ); + $treeBuilder = new TreeBuilder( $serializer ); + $dispatcher = new Dispatcher( $treeBuilder ); + $tokenizer = new Tokenizer( $dispatcher, $htmlFragment ); + + $tokenizer->execute( [ + 'fragmentNamespace' => HTMLData::NS_HTML, + 'fragmentName' => 'body', + ] ); + + return $serializer->getResult(); + } + +} diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 16c839ff4a0b..a3f796c0bd6d 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -1137,8 +1137,13 @@ class OutputPage extends ContextSource { * @return string HTML */ public function getUnprefixedDisplayTitle() { + $service = MediaWikiServices::getInstance(); + $languageConverter = $service->getLanguageConverterFactory() + ->getLanguageConverter( $service->getContentLanguage() ); $text = $this->getDisplayTitle(); - $nsPrefix = $this->getTitle()->getNsText() . ':'; + $nsPrefix = $languageConverter->convertNamespace( + $this->getTitle()->getNamespace() + ) . ':'; $prefix = preg_quote( $nsPrefix, '/' ); return preg_replace( "/^$prefix/i", '', $text ); diff --git a/includes/Revision/RenderedRevision.php b/includes/Revision/RenderedRevision.php index 1ff8ab755684..ae0b84b97937 100644 --- a/includes/Revision/RenderedRevision.php +++ b/includes/Revision/RenderedRevision.php @@ -115,7 +115,6 @@ class RenderedRevision implements SlotRenderingProvider { Authority $performer = null ) { $this->options = $options; - $this->contentRenderer = $contentRenderer; $this->setRevisionInternal( $revision ); diff --git a/includes/Settings/Config/ArrayConfigBuilder.php b/includes/Settings/Config/ArrayConfigBuilder.php index 79789ed35d24..40d3a478c52f 100644 --- a/includes/Settings/Config/ArrayConfigBuilder.php +++ b/includes/Settings/Config/ArrayConfigBuilder.php @@ -5,6 +5,7 @@ namespace MediaWiki\Settings\Config; use Config; use HashConfig; use MediaWiki\Config\IterableConfig; +use function array_key_exists; class ArrayConfigBuilder extends ConfigBuilderBase { @@ -23,8 +24,26 @@ class ArrayConfigBuilder extends ConfigBuilderBase { $this->config[$key] = $value; } - public function setMulti( array $values ): ConfigBuilder { - $this->config = array_merge( $this->config, $values ); + public function setMulti( array $values, array $mergeStrategies = [] ): ConfigBuilder { + if ( !$mergeStrategies ) { + $this->config = array_merge( $this->config, $values ); + return $this; + } + + foreach ( $values as $key => $newValue ) { + // Optimization: Inlined logic from set() for performance + if ( array_key_exists( $key, $this->config ) ) { + $mergeStrategy = $mergeStrategies[$key] ?? null; + if ( $mergeStrategy && is_array( $newValue ) ) { + $oldValue = $this->config[$key]; + if ( $oldValue && is_array( $oldValue ) ) { + $newValue = $mergeStrategy->merge( $oldValue, $newValue ); + } + } + } + $this->config[$key] = $newValue; + } + return $this; } @@ -37,4 +56,24 @@ class ArrayConfigBuilder extends ConfigBuilderBase { public function build(): Config { return new HashConfig( $this->config ); } + + public function setMultiDefault( $defaults, $mergeStrategies ): ConfigBuilder { + foreach ( $defaults as $key => $defaultValue ) { + // Optimization: Inlined logic from setDefault() for performance + if ( array_key_exists( $key, $this->config ) ) { + $mergeStrategy = $mergeStrategies[$key] ?? null; + if ( $mergeStrategy && $defaultValue && is_array( $defaultValue ) ) { + $customValue = $this->config[$key]; + if ( is_array( $customValue ) ) { + $newValue = $mergeStrategy->merge( $defaultValue, $customValue ); + $this->config[$key] = $newValue; + } + } + } else { + $this->config[$key] = $defaultValue; + } + } + return $this; + } + } diff --git a/includes/Settings/Config/ConfigBuilder.php b/includes/Settings/Config/ConfigBuilder.php index d4084894e1ae..b9636b4fb7c0 100644 --- a/includes/Settings/Config/ConfigBuilder.php +++ b/includes/Settings/Config/ConfigBuilder.php @@ -3,6 +3,7 @@ namespace MediaWiki\Settings\Config; use Config; +use MediaWiki\Settings\SettingsBuilderException; /** * Builder for Config objects. @@ -22,12 +23,13 @@ interface ConfigBuilder { public function set( string $key, $value, MergeStrategy $mergeStrategy = null ): ConfigBuilder; /** - * Set all values in the array, with no merge strategy applied. + * Set all values in the array. * * @param array $values + * @param MergeStrategy[] $mergeStrategies The merge strategies indexed by config key * @return ConfigBuilder */ - public function setMulti( array $values ): ConfigBuilder; + public function setMulti( array $values, array $mergeStrategies = [] ): ConfigBuilder; /** * Set the default for the configuration $key to $defaultValue. @@ -44,6 +46,17 @@ interface ConfigBuilder { public function setDefault( string $key, $defaultValue, MergeStrategy $mergeStrategy = null ): ConfigBuilder; /** + * Set defaults in a batch. + * + * @param array $defaults The default values + * @param MergeStrategy[] $mergeStrategies The merge strategies indexed by config key + * @return ConfigBuilder + * @throws SettingsBuilderException if a merge strategy is not provided and + * the value is not an array. + */ + public function setMultiDefault( array $defaults, array $mergeStrategies ): ConfigBuilder; + + /** * Build the resulting Config object. * * @return Config diff --git a/includes/Settings/Config/ConfigBuilderBase.php b/includes/Settings/Config/ConfigBuilderBase.php index 91c249283410..e235e4aea57f 100644 --- a/includes/Settings/Config/ConfigBuilderBase.php +++ b/includes/Settings/Config/ConfigBuilderBase.php @@ -18,7 +18,7 @@ abstract class ConfigBuilderBase implements ConfigBuilder { $newValue, MergeStrategy $mergeStrategy = null ): ConfigBuilder { - if ( $mergeStrategy && is_array( $newValue ) ) { + if ( $mergeStrategy && $this->has( $key ) && is_array( $newValue ) ) { $oldValue = $this->get( $key ); if ( $oldValue && is_array( $oldValue ) ) { $newValue = $mergeStrategy->merge( $oldValue, $newValue ); @@ -31,9 +31,9 @@ abstract class ConfigBuilderBase implements ConfigBuilder { /** * @inheritDoc */ - public function setMulti( array $values ): ConfigBuilder { + public function setMulti( array $values, array $mergeStrategies = [] ): ConfigBuilder { foreach ( $values as $key => $value ) { - $this->set( $key, $value ); + $this->set( $key, $value, $mergeStrategies[$key] ?? null ); } return $this; } @@ -61,4 +61,14 @@ abstract class ConfigBuilderBase implements ConfigBuilder { return $this; } + /** + * @inheritDoc + */ + public function setMultiDefault( array $defaults, array $mergeStrategies ): ConfigBuilder { + foreach ( $defaults as $key => $defaultValue ) { + $this->setDefault( $key, $defaultValue, $mergeStrategies[$key] ?? null ); + } + return $this; + } + } diff --git a/includes/Settings/Config/ConfigSchemaAggregator.php b/includes/Settings/Config/ConfigSchemaAggregator.php index 28b428e6c009..0215a24fe431 100644 --- a/includes/Settings/Config/ConfigSchemaAggregator.php +++ b/includes/Settings/Config/ConfigSchemaAggregator.php @@ -7,6 +7,7 @@ use JsonSchema\Constraints\Constraint; use JsonSchema\Validator; use MediaWiki\Settings\SettingsBuilderException; use StatusValue; +use function array_key_exists; /** * Aggregates multiple config schemas. @@ -32,6 +33,9 @@ class ConfigSchemaAggregator { /** @var Validator */ private $validator; + /** @var MergeStrategy[]|null */ + private $mergeStrategyCache; + /** * Add a config schema to the aggregator. * @@ -60,6 +64,9 @@ class ConfigSchemaAggregator { if ( isset( $schema['mergeStrategy'] ) ) { $this->mergeStrategies[$key] = $schema['mergeStrategy']; } + + // TODO: mark cache as incomplete rather than throwing it away + $this->mergeStrategyCache = null; } /** @@ -121,6 +128,9 @@ class ConfigSchemaAggregator { 'mergeStrategies', $sourceName ); + + // TODO: mark cache as incomplete rather than throwing it away + $this->mergeStrategyCache = null; } /** @@ -184,18 +194,18 @@ class ConfigSchemaAggregator { /** * Get all known types. * - * @return array + * @return array<string|array> */ public function getTypes(): array { return $this->types; } /** - * Get all known merge strategies. + * Get the names of all known merge strategies. * - * @return array + * @return array<string> */ - public function getMergeStrategies(): array { + public function getMergeStrategyNames(): array { return $this->mergeStrategies; } @@ -227,14 +237,56 @@ class ConfigSchemaAggregator { * @throws SettingsBuilderException if merge strategy name is invalid. */ public function getMergeStrategyFor( string $key ): ?MergeStrategy { - $strategyName = $this->mergeStrategies[$key] ?? null; + if ( $this->mergeStrategyCache === null ) { + $this->initMergeStrategies(); + } + return $this->mergeStrategyCache[$key] ?? null; + } + + /** + * Get all merge strategies indexed by config key. If there is no merge + * strategy for a given key, the element will be absent. + * + * @return MergeStrategy[] + */ + public function getMergeStrategies() { + if ( $this->mergeStrategyCache === null ) { + $this->initMergeStrategies(); + } + return $this->mergeStrategyCache; + } - if ( $strategyName === null ) { - $type = $this->types[ $key ] ?? null; - $strategyName = $type ? $this->getStrategyForType( $type ) : null; + /** + * Initialise $this->mergeStrategyCache + */ + private function initMergeStrategies() { + // XXX: Keep $strategiesByName for later, in case we reset the cache? + // Or we could make a bulk version of MergeStrategy::newFromName(), + // to make use of the cache there without the overhead of a method + // call for each setting. + + $strategiesByName = []; + $strategiesByKey = []; + + // Explicitly defined merge strategies + $strategyNamesByKey = $this->mergeStrategies; + + // Loop over settings for which we know a type but not a merge strategy, + // so we can add a merge strategy for them based on their type. + $types = array_diff_key( $this->types, $strategyNamesByKey ); + foreach ( $types as $key => $type ) { + $strategyNamesByKey[$key] = self::getStrategyForType( $type ); + } + + // Assign MergeStrategy objects to settings. Create only one object per strategy name. + foreach ( $strategyNamesByKey as $key => $strategyName ) { + if ( !array_key_exists( $strategyName, $strategiesByName ) ) { + $strategiesByName[$strategyName] = MergeStrategy::newFromName( $strategyName ); + } + $strategiesByKey[$key] = $strategiesByName[$strategyName]; } - return $strategyName ? MergeStrategy::newFromName( $strategyName ) : null; + $this->mergeStrategyCache = $strategiesByKey; } /** @@ -244,7 +296,7 @@ class ConfigSchemaAggregator { * * @return string */ - private function getStrategyForType( $type ): string { + private static function getStrategyForType( $type ) { if ( is_array( $type ) ) { if ( in_array( 'array', $type ) ) { $type = 'array'; diff --git a/includes/Settings/Config/GlobalConfigBuilder.php b/includes/Settings/Config/GlobalConfigBuilder.php index 2808ad395cf8..4e8a267e9f2f 100644 --- a/includes/Settings/Config/GlobalConfigBuilder.php +++ b/includes/Settings/Config/GlobalConfigBuilder.php @@ -4,6 +4,7 @@ namespace MediaWiki\Settings\Config; use Config; use GlobalVarConfig; +use function array_key_exists; class GlobalConfigBuilder extends ConfigBuilderBase { @@ -35,13 +36,26 @@ class GlobalConfigBuilder extends ConfigBuilderBase { $GLOBALS[ $var ] = $value; } - public function setMulti( array $values ): ConfigBuilder { + public function setMulti( array $values, array $mergeStrategies = [] ): ConfigBuilder { // NOTE: It is tempting to do $GLOBALS = array_merge( $GLOBALS, $values ). // But that no longer works in PHP 8.1! // See https://wiki.php.net/rfc/restrict_globals_usage - foreach ( $values as $key => $value ) { + + foreach ( $values as $key => $newValue ) { $var = $this->prefix . $key; // inline getVarName() to avoid function call - $GLOBALS[$var] = $value; + + // Optimization: Inlined logic from set() for performance + if ( isset( $GLOBALS[$var] ) && array_key_exists( $key, $mergeStrategies ) ) { + $mergeStrategy = $mergeStrategies[$key]; + if ( $mergeStrategy && is_array( $newValue ) ) { + $oldValue = $GLOBALS[$var]; + if ( $oldValue && is_array( $oldValue ) ) { + $newValue = $mergeStrategy->merge( $oldValue, $newValue ); + } + } + } + + $GLOBALS[$var] = $newValue; } return $this; } diff --git a/includes/Settings/Config/MergeStrategy.php b/includes/Settings/Config/MergeStrategy.php index 164aa58ba563..2bb3bdedc8d8 100644 --- a/includes/Settings/Config/MergeStrategy.php +++ b/includes/Settings/Config/MergeStrategy.php @@ -3,6 +3,7 @@ namespace MediaWiki\Settings\Config; use MediaWiki\Settings\SettingsBuilderException; +use function array_key_exists; class MergeStrategy { diff --git a/includes/Settings/SettingsBuilder.php b/includes/Settings/SettingsBuilder.php index 22dfb74de771..b09b32fd3cf4 100644 --- a/includes/Settings/SettingsBuilder.php +++ b/includes/Settings/SettingsBuilder.php @@ -19,6 +19,7 @@ use MediaWiki\Settings\Source\SettingsFileUtils; use MediaWiki\Settings\Source\SettingsIncludeLocator; use MediaWiki\Settings\Source\SettingsSource; use StatusValue; +use function array_key_exists; /** * Utility for loading settings files. @@ -368,13 +369,8 @@ class SettingsBuilder { } if ( $this->defaultsNeedMerging ) { - foreach ( $settings['config-schema'] ?? [] as $key => $schema ) { - $this->configSink->setDefault( - $key, - $schema['default'], - $this->configSchema->getMergeStrategyFor( $key ) - ); - } + $mergeStrategies = $this->configSchema->getMergeStrategies(); + $this->configSink->setMultiDefault( $defaults, $mergeStrategies ); } else { // Optimization: no merge strategy, just override in one go $this->configSink->setMulti( $defaults ); @@ -398,12 +394,9 @@ class SettingsBuilder { $this->applySchemas( $settings ); - foreach ( $settings['config'] ?? [] as $key => $value ) { - $this->configSink->set( - $key, - $value, - $this->configSchema->getMergeStrategyFor( $key ) - ); + if ( isset( $settings['config'] ) ) { + $mergeStrategies = $this->configSchema->getMergeStrategies(); + $this->configSink->setMulti( $settings['config'], $mergeStrategies ); } if ( isset( $settings['config-overrides'] ) ) { @@ -555,4 +548,5 @@ class SettingsBuilder { $this->apply(); $this->finished = true; } + } diff --git a/includes/Title.php b/includes/Title.php index a12db4e92b90..8ed41682759c 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -254,7 +254,8 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { * unless $forceClone is "clone". If $forceClone is "clone" and the given TitleValue * is already a Title instance, that instance is copied using the clone operator. * - * @deprecated since 1.34, use newFromLinkTarget or castFromLinkTarget + * @deprecated since 1.34, use newFromLinkTarget or castFromLinkTarget. Hard + * deprecated in 1.39. * * @param TitleValue $titleValue Assumed to be safe. * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned. @@ -262,6 +263,7 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { * @return Title */ public static function newFromTitleValue( TitleValue $titleValue, $forceClone = '' ) { + wfDeprecated( __METHOD__, '1.34' ); return self::newFromLinkTarget( $titleValue, $forceClone ); } diff --git a/includes/TrackingCategories.php b/includes/TrackingCategories.php index 98fd985887d1..9441af86d50e 100644 --- a/includes/TrackingCategories.php +++ b/includes/TrackingCategories.php @@ -157,7 +157,7 @@ class TrackingCategories { } // XXX: should be a better way to convert a TitleValue // to a PageReference! - $tempTitle = Title::newFromTitleValue( $tempTitle ); + $tempTitle = Title::newFromLinkTarget( $tempTitle ); $catName = $msgObj->page( $tempTitle )->text(); # Allow tracking categories to be disabled by setting them to "-" if ( $catName !== '-' ) { diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index c639fbd2d21b..d9461ebe0f56 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -21,6 +21,7 @@ */ use MediaWiki\Cache\LinkBatchFactory; +use MediaWiki\MediaWikiServices; /** * A query module to list all wiki links on a given set of pages. @@ -88,16 +89,27 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { if ( $pages === [] ) { return; // nothing to do } + $linksMigration = MediaWikiServices::getInstance()->getLinksMigration(); $params = $this->extractRequestParams(); + if ( isset( $linksMigration::$mapping[$this->table] ) ) { + list( $nsField, $titleField ) = $linksMigration->getTitleFields( $this->table ); + $queryInfo = $linksMigration->getQueryInfo( $this->table ); + $this->addTables( $queryInfo['tables'] ); + $this->addJoinConds( $queryInfo['joins'] ); + } else { + $this->addTables( $this->table ); + $nsField = $this->prefix . '_namespace'; + $titleField = $this->prefix . '_title'; + } + $this->addFields( [ 'pl_from' => $this->prefix . '_from', - 'pl_namespace' => $this->prefix . '_namespace', - 'pl_title' => $this->prefix . '_title' + 'pl_namespace' => $nsField, + 'pl_title' => $titleField, ] ); - $this->addTables( $this->table ); $this->addWhereFld( $this->prefix . '_from', array_keys( $pages ) ); $multiNS = true; @@ -125,7 +137,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { return; } } elseif ( $params['namespace'] ) { - $this->addWhereFld( $this->prefix . '_namespace', $params['namespace'] ); + $this->addWhereFld( $nsField, $params['namespace'] ); $multiNS = $params['namespace'] === null || count( $params['namespace'] ) !== 1; } @@ -139,9 +151,9 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $this->addWhere( "{$this->prefix}_from $op $plfrom OR " . "({$this->prefix}_from = $plfrom AND " . - "({$this->prefix}_namespace $op $plns OR " . - "({$this->prefix}_namespace = $plns AND " . - "{$this->prefix}_title $op= $pltitle)))" + "($nsField $op $plns OR " . + "($nsField = $plns AND " . + "$titleField $op= $pltitle)))" ); } @@ -156,10 +168,10 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $order[] = $this->prefix . '_from' . $sort; } if ( $multiNS ) { - $order[] = $this->prefix . '_namespace' . $sort; + $order[] = $nsField . $sort; } if ( $multiTitle ) { - $order[] = $this->prefix . '_title' . $sort; + $order[] = $titleField . $sort; } if ( $order ) { $this->addOption( 'ORDER BY', $order ); diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index 8e149844ba7b..7d61e8bfa3d3 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -35,7 +35,7 @@ "apihelp-main-param-servedby": "결과에 요청을 처리한 호스트네임을 포함합니다.", "apihelp-main-param-curtimestamp": "결과의 타임스탬프를 포함합니다.", "apihelp-main-param-responselanginfo": "<var>uselang</var> 및 <var>errorlang</var>에 사용되는 언어를 결과에 포함합니다.", - "apihelp-main-param-origin": "크로스 도메인 AJAX 요청 (CORS)을 사용하여 API에 접근할 때, 이것을 발신 도메인으로 설정하십시오. 모든 pre-flight 요청에 포함되어야 하며, 이에 따라 (POST 본문이 아닌) 요청 URI의 일부여야 합니다.\n\n인증된 요청의 경우, <code>Origin</code> 헤더의 발신지들 중 하나와 정확히 일치해야 하므로 <kbd>https://en.wikipedia.org</kbd> 또는 <kbd>https://meta.wikimedia.org</kbd>와 같이 설정되어야 합니다. 이 변수가 <code>Origin</code> 헤더와 일치하지 않으면 403 응답이 반환됩니다. 이 변수가 <code>Origin</code> 헤더와 일치하고 발신지가 화이트리스트에 있을 경우 <code>Access-Control-Allow-Origin</code>과 <code>Access-Control-Allow-Credentials</code> 헤더가 설정됩니다.\n\n인증되지 않은 요청의 경우, <kbd>*</kbd> 값을 지정하십시오. 이를 통해 <code>Access-Control-Allow-Origin</code> 헤더가 설정되지만 <code>Access-Control-Allow-Credentials</code>는 <code>false</code>로 설정되어 모든 사용자 지정 데이터가 제한을 받게 됩니다.", + "apihelp-main-param-origin": "크로스 도메인 AJAX 요청 (CORS)을 사용하여 API에 접근할 때, 이것을 발신 도메인으로 설정하십시오. 모든 pre-flight 요청에 포함되어야 하며, 이에 따라 (POST 본문이 아닌) 요청 URI의 일부여야 합니다.\n\n인증된 요청의 경우, <code>Origin</code> 헤더의 발신지들 중 하나와 정확히 일치해야 하므로 <kbd>https://en.wikipedia.org</kbd> 또는 <kbd>https://meta.wikimedia.org</kbd>와 같이 설정되어야 합니다. 이 변수가 <code>Origin</code> 헤더와 일치하지 않으면 403 응답이 반환됩니다. 이 변수가 <code>Origin</code> 헤더와 일치하고 발신지가 허용된 경우 <code>Access-Control-Allow-Origin</code>과 <code>Access-Control-Allow-Credentials</code> 헤더가 설정됩니다.\n\n인증되지 않은 요청의 경우, <kbd>*</kbd> 값을 지정하십시오. 이를 통해 <code>Access-Control-Allow-Origin</code> 헤더가 설정되지만 <code>Access-Control-Allow-Credentials</code>는 <code>false</code>로 설정되어 모든 사용자 지정 데이터가 제한을 받게 됩니다.", "apihelp-main-param-uselang": "메시지 번역을 위한 언어입니다. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>에 <kbd>siprop=languages</kbd>를 함께 사용하면 언어 코드의 목록을 반환하고, <kbd>user</kbd>를 지정하면 현재 사용자의 언어 환경 설정을 사용하며, <kbd>content</kbd>를 지정하면 이 위키의 콘텐츠 언어를 사용합니다.", "apihelp-main-param-errorformat": "경고 및 오류 텍스트 출력을 위해 사용할 형식", "apihelp-main-paramvalue-errorformat-plaintext": "HTML 태그를 제거하고 엔티티가 치환된 위키텍스트입니다.", @@ -113,9 +113,9 @@ "apihelp-compare-paramvalue-prop-rel": "해당하는 경우 'from' 이전과 'to' 이후 판의 판 ID입니다.", "apihelp-compare-paramvalue-prop-ids": "'from'과 'to' 판의 문서와 판 ID입니다.", "apihelp-compare-paramvalue-prop-title": "'from'과 'to' 판의 문서 제목입니다.", - "apihelp-compare-paramvalue-prop-user": "'from'과 'to' 판의 사용자 이름과 ID입니다.", - "apihelp-compare-paramvalue-prop-comment": "'from'과 'to' 판의 설명입니다.", - "apihelp-compare-paramvalue-prop-parsedcomment": "'from'과 to' 판의 변환된 설명입니다.", + "apihelp-compare-paramvalue-prop-user": "'from'과 'to' 판의 사용자 이름과 ID입니다. 사용자가 판을 삭제한 경우 <samp>fromuserhidden</samp> 또는 <samp>touserhidden</samp> 속성이 반환됩니다.", + "apihelp-compare-paramvalue-prop-comment": "'from'과 'to' 판의 설명입니다. 사용자가 판을 삭제한 경우 <samp>fromuserhidden</samp> 또는 <samp>touserhidden</samp> 속성이 반환됩니다.", + "apihelp-compare-paramvalue-prop-parsedcomment": "'from'과 to' 판의 변환된 설명입니다. 사용자가 판을 삭제한 경우 <samp>fromuserhidden</samp> 또는 <samp>touserhidden</samp> 속성이 반환됩니다.", "apihelp-compare-paramvalue-prop-size": "'from'과 'to' 판의 크기입니다.", "apihelp-compare-example-1": "판 1과 2의 차이를 생성합니다.", "apihelp-createaccount-summary": "새 사용자 계정을 만듭니다.", @@ -130,7 +130,7 @@ "apihelp-delete-param-reason": "삭제의 이유. 설정하지 않으면 자동 생성되는 이유를 사용합니다.", "apihelp-delete-param-tags": "삭제 기록의 항목에 적용할 변경 태그입니다.", "apihelp-delete-param-watch": "문서를 현재 사용자의 주시문서 목록에 추가합니다.", - "apihelp-delete-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 주시를 변경하지 않습니다.", + "apihelp-delete-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 (봇 사용자는 무시됨) 주시를 변경하지 않습니다.", "apihelp-delete-param-unwatch": "문서를 현재 사용자의 주시문서 목록에서 제거합니다.", "apihelp-delete-param-oldimage": "[[Special:ApiHelp/query+imageinfo|action=query&prop=imageinfo&iiprop=archivename]]에 지정된 바대로 삭제할 오래된 그림의 이름입니다.", "apihelp-delete-example-simple": "<kbd>Main Page</kbd>를 삭제합니다.", @@ -139,13 +139,13 @@ "apihelp-edit-summary": "문서를 만들고 편집합니다.", "apihelp-edit-param-title": "편집할 문서의 제목. <var>$1pageid</var>과 같이 사용할 수 없습니다.", "apihelp-edit-param-pageid": "편집할 문서의 문서 ID입니다. <var>$1title</var>과 함께 사용할 수 없습니다.", - "apihelp-edit-param-section": "문단 번호입니다. <kbd>0</kbd>은 최상위 문단, <kbd>new</kbd>는 새 문단입니다.", + "apihelp-edit-param-section": "문단 식별자입니다. <kbd>0</kbd>은 최상위 문단, <kbd>new</kbd>는 새 문단입니다. 종종 양의 정수이지만 숫자가 아닐 수도 있습니다.", "apihelp-edit-param-sectiontitle": "새 문단을 위한 제목.", "apihelp-edit-param-text": "문서 내용.", "apihelp-edit-param-summary": "편집 요약. 또한 $1section=new 및 $1sectiontitle이 설정되어 있지 않을 때 문단 제목.", "apihelp-edit-param-tags": "이 판에 적용할 태그를 변경합니다.", "apihelp-edit-param-minor": "이 편집을 사소한 편집으로 표시합니다.", - "apihelp-edit-param-notminor": "사소하지 않은 편집.", + "apihelp-edit-param-notminor": "\"{{int:tog-minordefault}}\" 사용자 환경 설정이 설정된 경우에도 이 편집을 사소한 편집으로 표시하지 않습니다.", "apihelp-edit-param-bot": "이 편집을 봇 편집으로 표시.", "apihelp-edit-param-basetimestamp": "기본 판의 타임스탬프이며, 편집 충돌을 발견하기 위해 사용됩니다. [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]를 통해 가져올 수 있습니다.", "apihelp-edit-param-starttimestamp": "편집 과정을 시작할 때의 타임스탬프이며 편집 충돌을 발견하기 위해 사용됩니다. 편집 과정을 시작할 때(예: 문서 내용을 편집으로 불러올 때) <var>[[Special:ApiHelp/main|curtimestamp]]</var>를 사용하여 적절한 값을 가져올 수 있습니다.", @@ -154,7 +154,7 @@ "apihelp-edit-param-nocreate": "페이지가 존재하지 않으면 오류를 출력합니다.", "apihelp-edit-param-watch": "문서를 현재 사용자의 주시문서 목록에 추가합니다.", "apihelp-edit-param-unwatch": "문서를 현재 사용자의 주시문서 목록에서 제거합니다.", - "apihelp-edit-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 주시를 변경하지 않습니다.", + "apihelp-edit-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 (봇 사용자는 무시됨) 주시를 변경하지 않습니다.", "apihelp-edit-param-prependtext": "이 텍스를 문서의 처음에 추가합니다. $1text를 무효로 합니다.", "apihelp-edit-param-appendtext": "이 텍스트를 문서의 끝에 추가합니다. $1text를 무효로 합니다.\n\n새 문단을 추가하려면 이 변수 대신 $1section=new를 사용하십시오.", "apihelp-edit-param-undo": "이 판의 편집을 취소합니다. $1text, $1prependtext, $1appendtext를 무효로 합니다.", @@ -249,7 +249,7 @@ "apihelp-move-param-noredirect": "넘겨주기 문서 만들지 않기", "apihelp-move-param-watch": "현재 사용자의 주시 문서에 이 문서와 넘겨주기 문서를 추가하기", "apihelp-move-param-unwatch": "현재 사용자의 주시 문서에 이 문서와 넘겨주기 문서를 제거하기", - "apihelp-move-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 주시를 변경하지 않습니다.", + "apihelp-move-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 (봇 사용자는 무시됨) 주시를 변경하지 않습니다.", "apihelp-move-param-ignorewarnings": "모든 경고 무시하기", "apihelp-move-example-move": "<kbd>기존 제목</kbd>에서 <kbd>대상 제목</kbd>으로 넘겨주기를 만들지 않고 이동하기.", "apihelp-opensearch-summary": "OpenSearch 프로토콜을 이용하여 위키를 검색합니다.", @@ -299,7 +299,7 @@ "apihelp-parse-param-preview": "미리 보기 모드에서 파싱합니다.", "apihelp-parse-param-sectionpreview": "문단 미리 보기 모드에서 파싱합니다. (미리 보기 모드도 활성화함)", "apihelp-parse-param-disabletoc": "출력에서 목차를 제외합니다.", - "apihelp-parse-param-useskin": "선택한 스킨을 파서 출력에 적용합니다. 다음의 속성에 영향을 줄 수 있습니다: <kbd>langlinks</kbd>, <kbd>headitems</kbd>, <kbd>modules</kbd>, <kbd>jsconfigvars</kbd>, <kbd>indicators</kbd>.", + "apihelp-parse-param-useskin": "선택한 스킨을 파서 출력에 적용합니다. 다음의 속성에 영향을 줄 수 있습니다: <kbd>text</kbd>, <kbd>langlinks</kbd>, <kbd>headitems</kbd>, <kbd>modules</kbd>, <kbd>jsconfigvars</kbd>, <kbd>indicators</kbd>.", "apihelp-parse-param-contentformat": "입력 텍스트에 사용할 내용 직렬화 포맷입니다. $1text와 함께 사용할 때에만 유효합니다.", "apihelp-parse-example-page": "페이지를 파싱합니다.", "apihelp-parse-example-text": "위키텍스트의 구문을 분석합니다.", @@ -311,7 +311,7 @@ "apihelp-patrol-example-revid": "판을 점검합니다.", "apihelp-protect-summary": "문서의 보호 수준을 변경합니다.", "apihelp-protect-param-reason": "보호 또는 보호 해제의 이유.", - "apihelp-protect-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 주시를 변경하지 않습니다.", + "apihelp-protect-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 (봇 사용자는 무시됨) 주시를 변경하지 않습니다.", "apihelp-protect-example-protect": "문서 보호", "apihelp-purge-summary": "주어진 제목을 위한 캐시를 새로 고침.", "apihelp-purge-param-forcelinkupdate": "링크 테이블을 업데이트합니다.", diff --git a/includes/api/i18n/nb.json b/includes/api/i18n/nb.json index eab9e23e5b05..67393f6382d8 100644 --- a/includes/api/i18n/nb.json +++ b/includes/api/i18n/nb.json @@ -1432,6 +1432,7 @@ "apihelp-undelete-param-fileids": "ID-ene til filrevisjonene som skal gjenopprettes. Hvis både <var>$1timestamps</var> og <var>$1fileids</var> er tomme blir alt gjenopprettet.", "apihelp-undelete-param-watchlist": "Legg til eller fjern siden fra den gjeldende brukerens overvåkningsliste, bruk innstillinger (ignoreres for botbrukere) eller ikke endre overvåkning.", "apihelp-undelete-param-watchlistexpiry": "Tidsstempel for utløp i overvåkningslisten. Utelat denne parameteren for å la den gjeldende utløpstiden være uendret.", + "apihelp-undelete-param-undeletetalk": "Gjenopprett alle revisjoner av den tilknyttede diskusjonssiden hvis det er noen.", "apihelp-undelete-example-page": "Gjenopprett siden <kbd>Main Page</kbd>.", "apihelp-undelete-example-revisions": "Gjenopprett to revisjoner av siden <kbd>Main Page</kbd>.", "apihelp-unlinkaccount-summary": "Fjern en lenket tredjepartskonto fra den gjeldende brukeren.", diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php index 2c81a8cd021b..d3e0803e45ad 100644 --- a/includes/installer/DatabaseUpdater.php +++ b/includes/installer/DatabaseUpdater.php @@ -170,6 +170,7 @@ abstract class DatabaseUpdater { $registry->clearQueue(); // Read extension.json files + // NOTE: As a side-effect, this registers classes and namespaces with the autoloader. $data = $registry->readFromQueue( $queue ); // Merge extension attribute hooks with hooks defined by a .php @@ -179,10 +180,10 @@ abstract class DatabaseUpdater { $legacySchemaHooks = array_merge( $legacySchemaHooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] ); } - // Merge classes from extension.json - global $wgAutoloadClasses; + // Register classes defined by extensions that are loaded by including of a file that + // updates global variables, rather than having an extension.json manifest. if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) { - $wgAutoloadClasses += $vars['wgAutoloadClasses']; + AutoLoader::registerClasses( $vars['wgAutoloadClasses'] ); } return new HookContainer( diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index db25ae4950c6..f34608580e7a 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -1563,7 +1563,7 @@ abstract class Installer { /** * Auto-detect extensions with an extension.json file. Load the extensions, - * populate $wgAutoloadClasses and return the merged registry data. + * register classes with the autoloader and return the merged registry data. * * @return array */ @@ -1579,8 +1579,7 @@ abstract class Installer { $registry = new ExtensionRegistry(); $data = $registry->readFromQueue( $queue ); - global $wgAutoloadClasses; - $wgAutoloadClasses += $data['globals']['wgAutoloadClasses']; + AutoLoader::registerClasses( $data['globals']['wgAutoloadClasses'] ); return $data; } diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index 97f997581e78..09ab53bc7134 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -215,6 +215,7 @@ class MysqlUpdater extends DatabaseUpdater { [ 'addTable', 'user_autocreate_serial', 'patch-user_autocreate_serial.sql' ], [ 'modifyField', 'ipblocks_restrictions', 'ir_ipb_id', 'patch-ipblocks_restrictions-ir_ipb_id.sql' ], [ 'modifyField', 'ipblocks', 'ipb_id', 'patch-ipblocks-ipb_id.sql' ], + [ 'modifyField', 'user', 'user_editcount', 'patch-user-user_editcount.sql' ], ]; } diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php index ed959bc5e567..d27126cbcb8a 100644 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@ -187,6 +187,7 @@ class SqliteUpdater extends DatabaseUpdater { [ 'addTable', 'user_autocreate_serial', 'patch-user_autocreate_serial.sql' ], [ 'modifyField', 'ipblocks_restrictions', 'ir_ipb_id', 'patch-ipblocks_restrictions-ir_ipb_id.sql' ], [ 'modifyField', 'ipblocks', 'ipb_id', 'patch-ipblocks-ipb_id.sql' ], + [ 'modifyField', 'user', 'user_editcount', 'patch-user-user_editcount.sql' ], ]; } diff --git a/includes/installer/i18n/it.json b/includes/installer/i18n/it.json index e2a9dbc4fc85..f47859caac89 100644 --- a/includes/installer/i18n/it.json +++ b/includes/installer/i18n/it.json @@ -238,6 +238,7 @@ "config-logo-preview-main": "Pagina principale", "config-logo-icon": "Logo (icona)", "config-logo-icon-help": "La tua icona del logo dovrebbe essere quadrata e possibilmente avere o più di 50px di risoluzione, oppure essere un file SVG.", + "config-logo-filedrop": "Trascina un file immagine qui", "config-instantcommons": "Abilita Instant Commons", "config-instantcommons-help": "[https://www.mediawiki.org/wiki/Special:MyLanguage/InstantCommons Instant Commons] è una funzionalità che consente ai wiki di usare immagini, suoni e altri file multimediali che trovate sul sito [https://commons.wikimedia.org/ Wikimedia Commons].\nPer fare questo, MediaWiki richiede l'accesso a Internet.\n\nPer ulteriori informazioni su questa funzionalità, incluse le istruzioni su come configurarlo per wiki diversi da Wikimedia Commons, consultare [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos il manuale].", "config-cc-error": "Il selettore di licenze Creative Commons non ha dato alcun risultato.\nInserisci manualmente il nome della licenza.", @@ -300,6 +301,7 @@ "config-install-subscribe-fail": "Impossibile sottoscrivere [https://lists.wikimedia.org/postorius/lists/mediawiki-announce.lists.wikimedia.org/ mediawiki-announce]: $1", "config-install-subscribe-notpossible": "cURL non è installato e <code>allow_url_fopen</code> non è disponibile.", "config-install-subscribe-alreadypending": "Una richiesta di sottoscrizione a mediawiki-announce è stata già inviata. Rispondere alla e-mail di conferma che è stata precedentemente inviata.", + "config-install-subscribe-possiblefail": "La richiesta di iscrizione a mediawiki-announce potrebbe non essere andata a buon fine. Se non ricevi un messaggio di posta elettronica di conferma entro pochi minuti, vai a [https://lists.wikimedia.org/postorius/lists/mediawiki-announce.lists.wikimedia.org/ lists.wikimedia.org] per provare di nuovo a inviare l'iscrizione.", "config-install-mainpage": "Creazione della pagina principale con contenuto predefinito", "config-install-mainpage-exists": "La pagina principale già esiste, saltata", "config-install-extension-tables": "Creazione delle tabelle per le estensioni attivate", diff --git a/includes/installer/i18n/lij.json b/includes/installer/i18n/lij.json index 30058f0ca748..5cce279e2f92 100644 --- a/includes/installer/i18n/lij.json +++ b/includes/installer/i18n/lij.json @@ -59,6 +59,7 @@ "config-no-fts3": "<strong>Atençión:</strong> SQLite o l'é conpilòu sénsa o [//sqlite.org/fts3.html mòdolo FTS3], e fonçionalitæ de riçèrca no saiàn disponìbili in sce 'sto backend chi.", "config-pcre-old": "<strong>Erô fatâle:</strong> o l'é domandòu PCRE $1 ò sucesîvo.\nO tò PHP binâio o l'é colegòu con PCRE $2.\n[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Errors_and_symptoms/PCRE Ciù informaçioîn].", "config-pcre-no-utf8": "<strong>Erô fatâle</strong>: o mòdolo PCRE de PHP o pâ ch'o ségge stæto conpilòu sénsa o sùporto PCRE_UTF8.\nMediaWiki o l'à bezéugno do sùporto UTF8 pe fonçionâ coretaménte.", + "config-pcre-invalid-newline": "<strong>Erô fatâle:</strong> o mòdolo PCRE de PHP o pâ ch'o ségge stæto conpilòu con PCRE_CONFIG_NEWLINE = -1 pe ANY.\nMediaWiki o no l'é conpatìbile con sta configuraçión chi.", "config-memory-raised": "O valô <code>memory_limit</code> de PHP o l'é $1, aomentòu a $2.", "config-memory-bad": "<strong>Atençión:</strong> o valô de <code>memory_limit</code> de PHP o l'é $1.\nFòscia o l'é tròppo bàsso.\nL'instalaçión a poriéiva falî!", "config-apc": "[https://www.php.net/apc APC] o l'é instalòu", @@ -212,6 +213,18 @@ "config-upload-help": "O caregaménto de file o poriéiva espónn-e o tò sèrver a di réizeghi de seguéssa.\nPe magioî informaçioîn, lêzi a [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security seçión in sciâ seguéssa] into manoâle.\n\nPe consentî o caregaménto de file, modìfica a modalitæ inta sotodirectory <code>images</code> da directory prinçipâ de MediaWiki coscì che o sèrver web o pòsse scrîve lì.\nDòppo ativâ quésta çèrnia.", "config-upload-deleted": "Directory pe-i file scàssæ:", "config-upload-deleted-help": "Çèrni 'na directory inta quæ archiviâ i file scàssæ.\nIdealménte, quésta a no doviéiva êse acescìbile da-o web.", + "config-personalization-settings": "Personalizaçión", + "config-logo-summary": "Pe ciaschedùn de sti cànpi chi o l'é necesâio caregâ 'n'inmàgine da-e dimenscioìn apropriæ, meténdo l'URL inta cazélla de sótta. Se peu dêuviâ <code>$wgStylePath</code> òpû <code>$wgScriptPath</code> se o lögo o fa riferiménto a sti percórsci chi. A ògni mòddo, e cazélle se pêuan lasciâ vêue.\n\nCo-i browser sùportæ se peu strascinâ 'n'inmàgine da-o pròpio scistêma òperatîvo inte cazélle de inseriménto ò inte l'àrea de anteprìmma.", + "config-logo-preview-main": "Pàgina prinçipâ", + "config-logo-icon": "Lögo (icónn-a):", + "config-logo-icon-help": "L'icónn-a do tò lögo a doviéiva êse quadrâta e co-ina resoluçión ciù âta de 50px, òpû into formâto SVG.", + "config-logo-wordmark": "Wordmark (facoltatîvo):", + "config-logo-wordmark-help": "O nómme do tò scîto. Se o no l'é indicòu, o saiâ repigiòu da-o tèsto. Idealménte 'n file SVG co-în'altéssa ciù bàssa ò pægia a 30px.", + "config-logo-tagline": "Tagline (facoltatîvo):", + "config-logo-tagline-help": "A tagline do tò scîto. Da dêuviâ sôlo se prìmma o l'é stæto definîo o wordmark. Se o no l'é indicâ, o saiâ repigiâ da-o tèsto. L'altéssa de tagline e watermark mìssi insémme a no dêve superâ i 50px.", + "config-logo-sidebar": "Lögo da bâra laterâle (facoltatîvo):", + "config-logo-filedrop": "Strascìnn-a 'n file inmàgine chi", + "config-logo-sidebar-help": "Çèrte skin de MediaWiki inclùddan 'n lögo co-în'altéssa de 160px mìsso inta bâra laterâle. Se o no l'é definîo o saiâ pægio a l'icónn-a za indicâ. Ti poriêsci voéi 'na gràfica dedicâ ch'a métte insémme o wordmark e l'icónn-a.", "config-instantcommons": "Abìlita Instant Commons", "config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] o l'é 'na fonçionalitæ ch'a permétte a-e wiki de dêuviâ inmàgine, soîn e âtri file multimediâli che se trêuvan in sciô scîto [https://commons.wikimedia.org/ Wikimedia Commons].\nPe fâ quésto, MediaWiki a domànda 'na conesción a l'Internétte.\n\nPe ciù informaçioîn in sce 'sta fonçionalitæ chi, con tànto de instruçioîn in sce cómme configurâlo pe de wiki despægie da Wikimedia Commons, consultâ [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos o manoâle].", "config-cc-error": "O seletô de licénse Creative Commons o no l'à dæto nisciùn rizultâto.\nInserìsci a màn o nómme da licénsa.", diff --git a/includes/languages/data/Names.php b/includes/languages/data/Names.php index 811c8af02f69..2e4c94dbf6a3 100644 --- a/includes/languages/data/Names.php +++ b/includes/languages/data/Names.php @@ -316,6 +316,7 @@ class Names { 'mni' => 'ꯃꯤꯇꯩ ꯂꯣꯟ', # Manipuri/Meitei 'mnw' => 'ဘာသာ မန်', # Mon, T201583 'mo' => 'молдовеняскэ', # Moldovan, deprecated (ISO 639-2: ro-Cyrl-MD) + 'mos' => 'moore', # Mooré 'mr' => 'मराठी', # Marathi 'mrh' => 'Mara', # Mara 'mrj' => 'кырык мары', # Hill Mari diff --git a/includes/registration/ExtensionRegistry.php b/includes/registration/ExtensionRegistry.php index 5388f5ebce4c..fb5711945921 100644 --- a/includes/registration/ExtensionRegistry.php +++ b/includes/registration/ExtensionRegistry.php @@ -135,6 +135,11 @@ class ExtensionRegistry { private static $instance; /** + * @var ?BagOStuff + */ + private $cache = null; + + /** * @codeCoverageIgnore * @return ExtensionRegistry */ @@ -147,6 +152,18 @@ class ExtensionRegistry { } /** + * Set the cache to use for extension info. + * Intended for use during testing. + * + * @internal + * + * @param BagOStuff $cache + */ + public function setCache( BagOStuff $cache ): void { + $this->cache = $cache; + } + + /** * @since 1.34 * @param bool $check */ @@ -188,9 +205,13 @@ class ExtensionRegistry { } private function getCache(): BagOStuff { - // Can't call MediaWikiServices here, as we must not cause services - // to be instantiated before extensions have loaded. - return ObjectCache::makeLocalServerCache(); + if ( !$this->cache ) { + // Can't call MediaWikiServices here, as we must not cause services + // to be instantiated before extensions have loaded. + return ObjectCache::makeLocalServerCache(); + } + + return $this->cache; } private function makeCacheKey( BagOStuff $cache, $component, ...$extra ) { @@ -433,7 +454,7 @@ class ExtensionRegistry { } // FIXME: It was a design mistake to handle autoloading separately (T240535) - $data['globals']['wgAutoloadClasses'] = $autoloadClasses; + $data['globals']['wgAutoloadClasses'] = $autoloadClasses; // NOTE: used by Installer! $data['autoloaderPaths'] = $autoloaderPaths; $data['autoloaderNS'] = $autoloadNamespaces; return $data; @@ -452,12 +473,12 @@ class ExtensionRegistry { ) { if ( isset( $info['AutoloadClasses'] ) ) { $autoload = self::processAutoLoader( $dir, $info['AutoloadClasses'] ); - $GLOBALS['wgAutoloadClasses'] += $autoload; + AutoLoader::registerClasses( $autoload ); $autoloadClasses += $autoload; } if ( isset( $info['AutoloadNamespaces'] ) ) { $autoloadNamespaces += self::processAutoLoader( $dir, $info['AutoloadNamespaces'] ); - AutoLoader::$psr4Namespaces += $autoloadNamespaces; + AutoLoader::registerNamespaces( $autoloadNamespaces ); } } @@ -475,12 +496,12 @@ class ExtensionRegistry { ) { if ( isset( $info['TestAutoloadClasses'] ) ) { $autoload = self::processAutoLoader( $dir, $info['TestAutoloadClasses'] ); - $GLOBALS['wgAutoloadClasses'] += $autoload; + AutoLoader::registerClasses( $autoload ); $autoloadClasses += $autoload; } if ( isset( $info['TestAutoloadNamespaces'] ) ) { $autoloadNamespaces += self::processAutoLoader( $dir, $info['TestAutoloadNamespaces'] ); - AutoLoader::$psr4Namespaces += $autoloadNamespaces; + AutoLoader::registerNamespaces( $autoloadNamespaces ); } } @@ -536,7 +557,7 @@ class ExtensionRegistry { } if ( isset( $info['autoloaderNS'] ) ) { - AutoLoader::$psr4Namespaces += $info['autoloaderNS']; + AutoLoader::registerNamespaces( $info['autoloaderNS'] ); } foreach ( $info['defines'] as $name => $val ) { diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index ddfc42d3c8b0..8f272f41db9a 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -27,6 +27,7 @@ use MediaWiki\Revision\RevisionLookup; use MediaWiki\Revision\RevisionStore; use MediaWiki\Skin\SkinComponent; use MediaWiki\Skin\SkinComponentRegistry; +use MediaWiki\Skin\SkinComponentRegistryContext; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserIdentityValue; use Wikimedia\WrappedStringList; @@ -266,7 +267,7 @@ abstract class Skin extends ContextSource { $this->skinname = $name; } $this->componentRegistry = new SkinComponentRegistry( - $this + new SkinComponentRegistryContext( $this ) ); } diff --git a/includes/skins/components/ComponentRegistryContext.php b/includes/skins/components/ComponentRegistryContext.php new file mode 100644 index 000000000000..b1bee4a0816f --- /dev/null +++ b/includes/skins/components/ComponentRegistryContext.php @@ -0,0 +1,42 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +namespace MediaWiki\Skin; + +use Config; +use Title; + +/** + * @internal for use inside Skin and SkinTemplate classes only + * @unstable + */ +interface ComponentRegistryContext { + /** + * Returns the config needed for the component. + * + * @return Config + */ + public function getConfig(): Config; + + /** + * Returns the Title object for the component. + * + * @return Title + */ + public function getTitle(): Title; +} diff --git a/includes/skins/components/SkinComponentRegistry.php b/includes/skins/components/SkinComponentRegistry.php index 50136fa23687..60e1941ea5f1 100644 --- a/includes/skins/components/SkinComponentRegistry.php +++ b/includes/skins/components/SkinComponentRegistry.php @@ -19,7 +19,6 @@ namespace MediaWiki\Skin; use RuntimeException; -use Skin; use SpecialPage; /** @@ -30,14 +29,14 @@ class SkinComponentRegistry { /** @var SkinComponent[]|null null if not initialized. */ private $components = null; - /** @var Skin */ - private $skin; + /** @var SkinComponentRegistryContext */ + private $skinContext; /** - * @param Skin $skin + * @param SkinComponentRegistryContext $skinContext */ - public function __construct( Skin $skin ) { - $this->skin = $skin; + public function __construct( SkinComponentRegistryContext $skinContext ) { + $this->skinContext = $skinContext; } /** @@ -82,27 +81,27 @@ class SkinComponentRegistry { * @throws RuntimeException if given an unknown name */ private function registerComponent( string $name ) { - $skin = $this->skin; + $skin = $this->skinContext; $user = $skin->getUser(); switch ( $name ) { case 'logos': $component = new SkinComponentLogo( - $this->skin->getConfig(), - $this->skin->getLanguage()->getCode() + $skin->getConfig(), + $skin->getLanguage() ); break; case 'search-box': $component = new SkinComponentSearch( $skin->getConfig(), $user, - $skin->getContext(), + $skin->getMessageLocalizer(), SpecialPage::newSearchPage( $user ), $skin->getRelevantTitle() ); break; case 'toc': $component = new SkinComponentTableOfContents( - $this->skin->getOutput() + $skin->getOutput() ); break; default: diff --git a/includes/skins/components/SkinComponentRegistryContext.php b/includes/skins/components/SkinComponentRegistryContext.php new file mode 100644 index 000000000000..26a757944dec --- /dev/null +++ b/includes/skins/components/SkinComponentRegistryContext.php @@ -0,0 +1,95 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +namespace MediaWiki\Skin; + +use Config; +use MessageLocalizer; +use OutputPage; +use Skin; +use Title; +use User; + +/** + * @internal for use inside Skin and SkinTemplate classes only + * @unstable + */ +class SkinComponentRegistryContext implements ComponentRegistryContext { + /** @var Skin */ + private $skin; + + /** @var MessageLocalizer */ + private $localizer; + + /** + * @param Skin $skin + */ + public function __construct( Skin $skin ) { + $this->skin = $skin; + $this->localizer = $skin->getContext(); + } + + /** + * @inheritDoc + */ + public function getConfig(): Config { + return $this->skin->getConfig(); + } + + /** + * @inheritDoc + */ + public function getTitle(): Title { + return $this->skin->getTitle() ?? Title::makeTitle( NS_MAIN, 'Foo' ); + } + + /** + * @return Title|null the "relevant" title - see Skin::getRelevantTitle + */ + public function getRelevantTitle() { + return $this->skin->getRelevantTitle() ?? $this->getTitle(); + } + + /** + * @return OutputPage + */ + public function getOutput(): OutputPage { + return $this->skin->getOutput(); + } + + /** + * @return User + */ + public function getUser() { + return $this->skin->getUser(); + } + + /** + * @return string|null $language + */ + public function getLanguage() { + return $this->skin->getLanguage()->getCode(); + } + + /** + * @return MessageLocalizer + */ + public function getMessageLocalizer(): MessageLocalizer { + return $this->localizer; + } +} |