From: jenkins-bot Date: Mon, 19 Sep 2016 19:31:44 +0000 (+0000) Subject: Merge "Avoid using cascadingDeletes()/cleanupTriggers()" X-Git-Tag: 1.31.0-rc.0~5476 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/operations/?a=commitdiff_plain;h=93845ed2ad47e13bf88d33893ceb547bc724b3b6;hp=4c08bf8d9613e8e0d94962d44d2e3ff32d1d70f8;p=lhc%2Fweb%2Fwiklou.git Merge "Avoid using cascadingDeletes()/cleanupTriggers()" --- diff --git a/autoload.php b/autoload.php index 3da4010e6a..55c42c693b 100644 --- a/autoload.php +++ b/autoload.php @@ -328,7 +328,7 @@ $wgAutoloadLocalClasses = [ 'DatabaseMysqli' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqli.php', 'DatabaseOracle' => __DIR__ . '/includes/db/DatabaseOracle.php', 'DatabasePostgres' => __DIR__ . '/includes/db/DatabasePostgres.php', - 'DatabaseSqlite' => __DIR__ . '/includes/db/DatabaseSqlite.php', + 'DatabaseSqlite' => __DIR__ . '/includes/libs/rdbms/database/DatabaseSqlite.php', 'DatabaseUpdater' => __DIR__ . '/includes/installer/DatabaseUpdater.php', 'DateFormats' => __DIR__ . '/maintenance/language/date-formats.php', 'DateFormatter' => __DIR__ . '/includes/parser/DateFormatter.php', @@ -438,7 +438,7 @@ $wgAutoloadLocalClasses = [ 'FSFileBackendFileList' => __DIR__ . '/includes/filebackend/FSFileBackend.php', 'FSFileBackendList' => __DIR__ . '/includes/filebackend/FSFileBackend.php', 'FSFileOpHandle' => __DIR__ . '/includes/filebackend/FSFileBackend.php', - 'FSLockManager' => __DIR__ . '/includes/filebackend/lockmanager/FSLockManager.php', + 'FSLockManager' => __DIR__ . '/includes/libs/lockmanager/FSLockManager.php', 'FSRepo' => __DIR__ . '/includes/filerepo/FSRepo.php', 'FakeAuthTemplate' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php', 'FakeConverter' => __DIR__ . '/languages/FakeConverter.php', @@ -659,9 +659,9 @@ $wgAutoloadLocalClasses = [ 'KuConverter' => __DIR__ . '/languages/classes/LanguageKu.php', 'LBFactory' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactory.php', 'LBFactoryMW' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMW.php', - 'LBFactoryMulti' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMulti.php', + 'LBFactoryMulti' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactoryMulti.php', 'LBFactorySimple' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactorySimple.php', - 'LBFactorySingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php', + 'LBFactorySingle' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactorySingle.php', 'LCStore' => __DIR__ . '/includes/cache/localisation/LCStore.php', 'LCStoreCDB' => __DIR__ . '/includes/cache/localisation/LCStoreCDB.php', 'LCStoreDB' => __DIR__ . '/includes/cache/localisation/LCStoreDB.php', @@ -733,7 +733,7 @@ $wgAutoloadLocalClasses = [ 'ListVariants' => __DIR__ . '/maintenance/language/listVariants.php', 'ListredirectsPage' => __DIR__ . '/includes/specials/SpecialListredirects.php', 'LoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancer.php', - 'LoadBalancerSingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php', + 'LoadBalancerSingle' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php', 'LoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitor.php', 'LoadMonitorMySQL' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php', 'LoadMonitorNull' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php', @@ -747,7 +747,7 @@ $wgAutoloadLocalClasses = [ 'LocalSettingsGenerator' => __DIR__ . '/includes/installer/LocalSettingsGenerator.php', 'LocalisationCache' => __DIR__ . '/includes/cache/localisation/LocalisationCache.php', 'LocalisationCacheBulkLoad' => __DIR__ . '/includes/cache/localisation/LocalisationCacheBulkLoad.php', - 'LockManager' => __DIR__ . '/includes/filebackend/lockmanager/LockManager.php', + 'LockManager' => __DIR__ . '/includes/libs/lockmanager/LockManager.php', 'LockManagerGroup' => __DIR__ . '/includes/filebackend/lockmanager/LockManagerGroup.php', 'LogEntry' => __DIR__ . '/includes/logging/LogEntry.php', 'LogEntryBase' => __DIR__ . '/includes/logging/LogEntry.php', @@ -979,7 +979,7 @@ $wgAutoloadLocalClasses = [ 'NullFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', 'NullIndexField' => __DIR__ . '/includes/search/NullIndexField.php', 'NullJob' => __DIR__ . '/includes/jobqueue/jobs/NullJob.php', - 'NullLockManager' => __DIR__ . '/includes/filebackend/lockmanager/LockManager.php', + 'NullLockManager' => __DIR__ . '/includes/libs/lockmanager/NullLockManager.php', 'NullRepo' => __DIR__ . '/includes/filerepo/NullRepo.php', 'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php', 'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php', @@ -1110,7 +1110,7 @@ $wgAutoloadLocalClasses = [ 'PurgeParserCache' => __DIR__ . '/maintenance/purgeParserCache.php', 'QueryPage' => __DIR__ . '/includes/specialpage/QueryPage.php', 'QuickTemplate' => __DIR__ . '/includes/skins/QuickTemplate.php', - 'QuorumLockManager' => __DIR__ . '/includes/filebackend/lockmanager/QuorumLockManager.php', + 'QuorumLockManager' => __DIR__ . '/includes/libs/lockmanager/QuorumLockManager.php', 'RCCacheEntry' => __DIR__ . '/includes/changes/RCCacheEntry.php', 'RCCacheEntryFactory' => __DIR__ . '/includes/changes/RCCacheEntryFactory.php', 'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php', diff --git a/includes/Defines.php b/includes/Defines.php index 077f39a350..529dfb39be 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -27,6 +27,17 @@ # Obsolete aliases define( 'DB_SLAVE', -1 ); +/**@{ + * Obsolete IDatabase::makeList() constants + * These are also available as Database class constants + */ +define( 'LIST_COMMA', IDatabase::LIST_COMMA ); +define( 'LIST_AND', IDatabase::LIST_AND ); +define( 'LIST_SET', IDatabase::LIST_SET ); +define( 'LIST_NAMES', IDatabase::LIST_NAMES ); +define( 'LIST_OR', IDatabase::LIST_OR ); +/**@}*/ + /**@{ * Virtual namespaces; don't appear in the page database */ diff --git a/includes/Html.php b/includes/Html.php index 8c01448749..2ef891d168 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -46,13 +46,12 @@ * @since 1.16 */ class Html { - // List of void elements from HTML5, section 8.1.2 as of 2011-08-12 + // List of void elements from HTML5, section 8.1.2 as of 2016-09-19 private static $voidElements = [ 'area', 'base', 'br', 'col', - 'command', 'embed', 'hr', 'img', @@ -339,7 +338,6 @@ class Html { 'height' => '150', 'width' => '300', ], - 'command' => [ 'type' => 'command' ], 'form' => [ 'action' => 'GET', 'autocomplete' => 'on', diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 7a34b3ab18..8c7d802d68 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -45,51 +45,13 @@ return [ 'DBLoadBalancerFactory' => function( MediaWikiServices $services ) { $mainConfig = $services->getMainConfig(); - $lbConf = $mainConfig->get( 'LBFactoryConf' ); - $lbConf += [ - 'localDomain' => new DatabaseDomain( - $mainConfig->get( 'DBname' ), null, $mainConfig->get( 'DBprefix' ) ), - // TODO: replace the global wfConfiguredReadOnlyReason() with a service. - 'readOnlyReason' => wfConfiguredReadOnlyReason(), - ]; - + $lbConf = LBFactoryMW::applyDefaultConfig( + $mainConfig->get( 'LBFactoryConf' ), + $mainConfig + ); $class = LBFactoryMW::getLBFactoryClass( $lbConf ); - if ( $class === 'LBFactorySimple' ) { - if ( is_array( $mainConfig->get( 'DBservers' ) ) ) { - foreach ( $mainConfig->get( 'DBservers' ) as $i => $server ) { - $lbConf['servers'][$i] = $server + [ - 'schema' => $mainConfig->get( 'DBmwschema' ), - 'tablePrefix' => $mainConfig->get( 'DBprefix' ), - 'flags' => DBO_DEFAULT, - 'sqlMode' => $mainConfig->get( 'SQLMode' ), - 'utf8Mode' => $mainConfig->get( 'DBmysql5' ) - ]; - } - } else { - $flags = DBO_DEFAULT; - $flags |= $mainConfig->get( 'DebugDumpSql' ) ? DBO_DEBUG : 0; - $flags |= $mainConfig->get( 'DBssl' ) ? DBO_SSL : 0; - $flags |= $mainConfig->get( 'DBcompress' ) ? DBO_COMPRESS : 0; - $lbConf['servers'] = [ - [ - 'host' => $mainConfig->get( 'DBserver' ), - 'user' => $mainConfig->get( 'DBuser' ), - 'password' => $mainConfig->get( 'DBpassword' ), - 'dbname' => $mainConfig->get( 'DBname' ), - 'schema' => $mainConfig->get( 'DBmwschema' ), - 'tablePrefix' => $mainConfig->get( 'DBprefix' ), - 'type' => $mainConfig->get( 'DBtype' ), - 'load' => 1, - 'flags' => $flags, - 'sqlMode' => $mainConfig->get( 'SQLMode' ), - 'utf8Mode' => $mainConfig->get( 'DBmysql5' ) - ] - ]; - } - $lbConf['externalServers'] = $mainConfig->get( 'ExternalServers' ); - } - return new $class( LBFactoryMW::applyDefaultConfig( $lbConf ) ); + return new $class( $lbConf ); }, 'DBLoadBalancer' => function( MediaWikiServices $services ) { diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 1f3c76a636..ae3f3f2458 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -1299,7 +1299,7 @@ class ApiMain extends ApiBase { } if ( $module->isWriteMode() - && in_array( 'bot', $this->getUser()->getGroups() ) + && $this->getUser()->isBot() && wfGetLB()->getServerCount() > 1 ) { $this->checkBotReadOnly(); diff --git a/includes/changes/RecentChange.php b/includes/changes/RecentChange.php index 306ea06e9f..794865e439 100644 --- a/includes/changes/RecentChange.php +++ b/includes/changes/RecentChange.php @@ -297,7 +297,8 @@ class RecentChange { } # If our database is strict about IP addresses, use NULL instead of an empty string - if ( $dbw->strictIPs() && $this->mAttribs['rc_ip'] == '' ) { + $strictIPs = in_array( $dbw->getType(), [ 'oracle', 'postgres' ] ); // legacy + if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) { unset( $this->mAttribs['rc_ip'] ); } diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 339174e7bb..2c6db103b9 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -50,7 +50,7 @@ class DatabaseMssql extends DatabaseBase { return false; } - public function strictIPs() { + public function realTimestamps() { return false; } @@ -1257,13 +1257,6 @@ class DatabaseMssql extends DatabaseBase { return $sql; } - /** - * @return string - */ - public function getSearchEngine() { - return "SearchMssql"; - } - /** * Returns an associative array for fields that are of type varbinary, binary, or image * $table can be either a raw table name or passed through tableName() first diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 9e821a15a3..ee1bf65731 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -1509,10 +1509,6 @@ class DatabaseOracle extends DatabaseBase { return 'CAST ( ' . $field . ' AS VARCHAR2 )'; } - public function getSearchEngine() { - return 'SearchOracle'; - } - public function getInfinity() { return '31-12-2030 12:00:00.000000'; } diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 2773067f83..e5ce283cbf 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -1533,10 +1533,6 @@ SQL; return $field . '::text'; } - public function getSearchEngine() { - return 'SearchPostgres'; - } - public function streamStatementEnd( &$sql, &$newLine ) { # Allow dollar quoting for function declarations if ( substr( $newLine, 0, 4 ) == '$mw$' ) { diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php deleted file mode 100644 index 28fb5b5db7..0000000000 --- a/includes/db/DatabaseSqlite.php +++ /dev/null @@ -1,1068 +0,0 @@ -dbDir = isset( $p['dbDirectory'] ) ? $p['dbDirectory'] : $wgSQLiteDataDir; - - 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'] ); - } else { - $this->mDBname = $p['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() ) { - if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) { - $done = []; - foreach ( $this->tableAliases as $params ) { - if ( isset( $done[$params['dbname']] ) ) { - continue; - } - $this->attachDatabase( $params['dbname'] ); - $done[$params['dbname']] = 1; - } - } - } - } - - $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null; - if ( $this->trxMode && - !in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] ) - ) { - $this->trxMode = null; - wfWarn( "Invalid SQLite transaction mode provided." ); - } - - $this->lockMgr = new FSLockManager( [ 'lockDirectory' => "{$this->dbDir}/locks" ] ); - } - - /** - * @param string $filename - * @param array $p Options map; supports: - * - flags : (same as __construct counterpart) - * - trxMode : (same as __construct counterpart) - * - dbDirectory : (same as __construct counterpart) - * @return DatabaseSqlite - * @since 1.25 - */ - public static function newStandaloneInstance( $filename, array $p = [] ) { - $p['dbFilePath'] = $filename; - $p['schema'] = false; - $p['tablePrefix'] = ''; - - return DatabaseBase::factory( 'sqlite', $p ); - } - - /** - * @return string - */ - function getType() { - return 'sqlite'; - } - - /** - * @todo Check if it should be true like parent class - * - * @return bool - */ - function implicitGroupby() { - return false; - } - - /** Open an SQLite database and return a resource handle to it - * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases - * - * @param string $server - * @param string $user - * @param string $pass - * @param string $dbName - * - * @throws DBConnectionError - * @return PDO - */ - function open( $server, $user, $pass, $dbName ) { - $this->close(); - $fileName = self::generateFileName( $this->dbDir, $dbName ); - if ( !is_readable( $fileName ) ) { - $this->mConn = false; - throw new DBConnectionError( $this, "SQLite database not accessible" ); - } - $this->openFile( $fileName ); - - return $this->mConn; - } - - /** - * Opens a database file - * - * @param string $fileName - * @throws DBConnectionError - * @return PDO|bool SQL connection or false if failed - */ - protected function openFile( $fileName ) { - $err = false; - - $this->dbPath = $fileName; - try { - if ( $this->mFlags & DBO_PERSISTENT ) { - $this->mConn = new PDO( "sqlite:$fileName", '', '', - [ PDO::ATTR_PERSISTENT => true ] ); - } else { - $this->mConn = new PDO( "sqlite:$fileName", '', '' ); - } - } catch ( PDOException $e ) { - $err = $e->getMessage(); - } - - if ( !$this->mConn ) { - wfDebug( "DB connection error: $err\n" ); - throw new DBConnectionError( $this, $err ); - } - - $this->mOpened = !!$this->mConn; - if ( $this->mOpened ) { - # Set error codes only, don't raise exceptions - $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); - # Enforce LIKE to be case sensitive, just like MySQL - $this->query( 'PRAGMA case_sensitive_like = 1' ); - - return $this->mConn; - } - - return false; - } - - /** - * @return string SQLite DB file path - * @since 1.25 - */ - public function getDbFilePath() { - return $this->dbPath; - } - - /** - * Does not actually close the connection, just destroys the reference for GC to do its work - * @return bool - */ - protected function closeConnection() { - $this->mConn = null; - - return true; - } - - /** - * Generates a database file name. Explicitly public for installer. - * @param string $dir Directory where database resides - * @param string $dbName Database name - * @return string - */ - public static function generateFileName( $dir, $dbName ) { - return "$dir/$dbName.sqlite"; - } - - /** - * Check if the searchindext table is FTS enabled. - * @return bool False if not enabled. - */ - function checkForEnabledSearch() { - if ( self::$fulltextEnabled === null ) { - self::$fulltextEnabled = false; - $table = $this->tableName( 'searchindex' ); - $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ ); - if ( $res ) { - $row = $res->fetchRow(); - self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false; - } - } - - return self::$fulltextEnabled; - } - - /** - * Returns version of currently supported SQLite fulltext search module or false if none present. - * @return string - */ - static function getFulltextSearchModule() { - static $cachedResult = null; - if ( $cachedResult !== null ) { - return $cachedResult; - } - $cachedResult = false; - $table = 'dummy_search_test'; - - $db = self::newStandaloneInstance( ':memory:' ); - if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) { - $cachedResult = 'FTS3'; - } - $db->close(); - - return $cachedResult; - } - - /** - * Attaches external database to our connection, see http://sqlite.org/lang_attach.html - * for details. - * - * @param string $name Database name to be used in queries like - * SELECT foo FROM dbname.table - * @param bool|string $file Database file name. If omitted, will be generated - * using $name and configured data directory - * @param string $fname Calling function name - * @return ResultWrapper - */ - function attachDatabase( $name, $file = false, $fname = __METHOD__ ) { - if ( !$file ) { - $file = self::generateFileName( $this->dbDir, $name ); - } - $file = $this->addQuotes( $file ); - - return $this->query( "ATTACH DATABASE $file AS $name", $fname ); - } - - function isWriteQuery( $sql ) { - return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql ); - } - - /** - * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result - * - * @param string $sql - * @return bool|ResultWrapper - */ - protected function doQuery( $sql ) { - $res = $this->mConn->query( $sql ); - if ( $res === false ) { - return false; - } else { - $r = $res instanceof ResultWrapper ? $res->result : $res; - $this->mAffectedRows = $r->rowCount(); - $res = new ResultWrapper( $this, $r->fetchAll() ); - } - - return $res; - } - - /** - * @param ResultWrapper|mixed $res - */ - function freeResult( $res ) { - if ( $res instanceof ResultWrapper ) { - $res->result = null; - } else { - $res = null; - } - } - - /** - * @param ResultWrapper|array $res - * @return stdClass|bool - */ - function fetchObject( $res ) { - if ( $res instanceof ResultWrapper ) { - $r =& $res->result; - } else { - $r =& $res; - } - - $cur = current( $r ); - if ( is_array( $cur ) ) { - next( $r ); - $obj = new stdClass; - foreach ( $cur as $k => $v ) { - if ( !is_numeric( $k ) ) { - $obj->$k = $v; - } - } - - return $obj; - } - - return false; - } - - /** - * @param ResultWrapper|mixed $res - * @return array|bool - */ - function fetchRow( $res ) { - if ( $res instanceof ResultWrapper ) { - $r =& $res->result; - } else { - $r =& $res; - } - $cur = current( $r ); - if ( is_array( $cur ) ) { - next( $r ); - - return $cur; - } - - return false; - } - - /** - * The PDO::Statement class implements the array interface so count() will work - * - * @param ResultWrapper|array $res - * @return int - */ - function numRows( $res ) { - $r = $res instanceof ResultWrapper ? $res->result : $res; - - return count( $r ); - } - - /** - * @param ResultWrapper $res - * @return int - */ - function numFields( $res ) { - $r = $res instanceof ResultWrapper ? $res->result : $res; - if ( is_array( $r ) && count( $r ) > 0 ) { - // The size of the result array is twice the number of fields. (Bug: 65578) - return count( $r[0] ) / 2; - } else { - // If the result is empty return 0 - return 0; - } - } - - /** - * @param ResultWrapper $res - * @param int $n - * @return bool - */ - function fieldName( $res, $n ) { - $r = $res instanceof ResultWrapper ? $res->result : $res; - if ( is_array( $r ) ) { - $keys = array_keys( $r[0] ); - - return $keys[$n]; - } - - return false; - } - - /** - * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks - * - * @param string $name - * @param string $format - * @return string - */ - function tableName( $name, $format = 'quoted' ) { - // table names starting with sqlite_ are reserved - if ( strpos( $name, 'sqlite_' ) === 0 ) { - return $name; - } - - return str_replace( '"', '', parent::tableName( $name, $format ) ); - } - - /** - * Index names have DB scope - * - * @param string $index - * @return string - */ - protected function indexName( $index ) { - return $index; - } - - /** - * This must be called after nextSequenceVal - * - * @return int - */ - function insertId() { - // PDO::lastInsertId yields a string :( - return intval( $this->mConn->lastInsertId() ); - } - - /** - * @param ResultWrapper|array $res - * @param int $row - */ - function dataSeek( $res, $row ) { - if ( $res instanceof ResultWrapper ) { - $r =& $res->result; - } else { - $r =& $res; - } - reset( $r ); - if ( $row > 0 ) { - for ( $i = 0; $i < $row; $i++ ) { - next( $r ); - } - } - } - - /** - * @return string - */ - function lastError() { - if ( !is_object( $this->mConn ) ) { - return "Cannot return last error, no db connection"; - } - $e = $this->mConn->errorInfo(); - - return isset( $e[2] ) ? $e[2] : ''; - } - - /** - * @return string - */ - function lastErrno() { - if ( !is_object( $this->mConn ) ) { - return "Cannot return last error, no db connection"; - } else { - $info = $this->mConn->errorInfo(); - - return $info[1]; - } - } - - /** - * @return int - */ - function affectedRows() { - return $this->mAffectedRows; - } - - /** - * Returns information about an index - * Returns false if the index does not exist - * - if errors are explicitly ignored, returns NULL on failure - * - * @param string $table - * @param string $index - * @param string $fname - * @return array - */ - function indexInfo( $table, $index, $fname = __METHOD__ ) { - $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')'; - $res = $this->query( $sql, $fname ); - if ( !$res ) { - return null; - } - if ( $res->numRows() == 0 ) { - return false; - } - $info = []; - foreach ( $res as $row ) { - $info[] = $row->name; - } - - return $info; - } - - /** - * @param string $table - * @param string $index - * @param string $fname - * @return bool|null - */ - function indexUnique( $table, $index, $fname = __METHOD__ ) { - $row = $this->selectRow( 'sqlite_master', '*', - [ - 'type' => 'index', - 'name' => $this->indexName( $index ), - ], $fname ); - if ( !$row || !isset( $row->sql ) ) { - return null; - } - - // $row->sql will be of the form CREATE [UNIQUE] INDEX ... - $indexPos = strpos( $row->sql, 'INDEX' ); - if ( $indexPos === false ) { - return null; - } - $firstPart = substr( $row->sql, 0, $indexPos ); - $options = explode( ' ', $firstPart ); - - return in_array( 'UNIQUE', $options ); - } - - /** - * Filter the options used in SELECT statements - * - * @param array $options - * @return array - */ - function makeSelectOptions( $options ) { - foreach ( $options as $k => $v ) { - if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) { - $options[$k] = ''; - } - } - - return parent::makeSelectOptions( $options ); - } - - /** - * @param array $options - * @return string - */ - protected function makeUpdateOptionsArray( $options ) { - $options = parent::makeUpdateOptionsArray( $options ); - $options = self::fixIgnore( $options ); - - return $options; - } - - /** - * @param array $options - * @return array - */ - static function fixIgnore( $options ) { - # SQLite uses OR IGNORE not just IGNORE - foreach ( $options as $k => $v ) { - if ( $v == 'IGNORE' ) { - $options[$k] = 'OR IGNORE'; - } - } - - return $options; - } - - /** - * @param array $options - * @return string - */ - function makeInsertOptions( $options ) { - $options = self::fixIgnore( $options ); - - return parent::makeInsertOptions( $options ); - } - - /** - * Based on generic method (parent) with some prior SQLite-sepcific adjustments - * @param string $table - * @param array $a - * @param string $fname - * @param array $options - * @return bool - */ - function insert( $table, $a, $fname = __METHOD__, $options = [] ) { - if ( !count( $a ) ) { - return true; - } - - # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts - if ( isset( $a[0] ) && is_array( $a[0] ) ) { - $ret = true; - foreach ( $a as $v ) { - if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) { - $ret = false; - } - } - } else { - $ret = parent::insert( $table, $a, "$fname/single-row", $options ); - } - - return $ret; - } - - /** - * @param string $table - * @param array $uniqueIndexes Unused - * @param string|array $rows - * @param string $fname - * @return bool|ResultWrapper - */ - function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { - if ( !count( $rows ) ) { - return true; - } - - # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries - if ( isset( $rows[0] ) && is_array( $rows[0] ) ) { - $ret = true; - foreach ( $rows as $v ) { - if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) { - $ret = false; - } - } - } else { - $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" ); - } - - return $ret; - } - - /** - * Returns the size of a text field, or -1 for "unlimited" - * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though. - * - * @param string $table - * @param string $field - * @return int - */ - function textFieldSize( $table, $field ) { - return -1; - } - - /** - * @return bool - */ - function unionSupportsOrderAndLimit() { - return false; - } - - /** - * @param string $sqls - * @param bool $all Whether to "UNION ALL" or not - * @return string - */ - function unionQueries( $sqls, $all ) { - $glue = $all ? ' UNION ALL ' : ' UNION '; - - return implode( $glue, $sqls ); - } - - /** - * @return bool - */ - function wasDeadlock() { - return $this->lastErrno() == 5; // SQLITE_BUSY - } - - /** - * @return bool - */ - function wasErrorReissuable() { - return $this->lastErrno() == 17; // SQLITE_SCHEMA; - } - - /** - * @return bool - */ - function wasReadOnlyError() { - return $this->lastErrno() == 8; // SQLITE_READONLY; - } - - /** - * @return string Wikitext of a link to the server software's web site - */ - public function getSoftwareLink() { - return "[{{int:version-db-sqlite-url}} SQLite]"; - } - - /** - * @return string Version information from the database - */ - function getServerVersion() { - $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION ); - - return $ver; - } - - /** - * @return string User-friendly database information - */ - public function getServerInfo() { - return wfMessage( self::getFulltextSearchModule() - ? 'sqlite-has-fts' - : 'sqlite-no-fts', $this->getServerVersion() )->text(); - } - - /** - * Get information about a given field - * Returns false if the field does not exist. - * - * @param string $table - * @param string $field - * @return SQLiteField|bool False on failure - */ - function fieldInfo( $table, $field ) { - $tableName = $this->tableName( $table ); - $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')'; - $res = $this->query( $sql, __METHOD__ ); - foreach ( $res as $row ) { - if ( $row->name == $field ) { - return new SQLiteField( $row, $tableName ); - } - } - - return false; - } - - protected function doBegin( $fname = '' ) { - if ( $this->trxMode ) { - $this->query( "BEGIN {$this->trxMode}", $fname ); - } else { - $this->query( 'BEGIN', $fname ); - } - $this->mTrxLevel = 1; - } - - /** - * @param string $s - * @return string - */ - function strencode( $s ) { - return substr( $this->addQuotes( $s ), 1, -1 ); - } - - /** - * @param string $b - * @return Blob - */ - function encodeBlob( $b ) { - return new Blob( $b ); - } - - /** - * @param Blob|string $b - * @return string - */ - function decodeBlob( $b ) { - if ( $b instanceof Blob ) { - $b = $b->fetch(); - } - - return $b; - } - - /** - * @param Blob|string $s - * @return string - */ - function addQuotes( $s ) { - if ( $s instanceof Blob ) { - return "x'" . bin2hex( $s->fetch() ) . "'"; - } elseif ( is_bool( $s ) ) { - return (int)$s; - } elseif ( strpos( $s, "\0" ) !== false ) { - // SQLite doesn't support \0 in strings, so use the hex representation as a workaround. - // This is a known limitation of SQLite's mprintf function which PDO - // should work around, but doesn't. I have reported this to php.net as bug #63419: - // https://bugs.php.net/bug.php?id=63419 - // There was already a similar report for SQLite3::escapeString, bug #62361: - // https://bugs.php.net/bug.php?id=62361 - // There is an additional bug regarding sorting this data after insert - // on older versions of sqlite shipped with ubuntu 12.04 - // https://phabricator.wikimedia.org/T74367 - wfDebugLog( - __CLASS__, - __FUNCTION__ . - ': Quoting value containing null byte. ' . - 'For consistency all binary data should have been ' . - 'first processed with self::encodeBlob()' - ); - return "x'" . bin2hex( $s ) . "'"; - } else { - return $this->mConn->quote( $s ); - } - } - - /** - * @return string - */ - function buildLike() { - $params = func_get_args(); - if ( count( $params ) > 0 && is_array( $params[0] ) ) { - $params = $params[0]; - } - - return parent::buildLike( $params ) . "ESCAPE '\' "; - } - - /** - * @param string $field Field or column to cast - * @return string - * @since 1.28 - */ - public function buildStringCast( $field ) { - return 'CAST ( ' . $field . ' AS TEXT )'; - } - - /** - * @return string - */ - public function getSearchEngine() { - return "SearchSqlite"; - } - - /** - * No-op version of deadlockLoop - * - * @return mixed - */ - public function deadlockLoop( /*...*/ ) { - $args = func_get_args(); - $function = array_shift( $args ); - - return call_user_func_array( $function, $args ); - } - - /** - * @param string $s - * @return string - */ - protected function replaceVars( $s ) { - $s = parent::replaceVars( $s ); - if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) { - // CREATE TABLE hacks to allow schema file sharing with MySQL - - // binary/varbinary column type -> blob - $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s ); - // no such thing as unsigned - $s = preg_replace( '/\b(un)?signed\b/i', '', $s ); - // INT -> INTEGER - $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s ); - // floating point types -> REAL - $s = preg_replace( - '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', - 'REAL', - $s - ); - // varchar -> TEXT - $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s ); - // TEXT normalization - $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s ); - // BLOB normalization - $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s ); - // BOOL -> INTEGER - $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s ); - // DATETIME -> TEXT - $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s ); - // No ENUM type - $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s ); - // binary collation type -> nothing - $s = preg_replace( '/\bbinary\b/i', '', $s ); - // auto_increment -> autoincrement - $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s ); - // No explicit options - $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s ); - // AUTOINCREMENT should immedidately follow PRIMARY KEY - $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s ); - } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) { - // No truncated indexes - $s = preg_replace( '/\(\d+\)/', '', $s ); - // No FULLTEXT - $s = preg_replace( '/\bfulltext\b/i', '', $s ); - } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) { - // DROP INDEX is database-wide, not table-specific, so no ON clause. - $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s ); - } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) { - // INSERT IGNORE --> INSERT OR IGNORE - $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s ); - } - - return $s; - } - - public function lock( $lockName, $method, $timeout = 5 ) { - if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed - if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) { - throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." ); - } - } - - return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK(); - } - - public function unlock( $lockName, $method ) { - return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isOK(); - } - - /** - * Build a concatenation list to feed into a SQL query - * - * @param string[] $stringList - * @return string - */ - function buildConcat( $stringList ) { - return '(' . implode( ') || (', $stringList ) . ')'; - } - - public function buildGroupConcatField( - $delim, $table, $field, $conds = '', $join_conds = [] - ) { - $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')'; - - return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')'; - } - - /** - * @param string $oldName - * @param string $newName - * @param bool $temporary - * @param string $fname - * @return bool|ResultWrapper - * @throws RuntimeException - */ - function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) { - $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . - $this->addQuotes( $oldName ) . " AND type='table'", $fname ); - $obj = $this->fetchObject( $res ); - if ( !$obj ) { - throw new RuntimeException( "Couldn't retrieve structure for table $oldName" ); - } - $sql = $obj->sql; - $sql = preg_replace( - '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/', - $this->addIdentifierQuotes( $newName ), - $sql, - 1 - ); - if ( $temporary ) { - if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) { - wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" ); - } else { - $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql ); - } - } - - $res = $this->query( $sql, $fname ); - - // Take over indexes - $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' ); - foreach ( $indexList as $index ) { - if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) { - continue; - } - - if ( $index->unique ) { - $sql = 'CREATE UNIQUE INDEX'; - } else { - $sql = 'CREATE INDEX'; - } - // Try to come up with a new index name, given indexes have database scope in SQLite - $indexName = $newName . '_' . $index->name; - $sql .= ' ' . $indexName . ' ON ' . $newName; - - $indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' ); - $fields = []; - foreach ( $indexInfo as $indexInfoRow ) { - $fields[$indexInfoRow->seqno] = $indexInfoRow->name; - } - - $sql .= '(' . implode( ',', $fields ) . ')'; - - $this->query( $sql ); - } - - return $res; - } - - /** - * List all tables on the database - * - * @param string $prefix Only show tables with this prefix, e.g. mw_ - * @param string $fname Calling function name - * - * @return array - */ - function listTables( $prefix = null, $fname = __METHOD__ ) { - $result = $this->select( - 'sqlite_master', - 'name', - "type='table'" - ); - - $endArray = []; - - foreach ( $result as $table ) { - $vars = get_object_vars( $table ); - $table = array_pop( $vars ); - - if ( !$prefix || strpos( $table, $prefix ) === 0 ) { - if ( strpos( $table, 'sqlite_' ) !== 0 ) { - $endArray[] = $table; - } - } - } - - return $endArray; - } - - /** - * Override due to no CASCADE support - * - * @param string $tableName - * @param string $fName - * @return bool|ResultWrapper - * @throws DBReadOnlyError - */ - public function dropTable( $tableName, $fName = __METHOD__ ) { - if ( !$this->tableExists( $tableName, $fName ) ) { - return false; - } - $sql = "DROP TABLE " . $this->tableName( $tableName ); - - return $this->query( $sql, $fName ); - } - - /** - * @return string - */ - public function __toString() { - return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION ); - } - -} // end DatabaseSqlite class diff --git a/includes/db/loadbalancer/LBFactoryMW.php b/includes/db/loadbalancer/LBFactoryMW.php index 69fd21dc16..e943a8ab40 100644 --- a/includes/db/loadbalancer/LBFactoryMW.php +++ b/includes/db/loadbalancer/LBFactoryMW.php @@ -27,69 +27,99 @@ use MediaWiki\Logger\LoggerFactory; * Legacy MediaWiki-specific class for generating database load balancers * @ingroup Database */ -abstract class LBFactoryMW extends LBFactory { +abstract class LBFactoryMW { /** - * Construct a factory based on a configuration array (typically from $wgLBFactoryConf) - * @param array $conf - * @TODO: inject objects via dependency framework - */ - public function __construct( array $conf ) { - parent::__construct( self::applyDefaultConfig( $conf ) ); - } - - /** - * @param array $conf + * @param array $lbConf Config for LBFactory::__construct() + * @param Config $mainConfig Main config object from MediaWikiServices * @return array - * @TODO: inject objects via dependency framework */ - public static function applyDefaultConfig( array $conf ) { - global $wgDBtype, $wgSQLMode, $wgDBmysql5, $wgDBname, $wgDBprefix, $wgDBmwschema; + public static function applyDefaultConfig( array $lbConf, Config $mainConfig ) { global $wgCommandLineMode; - $defaults = [ - 'localDomain' => new DatabaseDomain( $wgDBname, null, $wgDBprefix ), - 'hostname' => wfHostname(), + $lbConf += [ + 'localDomain' => new DatabaseDomain( + $mainConfig->get( 'DBname' ), + null, + $mainConfig->get( 'DBprefix' ) + ), 'profiler' => Profiler::instance(), 'trxProfiler' => Profiler::instance()->getTransactionProfiler(), 'replLogger' => LoggerFactory::getInstance( 'DBReplication' ), - 'queryLogger' => LoggerFactory::getInstance( 'wfLogDBError' ), - 'connLogger' => LoggerFactory::getInstance( 'wfLogDBError' ), + 'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ), + 'connLogger' => LoggerFactory::getInstance( 'DBConnection' ), 'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ), 'errorLogger' => [ MWExceptionHandler::class, 'logException' ], 'cliMode' => $wgCommandLineMode, - 'agent' => '' + 'hostname' => wfHostname(), + // TODO: replace the global wfConfiguredReadOnlyReason() with a service. + 'readOnlyReason' => wfConfiguredReadOnlyReason(), ]; + + if ( $lbConf['class'] === 'LBFactorySimple' ) { + if ( isset( $lbConf['servers'] ) ) { + // Server array is already explicitly configured; leave alone + } elseif ( is_array( $mainConfig->get( 'DBservers' ) ) ) { + foreach ( $mainConfig->get( 'DBservers' ) as $i => $server ) { + if ( $server['type'] === 'sqlite' ) { + $server += [ 'dbDirectory' => $mainConfig->get( 'SQLiteDataDir' ) ]; + } + $lbConf['servers'][$i] = $server + [ + 'schema' => $mainConfig->get( 'DBmwschema' ), + 'tablePrefix' => $mainConfig->get( 'DBprefix' ), + 'flags' => DBO_DEFAULT, + 'sqlMode' => $mainConfig->get( 'SQLMode' ), + 'utf8Mode' => $mainConfig->get( 'DBmysql5' ) + ]; + } + } else { + $flags = DBO_DEFAULT; + $flags |= $mainConfig->get( 'DebugDumpSql' ) ? DBO_DEBUG : 0; + $flags |= $mainConfig->get( 'DBssl' ) ? DBO_SSL : 0; + $flags |= $mainConfig->get( 'DBcompress' ) ? DBO_COMPRESS : 0; + $server = [ + 'host' => $mainConfig->get( 'DBserver' ), + 'user' => $mainConfig->get( 'DBuser' ), + 'password' => $mainConfig->get( 'DBpassword' ), + 'dbname' => $mainConfig->get( 'DBname' ), + 'schema' => $mainConfig->get( 'DBmwschema' ), + 'tablePrefix' => $mainConfig->get( 'DBprefix' ), + 'type' => $mainConfig->get( 'DBtype' ), + 'load' => 1, + 'flags' => $flags, + 'sqlMode' => $mainConfig->get( 'SQLMode' ), + 'utf8Mode' => $mainConfig->get( 'DBmysql5' ) + ]; + if ( $server['type'] === 'sqlite' ) { + $server[ 'dbDirectory'] = $mainConfig->get( 'SQLiteDataDir' ); + } + $lbConf['servers'] = [ $server ]; + } + if ( !isset( $lbConf['externalServers'] ) ) { + $lbConf['externalServers'] = $mainConfig->get( 'ExternalServers' ); + } + } elseif ( $lbConf['class'] === 'LBFactoryMulti' ) { + if ( isset( $lbConf['serverTemplate'] ) ) { + $lbConf['serverTemplate']['schema'] = $mainConfig->get( 'DBmwschema' ); + $lbConf['serverTemplate']['sqlMode'] = $mainConfig->get( 'SQLMode' ); + $lbConf['serverTemplate']['utf8Mode'] = $mainConfig->get( 'DBmysql5' ); + } + } + // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804) $sCache = ObjectCache::getLocalServerInstance(); if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) { - $defaults['srvCache'] = $sCache; + $lbConf['srvCache'] = $sCache; } $cCache = ObjectCache::getLocalClusterInstance(); if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) { - $defaults['memCache'] = $cCache; + $lbConf['memCache'] = $cCache; } $wCache = ObjectCache::getMainWANInstance(); if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) { - $defaults['wanCache'] = $wCache; - } - - // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema, - // and everything else doesn't use a schema (e.g. null) - // Although postgres and oracle support schemas, we don't use them (yet) - // to maintain backwards compatibility - $schema = ( $wgDBtype === 'mssql' ) ? $wgDBmwschema : null; - - if ( isset( $conf['serverTemplate'] ) ) { // LBFactoryMulti - $conf['serverTemplate']['schema'] = $schema; - $conf['serverTemplate']['sqlMode'] = $wgSQLMode; - $conf['serverTemplate']['utf8Mode'] = $wgDBmysql5; - } elseif ( isset( $conf['servers'] ) ) { // LBFactorySimple - foreach ( $conf['servers'] as $i => $server ) { - $conf['servers'][$i]['schema'] = $schema; - } + $lbConf['wanCache'] = $wCache; } - return $conf + $defaults; + return $lbConf; } /** diff --git a/includes/db/loadbalancer/LBFactoryMulti.php b/includes/db/loadbalancer/LBFactoryMulti.php deleted file mode 100644 index 1f7f528ab1..0000000000 --- a/includes/db/loadbalancer/LBFactoryMulti.php +++ /dev/null @@ -1,422 +0,0 @@ - lowest): - * - templateOverridesByServer - * - masterTemplateOverrides - * - templateOverridesBySection/templateOverridesByCluster - * - externalTemplateOverrides - * - serverTemplate - * Overrides only work on top level keys (so nested values will not be merged). - * - * Configuration: - * sectionsByDB A map of database names to section names. - * - * sectionLoads A 2-d map. For each section, gives a map of server names to - * load ratios. For example: - * [ - * 'section1' => [ - * 'db1' => 100, - * 'db2' => 100 - * ] - * ] - * - * serverTemplate A server info associative array as documented for $wgDBservers. - * The host, hostName and load entries will be overridden. - * - * groupLoadsBySection A 3-d map giving server load ratios for each section and group. - * For example: - * [ - * 'section1' => [ - * 'group1' => [ - * 'db1' => 100, - * 'db2' => 100 - * ] - * ] - * ] - * - * groupLoadsByDB A 3-d map giving server load ratios by DB name. - * - * hostsByName A map of hostname to IP address. - * - * externalLoads A map of external storage cluster name to server load map. - * - * externalTemplateOverrides A set of server info keys overriding serverTemplate for external - * storage. - * - * templateOverridesByServer A 2-d map overriding serverTemplate and - * externalTemplateOverrides on a server-by-server basis. Applies - * to both core and external storage. - * templateOverridesBySection A 2-d map overriding the server info by section. - * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster. - * - * masterTemplateOverrides An override array for all master servers. - * - * loadMonitorClass Name of the LoadMonitor class to always use. - * - * readOnlyBySection A map of section name to read-only message. - * Missing or false for read/write. - * - * @ingroup Database - */ -class LBFactoryMulti extends LBFactoryMW { - /** @var array A map of database names to section names */ - private $sectionsByDB; - - /** - * @var array A 2-d map. For each section, gives a map of server names to - * load ratios - */ - private $sectionLoads; - - /** - * @var array A server info associative array as documented for - * $wgDBservers. The host, hostName and load entries will be - * overridden - */ - private $serverTemplate; - - // Optional settings - - /** @var array A 3-d map giving server load ratios for each section and group */ - private $groupLoadsBySection = []; - - /** @var array A 3-d map giving server load ratios by DB name */ - private $groupLoadsByDB = []; - - /** @var array A map of hostname to IP address */ - private $hostsByName = []; - - /** @var array A map of external storage cluster name to server load map */ - private $externalLoads = []; - - /** - * @var array A set of server info keys overriding serverTemplate for - * external storage - */ - private $externalTemplateOverrides; - - /** - * @var array A 2-d map overriding serverTemplate and - * externalTemplateOverrides on a server-by-server basis. Applies to both - * core and external storage - */ - private $templateOverridesByServer; - - /** @var array A 2-d map overriding the server info by section */ - private $templateOverridesBySection; - - /** @var array A 2-d map overriding the server info by external storage cluster */ - private $templateOverridesByCluster; - - /** @var array An override array for all master servers */ - private $masterTemplateOverrides; - - /** - * @var array|bool A map of section name to read-only message. Missing or - * false for read/write - */ - private $readOnlyBySection = []; - - // Other stuff - - /** @var array Load balancer factory configuration */ - private $conf; - - /** @var LoadBalancer[] */ - private $mainLBs = []; - - /** @var LoadBalancer[] */ - private $extLBs = []; - - /** @var string */ - private $loadMonitorClass; - - /** @var string */ - private $lastWiki; - - /** @var string */ - private $lastSection; - - /** - * @param array $conf - * @throws InvalidArgumentException - */ - public function __construct( array $conf ) { - parent::__construct( $conf ); - - $this->conf = $conf; - $required = [ 'sectionsByDB', 'sectionLoads', 'serverTemplate' ]; - $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName', - 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer', - 'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides', - 'readOnlyBySection', 'loadMonitorClass' ]; - - foreach ( $required as $key ) { - if ( !isset( $conf[$key] ) ) { - throw new InvalidArgumentException( __CLASS__ . ": $key is required in configuration" ); - } - $this->$key = $conf[$key]; - } - - foreach ( $optional as $key ) { - if ( isset( $conf[$key] ) ) { - $this->$key = $conf[$key]; - } - } - } - - /** - * @param bool|string $wiki - * @return string - */ - private function getSectionForWiki( $wiki = false ) { - if ( $this->lastWiki === $wiki ) { - return $this->lastSection; - } - list( $dbName, ) = $this->getDBNameAndPrefix( $wiki ); - if ( isset( $this->sectionsByDB[$dbName] ) ) { - $section = $this->sectionsByDB[$dbName]; - } else { - $section = 'DEFAULT'; - } - $this->lastSection = $section; - $this->lastWiki = $wiki; - - return $section; - } - - /** - * @param bool|string $wiki - * @return LoadBalancer - */ - public function newMainLB( $wiki = false ) { - list( $dbName, ) = $this->getDBNameAndPrefix( $wiki ); - $section = $this->getSectionForWiki( $wiki ); - if ( isset( $this->groupLoadsByDB[$dbName] ) ) { - $groupLoads = $this->groupLoadsByDB[$dbName]; - } else { - $groupLoads = []; - } - - if ( isset( $this->groupLoadsBySection[$section] ) ) { - $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] ); - } - - $readOnlyReason = $this->readOnlyReason; - // Use the LB-specific read-only reason if everything isn't already read-only - if ( $readOnlyReason === false && isset( $this->readOnlyBySection[$section] ) ) { - $readOnlyReason = $this->readOnlyBySection[$section]; - } - - $template = $this->serverTemplate; - if ( isset( $this->templateOverridesBySection[$section] ) ) { - $template = $this->templateOverridesBySection[$section] + $template; - } - - return $this->newLoadBalancer( - $template, - $this->sectionLoads[$section], - $groupLoads, - $readOnlyReason - ); - } - - /** - * @param bool|string $wiki - * @return LoadBalancer - */ - public function getMainLB( $wiki = false ) { - $section = $this->getSectionForWiki( $wiki ); - if ( !isset( $this->mainLBs[$section] ) ) { - $lb = $this->newMainLB( $wiki ); - $this->getChronologyProtector()->initLB( $lb ); - $this->mainLBs[$section] = $lb; - } - - return $this->mainLBs[$section]; - } - - /** - * @param string $cluster - * @param bool|string $wiki - * @throws InvalidArgumentException - * @return LoadBalancer - */ - protected function newExternalLB( $cluster, $wiki = false ) { - if ( !isset( $this->externalLoads[$cluster] ) ) { - throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" ); - } - $template = $this->serverTemplate; - if ( isset( $this->externalTemplateOverrides ) ) { - $template = $this->externalTemplateOverrides + $template; - } - if ( isset( $this->templateOverridesByCluster[$cluster] ) ) { - $template = $this->templateOverridesByCluster[$cluster] + $template; - } - - return $this->newLoadBalancer( - $template, - $this->externalLoads[$cluster], - [], - $this->readOnlyReason - ); - } - - /** - * @param string $cluster External storage cluster, or false for core - * @param bool|string $wiki Wiki ID, or false for the current wiki - * @return LoadBalancer - */ - public function getExternalLB( $cluster, $wiki = false ) { - if ( !isset( $this->extLBs[$cluster] ) ) { - $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki ); - $this->getChronologyProtector()->initLB( $this->extLBs[$cluster] ); - } - - return $this->extLBs[$cluster]; - } - - /** - * Make a new load balancer object based on template and load array - * - * @param array $template - * @param array $loads - * @param array $groupLoads - * @param string|bool $readOnlyReason - * @return LoadBalancer - */ - private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) { - $lb = new LoadBalancer( array_merge( - $this->baseLoadBalancerParams(), - [ - 'servers' => $this->makeServerArray( $template, $loads, $groupLoads ), - 'loadMonitor' => $this->loadMonitorClass, - 'readOnlyReason' => $readOnlyReason - ] - ) ); - $this->initLoadBalancer( $lb ); - - return $lb; - } - - /** - * Make a server array as expected by LoadBalancer::__construct, using a template and load array - * - * @param array $template - * @param array $loads - * @param array $groupLoads - * @return array - */ - private function makeServerArray( $template, $loads, $groupLoads ) { - $servers = []; - $master = true; - $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads ); - foreach ( $groupLoadsByServer as $server => $stuff ) { - if ( !isset( $loads[$server] ) ) { - $loads[$server] = 0; - } - } - foreach ( $loads as $serverName => $load ) { - $serverInfo = $template; - if ( $master ) { - $serverInfo['master'] = true; - if ( isset( $this->masterTemplateOverrides ) ) { - $serverInfo = $this->masterTemplateOverrides + $serverInfo; - } - $master = false; - } else { - $serverInfo['replica'] = true; - } - if ( isset( $this->templateOverridesByServer[$serverName] ) ) { - $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo; - } - if ( isset( $groupLoadsByServer[$serverName] ) ) { - $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName]; - } - if ( isset( $this->hostsByName[$serverName] ) ) { - $serverInfo['host'] = $this->hostsByName[$serverName]; - } else { - $serverInfo['host'] = $serverName; - } - $serverInfo['hostName'] = $serverName; - $serverInfo['load'] = $load; - $serverInfo += [ 'flags' => DBO_DEFAULT ]; - - $servers[] = $serverInfo; - } - - return $servers; - } - - /** - * Take a group load array indexed by group then server, and reindex it by server then group - * @param array $groupLoads - * @return array - */ - private function reindexGroupLoads( $groupLoads ) { - $reindexed = []; - foreach ( $groupLoads as $group => $loads ) { - foreach ( $loads as $server => $load ) { - $reindexed[$server][$group] = $load; - } - } - - return $reindexed; - } - - /** - * Get the database name and prefix based on the wiki ID - * @param bool|string $wiki - * @return array - */ - private function getDBNameAndPrefix( $wiki = false ) { - if ( $wiki === false ) { - global $wgDBname, $wgDBprefix; - - return [ $wgDBname, $wgDBprefix ]; - } else { - return wfSplitWikiID( $wiki ); - } - } - - /** - * Execute a function for each tracked load balancer - * The callback is called with the load balancer as the first parameter, - * and $params passed as the subsequent parameters. - * @param callable $callback - * @param array $params - */ - public function forEachLB( $callback, array $params = [] ) { - foreach ( $this->mainLBs as $lb ) { - call_user_func_array( $callback, array_merge( [ $lb ], $params ) ); - } - foreach ( $this->extLBs as $lb ) { - call_user_func_array( $callback, array_merge( [ $lb ], $params ) ); - } - } -} diff --git a/includes/db/loadbalancer/LBFactorySingle.php b/includes/db/loadbalancer/LBFactorySingle.php deleted file mode 100644 index 3937dfd059..0000000000 --- a/includes/db/loadbalancer/LBFactorySingle.php +++ /dev/null @@ -1,126 +0,0 @@ -lb = new LoadBalancerSingle( array_merge( $this->baseLoadBalancerParams(), $conf ) ); - } - - /** - * @param bool|string $wiki - * @return LoadBalancerSingle - */ - public function newMainLB( $wiki = false ) { - return $this->lb; - } - - /** - * @param bool|string $wiki - * @return LoadBalancerSingle - */ - public function getMainLB( $wiki = false ) { - return $this->lb; - } - - /** - * @param string $cluster External storage cluster, or false for core - * @param bool|string $wiki Wiki ID, or false for the current wiki - * @return LoadBalancerSingle - */ - protected function newExternalLB( $cluster, $wiki = false ) { - return $this->lb; - } - - /** - * @param string $cluster External storage cluster, or false for core - * @param bool|string $wiki Wiki ID, or false for the current wiki - * @return LoadBalancerSingle - */ - public function getExternalLB( $cluster, $wiki = false ) { - return $this->lb; - } - - /** - * @param string|callable $callback - * @param array $params - */ - public function forEachLB( $callback, array $params = [] ) { - call_user_func_array( $callback, array_merge( [ $this->lb ], $params ) ); - } -} - -/** - * Helper class for LBFactorySingle. - */ -class LoadBalancerSingle extends LoadBalancer { - /** @var IDatabase */ - private $db; - - /** - * @param array $params - */ - public function __construct( array $params ) { - $this->db = $params['connection']; - - parent::__construct( [ - 'servers' => [ - [ - 'type' => $this->db->getType(), - 'host' => $this->db->getServer(), - 'dbname' => $this->db->getDBname(), - 'load' => 1, - ] - ], - 'trxProfiler' => isset( $params['trxProfiler'] ) ? $params['trxProfiler'] : null, - 'srvCache' => isset( $params['srvCache'] ) ? $params['srvCache'] : null, - 'wanCache' => isset( $params['wanCache'] ) ? $params['wanCache'] : null - ] ); - - if ( isset( $params['readOnlyReason'] ) ) { - $this->db->setLBInfo( 'readOnlyReason', $params['readOnlyReason'] ); - } - } - - /** - * - * @param string $server - * @param bool $dbNameOverride - * - * @return IDatabase - */ - protected function reallyOpenConnection( $server, $dbNameOverride = false ) { - return $this->db; - } -} diff --git a/includes/debug/logger/LegacyLogger.php b/includes/debug/logger/LegacyLogger.php index 526b4ab03b..ef7a994c9e 100644 --- a/includes/debug/logger/LegacyLogger.php +++ b/includes/debug/logger/LegacyLogger.php @@ -70,6 +70,14 @@ class LegacyLogger extends AbstractLogger { LogLevel::EMERGENCY => 600, ]; + /** + * @var array + */ + protected static $dbChannels = [ + 'DBQuery' => true, + 'DBConnection' => true + ]; + /** * @param string $channel */ @@ -83,14 +91,29 @@ class LegacyLogger extends AbstractLogger { * @param string|int $level * @param string $message * @param array $context + * @return null */ public function log( $level, $message, array $context = [] ) { - if ( self::shouldEmit( $this->channel, $message, $level, $context ) ) { - $text = self::format( $this->channel, $message, $context ); - $destination = self::destination( $this->channel, $message, $context ); + if ( isset( self::$dbChannels[$this->channel] ) + && isset( self::$levelMapping[$level] ) + && self::$levelMapping[$level] >= LogLevel::ERROR + ) { + // Format and write DB errors to the legacy locations + $effectiveChannel = 'wfLogDBError'; + } else { + $effectiveChannel = $this->channel; + } + + if ( self::shouldEmit( $effectiveChannel, $message, $level, $context ) ) { + $text = self::format( $effectiveChannel, $message, $context ); + $destination = self::destination( $effectiveChannel, $message, $context ); self::emit( $text, $destination ); } - if ( !isset( $context['private'] ) || !$context['private'] ) { + if ( $this->channel === 'DBQuery' && isset( $context['method'] ) + && isset( $context['master'] ) && isset( $context['runtime'] ) + ) { + MWDebug::query( $message, $context['method'], $context['master'], $context['runtime'] ); + } elseif ( !isset( $context['private'] ) || !$context['private'] ) { // Add to debug toolbar if not marked as "private" MWDebug::debugMsg( $message, [ 'channel' => $this->channel ] + $context ); } @@ -298,6 +321,7 @@ class LegacyLogger extends AbstractLogger { * @param string $channel * @param string $message * @param array $context + * @return null */ protected static function formatAsWfDebugLog( $channel, $message, $context ) { $time = wfTimestamp( TS_DB ); @@ -432,7 +456,6 @@ class LegacyLogger extends AbstractLogger { * * @param string $text * @param string $file Filename - * @throws MWException */ public static function emit( $text, $file ) { if ( substr( $file, 0, 4 ) == 'udp:' ) { diff --git a/includes/exception/MWExceptionRenderer.php b/includes/exception/MWExceptionRenderer.php index bb7a01f18e..e242da348b 100644 --- a/includes/exception/MWExceptionRenderer.php +++ b/includes/exception/MWExceptionRenderer.php @@ -28,11 +28,11 @@ class MWExceptionRenderer { const AS_PRETTY = 2; // show as HTML /** - * @param Exception $e Original exception + * @param Exception|Throwable $e Original exception * @param integer $mode MWExceptionExposer::AS_* constant - * @param Exception|null $eNew New exception from attempting to show the first + * @param Exception|Throwable|null $eNew New exception from attempting to show the first */ - public static function output( Exception $e, $mode, Exception $eNew = null ) { + public static function output( $e, $mode, $eNew = null ) { global $wgMimeType; if ( $e instanceof DBConnectionError ) { @@ -88,12 +88,12 @@ class MWExceptionRenderer { * * Called by MWException for b/c * - * @param Exception $e + * @param Exception|Throwable $e * @param string $name Class name of the exception * @param array $args Arguments to pass to the callback functions * @return string|null String to output or null if any hook has been called */ - public static function runHooks( Exception $e, $name, $args = [] ) { + public static function runHooks( $e, $name, $args = [] ) { global $wgExceptionHooks; if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) { @@ -129,10 +129,10 @@ class MWExceptionRenderer { } /** - * @param Exception $e + * @param Exception|Throwable $e * @return bool Should the exception use $wgOut to output the error? */ - private static function useOutputPage( Exception $e ) { + private static function useOutputPage( $e ) { // Can the extension use the Message class/wfMessage to get i18n-ed messages? foreach ( $e->getTrace() as $frame ) { if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) { @@ -150,9 +150,9 @@ class MWExceptionRenderer { /** * Output the exception report using HTML * - * @param Exception $e + * @param Exception|Throwable $e */ - private static function reportHTML( Exception $e ) { + private static function reportHTML( $e ) { global $wgOut, $wgSitename; if ( self::useOutputPage( $e ) ) { @@ -206,10 +206,10 @@ class MWExceptionRenderer { * backtrace to the error, otherwise show a message to ask to set it to true * to show that information. * - * @param Exception $e + * @param Exception|Throwable $e * @return string Html to output */ - public static function getHTML( Exception $e ) { + public static function getHTML( $e ) { if ( self::showBackTrace( $e ) ) { $html = "

" . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) . @@ -254,10 +254,10 @@ class MWExceptionRenderer { } /** - * @param Exception $e + * @param Exception|Throwable $e * @return string */ - private function getText( Exception $e ) { + private static function getText( $e ) { if ( self::showBackTrace( $e ) ) { return MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" . @@ -269,10 +269,10 @@ class MWExceptionRenderer { } /** - * @param Exception $e + * @param Exception|Throwable $e * @return bool */ - private static function showBackTrace( Exception $e ) { + private static function showBackTrace( $e ) { global $wgShowExceptionDetails, $wgShowDBErrorBacktrace; return ( @@ -324,9 +324,9 @@ class MWExceptionRenderer { } /** - * @param Exception $e + * @param Exception|Throwable $e */ - private static function reportOutageHTML( Exception $e ) { + private static function reportOutageHTML( $e ) { global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors; $sorry = htmlspecialchars( self::msg( diff --git a/includes/filebackend/FileBackend.php b/includes/filebackend/FileBackend.php index 1f91b3f13a..ed2bdcc140 100644 --- a/includes/filebackend/FileBackend.php +++ b/includes/filebackend/FileBackend.php @@ -1260,7 +1260,7 @@ abstract class FileBackend { final public function lockFiles( array $paths, $type, $timeout = 0 ) { $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); - return $this->lockManager->lock( $paths, $type, $timeout ); + return $this->wrapStatus( $this->lockManager->lock( $paths, $type, $timeout ) ); } /** @@ -1273,7 +1273,7 @@ abstract class FileBackend { final public function unlockFiles( array $paths, $type ) { $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); - return $this->lockManager->unlock( $paths, $type ); + return $this->wrapStatus( $this->lockManager->unlock( $paths, $type ) ); } /** diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php index cccf71a929..4667dde450 100644 --- a/includes/filebackend/lockmanager/DBLockManager.php +++ b/includes/filebackend/lockmanager/DBLockManager.php @@ -104,7 +104,7 @@ abstract class DBLockManager extends QuorumLockManager { // @todo change this code to work in one batch protected function getLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); foreach ( $pathsByType as $type => $paths ) { $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); } @@ -115,7 +115,7 @@ abstract class DBLockManager extends QuorumLockManager { abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type ); protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { - return Status::newGood(); + return StatusValue::newGood(); } /** diff --git a/includes/filebackend/lockmanager/FSLockManager.php b/includes/filebackend/lockmanager/FSLockManager.php deleted file mode 100644 index 8e149d6380..0000000000 --- a/includes/filebackend/lockmanager/FSLockManager.php +++ /dev/null @@ -1,248 +0,0 @@ - self::LOCK_SH, - self::LOCK_UW => self::LOCK_SH, - self::LOCK_EX => self::LOCK_EX - ]; - - protected $lockDir; // global dir for all servers - - /** @var array Map of (locked key => lock file handle) */ - protected $handles = []; - - /** - * Construct a new instance from configuration. - * - * @param array $config Includes: - * - lockDirectory : Directory containing the lock files - */ - function __construct( array $config ) { - parent::__construct( $config ); - - $this->lockDir = $config['lockDirectory']; - } - - /** - * @see LockManager::doLock() - * @param array $paths - * @param int $type - * @return StatusValue - */ - protected function doLock( array $paths, $type ) { - $status = Status::newGood(); - - $lockedPaths = []; // files locked in this attempt - foreach ( $paths as $path ) { - $status->merge( $this->doSingleLock( $path, $type ) ); - if ( $status->isOK() ) { - $lockedPaths[] = $path; - } else { - // Abort and unlock everything - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); - - return $status; - } - } - - return $status; - } - - /** - * @see LockManager::doUnlock() - * @param array $paths - * @param int $type - * @return StatusValue - */ - protected function doUnlock( array $paths, $type ) { - $status = Status::newGood(); - - foreach ( $paths as $path ) { - $status->merge( $this->doSingleUnlock( $path, $type ) ); - } - - return $status; - } - - /** - * Lock a single resource key - * - * @param string $path - * @param int $type - * @return StatusValue - */ - protected function doSingleLock( $path, $type ) { - $status = Status::newGood(); - - if ( isset( $this->locksHeld[$path][$type] ) ) { - ++$this->locksHeld[$path][$type]; - } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { - $this->locksHeld[$path][$type] = 1; - } else { - if ( isset( $this->handles[$path] ) ) { - $handle = $this->handles[$path]; - } else { - MediaWiki\suppressWarnings(); - $handle = fopen( $this->getLockPath( $path ), 'a+' ); - MediaWiki\restoreWarnings(); - if ( !$handle ) { // lock dir missing? - wfMkdirParents( $this->lockDir ); - $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again - } - } - if ( $handle ) { - // Either a shared or exclusive lock - $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX; - if ( flock( $handle, $lock | LOCK_NB ) ) { - // Record this lock as active - $this->locksHeld[$path][$type] = 1; - $this->handles[$path] = $handle; - } else { - fclose( $handle ); - $status->fatal( 'lockmanager-fail-acquirelock', $path ); - } - } else { - $status->fatal( 'lockmanager-fail-openlock', $path ); - } - } - - return $status; - } - - /** - * Unlock a single resource key - * - * @param string $path - * @param int $type - * @return StatusValue - */ - protected function doSingleUnlock( $path, $type ) { - $status = Status::newGood(); - - if ( !isset( $this->locksHeld[$path] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } elseif ( !isset( $this->locksHeld[$path][$type] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } else { - $handlesToClose = []; - --$this->locksHeld[$path][$type]; - if ( $this->locksHeld[$path][$type] <= 0 ) { - unset( $this->locksHeld[$path][$type] ); - } - if ( !count( $this->locksHeld[$path] ) ) { - unset( $this->locksHeld[$path] ); // no locks on this path - if ( isset( $this->handles[$path] ) ) { - $handlesToClose[] = $this->handles[$path]; - unset( $this->handles[$path] ); - } - } - // Unlock handles to release locks and delete - // any lock files that end up with no locks on them... - if ( wfIsWindows() ) { - // Windows: for any process, including this one, - // calling unlink() on a locked file will fail - $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); - $status->merge( $this->pruneKeyLockFiles( $path ) ); - } else { - // Unix: unlink() can be used on files currently open by this - // process and we must do so in order to avoid race conditions - $status->merge( $this->pruneKeyLockFiles( $path ) ); - $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); - } - } - - return $status; - } - - /** - * @param string $path - * @param array $handlesToClose - * @return StatusValue - */ - private function closeLockHandles( $path, array $handlesToClose ) { - $status = Status::newGood(); - foreach ( $handlesToClose as $handle ) { - if ( !flock( $handle, LOCK_UN ) ) { - $status->fatal( 'lockmanager-fail-releaselock', $path ); - } - if ( !fclose( $handle ) ) { - $status->warning( 'lockmanager-fail-closelock', $path ); - } - } - - return $status; - } - - /** - * @param string $path - * @return StatusValue - */ - private function pruneKeyLockFiles( $path ) { - $status = Status::newGood(); - if ( !isset( $this->locksHeld[$path] ) ) { - # No locks are held for the lock file anymore - if ( !unlink( $this->getLockPath( $path ) ) ) { - $status->warning( 'lockmanager-fail-deletelock', $path ); - } - unset( $this->handles[$path] ); - } - - return $status; - } - - /** - * Get the path to the lock file for a key - * @param string $path - * @return string - */ - protected function getLockPath( $path ) { - return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock"; - } - - /** - * Make sure remaining locks get cleared for sanity - */ - function __destruct() { - while ( count( $this->locksHeld ) ) { - foreach ( $this->locksHeld as $path => $locks ) { - $this->doSingleUnlock( $path, self::LOCK_EX ); - $this->doSingleUnlock( $path, self::LOCK_SH ); - } - } - } -} diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php deleted file mode 100644 index eff031b58b..0000000000 --- a/includes/filebackend/lockmanager/LockManager.php +++ /dev/null @@ -1,258 +0,0 @@ - self::LOCK_SH, - self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH - self::LOCK_EX => self::LOCK_EX - ]; - - /** @var array Map of (resource path => lock type => count) */ - protected $locksHeld = []; - - protected $domain; // string; domain (usually wiki ID) - protected $lockTTL; // integer; maximum time locks can be held - - /** Lock types; stronger locks have higher values */ - const LOCK_SH = 1; // shared lock (for reads) - const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) - const LOCK_EX = 3; // exclusive lock (for writes) - - /** - * Construct a new instance from configuration - * - * @param array $config Parameters include: - * - domain : Domain (usually wiki ID) that all resources are relative to [optional] - * - lockTTL : Age (in seconds) at which resource locks should expire. - * This only applies if locks are not tied to a connection/process. - */ - public function __construct( array $config ) { - $this->domain = isset( $config['domain'] ) ? $config['domain'] : wfWikiID(); - if ( isset( $config['lockTTL'] ) ) { - $this->lockTTL = max( 5, $config['lockTTL'] ); - } elseif ( PHP_SAPI === 'cli' ) { - $this->lockTTL = 3600; - } else { - $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode - $this->lockTTL = max( 5 * 60, 2 * (int)$met ); - } - } - - /** - * Lock the resources at the given abstract paths - * - * @param array $paths List of resource names - * @param int $type LockManager::LOCK_* constant - * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) - * @return StatusValue - */ - final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { - return $this->lockByType( [ $type => $paths ], $timeout ); - } - - /** - * Lock the resources at the given abstract paths - * - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) - * @return StatusValue - * @since 1.22 - */ - final public function lockByType( array $pathsByType, $timeout = 0 ) { - $pathsByType = $this->normalizePathsByType( $pathsByType ); - - $status = null; - $loop = new WaitConditionLoop( - function () use ( &$status, $pathsByType ) { - $status = $this->doLockByType( $pathsByType ); - - return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE; - }, - $timeout - ); - $loop->invoke(); - - return $status; - } - - /** - * Unlock the resources at the given abstract paths - * - * @param array $paths List of paths - * @param int $type LockManager::LOCK_* constant - * @return StatusValue - */ - final public function unlock( array $paths, $type = self::LOCK_EX ) { - return $this->unlockByType( [ $type => $paths ] ); - } - - /** - * Unlock the resources at the given abstract paths - * - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - * @since 1.22 - */ - final public function unlockByType( array $pathsByType ) { - $pathsByType = $this->normalizePathsByType( $pathsByType ); - $status = $this->doUnlockByType( $pathsByType ); - - return $status; - } - - /** - * Get the base 36 SHA-1 of a string, padded to 31 digits. - * Before hashing, the path will be prefixed with the domain ID. - * This should be used interally for lock key or file names. - * - * @param string $path - * @return string - */ - final protected function sha1Base36Absolute( $path ) { - return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 ); - } - - /** - * Get the base 16 SHA-1 of a string, padded to 31 digits. - * Before hashing, the path will be prefixed with the domain ID. - * This should be used interally for lock key or file names. - * - * @param string $path - * @return string - */ - final protected function sha1Base16Absolute( $path ) { - return sha1( "{$this->domain}:{$path}" ); - } - - /** - * Normalize the $paths array by converting LOCK_UW locks into the - * appropriate type and removing any duplicated paths for each lock type. - * - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return array - * @since 1.22 - */ - final protected function normalizePathsByType( array $pathsByType ) { - $res = []; - foreach ( $pathsByType as $type => $paths ) { - $res[$this->lockTypeMap[$type]] = array_unique( $paths ); - } - - return $res; - } - - /** - * @see LockManager::lockByType() - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - * @since 1.22 - */ - protected function doLockByType( array $pathsByType ) { - $status = Status::newGood(); - $lockedByType = []; // map of (type => paths) - foreach ( $pathsByType as $type => $paths ) { - $status->merge( $this->doLock( $paths, $type ) ); - if ( $status->isOK() ) { - $lockedByType[$type] = $paths; - } else { - // Release the subset of locks that were acquired - foreach ( $lockedByType as $lType => $lPaths ) { - $status->merge( $this->doUnlock( $lPaths, $lType ) ); - } - break; - } - } - - return $status; - } - - /** - * Lock resources with the given keys and lock type - * - * @param array $paths List of paths - * @param int $type LockManager::LOCK_* constant - * @return StatusValue - */ - abstract protected function doLock( array $paths, $type ); - - /** - * @see LockManager::unlockByType() - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - * @since 1.22 - */ - protected function doUnlockByType( array $pathsByType ) { - $status = Status::newGood(); - foreach ( $pathsByType as $type => $paths ) { - $status->merge( $this->doUnlock( $paths, $type ) ); - } - - return $status; - } - - /** - * Unlock resources with the given keys and lock type - * - * @param array $paths List of paths - * @param int $type LockManager::LOCK_* constant - * @return StatusValue - */ - abstract protected function doUnlock( array $paths, $type ); -} - -/** - * Simple version of LockManager that does nothing - * @since 1.19 - */ -class NullLockManager extends LockManager { - protected function doLock( array $paths, $type ) { - return Status::newGood(); - } - - protected function doUnlock( array $paths, $type ) { - return Status::newGood(); - } -} diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php index 2e2d0a3533..81ce424b50 100644 --- a/includes/filebackend/lockmanager/MemcLockManager.php +++ b/includes/filebackend/lockmanager/MemcLockManager.php @@ -90,7 +90,7 @@ class MemcLockManager extends QuorumLockManager { // @todo Change this code to work in one batch protected function getLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $lockedPaths = []; foreach ( $pathsByType as $type => $paths ) { @@ -112,7 +112,7 @@ class MemcLockManager extends QuorumLockManager { // @todo Change this code to work in one batch protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); foreach ( $pathsByType as $type => $paths ) { $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) ); @@ -129,7 +129,7 @@ class MemcLockManager extends QuorumLockManager { * @return StatusValue */ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $memc = $this->getCache( $lockSrv ); $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records @@ -205,7 +205,7 @@ class MemcLockManager extends QuorumLockManager { * @return StatusValue */ protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $memc = $this->getCache( $lockSrv ); $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records @@ -257,7 +257,7 @@ class MemcLockManager extends QuorumLockManager { * @return StatusValue */ protected function releaseAllLocks() { - return Status::newGood(); // not supported + return StatusValue::newGood(); // not supported } /** diff --git a/includes/filebackend/lockmanager/MySqlLockManager.php b/includes/filebackend/lockmanager/MySqlLockManager.php index 896e0ffd64..124d41038a 100644 --- a/includes/filebackend/lockmanager/MySqlLockManager.php +++ b/includes/filebackend/lockmanager/MySqlLockManager.php @@ -38,7 +38,7 @@ class MySqlLockManager extends DBLockManager { * @return StatusValue */ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $db = $this->getConnection( $lockSrv ); // checked in isServerUp() @@ -108,7 +108,7 @@ class MySqlLockManager extends DBLockManager { * @return StatusValue */ protected function releaseAllLocks() { - $status = Status::newGood(); + $status = StatusValue::newGood(); foreach ( $this->conns as $lockDb => $db ) { if ( $db->trxLevel() ) { // in transaction diff --git a/includes/filebackend/lockmanager/PostgreSqlLockManager.php b/includes/filebackend/lockmanager/PostgreSqlLockManager.php index 307c16447e..d6b1ce822d 100644 --- a/includes/filebackend/lockmanager/PostgreSqlLockManager.php +++ b/includes/filebackend/lockmanager/PostgreSqlLockManager.php @@ -14,7 +14,7 @@ class PostgreSqlLockManager extends DBLockManager { ]; protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); if ( !count( $paths ) ) { return $status; // nothing to lock } @@ -64,7 +64,7 @@ class PostgreSqlLockManager extends DBLockManager { * @return StatusValue */ protected function releaseAllLocks() { - $status = Status::newGood(); + $status = StatusValue::newGood(); foreach ( $this->conns as $lockDb => $db ) { try { diff --git a/includes/filebackend/lockmanager/QuorumLockManager.php b/includes/filebackend/lockmanager/QuorumLockManager.php deleted file mode 100644 index 0db9e815fe..0000000000 --- a/includes/filebackend/lockmanager/QuorumLockManager.php +++ /dev/null @@ -1,248 +0,0 @@ - (lsrv1, lsrv2, ...)) - - /** @var array Map of degraded buckets */ - protected $degradedBuckets = []; // (buckey index => UNIX timestamp) - - final protected function doLock( array $paths, $type ) { - return $this->doLockByType( [ $type => $paths ] ); - } - - final protected function doUnlock( array $paths, $type ) { - return $this->doUnlockByType( [ $type => $paths ] ); - } - - protected function doLockByType( array $pathsByType ) { - $status = Status::newGood(); - - $pathsToLock = []; // (bucket => type => paths) - // Get locks that need to be acquired (buckets => locks)... - foreach ( $pathsByType as $type => $paths ) { - foreach ( $paths as $path ) { - if ( isset( $this->locksHeld[$path][$type] ) ) { - ++$this->locksHeld[$path][$type]; - } else { - $bucket = $this->getBucketFromPath( $path ); - $pathsToLock[$bucket][$type][] = $path; - } - } - } - - $lockedPaths = []; // files locked in this attempt (type => paths) - // Attempt to acquire these locks... - foreach ( $pathsToLock as $bucket => $pathsToLockByType ) { - // Try to acquire the locks for this bucket - $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) ); - if ( !$status->isOK() ) { - $status->merge( $this->doUnlockByType( $lockedPaths ) ); - - return $status; - } - // Record these locks as active - foreach ( $pathsToLockByType as $type => $paths ) { - foreach ( $paths as $path ) { - $this->locksHeld[$path][$type] = 1; // locked - // Keep track of what locks were made in this attempt - $lockedPaths[$type][] = $path; - } - } - } - - return $status; - } - - protected function doUnlockByType( array $pathsByType ) { - $status = Status::newGood(); - - $pathsToUnlock = []; // (bucket => type => paths) - foreach ( $pathsByType as $type => $paths ) { - foreach ( $paths as $path ) { - if ( !isset( $this->locksHeld[$path][$type] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } else { - --$this->locksHeld[$path][$type]; - // Reference count the locks held and release locks when zero - if ( $this->locksHeld[$path][$type] <= 0 ) { - unset( $this->locksHeld[$path][$type] ); - $bucket = $this->getBucketFromPath( $path ); - $pathsToUnlock[$bucket][$type][] = $path; - } - if ( !count( $this->locksHeld[$path] ) ) { - unset( $this->locksHeld[$path] ); // no SH or EX locks left for key - } - } - } - } - - // Remove these specific locks if possible, or at least release - // all locks once this process is currently not holding any locks. - foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) { - $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) ); - } - if ( !count( $this->locksHeld ) ) { - $status->merge( $this->releaseAllLocks() ); - $this->degradedBuckets = []; // safe to retry the normal quorum - } - - return $status; - } - - /** - * Attempt to acquire locks with the peers for a bucket. - * This is all or nothing; if any key is locked then this totally fails. - * - * @param int $bucket - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - */ - final protected function doLockingRequestBucket( $bucket, array $pathsByType ) { - $status = Status::newGood(); - - $yesVotes = 0; // locks made on trustable servers - $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers - $quorum = floor( $votesLeft / 2 + 1 ); // simple majority - // Get votes for each peer, in order, until we have enough... - foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { - if ( !$this->isServerUp( $lockSrv ) ) { - --$votesLeft; - $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv ); - $this->degradedBuckets[$bucket] = time(); - continue; // server down? - } - // Attempt to acquire the lock on this peer - $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) ); - if ( !$status->isOK() ) { - return $status; // vetoed; resource locked - } - ++$yesVotes; // success for this peer - if ( $yesVotes >= $quorum ) { - return $status; // lock obtained - } - --$votesLeft; - $votesNeeded = $quorum - $yesVotes; - if ( $votesNeeded > $votesLeft ) { - break; // short-circuit - } - } - // At this point, we must not have met the quorum - $status->setResult( false ); - - return $status; - } - - /** - * Attempt to release locks with the peers for a bucket - * - * @param int $bucket - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - */ - final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) { - $status = Status::newGood(); - - $yesVotes = 0; // locks freed on trustable servers - $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers - $quorum = floor( $votesLeft / 2 + 1 ); // simple majority - $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum? - foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { - if ( !$this->isServerUp( $lockSrv ) ) { - $status->warning( 'lockmanager-fail-svr-release', $lockSrv ); - } else { - // Attempt to release the lock on this peer - $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) ); - ++$yesVotes; // success for this peer - // Normally the first peers form the quorum, and the others are ignored. - // Ignore them in this case, but not when an alternative quorum was used. - if ( $yesVotes >= $quorum && !$isDegraded ) { - break; // lock released - } - } - } - // Set a bad StatusValue if the quorum was not met. - // Assumes the same "up" servers as during the acquire step. - $status->setResult( $yesVotes >= $quorum ); - - return $status; - } - - /** - * Get the bucket for resource path. - * This should avoid throwing any exceptions. - * - * @param string $path - * @return int - */ - protected function getBucketFromPath( $path ) { - $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) - return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket ); - } - - /** - * Check if a lock server is up. - * This should process cache results to reduce RTT. - * - * @param string $lockSrv - * @return bool - */ - abstract protected function isServerUp( $lockSrv ); - - /** - * Get a connection to a lock server and acquire locks - * - * @param string $lockSrv - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - */ - abstract protected function getLocksOnServer( $lockSrv, array $pathsByType ); - - /** - * Get a connection to a lock server and release locks on $paths. - * - * Subclasses must effectively implement this or releaseAllLocks(). - * - * @param string $lockSrv - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - */ - abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType ); - - /** - * Release all locks that this session is holding. - * - * Subclasses must effectively implement this or freeLocksOnServer(). - * - * @return StatusValue - */ - abstract protected function releaseAllLocks(); -} diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php index 4121ecb29d..6fd819d637 100644 --- a/includes/filebackend/lockmanager/RedisLockManager.php +++ b/includes/filebackend/lockmanager/RedisLockManager.php @@ -79,7 +79,7 @@ class RedisLockManager extends QuorumLockManager { } protected function getLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) ); @@ -172,7 +172,7 @@ LUA; } protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) ); @@ -242,7 +242,7 @@ LUA; } protected function releaseAllLocks() { - return Status::newGood(); // not supported + return StatusValue::newGood(); // not supported } protected function isServerUp( $lockSrv ) { diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index b8b1cf6a17..8fee3bfa74 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -825,7 +825,7 @@ class FileRepo { $status = $this->storeBatch( [ [ $srcPath, $dstZone, $dstRel ] ], $flags ); if ( $status->successCount == 0 ) { - $status->ok = false; + $status->setOK( false ); } return $status; @@ -1166,7 +1166,7 @@ class FileRepo { $status = $this->publishBatch( [ [ $src, $dstRel, $archiveRel, $options ] ], $flags ); if ( $status->successCount == 0 ) { - $status->ok = false; + $status->setOK( false ); } if ( isset( $status->value[0] ) ) { $status->value = $status->value[0]; diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index f8b1ed9f79..55df1af03b 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -42,6 +42,9 @@ class ForeignDBViaLBRepo extends LocalRepo { /** @var array */ protected $fileFromRowFactory = [ 'ForeignDBFile', 'newFromRow' ]; + /** @var bool */ + protected $hasSharedCache; + /** * @param array|null $info */ diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index d515b05088..bd32de04e1 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -135,17 +135,18 @@ class RepoGroup { } # Check the cache + $dbkey = $title->getDBkey(); if ( empty( $options['ignoreRedirect'] ) && empty( $options['private'] ) && empty( $options['bypassCache'] ) ) { $time = isset( $options['time'] ) ? $options['time'] : ''; - $dbkey = $title->getDBkey(); if ( $this->cache->has( $dbkey, $time, 60 ) ) { return $this->cache->get( $dbkey, $time ); } $useCache = true; } else { + $time = false; $useCache = false; } diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php index d1e683ac70..921e129c36 100644 --- a/includes/filerepo/file/ArchivedFile.php +++ b/includes/filerepo/file/ArchivedFile.php @@ -425,6 +425,7 @@ class ArchivedFile { */ function pageCount() { if ( !isset( $this->pageCount ) ) { + // @FIXME: callers expect File objects if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) { $this->pageCount = $this->handler->pageCount( $this ); } else { diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php index f6752d8308..43b6855f82 100644 --- a/includes/filerepo/file/ForeignAPIFile.php +++ b/includes/filerepo/file/ForeignAPIFile.php @@ -28,7 +28,10 @@ * @ingroup FileAbstraction */ class ForeignAPIFile extends File { + /** @var bool */ private $mExists; + /** @var array */ + private $mInfo = []; protected $repoClass = 'ForeignApiRepo'; @@ -244,7 +247,7 @@ class ForeignAPIFile extends File { public function getUser( $type = 'text' ) { if ( $type == 'text' ) { return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null; - } elseif ( $type == 'id' ) { + } else { return 0; // What makes sense here, for a remote user? } } @@ -344,9 +347,6 @@ class ForeignAPIFile extends File { return $files; } - /** - * @see File::purgeCache() - */ function purgeCache( $options = [] ) { $this->purgeThumbnails( $options ); $this->purgeDescriptionPage(); diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 618272c0c9..396b47cc3c 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -1480,8 +1480,10 @@ class LocalFile extends File { ); if ( isset( $status->value['revision'] ) ) { + /** @var $rev Revision */ + $rev = $status->value['revision']; // Associate new page revision id - $logEntry->setAssociatedRevId( $status->value['revision']->getId() ); + $logEntry->setAssociatedRevId( $rev->getId() ); } // This relies on the resetArticleID() call in WikiPage::insertOn(), // which is triggered on $descTitle by doEditContent() above. @@ -2692,7 +2694,7 @@ class LocalFileRestoreBatch { // Even if some files could be copied, fail entirely as that is the // easiest thing to do without data loss $this->cleanupFailedBatch( $storeStatus, $storeBatch ); - $status->ok = false; + $status->setOK( false ); $this->file->unlock(); return $status; @@ -2952,7 +2954,7 @@ class LocalFileMoveBatch { if ( !$statusDb->isGood() ) { $destFile->unlock(); $this->file->unlock(); - $statusDb->ok = false; + $statusDb->setOK( false ); return $statusDb; } @@ -2971,7 +2973,7 @@ class LocalFileMoveBatch { $this->file->unlock(); wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText( false, false, 'en' ) ); - $statusMove->ok = false; + $statusMove->setOK( false ); return $statusMove; } diff --git a/includes/installer/DatabaseInstaller.php b/includes/installer/DatabaseInstaller.php index ded2bd8aba..4f10367e6a 100644 --- a/includes/installer/DatabaseInstaller.php +++ b/includes/installer/DatabaseInstaller.php @@ -334,8 +334,7 @@ abstract class DatabaseInstaller { $connection = $status->value; $services->redefineService( 'DBLoadBalancerFactory', function() use ( $connection ) { - return new LBFactorySingle( [ - 'connection' => $connection ] ); + return LBFactorySingle::newFromConnection( $connection ); } ); } diff --git a/includes/installer/SqliteInstaller.php b/includes/installer/SqliteInstaller.php index d59c16294d..6024331494 100644 --- a/includes/installer/SqliteInstaller.php +++ b/includes/installer/SqliteInstaller.php @@ -179,16 +179,12 @@ class SqliteInstaller extends DatabaseInstaller { * @return Status */ public function openConnection() { - global $wgSQLiteDataDir; - $status = Status::newGood(); $dir = $this->getVar( 'wgSQLiteDataDir' ); $dbName = $this->getVar( 'wgDBname' ); try { # @todo FIXME: Need more sensible constructor parameters, e.g. single associative array - # Setting globals kind of sucks - $wgSQLiteDataDir = $dir; - $db = DatabaseBase::factory( 'sqlite', [ 'dbname' => $dbName ] ); + $db = DatabaseBase::factory( 'sqlite', [ 'dbname' => $dbName, 'dbDirectory' => $dir ] ); $status->value = $db; } catch ( DBConnectionError $e ) { $status->fatal( 'config-sqlite-connection-error', $e->getMessage() ); @@ -243,10 +239,7 @@ class SqliteInstaller extends DatabaseInstaller { # Create the global cache DB try { - global $wgSQLiteDataDir; - # @todo FIXME: setting globals kind of sucks - $wgSQLiteDataDir = $dir; - $conn = DatabaseBase::factory( 'sqlite', [ 'dbname' => "wikicache" ] ); + $conn = DatabaseBase::factory( 'sqlite', [ 'dbname' => 'wikicache', 'dbDirectory' => $dir ] ); # @todo: don't duplicate objectcache definition, though it's very simple $sql = << self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ]; + + /** @var string Global dir for all servers */ + protected $lockDir; + + /** @var array Map of (locked key => lock file handle) */ + protected $handles = []; + + /** @var bool */ + protected $isWindows; + + /** + * Construct a new instance from configuration. + * + * @param array $config Includes: + * - lockDirectory : Directory containing the lock files + */ + function __construct( array $config ) { + parent::__construct( $config ); + + $this->lockDir = $config['lockDirectory']; + $this->isWindows = ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' ); + } + + /** + * @see LockManager::doLock() + * @param array $paths + * @param int $type + * @return StatusValue + */ + protected function doLock( array $paths, $type ) { + $status = StatusValue::newGood(); + + $lockedPaths = []; // files locked in this attempt + foreach ( $paths as $path ) { + $status->merge( $this->doSingleLock( $path, $type ) ); + if ( $status->isOK() ) { + $lockedPaths[] = $path; + } else { + // Abort and unlock everything + $status->merge( $this->doUnlock( $lockedPaths, $type ) ); + + return $status; + } + } + + return $status; + } + + /** + * @see LockManager::doUnlock() + * @param array $paths + * @param int $type + * @return StatusValue + */ + protected function doUnlock( array $paths, $type ) { + $status = StatusValue::newGood(); + + foreach ( $paths as $path ) { + $status->merge( $this->doSingleUnlock( $path, $type ) ); + } + + return $status; + } + + /** + * Lock a single resource key + * + * @param string $path + * @param int $type + * @return StatusValue + */ + protected function doSingleLock( $path, $type ) { + $status = StatusValue::newGood(); + + if ( isset( $this->locksHeld[$path][$type] ) ) { + ++$this->locksHeld[$path][$type]; + } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { + $this->locksHeld[$path][$type] = 1; + } else { + if ( isset( $this->handles[$path] ) ) { + $handle = $this->handles[$path]; + } else { + MediaWiki\suppressWarnings(); + $handle = fopen( $this->getLockPath( $path ), 'a+' ); + if ( !$handle ) { // lock dir missing? + mkdir( $this->lockDir, 0777, true ); + $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again + } + MediaWiki\restoreWarnings(); + } + if ( $handle ) { + // Either a shared or exclusive lock + $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX; + if ( flock( $handle, $lock | LOCK_NB ) ) { + // Record this lock as active + $this->locksHeld[$path][$type] = 1; + $this->handles[$path] = $handle; + } else { + fclose( $handle ); + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } else { + $status->fatal( 'lockmanager-fail-openlock', $path ); + } + } + + return $status; + } + + /** + * Unlock a single resource key + * + * @param string $path + * @param int $type + * @return StatusValue + */ + protected function doSingleUnlock( $path, $type ) { + $status = StatusValue::newGood(); + + if ( !isset( $this->locksHeld[$path] ) ) { + $status->warning( 'lockmanager-notlocked', $path ); + } elseif ( !isset( $this->locksHeld[$path][$type] ) ) { + $status->warning( 'lockmanager-notlocked', $path ); + } else { + $handlesToClose = []; + --$this->locksHeld[$path][$type]; + if ( $this->locksHeld[$path][$type] <= 0 ) { + unset( $this->locksHeld[$path][$type] ); + } + if ( !count( $this->locksHeld[$path] ) ) { + unset( $this->locksHeld[$path] ); // no locks on this path + if ( isset( $this->handles[$path] ) ) { + $handlesToClose[] = $this->handles[$path]; + unset( $this->handles[$path] ); + } + } + // Unlock handles to release locks and delete + // any lock files that end up with no locks on them... + if ( $this->isWindows ) { + // Windows: for any process, including this one, + // calling unlink() on a locked file will fail + $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); + $status->merge( $this->pruneKeyLockFiles( $path ) ); + } else { + // Unix: unlink() can be used on files currently open by this + // process and we must do so in order to avoid race conditions + $status->merge( $this->pruneKeyLockFiles( $path ) ); + $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); + } + } + + return $status; + } + + /** + * @param string $path + * @param array $handlesToClose + * @return StatusValue + */ + private function closeLockHandles( $path, array $handlesToClose ) { + $status = StatusValue::newGood(); + foreach ( $handlesToClose as $handle ) { + if ( !flock( $handle, LOCK_UN ) ) { + $status->fatal( 'lockmanager-fail-releaselock', $path ); + } + if ( !fclose( $handle ) ) { + $status->warning( 'lockmanager-fail-closelock', $path ); + } + } + + return $status; + } + + /** + * @param string $path + * @return StatusValue + */ + private function pruneKeyLockFiles( $path ) { + $status = StatusValue::newGood(); + if ( !isset( $this->locksHeld[$path] ) ) { + # No locks are held for the lock file anymore + if ( !unlink( $this->getLockPath( $path ) ) ) { + $status->warning( 'lockmanager-fail-deletelock', $path ); + } + unset( $this->handles[$path] ); + } + + return $status; + } + + /** + * Get the path to the lock file for a key + * @param string $path + * @return string + */ + protected function getLockPath( $path ) { + return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock"; + } + + /** + * Make sure remaining locks get cleared for sanity + */ + function __destruct() { + while ( count( $this->locksHeld ) ) { + foreach ( $this->locksHeld as $path => $locks ) { + $this->doSingleUnlock( $path, self::LOCK_EX ); + $this->doSingleUnlock( $path, self::LOCK_SH ); + } + } + } +} diff --git a/includes/libs/lockmanager/LockManager.php b/includes/libs/lockmanager/LockManager.php new file mode 100644 index 0000000000..80add5b8b7 --- /dev/null +++ b/includes/libs/lockmanager/LockManager.php @@ -0,0 +1,244 @@ + self::LOCK_SH, + self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH + self::LOCK_EX => self::LOCK_EX + ]; + + /** @var array Map of (resource path => lock type => count) */ + protected $locksHeld = []; + + protected $domain; // string; domain (usually wiki ID) + protected $lockTTL; // integer; maximum time locks can be held + + /** Lock types; stronger locks have higher values */ + const LOCK_SH = 1; // shared lock (for reads) + const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) + const LOCK_EX = 3; // exclusive lock (for writes) + + /** + * Construct a new instance from configuration + * + * @param array $config Parameters include: + * - domain : Domain (usually wiki ID) that all resources are relative to [optional] + * - lockTTL : Age (in seconds) at which resource locks should expire. + * This only applies if locks are not tied to a connection/process. + */ + public function __construct( array $config ) { + $this->domain = isset( $config['domain'] ) ? $config['domain'] : 'global'; + if ( isset( $config['lockTTL'] ) ) { + $this->lockTTL = max( 5, $config['lockTTL'] ); + } elseif ( PHP_SAPI === 'cli' ) { + $this->lockTTL = 3600; + } else { + $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode + $this->lockTTL = max( 5 * 60, 2 * (int)$met ); + } + } + + /** + * Lock the resources at the given abstract paths + * + * @param array $paths List of resource names + * @param int $type LockManager::LOCK_* constant + * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) + * @return StatusValue + */ + final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { + return $this->lockByType( [ $type => $paths ], $timeout ); + } + + /** + * Lock the resources at the given abstract paths + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) + * @return StatusValue + * @since 1.22 + */ + final public function lockByType( array $pathsByType, $timeout = 0 ) { + $pathsByType = $this->normalizePathsByType( $pathsByType ); + + $status = null; + $loop = new WaitConditionLoop( + function () use ( &$status, $pathsByType ) { + $status = $this->doLockByType( $pathsByType ); + + return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE; + }, + $timeout + ); + $loop->invoke(); + + return $status; + } + + /** + * Unlock the resources at the given abstract paths + * + * @param array $paths List of paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + final public function unlock( array $paths, $type = self::LOCK_EX ) { + return $this->unlockByType( [ $type => $paths ] ); + } + + /** + * Unlock the resources at the given abstract paths + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + final public function unlockByType( array $pathsByType ) { + $pathsByType = $this->normalizePathsByType( $pathsByType ); + $status = $this->doUnlockByType( $pathsByType ); + + return $status; + } + + /** + * Get the base 36 SHA-1 of a string, padded to 31 digits. + * Before hashing, the path will be prefixed with the domain ID. + * This should be used interally for lock key or file names. + * + * @param string $path + * @return string + */ + final protected function sha1Base36Absolute( $path ) { + return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 ); + } + + /** + * Get the base 16 SHA-1 of a string, padded to 31 digits. + * Before hashing, the path will be prefixed with the domain ID. + * This should be used interally for lock key or file names. + * + * @param string $path + * @return string + */ + final protected function sha1Base16Absolute( $path ) { + return sha1( "{$this->domain}:{$path}" ); + } + + /** + * Normalize the $paths array by converting LOCK_UW locks into the + * appropriate type and removing any duplicated paths for each lock type. + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return array + * @since 1.22 + */ + final protected function normalizePathsByType( array $pathsByType ) { + $res = []; + foreach ( $pathsByType as $type => $paths ) { + $res[$this->lockTypeMap[$type]] = array_unique( $paths ); + } + + return $res; + } + + /** + * @see LockManager::lockByType() + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + protected function doLockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + $lockedByType = []; // map of (type => paths) + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doLock( $paths, $type ) ); + if ( $status->isOK() ) { + $lockedByType[$type] = $paths; + } else { + // Release the subset of locks that were acquired + foreach ( $lockedByType as $lType => $lPaths ) { + $status->merge( $this->doUnlock( $lPaths, $lType ) ); + } + break; + } + } + + return $status; + } + + /** + * Lock resources with the given keys and lock type + * + * @param array $paths List of paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + abstract protected function doLock( array $paths, $type ); + + /** + * @see LockManager::unlockByType() + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + protected function doUnlockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doUnlock( $paths, $type ) ); + } + + return $status; + } + + /** + * Unlock resources with the given keys and lock type + * + * @param array $paths List of paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + abstract protected function doUnlock( array $paths, $type ); +} diff --git a/includes/libs/lockmanager/NullLockManager.php b/includes/libs/lockmanager/NullLockManager.php new file mode 100644 index 0000000000..5ad558fa74 --- /dev/null +++ b/includes/libs/lockmanager/NullLockManager.php @@ -0,0 +1,37 @@ + (lsrv1, lsrv2, ...)) + + /** @var array Map of degraded buckets */ + protected $degradedBuckets = []; // (buckey index => UNIX timestamp) + + final protected function doLock( array $paths, $type ) { + return $this->doLockByType( [ $type => $paths ] ); + } + + final protected function doUnlock( array $paths, $type ) { + return $this->doUnlockByType( [ $type => $paths ] ); + } + + protected function doLockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + + $pathsToLock = []; // (bucket => type => paths) + // Get locks that need to be acquired (buckets => locks)... + foreach ( $pathsByType as $type => $paths ) { + foreach ( $paths as $path ) { + if ( isset( $this->locksHeld[$path][$type] ) ) { + ++$this->locksHeld[$path][$type]; + } else { + $bucket = $this->getBucketFromPath( $path ); + $pathsToLock[$bucket][$type][] = $path; + } + } + } + + $lockedPaths = []; // files locked in this attempt (type => paths) + // Attempt to acquire these locks... + foreach ( $pathsToLock as $bucket => $pathsToLockByType ) { + // Try to acquire the locks for this bucket + $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) ); + if ( !$status->isOK() ) { + $status->merge( $this->doUnlockByType( $lockedPaths ) ); + + return $status; + } + // Record these locks as active + foreach ( $pathsToLockByType as $type => $paths ) { + foreach ( $paths as $path ) { + $this->locksHeld[$path][$type] = 1; // locked + // Keep track of what locks were made in this attempt + $lockedPaths[$type][] = $path; + } + } + } + + return $status; + } + + protected function doUnlockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + + $pathsToUnlock = []; // (bucket => type => paths) + foreach ( $pathsByType as $type => $paths ) { + foreach ( $paths as $path ) { + if ( !isset( $this->locksHeld[$path][$type] ) ) { + $status->warning( 'lockmanager-notlocked', $path ); + } else { + --$this->locksHeld[$path][$type]; + // Reference count the locks held and release locks when zero + if ( $this->locksHeld[$path][$type] <= 0 ) { + unset( $this->locksHeld[$path][$type] ); + $bucket = $this->getBucketFromPath( $path ); + $pathsToUnlock[$bucket][$type][] = $path; + } + if ( !count( $this->locksHeld[$path] ) ) { + unset( $this->locksHeld[$path] ); // no SH or EX locks left for key + } + } + } + } + + // Remove these specific locks if possible, or at least release + // all locks once this process is currently not holding any locks. + foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) { + $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) ); + } + if ( !count( $this->locksHeld ) ) { + $status->merge( $this->releaseAllLocks() ); + $this->degradedBuckets = []; // safe to retry the normal quorum + } + + return $status; + } + + /** + * Attempt to acquire locks with the peers for a bucket. + * This is all or nothing; if any key is locked then this totally fails. + * + * @param int $bucket + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + final protected function doLockingRequestBucket( $bucket, array $pathsByType ) { + $status = StatusValue::newGood(); + + $yesVotes = 0; // locks made on trustable servers + $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers + $quorum = floor( $votesLeft / 2 + 1 ); // simple majority + // Get votes for each peer, in order, until we have enough... + foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { + if ( !$this->isServerUp( $lockSrv ) ) { + --$votesLeft; + $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv ); + $this->degradedBuckets[$bucket] = time(); + continue; // server down? + } + // Attempt to acquire the lock on this peer + $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) ); + if ( !$status->isOK() ) { + return $status; // vetoed; resource locked + } + ++$yesVotes; // success for this peer + if ( $yesVotes >= $quorum ) { + return $status; // lock obtained + } + --$votesLeft; + $votesNeeded = $quorum - $yesVotes; + if ( $votesNeeded > $votesLeft ) { + break; // short-circuit + } + } + // At this point, we must not have met the quorum + $status->setResult( false ); + + return $status; + } + + /** + * Attempt to release locks with the peers for a bucket + * + * @param int $bucket + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) { + $status = StatusValue::newGood(); + + $yesVotes = 0; // locks freed on trustable servers + $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers + $quorum = floor( $votesLeft / 2 + 1 ); // simple majority + $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum? + foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { + if ( !$this->isServerUp( $lockSrv ) ) { + $status->warning( 'lockmanager-fail-svr-release', $lockSrv ); + } else { + // Attempt to release the lock on this peer + $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) ); + ++$yesVotes; // success for this peer + // Normally the first peers form the quorum, and the others are ignored. + // Ignore them in this case, but not when an alternative quorum was used. + if ( $yesVotes >= $quorum && !$isDegraded ) { + break; // lock released + } + } + } + // Set a bad StatusValue if the quorum was not met. + // Assumes the same "up" servers as during the acquire step. + $status->setResult( $yesVotes >= $quorum ); + + return $status; + } + + /** + * Get the bucket for resource path. + * This should avoid throwing any exceptions. + * + * @param string $path + * @return int + */ + protected function getBucketFromPath( $path ) { + $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) + return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket ); + } + + /** + * Check if a lock server is up. + * This should process cache results to reduce RTT. + * + * @param string $lockSrv + * @return bool + */ + abstract protected function isServerUp( $lockSrv ); + + /** + * Get a connection to a lock server and acquire locks + * + * @param string $lockSrv + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + abstract protected function getLocksOnServer( $lockSrv, array $pathsByType ); + + /** + * Get a connection to a lock server and release locks on $paths. + * + * Subclasses must effectively implement this or releaseAllLocks(). + * + * @param string $lockSrv + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType ); + + /** + * Release all locks that this session is holding. + * + * Subclasses must effectively implement this or freeLocksOnServer(). + * + * @return StatusValue + */ + abstract protected function releaseAllLocks(); +} diff --git a/includes/libs/rdbms/database/DBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php index 0d9b692816..2375678df8 100644 --- a/includes/libs/rdbms/database/DBConnRef.php +++ b/includes/libs/rdbms/database/DBConnRef.php @@ -308,7 +308,7 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } - public function makeList( $a, $mode = LIST_COMMA ) { + public function makeList( $a, $mode = self::LIST_COMMA ) { return $this->__call( __FUNCTION__, func_get_args() ); } diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index de0de6e59f..f9e9296b62 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -903,7 +903,11 @@ abstract class Database implements IDatabase, LoggerAwareInterface { $this->trxProfiler->recordQueryCompletion( $queryProf, $startTime, $isWrite, $this->affectedRows() ); - MWDebug::query( $sql, $fname, $isMaster, $queryRuntime ); + $this->queryLogger->debug( $sql, [ + 'method' => $fname, + 'master' => $isMaster, + 'runtime' => $queryRuntime, + ] ); return $ret; } @@ -1159,7 +1163,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface { } if ( isset( $options['HAVING'] ) ) { $having = is_array( $options['HAVING'] ) - ? $this->makeList( $options['HAVING'], LIST_AND ) + ? $this->makeList( $options['HAVING'], self::LIST_AND ) : $options['HAVING']; $sql .= ' HAVING ' . $having; } @@ -1229,7 +1233,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface { if ( !empty( $conds ) ) { if ( is_array( $conds ) ) { - $conds = $this->makeList( $conds, LIST_AND ); + $conds = $this->makeList( $conds, self::LIST_AND ); } $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail"; } else { @@ -1463,16 +1467,16 @@ abstract class Database implements IDatabase, LoggerAwareInterface { function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) { $table = $this->tableName( $table ); $opts = $this->makeUpdateOptions( $options ); - $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET ); + $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET ); if ( $conds !== [] && $conds !== '*' ) { - $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); + $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND ); } return $this->query( $sql, $fname ); } - public function makeList( $a, $mode = LIST_COMMA ) { + public function makeList( $a, $mode = self::LIST_COMMA ) { if ( !is_array( $a ) ) { throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' ); } @@ -1482,9 +1486,9 @@ abstract class Database implements IDatabase, LoggerAwareInterface { foreach ( $a as $field => $value ) { if ( !$first ) { - if ( $mode == LIST_AND ) { + if ( $mode == self::LIST_AND ) { $list .= ' AND '; - } elseif ( $mode == LIST_OR ) { + } elseif ( $mode == self::LIST_OR ) { $list .= ' OR '; } else { $list .= ','; @@ -1493,11 +1497,13 @@ abstract class Database implements IDatabase, LoggerAwareInterface { $first = false; } - if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) { + if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) { $list .= "($value)"; - } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) { + } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) { $list .= "$value"; - } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { + } elseif ( + ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value ) + ) { // Remove null from array to be handled separately if found $includeNull = false; foreach ( array_keys( $value, null, true ) as $nullKey ) { @@ -1505,7 +1511,8 @@ abstract class Database implements IDatabase, LoggerAwareInterface { unset( $value[$nullKey] ); } if ( count( $value ) == 0 && !$includeNull ) { - throw new InvalidArgumentException( __METHOD__ . ": empty input for field $field" ); + throw new InvalidArgumentException( + __METHOD__ . ": empty input for field $field" ); } elseif ( count( $value ) == 0 ) { // only check if $field is null $list .= "$field IS NULL"; @@ -1530,17 +1537,19 @@ abstract class Database implements IDatabase, LoggerAwareInterface { } } } elseif ( $value === null ) { - if ( $mode == LIST_AND || $mode == LIST_OR ) { + if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) { $list .= "$field IS "; - } elseif ( $mode == LIST_SET ) { + } elseif ( $mode == self::LIST_SET ) { $list .= "$field = "; } $list .= 'NULL'; } else { - if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { + if ( + $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET + ) { $list .= "$field = "; } - $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); + $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value ); } } @@ -1554,12 +1563,12 @@ abstract class Database implements IDatabase, LoggerAwareInterface { if ( count( $sub ) ) { $conds[] = $this->makeList( [ $baseKey => $base, $subKey => array_keys( $sub ) ], - LIST_AND ); + self::LIST_AND ); } } if ( $conds ) { - return $this->makeList( $conds, LIST_OR ); + return $this->makeList( $conds, self::LIST_OR ); } else { // Nothing to search for... return false; @@ -1880,7 +1889,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface { $tableClause .= ' ' . $ignore; } } - $on = $this->makeList( (array)$conds, LIST_AND ); + $on = $this->makeList( (array)$conds, self::LIST_AND ); if ( $on != '' ) { $tableClause .= ' ON (' . $on . ')'; } @@ -2149,10 +2158,10 @@ abstract class Database implements IDatabase, LoggerAwareInterface { foreach ( $index as $column ) { $rowKey[$column] = $row[$column]; } - $clauses[] = $this->makeList( $rowKey, LIST_AND ); + $clauses[] = $this->makeList( $rowKey, self::LIST_AND ); } } - $where = [ $this->makeList( $clauses, LIST_OR ) ]; + $where = [ $this->makeList( $clauses, self::LIST_OR ) ]; } else { $where = false; } @@ -2194,7 +2203,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface { $joinTable = $this->tableName( $joinTable ); $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; if ( $conds != '*' ) { - $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); + $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND ); } $sql .= ')'; @@ -2235,7 +2244,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface { if ( $conds != '*' ) { if ( is_array( $conds ) ) { - $conds = $this->makeList( $conds, LIST_AND ); + $conds = $this->makeList( $conds, self::LIST_AND ); } $sql .= ' WHERE ' . $conds; } @@ -2313,7 +2322,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface { if ( $conds != '*' ) { if ( is_array( $conds ) ) { - $conds = $this->makeList( $conds, LIST_AND ); + $conds = $this->makeList( $conds, self::LIST_AND ); } $sql .= " WHERE $conds"; } @@ -2364,7 +2373,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface { public function conditional( $cond, $trueVal, $falseVal ) { if ( is_array( $cond ) ) { - $cond = $this->makeList( $cond, LIST_AND ); + $cond = $this->makeList( $cond, self::LIST_AND ); } return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; diff --git a/includes/libs/rdbms/database/DatabaseBase.php b/includes/libs/rdbms/database/DatabaseBase.php index 2c8e67cd60..2c8d239073 100644 --- a/includes/libs/rdbms/database/DatabaseBase.php +++ b/includes/libs/rdbms/database/DatabaseBase.php @@ -75,10 +75,8 @@ abstract class DatabaseBase extends Database { } /** - * Get search engine class. All subclasses of this need to implement this - * if they wish to use searching. - * * @return string + * @deprecated since 1.27; use SearchEngineFactory::getSearchEngineClass() */ public function getSearchEngine() { return 'SearchEngineDummy'; diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index 46c6678730..2d19081f88 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -735,7 +735,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html */ protected function getHeartbeatData( array $conds ) { - $whereSQL = $this->makeList( $conds, LIST_AND ); + $whereSQL = $this->makeList( $conds, self::LIST_AND ); // Use ORDER BY for channel based queries since that field might not be UNIQUE. // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the // percision field is not supported in MySQL <= 5.5. @@ -1057,16 +1057,6 @@ abstract class DatabaseMysqlBase extends DatabaseBase { return true; } - /** - * Get search engine class. All subclasses of this - * need to implement this if they wish to use searching. - * - * @return string - */ - public function getSearchEngine() { - return 'SearchMySQL'; - } - /** * @param bool $value */ @@ -1107,7 +1097,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar "; if ( $conds != '*' ) { - $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); + $sql .= ' AND ' . $this->makeList( $conds, self::LIST_AND ); } return $this->query( $sql, $fname ); @@ -1141,7 +1131,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $rowTuples[] = '(' . $this->makeList( $row ) . ')'; } $sql .= implode( ',', $rowTuples ); - $sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, LIST_SET ); + $sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, self::LIST_SET ); return (bool)$this->query( $sql, $fname ); } diff --git a/includes/libs/rdbms/database/DatabaseSqlite.php b/includes/libs/rdbms/database/DatabaseSqlite.php new file mode 100644 index 0000000000..888198365b --- /dev/null +++ b/includes/libs/rdbms/database/DatabaseSqlite.php @@ -0,0 +1,1056 @@ +openFile( $p['dbFilePath'] ); + $lockDomain = md5( $p['dbFilePath'] ); + } elseif ( !isset( $p['dbDirectory'] ) ) { + throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." ); + } else { + $this->dbDir = $p['dbDirectory']; + $this->mDBname = $p['dbname']; + $lockDomain = $this->mDBname; + // 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() ) { + if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) { + $done = []; + foreach ( $this->tableAliases as $params ) { + if ( isset( $done[$params['dbname']] ) ) { + continue; + } + $this->attachDatabase( $params['dbname'] ); + $done[$params['dbname']] = 1; + } + } + } + } + + $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null; + if ( $this->trxMode && + !in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] ) + ) { + $this->trxMode = null; + $this->queryLogger->warning( "Invalid SQLite transaction mode provided." ); + } + + $this->lockMgr = new FSLockManager( [ + 'domain' => $lockDomain, + 'lockDirectory' => "{$this->dbDir}/locks" + ] ); + } + + /** + * @param string $filename + * @param array $p Options map; supports: + * - flags : (same as __construct counterpart) + * - trxMode : (same as __construct counterpart) + * - dbDirectory : (same as __construct counterpart) + * @return DatabaseSqlite + * @since 1.25 + */ + public static function newStandaloneInstance( $filename, array $p = [] ) { + $p['dbFilePath'] = $filename; + $p['schema'] = false; + $p['tablePrefix'] = ''; + + return DatabaseBase::factory( 'sqlite', $p ); + } + + /** + * @return string + */ + function getType() { + return 'sqlite'; + } + + /** + * @todo Check if it should be true like parent class + * + * @return bool + */ + function implicitGroupby() { + return false; + } + + /** Open an SQLite database and return a resource handle to it + * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases + * + * @param string $server + * @param string $user + * @param string $pass + * @param string $dbName + * + * @throws DBConnectionError + * @return PDO + */ + function open( $server, $user, $pass, $dbName ) { + $this->close(); + $fileName = self::generateFileName( $this->dbDir, $dbName ); + if ( !is_readable( $fileName ) ) { + $this->mConn = false; + throw new DBConnectionError( $this, "SQLite database not accessible" ); + } + $this->openFile( $fileName ); + + return $this->mConn; + } + + /** + * Opens a database file + * + * @param string $fileName + * @throws DBConnectionError + * @return PDO|bool SQL connection or false if failed + */ + protected function openFile( $fileName ) { + $err = false; + + $this->dbPath = $fileName; + try { + if ( $this->mFlags & DBO_PERSISTENT ) { + $this->mConn = new PDO( "sqlite:$fileName", '', '', + [ PDO::ATTR_PERSISTENT => true ] ); + } else { + $this->mConn = new PDO( "sqlite:$fileName", '', '' ); + } + } catch ( PDOException $e ) { + $err = $e->getMessage(); + } + + if ( !$this->mConn ) { + $this->queryLogger->debug( "DB connection error: $err\n" ); + throw new DBConnectionError( $this, $err ); + } + + $this->mOpened = !!$this->mConn; + if ( $this->mOpened ) { + # Set error codes only, don't raise exceptions + $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); + # Enforce LIKE to be case sensitive, just like MySQL + $this->query( 'PRAGMA case_sensitive_like = 1' ); + + return $this->mConn; + } + + return false; + } + + /** + * @return string SQLite DB file path + * @since 1.25 + */ + public function getDbFilePath() { + return $this->dbPath; + } + + /** + * Does not actually close the connection, just destroys the reference for GC to do its work + * @return bool + */ + protected function closeConnection() { + $this->mConn = null; + + return true; + } + + /** + * Generates a database file name. Explicitly public for installer. + * @param string $dir Directory where database resides + * @param string $dbName Database name + * @return string + */ + public static function generateFileName( $dir, $dbName ) { + return "$dir/$dbName.sqlite"; + } + + /** + * Check if the searchindext table is FTS enabled. + * @return bool False if not enabled. + */ + function checkForEnabledSearch() { + if ( self::$fulltextEnabled === null ) { + self::$fulltextEnabled = false; + $table = $this->tableName( 'searchindex' ); + $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ ); + if ( $res ) { + $row = $res->fetchRow(); + self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false; + } + } + + return self::$fulltextEnabled; + } + + /** + * Returns version of currently supported SQLite fulltext search module or false if none present. + * @return string + */ + static function getFulltextSearchModule() { + static $cachedResult = null; + if ( $cachedResult !== null ) { + return $cachedResult; + } + $cachedResult = false; + $table = 'dummy_search_test'; + + $db = self::newStandaloneInstance( ':memory:' ); + if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) { + $cachedResult = 'FTS3'; + } + $db->close(); + + return $cachedResult; + } + + /** + * Attaches external database to our connection, see http://sqlite.org/lang_attach.html + * for details. + * + * @param string $name Database name to be used in queries like + * SELECT foo FROM dbname.table + * @param bool|string $file Database file name. If omitted, will be generated + * using $name and configured data directory + * @param string $fname Calling function name + * @return ResultWrapper + */ + function attachDatabase( $name, $file = false, $fname = __METHOD__ ) { + if ( !$file ) { + $file = self::generateFileName( $this->dbDir, $name ); + } + $file = $this->addQuotes( $file ); + + return $this->query( "ATTACH DATABASE $file AS $name", $fname ); + } + + function isWriteQuery( $sql ) { + return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql ); + } + + /** + * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result + * + * @param string $sql + * @return bool|ResultWrapper + */ + protected function doQuery( $sql ) { + $res = $this->mConn->query( $sql ); + if ( $res === false ) { + return false; + } + + $r = $res instanceof ResultWrapper ? $res->result : $res; + $this->mAffectedRows = $r->rowCount(); + $res = new ResultWrapper( $this, $r->fetchAll() ); + + return $res; + } + + /** + * @param ResultWrapper|mixed $res + */ + function freeResult( $res ) { + if ( $res instanceof ResultWrapper ) { + $res->result = null; + } else { + $res = null; + } + } + + /** + * @param ResultWrapper|array $res + * @return stdClass|bool + */ + function fetchObject( $res ) { + if ( $res instanceof ResultWrapper ) { + $r =& $res->result; + } else { + $r =& $res; + } + + $cur = current( $r ); + if ( is_array( $cur ) ) { + next( $r ); + $obj = new stdClass; + foreach ( $cur as $k => $v ) { + if ( !is_numeric( $k ) ) { + $obj->$k = $v; + } + } + + return $obj; + } + + return false; + } + + /** + * @param ResultWrapper|mixed $res + * @return array|bool + */ + function fetchRow( $res ) { + if ( $res instanceof ResultWrapper ) { + $r =& $res->result; + } else { + $r =& $res; + } + $cur = current( $r ); + if ( is_array( $cur ) ) { + next( $r ); + + return $cur; + } + + return false; + } + + /** + * The PDO::Statement class implements the array interface so count() will work + * + * @param ResultWrapper|array $res + * @return int + */ + function numRows( $res ) { + $r = $res instanceof ResultWrapper ? $res->result : $res; + + return count( $r ); + } + + /** + * @param ResultWrapper $res + * @return int + */ + function numFields( $res ) { + $r = $res instanceof ResultWrapper ? $res->result : $res; + if ( is_array( $r ) && count( $r ) > 0 ) { + // The size of the result array is twice the number of fields. (Bug: 65578) + return count( $r[0] ) / 2; + } else { + // If the result is empty return 0 + return 0; + } + } + + /** + * @param ResultWrapper $res + * @param int $n + * @return bool + */ + function fieldName( $res, $n ) { + $r = $res instanceof ResultWrapper ? $res->result : $res; + if ( is_array( $r ) ) { + $keys = array_keys( $r[0] ); + + return $keys[$n]; + } + + return false; + } + + /** + * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks + * + * @param string $name + * @param string $format + * @return string + */ + function tableName( $name, $format = 'quoted' ) { + // table names starting with sqlite_ are reserved + if ( strpos( $name, 'sqlite_' ) === 0 ) { + return $name; + } + + return str_replace( '"', '', parent::tableName( $name, $format ) ); + } + + /** + * Index names have DB scope + * + * @param string $index + * @return string + */ + protected function indexName( $index ) { + return $index; + } + + /** + * This must be called after nextSequenceVal + * + * @return int + */ + function insertId() { + // PDO::lastInsertId yields a string :( + return intval( $this->mConn->lastInsertId() ); + } + + /** + * @param ResultWrapper|array $res + * @param int $row + */ + function dataSeek( $res, $row ) { + if ( $res instanceof ResultWrapper ) { + $r =& $res->result; + } else { + $r =& $res; + } + reset( $r ); + if ( $row > 0 ) { + for ( $i = 0; $i < $row; $i++ ) { + next( $r ); + } + } + } + + /** + * @return string + */ + function lastError() { + if ( !is_object( $this->mConn ) ) { + return "Cannot return last error, no db connection"; + } + $e = $this->mConn->errorInfo(); + + return isset( $e[2] ) ? $e[2] : ''; + } + + /** + * @return string + */ + function lastErrno() { + if ( !is_object( $this->mConn ) ) { + return "Cannot return last error, no db connection"; + } else { + $info = $this->mConn->errorInfo(); + + return $info[1]; + } + } + + /** + * @return int + */ + function affectedRows() { + return $this->mAffectedRows; + } + + /** + * Returns information about an index + * Returns false if the index does not exist + * - if errors are explicitly ignored, returns NULL on failure + * + * @param string $table + * @param string $index + * @param string $fname + * @return array + */ + function indexInfo( $table, $index, $fname = __METHOD__ ) { + $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')'; + $res = $this->query( $sql, $fname ); + if ( !$res ) { + return null; + } + if ( $res->numRows() == 0 ) { + return false; + } + $info = []; + foreach ( $res as $row ) { + $info[] = $row->name; + } + + return $info; + } + + /** + * @param string $table + * @param string $index + * @param string $fname + * @return bool|null + */ + function indexUnique( $table, $index, $fname = __METHOD__ ) { + $row = $this->selectRow( 'sqlite_master', '*', + [ + 'type' => 'index', + 'name' => $this->indexName( $index ), + ], $fname ); + if ( !$row || !isset( $row->sql ) ) { + return null; + } + + // $row->sql will be of the form CREATE [UNIQUE] INDEX ... + $indexPos = strpos( $row->sql, 'INDEX' ); + if ( $indexPos === false ) { + return null; + } + $firstPart = substr( $row->sql, 0, $indexPos ); + $options = explode( ' ', $firstPart ); + + return in_array( 'UNIQUE', $options ); + } + + /** + * Filter the options used in SELECT statements + * + * @param array $options + * @return array + */ + function makeSelectOptions( $options ) { + foreach ( $options as $k => $v ) { + if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) { + $options[$k] = ''; + } + } + + return parent::makeSelectOptions( $options ); + } + + /** + * @param array $options + * @return string + */ + protected function makeUpdateOptionsArray( $options ) { + $options = parent::makeUpdateOptionsArray( $options ); + $options = self::fixIgnore( $options ); + + return $options; + } + + /** + * @param array $options + * @return array + */ + static function fixIgnore( $options ) { + # SQLite uses OR IGNORE not just IGNORE + foreach ( $options as $k => $v ) { + if ( $v == 'IGNORE' ) { + $options[$k] = 'OR IGNORE'; + } + } + + return $options; + } + + /** + * @param array $options + * @return string + */ + function makeInsertOptions( $options ) { + $options = self::fixIgnore( $options ); + + return parent::makeInsertOptions( $options ); + } + + /** + * Based on generic method (parent) with some prior SQLite-sepcific adjustments + * @param string $table + * @param array $a + * @param string $fname + * @param array $options + * @return bool + */ + function insert( $table, $a, $fname = __METHOD__, $options = [] ) { + if ( !count( $a ) ) { + return true; + } + + # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts + if ( isset( $a[0] ) && is_array( $a[0] ) ) { + $ret = true; + foreach ( $a as $v ) { + if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) { + $ret = false; + } + } + } else { + $ret = parent::insert( $table, $a, "$fname/single-row", $options ); + } + + return $ret; + } + + /** + * @param string $table + * @param array $uniqueIndexes Unused + * @param string|array $rows + * @param string $fname + * @return bool|ResultWrapper + */ + function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { + if ( !count( $rows ) ) { + return true; + } + + # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries + if ( isset( $rows[0] ) && is_array( $rows[0] ) ) { + $ret = true; + foreach ( $rows as $v ) { + if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) { + $ret = false; + } + } + } else { + $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" ); + } + + return $ret; + } + + /** + * Returns the size of a text field, or -1 for "unlimited" + * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though. + * + * @param string $table + * @param string $field + * @return int + */ + function textFieldSize( $table, $field ) { + return -1; + } + + /** + * @return bool + */ + function unionSupportsOrderAndLimit() { + return false; + } + + /** + * @param string $sqls + * @param bool $all Whether to "UNION ALL" or not + * @return string + */ + function unionQueries( $sqls, $all ) { + $glue = $all ? ' UNION ALL ' : ' UNION '; + + return implode( $glue, $sqls ); + } + + /** + * @return bool + */ + function wasDeadlock() { + return $this->lastErrno() == 5; // SQLITE_BUSY + } + + /** + * @return bool + */ + function wasErrorReissuable() { + return $this->lastErrno() == 17; // SQLITE_SCHEMA; + } + + /** + * @return bool + */ + function wasReadOnlyError() { + return $this->lastErrno() == 8; // SQLITE_READONLY; + } + + /** + * @return string Wikitext of a link to the server software's web site + */ + public function getSoftwareLink() { + return "[{{int:version-db-sqlite-url}} SQLite]"; + } + + /** + * @return string Version information from the database + */ + function getServerVersion() { + $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION ); + + return $ver; + } + + /** + * Get information about a given field + * Returns false if the field does not exist. + * + * @param string $table + * @param string $field + * @return SQLiteField|bool False on failure + */ + function fieldInfo( $table, $field ) { + $tableName = $this->tableName( $table ); + $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')'; + $res = $this->query( $sql, __METHOD__ ); + foreach ( $res as $row ) { + if ( $row->name == $field ) { + return new SQLiteField( $row, $tableName ); + } + } + + return false; + } + + protected function doBegin( $fname = '' ) { + if ( $this->trxMode ) { + $this->query( "BEGIN {$this->trxMode}", $fname ); + } else { + $this->query( 'BEGIN', $fname ); + } + $this->mTrxLevel = 1; + } + + /** + * @param string $s + * @return string + */ + function strencode( $s ) { + return substr( $this->addQuotes( $s ), 1, -1 ); + } + + /** + * @param string $b + * @return Blob + */ + function encodeBlob( $b ) { + return new Blob( $b ); + } + + /** + * @param Blob|string $b + * @return string + */ + function decodeBlob( $b ) { + if ( $b instanceof Blob ) { + $b = $b->fetch(); + } + + return $b; + } + + /** + * @param Blob|string $s + * @return string + */ + function addQuotes( $s ) { + if ( $s instanceof Blob ) { + return "x'" . bin2hex( $s->fetch() ) . "'"; + } elseif ( is_bool( $s ) ) { + return (int)$s; + } elseif ( strpos( $s, "\0" ) !== false ) { + // SQLite doesn't support \0 in strings, so use the hex representation as a workaround. + // This is a known limitation of SQLite's mprintf function which PDO + // should work around, but doesn't. I have reported this to php.net as bug #63419: + // https://bugs.php.net/bug.php?id=63419 + // There was already a similar report for SQLite3::escapeString, bug #62361: + // https://bugs.php.net/bug.php?id=62361 + // There is an additional bug regarding sorting this data after insert + // on older versions of sqlite shipped with ubuntu 12.04 + // https://phabricator.wikimedia.org/T74367 + $this->queryLogger->debug( + __FUNCTION__ . + ': Quoting value containing null byte. ' . + 'For consistency all binary data should have been ' . + 'first processed with self::encodeBlob()' + ); + return "x'" . bin2hex( $s ) . "'"; + } else { + return $this->mConn->quote( $s ); + } + } + + /** + * @return string + */ + function buildLike() { + $params = func_get_args(); + if ( count( $params ) > 0 && is_array( $params[0] ) ) { + $params = $params[0]; + } + + return parent::buildLike( $params ) . "ESCAPE '\' "; + } + + /** + * @param string $field Field or column to cast + * @return string + * @since 1.28 + */ + public function buildStringCast( $field ) { + return 'CAST ( ' . $field . ' AS TEXT )'; + } + + /** + * No-op version of deadlockLoop + * + * @return mixed + */ + public function deadlockLoop( /*...*/ ) { + $args = func_get_args(); + $function = array_shift( $args ); + + return call_user_func_array( $function, $args ); + } + + /** + * @param string $s + * @return string + */ + protected function replaceVars( $s ) { + $s = parent::replaceVars( $s ); + if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) { + // CREATE TABLE hacks to allow schema file sharing with MySQL + + // binary/varbinary column type -> blob + $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s ); + // no such thing as unsigned + $s = preg_replace( '/\b(un)?signed\b/i', '', $s ); + // INT -> INTEGER + $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s ); + // floating point types -> REAL + $s = preg_replace( + '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', + 'REAL', + $s + ); + // varchar -> TEXT + $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s ); + // TEXT normalization + $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s ); + // BLOB normalization + $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s ); + // BOOL -> INTEGER + $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s ); + // DATETIME -> TEXT + $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s ); + // No ENUM type + $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s ); + // binary collation type -> nothing + $s = preg_replace( '/\bbinary\b/i', '', $s ); + // auto_increment -> autoincrement + $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s ); + // No explicit options + $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s ); + // AUTOINCREMENT should immedidately follow PRIMARY KEY + $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s ); + } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) { + // No truncated indexes + $s = preg_replace( '/\(\d+\)/', '', $s ); + // No FULLTEXT + $s = preg_replace( '/\bfulltext\b/i', '', $s ); + } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) { + // DROP INDEX is database-wide, not table-specific, so no ON

clause. + $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s ); + } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) { + // INSERT IGNORE --> INSERT OR IGNORE + $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s ); + } + + return $s; + } + + public function lock( $lockName, $method, $timeout = 5 ) { + if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed + if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) { + throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." ); + } + } + + return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK(); + } + + public function unlock( $lockName, $method ) { + return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isOK(); + } + + /** + * Build a concatenation list to feed into a SQL query + * + * @param string[] $stringList + * @return string + */ + function buildConcat( $stringList ) { + return '(' . implode( ') || (', $stringList ) . ')'; + } + + public function buildGroupConcatField( + $delim, $table, $field, $conds = '', $join_conds = [] + ) { + $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')'; + + return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')'; + } + + /** + * @param string $oldName + * @param string $newName + * @param bool $temporary + * @param string $fname + * @return bool|ResultWrapper + * @throws RuntimeException + */ + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) { + $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . + $this->addQuotes( $oldName ) . " AND type='table'", $fname ); + $obj = $this->fetchObject( $res ); + if ( !$obj ) { + throw new RuntimeException( "Couldn't retrieve structure for table $oldName" ); + } + $sql = $obj->sql; + $sql = preg_replace( + '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/', + $this->addIdentifierQuotes( $newName ), + $sql, + 1 + ); + if ( $temporary ) { + if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) { + $this->queryLogger->debug( + "Table $oldName is virtual, can't create a temporary duplicate.\n" ); + } else { + $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql ); + } + } + + $res = $this->query( $sql, $fname ); + + // Take over indexes + $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' ); + foreach ( $indexList as $index ) { + if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) { + continue; + } + + if ( $index->unique ) { + $sql = 'CREATE UNIQUE INDEX'; + } else { + $sql = 'CREATE INDEX'; + } + // Try to come up with a new index name, given indexes have database scope in SQLite + $indexName = $newName . '_' . $index->name; + $sql .= ' ' . $indexName . ' ON ' . $newName; + + $indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' ); + $fields = []; + foreach ( $indexInfo as $indexInfoRow ) { + $fields[$indexInfoRow->seqno] = $indexInfoRow->name; + } + + $sql .= '(' . implode( ',', $fields ) . ')'; + + $this->query( $sql ); + } + + return $res; + } + + /** + * List all tables on the database + * + * @param string $prefix Only show tables with this prefix, e.g. mw_ + * @param string $fname Calling function name + * + * @return array + */ + function listTables( $prefix = null, $fname = __METHOD__ ) { + $result = $this->select( + 'sqlite_master', + 'name', + "type='table'" + ); + + $endArray = []; + + foreach ( $result as $table ) { + $vars = get_object_vars( $table ); + $table = array_pop( $vars ); + + if ( !$prefix || strpos( $table, $prefix ) === 0 ) { + if ( strpos( $table, 'sqlite_' ) !== 0 ) { + $endArray[] = $table; + } + } + } + + return $endArray; + } + + /** + * Override due to no CASCADE support + * + * @param string $tableName + * @param string $fName + * @return bool|ResultWrapper + * @throws DBReadOnlyError + */ + public function dropTable( $tableName, $fName = __METHOD__ ) { + if ( !$this->tableExists( $tableName, $fName ) ) { + return false; + } + $sql = "DROP TABLE " . $this->tableName( $tableName ); + + return $this->query( $sql, $fName ); + } + + /** + * @return string + */ + public function __toString() { + return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION ); + } + +} diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index 495816fd69..25e5912ac5 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -63,6 +63,17 @@ interface IDatabase { /** @var string Estimate time to apply (scanning, applying) */ const ESTIMATE_DB_APPLY = 'apply'; + /** @var int Combine list with comma delimeters */ + const LIST_COMMA = 0; + /** @var int Combine list with AND clauses */ + const LIST_AND = 1; + /** @var int Convert map into a SET clause */ + const LIST_SET = 2; + /** @var int Treat as field name and do not apply value escaping */ + const LIST_NAMES = 3; + /** @var int Combine list with OR clauses */ + const LIST_OR = 4; + /** * A string describing the current software version, and possibly * other details in a user-friendly way. Will be listed on Special:Version, etc. @@ -897,18 +908,29 @@ interface IDatabase { /** * Makes an encoded list of strings from an array * + * These can be used to make conjunctions or disjunctions on SQL condition strings + * derived from an array (see IDatabase::select() $conds documentation). + * + * Example usage: + * @code + * $sql = $db->makeList( [ + * 'rev_user' => $id, + * $db->makeList( [ 'rev_minor' => 1, 'rev_len' < 500 ], $db::LIST_OR ] ) + * ], $db::LIST_AND ); + * @endcode + * This would set $sql to "rev_user = '$id' AND (rev_minor = '1' OR rev_len < '500')" + * * @param array $a Containing the data - * @param int $mode Constant - * - LIST_COMMA: Comma separated, no field names - * - LIST_AND: ANDed WHERE clause (without the WHERE). See the - * documentation for $conds in IDatabase::select(). - * - LIST_OR: ORed WHERE clause (without the WHERE) - * - LIST_SET: Comma separated with field names, like a SET clause - * - LIST_NAMES: Comma separated field names + * @param int $mode IDatabase class constant: + * - IDatabase::LIST_COMMA: Comma separated, no field names + * - IDatabase::LIST_AND: ANDed WHERE clause (without the WHERE). + * - IDatabase::LIST_OR: ORed WHERE clause (without the WHERE) + * - IDatabase::LIST_SET: Comma separated with field names, like a SET clause + * - IDatabase::LIST_NAMES: Comma separated field names * @throws DBError * @return string */ - public function makeList( $a, $mode = LIST_COMMA ); + public function makeList( $a, $mode = self::LIST_COMMA ); /** * Build a partial where clause from a 2-d array such as used for LinkBatch. diff --git a/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php index 774def8086..1a046cf696 100644 --- a/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php +++ b/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php @@ -4,35 +4,19 @@ * doesn't go anywhere near an actual database. */ class FakeResultWrapper extends ResultWrapper { - /** @var array */ - public $result = []; - - /** @var null And it's going to stay that way :D */ - protected $db = null; - - /** @var int */ - protected $pos = 0; - - /** @var array|stdClass|bool */ - protected $currentRow = null; + /** @var $result stdClass[] */ /** - * @param array $array + * @param stdClass[] $rows */ - function __construct( $array ) { - $this->result = $array; + function __construct( array $rows ) { + parent::__construct( null, $rows ); } - /** - * @return int - */ function numRows() { return count( $this->result ); } - /** - * @return array|bool - */ function fetchRow() { if ( $this->pos < count( $this->result ) ) { $this->currentRow = $this->result[$this->pos]; @@ -54,10 +38,6 @@ class FakeResultWrapper extends ResultWrapper { function free() { } - /** - * Callers want to be able to access fields with $this->fieldName - * @return bool|stdClass - */ function fetchObject() { $this->fetchRow(); if ( $this->currentRow ) { @@ -72,9 +52,6 @@ class FakeResultWrapper extends ResultWrapper { $this->currentRow = null; } - /** - * @return bool|stdClass - */ function next() { return $this->fetchObject(); } diff --git a/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php index cccb8f17d2..768511bf5b 100644 --- a/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php +++ b/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php @@ -1,5 +1,6 @@ db = $database; - + public function __construct( IDatabase $db = null, $result ) { + $this->db = $db; if ( $result instanceof ResultWrapper ) { $this->result = $result->result; } else { @@ -37,8 +50,8 @@ class ResultWrapper implements Iterator { * * @return int */ - function numRows() { - return $this->db->numRows( $this ); + public function numRows() { + return $this->getDB()->numRows( $this ); } /** @@ -49,8 +62,8 @@ class ResultWrapper implements Iterator { * @return stdClass|bool * @throws DBUnexpectedError Thrown if the database returns an error */ - function fetchObject() { - return $this->db->fetchObject( $this ); + public function fetchObject() { + return $this->getDB()->fetchObject( $this ); } /** @@ -60,38 +73,49 @@ class ResultWrapper implements Iterator { * @return array|bool * @throws DBUnexpectedError Thrown if the database returns an error */ - function fetchRow() { - return $this->db->fetchRow( $this ); + public function fetchRow() { + return $this->getDB()->fetchRow( $this ); } /** - * Free a result object + * Change the position of the cursor in a result object. + * See mysql_data_seek() + * + * @param int $row */ - function free() { - $this->db->freeResult( $this ); - unset( $this->result ); - unset( $this->db ); + public function seek( $row ) { + $this->getDB()->dataSeek( $this, $row ); } /** - * Change the position of the cursor in a result object. - * See mysql_data_seek() + * Free a result object * - * @param int $row + * This either saves memory in PHP (buffered queries) or on the server (unbuffered queries). + * In general, queries are not large enough in result sets for this to be worth calling. */ - function seek( $row ) { - $this->db->dataSeek( $this, $row ); + public function free() { + if ( $this->db ) { + $this->db->freeResult( $this ); + $this->db = null; + } + $this->result = null; } - /* - * ======= Iterator functions ======= - * Note that using these in combination with the non-iterator functions - * above may cause rows to be skipped or repeated. + /** + * @return IDatabase + * @throws RuntimeException */ + private function getDB() { + if ( !$this->db ) { + throw new RuntimeException( get_class( $this ) . ' needs a DB handle for iteration.' ); + } + + return $this->db; + } function rewind() { if ( $this->numRows() ) { - $this->db->dataSeek( $this, 0 ); + $this->getDB()->dataSeek( $this, 0 ); } $this->pos = 0; $this->currentRow = null; @@ -125,9 +149,6 @@ class ResultWrapper implements Iterator { return $this->currentRow; } - /** - * @return bool - */ function valid() { return $this->current() !== false; } diff --git a/includes/libs/rdbms/defines.php b/includes/libs/rdbms/defines.php index 48baa3c633..b420ca1078 100644 --- a/includes/libs/rdbms/defines.php +++ b/includes/libs/rdbms/defines.php @@ -22,14 +22,3 @@ define( 'DBO_COMPRESS', 512 ); define( 'DB_REPLICA', -1 ); # Read from a replica (or only server) define( 'DB_MASTER', -2 ); # Write to master (or only server) /**@}*/ - -/**@{ - * Flags for IDatabase::makeList() - * These are also available as Database class constants - */ -define( 'LIST_COMMA', 0 ); -define( 'LIST_AND', 1 ); -define( 'LIST_SET', 2 ); -define( 'LIST_NAMES', 3 ); -define( 'LIST_OR', 4 ); -/**@}*/ diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php b/includes/libs/rdbms/lbfactory/LBFactory.php index 00474fe0bc..40ba458826 100644 --- a/includes/libs/rdbms/lbfactory/LBFactory.php +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@ -141,7 +141,7 @@ abstract class LBFactory { * Create a new load balancer object. The resulting object will be untracked, * not chronology-protected, and the caller is responsible for cleaning it up. * - * @param bool|string $domain Wiki ID, or false for the current wiki + * @param bool|string $domain Domain ID, or false for the current domain * @return ILoadBalancer */ abstract public function newMainLB( $domain = false ); @@ -149,7 +149,7 @@ abstract class LBFactory { /** * Get a cached (tracked) load balancer object. * - * @param bool|string $domain Wiki ID, or false for the current wiki + * @param bool|string $domain Domain ID, or false for the current domain * @return ILoadBalancer */ abstract public function getMainLB( $domain = false ); @@ -160,7 +160,7 @@ abstract class LBFactory { * cleaning it up. * * @param string $cluster External storage cluster, or false for core - * @param bool|string $domain Wiki ID, or false for the current wiki + * @param bool|string $domain Domain ID, or false for the current domain * @return ILoadBalancer */ abstract protected function newExternalLB( $cluster, $domain = false ); @@ -169,7 +169,7 @@ abstract class LBFactory { * Get a cached (tracked) load balancer for external storage * * @param string $cluster External storage cluster, or false for core - * @param bool|string $domain Wiki ID, or false for the current wiki + * @param bool|string $domain Domain ID, or false for the current domain * @return ILoadBalancer */ abstract public function getExternalLB( $cluster, $domain = false ); diff --git a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php new file mode 100644 index 0000000000..0f1493a82b --- /dev/null +++ b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php @@ -0,0 +1,419 @@ + lowest): + * - templateOverridesByServer + * - masterTemplateOverrides + * - templateOverridesBySection/templateOverridesByCluster + * - externalTemplateOverrides + * - serverTemplate + * Overrides only work on top level keys (so nested values will not be merged). + * + * Configuration: + * sectionsByDB A map of database names to section names. + * + * sectionLoads A 2-d map. For each section, gives a map of server names to + * load ratios. For example: + * [ + * 'section1' => [ + * 'db1' => 100, + * 'db2' => 100 + * ] + * ] + * + * serverTemplate A server info associative array as documented for $wgDBservers. + * The host, hostName and load entries will be overridden. + * + * groupLoadsBySection A 3-d map giving server load ratios for each section and group. + * For example: + * [ + * 'section1' => [ + * 'group1' => [ + * 'db1' => 100, + * 'db2' => 100 + * ] + * ] + * ] + * + * groupLoadsByDB A 3-d map giving server load ratios by DB name. + * + * hostsByName A map of hostname to IP address. + * + * externalLoads A map of external storage cluster name to server load map. + * + * externalTemplateOverrides A set of server info keys overriding serverTemplate for external + * storage. + * + * templateOverridesByServer A 2-d map overriding serverTemplate and + * externalTemplateOverrides on a server-by-server basis. Applies + * to both core and external storage. + * templateOverridesBySection A 2-d map overriding the server info by section. + * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster. + * + * masterTemplateOverrides An override array for all master servers. + * + * loadMonitorClass Name of the LoadMonitor class to always use. + * + * readOnlyBySection A map of section name to read-only message. + * Missing or false for read/write. + * + * @ingroup Database + */ +class LBFactoryMulti extends LBFactory { + /** @var array A map of database names to section names */ + private $sectionsByDB; + + /** + * @var array A 2-d map. For each section, gives a map of server names to + * load ratios + */ + private $sectionLoads; + + /** + * @var array[] Server info associative array + * @note The host, hostName and load entries will be overridden + */ + private $serverTemplate; + + // Optional settings + + /** @var array A 3-d map giving server load ratios for each section and group */ + private $groupLoadsBySection = []; + + /** @var array A 3-d map giving server load ratios by DB name */ + private $groupLoadsByDB = []; + + /** @var array A map of hostname to IP address */ + private $hostsByName = []; + + /** @var array A map of external storage cluster name to server load map */ + private $externalLoads = []; + + /** + * @var array A set of server info keys overriding serverTemplate for + * external storage + */ + private $externalTemplateOverrides; + + /** + * @var array A 2-d map overriding serverTemplate and + * externalTemplateOverrides on a server-by-server basis. Applies to both + * core and external storage + */ + private $templateOverridesByServer; + + /** @var array A 2-d map overriding the server info by section */ + private $templateOverridesBySection; + + /** @var array A 2-d map overriding the server info by external storage cluster */ + private $templateOverridesByCluster; + + /** @var array An override array for all master servers */ + private $masterTemplateOverrides; + + /** + * @var array|bool A map of section name to read-only message. Missing or + * false for read/write + */ + private $readOnlyBySection = []; + + // Other stuff + + /** @var array Load balancer factory configuration */ + private $conf; + + /** @var LoadBalancer[] */ + private $mainLBs = []; + + /** @var LoadBalancer[] */ + private $extLBs = []; + + /** @var string */ + private $loadMonitorClass; + + /** @var string */ + private $lastDomain; + + /** @var string */ + private $lastSection; + + /** + * @param array $conf + * @throws InvalidArgumentException + */ + public function __construct( array $conf ) { + parent::__construct( $conf ); + + $this->conf = $conf; + $required = [ 'sectionsByDB', 'sectionLoads', 'serverTemplate' ]; + $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName', + 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer', + 'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides', + 'readOnlyBySection', 'loadMonitorClass' ]; + + foreach ( $required as $key ) { + if ( !isset( $conf[$key] ) ) { + throw new InvalidArgumentException( __CLASS__ . ": $key is required." ); + } + $this->$key = $conf[$key]; + } + + foreach ( $optional as $key ) { + if ( isset( $conf[$key] ) ) { + $this->$key = $conf[$key]; + } + } + } + + /** + * @param bool|string $domain + * @return string + */ + private function getSectionForDomain( $domain = false ) { + if ( $this->lastDomain === $domain ) { + return $this->lastSection; + } + list( $dbName, ) = $this->getDBNameAndPrefix( $domain ); + if ( isset( $this->sectionsByDB[$dbName] ) ) { + $section = $this->sectionsByDB[$dbName]; + } else { + $section = 'DEFAULT'; + } + $this->lastSection = $section; + $this->lastDomain = $domain; + + return $section; + } + + /** + * @param bool|string $domain + * @return LoadBalancer + */ + public function newMainLB( $domain = false ) { + list( $dbName, ) = $this->getDBNameAndPrefix( $domain ); + $section = $this->getSectionForDomain( $domain ); + if ( isset( $this->groupLoadsByDB[$dbName] ) ) { + $groupLoads = $this->groupLoadsByDB[$dbName]; + } else { + $groupLoads = []; + } + + if ( isset( $this->groupLoadsBySection[$section] ) ) { + $groupLoads = array_merge_recursive( + $groupLoads, $this->groupLoadsBySection[$section] ); + } + + $readOnlyReason = $this->readOnlyReason; + // Use the LB-specific read-only reason if everything isn't already read-only + if ( $readOnlyReason === false && isset( $this->readOnlyBySection[$section] ) ) { + $readOnlyReason = $this->readOnlyBySection[$section]; + } + + $template = $this->serverTemplate; + if ( isset( $this->templateOverridesBySection[$section] ) ) { + $template = $this->templateOverridesBySection[$section] + $template; + } + + return $this->newLoadBalancer( + $template, + $this->sectionLoads[$section], + $groupLoads, + $readOnlyReason + ); + } + + /** + * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain + * @return LoadBalancer + */ + public function getMainLB( $domain = false ) { + $section = $this->getSectionForDomain( $domain ); + if ( !isset( $this->mainLBs[$section] ) ) { + $lb = $this->newMainLB( $domain ); + $this->getChronologyProtector()->initLB( $lb ); + $this->mainLBs[$section] = $lb; + } + + return $this->mainLBs[$section]; + } + + /** + * @param string $cluster + * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain + * @throws InvalidArgumentException + * @return LoadBalancer + */ + protected function newExternalLB( $cluster, $domain = false ) { + if ( !isset( $this->externalLoads[$cluster] ) ) { + throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" ); + } + $template = $this->serverTemplate; + if ( isset( $this->externalTemplateOverrides ) ) { + $template = $this->externalTemplateOverrides + $template; + } + if ( isset( $this->templateOverridesByCluster[$cluster] ) ) { + $template = $this->templateOverridesByCluster[$cluster] + $template; + } + + return $this->newLoadBalancer( + $template, + $this->externalLoads[$cluster], + [], + $this->readOnlyReason + ); + } + + /** + * @param string $cluster External storage cluster, or false for core + * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain + * @return LoadBalancer + */ + public function getExternalLB( $cluster, $domain = false ) { + if ( !isset( $this->extLBs[$cluster] ) ) { + $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $domain ); + $this->getChronologyProtector()->initLB( $this->extLBs[$cluster] ); + } + + return $this->extLBs[$cluster]; + } + + /** + * Make a new load balancer object based on template and load array + * + * @param array $template + * @param array $loads + * @param array $groupLoads + * @param string|bool $readOnlyReason + * @return LoadBalancer + */ + private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) { + $lb = new LoadBalancer( array_merge( + $this->baseLoadBalancerParams(), + [ + 'servers' => $this->makeServerArray( $template, $loads, $groupLoads ), + 'loadMonitor' => $this->loadMonitorClass, + 'readOnlyReason' => $readOnlyReason + ] + ) ); + $this->initLoadBalancer( $lb ); + + return $lb; + } + + /** + * Make a server array as expected by LoadBalancer::__construct, using a template and load array + * + * @param array $template + * @param array $loads + * @param array $groupLoads + * @return array + */ + private function makeServerArray( $template, $loads, $groupLoads ) { + $servers = []; + $master = true; + $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads ); + foreach ( $groupLoadsByServer as $server => $stuff ) { + if ( !isset( $loads[$server] ) ) { + $loads[$server] = 0; + } + } + foreach ( $loads as $serverName => $load ) { + $serverInfo = $template; + if ( $master ) { + $serverInfo['master'] = true; + if ( isset( $this->masterTemplateOverrides ) ) { + $serverInfo = $this->masterTemplateOverrides + $serverInfo; + } + $master = false; + } else { + $serverInfo['replica'] = true; + } + if ( isset( $this->templateOverridesByServer[$serverName] ) ) { + $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo; + } + if ( isset( $groupLoadsByServer[$serverName] ) ) { + $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName]; + } + if ( isset( $this->hostsByName[$serverName] ) ) { + $serverInfo['host'] = $this->hostsByName[$serverName]; + } else { + $serverInfo['host'] = $serverName; + } + $serverInfo['hostName'] = $serverName; + $serverInfo['load'] = $load; + $serverInfo += [ 'flags' => DBO_DEFAULT ]; + + $servers[] = $serverInfo; + } + + return $servers; + } + + /** + * Take a group load array indexed by group then server, and reindex it by server then group + * @param array $groupLoads + * @return array + */ + private function reindexGroupLoads( $groupLoads ) { + $reindexed = []; + foreach ( $groupLoads as $group => $loads ) { + foreach ( $loads as $server => $load ) { + $reindexed[$server][$group] = $load; + } + } + + return $reindexed; + } + + /** + * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain + * @return array [database name, table prefix] + */ + private function getDBNameAndPrefix( $domain = false ) { + $domain = ( $domain === false ) + ? $this->localDomain + : DatabaseDomain::newFromId( $domain ); + + return [ $domain->getDatabase(), $domain->getTablePrefix() ]; + } + + /** + * Execute a function for each tracked load balancer + * The callback is called with the load balancer as the first parameter, + * and $params passed as the subsequent parameters. + * @param callable $callback + * @param array $params + */ + public function forEachLB( $callback, array $params = [] ) { + foreach ( $this->mainLBs as $lb ) { + call_user_func_array( $callback, array_merge( [ $lb ], $params ) ); + } + foreach ( $this->extLBs as $lb ) { + call_user_func_array( $callback, array_merge( [ $lb ], $params ) ); + } + } +} diff --git a/includes/libs/rdbms/lbfactory/LBFactorySingle.php b/includes/libs/rdbms/lbfactory/LBFactorySingle.php new file mode 100644 index 0000000000..4beb5d85ea --- /dev/null +++ b/includes/libs/rdbms/lbfactory/LBFactorySingle.php @@ -0,0 +1,96 @@ +lb = new LoadBalancerSingle( array_merge( $this->baseLoadBalancerParams(), $conf ) ); + } + + /** + * @param IDatabase $db Live connection handle + * @param array $params Parameter map to LBFactorySingle::__constructs() + * @return LBFactorySingle + * @since 1.28 + */ + public static function newFromConnection( IDatabase $db, array $params = [] ) { + return new static( [ 'connection' => $db ] + $params ); + } + + /** + * @param bool|string $wiki + * @return LoadBalancerSingle + */ + public function newMainLB( $wiki = false ) { + return $this->lb; + } + + /** + * @param bool|string $wiki + * @return LoadBalancerSingle + */ + public function getMainLB( $wiki = false ) { + return $this->lb; + } + + /** + * @param string $cluster External storage cluster, or false for core + * @param bool|string $wiki Wiki ID, or false for the current wiki + * @return LoadBalancerSingle + */ + protected function newExternalLB( $cluster, $wiki = false ) { + return $this->lb; + } + + /** + * @param string $cluster External storage cluster, or false for core + * @param bool|string $wiki Wiki ID, or false for the current wiki + * @return LoadBalancerSingle + */ + public function getExternalLB( $cluster, $wiki = false ) { + return $this->lb; + } + + /** + * @param string|callable $callback + * @param array $params + */ + public function forEachLB( $callback, array $params = [] ) { + call_user_func_array( $callback, array_merge( [ $this->lb ], $params ) ); + } +} diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php b/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php new file mode 100644 index 0000000000..9de4850fc1 --- /dev/null +++ b/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php @@ -0,0 +1,81 @@ +db = $params['connection']; + + parent::__construct( [ + 'servers' => [ + [ + 'type' => $this->db->getType(), + 'host' => $this->db->getServer(), + 'dbname' => $this->db->getDBname(), + 'load' => 1, + ] + ], + 'trxProfiler' => isset( $params['trxProfiler'] ) ? $params['trxProfiler'] : null, + 'srvCache' => isset( $params['srvCache'] ) ? $params['srvCache'] : null, + 'wanCache' => isset( $params['wanCache'] ) ? $params['wanCache'] : null + ] ); + + if ( isset( $params['readOnlyReason'] ) ) { + $this->db->setLBInfo( 'readOnlyReason', $params['readOnlyReason'] ); + } + } + + /** + * @param IDatabase $db Live connection handle + * @param array $params Parameter map to LoadBalancerSingle::__constructs() + * @return LoadBalancerSingle + * @since 1.28 + */ + public static function newFromConnection( IDatabase $db, array $params = [] ) { + return new static( [ 'connection' => $db ] + $params ); + } + + /** + * + * @param string $server + * @param bool $dbNameOverride + * + * @return IDatabase + */ + protected function reallyOpenConnection( $server, $dbNameOverride = false ) { + return $this->db; + } +} diff --git a/index.php b/index.php index 24230923bc..743f77bd27 100644 --- a/index.php +++ b/index.php @@ -8,7 +8,7 @@ * See the README, INSTALL, and UPGRADE files for basic setup instructions * and pointers to the online documentation. * - * https://www.mediawiki.org/ + * https://www.mediawiki.org/wiki/Special:MyLanguage/MediaWiki * * ---------- * diff --git a/languages/Language.php b/languages/Language.php index 169e0ff109..db71c5c7c9 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -1,6 +1,7 @@ $1 does not exist.", "htmlform-user-not-valid": "$1 isn't a valid username.", "rawmessage": "$1", - "sqlite-has-fts": "$1 with full-text search support", - "sqlite-no-fts": "$1 without full-text search support", "logentry-delete-delete": "$1 {{GENDER:$2|deleted}} page $3", "logentry-delete-restore": "$1 {{GENDER:$2|restored}} page $3", "logentry-delete-event": "$1 {{GENDER:$2|changed}} visibility of {{PLURAL:$5|a log event|$5 log events}} on $3: $4", diff --git a/languages/i18n/eu.json b/languages/i18n/eu.json index 523b2ef144..3be827a655 100644 --- a/languages/i18n/eu.json +++ b/languages/i18n/eu.json @@ -43,6 +43,7 @@ "tog-watchdefault": "Aldatzen ditudan orrialdeak eta fitxategiak nire jarraipen-zerrendara gehitu", "tog-watchmoves": "Izena aldatutako orrialdeak eta fitxategiak jarraipen-zerrendara gehitu", "tog-watchdeletion": "Ezabatzen ditudan orrialdeak eta fitxategiak nire jarraipen-zerrendara gehitu", + "tog-watchuploads": "Gehitu igotzen ditudan fitxategiak nire jarraipen zerrendara", "tog-watchrollback": "Nire jarraipen zerrendan rollbacka egin dudan orrialdeak erakutsi", "tog-minordefault": "Lehenetsi bezala aldaketa txiki bezala markatu guztiak", "tog-previewontop": "Aurrebista aldaketa koadroaren aurretik erakutsi", @@ -52,7 +53,7 @@ "tog-enotifminoredits": "Orrialde edo fitxategietan aldaketak txikiak direnean ere e-posta jaso", "tog-enotifrevealaddr": "Jakinarazpen mezuetan nire e-posta helbidea erakutsi", "tog-shownumberswatching": "Jarraitzen duen erabiltzaile kopurua erakutsi", - "tog-oldsig": "Egungo sinadura:", + "tog-oldsig": "Zure egungo sinadura:", "tog-fancysig": "Sinadura wikitestu gisa tratatu (lotura automatikorik gabe)", "tog-uselivepreview": "Zuzeneko aurrebista erabili", "tog-forceeditsummary": "Aldaketaren laburpena zuri uzterakoan ohartarazi", @@ -163,7 +164,7 @@ "newwindow": "(leiho berrian irekitzen da)", "cancel": "Utzi", "moredotdotdot": "Gehiago...", - "morenotlisted": "Zerrenda hau ez dago osorik.", + "morenotlisted": "Zerrenda hau agian ez dago osorik.", "mypage": "Orrialdea", "mytalk": "Eztabaida", "anontalk": "Eztabaida", @@ -433,6 +434,8 @@ "createacct-reason-ph": "Zergatik ari zaren beste erabiltzaile kontu bat", "createacct-submit": "Kontua sortu", "createacct-another-submit": "Kontu bat sortu", + "createacct-continue-submit": "Jarraitu kontua sortzen", + "createacct-another-continue-submit": "Jarraitu kontua sortzen", "createacct-benefit-heading": "{{SITENAME}} zu bezalako pertsonek egiten dute.", "createacct-benefit-body1": "{{PLURAL:$1|edizio bat|$1 edizio}}", "createacct-benefit-body2": "{{PLURAL:$1|Orrialde 1|$1 orrialde}}", @@ -509,6 +512,7 @@ "botpasswords-label-update": "Eguneratu", "botpasswords-label-cancel": "Utzi", "botpasswords-label-delete": "Ezabatu", + "botpasswords-label-resetpassword": "Pasahitza berrezarri", "resetpass_forbidden": "Ezin dira pasahitzak aldatu", "resetpass-no-info": "Orrialde honetara zuzenean sartzeko izena eman behar duzu.", "resetpass-submit-loggedin": "Pasahitza aldatu", @@ -617,7 +621,7 @@ "continue-editing": "Edizio-eremura joan", "previewconflict": "Aurreikuspenak aldaketen koadroan idatzitako testua erakusten du, gorde ondoren agertuko den bezala.", "session_fail_preview": "'''Sentitzen dugu! Ezin izan da zure aldaketa prozesatu, saioko datu batzuen galera dela-eta. Mesedez, saiatu berriz. Arazoak jarraitzen badu, saiatu [[Special:UserLogout|saioa amaitu]] eta berriz hasten.'''", - "session_fail_preview_html": "'''Sentitzen dugu! Ezin izan dugu zure aldaketa burutu, saio datu galera bat medio.'''\n\n''Wiki honek HTML kodea onartzen duenez, aurreikuspena ezgaituta dago JavaScript erasoak saihestu asmoz.''\n\n'''Aldaketa saiakera hau zuzena baldin bada, saiatu berriro mesedez. Arazoak jarraitzen badu, saiatu saioa itxi eta berriz hasten.'''", + "session_fail_preview_html": "Sentitzen dugu! Ezin izan dugu zure aldaketa burutu, saio datu galera bat medio.\n\nWiki honek HTML kodea onartzen duenez, aurreikuspena ezgaituta dago JavaScript erasoak saihestu asmoz.\n\nAldaketa saiakera hau zuzena baldin bada, saiatu berriro mesedez. Arazoak jarraitzen badu, saiatu [[Special:UserLogout|saioa itxi]] eta berriz hasten.", "token_suffix_mismatch": "'''Zure aldaketa ezeztatua izan da zure bezeroak puntuazio-karaktereak itxuragabetu dituelako.\nAldaketa ezeztatua izan da testuaren galtzea galarazteko.\nHau batzuetan gertatzen da buggyan oinarritutako web proxy zerbitzua erabiltzean.'''", "edit_form_incomplete": "'''Aldaketa formularioaren atal batzuk ez dira iritsi zerbitzarira; bi aldiz ziurtatu zure aldaketak osorik daudela eta berriro saiatu.'''", "editing": "«$1» aldatzen", @@ -634,7 +638,7 @@ "copyrightwarning": "Kontuan izan ezazu {{SITENAME}} webgunean egindako ekarpen guztiak $2 lizentziaren pean argitaratzen direla (xehetasunetarako, ikus $1). Zuk idatzitakoa libreki aldatua eta banatua izatea nahi ez baduzu, ez ezazu hemen jarri.
\nEra berean, hitzematen ari zara hau zuk zeuk idatzia dela, edo jabari publikotik nahiz askea den beste ituri batetik kopiatu duzula.\n'''Ez erabili copyright eskubideek babestutako lanik, baimenik gabe!'''", "copyrightwarning2": "Mesedez, kontuan izan ezazu {{SITENAME}} webgunean egindako ekarpen guztiak beste erabiltzaileek aldatu edo ezabatu ditzaketela. Zuk idatzitakoa libreki aldatua izatea nahi ez baduzu, ez ezazu hemen jarri.
\nEra berean, hitzematen ari zara hau zuk zeuk idatzia dela, edo jabari publikotik nahiz askea den beste ituri batetik kopiatu duzula (xehetasunetarako, ikus $1).\n'''Ez erabili copyright eskubideek babestutako lanik, baimenik gabe!'''", "longpageerror": "'''Errorea: Bidali duzun testuak {{PLURAL:$1|kilobyte 1eko|$1 kilobyteko}} luzera du, eta {{PLURAL:$2|kilobyte 1eko|$2 kilobyteko}} maximoa baino luzeagoa da.'''\nEzin da gorde.", - "readonlywarning": "'''Oharra: Datu-basea blokeatu egin da mantenu lanak burutzeko, beraz ezingo dituzu orain zure aldaketak gorde.'''\nTestua fitxategi baten kopiatu dezakezu, eta beranduago erabiltzeko gorde.\n\nBlokeatu zuen administratzaileak honako azalpena eman zuen: $1", + "readonlywarning": "Oharra: Datu-basea blokeatu egin da mantenu lanak burutzeko, beraz ezingo dituzu orain zure aldaketak gorde.I\nTestua fitxategi baten kopiatu dezakezu, eta beranduago erabiltzeko gorde.\n\nBlokeatu zuen administratzaileak honako azalpena eman zuen: $1", "protectedpagewarning": "'''Oharra: Orri hau blokeatua dago administratzaileek soilik eraldatu ahal dezaten.'''\nAzken erregistroa ondoren ikusgai dago erreferentzia gisa:", "semiprotectedpagewarning": "'''Oharra''': Orrialde hau erregistratutako erabiltzaileek bakarrik aldatzeko babestuta dago.\nErregistroko azken sarrera azpian jartzen da erreferentzia gisa:", "cascadeprotectedwarning": "'''Oharra:''' Orrialde hau blokeatua izan da eta administratzaileek baino ez dute berau aldatzeko ahalmena, honako {{PLURAL:$1|orrialdeko|orrialdeetako}} kaskada-babesean txertatuta dagoelako:", @@ -897,7 +901,7 @@ "rows": "Lerroak:", "columns": "Zutabeak:", "searchresultshead": "Bilaketa", - "stub-threshold": "stub link formaturako atalasea (byteak):", + "stub-threshold": "stub link formaturako atalasea ($1):", "stub-threshold-sample-link": "adibidea", "stub-threshold-disabled": "Ezgaitua", "recentchangesdays": "Aldaketa berrietan erakutsi beharreko egun kopurua:", @@ -1573,6 +1577,9 @@ "apisandbox-helpurls": "Laguntza estekak", "apisandbox-examples": "Adibideak", "apisandbox-dynamic-parameters": "Parametro gehigarriak", + "apisandbox-dynamic-parameters-add-label": "Gehitu parametroa:", + "apisandbox-dynamic-parameters-add-placeholder": "Parametroaren izena", + "apisandbox-dynamic-error-exists": "$1 parametro izena dagoeneko existitzen da", "apisandbox-results": "Emaitzak", "booksources": "Iturri liburuak", "booksources-search-legend": "Liburuen bilaketa", @@ -1588,6 +1595,7 @@ "logempty": "Ez dago emaitzarik erregistroan.", "log-title-wildcard": "Testu honekin hasten diren izenburuak bilatu", "showhideselectedlogentries": "Erakutsi/ezkutatu aukeratutako log sarrerak", + "checkbox-select": "Aukeratu:$1", "checkbox-all": "Denak", "checkbox-none": "Bat ere ez", "allpages": "Orri guztiak", @@ -1686,7 +1694,7 @@ "watchlistanontext": "Mesedez saioa hasi zure jarraipen zerrendako orrialdeak ikusi eta aldatu ahal izateko.", "watchnologin": "Saioa hasi gabe", "addwatch": "Jarraipen zerrendara gehitu", - "addedwatchtext": "«[[:$1]]» orria zure [[Special:Watchlist|jarraipen zerrendara]] erantsi da. \n\nOrri honetan aurrerantzean egindako aldaketak zerrenda horretan agertuko dira.", + "addedwatchtext": "\"[[:$1]]\" eta haren eztabaida orria zure [[Special:Watchlist|jarraipen zerrendara]] erantsi da. \n\nOrri honetan aurrerantzean egindako aldaketak zerrenda horretan agertuko dira.", "addedwatchtext-short": "$1 orria zure jarraipen zerrendara gehitu da.", "removewatch": "Kendu zure jarraipen zerrendatik", "removedwatchtext": "\"[[:$1]]\" orrialdea zure [[Special:Watchlist|jarraipen zerrendatik]] kendu da.", @@ -2164,7 +2172,7 @@ "import-upload": "Igo XML datuak", "import-token-mismatch": "Sesio data galdu da. Saia saitez berriro ere, mesedez.", "import-invalid-interwiki": "Ezin da esandako wikitik inportatu.", - "import-error-edit": "\"$1\" orrialdea ez da inportatu ez duzula baimenik aldatzeko.", + "import-error-edit": "\"$1\" orrialdea ez da inportatu aldatzeko baimenik ez duzulako.", "import-error-create": "\"$1\" orrialdea ez da inportatu ez duzula baimenik sortzeko.", "import-error-interwiki": "\"$1\" orrialdea ez da inportatu bere izena kanpo loturetarako gordeta dagoelako (interwiki).", "import-error-special": "\"$1\" orrialdea ez da inportatu izen-tarte berezi bati dagokiolako eta horretan orrialderik ezin delako egon.", @@ -2935,7 +2943,7 @@ "tags-actions-header": "Ekintzak", "tags-active-yes": "Bai", "tags-active-no": "Ez", - "tags-source-extension": "Luzapenak definitua", + "tags-source-extension": "Softwareak definitua", "tags-source-none": "Ez da gehiago erabiltzen", "tags-edit": "aldatu", "tags-delete": "ezabatu", @@ -2947,6 +2955,7 @@ "tags-create-tag-name": "Etiketaren izena:", "tags-create-reason": "Arrazoia:", "tags-create-submit": "Sortu", + "tags-create-no-name": "Etiketatutako izen bat zehaztu behar duzu.", "tags-create-already-exists": "\"$1\" etiketa badago.", "tags-create-warnings-below": "Etiketaren sorrerarekin jarraitu nahi duzu?", "tags-delete-title": "Etiketa ezabatu", @@ -3182,15 +3191,31 @@ "mw-widgets-titleinput-description-new-page": "orri hori oraindik ez da existitzen", "mw-widgets-titleinput-description-redirect": "$1ra birzuzendu", "sessionprovider-generic": "$1 sesio", + "log-action-filter-block": "Blokeatze mota:", + "log-action-filter-delete": "Ezabatze mota:", + "log-action-filter-import": "Inportazio mota:", + "log-action-filter-move": "Mugimendu mota:", + "log-action-filter-newusers": "Kontu sortze-mota:", + "log-action-filter-patrol": "Patruilatze mota:", + "log-action-filter-protect": "Babes mota:", + "log-action-filter-suppress": "Ezabatze mota:", + "log-action-filter-upload": "Igoera mota:", "log-action-filter-all": "Denak", "log-action-filter-block-block": "Blokeatu", "log-action-filter-block-reblock": "Blokeoa aldatu", "log-action-filter-block-unblock": "blokeoa kendu", + "log-action-filter-delete-revision": "Berrikuspen ezabaketa", + "log-action-filter-import-interwiki": "Transwiki inportazioa", + "log-action-filter-managetags-create": "Etiketa sorkuntza", + "log-action-filter-managetags-delete": "Etiketa ezabaketa", + "log-action-filter-managetags-activate": "Etiketa aktibazioa", + "log-action-filter-managetags-deactivate": "Etiketa desaktibazioa", "authmanager-userdoesnotexist": "\"$1\" erabiltzaile kontua ez dago erregistratua.", "authmanager-email-label": "Emaila", "authmanager-email-help": "Helbide elektronikoa", "authmanager-realname-label": "Benetako izena", "authmanager-realname-help": "Erabiltzailearen benetako izena", "authprovider-resetpass-skip-label": "Utzi", - "authform-wrongtoken": "Token okerra" + "authform-wrongtoken": "Token okerra", + "credentialsform-account": "Kontuaren izena:" } diff --git a/languages/i18n/fr.json b/languages/i18n/fr.json index 21f2e7c497..73bde72314 100644 --- a/languages/i18n/fr.json +++ b/languages/i18n/fr.json @@ -176,7 +176,7 @@ "tog-enotifminoredits": "M’avertir par courriel également lors des modifications mineures des pages ou des fichiers", "tog-enotifrevealaddr": "Afficher mon adresse électronique dans les courriels de notification", "tog-shownumberswatching": "Afficher le nombre d’utilisateurs en cours", - "tog-oldsig": "Signature existante :", + "tog-oldsig": "Votre signature existante :", "tog-fancysig": "Traiter la signature comme du wikitexte (sans lien automatique)", "tog-uselivepreview": "Utiliser l’aperçu rapide", "tog-forceeditsummary": "M’avertir lorsque je n’ai pas spécifié de résumé de modification", @@ -193,7 +193,7 @@ "tog-showhiddencats": "Afficher les catégories cachées", "tog-norollbackdiff": "Ne pas afficher le diff après avoir révoqué", "tog-useeditwarning": "M’avertir quand je quitte une page en cours de modification sans avoir sauvegardé", - "tog-prefershttps": "Toujours utiliser une connexion sécurisée pour se connecter", + "tog-prefershttps": "Utilisez toujours une connexion sécurisée pour vous connecter", "underline-always": "Toujours", "underline-never": "Jamais", "underline-default": "Valeur par défaut du thème ou du navigateur", @@ -288,7 +288,7 @@ "newwindow": "(ouvre dans une nouvelle fenêtre)", "cancel": "Annuler", "moredotdotdot": "Plus...", - "morenotlisted": "Cette liste n’est pas complète.", + "morenotlisted": "Cette liste peut être incomplète.", "mypage": "Page", "mytalk": "Discussion", "anontalk": "Discussion", diff --git a/languages/i18n/gsw.json b/languages/i18n/gsw.json index 489479b35c..faf1fc2e8e 100644 --- a/languages/i18n/gsw.json +++ b/languages/i18n/gsw.json @@ -21,7 +21,8 @@ "80686", "아라", "Macofe", - "Xð" + "Xð", + "Terfili" ] }, "tog-underline": "Links unterstryche:", @@ -401,7 +402,6 @@ "yourpasswordagain": "Passwort no mol yygee:", "createacct-yourpasswordagain": "Passwort bstetige", "createacct-yourpasswordagain-ph": "Gib s Passwort nomol yy", - "remembermypassword": "Uf däm Computer duurhaft aamälde (Maximal fir $1 {{PLURAL:$1|Tag|Täg}})", "userlogin-remembermypassword": "Aagmäldet blyybe", "userlogin-signwithsecure": "Sicheri Verbindig bruuche", "yourdomainname": "Dyyni Domäne", @@ -535,11 +535,8 @@ "passwordreset-emailtext-user": "Dr Benutzer $1 bi {{SITENAME}} het e Zrucksetzig vu Dym Passwort bi {{SITENAME}} aagforderet ($4). \n\n{{PLURAL:$3|Des Benutzerkonto isch|Die Benutzerkonte sin}} mit däre E-Mail-Adräss verchnipft: \n\n$2 \n\n{{PLURAL:$3|Des temporär Passwort lauft|Die temporäre Passwerter laufe}} in {{PLURAL:$5|eim Tag|$5 Täg}} ab.\nDu sottsch di aamälden un e nej Passwort vergee. Wänn eber ander die Aafrog gstellt het oder Du di wider an Dyy alt Passwort chasch erinnere un s nimi wettsch ändere, chasch die Nochricht ignorieren un alsfurt Dyy alt Passwort bruche.", "passwordreset-emailelement": "Benutzername: \n$1\n\nTemporär Passwort: \n$2", "passwordreset-emailsentemail": "We das di bestätigti E-Mail-Adrässen vo dym Wiki-Konto isch, de wird es E-Mail verschickt, für ds Passwort zrüggzsetze.", - "passwordreset-emailsent-capture": "E Passwort-Zrucksetzigs-Mail isch vergschickt worde, un isch unte aazeigt.", - "passwordreset-emailerror-capture": "Die unten angezeigte Passwortzrucksetzigsmail, wu unten aazeigt wird, isch generiert wore, aber dr Versand an {{GENDER:$2|dr Benutzer|d Benutzeri}} het nit funktioniert: $1", "changeemail": "E-Mail-Adrässen änderen oder lösche", "changeemail-header": "Füll das Formular uus, für dyni E-Mail-Adrässe z ändere. We du möchtisch, das dys Wiki-Konto nümm mit eren E-Mail-Adrässe verbunden isch, de chasch ds Fäld für’ne nöüi E-Mail-Adrässe läär la und ds Formular abschicke.", - "changeemail-passwordrequired": "Du muesch dys Passwort agä, für d Änderig z bestätige.", "changeemail-no-info": "Du muesch aagmolde sy zum uff die Syte diräkt zuegryfe z chönne.", "changeemail-oldemail": "Aktuelli E-Mail-Adräss", "changeemail-newemail": "Nöii E-Mail-Adräss:", @@ -719,7 +716,6 @@ "undo-nochange": "Schyns isch die Bearbeitig scho rugggängig gmacht wore.", "undo-summary": "D Änderig $1 vu [[Special:Contributions/$2|$2]] ([[User talk:$2|Diskussion]]) isch ruckgängig gmacht wore.", "undo-summary-username-hidden": "Änderig $1 vun eme versteckte Benutzer ruckgängig gmacht.", - "cantcreateaccounttitle": "Benutzerkonto cha nid aagleit wäre.", "cantcreateaccount-text": "S Aalege vu me Benutzerkonto vu dr IP-Adräss '''($1)''' isch dur [[User:$3|$3]] gsperrt wore.\n\nGrund vu dr Sperri: ''$2''", "cantcreateaccount-range-text": "S Aalege vu Benutzerkonte vu IP-Adrässen im Berych $1, wu s Dyni IP-Adräss ($4) din het, isch vu [[User:$3|$3]] gsperrt wore.\n\nDr Grund, wu vu $3 aagee woren isch: $2", "viewpagelogs": "Logbüecher für die Syten azeige", @@ -1977,6 +1973,7 @@ "contributions": "{{GENDER:$1|Benutzer-Byträg}}", "contributions-title": "Benutzerbyytreg vu „$1“", "mycontris": "Myyni Byyträg", + "anoncontribs": "Byyträg", "contribsub2": "Vu {{GENDER:$3|$1}} ($2)", "contributions-userdoesnotexist": "Ds Benutzerkonto «$1» isch nid registriert.", "nocontribs": "S sin keini Benutzerbyytreg mit däne Kriterie gfunde wore.", @@ -2291,13 +2288,13 @@ "javascripttest": "JavaScript-Tescht", "javascripttest-pagetext-unknownaction": "Unbekannti Aktion «$1».", "javascripttest-qunit-intro": "Lueg d [$1 Dokumentation zue Tescht] uf mediawiki.org", - "tooltip-pt-userpage": "Dyyni Benutzersyte", + "tooltip-pt-userpage": "{{GENDER:|Dyyni}} Benutzersyte", "tooltip-pt-anonuserpage": "D Benutzersyte vo der IP-Adress wo du mit schaffsch", - "tooltip-pt-mytalk": "Dyyni Diskussionssyte", + "tooltip-pt-mytalk": "{{GENDER:|Dyyni}} Diskussionssyte", "tooltip-pt-anontalk": "Diskussione über Änderige vo dere IP-Adress", - "tooltip-pt-preferences": "Myni Ystellige", + "tooltip-pt-preferences": "{{GENDER:|Dyni}} Ystellige", "tooltip-pt-watchlist": "Lischte vo de beobachtete Syte.", - "tooltip-pt-mycontris": "Lischt vu Dyyne Byyträg", + "tooltip-pt-mycontris": "E Lischt vu {{GENDER:|Dyyne}} Byyträg", "tooltip-pt-login": "Aamälde", "tooltip-pt-logout": "Abmälde", "tooltip-pt-createaccount": "Du chasch gärn e Benutzerkonto aalege un Di aamälde. Du muesch s aber nit", @@ -2328,7 +2325,7 @@ "tooltip-t-recentchangeslinked": "Letschti Änderige vo de Syte, wo vo do verlinkt sin", "tooltip-feed-rss": "RSS-Feed für selli Syte", "tooltip-feed-atom": "Atom-Feed für selli Syte", - "tooltip-t-contributions": "Lischte vo de Byträg vo däm Benutzer", + "tooltip-t-contributions": "E Lischt vo de Byträg vo {{GENDER:$1|däm Benutzer}}", "tooltip-t-emailuser": "Schick däm Benutzer e E-Bost", "tooltip-t-info": "Meh Informationen über die Syte", "tooltip-t-upload": "Dateien ufelade", @@ -3386,6 +3383,5 @@ "mw-widgets-dateinput-placeholder-month": "JJJJ-MM", "mw-widgets-titleinput-description-new-page": "d Syte git’s no nid", "mw-widgets-titleinput-description-redirect": "Wyterleitig uf $1", - "api-error-blacklisted": "Bitte due en andre, ussagechräftigere Titel usswääle.", "randomrootpage": "Zuefelligi Stammsyte" } diff --git a/languages/i18n/he.json b/languages/i18n/he.json index e76347fd20..8dad7796a6 100644 --- a/languages/i18n/he.json +++ b/languages/i18n/he.json @@ -2002,22 +2002,22 @@ "watcherrortext": "אירעה שגיאה בעת שינוי הגדרות רשימת המעקב של \"$1\".", "enotif_reset": "סימון כל הדפים כאילו נצפו", "enotif_impersonal_salutation": "משתמש ב{{GRAMMAR:תחילית|{{SITENAME}}}}", - "enotif_subject_deleted": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} נמחק על־ידי $2", - "enotif_subject_created": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} נוצר על־ידי $2", - "enotif_subject_moved": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} הועבר על־ידי $2", - "enotif_subject_restored": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} שוחזר על־ידי $2", - "enotif_subject_changed": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} שונה על־ידי $2", - "enotif_body_intro_deleted": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} נמחק ב־$PAGEEDITDATE על ידי $2, ראו $3.", - "enotif_body_intro_created": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} נוצר ב־$PAGEEDITDATE על ידי $2, ראו $3 לגרסה הנוכחית.", - "enotif_body_intro_moved": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} הועבר ב־$PAGEEDITDATE על ידי $2, ראו $3 לגרסה הנוכחית.", - "enotif_body_intro_restored": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} שוחזר ב־$PAGEEDITDATE על ידי $2, ראו $3 לגרסה הנוכחית.", - "enotif_body_intro_changed": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} שונה ב־$PAGEEDITDATE על ידי $2, ראו $3 לגרסה הנוכחית.", - "enotif_lastvisited": "ראו $1 לכל השינויים מאז ביקורכם האחרון.", + "enotif_subject_deleted": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} נמחק על־ידי $2", + "enotif_subject_created": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} נוצר על־ידי $2", + "enotif_subject_moved": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} הועבר על־ידי $2", + "enotif_subject_restored": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} שוחזר על־ידי $2", + "enotif_subject_changed": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} שוּנה על־ידי $2", + "enotif_body_intro_deleted": "הדף \"$1\" באתר {{SITENAME}} נמחק ב־$PAGEEDITDATE על־ידי $2; ראו $3.", + "enotif_body_intro_created": "הדף \"$1\" באתר {{SITENAME}} נוצר ב־$PAGEEDITDATE על־ידי $2; ראו $3 לגרסה הנוכחית של הדף.", + "enotif_body_intro_moved": "הדף \"$1\" באתר {{SITENAME}} הועבר ב־$PAGEEDITDATE על־ידי $2; ראו $3 לגרסה הנוכחית של הדף.", + "enotif_body_intro_restored": "הדף \"$1\" באתר {{SITENAME}} שוחזר ב־$PAGEEDITDATE על־ידי $2; ראו $3 לגרסה הנוכחית של הדף.", + "enotif_body_intro_changed": "הדף \"$1\" באתר {{SITENAME}} שוּנה ב־$PAGEEDITDATE על־ידי $2; ראו $3 לגרסה הנוכחית של הדף.", + "enotif_lastvisited": "ראו $1 לכל השינויים מאז ביקורכם האחרון בדף.", "enotif_lastdiff": "ראו $1 לשינוי זה.", "enotif_anon_editor": "משתמש אנונימי $1", - "enotif_body": "לכבוד $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nתקציר העריכה: $PAGESUMMARY $PAGEMINOREDIT\n\nבאפשרותכם ליצור קשר עם העורך:\nבדואר האלקטרוני: $PAGEEDITOR_EMAIL\nבאתר: $PAGEEDITOR_WIKI\n\nלא תהיינה הודעות על פעולות נוספות עד שתבקרו בדף כשאתם מחוברים לחשבון. באפשרותכם גם לאפס את דגלי ההודעות בכל הדפים שברשימת המעקב.\n\nמערכת ההודעות של {{SITENAME}}\n\n--\nכדי לשנות את ההגדרות של הודעות הדוא\"ל הנשלחות אליכם, בקרו בדף\n{{canonicalurl:{{#special:Preferences}}}}\n\nכדי לשנות את הגדרות רשימת המעקב, בקרו בדף\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nכדי למחוק את הדף מרשימת המעקב שלכם, בקרו בדף\n$UNWATCHURL\n\nלמשוב ולעזרה נוספת:\n$HELPPAGE", + "enotif_body": "לכבוד $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nתקציר העריכה: $PAGESUMMARY $PAGEMINOREDIT\n\nבאפשרותכם ליצור קשר עם העורך:\nבדואר אלקטרוני: $PAGEEDITOR_EMAIL\nבאתר: $PAGEEDITOR_WIKI\n\nלא תקבלו הודעות על פעולות נוספות עד שתבקרו בדף הזה כשאתם מחוברים לחשבון. באפשרותכם גם לאפס את דגלי ההודעות עבור כל הדפים שברשימת המעקב שלכם.\n\nבברכה, מערכת ההודעות של {{SITENAME}}.\n\n--\nכדי לשנות את ההגדרות של הודעות הדוא\"ל הנשלחות אליכם, בקרו בדף:\n{{canonicalurl:{{#special:Preferences}}}}\n\nכדי לשנות את ההגדרות של רשימת המעקב שלכם, בקרו בדף:\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nכדי להסיר את הדף הזה מרשימת המעקב שלכם, בקרו בדף:\n$UNWATCHURL\n\nלמשוב ולעזרה נוספת:\n$HELPPAGE", "created": "נוצר", - "changed": "שונה", + "changed": "שוּנה", "deletepage": "מחיקת הדף", "confirm": "אישור", "excontent": "התוכן היה: \"$1\"", diff --git a/languages/i18n/it.json b/languages/i18n/it.json index a752071421..7ee30bca5b 100644 --- a/languages/i18n/it.json +++ b/languages/i18n/it.json @@ -101,7 +101,8 @@ "Matteocng", "Einreiher", "Anto", - "Saracrovetto" + "Saracrovetto", + "Tosky" ] }, "tog-underline": "Sottolinea i collegamenti:", @@ -657,7 +658,7 @@ "passwordreset": "Reimposta password", "passwordreset-text-one": "Compila questo modulo per reimpostare la tua password.", "passwordreset-text-many": "{{PLURAL:$1|Compila uno dei campi per ricevere una password temporanea tramite email.}}", - "passwordreset-disabled": "La reimpostazione delle password è stata disabilitata su questa wiki", + "passwordreset-disabled": "La reimpostazione delle password è stata disabilitata per questo wiki", "passwordreset-emaildisabled": "Le funzionalità di posta elettronica sono state disabilitate su questa wiki.", "passwordreset-username": "Nome utente:", "passwordreset-domain": "Dominio:", diff --git a/languages/i18n/ka.json b/languages/i18n/ka.json index e9e2f927b2..d149b527c5 100644 --- a/languages/i18n/ka.json +++ b/languages/i18n/ka.json @@ -431,7 +431,7 @@ "password-change-forbidden": "თქვენ არ შეგიძლიათ ამ ვიკიში პაროლის შეცვლა.", "externaldberror": "საგარეო მონაცემთა ბაზაში აუტენტიფიკაციის შეცდომაა, ან თქვენ არ გაქვთ საკმარისი უფლებები საგარეო ანგარიშში ცვლილებების შესატანად.", "login": "შესვლა", - "login-security": "დაადასტურეთ თქვენი ავთენტურობა", + "login-security": "დაადასტურეთ იდენტიფიკაცია", "nav-login-createaccount": "შესვლა / რეგისტრაცია", "userlogin": "შესვლა/ანგარიშის შექმნა", "userloginnocreate": "შესვლა", @@ -449,7 +449,7 @@ "userlogin-resetpassword-link": "დაგავიწყდათ პაროლი?", "userlogin-helplink2": "დახმარება:შესვლა", "userlogin-loggedin": "თქვენ უკვე შეხვედით როგორც {{GENDER:$1|$1}}.\nგამოიყენეთ ფორმა ქვემოთ, რათა შეხვიდეთ სხვა ანგარიშიდან.", - "userlogin-reauth": "თქვენ კვლავ უნდა შეხვიდეთ სისტემაში რათა შემოწმდეს რომ ხართ $1", + "userlogin-reauth": "თქვენ უნდა გაიაროთ ავტორიზაცია, რათა კიდევ ერთხელ მოხდეს თქვენი იდენტიფიცირება ანგარიშთან „{{GENDER:$1|$1}}“.", "userlogin-createanother": "სხვა ანგარიშის შექმნა", "createacct-emailrequired": "ელ. ფოსტის მისამართი", "createacct-emailoptional": "ელ. ფოსტის მისამართი (არასავალდებულო)", diff --git a/languages/i18n/kiu.json b/languages/i18n/kiu.json index 43dc0ded9c..1580a47918 100644 --- a/languages/i18n/kiu.json +++ b/languages/i18n/kiu.json @@ -113,7 +113,7 @@ "hidden-categories": "{{PLURAL:$1|Kategoriya wedariyaiye|Kategoriyê wedariyaey}}", "hidden-category-category": "Kategoriyê wedariyaey", "category-subcat-count": "{{PLURAL:$2|Na kategoriye de ana kategoriya bınêne esta.|Na kategoriye de $2 ra pêro pia, {{PLURAL:$1|ana kategoriya bınêne esta|ani $1 kategoriyê bınêni estê.}}, be $2 ra pia.}}", - "category-subcat-count-limited": "Na kategoriye de {{PLURAL:$1|ana kategoriya bınêne esta|ani $1 kategoriyê bınêni estê}}.", + "category-subcat-count-limited": "Na kategoriya de {{PLURAL:$1|ana kategoriya bınêne esta|ani $1 kategoriyê bınêni estê}}.", "category-article-count": "{{PLURAL:$2|Na kategoriye de teyna ana pele esta.|Na kategoriye de $2 ra pêro pia, {{PLURAL:$1|ana pele esta|ani $1 peli estê.}}, be $2 ra pêro pia}}", "category-article-count-limited": "{{PLURAL:$1|Ana pele kategoriya peyêne dera|Ani $1 peli kategoriya peyêne derê}}.", "category-file-count": "{{PLURAL:$2|Na kategoriye de teyna ana dosya esta.|Na kategoriye de $2 ra pêro pia, {{PLURAL:$1|ana dosya esta|ani $1 dosyey estê.}}}}", diff --git a/languages/i18n/lt.json b/languages/i18n/lt.json index f3cf09b2ac..1bf6dfaf3e 100644 --- a/languages/i18n/lt.json +++ b/languages/i18n/lt.json @@ -63,7 +63,7 @@ "tog-enotifminoredits": "Siųsti man laišką, kai puslapio keitimas yra smulkus", "tog-enotifrevealaddr": "Rodyti mano el. pašto adresą priminimo laiškuose", "tog-shownumberswatching": "Rodyti stebinčių naudotojų skaičių", - "tog-oldsig": "Galiojantis parašas:", + "tog-oldsig": "Jūsų egzistuojantis parašas:", "tog-fancysig": "Laikyti parašą vikitekstu (be automatinių nuorodų)", "tog-uselivepreview": "Naudoti tiesioginę peržiūrą", "tog-forceeditsummary": "Klausti, kai palieku tuščią keitimo komentarą", @@ -80,7 +80,7 @@ "tog-showhiddencats": "Rodyti paslėptas kategorijas", "tog-norollbackdiff": "Nerodyti skirtumo atlikus atmetimą", "tog-useeditwarning": "Perspėti mane, kai palieku redagavimo puslapį, o jame yra neišsaugotų pakeitimų", - "tog-prefershttps": "Prisiregistruojant visada naudokite saugų ryšį", + "tog-prefershttps": "Visada naudoti saugų ryšį esant prisijungus", "underline-always": "Visada", "underline-never": "Niekada", "underline-default": "Pagal naršyklės nustatymus", @@ -175,7 +175,7 @@ "newwindow": "(atsidaro naujame lange)", "cancel": "Atšaukti", "moredotdotdot": "Daugiau...", - "morenotlisted": "Šis sąrašas nėra išsamus.", + "morenotlisted": "Šis sąrašas gali būti nepilnas.", "mypage": "Puslapis", "mytalk": "Aptarimas", "anontalk": "Aptarimas", @@ -760,6 +760,8 @@ "invalid-content-data": "Neleistinas turinys.", "content-not-allowed-here": "Turinys \"$1\" puslapyje [[$2]] nėra leistinas.", "editwarning-warning": "Palikdamas šį puslapį jūs galite prarasti visus padarytus pakeitimus.\nJei esate prisijungęs, galite išjungti šį perspėjimą jūsų nustatymų skyrelyje \"{{int:prefs-editing}}\".", + "editpage-invalidcontentmodel-title": "Turinio modelis nepalaikomas", + "editpage-invalidcontentmodel-text": "Turinio modulis „$1“ nėra palaikomas.", "editpage-notsupportedcontentformat-title": "Turinio formatas nepalaikomas", "editpage-notsupportedcontentformat-text": "Turinio formatas $1 nepalaiko turinio modelio $2.", "content-model-wikitext": "vikitekstas", @@ -3216,6 +3218,8 @@ "tag-filter": "[[Special:Tags|Žymų]] filtras:", "tag-filter-submit": "Filtras", "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Žyma|Žymos}}]]: $2)", + "tag-mw-contentmodelchange": "turinio modulio keitimas", + "tag-mw-contentmodelchange-description": "Pakeitimai, kurie [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel keičia puslapio turinio modelį]", "tags-title": "Žymos", "tags-intro": "Šiame puslapyje yra žymų, kuriomis programinė įranga gali pažymėti keitimus, sąrašas bei jų reikšmės.", "tags-tag": "Žymos pavadinimas", @@ -3227,7 +3231,7 @@ "tags-actions-header": "Veiksmai", "tags-active-yes": "Taip", "tags-active-no": "Ne", - "tags-source-extension": "Apibrėžta papildinio", + "tags-source-extension": "Apibrėžta programinės įrangos", "tags-source-manual": "Taikoma vartotojų ar robotų rankiniu būdu", "tags-source-none": "Nebevartojamas", "tags-edit": "taisyti", diff --git a/languages/i18n/my.json b/languages/i18n/my.json index fb7b91842d..b2f84c0810 100644 --- a/languages/i18n/my.json +++ b/languages/i18n/my.json @@ -1215,6 +1215,7 @@ "deleteotherreason": "အခြားသော/နောက်ထပ် အကြောင်းပြချက် -", "deletereasonotherlist": "အခြား အကြောင်းပြချက်", "delete-edit-reasonlist": "ဖျက်ပစ်ရသော အကြောင်းရင်းများကို တည်းဖြတ်ရန်", + "deleting-backlinks-warning": "သတိပေးချက်။ သင်ဖျက်ပစ်တော့မည့် စာမျက်နှာအား [[Special:WhatLinksHere/{{FULLPAGENAME}}|အခြားစာမျက်နှာများမှ]] ချိတ်ဆက်ထားခြင်း သို့မဟုတ် ထည့်သွင်းထားခြင်း ရှိနေသည်။", "rollbacklink": "နောက်ပြန် ပြန်သွားရန်", "rollbacklinkcount": "{{PLURAL:$1|တည်းဖြတ်မှု|တည်းဖြတ်မှုများ}} $1 ကို နောက်ပြန်ပြင်ရန်", "protectlogpage": "ကာကွယ်မှုများ၏ မှတ်တမ်း", diff --git a/languages/i18n/pt-br.json b/languages/i18n/pt-br.json index 5dac084b33..8738485b9d 100644 --- a/languages/i18n/pt-br.json +++ b/languages/i18n/pt-br.json @@ -99,7 +99,8 @@ "Anderson Costa", "LucyDiniz", "Tusca", - "Cristofer Alves" + "Cristofer Alves", + "Tark" ] }, "tog-underline": "Sublinhar links:", @@ -1322,8 +1323,8 @@ "rightslogtext": "Este é um registro de mudanças nos privilégios de usuários.", "action-read": "ler esta página", "action-edit": "editar esta página", - "action-createpage": "criar esta páginas", - "action-createtalk": "criar esta páginas de discussão", + "action-createpage": "criar esta página", + "action-createtalk": "criar esta página de discussão", "action-createaccount": "criar esta conta de usuário", "action-autocreateaccount": "Criar uma conta de usuário externa automaticamente", "action-history": "Ver o histórico desta página", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index fbf95cc359..163b613247 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -3977,8 +3977,6 @@ "htmlform-user-not-exists": "Error message shown if a user with the name provided by the user does not exist. $1 is the username.", "htmlform-user-not-valid": "Error message shown if the name provided by the user isn't a valid username. $1 is the username.", "rawmessage": "{{notranslate}} Used to pass arbitrary text as a message specifier array", - "sqlite-has-fts": "Shown on [[Special:Version]].\nParameters:\n* $1 - version", - "sqlite-no-fts": "Shown on [[Special:Version]].\nParameters:\n* $1 - version", "logentry-delete-delete": "{{Logentry|[[Special:Log/delete]]}}", "logentry-delete-restore": "{{Logentry|[[Special:Log/delete]]}}", "logentry-delete-event": "{{Logentry|[[Special:Log/delete]]}}\n{{Logentryparam}}\n* $5 - count of affected log events", diff --git a/languages/i18n/rm.json b/languages/i18n/rm.json index 2a2e31cf87..02eeaa58b3 100644 --- a/languages/i18n/rm.json +++ b/languages/i18n/rm.json @@ -12,7 +12,8 @@ "아라", "Macofe", "Matma Rex", - "Translaziuns" + "Translaziuns", + "Terfili" ] }, "tog-underline": "Suttastritgar colliaziuns:", @@ -195,6 +196,7 @@ "otherlanguages": "En autras linguas", "redirectedfrom": "(renvià da $1)", "redirectpagesub": "questa pagina renviescha tar in'auter artitgel", + "redirectto": "Renviescha a:", "lastmodifiedat": "Questa pagina è vegnida modifitgada l'ultima giada ils $1 a las $2.", "viewcount": "Questa pagina è vegnida contemplada {{PLURAL:$1|ina giada|$1 giadas}}.", "protectedpage": "Pagina protegida", @@ -270,6 +272,7 @@ "nstab-template": "Model", "nstab-help": "Agid", "nstab-category": "Categoria", + "mainpage-nstab": "Pagina principala", "nosuchaction": "Talas acziuns n'existan betg", "nosuchactiontext": "L'acziun specifitgada per questa URL è faussa.\nTi has endatà fauss la URL, u es suandà in link incorrect.\nI po dentant er esser ina errur en la software da {{SITENAME}}.", "nosuchspecialpage": "I n'exista betg ina tala pagina speziala", @@ -329,6 +332,7 @@ "welcomeuser": "Bainvegni, $1!", "welcomecreation-msg": "Tes conto è vegnì creà. \nN'emblida betg da midar tias [[Special:Preferences|{{SITENAME}} preferenzas]].", "yourname": "Num d'utilisader", + "userlogin-yourname": "Num d'utilisader", "userlogin-yourname-ph": "Endatescha tes num d'utilisader", "createacct-another-username-ph": "Endatescha in num d'utilisader", "yourpassword": "pled-clav", @@ -338,7 +342,6 @@ "yourpasswordagain": "repeter pled-clav", "createacct-yourpasswordagain": "Confermar il pled-clav", "createacct-yourpasswordagain-ph": "Endatescha il pled-clav anc ina giada", - "remembermypassword": "S'annunziar permanantamain sin quest computer (per maximalmain $1 {{PLURAL:$1|di|dis}})", "userlogin-remembermypassword": "Restar annunzià", "userlogin-signwithsecure": "Duvrar ina connexiun segira", "yourdomainname": "Vossa domain", @@ -427,6 +430,7 @@ "pt-login": "T'annunziar", "pt-login-button": "T'annunziar", "pt-createaccount": "Crear in conto d'utilisader", + "pt-userlogout": "Sortir", "php-mail-error-unknown": "Errur nunenconuschenta en la funcziun mail() da PHP", "user-mail-no-addy": "Empruvà da trametter in e-mail senza ina adressa dad e-mail.", "changepassword": "Midar pled-clav", @@ -455,8 +459,6 @@ "passwordreset-emailtext-user": "L'utilisader $1 sin {{SITENAME}} ha dumandà da redefinir il pled-clav per {{SITENAME}} ($4). \n{{PLURAL:$3|Il suandant conto d'utilisader è collià|Ils suandants contos d'utilisader èn colliads}} cun questa adressa dad e-mail:\n\n$2\n\n{{PLURAL:$3|Quest pled-clav temporar|Quests pled-clav temporars}} èn valids {{PLURAL:$5|in di|$5 dis}}.\nTi duessas t'annunziar ussa e tscherner in nov pled-clav. Sche ti na levas betg quests novs pleds-clav u sche ti ta regordas puspè da tes pled-clav original e na vuls betg pli midar il pled-clav pos ti ignorar quest messadi e cuntinuar dad utilisar tes pled-clav original.", "passwordreset-emailelement": "Num d'utilisader: \n$1\n\nPled-clav temporar: \n$2", "passwordreset-emailsentemail": "In e-mail per redefinir il pled-clav è vegnì tramess.", - "passwordreset-emailsent-capture": "In e-mail (sco mussà sutvart) per redefinir il pled-clav è vegnì tramess.", - "passwordreset-emailerror-capture": "In e-mail (sco mussà sutvart) per redefinir il pled-clav è vegnì generà ma n'ha betg pudì envià a l'{{GENDER:$2|utilisader|utilisadra}}: $1", "changeemail": "Midar l'adressa dad e-mail", "changeemail-header": "Midar l'adressa dad e-mail dal conto", "changeemail-no-info": "Ti stos t'annunziar per acceder directamain questa pagina.", @@ -513,7 +515,7 @@ "newarticle": "(Nov)", "newarticletext": "Ti has cliccà ina colliaziun ad ina pagina che n'exista anc betg. Per crear ina pagina, entschaiva a tippar en la stgaffa sutvart (guarda [$1 la pagina d'agid] per t'infurmar).", "anontalkpagetext": "----''Quai è la pagina da discussiun per in utilisader anomim che n'ha anc betg creà in conto d'utilisader u che n'al utilisescha betg.\nPerquai avain nus d'utilisar l'adressa dad IP per l'identifitgar.\nIna tala adressa dad IP po vegnir utilisada da differents utilisaders.\nSche ti es in utilisaders anonim e pensas che commentaris che na pertutgan betg tai vegnan adressads a tai, lura [[Special:CreateAccount|creescha in conto]] u [[Special:UserLogin|t'annunzia]] per evitar en futur che ti vegns sbaglià cun auters utilisaders.''", - "noarticletext": "Quest artitgel na cuntegna actualmain nagin text.\nTi pos [[Special:Search/{{PAGENAME}}|tschertgar il term]] sin in'autra pagina,\n[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tschertgar en ils protocols],\nu [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear questa pagina].", + "noarticletext": "Questa pagina na cuntegna actualmain nagin text.\nTi pos [[Special:Search/{{PAGENAME}}|tschertgar il term]] sin in'autra pagina,\n[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tschertgar en ils protocols],\nu [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear questa pagina].", "noarticletext-nopermission": "Questa pagina na cuntegna actualmain nagin text.\nTi pos [[Special:Search/{{PAGENAME}}|tschertgar quest titel]] en autras paginas u [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tschertgar en ils protocols correspundents], ma ti n'has betg ils dretgs da crear questa pagina.", "missing-revision": "La versiun #$1 da la pagina cun il num \"{{FULLPAGENAME}}\" n'exista betg.\n\nQuai capita savnes sche ti cliccas sin ina colliaziun antiquada en la cronologia per ina pagina ch'è vegnida stizzada.\nDetagls pon vegnri chattads en il [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} protocol da stizzar].", "userpage-userdoesnotexist": "Il conto d'utilisader \"$1\" n'èxista betg.\nControllescha sch ti vuls propi crear/modiftgar questa pagina.", @@ -604,7 +606,6 @@ "undo-failure": "La modificaziun na pudeva betg vegnir revocada causa modificaziuns pli novas che stattan en conflict cun questa acziun.", "undo-norev": "La modificaziun na pudeva betg vegnir revocada perquai ch'ella n'exista betg u è vegnida stizzada.", "undo-summary": "Revocar la versiun $1 da [[Special:Contributions/$2|$2]] ([[User talk:$2|discussiun]])", - "cantcreateaccounttitle": "Betg pussaivel da crear il conto", "cantcreateaccount-text": "La creaziun da contos du'utilisader è vegnida bloccada da l'utilisader [[User:$3|$3]] per questa adressa IP ('''$1''').\n\nIl motiv inditgà da $3 è ''$2''", "viewpagelogs": "Guardar ils protocols da questa pagina", "nohistory": "Per questa pagina n'exista nagina cronologia.", @@ -998,6 +999,7 @@ "action-siteadmin": "bloccar u debloccar la banca da datas", "action-sendemail": "trametter e-mails", "nchanges": "$1 {{PLURAL:$1|midada|midadas}}", + "enhancedrc-history": "Cronologia", "recentchanges": "Ultimas midadas", "recentchanges-legend": "Opziuns per las ultimas midadas", "recentchanges-summary": "Sin questa pagina pos ti suandar las ultimas midadas sin '''{{SITENAME}}'''.", @@ -1388,6 +1390,7 @@ "querypage-disabled": "Questa pagina speciala è deactivada ord motivs da prestaziun.", "booksources": "Tschertga da ISBN", "booksources-search-legend": "Tschertgar pussaivladad da cumpra per cudeschs", + "booksources-search": "Tschertgar", "booksources-text": "Sutvart è ina glista da las colliaziuns ad autras paginas che vendan cudeschs novs ed utilisads e che pudessan avair dapli infurmaziuns davart ils cudeschs che ti tschertgas:", "booksources-invalid-isbn": "Il numer ISBN na para betg dad esser valid; controllescha che ti n'has betg fatg errurs cun la scriver.", "specialloguserlabel": "Acziun exequida da:", @@ -1660,6 +1663,7 @@ "contributions": "Contribuziuns {{GENDER:$1|da l'utilisader|da l'utilisadra}}", "contributions-title": "Contribuziuns d'utilisader da $1", "mycontris": "Contribuziuns", + "anoncontribs": "Contribuziuns", "contribsub2": "Per {{GENDER:$3|$1}} ($2)", "nocontribs": "Chattà naginas modificaziuns che correspundan a quests criteris.", "uctop": "(actual)", @@ -1954,9 +1958,9 @@ "import-logentry-interwiki-detail": "{{PLURAL:$1|Ina versiun|$1 versiuns}} da $2", "javascripttest": "Test da JavaScript", "javascripttest-qunit-intro": "Legia la [$1 documentaziun da tests] sin mediawiki.org.", - "tooltip-pt-userpage": "Mussar tia pagina d'utilisader", + "tooltip-pt-userpage": "Mussar {{GENDER:|tia pagina d'utilisader}}", "tooltip-pt-anonuserpage": "La pagina d'utilisader per l'adressa IP cun la quala che ti fas modificaziuns", - "tooltip-pt-mytalk": "Mussar tia pagina da discussiun", + "tooltip-pt-mytalk": "Mussar {{GENDER:|tia}} pagina da discussiun", "tooltip-pt-anontalk": "Discussiun davart modificaziuns che derivan da questa adressa dad IP", "tooltip-pt-preferences": "mias preferenzas", "tooltip-pt-watchlist": "La glista da las paginas da las qualas jau observ las midadas", @@ -1964,7 +1968,7 @@ "tooltip-pt-login": "I fiss bun sche ti s'annunziassas, ti na stos dentant betg.", "tooltip-pt-logout": "Sortir", "tooltip-ca-talk": "Discussiuns davart il cuntegn da l'artitgel", - "tooltip-ca-edit": "Ti pos modifitgar questa pagina.\nUtilisescha per plaschair il buttun 'mussar prevista' avant che memorisar.", + "tooltip-ca-edit": "Modifitgar questa pagina", "tooltip-ca-addsection": "Cumenzar nov paragraf", "tooltip-ca-viewsource": "Questa pagina è protegida.\nTi pos vesair il code-fundamental.", "tooltip-ca-history": "Versiuns pli veglias da questa pagina", @@ -1990,7 +1994,7 @@ "tooltip-t-recentchangeslinked": "Ultimas midadas sin paginas colliadas cun questa pagina", "tooltip-feed-rss": "RSS feed per questa pagina", "tooltip-feed-atom": "Atom feed per questa pagina", - "tooltip-t-contributions": "Mussar las contribuziuns da quest utilisader", + "tooltip-t-contributions": "Mussar las contribuziuns da {{GENDER:$1|quest utilisader}}", "tooltip-t-emailuser": "Trametter in e-mail a quest utilisader", "tooltip-t-upload": "Chargiar si datotecas", "tooltip-t-specialpages": "Glista da tut las paginas spezialas", @@ -2661,14 +2665,14 @@ "revdelete-uname-unhid": "dà liber il num d'utilisader", "revdelete-restricted": "applitgà restricziuns per administraturs", "revdelete-unrestricted": "allontanà restricziuns per administraturs", - "logentry-move-move": "$1 ha spustà la pagina $3 a $4", + "logentry-move-move": "$1 {{GENDER:$2|ha spustà}} la pagina $3 a $4", "logentry-move-move-noredirect": "$1 ha spustà la pagina $3 a $4 senza crear in renviament", "logentry-move-move_redir": "$1 ha spustà la pagina $3 a $4 e surscrit quatras in renviament", "logentry-move-move_redir-noredirect": "$1 ha spustà la pagina $3 a $4 e surscrit quatras in renviament senza crear in renviament", "logentry-patrol-patrol": "$1 ha marcà la versiun $4 da la pagina $3 sco controllada", "logentry-patrol-patrol-auto": "$1 ha marcà automaticamain la versiun $4 da la pagina $3 sco controllada", "logentry-newusers-newusers": "Il conto $1 è vegnì creà", - "logentry-newusers-create": "Il conto $1 è vegnì creà", + "logentry-newusers-create": "Il conto $1 è vegnì {{GENDER:$2|creà}}", "logentry-newusers-create2": "Il conto $3 è vegnì creà da $1", "logentry-newusers-autocreate": "Il conto $1 è vegnì creà automaticamain", "logentry-rights-rights": "$1 ha midà la commembranza da gruppas per $3 da $4 a $5", diff --git a/languages/i18n/sv.json b/languages/i18n/sv.json index 3ea3fc41ce..9e4970256f 100644 --- a/languages/i18n/sv.json +++ b/languages/i18n/sv.json @@ -102,7 +102,7 @@ "tog-enotifminoredits": "Skicka mig e-post även för mindre ändringar av sidor och filer", "tog-enotifrevealaddr": "Visa min e-postadress i e-postmeddelanden om ändringar som skickas till andra", "tog-shownumberswatching": "Visa antalet användare som bevakar", - "tog-oldsig": "Nuvarande signatur:", + "tog-oldsig": "Din nuvarande signatur:", "tog-fancysig": "Behandla signatur som wikitext (utan en automatisk länk)", "tog-uselivepreview": "Använd direktuppdaterad förhandsgranskning", "tog-forceeditsummary": "Påminn mig om jag inte fyller i en redigeringskommentar", @@ -119,7 +119,7 @@ "tog-showhiddencats": "Visa dolda kategorier", "tog-norollbackdiff": "Visa inte diff efter tillbakarullning", "tog-useeditwarning": "Varna mig om jag lämnar en redigeringssida där jag gjort ändringar men inte sparat.", - "tog-prefershttps": "Använd alltid en säker anslutning när jag är inloggad", + "tog-prefershttps": "Använd alltid en säker anslutning medan jag är inloggad", "underline-always": "Alltid", "underline-never": "Aldrig", "underline-default": "Webbläsarens eller utseendets standardinställning", @@ -214,7 +214,7 @@ "newwindow": "(öppnas i ett nytt fönster)", "cancel": "Avbryt", "moredotdotdot": "Mer...", - "morenotlisted": "Denna lista är inte fullständig.", + "morenotlisted": "Denna lista är kanske inte fullständig.", "mypage": "Sida", "mytalk": "Diskussion", "anontalk": "Diskussion", @@ -610,7 +610,7 @@ "botpasswords-updated-body": "Botlösenordet för botnamnet \"$1\" till användaren \"$2\" uppdaterades.", "botpasswords-deleted-title": "Botlösenord raderades", "botpasswords-deleted-body": "Botlösenordet för botnamnet \"$1\" till användaren \"$2\" raderades.", - "botpasswords-newpassword": "Det nya lösenordet att logga in för $1 är $2. Spara detta som framtida referens.", + "botpasswords-newpassword": "Det nya lösenordet att logga in för $1 är $2. Spara detta som framtida referens.
(För äldre botar som kräver att inloggningsnamnet är detsamma som det eventuella användarnamnet kan du även använda $3 som användarnamn och $4 som lösenord.)", "botpasswords-no-provider": "BotPasswordsSessionProvider är inte tillgänglig.", "botpasswords-restriction-failed": "Begränsningar av botlösenord tillåter inte denna inloggning.", "botpasswords-invalid-name": "Det angivna användarnamnet innehåller inte separatorn för botlösenord (\"$1\").", @@ -800,6 +800,8 @@ "invalid-content-data": "Ogiltig innehållsdata", "content-not-allowed-here": "innehåll av \"$1\" är inte tillåtet på sidan [[$2]]", "editwarning-warning": "Om du lämnar den här sidan kommer du att förlora alla ändringar du har gjort.\nOm du är inloggad kan du slå av den här varningen under \"{{int:prefs-editing}}\" i dina inställningar.", + "editpage-invalidcontentmodel-title": "Innehållsmodellen stöds inte", + "editpage-invalidcontentmodel-text": "Innehållsmodellen \"$1\" stöds inte.", "editpage-notsupportedcontentformat-title": "Innehållsformat stöds inte", "editpage-notsupportedcontentformat-text": "Innehållsformatet $1 stöds inte av innehållsmodellen $2.", "content-model-wikitext": "wikitext", @@ -3306,6 +3308,8 @@ "tag-filter": "Filter för [[Special:Tags|märken]]:", "tag-filter-submit": "Filter", "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Märke|Märken}}]]: $2)", + "tag-mw-contentmodelchange": "ändring av innehållsmodell", + "tag-mw-contentmodelchange-description": "Redigeringar som [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel ändrar innehållsmodellen] för en sida", "tags-title": "Märken", "tags-intro": "Denna sida listar de taggar som mjukvaran kan markera en redigering med, och deras betydelse.", "tags-tag": "Märkesnamn", @@ -3317,7 +3321,7 @@ "tags-actions-header": "Handlingar", "tags-active-yes": "Ja", "tags-active-no": "Nej", - "tags-source-extension": "Definieras av ett tillägg", + "tags-source-extension": "Definieras av programvaran", "tags-source-manual": "Används manuellt av användare och robotar", "tags-source-none": "Används inte längre", "tags-edit": "redigera", diff --git a/languages/i18n/ur.json b/languages/i18n/ur.json index 3a16bb583a..85ce74599a 100644 --- a/languages/i18n/ur.json +++ b/languages/i18n/ur.json @@ -240,7 +240,7 @@ "redirectedfrom": "($1 سے پلٹایا گیا)", "redirectpagesub": "لوٹایا گیا صفحہ", "redirectto": "لوٹایا گیا صفحہ:", - "lastmodifiedat": "آخری بار تدوین $2, $1 کو کی گئی۔", + "lastmodifiedat": "اس صفحہ کی تدوین آخری بار $2، مورخہ $1ء کو کی گئی۔", "viewcount": "اِس صفحہ تک {{PLURAL:$1|ایک‌بار|$1 مرتبہ}} رسائی کی گئی", "protectedpage": "محفوظ شدہ صفحہ", "jumpto": ":چھلانگ بطرف", @@ -515,7 +515,7 @@ "cannotchangeemail": "کھاتے کا برقی پتہ اس ویکی سے پر رہتے ہوئے نہیں تبدیل کیا جا سکتا۔", "emaildisabled": "اس سائٹ سے برقی خط نہیں بھیجے جاسکتے", "accountcreated": "تخلیقِ کھاتہ", - "accountcreatedtext": "[[{{ns:صارف}}:$1|$1]] ([[{{ns:تبادلۂ خیال صارف}}:$1|تبادلۂ خیال]]) کا صارف کھاتہ بن چکا ہے۔", + "accountcreatedtext": "[[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|تبادلۂ خیال]]) کا صارف کھاتہ بن چکا ہے۔", "createaccount-title": "کھاتہ سازی برائے {{SITENAME}}", "createaccount-text": "کسی نے {{SITENAME}} ($4) پر \"$2\" کے نام سے اور \"$3\" پارلفظ کے ساتھ آپ کا برقی پتہ استعمال کرتے ہوئے کھاتہ بنایا ہے.\nآپ کو چاہئے کہ ابھی داخلِ نوشتہ ہوکر اپنا پارلفظ تبدیل کردیں.\n\nاگر یہ کھاتہ غلطی سے بنا تھا تو آپ یہ پیغام نظرانداز کرسکتے ہیں.", "login-throttled": "آپ نے حال ہی میں متعدد مرتبہ لاگ ان ہونے کی کوشش کی ہے۔\nدوبارہ کوشش کرنے سے پہلے $1 انتظار فرمائیے۔", @@ -542,6 +542,7 @@ "changepassword-success": "آپ کا پاس ورڈ تبدیل کر دیا گیا!", "changepassword-throttled": "آپ نے حال ہی میں متعدد مرتبہ داخل ہونے کی کوشش کی ہے۔\nدوبارہ کوشش کرنے سے پہلے $1 انتظار کریں۔", "botpasswords": "روبہ پاس ورڈ", + "botpasswords-summary": "روبہ کے پاس ورڈ کے ذریعہ اصل کھاتے کی لاگ ان معلومات کے بغیر اے پی آئی کی مدد سے صارف کھاتے میں رسائی حاصل ہوتی ہے۔\n\nاگر آپ اس سے واقف نہیں ہیں تو بہتر ہوگا کہ آپ اسے نہ چھیڑیں۔ کوئی دوسرا صارف کبھی اس پاس ورڈ کے بنانے اور اسے سپرد کرنے کا آپ سے مطالبہ نہیں کرے گا۔", "botpasswords-disabled": "روبہ کے پاس ورڈ غیر فعال ہیں۔", "botpasswords-no-central-id": "روبہ کے پاس ورڈ کو استعمال کرنے کے لیے آپ کا مرکزی کھاتے میں داخل رہنا ضروری ہے۔", "botpasswords-existing": "روبہ کے موجودہ پاس ورڈ", @@ -600,6 +601,7 @@ "passwordreset-emailerror-capture2": "{{GENDER:$2|صارف}} کو برقی خط بھیجنے میں ناکامی: $1\n{{PLURAL:$3|صارف نام اور پاس ورڈ|صارف ناموں کی فہرست اور ان کے پاس ورڈ}} ذیل میں ملاحظہ فرمائیں۔", "passwordreset-nocaller": "کالر کا فراہم کیا جانا لازمی ہے", "passwordreset-nosuchcaller": "کالر موجود نہیں: $1", + "passwordreset-ignored": "پاس ورڈ کی ترتیب نو مکمل نہیں ہو سکی۔ شاید کوئی پرووائڈر فراہم نہیں کیا گیا تھا؟", "passwordreset-invalideamil": "نادرست برقی ڈاک پتا", "passwordreset-nodata": "کوئی صارف نام اور نہ کوئی برقی ڈاک پتا فراہم کیا گیا", "changeemail": "برقی ڈاک پتا تبدیل یا حذف کریں", @@ -708,7 +710,7 @@ "editpage-cannot-use-custom-model": "اس صفحہ کے مواد کے ماڈل کو تبدیل نہیں کیا جا سکتا۔", "readonlywarning": "انتباہ: انتظامی نگہداشت کی خاطر ڈیٹابیس کو مقفل کر دیا گیا ہے، لہذا اس وقت آپ اپنی ترامیم کو محفوظ نہیں کر سکتے۔\nآپ اپنی تحریر کو کسی ٹیکسٹ فائل میں محفوظ کر سکتے ہیں تاکہ وہ ضائع نہ ہو اور آئندہ اسے استعمال کیا جا سکے۔\n\nانتظامیہ کی جانب سے مقفل کرنے کی حسب ذیل وجہ بیان کی گئی ہے:\n\n$1", "protectedpagewarning": "انتباہ: اس صفحہ میں ترمیم کاری کو مقفل کر دیا گیا ہے اور محض انتظامی اختیارات کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔\nحوالہ کے لیے ذیل میں نوشتہ جاتی اندراج فراہم کیا گیا ہے:", - "semiprotectedpagewarning": "اطلاع: اس صفحہ کو یوں مقفل کیا جاچکا ہے کہ اس میں صرف اندراج شدہ صارفین ہی ترمیم کرسکتے ہیں۔\nحوالہ کے لیے ذیل میں تازہ ترین نوشتہ جاتی اندراج دیا گیا ہے:", + "semiprotectedpagewarning": "اطلاع: اس صفحہ کو محفوظ کر دیا گیا ہے، لہذا اب اس میں محض اندراج شدہ صارفین ہی ترمیم کر سکتے ہیں۔\nحوالہ کے لیے ذیل میں نوشتہ کا تازہ ترین اندراج درج ہے:", "cascadeprotectedwarning": "انتباہ: اس صفحہ میں ترمیم کاری کو مقفل کر دیا گیا ہے اور محض انتظامی اختیارات کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔ اسے مقفل کرنے کی وجہ یہ ہے کہ پیش نظر صفحہ درج ذیل محفوظ {{PLURAL:$1|صفحہ|صفحات}} کی آبشاری حفاظت میں شامل ہے:", "titleprotectedwarning": "انتباہ: اس صفحہ کو محفوظ کر دیا گیا ہے، چنانچہ اسے تخلیق کرنے کے لیے [[Special:ListGroupRights|خصوصی اختیارات]] درکار ہونگے۔\nحوالہ کے لیے ذیل میں نوشتہ کا تازہ ترین اندراج موجود ہے:", "templatesused": "اِس صفحہ پر مستعمل {{PLURAL:$1|سانچہ|سانچے}}:", @@ -723,10 +725,10 @@ "sectioneditnotsupported-text": "اِس صفحہ میں قطعہ کی تدوین حمایت شدہ نہیں ہے.", "permissionserrors": "خطائے اجازت", "permissionserrorstext": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو ایسا کرنے کی اجازت نہیں ہے:", - "permissionserrorstext-withaction": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو $2 کرنے کی اجازت نہیں ہے:", + "permissionserrorstext-withaction": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو $2 کی اجازت نہیں ہے:", "contentmodelediterror": "آپ اس نسخے میں ترمیم نہیں کر سکتے کیونکہ اس کے مواد کا ماڈل ‌‌$1 ہے جو اس صفحہ کے مواد کے موجودہ ماڈل $2 سے مختلف ہے۔", "recreate-moveddeleted-warn": "''' انتباہ: آپ ایک گزشتہ حذف شدہ صفحہ دوبارہ تخلیق کررہے ہیں. '''\n\nآپ کو اِس بات پر غور کرنا چاہئے کہ آیا اِس صفحہ کی تدوین جاری رکھنا موزوں ہے یا نہیں.\nصفحہ کا نوشتۂ حذف شدگی و منتقلی یہاں سہولت کی خاطر مہیّا کیا جارہا ہے:", - "moveddeleted-notice": "یہ ایک حذف شدہ صفحہ ہے.\nصفحہ کا نوشتۂ حذف شدگی و منتقلی ذیل میں بطورِ حوالہ دیا جارہا ہے.", + "moveddeleted-notice": "اس صفحہ کو حذف کر دیا گیا ہے۔\nحوالہ کے لیے ذیل میں اس صفحہ کا نوشتہ حذف شدگی اور نوشتہ منتقلی درج ہے۔", "moveddeleted-notice-recent": "معذرت، اس صفحہ کو حال ہی میں حذف کیا گیا ہے (گزشتہ چوبیس گھنٹوں میں)۔\nحوالہ کے لیے ذیل میں اس صفحہ کا نوشتہ حذف اور نوشتہ منتقلی موجود ہے۔", "log-fulllog": "پورا نوشتہ دیکھئے", "edit-gone-missing": "صفحہ تجدید نہیں کیا جاسکتا.\nلگتا ہے یہ حذف ہوچکا ہے.", @@ -752,6 +754,28 @@ "content-json-empty-array": "خالی ایرے", "deprecated-self-close-category": "صفحات مع نادرست ایچ ٹی ایم ایل ٹیگ", "deprecated-self-close-category-desc": "اس صفحہ میں ایچ ٹی ایم ایل کے نادرست ٹیگ مثلاً <b/> or <span/> استعمال کیے گئے ہیں۔ چونکہ ایچ ٹی ایم ایل 5 میں ان ٹیگوں کا رویہ تبدیل ہو جائے گا، لہذا ویکی متن میں ان کا استعمال متروک ہو چکا ہے۔", + "duplicate-args-category": "سانچے میں دوہرے آرگومنٹ کے حامل صفحات", + "duplicate-args-category-desc": "وہ صفحات جن میں مکرر یا دوہرے آرگومنٹ مستعمل ہیں، مثلاً {{foo|bar=1|bar=2}} یا {{foo|bar|1=baz}}۔", + "expensive-parserfunction-category": "سنگین پارسر فنکشنوں کے بے پناہ استعمال والے صفحات", + "post-expand-template-inclusion-warning": "انتباہ: سانچہ کا حجم بہت زیادہ ہے۔ کچھ سانچے شامل نہیں ہو سکیں گے۔", + "post-expand-template-inclusion-category": "حجم سے متجاوز سانچوں والے صفحات", + "post-expand-template-argument-warning": "انتباہ: اس صفحہ میں موجود سانچہ کے کم از کم کسی ایک پیرامیٹر کا حجم بہت زیادہ ہے۔\nان پیرامیٹروں کو ترک کر دیا گیا ہے۔", + "post-expand-template-argument-category": "سانچہ کے ترک کردہ پیرامیٹروں کے حامل صفحات", + "parser-template-loop-warning": "سانچہ میں تکرار پایا گیا: [[$1]]", + "parser-template-recursion-depth-warning": "سانچہ میں تکرار کی گہرائی اپنی حد سے تجاوز کر گئی ($1)", + "language-converter-depth-warning": "لسانی مبدل کی گہرائی اپنی حد سے تجاوز کر گئی ($1)", + "node-count-exceeded-category": "گرہوں کی تعداد سے تجاوز کرنے والے صفحات", + "node-count-exceeded-category-desc": "اس صفحہ میں گرہیں اپنی مقررہ تعداد سے تجاوز کر گئیں۔", + "node-count-exceeded-warning": "صفحہ کی گرہ اپنی تعداد سے تجاوز کر گئی", + "expansion-depth-exceeded-category": "توسیع کی گہرائی سے تجاوز کرنے والے صفحات", + "expansion-depth-exceeded-category-desc": "اس صفحہ میں توسیع کی گہرائی اپنی حد سے تجاوز کر گئی۔", + "expansion-depth-exceeded-warning": "صفحہ میں توسیع کی گہرائی اپنی حد سے تجاوز کر گئی", + "parser-unstrip-loop-warning": "unstrip فنکشن میں تکرار پایا گیا", + "parser-unstrip-recursion-limit": "unstrip فنکشن میں تکرار اپنی حد سے تجاوز کر گیا ($1)", + "converter-manual-rule-error": "زبان کی دستی تبدیلی کے ضوابط میں نقص دریافت ہوا", + "undo-success": "اس ترمیم کو واپس پھیرا جا سکتا ہے۔\nبراہ کرم ذیل میں موجود موازنہ ملاحظہ فرمائیں اور یقین کر لیں کہ اس موازنے میں موجود فرق ہی آپ کا مقصود ہے۔ اس کے بعد تبدیلیوں کو محفوظ کر دیں، ترمیم واپس پھیر دی جائے گی۔", + "undo-failure": "درمیان میں متنازع ترامیم کی موجودگی کی بنا پر اس ترمیم کو واپس نہیں پھیرا جا سکا۔", + "undo-norev": "اس ترمیم کو واپس نہیں پھیرا جا سکا کیونکہ یہ موجود ہی نہیں یا حذف کر دی گئی ہے۔", "undo-nochange": "معلوم ہوتا ہے کہ اس ترمیم کو پہلے ہی واپس پھیر دیا گیا ہے۔", "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|تبادلہ خیال]]) کی جانب سے کی گئی ترمیم $1 رد کردی گئی ہے۔", "undo-summary-username-hidden": "پوشیدہ صارف کے نسخہ $1 کو واپس پھیریں", @@ -805,6 +829,7 @@ "revdelete-nooldid-title": "ناقص مقصود نظرثانی", "revdelete-nooldid-text": "اس فنکشن کو جس نسخے پر انجام دینا ہے اسے آپ نے منتخب نہیں کیا، یا منتخب کردہ نسخہ موجود نہیں، یا آپ موجودہ نسخہ کو پوشیدہ کرنے کی کوشش کر رہے ہیں۔", "revdelete-no-file": "درج کردہ فائل موجود نہیں ہے۔", + "revdelete-show-file-confirm": "کیا آپ واقعی فائل «$1» کے مورخہ $2 بوقت $3 بجے حذف ہونے والے نسخے کو دیکھنا چاہتے ہیں؟", "revdelete-show-file-submit": "ہاں", "revdelete-selected-text": "[[:$2]] {{PLURAL:$1|کا منتخب نسخہ|کے منتخب نسخے}}:", "revdelete-selected-file": "[[:$2]] {{PLURAL:$1|کا منتخب فائل نسخہ|کے منتخب فائل نسخے}}:", @@ -827,25 +852,42 @@ "revdelete-suppress": "منتظمین اور دیگر صارفین سے معلومات کو پوشیدہ کریں", "revdelete-unsuppress": "بحال شدہ نظرثانیوں پر پابندیاں ہٹاؤ", "revdelete-log": "وجہ", - "revdelete-success": "'''رؤیتِ نظرثانی کی تجدید کامیابی سے ہوئی.'''", - "logdelete-success": "'''نوشتۂ رویت کامیابی سے مرتب.'''", + "revdelete-submit": "منتخب {{PLURAL:$1|نسخے|نسخوں}} پر منطبق کریں", + "revdelete-success": "نسخہ کی مرئیت کی تجدید مکمل۔", + "revdelete-failure": "نسخہ کی مرئیت کی تجدید نہیں ہو سکی:\n$1", + "logdelete-success": "نوشتہ مرئیت میں تبدیلی مکمل۔", "logdelete-failure": "'''نوشتۂ رویت مرتب نہیں کیا جاسکتا:'''\n\n$1", "revdel-restore": "ظاہریت تبدیل کرو", "pagehist": "تاریخچۂ صفحہ", "deletedhist": "حذف شدہ تاریخچہ", + "revdelete-hide-current": "مورخہ $2، بوقت $1 بجے والے آئٹم کو پوشیدہ کرنے کے دوران میں نقص: یہ موجودہ نسخہ ہے۔ اسے پوشیدہ نہیں کیا جا سکتا۔", + "revdelete-show-no-access": "مورخہ $2، بوقت $1 بجے والا آئٹم دکھانے کے دوران میں نقص: اس نسخہ کو بطور «محدود» نشان زد کر دیا گیا ہے۔ چنانچہ اب یہ آپ کی دسترس سے باہر ہے۔", + "revdelete-modify-no-access": "مورخہ $2، بوقت $1 بجے والے آئٹم میں تبدیلی کے دوران میں نقص: اس نسخہ کو بطور «محدود» نشان زد کر دیا گیا ہے۔ چنانچہ اب یہ آپ کی دسترس سے باہر ہے۔", + "revdelete-modify-missing": "آئٹم آئی ڈی $1 میں تبدیلی کے دوران میں نقص: یہ نسخہ ڈیٹابیس میں موجود نہیں ہے!", + "revdelete-no-change": "انتباہ: مورخہ $2، بوقت $1 بجے والے آئٹم میں پہلے ہی سے مرئیت کی مطلوبہ ترتیبات موجود ہیں۔", + "revdelete-concurrent-change": "مورخہ $2، بوقت $1 بجے والے آئٹم میں تبدیلی کے دوران میں نقص: ایسا معلوم ہوتا ہے کہ آپ کی جانب سے تبدیلی کی کوشش کے دوران میں کسی اور نے اس میں تبدیلی کر دی ہے۔\nبراہ کرم نوشتے دیکھ لیں۔", + "revdelete-only-restricted": "مورخہ $2، بوقت $1 بجے والے آئٹم کو پوشیدہ کرنے کے دوران میں نقص: مرئیت کے دیگر اختیارات میں سے مزید کسی ایک اختیار کو منتخب کیے بغیر آپ ان آئٹموں کو منتظمین کی نگاہوں سے مخفی نہیں کر سکتے۔", "revdelete-reason-dropdown": "* عمومی وجوہات حذف شدگی\n** کاپی رائٹ کی خلاف ورزی\n** نامناسب تبصرہ یا ذاتی معلومات\n** نامناسب صارف نام\n** ممکنہ طور پر افترا آمیر معلومات", "revdelete-otherreason": "دوسری/اضافی وجہ:", "revdelete-reasonotherlist": "کوئی اَور وجہ", "revdelete-edit-reasonlist": "تحذیفی وجوہات کی تدوین", "revdelete-offender": "نظرثانی مصنف:", "suppressionlog": "نوشتہ پوشیدگی", + "suppressionlogtext": "ذیل میں ان حذف شدگیوں اور پابندیوں کی فہرست ہے جن میں منتظمین سے پوشیدہ رکھا گیا مواد موجود ہے۔\nموجودہ جاری پابندیوں اور معطل صارفین کی فہرست دیکھنے کے لیے [[Special:BlockList|فہرست پابندی]] ملاحظہ فرمائیں۔", "mergehistory": "تواریخِ صفحہ کا انضمام", + "mergehistory-header": "اس صفحہ کے ذریعہ آپ ماخذ صفحہ کے تاریخچہ کے نسخوں کو نئے صفحہ میں ضم کر سکتے ہیں۔\nالبتہ اس بات کا یقین کر لیں کہ اس تبدیلی کے بعد بھی تاریخچہ کا تسلسل حسب سابق برقرار رہے گا۔", "mergehistory-box": "دو صفحات کی نظرثانیوں کا انضمام:", "mergehistory-from": "مآخذ صفحہ:", "mergehistory-into": "صفحۂ مقصود:", + "mergehistory-list": "قابل ضم تاریخچہ", "mergehistory-go": "ضم پذیر ترامیم دِکھاؤ", "mergehistory-submit": "نظرثانیاں ضم کرو", "mergehistory-empty": "نظرثانیاں ضم نہیں کی جاسکتیں.", + "mergehistory-fail-bad-timestamp": "وقت کی مہر نادرست ہے۔", + "mergehistory-fail-invalid-source": "ماخذ درست نہیں۔", + "mergehistory-fail-invalid-dest": "مقصود صفحہ درست نہیں۔", + "mergehistory-fail-permission": "ناکافی اختیارات برائے ضم تاریخچہ۔", + "mergehistory-fail-self-merge": "ماخذ و مقصود صفحات یکساں ہیں۔", "mergehistory-no-source": "مآخذ صفحہ $1 موجود نہیں.", "mergehistory-no-destination": "مقصود صفحہ $1 موجود نہیں.", "mergehistory-invalid-source": "مآخذ صفحہ کا عنوان صحیح ہونا چاہئے.", @@ -858,9 +900,11 @@ "revertmerge": "غیر ضم", "history-title": "\"$1\" کا نظرثانی تاریخچہ", "difference-title": "\"$1\" کے نسخوں کے درمیان فرق", + "difference-title-multipage": "«$1» اور «$2» صفحوں کے درمیان فرق", "difference-multipage": "(فرق مابین صفحات)", "lineno": "لکیر $1:", "compareselectedversions": "منتخب متـن کا موازنہ", + "showhideselectedversions": "منتخب نسخوں کی مرئیت تبدیل کریں", "editundo": "رد ترمیم", "diff-empty": "(کوئی فرق نہیں)", "diff-multi-sameuser": "({{PLURAL: $1 | ایک متوسط نظرثانی | $1 کئی متوسط نظرثانیاں}}ایک ہی صارف کی جانب سے نہیں دکھائی گئی)", @@ -891,6 +935,7 @@ "search-section": "(حصہ $1)", "search-category": "(زمرہ $1)", "search-suggest": "کیا آپ کا مطلب تھا: $1", + "search-rewritten": "$1 کے نتائج کی نمائش، اس کی بجائے آپ $2 کو تلاش کر سکتے ہیں۔", "search-interwiki-caption": "ساتھی منصوبے", "search-interwiki-default": "$1 نتائج:", "search-interwiki-more": "(مزید)", @@ -898,19 +943,24 @@ "searchrelated": "متعلقہ", "searchall": "تمام", "search-nonefound": "استفسار کے مطابق نتائج نہیں ملے.", + "search-nonefound-thiswiki": "اس سائٹ پر استفسار کے مطابق کوئی نتیجہ برآمد نہیں ہوا۔", "powersearch-legend": "پیشرفتہ تلاش", "powersearch-ns": "جائے نام میں تلاش:", "powersearch-togglelabel": "جانچ", "powersearch-toggleall": "تمام", "powersearch-togglenone": "کوئی نہیں", + "powersearch-remember": "اس انتخاب کو مستقبل کی تلاشوں کے لیے یاد رکھیں", "search-external": "بیرونی تلاش", "searchdisabled": "{{SITENAME}} تلاش غیرفعال.\nآپ فی الحال گوگل کے ذریعے تلاش کرسکتے ہیں.\nیاد رکھئے کہ اُن کے {{SITENAME}} اشاریے ممکناً پرانے ہوسکتے ہیں.", + "search-error": "تلاش کے دوران میں کوئی نقص واقع ہوا: $1", "preferences": "ترجیحات", "mypreferences": "ترجیحات", "prefs-edits": "تعداد ترامیم:", + "prefsnologintext2": "اپنی ترجیحات میں تبدیلی کے لیے براہ کرم لاگ ان کریں", "prefs-skin": "جِلد", "skin-preview": "پیش منظر", "datedefault": "کوئی ترجیح نہیں", + "prefs-labs": "تجرباتی خصوصیتیں", "prefs-user-pages": "صارف صفحات", "prefs-personal": "پروفائل", "prefs-rc": "حالیہ تبدیلیاں", @@ -1012,20 +1062,27 @@ "prefs-tokenwatchlist": "ٹوکن", "prefs-diffs": "فرق", "prefs-help-prefershttps": "یہ ترجیح آپ کے اگلے لاگ ان پر اثر انداز ہوگی۔", + "prefswarning-warning": "ترجیحات میں آپ کی جانب سے کی جانے والی تبدیلیاں ابھی محفوظ نہیں ہوئی ہیں۔\nاگر آپ «$1» پر کلک کیے بغیر اس صفحہ کو چھوڑ دیں تو آپ کی تبدیلیاں محفوظ نہیں ہوگی۔", + "prefs-tabs-navigation-hint": "نکتہ: مختلف خانوں میں جانے کے لیے آپ دائیں اور بائیں کی جہت نما کلیدیں استعمال کر سکتے ہیں۔", "userrights": "حقوقِ صارف کی نظامت", "userrights-lookup-user": "گروہائے صارف کا انتظام", "userrights-user-editname": "کوئی اسم‌صارف داخل کیجئے:", - "editusergroup": "ترمیم گروہائے صارف", - "editinguser": "تبدیلی اختیارات صارف برائے {{GENDER:$1|صارف}} [[صارف:$1|$1]] $2", + "editusergroup": "{{GENDER:$1|صارف}} کے گروہوں میں ترمیم کریں", + "editinguser": "{{GENDER:$1|صارف}} [[صارف:$1|$1]] $2 کے اختیارات میں تبدیلی", "userrights-editusergroup": "ترمیم گروہائے صارف", - "saveusergroups": "گروہائے صارف محفوظ", + "saveusergroups": "{{GENDER:$1|صارف}} کے گروہوں کو محفوظ کریں", "userrights-groupsmember": "رکنِ:", "userrights-groupsmember-auto": "اعتباری صارف در", "userrights-groups-help": "آپ ان گروہان میں تبدیلی کرسکتے ہیں جن سے صارف متعلق ہے: \n* نشان زد خانہ کا مطلب یہ ہے کہ صارف کا تعلق اس گروہ سے ہے۔ \n* غیر نشان زد خانہ کا مطلب یہ ہے کہ صارف کا تعلق اس گروہ سے نہیں ہے۔ \n* یہ * علامت اس بات کا اشارہ ہے کہ آپ اس گروہ کو نہیں ہٹا سکتے جسے ایک مرتبہ آپ نے شامل کردیا ہو، یا اس کے بر عکس۔", "userrights-reason": "وجہ:", "userrights-no-interwiki": "دوسرے ویکیوں پر حقوقِ صارف میں ترمیم کی آپ کو اجازت نہیں ہے.", + "userrights-nodatabase": "ڈیٹابیس $1 موجود نہیں یا مقامی نہیں۔", + "userrights-nologin": "اختیارات تفویض کرنے کے لیے آپ کا کسی منتظم کھاتے سے [[Special:UserLogin|داخل ہونا]] ضروری ہے۔", + "userrights-notallowed": "آپ کو اختیارات تفویض کرنے یا انہیں واپس لینے کی اجازت نہیں ہے۔", "userrights-changeable-col": "مجموعات جو آپ تبدیل کرسکتے ہیں", "userrights-unchangeable-col": "مجموعات جو آپ تبدیل نہیں کرسکتے", + "userrights-conflict": "اختیارات کی تبدیلی میں تنازعہ! براہ کرم نظر ثانی کریں اور اپنی تبدیلیوں کی تصدیق کریں۔", + "userrights-removed-self": "آپ نے اپنے اختیارات ختم کر لیے ہیں، چنانچہ اب یہ صفحہ آپ کی دسترس سے باہر ہو گیا ہے۔", "group": "گروہ:", "group-user": "صارفین", "group-autoconfirmed": "خود توثیق شدہ صارفین", @@ -1045,6 +1102,7 @@ "grouppage-bot": "{{ns:project}}:روبہ جات", "grouppage-sysop": "{{ns:project}}:منتظمین", "grouppage-bureaucrat": "{{ns:project}}:مامورین اداری", + "grouppage-suppress": "{{ns:project}}:پوشیدگی", "right-read": "مطالعہ صفحات", "right-edit": "ترمیم صفحات", "right-createpage": "تخلیق صفحات (تبادلہ خیال صفحات نہیں)", @@ -1054,20 +1112,154 @@ "right-minoredit": "ترامیم کی بطور معمولی ترمیم نشان زدگی", "right-move": "منتقلی صفحات", "right-move-subpages": "منتقلی صفحات مع ذیلی صفحات", - "right-upload": "ملفات زبراثقال (اپ لوڈ) کریں", + "right-move-rootuserpages": "منتقلی صارف صفحات", + "right-move-categorypages": "منتقلی زمرہ صفحات", + "right-movefile": "منتقلی فائل", + "right-suppressredirect": "پرانے عنوان سے رجوع مکرر کے بغیر منتقلی صفحہ", + "right-upload": "فائلوں کو اپلوڈ کرنا", + "right-reupload": "موجود فائلوں کا دوبارہ اپلوڈ", + "right-reupload-own": "ذاتی اپلوڈ کردہ فائلوں کا دوبارہ اپلوڈ", + "right-reupload-shared": "مقامی طور پر مشترکہ میڈیا کے ذخیرے میں فائلوں کی منسوخی", + "right-upload_by_url": "بذریعہ یوآرایل فائل اپلوڈ", + "right-purge": "بدون تصدیق صفحہ کے کیشے کی صفائی", + "right-autoconfirmed": "آئی پی پر مبنی پابندیوں سے غیر متاثر", + "right-bot": "خودکار عمل کے طور پر تعامل", + "right-nominornewtalk": "تبادلۂ خیال صفحات میں معمولی ترامیم کرنے پر نئے پیغام کے اعلان کی عدم نمائش", + "right-apihighlimits": "API کا بڑے پیمانے پر استعمال", "right-writeapi": "اے پی آئی لکھائی کا استعمال", "right-delete": "صفحات حذف کریں", + "right-bigdelete": "بڑے تاریخچوں پر مشتمل صفحات کی حذف شدگی", + "right-deletelogentry": "نوشتہ کے مخصوص اندراجات کی حذف شدگی و بحالی", + "right-deleterevision": "صفحات کے مخصوص نسخوں کی حذف شدگی و بحالی", + "right-deletedhistory": "منسلکہ متن کے بغیر تاریخچہ کے حذف شدہ اندراجات کا مشاہدہ", + "right-deletedtext": "حذف شدہ متن اور حذف شدہ نسخوں کے درمیان میں تبدیلیوں کا مشاہدہ", + "right-browsearchive": "حذف شدہ صفحات میں تلاش", + "right-undelete": "بحالی صفحہ", + "right-suppressrevision": "صفحات کے مخصوص نسخوں کا مشاہدہ و پوشیدگی", + "right-viewsuppressed": "پوشیدہ نسخوں کا مشاہدہ", + "right-suppressionlog": "نجی نوشتوں کا مشاہدہ", + "right-block": "صارفین کی ترمیم کاری پر پابندی کا نفاذ", + "right-blockemail": "برقی خط بھیجنے پر پابندی کا نفاذ", + "right-hideuser": "عمومی نگاہ سے مخفی رکھتے ہوئے صارف نام پر پابندی کا نفاذ", + "right-ipblock-exempt": "آئی پی، خودکار اور رینج پر پابندیوں سے خلاصی", + "right-unblockself": "رفع پابندی", + "right-protect": "آبشاری حفاظت کے حامل صفحات میں ترمیم اور درجات حفاظت میں تبدیلی", + "right-editprotected": "\"{{int:protect-level-sysop}}\" کے طور پر محفوظ صفحات میں ترمیم", + "right-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" کے طور پر محفوظ صفحات میں ترمیم", + "right-editcontentmodel": "صفحہ کے مواد کے ماڈل میں ترمیم", + "right-editinterface": "صارف انٹرفیس میں ترمیم", + "right-editusercssjs": "دیگر صارفین کی سی ایس ایس اور جاوا اسکرپٹ فائلوں میں ترمیم", + "right-editusercss": "دیگر صارفین کی سی ایس ایس فائلوں میں ترمیم", + "right-edituserjs": "دیگر صارفین کی جاوا اسکرپٹ فائلوں میں ترمیم", + "right-editmyusercss": "اپنی ذاتی سی ایس ایس فائلوں میں ترمیم", + "right-editmyuserjs": "اپنی ذاتی جاوا اسکرپٹ فائلوں میں ترمیم", + "right-viewmywatchlist": "اپنی ذاتی زیرنظر فہرست کا مشاہدہ", + "right-editmywatchlist": "اپنی ذاتی زیرنظر فہرست میں ترمیم۔ خیال رکھیں کہ اس اختیار کے بغیر بھی بعض اقدامات کے ذریعہ صفحات شامل کیے جا سکتے ہیں۔", + "right-viewmyprivateinfo": "اپنی ذاتی نجی معلومات کا مشاہدہ (مثلاً برقی ڈاک پتہ، حقیقی نام وغیرہ)", + "right-editmyprivateinfo": "اپنی ذاتی نجی معلومات میں ترمیم (مثلاً برقی ڈاک پتہ، حقیقی نام وغیرہ)", + "right-editmyoptions": "اپنی ذاتی ترجیحات میں ترمیم", + "right-rollback": "کسی مخصوص صفحہ پر ترمیم کرنے والے آخری صارف کی ترامیم کا فوری استرجع", + "right-markbotedits": "استرجع شدہ ترامیم کی روبہ ترامیم کے طور پر نشان زدگی", + "right-noratelimit": "وقت کی پابندیوں سے آزادی", + "right-import": "دوسری ویکیوں سے صفحات کی درآمد", + "right-importupload": "بذریعہ اپلوڈ صفحات کی درآمد", + "right-patrol": "دیگر صارفین کی ترامیم کی مراجعت", + "right-autopatrol": "ذاتی ترامیم کی خودکار مراجعت", + "right-patrolmarks": "حالیہ تبدیلیوں میں علامات مراجعت کا مشاہدہ", + "right-unwatchedpages": "نادیدہ صفحات کی فہرست کا مشاہدہ", + "right-mergehistory": "صفحات کے تاریخچے کا انضمام", + "right-userrights": "تمام اختیارات میں ترمیم", + "right-userrights-interwiki": "دوسری ویکیوں پر صارف کے اختیارات میں ترمیم", + "right-siteadmin": "ڈیٹابیس کو مقفل یا غیر مقفل کرنا", + "right-override-export-depth": "پانچویں سطح کی گہرائی تک مربوط صفحات پر مشتمل صفحات کی برآمد", "right-sendemail": "دیگر صارفین کو برقی ڈاک بھیجیں", + "right-passwordreset": "پاس ورڈ کی ترتیب نو کے حامل برقی خطوط کا مشاہدہ", + "right-managechangetags": "[[Special:Tags|ٹیگوں]] کی تخلیق اور (غیر)فعالی", + "right-applychangetags": "کسی کی تبدیلیوں کے ساتھ [[Special:Tags|ٹیگوں]] کا اطلاق", + "right-changetags": "انفرادی نسخوں اور نوشتہ کے اندراج پر [[Special:Tags|ٹیگوں]] کا حذف و اضافہ", + "right-deletechangetags": "ڈیٹابیس سے [[Special:Tags|ٹیگوں]] کی حذف شدگی", + "grant-generic": "\"$1\" مجموعہ اختیارات", + "grant-group-page-interaction": "صفحات سے تعامل", + "grant-group-file-interaction": "میڈیا سے تعامل", + "grant-group-watchlist-interaction": "اپنی زیرنظر فہرست سے تعامل", + "grant-group-email": "برقی خط کی ترسیل", + "grant-group-high-volume": "بڑے پیمانے کی سرگرمی کی انجام دہی", + "grant-group-customization": "شخصی سازی اور ترجیحات", + "grant-group-administration": "انتظامی امور کی انجام دہی", + "grant-group-private-information": "اپنے متعلق نجی معلومات تک رسائی", + "grant-group-other": "متفرق سرگرمیاں", + "grant-blockusers": "پابندی و رفع پابندی", + "grant-createaccount": "کھاتہ سازی", + "grant-createeditmovepage": "تخلیق، ترمیم و منتقلی صفحات", + "grant-delete": "صفحات، اندراجات نوشتہ اور نسخوں کی حذف شدگی", + "grant-editinterface": "صارف کی سی ایس ایس/جاوا اسکرپٹ اور میڈیاویکی نام فضا میں ترمیم", + "grant-editmycssjs": "اپنی سی ایس ایس/جاوا اسکرپٹ میں ترمیم", + "grant-editmyoptions": "اپنی ترجیحات میں ترمیم", + "grant-editmywatchlist": "اپنی زیرنظر فہرست میں ترمیم", + "grant-editpage": "موجودہ صفحات میں ترمیم", + "grant-editprotected": "محفوظ صفحات میں ترمیم", + "grant-basic": "بنیادی اختیارات", + "grant-viewdeleted": "حذف شدہ فائلوں اور صفحات کا مشاہدہ", + "grant-viewmywatchlist": "اپنی زیرنظر فہرست کا مشاہدہ", "newuserlogpage": "نوشتۂ آمد صارف", "newuserlogpagetext": "یہ نۓ صارفوں کی آمد کا نوشتہ ہے", "rightslog": "نوشتہ صارفی اختیارات", "rightslogtext": "یہ صارفی اختیارات میں تبدیلیوں کا نوشتہ ہے۔", + "action-read": "اس صفحہ کو پڑھنے", "action-edit": "اس صفحہ میں ترمیم کریں", + "action-createpage": "اس صفحہ کو تخلیق کرنے", + "action-createtalk": "اس تبادلۂ خیال صفحہ کو تخلیق کرنے", + "action-createaccount": "اس کھاتے کو بنانے", + "action-autocreateaccount": "اس بیرونی کھاتے کو خودکار طور پر بنانے", + "action-history": "اس صفحہ کا تاریخچہ دیکھنے", + "action-minoredit": "اس ترمیم کو معمولی نشان زد کرنے", + "action-move": "اس صفحہ کو منتقل کرنے", + "action-move-subpages": "اس صفحہ اور اس کے ذیلی صفحات کو منتقل کرنے", + "action-move-rootuserpages": "اصل صارف صفحات کو منتقل کرنے", + "action-move-categorypages": "زمرے کے صفحات کو منتقل کرنے", + "action-movefile": "اس فائل کو منتقل کرنے", + "action-upload": "اس فائل کو اپلوڈ کرنے", + "action-reupload": "اس موجودہ فائل کو دوبارہ اپلوڈ کرنے", + "action-reupload-shared": "مشترکہ ذخیرے میں فائل کو منسوخ کرنے", + "action-upload_by_url": "بذریعہ یوآرایل اس فائل کو اپلوڈ کرنے", + "action-writeapi": "API تحریر کرنے", + "action-delete": "یہ صفحہ حذف کرنے", + "action-deleterevision": "یہ نسخہ حذف کرنے", + "action-deletedhistory": "اس صفحہ کا حذف شدہ تاریخچہ دیکھنے", + "action-browsearchive": "حذف شدہ صفحات میں تلاش کرنے", + "action-undelete": "اس صفحہ کو بحال کرنے", + "action-suppressrevision": "اس پوشیدہ ترمیم کی نظرثانی اور بحال کرنے", + "action-suppressionlog": "نجی نوشتہ کے دیکھنے", + "action-block": "اس صارف پر پابندی لگانے", + "action-protect": "اس صفحہ کے درجات حفاظت میں تبدیلی کرنے", + "action-rollback": "آخری صارف جس نے ایک متعین صفحہ میں ترمیم کی ہے، اس کی ترامیم کا فوری استرجع کرنے", + "action-import": "دوسری ویکی سے صفحات درآمد کرنے", + "action-importupload": "بذریعہ اپلوڈ صفحات درآمد کرنے", + "action-patrol": "دیگر صارفین کی ترامیم کو بطور مراجعت شدہ نشان زد کرنے", + "action-autopatrol": "اپنی ترمیم کو بطور مراجعت شدہ نشان زد کرنے", + "action-unwatchedpages": "نادیدہ صفحات کی فہرست دیکھنے", + "action-mergehistory": "اس صفحہ کے تاریخچہ کو ضم کرنے", + "action-userrights": "تمام اختیارات میں تبدیلی کرنے", + "action-userrights-interwiki": "دوسری ویکیوں پر صارف کے اختیارات میں ترمیم کرنے", + "action-siteadmin": "ڈیٹابیس کو مقفل کرنے یا کھولنے", + "action-sendemail": "برقی خطوط روانہ کرنے", + "action-editmywatchlist": "اپنی زیرنظر فہرست میں ترمیم کرنے", + "action-viewmywatchlist": "اپنی زیر نظر فہرست دیکھنے", + "action-viewmyprivateinfo": "اپنی نجی معلومات دیکھنے", + "action-editmyprivateinfo": "اپنی نجی معلومات میں ترمیم کرنے", + "action-editcontentmodel": "صفحہ کے مواد کے ماڈل میں ترمیم کرنے", + "action-managechangetags": "ٹیگوں کو بنانے اور انہیں غیر فعال کرنے", + "action-applychangetags": "اپنی تبدیلیوں پر ٹیگ گاری کرنے", + "action-changetags": "انفرادی نسخوں اور نوشتہ کے اندراج پر ٹیگوں کو لگانے اور ہٹانے", + "action-deletechangetags": "ڈیٹابیس سے ٹیگوں کو حذف کرنے", + "action-purge": "اس صفحہ کا کیشے خالی کرنے", "nchanges": "$1 {{PLURAL:$1|تبدیلی|تبدیلیاں}}", + "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|آخری آمد کے بعد سے}}", "enhancedrc-history": "تاریخچہ", "recentchanges": "حالیہ تبدیلیاں", "recentchanges-legend": "اِختیاراتِ حالیہ تبدیلیاں", "recentchanges-summary": "اس صفحے پر ویکی میں ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کیجیۓ۔", + "recentchanges-noresult": "مقررہ مدت کے دوران میں اس معیار سے مشابہت رکھنے والی کوئی تبدیلی نہیں ہوئی۔", "recentchanges-feed-description": "اس خورد میں ویکی پر ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کیجیۓ۔", "recentchanges-label-newpage": "یہ ترمیم ایک نئے صفحے کی تخلیق ہے", "recentchanges-label-minor": "یہ ایک معمولی ترمیم ہے", @@ -1108,41 +1300,92 @@ "minoreditletter": "م", "newpageletter": "نیا ..", "boteditletter": " خودکار", + "number_of_watching_users_pageview": "[$1 مشاہد {{PLURAL:$1|صارف|صارفین}}]", + "rc_categories": "ان زمروں تک محدود رکھیں («|» سے علاحدہ کریں):", "rc_categories_any": "کوئی بھی منتخب", - "rc-change-size-new": "$1 {{PLURAL:$1|بائٹ|بائٹ}} تبدیلی کے بعد", + "rc-change-size-new": "تبدیلی کے بعد $1 {{PLURAL:$1|بائٹ}}", + "newsectionsummary": "/* $1 */ نیا قطعہ", "rc-enhanced-expand": "تفصیلات دکھائیں", "rc-enhanced-hide": "تفصیلات چھپائیے", + "rc-old-title": "اصلاً «$1» کے عنوان سے تخلیق شدہ", "recentchangeslinked": "متعلقہ تبدیلیاں", "recentchangeslinked-feed": "متعلقہ تبدیلیاں", "recentchangeslinked-toolbox": "متعلقہ تبدیلیاں", "recentchangeslinked-title": "\"$1\" سے متعلقہ تبدیلیاں", "recentchangeslinked-summary": "یہ ان تبدیلیوں کی فہرست ہے جو حال ہی میں کسی مخصوص صفحہ سے مربوط صفحات (یا مخصوص زمرہ کے اراکین) میں کی گئی ہیں\n\n[[Special:Watchlist|آپ کی زیر نظر فہرست]] میں یہ صفحات متجل (bold) نظر آئیں گےـ", "recentchangeslinked-page": "صفحۂ منصوبہ دیکھئے", + "recentchangeslinked-to": "اس کی بجائے درج کردہ صفحہ سے مربوط صفحات کی تبدیلیاں دکھائیں", "recentchanges-page-added-to-category": "[[:$1]] کو زمرہ میں شامل کیا گیا", - "recentchanges-page-added-to-category-bundled": "[[:$1]] اور {{PLURAL:$2|ایک صفحہ|$2 صفحات}} زمرہ میں شامل {{PLURAL:$2|کیا گیا|$2 کیے گئے}}", + "recentchanges-page-added-to-category-bundled": "[[:$1]] کو زمرہ میں شامل کر دیا گیا، [[Special:WhatLinksHere/$1|یہ صفحہ دیگر صفحات میں بھی موجود ہے]]", "recentchanges-page-removed-from-category": "[[:$1]] کو زمرہ سے ہٹایا", + "recentchanges-page-removed-from-category-bundled": "[[:$1]] زمرے سے ہٹا دیا گیا ہے، [[Special:WhatLinksHere/$1|یہ صفحہ دیگر صفحات میں بھی موجود ہے]]", "autochange-username": "میڈیاویکی خودکار تبدیلیاں", "upload": "اپلوڈ", "uploadbtn": "زبراثقال ملف (اپ لوڈ فائل)", "reuploaddesc": "زبراثقال ورقہ (فارم) کیجانب واپس۔", + "upload-tryagain": "فائل کی تبدیل شدہ وضاحت روانہ کریں", "uploadnologin": "آپ داخل شدہ حالت میں نہیں", "uploadnologintext": "فائلیں اپلوڈ کرنے کے لیے براہ کرم $1 ہوں", - "uploadtext": "\n'''اطلاع''': اگر آپ اپنی فائل اپلوڈ کرتے وقت خلاصہ کے خانے میں درج ذیل دو باتوں کی وضاحت نہیں کریں گے تو اس فائل کو حذف کیا جاسکتا ہے:\n# فائل کا '''مـاخـذ''' ، یعنی:\n#*اگر یہ آپ نے خود تخلیق کی ہے تو اسے بیان کریں۔\n#*اگر یہ آن لائن دستیاب ہے تو اس سائٹ کا '''ربط''' درج کریں۔\n#*اگر آپ نے اسے کسی دوسری زبان کے {{SITENAME}} سے لیا ہے تو اسکا نام تحریر کریں۔\n#صاحب حق طبع و نشر اور فائل کے اجازت نامہ کے بارے میں:\n#* فائل کے اجازت نامہ کے متعلق یہ درج کریں کہ اس کی موجودہ حیثیت کیا ہے۔\n#*اگر آپ خود اسکا حق طبع و نشر رکھتے ہیں تو آپ پر لازم ہے کہ آپ اسے [[دائرۂ عام]] (پبلک ڈومین) میں بھی شائع کریں۔\n\nجب کوئی صارف مستقل ایسی فائل اپلوڈ کرتا رہے جس کے اجازت نامہ کے بارے میں غلط بیانی کی گئی ہو یا وہ مستقل ایسی تصاویر اپلوڈ کرے جن کے بارے میں کوئی وضاحت موجود نہ ہو تو ایسی صورت میں اس صارف پر پابندی لگائے جانے کا قوی امکان موجود ہے۔\n\nفائل اپلوڈ کرنے کے لیے ذیل میں موجود فارم استعمال کریں، اگر آپ جملہ اپلوڈ کردہ تصاویر کو دیکھنا یا تلاش کرنا چاہتے ہیں تو [[Special:FileList|اس فہرست]] کو ملاحظہ فرمائیں۔
تمام اپلوڈ کردہ و حذف شدہ تصاویر کو [[Special:Log/upload|نوشتۂ منتقلی]] میں درج کر لیا جاتا ہے۔\n\nتصویر کی منتقلی کے بعد، اسکو کسی صفحہ پر رکھنے کیلیے مندرجہ ذیل طریقہ سے استعمال کریں۔\n\n'''[[تصویر:فائل کا نام|متبادل متن]]'''\n\n* مندرجہ بالا رموز آپ انگریزی میں بھی درج کرسکتے ہیں، یعنی\n[[Image:File name|Alt.text]]\n* فائل کا ربط درج کرنے کے لیے۔ '''[[{{ns:media}}:File.ogg]]'''\n* ملف کا نام؛ حرف ابجد کے لیے حساس ہے لہذا اگر اپلوڈ کرتے وقت فائل کا نام -- name:JPG ہے اور آپ name:jpg یــا Name:jpg کا ربط درج کرتے ہیں تو ربط کام نہیں کرے گا۔", + "upload_directory_missing": "اپلوڈ فولڈر ($1) موجود نہیں اور ویب سرور کے ذریعہ اسے تخلیق نہیں کیا جا سکا۔", + "upload_directory_read_only": "اپلوڈ فولڈر ($1) میں ویب سرور لکھ نہیں پا رہا ہے۔", + "uploaderror": "اپلوڈ کے دوران میں نقص", + "upload-recreate-warning": "انتباہ: اس نام کی فائل حذف یا منتقل کر دی گئی ہے۔\n\nآسانی کے لیے ذیل میں اس صفحہ کا نوشتہ منتقلی و حذف شدگی درج ہے:", + "uploadtext": "فائلیں اپلوڈ کرنے کے لیے درج ذیل فارم پُر کریں۔\n\n'''اطلاع''': اگر آپ اپنی فائل اپلوڈ کرتے وقت خلاصہ کے خانے میں درج ذیل دو باتوں کی وضاحت نہیں کریں گے تو اس فائل کو حذف کیا جاسکتا ہے:\n# فائل کا '''مـاخـذ''' ، یعنی:\n#*اگر یہ آپ نے خود تخلیق کی ہے تو اسے بیان کریں۔\n#*اگر یہ آن لائن دستیاب ہے تو اس سائٹ کا '''ربط''' درج کریں۔\n#*اگر آپ نے اسے کسی دوسری زبان کے {{SITENAME}} سے لیا ہے تو اس کا نام تحریر کریں۔\n#صاحب حق طبع و نشر اور فائل کے اجازت نامہ کے بارے میں:\n#* فائل کے اجازت نامہ کے متعلق یہ درج کریں کہ اس کی موجودہ حیثیت کیا ہے۔\n#*اگر آپ خود اسکا حق طبع و نشر رکھتے ہیں تو آپ پر لازم ہے کہ آپ اسے [[دائرۂ عام]] (پبلک ڈومین) میں بھی شائع کریں۔\n\nجب کوئی صارف مستقل ایسی فائل اپلوڈ کرتا رہے جس کے اجازت نامہ کے بارے میں غلط بیانی کی گئی ہو یا وہ مستقل ایسی تصاویر اپلوڈ کرے جن کے بارے میں کوئی وضاحت موجود نہ ہو تو ایسی صورت میں اس صارف پر پابندی لگائے جانے کا قوی امکان موجود ہے۔\n\nفائل اپلوڈ کرنے کے لیے ذیل میں موجود فارم استعمال کریں، اگر آپ جملہ اپلوڈ کردہ تصاویر کو دیکھنا یا تلاش کرنا چاہتے ہیں تو [[Special:FileList|اس فہرست]] کو ملاحظہ فرمائیں۔
تمام اپلوڈ کردہ و حذف شدہ تصاویر کو [[Special:Log/upload|نوشتۂ منتقلی]] اور [[Special:Log/delete|نوشتہ حذف شدگی]] میں درج کر لیا جاتا ہے۔\n\nتصویر کی منتقلی کے بعد، اس کو کسی صفحہ پر رکھنے کیلیے مندرجہ ذیل طریقہ سے استعمال کریں۔\n\n'''[[تصویر:فائل کا نام|متبادل متن]]'''\n\n* فائل کا ربط درج کرنے کے لیے۔ '''[[{{ns:media}}:File.ogg]]'''\n* فائل کا نام چھوٹے بڑے حروف کے معاملہ میں حساس ہے لہذا اگر اپلوڈ کرتے وقت فائل کا نام -- name:JPG ہے اور آپ name:jpg یــا Name:jpg کا ربط درج کرتے ہیں تو ربط کام نہیں کرے گا۔", + "upload-permitted": "اجازت یافتہ فائلوں کی {{PLURAL:$2|قسم|قسمیں}}: $1", + "upload-preferred": "ترجیحی فائلوں کی {{PLURAL:$2|قسم|قسمیں}}: $1", + "upload-prohibited": "ممنوع فائلوں کی {{PLURAL:$2|قسم|قسمیں}}: $1", "uploadlogpage": "نوشتۂ زبراثقال (اپ لوڈ لاگ)", "uploadlogpagetext": "درج ذیل میں حالیہ زبراثقال (اپ لوڈ) کی گئی املاف (فائلوں) کی فہرست دی گئی ہے۔", + "filename": "فائل کا نام", "filedesc": "خلاصہ", "fileuploadsummary": "خلاصہ :", + "filereuploadsummary": "فائل کی تبدیلیاں:", + "filestatus": "کاپی رائٹ کی صورت حال:", "filesource": "ذرائع", "ignorewarning": "انتباہ نظرانداز کرتے ہوۓ بہرصورت ملف (فائل) کو محفوظ کرلیا جاۓ۔", "ignorewarnings": "ہر انتباہ نظرانداز کردیا جاۓ۔", + "minlength1": "فائل کے ناموں میں کم از کم ایک حرف ہونا ضروری ہے۔", + "illegalfilename": "اس فائل کے نام \"$1\" میں ایسے حروف موجود ہیں جو صفحہ کے عنوانات میں ممنوع ہیں۔\nبراہ کرم فائل کا نام تبدیل کرکے دوبارہ اپلوڈ کرنے کی کوشش کریں۔", + "filename-toolong": "فائل کے نام 240 بائٹ سے زیادہ طویل نہ ہوں۔", "badfilename": "ملف (فائل) کا نام \"$1\" ، تبدیل کردیا گیا۔", + "filetype-mime-mismatch": "فائل کی توسیع «$1.‎» فائل کی MIME قسم ($2) کے مطابق نہیں۔", + "filetype-badmime": "MIME قسم \"$1\" کی فائلوں کو اپلوڈ کرنے کی اجازت نہیں ہے۔", + "filetype-missing": "اس فائل کی کوئی توسیع نہیں ہے (مثلاً \".jpg\")۔", + "empty-file": "آپ کی ارسال کردہ فائل خالی تھی۔", + "file-too-large": "آپ کی ارسال کردہ فائل بہت بڑی تھی", + "filename-tooshort": "فائل کا نام انتہائی مختصر ہے۔", + "filetype-banned": "فائل کی اس قسم پر پابندی عائد ہے۔", + "verification-error": "یہ فائل، فائل کی تصدیق میں کامیاب نہیں ہو سکی۔", + "hookaborted": "آپ نے جو تبدیلی کرنے کی کوشش کی اسے کسی توسیع نے منسوخ کر دیا۔", + "illegal-filename": "اس نام کی فائل ممنوع ہے۔", + "overwrite": "موجودہ فائل کو دوبارہ اپلوڈ کرنے کی اجازت نہیں۔", + "unknown-error": "نامعلوم نقص واقع ہوا۔", + "tmp-create-error": "عارضی فائل نہیں بن سکی۔", + "tmp-write-error": "عارضی فائل کی تحریر کے دوران میں نقص۔", + "large-file": "اس بات کی سفارش کی جاتی ہے کہ فائلوں کا حجم $1 سے زیادہ نہ ہو؛\nاس فائل کا حجم $2 ہے۔", "fileexists": "اس نام سے ایک فائل پہلے سے موجود ہے، اگر آپ کو یقین نہ ہو کہ اسے حذف کردیا جانا چاہیے تو براہ کرم [[:$1]] کو ایک نظر دیکھ لیجیے۔ [[$1|thumb]]", "uploadwarning": "انتباہ بہ سلسلۂ زبراثقال", + "uploadwarning-text": "ذیل میں موجود فائل کی وضاحت میں تبدیلی کریں اور دوبارہ کوشش کریں۔", "savefile": "فائل محفوظ کریں", + "uploaddisabled": "اپلوڈ غیر فعال ہے۔", + "copyuploaddisabled": "بذریعہ یوآرایل اپلوڈ غیر فعال ہے۔", + "uploaddisabledtext": "فائل اپلوڈ غیر فعال ہے۔", + "uploadvirus": "اس فائل میں وائرس موجود ہے!\nتفصیلات: $1", + "upload-source": "اصل فائل", "sourcefilename": "اسم ملف (فائل) کا منبع:", + "sourceurl": "اصل یوآرایل", "destfilename": "تعین شدہ اسم ملف:", + "upload-maxfilesize": "فائل کا زیادہ سے زیادہ حجم: $1", + "upload-description": "فائل کی وضاحت", + "upload-options": "اپلوڈ کے اختیارات", "watchthisupload": "یہ صفحہ زیر نظر کریں", + "upload-proto-error": "غلط پروٹوکول", + "upload-file-error": "داخلی نقص", + "upload-misc-error": "اپلوڈ کے دوران میں نامعلوم نقص", + "upload-too-many-redirects": "اس یوآرایل میں بہت سارے رجوع مکررات ہیں", + "upload-http-error": "ایچ ٹی ٹی پی نقص واقع ہوا: $1", "upload-dialog-disabled": "اس ویکی پر اس ڈائیلاگ سے فائل اپ لوڈز غیر فعال ہیںَ", + "upload-dialog-title": "فائل اپلوڈ کریں", "upload-dialog-button-cancel": "منسوخ", "upload-dialog-button-done": "مکمل", "upload-dialog-button-save": "محفوظ", @@ -1155,17 +1398,34 @@ "upload-form-label-own-work": "یہ میرا ذاتی کام ہے", "upload-form-label-infoform-categories": "زمرہ جات", "upload-form-label-infoform-date": "تاریخ", + "backend-fail-alreadyexists": "فائل \"$1\" پہلے سے موجود ہے۔", + "backend-fail-opentemp": "عارضی فائل کھل نہیں سکی۔", + "backend-fail-writetemp": "عارضی فائل میں لکھا نہیں جا سکا۔", + "backend-fail-closetemp": "عارضی فائل بند نہیں ہو سکی۔", + "backend-fail-read": "فائل \"$1\" کو پڑھا نہ جا سکا۔", + "backend-fail-create": "فائل \"$1\" کو لکھا نہ جا سکا۔", + "zip-wrong-format": "یہ زپ فائل نہیں تھی۔", + "uploadstash-thumbnail": "تھمب نیل دیکھیں", + "img-auth-accessdenied": "رسائی معطل", + "http-invalid-url": "نادرست یوآرایل: $1", + "upload-curl-error28": "اپلوڈ کی مہلت ختم", "license": "اجازہ:", "license-header": "اجازہ کاری", + "nolicense": "غیر منتخب", + "licenses-edit": "اجازت نامہ کے اختیارات میں ترمیم کریں", + "license-nopreview": "(نمائش دستیاب نہیں)", "listfiles-delete": "حذف", + "listfiles-userdoesnotexist": "«$1» کے نام سے کھاتہ موجود نہیں۔", "imgfile": "ملف", "listfiles": "فہرست فائل", + "listfiles_thumb": "تھمب نیل", "listfiles_date": "تاریخ", "listfiles_name": "نام", "listfiles_user": "صارف", "listfiles_size": "حجم", "listfiles_description": "تفصیل", "listfiles_count": "ورژن", + "listfiles-show-all": "تصویروں کے پرانے نسخے شامل کریں", "listfiles-latestversion": "موجودہ ورژن", "listfiles-latestversion-yes": "ہاں", "listfiles-latestversion-no": "نہیں", @@ -1179,6 +1439,7 @@ "filehist-datetime": "تاریخ/وقت", "filehist-thumb": "اظفورہ", "filehist-thumbtext": "$1 کا تھمب نیل (thumbnail) ورژن", + "filehist-nothumb": "تھمب نیل نہیں ہے", "filehist-user": "صارف", "filehist-dimensions": "ابعاد", "filehist-filesize": "تصویر کا حجم", @@ -1186,19 +1447,45 @@ "imagelinks": "ملف کا استعمال", "linkstoimage": "اِس ملف کے ساتھ درج ذیل {{PLURAL:$1|صفحہ مربوط ہے|$1 صفحات مربوط ہیں}}", "nolinkstoimage": "ایسے کوئی صفحات نہیں جو اس ملف (فائل) سے رابطہ رکھتے ہوں۔", + "linkstoimage-redirect": "$1 (فائل رجوع مکرر) $2", "sharedupload-desc-here": "یہ ملف $1 سے ہے اور دوسرے منصوبوں میں استعمال ہوسکتا ہے۔\nاِس کے [$2 ملفاتی صفحۂ وضاحت] سے تفصیل درج ذیل ہے۔", + "uploadnewversion-linktext": "اس فائل کا نیا نسخہ اپلوڈ کریں", + "shared-repo-from": "از $1", + "shared-repo": "مشترکہ ذخیرہ", "upload-disallowed-here": "آپ اوپر چھڑا کر اس ملف کو نہیں لکھ سکتے۔", + "filerevert": "$1 کا استرجع کریں", + "filerevert-legend": "فائل کا استرجع کریں", + "filerevert-comment": "وجہ:", + "filerevert-submit": "استرجع کریں", + "filedelete": "$1 کو حذف کریں", + "filedelete-legend": "فائل حذف کریں", "filedelete-comment": "وجہ:", "filedelete-submit": "حذف کریں", "filedelete-success": " (\"اقدام مکمل ہوا\")۔", "filedelete-success-old": " (\"اقدام مکمل ہوا\")", + "filedelete-nofile": "$1 موجود نہیں ہے۔", + "filedelete-otherreason": "دوسری/اضافی وجہ:", + "filedelete-reason-otherlist": "دوسری وجہ", + "filedelete-reason-dropdown": "* عمومی وجوہات حذف\n** کاپی رائٹ کی خلاف ورزی\n** دوہری فائل", + "filedelete-edit-reasonlist": "حذف کی وجوہات میں ترمیم کریں", + "filedelete-maintenance-title": "فائل حذف نہیں کی جا سکتی", + "mimesearch": "MIME تلاش", + "mimetype": "MIME قسم:", "download": "زیراثقال (ڈاؤن لوڈ)", + "unwatchedpages": "نادیدہ صفحات", "listredirects": "فہرست متبادل ربط", + "listduplicatedfiles": "مکررات کے ساتھ فائلوں کی فہرست", "unusedtemplates": "غیر استعمال شدہ سانچے", "unusedtemplateswlh": "دیگر روابط", "randompage": "بےترتیب صفحہ", + "randomincategory": "زمرہ میں بے ترتیب صفحہ", + "randomincategory-invalidcategory": "عنوان «$1» زمرے کا درست نام نہیں ہے۔", + "randomincategory-nopages": "[[:Category:$1|$1]] زمرہ میں کوئی صفحہ نہیں ہے۔", "randomincategory-category": "زمرہ:", + "randomincategory-legend": "زمرہ میں بے ترتیب صفحہ", "randomincategory-submit": "جانا", + "randomredirect": "بے ترتيب رجوع مکرر", + "randomredirect-nopages": "«$1» نام فضا میں کوئی رجوع مکرر نہیں ہے۔", "statistics": "اعداد و شمار", "statistics-header-pages": "صفحات کے اعداد و شمار", "statistics-header-edits": "ترمیمی اعداد و شمار", @@ -1214,9 +1501,11 @@ "statistics-users-active": "متحرک صارفین", "pageswithprop-submit": "ٹھیک", "doubleredirects": "دوہرے متبادل ربط", + "double-redirect-fixed-move": "[[$1]] کو منتقل کر دیا گیا۔\nیہ از خود تازہ ہو گیا اور اب [[$2]] سے رجوع مکرر ہے۔", "brokenredirects": "نامکمل متبادل ربط", "brokenredirects-edit": "ترمیم کریں", "brokenredirects-delete": "حذف", + "withoutinterwiki": "صفحات بدون بین الویکی روابط", "withoutinterwiki-legend": "سابقہ", "withoutinterwiki-submit": "دکھائیں", "fewestrevisions": "کم نظرِ ثانی شدہ مضامین", @@ -1526,6 +1815,7 @@ "movepage-moved-redirect": "رجوع مکرر تخلیق کر دیا گیا۔", "movepage-moved-noredirect": "رجوع مکرر کو بننے سے روک دیا گیا ہے۔", "articleexists": "اس عنوان سے کوئی صفحہ پہلے ہی موجود ہے، یا آپکا منتخب کردہ نام مستعمل نہیں۔ براۓ مہربانی دوسرا نام منتخب کیجیۓ۔", + "movepage-page-moved": "صفحہ $1 کو $2 کی جانب منتقل کر دیا گیا۔", "movelogpage": "نوشتۂ منتقلی", "movereason": "وجہ:", "revertmove": "رجوع", @@ -1747,6 +2037,7 @@ "revdelete-summary": "خلاصۂ تدوین", "feedback-thanks-title": "شکریہ!", "searchsuggest-search": "تلاش", + "searchsuggest-containing": "نتائج...", "expandtemplates": "سانچے کو وسیع کریں", "expand_templates_input": "ان پٹ متن:", "expand_templates_output": "نتیجہ", diff --git a/resources/lib/oojs-ui/oojs-ui-core-apex.css b/resources/lib/oojs-ui/oojs-ui-core-apex.css index a4479f7c45..6437ca8108 100644 --- a/resources/lib/oojs-ui/oojs-ui-core-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-core-apex.css @@ -401,7 +401,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { } .oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label { color: inherit; - display: table; + display: inline-table; box-sizing: border-box; max-width: 100%; padding: 0; @@ -421,7 +421,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { .oo-ui-fieldsetLayout + .oo-ui-formLayout { margin-top: 2em; } -.oo-ui-fieldsetLayout > .oo-ui-labelElement-label { +.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label { font-size: 1.1em; margin-bottom: 0.5em; padding: 0.25em 0; diff --git a/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css index 09e6cfc524..08d91b4c3b 100644 --- a/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css @@ -524,7 +524,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { } .oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label { color: inherit; - display: table; + display: inline-table; box-sizing: border-box; max-width: 100%; padding: 0; @@ -544,7 +544,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { .oo-ui-fieldsetLayout + .oo-ui-formLayout { margin-top: 2em; } -.oo-ui-fieldsetLayout > .oo-ui-labelElement-label { +.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label { margin-bottom: 0.5em; font-size: 1.1em; font-weight: bold; diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php index 85c95e4768..bc5096639b 100644 --- a/tests/phpunit/includes/HtmlTest.php +++ b/tests/phpunit/includes/HtmlTest.php @@ -514,10 +514,6 @@ class HtmlTest extends MediaWikiTestCase { 'canvas', [ 'width' => 300 ] ]; - $cases[] = [ '', - 'command', [ 'type' => 'command' ] - ]; - $cases[] = [ '
', 'form', [ 'action' => 'GET' ] ]; diff --git a/tests/phpunit/includes/db/DatabaseTestHelper.php b/tests/phpunit/includes/db/DatabaseTestHelper.php index 63322cc9a2..caa29bd448 100644 --- a/tests/phpunit/includes/db/DatabaseTestHelper.php +++ b/tests/phpunit/includes/db/DatabaseTestHelper.php @@ -34,6 +34,8 @@ class DatabaseTestHelper extends DatabaseBase { $this->profiler = new ProfilerStub( [] ); $this->trxProfiler = new TransactionProfiler(); $this->cliMode = isset( $opts['cliMode'] ) ? $opts['cliMode'] : true; + $this->connLogger = new \Psr\Log\NullLogger(); + $this->queryLogger = new \Psr\Log\NullLogger(); } /**