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 ===
);
$class = MWLBFactory::getLBFactoryClass( $lbConf );
- return new $class( $lbConf );
+ $instance = new $class( $lbConf );
+ MWLBFactory::setSchemaAliases( $instance );
+
+ return $instance;
},
'DBLoadBalancer' => function ( MediaWikiServices $services ) {
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\DatabaseDomain;
/**
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',
+ ] );
+ }
+ }
}
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.
*/
/**
- * Example class for HTTP accessable external objects.
+ * Example class for HTTP accessible external objects.
* Only supports reading, not storing.
*
* @ingroup ExternalStorage
*/
/**
- * 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
$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 );
}
public function estimateRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+ $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
) {
return $this->__call( __FUNCTION__, func_get_args() );
}
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
*/
use Wikimedia;
use BagOStuff;
use HashBagOStuff;
+use LogicException;
use InvalidArgumentException;
use Exception;
use RuntimeException;
/** @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 */
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'];
// 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
}
}
+ /**
+ * 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
*
* - 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 ) ) {
};
}
+ /** @var Database $conn */
$conn = new $class( $p );
+ if ( $connect == self::NEW_CONNECTED ) {
+ $conn->initConnection();
+ }
} else {
$conn = null;
}
public function writesOrCallbacksPending() {
return $this->trxLevel && (
- $this->trxDoneWrites || $this->trxIdleCallbacks || $this->trxPreCommitCallbacks
+ $this->trxDoneWrites ||
+ $this->trxIdleCallbacks ||
+ $this->trxPreCommitCallbacks ||
+ $this->trxEndCallbacks
);
}
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;
}
/**
- * 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 );
}
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 );
* @return string
*/
protected function indexName( $index ) {
- return $index;
+ return isset( $this->indexAliases[$index] )
+ ? $this->indexAliases[$index]
+ : $index;
}
public function addQuotes( $s ) {
$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;
}
/**
* 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
*/
* @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 ) {
* @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;
}
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 );
class DatabaseMysqli extends DatabaseMysqlBase {
/**
* @param string $sql
- * @return resource
+ * @return mysqli_result
*/
protected function doQuery( $sql ) {
$conn = $this->getBindingHandle();
return (string)$this->conn;
}
}
+
+ /**
+ * @return mysqli
+ */
+ protected function getBindingHandle() {
+ return parent::getBindingHandle();
+ }
}
class_alias( DatabaseMysqli::class, 'DatabaseMysqli' );
* @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 );
*/
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;
'domain' => $lockDomain,
'lockDirectory' => "{$this->dbDir}/locks"
] );
+
+ parent::__construct( $p );
}
protected static function getAttributes() {
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
*/
* 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
*
}
$this->openFile( $fileName );
+ if ( $this->conn ) {
+ $this->dbName = $dbName;
+ }
+
return (bool)$this->conn;
}
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 );
* @return bool|ResultWrapper
*/
protected function doQuery( $sql ) {
- $res = $this->conn->query( $sql );
+ $res = $this->getBindingHandle()->query( $sql );
if ( $res === false ) {
return false;
}
*/
function insertId() {
// PDO::lastInsertId yields a string :(
- return intval( $this->conn->lastInsertId() );
+ return intval( $this->getBindingHandle()->lastInsertId() );
}
/**
* @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;
}
);
return "x'" . bin2hex( (string)$s ) . "'";
} else {
- return $this->conn->quote( (string)$s );
+ return $this->getBindingHandle()->quote( (string)$s );
}
}
}
}
- 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();
}
}
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
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.
* @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 = []
);
/**
* @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' );
*
* @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 );
* - 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 );
}
/** @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 */
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 ) {
* @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 );
}
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;
$this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
);
$db->setTableAliases( $this->tableAliases );
+ $db->setIndexAliases( $this->indexAliases );
if ( $server['serverIndex'] === $this->getWriterIndex() ) {
if ( $this->trxRoundId !== false ) {
$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 = [];
/**
* 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 );
}
/**
}
/**
- * 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
*/
[ "(.+)ды$", "$1дов" ],
[ "(.+)д$", "$1да" ],
[ "(.+)ник$", "$1ника" ],
+ [ "(.+)тет$", "$1тета" ],
[ "(.+)ные$", "$1ных" ]
],
"prepositional": [
[ "(.+)ды$", "$1дах" ],
[ "(.+)д$", "$1де" ],
[ "(.+)ник$", "$1нике" ],
+ [ "(.+)тет$", "$1тете" ],
[ "(.+)ные$", "$1ных" ]
],
"languagegen": [
"right-editmywatchlist": "Рэдагаваньне ўласнага сьпісу назіраньня. Некаторыя дзеяньні будуць дадаваць туды старонкі нават бяз гэтага права.",
"right-viewmyprivateinfo": "Праглядаць уласныя прыватныя зьвесткі (напрыклад, адрас электроннай пошты, сапраўднае імя)",
"right-editmyprivateinfo": "Рэдагаваць уласныя прыватныя зьвесткі (напрыклад, адрас электроннай пошты, сапраўднае імя)",
- "right-editmyoptions": "рэдагаваць уласныя налады",
+ "right-editmyoptions": "Рэдагаваць уласныя налады",
"right-rollback": "хуткі адкат правак апошняга ўдзельніка, які рэдагаваў старонку",
"right-markbotedits": "пазначэньне адкатаў як рэдагаваньне робатам",
"right-noratelimit": "няма абмежаваньняў па хуткасьці",
"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.",
"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": "نرخ:",
"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",
"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": "שגיאת תחביר",
"tog-shownumberswatching": "Ցույց տալ հսկող մասնակիցների թիվը",
"tog-oldsig": "Ձեր ընթացիկ ստորագրությունը՝",
"tog-fancysig": "Ստորագրությունը վիքիտեքստի տեսքով (առանց ավտոմատ հղման)",
- "tog-uselivepreview": "Õ\86Õ¡ÕÕ¡Õ¤Õ«Õ¿Õ¥Õ¬ Õ¡Õ¼Õ¡Õ¶Ö\81 Õ¾Õ¥Ö\80Õ¡Õ¢Õ¥Õ¼Õ¶Õ¥Õ¬Õ¸Ö\82 Õ§Õ»Õ¨",
+ "tog-uselivepreview": "Նախադիտել առանց վերբեռնելու էջը",
"tog-forceeditsummary": "Նախազգուշացնել խմբագրման ամփոփումը դատարկ թողնելու դեպքում",
"tog-watchlisthideown": "Թաքցնել իմ խմբագրումները հսկացանկից",
"tog-watchlisthidebots": "Թաքցնել բոտերի խմբագրումները հսկացանկից",
"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.",
"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.:",
"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 <html> 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",
"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": "Прикажи нове измене уживо",
"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": "Окт",
"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": "插入",
Wikis setup in this way are NOT meant to be publicly available. They use a development database not acceptable for use in production. Place a sqlite database in an unsafe location a real wiki should never place it in. And use predictable default logins for the initial administrator user.
-Running maintenance/dev/install.sh will download and install a local copy of php 5.4, install a sqlite powered instance of MW for development, and then start up a local webserver to view the wiki.
+Running maintenance/dev/install.sh will download and install a local copy of php 5.6, install a sqlite powered instance of MW for development, and then start up a local webserver to view the wiki.
After installation you can bring the webserver back up at any time you want with maintenance/dev/start.sh
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() {
}
/**
- * 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 );
}
}
}
$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()
);
$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
+ );
+ }
}
<?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 {
$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' ],
'Викисклад',
'genitive',
],
+ [
+ 'Викиверситета',
+ 'Викиверситет',
+ 'genitive',
+ ],
[
'Викискладе',
'Викисклад',
'Викиданные',
'prepositional',
],
+ [
+ 'Викиверситете',
+ 'Викиверситет',
+ 'prepositional',
+ ],
[
'русского',
'русский',
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',