'KkConverter' => __DIR__ . '/languages/classes/LanguageKk.php',
'KuConverter' => __DIR__ . '/languages/classes/LanguageKu.php',
'LBFactory' => __DIR__ . '/includes/db/loadbalancer/LBFactory.php',
- 'LBFactoryFake' => __DIR__ . '/includes/db/loadbalancer/LBFactoryFake.php',
'LBFactoryMulti' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMulti.php',
'LBFactorySimple' => __DIR__ . '/includes/db/loadbalancer/LBFactorySimple.php',
'LBFactorySingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php',
'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php',
'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php',
+ 'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php',
'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
'MediaWiki\\Linker\\LinkTarget' => __DIR__ . '/includes/linker/LinkTarget.php',
'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . '/includes/debug/logger/monolog/WikiProcessor.php',
'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php',
'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
- 'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php',
+ 'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/Services/DestructibleService.php',
'MediaWiki\\Services\\ServiceContainer' => __DIR__ . '/includes/Services/ServiceContainer.php',
+ 'MediaWiki\\Services\\NoSuchServiceException' => __DIR__ . '/includes/Services/NoSuchServiceException.php',
+ 'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/Services/CannotReplaceActiveServiceException.php',
+ 'MediaWiki\\Services\\ServiceAlreadyDefinedException' => __DIR__ . '/includes/Services/ServiceAlreadyDefinedException.php',
+ 'MediaWiki\\Services\\ServiceDisabledException' => __DIR__ . '/includes/Services/ServiceDisabledException.php',
+ 'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/Services/ContainerDisabledException.php',
'MediaWiki\\Session\\BotPasswordSessionProvider' => __DIR__ . '/includes/session/BotPasswordSessionProvider.php',
'MediaWiki\\Session\\CookieSessionProvider' => __DIR__ . '/includes/session/CookieSessionProvider.php',
'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' => __DIR__ . '/includes/session/ImmutableSessionProviderWithCookie.php',
"psr/log": "1.0.0",
"wikimedia/assert": "0.2.2",
"wikimedia/base-convert": "1.0.1",
- "wikimedia/cdb": "1.3.0",
+ "wikimedia/cdb": "1.4.0",
"wikimedia/cldr-plural-rule-parser": "1.0.0",
"wikimedia/composer-merge-plugin": "1.3.1",
"wikimedia/html-formatter": "1.0.1",
- "wikimedia/ip-set": "1.0.1",
+ "wikimedia/ip-set": "1.1.0",
"wikimedia/php-session-serializer": "1.0.3",
"wikimedia/relpath": "1.0.3",
"wikimedia/running-stat": "1.1.0",
$request: $wgRequest
$mediaWiki: The $mediawiki object
-'MediaWikiServices': Override services in the default MediaWikiServices instance.
-Extensions may use this to define, replace, or wrap existing services.
-However, the preferred way to define a new service is the $wgServiceWiringFiles array.
+'MediaWikiServices': Called when a global MediaWikiServices instance is
+initialized. Extensions may use this to define, replace, or wrap services.
+However, the preferred way to define a new service is
+the $wgServiceWiringFiles array.
$services: MediaWikiServices
'MessageCache::get': When fetching a message. Can be used to override the key
entry points" such as hook handler functions. See "Migration" below.
+== Service Reset ==
+
+Services get their configuration injected, and changes to global
+configuration variables will not have any effect on services that were already
+instantiated. This would typically be the case for low level services like
+the ConfigFactory or the ObjectCacheManager, which are used during extension
+registration. To address this issue, Setup.php resets the global service
+locator instance by calling MediaWikiServices::resetGlobalInstance() once
+configuration and extension registration is complete.
+
+Note that "unmanaged" legacy services services that manage their own singleton
+must not keep references to services managed by MediaWikiServices, to allow a
+clean reset. After the global MediaWikiServices instance got reset, any such
+references would be stale, and using a stale service will result in an error.
+
+Services should either have all dependencies injected and be themselves managed
+by MediaWikiServices, or they should use the Service Locator pattern, accessing
+service instances via the global MediaWikiServices instance state when needed.
+This ensures that no stale service references remain after a reset.
+
+
== Configuration ==
When the default MediaWikiServices instance is created, a Config object is
* Note 2: use $this->getDB() in maintenance scripts that may be invoked by
* updater to ensure that a proper database is being updated.
*
+ * @todo Replace calls to wfGetDB with calls to LoadBalancer::getConnection()
+ * on an injected instance of LoadBalancer.
+ *
* @return DatabaseBase
*/
function wfGetDB( $db, $groups = [], $wiki = false ) {
/**
* Get a load balancer object.
*
+ * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancer()
+ * or MediaWikiServices::getDBLoadBalancerFactory() instead.
+ *
* @param string|bool $wiki Wiki ID, or false for the current wiki
* @return LoadBalancer
*/
function wfGetLB( $wiki = false ) {
- return wfGetLBFactory()->getMainLB( $wiki );
+ if ( $wiki === false ) {
+ return \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer();
+ } else {
+ $factory = \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ return $factory->getMainLB( $wiki );
+ }
}
/**
* Get the load balancer factory object
*
+ * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.
+ *
* @return LBFactory
*/
function wfGetLBFactory() {
- return LBFactory::singleton();
+ return \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
}
/**
<?php
namespace MediaWiki;
+use Config;
use ConfigFactory;
use EventRelayerGroup;
use GlobalVarConfig;
-use Config;
use Hooks;
+use LBFactory;
use Liuggio\StatsdClient\Factory\StatsdDataFactory;
+use LoadBalancer;
use MediaWiki\Services\ServiceContainer;
+use MWException;
+use ResourceLoader;
use SearchEngine;
use SearchEngineConfig;
use SearchEngineFactory;
*/
class MediaWikiServices extends ServiceContainer {
+ /**
+ * @var MediaWikiServices|null
+ */
+ private static $instance = null;
+
/**
* Returns the global default instance of the top level service locator.
*
* @return MediaWikiServices
*/
public static function getInstance() {
- static $instance = null;
-
- if ( $instance === null ) {
+ if ( self::$instance === null ) {
// NOTE: constructing GlobalVarConfig here is not particularly pretty,
// but some information from the global scope has to be injected here,
// even if it's just a file name or database credentials to load
// configuration from.
- $config = new GlobalVarConfig();
- $instance = new self( $config );
+ $bootstrapConfig = new GlobalVarConfig();
+ self::$instance = self::newInstance( $bootstrapConfig );
+ }
- // Load the default wiring from the specified files.
- $wiringFiles = $config->get( 'ServiceWiringFiles' );
- $instance->loadWiringFiles( $wiringFiles );
+ return self::$instance;
+ }
- // Provide a traditional hook point to allow extensions to configure services.
- Hooks::run( 'MediaWikiServices', [ $instance ] );
+ /**
+ * Replaces the global MediaWikiServices instance.
+ *
+ * @note This is for use in PHPUnit tests only!
+ *
+ * @throws MWException if called outside of PHPUnit tests.
+ *
+ * @param MediaWikiServices $services The new MediaWikiServices object.
+ *
+ * @return MediaWikiServices The old MediaWikiServices object, so it can be restored later.
+ */
+ public static function forceGlobalInstance( MediaWikiServices $services ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ throw new MWException( __METHOD__ . ' must not be used outside unit tests.' );
}
+ $old = self::getInstance();
+ self::$instance = $services;
+
+ return $old;
+ }
+
+ /**
+ * Creates a new instance of MediaWikiServices and sets it as the global default
+ * instance. getInstance() will return a different MediaWikiServices object
+ * after every call to resetGlobalServiceLocator().
+ *
+ * @warning This should not be used during normal operation. It is intended for use
+ * when the configuration has changed significantly since bootstrap time, e.g.
+ * during the installation process or during testing.
+ *
+ * @warning Calling resetGlobalServiceLocator() may leave the application in an inconsistent
+ * state. Calling this is only safe under the ASSUMPTION that NO REFERENCE to
+ * any of the services managed by MediaWikiServices exist. If any service objects
+ * managed by the old MediaWikiServices instance remain in use, they may INTERFERE
+ * with the operation of the services managed by the new MediaWikiServices.
+ * Operating with a mix of services created by the old and the new
+ * MediaWikiServices instance may lead to INCONSISTENCIES and even DATA LOSS!
+ * Any class implementing LAZY LOADING is especially prone to this problem,
+ * since instances would typically retain a reference to a storage layer service.
+ *
+ * @see forceGlobalInstance()
+ * @see resetGlobalInstance()
+ * @see resetBetweenTest()
+ *
+ * @param Config|null $bootstrapConfig The Config object to be registered as the
+ * 'BootstrapConfig' service. This has to contain at least the information
+ * needed to set up the 'ConfigFactory' service. If not given, the bootstrap
+ * config of the old instance of MediaWikiServices will be re-used. If there
+ * was no previous instance, a new GlobalVarConfig object will be used to
+ * bootstrap the services.
+ *
+ * @throws MWException If called after MW_SERVICE_BOOTSTRAP_COMPLETE has been defined in
+ * Setup.php (unless MW_PHPUNIT_TEST or MEDIAWIKI_INSTALL or RUN_MAINTENANCE_IF_MAIN
+ * is defined).
+ */
+ public static function resetGlobalInstance( Config $bootstrapConfig = null ) {
+ if ( self::$instance === null ) {
+ // no global instance yet, nothing to reset
+ return;
+ }
+
+ self::failIfResetNotAllowed( __METHOD__ );
+
+ if ( $bootstrapConfig === null ) {
+ $bootstrapConfig = self::$instance->getBootstrapConfig();
+ }
+
+ self::$instance->destroy();
+
+ self::$instance = self::newInstance( $bootstrapConfig );
+ }
+
+ /**
+ * Creates a new MediaWikiServices instance and initializes it according to the
+ * given $bootstrapConfig. In particular, all wiring files defined in the
+ * ServiceWiringFiles setting are loaded, and the MediaWikiServices hook is called.
+ *
+ * @param Config|null $bootstrapConfig The Config object to be registered as the
+ * 'BootstrapConfig' service. This has to contain at least the information
+ * needed to set up the 'ConfigFactory' service. If not provided, any call
+ * to getBootstrapConfig(), getConfigFactory, or getMainConfig will fail.
+ * A MediaWikiServices instance without access to configuration is called
+ * "primordial".
+ *
+ * @return MediaWikiServices
+ * @throws MWException
+ */
+ private static function newInstance( Config $bootstrapConfig ) {
+ $instance = new self( $bootstrapConfig );
+
+ // Load the default wiring from the specified files.
+ $wiringFiles = $bootstrapConfig->get( 'ServiceWiringFiles' );
+ $instance->loadWiringFiles( $wiringFiles );
+
+ // Provide a traditional hook point to allow extensions to configure services.
+ Hooks::run( 'MediaWikiServices', [ $instance ] );
+
return $instance;
}
+ /**
+ * Disables all storage layer services. After calling this, any attempt to access the
+ * storage layer will result in an error. Use resetGlobalInstance() to restore normal
+ * operation.
+ *
+ * @warning This is intended for extreme situations only and should never be used
+ * while serving normal web requests. Legitimate use cases for this method include
+ * the installation process. Test fixtures may also use this, if the fixture relies
+ * on globalState.
+ *
+ * @see resetGlobalInstance()
+ * @see resetChildProcessServices()
+ */
+ public static function disableStorageBackend() {
+ // TODO: also disable some Caches, JobQueues, etc
+ $destroy = [ 'DBLoadBalancer', 'DBLoadBalancerFactory' ];
+ $services = self::getInstance();
+
+ foreach ( $destroy as $name ) {
+ $services->disableService( $name );
+ }
+ }
+
+ /**
+ * Resets any services that may have become stale after a child process
+ * returns from after pcntl_fork(). It's also safe, but generally unnecessary,
+ * to call this method from the parent process.
+ *
+ * @note This is intended for use in the context of process forking only!
+ *
+ * @see resetGlobalInstance()
+ * @see disableStorageBackend()
+ */
+ public static function resetChildProcessServices() {
+ // NOTE: for now, just reset everything. Since we don't know the interdependencies
+ // between services, we can't do this more selectively at this time.
+ self::resetGlobalInstance();
+
+ // Child, reseed because there is no bug in PHP:
+ // http://bugs.php.net/bug.php?id=42465
+ mt_srand( getmypid() );
+ }
+
+ /**
+ * Resets the given service for testing purposes.
+ *
+ * @warning This is generally unsafe! Other services may still retain references
+ * to the stale service instance, leading to failures and inconsistencies. Subclasses
+ * may use this method to reset specific services under specific instances, but
+ * it should not be exposed to application logic.
+ *
+ * @note With proper dependency injection used throughout the codebase, this method
+ * should not be needed. It is provided to allow tests that pollute global service
+ * instances to clean up.
+ *
+ * @param string $name
+ * @param string $destroy Whether the service instance should be destroyed if it exists.
+ * When set to false, any existing service instance will effectively be detached
+ * from the container.
+ *
+ * @throws MWException if called outside of PHPUnit tests.
+ */
+ public function resetServiceForTesting( $name, $destroy = true ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
+ throw new MWException( 'resetServiceForTesting() must not be used outside unit tests.' );
+ }
+
+ $this->resetService( $name, $destroy );
+ }
+
+ /**
+ * Convenience method that throws an exception unless it is called during a phase in which
+ * resetting of global services is allowed. In general, services should not be reset
+ * individually, since that may introduce inconsistencies.
+ *
+ * This method will throw an exception if:
+ *
+ * - self::$resetInProgress is false (to allow all services to be reset together
+ * via resetGlobalInstance)
+ * - and MEDIAWIKI_INSTALL is not defined (to allow services to be reset during installation)
+ * - and MW_PHPUNIT_TEST is not defined (to allow services to be reset during testing)
+ *
+ * This method is intended to be used to safeguard against accidentally resetting
+ * global service instances that are not yet managed by MediaWikiServices. It is
+ * defined here in the MediaWikiServices services class to have a central place
+ * for managing service bootstrapping and resetting.
+ *
+ * @param string $method the name of the caller method, as given by __METHOD__.
+ *
+ * @throws MWException if called outside bootstrap mode.
+ *
+ * @see resetGlobalInstance()
+ * @see forceGlobalInstance()
+ * @see disableStorageBackend()
+ */
+ public static function failIfResetNotAllowed( $method ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' )
+ && !defined( 'MW_PARSER_TEST' )
+ && !defined( 'MEDIAWIKI_INSTALL' )
+ && !defined( 'RUN_MAINTENANCE_IF_MAIN' )
+ && defined( 'MW_SERVICE_BOOTSTRAP_COMPLETE' )
+ ) {
+ throw new MWException( $method . ' may only be called during bootstrapping and unit tests!' );
+ }
+ }
+
/**
* @param Config $config The Config object to be registered as the 'BootstrapConfig' service.
* This has to contain at least the information needed to set up the 'ConfigFactory'
public function __construct( Config $config ) {
parent::__construct();
- // register the given Config object as the bootstrap config service.
+ // Register the given Config object as the bootstrap config service.
$this->defineService( 'BootstrapConfig', function() use ( $config ) {
return $config;
} );
}
+ // CONVENIENCE GETTERS ////////////////////////////////////////////////////
+
/**
* Returns the Config object containing the bootstrap configuration.
* Bootstrap configuration would typically include database credentials
return $this->getService( 'SkinFactory' );
}
+ /**
+ * @return LBFactory
+ */
+ public function getDBLoadBalancerFactory() {
+ return $this->getService( 'DBLoadBalancerFactory' );
+ }
+
+ /**
+ * @return LoadBalancer The main DB load balancer for the local wiki.
+ */
+ public function getDBLoadBalancer() {
+ return $this->getService( 'DBLoadBalancer' );
+ }
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service getter here, don't forget to add a test
// case for it in MediaWikiServicesTest::provideGetters() and in
use MediaWiki\MediaWikiServices;
return [
+ 'DBLoadBalancerFactory' => function( MediaWikiServices $services ) {
+ $config = $services->getMainConfig()->get( 'LBFactoryConf' );
+
+ $class = LBFactory::getLBFactoryClass( $config );
+ if ( !isset( $config['readOnlyReason'] ) ) {
+ // TODO: replace the global wfConfiguredReadOnlyReason() with a service.
+ $config['readOnlyReason'] = wfConfiguredReadOnlyReason();
+ }
+
+ return new $class( $config );
+ },
+
+ 'DBLoadBalancer' => function( MediaWikiServices $services ) {
+ // just return the default LB from the DBLoadBalancerFactory service
+ return $services->getDBLoadBalancerFactory()->getMainLB();
+ },
+
'SiteStore' => function( MediaWikiServices $services ) {
- $loadBalancer = wfGetLB(); // TODO: use LB from MediaWikiServices
- $rawSiteStore = new DBSiteStore( $loadBalancer );
+ $rawSiteStore = new DBSiteStore( $services->getDBLoadBalancer() );
// TODO: replace wfGetCache with a CacheFactory service.
// TODO: replace wfIsHHVM with a capabilities service.
},
'SkinFactory' => function( MediaWikiServices $services ) {
- return new SkinFactory();
+ $factory = new SkinFactory();
+
+ $names = $services->getMainConfig()->get( 'ValidSkinNames' );
+
+ foreach ( $names as $name => $skin ) {
+ $factory->register( $name, $skin, function () use ( $name, $skin ) {
+ $class = "Skin$skin";
+ return new $class( $name );
+ } );
+ }
+ // Register a hidden "fallback" skin
+ $factory->register( 'fallback', 'Fallback', function () {
+ return new SkinFallback;
+ } );
+ // Register a hidden skin for api output
+ $factory->register( 'apioutput', 'ApiOutput', function () {
+ return new SkinApi;
+ } );
+
+ return $factory;
},
///////////////////////////////////////////////////////////////////////////
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to replace an already active service.
+ *
+ * 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
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to replace an already active service.
+ */
+class CannotReplaceActiveServiceException extends RuntimeException {
+
+ /**
+ * @param string $serviceName
+ * @param Exception|null $previous
+ */
+ public function __construct( $serviceName, Exception $previous = null ) {
+ parent::__construct( "Cannot replace an active service: $serviceName", 0, $previous );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to access a service on a disabled container or factory.
+ *
+ * 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
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to access a service on a disabled container or factory.
+ */
+class ContainerDisabledException extends RuntimeException {
+
+ /**
+ * @param Exception|null $previous
+ */
+ public function __construct( Exception $previous = null ) {
+ parent::__construct( 'Container disabled!', 0, $previous );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+/**
+ * Interface for destructible services.
+ *
+ * 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
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * DestructibleService defines a standard interface for shutting down a service instance.
+ * The intended use is for a service container to be able to shut down services that should
+ * no longer be used, and allow such services to release any system resources.
+ *
+ * @note There is no expectation that services will be destroyed when the process (or web request)
+ * terminates.
+ */
+interface DestructibleService {
+
+ /**
+ * Notifies the service object that it should expect to no longer be used, and should release
+ * any system resources it may own. The behavior of all service methods becomes undefined after
+ * destroy() has been called. It is recommended that implementing classes should throw an
+ * exception when service methods are accessed after destroy() has been called.
+ */
+ public function destroy();
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when the requested service is not known.
+ *
+ * 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
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when the requested service is not known.
+ */
+class NoSuchServiceException extends RuntimeException {
+
+ /**
+ * @param string $serviceName
+ * @param Exception|null $previous
+ */
+ public function __construct( $serviceName, Exception $previous = null ) {
+ parent::__construct( "No such service: $serviceName", 0, $previous );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when a service was already defined, but the
+ * caller expected it to not exist.
+ *
+ * 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
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when a service was already defined, but the
+ * caller expected it to not exist.
+ */
+class ServiceAlreadyDefinedException extends RuntimeException {
+
+ /**
+ * @param string $serviceName
+ * @param Exception|null $previous
+ */
+ public function __construct( $serviceName, Exception $previous = null ) {
+ parent::__construct( "Service already defined: $serviceName", 0, $previous );
+ }
+
+}
* @see docs/injection.txt for an overview of using dependency injection in the
* MediaWiki code base.
*/
-class ServiceContainer {
+class ServiceContainer implements DestructibleService {
/**
* @var object[]
*/
private $extraInstantiationParams;
+ /**
+ * @var boolean
+ */
+ private $destroyed = false;
+
/**
* @param array $extraInstantiationParams Any additional parameters to be passed to the
* instantiator function when creating a service. This is typically used to provide
$this->extraInstantiationParams = $extraInstantiationParams;
}
+ /**
+ * Destroys all contained service instances that implement the DestructibleService
+ * interface. This will render all services obtained from this MediaWikiServices
+ * instance unusable. In particular, this will disable access to the storage backend
+ * via any of these services. Any future call to getService() will throw an exception.
+ *
+ * @see resetGlobalInstance()
+ */
+ public function destroy() {
+ foreach ( $this->getServiceNames() as $name ) {
+ $service = $this->peekService( $name );
+ if ( $service !== null && $service instanceof DestructibleService ) {
+ $service->destroy();
+ }
+ }
+
+ $this->destroyed = true;
+ }
+
/**
* @param array $wiringFiles A list of PHP files to load wiring information from.
* Each file is loaded using PHP's include mechanism. Each file is expected to
return isset( $this->serviceInstantiators[$name] );
}
+ /**
+ * Returns the service instance for $name only if that service has already been instantiated.
+ * This is intended for situations where services get destroyed/cleaned up, so we can
+ * avoid creating a service just to destroy it again.
+ *
+ * @note This is intended for internal use and for test fixtures.
+ * Application logic should use getService() instead.
+ *
+ * @see getService().
+ *
+ * @param string $name
+ *
+ * @return object|null The service instance, or null if the service has not yet been instantiated.
+ * @throws RuntimeException if $name does not refer to a known service.
+ */
+ public function peekService( $name ) {
+ if ( !$this->hasService( $name ) ) {
+ throw new NoSuchServiceException( $name );
+ }
+
+ return isset( $this->services[$name] ) ? $this->services[$name] : null;
+ }
+
/**
* @return string[]
*/
Assert::parameterType( 'string', $name, '$name' );
if ( $this->hasService( $name ) ) {
- throw new RuntimeException( 'Service already defined: ' . $name );
+ throw new ServiceAlreadyDefinedException( $name );
}
$this->serviceInstantiators[$name] = $instantiator;
Assert::parameterType( 'string', $name, '$name' );
if ( !$this->hasService( $name ) ) {
- throw new RuntimeException( 'Service not defined: ' . $name );
+ throw new NoSuchServiceException( $name );
}
if ( isset( $this->services[$name] ) ) {
- throw new RuntimeException( 'Cannot redefine a service that is already in use: ' . $name );
+ throw new CannotReplaceActiveServiceException( $name );
}
$this->serviceInstantiators[$name] = $instantiator;
}
+ /**
+ * Disables a service.
+ *
+ * @note Attempts to call getService() for a disabled service will result
+ * in a DisabledServiceException. Calling peekService for a disabled service will
+ * return null. Disabled services are listed by getServiceNames(). A disabled service
+ * can be enabled again using redefineService().
+ *
+ * @note If the service was already active (that is, instantiated) when getting disabled,
+ * and the service instance implements DestructibleService, destroy() is called on the
+ * service instance.
+ *
+ * @see redefineService()
+ * @see resetService()
+ *
+ * @param string $name The name of the service to disable.
+ *
+ * @throws RuntimeException if $name is not a known service.
+ */
+ public function disableService( $name ) {
+ $this->resetService( $name );
+
+ $this->redefineService( $name, function() use ( $name ) {
+ throw new ServiceDisabledException( $name );
+ } );
+ }
+
+ /**
+ * Resets a service by dropping the service instance.
+ * If the service instances implements DestructibleService, destroy()
+ * is called on the service instance.
+ *
+ * @warning This is generally unsafe! Other services may still retain references
+ * to the stale service instance, leading to failures and inconsistencies. Subclasses
+ * may use this method to reset specific services under specific instances, but
+ * it should not be exposed to application logic.
+ *
+ * @note This is declared final so subclasses can not interfere with the expectations
+ * disableService() has when calling resetService().
+ *
+ * @see redefineService()
+ * @see disableService().
+ *
+ * @param string $name The name of the service to reset.
+ * @param bool $destroy Whether the service instance should be destroyed if it exists.
+ * When set to false, any existing service instance will effectively be detached
+ * from the container.
+ *
+ * @throws RuntimeException if $name is not a known service.
+ */
+ final protected function resetService( $name, $destroy = true ) {
+ Assert::parameterType( 'string', $name, '$name' );
+
+ $instance = $this->peekService( $name );
+
+ if ( $destroy && $instance instanceof DestructibleService ) {
+ $instance->destroy();
+ }
+
+ unset( $this->services[$name] );
+ }
+
/**
* Returns a service object of the kind associated with $name.
* Services instances are instantiated lazily, on demand.
*
* @param string $name The service name
*
- * @throws InvalidArgumentException if $name is not a known service.
+ * @throws NoSuchServiceException if $name is not a known service.
+ * @throws ServiceDisabledException if this container has already been destroyed.
+ *
* @return object The service instance
*/
public function getService( $name ) {
+ if ( $this->destroyed ) {
+ throw new ContainerDisabledException();
+ }
+
if ( !isset( $this->services[$name] ) ) {
$this->services[$name] = $this->createService( $name );
}
array_merge( [ $this ], $this->extraInstantiationParams )
);
} else {
- throw new InvalidArgumentException( 'Unknown service: ' . $name );
+ throw new NoSuchServiceException( $name );
}
return $service;
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to access a disabled service.
+ *
+ * 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
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to access a disabled service.
+ */
+class ServiceDisabledException extends RuntimeException {
+
+ /**
+ * @param string $serviceName
+ * @param Exception|null $previous
+ */
+ public function __construct( $serviceName, Exception $previous = null ) {
+ parent::__construct( "Service disabled: $serviceName", 0, $previous );
+ }
+
+}
*
* @file
*/
+use MediaWiki\MediaWikiServices;
/**
* This file is not a valid entry point, perform no further processing unless
$wgSkipSkins[] = $wgSkipSkin;
}
-// Register skins
-// Use a closure to avoid leaking into global state
-call_user_func( function () use ( $wgValidSkinNames ) {
- $factory = SkinFactory::getDefaultInstance();
- foreach ( $wgValidSkinNames as $name => $skin ) {
- $factory->register( $name, $skin, function () use ( $name, $skin ) {
- $class = "Skin$skin";
- return new $class( $name );
- } );
- }
- // Register a hidden "fallback" skin
- $factory->register( 'fallback', 'Fallback', function () {
- return new SkinFallback;
- } );
- // Register a hidden skin for api output
- $factory->register( 'apioutput', 'ApiOutput', function () {
- return new SkinApi;
- } );
-} );
$wgSkipSkins[] = 'fallback';
$wgSkipSkins[] = 'apioutput';
require_once "$IP/includes/AutoLoader.php";
}
+// Reset the global service locator, so any services that have already been created will be
+// re-created while taking into account any custom settings and extensions.
+MediaWikiServices::resetGlobalInstance( new GlobalVarConfig() );
+
+// Define a constant that indicates that the bootstrapping of the service locator
+// is complete.
+define( 'MW_SERVICE_BOOTSTRAP_COMPLETE', 1 );
+
// Install a header callback to prevent caching of responses with cookies (T127993)
if ( !$wgCommandLineMode ) {
header_register_callback( function () {
"Kurousagi",
"Revi",
"Yearning",
- "Priviet"
+ "Priviet",
+ "Ykhwong"
]
},
"apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|설명문서]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 메일링 리스트]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API 알림 사항]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 버그 및 요청]\n</div>\n<strong>상태:</strong> 이 페이지에 보여지는 모든 기능은 정상적으로 작동하지만, API는 여전히 활발하게 개발되고 있으며, 언제든지 변경될 수 있습니다. 업데이트 공지를 받아보려면 [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 메일링 리스트]를 구독하십시오.\n\n<strong>잘못된 요청:</strong> API에 잘못된 요청이 전송되면 HTTP 헤더에서 \"MediaWiki-API-Error\" 키를 보내고, 헤더 값과 오류 코드가 같게 설정됩니다. 자세한 정보에 대해서는 [[mw:API:Errors_and_warnings|API:오류와 경고]]를 참조하십시오.\n\n<strong>테스트하기:</strong> API 요청을 테스트의 편의를 위해, [[Special:ApiSandbox]]를 보세요.",
"apihelp-createaccount-example-pass": "사용자 <kbd>testuser</kbd>를 만들고 비밀번호를 <kbd>test123</kbd>으로 설정합니다.",
"apihelp-createaccount-example-mail": "사용자 <kbd>testmailuser</kbd>를 만들고 자동 생성된 비밀번호를 이메일로 보냅니다.",
"apihelp-delete-description": "문서 삭제",
+ "apihelp-delete-param-pageid": "삭제할 문서의 ID. <var>$1title</var>과 함께 사용할 수 없습니다.",
+ "apihelp-delete-param-reason": "삭제의 이유. 설정하지 않으면 자동 생성되는 이유를 사용합니다.",
"apihelp-delete-param-unwatch": "문서를 현재 사용자의 주시문서 목록에서 제거합니다.",
"apihelp-delete-example-simple": "<kbd>Main Page</kbd>를 삭제합니다.",
"apihelp-disabled-description": "이 모듈은 해제되었습니다.",
"apihelp-feedcontributions-param-deletedonly": "삭제된 기여만 봅니다.",
"apihelp-feedcontributions-param-toponly": "최신 판인 편집만 봅니다.",
"apihelp-feedrecentchanges-param-feedformat": "피드 포맷.",
+ "apihelp-feedrecentchanges-param-invert": "선택한 항목을 제외한 모든 이름공간.",
"apihelp-feedrecentchanges-param-hideminor": "사소한 편집을 숨깁니다.",
"apihelp-feedrecentchanges-param-hidebots": "봇의 편집을 숨깁니다.",
"apihelp-feedrecentchanges-param-hideanons": "익명 사용자의 편집을 숨깁니다.",
"apihelp-feedrecentchanges-example-30days": "30일간의 최근 바뀜을 봅니다.",
"apihelp-filerevert-description": "파일을 이전 판으로 되돌립니다.",
"apihelp-filerevert-example-revert": "<kbd>Wiki.png</kbd>를 <kbd>2011-03-05T15:27:40Z</kbd> 판으로 되돌립니다.",
+ "apihelp-help-param-helpformat": "도움말 출력 포맷.",
"apihelp-import-param-xml": "업로드한 XML 파일.",
"apihelp-login-param-name": "계정 이름.",
"apihelp-login-param-password": "비밀번호.",
"apihelp-login-param-domain": "도메인 (선택).",
"apihelp-login-example-login": "로그인.",
+ "apihelp-mergehistory-param-reason": "문서 병합 이유.",
"apihelp-move-description": "문서 이동하기.",
"apihelp-move-param-reason": "제목을 변경하는 이유",
"apihelp-move-param-movetalk": "토론 문서가 존재한다면, 토론 문서도 이름을 변경해주세요.",
"apihelp-opensearch-description": "OpenSearch 프로토콜을 이용하여 위키 검색하기",
"apihelp-opensearch-param-search": "문자열 검색",
"apihelp-opensearch-param-limit": "반환할 결과의 최대 수",
+ "apihelp-opensearch-param-format": "출력 포맷.",
"apihelp-options-param-reset": "사이트 기본으로 설정 초기화",
"apihelp-options-example-reset": "모든 설정 초기화",
+ "apihelp-paraminfo-param-helpformat": "도움말 문자열 포맷.",
+ "apihelp-protect-param-reason": "보호 또는 보호 해제의 이유.",
"apihelp-protect-example-protect": "문서 보호",
"apihelp-query+allmessages-example-ipb": "<kbd>ipb-</kbd>로 시작하는 메시지를 보입니다.",
"apihelp-query+allrevisions-description": "모든 판 표시.",
"apihelp-mergehistory-param-fromid": "将被合并历史的页面的页面ID。不能与<var>$1from</var>一起使用。",
"apihelp-mergehistory-param-to": "将要合并历史的页面的标题。不能与<var>$1toid</var>一起使用。",
"apihelp-mergehistory-param-toid": "将要合并历史的页面的页面ID。不能与<var>$1to</var>一起使用。",
+ "apihelp-mergehistory-param-timestamp": "指定时间戳,决定源页面的哪些修订历史被移动到目标页面的历史中。如果省略,源页面的所有历史记录都将被合并到目标页面。",
"apihelp-mergehistory-param-reason": "历史合并的原因。",
"apihelp-mergehistory-example-merge": "将<kbd>Oldpage</kbd>的完整历史合并至<kbd>Newpage</kbd>。",
"apihelp-mergehistory-example-merge-timestamp": "将<kbd>Oldpage</kbd>直到<kbd>2015-12-31T04:37:41Z</kbd>的页面修订版本合并至<kbd>Newpage</kbd>。",
"apihelp-query+pageswithprop-param-dir": "排序的方向。",
"apihelp-query+pageswithprop-example-simple": "列出前10个使用<code>{{DISPLAYTITLE:}}</code>的页面。",
"apihelp-query+pageswithprop-example-generator": "获取有关前10个使用<code>__NOTOC__</code>的页面的额外信息。",
- "apihelp-query+prefixsearch-description": "为页面标题执行前缀搜索。\n\nDespite the similarity in names, this module is not intended to be equivalent to [[Special:PrefixIndex]]; for that, see <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> with the <kbd>apprefix</kbd> parameter. The purpose of this module is similar to <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>: to take user input and provide the best-matching titles. Depending on the search engine backend, this might include typo correction, redirect avoidance, or other heuristics.",
+ "apihelp-query+prefixsearch-description": "执行页面标题的带前缀搜索。\n\n尽管名称类似,但此模块不等于[[Special:PrefixIndex]];详见<kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>中的<kbd>apprefix</kbd>参数。此模块的目的类似<kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>:基于用户的输入提供最佳匹配的标题。取决于搜索引擎后端,这可能包括错拼纠正、避免重定向和其他启发性行为。",
"apihelp-query+prefixsearch-param-search": "搜索字符串。",
"apihelp-query+prefixsearch-param-namespace": "搜索的名字空间。",
"apihelp-query+prefixsearch-param-limit": "要返回的结果最大数。",
}
/**
- * Register a new config factory function
- * Will override if it's already registered
+ * @return string[]
+ */
+ public function getConfigNames() {
+ return array_keys( $this->factoryFunctions );
+ }
+
+ /**
+ * Register a new config factory function.
+ * Will override if it's already registered.
+ * Use "*" for $name to provide a fallback config for all unknown names.
* @param string $name
- * @param callable $callback That takes this ConfigFactory as an argument
+ * @param callable|Config $callback A factory callabck that takes this ConfigFactory
+ * as an argument and returns a Config instance, or an existing Config instance.
* @throws InvalidArgumentException If an invalid callback is provided
*/
public function register( $name, $callback ) {
+ if ( $callback instanceof Config ) {
+ $instance = $callback;
+
+ // Register a callback anyway, for consistency. Note that getConfigNames()
+ // relies on $factoryFunctions to have all config names.
+ $callback = function() use ( $instance ) {
+ return $instance;
+ };
+ } else {
+ $instance = null;
+ }
+
if ( !is_callable( $callback ) ) {
throw new InvalidArgumentException( 'Invalid callback provided' );
}
+
+ $this->configs[$name] = $instance;
$this->factoryFunctions[$name] = $callback;
}
*/
public function makeConfig( $name ) {
if ( !isset( $this->configs[$name] ) ) {
- if ( !isset( $this->factoryFunctions[$name] ) ) {
+ $key = $name;
+ if ( !isset( $this->factoryFunctions[$key] ) ) {
+ $key = '*';
+ }
+ if ( !isset( $this->factoryFunctions[$key] ) ) {
throw new ConfigException( "No registered builder available for $name." );
}
- $conf = call_user_func( $this->factoryFunctions[$name], $this );
+ $conf = call_user_func( $this->factoryFunctions[$key], $this );
if ( $conf instanceof Config ) {
$this->configs[$name] = $conf;
} else {
return $this->configs[$name];
}
+
}
* @ingroup Database
*/
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Services\DestructibleService;
use Psr\Log\LoggerInterface;
use MediaWiki\Logger\LoggerFactory;
* An interface for generating database load balancers
* @ingroup Database
*/
-abstract class LBFactory {
+abstract class LBFactory implements DestructibleService {
+
/** @var ChronologyProtector */
protected $chronProt;
/** @var LoggerInterface */
protected $logger;
- /** @var LBFactory */
- private static $instance;
-
/** @var string|bool Reason all LBs are read-only or false if not */
protected $readOnlyReason = false;
$this->logger = LoggerFactory::getInstance( 'DBTransaction' );
}
+ /**
+ * Disables all load balancers. All connections are closed, and any attempt to
+ * open a new connection will result in a DBAccessError.
+ * @see LoadBalancer::disable()
+ */
+ public function destroy() {
+ $this->shutdown();
+ $this->forEachLBCallMethod( 'disable' );
+ }
+
/**
* Disables all access to the load balancer, will cause all database access
* to throw a DBAccessError
*/
public static function disableBackend() {
- global $wgLBFactoryConf;
- self::$instance = new LBFactoryFake( $wgLBFactoryConf );
+ MediaWikiServices::disableStorageBackend();
}
/**
* Get an LBFactory instance
*
+ * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.
+ *
* @return LBFactory
*/
public static function singleton() {
- global $wgLBFactoryConf;
-
- if ( is_null( self::$instance ) ) {
- $class = self::getLBFactoryClass( $wgLBFactoryConf );
- $config = $wgLBFactoryConf;
- if ( !isset( $config['readOnlyReason'] ) ) {
- $config['readOnlyReason'] = wfConfiguredReadOnlyReason();
- }
- self::$instance = new $class( $config );
- }
-
- return self::$instance;
+ return MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
}
/**
* Returns the LBFactory class to use and the load balancer configuration.
*
+ * @todo instead of this, use a ServiceContainer for managing the different implementations.
+ *
* @param array $config (e.g. $wgLBFactoryConf)
* @return string Class name
*/
/**
* Shut down, close connections and destroy the cached instance.
- */
- public static function destroyInstance() {
- if ( self::$instance ) {
- self::$instance->shutdown();
- self::$instance->forEachLBCallMethod( 'closeAll' );
- self::$instance = null;
- }
- }
-
- /**
- * Set the instance to be the given object
*
- * @param LBFactory $instance
+ * @deprecated since 1.27, use LBFactory::destroy()
*/
- public static function setInstance( $instance ) {
- self::destroyInstance();
- self::$instance = $instance;
+ public static function destroyInstance() {
+ self::singleton()->destroy();
}
/**
class DBAccessError extends MWException {
public function __construct() {
parent::__construct( "Mediawiki tried to access the database via wfGetDB(). " .
- "This is not allowed." );
+ "This is not allowed, because database access has been disabled." );
}
}
/** @var integer Max time to wait for a slave to catch up (e.g. ChronologyProtector) */
const POS_WAIT_TIMEOUT = 10;
+ /**
+ * @var boolean
+ */
+ private $disabled = false;
+
/**
* @param array $params Array with keys:
* - servers : Required. Array of server info structures.
* On error, returns false, and the connection which caused the
* error will be available via $this->mErrorConnection.
*
+ * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
+ *
* @param int $i Server index
* @param string|bool $wiki Wiki ID, or false for the current wiki
* @return DatabaseBase|bool Returns false on errors
* On error, returns false, and the connection which caused the
* error will be available via $this->mErrorConnection.
*
+ * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
+ *
* @param int $i Server index
* @param string $wiki Wiki ID to open
* @return DatabaseBase
* @return DatabaseBase
*/
protected function reallyOpenConnection( $server, $dbNameOverride = false ) {
+ if ( $this->disabled ) {
+ throw new DBAccessError();
+ }
+
if ( !is_array( $server ) ) {
throw new MWException( 'You must update your load-balancing configuration. ' .
'See DefaultSettings.php entry for $wgDBservers.' );
return false;
}
+ /**
+ * Disable this load balancer. All connections are closed, and any attempt to
+ * open a new connection will result in a DBAccessError.
+ *
+ * @since 1.27
+ */
+ public function disable() {
+ $this->closeAll();
+ $this->disabled = true;
+ }
+
/**
* Close all open connections
*/
if ( !$status->isOK() ) {
throw new MWException( __METHOD__ . ': unexpected DB connection error' );
}
- LBFactory::setInstance( new LBFactorySingle( [
- 'connection' => $status->value ] ) );
+
+ \MediaWiki\MediaWikiServices::resetGlobalInstance();
+ $services = \MediaWiki\MediaWikiServices::getInstance();
+
+ $connection = $status->value;
+ $services->redefineService( 'DBLoadBalancerFactory', function() use ( $connection ) {
+ return new LBFactorySingle( [
+ 'connection' => $connection ] );
+ } );
+
}
/**
*/
abstract public function showStatusMessage( Status $status );
+ /**
+ * Constructs a Config object that contains configuration settings that should be
+ * overwritten for the installation process.
+ *
+ * @since 1.27
+ *
+ * @param Config $baseConfig
+ *
+ * @return Config The config to use during installation.
+ */
+ public static function getInstallerConfig( Config $baseConfig ) {
+ $configOverrides = new HashConfig();
+
+ // disable (problematic) object cache types explicitly, preserving all other (working) ones
+ // bug T113843
+ $emptyCache = [ 'class' => 'EmptyBagOStuff' ];
+
+ $objectCaches = [
+ CACHE_NONE => $emptyCache,
+ CACHE_DB => $emptyCache,
+ CACHE_ANYTHING => $emptyCache,
+ CACHE_MEMCACHED => $emptyCache,
+ ] + $baseConfig->get( 'ObjectCaches' );
+
+ $configOverrides->set( 'ObjectCaches', $objectCaches );
+
+ // Load the installer's i18n.
+ $messageDirs = $baseConfig->get( 'MessagesDirs' );
+ $messageDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
+
+ $configOverrides->set( 'MessagesDirs', $messageDirs );
+
+ return new MultiConfig( [ $configOverrides, $baseConfig ] );
+ }
+
/**
* Constructor, always call this from child classes.
*/
public function __construct() {
- global $wgMessagesDirs, $wgUser;
+ global $wgMemc, $wgUser;
+
+ $defaultConfig = new GlobalVarConfig(); // all the stuff from DefaultSettings.php
+ $installerConfig = self::getInstallerConfig( $defaultConfig );
+
+ // Reset all services and inject config overrides
+ MediaWiki\MediaWikiServices::resetGlobalInstance( $installerConfig );
// Don't attempt to load user language options (T126177)
// This will be overridden in the web installer with the user-specified language
RequestContext::getMain()->setLanguage( 'en' );
// Disable the i18n cache
+ // TODO: manage LocalisationCache singleton in MediaWikiServices
Language::getLocalisationCache()->disableBackend();
- // Disable LoadBalancer and wfGetDB etc.
- LBFactory::disableBackend();
+
+ // Disable all global services, since we don't have any configuration yet!
+ MediaWiki\MediaWikiServices::disableStorageBackend();
// Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
// SqlBagOStuff will then throw since we just disabled wfGetDB)
- $GLOBALS['wgMemc'] = new EmptyBagOStuff;
- ObjectCache::clear();
- $emptyCache = [ 'class' => 'EmptyBagOStuff' ];
- // disable (problematic) object cache types explicitly, preserving all other (working) ones
- // bug T113843
- $GLOBALS['wgObjectCaches'] = [
- CACHE_NONE => $emptyCache,
- CACHE_DB => $emptyCache,
- CACHE_ANYTHING => $emptyCache,
- CACHE_MEMCACHED => $emptyCache,
- ] + $GLOBALS['wgObjectCaches'];
-
- // Load the installer's i18n.
- $wgMessagesDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
+ $wgMemc = ObjectCache::getInstance( CACHE_NONE );
// Having a user with id = 0 safeguards us from DB access via User::loadOptions().
$wgUser = User::newFromId( 0 );
"config-ctype": "<strong>Lỗi chí tử:</strong> PHP phải được biên dịch với hỗ trợ cho [http://www.php.net/manual/en/ctype.installation.php phần mở rộng Ctype].",
"config-iconv": "<strong>Lỗi chí tử:</strong> PHP phải được biên dịch với hỗ trợ cho [http://www.php.net/manual/en/iconv.installation.php phần mở rộng iconv].",
"config-json": "<strong>Lỗi chí tử:</strong> PHP được biên dịch mà không có hỗ trợ cho JSON.\nBạn phải cài đặt hoặc phần mở rộng JSON PHP hoặc phần mở rộng [http://pecl.php.net/package/jsonc PECL jsonc] trước khi cài đặt MediaWiki.\n* Phần mở rộng PHP có sẵn trong Red Hat Enterprise Linux (CentOS) 5 và 6 nhưng phải được kích hoạt trong <code>/etc/php.ini</code> hoặc <code>/etc/php.d/json.ini</code>.\n* Một số phiên bản Linux được phát hành sau tháng 5 năm 2013 bỏ qua phần mở rộng PHP và gói lại phần mở rộng PECL là <code>php5-json</code> hoặc <code>php-pecl-jsonc</code> thay thế.",
+ "config-mbstring-absent": "<strong>Lỗi chí tử:</strong> PHP phải được biên dịch với hỗ trợ cho [http://www.php.net/manual/en/mbstring.setup.php phần mở rộng mbstring].",
"config-xcache": "[http://xcache.lighttpd.net/ XCache] đã được cài đặt",
"config-apc": "[http://www.php.net/apc APC] đã được cài đặt",
"config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] đã được cài đặt",
"listgrouprights-namespaceprotection-restrictedto": "Правы, якія дазваляюць удзельніку рэдагаваць",
"listgrants": "Дазволы",
"listgrants-grant": "Дазвол",
+ "listgrants-rights": "Правы",
"trackingcategories": "Катэгорыі, якія патрабуюць увагі",
"trackingcategories-summary": "На гэтай старонцы пералічаныя катэгорыя, які патрабуюць увагі і якія аўтаматычна запаўняюцца праграмным забесьяпчэньнем MediaWiki. Іх назвы могуць быць зьмененыя рэдагаваньнем сыстэмных паведамленьняў у прасторы назваў {{ns:8}}.",
"trackingcategories-msg": "Катэгорыя, якая патрабуе ўвагі",
"tags-edit-chosen-no-results": "Не знойдзена бірак, якія б адпавядалі запыту",
"tags-edit-reason": "Прычына:",
"tags-edit-nooldid-title": "Недапушчальная мэтавая версія",
+ "tags-edit-nooldid-text": "Вы або не пазначылі мэтавую версію для выканання гэтай функцыі, або пазначаная версія не існуе.",
+ "tags-edit-none-selected": "Калі ласка, выберыце прынамсі адну бірку для дадання ці выдалення.",
"comparepages": "Параўнанне старонак",
"compare-page1": "Старонка 1",
"compare-page2": "Старонка 2",
"compare-revision-not-exists": "Паказанай вамі версіі не існуе.",
"dberr-problems": "Прабачце, на пляцоўцы здарыліся тэхнічныя цяжкасці.",
"dberr-again": "Паспрабуйце перачытаць праз некалькі хвілін.",
- "dberr-info": "(Немагчыма звязацца з серверам баз даных: $1)",
- "dberr-info-hidden": "(Немагчыма звязацца з серверам базы звестак)",
+ "dberr-info": "(Немагчыма звязацца з базай даных: $1)",
+ "dberr-info-hidden": "(Немагчыма звязацца з базай звестак)",
"dberr-usegoogle": "Тымчасам можна паспрабаваць пошук праз Гугл.",
"dberr-outofdate": "Заўважце, што тамтэйшыя індэксы тутэйшага зместу могуць быць састарэлымі.",
"dberr-cachederror": "Гэта копія старонкі, узятая з кэшу, і, магчыма, састарэлая.",
"htmlform-cloner-create": "Дадаць яшчэ",
"htmlform-cloner-delete": "Сцерці",
"htmlform-cloner-required": "Неабходна хаця б адно значэнне.",
+ "htmlform-title-badnamespace": "[[:$1]] не ў прасторы назваў \"{{ns:$2}}\".",
"htmlform-title-not-exists": "$1 не існуе.",
"htmlform-user-not-exists": "<strong>$1</strong> не існуе.",
"sqlite-has-fts": "$1 з падтрымкай поўна-тэкставага пошуку",
"currentrev-asof": "Revisió de $1",
"revisionasof": "Revisió del $1",
"revision-info": "La revisió el $1 per {{GENDER:$6|$2}}$7",
- "previousrevision": "←Versió més antiga",
- "nextrevision": "Versió més nova→",
+ "previousrevision": "← Versió més antiga",
+ "nextrevision": "Versió més nova →",
"currentrevisionlink": "Versió actual",
"cur": "act",
"next": "seg",
"feedback-useragent": "Uživatelský agent:",
"searchsuggest-search": "Hledat",
"searchsuggest-containing": "obsahující…",
+ "api-error-autoblocked": "Vaše IP adresa byla automaticky zablokována, protože ji používal zablokovaný uživatel.",
"api-error-badaccess-groups": "Nemáte povoleno nahrávat soubory na tuto wiki.",
"api-error-badtoken": "Vnitřní chyba: špatný token.",
+ "api-error-blocked": "Byla vám zablokována možnost editace.",
"api-error-copyuploaddisabled": "Načítání z URL je na tomto severu zakázáno.",
"api-error-duplicate": "Na této wiki již {{PLURAL:$1|existuje jiný soubor|existují jiné soubory}} se shodným obsahem.",
"api-error-duplicate-archive": "{{PLURAL:$1|Soubor|Soubory}} se stejným obsahem již zde dříve {{PLURAL:$1|byl|byly}}, ale {{PLURAL:$1|byl smazán|byly smazány}}.",
"feedback-useragent": "User Agent:",
"searchsuggest-search": "Suchen",
"searchsuggest-containing": "enthält …",
+ "api-error-autoblocked": "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem gesperrten Benutzer verwendet wurde.",
"api-error-badaccess-groups": "Du hast nicht die Berechtigung Dateien in dieses Wiki hochzuladen.",
"api-error-badtoken": "Interner Fehler: Der Token ist fehlerhaft.",
+ "api-error-blocked": "Du wurdest für das Bearbeiten gesperrt.",
"api-error-copyuploaddisabled": "Das Hochladen via URL wurde auf diesem Server deaktiviert.",
"api-error-duplicate": "Es gibt im Wiki bereits {{PLURAL:$1|eine andere Datei|mehrere andere Dateien}} gleichen Inhalts.",
"api-error-duplicate-archive": "Es {{PLURAL:$1|war bereits eine andere Datei|waren bereits andere Dateien}} gleichen Inhalts vorhanden. Sie {{PLURAL:$1|wurde|wurden}} allerdings gelöscht.",
"feedback-useragent": "Agent utilisateur :",
"searchsuggest-search": "Rechercher",
"searchsuggest-containing": "contenant...",
+ "api-error-autoblocked": "Votre adresse IP a été bloquée automatiquement, parce qu’elle a été utilisée par un utilisateur bloqué.",
"api-error-badaccess-groups": "Vous n'êtes pas autorisé à verser des fichiers sur ce wiki.",
"api-error-badtoken": "Erreur interne : mauvais « jeton ».",
+ "api-error-blocked": "Vous avez été bloqué en édition.",
"api-error-copyuploaddisabled": "Les versements via URL sont désactivés sur ce serveur.",
"api-error-duplicate": "Il y a déjà {{PLURAL:$1|un autre fichier présent|d'autres fichiers présents}} sur le site avec le même contenu.",
"api-error-duplicate-archive": "Il y avait déjà {{PLURAL:$1|un autre fichier présent|d'autres fichiers présents}} sur le site avec le même contenu, mais {{PLURAL:$1|il a été supprimé|ils ont été supprimés}}.",
"feedback-useragent": "Axente de usuario:",
"searchsuggest-search": "Procurar",
"searchsuggest-containing": "que conteña...",
+ "api-error-autoblocked": "A súa dirección IP foi bloqueada automaticamente porque foi usada por un usuario bloqueado.",
"api-error-badaccess-groups": "Non ten os permisos necesarios para cargar ficheiros neste wiki.",
"api-error-badtoken": "Erro interno: Pase incorrecto.",
+ "api-error-blocked": "Foi bloqueado fronte á edición.",
"api-error-copyuploaddisabled": "As cargas mediante URL están desactivadas neste servidor.",
"api-error-duplicate": "Xa hai {{PLURAL:$1|outro ficheiro| outros ficheiros}} no wiki co mesmo contido.",
"api-error-duplicate-archive": "Había {{PLURAL:$1|outro ficheiro|outros ficheiros}} no sitio co mesmo contido, pero {{PLURAL:$1|foi borrado|foron borrados}}.",
"redirectedfrom": "(הופנה מהדף $1)",
"redirectpagesub": "דף הפניה",
"redirectto": "הפניה ל:",
- "lastmodifiedat": "שׁוּנה לאחרונה ב־$1, בשעה $2.",
+ "lastmodifiedat": "×\93×£ ×\96×\94 ש×\81×\95Ö¼× ×\94 ×\9c×\90×\97ר×\95× ×\94 ×\91Ö¾$1, ×\91שע×\94 $2.",
"viewcount": "דף זה נצפה {{PLURAL:$1|פעם אחת|פעמיים|$1 פעמים}}.",
"protectedpage": "דף מוגן",
"jumpto": "קפיצה אל:",
"userinvalidcssjstitle": "'''אזהרה:''' העיצוב \"$1\" אינו קיים.\nדפי .css ו־.js מותאמים אישית משתמשים בכותרת עם אותיות קטנות – למשל, {{ns:user}}:דוגמה/vector.css ולא {{ns:user}}:דוגמה/Vector.css.",
"updated": "(מעודכן)",
"note": "'''הערה:'''",
- "previewnote": "<strong>×\96Ö´×\9bר×\95 ש×\96×\95 רק תצ×\95×\92×\94 ×\9eק×\93×\99×\9e×\94.</strong>\n×\94ש×\99× ×\95×\99×\99×\9d ש×\9c×\9b×\9d ×\98ר×\9d נשמרו!",
+ "previewnote": "<strong>×\96Ö´×\9bר×\95 ש×\96×\95 רק תצ×\95×\92×\94 ×\9eק×\93×\99×\9e×\94.</strong>\n×\94ש×\99× ×\95×\99×\99×\9d ש×\9c×\9b×\9d ×¢×\93×\99×\99×\9f ×\9c×\90 נשמרו!",
"continue-editing": "מעבר לאזור העריכה",
"previewconflict": "תצוגה מקדימה זו מציגה כיצד ייראה הטקסט בחלון העריכה העליון, אם תבחרו לשמור אותו.",
"session_fail_preview": "מצטערים! לא ניתן לבצע את עריכתכם עקב אובדן מידע הכניסה.\n\nייתכן שנותקתם מהחשבון. <strong>אנא ודאו שאתם עדיין מחוברים לחשבון ונסו שוב.</strong>\nאם זה עדיין לא עובד, נסו [[Special:UserLogout|לצאת מהחשבון]] ולהיכנס אליו שנית, וודאו שהדפדפן שלכם מאפשר קבלת עוגיות מאתר זה.",
"uploadvirus": "הקובץ מכיל וירוס!\nפרטים:\n<div dir=\"ltr\">$1</div>",
"uploadjava": "קובץ זה הוא קובץ ZIP שמכיל קובץ ‎.class של Java.\nהעלאת קובצי Java אסורה, כיוון שהם יכולים לגרום לעקיפת מגבלות האבטחה.",
"upload-source": "קובץ המקור",
- "sourcefilename": "ש×\9d ×\94ק×\95×\91×¥:",
+ "sourcefilename": "ש×\9d ק×\95×\91×¥ ×\94×\9eק×\95ר:",
"sourceurl": "כתובת URL של המקור:",
"destfilename": "שמירת הקובץ בשם:",
"upload-maxfilesize": "גודל הקובץ המרבי: $1",
"filedelete-old-unregistered": "גרסת הקובץ \"$1\" אינה רשומה בבסיס הנתונים.",
"filedelete-current-unregistered": "הקובץ \"$1\" אינו רשום בבסיס הנתונים.",
"filedelete-archive-read-only": "השרת אינו יכול לכתוב לתיקיית הארכיון \"$1\".",
- "previousdiff": "â\86\92 ×\9e×¢×\91ר ×\9c×\94ש×\95×\95×\90ת ×\94×\92רס×\90×\95ת הקודמת",
- "nextdiff": "×\9e×¢×\91ר ×\9c×\94ש×\95×\95×\90ת ×\94×\92רס×\90×\95ת הבאה ←",
+ "previousdiff": "â\86\92 ×\94ער×\99×\9b×\94 הקודמת",
+ "nextdiff": "×\94ער×\99×\9b×\94 הבאה ←",
"mediawarning": "<strong>אזהרה:</strong> סוג קובץ זה עלול להכיל קוד זדוני.\nהרצת הקוד עלולה לסכן את המחשב שלך.",
"imagemaxsize": "גודל תמונה מרבי:<br /><em>(בדפי תיאור של קבצים)</em>",
"thumbsize": "גודל של תמונות ממוזערות:",
"feedback-useragent": "User agent:",
"searchsuggest-search": "חיפוש",
"searchsuggest-containing": "כולל...",
+ "api-error-autoblocked": "כתובת ה־IP שלך נחסמה אוטומטית, כי היא הייתה בשימוש על־ידי משתמש חסום.",
"api-error-badaccess-groups": "אינך מורשה להעלות קבצים לאתר הוויקי הזה.",
"api-error-badtoken": "שגיאה פנימית: אסימון שבור.",
+ "api-error-blocked": "נחסמת מעריכה.",
"api-error-copyuploaddisabled": "העלאה לפי כתובת כובתה בשרת זה.",
"api-error-duplicate": "כבר יש באתר הזה {{PLURAL:$1|קובץ אחר|קבצים אחרים}} עם אותו תוכן.",
"api-error-duplicate-archive": "כבר {{PLURAL:$1|היה|היו}} באתר הזה {{PLURAL:$1|קובץ|קבצים}} עם אותו תוכן, אבל {{PLURAL:$1|הוא נמחק|הם נמחקו}}.",
"preview": "Хьалхе бӀаргтассар",
"showpreview": "Хьалххе хьажар",
"showdiff": "Даь хувцамаш",
- "anoneditwarning": "Зем хила! Шо кхы чудаьннадац. Шун IP-моттиг укх хийца оагӀув искаречу дӀаяздаь хургья.",
+ "anoneditwarning": "<strong>Теркам бе!</strong> Хьо автор хинна система чуваьннавац. Нагахьа санна Iа моллагIа хувцам бой, Хьа IP-адрес дийла массанен бIаргагуш хургда. Нагахьа санна Хьо <strong>[$1 хьачувоале]</strong> е <strong>[$2 учёта яздар хьакхолле]</strong>, нийсдараш (хувцамаш) бувзам болаш хургда Хьа доакъашхой цIерца, иштта кхыдола толажагIи гIойленагIи дола дикаьш хургда Хьона.",
"summary-preview": "Лоацам ба:",
"subject-preview": "Кортале хургья:",
"blockedtitle": "Дакъалаьцархо чӀега бела ва/я",
"editconflict": "ГӀалатнийсдара къовсам: $1",
"yourtext": "Хьа яздам",
"copyrightwarning": "Теркам бе, $2 ($1 хьажа) бокъонаца лорадеш, тӀахьежама кӀала уллаш, оаш мел чуяккхаш дола хоамаш, яздамаш долга.\nНаггахь санна шоай яздамаш пурам доацаш мала волашву саго хувца е кхы дола моттиге яздердолаш, безам беци, укхаз Ӏочуцаяздеча, дикаьгӀа да.<br />\nОаш дош лу, даь дола хувцама да волга/йолга, е оаш пурам долаш Ӏочуяздеш да кхычера меттигара шоай яздамаш/хоамаш.\n'''Яздархой бокъоца лорадеш дола хӀамаш, цара пурам доацаш, Ӏочумаязаде!'''",
- "templatesused": "УкÑ\85 бÓ\80аÑ\80гоагÓ\80Ñ\83вни оагÓ\80Ñ\83в Ñ\82Ó\80а лелаÑ\8fÑ\8c {{PLURAL:$1|1=Ð\9aÑ\83Ñ\86кеп|Ð\9aÑ\83Ñ\86кепаш}}:",
+ "templatesused": "УкÑ\85 оагIон Ñ\82Iа {{PLURAL:$1|1=пайда Ñ\8dÑ\86а Ð\9bо|пайда Ñ\8dÑ\86а Ð\9bонаш}}:",
"templatesusedpreview": "Хьалхе бӀаргтассама оагӀув тӀа леладеш дола {{PLURAL:$1|1=Куцкеп|Куцкепаш}}:",
"template-protected": "(лорадаь да)",
"template-semiprotected": "(цхьа долча даькъе гIо оттадаь да)",
- "hiddencategories": "Ер оагӀув укх {{PLURAL:$1|1=къайла цатегаца|къайла цатегашца}} дакъа лоаца:",
- "permissionserrorstext-withaction": "$2 де бокъо яц {{PLURAL:$1|1=из бахьан долаш|из бахьанаш долаш}}:",
+ "hiddencategories": "Ер оагIув {{PLURAL:$1|$1 къайла категориех|1=цаI къайла категорех}} я:",
+ "permissionserrorstext-withaction": "Ер $2 де Хьа бокъо яц {{PLURAL:$1|1=из бахьан долаш|из бахьанаш долаш}}:",
"recreate-moveddeleted-warn": "'''Зем бе! Шо хьалххе дIайоаккхаш хинна оагӀув хьае гӀерта.'''\n\nХьажа, бокъонцахь езаш йолга.\nКӀалхагIа укх оагӀуви дӀадаккхами цӀи хувцами тептараш хьекха да.",
"moveddeleted-notice": "Ер оагӀув дӀаяккха хиннай.\nНовкъостала, кӀалха хьахьекха да дӀадаккхама а хувцама а тептарашкара дIаяздараш.",
"log-fulllog": "Деррига таптара бӀаргтасса",
"viewpagelogs": "Укх оагӀон тептараш хьокха",
"currentrev-asof": "тӀеххьара верси $1",
"revisionasof": "Верси $1",
- "revision-info": "$1; $2 хувцам",
+ "revision-info": "Верси $1; {{GENDER:$6|$2}}$7",
"previousrevision": "← Xьалхарча",
"nextrevision": "ТIехьайоагIараш →",
"currentrevisionlink": "ХIанзара верси",
"thumbnail_error": "ЗIамигасуртанчий кхеллама гIалат: $1",
"import-upload-filename": "ПаьлацIи:",
"tooltip-pt-userpage": "{{GENDER:|Хьа}} доакъашхочунна оагIув",
- "tooltip-pt-mytalk": "Шун дувцамий оагIув",
+ "tooltip-pt-mytalk": "{{GENDER:|Хьа}} дувца оттадара оагIув",
"tooltip-pt-preferences": "{{GENDER:|Хьа оттамаш}}",
"tooltip-pt-watchlist": "ОоагIувна дагарле, шо бIаргалокхаш йола",
- "tooltip-pt-mycontris": "Шун хувцамаш",
+ "tooltip-pt-mycontris": "{{GENDER:|хьа}} хувцамаш",
"tooltip-pt-login": "Укхаза хьай цIи аьле чувала/яла йиша я, амма из параз дац",
"tooltip-pt-logout": "Аравала/яла",
"tooltip-pt-createaccount": "Хьа бокъо я учёта яздар кхелла система чу вала, амма параз долаш дац из.",
"feedback-useragent": "Agente utente:",
"searchsuggest-search": "Ricerca",
"searchsuggest-containing": "contenente...",
+ "api-error-autoblocked": "Il tuo indirizzo IP è stato bloccato automaticamente, perché è stato utilizzato da un utente bloccato.",
"api-error-badaccess-groups": "Non sei autorizzato a caricare documenti su questa wiki.",
"api-error-badtoken": "Errore interno: token errato.",
+ "api-error-blocked": "Sei stato bloccato, non puoi fare modifiche.",
"api-error-copyuploaddisabled": "Il caricamento tramite URL è disabilitato su questo server.",
"api-error-duplicate": "Sul sito {{PLURAL:$1|c'è già un altro documento|ci sono già altri documenti}} con lo stesso contenuto.",
"api-error-duplicate-archive": "{{PLURAL:$1|C'era un altro file|C'erano altri file}} già nel sito con lo stesso contenuto, ma {{PLURAL:$1|è stato cancellato|sono stati cancellati}}.",
"december-date": "12월 $1일",
"period-am": "오전",
"period-pm": "오후",
- "pagecategories": "{{PLURAL:$1|분류}}",
+ "pagecategories": "{{PLURAL:$1|분류|분류}}",
"category_header": "\"$1\" 분류에 속하는 문서",
"subcategories": "하위 분류",
"category-media-header": "\"$1\" 분류에 속하는 미디어",
"category-empty": "이 분류에 속하는 문서나 자료가 없습니다.",
- "hidden-categories": "{{PLURAL:$1|숨은 분류}}",
+ "hidden-categories": "{{PLURAL:$1|숨은 분류|숨은 분류}}",
"hidden-category-category": "숨은 분류",
"category-subcat-count": "{{PLURAL:$2|이 분류에는 하위 분류 1개만이 속해 있습니다.|다음은 이 분류에 속하는 {{PLURAL:$1|하위 분류}} $2개 가운데 $1개입니다.}}",
"category-subcat-count-limited": "이 분류에 {{PLURAL:$1|하위 분류가|하위 분류 $1개가}} 있습니다.",
"badaccess-group0": "요청한 명령을 실행할 권한이 없습니다.",
"badaccess-groups": "요청한 명령은 {{PLURAL:$2|다음|다음 중 하나의}} 권한을 가진 사용자에게 제한됩니다: $1.",
"versionrequired": "미디어위키 $1 버전 필요",
- "versionrequiredtext": "이 문서를 사용하려면 $1 버전 미디어위키가 필요합니다.\n[[Special:Version|설치된 미디어위키 버전]]을 참조하세요.",
+ "versionrequiredtext": "이 문서를 사용하려면 $1 버전의 미디어위키가 필요합니다.\n[[Special:Version|설치된 미디어위키 버전]]을 참조하세요.",
"ok": "확인",
"retrievedfrom": "원본 주소 \"$1\"",
"youhavenewmessages": "다른 사용자로부터의 $1가 {{PLURAL:$3|있습니다}}. ($2)",
"laggedslavemode": "<strong>경고:</strong> 문서가 최근에 바뀐 내용을 포함하지 않을 수도 있습니다.",
"readonly": "데이터베이스 잠김",
"enterlockreason": "데이터베이스를 잠그는 이유와 예상되는 기간을 적어 주세요.",
- "readonlytext": "데이터베이스가 잠겨 있어서 문서를 편집할 수 없습니다. 데이터베이스 관리가 끝난 후에는 정상으로 돌아올 것입니다.\n\n관리자가 데이터베이스를 잠글 때 남긴 메시지는 다음과 같습니다: $1",
+ "readonlytext": "데이터베이스가 잠겨 있어서 문서를 편집할 수 없습니다. 데이터베이스 관리가 끝난 후에는 정상으로 돌아올 것입니다.\n\n시스템 관리자가 데이터베이스를 잠글 때 남긴 메시지는 다음과 같습니다: $1",
"missing-article": "데이터베이스에서 \"$1\" 문서의 $2 텍스트를 찾지 못했습니다.\n\n삭제된 문서의 오래된 차이나 역사 링크를 보려고 시도할 때 이러한 문제가 발생할 수 있습니다.\n\n그렇지 않다면, 소프트웨어에 버그가 발생했을 수도 있습니다.\n[[Special:ListUsers/sysop|관리자]]에게 URL을 참조하여 알려주세요.",
"missingarticle-rev": "(판번호: $1)",
"missingarticle-diff": "(차이: $1, $2)",
- "readonly_lag": "슬레이브 데이터베이스가 마스터 서버의 자료를 새로 고치는 중입니다. 데이터베이스가 자동으로 잠겨져 있습니다",
+ "readonly_lag": "슬레이브 데이터베이스 서버들이 마스터 서버와 동기화되고 있습니다. 그 동안 데이터베이스가 자동으로 잠겨져 있습니다.",
"nonwrite-api-promise-error": "'Promise-Non-Write-API-Action' HTTP 헤더가 붙어있지만 API 쓰기 모듈에 대한 요청을 했습니다.",
"internalerror": "내부 오류",
"internalerror_info": "내부 오류: $1",
"feedback-useragent": "사용자 에이전트:",
"searchsuggest-search": "검색",
"searchsuggest-containing": "다음 문자열 포함...",
+ "api-error-autoblocked": "사용자의 IP 주소는 차단된 사용자에 의해 사용되었으므로 자동으로 차단된 상태입니다.",
"api-error-badaccess-groups": "이 위키에 파일을 올릴 권한이 없습니다.",
"api-error-badtoken": "내부 오류: 토큰이 잘못되었습니다.",
+ "api-error-blocked": "편집에서 차단되어 있습니다.",
"api-error-copyuploaddisabled": "이 서버에서 URL을 통해 파일 올리기가 비활성화되어 있습니다.",
"api-error-duplicate": "이 위키에 내용이 똑같은 {{PLURAL:$1|다른 파일}}이 있습니다.",
"api-error-duplicate-archive": "같은 내용을 담고 있던 {{PLURAL:$1|다른 파일}}이 있었지만 이 {{PLURAL:$1|파일}}은 삭제되었습니다.",
"log-action-filter-patrol": "점검 종류:",
"log-action-filter-protect": "보호 종류:",
"log-action-filter-rights": "권한 변경 종류",
+ "log-action-filter-suppress": "숨기기 종류",
"log-action-filter-upload": "업로드 종류:",
"log-action-filter-all": "모두",
"log-action-filter-block-block": "차단",
"log-action-filter-newusers-create": "익명의 사용자에 의한 생성",
"log-action-filter-newusers-create2": "등록된 사용자에 의한 생성",
"log-action-filter-newusers-autocreate": "자동 생성",
+ "log-action-filter-newusers-byemail": "이메일로 보낸 비밀번호로 생성",
"log-action-filter-patrol-patrol": "수동 점검",
"log-action-filter-patrol-autopatrol": "자동 점검",
"log-action-filter-protect-protect": "보호",
"log-action-filter-protect-move_prot": "이동 보호",
"log-action-filter-rights-rights": "수동 변경",
"log-action-filter-rights-autopromote": "자동 변경",
+ "log-action-filter-suppress-event": "로그 숨기기",
+ "log-action-filter-suppress-revision": "판 숨기기",
+ "log-action-filter-suppress-delete": "문서 숨기기",
+ "log-action-filter-suppress-block": "차단을 통한 사용자 숨기기",
+ "log-action-filter-suppress-reblock": "재차단을 통한 사용자 숨기기",
"log-action-filter-upload-upload": "새로 업로드",
"log-action-filter-upload-overwrite": "다시 업로드"
}
"rcshowhidemine-show": "nîşan bide",
"rcshowhidemine-hide": "veşêre",
"rcshowhidecategorization": "Kategorîzekirina rûpelan $1",
- "rcshowhidecategorization-show": "Nîşan bide",
- "rcshowhidecategorization-hide": "Veşêre",
+ "rcshowhidecategorization-show": "nîşan bide",
+ "rcshowhidecategorization-hide": "veşêre",
"rclinks": "$1 guherandinên di $2 rojên dawî de nîşan bide<br />$3",
"diff": "cudahî",
"hist": "dîrok",
"feedback-useragent": "User Agent:",
"searchsuggest-search": "Sichen",
"searchsuggest-containing": "mat ...",
+ "api-error-autoblocked": "Är IP-Adress gouf automatesch gespaart wëll se vun engem gespaarte Benotzer benotzt gouf",
"api-error-badaccess-groups": "Et ass Iech net erlaabt fir Fichieren op dës Wiki eropzelueden.",
"api-error-badtoken": "Interne Feeler: falschen Token.",
+ "api-error-blocked": "Dir gouft gespaart a kënnt dofir keng Ännerunge maachen.",
"api-error-copyuploaddisabled": "D'Eroplueden iwwer eng URL ass op dësem Server desaktivéiert.",
"api-error-duplicate": "Et gëtt schonn {{PLURAL:$1|en anere Fichier|e puer aner Fichiere}} mat dem selwechten Inhalt op dem Site",
"api-error-duplicate-archive": "Et gouf schonn {{PLURAL:$1| een anere Fichier|e puer aner Fichieren}} op dem Site mat deemselwechten Inhalt, {{PLURAL:$1|e gouf|se goufen}} awer geläscht.",
"minoredit": "Tai smulkus pataisymas",
"watchthis": "Stebėti šį puslapį",
"savearticle": "Išsaugoti puslapį",
+ "publishpage": "Skelbti puslapį",
"preview": "Peržiūra",
"showpreview": "Rodyti peržiūrą",
"showdiff": "Rodyti skirtumus",
"ipb-unblock": "Atblokuoti naudotojo vardą arba IP adresą",
"ipb-blocklist": "Rodyti egzistuojančius blokavimus",
"ipb-blocklist-contribs": "{{GENDER:$1|$1}} indėlis",
+ "ipb-blocklist-duration-left": "$1 kairėje",
"unblockip": "Atblokuoti naudotoją",
"unblockiptext": "Naudokite šią formą, kad atkurtumėte redagavimo galimybę\nankščiau užblokuotam IP adresui ar naudotojui.",
"ipusubmit": "Atblokuoti šį adresą",
"tooltip-ca-nstab-category": "Rodyti kategorijos puslapį",
"tooltip-minoredit": "Pažymėti keitimą kaip smulkų",
"tooltip-save": "Išsaugoti pakeitimus",
+ "tooltip-publish": "Skelbti jūsų pakeitimus",
"tooltip-preview": "Pakeitimų peržiūra, prašome pažiūrėti prieš išsaugant!",
"tooltip-diff": "Rodo, kokius pakeitimus padarėte tekste",
"tooltip-compareselectedversions": "Žiūrėti dviejų pasirinktų puslapio versijų skirtumus.",
"sessionprovider-generic": "$1 sesijos",
"sessionprovider-mediawiki-session-cookiesessionprovider": "sesijos su slapukais",
"sessionprovider-nocookies": "Slapukai gali būti neaktyvuoti. Įsitikinkite, kad slapukai yra aktyvuoti ir pradėkite vėl.",
- "randomrootpage": "Atsitiktinis šakninis puslapis"
+ "randomrootpage": "Atsitiktinis šakninis puslapis",
+ "log-action-filter-all": "Visi",
+ "log-action-filter-newusers-autocreate": "Automatinis kūrimas",
+ "log-action-filter-protect-protect": "Apsauga"
}
"creditspage": "Panghargaan laman",
"spam_blanking": "Sado revisi nan ado pautan ka $1, kosong",
"spam_deleting": "Sado revisi nan ado pautan ka $1, dihapuih",
- "simpleantispam-label": "Pamarisoan anti-spam.\nMasukan ko '''DILARANG'''!",
+ "simpleantispam-label": "Pamarisoan anti-spam.\n<strong>Jan</strong> diisi!",
"pageinfo-title": "Informasi untuak \"$1\"",
"pageinfo-not-current": "Maaf, indak dapek mangagiahan informasi ko ka revisi lamo.",
"pageinfo-header-basic": "Informasi dasar",
"feedback-useragent": "Aplikacja klienta:",
"searchsuggest-search": "Szukaj",
"searchsuggest-containing": "zawierające...",
+ "api-error-autoblocked": "Twój adres IP został automatycznie zablokowany, ponieważ był używany przez zablokowanego użytkownika.",
"api-error-badaccess-groups": "Nie masz uprawnień aby przesyłać pliki do tej wiki.",
"api-error-badtoken": "Błąd wewnętrzny – nieprawidłowy kod weryfikacyjny (token).",
+ "api-error-blocked": "Została ci zablokowana możliwość edycji.",
"api-error-copyuploaddisabled": "Przesyłanie poprzez podanie adresu URL zostało na tym serwerze wyłączone.",
"api-error-duplicate": "{{PLURAL:$1|Jest już inny plik|Są już inne pliki}} o tej samej zawartości",
"api-error-duplicate-archive": "{{PLURAL:$1|Był już inny plik|Były już inne pliki}} o takiej samej zawartości, ale {{PLURAL:$1|został usunięty|zostały usunięte}}.",
"variantname-gan": "{{Optional}}\n\nVariant option for wikis with variants conversion enabled.",
"variantname-sr-ec": "{{optional}}\nVariant Option for wikis with variants conversion enabled.\n\nNote that <code>sr-ec</code> is not a conforming BCP47 language tag. Wikis should be migrated by:\n* allowing it only as a legacy alias of the preferred tag <code>sr-cyrl</code> (possibly insert a tracking category in templates as long as they must support the legacy tag),\n* making the new tag the default to look first, before looking for the old tag,\n* moving the translations to the new code by renaming them,\n* checking links in source pages still using the legacy tag to change it to the new tag,\n* possibly cleanup the redirect pages.",
"variantname-sr-el": "{{optional}}\nVariant Option for wikis with variants conversion enabled.\n\nNote that <code>sr-el</code> is not a conforming BCP47 language tag. Wikis should be migrated by:\n* allowing it only as a legacy alias of the preferred tag <code>sr-latn</code> (possibly insert a tracking category in templates as long as they must support the legacy tag),\n* making the new tag the default to look first, before looking for the old tag,\n* moving the translations to the new code by renaming them,\n* checking links in source pages still using the legacy tag to change it to the new tag,\n* possibly cleanup the redirect pages.",
- "variantname-sr": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
+ "variantname-sr": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
"variantname-kk-kz": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
"variantname-kk-tr": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
"variantname-kk-cn": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
"feedback-useragent": "A label denoting the user agent in the feedback that is posted to the feedback page.\n{{Identical|User agent}}",
"searchsuggest-search": "Greyed out default text in the simple search box in the Vector skin. (It disappears and lets the user enter the requested search terms when the search box receives focus.)\n\n{{Identical|Search}}",
"searchsuggest-containing": "Label used in the special item of the search suggestions list which gives the user an option to perform a full text search for the term.",
- "api-error-autoblocked": "API error message that can be used for client side localisation of API errors.",
+ "api-error-autoblocked": "API error message that can be used for client side localisation of API errors.\n\nCf. {{msg-mw|Autoblockedtext}}.",
"api-error-badaccess-groups": "API error message that can be used for client side localisation of API errors.",
"api-error-badtoken": "API error message that can be used for client side localisation of API errors.",
"api-error-blocked": "API error message that can be used for client side localisation of API errors.",
"feedback-useragent": "Uporabniški agent:",
"searchsuggest-search": "Iskanje",
"searchsuggest-containing": "vsebujoč ...",
+ "api-error-autoblocked": "Vaš IP-naslov smo samodejno blokirali, saj ga je uporabljal blokiran uporabnik.",
"api-error-badaccess-groups": "Nalaganje datotek na ta wiki vam ni dovoljeno.",
"api-error-badtoken": "Notranja napaka: slab žeton.",
+ "api-error-blocked": "Urejanje vam je preprečeno.",
"api-error-copyuploaddisabled": "Nalaganje preko URL je na tem strežniku onemogočeno.",
"api-error-duplicate": "Na strani že {{PLURAL:$1|obstaja druga datoteka|obstajata drugi datoteki|obstajajo druge datoteke}} z enako vsebino.",
"api-error-duplicate-archive": "Na strani {{PLURAL:$1|je že bila druga datoteka|sta že bili drugi datoteki|so že bile nekatere druge datoteke}} z enako vsebino, vendar {{PLURAL:$1|je bila izbrisana|sta bili izbrisani|so bile izbrisane}}.",
"tog-watchdefault": "Tự động theo dõi các trang và tập tin tôi sửa",
"tog-watchmoves": "Tự động theo dõi các trang và tập tin tôi di chuyển",
"tog-watchdeletion": "Tự động theo dõi các trang và tập tin tôi xóa",
+ "tog-watchuploads": "Thêm các tập tin tải lên của tôi vào danh sách theo dõi của tôi",
"tog-watchrollback": "Tự động theo dõi các trang tôi lùi sửa",
"tog-minordefault": "Mặc định đánh dấu tất cả sửa đổi của tôi là sửa đổi nhỏ",
"tog-previewontop": "Hiển thị phần xem trước nằm trên hộp sửa đổi",
"minoredit": "Sửa đổi nhỏ",
"watchthis": "Theo dõi trang này",
"savearticle": "Lưu trang",
+ "publishpage": "Xuất bản trang",
"preview": "Xem trước",
"showpreview": "Xem trước",
"showdiff": "Xem thay đổi",
"userpage-userdoesnotexist": "Đây chưa có tài khoản với tên “<nowiki>$1</nowiki>”. Xin hãy kiểm tra lại nếu bạn muốn tạo hay sửa trang này.",
"userpage-userdoesnotexist-view": "Chưa có tài khoản với tên “$1”.",
"blocked-notice-logextract": "Người dùng này hiện đang bị cấm sửa đổi. Nhật trình cấm gần nhất được ghi ở dưới để tiện theo dõi:",
- "clearyourcache": "'''Chú ý:''' Sau khi lưu trang, có thể bạn sẽ phải xóa bộ nhớ đệm của trình duyệt để xem các thay đổi.\n* '''Firefox / Safari:''' Nhấn giữ phím ''Shift'' trong khi nhấn ''Tải lại'' (''Reload''), hoặc nhấn tổ hợp ''Ctrl-F5'' hay ''Ctrl-R'' (⌘R trên Mac)\n* '''Google Chrome:''' Nhấn tổ hợp ''Ctrl-Shift-R'' (⇧⌘R trên Mac)\n* '''Internet Explorer:''' Nhấn giữ phím ''Ctrl'' trong khi nhấn ''Làm tươi'' (''Refresh''), hoặc nhấn tổ hợp ''Ctrl-F5''\n* '''Opera:''' Xóa bộ nhớ đệm trong ''Công cụ → Sở thích'' (''Tools → Preferences'')",
+ "clearyourcache": "<strong>Chú ý:</strong> Sau khi lưu trang, có thể bạn sẽ phải xóa bộ nhớ đệm của trình duyệt để xem các thay đổi.\n* <strong>Firefox / Safari:</strong> Nhấn giữ phím <em>Shift</em> trong khi nhấn <em>Tải lại</em> (<em>Reload</em>), hoặc nhấn tổ hợp <em>Ctrl-F5</em> hay <em>Ctrl-R</em> (⌘R trên Mac)\n* <strong>Google Chrome:</strong> Nhấn tổ hợp <em>Ctrl-Shift-R</em> (⇧⌘R trên Mac)\n* <strong>Internet Explorer:</strong> Nhấn giữ phím <em>Ctrl</em> trong khi nhấn <em>Làm tươi</em>, hoặc nhấn tổ hợp <em>Ctrl-F5</em>\n* <strong>Opera:</strong> Mở <em>Menu → Cài đặt</em> (<em>Opera → Tùy chỉnh</em> trên Mac), mở tab <em>Quyền riêng tư & bảo mật</em>, bấm <em>Xóa dữ liệu duyệt web</em> và đánh hộp kiểm <em>Hình ảnh và tệp trong cache</em>.",
"usercssyoucanpreview": "'''Mẹo:''' Sử dụng nút “{{int:showpreview}}” để kiểm thử bản CSS của bạn trước khi lưu trang.",
"userjsyoucanpreview": "'''Mẹo:''' Sử dụng nút “{{int:showpreview}}” để kiểm thử bản JS của bạn trước khi lưu trang.",
"usercsspreview": "'''Hãy nhớ rằng bạn chỉ đang xem trước trang CSS cá nhân của bạn.\nNó chưa được lưu!'''",
"recentchangeslinked-page": "Tên trang:",
"recentchangeslinked-to": "Hiện thay đổi tại những trang có liên kết đến trang này thay thế",
"recentchanges-page-added-to-category": "[[:$1]] được xếp vào thể loại",
- "recentchanges-page-added-to-category-bundled": "[[:$1]] và [[Special:WhatLinksHere/$1|{{PLURAL:$2|một trang|$2 trang}} nữa]] được xếp vào thể loại",
+ "recentchanges-page-added-to-category-bundled": "[[:$1]] được xếp vào thể loại; [[Special:WhatLinksHere/$1|trang này được nhúng vào các trang khác]]",
"recentchanges-page-removed-from-category": "[[:$1]] được gỡ khỏi thể loại",
- "recentchanges-page-removed-from-category-bundled": "[[:$1]] và [[Special:WhatLinksHere/$1|{{PLURAL:$2|một trang|$2 trang}} nữa]] được gỡ khỏi thể loại",
+ "recentchanges-page-removed-from-category-bundled": "[[:$1]] được xóa gỡ thể loại; [[Special:WhatLinksHere/$1|trang này được nhúng vào các trang khác]]",
"autochange-username": "MediaWiki thay đổi tự động",
"upload": "Tải tập tin lên",
"uploadbtn": "Tải tập tin lên",
"changecontentmodel-nodirectediting": "Kiểu nội dung $1 không hỗ trợ sửa đổi trực tiếp",
"log-name-contentmodel": "Nhật trình thay đổi kiểu nội dung",
"log-description-contentmodel": "Sự kiện có liên quan đến kiểu nội dung của trang.",
- "logentry-contentmodel-new": "$1 {{GENDER:$2}}đã tạo trang $3 với mô hình nội dung không mặc định “$5”",
+ "logentry-contentmodel-new": "$1 {{GENDER:$2}}đã tạo trang $3 với kiểu nội dung không mặc định “$5”",
"logentry-contentmodel-change": "$1 {{GENDER:$2}}đã thay đổi kiểu nội dung của trang $3 từ “$4” thành “$5”",
"logentry-contentmodel-change-revertlink": "lùi lại",
"logentry-contentmodel-change-revert": "lùi lại",
"ipb-unblock": "Bỏ cấm thành viên hay địa chỉ IP",
"ipb-blocklist": "Xem danh sách đang bị cấm",
"ipb-blocklist-contribs": "Đóng góp của $1",
+ "ipb-blocklist-duration-left": "còn $1 nữa",
"unblockip": "Bỏ cấm thành viên",
"unblockiptext": "Sử dụng mẫu sau để phục hồi lại quyền sửa đổi đối với một địa chỉ IP hoặc tên thành viên đã bị cấm trước đó.",
"ipusubmit": "Bỏ cấm",
"tooltip-ca-nstab-category": "Xem trang thể loại",
"tooltip-minoredit": "Đánh dấu đây là sửa đổi nhỏ",
"tooltip-save": "Lưu lại những thay đổi của bạn",
+ "tooltip-publish": "Xuất bản các thay đổi của bạn",
"tooltip-preview": "Xem trước những thay đổi, hãy dùng nó trước khi lưu!",
"tooltip-diff": "Xem thay đổi bạn đã thực hiện.",
"tooltip-compareselectedversions": "Xem khác biệt giữa hai phiên bản đã chọn của trang này.",
"confirmemail_body_set": "Ai đó, có thể là bạn, từ địa chỉ IP $1, đã đặt địa chỉ này là địa\nchỉ thư điện tử của tài khoản “$2” tại {{SITENAME}}.\n\nĐể xác nhận rằng tài khoản này thực sự là của bạn và để kích hoạt các tính năng\nthư điện tử tại {{SITENAME}}, xin mở liên kết này trong trình duyệt:\n\n$3\n\nNếu tài khoản *không* phải là của bạn, hãy nhấn vào liên kết này để hủy thủ tục\nxác nhận địa chỉ thư điện tử:\n\n$5\n\nMã xác nhận này sẽ hết hạn vào $4.",
"confirmemail_invalidated": "Đã hủy xác nhận địa chỉ thư điện tử",
"invalidateemail": "Hủy xác nhận thư điện tử",
+ "notificationemail_subject_changed": "Địa chỉ thư điện tử đăng ký tại {{SITENAME}} đã được thay đổi",
+ "notificationemail_subject_removed": "Địa chỉ thư điện tử đăng ký tại {{SITENAME}} đã được loại bỏ",
+ "notificationemail_body_changed": "Ai đó, có thể là bạn, từ địa chỉ IP $1,\nđã thay đổi địa chỉ thư điện tử của tài khoản “$2” thành “$3” tại {{SITENAME}}.\n\nNếu bạn không phải là người thay đổi địa chỉ này, xin hãy liên lạc với một bảo quản viên của trang Web ngay lập tức.",
+ "notificationemail_body_removed": "Ai đó, có thể là bạn, từ địa chỉ IP $1,\nđã loại bỏ địa chỉ thư điện tử của tài khoản “$2” tại {{SITENAME}}.\n\nNếu bạn không phải là người loại bỏ địa chỉ này, xin hãy liên lạc với một bảo quản viên của trang Web ngay lập tức.",
"scarytranscludedisabled": "[Nhúng giữa các wiki bị tắt]",
"scarytranscludefailed": "[Truy xuất bản mẫu $1 bị thất bại]",
"scarytranscludefailed-httpstatus": "[Truy xuất bản mẫu $1 bị thất bại: HTTP $2]",
"watchlistedit-raw-done": "Danh sách các trang bạn theo dõi đã được cập nhật.",
"watchlistedit-raw-added": "$1 tựa đề đã được thêm vào:",
"watchlistedit-raw-removed": "$1 tựa đề đã được xóa khỏi danh sách:",
- "watchlistedit-clear-title": "Đã xóa sạch danh sách theo dõi",
+ "watchlistedit-clear-title": "Xóa sạch danh sách theo dõi",
"watchlistedit-clear-legend": "Xóa sạch danh sách theo dõi",
"watchlistedit-clear-explain": "Tất cả các tiêu đề sẽ được xóa khỏi danh sách theo dõi của bạn.",
"watchlistedit-clear-titles": "Các tiêu đề:",
"logentry-protect-protect-cascade": "$1 {{GENDER:$2}}đã khóa $3 $4 [theo tầng]",
"logentry-protect-modify": "$1 {{GENDER:$2}}đã đổi mức khóa $3 $4",
"logentry-protect-modify-cascade": "$1 {{GENDER:$2}}đã đổi mức khóa $3 $4 [theo tầng]",
- "logentry-rights-rights": "$1 {{GENDER:$2}}đã đổi các nhóm bao gồm $3 từ $4 đến $5",
+ "logentry-rights-rights": "$1 {{GENDER:$2}}đã đổi các nhóm bao gồm {{GENDER:$6}}$3 từ $4 đến $5",
"logentry-rights-rights-legacy": "{{GENDER:$2}}$1 đã đổi các nhóm bao gồm $3",
"logentry-rights-autopromote": "$1 {{GENDER:$2}}đã được tự động phong cấp từ $4 đến $5",
"logentry-upload-upload": "$1 {{GENDER:$2}}đã tải lên $3",
"feedback-useragent": "Tác nhân người dùng:",
"searchsuggest-search": "Tìm kiếm",
"searchsuggest-containing": "có chứa…",
+ "api-error-autoblocked": "Địa chỉ IP của bạn bị cấm tự động vì nó đã được sử dụng bởi một người dùng bị cấm.",
"api-error-badaccess-groups": "Bạn không được phép tải tập tin lên wiki này.",
"api-error-badtoken": "Lỗi nội bộ: Dấu hiệu bị hỏng.",
+ "api-error-blocked": "Bạn đã bị cấm không được sửa đổi.",
"api-error-copyuploaddisabled": "Chức năng tải lên từ URL đã bị tắt trên máy chủ này.",
"api-error-duplicate": "Wiki này đã có {{PLURAL:$1|tập tin|$1 tập tin}} cùng nội dung có tên khác.",
"api-error-duplicate-archive": "{{PLURAL:$1|Một|Các}} tập tin khác cùng nội dung đã tồn tại trên website, nhưng {{PLURAL:$1|nó|chúng}} đã bị xóa.",
"api-error-nomodule": "Lỗi nội bộ: Mô đun tải lên không được định rõ.",
"api-error-ok-but-empty": "Lỗi nội bộ: Máy chủ không phản hồi.",
"api-error-overwrite": "Không được ghi đè một tập tin đã tồn tại.",
+ "api-error-ratelimited": "Bạn cố tải lên nhiều tập tin trong một thời gian ngắn vượt quá hạn chế của wiki này.",
"api-error-stashfailed": "Lỗi nội bộ: Máy chủ bị thất bại trong việc lưu giữ tập tin tạm.",
"api-error-publishfailed": "Lỗi nội bộ: Máy chủ bị thất bại trong việc xuất bản tập tin tạm.",
"api-error-stasherror": "Đã xuất hiện lỗi khi tải tập tin lên hàng đợi.",
"api-error-unknownerror": "Lỗi không rõ: “$1”.",
"api-error-uploaddisabled": "Chức năng tải lên đã bị tắt trên wiki này.",
"api-error-verification-error": "Tập tin này có thể bị hỏng hoặc có phần mở rộng sai.",
+ "api-error-was-deleted": "Một tập tin cùng tên này đã được tải lên và bị xóa về sau.",
"duration-seconds": "$1 giây",
"duration-minutes": "$1 phút",
"duration-hours": "$1 giờ",
"special-characters-group-ipa": "Phiên âm quốc tế",
"special-characters-group-symbols": "Ký hiệu",
"special-characters-group-greek": "Hy Lạp",
+ "special-characters-group-greekextended": "Hy Lạp mở rộng",
"special-characters-group-cyrillic": "Kirin",
"special-characters-group-arabic": "Ả Rập",
"special-characters-group-arabicextended": "Ả Rập mở rộng",
"sessionprovider-mediawiki-session-cookiesessionprovider": "phiên dựa trên cookie",
"sessionprovider-nocookies": "Cookie có thể bị vô hiệu hóa. Đảm bảo bạn đã bật cookie và bắt đầu một lần nữa.",
"randomrootpage": "Trang gốc ngẫu nhiên",
+ "log-action-filter-block": "Kiểu cấm:",
+ "log-action-filter-contentmodel": "Kiểu thay đổi kiểu nội dung:",
+ "log-action-filter-delete": "Kiểu xóa:",
+ "log-action-filter-import": "Kiểu nhập:",
+ "log-action-filter-managetags": "Kiểu tác vụ quản lý thẻ:",
+ "log-action-filter-move": "Kiểu di chuyển:",
+ "log-action-filter-newusers": "Kiểu tạo tài khoản:",
+ "log-action-filter-patrol": "Kiểu tuần tra:",
"log-action-filter-protect": "Loại bảo vệ:",
+ "log-action-filter-rights": "Kiểu thay đổi quyền:",
+ "log-action-filter-suppress": "Kiểu ẩn giấu",
"log-action-filter-upload": "Loại tải lên:",
"log-action-filter-all": "Tất cả",
"log-action-filter-block-block": "Khối",
+ "log-action-filter-block-reblock": "Thay đổi tác vụ cấm",
+ "log-action-filter-block-unblock": "Bỏ cấm",
+ "log-action-filter-contentmodel-change": "Thay đổi kiểu nội dung",
+ "log-action-filter-contentmodel-new": "Tạo trang có kiểu nội dung không chuẩn",
+ "log-action-filter-delete-delete": "Xóa trang",
+ "log-action-filter-delete-restore": "Phục hồi trang",
+ "log-action-filter-delete-event": "Xóa nhật trình",
+ "log-action-filter-delete-revision": "Xóa phiên bản",
+ "log-action-filter-import-interwiki": "Nhập liên wiki",
"log-action-filter-import-upload": "Nhập bằng cách tải lên XML",
+ "log-action-filter-managetags-create": "Tạo thẻ",
+ "log-action-filter-managetags-delete": "Xóa thẻ",
+ "log-action-filter-managetags-activate": "Kích hoạt thẻ",
+ "log-action-filter-managetags-deactivate": "Vô hiệu thẻ",
+ "log-action-filter-move-move": "Di chuyển mà không ghi đè trang đổi hướng",
+ "log-action-filter-move-move_redir": "Di chuyển mà ghi đè trang đổi hướng",
"log-action-filter-newusers-create": "Tạo bởi người dùng vô danh",
"log-action-filter-newusers-create2": "Tạo bởi người dùng đã đăng ký",
+ "log-action-filter-newusers-autocreate": "Tạo tự động",
"log-action-filter-newusers-byemail": "Tạo với mật khẩu được gửi qua thư điện tử",
+ "log-action-filter-patrol-patrol": "Tuần tra thủ công",
+ "log-action-filter-patrol-autopatrol": "Tuần tra tự động",
"log-action-filter-protect-protect": "Bảo vệ",
+ "log-action-filter-protect-modify": "Thay đổi mức khóa",
+ "log-action-filter-protect-unprotect": "Mở khóa",
+ "log-action-filter-protect-move_prot": "Di chuyển khóa",
+ "log-action-filter-rights-rights": "Thay đổi thủ công",
"log-action-filter-rights-autopromote": "Tự động thay đổi",
+ "log-action-filter-suppress-event": "Ẩn giấu nhật trình",
+ "log-action-filter-suppress-revision": "Ẩn giấy phiên bản",
+ "log-action-filter-suppress-delete": "Ẩn giấu trang",
+ "log-action-filter-suppress-block": "Ẩn giấu người dùng bằng cách cấm",
+ "log-action-filter-suppress-reblock": "Ẩn giấu người dùng bằng cách cấm lại",
"log-action-filter-upload-upload": "Tải lên mới",
"log-action-filter-upload-overwrite": "Tải lên lại"
}
"feedback-useragent": "用户代理:",
"searchsuggest-search": "搜索",
"searchsuggest-containing": "含有...",
+ "api-error-autoblocked": "您的IP地址已被自动封禁,因为它曾被一位已封禁用户使用。",
"api-error-badaccess-groups": "您没有将文件上传到此 wiki 的权限。",
"api-error-badtoken": "内部错误:会话无效。",
+ "api-error-blocked": "您已被封禁,不能编辑。",
"api-error-copyuploaddisabled": "通过URL上传的功能已被此服务器禁用。",
"api-error-duplicate": "在网站上已经具有相同内容的{{PLURAL:$1|另一个文件|另一些文件}}。",
"api-error-duplicate-archive": "在网站上曾经具有相同内容的{{PLURAL:$1|另一个文件|另一些文件}},但已被删除。",
use MediaWiki\Logger\LegacySpi;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Logger\MonologSpi;
+use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerInterface;
/**
* @since 1.18
*/
abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
+
+ /**
+ * The service locator created by prepareServices(). This service locator will
+ * be restored after each test. Tests that pollute the global service locator
+ * instance should use overrideMwServices() to isolate the test.
+ *
+ * @var MediaWikiServices|null
+ */
+ private static $serviceLocator = null;
+
/**
* $called tracks whether the setUp and tearDown method has been called.
* class extending MediaWikiTestCase usually override setUp and tearDown
}
}
- public function run( PHPUnit_Framework_TestResult $result = null ) {
+ public static function setUpBeforeClass() {
+ parent::setUpBeforeClass();
+
+ // NOTE: Usually, PHPUnitMaintClass::finalSetup already called this,
+ // but let's make doubly sure.
+ self::prepareServices( new GlobalVarConfig() );
+ }
+
+ /**
+ * Prepare service configuration for unit testing.
+ *
+ * This calls MediaWikiServices::resetGlobalInstance() to allow some critical services
+ * to be overridden for testing.
+ *
+ * prepareServices() only needs to be called once, but should be called as early as possible,
+ * before any class has a chance to grab a reference to any of the global services
+ * instances that get discarded by prepareServices(). Only the first call has any effect,
+ * later calls are ignored.
+ *
+ * @note This is called by PHPUnitMaintClass::finalSetup.
+ *
+ * @see MediaWikiServices::resetGlobalInstance()
+ *
+ * @param Config $bootstrapConfig The bootstrap config to use with the new
+ * MediaWikiServices. Only used for the first call to this method.
+ */
+ public static function prepareServices( Config $bootstrapConfig ) {
+ static $servicesPrepared = false;
+
+ if ( $servicesPrepared ) {
+ return;
+ } else {
+ $servicesPrepared = true;
+ }
+
+ self::resetGlobalServices( $bootstrapConfig );
+ }
+
+ /**
+ * Reset global services, and install testing environment.
+ * This is the testing equivalent of MediaWikiServices::resetGlobalInstance().
+ * This should only be used to set up the testing environment, not when
+ * running unit tests. Use overrideMwServices() for that.
+ *
+ * @see MediaWikiServices::resetGlobalInstance()
+ * @see prepareServices()
+ * @see overrideMwServices()
+ *
+ * @param Config|null $bootstrapConfig The bootstrap config to use with the new
+ * MediaWikiServices.
+ */
+ protected static function resetGlobalServices( Config $bootstrapConfig = null ) {
+ $oldServices = MediaWikiServices::getInstance();
+ $oldConfigFactory = $oldServices->getConfigFactory();
+
+ $testConfig = self::makeTestConfig( $bootstrapConfig );
+
+ MediaWikiServices::resetGlobalInstance( $testConfig );
+
+ self::$serviceLocator = MediaWikiServices::getInstance();
+ self::installTestServices(
+ $oldConfigFactory,
+ self::$serviceLocator
+ );
+ }
+
+ /**
+ * Create a config suitable for testing, based on a base config, default overrides,
+ * and custom overrides.
+ *
+ * @param Config|null $baseConfig
+ * @param Config|null $customOverrides
+ *
+ * @return Config
+ */
+ private static function makeTestConfig(
+ Config $baseConfig = null,
+ Config $customOverrides = null
+ ) {
+ $defaultOverrides = new HashConfig();
+
+ if ( !$baseConfig ) {
+ $baseConfig = MediaWikiServices::getInstance()->getBootstrapConfig();
+ }
+
/* Some functions require some kind of caching, and will end up using the db,
* which we can't allow, as that would open a new connection for mysql.
* Replace with a HashBag. They would not be going to persist anyway.
*/
- ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
+ $hashCache = [ 'class' => 'HashBagOStuff' ];
+ $objectCaches = [
+ CACHE_DB => $hashCache,
+ CACHE_ACCEL => $hashCache,
+ CACHE_MEMCACHED => $hashCache,
+ 'apc' => $hashCache,
+ 'xcache' => $hashCache,
+ 'wincache' => $hashCache,
+ ] + $baseConfig->get( 'ObjectCaches' );
+
+ $defaultOverrides->set( 'ObjectCaches', $objectCaches );
+ $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
+
+ $testConfig = $customOverrides
+ ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
+ : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
+
+ return $testConfig;
+ }
+
+ /**
+ * @param ConfigFactory $oldConfigFactory
+ * @param MediaWikiServices $newServices
+ *
+ * @throws MWException
+ */
+ private static function installTestServices(
+ ConfigFactory $oldConfigFactory,
+ MediaWikiServices $newServices
+ ) {
+ // Use bootstrap config for all configuration.
+ // This allows config overrides via global variables to take effect.
+ $bootstrapConfig = $newServices->getBootstrapConfig();
+ $newServices->resetServiceForTesting( 'ConfigFactory' );
+ $newServices->redefineService(
+ 'ConfigFactory',
+ self::makeTestConfigFactoryInstantiator(
+ $oldConfigFactory,
+ [ 'main' => $bootstrapConfig ]
+ )
+ );
+ }
+
+ /**
+ * @param ConfigFactory $oldFactory
+ * @param Config[] $configurations
+ *
+ * @return Closure
+ */
+ private static function makeTestConfigFactoryInstantiator(
+ ConfigFactory $oldFactory,
+ array $configurations
+ ) {
+ return function( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
+ $factory = new ConfigFactory();
+
+ // clone configurations from $oldFactory that are not overwritten by $configurations
+ $namesToClone = array_diff(
+ $oldFactory->getConfigNames(),
+ array_keys( $configurations )
+ );
+
+ foreach ( $namesToClone as $name ) {
+ $factory->register( $name, $oldFactory->makeConfig( $name ) );
+ }
+
+ foreach ( $configurations as $name => $config ) {
+ $factory->register( $name, $config );
+ }
+
+ return $factory;
+ };
+ }
+
+ /**
+ * Resets some well known services that typically have state that may interfere with unit tests.
+ * This is a lightweight alternative to resetGlobalServices().
+ *
+ * @note There is no guarantee that no references remain to stale service instances destroyed
+ * by a call to doLightweightServiceReset().
+ *
+ * @throws MWException if called outside of PHPUnit tests.
+ *
+ * @see resetGlobalServices()
+ */
+ private function doLightweightServiceReset() {
+ global $wgRequest;
- // Sandbox APC by replacing with in-process hash instead.
- // Ensures values are removed between tests.
- ObjectCache::$instances['apc'] =
- ObjectCache::$instances['xcache'] =
- ObjectCache::$instances['wincache'] = new HashBagOStuff;
+ JobQueueGroup::destroySingletons();
+ ObjectCache::clear();
+ FileBackendGroup::destroySingleton();
+
+ // TODO: move global state into MediaWikiServices
+ RequestContext::resetMain();
+ MediaHandler::resetCache();
+ if ( session_id() !== '' ) {
+ session_write_close();
+ session_id( '' );
+ }
+
+ $wgRequest = new FauxRequest();
+ MediaWiki\Session\SessionManager::resetCache();
+ }
+
+ public function run( PHPUnit_Framework_TestResult $result = null ) {
+ // Reset all caches between tests.
+ $this->doLightweightServiceReset();
$needsResetDB = false;
}
$this->mwGlobals = [];
$this->restoreLoggers();
+
+ if ( self::$serviceLocator && MediaWikiServices::getInstance() !== self::$serviceLocator ) {
+ MediaWikiServices::forceGlobalInstance( self::$serviceLocator );
+ }
+
+ // TODO: move global state into MediaWikiServices
RequestContext::resetMain();
MediaHandler::resetCache();
if ( session_id() !== '' ) {
);
}
+ /**
+ * Sets a service, maintaining a stashed version of the previous service to be
+ * restored in tearDown
+ *
+ * @since 1.27
+ *
+ * @param string $name
+ * @param object $object
+ */
+ protected function setService( $name, $object ) {
+ // If we did not yet override the service locator, so so now.
+ if ( MediaWikiServices::getInstance() === self::$serviceLocator ) {
+ $this->overrideMwServices();
+ }
+
+ MediaWikiServices::getInstance()->disableService( $name );
+ MediaWikiServices::getInstance()->redefineService(
+ $name,
+ function () use ( $object ) {
+ return $object;
+ }
+ );
+ }
+
/**
* Sets a global, maintaining a stashed version of the previous global to be
* restored in tearDown
* @param mixed $value Value to set the global to (ignored
* if an array is given as first argument).
*
+ * @note To allow changes to global variables to take effect on global service instances,
+ * call overrideMwServices().
+ *
* @since 1.21
*/
protected function setMwGlobals( $pairs, $value = null ) {
* @param array|string $globalKeys Key to the global variable, or an array of keys.
*
* @throws Exception When trying to stash an unset global
+ *
+ * @note To allow changes to global variables to take effect on global service instances,
+ * call overrideMwServices().
+ *
* @since 1.23
*/
protected function stashMwGlobals( $globalKeys ) {
*
* @throws MWException If the designated global is not an array.
*
+ * @note To allow changes to global variables to take effect on global service instances,
+ * call overrideMwServices().
+ *
* @since 1.21
*/
protected function mergeMwGlobalArrayValue( $name, $values ) {
$this->setMwGlobals( $name, $merged );
}
+ /**
+ * Stashes the global instance of MediaWikiServices, and installs a new one,
+ * allowing test cases to override settings and services.
+ * The previous instance of MediaWikiServices will be restored on tearDown.
+ *
+ * @since 1.27
+ *
+ * @param Config $configOverrides Configuration overrides for the new MediaWikiServices instance.
+ * @param callable[] $services An associative array of services to re-define. Keys are service
+ * names, values are callables.
+ *
+ * @return MediaWikiServices
+ * @throws MWException
+ */
+ protected function overrideMwServices( Config $configOverrides = null, array $services = [] ) {
+ if ( !$configOverrides ) {
+ $configOverrides = new HashConfig();
+ }
+
+ $oldInstance = MediaWikiServices::getInstance();
+ $oldConfigFactory = $oldInstance->getConfigFactory();
+
+ $testConfig = self::makeTestConfig( null, $configOverrides );
+ $newInstance = new MediaWikiServices( $testConfig );
+
+ // Load the default wiring from the specified files.
+ // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
+ $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
+ $newInstance->loadWiringFiles( $wiringFiles );
+
+ // Provide a traditional hook point to allow extensions to configure services.
+ Hooks::run( 'MediaWikiServices', [ $newInstance ] );
+
+ foreach ( $services as $name => $callback ) {
+ $newInstance->redefineService( $name, $callback );
+ }
+
+ self::installTestServices(
+ $oldConfigFactory,
+ $newInstance
+ );
+ MediaWikiServices::forceGlobalInstance( $newInstance );
+
+ return $newInstance;
+ }
+
/**
* @since 1.27
* @param string|Language $lang
* @param LoggerInterface $logger
*/
protected function setLogger( $channel, LoggerInterface $logger ) {
+ // TODO: Once loggers are managed by MediaWikiServices, use
+ // overrideMwServices() to set loggers.
+
$provider = LoggerFactory::getProvider();
$wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
$singletons = $wrappedProvider->singletons;
$user = User::newFromName( 'UTSysop' );
$comment = __METHOD__ . ': Sample page for unit test.';
+ // Avoid memory leak...?
+ // LinkCache::singleton()->clear();
+ // Maybe. But doing this absolutely breaks $title->isRedirect() when called during unit tests....
+
$page = WikiPage::factory( $title );
$page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
return;
}
+ // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
+ // and DatabaseBase no longer use global state.
+
self::$dbSetup = true;
if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
<?php
use Liuggio\StatsdClient\Factory\StatsdDataFactory;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Services\ServiceDisabledException;
/**
* @covers MediaWiki\MediaWikiServices
*/
class MediaWikiServicesTest extends PHPUnit_Framework_TestCase {
+ /**
+ * @return Config
+ */
+ private function newTestConfig() {
+ $globalConfig = new GlobalVarConfig();
+
+ $testConfig = new HashConfig();
+ $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) );
+ $testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) );
+
+ return $testConfig;
+ }
+
+ /**
+ * @return MediaWikiServices
+ */
+ private function newMediaWikiServices( Config $config = null ) {
+ if ( $config === null ) {
+ $config = $this->newTestConfig();
+ }
+
+ $instance = new MediaWikiServices( $config );
+
+ // Load the default wiring from the specified files.
+ $wiringFiles = $config->get( 'ServiceWiringFiles' );
+ $instance->loadWiringFiles( $wiringFiles );
+
+ return $instance;
+ }
+
public function testGetInstance() {
$services = MediaWikiServices::getInstance();
$this->assertInstanceOf( 'MediaWiki\\MediaWikiServices', $services );
}
+ public function testForceGlobalInstance() {
+ $newServices = $this->newMediaWikiServices();
+ $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+ $this->assertInstanceOf( 'MediaWiki\\MediaWikiServices', $oldServices );
+ $this->assertNotSame( $oldServices, $newServices );
+
+ $theServices = MediaWikiServices::getInstance();
+ $this->assertSame( $theServices, $newServices );
+
+ MediaWikiServices::forceGlobalInstance( $oldServices );
+
+ $theServices = MediaWikiServices::getInstance();
+ $this->assertSame( $theServices, $oldServices );
+ }
+
+ public function testResetGlobalInstance() {
+ $newServices = $this->newMediaWikiServices();
+ $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+ MediaWikiServices::resetGlobalInstance( $this->newTestConfig() );
+ $theServices = MediaWikiServices::getInstance();
+
+ $this->assertNotSame( $theServices, $newServices );
+ $this->assertNotSame( $theServices, $oldServices );
+
+ MediaWikiServices::forceGlobalInstance( $oldServices );
+ }
+
+ public function testDisableStorageBackend() {
+ $newServices = $this->newMediaWikiServices();
+ $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+ $lbFactory = $this->getMockBuilder( 'LBFactorySimple' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $lbFactory->expects( $this->once() )
+ ->method( 'destroy' );
+
+ $newServices->redefineService(
+ 'DBLoadBalancerFactory',
+ function() use ( $lbFactory ) {
+ return $lbFactory;
+ }
+ );
+
+ // force the service to become active, so we can check that it does get destroyed
+ $newServices->getService( 'DBLoadBalancerFactory' );
+
+ MediaWikiServices::disableStorageBackend(); // should destroy DBLoadBalancerFactory
+
+ try {
+ MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
+ $this->fail( 'DBLoadBalancerFactory shoudl have been disabled' );
+ }
+ catch ( ServiceDisabledException $ex ) {
+ // ok, as expected
+ }
+ catch ( Throwable $ex ) {
+ $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
+ }
+
+ MediaWikiServices::forceGlobalInstance( $oldServices );
+ }
+
+ public function testResetChildProcessServices() {
+ $newServices = $this->newMediaWikiServices();
+ $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+ $lbFactory = $this->getMockBuilder( 'LBFactorySimple' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $lbFactory->expects( $this->once() )
+ ->method( 'destroy' );
+
+ $newServices->redefineService(
+ 'DBLoadBalancerFactory',
+ function() use ( $lbFactory ) {
+ return $lbFactory;
+ }
+ );
+
+ // force the service to become active, so we can check that it does get destroyed
+ $oldLBFactory = $newServices->getService( 'DBLoadBalancerFactory' );
+
+ MediaWikiServices::resetChildProcessServices();
+ $finalServices = MediaWikiServices::getInstance();
+
+ $newLBFactory = $finalServices->getService( 'DBLoadBalancerFactory' );
+
+ $this->assertNotSame( $oldLBFactory, $newLBFactory );
+
+ MediaWikiServices::forceGlobalInstance( $oldServices );
+ }
+
+ public function testResetServiceForTesting() {
+ $services = $this->newMediaWikiServices();
+ $serviceCounter = 0;
+
+ $services->defineService(
+ 'Test',
+ function() use ( &$serviceCounter ) {
+ $serviceCounter++;
+ $service = $this->getMock( 'MediaWiki\Services\DestructibleService' );
+ $service->expects( $this->once() )->method( 'destroy' );
+ return $service;
+ }
+ );
+
+ // This should do nothing. In particular, it should not create a service instance.
+ $services->resetServiceForTesting( 'Test' );
+ $this->assertEquals( 0, $serviceCounter, 'No service instance should be created yet.' );
+
+ $oldInstance = $services->getService( 'Test' );
+ $this->assertEquals( 1, $serviceCounter, 'A service instance should exit now.' );
+
+ // The old instance should be detached, and destroy() called.
+ $services->resetServiceForTesting( 'Test' );
+ $newInstance = $services->getService( 'Test' );
+
+ $this->assertNotSame( $oldInstance, $newInstance );
+
+ // Satisfy the expectation that destroy() is called also for the second service instance.
+ $newInstance->destroy();
+ }
+
+ public function testResetServiceForTesting_noDestroy() {
+ $services = $this->newMediaWikiServices();
+
+ $services->defineService(
+ 'Test',
+ function() {
+ $service = $this->getMock( 'MediaWiki\Services\DestructibleService' );
+ $service->expects( $this->never() )->method( 'destroy' );
+ return $service;
+ }
+ );
+
+ $oldInstance = $services->getService( 'Test' );
+
+ // The old instance should be detached, but destroy() not called.
+ $services->resetServiceForTesting( 'Test', false );
+ $newInstance = $services->getService( 'Test' );
+
+ $this->assertNotSame( $oldInstance, $newInstance );
+ }
+
public function provideGetters() {
- // NOTE: This should list all service getters defined in MediaWikiServices.
- // NOTE: For every test case defined here there should be a corresponding
- // test case defined in provideGetService().
- return [
- 'BootstrapConfig' => [ 'getBootstrapConfig', Config::class ],
- 'ConfigFactory' => [ 'getConfigFactory', ConfigFactory::class ],
- 'MainConfig' => [ 'getMainConfig', Config::class ],
- 'SiteStore' => [ 'getSiteStore', SiteStore::class ],
- 'SiteLookup' => [ 'getSiteLookup', SiteLookup::class ],
- 'StatsdDataFactory' => [ 'getStatsdDataFactory', StatsdDataFactory::class ],
- 'EventRelayerGroup' => [ 'getEventRelayerGroup', EventRelayerGroup::class ],
- 'SearchEngine' => [ 'newSearchEngine', SearchEngine::class ],
- 'SearchEngineFactory' => [ 'getSearchEngineFactory', SearchEngineFactory::class ],
- 'SearchEngineConfig' => [ 'getSearchEngineConfig', SearchEngineConfig::class ],
- 'SkinFactory' => [ 'getSkinFactory', SkinFactory::class ],
- ];
+ $getServiceCases = $this->provideGetService();
+ $getterCases = [];
+
+ // All getters should be named just like the service, with "get" added.
+ foreach ( $getServiceCases as $name => $case ) {
+ list( $service, $class ) = $case;
+ $getterCases[$name] = [
+ 'get' . $service,
+ $class,
+ ];
+ }
+
+ return $getterCases;
}
/**
'SearchEngineFactory' => [ 'SearchEngineFactory', SearchEngineFactory::class ],
'SearchEngineConfig' => [ 'SearchEngineConfig', SearchEngineConfig::class ],
'SkinFactory' => [ 'SkinFactory', SkinFactory::class ],
+ 'DBLoadBalancerFactory' => [ 'DBLoadBalancerFactory', 'LBFactory' ],
+ 'DBLoadBalancer' => [ 'DBLoadBalancer', 'LoadBalancer' ],
];
}
$name = 'TestService92834576';
- $this->setExpectedException( 'InvalidArgumentException' );
+ $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' );
$services->getService( $name );
}
+ public function testPeekService() {
+ $services = $this->newServiceContainer();
+
+ $services->defineService(
+ 'Foo',
+ function() {
+ return new stdClass();
+ }
+ );
+
+ $services->defineService(
+ 'Bar',
+ function() {
+ return new stdClass();
+ }
+ );
+
+ // trigger instantiation of Foo
+ $services->getService( 'Foo' );
+
+ $this->assertInternalType(
+ 'object',
+ $services->peekService( 'Foo' ),
+ 'Peek should return the service object if it had been accessed before.'
+ );
+
+ $this->assertNull(
+ $services->peekService( 'Bar' ),
+ 'Peek should return null if the service was never accessed.'
+ );
+ }
+
+ public function testPeekService_fail_unknown() {
+ $services = $this->newServiceContainer();
+
+ $name = 'TestService92834576';
+
+ $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' );
+
+ $services->peekService( $name );
+ }
+
public function testDefineService() {
$services = $this->newServiceContainer();
return $theService;
} );
- $this->setExpectedException( 'RuntimeException' );
+ $this->setExpectedException( 'MediaWiki\Services\ServiceAlreadyDefinedException' );
$services->defineService( $name, function() use ( $theService ) {
return $theService;
];
// loading the same file twice should fail, because
- $this->setExpectedException( 'RuntimeException' );
+ $this->setExpectedException( 'MediaWiki\Services\ServiceAlreadyDefinedException' );
$services->loadWiringFiles( $wiringFiles );
}
$theService = new stdClass();
$name = 'TestService92834576';
- $this->setExpectedException( 'RuntimeException' );
+ $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' );
$services->redefineService( $name, function() use ( $theService ) {
return $theService;
// create the service, so it can no longer be redefined
$services->getService( $name );
- $this->setExpectedException( 'RuntimeException' );
+ $this->setExpectedException( 'MediaWiki\Services\CannotReplaceActiveServiceException' );
+
+ $services->redefineService( $name, function() use ( $theService ) {
+ return $theService;
+ } );
+ }
+
+ public function testDisableService() {
+ $services = $this->newServiceContainer( [ 'Foo' ] );
+
+ $destructible = $this->getMock( 'MediaWiki\Services\DestructibleService' );
+ $destructible->expects( $this->once() )
+ ->method( 'destroy' );
+
+ $services->defineService( 'Foo', function() use ( $destructible ) {
+ return $destructible;
+ } );
+ $services->defineService( 'Bar', function() {
+ return new stdClass();
+ } );
+ $services->defineService( 'Qux', function() {
+ return new stdClass();
+ } );
+
+ // instantiate Foo and Bar services
+ $services->getService( 'Foo' );
+ $services->getService( 'Bar' );
+
+ // disable service, should call destroy() once.
+ $services->disableService( 'Foo' );
+
+ // disabled service should still be listed
+ $this->assertContains( 'Foo', $services->getServiceNames() );
+
+ // getting other services should still work
+ $services->getService( 'Bar' );
+
+ // disable non-destructible service, and not-yet-instantiated service
+ $services->disableService( 'Bar' );
+ $services->disableService( 'Qux' );
+
+ $this->assertNull( $services->peekService( 'Bar' ) );
+ $this->assertNull( $services->peekService( 'Qux' ) );
+
+ // disabled service should still be listed
+ $this->assertContains( 'Bar', $services->getServiceNames() );
+ $this->assertContains( 'Qux', $services->getServiceNames() );
+
+ // re-enable Bar service
+ $services->redefineService( 'Bar', function() {
+ return new stdClass();
+ } );
+
+ $services->getService( 'Bar' );
+
+ $this->setExpectedException( 'MediaWiki\Services\ServiceDisabledException' );
+ $services->getService( 'Qux' );
+ }
+
+ public function testDisableService_fail_undefined() {
+ $services = $this->newServiceContainer();
+
+ $theService = new stdClass();
+ $name = 'TestService92834576';
+
+ $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' );
$services->redefineService( $name, function() use ( $theService ) {
return $theService;
} );
}
+ public function testDestroy() {
+ $services = $this->newServiceContainer();
+
+ $destructible = $this->getMock( 'MediaWiki\Services\DestructibleService' );
+ $destructible->expects( $this->once() )
+ ->method( 'destroy' );
+
+ $services->defineService( 'Foo', function() use ( $destructible ) {
+ return $destructible;
+ } );
+
+ $services->defineService( 'Bar', function() {
+ return new stdClass();
+ } );
+
+ // create the service
+ $services->getService( 'Foo' );
+
+ // destroy the container
+ $services->destroy();
+
+ $this->setExpectedException( 'MediaWiki\Services\ContainerDisabledException' );
+ $services->getService( 'Bar' );
+ }
+
}
public function testRegister() {
$factory = new ConfigFactory();
$factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
- $this->assertTrue( true ); // No exception thrown
+ $this->assertInstanceOf( GlobalVarConfig::class, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterInvalid() {
+ $factory = new ConfigFactory();
$this->setExpectedException( 'InvalidArgumentException' );
$factory->register( 'invalid', 'Invalid callback' );
}
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterInstance() {
+ $config = GlobalVarConfig::newInstance();
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', $config );
+ $this->assertSame( $config, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterAgain() {
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $config1 = $factory->makeConfig( 'unittest' );
+
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $config2 = $factory->makeConfig( 'unittest' );
+
+ $this->assertNotSame( $config1, $config2 );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testGetConfigNames() {
+ $factory = new ConfigFactory();
+ $factory->register( 'foo', 'GlobalVarConfig::newInstance' );
+ $factory->register( 'bar', new HashConfig() );
+
+ $this->assertEquals( [ 'foo', 'bar' ], $factory->getConfigNames() );
+ }
+
/**
* @covers ConfigFactory::makeConfig
*/
public function testMakeConfig() {
$factory = new ConfigFactory();
$factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+
+ $conf = $factory->makeConfig( 'unittest' );
+ $this->assertInstanceOf( 'Config', $conf );
+ $this->assertSame( $conf, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfigFallback() {
+ $factory = new ConfigFactory();
+ $factory->register( '*', 'GlobalVarConfig::newInstance' );
$conf = $factory->makeConfig( 'unittest' );
$this->assertInstanceOf( 'Config', $conf );
}
* @covers ConfigFactory::getDefaultInstance
*/
public function testGetDefaultInstance() {
+ // NOTE: the global config factory returned here has been overwritten
+ // for operation in test mode. It may not reflect LocalSettings.
$factory = ConfigFactory::getDefaultInstance();
$this->assertInstanceOf( 'Config', $factory->makeConfig( 'main' ) );
-
- $this->setExpectedException( 'ConfigException' );
- $factory->makeConfig( 'xyzzy' );
}
+
}
// may break testing against floating point values
// treated with PHP's serialize()
ini_set( 'serialize_precision', 17 );
+
+ // TODO: we should call MediaWikiTestCase::prepareServices( new GlobalVarConfig() ) here.
+ // But PHPUnit may not be loaded yet, so we have to wait until just
+ // before PHPUnit_TextUI_Command::main() is executed at the end of this file.
}
public function execute() {
'Using HHVM ' . HHVM_VERSION . ' (' . PHP_VERSION . ")\n" :
'Using PHP ' . PHP_VERSION . "\n";
+// Prepare global services for unit tests.
+// FIXME: this should be done in the finalSetup() method,
+// but PHPUnit may not have been loaded at that point.
+MediaWikiTestCase::prepareServices( new GlobalVarConfig() );
+
$wgPhpUnitClass::main();