Merge "Correct PHP version in maintenance/dev/README"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 14 Mar 2018 14:09:01 +0000 (14:09 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 14 Mar 2018 14:09:01 +0000 (14:09 +0000)
41 files changed:
RELEASE-NOTES-1.31
includes/ServiceWiring.php
includes/db/MWLBFactory.php
includes/externalstore/ExternalStoreDB.php
includes/externalstore/ExternalStoreHttp.php
includes/externalstore/ExternalStoreMwstore.php
includes/installer/CliInstaller.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMssql.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabaseMysqli.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/lbfactory/ILBFactory.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderContext.php
languages/data/grammarTransformations/ru.json
languages/i18n/be-tarask.json
languages/i18n/br.json
languages/i18n/ckb.json
languages/i18n/gor.json
languages/i18n/he.json
languages/i18n/hy.json
languages/i18n/mr.json
languages/i18n/nb.json
languages/i18n/ro.json
languages/i18n/sr-ec.json
languages/i18n/tg-cyrl.json
languages/i18n/yue.json
maintenance/install.php
resources/src/mediawiki/mediawiki.js
tests/phpunit/includes/import/ImportLinkCacheIntegrationTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php
tests/phpunit/languages/classes/LanguageRuTest.php
tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js

index 7b2ece9..9ecfb3e 100644 (file)
@@ -66,6 +66,8 @@ production.
     the SQL query. The ActorMigration class may also be used to get feature-flagged
     information needed to access actor-related fields during the migration
     period.
+* The CLI installer (maintenance/install.php) learned to detect and include
+  extensions. Pass --with-extensions to enable that feature.
 
 === External library changes in 1.31 ===
 
index 08d343b..5131917 100644 (file)
@@ -61,7 +61,10 @@ return [
                );
                $class = MWLBFactory::getLBFactoryClass( $lbConf );
 
-               return new $class( $lbConf );
+               $instance = new $class( $lbConf );
+               MWLBFactory::setSchemaAliases( $instance );
+
+               return $instance;
        },
 
        'DBLoadBalancer' => function ( MediaWikiServices $services ) {
index 5c79117..f0a17f7 100644 (file)
@@ -23,6 +23,7 @@
 
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LBFactory;
 use Wikimedia\Rdbms\DatabaseDomain;
 
 /**
@@ -201,4 +202,30 @@ abstract class MWLBFactory {
 
                return $class;
        }
+
+       public static function setSchemaAliases( LBFactory $lbFactory ) {
+               $mainLB = $lbFactory->getMainLB();
+               $masterType = $mainLB->getServerType( $mainLB->getWriterIndex() );
+               if ( $masterType === 'mysql' ) {
+                       /**
+                        * When SQLite indexes were introduced in r45764, it was noted that
+                        * SQLite requires index names to be unique within the whole database,
+                        * not just within a schema. As discussed in CR r45819, to avoid the
+                        * need for a schema change on existing installations, the indexes
+                        * were implicitly mapped from the new names to the old names.
+                        *
+                        * This mapping can be removed if DB patches are introduced to alter
+                        * the relevant tables in existing installations. Note that because
+                        * this index mapping applies to table creation, even new installations
+                        * of MySQL have the old names (except for installations created during
+                        * a period where this mapping was inappropriately removed, see
+                        * T154872).
+                        */
+                       $lbFactory->setIndexAliases( [
+                               'ar_usertext_timestamp' => 'usertext_timestamp',
+                               'un_user_id' => 'user_id',
+                               'un_user_ip' => 'user_ip',
+                       ] );
+               }
+       }
 }
index ad0c217..5edb4b2 100644 (file)
@@ -27,7 +27,7 @@ use Wikimedia\Rdbms\DBConnRef;
 use Wikimedia\Rdbms\MaintainableDBConnRef;
 
 /**
- * DB accessable external objects.
+ * DB accessible external objects.
  *
  * In this system, each store "location" maps to a database "cluster".
  * The clusters must be defined in the normal LBFactory configuration.
index 3d812c9..879686f 100644 (file)
@@ -21,7 +21,7 @@
  */
 
 /**
- * Example class for HTTP accessable external objects.
+ * Example class for HTTP accessible external objects.
  * Only supports reading, not storing.
  *
  * @ingroup ExternalStorage
index 0c6d022..5d7155e 100644 (file)
@@ -21,7 +21,7 @@
  */
 
 /**
- * File backend accessable external objects.
+ * File backend accessible external objects.
  *
  * In this system, each store "location" maps to the name of a file backend.
  * The file backends must be defined in $wgFileBackends and must be global
index 32d2634..d5f0c67 100644 (file)
@@ -107,6 +107,11 @@ class CliInstaller extends Installer {
                        $this->setVar( '_AdminPassword', $option['pass'] );
                }
 
+               // Detect and inject any extension found
+               if ( isset( $options['with-extensions'] ) ) {
+                       $this->setVar( '_Extensions', array_keys( $installer->findExtensions() ) );
+               }
+
                // Set up the default skins
                $skins = array_keys( $this->findExtensions( 'skins' ) );
                $this->setVar( '_Skins', $skins );
index f26b985..acb21ed 100644 (file)
@@ -281,7 +281,7 @@ class DBConnRef implements IDatabase {
        }
 
        public function estimateRowCount(
-               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
@@ -618,6 +618,10 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
+       public function setIndexAliases( array $aliases ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
        /**
         * Clean up the connection when out of scope
         */
index 014c4af..53cf55c 100644 (file)
@@ -33,6 +33,7 @@ use Wikimedia\Timestamp\ConvertibleTimestamp;
 use Wikimedia;
 use BagOStuff;
 use HashBagOStuff;
+use LogicException;
 use InvalidArgumentException;
 use Exception;
 use RuntimeException;
@@ -62,27 +63,35 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var string Whether lock granularity is on the level of the entire database */
        const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
 
+       /** @var int New Database instance will not be connected yet when returned */
+       const NEW_UNCONNECTED = 0;
+       /** @var int New Database instance will already be connected when returned */
+       const NEW_CONNECTED = 1;
+
        /** @var string SQL query */
        protected $lastQuery = '';
        /** @var float|bool UNIX timestamp of last write query */
        protected $lastWriteTime = false;
        /** @var string|bool */
        protected $phpError = false;
-       /** @var string */
+       /** @var string Server that this instance is currently connected to */
        protected $server;
-       /** @var string */
+       /** @var string User that this instance is currently connected under the name of */
        protected $user;
-       /** @var string */
+       /** @var string Password used to establish the current connection */
        protected $password;
-       /** @var string */
+       /** @var string Database that this instance is currently connected to */
        protected $dbName;
-       /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
+       /** @var array[] Map of (table => (dbname, schema, prefix) map) */
        protected $tableAliases = [];
+       /** @var string[] Map of (index alias => index) */
+       protected $indexAliases = [];
        /** @var bool Whether this PHP instance is for a CLI script */
        protected $cliMode;
        /** @var string Agent name for query profiling */
        protected $agent;
-
+       /** @var array Parameters used by initConnection() to establish a connection */
+       protected $connectionParams = [];
        /** @var BagOStuff APC cache */
        protected $srvCache;
        /** @var LoggerInterface */
@@ -244,18 +253,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected $nonNativeInsertSelectBatchSize = 10000;
 
        /**
-        * Constructor and database handle and attempt to connect to the DB server
-        *
-        * IDatabase classes should not be constructed directly in external
-        * code. Database::factory() should be used instead.
-        *
+        * @note: exceptions for missing libraries/drivers should be thrown in initConnection()
         * @param array $params Parameters passed from Database::factory()
         */
