* The 'user.groups' module, deprecated in 1.28, was removed.
Use the 'user' module instead.
* The ability to override User::$mRights has been removed.
+* Previously, when iterating ResultWrapper with foreach() or a similar
+ construct, the range of the index was 1..numRows. This has been fixed to be
+ 0..(numRows-1).
* …
=== Deprecations in 1.34 ===
->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors );
}
- /**
- * Add the resulting error code to the errors array
- *
- * @param array $errors List of current errors
- * @param array|string|MessageSpecifier|false $result Result of errors
- *
- * @return array List of errors
- */
- private function resultToError( $errors, $result ) {
- if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
- // A single array representing an error
- $errors[] = $result;
- } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
- // A nested array representing multiple errors
- $errors = array_merge( $errors, $result );
- } elseif ( $result !== '' && is_string( $result ) ) {
- // A string representing a message-id
- $errors[] = [ $result ];
- } elseif ( $result instanceof MessageSpecifier ) {
- // A message specifier representing an error
- $errors[] = [ $result ];
- } elseif ( $result === false ) {
- // a generic "We don't want them to do that"
- $errors[] = [ 'badaccess-group0' ];
- }
- return $errors;
- }
-
/**
* Get a filtered list of all restriction types supported by this wiki.
* @param bool $exists True to get all restriction types that apply to
$this->mHasSubpages = false;
$subpages = $this->getSubpages( 1 );
if ( $subpages instanceof TitleArray ) {
- $this->mHasSubpages = (bool)$subpages->count();
+ $this->mHasSubpages = (bool)$subpages->current();
}
}
*/
function formatRow( $row ) {
if ( $this->lastRow ) {
- $latest = ( $this->counter == 1 && $this->mIsFirst );
$firstInList = $this->counter == 1;
$this->counter++;
? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
: false;
- $s = $this->historyLine(
- $this->lastRow, $row, $notifTimestamp, $latest, $firstInList );
+ $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
} else {
$s = '';
}
$s .= Html::hidden( 'type', 'revision' ) . "\n";
// Button container stored in $this->buttons for re-use in getEndBody()
- $this->buttons = Html::openElement( 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
- $className = 'historysubmit mw-history-compareselectedversions-button';
- $attrs = [ 'class' => $className ]
- + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
- $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
- $attrs
- ) . "\n";
-
- $user = $this->getUser();
- $actionButtons = '';
- if ( $user->isAllowed( 'deleterevision' ) ) {
- $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' );
- }
- if ( $this->showTagEditUI ) {
- $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' );
- }
- if ( $actionButtons ) {
- $this->buttons .= Xml::tags( 'div', [ 'class' =>
- 'mw-history-revisionactions' ], $actionButtons );
- }
+ $this->buttons = '';
+ if ( $this->getNumRows() > 0 ) {
+ $this->buttons .= Html::openElement(
+ 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
+ $className = 'historysubmit mw-history-compareselectedversions-button';
+ $attrs = [ 'class' => $className ]
+ + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
+ $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
+ $attrs
+ ) . "\n";
+
+ $user = $this->getUser();
+ $actionButtons = '';
+ if ( $user->isAllowed( 'deleterevision' ) ) {
+ $actionButtons .= $this->getRevisionButton(
+ 'revisiondelete', 'showhideselectedversions' );
+ }
+ if ( $this->showTagEditUI ) {
+ $actionButtons .= $this->getRevisionButton(
+ 'editchangetags', 'history-edit-tags' );
+ }
+ if ( $actionButtons ) {
+ $this->buttons .= Xml::tags( 'div', [ 'class' =>
+ 'mw-history-revisionactions' ], $actionButtons );
+ }
- if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
- $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
- }
+ if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
+ $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
+ }
- $this->buttons .= '</div>';
+ $this->buttons .= '</div>';
- $s .= $this->buttons;
+ $s .= $this->buttons;
+ }
$s .= '<ul id="pagehistory">' . "\n";
return $s;
protected function getEndBody() {
if ( $this->lastRow ) {
- $latest = $this->counter == 1 && $this->mIsFirst;
$firstInList = $this->counter == 1;
if ( $this->mIsBackwards ) {
# Next row is unknown, but for UI reasons, probably exists if an offset has been specified
? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
: false;
- $s = $this->historyLine(
- $this->lastRow, $next, $notifTimestamp, $latest, $firstInList );
+ $s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
} else {
$s = '';
}
* @param mixed $next The database row corresponding to the next line
* (chronologically previous)
* @param bool|string $notificationtimestamp
- * @param bool $latest Whether this row corresponds to the page's latest revision.
+ * @param bool $dummy Unused.
* @param bool $firstInList Whether this row corresponds to the first
* displayed on this history page.
* @return string HTML output for the row
*/
function historyLine( $row, $next, $notificationtimestamp = false,
- $latest = false, $firstInList = false ) {
+ $dummy = false, $firstInList = false ) {
$rev = new Revision( $row, 0, $this->getTitle() );
if ( is_object( $next ) ) {
$prevRev = null;
}
- $curlink = $this->curLink( $rev, $latest );
+ $latest = $rev->getId() === $this->getWikiPage()->getLatest();
+ $curlink = $this->curLink( $rev );
$lastlink = $this->lastLink( $rev, $next );
$curLastlinks = Html::rawElement( 'span', [], $curlink ) .
Html::rawElement( 'span', [], $lastlink );
* Create a diff-to-current link for this revision for this page
*
* @param Revision $rev
- * @param bool $latest This is the latest revision of the page?
* @return string
*/
- function curLink( $rev, $latest ) {
+ function curLink( $rev ) {
$cur = $this->historyPage->message['cur'];
- if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+ $latest = $this->getWikiPage()->getLatest();
+ if ( $latest === $rev->getId() || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
return $cur;
} else {
return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
new HtmlArmor( $cur ),
[],
[
- 'diff' => $this->getWikiPage()->getLatest(),
+ 'diff' => $latest,
'oldid' => $rev->getId()
]
);
private function isCacheable( LinkTarget $title ) {
$ns = $title->getNamespace();
- if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY ] ) ) {
+ if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
return true;
}
// Focus on transcluded pages more than the main content
* @param array $params Additional parameters include:
* - keywordTableMap : Map of reserved table names to alternative table names to use
*/
- function __construct( array $params ) {
+ public function __construct( array $params ) {
$this->keywordTableMap = $params['keywordTableMap'] ?? [];
$params['tablePrefix'] = strtoupper( $params['tablePrefix'] );
parent::__construct( $params );
"and database)\n" );
}
+ if ( $schema !== null ) {
+ // We use the *database* aspect of $domain for schema, not the domain schema
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": cannot use schema '$schema'; " .
+ "the database component '$dbName' is actually interpreted as the Oracle schema."
+ );
+ }
+
$this->close();
$this->user = $user;
$this->password = $password;
protected function doSelectDomain( DatabaseDomain $domain ) {
if ( $domain->getSchema() !== null ) {
// We use the *database* aspect of $domain for schema, not the domain schema
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": domain '{$domain->getId()}' has a schema component; " .
+ "the database component is actually interpreted as the Oracle schema."
+ );
}
$database = $domain->getDatabase();
* @since 1.27
*/
public static function acquirePageLock( IDatabase $dbw, $pageId, $why = 'atomicity' ) {
- $key = "LinksUpdate:$why:pageid:$pageId";
+ $key = "{$dbw->getDomainID()}:LinksUpdate:$why:pageid:$pageId"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $key, __METHOD__, 15 );
if ( !$scopedLock ) {
$logger = LoggerFactory::getInstance( 'SecondaryDataUpdate' );
'type' => 'sqlite',
'dbname' => \"{\$wgDBname}_jobqueue\",
'tablePrefix' => '',
+ 'variables' => [ 'synchronous' => 'NORMAL' ],
'dbDirectory' => \$wgSQLiteDataDir,
'trxMode' => 'IMMEDIATE',
'flags' => 0
"config-copyright": "== Аўтарскае права і ўмовы ==\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but '''without any warranty'''; without even the implied warranty of '''merchantability''' or '''fitness for a particular purpose'''.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. or [https://www.gnu.org/copyleft/gpl.html read it online].",
"config-sidebar": "* [https://www.mediawiki.org Хатняя старонка MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Даведка для ўдзельнікаў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Даведка для адміністратараў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Адказы на частыя пытаньні]",
"config-sidebar-readme": "Прачытай мяне",
+ "config-sidebar-relnotes": "Заўвагі да выпуску",
"config-env-good": "Асяродзьдзе было праверанае.\nВы можаце ўсталёўваць MediaWiki.",
"config-env-bad": "Асяродзьдзе было праверанае.\nУсталяваньне MediaWiki немагчымае.",
"config-env-php": "Усталяваны PHP $1.",
"config-page-existingwiki": "Eksisterende wiki",
"config-help-restart": "Vil du rydde alle gemte data, du har indtastet og genstarte installationen?",
"config-restart": "Ja, genstart den",
+ "config-sidebar-upgrade": "Opgraderer",
"config-env-php": "PHP $1 er installeret.",
"config-env-hhvm": "HHVM $1 er installeret.",
"config-apc": "[https://www.php.net/apc APC] er installeret",
"Tosky",
"Selven",
"Sarah Bernabei",
- "ArTrix"
+ "ArTrix",
+ "Annibale covini gerolamo"
]
},
"config-desc": "Programma di installazione per MediaWiki",
"config-sidebar": "* [https://www.mediawiki.org Pagina principale MediaWiki]\n* [https://www.mediawiki.org/Special:MyLanguage/Help:Contents Guida ai contenuti per utenti]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:Contents Guida ai contenuti per admin]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:FAQ FAQ]",
"config-sidebar-readme": "Leggimi",
"config-sidebar-relnotes": "Note di versione",
+ "config-sidebar-license": "copiando",
"config-sidebar-upgrade": "Aggiornamento",
"config-env-good": "L'ambiente è stato controllato.\nÈ possibile installare MediaWiki.",
"config-env-bad": "L'ambiente è stato controllato.\nNon è possibile installare MediaWiki.",
*
* @file
*/
+
+use MediaWiki\Logger\LoggerFactory;
use Psr\Log\LoggerInterface;
/**
"Non-daemonized mode is no longer supported. Please install the " .
"mediawiki/services/jobrunner service and update \$wgJobTypeConf as needed." );
}
- $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
+ $this->logger = LoggerFactory::getInstance( 'redis' );
}
protected function supportedOrders() {
try {
return $conn->lSize( $this->getQueueKey( 'l-unclaimed' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
return array_sum( $conn->exec() );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
return $conn->zSize( $this->getQueueKey( 'z-delayed' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
return $conn->zSize( $this->getQueueKey( 'z-abandoned' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
throw new RedisException( $err );
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
$job = $this->getJobFromFields( $item ); // may be false
} while ( !$job ); // job may be false if invalid
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $job;
$this->incrStats( 'acks', $this->type );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return true;
// Update the timestamp of the last root job started at the location...
return $conn->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL ); // 2 weeks
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
// Get the last time this root job was enqueued
$timestamp = $conn->get( $this->getRootJobCacheKey( $params['rootJobSignature'] ) );
} catch ( RedisException $e ) {
- $timestamp = false;
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
// Check if a new root job was started at the location after this one's...
return $ok;
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
$uids = $conn->lRange( $this->getQueueKey( 'l-unclaimed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-delayed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-claimed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-abandoned' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
}
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $sizes;
* This function should not be called outside JobQueueRedis
*
* @param string $uid
- * @param RedisConnRef $conn
+ * @param RedisConnRef|Redis $conn
* @return RunnableJob|bool Returns false if the job does not exist
* @throws JobQueueError
* @throws UnexpectedValueException
*/
- public function getJobFromUidInternal( $uid, RedisConnRef $conn ) {
+ public function getJobFromUidInternal( $uid, $conn ) {
try {
$data = $conn->hGet( $this->getQueueKey( 'h-data' ), $uid );
if ( $data === false ) {
return $job;
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
$queues[] = $this->decodeQueueName( $queue );
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $queues;
/**
* Get a connection to the server that handles all sub-queues for this queue
*
- * @return RedisConnRef
+ * @return RedisConnRef|Redis
* @throws JobQueueConnectionError
*/
protected function getConnection() {
/**
* @param RedisConnRef $conn
* @param RedisException $e
- * @throws JobQueueError
+ * @return JobQueueError
*/
- protected function throwRedisException( RedisConnRef $conn, $e ) {
+ protected function handleErrorAndMakeException( RedisConnRef $conn, $e ) {
$this->redisPool->handleError( $conn, $e );
- throw new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
+ return new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
}
/**
}
// Use a named lock so that jobs for this page see each others' changes
- $lockKey = "CategoryMembershipUpdates:{$page->getId()}";
+ $lockKey = "{$dbw->getDomainID()}:CategoryMembershipChange:{$page->getId()}"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 3 );
if ( !$scopedLock ) {
$this->setLastError( "Could not acquire lock '$lockKey'" );
}
// Use a named lock so that jobs for this user see each others' changes
- $lockKey = "ClearUserWatchlistJob:$userId";
+ $lockKey = "{{$dbw->getDomainID()}}:ClearUserWatchlist:$userId"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 );
if ( !$scopedLock ) {
$this->setLastError( "Could not acquire lock '$lockKey'" );
/** @var float|null */
private $wallClockOverride;
+ /** @var float */
const RANK_TOP = 1.0;
/** @var int Array key that holds the entry's main timestamp (flat key use) */
*
* @param string $key
* @param mixed $value
- * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
+ * @param float $rank Bottom fraction of the list where keys start off [default: 1.0]
* @return void
*/
public function set( $key, $value, $rank = self::RANK_TOP ) {
* Check if a key exists
*
* @param string $key
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return bool
+ * @since 1.32 Added $maxAge
*/
- public function has( $key, $maxAge = 0.0 ) {
+ public function has( $key, $maxAge = INF ) {
if ( !is_int( $key ) && !is_string( $key ) ) {
throw new UnexpectedValueException(
__METHOD__ . ': invalid key; must be string or integer.' );
* If the item is already set, it will be pushed to the top of the cache.
*
* @param string $key
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
- * @return mixed Returns null if the key was not found or is older than $maxAge
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
+ * @param mixed|null $default Value to return if no key is found [default: null]
+ * @return mixed Returns $default if the key was not found or is older than $maxAge
+ * @since 1.32 Added $maxAge
+ * @since 1.34 Added $default
*/
- public function get( $key, $maxAge = 0.0 ) {
+ public function get( $key, $maxAge = INF, $default = null ) {
if ( !$this->has( $key, $maxAge ) ) {
- return null;
+ return $default;
}
$this->ping( $key );
/**
* @param string|int $key
* @param string|int $field
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return bool
+ * @since 1.32 Added $maxAge
*/
- public function hasField( $key, $field, $maxAge = 0.0 ) {
+ public function hasField( $key, $field, $maxAge = INF ) {
$value = $this->get( $key );
if ( !is_int( $field ) && !is_string( $field ) ) {
/**
* @param string|int $key
* @param string|int $field
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return mixed Returns null if the key was not found or is older than $maxAge
+ * @since 1.32 Added $maxAge
*/
- public function getField( $key, $field, $maxAge = 0.0 ) {
+ public function getField( $key, $field, $maxAge = INF ) {
if ( !$this->hasField( $key, $field, $maxAge ) ) {
return null;
}
* @since 1.28
* @param string $key
* @param callable $callback Callback that will produce the value
- * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
- * @param float $maxAge Ignore items older than this many seconds [Default: 0.0] (since 1.32)
+ * @param float $rank Bottom fraction of the list where keys start off [default: 1.0]
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return mixed The cached value if found or the result of $callback otherwise
+ * @since 1.32 Added $maxAge
*/
public function getWithSetCallback(
- $key, callable $callback, $rank = self::RANK_TOP, $maxAge = 0.0
+ $key, callable $callback, $rank = self::RANK_TOP, $maxAge = INF
) {
if ( $this->has( $key, $maxAge ) ) {
$value = $this->get( $key );
protected function doSelectDomain( DatabaseDomain $domain ) {
if ( $domain->getSchema() !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+ );
}
$database = $domain->getDatabase();
* - sslCiphers : array list of allowable ciphers [default: null]
* @param array $params
*/
- function __construct( array $params ) {
+ public function __construct( array $params ) {
$this->lagDetectionMethod = $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master';
$this->lagDetectionOptions = $params['lagDetectionOptions'] ?? [];
$this->useGTIDs = !empty( $params['useGTIDs' ] );
$this->close();
if ( $schema !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
}
$this->server = $server;
protected function doSelectDomain( DatabaseDomain $domain ) {
if ( $domain->getSchema() !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+ );
}
$database = $domain->getDatabase();
);
}
+ $this->close();
+
$this->server = $server;
$this->user = $user;
$this->password = $password;
}
$this->connectString = $this->makeConnectionString( $connectVars );
- $this->close();
- $this->installErrorHandler();
+ $this->installErrorHandler();
try {
// Use new connections to let LoadBalancer/LBFactory handle reuse
$this->conn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
$this->restoreErrorHandler();
throw $ex;
}
-
$phpError = $this->restoreErrorHandler();
if ( !$this->conn ) {
// See https://www.postgresql.org/docs/8.3/sql-set.html
throw new DBUnexpectedError(
$this,
- __METHOD__ . ": a transaction is currently active."
+ __METHOD__ . ": a transaction is currently active"
);
}
use Exception;
use LockManager;
use FSLockManager;
-use InvalidArgumentException;
use RuntimeException;
use stdClass;
* @ingroup Database
*/
class DatabaseSqlite extends Database {
- /** @var bool Whether full text is enabled */
- private static $fulltextEnabled = null;
-
- /** @var string|null Directory */
+ /** @var string|null Directory for SQLite database files listed under their DB name */
protected $dbDir;
- /** @var string File name for SQLite database file */
+ /** @var string|null Explicit path for the SQLite database file */
protected $dbPath;
/** @var string Transaction mode */
protected $trxMode;
/** @var array List of shared database already attached to this connection */
private $alreadyAttached = [];
+ /** @var bool Whether full text is enabled */
+ private static $fulltextEnabled = null;
+
/**
* Additional params include:
* - dbDirectory : directory containing the DB and the lock file directory
- * [defaults to $wgSQLiteDataDir]
* - dbFilePath : use this to force the path of the DB file
* - trxMode : one of (deferred, immediate, exclusive)
* @param array $p
*/
- function __construct( array $p ) {
+ public function __construct( array $p ) {
if ( isset( $p['dbFilePath'] ) ) {
$this->dbPath = $p['dbFilePath'];
- $lockDomain = md5( $this->dbPath );
- // Use "X" for things like X.sqlite and ":memory:" for RAM-only DBs
- if ( !isset( $p['dbname'] ) || !strlen( $p['dbname'] ) ) {
- $p['dbname'] = preg_replace( '/\.sqlite\d?$/', '', basename( $this->dbPath ) );
+ if ( !strlen( $p['dbname'] ) ) {
+ $p['dbname'] = self::generateDatabaseName( $this->dbPath );
}
} elseif ( isset( $p['dbDirectory'] ) ) {
$this->dbDir = $p['dbDirectory'];
- $lockDomain = $p['dbname'];
- } else {
- throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
}
- $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." );
- }
+ // Set a dummy user to make initConnection() trigger open()
+ parent::__construct( [ 'user' => '@' ] + $p );
- if ( $this->hasProcessMemoryPath() ) {
- $this->lockMgr = new NullLockManager( [ 'domain' => $lockDomain ] );
- } else {
+ $this->trxMode = strtoupper( $p['trxMode'] ?? '' );
+
+ $lockDirectory = $this->getLockFileDirectory();
+ if ( $lockDirectory !== null ) {
$this->lockMgr = new FSLockManager( [
- 'domain' => $lockDomain,
- 'lockDirectory' => is_string( $this->dbDir )
- ? "{$this->dbDir}/locks"
- : dirname( $this->dbPath ) . "/locks"
+ 'domain' => $this->getDomainID(),
+ 'lockDirectory' => $lockDirectory
] );
+ } else {
+ $this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
}
-
- parent::__construct( $p );
}
protected static function getAttributes() {
return $db;
}
- protected function doInitConnection() {
- if ( $this->dbPath !== null ) {
- // Standalone .sqlite file mode.
- $this->openFile(
- $this->dbPath,
- $this->connectionParams['dbname'],
- $this->connectionParams['tablePrefix']
- );
- } elseif ( $this->dbDir !== null ) {
- // Stock wiki mode using standard file names per DB
- if ( strlen( $this->connectionParams['dbname'] ) ) {
- $this->open(
- $this->connectionParams['host'],
- $this->connectionParams['user'],
- $this->connectionParams['password'],
- $this->connectionParams['dbname'],
- $this->connectionParams['schema'],
- $this->connectionParams['tablePrefix']
- );
- } else {
- // Caller will manually call open() later?
- $this->connLogger->debug( __METHOD__ . ': no database opened.' );
- }
- } else {
- throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
- }
- }
-
/**
* @return string
*/
- function getType() {
+ public function getType() {
return 'sqlite';
}
*
* @return bool
*/
- function implicitGroupby() {
+ public function implicitGroupby() {
return false;
}
protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
$this->close();
+ // Note that for SQLite, $server, $user, and $pass are ignored
+
if ( $schema !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
}
- // Only $dbName is used, the other parameters are irrelevant for SQLite databases
- $this->openFile( self::generateFileName( $this->dbDir, $dbName ), $dbName, $tablePrefix );
- }
+ if ( $this->dbPath !== null ) {
+ $path = $this->dbPath;
+ } elseif ( $this->dbDir !== null ) {
+ $path = self::generateFileName( $this->dbDir, $dbName );
+ } else {
+ throw new DBExpectedError( $this, __CLASS__ . ": DB path or directory required" );
+ }
- /**
- * Opens a database file
- *
- * @param string $fileName
- * @param string $dbName
- * @param string $tablePrefix
- * @throws DBConnectionError
- */
- protected function openFile( $fileName, $dbName, $tablePrefix ) {
- if ( !$this->hasProcessMemoryPath() && !is_readable( $fileName ) ) {
+ if ( !in_array( $this->trxMode, [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ], true ) ) {
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": invalid transaction mode '{$this->trxMode}'"
+ );
+ }
+
+ if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) {
$error = "SQLite database file not readable";
$this->connLogger->error(
"Error connecting to {db_server}: {error}",
throw new DBConnectionError( $this, $error );
}
- $this->dbPath = $fileName;
try {
- $this->conn = new PDO(
- "sqlite:$fileName",
+ $conn = new PDO(
+ "sqlite:$path",
'',
'',
[ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ]
);
- $error = 'unknown error';
+ // Set error codes only, don't raise exceptions
+ $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
} catch ( PDOException $e ) {
$error = $e->getMessage();
- }
-
- if ( !$this->conn ) {
$this->connLogger->error(
"Error connecting to {db_server}: {error}",
$this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
throw new DBConnectionError( $this, $error );
}
- try {
- // Set error codes only, don't raise exceptions
- $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
-
- $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
+ $this->conn = $conn;
+ $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
+ try {
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
// Enforce LIKE to be case sensitive, just like MySQL
$this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
// Apply an optimizations or requirements regarding fsync() usage
$sync = $this->connectionVariables['synchronous'] ?? null;
if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
- $this->query( "PRAGMA synchronous = $sync", __METHOD__ );
+ $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
}
} catch ( Exception $e ) {
// Connection was not fully initialized and is not safe for use
}
/**
- * @return string SQLite DB file path
+ * @return string|null SQLite DB file path
+ * @throws DBUnexpectedError
* @since 1.25
*/
public function getDbFilePath() {
- return $this->dbPath;
+ return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
+ }
+
+ /**
+ * @return string|null Lock file directory
+ */
+ public function getLockFileDirectory() {
+ if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
+ return dirname( $this->dbPath ) . '/locks';
+ } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
+ return $this->dbDir . '/locks';
+ }
+
+ return null;
}
/**
/**
* Generates a database file name. Explicitly public for installer.
* @param string $dir Directory where database resides
- * @param string $dbName Database name
+ * @param string|bool $dbName Database name (or false from Database::factory, validated here)
* @return string
+ * @throws DBUnexpectedError
*/
public static function generateFileName( $dir, $dbName ) {
+ if ( $dir == '' ) {
+ throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
+ } elseif ( self::isProcessMemoryPath( $dir ) ) {
+ throw new DBUnexpectedError(
+ null,
+ __CLASS__ . ": cannot use process memory directory '$dir'"
+ );
+ } elseif ( !strlen( $dbName ) ) {
+ throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
+ }
+
return "$dir/$dbName.sqlite";
}
+ /**
+ * @param string $path
+ * @return string
+ */
+ private static function generateDatabaseName( $path ) {
+ if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
+ // E.g. "file::memory:?cache=shared" => ":memory":
+ return ':memory:';
+ } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
+ // E.g. "file:memdb1?mode=memory" => ":memdb1:"
+ return ":{$m[1]}:";
+ } else {
+ // E.g. "/home/.../some_db.sqlite3" => "some_db"
+ return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
+ }
+ }
+
+ /**
+ * @param string $path
+ * @return bool
+ */
+ private static function isProcessMemoryPath( $path ) {
+ return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
+ }
+
/**
* Check if the searchindext table is FTS enabled.
* @return bool False if not enabled.
* @param string $fname Calling function name
* @return IResultWrapper
*/
- function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
- if ( !$file ) {
- $file = self::generateFileName( $this->dbDir, $name );
- }
- $file = $this->addQuotes( $file );
+ public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
+ $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
+ $encFile = $this->addQuotes( $file );
- return $this->query( "ATTACH DATABASE $file AS $name", $fname );
+ return $this->query( "ATTACH DATABASE $encFile AS $name", $fname );
}
protected function isWriteQuery( $sql ) {
}
public function serverIsReadOnly() {
- return ( !$this->hasProcessMemoryPath() && !is_writable( $this->dbPath ) );
- }
+ $path = $this->getDbFilePath();
- /**
- * @return bool
- */
- private function hasProcessMemoryPath() {
- return ( strpos( $this->dbPath, ':memory:' ) === 0 );
+ return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
}
/**
}
protected function doBegin( $fname = '' ) {
- if ( $this->trxMode ) {
+ if ( $this->trxMode != '' ) {
$this->query( "BEGIN {$this->trxMode}", $fname );
} else {
$this->query( 'BEGIN', $fname );
}
public function lock( $lockName, $method, $timeout = 5 ) {
- // Give better error message for permission problems than just returning false
+ $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
if (
- !is_dir( "{$this->dbDir}/locks" ) &&
- ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) )
+ $this->lockMgr instanceof FSLockManager &&
+ $status->hasMessage( 'lockmanager-fail-openlock' )
) {
- throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." );
+ throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
}
- return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
+ return $status->isOK();
}
public function unlock( $lockName, $method ) {
return $out;
}
+ public function execute( $par ) {
+ $this->addHelpLink( 'Help:Redirects' );
+ parent::execute( $par );
+ }
+
/**
* Cache page content model for performance
*
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:Diff' );
$form = HTMLForm::factory( 'ooui', [
'Page1' => [
$this->setHeaders();
$this->outputHeader();
$this->checkPermissions();
+ $this->addHelpLink( 'Help:User contributions' );
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'deletedcontributions-title' ) );
return ( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
}
+ public function execute( $par ) {
+ $this->addHelpLink( 'Help:Redirects' );
+ parent::execute( $par );
+ }
+
/**
* Cache page content model and gender distinction for performance
*
$out = $this->getOutput();
$out->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:User_rights_and_groups' );
$out->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
}
}
+ public function execute( $par ) {
+ $this->addHelpLink( 'Help:Redirects' );
+ parent::execute( $par );
+ }
+
protected function getGroupName() {
return 'pages';
}
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:Protected_pages' );
$request = $this->getRequest();
$type = $request->getVal( $this->IdType );
function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Help:Protected_pages' );
$request = $this->getRequest();
$type = $request->getVal( $this->IdType );
function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Manual:Tags' );
$request = $this->getRequest();
switch ( $par ) {
class UncategorizedImagesPage extends ImageQueryPage {
function __construct( $name = 'Uncategorizedimages' ) {
parent::__construct( $name );
+ $this->addHelpLink( 'Help:Categories' );
}
function sortDescending() {
function __construct( $name = 'Uncategorizedpages' ) {
parent::__construct( $name );
+ $this->addHelpLink( 'Help:Categories' );
}
function sortDescending() {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Help:Deletion_and_undeletion' );
$this->loadRequest( $par );
$this->checkPermissions(); // Needs to be after mTargetObj is set
}
// Purge old, expired memberships from the DB
- JobQueueGroup::singleton()->push( new UserGroupExpiryJob() );
+ $hasExpiredRow = $dbw->selectField(
+ 'user_groups',
+ '1',
+ [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
+ __METHOD__
+ );
+ if ( $hasExpiredRow ) {
+ JobQueueGroup::singleton()->lazyPush( new UserGroupExpiryJob() );
+ }
// Check that the values make sense
if ( $this->group === null ) {
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
$dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
- $lockKey = $dbw->getDomainID() . ':usergroups-prune'; // specific to this wiki
+ $lockKey = "{$dbw->getDomainID()}:UserGroupMembership:purge"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
if ( !$scopedLock ) {
return false; // already running
}
$oldRev = $this->revisionLookup->getRevisionById( $oldid );
+ if ( !$oldRev ) {
+ // Oldid given but does not exist (probably deleted)
+ return false;
+ }
+
$nextRev = $this->revisionLookup->getNextRevision( $oldRev );
if ( !$nextRev ) {
// Oldid given and is the latest revision for this title; clear the timestamp.
"delete-legend": "Выдаліць",
"historywarning": "<strong>Папярэджаньне</strong>: старонка, якую Вы зьбіраецеся выдаліць, мае гісторыю з $1 {{PLURAL:$1|вэрсіі|вэрсіяў|вэрсіяў}}:",
"historyaction-submit": "Паказаць вэрсіі",
- "confirmdeletetext": "Ð\97аÑ\80аз Ð\92Ñ\8b вÑ\8bдалÑ\96Ñ\86е Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам з Ñ\83Ñ\81Ñ\91й гÑ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй зÑ\8cменаÑ\9e.\nÐ\9aалÑ\96 лаÑ\81ка, паÑ\86Ñ\8cвеÑ\80дзÑ\96Ñ\86е, Ñ\88Ñ\82о Ð\92Ñ\8b зÑ\8cбÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f гÑ\8dÑ\82а зÑ\80абÑ\96Ñ\86Ñ\8c Ñ\96 Ñ\88Ñ\82о Ð\92ы разумееце ўсе наступствы, а таксама робіце гэта ў адпаведнасьці з [[{{MediaWiki:Policy-url}}|правіламі]].",
+ "confirmdeletetext": "Ð\97аÑ\80аз вÑ\8b вÑ\8bдалÑ\96Ñ\86е Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам з Ñ\83Ñ\81Ñ\91й гÑ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй зÑ\8cменаÑ\9e.\nÐ\9aалÑ\96 лаÑ\81ка, паÑ\86Ñ\8cвеÑ\80дзÑ\96Ñ\86е, Ñ\88Ñ\82о вÑ\8b зÑ\8cбÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f гÑ\8dÑ\82а зÑ\80абÑ\96Ñ\86Ñ\8c Ñ\96 Ñ\88Ñ\82о вы разумееце ўсе наступствы, а таксама робіце гэта ў адпаведнасьці з [[{{MediaWiki:Policy-url}}|правіламі]].",
"actioncomplete": "Дзеяньне выкананае",
"actionfailed": "Дзеяньне ня выкананае",
"deletedtext": "«$1» была выдаленая.\nЗапісы пра выдаленыя старонкі зьмяшчаюцца ў $2.",
"autoblockedtext": "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem anderen Benutzer genutzt wurde, der von $1 gesperrt wurde.\nAls Grund wurde angegeben:\n\n:''$2''\n\n* Beginn der Sperre: $8\n* Ende der Sperre: $6\n* Sperre betrifft: $7\n\nDu kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.\n\nDu kannst die „{{int:emailuser}}“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.\n\nDeine aktuelle IP-Adresse ist $3, und die Sperr-ID ist $5.\nBitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
"systemblockedtext": "Dein Benutzername oder deine IP-Adresse wurde von MediaWiki automatisch gesperrt.\nDer angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der Sperre: $6\n* Sperre betrifft: $7\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
"blockednoreason": "keine Begründung angegeben",
+ "blockedtext-composite": "<strong>Dein Benutzername oder deine IP-Adresse wurde gesperrt.</strong>\n\nDer Angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der längsten Sperre: $6\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
+ "blockedtext-composite-reason": "Es gibt mehrere Sperren gegen dein Benutzerkonto und/oder deine IP-Adresse",
"whitelistedittext": "Du musst dich $1, um Seiten bearbeiten zu können.",
"confirmedittext": "Du musst deine E-Mail-Adresse erst bestätigen, bevor du Bearbeitungen durchführen kannst. Bitte ergänze und bestätige deine E-Mail in den [[Special:Preferences|Einstellungen]].",
"nosuchsectiontitle": "Abschnitt nicht gefunden",
"mw-widgets-abandonedit-title": "Bist du sicher?",
"mw-widgets-copytextlayout-copy": "Kopieren",
"mw-widgets-copytextlayout-copy-fail": "Der Text konnte nicht in die Zwischenablage kopiert werden.",
+ "mw-widgets-copytextlayout-copy-success": "Text in die Zwischenablage kopiert.",
"mw-widgets-dateinput-no-date": "Kein Datum ausgewählt",
"mw-widgets-dateinput-placeholder-day": "JJJJ-MM-TT",
"mw-widgets-dateinput-placeholder-month": "JJJJ-MM",
"restrictionsfield-help": "Eine IP-Adresse oder ein CIDR-Bereich pro Zeile. Um alles zu aktivieren, verwende:\n<pre>\n0.0.0.0/0\n::/0\n</pre>",
"edit-error-short": "Fehler: $1",
"edit-error-long": "Fehler:\n\n$1",
+ "specialmute": "Stumm",
+ "specialmute-success": "Deine Stummschaltungseinstellungen wurden aktualisiert. Schau dir alle stummgeschalteten Benutzer in [[Special:Preferences|deinen Einstellungen]] an.",
+ "specialmute-submit": "Bestätigen",
+ "specialmute-label-mute-email": "E-Mails von diesem Benutzer stummschalten",
+ "specialmute-header": "Bitte wähle deine Stummschaltungseinstellungen für {{BIDI:[[User:$1]]}}.",
+ "specialmute-error-invalid-user": "Der gesuchte Benutzername konnte nicht gefunden werden.",
+ "specialmute-error-email-blacklist-disabled": "Das Stummschalten von E-Mails von Benutzern ist nicht aktiviert.",
+ "specialmute-error-email-preferences": "Du musst deine E-Mail Adresse bestätigen bevor du einen Benutzer bestätigen kannst. Du kannst dies [[Special:Preferences|in deinen Einstellungen]] tun.",
+ "specialmute-email-footer": "Um deine E-Mail Einstellungen für {{BIDI:$2}} zu verwalten besuche bitte $1.",
+ "specialmute-login-required": "Bitte melde dich an um deine Stummschaltungseinstellungen zu ändern.",
"revid": "Version $1",
"pageid": "Seitenkennung $1",
"interfaceadmin-info": "$1\n\nBerechtigungen für das Bearbeiten von wikiweiten CSS/JS/JSON-Dateien wurden kürzlich vom Recht <code>editinterface</code> getrennt. Falls du nicht verstehst, warum du diesen Fehler erhältst, siehe [[mw:MediaWiki_1.32/interface-admin]].",
"logentry-pagelang-pagelang": "$1 {{GENDER:$2|promijenio|promijenila}} je jezik stranice $3 iz $4 u $5.",
"mediastatistics": "Statistika datoteka",
"mediastatistics-summary": "Slijede statistike postavljenih datoteka koje pokazuju zadnju inačicu datoteke. Starije ili izbrisane inačice nisu prikazane.",
- "mediastatistics-nfiles": "$1 ($2%)",
+ "mediastatistics-nfiles": "$1 ($2 %)",
"mediastatistics-nbytes": "{{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3 %)",
"mediastatistics-bytespertype": "Ukupna veličina datoteka za ovaj odlomak: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3%).",
"mediastatistics-allbytes": "Ukupna veličina svih datoteka: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2).",
"specialmute-success": "Vaše postavke utišavanja su uspješno ažurirane. Vidite sve utišane korisnike ovdje: [[Special:Preferences]].",
"specialmute-submit": "Potvrdi",
"specialmute-error-invalid-user": "Korisničko ime koje ste tražili nije moguće pronaći.",
- "specialmute-error-email-preferences": "Morate potvrditi svoju email adresu prije nego što možete utišati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].",
- "specialmute-login-required": "Molimo Vas prijavite se da biste promijenili postavke.",
+ "specialmute-error-email-preferences": "Morate potvrditi svoju adresu e-pošte prije nego što možete utišati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].",
+ "specialmute-login-required": "Molimo Vas, prijavite se da biste promijenili postavke.",
"gotointerwiki": "Napuštate projekt {{SITENAME}}",
"gotointerwiki-invalid": "Navedeni naslov nije valjan.",
"gotointerwiki-external": "Napuštate projekt {{SITENAME}} da biste posjetili zasebno mrežno mjesto [[$2]].\n\n<strong>[$1 Nastavljate na $1]</strong>",
"history": "Riwayat halaman",
"history_short": "Versi terdahulu",
"history_small": "riwayat",
- "updatedmarker": "diubah sejak kunjungan terakhir saya",
+ "updatedmarker": "berubah sejak kunjungan terakhir saya",
"printableversion": "Versi cetak",
"permalink": "Pranala permanen",
"print": "Cetak",
"autoblockedtext": "Alamat IP Anda telah terblokir secara otomatis karena digunakan oleh pengguna lain, yang diblokir oleh $1. Pemblokiran dilakukan dengan alasan:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAnda dapat menghubungi $1 atau [[{{MediaWiki:Grouppage-sysop}}|pengurus]] lainnya untuk membicarakan pemblokiran ini.\n\nAnda tidak dapat menggunakan fitur \"{{int:emailuser}}\" kecuali Anda telah memasukkan alamat surel yang sah di [[Special:Preferences|preferensi akun]] Anda dan Anda tidak diblokir untuk menggunakannya.\n\nAlamat IP Anda saat ini adalah $3, dan ID pemblokiran adalah #$5.\nTolong sertakan informasi-informasi ini dalam setiap pertanyaan Anda.",
"systemblockedtext": "Nama pengguna atau alamat IP Anda telah diblokir secara otomatis oleh MediaWiki.\nAlasan yang diberikan adalah:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAlamat IP Anda saat ini adalah $3\nMohon sertakan semua perincian di atas dalam setiap pertanyaan yang Anda ajukan.",
"blockednoreason": "tidak ada alasan yang diberikan",
+ "blockedtext-composite-reason": "Ada pemblokiran berganda terhadap akun Anda dan/atau alamat IP Anda.",
"whitelistedittext": "Anda harus $1 untuk dapat menyunting halaman.",
"confirmedittext": "Anda harus mengkonfirmasikan dulu alamat surel Anda sebelum menyunting halaman.\nHarap masukkan dan validasikan alamat surel Anda melalui [[Special:Preferences|halaman preferensi pengguna]] Anda.",
"nosuchsectiontitle": "Bagian tidak ditemukan",
"mw-widgets-abandonedit-discard": "Buang suntingan",
"mw-widgets-abandonedit-keep": "Lanjutkan penyuntingan",
"mw-widgets-abandonedit-title": "Apakah Anda yakin?",
+ "mw-widgets-copytextlayout-copy": "Salin",
+ "mw-widgets-copytextlayout-copy-fail": "Gagal menyalin ke papan klip.",
+ "mw-widgets-copytextlayout-copy-success": "Salin ke papan klip.",
"mw-widgets-dateinput-no-date": "Tanggal tidak ada yang terpilih",
"mw-widgets-dateinput-placeholder-day": "TTTT-BB-HH",
"mw-widgets-dateinput-placeholder-month": "TTTT-BB",
"restrictionsfield-help": "Satu alamat IP atau rentang CIDR per baris. Untuk mengaktifkan semuanya, gunakan:\n<pre>0.0.0.0/0\n::/0</pre>",
"edit-error-short": "Galat: $1",
"edit-error-long": "Galat:\n\n$1",
+ "specialmute": "Diam",
+ "specialmute-submit": "Konfirmasi",
"revid": "revisi $1",
"pageid": "ID halaman $1",
"rawhtml-notallowed": "Tag <html> tidak dapat digunakan di luar halaman normal.",
"log-action-filter-suppress-block": "Сокрытие пользователя через блокировки",
"log-action-filter-suppress-reblock": "Сокрытие пользователя через повторное блокирование",
"log-action-filter-upload-upload": "Новая загрузка",
- "log-action-filter-upload-overwrite": "Ð\9fовÑ\82оÑ\80но загÑ\80Ñ\83зиÑ\82Ñ\8c",
- "log-action-filter-upload-revert": "Ð\9eÑ\82каÑ\82иÑ\82Ñ\8c",
+ "log-action-filter-upload-overwrite": "Ð\9fеÑ\80езапиÑ\81Ñ\8c Ñ\84айла",
+ "log-action-filter-upload-revert": "Ð\92озвÑ\80аÑ\82 Ñ\81Ñ\82аÑ\80ой веÑ\80Ñ\81ии Ñ\84айла",
"authmanager-authn-not-in-progress": "Проверка подлинности не выполняется или данные сессии были утеряны. Пожалуйста, начните снова с самого начала.",
"authmanager-authn-no-primary": "Предоставленные учётные данные не могут быть проверены на подлинность.",
"authmanager-authn-no-local-user": "Предоставленные учётные данные не связаны ни с одним участником этой вики.",
"revertmerge": "растави",
"mergelogpagetext": "Испод се налази списак најновијих обједињавања историја једне странице у другу.",
"history-title": "Историја измена странице „$1”",
- "difference-title": "Разлика између измена на страници „$1”",
+ "difference-title": "$1 — разлика између измена",
"difference-title-multipage": "Разлика између страница „$1“ и „$2“",
"difference-multipage": "(разлике између страница)",
"lineno": "Ред $1:",
"svg-long-desc": "SVG датотека, номинално $1 × $2 пиксела, величина: $3",
"svg-long-desc-animated": "Анимирана SVG датотека, номинално: $1 × $2 пиксела, величина: $3",
"svg-long-error": "Неважећа SVG датотека: $1",
- "show-big-image": "Ð\9fÑ\80вобиÑ\82на датотека",
+ "show-big-image": "Ð\9eÑ\80игинална датотека",
"show-big-image-preview": "Величина овог приказа: $1.",
"show-big-image-preview-differ": "Величина $3 прегледа за ову $2 датотеку је $1.",
"show-big-image-other": "$2 {{PLURAL:$2|друга резолуција|друге резолуције|других резолуција}}: $1.",
}
if ( count( $batchPaths ) ) { // left-overs
$this->copyFileBatch( array_keys( $batchPaths ), $backendRel, $src, $dst );
- $batchPaths = []; // done
}
$this->output( "\tCopied $count file(s).\n" );
}
if ( count( $batchPaths ) ) { // left-overs
$this->delFileBatch( array_keys( $batchPaths ), $backendRel, $dst );
- $batchPaths = []; // done
}
$this->output( "\tDeleted $count file(s).\n" );
$ops = [];
$fsFiles = [];
$copiedRel = []; // for output message
- $wikiId = $src->getWikiId();
+ $domainId = $src->getDomainId();
// Download the batch of source files into backend cache...
if ( $this->hasOption( 'missingonly' ) ) {
$srcPath = $src->getRootStoragePath() . "/$backendRel/$srcPathRel";
$dstPath = $dst->getRootStoragePath() . "/$backendRel/$srcPathRel";
if ( $this->hasOption( 'utf8only' ) && !mb_check_encoding( $srcPath, 'UTF-8' ) ) {
- $this->error( "$wikiId: Detected illegal (non-UTF8) path for $srcPath." );
+ $this->error( "$domainId: Detected illegal (non-UTF8) path for $srcPath." );
continue;
} elseif ( !$this->hasOption( 'missingonly' )
&& $this->filesAreSame( $src, $dst, $srcPath, $dstPath )
if ( !$fsFile ) {
$src->clearCache( [ $srcPath ] );
if ( $src->fileExists( [ 'src' => $srcPath, 'latest' => 1 ] ) === false ) {
- $this->error( "$wikiId: File '$srcPath' was listed but does not exist." );
+ $this->error( "$domainId: File '$srcPath' was listed but does not exist." );
} else {
- $this->error( "$wikiId: Could not get local copy of $srcPath." );
+ $this->error( "$domainId: Could not get local copy of $srcPath." );
}
continue;
} elseif ( !$fsFile->exists() ) {
// FSFileBackends just return the path for getLocalReference() and paths with
// illegal slashes may get normalized to a different path. This can cause the
// local reference to not exist...skip these broken files.
- $this->error( "$wikiId: Detected possible illegal path for $srcPath." );
+ $this->error( "$domainId: Detected possible illegal path for $srcPath." );
continue;
}
$fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed
// Note: prepare() is usually fast for key/value backends
$status = $dst->prepare( [ 'dir' => dirname( $dstPath ), 'bypassReadOnly' => 1 ] );
if ( !$status->isOK() ) {
- $this->error( print_r( $status->getErrorsArray(), true ) );
- $this->fatalError( "$wikiId: Could not copy $srcPath to $dstPath." );
+ $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+ $this->fatalError( "$domainId: Could not copy $srcPath to $dstPath." );
}
$ops[] = [ 'op' => 'store',
'src' => $fsFile->getPath(), 'dst' => $dstPath, 'overwrite' => 1 ];
}
$elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
if ( !$status->isOK() ) {
- $this->error( print_r( $status->getErrorsArray(), true ) );
- $this->fatalError( "$wikiId: Could not copy file batch." );
+ $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+ $this->fatalError( "$domainId: Could not copy file batch." );
} elseif ( count( $copiedRel ) ) {
$this->output( "\n\tCopied these file(s) [{$elapsed_ms}ms]:\n\t" .
implode( "\n\t", $copiedRel ) . "\n\n" );
) {
$ops = [];
$deletedRel = []; // for output message
- $wikiId = $dst->getWikiId();
+ $domainId = $dst->getDomainId();
// Determine what files need to be copied over...
foreach ( $dstPathsRel as $dstPathRel ) {
}
$elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
if ( !$status->isOK() ) {
- $this->error( print_r( $status->getErrorsArray(), true ) );
- $this->fatalError( "$wikiId: Could not delete file batch." );
+ $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+ $this->fatalError( "$domainId: Could not delete file batch." );
} elseif ( count( $deletedRel ) ) {
$this->output( "\n\tDeleted these file(s) [{$elapsed_ms}ms]:\n\t" .
implode( "\n\t", $deletedRel ) . "\n\n" );
if ( !$exists ) {
# Increment site_stats.ss_users
- $ssu = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+ $ssu = SiteStatsUpdate::factory( [ 'users' => 1 ] );
$ssu->doUpdate();
}
if ( $delete ) {
$this->output( "Updating site stats..." );
$ga = $isGoodArticle ? -1 : 0; // if it was good, decrement that too
- $stats = new SiteStatsUpdate( 0, -$count, $ga, -1 );
+ $stats = SiteStatsUpdate::factory( [
+ 'edits' => -$count,
+ 'articles' => $ga,
+ 'pages' => -1
+ ] );
$stats->doUpdate();
$this->output( "done.\n" );
}
);
}
+ /**
+ * @covers MapCacheLRU::has()
+ * @covers MapCacheLRU::get()
+ * @covers MapCacheLRU::set()
+ */
+ function testMissing() {
+ $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
+ $cache = MapCacheLRU::newFromArray( $raw, 3 );
+
+ $this->assertFalse( $cache->has( 'd' ) );
+ $this->assertNull( $cache->get( 'd' ) );
+ $this->assertNull( $cache->get( 'd', 0.0, null ) );
+ $this->assertFalse( $cache->get( 'd', 0.0, false ) );
+ }
+
/**
* @covers MapCacheLRU::has()
* @covers MapCacheLRU::get()
--- /dev/null
+<?php
+
+/**
+ * Holds tests for FakeResultWrapper MediaWiki class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use Wikimedia\Rdbms\FakeResultWrapper;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\FakeResultWrapper
+ */
+class FakeResultWrapperTest extends PHPUnit\Framework\TestCase {
+ public function testIteration() {
+ $res = new FakeResultWrapper( [
+ [ 'colA' => 1, 'colB' => 'a' ],
+ [ 'colA' => 2, 'colB' => 'b' ],
+ (object)[ 'colA' => 3, 'colB' => 'c' ],
+ [ 'colA' => 4, 'colB' => 'd' ],
+ [ 'colA' => 5, 'colB' => 'e' ],
+ (object)[ 'colA' => 6, 'colB' => 'f' ],
+ (object)[ 'colA' => 7, 'colB' => 'g' ],
+ [ 'colA' => 8, 'colB' => 'h' ]
+ ] );
+
+ $expectedRows = [
+ 0 => (object)[ 'colA' => 1, 'colB' => 'a' ],
+ 1 => (object)[ 'colA' => 2, 'colB' => 'b' ],
+ 2 => (object)[ 'colA' => 3, 'colB' => 'c' ],
+ 3 => (object)[ 'colA' => 4, 'colB' => 'd' ],
+ 4 => (object)[ 'colA' => 5, 'colB' => 'e' ],
+ 5 => (object)[ 'colA' => 6, 'colB' => 'f' ],
+ 6 => (object)[ 'colA' => 7, 'colB' => 'g' ],
+ 7 => (object)[ 'colA' => 8, 'colB' => 'h' ]
+ ];
+
+ $this->assertEquals( 8, $res->numRows() );
+
+ $res->seek( 7 );
+ $this->assertEquals( [ 'colA' => 8, 'colB' => 'h' ], $res->fetchRow() );
+ $res->seek( 7 );
+ $this->assertEquals( (object)[ 'colA' => 8, 'colB' => 'h' ], $res->fetchObject() );
+
+ $this->assertEquals( $expectedRows, iterator_to_array( $res, true ) );
+
+ $rows = [];
+ foreach ( $res as $i => $row ) {
+ $rows[$i] = $row;
+ }
+ $this->assertEquals( $expectedRows, $rows );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Holds tests for ResultWrapper MediaWiki class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\ResultWrapper
+ */
+class ResultWrapperTest extends PHPUnit\Framework\TestCase {
+ /**
+ * @return IDatabase
+ * @param array[] $rows
+ */
+ private function getDatabaseMock( array $rows ) {
+ $db = $this->getMockBuilder( IDatabase::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $db->method( 'select' )->willReturnCallback(
+ function () use ( $db, $rows ) {
+ return new ResultWrapper( $db, $rows );
+ }
+ );
+ $db->method( 'dataSeek' )->willReturnCallback(
+ function ( ResultWrapper $res, $pos ) use ( $db ) {
+ // Position already set in ResultWrapper
+ }
+ );
+ $db->method( 'fetchRow' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ $row = $res::unwrap( $res )[$res->key()] ?? false;
+
+ return $row;
+ }
+ );
+ $db->method( 'fetchObject' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ $row = $res::unwrap( $res )[$res->key()] ?? false;
+
+ return $row ? (object)$row : false;
+ }
+ );
+ $db->method( 'numRows' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ return count( $res::unwrap( $res ) );
+ }
+ );
+
+ return $db;
+ }
+
+ public function testIteration() {
+ $db = $this->getDatabaseMock( [
+ [ 'colA' => 1, 'colB' => 'a' ],
+ [ 'colA' => 2, 'colB' => 'b' ],
+ [ 'colA' => 3, 'colB' => 'c' ],
+ [ 'colA' => 4, 'colB' => 'd' ],
+ [ 'colA' => 5, 'colB' => 'e' ],
+ [ 'colA' => 6, 'colB' => 'f' ],
+ [ 'colA' => 7, 'colB' => 'g' ],
+ [ 'colA' => 8, 'colB' => 'h' ]
+ ] );
+
+ $expectedRows = [
+ 0 => (object)[ 'colA' => 1, 'colB' => 'a' ],
+ 1 => (object)[ 'colA' => 2, 'colB' => 'b' ],
+ 2 => (object)[ 'colA' => 3, 'colB' => 'c' ],
+ 3 => (object)[ 'colA' => 4, 'colB' => 'd' ],
+ 4 => (object)[ 'colA' => 5, 'colB' => 'e' ],
+ 5 => (object)[ 'colA' => 6, 'colB' => 'f' ],
+ 6 => (object)[ 'colA' => 7, 'colB' => 'g' ],
+ 7 => (object)[ 'colA' => 8, 'colB' => 'h' ]
+ ];
+
+ $res = $db->select( 'faketable', [ 'colA', 'colB' ], '1 = 1', __METHOD__ );
+ $this->assertEquals( 8, $res->numRows() );
+
+ $res->seek( 7 );
+ $this->assertEquals( [ 'colA' => 8, 'colB' => 'h' ], $res->fetchRow() );
+ $res->seek( 7 );
+ $this->assertEquals( (object)[ 'colA' => 8, 'colB' => 'h' ], $res->fetchObject() );
+
+ $this->assertEquals( $expectedRows, iterator_to_array( $res, true ) );
+
+ $rows = [];
+ foreach ( $res as $i => $row ) {
+ $rows[$i] = $row;
+ }
+ $this->assertEquals( $expectedRows, $rows );
+ }
+}