configurable via $wgDebugLogFile.
* $wgPasswordSalt – This setting, used for migrating exceptionally old, insecure
password setups and deprecated since 1.24, is now removed.
+* $wgDBOracleDRCP - If you must use persistent connections, set DBO_PERSISTENT
+ in the 'flags' field for servers in $wgDBServers (or $wgLBFactoryConf).
=== New user-facing features in 1.34 ===
* Special:Mute has been added as a quick way for users to block unwanted emails
deprecated since 1.33.
* The static properties mw.Api.errors and mw.Api.warnings, deprecated in 1.29,
have been removed.
+* ParserOption::getSpeculativeRevIdCallback(), deprecated in 1.28, has been
+ removed.
* The UploadVerification hook, deprecated in 1.28, has been removed. Instead,
use the UploadVerifyFile hook.
* UploadBase:: and UploadFromChunks::stashFileGetKey() and stashSession(),
deprecated in 1.28, have been removed. Instead, please use the getFileKey()
method on the response from doStashFile().
+* LBFactory::setDomainPrefix() and LoadBalancer::setDomainPrefix(), deprecated
+ in 1.33, have been removed. Use setLocalDomainPrefix() instead.
+* IDatabase::implicitGroupby(), deprecated in 1.30, has been removed.
+* IDatabase::doneWrites(), deprecated in 1.31, has been removed.
+ Use IDatabase::lastDoneWrites() instead.
+* Database::reportConnectionError(), deprecated in 1.32, has been removed.
+* LoadBalancer::laggedSlaveUsed(), deprecated in 1.28, has been removed.
+ Use LoadBalancer::laggedReplicaUsed() instead.
+* Database::getProperty(), deprecated in 1.28, has been removed.
+* IDatabase::getWikiId(), deprecated in 1.30, has been removed.
+ Use IDatabase::getDomainID() instead.
* …
=== Deprecations in 1.34 ===
PermissionManager::getUserPermissions() instead.
* The LocalisationCacheRecache hook no longer allows purging of message blobs
to be prevented. Modifying the $purgeBlobs parameter now has no effect.
+* The use of $wgProxyList with IP addresses in the array keys, deprecated in
+ 1.30, was removed. Instead, $wgProxyList should be an array with IP addresses
+ as the values, or a string path to a file containing one IP address per line.
+* SVGMetadataExtractor::getMetadata has been deprecated. Instead, you should
+ use SVGReader->getMetadata() directly.
=== Other changes in 1.34 ===
* …
case APCOND_IPINRANGE:
return IP::isInRange( $user->getRequest()->getIP(), $cond[1] );
case APCOND_BLOCKED:
- // @TODO Should partial blocks prevent auto promote?
- return (bool)$user->getBlock();
+ return $user->getBlock() && $user->getBlock()->isSitewide();
case APCOND_ISBOT:
return in_array( 'bot', User::getGroupPermissions( $user->getGroups() ) );
default:
* sent to it. It will be excluded from lag checks in maintenance scripts.
* The only way it can receive traffic is if groupLoads is used.
*
- * - groupLoads: array of load ratios, the key is the query group name. A query may belong
- * to several groups, the most specific group defined here is used.
- *
- * - flags: bit field
- * - DBO_DEFAULT -- turns on DBO_TRX only if "cliMode" is off (recommended)
- * - DBO_DEBUG -- equivalent of $wgDebugDumpSql
- * - DBO_TRX -- wrap entire request in a transaction
- * - DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php)
- * - DBO_PERSISTENT -- enables persistent database connections
- * - DBO_SSL -- uses SSL/TLS encryption in database connections, if available
- * - DBO_COMPRESS -- uses internal compression in database connections,
- * if available
+ * - groupLoads: (optional) Array of load ratios, the key is the query group name. A query
+ * may belong to several groups, the most specific group defined here is used.
+ *
+ * - flags: (optional) Bit field of properties:
+ * - DBO_DEFAULT: Transactionalize web requests and use autocommit otherwise
+ * - DBO_DEBUG: Equivalent of $wgDebugDumpSql
+ * - DBO_SSL: Use TLS connection encryption if available
+ * - DBO_COMPRESS: Use protocol compression with database connections
+ * - DBO_PERSISTENT: Enables persistent database connections
*
* - max lag: (optional) Maximum replication lag before a replica DB goes out of rotation
* - is static: (optional) Set to true if the dataset is static and no replication is used.
* - cliMode: (optional) Connection handles will not assume that requests are short-lived
* nor that INSERT..SELECT can be rewritten into a buffered SELECT and INSERT.
+ * This is what DBO_DEFAULT uses to determine when a web request is present.
* [Default: uses value of $wgCommandLineMode]
*
* These and any other user-defined properties will be assigned to the mLBInfo member
*/
$wgDBerrorLogTZ = false;
-/**
- * Set true to enable Oracle DCRP (supported from 11gR1 onward)
- *
- * To use this feature set to true and use a datasource defined as
- * POOLED (i.e. in tnsnames definition set server=pooled in connect_data
- * block).
- *
- * Starting from 11gR1 you can use DCRP (Database Resident Connection
- * Pool) that maintains established sessions and reuses them on new
- * connections.
- *
- * Not completely tested, but it should fall back on normal connection
- * in case the pool is full or the datasource is not configured as
- * pooled.
- * And the other way around; using oci_pconnect on a non pooled
- * datasource should produce a normal connection.
- *
- * When it comes to frequent shortlived DB connections like with MW
- * Oracle tends to s***. The problem is the driver connects to the
- * database reasonably fast, but establishing a session takes time and
- * resources. MW does not rely on session state (as it does not use
- * features such as package variables) so establishing a valid session
- * is in this case an unwanted overhead that just slows things down.
- *
- * @warning EXPERIMENTAL!
- */
-$wgDBOracleDRCP = false;
-
/**
* Other wikis on this site, can be administered from a single developer account.
*
* - [ APCOND_IPINRANGE, range ]:
* true if the user has an IP address in the range of the passed parameter
* - [ APCOND_BLOCKED ]:
- * true if the user is blocked
+ * true if the user is sitewide blocked
* - [ APCOND_ISBOT ]:
* true if the user is a bot
* - similar constructs can be defined by extensions
* Big list of banned IP addresses.
*
* This can have the following formats:
- * - An array of addresses, either in the values
- * or the keys (for backward compatibility, deprecated since 1.30)
- * - A string, in that case this is the path to a file
+ * - An array of addresses
+ * - A string, in which case this is the path to a file
* containing the list of IP addresses, one per line
*/
$wgProxyList = [];
* @todo Replace calls to wfGetDB with calls to LoadBalancer::getConnection()
* on an injected instance of LoadBalancer.
*
- * @return \Wikimedia\Rdbms\Database
+ * @return \Wikimedia\Rdbms\DBConnRef
*/
function wfGetDB( $db, $groups = [], $wiki = false ) {
- return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki );
+ return wfGetLB( $wiki )->getMaintenanceConnectionRef( $db, $groups, $wiki );
}
/**
$this->setRevisionInternal( $rev );
$this->pruneRevisionSensitiveOutput(
+ $this->revision->getPageId(),
$this->revision->getId(),
$this->revision->getTimestamp()
);
/**
* Prune any output that depends on the revision ID.
*
+ * @param int|bool $actualPageId The actual page id, to check the used speculative page ID
+ * against; false, to not purge on vary-page-id; true, to purge on vary-page-id
+ * unconditionally.
* @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID
- * against, or false to not purge on vary-revision-id, or true to purge on
+ * against,; false, to not purge on vary-revision-id; true, to purge on
* vary-revision-id unconditionally.
* @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the
- * parser output revision timestamp, or false to not purge on vary-revision-timestamp
+ * parser output revision timestamp; false, to not purge on vary-revision-timestamp;
+ * true, to purge on vary-revision-timestamp unconditionally.
*/
- private function pruneRevisionSensitiveOutput( $actualRevId, $actualRevTimestamp ) {
+ private function pruneRevisionSensitiveOutput(
+ $actualPageId,
+ $actualRevId,
+ $actualRevTimestamp
+ ) {
if ( $this->revisionOutput ) {
if ( $this->outputVariesOnRevisionMetaData(
$this->revisionOutput,
+ $actualPageId,
$actualRevId,
$actualRevTimestamp
) ) {
$this->revisionOutput = null;
}
} else {
- $this->saveParseLogger->debug( __METHOD__ . ": no prepared revision output...\n" );
+ $this->saveParseLogger->debug( __METHOD__ . ": no prepared revision output" );
}
foreach ( $this->slotsOutput as $role => $output ) {
if ( $this->outputVariesOnRevisionMetaData(
$output,
+ $actualPageId,
$actualRevId,
$actualRevTimestamp
) ) {
/**
* @param ParserOutput $out
- * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID
- * against, false to not purge on vary-revision-id, or true to purge on
+ * @param int|bool $actualPageId The actual page id, to check the used speculative page ID
+ * against; false, to not purge on vary-page-id; true, to purge on vary-page-id
+ * unconditionally.
+ * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID
+ * against,; false, to not purge on vary-revision-id; true, to purge on
* vary-revision-id unconditionally.
* @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the
- * parser output revision timestamp, false to not purge on vary-revision-timestamp,
- * or true to purge on vary-revision-timestamp unconditionally.
+ * parser output revision timestamp; false, to not purge on vary-revision-timestamp;
+ * true, to purge on vary-revision-timestamp unconditionally.
* @return bool
*/
private function outputVariesOnRevisionMetaData(
ParserOutput $out,
+ $actualPageId,
$actualRevId,
$actualRevTimestamp
) {
- $method = __METHOD__;
+ $logger = $this->saveParseLogger;
+ $varyMsg = __METHOD__ . ": cannot use prepared output for '{title}'";
+ $context = [ 'title' => $this->title->getPrefixedText() ];
if ( $out->getFlag( 'vary-revision' ) ) {
// If {{PAGEID}} resolved to 0, then that word need to resolve to the actual page ID
- $this->saveParseLogger->info(
- "$method: Prepared output has vary-revision..."
- );
+ $logger->info( "$varyMsg (vary-revision)", $context );
return true;
- } elseif ( $out->getFlag( 'vary-revision-id' )
+ } elseif (
+ $out->getFlag( 'vary-revision-id' )
&& $actualRevId !== false
&& ( $actualRevId === true || $out->getSpeculativeRevIdUsed() !== $actualRevId )
) {
- $this->saveParseLogger->info(
- "$method: Prepared output has vary-revision-id with wrong ID..."
- );
+ $logger->info( "$varyMsg (vary-revision-id and wrong ID)", $context );
return true;
- } elseif ( $out->getFlag( 'vary-revision-timestamp' )
+ } elseif (
+ $out->getFlag( 'vary-revision-timestamp' )
&& $actualRevTimestamp !== false
&& ( $actualRevTimestamp === true ||
$out->getRevisionTimestampUsed() !== $actualRevTimestamp )
) {
- $this->saveParseLogger->info(
- "$method: Prepared output has vary-revision-timestamp with wrong timestamp..."
- );
+ $logger->info( "$varyMsg (vary-revision-timestamp and wrong timestamp)", $context );
+ return true;
+ } elseif (
+ $out->getFlag( 'vary-page-id' )
+ && $actualPageId !== false
+ && ( $actualPageId === true || $out->getSpeculativePageIdUsed() !== $actualPageId )
+ ) {
+ $logger->info( "$varyMsg (vary-page-id and wrong ID)", $context );
return true;
} elseif ( $out->getFlag( 'vary-revision-exists' ) ) {
// If {{REVISIONID}} resolved to '', it now needs to resolve to '-'.
// Note that edit stashing always uses '-', which can be used for both
// edit filter checks and canonical parser cache.
- $this->saveParseLogger->info(
- "$method: Prepared output has vary-revision-exists..."
- );
+ $logger->info( "$varyMsg (vary-revision-exists)", $context );
return true;
} elseif (
$out->getFlag( 'vary-revision-sha1' ) &&
) {
// If a self-transclusion used the proposed page text, it must match the final
// page content after PST transformations and automatically merged edit conflicts
- $this->saveParseLogger->info(
- "$method: Prepared output has vary-revision-sha1 with wrong SHA-1..."
- );
+ $logger->info( "$varyMsg (vary-revision-sha1 with wrong SHA-1)" );
return true;
- } else {
- // NOTE: In the original fix for T135261, the output was discarded if 'vary-user' was
- // set for a null-edit. The reason was that the original rendering in that case was
- // targeting the user making the null-edit, not the user who made the original edit,
- // causing {{REVISIONUSER}} to return the wrong name.
- // This case is now expected to be handled by the code in RevisionRenderer that
- // constructs the ParserOptions: For a null-edit, setCurrentRevisionCallback is called
- // with the old, existing revision.
-
- $this->saveParseLogger->debug( "$method: Keeping prepared output..." );
- return false;
}
- }
+ // NOTE: In the original fix for T135261, the output was discarded if 'vary-user' was
+ // set for a null-edit. The reason was that the original rendering in that case was
+ // targeting the user making the null-edit, not the user who made the original edit,
+ // causing {{REVISIONUSER}} to return the wrong name.
+ // This case is now expected to be handled by the code in RevisionRenderer that
+ // constructs the ParserOptions: For a null-edit, setCurrentRevisionCallback is called
+ // with the old, existing revision.
+ $logger->debug( __METHOD__ . ": reusing prepared output for '{title}'", $context );
+ return false;
+ }
}
$options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
return $this->getSpeculativeRevId( $dbIndex );
} );
+ $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) {
+ return $this->getSpeculativePageId( $dbIndex );
+ } );
if ( !$rev->getId() && $rev->getTimestamp() ) {
// This is an unsaved revision with an already determined timestamp.
// HACK: But don't use a fresh connection in unit tests, since it would not have
// the fake tables. This should be handled by the LoadBalancer!
$flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA
- ? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
+ ? 0
+ : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
$db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags );
);
}
+ private function getSpeculativePageId( $dbIndex ) {
+ // Use a fresh master connection in order to see the latest data, by avoiding
+ // stale data from REPEATABLE-READ snapshots.
+ // HACK: But don't use a fresh connection in unit tests, since it would not have
+ // the fake tables. This should be handled by the LoadBalancer!
+ $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA
+ ? 0
+ : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
+
+ $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->wikiId, $flags );
+
+ return 1 + (int)$db->selectField(
+ 'page',
+ 'MAX(page_id)',
+ [],
+ __METHOD__
+ );
+ }
+
/**
* This implements the layout for combining the output of multiple slots.
*
*/
private function getDBConnection( $mode, $groups = [] ) {
$lb = $this->getDBLoadBalancer();
- return $lb->getConnection( $mode, $groups, $this->dbDomain );
+ return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
}
/**
'LocalServerObjectCache' => function ( MediaWikiServices $services ) : BagOStuff {
$cacheId = \ObjectCache::detectLocalServerCache();
+
return \ObjectCache::newFromId( $cacheId );
},
wfUrlProtocols(),
$services->getSpecialPageFactory(),
$services->getLinkRendererFactory(),
- $services->getNamespaceInfo()
+ $services->getNamespaceInfo(),
+ LoggerFactory::getInstance( 'Parser' )
);
},
'SiteStore' => function ( MediaWikiServices $services ) : SiteStore {
$rawSiteStore = new DBSiteStore( $services->getDBLoadBalancer() );
- // TODO: replace wfGetCache with a CacheFactory service.
- // TODO: replace wfIsHHVM with a capabilities service.
- $cache = wfGetCache( wfIsHHVM() ? CACHE_ACCEL : CACHE_ANYTHING );
+ $cache = $services->getLocalServerObjectCache();
+ if ( $cache instanceof EmptyBagOStuff ) {
+ $cache = ObjectCache::getLocalClusterInstance();
+ }
return new CachingSiteStore( $rawSiteStore, $cache );
},
* @return IDatabase
*/
private static function getDB( $index, $groups = [] ) {
- $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-
- return $lb->getConnection( $index, $groups );
+ return MediaWikiServices::getInstance()
+ ->getDBLoadBalancer()
+ ->getConnectionRef( $index, $groups );
}
}
// This can be used for the initial parse, e.g. for filters or doEditContent(),
// but a second parse will be triggered in doEditUpdates() no matter what
$logger->info(
- "Cache for key '{key}' has 'vary-revision'; post-insertion parse inevitable.",
+ "Cache for key '{key}' has vary-revision; post-insertion parse inevitable.",
$context
);
} else {
// Similar to the above if we didn't guess the timestamp correctly
'vary-revision-timestamp',
// Similar to the above if we didn't guess the content correctly
- 'vary-revision-sha1'
+ 'vary-revision-sha1',
+ // Similar to the above if we didn't guess page ID correctly
+ 'vary-page-id'
];
foreach ( $flagsMaybeReparse as $flag ) {
if ( $editInfo->output->getFlag( $flag ) ) {
*/
private function getDBConnection( $index ) {
$lb = $this->getDBLoadBalancer();
- return $lb->getConnection( $index, [], $this->dbDomain );
+ return $lb->getConnectionRef( $index, [], $this->dbDomain );
}
/**
/**
* @param Title $title
- * @param Title $origTitle
+ * @param ForeignTitle $foreignTitle
* @param int $revisionCount
* @param int $successCount
* @param array $pageInfo
* @return void
*/
- public function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) {
+ public function reportPage( $title, $foreignTitle, $revisionCount, $successCount, $pageInfo ) {
// Add a result entry
$r = [];
$this->mResultArr[] = $r;
// Piggyback on the parent to do the logging
- parent::reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo );
+ parent::reportPage( $title, $foreignTitle, $revisionCount, $successCount, $pageInfo );
}
public function getData() {
// Printer may not be initialized if the extractRequestParams() fails for the main module
$this->createErrorPrinter();
+ // Get desired HTTP code from an ApiUsageException. Don't use codes from other
+ // exception types, as they are unlikely to be intended as an HTTP code.
+ $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0;
+
$failed = false;
try {
- $this->printResult( $e->getCode() );
+ $this->printResult( $httpCode );
} catch ( ApiUsageException $ex ) {
// The error printer itself is failing. Try suppressing its request
// parameters and redo.
$this->mPrinter = null;
$this->createErrorPrinter();
$this->mPrinter->forceDefaultParams();
- if ( $e->getCode() ) {
+ if ( $httpCode ) {
$response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
}
- $this->printResult( $e->getCode() );
+ $this->printResult( $httpCode );
}
}
* @return array
*/
protected function getApiWarnings() {
- $warnings = $this->mUpload->checkWarnings();
+ $warnings = UploadBase::makeWarningsSerializable( $this->mUpload->checkWarnings() );
return $this->transformWarnings( $warnings );
}
if ( isset( $warnings['duplicate'] ) ) {
$dupes = [];
- /** @var File $dupe */
foreach ( $warnings['duplicate'] as $dupe ) {
- $dupes[] = $dupe->getName();
+ $dupes[] = $dupe['fileName'];
}
ApiResult::setIndexedTagName( $dupes, 'duplicate' );
$warnings['duplicate'] = $dupes;
if ( isset( $warnings['exists'] ) ) {
$warning = $warnings['exists'];
unset( $warnings['exists'] );
- /** @var LocalFile $localFile */
$localFile = $warning['normalizedFile'] ?? $warning['file'];
- $warnings[$warning['warning']] = $localFile->getName();
+ $warnings[$warning['warning']] = $localFile['fileName'];
}
if ( isset( $warnings['no-change'] ) ) {
- /** @var File $file */
$file = $warnings['no-change'];
unset( $warnings['no-change'] );
$warnings['nochange'] = [
- 'timestamp' => wfTimestamp( TS_ISO_8601, $file->getTimestamp() )
+ 'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
];
}
if ( isset( $warnings['duplicate-version'] ) ) {
$dupes = [];
- /** @var File $dupe */
foreach ( $warnings['duplicate-version'] as $dupe ) {
$dupes[] = [
- 'timestamp' => wfTimestamp( TS_ISO_8601, $dupe->getTimestamp() )
+ 'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
];
}
unset( $warnings['duplicate-version'] );
$proxyList = array_map( 'trim', file( $proxyList ) );
}
- $resultProxyList = [];
- $deprecatedIPEntries = [];
-
- // backward compatibility: move all ip addresses in keys to values
- foreach ( $proxyList as $key => $value ) {
- $keyIsIP = IP::isIPAddress( $key );
- $valueIsIP = IP::isIPAddress( $value );
- if ( $keyIsIP && !$valueIsIP ) {
- $deprecatedIPEntries[] = $key;
- $resultProxyList[] = $key;
- } elseif ( $keyIsIP && $valueIsIP ) {
- $deprecatedIPEntries[] = $key;
- $resultProxyList[] = $key;
- $resultProxyList[] = $value;
- } else {
- $resultProxyList[] = $value;
- }
- }
-
- if ( $deprecatedIPEntries ) {
- wfDeprecated(
- 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
- implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
- }
-
- $proxyListIPSet = new IPSet( $resultProxyList );
+ $proxyListIPSet = new IPSet( $proxyList );
return $proxyListIPSet->match( $ip );
}
* using the global Parser service.
*
* @param Title $title
- * @param int $revId Revision to pass to the parser (default: null)
+ * @param int|null $revId Revision to pass to the parser (default: null)
* @param ParserOptions $options (default: null)
* @param bool $generateHtml (default: true)
* @param ParserOutput &$output ParserOutput representing the HTML form of the text,
use Wikimedia\Rdbms\Blob;
use Wikimedia\Rdbms\ResultWrapper;
use Wikimedia\Rdbms\IResultWrapper;
-use Wikimedia\Rdbms\DBConnectionError;
use Wikimedia\Rdbms\DBUnexpectedError;
use Wikimedia\Rdbms\DBExpectedError;
return 'oracle';
}
- function implicitGroupby() {
- return false;
- }
-
function implicitOrderby() {
return false;
}
protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
if ( !function_exists( 'oci_connect' ) ) {
- throw new DBConnectionError(
- $this,
+ throw $this->newExceptionAfterConnectError(
"Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n " .
- "(Note: if you recently installed PHP, you may need to restart your webserver\n " .
- "and database)\n" );
+ "(Note: if you recently installed PHP, you may need to restart your webserver\n " .
+ "and database)"
+ );
}
+ $this->close();
+
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 uses the *database* aspect of $domain for schema, not the domain schema
+ throw $this->newExceptionAfterConnectError(
+ "Got schema '$schema'; not supported. " .
+ "The database component '$dbName' is actually interpreted as the Oracle schema."
);
}
- $this->close();
$this->user = $user;
$this->password = $password;
- if ( !$server ) {
- // Backward compatibility (server used to be null and TNS was supplied in dbname)
+ if ( strlen( $server ) ) {
+ // Transparent Network Substrate (TNS) endpoint
+ $this->server = $server;
+ // Database name, defaulting to the user name
+ $realDatabase = strlen( $dbName ) ? $dbName : $user;
+ } else {
+ // Backward compatibility; $server used to be null and $dbName was the TNS
$this->server = $dbName;
$realDatabase = $user;
- } else {
- // $server now holds the TNS endpoint
- $this->server = $server;
- // $dbName is schema name if different from username
- $realDatabase = $dbName ?: $user;
- }
-
- if ( !strlen( $user ) ) { # e.g. the class is being loaded
- return null;
}
-
$session_mode = ( $this->flags & DBO_SYSDBA ) ? OCI_SYSDBA : OCI_DEFAULT;
- Wikimedia\suppressWarnings();
- if ( $this->flags & DBO_PERSISTENT ) {
- $this->conn = oci_pconnect(
- $this->user,
- $this->password,
- $this->server,
- $this->defaultCharset,
- $session_mode
- );
- } elseif ( $this->flags & DBO_DEFAULT ) {
- $this->conn = oci_new_connect(
- $this->user,
- $this->password,
- $this->server,
- $this->defaultCharset,
- $session_mode
- );
- } else {
- $this->conn = oci_connect(
- $this->user,
- $this->password,
- $this->server,
- $this->defaultCharset,
- $session_mode
- );
- }
- Wikimedia\restoreWarnings();
-
- if ( $this->user != $realDatabase ) {
- // change current schema in session
- $this->selectDB( $realDatabase );
- } else {
- $this->currentDomain = new DatabaseDomain(
- $realDatabase,
- null,
- $tablePrefix
- );
- }
+ $this->installErrorHandler();
+ try {
+ $this->conn = $this->getFlag( DBO_PERSISTENT )
+ ? oci_pconnect(
+ $this->user,
+ $this->password,
+ $this->server,
+ $this->defaultCharset,
+ $session_mode
+ )
+ : oci_new_connect(
+ $this->user,
+ $this->password,
+ $this->server,
+ $this->defaultCharset,
+ $session_mode
+ );
+ } catch ( Exception $e ) {
+ $this->restoreErrorHandler();
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
+ }
+ $error = $this->restoreErrorHandler();
if ( !$this->conn ) {
- throw new DBConnectionError( $this, $this->lastError() );
+ throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
}
- # removed putenv calls because they interfere with the system globaly
- $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
- $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
- $this->doQuery( 'ALTER SESSION SET NLS_NUMERIC_CHARACTERS=\'.,\'' );
-
- return (bool)$this->conn;
+ try {
+ if ( $this->user != $realDatabase ) {
+ // Change current schema for the entire session
+ $this->selectDomain( new DatabaseDomain(
+ $realDatabase,
+ $this->currentDomain->getSchema(),
+ $this->currentDomain->getTablePrefix()
+ ) );
+ } else {
+ $this->currentDomain = new DatabaseDomain( $realDatabase, null, $tablePrefix );
+ }
+ $set = [
+ 'NLS_TIMESTAMP_FORMAT' => 'DD-MM-YYYY HH24:MI:SS.FF6',
+ 'NLS_TIMESTAMP_TZ_FORMAT' => 'DD-MM-YYYY HH24:MI:SS.FF6',
+ 'NLS_NUMERIC_CHARACTERS' => '.,'
+ ];
+ foreach ( $set as $var => $val ) {
+ $this->query(
+ "ALTER SESSION SET {$var}=" . $this->addQuotes( $val ),
+ __METHOD__,
+ self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY
+ );
+ }
+ } catch ( Exception $e ) {
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
+ }
}
/**
$flags = DBO_DEFAULT;
$flags |= $options->get( 'DebugDumpSql' ) ? DBO_DEBUG : 0;
$flags |= $options->get( 'DebugLogFile' ) ? DBO_DEBUG : 0;
- if ( $server['type'] === 'oracle' ) {
- $flags |= $options->get( 'DBOracleDRCP' ) ? DBO_PERSISTENT : 0;
- }
$server += [
'tablePrefix' => $options->get( 'DBprefix' ),
* @ingroup Dump
*/
class DumpFileOutput extends DumpOutput {
- protected $handle = false, $filename;
+ /** @var resource|false */
+ protected $handle = false;
+ /** @var string */
+ protected $filename;
/**
* @param string $file
}
/**
- * @param array $newname
+ * @param string|string[] $newname
* @return string
* @throws MWException
*/
* various row objects and XML output for filtering. Filters
* can be chained or used as callbacks.
*
- * @param DumpOutput &$sink
+ * @param DumpOutput|DumpFilter &$sink
*/
public function setOutputSink( &$sink ) {
$this->sink =& $sink;
* Not called by default (depends on $this->list_authors)
* Can be set by Special:Export when not exporting whole history
*
- * @param array $cond
+ * @param string $cond
*/
protected function do_list_authors( $cond ) {
$this->author_list = "<contributors>";
/**
* Notify the callback function when a new "<page>" is reached.
- * @param Title $title
+ * @param array $title
*/
function pageCallback( $title ) {
if ( isset( $this->mPageCallback ) ) {
/**
* @since 1.12.2
- * @param array $params
+ * @param string $params
*/
public function setParams( $params ) {
$this->params = $params;
// We can only get warnings like 'duplicate' after concatenating the chunks
$status = Status::newGood();
- $status->value = [ 'warnings' => $upload->checkWarnings() ];
+ $status->value = [
+ 'warnings' => UploadBase::makeWarningsSerializable( $upload->checkWarnings() )
+ ];
// We have a new filekey for the fully concatenated file
$newFileKey = $upload->getStashFile()->getFileKey();
// Check for ZIP variants (before getimagesize)
$eocdrPos = strpos( $tail, "PK\x05\x06" );
- if ( $eocdrPos !== false ) {
+ if ( $eocdrPos !== false && $eocdrPos <= strlen( $tail ) - 22 ) {
$this->logger->info( __METHOD__ . ": ZIP signature present in $file\n" );
// Check if it really is a ZIP file, make sure the EOCDR is at the end (T40432)
- $commentLength = unpack( "n", substr( $tail, $eocdrPos + 20 ) )[0];
+ $commentLength = unpack( "n", substr( $tail, $eocdrPos + 20 ) )[1];
if ( $eocdrPos + 22 + $commentLength !== strlen( $tail ) ) {
$this->logger->info( __METHOD__ . ": ZIP EOCDR not at end. Not a ZIP file." );
} else {
/**
* Get an item with the given key, regenerating and setting it if not found
*
- * Nothing is stored nor deleted if the callback returns false
+ * The callback can take $ttl as argument by reference and modify it.
+ * Nothing is stored nor deleted if the callback returns false.
*
* @param string $key
* @param int $ttl Time-to-live (seconds)
$value = $this->get( $key, $flags );
if ( $value === false ) {
- if ( !is_callable( $callback ) ) {
- throw new InvalidArgumentException( "Invalid cache miss callback provided." );
- }
- $value = call_user_func( $callback );
+ $value = $callback( $ttl );
if ( $value !== false ) {
$this->set( $key, $value, $ttl, $flags );
}
* - positive (< 10 years): relative TTL; return UNIX timestamp offset by this value
* - positive (>= 10 years): absolute UNIX timestamp; return this value
*
- * @param int $exptime Absolute TTL or 0 for indefinite
- * @return int
+ * @param int $exptime
+ * @return int Absolute TTL or 0 for indefinite
*/
final protected function convertToExpiry( $exptime ) {
return $this->expiryIsRelative( $exptime )
* Convert an optionally absolute expiry time to a relative time. If an
* absolute time is specified which is in the past, use a short expiry time.
*
+ * The input value will be cast to an integer and interpreted as follows:
+ * - zero: no expiry; return zero (e.g. TTL_INDEFINITE)
+ * - negative: relative TTL; return a short expiry time (1 second)
+ * - positive (< 10 years): relative TTL; return this value
+ * - positive (>= 10 years): absolute UNIX timestamp; return offset to current time
+ *
* @param int $exptime
- * @return int
+ * @return int Relative TTL or 0 for indefinite
*/
final protected function convertToRelative( $exptime ) {
- return $this->expiryIsRelative( $exptime )
+ return $this->expiryIsRelative( $exptime ) || !$exptime
? (int)$exptime
: max( $exptime - (int)$this->getCurrentTime(), 1 );
}
throw new DBUnexpectedError( $this, "Database injection is disallowed to enable reuse." );
}
- public function implicitGroupby() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function implicitOrderby() {
return $this->__call( __FUNCTION__, func_get_args() );
}
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function doneWrites() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function lastDoneWrites() {
return $this->__call( __FUNCTION__, func_get_args() );
}
return $this->__call( __FUNCTION__, func_get_args() );
}
- /**
- * @codeCoverageIgnore
- */
- public function getWikiID() {
- return $this->getDomainID();
- }
-
public function getType() {
if ( $this->conn === null ) {
// Avoid triggering a database connection
protected $cliMode;
/** @var string Agent name for query profiling */
protected $agent;
- /** @var int Bitfield of class DBO_* constants */
+ /** @var int Bit field of class DBO_* constants */
protected $flags;
/** @var array LoadBalancer tracking information */
protected $lbInfo = [];
/** @var float Assume an insert of this many rows or less should be fast to replicate */
private static $SMALL_WRITE_ROWS = 100;
+ /** @var string[] List of DBO_* flags that can be changed after connection */
+ protected static $MUTABLE_FLAGS = [
+ 'DBO_DEBUG',
+ 'DBO_NOBUFFER',
+ 'DBO_TRX',
+ 'DBO_DDLMODE',
+ ];
+ /** @var int Bit field of all DBO_* flags that can be changed after connection */
+ protected static $DBO_MUTABLE = (
+ self::DBO_DEBUG | self::DBO_NOBUFFER | self::DBO_TRX | self::DBO_DDLMODE
+ );
+
/**
* @note exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
/**
* Actually connect to the database over the wire (or to local files)
*
- * @throws InvalidArgumentException
* @throws DBConnectionError
* @since 1.31
*/
protected function doInitConnection() {
- if ( strlen( $this->connectionParams['user'] ) ) {
- $this->open(
- $this->connectionParams['host'],
- $this->connectionParams['user'],
- $this->connectionParams['password'],
- $this->connectionParams['dbname'],
- $this->connectionParams['schema'],
- $this->connectionParams['tablePrefix']
- );
- } else {
- throw new InvalidArgumentException( "No database user provided" );
- }
+ $this->open(
+ $this->connectionParams['host'],
+ $this->connectionParams['user'],
+ $this->connectionParams['password'],
+ $this->connectionParams['dbname'],
+ $this->connectionParams['schema'],
+ $this->connectionParams['tablePrefix']
+ );
}
/**
* equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas.
* - tablePrefix : Optional table prefix that is implicitly added on to all table names
* recognized in queries. This can be used in place of schemas for handle site farms.
- * - flags : Optional bitfield of DBO_* constants that define connection, protocol,
+ * - flags : Optional bit field of DBO_* constants that define connection, protocol,
* buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT
* flag in place UNLESS this this database simply acts as a key/value store.
* - driver: Optional name of a specific DB client driver. For MySQL, there is only the
return $this->lazyMasterHandle;
}
- public function implicitGroupby() {
- return true;
- }
-
public function implicitOrderby() {
return true;
}
return $this->lastQuery;
}
- public function doneWrites() {
- return (bool)$this->lastWriteTime;
- }
-
public function lastDoneWrites() {
return $this->lastWriteTime ?: false;
}
}
public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
- if ( ( $flag & self::DBO_IGNORE ) ) {
- throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
+ if ( $flag & ~static::$DBO_MUTABLE ) {
+ throw new DBUnexpectedError(
+ $this,
+ "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
+ );
}
if ( $remember === self::REMEMBER_PRIOR ) {
array_push( $this->priorFlags, $this->flags );
}
+
$this->flags |= $flag;
}
public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
- if ( ( $flag & self::DBO_IGNORE ) ) {
- throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
+ if ( $flag & ~static::$DBO_MUTABLE ) {
+ throw new DBUnexpectedError(
+ $this,
+ "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
+ );
}
if ( $remember === self::REMEMBER_PRIOR ) {
array_push( $this->priorFlags, $this->flags );
}
+
$this->flags &= ~$flag;
}
}
public function getFlag( $flag ) {
- return (bool)( $this->flags & $flag );
- }
-
- /**
- * @param string $name Class field name
- * @return mixed
- * @deprecated Since 1.28
- */
- public function getProperty( $name ) {
- return $this->$name;
+ return ( ( $this->flags & $flag ) === $flag );
}
public function getDomainID() {
return $this->currentDomain->getId();
}
- final public function getWikiID() {
- return $this->getDomainID();
- }
-
/**
* Get information about an index into an object
* @param string $table Table name
$closed = true; // already closed; nothing to do
}
- $this->conn = false;
+ $this->conn = null;
// Throw any unexpected errors after having disconnected
if ( $exception instanceof Exception ) {
*
* @param string $sql Original SQL query
* @param string $fname Name of the calling function
- * @param int $flags Bitfield of class QUERY_* constants
+ * @param int $flags Bit field of class QUERY_* constants
* @return array An n-tuple of:
* - mixed|bool: An object, resource, or true on success; false on failure
* - string: The result of calling lastError()
* @param string $commentedSql SQL query with debugging/trace comment
* @param bool $isPermWrite Whether the query is a (non-temporary table) write
* @param string $fname Name of the calling function
- * @param int $flags Bitfield of class QUERY_* constants
+ * @param int $flags Bit field of class QUERY_* constants
* @return array An n-tuple of:
* - mixed|bool: An object, resource, or true on success; false on failure
* - string: The result of calling lastError()
if ( $ignore ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error" );
} else {
- $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
-
- throw $exception;
+ throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
}
}
* @return DBError
*/
private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
- $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
$this->queryLogger->error(
"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
$this->getLogContext( [
'method' => __METHOD__,
'errno' => $errno,
'error' => $error,
- 'sql1line' => $sql1line,
+ 'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
'fname' => $fname,
'trace' => ( new RuntimeException() )->getTraceAsString()
] )
);
- $this->queryLogger->debug( "SQL ERROR: " . $error . "" );
+
if ( $this->wasQueryTimeout( $error, $errno ) ) {
$e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
} elseif ( $this->wasConnectionError( $errno ) ) {
return $e;
}
+ /**
+ * @param string $error
+ * @return DBConnectionError
+ */
+ final protected function newExceptionAfterConnectError( $error ) {
+ // Connection was not fully initialized and is not safe for use
+ $this->conn = null;
+
+ $this->connLogger->error(
+ "Error connecting to {db_server} as user {db_user}: {error}",
+ $this->getLogContext( [
+ 'error' => $error,
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
+ ] )
+ );
+
+ return new DBConnectionError( $this, $error );
+ }
+
public function freeResult( $res ) {
}
*/
protected function replaceLostConnection( $fname ) {
$this->closeConnection();
- $this->conn = false;
+ $this->conn = null;
$this->handleSessionLossPreconnect();
if ( $this->isOpen() ) {
// Open a new connection resource without messing with the old one
- $this->conn = false;
+ $this->conn = null;
$this->trxEndCallbacks = []; // don't copy
$this->trxSectionCancelCallbacks = []; // don't copy
$this->handleSessionLossPreconnect(); // no trx or locks anymore
/** @var string[] */
protected $ignoreErrors = [];
- public function implicitGroupby() {
- return false;
- }
-
public function implicitOrderby() {
return false;
}
}
protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
- // Test for driver support, to avoid suppressed fatal error
if ( !function_exists( 'sqlsrv_connect' ) ) {
throw new DBConnectionError(
$this,
- "Microsoft SQL Server Native (sqlsrv) functions missing.
- You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n"
+ "Microsoft SQL Server Native (sqlsrv) functions missing.\n
+ You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470"
);
}
$this->close();
+
+ if ( $schema !== null ) {
+ throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
+ }
+
$this->server = $server;
$this->user = $user;
$this->password = $password;
$connectionInfo = [];
-
- if ( $dbName != '' ) {
+ if ( strlen( $dbName ) ) {
$connectionInfo['Database'] = $dbName;
}
-
- // Decide which auth scenerio to use
- // if we are using Windows auth, then don't add credentials to $connectionInfo
if ( !$this->useWindowsAuth ) {
$connectionInfo['UID'] = $user;
$connectionInfo['PWD'] = $password;
}
AtEase::suppressWarnings();
- $this->conn = sqlsrv_connect( $server, $connectionInfo );
+ $this->conn = sqlsrv_connect( $server, $connectionInfo ) ?: null;
AtEase::restoreWarnings();
- if ( $this->conn === false ) {
- $error = $this->lastError();
- $this->connLogger->error(
- "Error connecting to {db_server}: {error}",
- $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
- );
- throw new DBConnectionError( $this, $error );
+ if ( !$this->conn ) {
+ throw $this->newExceptionAfterConnectError( $this->lastError() );
}
- $this->currentDomain = new DatabaseDomain(
- ( $dbName != '' ) ? $dbName : null,
- null,
- $tablePrefix
- );
-
- return (bool)$this->conn;
+ try {
+ $this->currentDomain = new DatabaseDomain(
+ strlen( $dbName ) ? $dbName : null,
+ null,
+ $tablePrefix
+ );
+ } catch ( Exception $e ) {
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
+ }
}
/**
$this->close();
if ( $schema !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
+ throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
}
$this->server = $server;
$this->installErrorHandler();
try {
$this->conn = $this->mysqlConnect( $this->server, $dbName );
- } catch ( Exception $ex ) {
+ } catch ( Exception $e ) {
$this->restoreErrorHandler();
- throw $ex;
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
}
$error = $this->restoreErrorHandler();
- # Always log connection errors
if ( !$this->conn ) {
- $error = $error ?: $this->lastError();
- $this->connLogger->error(
- "Error connecting to {db_server}: {error}",
- $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
- );
- $this->connLogger->debug( "DB connection error\n" .
- "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
- throw new DBConnectionError( $this, $error );
+ throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
}
try {
null,
$tablePrefix
);
-
// Abstract over any insane MySQL defaults
$set = [ 'group_concat_max_len = 262144' ];
// Set SQL mode, default is turning them all off, can be overridden or skipped with null
);
}
} catch ( Exception $e ) {
- // Connection was not fully initialized and is not safe for use
- $this->conn = false;
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
}
-
- return true;
}
protected function doSelectDomain( DatabaseDomain $domain ) {
*
* @param string $realServer
* @param string|null $dbName
- * @return mixed Raw connection
+ * @return mixed|null Driver connection handle
* @throws DBConnectionError
*/
abstract protected function mysqlConnect( $realServer, $dbName );
/**
* @param string $realServer
* @param string|null $dbName
- * @return bool|mysqli
+ * @return mysqli|null
* @throws DBConnectionError
*/
protected function mysqlConnect( $realServer, $dbName ) {
- # Avoid suppressed fatal error, which is very hard to track down
if ( !function_exists( 'mysqli_init' ) ) {
- throw new DBConnectionError( $this, "MySQLi functions missing,"
- . " have you compiled PHP with the --with-mysqli option?\n" );
+ throw $this->newExceptionAfterConnectError(
+ "MySQLi functions missing, have you compiled PHP with the --with-mysqli option?"
+ );
}
// Other than mysql_connect, mysqli_real_connect expects an explicit port
$mysqli = mysqli_init();
$connFlags = 0;
- if ( $this->flags & self::DBO_SSL ) {
+ if ( $this->getFlag( self::DBO_SSL ) ) {
$connFlags |= MYSQLI_CLIENT_SSL;
$mysqli->ssl_set(
$this->sslKeyPath,
$this->sslCiphers
);
}
- if ( $this->flags & self::DBO_COMPRESS ) {
+ if ( $this->getFlag( self::DBO_COMPRESS ) ) {
$connFlags |= MYSQLI_CLIENT_COMPRESS;
}
- if ( $this->flags & self::DBO_PERSISTENT ) {
+ if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
$realServer = 'p:' . $realServer;
}
return $mysqli;
}
- return false;
+ return null;
}
/**
* @ingroup Database
*/
class DatabasePostgres extends Database {
- /** @var int|bool */
- protected $port;
-
- /** @var resource */
- protected $lastResultHandle = null;
-
- /** @var float|string */
- private $numericVersion = null;
- /** @var string Connect string to open a PostgreSQL connection */
- private $connectString;
+ /** @var int|null */
+ private $port;
/** @var string */
private $coreSchema;
/** @var string */
private $tempSchema;
/** @var string[] Map of (reserved table name => alternate table name) */
private $keywordTableMap = [];
+ /** @var float|string */
+ private $numericVersion;
+
+ /** @var resource|null */
+ private $lastResultHandle;
/**
* @see Database::__construct()
* - keywordTableMap : Map of reserved table names to alternative table names to use
*/
public function __construct( array $params ) {
- $this->port = $params['port'] ?? false;
+ $this->port = intval( $params['port'] ?? null );
$this->keywordTableMap = $params['keywordTableMap'] ?? [];
parent::__construct( $params );
return 'postgres';
}
- public function implicitGroupby() {
- return false;
- }
-
public function implicitOrderby() {
return false;
}
}
protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
- // Test for Postgres support, to avoid suppressed fatal error
if ( !function_exists( 'pg_connect' ) ) {
- throw new DBConnectionError(
- $this,
+ throw $this->newExceptionAfterConnectError(
"Postgres functions missing, have you compiled PHP with the --with-pgsql\n" .
"option? (Note: if you recently installed PHP, you may need to restart your\n" .
- "webserver and database)\n"
+ "webserver and database)"
);
}
$this->password = $password;
$connectVars = [
- // pg_connect() user $user as the default database. Since a database is *required*,
- // at least pick a "don't care" database that is more likely to exist. This case
- // arrises when LoadBalancer::getConnection( $i, [], '' ) is used.
+ // pg_connect() user $user as the default database. Since a database is required,
+ // then pick a "don't care" database that is more likely to exist than that one.
'dbname' => strlen( $dbName ) ? $dbName : 'postgres',
'user' => $user,
'password' => $password
];
- if ( $server != false && $server != '' ) {
+ if ( strlen( $server ) ) {
$connectVars['host'] = $server;
}
- if ( (int)$this->port > 0 ) {
- $connectVars['port'] = (int)$this->port;
+ if ( $this->port > 0 ) {
+ $connectVars['port'] = $this->port;
}
- if ( $this->flags & self::DBO_SSL ) {
+ if ( $this->getFlag( self::DBO_SSL ) ) {
$connectVars['sslmode'] = 'require';
}
-
- $this->connectString = $this->makeConnectionString( $connectVars );
+ $connectString = $this->makeConnectionString( $connectVars );
$this->installErrorHandler();
try {
- // Use new connections to let LoadBalancer/LBFactory handle reuse
- $this->conn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
- } catch ( Exception $ex ) {
+ $this->conn = pg_connect( $connectString, PGSQL_CONNECT_FORCE_NEW ) ?: null;
+ } catch ( Exception $e ) {
$this->restoreErrorHandler();
- throw $ex;
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
}
- $phpError = $this->restoreErrorHandler();
+ $error = $this->restoreErrorHandler();
if ( !$this->conn ) {
- $this->queryLogger->debug(
- "DB connection error\n" .
- "Server: $server, Database: $dbName, User: $user, Password: " .
- substr( $password, 0, 3 ) . "...\n"
- );
- $this->queryLogger->debug( $this->lastError() . "\n" );
- throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
+ throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
}
try {
- // If called from the command-line (e.g. importDump), only show errors.
- // No transaction should be open at this point, so the problem of the SET
- // effects being rolled back should not be an issue.
+ // Since no transaction is active at this point, any SET commands should apply
+ // for the entire session (e.g. will not be reverted on transaction rollback).
// See https://www.postgresql.org/docs/8.3/sql-set.html
- $variables = [];
- if ( $this->cliMode ) {
- $variables['client_min_messages'] = 'ERROR';
- }
- $variables += [
+ $variables = [
'client_encoding' => 'UTF8',
'datestyle' => 'ISO, YMD',
'timezone' => 'GMT',
'standard_conforming_strings' => 'on',
- 'bytea_output' => 'escape'
+ 'bytea_output' => 'escape',
+ 'client_min_messages' => 'ERROR'
];
foreach ( $variables as $var => $val ) {
$this->query(
self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY
);
}
-
$this->determineCoreSchema( $schema );
$this->currentDomain = new DatabaseDomain( $dbName, $schema, $tablePrefix );
} catch ( Exception $e ) {
- // Connection was not fully initialized and is not safe for use
- $this->conn = false;
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
}
}
* Values may contain magic keywords like "$user"
* @since 1.19
*
- * @param array $search_path List of schemas to be searched by default
+ * @param string[] $search_path List of schemas to be searched by default
*/
private function setSearchPath( $search_path ) {
$this->query(
$this->queryLogger->debug(
"Schema \"" . $desiredSchema . "\" already in the search path\n" );
} else {
- /**
- * Prepend our schema (e.g. 'mediawiki') in front
- * of the search path
- * Fixes T17816
- */
+ // Prepend the desired schema to the search path (T17816)
$search_path = $this->getSearchPath();
array_unshift( $search_path, $this->addIdentifierQuotes( $desiredSchema ) );
$this->setSearchPath( $search_path );
/** @var bool Whether full text is enabled */
private static $fulltextEnabled = null;
+ /** @var string[] See https://www.sqlite.org/lang_transaction.html */
+ private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
+
/**
* Additional params include:
* - dbDirectory : directory containing the DB and the lock file directory
$this->dbDir = $p['dbDirectory'];
}
- // Set a dummy user to make initConnection() trigger open()
- parent::__construct( [ 'user' => '@' ] + $p );
+ parent::__construct( $p );
$this->trxMode = strtoupper( $p['trxMode'] ?? '' );
return 'sqlite';
}
- /**
- * @todo Check if it should be true like parent class
- *
- * @return bool
- */
- 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__ . ": cannot use schemas ('$schema')" );
+ throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
}
if ( $this->dbPath !== null ) {
} elseif ( $this->dbDir !== null ) {
$path = self::generateFileName( $this->dbDir, $dbName );
} else {
- throw new DBExpectedError( $this, __CLASS__ . ": DB path or directory required" );
+ throw $this->newExceptionAfterConnectError( "DB path or directory required" );
}
- 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 ) ) {
+ throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
+ } elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) {
+ throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
}
- if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) {
- $error = "SQLite database file not readable";
- $this->connLogger->error(
- "Error connecting to {db_server}: {error}",
- $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
- );
- throw new DBConnectionError( $this, $error );
+ $attributes = [];
+ if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
+ // Persistent connections can avoid some schema index reading overhead.
+ // On the other hand, they can cause horrible contention with DBO_TRX.
+ if ( $this->getFlag( self::DBO_TRX ) ) {
+ $this->connLogger->warning( __METHOD__ . ": DBO_PERSISTENT mixed with DBO_TRX" );
+ } else {
+ $attributes[PDO::ATTR_PERSISTENT] = true;
+ }
}
try {
- $conn = new PDO(
- "sqlite:$path",
- '',
- '',
- [ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ]
- );
- // Set error codes only, don't raise exceptions
- $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
+ $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
} catch ( PDOException $e ) {
- $error = $e->getMessage();
- $this->connLogger->error(
- "Error connecting to {db_server}: {error}",
- $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
- );
- throw new DBConnectionError( $this, $error );
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
}
- $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
+ // Apply 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__, $flags );
}
} catch ( Exception $e ) {
- // Connection was not fully initialized and is not safe for use
- $this->conn = false;
- throw $e;
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
}
}
/** @var int Combine list with OR clauses */
const LIST_OR = 4;
- /** @var int Enable debug logging */
+ /** @var int Enable debug logging of all SQL queries */
const DBO_DEBUG = 1;
/** @var int Disable query buffering (only one result set can be iterated at a time) */
const DBO_NOBUFFER = 2;
*/
public function setLazyMasterHandle( IDatabase $conn );
- /**
- * Returns true if this database does an implicit sort when doing GROUP BY
- *
- * @return bool
- * @deprecated Since 1.30; only use grouped or aggregated fields in the SELECT
- */
- public function implicitGroupby();
-
/**
* Returns true if this database does an implicit order by when the column has an index
* For example: SELECT page_title FROM page LIMIT 1
*/
public function lastQuery();
- /**
- * Returns true if the connection may have been used for write queries.
- * Should return true if unsure.
- *
- * @return bool
- * @deprecated Since 1.31; use lastDoneWrites()
- */
- public function doneWrites();
-
/**
* Returns the last time the connection may have been used for write queries.
* Should return a timestamp if unsure.
/**
* Set a flag for this connection
*
- * @param int $flag DBO_* constants from Defines.php:
- * - DBO_DEBUG: output some debug info (same as debug())
- * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
- * - DBO_TRX: automatically start transactions
- * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode
- * and removes it in command line mode
- * - DBO_PERSISTENT: use persistant database connection
+ * @param int $flag IDatabase::DBO_DEBUG, IDatabase::DBO_NOBUFFER, or IDatabase::DBO_TRX
* @param string $remember IDatabase::REMEMBER_* constant [default: REMEMBER_NOTHING]
*/
public function setFlag( $flag, $remember = self::REMEMBER_NOTHING );
/**
* Clear a flag for this connection
*
- * @param int $flag DBO_* constants from Defines.php:
- * - DBO_DEBUG: output some debug info (same as debug())
- * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
- * - DBO_TRX: automatically start transactions
- * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode
- * and removes it in command line mode
- * - DBO_PERSISTENT: use persistant database connection
+ * @param int $flag IDatabase::DBO_DEBUG, IDatabase::DBO_NOBUFFER, or IDatabase::DBO_TRX
* @param string $remember IDatabase::REMEMBER_* constant [default: REMEMBER_NOTHING]
*/
public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING );
/**
* Returns a boolean whether the flag $flag is set for this connection
*
- * @param int $flag DBO_* constants from Defines.php:
- * - DBO_DEBUG: output some debug info (same as debug())
- * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
- * - DBO_TRX: automatically start transactions
- * - DBO_PERSISTENT: use persistant database connection
+ * @param int $flag One of the class IDatabase::DBO_* constants
* @return bool
*/
public function getFlag( $flag );
*/
public function getDomainID();
- /**
- * Alias for getDomainID()
- *
- * @return string
- * @deprecated 1.30
- */
- public function getWikiID();
-
/**
* Get the type of the DBMS, as it appears in $wgDBtype.
*
$this->indexAliases = $aliases;
}
- /**
- * @param string $prefix
- * @deprecated Since 1.33
- */
- public function setDomainPrefix( $prefix ) {
- $this->setLocalDomainPrefix( $prefix );
- }
-
public function setLocalDomainPrefix( $prefix ) {
$this->localDomain = new DatabaseDomain(
$this->localDomain->getDatabase(),
$ok = true; // no applicable loads
}
} finally {
- # Restore the old position, as this is not used for lag-protection but for throttling
+ // Restore the old position; this is used for throttling, not lag-protection
$this->waitForPos = $oldPos;
}
$ok = true;
for ( $i = 1; $i < $serverCount; $i++ ) {
- if ( $this->groupLoads[self::GROUP_GENERIC][$i] > 0 ) {
+ if ( $this->serverHasLoadInAnyGroup( $i ) ) {
$start = microtime( true );
$ok = $this->doWait( $i, true, $timeout ) && $ok;
$timeout -= intval( microtime( true ) - $start );
}
}
} finally {
- # Restore the old position, as this is not used for lag-protection but for throttling
+ // Restore the old position; this is used for throttling, not lag-protection
$this->waitForPos = $oldPos;
}
return $ok;
}
+ /**
+ * @param int $i Specific server index
+ * @return bool
+ */
+ private function serverHasLoadInAnyGroup( $i ) {
+ foreach ( $this->groupLoads as $loadsByIndex ) {
+ if ( ( $loadsByIndex[$i] ?? 0 ) > 0 ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* @param DBMasterPos|bool $pos
*/
return $this->laggedReplicaMode;
}
- /**
- * @return bool
- * @since 1.27
- * @deprecated Since 1.28; use laggedReplicaUsed()
- */
- public function laggedSlaveUsed() {
- return $this->laggedReplicaUsed();
- }
-
public function getReadOnlyReason( $domain = false, IDatabase $conn = null ) {
if ( $this->readOnlyReason !== false ) {
return $this->readOnlyReason;
$this->indexAliases = $aliases;
}
- /**
- * @param string $prefix
- * @deprecated Since 1.33
- */
- public function setDomainPrefix( $prefix ) {
- $this->setLocalDomainPrefix( $prefix );
- }
-
public function setLocalDomainPrefix( $prefix ) {
// Find connections to explicit foreign domains still marked as in-use...
$domainsInUse = [];
: self::FOR_PUBLIC;
}
+ /**
+ * Check if a log item type can be displayed
+ * @return bool
+ */
+ public function canViewLogType() {
+ // If the user doesn't have the right permission to view the specific
+ // log type, return false
+ $logRestrictions = $this->context->getConfig()->get( 'LogRestrictions' );
+ $type = $this->entry->getType();
+ return !isset( $logRestrictions[$type] )
+ || $this->context->getUser()->isAllowed( $logRestrictions[$type] );
+ }
+
/**
* Check if a log item can be displayed
* @param int $field LogPage::DELETED_* constant
protected function canView( $field ) {
if ( $this->audience == self::FOR_THIS_USER ) {
return LogEventsList::userCanBitfield(
- $this->entry->getDeleted(), $field, $this->context->getUser() );
+ $this->entry->getDeleted(), $field, $this->context->getUser() ) &&
+ self::canViewLogType();
} else {
- return !$this->entry->isDeleted( $field );
+ return !$this->entry->isDeleted( $field ) && self::canViewLogType();
}
}
/**
* @ingroup Media
+ * @deprecated since 1.34
*/
class SVGMetadataExtractor {
- static function getMetadata( $filename ) {
+ /**
+ * @param string $filename
+ * @return array
+ * @deprecated since 1.34, use SVGReader->getMetadata() directly
+ */
+ public static function getMetadata( $filename ) {
+ wfDeprecated( __METHOD__, '1.34' );
+
$svg = new SVGReader( $filename );
return $svg->getMetadata();
*/
public function getMetadata( $file, $filename ) {
$metadata = [ 'version' => self::SVG_METADATA_VERSION ];
+
try {
- $metadata += SVGMetadataExtractor::getMetadata( $filename );
+ $svgReader = new SVGReader( $filename );
+ $metadata += $svgReader->getMetadata();
} catch ( Exception $e ) { // @todo SVG specific exceptions
// File not found, broken, etc.
$metadata['error'] = [
use Wikimedia\Rdbms\DBError;
use Wikimedia\Rdbms\DBQueryError;
use Wikimedia\Rdbms\DBConnectionError;
+use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\Rdbms\LoadBalancer;
use Wikimedia\ScopedCallback;
use Wikimedia\WaitConditionLoop;
* Get a connection to the specified database
*
* @param int $serverIndex
- * @return Database
+ * @return IMaintainableDatabase
* @throws MWException
*/
protected function getDB( $serverIndex ) {
$index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
// Keep a separate connection to avoid contention and deadlocks
- $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
+ $db = $lb->getConnectionRef( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
} else {
// However, SQLite has the opposite behavior due to DB-level locking.
// Stock sqlite MediaWiki installs use a separate sqlite cache DB instead.
- $db = $lb->getConnection( $index );
+ $db = $lb->getConnectionRef( $index );
}
}
*/
public static function pageid( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null( $t ) ) {
+ if ( !$t ) {
return '';
+ } elseif ( !$t->canExist() || $t->isExternal() ) {
+ return 0; // e.g. special page or interwiki link
}
- // Use title from parser to have correct pageid after edit
+
+ $parserOutput = $parser->getOutput();
+
if ( $t->equals( $parser->getTitle() ) ) {
- $t = $parser->getTitle();
- return $t->getArticleID();
- }
+ // Revision is for the same title that is currently being parsed.
+ // Use the title from Parser in case a new page ID was injected into it.
+ $parserOutput->setFlag( 'vary-page-id' );
+ $id = $parser->getTitle()->getArticleID();
+ if ( $id ) {
+ $parserOutput->setSpeculativePageIdUsed( $id );
+ }
- // These can't have ids
- if ( !$t->canExist() || $t->isExternal() ) {
- return 0;
+ return $id;
}
- // Check the link cache, maybe something already looked it up.
+ // Check the link cache for the title
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$pdbk = $t->getPrefixedDBkey();
$id = $linkCache->getGoodLinkID( $pdbk );
- if ( $id != 0 ) {
- $parser->mOutput->addLink( $t, $id );
- return $id;
- }
- if ( $linkCache->isBadLink( $pdbk ) ) {
- $parser->mOutput->addLink( $t, 0 );
+ if ( $id != 0 || $linkCache->isBadLink( $pdbk ) ) {
+ $parserOutput->addLink( $t, $id );
+
return $id;
}
// We need to load it from the DB, so mark expensive
if ( $parser->incrementExpensiveFunctionCount() ) {
$id = $t->getArticleID();
- $parser->mOutput->addLink( $t, $id );
+ $parserOutput->addLink( $t, $id );
+
return $id;
}
+
return null;
}
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Special\SpecialPageFactory;
+use Psr\Log\NullLogger;
use Wikimedia\ScopedCallback;
+use Psr\Log\LoggerInterface;
/**
* @defgroup Parser Parser
/** @var NamespaceInfo */
private $nsInfo;
+ /** @var LoggerInterface */
+ private $logger;
+
/**
* TODO Make this a const when HHVM support is dropped (T192166)
*
* @param SpecialPageFactory|null $spFactory
* @param LinkRendererFactory|null $linkRendererFactory
* @param NamespaceInfo|null $nsInfo
+ * @param LoggerInterface|null $logger
*/
public function __construct(
- $svcOptions = null, MagicWordFactory $magicWordFactory = null,
- Language $contLang = null, ParserFactory $factory = null, $urlProtocols = null,
- SpecialPageFactory $spFactory = null, $linkRendererFactory = null, $nsInfo = null
+ $svcOptions = null,
+ MagicWordFactory $magicWordFactory = null,
+ Language $contLang = null,
+ ParserFactory $factory = null,
+ $urlProtocols = null,
+ SpecialPageFactory $spFactory = null,
+ $linkRendererFactory = null,
+ $nsInfo = null,
+ $logger = null
) {
$services = MediaWikiServices::getInstance();
if ( !$svcOptions || is_array( $svcOptions ) ) {
$this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory();
$this->linkRendererFactory = $linkRendererFactory ?? $services->getLinkRendererFactory();
$this->nsInfo = $nsInfo ?? $services->getNamespaceInfo();
+ $this->logger = $logger ?: new NullLogger();
}
/**
$value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
break;
case 'pageid': // requested in T25427
- $pageid = $this->getTitle()->getArticleID();
- if ( $pageid == 0 ) {
- # 0 means the page doesn't exist in the database,
- # which means the user is previewing a new page.
- # The vary-revision flag must be set, because the magic word
- # will have a different value once the page is saved.
- $this->mOutput->setFlag( 'vary-revision' );
- wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision" );
+ # Inform the edit saving system that getting the canonical output
+ # after page insertion requires a parse that used that exact page ID
+ $this->setOutputFlag( 'vary-page-id', '{{PAGEID}} used' );
+ $value = $this->mTitle->getArticleID();
+ if ( !$value ) {
+ $value = $this->mOptions->getSpeculativePageId();
+ if ( $value ) {
+ $this->mOutput->setSpeculativePageIdUsed( $value );
+ }
}
- $value = $pageid ?: null;
break;
case 'revisionid':
if (
if ( $this->getRevisionId() || $this->mOptions->getSpeculativeRevId() ) {
$value = '-';
} else {
- $this->mOutput->setFlag( 'vary-revision-exists' );
- wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-exists" );
+ $this->setOutputFlag( 'vary-revision-exists', '{{REVISIONID}} used' );
$value = '';
}
} else {
# Inform the edit saving system that getting the canonical output after
- # revision insertion requires another parse using the actual revision ID
- $this->mOutput->setFlag( 'vary-revision-id' );
- wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id" );
+ # revision insertion requires a parse that used that exact revision ID
+ $this->setOutputFlag( 'vary-revision-id', '{{REVISIONID}} used' );
$value = $this->getRevisionId();
if ( $value === 0 ) {
$rev = $this->getRevisionObject();
case 'revisionuser':
# Inform the edit saving system that getting the canonical output after
# revision insertion requires a parse that used the actual user ID
- $this->mOutput->setFlag( 'vary-user' );
- wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user" );
+ $this->setOutputFlag( 'vary-user', '{{REVISIONUSER}} used' );
$value = $this->getRevisionUser();
break;
case 'revisionsize':
if ( $resNow !== $resThen ) {
# Inform the edit saving system that getting the canonical output after
# revision insertion requires a parse that used an actual revision timestamp
- $this->mOutput->setFlag( 'vary-revision-timestamp' );
- wfDebug( __METHOD__ . ": $variable used, setting vary-revision-timestamp" );
+ $this->setOutputFlag( 'vary-revision-timestamp', "$variable used" );
}
}
if ( $frame === false ) {
$frame = $this->getPreprocessor()->newFrame();
} elseif ( !( $frame instanceof PPFrame ) ) {
- wfDebug( __METHOD__ . " called using plain parameters instead of "
- . "a PPFrame instance. Creating custom frame.\n" );
+ $this->logger->debug(
+ __METHOD__ . " called using plain parameters instead of " .
+ "a PPFrame instance. Creating custom frame."
+ );
$frame = $this->getPreprocessor()->newCustomFrame( $frame );
}
}
} elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
$found = false; # access denied
- wfDebug( __METHOD__ . ": template inclusion denied for " .
- $title->getPrefixedDBkey() . "\n" );
+ $this->logger->debug(
+ __METHOD__ .
+ ": template inclusion denied for " . $title->getPrefixedDBkey()
+ );
} else {
list( $text, $title ) = $this->getTemplateDom( $title );
if ( $text !== false ) {
$this->addTrackingCategory( 'template-loop-category' );
$this->mOutput->addWarning( wfMessage( 'template-loop-warning',
wfEscapeWikiText( $titleText ) )->text() );
- wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
+ $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
}
}
$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
if ( $dep['title']->equals( $this->getTitle() ) ) {
// Self-transclusion; final result may change based on the new page version
- $this->mOutput->setFlag( 'vary-revision' );
- wfDebug( __METHOD__ . ": self transclusion, setting vary-revision" );
+ $this->setOutputFlag( 'vary-revision', 'Self transclusion' );
}
}
}
'~~~' => $sigText
] );
# The main two signature forms used above are time-sensitive
- $this->mOutput->setFlag( 'user-signature' );
+ $this->setOutputFlag( 'user-signature', 'User signature detected' );
}
# Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
if ( mb_strlen( $nickname ) > $this->svcOptions->get( 'MaxSigChars' ) ) {
$nickname = $username;
- wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
+ $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
} elseif ( $fancySig !== false ) {
# Sig. might contain markup; validate this
if ( $this->validateSig( $nickname ) !== false ) {
} else {
# Failed to validate; fall back to the default
$nickname = $username;
- wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
+ $this->logger->debug( __METHOD__ . ": $username has bad XML tags in signature." );
}
}
$handlerOptions[$paramName] = $match;
} else {
// Guess not, consider it as caption.
- wfDebug( "$parameterMatch failed parameter validation\n" );
+ $this->logger->debug(
+ "$parameterMatch failed parameter validation" );
$label = $parameterMatch;
}
}
* @deprecated since 1.28; use getOutput()->updateCacheExpiry()
*/
public function disableCache() {
- wfDebug( "Parser output marked as uncacheable.\n" );
+ $this->logger->debug( "Parser output marked as uncacheable." );
if ( !$this->mOutput ) {
throw new MWException( __METHOD__ .
" can only be called when actually parsing something" );
* @since 1.23 (public since 1.23)
*/
public function getRevisionObject() {
- if ( !is_null( $this->mRevisionObject ) ) {
+ if ( $this->mRevisionObject ) {
return $this->mRevisionObject;
}
// NOTE: try to get the RevisionObject even if mRevisionId is null.
- // This is useful when parsing revision that has not yet been saved.
+ // This is useful when parsing a revision that has not yet been saved.
// However, if we get back a saved revision even though we are in
// preview mode, we'll have to ignore it, see below.
// NOTE: This callback may be used to inject an OLD revision that was
// already loaded, so "current" is a bit of a misnomer. We can't just
// skip it if mRevisionId is set.
$rev = call_user_func(
- $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
+ $this->mOptions->getCurrentRevisionCallback(),
+ $this->getTitle(),
+ $this
);
if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
OutputPage::setupOOUI();
$this->mOutput->setEnableOOUI( true );
}
+
+ /**
+ * @param string $flag
+ * @param string $reason
+ */
+ protected function setOutputFlag( $flag, $reason ) {
+ $this->mOutput->setFlag( $flag );
+ $name = $this->mTitle->getPrefixedText();
+ $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
+ }
}
use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Special\SpecialPageFactory;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
/**
* @since 1.32
/** @var NamespaceInfo */
private $nsInfo;
+ /** @var LoggerInterface */
+ private $logger;
+
/**
* Old parameter list, which we support for backwards compatibility, were:
* array $parserConf See $wgParserConf documentation
* @param SpecialPageFactory $spFactory
* @param LinkRendererFactory $linkRendererFactory
* @param NamespaceInfo|LinkRendererFactory|null $nsInfo
+ * @param LoggerInterface|null $logger
* @since 1.32
*/
public function __construct(
- $svcOptions, MagicWordFactory $magicWordFactory, Language $contLang,
- $urlProtocols, SpecialPageFactory $spFactory, $linkRendererFactory,
- $nsInfo = null
+ $svcOptions,
+ MagicWordFactory $magicWordFactory,
+ Language $contLang,
+ $urlProtocols,
+ SpecialPageFactory $spFactory,
+ $linkRendererFactory,
+ $nsInfo = null,
+ $logger = null
) {
// @todo Do we need to retain compat for constructing this class directly?
if ( !$nsInfo ) {
$this->specialPageFactory = $spFactory;
$this->linkRendererFactory = $linkRendererFactory;
$this->nsInfo = $nsInfo;
+ $this->logger = $logger ?: new NullLogger();
}
/**
* @since 1.32
*/
public function create() : Parser {
- return new Parser( $this->svcOptions, $this->magicWordFactory, $this->contLang, $this,
- $this->urlProtocols, $this->specialPageFactory, $this->linkRendererFactory,
- $this->nsInfo );
+ return new Parser(
+ $this->svcOptions,
+ $this->magicWordFactory,
+ $this->contLang,
+ $this,
+ $this->urlProtocols,
+ $this->specialPageFactory,
+ $this->linkRendererFactory,
+ $this->nsInfo,
+ $this->logger
+ );
}
}
private static $lazyOptions = [
'dateformat' => [ __CLASS__, 'initDateFormat' ],
'speculativeRevId' => [ __CLASS__, 'initSpeculativeRevId' ],
+ 'speculativePageId' => [ __CLASS__, 'initSpeculativePageId' ],
];
/**
*/
private $mExtraKey = '';
- /**
- * @name Option accessors
- * @{
- */
-
/**
* Fetch an option and track that is was accessed
* @since 1.30
return $this->getOption( 'speculativeRevId' );
}
+ /**
+ * A guess for {{PAGEID}}, calculated using the callback provided via
+ * setSpeculativeRevPageCallback(). For consistency, the value will be calculated upon the
+ * first call of this method, and re-used for subsequent calls.
+ *
+ * If no callback was defined via setSpeculativePageIdCallback(), this method will return false.
+ *
+ * @since 1.34
+ * @return int|false
+ */
+ public function getSpeculativePageId() {
+ return $this->getOption( 'speculativePageId' );
+ }
+
/**
* Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativeRevId().
*
* @param ParserOptions $popt
- * @return bool|false
+ * @return int|false
*/
private static function initSpeculativeRevId( ParserOptions $popt ) {
$cb = $popt->getOption( 'speculativeRevIdCallback' );
}
/**
- * Callback to generate a guess for {{REVISIONID}}
- * @since 1.28
- * @deprecated since 1.32, use getSpeculativeRevId() instead!
- * @return callable|null
+ * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativePageId().
+ *
+ * @param ParserOptions $popt
+ * @return int|false
*/
- public function getSpeculativeRevIdCallback() {
- return $this->getOption( 'speculativeRevIdCallback' );
+ private static function initSpeculativePageId( ParserOptions $popt ) {
+ $cb = $popt->getOption( 'speculativePageIdCallback' );
+ $id = $cb ? $cb() : null;
+
+ // returning null would result in this being re-called every access
+ return $id ?? false;
}
/**
* Callback to generate a guess for {{REVISIONID}}
- * @since 1.28
- * @param callable|null $x New value (null is no change)
+ * @param callable|null $x New value
* @return callable|null Old value
+ * @since 1.28
*/
public function setSpeculativeRevIdCallback( $x ) {
$this->setOption( 'speculativeRevId', null ); // reset
- return $this->setOptionLegacy( 'speculativeRevIdCallback', $x );
+ return $this->setOption( 'speculativeRevIdCallback', $x );
}
- /**@}*/
+ /**
+ * Callback to generate a guess for {{PAGEID}}
+ * @param callable|null $x New value
+ * @return callable|null Old value
+ * @since 1.34
+ */
+ public function setSpeculativePageIdCallback( $x ) {
+ $this->setOption( 'speculativePageId', null ); // reset
+ return $this->setOption( 'speculativePageIdCallback', $x );
+ }
/**
* Timestamp used for {{CURRENTDAY}} etc.
'templateCallback' => [ Parser::class, 'statelessFetchTemplate' ],
'speculativeRevIdCallback' => null,
'speculativeRevId' => null,
+ 'speculativePageIdCallback' => null,
+ 'speculativePageId' => null,
];
Hooks::run( 'ParserOptionsRegister', [
*/
private $mFlags = [];
- /** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */
- private $mSpeculativeRevId;
+ /** @var string[] */
+ private static $speculativeFields = [
+ 'speculativePageIdUsed',
+ 'speculativeRevIdUsed',
+ 'revisionTimestampUsed'
+ ];
+ /** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */
+ private $speculativeRevIdUsed;
+ /** @var int|null Assumed page ID for {{PAGEID}} if no revision is set */
+ private $speculativePageIdUsed;
/** @var int|null Assumed rev timestamp for {{REVISIONTIMESTAMP}} if no revision is set */
private $revisionTimestampUsed;
* @since 1.28
*/
public function setSpeculativeRevIdUsed( $id ) {
- $this->mSpeculativeRevId = $id;
+ $this->speculativeRevIdUsed = $id;
}
/**
* @since 1.28
*/
public function getSpeculativeRevIdUsed() {
- return $this->mSpeculativeRevId;
+ return $this->speculativeRevIdUsed;
+ }
+
+ /**
+ * @param int $id
+ * @since 1.34
+ */
+ public function setSpeculativePageIdUsed( $id ) {
+ $this->speculativePageIdUsed = $id;
+ }
+
+ /**
+ * @return int|null
+ * @since 1.34
+ */
+ public function getSpeculativePageIdUsed() {
+ return $this->speculativePageIdUsed;
}
/**
$this->mWarnings = self::mergeMap( $this->mWarnings, $source->mWarnings ); // don't use getter
$this->mTimestamp = $this->useMaxValue( $this->mTimestamp, $source->getTimestamp() );
- if ( $this->mSpeculativeRevId && $source->mSpeculativeRevId
- && $this->mSpeculativeRevId !== $source->mSpeculativeRevId
- ) {
- wfLogWarning(
- 'Inconsistent speculative revision ID encountered while merging parser output!'
- );
+ foreach ( self::$speculativeFields as $field ) {
+ if ( $this->$field && $source->$field && $this->$field !== $source->$field ) {
+ wfLogWarning( __METHOD__ . ": inconsistent '$field' properties!" );
+ }
+ $this->$field = $this->useMaxValue( $this->$field, $source->$field );
}
- $this->mSpeculativeRevId = $this->useMaxValue(
- $this->mSpeculativeRevId,
- $source->getSpeculativeRevIdUsed()
- );
$this->mParseStartTime = $this->useEachMinValue(
$this->mParseStartTime,
$source->mParseStartTime
* @return array
*/
public function getTestModuleNames( $framework = 'all' ) {
- /** @todo api siteinfo prop testmodulenames modulenames */
if ( $framework == 'all' ) {
return $this->testModuleNames;
} elseif ( isset( $this->testModuleNames[$framework] )
file_put_contents( $tempFilenameSvg, $svg );
- $metadata = SVGMetadataExtractor::getMetadata( $tempFilenameSvg );
+ $svgReader = new SVGReader( $tempFilenameSvg );
+ $metadata = $svgReader->getMetadata();
if ( !isset( $metadata['width'] ) || !isset( $metadata['height'] ) ) {
unlink( $tempFilenameSvg );
return false;
'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
- 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
'wgEnableUploads' => $conf->get( 'EnableUploads' ),
'wgCommentByteLimit' => null,
'$VARS.maxQueryLength' => ResourceLoader::encodeJsonForScript(
$conf->get( 'ResourceLoaderMaxQueryLength' )
),
+ // The client-side module cache can be disabled by site configuration.
+ // It is also always disabled in debug mode.
+ '$VARS.storeEnabled' => ResourceLoader::encodeJsonForScript(
+ $conf->get( 'ResourceLoaderStorageEnabled' ) && !$context->getDebug()
+ ),
+ '$VARS.wgLegacyJavaScriptGlobals' => ResourceLoader::encodeJsonForScript(
+ $conf->get( 'LegacyJavaScriptGlobals' )
+ ),
'$VARS.storeKey' => ResourceLoader::encodeJsonForScript( $this->getStoreKey() ),
'$VARS.storeVary' => ResourceLoader::encodeJsonForScript( $this->getStoreVary( $context ) ),
];
// Perform string replacements for startup.js
$pairs = [
- '$VARS.wgLegacyJavaScriptGlobals' => ResourceLoader::encodeJsonForScript(
- $conf->get( 'LegacyJavaScriptGlobals' )
- ),
'$VARS.configuration' => ResourceLoader::encodeJsonForScript(
$this->getConfigSettings( $context )
),
return $titleInfo;
}
+ /** @return array */
protected static function fetchTitleInfo( IDatabase $db, array $pages, $fname = __METHOD__ ) {
$titleInfo = [];
$batch = new LinkBatch;
// Check, if the page can hold some kind of content, otherwise do nothing
$title = $this->getRelevantTitle();
- if ( $title->canExist() ) {
+ if ( $title->canExist() && $title->canHaveTalkPage() ) {
if ( $title->isTalkPage() ) {
$titles[] = $title->getSubjectPage();
} else {
return $warnings;
}
+ /**
+ * Convert the warnings array returned by checkWarnings() to something that
+ * can be serialized. File objects will be converted to an associative array
+ * with the following keys:
+ *
+ * - fileName: The name of the file
+ * - timestamp: The upload timestamp
+ *
+ * @param mixed[] $warnings
+ * @return mixed[]
+ */
+ public static function makeWarningsSerializable( $warnings ) {
+ array_walk_recursive( $warnings, function ( &$param, $key ) {
+ if ( $param instanceof File ) {
+ $param = [
+ 'fileName' => $param->getName(),
+ 'timestamp' => $param->getTimestamp()
+ ];
+ } elseif ( is_object( $param ) ) {
+ throw new InvalidArgumentException(
+ 'UploadBase::makeWarningsSerializable: ' .
+ 'Unexpected object of class ' . get_class( $param ) );
+ }
+ } );
+ return $warnings;
+ }
+
/**
* Check whether the resulting filename is different from the desired one,
* but ignore things like ucfirst() and spaces/underscore things
/**
* @param UserIdentity $user
- * @param TitleValue[] $titles
+ * @param LinkTarget[] $titles
* @return bool
* @throws MWException
*/
"Elbasyouny",
"Omar Ghrida",
"AHmed Khaled",
- "البراء صالح"
+ "البراء صالح",
+ "Dyolf77 (WMF)"
]
},
"tog-underline": "سطر تحت الوصلات:",
"password-change-forbidden": "لا يمكنك تغيير كلمات السر على هذا الويكي.",
"externaldberror": "هناك إما خطأ في دخول قاعدة البيانات الخارجية أو أنه غير مسموح لك بتحديث حسابك الخارجي.",
"login": "تسجيل الدخول",
- "login-security": "توكيد هويتك",
+ "login-security": "تأكيد هويتك",
"nav-login-createaccount": "دخول / إنشاء حساب",
"logout": "تسجيل الخروج",
"userlogout": "خروج",
"changeemail-no-info": "يجب تسجيل الدخول للوصول إلى هذه الصفحة مباشرة.",
"changeemail-oldemail": "عنوان البريد الإلكتروني الحالي:",
"changeemail-newemail": "عنوان البريد الإلكتروني الجديد:",
- "changeemail-newemail-help": "هذا الحقل ينبغي أن يترك فارغا في حالة لو كنت تريد إزالة عنوان البريد الإلكتروني الخاص بك. أنت لن تكون قادرا على إعادة ضبط كلمة سر ضائعة ولن تتلقى رسئل بريد إلكتروني من هذه الويكي لو أزيل عنوان البريد الإلكتروني.",
+ "changeemail-newemail-help": "هذا الحقل يجب أن يترك فارغا لو كنت تريد إزالة عنوان البريد الإلكتروني الخاص بك. لو أزيل عنوان البريد الإلكتروني لن تكون قادرا على إعادة ضبط كلمة سر ضائعة ولن تتلقى رسائل بريد إلكتروني من هذه الويكي.",
"changeemail-none": "(لا شيء)",
"changeemail-password": "كلمة سر {{SITENAME}} الخاصة بك:",
"changeemail-submit": "غيّر البريد الإلكتروني",
"accmailtext": "[[User talk:$1|$1]]-ৰ কাৰণে যাদৃচ্ছিকভাৱে উৎপন্ন কৰা গুপ্তশব্দ $2লৈ পঠোৱা হ'ল । \nএই নতুন একাউন্টত প্ৰৱেশ কৰি ''[[Special:ChangePassword|গুপ্তশব্দ সলনি কৰক]]'' পৃষ্ঠাখনত শব্দতো সলনি কৰি ল’ব পাৰিব ।",
"newarticle": "(নতুন)",
"newarticletext": "আপুনি বিচৰা প্ৰবন্ধটো বিচাৰি পোৱা নগ'ল।\n\nইচ্ছা কৰিলে আপুনিয়েই এই প্ৰবন্ধটো লিখা আৰম্ভ কৰিব পাৰে। [$1 ইয়াত] সহায় পাব।\n\nআপুনি যদি ইয়ালৈ ভুলতে আহিছে, তেনেহলে আপোনাৰ ব্ৰাওজাৰৰ '''BACK''' বুটামত টিপা মাৰক।",
- "anontalkpagetext": "----''এইখন আলোচনা পৃষ্ঠা বেনামী সদস্যৰ বাবে, যিয়ে নিজা একাউণ্ট সৃষ্টি কৰা নাই বা যিয়ে সেই একাউণ্ট ব্যৱহাৰ নকৰে।\nএতেকে আমি তেখেতসকলক আই-পি ঠিকনাৰে চিনাক্ত কৰিবলৈ বাধ্য।\nসেই একেই আই-পি ঠিকনা অনেকেই ব্যৱহাৰ কৰিব পাৰে।\nআপুনি যদি এজন বেনামী সদস্য আৰু যদি আপুনি অনুভৱ কৰে যে আপোনাৰ প্ৰতি অপ্ৰাসঙ্গিক মন্তব্য কৰা হৈছে, তেনেহলে আন বেনামী সদস্যৰ পৰা পৃথক কৰিবলৈ \n[[Special:CreateAccount|একাউণ্ট সৃষ্টি কৰক]] বা [[Special:UserLogin|প্ৰৱেশ কৰক]] ।''",
+ "anontalkpagetext": "----\n<em>এইখন আলোচনা পৃষ্ঠা বেনামী সদস্যৰ বাবে, যিয়ে নিজা একাউণ্ট সৃষ্টি কৰা নাই বা যিয়ে সেই একাউণ্ট ব্যৱহাৰ নকৰে।</em>\nএতেকে আমি তেখেতসকলক আই-পি ঠিকনাৰে চিনাক্ত কৰিবলৈ বাধ্য।\nসেই একেই আই-পি ঠিকনা অনেকেই ব্যৱহাৰ কৰিব পাৰে।\nআপুনি যদি এজন বেনামী সদস্য আৰু যদি আপুনি অনুভৱ কৰে যে আপোনাৰ প্ৰতি অপ্ৰাসঙ্গিক মন্তব্য কৰা হৈছে, তেনেহলে আন বেনামী সদস্যৰ পৰা পৃথক কৰিবলৈ [[Special:CreateAccount|এ টাএকাউণ্ট সৃষ্টি কৰক]] বা [[Special:UserLogin|প্ৰৱেশ কৰক]]।",
"noarticletext": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই ।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}|এই শিৰোনামা সন্ধান কৰিব পাৰে]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে],\nবা [{{fullurl:{{FULLPAGENAME}}|action=edit}} এই পৃষ্ঠা সৃষ্টি কৰিব পাৰে]</span>",
"noarticletext-nopermission": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}|এই শিৰোনামা সন্ধান কৰিব পাৰে]],\nবা <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে]</span>, কিন্তু এই পৃষ্ঠা সৃষ্টি কৰিবলৈ আপোনাৰ অনুমতি নাই।",
"missing-revision": "\"{{FULLPAGENAME}}\" নামৰ পৃষ্ঠাৰ #$1 সংশোধনৰ অস্তিত্ব নাই।\n\nসাধাৰণতে বিলোপ কৰা এখন পৃষ্ঠাৰ পুৰণা ইতিহাস লিংক অনুসৰণ কৰিলে এনে হয়।\n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} বিলোপন ল'গ]ত অধিক তথ্য পাব।",
"page_first": "প্ৰথম",
"page_last": "অন্তিম",
"histlegend": "পাৰ্থক্য বাছনি: পাৰ্থক্য চাবলৈ সংকলনবোৰৰ সম্মুখত থকা ৰেডিঅ' বুটামবোৰ বাচনী কৰি এণ্টাৰ টিপক অথবা একেবাৰে তলত দিয়া বুটামতো ক্লিক কৰক <br />\nলিজেণ্ড: '''({{int:cur}})''' = বৰ্তমানৰ সংকলনৰ লগত পাৰ্থক্য,\n'''({{int:last}})''' = আগৰ সংকলনৰ লগত পাৰ্থক্য, '''{{int:minoreditletter}}'' = অগুৰুত্বপূৰ্ণ সম্পাদনা।",
- "history-fieldset-title": "সংশোধিত সংস্কৰণ সন্ধান কৰক",
+ "history-fieldset-title": "সংশোধিত সংস্কৰণ",
"history-show-deleted": "মাথোঁ বিলোপ কৰা",
"histfirst": "আটাইতকৈ পুৰণি",
"histlast": "শেহতীয়া",
"editundo": "পূৰ্ববত কৰক",
"diff-empty": "(কোনো পাৰ্থক্য নাই)",
"diff-multi-sameuser": "একেজন সদস্যই কৰা ({{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1 মধ্যৱৰ্তী সংশোধন}} দেখুওৱা হোৱা নাই)",
- "diff-multi-otherusers": "({{PLURAL:$2|আন এজন সদস্যই|$2জন সদস্যই}} কৰা ({{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1টা মধ্যৱৰ্তী সংশোধন}} দেখুওৱা হোৱা নাই)",
+ "diff-multi-otherusers": "({{PLURAL:$2|আন এজন সদস্য|$2জন সদস্য}}ই কৰা {{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1টা মধ্যৱৰ্তী সংশোধন}} দেখুওৱা হোৱা নাই)",
"diff-multi-manyusers": "({{PLURAL:$2|এজনতকৈ|$2-জনতকৈ}} অধিক সদস্যৰ দ্বাৰা {{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1-টা মধ্যৱৰ্তী সংশোধন}} দেখুওৱা হোৱা নাই)",
"difference-missing-revision": "{{PLURAL:$2|এটা সংস্কৰণ|$2 সংস্কৰণসমূহৰ}} সংশোধনৰ পাৰ্থক্য ($1) {{PLURAL:$2| পোৱা নগ’ল}}।\n\n\nসাধাৰণতে বিলোপ কৰা এখন পৃষ্ঠাৰ পুৰণা ইতিহাস লিংক অনুসৰণ কৰিলে এনে হয়।\n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} বিলোপন অভিলেখ] চালে অধিক তথ্য পাব।",
"searchresults": "অনুসন্ধানৰ ফলাফল",
"filehist-comment": "মন্তব্য",
"imagelinks": "ফাইল ব্যৱহাৰ",
"linkstoimage": "তলত দিয়া {{PLURAL:$1|পৃষ্ঠাটোৱে|$1 পৃষ্ঠাবোৰে}} এই ফাইলটো ব্যৱহাৰ কৰে:",
- "linkstoimage-more": "à¦\8fà¦\87 ফাà¦\87লৰ লà¦\97ত $1ৰà§\8b বà§\87à¦\9bি {{PLURAL:$1|পà§\83ষà§\8dঠা সà¦\82যà§\8bà¦\97|পà§\83ষà§\8dঠা সà¦\82যà§\8bà¦\97}} হà§\88 à¦\86à¦\9bà§\87 ।\nতলৰ তালিà¦\95াত {{PLURAL:$1|পà§\8dৰথম পà§\83ষà§\8dঠা সà¦\82যà§\8bà¦\97|পà§\8dৰথম $1 পà§\83ষà§\8dঠা সà¦\82যà§\8bà¦\97}} দà§\87à¦\96à§\81à¦\93ৱা হà§\88à¦\9bà§\87 ।\nà¦\8fà¦\96ন [[Special:WhatLinksHere/$2|সমà§\8dপà§\82ৰà§\8dণ তালিà¦\95া]]à¦\93 পাব ।",
+ "linkstoimage-more": "à¦\8fà¦\87 ফাà¦\87লà¦\9fà§\8b $1ৰà§\8b বà§\87à¦\9bি {{PLURAL:$1|পà§\83ষà§\8dঠাà¦\87 বà§\8dযৱহাৰ}} à¦\95ৰà§\87।\nতলৰ তালিà¦\95াত সà¦\82যà§\8bà¦\9cিত {{PLURAL:$1|পà§\8dৰথম পà§\83ষà§\8dঠা|পà§\8dৰথম $1à¦\9fা পà§\83ষà§\8dঠা}} দà§\87à¦\96à§\81à¦\93ৱা হà§\88à¦\9bà§\87।\n[[Special:WhatLinksHere/$2|সমà§\8dপà§\82ৰà§\8dণ তালিà¦\95া à¦\87য়াত]] পাব। $1 {{PLURAL:$1|page uses|pages use}} this file.\nThe following list shows the {{PLURAL:$1|first page|first $1 pages}} that use this file only.\nA [[Special:WhatLinksHere/$2|full list]] is available.",
"nolinkstoimage": "এই ফাইলটো কোনো পৃষ্ঠাই ব্যৱহাৰ কৰা নাই",
"morelinkstoimage": "এই ফাইলৰ [[Special:WhatLinksHere/$1|অধিক সংযোগ]] চাওক ।",
"linkstoimage-redirect": "$1 (ফাইল পুনৰ্নিৰ্দেশ) $2",
"htmlform-cloner-create": "আৰু যোগ কৰক",
"htmlform-cloner-delete": "আঁতৰাওক",
"logentry-delete-delete": "$3 পৃষ্ঠাটো $1ৰদ্বাৰা {{GENDER:$2|বিলোপ কৰা হ'ল}}",
- "logentry-delete-restore": "$1-এ $3 পৃষ্ঠাটো {{GENDER:$2|পুনৰ্সংৰক্ষণ কৰিলে}}",
+ "logentry-delete-restore": "$1-এ $3 ($4) পৃষ্ঠাটো {{GENDER:$2|পুনৰ্সংৰক্ষণ কৰিলে}}",
"logentry-delete-event": "$3: $4 -ত {{PLURAL:$5|এটা লগ ঘটনা|$5 লগ ঘটনাসমূহ}} -ৰ $1 পৰিৱৰ্তন কৰা দৃশ্যমানতা",
"logentry-delete-revision": "পৃষ্ঠা $3ত {{PLURAL:$5|এটা সংশোধন|$5 সংশোধনসমূহ}}ৰ দৃশ্যমানতা $1 {{GENDER:$2|য়ে সলালে}}: $4",
"logentry-delete-event-legacy": "$3ত ল'গ ঘটনাসমূহৰ দৃশ্যমানতা $1 {{GENDER:$2|ৰদ্বাৰা সলোৱা হ'ল}}",
"logentry-move-move_redir": "পুনৰ্নিৰ্দেশৰে পৃষ্ঠা $3ৰ পৰা $4 $1লৈ স্থানান্তৰ কৰা হ’ল",
"logentry-move-move_redir-noredirect": "পুনৰ্নিৰ্দেশ নেৰাকৈ এটা পুনৰ্নিৰ্দেশৰ ওপৰেৰে পৃষ্ঠা $3 -ৰ পৰা $4 $1 স্থানান্তৰ কৰা হল",
"logentry-patrol-patrol": "পৃষ্ঠা $3 -ৰ $1 চিহ্নিত সংশোধন $4 নিৰীক্ষণ কৰা হ'ল",
- "logentry-patrol-patrol-auto": "পৃষ্ঠা $3 -ৰ $1 চিহ্নিত সংশোধন $4 স্বচালিতভাৱে নিৰীক্ষণ কৰা হ'ল",
+ "logentry-patrol-patrol-auto": "$1 স্বচালিতভাৱে $3 পৃষ্ঠাৰ $4 নং সংশোধন নিৰীক্ষণ কৰা হ'ল বুলি {{GENDER:$2|চিহ্নিত}} কৰিছে",
"logentry-newusers-newusers": "ব্যৱহাৰকাৰী একাউণ্ট $1 সৃষ্টি কৰা হ'ল",
"logentry-newusers-create": "ব্যৱহাৰকাৰী একাউণ্ট $1 {{GENDER:$2|সৃষ্টি কৰা হ'ল}}",
"logentry-newusers-create2": "$1ৰ দ্বাৰা এটা ব্যৱহাৰকাৰী একাউণ্ট $3 {{GENDER:$2|সৃষ্টি কৰা হ'ল}}",
"helppage-top-gethelp": "Даведка",
"mainpage": "Галоўная старонка",
"mainpage-description": "Першая старонка",
- "policy-url": "Project:Ð\90Ñ\80ганÑ\96заÑ\86Ñ\8bйнаÑ\8f палÑ\96Ñ\82Ñ\8bка",
+ "policy-url": "Project:СпÑ\96Ñ\81 пÑ\80авÑ\96л Ñ\96 Ñ\80Ñ\8dкамендаÑ\86Ñ\8bй",
"portal": "Супольнасць",
"portal-url": "Project:Супольнасць",
"privacy": "Палітыка прыватнасці",
"delete-legend": "Выдаліць",
"historywarning": "<strong>Увага:</strong> Старонка, якую вы хочаце выдаліць, мае гісторыю з прыблізна $1 {{PLURAL:$1|праўку|праўкі|правак}}:",
"historyaction-submit": "Паказаць",
- "confirmdeletetext": "Ð\92Ñ\8b збÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f вÑ\8bдалÑ\96Ñ\86Ñ\8c Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам з Ñ\83Ñ\81Ñ\91й Ñ\8fе гÑ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй пÑ\80авак.\nÐ\9fаÑ\86веÑ\80дзÑ\96Ñ\86е Ñ\81вой намеÑ\80 зÑ\80абÑ\96Ñ\86Ñ\8c гÑ\8dÑ\82а, Ñ\81ваÑ\91 Ñ\80азÑ\83менне наÑ\81Ñ\82Ñ\83пÑ\81Ñ\82ваÑ\9e, Ñ\96 Ñ\88Ñ\82о Ð\92Ñ\8b Ñ\80обÑ\96Ñ\86е гÑ\8dÑ\82а Ñ\9e адпаведнаÑ\81Ñ\86Ñ\96 з [[{{MediaWiki:Policy-url}}|палÑ\96Ñ\82Ñ\8bкай (аÑ\81ноÑ\9eнÑ\8bмÑ\96 пÑ\80авÑ\96ламÑ\96)]].",
+ "confirmdeletetext": "Ð\92Ñ\8b збÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f вÑ\8bдалÑ\96Ñ\86Ñ\8c Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам з Ñ\83Ñ\81Ñ\91й Ñ\8fе гÑ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй пÑ\80авак.\nÐ\9fаÑ\86веÑ\80дзÑ\96Ñ\86е Ñ\81вой намеÑ\80 зÑ\80абÑ\96Ñ\86Ñ\8c гÑ\8dÑ\82а, Ñ\81ваÑ\91 Ñ\80азÑ\83менне наÑ\81Ñ\82Ñ\83пÑ\81Ñ\82ваÑ\9e, Ñ\96 Ñ\88Ñ\82о Ð\92Ñ\8b Ñ\80обÑ\96Ñ\86е гÑ\8dÑ\82а Ñ\9e адпаведнаÑ\81Ñ\86Ñ\96 з [[{{MediaWiki:Policy-url}}|аÑ\81ноÑ\9eнÑ\8bмÑ\96 пÑ\80авÑ\96ламÑ\96 пÑ\80аекÑ\82а]].",
"actioncomplete": "Завершана аперацыя",
"actionfailed": "Памылка дзеяння",
"deletedtext": "Старонка \"$1\" была выдалена.\nЗапісы аб нядаўніх выдаленнях гл. ў $2.",
"McDutchie",
"Johanna Strodt (WMDE)",
"Andi-3",
- "1233qwer1234qwer4"
+ "1233qwer1234qwer4",
+ "MarkusRost"
]
},
"tog-underline": "Links unterstreichen:",
"mainpage": "Hauptseite",
"mainpage-description": "Hauptseite",
"policy-url": "Project:Richtlinien",
- "portal": "Gemeinschaftsportal",
+ "portal": "Gemeinschafts­portal",
"portal-url": "Project:Gemeinschaftsportal",
"privacy": "Datenschutz",
"privacypage": "Project:Datenschutz",
"authors": [
"Don Alessandro",
"Ильнар",
- "Рашат Якупов"
+ "Рашат Якупов",
+ "Ерней"
]
},
"exif-imagewidth": "Киңлек",
"exif-colorspace": "Төсләр тирәлеге",
"exif-componentsconfiguration": "Төсләр төзелешенең конфигурациясе",
"exif-compressedbitsperpixel": "Кысылудан соң төснең тирәнлеге",
- "exif-pixelxdimension": "Ð Ó\99Ñ\81емнең киңлеге",
- "exif-pixelydimension": "Ð Ó\99Ñ\81емнең биеклеге",
+ "exif-pixelxdimension": "СÑ\83Ñ\80Ó\99Ñ\82 киңлеге",
+ "exif-pixelydimension": "СÑ\83Ñ\80Ó\99Ñ\82 биеклеге",
"exif-usercomment": "Өстәмә җавап",
"exif-relatedsoundfile": "Тавыш файлы җавабы",
"exif-datetimeoriginal": "Чын вакыты",
"statistics-pages-desc": "Omna pagini dil Wiki, inkluzite pagini por facar diskuti, ridirektadi, edc.",
"statistics-files": "Adkargita arkivi",
"statistics-edits": "Quanto di redakti pos ke {{SITENAME}} kreesis",
- "statistics-edits-average": "Mezavalora quanto di redakti per pagino",
+ "statistics-edits-average": "Mezavalora quanto di redakti po pagino",
"statistics-users": "Enrejistrita uzeri",
"statistics-users-active": "Aktiva uzeri",
"statistics-users-active-desc": "Uzeri qui facis ula agado dum la lasta {{PLURAL:$1|dio|$1 dii}}",
"cachedspecial-refresh-now": "Vidar la lasta.",
"categories": "Kategorii",
"categories-submit": "Montrez",
+ "categoriespagetext": "La sequanta {{PLURAL:$1|kategorio|kategorii}} existas en ca wiki, e povas uzesar, o ne.\nVidez anke [[Special:WantedCategories|dezirata kategorii]].",
"categoriesfrom": "Montrez kategorii komencante en:",
"deletedcontributions": "Efacita uzero-kontributaji",
"deletedcontributions-title": "Efacita uzero-kontributaji",
"search-interwiki-more": "(meer)",
"search-interwiki-more-results": "meer resultaten",
"search-relatedarticle": "Gerelateerd",
+ "search-invalid-sort-order": "De sorteervolgorde $1 is onbekend, de normale sorteervolgorde is in plaats daarvan toegepast. Geldige sorteervolgorden zijn: $2",
+ "search-unknown-profile": "Het zoekprofiel $1 is onbekend. Het standaard zoekprofiel zal worden toegepast.",
"searchrelated": "gerelateerd",
"searchall": "alle",
"showingresults": "Hieronder {{PLURAL:$1|staat '''1''' resultaat|staan '''$1''' resultaten}} vanaf #'''$2'''.",
"editingcomment": "(ߛߌ߰ߘߊ߬ ߞߎߘߊ߫) ߡߊߦߟߍ߬ߡߊ߲ ߦߴߌ ߘߐ߫ $1",
"editconflict": "ߝߐߢߐ߲߯ߞߐ ߡߊߦߟߍ߬ߡߊ߲߬: $1",
"yourtext": "ߌ ߟߊ߫ ߛߓߍߟߌ",
+ "storedversion": "ߟߢߊ߬ߟߌ߬ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲",
+ "editingold": "<strong>ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ: ߌ ߦߋ߫ ߟߢߊ߬ߟߌ ߕߎ߬ߡߊ ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߋ߬ ߡߊߦߟߍߡߊ߲ ߞߊ߲߬ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫ ߣߌ߲߬.</strong> \nߣߴߌ ߞߵߊ߬ ߟߊߞߎ߲߬ߘߎ߫߸ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߝߋ߲߫-ߋ-ߝߋ߲߫ ߞߍߣߍ߲߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߞߐ߫߸ ߓߣߐ߬ ߘߌ߫ ߞߴߏ߬ ߓߍ߯ ߘߐ߫.",
+ "unicode-support-fail": "ߊ߬ ߛߓߍߣߍ߲߫ ߦߋ߫ ߞߏ߫ ߞߏ߫ ߌ ߟߊ߫ ߛߏ߲߯ߓߊߟߊ߲ ߘߌ߬ߢߍ߬ߣߍ߲߬ ߕߍ߫ ߎߣߌߞߐߘ ߡߊ߬.ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߞߊ߬ߣߌ߲߬ ߣߍ߲߫߸ ߏ߬ ߘߐ߫ ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߎ߲߬ ߡߊ߫ ߟߊߞߎ߲߬ߘߎ߬ ߡߎߣߎ߲߬.",
+ "yourdiff": "ߓߐߣߍ߲ߢߐ߲߰ߡߊ ߟߎ߬",
+ "editpage-cannot-use-custom-model": "ߞߐߜߍ ߣߌ߲߬ ߞߣߐߘߐ ߛߎ߮ߦߊ ߕߍߣߊ߬ ߛߐ߲߬ ߠߊ߫ ߡߊߦߟߍ߬ߡߊ߲߫ ߠߊ߫.",
"templatesused": "{{PLURAL:$1|ߞߙߊߞߏ|ߞߙߊߞߏ ߟߎ߫}} ߟߎ߫ ߟߊߓߊ߯ߙߊ߫ ߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫",
"templatesusedpreview": "{{PLURAL:$1|ߞߙߊߞߏ|ߞߙߊߞߏ ߟߎ߬}} ߟߋ߬ ߟߊߓߊ߯ߙߊ߫ ߣߍ߲߫ ߢߍߦߋߟߌ ߣߌ߲߬ ߘߐ߫",
"template-protected": "(ߊ߬ ߡߊߞߊ߲ߞߊ߲ߣߍ߲߫ ߠߋ߬)",
"search-filter-title-prefix-reset": "ߞߐߜߍ ߓߍ߯ ߢߌߣߌ߲߫",
"searchresults-title": "ߣߌ߲߬ \"$1\" ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ",
"titlematches": "ߞߐߜߍ ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߢߐ߲߰ߡߊ߬ߣߍ߲߫",
+ "textmatches": "ߞߐߜߍ ߞߟߏߜߍ ߦߋ߫ ߦߋ߲߬",
+ "notextmatches": "ߞߐߜߍ ߞߟߏߜߍ߫ ߕߴߦߋ߲߬",
"prevn": "ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬ {{PLURAL:$1|$1}}",
"nextn": "ߟߊߕߎ߲߰ߠߊ {{PLURAL:$1|$1}}",
"prev-page": "ߞߐߜߍ ߢߍߕߊ",
"prefs-changeswatchlist": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߦߌ߬ߘߊ߬",
"prefs-pageswatchlist": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߣߍ߲ ߠߎ߬",
"prefs-tokenwatchlist": "ߖߐߟߐ߲ߞߐ",
+ "prefs-diffs": "ߓߐߣߍ߲ߢߐ߲߰ߡߊ ߟߎ߬",
"prefs-help-prefershttps": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߣߌ߲߬ ߘߴߊ߬ ߝߏ߲߬ߝߏ߲ ߟߴߌ ߟߊ߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߣߊ߬ߕߐ ߞߊ߲߬.",
"userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߤߊߞߍ",
"userrights-lookup-user": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ߫ ߛߎߥߊ߲ߘߌ߫",
"rcfilters-filter-user-experience-level-unregistered-label": "ߕߐ߯ߛߓߍߓߊߟߌ",
"rcfilters-filter-user-experience-level-unregistered-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߊ ߡߍ߲ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫.",
"rcfilters-filter-user-experience-level-learner-label": "ߞߊ߬ߙߊ߲߬ߠߊ ߟߎ߬",
+ "rcfilters-filter-user-experience-level-experienced-label": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ߫ ߖߊߙߌ߲ߒߕߋ",
"rcfilters-filter-user-experience-level-experienced-description": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ߬ ߕߐ߯ߛߓߍߣߍ߲ ߡߍ߲ ߠߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬ ߅߀߀ ߞߊ߲߬ ߕߟߋ߬ ߃߀ ߓߊ߯ߙߊ߫ ߣߐ.",
+ "rcfilters-filtergroup-automated": "ߞߍߒߖߘߍߦߋ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲",
"rcfilters-filter-bots-label": "ߓߏߕ",
"rcfilters-filter-bots-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߍ߲ ߠߎ߬ ߛߌ߲ߘߌߣߍ߲߫ ߞߍߒߖߘߍߦߋ߫ ߖߐ߯ߙߊ߲ ߠߎ߬ ߘߐ߫.",
"rcfilters-filter-humans-label": "ߡߐ߱ (ߓߏߕ ߕߍ߫)",
"rcfilters-filter-watchlist-watched-description": "ߊ߬ ߡߊߝߊ߬ߟߋ߲߬ ߌ ߟߊ߫ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߞߐߜߍ ߟߎ߬ ߘߐ߫.",
"rcfilters-filter-watchlist-watchednew-label": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ߫ ߞߎߘߊ߫ ߓߘߊ߫ ߡߊߦߟߍ߬ߡߊ߲߫",
"rcfilters-filter-watchlist-notwatched-label": "ߊ߬ ߕߍ߫ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߘߐ߫",
+ "rcfilters-filtergroup-watchlistactivity": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߡߛߍ߬ߞߍ߬ߡߛߍߞߍ",
+ "rcfilters-filter-watchlistactivity-unseen-label": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߦߋߓߊߟߌ ߟߎ߬",
+ "rcfilters-filter-watchlistactivity-seen-label": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߠߎ߫ ߦߋ߫",
"rcfilters-filtergroup-changetype": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߛߎ߯ߦߊ",
"rcfilters-filter-pageedits-label": "ߞߐߜߐ ߡߊߦߟߍ߬ߡߊ߲߫",
"rcfilters-filter-pageedits-description": "ߞߐߜߍ ߛߌ߲ߘߟߌ",
"uploadwarning-text": "ߞߐߕߐ߮ ߘߎ߰ߟߊ߬ߘߐ߫ ߞߊ߲ߛߓߍߟߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߫ ߖߊ߰ߣߌ߲߬߸ ߞߵߊ߬ ߡߊߝߍߣߍ߲߫ ߕߎ߲߯.",
"savefile": "ߞߐߕߐ߮ ߟߊߞߎ߲߬ߘߎ߬",
"upload-source": "ߞߐߕߐ߮ ߛߎ߲",
+ "sourcefilename": "ߞߐߕߐ߮ ߕߐ߮ ߛߎ߲:",
"sourceurl": "URL ߛߎ߲:",
"destfilename": "ߞߐߕߐ߮ ߕߐ߮ ߞߎ߲߬ߕߋߟߋ߲:",
"upload-maxfilesize": "ߞߐߕߐ߮ ߢߊ߲ߞߊ߲ ߞߐߘߊ߲: $1",
"upload-dialog-button-upload": "ߟߊ߬ߦߟߍ߬ߟߌ",
"upload-form-label-infoform-title": "ߝߊߙߊ߲ߝߊ߯ߛߟߌ",
"upload-form-label-infoform-name": "ߕߐ߮",
+ "upload-form-label-infoform-description": "ߞߊ߲߬ߛߓߍߟߌ",
"upload-form-label-usage-title": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ",
"upload-form-label-usage-filename": "ߞߐߕߐ߮ ߕߐ߮",
"upload-form-label-own-work": "ߒ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߓߊ߯ߙߊ ߟߋ߬",
"license-header": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߴߌ ߘߐ߫",
"nolicense": "ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬",
"listfiles-delete": "ߊ߬ ߖߏ߬ߛߌ߬",
+ "listfiles_search_for": "ߡߍ߲ߕߊߦߋߕߊ ߕߐ߮ ߢߌߣߌ߲ߠߌ߲:",
+ "listfiles-userdoesnotexist": "ߟߊ߬ߓߊ߰ߙߊ߬ ߖߊߕߋߘߊ \"$1\" ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲߫ ߕߍ߫.",
"imgfile": "ߞߐߕߐ߮",
"listfiles": "ߞߐߕߐ߮ ߛߙߍߘߍ",
"listfiles_thumb": "ߞߝߊ߬ߟߋ߲ߛߋ߲",
"search-interwiki-more-results": "Więcej wyników",
"search-relatedarticle": "Pokrewne",
"search-invalid-sort-order": "Kolejność sortowania $1 jest nierozpoznawana. Zastosowane zostanie domyślne sortowanie. Właściwymi kolejnościami są: $2",
+ "search-unknown-profile": "Profil wyszukiwania $1 jest nierozpoznawany. Zostanie zastosowany domyślny profil.",
"searchrelated": "pokrewne",
"searchall": "wszystkie",
"showingresults": "Poniżej znajduje się lista {{PLURAL:$1|z '''1''' wynikiem|'''$1''' wyników}}, rozpoczynając od wyniku numer '''$2'''.",
/* MediaWiki Special pages */
'mediawiki.rcfilters.filters.base.styles' => [
+ 'targets' => [ 'desktop', 'mobile' ],
'skinStyles' => [
'default' => 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less',
],
],
],
'mediawiki.rcfilters.filters.dm' => [
+ 'targets' => [ 'desktop', 'mobile' ],
'localBasePath' => "$IP/resources/src/mediawiki.rcfilters",
'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.rcfilters",
'packageFiles' => [
],
],
'mediawiki.rcfilters.filters.ui' => [
+ 'targets' => [ 'desktop', 'mobile' ],
'localBasePath' => "$IP/resources/src/mediawiki.rcfilters",
'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.rcfilters",
'packageFiles' => [
// Can't, sorry.
},
apiCheckValid: function () {
- var ok = this.getValue() !== null || suppressErrors;
- this.setIcon( ok ? null : 'alert' );
+ var ok = this.getValue() !== null && this.getValue() !== undefined || suppressErrors;
+ this.info.setIcon( ok ? null : 'alert' );
this.setTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
return $.Deferred().resolve( ok ).promise();
}
*
* @property {mw.Map} config
*/
- // Dummy placeholder later assigned in ResourceLoaderStartUpModule
- config: null,
+ config: new Map( $VARS.wgLegacyJavaScriptGlobals ),
/**
* Empty object for third-party libraries, for cases where you don't
}
if (
+ !$VARS.storeEnabled ||
+
// Disabled because localStorage quotas are tight and (in Firefox's case)
// shared by multiple origins.
// See T66721, and <https://bugzilla.mozilla.org/show_bug.cgi?id=1064466>.
- /Firefox/.test( navigator.userAgent ) ||
-
- // Disabled by configuration.
- !mw.config.get( 'wgResourceLoaderStorageEnabled' )
+ /Firefox/.test( navigator.userAgent )
) {
// Clear any previous store to free up space. (T66721)
this.clear();
this.enabled = false;
return;
}
- if ( mw.config.get( 'debug' ) ) {
- // Disable module store in debug mode
- this.enabled = false;
- return;
- }
try {
// This a string we stored, or `null` if the key does not (yet) exist.
*/
( function () {
/* global mw */
- mw.config = new mw.Map( $VARS.wgLegacyJavaScriptGlobals );
$CODE.registrations();
public function setupDatabase( $nextTeardown = null ) {
global $wgDBprefix;
- $this->db = wfGetDB( DB_MASTER );
+ $this->db = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_MASTER );
$dbType = $this->db->getType();
if ( $dbType == 'oracle' ) {
*/
public function testValidCovers() {
$methods = get_class_methods( $this );
- $class = get_class( $this );
+ $class = static::class;
$bad = '';
foreach ( $methods as $method ) {
if ( strpos( $method, 'test' ) === 0 ) {
use MediaWiki\Revision\SlotRoleRegistry;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\SqlBlobStore;
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\Rdbms\MaintainableDBConnRef;
use MediaWikiTestCase;
use MWException;
use Title;
->disableOriginalConstructor()->getMock();
}
+ /**
+ * @param ILoadBalancer $mockLoadBalancer
+ * @param Database $db
+ * @return callable
+ */
+ private function getMockDBConnRefCallback( ILoadBalancer $mockLoadBalancer, IDatabase $db ) {
+ return function ( $i, $g, $domain, $flg ) use ( $mockLoadBalancer, $db ) {
+ return new MaintainableDBConnRef( $mockLoadBalancer, $db, $i );
+ };
+ }
+
/**
* @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
*/
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
+ // RevisionStore uses getConnectionRef
+ $mockLoadBalancer->expects( $this->any() )
+ ->method( 'getConnectionRef' )
+ ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
+ // Title calls wfGetDB() which uses getMaintenanceConnectionRef
$mockLoadBalancer->expects( $this->atLeastOnce() )
- ->method( 'getConnection' )
- ->willReturn( $db );
+ ->method( 'getMaintenanceConnectionRef' )
+ ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
+ // Title calls wfGetDB() which uses getMaintenanceConnectionRef
// Assert that the first call uses a REPLICA and the second falls back to master
- $mockLoadBalancer->expects( $this->exactly( 2 ) )
- ->method( 'getConnection' )
- ->willReturn( $db );
- // RevisionStore getTitle uses a ConnectionRef
$mockLoadBalancer->expects( $this->atLeastOnce() )
->method( 'getConnectionRef' )
- ->willReturn( $db );
+ ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
+ // Title calls wfGetDB() which uses getMaintenanceConnectionRef
+ $mockLoadBalancer->expects( $this->exactly( 2 ) )
+ ->method( 'getMaintenanceConnectionRef' )
+ ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
- $mockLoadBalancer->expects( $this->atLeastOnce() )
- ->method( 'getConnection' )
- ->willReturn( $db );
- // RevisionStore getTitle uses a ConnectionRef
$mockLoadBalancer->expects( $this->atLeastOnce() )
->method( 'getConnectionRef' )
- ->willReturn( $db );
+ ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
+ // Title calls wfGetDB() which uses getMaintenanceConnectionRef
+ // RevisionStore getTitle uses getMaintenanceConnectionRef
+ $mockLoadBalancer->expects( $this->atLeastOnce() )
+ ->method( 'getMaintenanceConnectionRef' )
+ ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
// Assert that the first call uses a REPLICA and the second falls back to master
- $mockLoadBalancer->expects( $this->exactly( 2 ) )
- ->method( 'getConnection' )
- ->willReturn( $db );
- // RevisionStore getTitle uses a ConnectionRef
+ // RevisionStore uses getMaintenanceConnectionRef
$mockLoadBalancer->expects( $this->atLeastOnce() )
->method( 'getConnectionRef' )
- ->willReturn( $db );
+ ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
+ // Title calls wfGetDB() which uses getMaintenanceConnectionRef
+ $mockLoadBalancer->expects( $this->exactly( 2 ) )
+ ->method( 'getMaintenanceConnectionRef' )
+ ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
+ // Title calls wfGetDB() which uses getMaintenanceConnectionRef
// Assert that the first call uses a REPLICA and the second falls back to master
// RevisionStore getTitle uses getConnectionRef
- // Title::newFromID uses getConnection
- foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
+ // Title::newFromID uses getMaintenanceConnectionRef
+ foreach ( [
+ 'getConnectionRef', 'getMaintenanceConnectionRef'
+ ] as $method ) {
$mockLoadBalancer->expects( $this->exactly( 2 ) )
->method( $method )
->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
];
}
- /**
- * @covers ::isLocallyBlockedProxy
- */
- public function testIsLocallyBlockedProxyDeprecated() {
- $proxy = '1.2.3.4';
-
- $this->hideDeprecated(
- 'IP addresses in the keys of $wgProxyList (found the following IP ' .
- 'addresses in keys: ' . $proxy . ', please move them to values)'
- );
-
- $blockManager = TestingAccessWrapper::newFromObject(
- $this->getBlockManager( [
- 'wgProxyList' => [ $proxy => 'test' ]
- ] )
- );
-
- $ip = '1.2.3.4';
- $this->assertTrue( $blockManager->isLocallyBlockedProxy( $ip ) );
- }
-
/**
* @dataProvider provideIsDnsBlacklisted
* @covers ::isDnsBlacklisted
];
}
+ function providePngZipConfusion() {
+ return [
+ [
+ 'An invalid ZIP file due to the signature being too close to the ' .
+ 'end to accomodate an EOCDR',
+ 'zip-sig-near-end.png',
+ 'image/png',
+ ],
+ [
+ 'An invalid ZIP file due to the comment length running beyond the ' .
+ 'end of the file',
+ 'zip-comment-overflow.png',
+ 'image/png',
+ ],
+ [
+ 'A ZIP file similar to the above, but without either of those two ' .
+ 'problems. Not a valid ZIP file, but it passes MimeAnalyzer\'s ' .
+ 'definition of a ZIP file. This is mostly a sanity check of the ' .
+ 'above two tests.',
+ 'zip-kind-of-valid.png',
+ 'application/zip',
+ ],
+ [
+ 'As above with non-zero comment length',
+ 'zip-kind-of-valid-2.png',
+ 'application/zip',
+ ],
+ ];
+ }
+
+ /** @dataProvider providePngZipConfusion */
+ function testPngZipConfusion( $description, $fileName, $expectedType ) {
+ $file = __DIR__ . '/../../../data/media/' . $fileName;
+ $actualType = $this->doGuessMimeType( [ $file, 'png' ] );
+ $this->assertEquals( $expectedType, $actualType, $description );
+ }
}
* @covers MediumSpecificBagOStuff::changeTTL
*/
public function testChangeTTL() {
+ $now = 1563892142;
+ $this->cache->setMockTime( $now );
+
$key = $this->cache->makeKey( self::TEST_KEY );
$value = 'meow';
$this->assertFalse( $this->cache->changeTTL( $key, 15 ) );
$this->cache->add( $key, $value, 5 );
- $this->assertTrue( $this->cache->changeTTL( $key, time() - 3600 ) );
+ $this->assertTrue( $this->cache->changeTTL( $key, $now - 3600 ) );
$this->assertFalse( $this->cache->get( $key ) );
}
* @covers MediumSpecificBagOStuff::changeTTLMulti
*/
public function testChangeTTLMulti() {
+ $now = 1563892142;
+ $this->cache->setMockTime( $now );
+
$key1 = $this->cache->makeKey( 'test-key1' );
$key2 = $this->cache->makeKey( 'test-key2' );
$key3 = $this->cache->makeKey( 'test-key3' );
$this->assertEquals( 2, $this->cache->get( $key2 ) );
$this->assertEquals( 3, $this->cache->get( $key3 ) );
- $ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3 ], time() + 86400 );
+ $ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3 ], $now + 86400 );
$this->assertTrue( $ok, "Expiry set for all keys" );
$ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3, $key4 ], 300 );
* @covers MediumSpecificBagOStuff::getWithSetCallback
*/
public function testGetWithSetCallback() {
+ $now = 1563892142;
+ $this->cache->setMockTime( $now );
$key = $this->cache->makeKey( self::TEST_KEY );
+
+ $this->assertFalse( $this->cache->get( $key ), "No value" );
+
$value = $this->cache->getWithSetCallback(
$key,
30,
- function () {
+ function ( &$ttl ) {
+ $ttl = 10;
+
return 'hello kitty';
}
);
$this->assertEquals( 'hello kitty', $value );
- $this->assertEquals( $value, $this->cache->get( $key ) );
+ $this->assertEquals( $value, $this->cache->get( $key ), "Value set" );
+
+ $now += 11;
+
+ $this->assertFalse( $this->cache->get( $key ), "Value expired" );
}
/**
public function testFlagSetting() {
$db = $this->db;
$origTrx = $db->getFlag( DBO_TRX );
- $origSsl = $db->getFlag( DBO_SSL );
+ $origNoBuffer = $db->getFlag( DBO_NOBUFFER );
$origTrx
? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
: $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
$this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
- $origSsl
- ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
- : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
- $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
+ $origNoBuffer
+ ? $db->clearFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR )
+ : $db->setFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR );
+ $this->assertEquals( !$origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
$db->restoreFlags( $db::RESTORE_INITIAL );
$this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
- $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+ $this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
$origTrx
? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
: $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
- $origSsl
- ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
- : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
+ $origNoBuffer
+ ? $db->clearFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR )
+ : $db->setFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR );
$db->restoreFlags();
- $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+ $this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
$this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
$db->restoreFlags();
- $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+ $this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
$this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
}
+ public function provideImmutableDBOFlags() {
+ return [
+ [ Database::DBO_IGNORE ],
+ [ Database::DBO_DEFAULT ],
+ [ Database::DBO_PERSISTENT ]
+ ];
+ }
+
/**
- * @expectedException UnexpectedValueException
+ * @expectedException DBUnexpectedError
* @covers Wikimedia\Rdbms\Database::setFlag
+ * @dataProvider provideImmutableDBOFlags
+ * @param int $flag
*/
- public function testDBOIgnoreSet() {
+ public function testDBOCannotSet( $flag ) {
$db = $this->getMockBuilder( DatabaseMysqli::class )
->disableOriginalConstructor()
->setMethods( null )
->getMock();
- $db->setFlag( Database::DBO_IGNORE );
+ $db->setFlag( $flag );
}
/**
- * @expectedException UnexpectedValueException
+ * @expectedException DBUnexpectedError
* @covers Wikimedia\Rdbms\Database::clearFlag
+ * @dataProvider provideImmutableDBOFlags
+ * @param int $flag
*/
- public function testDBOIgnoreClear() {
+ public function testDBOCannotClear( $flag ) {
$db = $this->getMockBuilder( DatabaseMysqli::class )
->disableOriginalConstructor()
->setMethods( null )
->getMock();
- $db->clearFlag( Database::DBO_IGNORE );
+ $db->clearFlag( $flag );
}
/**
* @dataProvider provideSuppressBlockLogDatabaseRows
*/
public function testSuppressBlockLogDatabaseRows( $row, $extra ) {
+ $this->setMwGlobals(
+ 'wgGroupPermissions',
+ [
+ 'oversight' => [
+ 'viewsuppressed' => true,
+ 'suppressionlog' => true,
+ ],
+ ]
+ );
+ $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] );
+ }
+
+ /**
+ * Provide different rows from the logging table to test
+ * for backward compatibility.
+ * Do not change the existing data, just add a new database row
+ */
+ public static function provideSuppressBlockLogDatabaseRowsNonPrivileged() {
+ return [
+ // Current log format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'block',
+ 'comment' => 'Block comment',
+ 'user' => 0,
+ 'user_text' => 'Sysop',
+ 'namespace' => NS_USER,
+ 'title' => 'Logtestuser',
+ 'params' => [
+ '5::duration' => 'infinite',
+ '6::flags' => 'anononly',
+ ],
+ ],
+ [
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [
+ 'duration' => 'infinite',
+ 'flags' => [ 'anononly' ],
+ ],
+ ],
+ ],
+
+ // legacy log
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'block',
+ 'comment' => 'Block comment',
+ 'user' => 0,
+ 'user_text' => 'Sysop',
+ 'namespace' => NS_USER,
+ 'title' => 'Logtestuser',
+ 'params' => [
+ 'infinite',
+ 'anononly',
+ ],
+ ],
+ [
+ 'legacy' => true,
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [
+ 'duration' => 'infinite',
+ 'flags' => [ 'anononly' ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSuppressBlockLogDatabaseRowsNonPrivileged
+ */
+ public function testSuppressBlockLogDatabaseRowsNonPrivileged( $row, $extra ) {
+ $this->user = $this->getTestUser()->getUser();
$this->doTestLogFormatter( $row, $extra );
}
* @dataProvider provideSuppressReblockLogDatabaseRows
*/
public function testSuppressReblockLogDatabaseRows( $row, $extra ) {
+ $this->setMwGlobals(
+ 'wgGroupPermissions',
+ [
+ 'oversight' => [
+ 'viewsuppressed' => true,
+ 'suppressionlog' => true,
+ ],
+ ]
+ );
+ $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] );
+ }
+
+ /**
+ * Provide different rows from the logging table to test
+ * for backward compatibility.
+ * Do not change the existing data, just add a new database row
+ */
+ public static function provideSuppressReblockLogDatabaseRowsNonPrivileged() {
+ return [
+ // Current log format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'reblock',
+ 'comment' => 'Block comment',
+ 'user' => 0,
+ 'user_text' => 'Sysop',
+ 'namespace' => NS_USER,
+ 'title' => 'Logtestuser',
+ 'params' => [
+ '5::duration' => 'infinite',
+ '6::flags' => 'anononly',
+ ],
+ ],
+ [
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [
+ 'duration' => 'infinite',
+ 'flags' => [ 'anononly' ],
+ ],
+ ],
+ ],
+
+ // Legacy format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'reblock',
+ 'comment' => 'Block comment',
+ 'user' => 0,
+ 'user_text' => 'Sysop',
+ 'namespace' => NS_USER,
+ 'title' => 'Logtestuser',
+ 'params' => [
+ 'infinite',
+ 'anononly',
+ ],
+ ],
+ [
+ 'legacy' => true,
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [
+ 'duration' => 'infinite',
+ 'flags' => [ 'anononly' ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSuppressReblockLogDatabaseRowsNonPrivileged
+ */
+ public function testSuppressReblockLogDatabaseRowsNonPrivileged( $row, $extra ) {
+ $this->user = $this->getTestUser()->getUser();
$this->doTestLogFormatter( $row, $extra );
}
* @dataProvider provideSuppressRevisionLogDatabaseRows
*/
public function testSuppressRevisionLogDatabaseRows( $row, $extra ) {
+ $this->setMwGlobals(
+ 'wgGroupPermissions',
+ [
+ 'oversight' => [
+ 'viewsuppressed' => true,
+ 'suppressionlog' => true,
+ ],
+ ]
+ );
+ $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] );
+ }
+
+ /**
+ * Provide different rows from the logging table to test
+ * for backward compatibility.
+ * Do not change the existing data, just add a new database row
+ */
+ public static function provideSuppressRevisionLogDatabaseRowsNonPrivileged() {
+ return [
+ // Current format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'revision',
+ 'comment' => 'Suppress comment',
+ 'namespace' => NS_MAIN,
+ 'title' => 'Page',
+ 'params' => [
+ '4::type' => 'archive',
+ '5::ids' => [ '1', '3', '4' ],
+ '6::ofield' => '1',
+ '7::nfield' => '10',
+ ],
+ ],
+ [
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [
+ 'type' => 'archive',
+ 'ids' => [ '1', '3', '4' ],
+ 'old' => [
+ 'bitmask' => 1,
+ 'content' => true,
+ 'comment' => false,
+ 'user' => false,
+ 'restricted' => false,
+ ],
+ 'new' => [
+ 'bitmask' => 10,
+ 'content' => false,
+ 'comment' => true,
+ 'user' => false,
+ 'restricted' => true,
+ ],
+ ],
+ ],
+ ],
+
+ // Legacy format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'revision',
+ 'comment' => 'Suppress comment',
+ 'namespace' => NS_MAIN,
+ 'title' => 'Page',
+ 'params' => [
+ 'archive',
+ '1,3,4',
+ 'ofield=1',
+ 'nfield=10',
+ ],
+ ],
+ [
+ 'legacy' => true,
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [
+ 'type' => 'archive',
+ 'ids' => [ '1', '3', '4' ],
+ 'old' => [
+ 'bitmask' => 1,
+ 'content' => true,
+ 'comment' => false,
+ 'user' => false,
+ 'restricted' => false,
+ ],
+ 'new' => [
+ 'bitmask' => 10,
+ 'content' => false,
+ 'comment' => true,
+ 'user' => false,
+ 'restricted' => true,
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSuppressRevisionLogDatabaseRowsNonPrivileged
+ */
+ public function testSuppressRevisionLogDatabaseRowsNonPrivileged( $row, $extra ) {
+ $this->user = $this->getTestUser()->getUser();
$this->doTestLogFormatter( $row, $extra );
}
* @dataProvider provideSuppressEventLogDatabaseRows
*/
public function testSuppressEventLogDatabaseRows( $row, $extra ) {
+ $this->setMwGlobals(
+ 'wgGroupPermissions',
+ [
+ 'oversight' => [
+ 'viewsuppressed' => true,
+ 'suppressionlog' => true,
+ ],
+ ]
+ );
+ $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] );
+ }
+
+ /**
+ * Provide different rows from the logging table to test
+ * for backward compatibility.
+ * Do not change the existing data, just add a new database row
+ */
+ public static function provideSuppressEventLogDatabaseRowsNonPrivileged() {
+ return [
+ // Current format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'event',
+ 'comment' => 'Suppress comment',
+ 'namespace' => NS_MAIN,
+ 'title' => 'Page',
+ 'params' => [
+ '4::ids' => [ '1', '3', '4' ],
+ '5::ofield' => '1',
+ '6::nfield' => '10',
+ ],
+ ],
+ [
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [
+ 'type' => 'logging',
+ 'ids' => [ '1', '3', '4' ],
+ 'old' => [
+ 'bitmask' => 1,
+ 'content' => true,
+ 'comment' => false,
+ 'user' => false,
+ 'restricted' => false,
+ ],
+ 'new' => [
+ 'bitmask' => 10,
+ 'content' => false,
+ 'comment' => true,
+ 'user' => false,
+ 'restricted' => true,
+ ],
+ ],
+ ],
+ ],
+
+ // Legacy format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'event',
+ 'comment' => 'Suppress comment',
+ 'namespace' => NS_MAIN,
+ 'title' => 'Page',
+ 'params' => [
+ '1,3,4',
+ 'ofield=1',
+ 'nfield=10',
+ ],
+ ],
+ [
+ 'legacy' => true,
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [
+ 'type' => 'logging',
+ 'ids' => [ '1', '3', '4' ],
+ 'old' => [
+ 'bitmask' => 1,
+ 'content' => true,
+ 'comment' => false,
+ 'user' => false,
+ 'restricted' => false,
+ ],
+ 'new' => [
+ 'bitmask' => 10,
+ 'content' => false,
+ 'comment' => true,
+ 'user' => false,
+ 'restricted' => true,
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSuppressEventLogDatabaseRowsNonPrivileged
+ */
+ public function testSuppressEventLogDatabaseRowsNonPrivileged( $row, $extra ) {
+ $this->user = $this->getTestUser()->getUser();
$this->doTestLogFormatter( $row, $extra );
}
* @dataProvider provideSuppressDeleteLogDatabaseRows
*/
public function testSuppressDeleteLogDatabaseRows( $row, $extra ) {
+ $this->setMwGlobals(
+ 'wgGroupPermissions',
+ [
+ 'oversight' => [
+ 'viewsuppressed' => true,
+ 'suppressionlog' => true,
+ ],
+ ]
+ );
+ $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] );
+ }
+
+ /**
+ * Provide different rows from the logging table to test
+ * for backward compatibility.
+ * Do not change the existing data, just add a new database row
+ */
+ public static function provideSuppressDeleteLogDatabaseRowsNonPrivileged() {
+ return [
+ // Current format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'delete',
+ 'comment' => 'delete comment',
+ 'namespace' => NS_MAIN,
+ 'title' => 'Page',
+ 'params' => [],
+ ],
+ [
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [],
+ ],
+ ],
+
+ // Legacy format
+ [
+ [
+ 'type' => 'suppress',
+ 'action' => 'delete',
+ 'comment' => 'delete comment',
+ 'namespace' => NS_MAIN,
+ 'title' => 'Page',
+ 'params' => [],
+ ],
+ [
+ 'legacy' => true,
+ 'text' => '(username removed) (log details removed)',
+ 'api' => [],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSuppressDeleteLogDatabaseRowsNonPrivileged
+ */
+ public function testSuppressDeleteLogDatabaseRowsNonPrivileged( $row, $extra ) {
+ $this->user = $this->getTestUser()->getUser();
$this->doTestLogFormatter( $row, $extra );
}
}
*/
abstract class LogFormatterTestCase extends MediaWikiLangTestCase {
- public function doTestLogFormatter( $row, $extra ) {
+ public function doTestLogFormatter( $row, $extra, $userGroups = [] ) {
RequestContext::resetMain();
$row = $this->expandDatabaseRow( $row, $this->isLegacy( $extra ) );
+ $context = new RequestContext();
+ $context->setUser( $this->getTestUser( $userGroups )->getUser() );
+
$formatter = LogFormatter::newFromRow( $row );
+ $formatter->setContext( $context );
$this->assertEquals(
$extra['text'],
+++ /dev/null
-<?php
-
-/**
- * @group Media
- * @covers SVGMetadataExtractor
- */
-class SVGMetadataExtractorTest extends \MediaWikiIntegrationTestCase {
-
- /**
- * @dataProvider provideSvgFiles
- */
- public function testGetMetadata( $infile, $expected ) {
- $this->assertMetadata( $infile, $expected );
- }
-
- /**
- * @dataProvider provideSvgFilesWithXMLMetadata
- */
- public function testGetXMLMetadata( $infile, $expected ) {
- $r = new XMLReader();
- $this->assertMetadata( $infile, $expected );
- }
-
- /**
- * @dataProvider provideSvgUnits
- */
- public function testScaleSVGUnit( $inUnit, $expected ) {
- $this->assertEquals(
- $expected,
- SVGReader::scaleSVGUnit( $inUnit ),
- 'SVG unit conversion and scaling failure'
- );
- }
-
- function assertMetadata( $infile, $expected ) {
- try {
- $data = SVGMetadataExtractor::getMetadata( $infile );
- $this->assertEquals( $expected, $data, 'SVG metadata extraction test' );
- } catch ( MWException $e ) {
- if ( $expected === false ) {
- $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' );
- } else {
- throw $e;
- }
- }
- }
-
- public static function provideSvgFiles() {
- $base = __DIR__ . '/../../data/media';
-
- return [
- [
- "$base/Wikimedia-logo.svg",
- [
- 'width' => 1024,
- 'height' => 1024,
- 'originalWidth' => '1024',
- 'originalHeight' => '1024',
- 'translations' => [],
- ]
- ],
- [
- "$base/QA_icon.svg",
- [
- 'width' => 60,
- 'height' => 60,
- 'originalWidth' => '60',
- 'originalHeight' => '60',
- 'translations' => [],
- ]
- ],
- [
- "$base/Gtk-media-play-ltr.svg",
- [
- 'width' => 60,
- 'height' => 60,
- 'originalWidth' => '60.0000000',
- 'originalHeight' => '60.0000000',
- 'translations' => [],
- ]
- ],
- [
- "$base/Toll_Texas_1.svg",
- // This file triggered T33719, needs entity expansion in the xmlns checks
- [
- 'width' => 385,
- 'height' => 385,
- 'originalWidth' => '385',
- 'originalHeight' => '385.0004883',
- 'translations' => [],
- ]
- ],
- [
- "$base/Tux.svg",
- [
- 'width' => 512,
- 'height' => 594,
- 'originalWidth' => '100%',
- 'originalHeight' => '100%',
- 'title' => 'Tux',
- 'translations' => [],
- 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
- ]
- ],
- [
- "$base/Speech_bubbles.svg",
- [
- 'width' => 627,
- 'height' => 461,
- 'originalWidth' => '17.7cm',
- 'originalHeight' => '13cm',
- 'translations' => [
- 'de' => SVGReader::LANG_FULL_MATCH,
- 'fr' => SVGReader::LANG_FULL_MATCH,
- 'nl' => SVGReader::LANG_FULL_MATCH,
- 'tlh-ca' => SVGReader::LANG_FULL_MATCH,
- 'tlh' => SVGReader::LANG_PREFIX_MATCH
- ],
- ]
- ],
- [
- "$base/Soccer_ball_animated.svg",
- [
- 'width' => 150,
- 'height' => 150,
- 'originalWidth' => '150',
- 'originalHeight' => '150',
- 'animated' => true,
- 'translations' => []
- ],
- ],
- [
- "$base/comma_separated_viewbox.svg",
- [
- 'width' => 512,
- 'height' => 594,
- 'originalWidth' => '100%',
- 'originalHeight' => '100%',
- 'translations' => []
- ],
- ],
- ];
- }
-
- public static function provideSvgFilesWithXMLMetadata() {
- $base = __DIR__ . '/../../data/media';
- // phpcs:disable Generic.Files.LineLength
- $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
- <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about="">
- <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format>
- <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
- </ns4:Work>
- </rdf:RDF>';
- // phpcs:enable
-
- $metadata = str_replace( "\r", '', $metadata ); // Windows compat
- return [
- [
- "$base/US_states_by_total_state_tax_revenue.svg",
- [
- 'height' => 593,
- 'metadata' => $metadata,
- 'width' => 959,
- 'originalWidth' => '958.69',
- 'originalHeight' => '592.78998',
- 'translations' => [],
- ]
- ],
- ];
- }
-
- public static function provideSvgUnits() {
- return [
- [ '1' , 1 ],
- [ '1.1' , 1.1 ],
- [ '0.1' , 0.1 ],
- [ '.1' , 0.1 ],
- [ '1e2' , 100 ],
- [ '1E2' , 100 ],
- [ '+1' , 1 ],
- [ '-1' , -1 ],
- [ '-1.1' , -1.1 ],
- [ '1e+2' , 100 ],
- [ '1e-2' , 0.01 ],
- [ '10px' , 10 ],
- [ '10pt' , 10 * 1.25 ],
- [ '10pc' , 10 * 15 ],
- [ '10mm' , 10 * 3.543307 ],
- [ '10cm' , 10 * 35.43307 ],
- [ '10in' , 10 * 90 ],
- [ '10em' , 10 * 16 ],
- [ '10ex' , 10 * 12 ],
- [ '10%' , 51.2 ],
- [ '10 px' , 10 ],
- // Invalid values
- [ '1e1.1', 10 ],
- [ '10bp', 10 ],
- [ 'p10', null ],
- ];
- }
-}
--- /dev/null
+<?php
+
+/**
+ * @group Media
+ * @covers SVGReader
+ */
+class SVGReaderTest extends \MediaWikiIntegrationTestCase {
+
+ /**
+ * @dataProvider provideSvgFiles
+ */
+ public function testGetMetadata( $infile, $expected ) {
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ /**
+ * @dataProvider provideSvgFilesWithXMLMetadata
+ */
+ public function testGetXMLMetadata( $infile, $expected ) {
+ $r = new XMLReader();
+ $this->assertMetadata( $infile, $expected );
+ }
+
+ /**
+ * @dataProvider provideSvgUnits
+ */
+ public function testScaleSVGUnit( $inUnit, $expected ) {
+ $this->assertEquals(
+ $expected,
+ SVGReader::scaleSVGUnit( $inUnit ),
+ 'SVG unit conversion and scaling failure'
+ );
+ }
+
+ function assertMetadata( $infile, $expected ) {
+ try {
+ $svgReader = new SVGReader( $infile );
+ $data = $svgReader->getMetadata();
+
+ $this->assertEquals( $expected, $data, 'SVG metadata extraction test' );
+ } catch ( MWException $e ) {
+ if ( $expected === false ) {
+ $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' );
+ } else {
+ throw $e;
+ }
+ }
+ }
+
+ public static function provideSvgFiles() {
+ $base = __DIR__ . '/../../data/media';
+
+ return [
+ [
+ "$base/Wikimedia-logo.svg",
+ [
+ 'width' => 1024,
+ 'height' => 1024,
+ 'originalWidth' => '1024',
+ 'originalHeight' => '1024',
+ 'translations' => [],
+ ]
+ ],
+ [
+ "$base/QA_icon.svg",
+ [
+ 'width' => 60,
+ 'height' => 60,
+ 'originalWidth' => '60',
+ 'originalHeight' => '60',
+ 'translations' => [],
+ ]
+ ],
+ [
+ "$base/Gtk-media-play-ltr.svg",
+ [
+ 'width' => 60,
+ 'height' => 60,
+ 'originalWidth' => '60.0000000',
+ 'originalHeight' => '60.0000000',
+ 'translations' => [],
+ ]
+ ],
+ [
+ "$base/Toll_Texas_1.svg",
+ // This file triggered T33719, needs entity expansion in the xmlns checks
+ [
+ 'width' => 385,
+ 'height' => 385,
+ 'originalWidth' => '385',
+ 'originalHeight' => '385.0004883',
+ 'translations' => [],
+ ]
+ ],
+ [
+ "$base/Tux.svg",
+ [
+ 'width' => 512,
+ 'height' => 594,
+ 'originalWidth' => '100%',
+ 'originalHeight' => '100%',
+ 'title' => 'Tux',
+ 'translations' => [],
+ 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
+ ]
+ ],
+ [
+ "$base/Speech_bubbles.svg",
+ [
+ 'width' => 627,
+ 'height' => 461,
+ 'originalWidth' => '17.7cm',
+ 'originalHeight' => '13cm',
+ 'translations' => [
+ 'de' => SVGReader::LANG_FULL_MATCH,
+ 'fr' => SVGReader::LANG_FULL_MATCH,
+ 'nl' => SVGReader::LANG_FULL_MATCH,
+ 'tlh-ca' => SVGReader::LANG_FULL_MATCH,
+ 'tlh' => SVGReader::LANG_PREFIX_MATCH
+ ],
+ ]
+ ],
+ [
+ "$base/Soccer_ball_animated.svg",
+ [
+ 'width' => 150,
+ 'height' => 150,
+ 'originalWidth' => '150',
+ 'originalHeight' => '150',
+ 'animated' => true,
+ 'translations' => []
+ ],
+ ],
+ [
+ "$base/comma_separated_viewbox.svg",
+ [
+ 'width' => 512,
+ 'height' => 594,
+ 'originalWidth' => '100%',
+ 'originalHeight' => '100%',
+ 'translations' => []
+ ],
+ ],
+ ];
+ }
+
+ public static function provideSvgFilesWithXMLMetadata() {
+ $base = __DIR__ . '/../../data/media';
+ // phpcs:disable Generic.Files.LineLength
+ $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about="">
+ <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format>
+ <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ </ns4:Work>
+ </rdf:RDF>';
+ // phpcs:enable
+
+ $metadata = str_replace( "\r", '', $metadata ); // Windows compat
+ return [
+ [
+ "$base/US_states_by_total_state_tax_revenue.svg",
+ [
+ 'height' => 593,
+ 'metadata' => $metadata,
+ 'width' => 959,
+ 'originalWidth' => '958.69',
+ 'originalHeight' => '592.78998',
+ 'translations' => [],
+ ]
+ ],
+ ];
+ }
+
+ public static function provideSvgUnits() {
+ return [
+ [ '1' , 1 ],
+ [ '1.1' , 1.1 ],
+ [ '0.1' , 0.1 ],
+ [ '.1' , 0.1 ],
+ [ '1e2' , 100 ],
+ [ '1E2' , 100 ],
+ [ '+1' , 1 ],
+ [ '-1' , -1 ],
+ [ '-1.1' , -1.1 ],
+ [ '1e+2' , 100 ],
+ [ '1e-2' , 0.01 ],
+ [ '10px' , 10 ],
+ [ '10pt' , 10 * 1.25 ],
+ [ '10pc' , 10 * 15 ],
+ [ '10mm' , 10 * 3.543307 ],
+ [ '10cm' , 10 * 35.43307 ],
+ [ '10in' , 10 * 90 ],
+ [ '10em' , 10 * 16 ],
+ [ '10ex' , 10 * 12 ],
+ [ '10%' , 51.2 ],
+ [ '10 px' , 10 ],
+ // Invalid values
+ [ '1e1.1', 10 ],
+ [ '10bp', 10 ],
+ [ 'p10', null ],
+ ];
+ }
+}
public static function provideGetContent() {
yield 'Bad title' => [ null, '[x]' ];
- yield 'Dead redirect' => [ null, [
- 'text' => 'Dead redirect',
- 'title' => 'Dead_redirect',
- 'redirect' => 1,
- ] ];
- yield 'Bad content model' => [ null, [
- 'text' => 'MediaWiki:Wikitext',
- 'ns' => NS_MEDIAWIKI,
- 'title' => 'Wikitext',
- ] ];
+
yield 'No JS content found' => [ null, [
- 'text' => 'MediaWiki:Script.js',
+ 'text' => 'MediaWiki:Foo.js',
'ns' => NS_MEDIAWIKI,
- 'title' => 'Script.js',
+ 'title' => 'Foo.js',
] ];
- yield 'No CSS content found' => [ null, [
- 'text' => 'MediaWiki:Styles.css',
+
+ yield 'JS content' => [ 'code;', [
+ 'text' => 'MediaWiki:Foo.js',
'ns' => NS_MEDIAWIKI,
- 'title' => 'Script.css',
- ] ];
+ 'title' => 'Foo.js',
+ ], new JavaScriptContent( 'code;' ) ];
+
+ yield 'CSS content' => [ 'code {}', [
+ 'text' => 'MediaWiki:Foo.css',
+ 'ns' => NS_MEDIAWIKI,
+ 'title' => 'Foo.css',
+ ], new CssContent( 'code {}' ) ];
+
+ yield 'Wikitext content' => [ null, [
+ 'text' => 'MediaWiki:Foo',
+ 'ns' => NS_MEDIAWIKI,
+ 'title' => 'Foo',
+ ], new WikitextContent( 'code;' ) ];
}
/**
* @dataProvider provideGetContent
*/
- public function testGetContent( $expected, $title ) {
+ public function testGetContent( $expected, $title, Content $contentObj = null ) {
$context = $this->getResourceLoaderContext( [], new EmptyResourceLoader );
$module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
->setMethods( [ 'getContentObj' ] )->getMock();
$module->method( 'getContentObj' )
- ->willReturn( null );
+ ->willReturn( $contentObj );
if ( is_array( $title ) ) {
$title += [ 'ns' => NS_MAIN, 'id' => 1, 'len' => 1, 'redirect' => 0 ];
]
);
$this->assertTrue( User::isLocallyBlockedProxy( $ip ) );
-
- $this->hideDeprecated(
- 'IP addresses in the keys of $wgProxyList (found the following IP ' .
- 'addresses in keys: ' . $blockListEntry . ', please move them to values)'
- );
- $this->setMwGlobals(
- 'wgProxyList',
- [
- $blockListEntry => 'test'
- ]
- );
- $this->assertTrue( User::isLocallyBlockedProxy( $ip ) );
}
/**
<?php
+use MediaWiki\MediaWikiServices;
use Wikimedia\ScopedCallback;
/**
public function setUp() {
wfDebug( __METHOD__ );
- $db = wfGetDB( DB_MASTER );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ $db = $lb->getConnection( DB_MASTER );
$type = $db->getType();
$prefix = $type === 'oracle' ?
MediaWikiTestCase::ORA_DB_PREFIX : MediaWikiTestCase::DB_PREFIX;