-       function __construct( array $params ) {
-               $server = $params['host'];
-               $user = $params['user'];
-               $password = $params['password'];
-               $dbName = $params['dbname'];
+       protected function __construct( array $params ) {
+               foreach ( [ 'host', 'user', 'password', 'dbname' ] as $name ) {
+                       $this->connectionParams[$name] = $params[$name];
+               }
 
                $this->schema = $params['schema'];
                $this->tablePrefix = $params['tablePrefix'];
@@ -291,13 +295,22 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
                // Set initial dummy domain until open() sets the final DB/prefix
                $this->currentDomain = DatabaseDomain::newUnspecified();
+       }
 
-               if ( $user ) {
-                       $this->open( $server, $user, $password, $dbName );
-               } elseif ( $this->requiresDatabaseUser() ) {
-                       throw new InvalidArgumentException( "No database user provided." );
+       /**
+        * Initialize the connection to the database over the wire (or to local files)
+        *
+        * @throws LogicException
+        * @throws InvalidArgumentException
+        * @throws DBConnectionError
+        * @since 1.31
+        */
+       final public function initConnection() {
+               if ( $this->isOpen() ) {
+                       throw new LogicException( __METHOD__ . ': already connected.' );
                }
-
+               // Establish the connection
+               $this->doInitConnection();
                // Set the domain object after open() sets the relevant fields
                if ( $this->dbName != '' ) {
                        // Domains with server scope but a table prefix are not used by IDatabase classes
@@ -305,6 +318,26 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
        }
 
+       /**
+        * Actually connect to the database over the wire (or to local files)
+        *
+        * @throws InvalidArgumentException
+        * @throws DBConnectionError
+        * @since 1.31
+        */
+       protected function doInitConnection() {
+               if ( strlen( $this->connectionParams['user'] ) ) {
+                       $this->open(
+                               $this->connectionParams['host'],
+                               $this->connectionParams['user'],
+                               $this->connectionParams['password'],
+                               $this->connectionParams['dbname']
+                       );
+               } else {
+                       throw new InvalidArgumentException( "No database user provided." );
+               }
+       }
+
        /**
         * Construct a Database subclass instance given a database type and parameters
         *
@@ -343,11 +376,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *   - agent: Optional name used to identify the end-user in query profiling/logging.
         *   - srvCache: Optional BagOStuff instance to an APC-style cache.
         *   - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT emulation.
+        * @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional]
         * @return Database|null If the database driver or extension cannot be found
         * @throws InvalidArgumentException If the database driver or extension cannot be found
         * @since 1.18
         */
-       final public static function factory( $dbType, $p = [] ) {
+       final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
                $class = self::getClass( $dbType, isset( $p['driver'] ) ? $p['driver'] : null );
 
                if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
@@ -380,7 +414,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                };
                        }
 
+                       /** @var Database $conn */
                        $conn = new $class( $p );
+                       if ( $connect == self::NEW_CONNECTED ) {
+                               $conn->initConnection();
+                       }
                } else {
                        $conn = null;
                }
@@ -607,7 +645,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function writesOrCallbacksPending() {
                return $this->trxLevel && (
-                       $this->trxDoneWrites || $this->trxIdleCallbacks || $this->trxPreCommitCallbacks
+                       $this->trxDoneWrites ||
+                       $this->trxIdleCallbacks ||
+                       $this->trxPreCommitCallbacks ||
+                       $this->trxEndCallbacks
                );
        }
 
@@ -810,21 +851,38 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function close() {
                if ( $this->conn ) {
+                       // Resolve any dangling transaction first
                        if ( $this->trxLevel() ) {
+                               // Meaningful transactions should ideally have been resolved by now
+                               if ( $this->writesOrCallbacksPending() ) {
+                                       $this->queryLogger->warning(
+                                               __METHOD__ . ": writes or callbacks still pending.",
+                                               [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
+                                       );
+                               }
+                               // Check if it is possible to properly commit and trigger callbacks
+                               if ( $this->trxEndCallbacksSuppressed ) {
+                                       throw new DBUnexpectedError(
+                                               $this,
+                                               __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
+                                       );
+                               }
+                               // Commit the changes and run any callbacks as needed
                                $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
                        }
-
+                       // Close the actual connection in the binding handle
                        $closed = $this->closeConnection();
                        $this->conn = false;
-               } elseif (
-                       $this->trxIdleCallbacks ||
-                       $this->trxPreCommitCallbacks ||
-                       $this->trxEndCallbacks
-               ) { // sanity
-                       throw new RuntimeException( "Transaction callbacks still pending." );
+                       // Sanity check that no callbacks are dangling
+                       if (
+                               $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks
+                       ) {
+                               throw new RuntimeException( "Transaction callbacks still pending." );
+                       }
                } else {
-                       $closed = true;
+                       $closed = true; // already closed; nothing to do
                }
+
                $this->opened = false;
 
                return $closed;
@@ -859,11 +917,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        /**
-        * The DBMS-dependent part of query()
+        * Run a query and return a DBMS-dependent wrapper (that has all IResultWrapper methods)
+        *
+        * This might return things, such as mysqli_result, that do not formally implement
+        * IResultWrapper, but nonetheless implement all of its methods correctly
         *
         * @param string $sql SQL query.
-        * @return ResultWrapper|bool Result object to feed to fetchObject,
-        *   fetchRow, ...; or false on failure
+        * @return IResultWrapper|bool Iterator to feed to fetchObject/fetchRow; false on failure
         */
        abstract protected function doQuery( $sql );
 
@@ -1528,10 +1588,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function estimateRowCount(
-               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        ) {
                $rows = 0;
-               $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
+               $res = $this->select(
+                       $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
+               );
 
                if ( $res ) {
                        $row = $this->fetchRow( $res );
@@ -2220,7 +2282,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @return string
         */
        protected function indexName( $index ) {
-               return $index;
+               return isset( $this->indexAliases[$index] )
+                       ? $this->indexAliases[$index]
+                       : $index;
        }
 
        public function addQuotes( $s ) {
@@ -3875,12 +3939,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->tableAliases = $aliases;
        }
 
-       /**
-        * @return bool Whether a DB user is required to access the DB
-        * @since 1.28
-        */
-       protected function requiresDatabaseUser() {
-               return true;
+       public function setIndexAliases( array $aliases ) {
+               $this->indexAliases = $aliases;
        }
 
        /**
@@ -3890,7 +3950,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * This catches broken callers than catch and ignore disconnection exceptions.
         * Unlike checking isOpen(), this is safe to call inside of open().
         *
-        * @return resource|object
+        * @return mixed
         * @throws DBUnexpectedError
         * @since 1.26
         */
index b6428c7..885880a 100644 (file)
@@ -509,15 +509,16 @@ class DatabaseMssql extends Database {
         * @param string $conds
         * @param string $fname
         * @param array $options
+        * @param array $join_conds
         * @return int
         */
        public function estimateRowCount( $table, $vars = '*', $conds = '',
-               $fname = __METHOD__, $options = []
+               $fname = __METHOD__, $options = [], $join_conds = []
        ) {
                // http://msdn2.microsoft.com/en-us/library/aa259203.aspx
                $options['EXPLAIN'] = true;
                $options['FOR COUNT'] = true;
-               $res = $this->select( $table, $vars, $conds, $fname, $options );
+               $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
 
                $rows = -1;
                if ( $res ) {
index 8fb8db5..b7778b4 100644 (file)
@@ -562,13 +562,14 @@ abstract class DatabaseMysqlBase extends Database {
         * @param string|array $conds
         * @param string $fname
         * @param string|array $options
+        * @param array $join_conds
         * @return bool|int
         */
        public function estimateRowCount( $table, $vars = '*', $conds = '',
-               $fname = __METHOD__, $options = []
+               $fname = __METHOD__, $options = [], $join_conds = []
        ) {
                $options['EXPLAIN'] = true;
-               $res = $this->select( $table, $vars, $conds, $fname, $options );
+               $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
                if ( $res === false ) {
                        return false;
                }
@@ -1418,40 +1419,6 @@ abstract class DatabaseMysqlBase extends Database {
                return in_array( $name, $this->listViews( $prefix ) );
        }
 
-       /**
-        * Allows for index remapping in queries where this is not consistent across DBMS
-        *
-        * @param string $index
-        * @return string
-        */
-       protected function indexName( $index ) {
-               /**
-                * When SQLite indexes were introduced in r45764, it was noted that
-                * SQLite requires index names to be unique within the whole database,
-                * not just within a schema. As discussed in CR r45819, to avoid the
-                * need for a schema change on existing installations, the indexes
-                * were implicitly mapped from the new names to the old names.
-                *
-                * This mapping can be removed if DB patches are introduced to alter
-                * the relevant tables in existing installations. Note that because
-                * this index mapping applies to table creation, even new installations
-                * of MySQL have the old names (except for installations created during
-                * a period where this mapping was inappropriately removed, see
-                * T154872).
-                */
-               $renamed = [
-                       'ar_usertext_timestamp' => 'usertext_timestamp',
-                       'un_user_id' => 'user_id',
-                       'un_user_ip' => 'user_ip',
-               ];
-
-               if ( isset( $renamed[$index] ) ) {
-                       return $renamed[$index];
-               } else {
-                       return $index;
-               }
-       }
-
        protected function isTransactableQuery( $sql ) {
                return parent::isTransactableQuery( $sql ) &&
                        !preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
index 984e1c0..0a5450c 100644 (file)
@@ -37,7 +37,7 @@ use stdClass;
 class DatabaseMysqli extends DatabaseMysqlBase {
        /**
         * @param string $sql
-        * @return resource
+        * @return mysqli_result
         */
        protected function doQuery( $sql ) {
                $conn = $this->getBindingHandle();
@@ -332,6 +332,13 @@ class DatabaseMysqli extends DatabaseMysqlBase {
                        return (string)$this->conn;
                }
        }
+
+       /**
+        * @return mysqli
+        */
+       protected function getBindingHandle() {
+               return parent::getBindingHandle();
+       }
 }
 
 class_alias( DatabaseMysqli::class, 'DatabaseMysqli' );
index 38cc4ae..7b2ef83 100644 (file)
@@ -413,13 +413,14 @@ class DatabasePostgres extends Database {
         * @param string $conds
         * @param string $fname
         * @param array $options
+        * @param array $join_conds
         * @return int
         */
        public function estimateRowCount( $table, $vars = '*', $conds = '',
-               $fname = __METHOD__, $options = []
+               $fname = __METHOD__, $options = [], $join_conds = []
        ) {
                $options['EXPLAIN'] = true;
-               $res = $this->select( $table, $vars, $conds, $fname, $options );
+               $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
                $rows = -1;
                if ( $res ) {
                        $row = $this->fetchRow( $res );
index 83c8814..d0d62e9 100644 (file)
@@ -69,24 +69,13 @@ class DatabaseSqlite extends Database {
         */
        function __construct( array $p ) {
                if ( isset( $p['dbFilePath'] ) ) {
-                       parent::__construct( $p );
-                       // Standalone .sqlite file mode.
-                       // Super doesn't open when $user is false, but we can work with $dbName,
-                       // which is derived from the file path in this case.
-                       $this->openFile( $p['dbFilePath'] );
-                       $lockDomain = md5( $p['dbFilePath'] );
-               } elseif ( !isset( $p['dbDirectory'] ) ) {
-                       throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
-               } else {
+                       $this->dbPath = $p['dbFilePath'];
+                       $lockDomain = md5( $this->dbPath );
+               } elseif ( isset( $p['dbDirectory'] ) ) {
                        $this->dbDir = $p['dbDirectory'];
-                       $this->dbName = $p['dbname'];
-                       $lockDomain = $this->dbName;
-                       // Stock wiki mode using standard file names per DB.
-                       parent::__construct( $p );
-                       // Super doesn't open when $user is false, but we can work with $dbName
-                       if ( $p['dbname'] && !$this->isOpen() ) {
-                               $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] );
-                       }
+                       $lockDomain = $p['dbname'];
+               } else {
+                       throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
                }
 
                $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
@@ -101,6 +90,8 @@ class DatabaseSqlite extends Database {
                        'domain' => $lockDomain,
                        'lockDirectory' => "{$this->dbDir}/locks"
                ] );
+
+               parent::__construct( $p );
        }
 
        protected static function getAttributes() {
@@ -126,6 +117,28 @@ class DatabaseSqlite extends Database {
                return $db;
        }
 
+       protected function doInitConnection() {
+               if ( $this->dbPath !== null ) {
+                       // Standalone .sqlite file mode.
+                       $this->openFile( $this->dbPath );
+               } elseif ( $this->dbDir !== null ) {
+                       // Stock wiki mode using standard file names per DB
+                       if ( strlen( $this->connectionParams['dbname'] ) ) {
+                               $this->open(
+                                       $this->connectionParams['host'],
+                                       $this->connectionParams['user'],
+                                       $this->connectionParams['password'],
+                                       $this->connectionParams['dbname']
+                               );
+                       } else {
+                               // Caller will manually call open() later?
+                               $this->connLogger->debug( __METHOD__ . ': no database opened.' );
+                       }
+               } else {
+                       throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
+               }
+       }
+
        /**
         * @return string
         */
@@ -146,7 +159,7 @@ class DatabaseSqlite extends Database {
         *  NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
         *
         * @param string $server
-        * @param string $user
+        * @param string $user Unused
         * @param string $pass
         * @param string $dbName
         *
@@ -162,6 +175,10 @@ class DatabaseSqlite extends Database {
                }
                $this->openFile( $fileName );
 
+               if ( $this->conn ) {
+                       $this->dbName = $dbName;
+               }
+
                return (bool)$this->conn;
        }
 
@@ -192,7 +209,7 @@ class DatabaseSqlite extends Database {
                        throw new DBConnectionError( $this, $err );
                }
 
-               $this->opened = !!$this->conn;
+               $this->opened = is_object( $this->conn );
                if ( $this->opened ) {
                        # Set error codes only, don't raise exceptions
                        $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
@@ -315,7 +332,7 @@ class DatabaseSqlite extends Database {
         * @return bool|ResultWrapper
         */
        protected function doQuery( $sql ) {
-               $res = $this->conn->query( $sql );
+               $res = $this->getBindingHandle()->query( $sql );
                if ( $res === false ) {
                        return false;
                }
@@ -451,7 +468,7 @@ class DatabaseSqlite extends Database {
         */
        function insertId() {
                // PDO::lastInsertId yields a string :(
-               return intval( $this->conn->lastInsertId() );
+               return intval( $this->getBindingHandle()->lastInsertId() );
        }
 
        /**
@@ -724,7 +741,7 @@ class DatabaseSqlite extends Database {
         * @return string Version information from the database
         */
        function getServerVersion() {
-               $ver = $this->conn->getAttribute( PDO::ATTR_SERVER_VERSION );
+               $ver = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
 
                return $ver;
        }
@@ -814,7 +831,7 @@ class DatabaseSqlite extends Database {
                        );
                        return "x'" . bin2hex( (string)$s ) . "'";
                } else {
-                       return $this->conn->quote( (string)$s );
+                       return $this->getBindingHandle()->quote( (string)$s );
                }
        }
 
@@ -1059,15 +1076,20 @@ class DatabaseSqlite extends Database {
                }
        }
 
-       protected function requiresDatabaseUser() {
-               return false; // just a file
-       }
-
        /**
         * @return string
         */
        public function __toString() {
-               return 'SQLite ' . (string)$this->conn->getAttribute( PDO::ATTR_SERVER_VERSION );
+               return is_object( $this->conn )
+                       ? 'SQLite ' . (string)$this->conn->getAttribute( PDO::ATTR_SERVER_VERSION )
+                       : '(not connected)';
+       }
+
+       /**
+        * @return PDO
+        */
+       protected function getBindingHandle() {
+               return parent::getBindingHandle();
        }
 }
 
index 28a8125..09abaa8 100644 (file)
@@ -357,7 +357,7 @@ interface IDatabase {
        public function getType();
 
        /**
-        * Open a connection to the database. Usually aborts on failure
+        * Open a new connection to the database (closing any existing one)
         *
         * @param string $server Database server host
         * @param string $user Database user name
@@ -492,8 +492,11 @@ interface IDatabase {
        public function getServerVersion();
 
        /**
-        * Closes a database connection.
-        * if it is open : commits any open transactions
+        * Close the database connection
+        *
+        * This should only be called after any transactions have been resolved,
+        * aside from read-only transactions (assuming no callbacks are registered).
+        * If a transaction is still open anyway, it will be committed if possible.
         *
         * @throws DBError
         * @return bool Operation success. true if already closed.
@@ -822,11 +825,12 @@ interface IDatabase {
         * @param array|string $conds Filters on the table
         * @param string $fname Function name for profiling
         * @param array $options Options for select
+        * @param array|string $join_conds Join conditions
         * @return int Row count
         * @throws DBError
         */
        public function estimateRowCount(
-               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        );
 
        /**
@@ -1927,6 +1931,21 @@ interface IDatabase {
         * @since 1.28
         */
        public function setTableAliases( array $aliases );
+
+       /**
+        * Convert certain index names to alternative names before querying the DB
+        *
+        * Note that this applies to indexes regardless of the table they belong to.
+        *
+        * This can be employed when an index was renamed X => Y in code, but the new Y-named
+        * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+        * the aliases can be removed, and then the old X-named indexes dropped.
+        *
+        * @param string[] $aliases
+        * @return mixed
+        * @since 1.31
+        */
+       public function setIndexAliases( array $aliases );
 }
 
 class_alias( IDatabase::class, 'IDatabase' );
index 98108a7..32d9008 100644 (file)
@@ -42,19 +42,19 @@ interface ILBFactory {
         *
         * @param array $conf Array with keys:
         *  - localDomain: A DatabaseDomain or domain ID string.
-        *  - readOnlyReason : Reason the master DB is read-only if so [optional]
-        *  - srvCache : BagOStuff object for server cache [optional]
-        *  - memStash : BagOStuff object for cross-datacenter memory storage [optional]
-        *  - wanCache : WANObjectCache object [optional]
-        *  - hostname : The name of the current server [optional]
+        *  - readOnlyReason: Reason the master DB is read-only if so [optional]
+        *  - srvCache: BagOStuff object for server cache [optional]
+        *  - memStash: BagOStuff object for cross-datacenter memory storage [optional]
+        *  - wanCache: WANObjectCache object [optional]
+        *  - hostname: The name of the current server [optional]
         *  - cliMode: Whether the execution context is a CLI script. [optional]
-        *  - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
+        *  - profiler: Class name or instance with profileIn()/profileOut() methods. [optional]
         *  - trxProfiler: TransactionProfiler instance. [optional]
         *  - replLogger: PSR-3 logger instance. [optional]
         *  - connLogger: PSR-3 logger instance. [optional]
         *  - queryLogger: PSR-3 logger instance. [optional]
         *  - perfLogger: PSR-3 logger instance. [optional]
-        *  - errorLogger : Callback that takes an Exception and logs it. [optional]
+        *  - errorLogger: Callback that takes an Exception and logs it. [optional]
         * @throws InvalidArgumentException
         */
        public function __construct( array $conf );
@@ -323,4 +323,34 @@ interface ILBFactory {
         *   - ChronologyPositionIndex: timestamp used to get up-to-date DB positions for the agent
         */
        public function setRequestInfo( array $info );
+
+       /**
+        * Make certain table names use their own database, schema, and table prefix
+        * when passed into SQL queries pre-escaped and without a qualified database name
+        *
+        * For example, "user" can be converted to "myschema.mydbname.user" for convenience.
+        * Appearances like `user`, somedb.user, somedb.someschema.user will used literally.
+        *
+        * Calling this twice will completely clear any old table aliases. Also, note that
+        * callers are responsible for making sure the schemas and databases actually exist.
+        *
+        * @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
+        * @since 1.31
+        */
+       public function setTableAliases( array $aliases );
+
+       /**
+        * Convert certain index names to alternative names before querying the DB
+        *
+        * Note that this applies to indexes regardless of the table they belong to.
+        *
+        * This can be employed when an index was renamed X => Y in code, but the new Y-named
+        * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+        * the aliases can be removed, and then the old X-named indexes dropped.
+        *
+        * @param string[] $aliases
+        * @return mixed
+        * @since 1.31
+        */
+       public function setIndexAliases( array $aliases );
 }
index 2324553..32886e2 100644 (file)
@@ -75,6 +75,11 @@ abstract class LBFactory implements ILBFactory {
        /** @var callable[] */
        protected $replicationWaitCallbacks = [];
 
+       /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
+       protected $tableAliases = [];
+       /** @var string[] Map of (index alias => index) */
+       protected $indexAliases = [];
+
        /** @var bool Whether this PHP instance is for a CLI script */
        protected $cliMode;
        /** @var string Agent name for query profiling */
@@ -523,6 +528,17 @@ abstract class LBFactory implements ILBFactory {
                if ( $this->trxRoundId !== false ) {
                        $lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX
                }
+
+               $lb->setTableAliases( $this->tableAliases );
+               $lb->setIndexAliases( $this->indexAliases );
+       }
+
+       public function setTableAliases( array $aliases ) {
+               $this->tableAliases = $aliases;
+       }
+
+       public function setIndexAliases( array $aliases ) {
+               $this->indexAliases = $aliases;
        }
 
        public function setDomainPrefix( $prefix ) {
index 8210507..767cc49 100644 (file)
@@ -615,4 +615,19 @@ interface ILoadBalancer {
         * @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
         */
        public function setTableAliases( array $aliases );
+
+       /**
+        * Convert certain index names to alternative names before querying the DB
+        *
+        * Note that this applies to indexes regardless of the table they belong to.
+        *
+        * This can be employed when an index was renamed X => Y in code, but the new Y-named
+        * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+        * the aliases can be removed, and then the old X-named indexes dropped.
+        *
+        * @param string[] $aliases
+        * @return mixed
+        * @since 1.31
+        */
+       public function setIndexAliases( array $aliases );
 }
index 99a24c2..35198ac 100644 (file)
@@ -54,6 +54,8 @@ class LoadBalancer implements ILoadBalancer {
        private $loadMonitorConfig;
        /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
        private $tableAliases = [];
+       /** @var string[] Map of (index alias => index) */
+       private $indexAliases = [];
 
        /** @var ILoadMonitor */
        private $loadMonitor;
@@ -1088,6 +1090,7 @@ class LoadBalancer implements ILoadBalancer {
                        $this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
                );
                $db->setTableAliases( $this->tableAliases );
+               $db->setIndexAliases( $this->indexAliases );
 
                if ( $server['serverIndex'] === $this->getWriterIndex() ) {
                        if ( $this->trxRoundId !== false ) {
@@ -1757,6 +1760,10 @@ class LoadBalancer implements ILoadBalancer {
                $this->tableAliases = $aliases;
        }
 
+       public function setIndexAliases( array $aliases ) {
+               $this->indexAliases = $aliases;
+       }
+
        public function setDomainPrefix( $prefix ) {
                // Find connections to explicit foreign domains still marked as in-use...
                $domainsInUse = [];
index f9b03c7..5ddb99b 100644 (file)
@@ -1532,27 +1532,31 @@ MESSAGE;
        /**
         * Convert an array of module names to a packed query string.
         *
-        * For example, [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ]
-        * becomes 'foo.bar,baz|bar.baz,quux'
+        * For example, `[ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ]`
+        * becomes `'foo.bar,baz|bar.baz,quux'`.
+        *
+        * This process is reversed by ResourceLoaderContext::expandModuleNames().
+        * See also mw.loader#buildModulesString() which is a port of this, used
+        * on the client-side.
+        *
         * @param array $modules List of module names (strings)
         * @return string Packed query string
         */
        public static function makePackedModulesString( $modules ) {
-               $groups = []; // [ prefix => [ suffixes ] ]
+               $moduleMap = []; // [ prefix => [ suffixes ] ]
                foreach ( $modules as $module ) {
                        $pos = strrpos( $module, '.' );
                        $prefix = $pos === false ? '' : substr( $module, 0, $pos );
                        $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
-                       $groups[$prefix][] = $suffix;
+                       $moduleMap[$prefix][] = $suffix;
                }
 
                $arr = [];
-               foreach ( $groups as $prefix => $suffixes ) {
+               foreach ( $moduleMap as $prefix => $suffixes ) {
                        $p = $prefix === '' ? '' : $prefix . '.';
                        $arr[] = $p . implode( ',', $suffixes );
                }
-               $str = implode( '|', $arr );
-               return $str;
+               return implode( '|', $arr );
        }
 
        /**
index 7478266..370046a 100644 (file)
@@ -98,9 +98,12 @@ class ResourceLoaderContext implements MessageLocalizer {
        }
 
        /**
-        * Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to
-        * an array of module names like [ 'jquery.foo', 'jquery.bar',
-        * 'jquery.ui.baz', 'jquery.ui.quux' ]
+        * Expand a string of the form `jquery.foo,bar|jquery.ui.baz,quux` to
+        * an array of module names like `[ 'jquery.foo', 'jquery.bar',
+        * 'jquery.ui.baz', 'jquery.ui.quux' ]`.
+        *
+        * This process is reversed by ResourceLoader::makePackedModulesString().
+        *
         * @param string $modules Packed module name list
         * @return array Array of module names
         */
index deb58b7..8089118 100644 (file)
@@ -14,6 +14,7 @@
                [ "(.+)ды$", "$1дов" ],
                [ "(.+)д$", "$1да" ],
                [ "(.+)ник$", "$1ника" ],
+               [ "(.+)тет$", "$1тета" ],
                [ "(.+)ные$", "$1ных" ]
        ],
        "prepositional": [
@@ -24,6 +25,7 @@
                [ "(.+)ды$", "$1дах" ],
                [ "(.+)д$", "$1де" ],
                [ "(.+)ник$", "$1нике" ],
+               [ "(.+)тет$", "$1тете" ],
                [ "(.+)ные$", "$1ных" ]
        ],
        "languagegen": [
index cbb3295..d12c898 100644 (file)
        "right-editmywatchlist": "Рэдагаваньне ўласнага сьпісу назіраньня. Некаторыя дзеяньні будуць дадаваць туды старонкі нават бяз гэтага права.",
        "right-viewmyprivateinfo": "Праглядаць уласныя прыватныя зьвесткі (напрыклад, адрас электроннай пошты, сапраўднае імя)",
        "right-editmyprivateinfo": "Рэдагаваць уласныя прыватныя зьвесткі (напрыклад, адрас электроннай пошты, сапраўднае імя)",
-       "right-editmyoptions": "рэдагаваць уласныя налады",
+       "right-editmyoptions": "Рэдагаваць уласныя налады",
        "right-rollback": "хуткі адкат правак апошняга ўдзельніка, які рэдагаваў старонку",
        "right-markbotedits": "пазначэньне адкатаў як рэдагаваньне робатам",
        "right-noratelimit": "няма абмежаваньняў па хуткасьці",
index 736ee00..cc25a65 100644 (file)
        "permissionserrorstext-withaction": "N'oc'h ket aotreet da $2, evit an {{PLURAL:$1|abeg-mañ|abeg-mañ}} :",
        "contentmodelediterror": "N'hallit ket kemmañ an adweladenn-mañ peogwir ez eo par he fatrom danvez da <code>$1</code>, ar pezh zo disheñvel diouzh ar patrom danvez implijet bremañ war ar bajenn <code>$2</code>.",
        "recreate-moveddeleted-warn": "'''Diwallit : Emaoc'h o krouiñ ur bajenn zo bet diverket c'hoazh.'''\n\nEn em soñjit ervat ha talvoudus eo kenderc'hel krouiñ ar bajenn.\nDeoc'h da c'houzout, aze emañ ar marilhoù diverkañ hag adenvel :",
-       "moveddeleted-notice": "Diverket eo bet ar bajenn-mañ.\nDindan emañ ar marilh diverkañ hag adenvel.",
+       "moveddeleted-notice": "Diverket eo bet ar bajenn-mañ.\nDindan emañ ar marilh diverkañ, adenvel ha gwareziñ evit ar bajenn.",
        "moveddeleted-notice-recent": "Ho tigarez, nevez ziverket eo bet ar bajenn-mañ (e-kerzh an 24 eurvezh tremenet).\nDindan emañ ar marilhoù diverkañ hag adenvel evit ho kelaouiñ.",
        "log-fulllog": "Gwelet ar marilh klok",
        "edit-hook-aborted": "C'hwitet ar c'hemmañ gant un astenn.\nAbeg dianav.",
        "recentchangeslinked-feed": "Heuliañ ar pajennoù liammet",
        "recentchangeslinked-toolbox": "Heuliañ ar pajennoù liammet",
        "recentchangeslinked-title": "Kemmoù a denn da \"$1\"",
-       "recentchangeslinked-summary": "Rollet eo war ar bajenn dibar-mañ ar c'hemmoù diwezhañ bet degaset war ar pajennoù liammet ouzh ur bajenn lakaet (pe ouzh izili ur rummad lakaet).\nE <strong>tev</strong> emañ ar pajennoù zo war ho [[Special:Watchlist|roll evezhiañ]].",
+       "recentchangeslinked-summary": "Merkañ anv ur bajenn evit gwelet ar c'hemmoù war ar pajennoù liammet da pe adalek ar bajenn-se (evit gwelet izili ur rummad bennak, skrivañ Rummad:anv ar rummad).\nE <strong>tev</strong> emañ kemmoù ar pajennoù zo war ho [[Special:Watchlist|roll evezhiañ]].",
        "recentchangeslinked-page": "Anv ar bajenn :",
        "recentchangeslinked-to": "Diskouez ar c'hemmoù war-du ar pajennoù liammet kentoc'h eget re ar bajenn lakaet",
        "recentchanges-page-added-to-category": "[[:$1]] ouzhpennet d'ar rummad",
        "unwatchthispage": "Paouez da evezhiañ",
        "notanarticle": "Pennad ebet",
        "notvisiblerev": "Stumm diverket",
-       "watchlist-details": "Lakaet hoc'h eus {{PLURAL:$1|$1 bajenn|$1 a bajennoù}} war ho roll evezhiañ, anez kontañ ar pajennoù kaozeal.",
+       "watchlist-details": "Bez' ez eus {{PLURAL:$1|$1 bajenn|$1 a bajennoù}} war ho roll evezhiañ, (mui ar pajennoù kaozeal).",
        "wlheader-enotif": "Gweredekaet eo ar c'has posteloù.",
        "wlheader-showupdated": "E '''tev''' emañ merket ar pajennoù bet kemmet abaoe ar wezh ziwezhañ hoc'h eus sellet outo",
        "wlnote": "Setu aze {{PLURAL:$1|ar c'hemm diwezhañ|ar '''$1''' kemm diwezhañ}} c'hoarvezet e-kerzh an {{PLURAL:$2|eurvezh|'''$2''' eurvezh}} diwezhañ, d'an $3 da $4.",
index 9ea45d4..6b54759 100644 (file)
        "recentchangeslinked-feed": "گۆڕانکارییە پەیوەندیدارەکان",
        "recentchangeslinked-toolbox": "گۆڕانکارییە پەیوەندیدارەکان",
        "recentchangeslinked-title": "گۆڕانکارییە پەیوەندیدارەکان بە \"$1\" ـەوە",
-       "recentchangeslinked-summary": "ئەمە لیستێکی گۆڕانکارییەکانی ئەم دوایییانەی ئەو پەڕانەیە کە بەستەریان ھەیە لە پەڕەیەکی دیاریکراو (یان بۆ ئەندامەکانی پۆلێکی دیاریکراو)\nپەڕەکانی [[Special:Watchlist|لیستی چاودێرییەکەت]] '''ئەستوورن'''.",
+       "recentchangeslinked-summary": "ناوی پەڕەک داخل بکە بۆ بینینی گۆڕانکارییەکانی ئەو پەڕانەی کە بەستەریان ھەیە بۆ ئەو پەڕەیە یان لەو پەڕەیەوە پەیوەست کراون. (بۆ بینینی ئەندامەکانی پۆلێک، پۆل:ناوی پۆلەکە داخل بکە). گۆڕانکارییەکانی پەڕەکانی [[Special:Watchlist|لیستی چاودێرییەکەت]] <strong>ئەستوورن</strong>.",
        "recentchangeslinked-page": "ناوی پەڕە:",
        "recentchangeslinked-to": "بەجێگەی ئەوە گۆڕانکارییەکانی ئەو پەڕانە نیشانبدە کە بەستەریان ھەیە بۆ پەڕەی دیاریکراو",
        "recentchanges-page-added-to-category": "[[:$1]] زیادکرا بۆ پۆل",
        "unwatchthispage": "ئیتر چاودێری مەکە",
        "notanarticle": "پەڕەی بێ ناوەڕۆک",
        "notvisiblerev": "پیاچوونەوە سڕاوەتەوە",
-       "watchlist-details": "بێجگە لە پەڕەکانی لێدوان، {{PLURAL:$1|$1 پەڕە}} لە پێرستی {{PLURAL:$1|چاودێرییەکەتدایە|چاودێرییەکەتدان}}.",
+       "watchlist-details": "{{PLURAL:$1|$1 پەڕە}} لە پێرستی {{PLURAL:$1|چاودێرییەکەتدایە|چاودێرییەکەتدان}} (سەرەڕای پەڕەکانی لێدوان).",
        "wlheader-enotif": "ئاگاداری بە ئیمەیل چالاکە.",
        "wlheader-showupdated": "‏ئەو پەڕانە کە لە پاش دوایین سەردانت دەستکاری کراون بە <strong>ئەستوور</strong> نیشان دراون.",
        "wlnote": "خوارەوە {{PLURAL:$1|دوایین گۆڕانکارییە|دوایین <strong>$1</strong> گۆڕانکارییە}} لە دوایین {{PLURAL:$2|کاتژمێر|<strong>$2</strong> کاتژمێر}}دا ھەتا $4ی $3.",
        "widthheightpage": "$1 × $2، $3 {{PLURAL:$3|پەڕە|پەڕە}}",
        "file-info": "قه‌باره‌: $1, جۆر: $2",
        "file-info-size": "$1 × $2 پیکسێل، قەبارەی پەڕگە: $3، جۆری MIME: $4",
+       "file-info-size-pages": "$1 × $2 پیکسڵ، قەبارەی پەڕگە: $3، جۆری پەڕگە: $4، $5 {{PLURAL:$5|پەڕە}}.",
        "file-nohires": "رەزۆلوشنی سەرتر لەمە لە بەردەست دا نیە.",
        "svg-long-desc": "پەڕگەی SVG، بە ناو $1 × $2 پیکسەڵ، قەبارەی پەڕگە: $3",
        "svg-long-error": "پەڕگەی SVGی نادروست: $1",
        "version-libraries-description": "وەسف",
        "version-libraries-authors": "نووسەر",
        "redirect": "ڕەوانەکەر بە پێی پەڕگە، بەکارھێنەر، پەڕە، پێداچوونەوە یان پێناسەی لۆگ",
-       "redirect-summary": "ئەم پەڕە تایبەتە ڕەوانە دەکرێ بۆ پەڕگەیەک (ناوی پەڕگەکە)، پەڕەیەک (پێناسەی پێداچوونەوەیەک یان پێناسەی پەڕە) یان پەڕەیەکی بەکارھێنەر (پێناسەیەکی  ژمارەیی بەکارھێنەر). بەکارھێنان: [[{{#Special:Redirect}}/file/Example.jpg]]، [[{{#Special:Redirect}}/page/64308]]، [[{{#Special:Redirect}}/revision/328429]] یان [[{{#Special:Redirect}}/user/101]].",
+       "redirect-summary": "ئەم پەڕە تایبەتە ڕەوانە دەکرێ بۆ پەڕگەیەک (ناوی پەڕگەکە)، پەڕەیەک (پێناسەی پێداچوونەوەیەک یان پێناسەی پەڕە)، پەڕەیەکی بەکارھێنەر (پێناسەیەکی ژمارەیی بەکارھێنەر)، یان تۆمارێک (پێناسەی تۆمار). بەکارھێنان:\n[[{{#Special:Redirect}}/file/Example.jpg]]، [[{{#Special:Redirect}}/page/64308]]، [[{{#Special:Redirect}}/revision/328429]] ،[[{{#Special:Redirect}}/user/101]] یان [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "بڕۆ",
        "redirect-lookup": "گەڕان لە:",
        "redirect-value": "نرخ:",
index 15797db..8607c23 100644 (file)
        "tog-hideminor": "Wanto'a u biloli'a ngo'idi to'u lobohuwa",
        "tog-hidepatrolled": "Wanto'a u biloli'a lo patroli to'u lobohuwa",
        "tog-newpageshidepatrolled": "Wanto'a halaman patroli lonto daputari halaman bohu",
-       "tog-hidecategorization": "Wanto'a tayadu halaman",
+       "tog-hidecategorization": "Wanto'a dalala lo halaman",
        "tog-extendwatchlist": "Bu'ade daputari he'awasiyalo mopobilohu nga'amila u loboli'a, diila bo ubohu",
        "tog-usenewrc": "Tayade u biloli'o to bibilohu halaman lobohuwa wawu daputari he awasiyalo",
        "tog-numberheadings": "Otomatis modulade nomoro",
        "tog-showtoolbar": "Popobilohe pilakasi pomoli'o",
        "tog-editondblclick": "Boli'a halaman lo klik po'oluwo",
-       "tog-editsectiononrightclick": "Popohunawa momoli'a tayadu wolo mengeklik olowala to judul lo tayadu",
+       "tog-editsectiononrightclick": "Popohunawa momoli'a tayadu wolo motepu olowala to judul lo tayadu",
        "tog-watchcreations": "Duhengi halaman pilohutu'u wawu berkas diletohu ode daputari he awasiyalo",
        "tog-watchdefault": "Duhengi halaman wawu berkas biloli'o ode daputari he awasiya'u",
        "tog-watchmoves": "Duhengi halaman wawu berkas hileyi'u ode daputari he awasiya'u",
        "tog-watchdeletion": "Duhengi halaman wawu berkas yilulutu'u ode daputari he awasiya'u",
        "tog-watchuploads": "Duhengi berkas bohu u diletohu'u to daputari he'awasiyalo",
        "tog-watchrollback": "Duhengi halaman u pilohuwalingu'u ode daputari he awasiya'u",
-       "tog-minordefault": "Tandai nga'amila odelo biloli'o keke'ingo secara baku",
+       "tog-minordefault": "Tuwoti nga'amila odelo biloli'o kikingo secara baku",
        "tog-previewontop": "Popobilohe po'olo to'udiipo dosi momoli'o",
        "tog-previewonfirst": "Popobilohe po'olo to'u momoli'a bohuliyo",
-       "tog-enotifwatchlistpages": "Lawoli wa'u surel wonu halamani tuwawu u awasiya'u loboli'a",
+       "tog-enotifwatchlistpages": "Lawoli wa'u surel wonu halaman tuwawu u awasiya'u loboli'a",
        "tog-enotifusertalkpages": "Lawoli wa'u surel wonu halaman tombilu'u loboli'a",
        "tog-enotifminoredits": "Lawoli surel olo wa'u to'u lo'ubawa ngo'idi halaamani wawu berkas",
        "tog-enotifrevealaddr": "Popobilohe alamati lo surel ola'u to surel lopo'ota",
        "tog-shownumberswatching": "Popobilohe jumula lo ta he'awasiyalo",
        "tog-oldsig": "Pali lo ulu'umu masatiya",
-       "tog-fancysig": "Popopasiya pali lo'ulu'u odelo tuladuwiki (diyalu tuwawu pranala otomatis)",
+       "tog-fancysig": "Popopasiya pali lo'ulu'u odelo tuladuwiki (diyalu tuwawu wumbuta otomatis)",
        "tog-uselivepreview": "Popobilohe pratayang wawu ja detohe ulangi halaman",
-       "tog-forceeditsummary": "Popo'eelawa wa'u wonu dosi monguba diipo otuwa",
-       "tog-watchlisthideown": "Wantoa u iluba'u to daputari lo he'awasiyalo",
-       "tog-watchlisthidebots": "Wanto'a u iluba lo bot to daputari lo he'awasiyalo",
-       "tog-watchlisthideminor": "Wanto'a u iluba ngo'idi to daputari lo he'awasiyalo",
-       "tog-watchlisthideliu": "Wanto'a u iluba pengguna maso log to daputari he awasiyalo",
-       "tog-watchlistreloadautomatically": "Muwatiya ulangi daputari he awasiyalo secara otomatis timi'idu saringan lo'ubawa (JavaScript paraluwolo)",
-       "tog-watchlisthideanons": "Wanto'a u iluba lo pengguna anonim monto daputari he awasiyalo",
-       "tog-watchlisthidepatrolled": "Wanto'a u iluba patroli monto daputari he'awasiyalo",
-       "tog-watchlisthidecategorization": "Wanto'a kategori halaman",
+       "tog-forceeditsummary": "Popo'eelawa wa'u wonu dosi momoli'o diipo otuwa",
+       "tog-watchlisthideown": "Wantoa u biloli'u'u to daputari lo he'awasiyalo",
+       "tog-watchlisthidebots": "Wanto'a u biloli'o bot to daputari lo he'awasiyalo",
+       "tog-watchlisthideminor": "Wanto'a u loboli'a ngo'idi to daputari lo he'awasiyalo",
+       "tog-watchlisthideliu": "Wanto'a u biloli'o ta ohu'uwo tilumuwoto log to daputari he awasiyalo",
+       "tog-watchlistreloadautomatically": "Detohe ulangi daputari he awasiyalo secara otomatis timi'idu saringan loboli'a (JavaScript paraluwolo)",
+       "tog-watchlisthideanons": "Wanto'a u bilo;i'o ta ohu'uwo anonim monto daputari he awasiyalo",
+       "tog-watchlisthidepatrolled": "Wanto'a u biloli'o patroli monto daputari he'awasiyalo",
+       "tog-watchlisthidecategorization": "Wanto'a dalala lo halaman",
        "tog-ccmeonemails": "Lawoli wa'u wami lo surel u yilawou to tawu",
-       "tog-diffonly": "Ja popobilohe tuwango halaman iluba u bebedawa",
-       "tog-showhiddencats": "Popobilehe kategori u hewanto'a",
-       "tog-norollbackdiff": "Japopobilohe u beda yilapato pilopohalingo",
-       "tog-useeditwarning": "Popo'ingatiya wa'u wonu molola halaman he'ubalo wonu dipo tilahu",
-       "tog-prefershttps": "Layito momake koneksi aamani wonu tumuwato log",
+       "tog-diffonly": "Ja popobilohe tuwango halaman u hihihede",
+       "tog-showhiddencats": "Popobilehe dalala u hewanto'a",
+       "tog-norollbackdiff": "Japopobilohe hihedeliyo to'u yilapato pilopohuwalingo",
+       "tog-useeditwarning": "Popo'eelawa wa'u wonu molola halaman heboli'olo wonu dipo tilahu",
+       "tog-prefershttps": "Layito momake koneksi amani wonu tumuwoto log",
        "underline-always": "Layito",
        "underline-never": "Dila ta",
        "underline-default": "Alipo meyalo browser dudelo",
-       "editfont-style": "Ubawa area gaya lo tuladu",
+       "editfont-style": "Boli'a area gaya lo tuladu",
        "editfont-monospace": "Tuladu Monospaced",
        "editfont-sansserif": "Tuladu San-serif",
        "editfont-serif": "Tuladu Serif",
        "december-date": "$1 Desember",
        "period-am": "AM",
        "period-pm": "PM",
-       "pagecategories": "{{PLURAL:$1|Tayadu}}",
-       "category_header": "Halaman to delomo kategori \"$1\"",
+       "pagecategories": "{{PLURAL:$1|Dalala}}",
+       "category_header": "Halaman to delomo dalala \"$1\"",
        "subcategories": "Subkategori",
-       "category-media-header": "Media to delomo kategori \"$1\"",
+       "category-media-header": "Media to delomo dalala \"$1\"",
        "category-empty": "<em>Kategori botiye ja o halaman meyalo media.<em>",
        "hidden-categories": "{{PLURAL:$1|Tayadu wanto-wanto'o}}",
        "hidden-category-category": "Kategori wanto-wanto'o",
        "listingcontinuesabbrev": "wumb",
        "index-category": "Halaman to indeks",
        "noindex-category": "Halaman diila to indeks",
-       "broken-file-category": "Halaamani wolo pranala berkas ma lorusa",
+       "broken-file-category": "Halaman wolo wumbuta berkas ma lorusa",
        "about": "Tomimbihu",
        "article": "Tuwango halaman",
-       "newwindow": "hu'owa to janela bohu",
+       "newwindow": "hu'owa to tutulowa bohu",
        "cancel": "Batali",
        "moredotdotdot": "Uweewo",
        "morenotlisted": "Daputari boti kira-kira diipo ganapu",
-       "mypage": "Halaamani",
+       "mypage": "Halaman",
        "mytalk": "Lo'iya",
        "anontalk": "Lo'iya",
        "navigation": "Navigasi",
index 2c81cb8..e6183c5 100644 (file)
        "hidden-categories": "{{PLURAL:$1|קטגוריה מוסתרת|קטגוריות מוסתרות}}",
        "hidden-category-category": "קטגוריות מוסתרות",
        "category-subcat-count": "{{PLURAL:$2|קטגוריה זו מכילה את קטגוריית המשנה הבאה בלבד.|קטגוריה זו מכילה את {{PLURAL:$1|קטגוריית המשנה המוצגת להלן|$1 קטגוריות המשנה המוצגות להלן}}, ומכילה בסך־הכול $2 קטגוריות משנה.}}",
-       "category-subcat-count-limited": "ק×\98×\92×\95ר×\99×\94 ×\96×\95 ×\9b×\95×\9c×\9cת את {{PLURAL:$1|קטגוריית המשנה הבאה|$1 קטגוריות המשנה הבאות}}.",
+       "category-subcat-count-limited": "ק×\98×\92×\95ר×\99×\94 ×\96×\95 ×\9e×\9b×\99×\9c×\94 את {{PLURAL:$1|קטגוריית המשנה הבאה|$1 קטגוריות המשנה הבאות}}.",
        "category-article-count": "{{PLURAL:$2|קטגוריה זו מכילה את הדף הבא בלבד.|קטגוריה זו מכילה את {{PLURAL:$1|הדף המוצג להלן|$1 הדפים המוצגים להלן}}, ומכילה בסך־הכול $2 דפים.}}",
-       "category-article-count-limited": "ק×\98×\92×\95ר×\99×\94 ×\96×\95 ×\9b×\95×\9c×\9cת את {{PLURAL:$1|הדף הבא|$1 הדפים הבאים}}.",
+       "category-article-count-limited": "ק×\98×\92×\95ר×\99×\94 ×\96×\95 ×\9e×\9b×\99×\9c×\94 את {{PLURAL:$1|הדף הבא|$1 הדפים הבאים}}.",
        "category-file-count": "{{PLURAL:$2|קטגוריה זו מכילה את הקובץ הבא בלבד.|קטגוריה זו מכילה את {{PLURAL:$1|הקובץ המוצג להלן|$1 הקבצים המוצגים להלן}}, ומכילה בסך־הכול $2 קבצים.}}",
-       "category-file-count-limited": "ק×\98×\92×\95ר×\99×\94 ×\96×\95 ×\9b×\95×\9c×\9cת את {{PLURAL:$1|הקובץ הבא|$1 הקבצים הבאים}}.",
+       "category-file-count-limited": "ק×\98×\92×\95ר×\99×\94 ×\96×\95 ×\9e×\9b×\99×\9c×\94 את {{PLURAL:$1|הקובץ הבא|$1 הקבצים הבאים}}.",
        "listingcontinuesabbrev": "(המשך)",
        "index-category": "דפים המופיעים במנועי חיפוש",
        "noindex-category": "דפים המוסתרים ממנועי חיפוש",
        "recentchangesdays": "מספר הימים שמוצגים בדף השינויים האחרונים:",
        "recentchangesdays-max": "לכל היותר {{PLURAL:$1|יום אחד|יומיים|$1 ימים}}",
        "recentchangescount": "מספר העריכות שמוצגות כברירת מחדל בדף השינויים האחרונים, בדפי היסטוריית גרסאות ובדפי יומנים:",
-       "prefs-help-recentchangescount": "×\9eספר ×\9eקס×\99×\9e×\9cי: 1000",
+       "prefs-help-recentchangescount": "×\9eספר ×\9eר×\91י: 1000",
        "prefs-help-watchlist-token2": "זהו המפתח הסודי ל־Feed האינטרנטי של רשימת המעקב שלך.\nכל מי שיודע אותו יכול לקרוא את רשימת המעקב שלך, לכן אין לשתף אותו.\nבמקרה הצורך, אפשר [[Special:ResetTokens|לאפס את המפתח]].",
        "savedprefs": "ההעדפות שלך נשמרו.",
        "savedrights": "קבוצות {{GENDER:$1|המשתמש|המשתמשת}} של \"$1\" נשמרו.",
        "mediastatistics-header-total": "כל הקבצים",
        "json-warn-trailing-comma": "{{PLURAL:$1|פסיק מסיים אחד הוסר|$1 פסיקים מסיימים הוסרו}} מטקסט ה־JSON",
        "json-error-unknown": "הייתה בעיה עם טקסט ה־JSON. שגיאה: $1",
-       "json-error-depth": "×\94×\99×\99ת×\94 ×\97ר×\99×\92×\94 ×\9e×\94×¢×\95×\9eק ×\94×\9eקס×\99×\9e×\9cי של המחסנית",
+       "json-error-depth": "×\94×\99×\99ת×\94 ×\97ר×\99×\92×\94 ×\9e×\94×¢×\95×\9eק ×\94×\9eר×\91י של המחסנית",
        "json-error-state-mismatch": "נתוני JSON בלתי־תקינים או פגומים",
        "json-error-ctrl-char": "שגיאה בתו בקרה, ייתכן שהקידוד שגוי",
        "json-error-syntax": "שגיאת תחביר",
index abfce6a..55380e3 100644 (file)
@@ -60,7 +60,7 @@
        "tog-shownumberswatching": "Ցույց տալ հսկող մասնակիցների թիվը",
        "tog-oldsig": "Ձեր ընթացիկ ստորագրությունը՝",
        "tog-fancysig": "Ստորագրությունը վիքիտեքստի տեսքով (առանց ավտոմատ հղման)",
-       "tog-uselivepreview": "Õ\86Õ¡Õ­Õ¡Õ¤Õ«Õ¿Õ¥Õ¬ Õ¡Õ¼Õ¡Õ¶Ö\81 Õ¾Õ¥Ö\80Õ¡Õ¢Õ¥Õ¼Õ¶Õ¥Õ¬Õ¸Ö\82 Õ§Õ»Õ¨",
+       "tog-uselivepreview": "Նախադիտել առանց վերբեռնելու էջը",
        "tog-forceeditsummary": "Նախազգուշացնել խմբագրման ամփոփումը դատարկ թողնելու դեպքում",
        "tog-watchlisthideown": "Թաքցնել իմ խմբագրումները հսկացանկից",
        "tog-watchlisthidebots": "Թաքցնել բոտերի խմբագրումները հսկացանկից",
index 6e8d930..ad211e7 100644 (file)
        "copyright": "येथील मजकूर $1च्या अंतर्गत उपलब्ध आहे जोपर्यंत इतर नोंदी केलेल्या नाहीत.",
        "copyrightpage": "{{ns:project}}:प्रताधिकार",
        "currentevents": "सद्य घटना",
-       "currentevents-url": "प्रकल्प:सद्य घटना",
+       "currentevents-url": "Project:सद्य घटना",
        "disclaimers": "उत्तरदायित्वास नकार",
-       "disclaimerpage": "प्रकल्प : सर्वसाधारण उत्तरदायकत्वास नकार",
+       "disclaimerpage": "Project:सर्वसाधारण उत्तरदायकत्वास नकार",
        "edithelp": "संपादन साहाय्य",
        "helppage-top-gethelp": "साहाय्य",
        "mainpage": "मुखपृष्ठ",
        "mainpage-description": "मुखपृष्ठ",
        "policy-url": "Project:नीती",
        "portal": "समाज मुखपृष्ठ",
-       "portal-url": "प्रकल्प:समाज मुखपृष्ठ",
+       "portal-url": "Project:समाज मुखपृष्ठ",
        "privacy": "गुप्तता नीती",
-       "privacypage": "प्रकल्प:गुप्तता नीती",
+       "privacypage": "Project:गुप्तता नीती",
        "badaccess": "परवानगी त्रुटी",
        "badaccess-group0": "आपण विनंती केलेल्या क्रियेच्या पूर्ततेचे तुम्हाला अधिकार नाहीत.",
        "badaccess-groups": "आपण विनीत केलेली कृती खालील {{PLURAL:$2|समूहासाठी|पैकी एका समूहासाठी}} मर्यादित आहे: $1.",
index 9a00b9c..2398d33 100644 (file)
        "recentchangesdays": "Antall dager som skal vises i siste endringer:",
        "recentchangesdays-max": "Maks $1 {{PLURAL:$1|dag|dager}}",
        "recentchangescount": "Antall redigeringer som skal vises som standard:",
-       "prefs-help-recentchangescount": "Dette inkluderer nylige endringer, sidehistorikk og logger.",
+       "prefs-help-recentchangescount": "Maksimalt antall: 1000",
        "prefs-help-watchlist-token2": "Dette er den hemmelige nøkkelen til webmatingen for din overvåkningsliste.\nEnhver som kjenner nøkkelen vil kunne lese din overvåkningsliste, så ikke vis den til andre.\nOm du trenger å gjøre det kan du [[Special:ResetTokens|nullstille nøkkelen]].",
        "savedprefs": "Innstillingene ble lagret.",
        "savedrights": "Brukergruppene til {{GENDER:$1|$1}} har blitt lagret.",
        "limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|byte|bytes}}",
        "limitreport-expansiondepth": "Største ekspansjonsdybde",
        "limitreport-expensivefunctioncount": "Antall kostbare parserfunksjoner",
+       "limitreport-unstrip-size-value": "$1/$2 {{PLURAL:$2|byte}}",
        "expandtemplates": "Utvid maler",
        "expand_templates_intro": "Denne spesialsiden tar wikitekst og utvider rekusivt alle maler brukt i teksten. \nDen utvider også alle parserfunksjoner som \n<code><nowiki>{{</nowiki>#language:…}}</code>, og variabler som \n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nFaktisk utvider den det meste innkapslet i doble krøllparenteser.",
        "expand_templates_title": "Konteksttittel, for {{FULLPAGENAME}}, etc.:",
index 4da3e9f..6abade2 100644 (file)
        "upload-http-error": "A avut loc o eroare HTTP: $1",
        "upload-copy-upload-invalid-domain": "Încărcarea copiilor nu este disponibilă pentru acest domeniu.",
        "upload-foreign-cant-upload": "Acest wiki nu este configurat pentru a încărca fișiere în depozitul de fișiere străin solicitat.",
+       "upload-foreign-cant-load-config": "Nu am putut încărca configurația pentru încărcările de fișiere în biblioteca externă de fișiere.",
+       "upload-dialog-disabled": "Încărcarea de fișiere folosind acest dialog este dezactivată pe acest wiki.",
        "upload-dialog-title": "Încărcare fișier",
        "upload-dialog-button-cancel": "Revocare",
        "upload-dialog-button-back": "Înapoi",
        "uploadstash-bad-path": "Calea nu există.",
        "uploadstash-bad-path-invalid": "Calea nu este validă.",
        "uploadstash-bad-path-unknown-type": "Tip necunoscut „$1”",
+       "uploadstash-bad-path-unrecognized-thumb-name": "Numele miniaturii nerecunoscut.",
        "uploadstash-bad-path-bad-format": "Cheia „$1” nu este într-un format recunoscut.",
        "uploadstash-file-not-found": "Cheia „$1” nu a fost găsită în locația temporară.",
        "uploadstash-file-not-found-no-thumb": "Nu pot obține miniatura.",
        "uploadstash-file-too-large": "Nu pot servi un fișier mai mare de $1 octeți.",
        "uploadstash-not-logged-in": "Nu există niciun utilizator logat, fișierele trebuie să aparțină unui utilizator.",
        "uploadstash-wrong-owner": "Fișierul ($1) nu aparține utilizatorului curent.",
+       "uploadstash-no-such-key": "Nu există cheia ($1), nu se poate șterge.",
        "uploadstash-no-extension": "Extensia este vidă.",
        "uploadstash-zero-length": "Fișierul are lungime zero.",
        "invalid-chunk-offset": "Decalaj de segment nevalid",
        "apisandbox": "Cutia cu nisip pentru API",
        "apisandbox-jsonly": "Este nevoie de JavaScript pentru a folosi Cutia cu nisip pentru API.",
        "apisandbox-api-disabled": "API este dezactivat pe acest site.",
+       "apisandbox-intro": "Folosiți această pagină pentru a experimenta cu <strong>API-ul MediaWiki</strong>. Citiți [[mw:API:Main page|documentația API-ului]] pentru mai multe detalii de utilizare. Exemplu: [https://www.mediawiki.org/wiki/API#A_simple_example obțineți conținutul paginii principale]. Selectați o acțiune pentru a vedea mai multe exemple.",
        "apisandbox-fullscreen": "Extinde panoul",
+       "apisandbox-fullscreen-tooltip": "Măriți panoul gropii cu nisip pentru a umple fereastra browserului.",
        "apisandbox-unfullscreen": "Arată pagina",
+       "apisandbox-unfullscreen-tooltip": "Reduceți panoul gropii cu nisip pentru a vedea legăturile de navigare din MediaWiki.",
        "apisandbox-submit": "Efectuați cererea",
        "apisandbox-reset": "Curăță",
        "apisandbox-retry": "Reîncercare",
        "apisandbox-sending-request": "Se trimite solicitarea API...",
        "apisandbox-loading-results": "Se obțin rezultatele API...",
        "apisandbox-results-error": "A apărut o eroare la încărcarea răspunsului solicitării API: $1.",
+       "apisandbox-results-login-suppressed": "Această cerere a fost procesată ca venind din partea unui utilizator neautentificat deoarece poate fi folosită pentru a evita verificările cu privire la originea comună făcute de browser. Metoda automată de administrare a token-urilor din groapa cu nisip pentru APU nu funcționează corect cu aceste cereri, vă rugăm să le completați manual.",
        "apisandbox-request-selectformat-label": "Afișați datele solicitate ca:",
        "apisandbox-request-url-label": "URL cerere:",
        "apisandbox-request-format-json-label": "JSON",
        "ipb_blocked_as_range": "Eroare: Adresa IP $1 nu este blocată direct deci nu poate fi deblocată.\nFace parte din area de blocare $2, care nu poate fi deblocată.",
        "ip_range_invalid": "Serie IP invalidă.",
        "ip_range_toolarge": "Blocările mai mari de /$1 nu sunt permise.",
+       "ip_range_toolow": "Gamele de IP-uri nu sunt permise.",
        "proxyblocker": "Blocaj de proxy",
        "proxyblockreason": "Adresa dumneavoastră IP a fost blocată pentru că este un proxy deschis.\nVă rugăm să vă contactați furnizorul de servicii Internet sau tehnicienii IT și să-i informați asupra acestei probleme serioase de securitate.",
        "sorbs": "DNSBL",
        "confirmemail_body_set": "Cineva, probabil dumneavoastră de la adresa IP $1, a asociat prezenta adresă de e-mail contului „$2” de la la {{SITENAME}}.\n\nPentru a confirma că acest cont vă aparține într-adevăr și pentru a vă activa funcțiile de e-mail de la {{SITENAME}}, accesați pagina:\n\n$3\n\nDacă însă NU este contul dumneavoastră, accesați pagina de mai jos pentru a anula confirmarea adresei de e-mail:\n\n$5\n\nAcest cod de confirmare va expira la $4.",
        "confirmemail_invalidated": "Confirmarea adresei de e-mail a fost anulată",
        "invalidateemail": "Anulează confirmarea adresei de e-mail",
+       "notificationemail_subject_changed": "Adresa de email înregistrată pe {{SITENAME}} a fost schimbată",
+       "notificationemail_subject_removed": "Adresa de email înregistrată pe {{SITENAME}} a fost ștearsă",
+       "notificationemail_body_changed": "Cineva, probabil dumneavoastră, a schimbat adresa de email a contului \"$2\" de pe {{SITENAME}} la \"$3\", de la adresa IP $1.\n\nDacă nu ați fost dumneavoastră, contactați un administrator al site-ului imediat.",
+       "notificationemail_body_removed": "Cineva, probabil dumneavoastră, a șters adresa de email a contului \"$2\" de pe {{SITENAME}} de la adresa IP $1.\n\nDacă nu ați fost dumneavoastră, contactați un administrator al site-ului imediat.",
        "scarytranscludedisabled": "[Transcluderea interwiki este dezactivată]",
        "scarytranscludefailed": "[Șiretlicul formatului a dat greș pentru $1]",
        "scarytranscludefailed-httpstatus": "[Șiretlicul formatului a dat greș pentru $1: HTTP $2]",
        "autosumm-blank": "Ștergerea conținutului paginii",
        "autosumm-replace": "Pagină înlocuită cu „$1”",
        "autoredircomment": "Redirecționat înspre [[$1]]",
+       "autosumm-removed-redirect": "Redirecționarea spre [[$1]] a fost ștearsă",
+       "autosumm-changed-redirect-target": "Ținta redirecționării a fost schimbată de la [[$1]] la [[$2]]",
        "autosumm-new": "Pagină nouă: $1",
        "autosumm-newblank": "Creat o pagină goală",
        "size-bytes": "{{PLURAL:$1|un octet|$1 octeți|$1 de octeți}}",
        "watchlistedit-clear-titles": "Titluri:",
        "watchlistedit-clear-submit": "Golește lista de pagini urmărite (ireversibil!)",
        "watchlistedit-clear-done": "Lista dumnevoastră de pagini urmărite a fost golită.",
+       "watchlistedit-clear-jobqueue": "Lista dumneavoastră de pagini urmărite este ștearsă. Această operație poate dura!",
        "watchlistedit-clear-removed": "{{PLURAL:$1|1 titlu a|$1 titlu au|$1 de titluri au}} fost înlăturat{{PLURAL:$1||e|e}}:",
        "watchlistedit-too-many": "Sunt prea multe pagini care trebuie afișate aici.",
        "watchlisttools-clear": "Golește lista de pagini urmărite",
        "timezone-local": "Local",
        "duplicate-defaultsort": "'''Atenție:''' Cheia de sortare implicită („$2”) o înlocuiește pe precedenta („$1”).",
        "duplicate-displaytitle": "<strong>Atenție:</strong> Titlul afișat „$2” înlocuieşte titlul afișat anterior, „$1”.",
+       "restricted-displaytitle": "<strong>Atenție:</strong> Titlul de afișat \"$1\" a fost ignorat deoarece nu este echivalent cu titlul real al paginii.",
        "invalid-indicator-name": "<strong>Eroare:</strong> Parametrul <code>nume</code> al indicatorilor de stare a paginii nu trebuie să fie gol.",
        "version": "Versiune",
        "version-extensions": "Extensii instalate",
        "fileduplicatesearch-noresults": "Nu s-a găsit niciun fișier cu numele „$1”.",
        "specialpages": "Pagini speciale",
        "specialpages-note-top": "Legendă",
+       "specialpages-note-restricted": "* Pagini speciale normale.\n* <span class=\"mw-specialpagerestricted\">Pagini speciale restricționate.</span>",
        "specialpages-group-maintenance": "Întreținere",
        "specialpages-group-other": "Alte pagini speciale",
        "specialpages-group-login": "Autentificare / creare cont",
        "tag-filter": "Filtru pentru [[Special:Tags|etichete]]:",
        "tag-filter-submit": "Filtru",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Etichetă|Etichete}}]]: $2)",
+       "tag-mw-contentmodelchange": "schimbare a modelului de conținut",
+       "tag-mw-contentmodelchange-description": "Editări ce  [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel schimbă modelul de conținut] al unei pagini",
+       "tag-mw-new-redirect": "Redirecționare nouă",
+       "tag-mw-new-redirect-description": "Editări ce creează o nouă redirecționare sau transformă o pagină într-o redirecționare",
+       "tag-mw-removed-redirect": "Redirecționare ștearsă",
+       "tag-mw-removed-redirect-description": "Editări ce transformă o redirecționare într-o non-redirecționare",
+       "tag-mw-changed-redirect-target": "Destinația redirecționării schimbată",
+       "tag-mw-changed-redirect-target-description": "Editări ce schimbă destinația unei redirecționări",
+       "tag-mw-blank": "Golire",
+       "tag-mw-blank-description": "Editare ce golește o pagină",
        "tag-mw-replace": "Înlocuit",
        "tag-mw-replace-description": "Editări care șterg mai mult de 90% din conținutul unei pagini",
        "tag-mw-rollback": "Revenire",
        "log-action-filter-upload-overwrite": "Reîncărcare",
        "authmanager-authplugin-setpass-failed-title": "Schimbarea parolei a eșuat",
        "authmanager-authplugin-setpass-bad-domain": "Domeniu invalid.",
+       "authmanager-userdoesnotexist": "Contul de utilizator „$1” nu este înregistrat.",
+       "authmanager-userlogin-remembermypassword-help": "Dacă parola ar trebui reținută mai mult decât durata sesiunii.",
+       "authmanager-username-help": "Nume de utilizator pentru autentificare.",
+       "authmanager-password-help": "Parolă pentru autentificare.",
+       "authmanager-domain-help": "Domeniu pentru autentificare externă.",
+       "authmanager-retype-help": "Introduceți parola din nou pentru a confirma.",
        "authmanager-email-label": "E-mail",
        "authmanager-email-help": "Adresă de e-mail",
        "authmanager-realname-label": "Nume real",
        "authmanager-realname-help": "Numele real al utilizatorului",
+       "authmanager-provider-password": "Autentificare pe bază de parolă",
+       "authmanager-provider-password-domain": "Autentificare pe bază de parolă și domeniu",
+       "authmanager-provider-temporarypassword": "Parolă temporară",
+       "authprovider-confirmlink-request-label": "Conturi care trebuie conectate",
+       "authprovider-confirmlink-success-line": "$1: conectat cu succes.",
+       "authprovider-confirmlink-failed": "Conectarea contului nu s-a realizat: $1",
+       "authprovider-confirmlink-ok-help": "Continuă după afișarea mesajelor de eroare.",
        "authprovider-resetpass-skip-label": "Omite",
+       "authprovider-resetpass-skip-help": "Sari peste resetarea parolei.",
        "specialpage-securitylevel-not-allowed-title": "Nepermis",
        "specialpage-securitylevel-not-allowed": "Ne pare rău, nu aveți dreptul de a folosi această pagină deoarece identitatea dvs. nu a putut fi verificată.",
+       "cannotauth-not-allowed-title": "Permisiune refuzată.",
+       "cannotauth-not-allowed": "Nu aveți permisiunea de a folosi această pagină",
+       "changecredentials": "Schimbă credențialele",
+       "changecredentials-submit": "Schimbă credențialele",
+       "changecredentials-invalidsubpage": "„$1” nu este un tip de credențiale valid.",
+       "credentialsform-account": "Numele contului:",
+       "cannotlink-no-provider-title": "Nu există conturi conectate",
+       "cannotlink-no-provider": "Nu există conturi conectate.",
+       "linkaccounts": "Conectează conturile",
+       "linkaccounts-success-text": "Contul a fost conectat.",
        "linkaccounts-submit": "Leagă conturile",
        "unlinkaccounts": "Dezleagă conturile",
        "unlinkaccounts-success": "Contul a fost dezlegat",
        "userjsispublic": "Atenție: subpaginile JavaScript nu trebuie să conțină date confidențiale, întrucât ele sunt vizibile altor utilizatori.",
+       "restrictionsfield-badip": "Adresă IP sau gamă de adrese invalidă: $1",
+       "restrictionsfield-label": "Game de IP permise:",
+       "restrictionsfield-help": "O adresă IP sau gamă CIDR pe linie. Pentru a activa tot, folosiți:<pre>0.0.0.0/0\n::/0</pre>",
        "edit-error-short": "Eroare: $1",
        "edit-error-long": "Erori:\n\n$1",
        "revid": "versiunea $1",
+       "rawhtml-notallowed": "Tagurile &lt;html&gt; nu pot fi folosite în afara paginilor normale.",
        "gotointerwiki": "Se părăsește {{SITENAME}}",
        "gotointerwiki-invalid": "Titlul specificat nu este valid.",
        "pagedata-title": "Datele paginii",
index f72323e..b1c9b12 100644 (file)
        "rcfilters-state-message-subset": "Овај филтер нема ефекта јер су његови резултати укључени са онима {{PLURAL:$2|следећег, ширег филтера|следећих, ширих филтера}} (покушајте са означавањем да бисте их распознали): $1",
        "rcfilters-state-message-fullcoverage": "Одабир свих филтера у групи је исто као и одабир ниједног, тако да овај филтер нема ефекта. Група укључује: $1",
        "rcfilters-filtergroup-authorship": "Ауторство доприноса",
-       "rcfilters-filter-editsbyself-label": "Ваше измјене",
+       "rcfilters-filter-editsbyself-label": "Ваше измене",
        "rcfilters-filter-editsbyself-description": "Ваши доприноси.",
        "rcfilters-filter-editsbyother-label": "Измјене других",
-       "rcfilters-filter-editsbyother-description": "Све измјене осим Ваших.",
+       "rcfilters-filter-editsbyother-description": "Све измене осим Ваших.",
        "rcfilters-filtergroup-userExpLevel": "Корисничка регистрација и искуство",
        "rcfilters-filter-user-experience-level-registered-label": "Регистровани",
        "rcfilters-filter-user-experience-level-registered-description": "Пријављени уредници.",
        "rcfilters-filtergroup-significance": "Значај",
        "rcfilters-filter-minor-label": "Мање измене",
        "rcfilters-filter-minor-description": "Измјене које је аутор означио као мање.",
-       "rcfilters-filter-major-label": "Не-мање измјене",
+       "rcfilters-filter-major-label": "Не-мање измене",
        "rcfilters-filter-major-description": "Измјене које нису означене као мање.",
        "rcfilters-filtergroup-watchlist": "Странице на списку надгледања",
        "rcfilters-filter-watchlist-watched-label": "На списку надгледања",
        "rcfilters-filter-watchlist-watched-description": "Измјене страница на Вашем списку надгледања.",
-       "rcfilters-filter-watchlist-watchednew-label": "Нове измјене на списку надгледања",
-       "rcfilters-filter-watchlist-watchednew-description": "Измјене страница на списку надгледања које нисте посјетили од када су направљене измјене.",
+       "rcfilters-filter-watchlist-watchednew-label": "Нове измене на списку надгледања",
+       "rcfilters-filter-watchlist-watchednew-description": "Измјене страница на списку надгледања које нисте посјетили од када су направљене измене.",
        "rcfilters-filter-watchlist-notwatched-label": "Није на списку надгледања",
-       "rcfilters-filter-watchlist-notwatched-description": "Све осим измјена страница на Вашем списку надгледања.",
+       "rcfilters-filter-watchlist-notwatched-description": "Све осим измена страница на Вашем списку надгледања.",
        "rcfilters-filtergroup-watchlistactivity": "Стање на списку надгледања",
        "rcfilters-filter-watchlistactivity-unseen-label": "Непогледане измене",
        "rcfilters-filter-watchlistactivity-unseen-description": "Измене страница које нисте посетили од када су направљене измене.",
        "rcfilters-filter-watchlistactivity-seen-label": "Погледане измене",
        "rcfilters-filter-watchlistactivity-seen-description": "Измене страница које сте посетили од када су направљене измене.",
-       "rcfilters-filtergroup-changetype": "Тип измјене",
+       "rcfilters-filtergroup-changetype": "Тип измене",
        "rcfilters-filter-pageedits-label": "Измене страница",
        "rcfilters-filter-pageedits-description": "Измјене вики садржаја, расправа, описа категорија…",
        "rcfilters-filter-newpages-label": "Стварање страница",
        "rcfilters-hideminor-conflicts-typeofchange-global": "Филтер за „мање” измене је у сукобу са једним или више филтера типа измена, зато што одређени типови измена не могу да се означе као „мање”. Сукобљени филтери су означени у подручју Активни филтери, изнад.",
        "rcfilters-hideminor-conflicts-typeofchange": "Одређени типови измена не могу да се означе као „мање”, тако да је овај филтер у сукобу са следећим филтерима типа измена: $1",
        "rcfilters-typeofchange-conflicts-hideminor": "Овај филтер типа измене је у сукобу са филтером за „мање” измене. Одређени типови измена не могу да се означе као „мање”.",
-       "rcfilters-filtergroup-lastRevision": "Посљедње измјене",
-       "rcfilters-filter-lastrevision-label": "Посљедња измјена",
+       "rcfilters-filtergroup-lastRevision": "Посљедње измене",
+       "rcfilters-filter-lastrevision-label": "Посљедња измена",
        "rcfilters-filter-lastrevision-description": "Само најновија измена на страници.",
-       "rcfilters-filter-previousrevision-label": "Није посљедња измјена",
-       "rcfilters-filter-previousrevision-description": "Све измјене које нису „посљедње измјене”.",
+       "rcfilters-filter-previousrevision-label": "Није последња измена",
+       "rcfilters-filter-previousrevision-description": "Све измене које нису „последње измене”.",
        "rcfilters-filter-excluded": "Изостављено",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:није</strong> $1",
        "rcfilters-exclude-button-off": "Изостави означено",
        "rcfilters-exclude-button-on": "Изостави одабрано",
-       "rcfilters-view-tags": "Означене измјене",
+       "rcfilters-view-tags": "Означене измене",
        "rcfilters-view-namespaces-tooltip": "Филтрирај резултате према именском простору",
-       "rcfilters-view-tags-tooltip": "Филтрирање резултата према ознаци измјене",
+       "rcfilters-view-tags-tooltip": "Филтрирање резултата према ознаци измене",
        "rcfilters-view-return-to-default-tooltip": "Повратак на главни мени",
-       "rcfilters-view-tags-help-icon-tooltip": "Сазнајте више о означеним измјенама",
+       "rcfilters-view-tags-help-icon-tooltip": "Сазнајте више о означеним изменама",
        "rcfilters-liveupdates-button": "Ажурирај уживо",
        "rcfilters-liveupdates-button-title-on": "Искључи ажурирања уживо",
        "rcfilters-liveupdates-button-title-off": "Прикажи нове измене уживо",
index 03ca342..11fe1a0 100644 (file)
        "thu": "Пш",
        "fri": "Ҷу",
        "sat": "Шн",
-       "january": "Январ",
-       "february": "Феврал",
+       "january": "январ",
+       "february": "феврал",
        "march": "март",
        "april": "апрел",
        "may_long": "май",
-       "june": "Ð\98юн",
-       "july": "Ð\98юл",
-       "august": "Ð\90вгуст",
-       "september": "Сентябр",
-       "october": "Ð\9eктябр",
-       "november": "Ð\9dоябр",
-       "december": "Ð\94екабр",
+       "june": "июн",
+       "july": "июл",
+       "august": "август",
+       "september": "сентябр",
+       "october": "октябр",
+       "november": "ноябр",
+       "december": "декабр",
        "january-gen": "январи",
        "february-gen": "феврали",
        "march-gen": "марти",
        "mar": "Мар",
        "apr": "Апр",
        "may": "май",
-       "jun": "Ð\98юн",
-       "jul": "Ð\98юл",
+       "jun": "июн",
+       "jul": "июл",
        "aug": "Авг",
        "sep": "Сент",
        "oct": "Окт",
index 77becb9..6d30712 100644 (file)
        "botpasswords-insert-failed": "加機械人名 \"$1\" 衰咗。係咪之前已經加咗?",
        "botpasswords-update-failed": "更新機械人名 \"$1\" 衰咗。係咪之前已經剷走咗?",
        "botpasswords-created-title": "生成咗機械人密碼",
-       "botpasswords-created-body": "用戶 \"$2\" 嘅機械人 \"$1\" 嘅密碼已經開咗。",
+       "botpasswords-created-body": "{{GENDER:$2|用戶}}「$2」嘅機械人「$1」嘅密碼已經開咗。",
        "botpasswords-updated-title": "改咗機械人密碿",
-       "botpasswords-updated-body": "用戶 \"$2\" 嘅機械人 \"$1\" 嘅密碼已經更新咗。",
+       "botpasswords-updated-body": "{{GENDER:$2|用戶}}「$2」嘅機械人「$1」嘅密碼已經更新咗。",
        "botpasswords-deleted-title": "鏟咗機械人密碼",
-       "botpasswords-deleted-body": "用戶 \"$2\" 嘅機械人 \"$1\" 嘅密碼已經剷走咗。",
+       "botpasswords-deleted-body": "{{GENDER:$2|用戶}}「$2」嘅機械人「$1」嘅密碼已經剷走咗。",
        "botpasswords-restriction-failed": "機械人密碼限制令到呢次簽到失敗。",
        "botpasswords-invalid-name": "呢個用戶名無機械人密碼分隔字(「$1」)",
        "resetpass_forbidden": "唔可以更改密碼",
        "postedit-confirmation-created": "呢版經已開咗。",
        "postedit-confirmation-restored": "呢版經已恢復咗。",
        "postedit-confirmation-saved": "呢版經已儲存咗。",
+       "postedit-confirmation-published": "你嘅修改發佈咗。",
        "edit-already-exists": "唔可以開一新版。\n佢已經存在。",
        "defaultmessagetext": "預設訊息文字",
        "content-failed-to-parse": "從$1模型解析到$2目錄時肥佬咗。原因:$3。",
        "recentchangesdays": "最近更改中嘅顯示日數:",
        "recentchangesdays-max": "最多 $1 日",
        "recentchangescount": "預設顯示嘅編輯數:",
-       "prefs-help-recentchangescount": "呢個包埋最近修改、頁歷史同埋日誌紀錄。",
+       "prefs-help-recentchangescount": "最大數目:1000",
        "prefs-help-watchlist-token2": "呢個係網上訂閱你個監視清單嘅密匙。\n任何人只要知道個密匙,就會睇到你個監視清單,所以唔好畀人知。\n如果有需要嘅話,[[Special:ResetTokens|你可以重設佢]]。",
        "savedprefs": "你嘅喜好設定已經儲存。",
        "savedrights": "儲存咗 {{GENDER:$1|$1}} 嘅用戶群組。",
        "action-userrights-interwiki": "編輯響其它wiki用戶嘅權限",
        "action-siteadmin": "鎖同解鎖資料庫",
        "action-sendemail": "寄電郵",
+       "action-editmyoptions": "改你嘅喜好設定",
        "action-editmywatchlist": "改監視清單",
        "action-viewmywatchlist": "睇監視清單",
        "action-viewmyprivateinfo": "睇你嘅私人資料",
        "uploadstash-badtoken": "進行呢個動作唔成功,可能係你嘅編輯資訊已經過咗期。再試吓喇。",
        "uploadstash-errclear": "清除檔案唔成功。",
        "uploadstash-refresh": "更新檔案清單",
+       "uploadstash-thumbnail": "睇縮圖",
        "invalid-chunk-offset": "非法偏移塊",
        "img-auth-accessdenied": "拒絕通行",
        "img-auth-nopathinfo": "PATH_INFO唔見咗。\n你嘅伺服器重未設定呢個資料。\n佢可能係CGI為本,唔支援img_auth。\n睇吓 https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization 。",
        "doubleredirects": "雙重跳轉",
        "doubleredirectstext": "每一行都順次序寫住第一頁名,佢嘅目的頁,同埋目的頁再指去邊度。改嘅時候,應該將第一個跳轉頁轉入第三頁。\n<del>劃咗</del>嘅項目係已經解決咗嘅。",
        "double-redirect-fixed-move": "[[$1]]已經搬好咗。\n佢自動更新咗,而家跳轉過去[[$2]]。",
-       "double-redirect-fixed-maintenance": "å\96ºç¶­è­·å·¥ä½\9c度è\87ªå\8b\95修復[[$1]]å\98\85è·³è½\89å\88°[[$2]]ã\80\82",
+       "double-redirect-fixed-maintenance": "å\96ºç¶­è­·å·¥ä½\9c度è\87ªå\8b\95修復[[$1]]å\88°[[$2]]å\98\85é\9b\99é\87\8dè·³è½\89",
        "double-redirect-fixer": "跳轉修正器",
        "brokenredirects": "破碎嘅跳轉",
        "brokenredirectstext": "以下嘅跳轉係指向唔存在嘅頁面:",
        "querypage-disabled": "呢個特別版基於效能嘅原因停用咗。",
        "apihelp": "API幫手",
        "apihelp-no-such-module": "搵唔到模組「$1」。",
+       "apisandbox": "API沙盤",
+       "apisandbox-jsonly": "需要JavaScript來用API沙盤。",
+       "apisandbox-api-disabled": "爾個網站閂咗API。",
        "apisandbox-reset": "清除",
        "apisandbox-retry": "再試過",
        "apisandbox-examples": "範例",
        "apisandbox-results": "結果",
+       "apisandbox-request-url-label": "請求URL:",
+       "apisandbox-request-json-label": "請求JSON:",
+       "apisandbox-request-time": "請求時間:{{PLURAL:$1|$1毫秒}}",
        "apisandbox-continue": "繼續",
        "apisandbox-continue-clear": "清除",
        "booksources": "書籍來源",
        "alllogstext": "響{{SITENAME}}度全部日誌嘅綜合顯示。你可以選擇一個日誌類型、用戶名、或者受影響嘅頁面,嚟縮窄顯示嘅範圍。",
        "logempty": "日誌中冇符合嘅項目。",
        "log-title-wildcard": "搵以呢個文字開始嘅標題",
+       "checkbox-select": "揀:$1",
        "checkbox-all": "全部",
        "checkbox-none": "冇",
        "checkbox-invert": "插入",
index 6249094..438e9dc 100644 (file)
@@ -88,6 +88,8 @@ class CommandLineInstaller extends Maintenance {
                        false, true );
                */
                $this->addOption( 'env-checks', "Run environment checks only, don't change anything" );
+
+               $this->addOption( 'with-extensions', "Detect and include extensions" );
        }
 
        public function getDbType() {
index a2e071e..aa93ca2 100644 (file)
                        }
 
                        /**
-                        * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
-                        * to a query string of the form foo.bar,baz|bar.baz,quux
+                        * Converts a module map of the form `{ foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }`
+                        * to a query string of the form `foo.bar,baz|bar.baz,quux`.
+                        *
+                        * See `ResourceLoader::makePackedModulesString()` in PHP, of which this is a port.
+                        * On the server, unpacking is done by `ResourceLoaderContext::expandModuleNames()`.
+                        *
+                        * Note: This is only half of the logic, the other half has to be in #batchRequest(),
+                        * because its implementation needs to keep track of potential string size in order
+                        * to decide when to split the requests due to url size.
                         *
                         * @private
                         * @param {Object} moduleMap Module map
                         * Make a network request to load modules from the server.
                         *
                         * @private
-                        * @param {Object} moduleMap Module map, see #buildModulesString
+                        * @param {string} moduleStr Module list for load.php `module` query parameter
                         * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
                         * @param {string} sourceLoadScript URL of load.php
                         */
-                       function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
+                       function doRequest( moduleStr, currReqBase, sourceLoadScript ) {
                                // Optimisation: Inherit (Object.create), not copy ($.extend)
                                var query = Object.create( currReqBase );
-                               query.modules = buildModulesString( moduleMap );
+                               query.modules = moduleStr;
                                query = sortQuery( query );
                                addScript( sourceLoadScript + '?' + $.param( query ) );
                        }
                                                        // but don't create empty requests
                                                        if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
                                                                // This url would become too long, create a new one, and start the old one
-                                                               doRequest( moduleMap, currReqBase, sourceLoadScript );
+                                                               doRequest( buildModulesString( moduleMap ), currReqBase, sourceLoadScript );
                                                                moduleMap = {};
                                                                l = currReqBaseLength + 9;
                                                                mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
                                                }
                                                // If there's anything left in moduleMap, request that too
                                                if ( !$.isEmptyObject( moduleMap ) ) {
-                                                       doRequest( moduleMap, currReqBase, sourceLoadScript );
+                                                       doRequest( buildModulesString( moduleMap ), currReqBase, sourceLoadScript );
                                                }
                                        }
                                }
index bdb4831..076924c 100644 (file)
@@ -89,20 +89,11 @@ class ImportLinkCacheIntegrationTest extends MediaWikiTestCase {
 
                $reporter->setContext( new RequestContext() );
                $reporter->open();
-               $exception = false;
 
-               try {
-                       $importer->doImport();
-               } catch ( Exception $e ) {
-                       $exception = $e;
-               }
+               $importer->doImport();
 
                $result = $reporter->close();
 
-               $this->assertFalse(
-                       $exception
-               );
-
                $this->assertTrue(
                        $result->isGood()
                );
index bf3689b..1eca89b 100644 (file)
@@ -517,4 +517,69 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
                $this->assertSame( 'CAST( fieldName AS SIGNED )', $output );
        }
 
+       /*
+        * @covers Wikimedia\Rdbms\Database::setIndexAliases
+        */
+       public function testIndexAliases() {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'mysqlRealEscapeString' ] )
+                       ->getMock();
+               $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
+                       function ( $s ) {
+                               return str_replace( "'", "\\'", $s );
+                       }
+               );
+
+               $db->setIndexAliases( [ 'a_b_idx' => 'a_c_idx' ] );
+               $sql = $db->selectSQLText(
+                       'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] );
+
+               $this->assertEquals(
+                       "SELECT  field  FROM `zend`  FORCE INDEX (a_c_idx)  WHERE a = 'x'  ",
+                       $sql
+               );
+
+               $db->setIndexAliases( [] );
+               $sql = $db->selectSQLText(
+                       'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] );
+
+               $this->assertEquals(
+                       "SELECT  field  FROM `zend`  FORCE INDEX (a_b_idx)  WHERE a = 'x'  ",
+                       $sql
+               );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::setTableAliases
+        */
+       public function testTableAliases() {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'mysqlRealEscapeString' ] )
+                       ->getMock();
+               $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
+                       function ( $s ) {
+                               return str_replace( "'", "\\'", $s );
+                       }
+               );
+
+               $db->setTableAliases( [
+                       'meow' => [ 'dbname' => 'feline', 'schema' => null, 'prefix' => 'cat_' ]
+               ] );
+               $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ );
+
+               $this->assertEquals(
+                       "SELECT  field  FROM `feline`.`cat_meow`    WHERE a = 'x'  ",
+                       $sql
+               );
+
+               $db->setTableAliases( [] );
+               $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ );
+
+               $this->assertEquals(
+                       "SELECT  field  FROM `meow`    WHERE a = 'x'  ",
+                       $sql
+               );
+       }
 }
index 6adbc75..85574b7 100644 (file)
@@ -1,11 +1,14 @@
 <?php
 
-use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\DatabaseMysqli;
 use Wikimedia\Rdbms\LBFactorySingle;
 use Wikimedia\Rdbms\TransactionProfiler;
 use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Rdbms\DatabaseSqlite;
+use Wikimedia\Rdbms\DatabasePostgres;
+use Wikimedia\Rdbms\DatabaseMssql;
 
 class DatabaseTest extends PHPUnit\Framework\TestCase {
 
@@ -15,6 +18,29 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
                $this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
        }
 
+       /**
+        * @dataProvider provideAddQuotes
+        * @covers Wikimedia\Rdbms\Database::factory
+        */
+       public function testFactory() {
+               $m = Database::NEW_UNCONNECTED; // no-connect mode
+               $p = [ 'host' => 'localhost', 'user' => 'me', 'password' => 'myself', 'dbname' => 'i' ];
+
+               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'mysqli', $p, $m ) );
+               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySqli', $p, $m ) );
+               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySQLi', $p, $m ) );
+               $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'postgres', $p, $m ) );
+               $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'Postgres', $p, $m ) );
+
+               $x = $p + [ 'port' => 10000, 'UseWindowsAuth' => false ];
+               $this->assertInstanceOf( DatabaseMssql::class, Database::factory( 'mssql', $x, $m ) );
+
+               $x = $p + [ 'dbFilePath' => 'some/file.sqlite' ];
+               $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) );
+               $x = $p + [ 'dbDirectory' => 'some/file' ];
+               $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) );
+       }
+
        public static function provideAddQuotes() {
                return [
                        [ null, 'NULL' ],
index 3e120f4..a34c03f 100644 (file)
@@ -99,6 +99,11 @@ class LanguageRuTest extends LanguageClassesTestCase {
                                'Викисклад',
                                'genitive',
                        ],
+                       [
+                               'Викиверситета',
+                               'Викиверситет',
+                               'genitive',
+                       ],
                        [
                                'Викискладе',
                                'Викисклад',
@@ -109,6 +114,11 @@ class LanguageRuTest extends LanguageClassesTestCase {
                                'Викиданные',
                                'prepositional',
                        ],
+                       [
+                               'Викиверситете',
+                               'Викиверситет',
+                               'prepositional',
+                       ],
                        [
                                'русского',
                                'русский',
index 5ce61ea..7da1502 100644 (file)
                                expected: 'привилегии',
                                description: 'Grammar test for prepositional case, привилегия -> привилегии'
                        },
+                       {
+                               word: 'университет',
+                               grammarForm: 'prepositional',
+                               expected: 'университете',
+                               description: 'Grammar test for prepositional case, университет -> университете'
+                       },
+                       {
+                               word: 'университет',
+                               grammarForm: 'genitive',
+                               expected: 'университета',
+                               description: 'Grammar test for prepositional case, университет -> университете'
+                       },
                        {
                                word: 'установка',
                                grammarForm: 'prepositional',