1.23, is now hard-deprecated.
* $wgProfileOnly — Setting this, deprecated in 1.23, is now hard-deprecated.
Instead, set the log file in $wgDebugLogGroups['profileoutput'].
+* $wgProxyList — Setting this to an array with IP addresses in the array keys,
+ which was deprecated in 1.30, no longer works. 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.
* …
==== Removed configuration ====
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.
+* SVGMetadataExtractor::getMetadata has been deprecated. Instead, you should
+ use SVGReader->getMetadata() directly.
+* The following public properties on AbstractBlock are deprecated: $mReason,
+ $mTimestamp, $mExpiry, $mHideName. Use the getters/setters instead.
+* The following public properties on DatabaseBlock are deprecated: $mAuto,
+ $mParentBlockId. To check for an autoblock use DatabaseBlock::getType; to
+ check for the parent ID, use DatabaseBlock::getParentBlockId.
=== Other changes in 1.34 ===
* …
'BadTitleError' => __DIR__ . '/includes/exception/BadTitleError.php',
'BagOStuff' => __DIR__ . '/includes/libs/objectcache/BagOStuff.php',
'BaseDump' => __DIR__ . '/includes/export/BaseDump.php',
+ 'BaseSearchResultSet' => __DIR__ . '/includes/search/BaseSearchResultSet.php',
'BaseTemplate' => __DIR__ . '/includes/skins/BaseTemplate.php',
'BashkirUppercaseCollation' => __DIR__ . '/includes/collation/BashkirUppercaseCollation.php',
'BatchRowIterator' => __DIR__ . '/includes/utils/BatchRowIterator.php',
'SearchPostgres' => __DIR__ . '/includes/search/SearchPostgres.php',
'SearchResult' => __DIR__ . '/includes/search/SearchResult.php',
'SearchResultSet' => __DIR__ . '/includes/search/SearchResultSet.php',
+ 'SearchResultSetTrait' => __DIR__ . '/includes/search/SearchResultSetTrait.php',
'SearchSqlite' => __DIR__ . '/includes/search/SearchSqlite.php',
'SearchSuggestion' => __DIR__ . '/includes/search/SearchSuggestion.php',
'SearchSuggestionSet' => __DIR__ . '/includes/search/SearchSuggestionSet.php',
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'] );
"An13sa",
"Gorkaazk",
"Mikel Ibaiba",
- "Iñaki LL"
+ "Iñaki LL",
+ "Xabier Armendaritz"
]
},
"apihelp-main-param-action": "Zein ekintza burutuko da.",
"apihelp-compare-param-torev": "Aldaratzeko bigarren berrikusketa.",
"apihelp-compare-param-prop": "Hartu beharreko informazio zatiak.",
"apihelp-compare-paramvalue-prop-diff": "HTML diff-a",
- "apihelp-compare-paramvalue-prop-diffsize": "HTML diff-aren tamainia, byte-tan",
+ "apihelp-compare-paramvalue-prop-diffsize": "Aldeen HTMLaren tamaina, bytetan",
"apihelp-compare-paramvalue-prop-size": "\"nondik\" eta \"nora\" berrikuspenen tamaina.",
"apihelp-compare-example-1": "1. eta 2. berrikusketen arteko \"diff\"-a sortu.",
"apihelp-createaccount-summary": "Erabiltzaile kontu berria sortu.",
"apihelp-clearhasmsg-example-1": "清除目前使用者的 <code>hasmsg</code> 標記。",
"apihelp-clientlogin-summary": "使用互動流程來登入 wiki。",
"apihelp-clientlogin-example-login": "開始以使用者 <kbd>Example</kbd> 與密碼 <kbd>ExamplePassword</kbd> 來登入至 wiki 的過程。",
- "apihelp-clientlogin-example-login2": "在 <samp>UI</samp> 回應雙重認證後繼續登入,提供 <kbd>987654</kbd> 的 <var>OATHToken</var>。",
+ "apihelp-clientlogin-example-login2": "在 <samp>UI</samp> 回應雙因素驗證後繼續登入,提供 <kbd>987654</kbd> 的 <var>OATHToken</var>。",
"apihelp-compare-summary": "比較 2 個頁面間的差異。",
"apihelp-compare-extended-description": "\"from\" 以及 \"to\" 的修訂編號,頁面標題或頁面 ID 為必填。",
"apihelp-compare-param-fromtitle": "要比對的第一個標題。",
* @since 1.34 Factored out from DatabaseBlock (previously Block).
*/
abstract class AbstractBlock {
- /** @var string */
+ /**
+ * @deprecated since 1.34. Use getReason and setReason instead.
+ * @var string
+ */
public $mReason;
- /** @var string */
+ /**
+ * @deprecated since 1.34. Use getTimestamp and setTimestamp instead.
+ * @var string
+ */
public $mTimestamp;
- /** @var string */
+ /**
+ * @deprecated since 1.34. Use getExpiry and setExpiry instead.
+ * @var string
+ */
public $mExpiry = '';
/** @var bool */
/** @var bool */
protected $blockCreateAccount = false;
- /** @var bool */
+ /**
+ * @deprecated since 1.34. Use getHideName and setHideName instead.
+ * @var bool
+ */
public $mHideName = false;
/** @var User|string */
$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 );
}
* @since 1.34 Renamed from Block.
*/
class DatabaseBlock extends AbstractBlock {
- /** @var bool */
+ /**
+ * @deprecated since 1.34. Use getType to check whether a block is autoblocking.
+ * @var bool
+ */
public $mAuto;
- /** @var int */
+ /**
+ * @deprecated since 1.34. Use getParentBlockId instead.
+ * @var int
+ */
public $mParentBlockId;
/** @var int */
/** @var Language */
protected $contLang;
+ /**
+ * Track which languages have been loaded by load().
+ * @var array
+ */
+ private $loadedLanguages = [];
+
/**
* Singleton instance
*
}
# Don't do double loading...
- if ( $this->cache->has( $code ) && $mode != self::FOR_UPDATE ) {
+ if ( isset( $this->loadedLanguages[$code] ) && $mode != self::FOR_UPDATE ) {
return true;
}
$this->overridable = array_flip( Language::getMessageKeysFor( $code ) );
- // T208897 array_flip can fail and return null
- if ( is_null( $this->overridable ) ) {
- LoggerFactory::getInstance( 'MessageCache' )->error(
- __METHOD__ . ': $this->overridable is null',
- [
- 'message_keys' => Language::getMessageKeysFor( $code ),
- 'code' => $code
- ]
- );
- }
-
# 8 lines of code just to say (once) that message cache is disabled
if ( $this->mDisable ) {
static $shownDisabled = false;
wfDebugLog( 'MessageCacheError', __METHOD__ . ": Failed to load $code\n" );
# This used to throw an exception, but that led to nasty side effects like
# the whole wiki being instantly down if the memcached server died
+ } else {
+ # All good, just record the success
+ $this->loadedLanguages[$code] = true;
}
if ( !$this->cache->has( $code ) ) { // sanity
$this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
}
$this->cache->clear();
+ $this->loadedLanguages = [];
}
/**
* 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,
Hooks::run( 'RequestContextCreateSkin', [ $this, &$skin ] );
$factory = MediaWikiServices::getInstance()->getSkinFactory();
- // If the hook worked try to set a skin from it
if ( $skin instanceof Skin ) {
+ // The hook provided a skin object
$this->skin = $skin;
} elseif ( is_string( $skin ) ) {
+ // The hook provided a skin name
// Normalize the key, just in case the hook did something weird.
$normalized = Skin::normalizeKey( $skin );
$this->skin = $factory->makeSkin( $normalized );
- }
-
- // If this is still null (the hook didn't run or didn't work)
- // then go through the normal processing to load a skin
- if ( $this->skin === null ) {
+ } else {
+ // No hook override, go through normal processing
if ( !in_array( 'skin', $this->getConfig()->get( 'HiddenPrefs' ) ) ) {
- # get the user skin
$userSkin = $this->getUser()->getOption( 'skin' );
$userSkin = $this->getRequest()->getVal( 'useskin', $userSkin );
} else {
- # if we're not allowing users to override, then use the default
$userSkin = $this->getConfig()->get( 'DefaultSkin' );
}
- // Normalize the key in case the user is passing gibberish
- // or has old preferences (T71566).
+ // Normalize the key in case the user is passing gibberish query params
+ // or has old user preferences (T71566).
+ // Skin::normalizeKey will also validate it, so makeSkin() won't throw.
$normalized = Skin::normalizeKey( $userSkin );
-
- // Skin::normalizeKey will also validate it, so
- // this won't throw an exception
$this->skin = $factory->makeSkin( $normalized );
}
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' ),
/**
* Renders a diff for a single slot (that is, a diff between two content objects).
*
- * Callers should obtain this class by invoking ContentHandler::getSlotDiffRendererClass
+ * Callers should obtain instances of this class by invoking ContentHandler::getSlotDiffRenderer
* on the content handler of the new content object (ie. the one shown on the right side
* of the diff), or of the old one if the new one does not exist.
*
* The default implementation just does a text diff on the native text representation.
* Content handler extensions can subclass this to provide a more appropriate diff method by
- * overriding ContentHandler::getSlotDiffRendererClass. Other extensions that want to interfere
+ * overriding ContentHandler::getSlotDiffRendererInternal. Other extensions that want to interfere
* with diff generation in some way can use the GetSlotDiffRenderer hook.
*
* @ingroup DifferenceEngine
* @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>";
*/
public function getSlave( $cluster ) {
$lb = $this->getLoadBalancer( $cluster );
- $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
-
- $db = $lb->getConnectionRef( DB_REPLICA, [], $domainId );
- $db->clearFlag( DBO_TRX ); // sanity
- return $db;
+ return $lb->getConnectionRef(
+ DB_REPLICA,
+ [],
+ $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) ),
+ $lb::CONN_TRX_AUTOCOMMIT
+ );
}
/**
*/
public function getMaster( $cluster ) {
$lb = $this->getLoadBalancer( $cluster );
- $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
-
- $db = $lb->getMaintenanceConnectionRef( DB_MASTER, [], $domainId );
- $db->clearFlag( DBO_TRX ); // sanity
- return $db;
+ return $lb->getMaintenanceConnectionRef(
+ DB_MASTER,
+ [],
+ $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) ),
+ $lb::CONN_TRX_AUTOCOMMIT
+ );
}
/**
/**
* Object handling generic submission, CSRF protection, layout and
- * other logic for UI forms. in a reusable manner.
+ * other logic for UI forms in a reusable manner.
*
* In order to generate the form, the HTMLForm object takes an array
* structure detailing the form fields available. Each element of the
'textinput' => $textAttribs,
'dropdowninput' => $dropdownInputAttribs,
'or' => false,
+ 'required' => $this->mParams[ 'required' ] ?? false,
'classes' => [ 'mw-htmlform-select-and-other-field' ],
'data' => [
'maxlengthUnit' => $this->mParams['maxlength-unit'] ?? 'bytes'
'disabled' => $disabled,
'textinput' => $textAttribs,
'dropdowninput' => $dropdownAttribs,
+ 'required' => $this->mParams[ 'required' ] ?? false,
'or' => true,
] );
}
* @ingroup SpecialPage
*/
class UploadSourceAdapter {
- /** @var array */
+ /** @var ImportSource[] */
public static $sourceRegistrations = [];
/** @var ImportSource */
/**
* @param string $path
* @param string $mode
- * @param array $options
+ * @param int $options
* @param string &$opened_path
* @return bool
*/
/**
* @param string $data
- * @return bool
+ * @return false
*/
function stream_write( $data ) {
return false;
}
/**
- * @return mixed
+ * @return int
*/
function stream_tell() {
return $this->mPosition;
}
/**
- * @return array
+ * @return int[]
*/
function url_stat() {
$result = [];
/**
* 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;
"config-restart": "Sí, torna a començar",
"config-welcome": "=== Comprovacions de l'entorn ===\nS'efectuaran comprovacions bàsiques per veure si l'entorn és adequat per a la instal·lació del MediaWiki.\nRecordeu d'incloure aquesta informació si heu de demanar ajuda sobre com completar la instal·lació.",
"config-welcome-section-copyright": "=== Drets d'autor i condicions ===\n\n$1\n\nAquest programa és de programari lliure; podeu redistribuir-lo i/o modificar-lo sota les condicions de la Llicència Pública General GNU com es publicada per la Free Software Foundation; qualsevol versió 2 de la llicència, o (opcionalment) qualsevol versió posterior.\n\nAquest programa és distribueix amb l'esperança que serà útil, però <strong>sense cap garantia</strong>; sense ni tan sols la garantia implícita de <strong>\ncomerciabilitat</strong> o <strong>idoneïtat per a un propòsit particular</strong>.\nConsulteu la Llicència Pública General GNU, per a més detalls.\n\nHauríeu d'haver rebut [$2 una còpia de la Llicència Pública General GNU] amb aquest programa; si no, escriviu a la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA o [https://www.gnu.org/copyleft/gpl.html per llegir-lo en línia].",
- "config-sidebar": "* [https://www.mediawiki.org la Pàgina d'inici]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guia de l'usuari]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guia de l'administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ PMF]\n----\n* <doclink href=Readme>Llegeix-me</doclink>\n* <doclink href=ReleaseNotes>Notes de la versió</doclink>\n* <doclink href=Còpia>Còpia</doclink>\n* <doclink href=UpgradeDoc>Actualització</doclink>",
+ "config-sidebar": "* [https://www.mediawiki.org la Pàgina d'inici]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guia de l'usuari]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guia de l'administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ PMF]",
"config-sidebar-readme": "Llegeix-me",
"config-sidebar-relnotes": "Notes de la versió",
"config-sidebar-license": "Còpia",
"config-uploads-not-safe": "<strong>Avís:</strong> El directori de càrregues per defecte <code>$1</code> és vulnerable a l'execució d'scripts arbitraris.\nEncara que el MediaWiki comprova tots els fitxers que es carreguen davant d'amenaces de seguretat, és molt recomanable [https://www.mediawiki.org/ wiki/Special:MyLanguage/Manual:Security#Upload_security tancar aquesta vulnerabilitat de seguretat] abans d'habilitar les càrregues.",
"config-no-cli-uploads-check": "<strong>Avís</strong>: no s'ha comprovat el directori per defecte per a càrregues (<code><span class=\"notranslate\">$1</span></code>$) per vulnerabilitats en l'execució arbitrària durant la instal·lació amb la línia d'ordres.",
"config-brokenlibxml": "El vostre sistema té una combinació de versions de PHP i libxml2 que són problemàtiques i que poden causar corrupció de dades no aparent a MediaWiki i a altres aplicacions web.\nActualitzeu-vos a libxml2 2.7.3 o superior ([https://bugs.php.net/bug.php?id=45996 informe d'error al projecte PHP]).\nS'ha interromput la instal·lació.",
+ "config-using-32bit": "<strong>Avís</strong>: el vostre sistema sembla que s'executa amb enters de 32 bits. Això [https://www.mediawiki.org/wiki/special:MyLanguage/Manual:32-bit no és aconsellable].",
"config-db-type": "Tipus de base de dades:",
"config-db-host": "Servidor de la base de dades:",
"config-db-host-help": "Si el servidor de base de dades és en un servidor diferent, introduïu el nom del servidor o l'adreça IP a continuació.\n\nSi feu servir un hostatge web compartit, el vostre proveïdor us hauria de proporcionar el nom del servidor a la documentació.\n\nSi feu servir MySQL, «localhost» podria no funcionar com a nom de servidor. Si no funciona, proveu «127.0.0.1» com a adreça IP local.\n\nSi feu servir PostgreSQL, deixeu aquest camp en blanc per a connectar-vos a través d'un sòcol Unix.",
"config-postgres-old": "Cal el PostgreSQL $1 o posterior. Teniu el $2.",
"config-mssql-old": "Cal utilitzar el Microsoft SQL Server $1 o posterior. Teniu la versió $2.",
"config-sqlite-name-help": "Trieu un nom per identificar el wiki.\nNo feu servir espais ni guionets.\nAquest nom s’utilitzarà per a denominar el fitxer de les dades de l’SQLite.",
+ "config-sqlite-parent-unwritable-group": "No es pot crear el directori de dades <code><nowiki>$1</nowiki></code>, perquè el directori pare <code><nowiki>$2</nowiki></code> no el pot escriure el servidor web.\n\nL'instal·lador no pot determinar l'usuari amb què s'executa el servidor web.\nFeu el directori <code><nowiki>$3</nowiki></code> escrivible globalment per l'usuari del servidor web (i altres) per continuar.\nEn un sistema Unix/Linux feu:\n\n<pre>cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3</pre>",
+ "config-sqlite-parent-unwritable-nogroup": "No es pot crear el directori de dades <code><nowiki>$1</nowiki></code>, perquè el directori pare <code><nowiki>$2</nowiki></code> no el pot escriure el servidor web.\n\nL'instal·lador no pot determinar l'usuari amb què s'executa el servidor web.\nFeu el directori <code><nowiki>$3</nowiki></code> escrivible globalment per l'usuari del servidor web (i altres) per continuar.\nEn un sistema Unix/Linux feu:\n\n<pre>cd $2\nmkdir $3\nchmod a+w $3</pre>",
"config-sqlite-mkdir-error": "S'ha produït un error en crear el directori de dades «$1».\nComproveu la ubicació i torneu-ho a provar.",
"config-sqlite-dir-unwritable": "No s'ha pogut escriure al directori «$1».\nCanvieu els permisos perquè el servidor web pugui escriure-hi i torneu-ho a provar.",
"config-sqlite-connection-error": "$1. \n\nComproveu el directori de dades i el nom de la base de dades a continuació i torneu-ho a provar.",
"config-admin-error-password": "S'ha produït un error intern en definir una contrasenya per a l'administrador «<nowiki>$1</nowiki>»: <pre>$2</pre>",
"config-admin-error-bademail": "Heu introduït una adreça electrònica no vàlida.",
"config-subscribe": "Subscriu a la [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce llista de correu d'anunci de noves versions].",
+ "config-subscribe-help": "És una llista de poc volum que s'utilitza per fer anuncis de noves versions, incloent-hi aquells de seguretat importants.\nCal que us hi subscriviu i actualitzeu la instal·lació de MediaWiki quan apareguin noves versions.",
"config-subscribe-noemail": "Us heu provat de subscriure a la llista de correu d'anuncis de noves versions sense proporcionar-hi una adreça electrònica.\nProporcioneu-ne una si voleu subscriure-us a la llista de correu electrònic.",
"config-pingback": "Comparteix dades d'aquesta instal·lació amb els desenvolupadors de MediaWiki.",
+ "config-pingback-help": "Si seleccioneu aquesta opció, el MediaWiki farà ping periòdicament a https://www.mediawiki.org amb dades bàsiques d'aquesta instància. Les dades inclouen, per exemple, el tipus de sistema, la versió PHP i el sistema de bases de dades que s'utilitza. La Fundació Wikimedia comparteix les dades amb els desenvolupadors de MediaWiki per tal d'ajudar-los a guiar empreses de desenvolupament futures. Les dades següents s'enviaran del vostre sistema:\n<pre>$1</pre>",
"config-almost-done": "Gairebé ja heu acabat!\nPodeu ometre el que queda de la configuració i procedir amb la instal·lació del wiki.",
"config-optional-continue": "Fes-me més preguntes.",
"config-optional-skip": "Ja estic avorrit. Simplement instal·leu el wiki.",
"Macofe",
"Rachmat.Wahidi",
"Gombang",
- "Rachmat04"
+ "Rachmat04",
+ "ArlandGa"
]
},
"config-desc": "Penginstal untuk MediaWiki",
"config-help-restart": "Apakah Anda ingin menghapus semua data tersimpan yang telah Anda masukkan dan mengulang proses instalasi?",
"config-restart": "Ya, nyalakan ulang",
"config-welcome": "=== Pengecekan lingkungan ===\nPengecekan dasar kini akan dilakukan untuk melihat apakah lingkungan ini memadai untuk instalasi MediaWiki.\nIngatlah untuk menyertakan informasi ini jika Anda mencari bantuan tentang cara menyelesaikan instalasi.",
- "config-welcome-section-copyright": "=== Hak cipta dan persyaratan ===\n\n$1\n\nProgram ini adalah perangkat lunak bebas; Anda dapat mendistribusikan dan/atau memodifikasi di bawah persyaratan GNU General Public License seperti yang diterbitkan oleh Free Software Foundation; baik versi 2 lisensi, atau (sesuai pilihan Anda) versi yang lebih baru.\n\nProgram ini didistribusikan dengan harapan bahwa itu akan berguna, tetapi <strong>tanpa jaminan apa pun</strong>; bahkan tanpa jaminan tersirat untuk <strong>dapat diperjualbelikan</strong> atau <strong>sesuai untuk tujuan tertentu</strong>.\nLihat GNU General Public License untuk lebih jelasnya.\n\nAnda seharusnya telah menerima [$2 salinan dari GNU General Public License] bersama dengan program ini; jika tidak, kirimkan surat untuk Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, atau [https://www.gnu.org/copyleft/gpl.html baca versi daring].",
+ "config-welcome-section-copyright": "=== Hak cipta dan persyaratan ===\n\n$1\n\nProgram ini adalah perangkat lunak bebas; Anda dapat mendistribusikan dan/atau memodifikasinya di bawah persyaratan GNU General Public License seperti yang diterbitkan oleh Free Software Foundation; baik versi 2 lisensi, atau (sesuai pilihan Anda) versi yang lebih baru.\n\nProgram ini didistribusikan dengan harapan bahwa itu akan berguna, tetapi <strong>tanpa jaminan apa pun</strong>; bahkan tanpa jaminan tersirat untuk <strong>dapat diperjualbelikan</strong> atau <strong>sesuai untuk tujuan tertentu</strong>.\nLihat GNU General Public License untuk lebih jelasnya.\n\nAnda seharusnya telah menerima [$2 salinan dari GNU General Public License] bersama dengan program ini; jika tidak, kirimkan surat untuk Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, atau [https://www.gnu.org/copyleft/gpl.html baca versi daring].",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/id Situs MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/id Pedoman Pengguna]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/id Pedoman Administrator]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/id FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Release notes</doclink>\n* <doclink href=Copying>Copying</doclink>\n* <doclink href=UpgradeDoc>Upgrading</doclink>",
"config-env-good": "Kondisi telah diperiksa.\nAnda dapat menginstal MediaWiki.",
"config-env-bad": "Kondisi telah diperiksa.\nAnda tidak dapat menginstal MediaWiki.",
"config-unicode-pure-php-warning": "'''Upozorenje''': Dodatak [https://pecl.php.net/intl intl PECL] nije dostupan za normalizaciju Unicode, vraćajući se na sporu primjenu čistog PHP-a.\n\nAko imate web-lokaciju s visokim prometom, morat ćete pročitati više o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalizacije].",
"config-unicode-update-warning": "'''Upozorenje:''' Uspostavljena verzija omotnice Unicode normalizacije koristi stariju verziju biblioteke [http://site.icu-project.org/ projekta ICU].\nDa biste koristili Unicode, trebate napraviti [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations nadogradnju].",
"config-no-db": "Nisam mogao pronaći odgovarajući upravljački program za bazu podataka! Morat ćete uspostaviti upravljački program za PHP-bazu.\n{{PLURAL:$2|Podržana je sljedeća vrsta|Podržane su sljedeće vrste}} baze: $1.\n\nAko ste sami kompilirali PHP, omogućite bazni klijent u postavkama - npr. s <code>./configure --with-mysqli</code>.\nAko ovaj PHP uspostavite iz Debian ili Ubuntu paketa, tada ćete ga morati uspostaviti, npr., paket <code>php-mysql</code>.",
- "config-outdated-sqlite": "'''Upozorenje''': imate SQLite $1. Najstarija dopuštena verzija je $2. Stoga će SQLite biti nedostupan.",
+ "config-outdated-sqlite": "<strong>Upozorenje</strong>: imate SQLite $2. Najstarija dopuštena verzija je $1. Stoga, SQLite će biti nedostupan.",
"config-no-fts3": "'''Upozorenje''': SQLite se kompilira bez modula [//sqlite.org/fts3.html FTS3] - za tu bazu podataka neće biti mogućnosti pretrage.",
"config-pcre-old": "'''Kobno:''' Potreban je PCRE $1 ili novija verzija.\nVaš PHP-binarni je svezan s PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Više informacija].",
"config-pcre-no-utf8": "<strong>Kobno</strong>: PCRE modul PHP-a je hitan bez podrške za PCRE_UTF8.\nMediaWiki zahtijeva podršku za UTF-8 kako bi ispravno funkcionirao.",
"config-mssql-windowsauth": "Potvrda identiteta za Windows",
"config-site-name": "Ime wikija:",
"config-site-name-help": "Ovo će se pojaviti u naslovnoj traci pregledača i na raznim drugim mestima.",
+ "config-project-namespace": "Projektni imenski prostor:",
+ "config-ns-generic": "Projekat",
+ "config-ns-site-name": "Isto ime kao wikija: $1",
+ "config-ns-other": "Drugo (navedite)",
+ "config-ns-other-default": "MyWiki",
+ "config-admin-box": "Administratoski račun",
+ "config-admin-name": "Vaše korisničko ime:",
"config-admin-password": "Lozinka:",
+ "config-admin-password-confirm": "Ponovite lozinku:",
+ "config-admin-name-blank": "Upišite administratorsko korisničko ime.",
+ "config-admin-password-blank": "Upišite lozinku za administratorski račun",
+ "config-admin-password-mismatch": "Lozinke što ste upisali se ne poklapaju.",
+ "config-admin-email": "E-mail adresa:",
+ "config-profile": "Profil korisničkih prava:",
+ "config-profile-wiki": "Otvoren wiki",
+ "config-profile-no-anon": "Neophodno otvaranje računa",
+ "config-profile-fishbowl": "Samo ovlašteni urednici",
+ "config-profile-private": "Privatan wiki",
+ "config-license": "Autorska prava i licenca:",
+ "config-license-none": "Bez podnožja za licencu",
+ "config-email-settings": "Podešavanja e-pošte",
+ "config-enable-email": "Omogući odlaznu e-poštu",
+ "config-email-user": "Omogući slanje e-poruka među korisnicima",
+ "config-email-user-help": "Dozvoli svim korisnicima da međusobno šalju e-poruke ako imaju omogućeno u podešavanjima.",
+ "config-email-usertalk": "Omogući obaveštenja o promjenama u korisničkim stranicama za razgovor",
+ "config-email-usertalk-help": "Omogući korisnicima da primaju obaveštenja o promenama u njihovim korisničkim razgovornim stranicama ako su ih omogućili u podešavanjima.",
+ "config-email-watchlist": "Omogući obaveštenja o spisku praćenja",
+ "config-email-watchlist-help": "Omogući korisnicima da primaju obaveštenja o svojim nadgledanim stranicama ako su ih omogućili u podešavanjima.",
+ "config-upload-settings": "Otpremanja slika i datoteka",
+ "config-upload-enable": "Omogući postavljanje datoteka",
+ "config-upload-deleted": "Folder za obrisane datoteke:",
+ "config-upload-deleted-help": "Odaberite u kojem folderu će se arhivirati izbrisane datoteke.\nNajbolje bi bilo ako taj nije dostupan putem svemrežja.",
+ "config-logo": "URL za logotipa:",
+ "config-instantcommons": "Omogući Instant Commons",
+ "config-cc-again": "Odaberite ponovo...",
+ "config-cc-not-chosen": "Odaberite željenu licencu Creative Commons i kliknite na „proceed”.",
+ "config-advanced-settings": "Napredna podešavanja",
+ "config-cache-options": "Podešavanja za međuspremanje objekta:",
+ "config-cache-none": "Nema međuspremanja (ne uklanja se nijedna funkcija, ali može uticati na brzinu na veće wiki lokacije)",
+ "config-cache-accel": "Međuspremanje PHP-objekta (APC, APCu ili WinCache)",
+ "config-cache-memcached": "Koristi Memcached (zahtijeva dodatno postavljivanje i podešavanje)",
+ "config-memcached-servers": "Memcached-serveri:",
+ "config-memcached-help": "Lista IP adresa za uporabu u Memcached.\nTreba da se navede jednu u svaki red, kao i port što će se koristiti. Na primer:\n 127.0.0.1:11211\n 192.168.1.25:1234",
+ "config-memcache-needservers": "Odabrali ste Memcached kao vaš tip međuspremnika (keša), ali niste naveli nijedan server.",
"mainpagetext": "<strong>MediaWiki je uspješno instaliran.</strong>",
"mainpagedocfooter": "Za informacije o korištenju wiki softvera konzultirajte [https://meta.wikimedia.org/wiki/Help:Contents Vodič za korisnike].\n\n== Uvod u rad ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista konfiguracije postavki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista primatelja izdanja MediaWikija]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Lokalizirajte MediaWiki za svoj jezik]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Saznajte kako se boriti protiv spama na svojem wikiju]"
}
"Ерней"
]
},
- "config-desc": "MediaWiki йөкләүче",
- "config-title": "MediaWiki $1 куелышы",
+ "config-desc": "MediaWiki урнаштыручы",
+ "config-title": "MediaWiki $1 урнаштыруы",
"config-information": "Мәгълүмат",
+ "config-localsettings-upgrade": "<code>LocalSettings.php</code> файлы табылды.\nБу урнаштыру яңартырга өчен, түбәндәге кырда <code>$wgUpgradeKey</code> кыйммәтен кертегез әле.\nСез аны <code>LocalSettings.php</code> файлында табасыз.",
+ "config-localsettings-cli-upgrade": "<code>LocalSettings.php</code> файлы табылды.\nБу урнаштыру яңартыга өчен, <code>update.php</code> җибәрегез әле",
"config-localsettings-key": "Яңарту ачкычы:",
+ "config-localsettings-badkey": "Сез ялгыш яңарту ачкычы бирдегез.",
"config-your-language": "Телегез:",
"config-wiki-language": "Вики теле:",
- "config-back": "â\86\90 Ð\90Ñ\80Ñ\82ка",
- "config-continue": "Ð\9aилÓ\99Ñ\81е →",
+ "config-back": "â\86\90 Ð\9aиÑ\80егÓ\99",
+ "config-continue": "Ð\94Ó\99вам иÑ\82Ò¯ →",
"config-page-language": "Тел",
"config-page-welcome": "MediaWiki проектына рәхим итегез!",
+ "config-page-dbconnect": "Мәгълүматлар базасына тоташтыру",
+ "config-page-upgrade": "Булган урнаштыруны яңарту",
+ "config-page-dbsettings": "Мәгълүматлар базасы көйләнмәләре",
"config-page-name": "Исем",
"config-page-options": "Көйләнмәләр",
"config-page-install": "Урнаштыру",
"config-page-copying": "Лицензия",
"config-page-upgradedoc": "Яңарту",
"config-page-existingwiki": "Хәзерге вики",
- "config-restart": "Әйе, яңадан башларга",
+ "config-restart": "Әйе, яңадан башлау",
+ "config-sidebar-readme": "Укып чык",
+ "config-sidebar-relnotes": "Чыгарыш турында мәгълүмат",
+ "config-sidebar-license": "Күчермә алу",
+ "config-sidebar-upgrade": "Яңарту",
"config-env-php": "PHP $1 куелды.",
"config-env-hhvm": "HHVM $1 куелды.",
"config-apc": "[https://www.php.net/apc APC] куелды",
+ "config-apcu": "[https://www.php.net/apcu APCu] куелды",
"config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] куелды",
"config-diff3-bad": "GNU diff3 табылмады.",
"config-git": "Git юрамалар идарә итү системасы табылды: <code>$1</code>.",
"config-using-server": "«<nowiki>$1</nowiki>» сервер исеме файдаланыла.",
"config-using-uri": "«<nowiki>$1$2</nowiki>» URL исемле сервер файдаланыла.",
- "config-db-type": "Мәгълүмат базасы төре:",
- "config-db-host": "Мәгълүмат базасы хосты:",
+ "config-db-type": "Мәгълүматлар базасы төре:",
+ "config-db-host": "Мәгълүматлар базасы хосты:",
"config-db-host-oracle": "TNS мәгълүмат базасы:",
- "config-db-wiki-settings": "Бу вики идентификациясе",
+ "config-db-wiki-settings": "Бу вики тәңгәлләштерү",
"config-db-name": "Мәгълүматлар базасы исеме (сызыкчасыз):",
- "config-db-name-oracle": "Мәгълүмат базасы төзелеше:",
+ "config-db-name-oracle": "Мәгълүматлар базасы төзелеше:",
"config-db-username": "Мәгълүмат базасын кулланучы исеме:",
"config-db-password": "Мәгълүмат базасының серсүзе:",
- "config-db-port": "Мәгълүмат базасы порты:",
+ "config-db-port": "Мәгълүматлар базасы порты:",
"config-db-schema": "MediaWiki өчен (сызыкчасыз) төзелеш:",
- "config-type-mysql": "MariaDB, MySQL Ñ\8fиÑ\81Ó\99 ярашлы",
+ "config-type-mysql": "MariaDB, MySQL Ñ\8fки ярашлы",
"config-type-mssql": "Microsoft SQL Server",
"config-header-mysql": "MariaDB/MySQL көйләнмәләре",
"config-header-postgres": "PostgreSQL көйләнмәләре",
"config-header-sqlite": "SQLite көйләнмәләре",
"config-header-oracle": "Oracle көйләнмәләре",
"config-header-mssql": "Microsoft SQL Server көйләнмәләре",
- "config-invalid-db-type": "Ялган биÑ\80елмÓ\99лÓ\99Ñ\80 базаÑ\81Ñ\8b Ñ\82Ó©Ñ\80е",
+ "config-invalid-db-type": "Ð\9cÓ\99гÑ\8aлүмаÑ\82лаÑ\80 базаÑ\81Ñ\8b Ñ\8fлгÑ\8bÑ\88 Ñ\82Ó©Ñ\80е.",
"config-upgrade-done-no-regenerate": "Яңартү тәмамланды.\n\nХәзер сез [$1 вики] белән эшли аласыз.",
"config-regenerate": "LocalSettings.php яңадан төзү →",
"config-show-table-status": "«<code>SHOW TABLE STATUS</code>» таләбе эшләнмәде!",
"config-mysql-engine": "Саклау системасы:",
- "config-mysql-innodb": "InnoDB (тәкъдим ителә)",
+ "config-mysql-innodb": "InnoDB (киңәш ителә)",
"config-mssql-auth": "Аутентификация төре:",
"config-mssql-sqlauth": "SQL Server чынлыгын раслау",
"config-mssql-windowsauth": "Windows чынлыгын раслау",
"config-admin-password": "Серсүз:",
"config-admin-password-confirm": "Серсүзне кабатлагыз:",
"config-admin-email": "Электрон почта адресы:",
+ "config-profile": "Кулланучы хокуклары профиле:",
"config-profile-wiki": "Ачык вики",
"config-profile-private": "Ябык вики",
- "config-license": "Автор хокуклары һәм лицензияләр:",
+ "config-license": "Авторлык хокукы һәм рөхсәтнамә:",
"config-license-cc-by-sa": "Creative Commons Attribution Share Alike",
"config-license-cc-by": "Creative Commons Attribution",
"config-license-cc-by-nc-sa": "Creative Commons Attribution Non-Commercial Share Alike",
"config-license-cc-0": "Creative Commons Zero (җәмгыять мирасы)",
"config-license-gfdl": "GNU Free Documentation License 1.3 яки яңарагы",
"config-license-pd": "Җәмгыять мирасы",
+ "config-email-settings": "E-mail көйләнмәләре",
"config-logo": "Логотип URL:",
- "config-cc-again": "Кабат сайлагыз...",
+ "config-cc-again": "Кабат сайлагыз…",
"config-advanced-settings": "Өстәмә көйләнмәләр",
"config-memcached-servers": "Memcached серверлары:",
"config-extensions": "Киңәйтүләр",
"config-skins": "Бизәлеш",
- "config-install-step-done": "әзер",
+ "config-install-step-done": "тәмам",
"config-install-step-failed": "булмады",
+ "config-install-database": "Мәгълүматлар базасы көйләве",
"config-install-schema": "Схема төзү",
- "config-install-tables": "Табын төзү",
+ "config-install-user-alreadyexists": "Кулланучы «$1» бар инде",
+ "config-install-user-create-failed": "«$1» кулланучыны китереп булмады: $2",
+ "config-install-tables": "Җәдвәлләр төзү",
"config-install-stats": "Инициализация статистикасы",
"config-download-localsettings": "<code>LocalSettings.php</code> йөкләү",
- "config-help": "ярдәм",
+ "config-help": "белешмә",
"config-help-tooltip": "ачу өчен басыгыз",
"config-skins-screenshots": "$1 (скриншотлар: $2)",
"config-extensions-requires": "$1 ($2 кирәк)",
"Wehwei",
"Wwycheuk",
"蘭斯特",
- "Kly"
+ "Kly",
+ "Winston Sung"
]
},
"config-desc": "MediaWiki 安裝程式",
"config-memcache-badport": "Memcached 埠號應介於 $1 到 $2 之間。",
"config-extensions": "擴充套件",
"config-extensions-help": "已在您的 <code>./extensions</code> 目錄中發現下列擴充套件。\n\n這些擴充套件可能需要做額外的設定,但您可以現在先開啟功能。",
- "config-skins": "外觀",
+ "config-skins": "佈景主題",
"config-skins-help": "系統偵測到您於 <code>./skins</code> 資料夾中含有外觀如上清單。 您必須開啟其中一項並設為預設值。",
"config-skins-use-as-default": "使用此外觀作為預設",
"config-skins-missing": "沒有發現任何外觀;MediaWiki 在您安裝一些恰當的外觀前將會使用備用外觀。",
// 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 );
}
* being no stale value available until after a thread completes the callback.
* Use WANObjectCache::TSE_NONE to disable this logic.
* Default: WANObjectCache::TSE_NONE.
- * - busyValue: If no value exists and another thread is currently regenerating it, use this
- * as a fallback value (or a callback to generate such a value). This assures that cache
- * stampedes cannot happen if the value falls out of cache. This can be used as insurance
- * against cache regeneration becoming very slow for some reason (greater than the TTL).
+ * - busyValue: Specify a placeholder value to use when no value exists and another thread
+ * is currently regenerating it. This assures that cache stampedes cannot happen if the
+ * value falls out of cache. This also mitigates stampedes when value regeneration
+ * becomes very slow (greater than $ttl/"lowTTL"). If this is a closure, then it will
+ * be invoked to get the placeholder when needed.
* Default: null.
* - pcTTL: Process cache the value in this PHP instance for this many seconds. This avoids
* network I/O when a key is read several times. This will not cache when the callback
* @return array Ordered list of the following:
* - Cached or regenerated value
* - Cached or regenerated value version number or null if not versioned
- * - Timestamp of the cached value or null if there is no value
+ * - Timestamp of the current cached value at the key or null if there is no value
* @note Callable type hints are not used to avoid class-autoloading
*/
private function fetchOrRegenerate( $key, $ttl, $callback, array $opts ) {
$miss = is_infinite( $minAsOf ) ? 'renew' : 'miss';
$this->stats->increment( "wanobjectcache.$kClass.$miss.busy" );
- return [
- is_callable( $busyValue ) ? $busyValue() : $busyValue,
- $version,
- $curInfo['asOf']
- ];
+ return [ $this->resolveBusyValue( $busyValue ), $version, $curInfo['asOf'] ];
}
}
);
}
+ /**
+ * @param mixed $busyValue
+ * @return mixed
+ */
+ private function resolveBusyValue( $busyValue ) {
+ return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue;
+ }
+
/**
* Method to fetch multiple cache keys at once with regeneration
*
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->getFlag( self::DBO_DEFAULT ) ) {
+ $this->connLogger->warning(
+ __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
+ $this->getLogContext()
+ );
+ } 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();
}
}
* @param File $image File associated with this thumbnail
* @param array $params Array with scaler params
*
- * @return MediaTransformError|bool Error object if error occurred, false (=no error) otherwise
+ * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
*/
protected function transformImageMagick( $image, $params ) {
# use ImageMagick
* @param File $image File associated with this thumbnail
* @param array $params Array with scaler params
*
- * @return MediaTransformError Error|bool object if error occurred, false (=no error) otherwise
+ * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
*/
protected function transformImageMagickExt( $image, $params ) {
global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
* @param File $image File associated with this thumbnail
* @param array $params Array with scaler params
*
- * @return MediaTransformError Error|bool object if error occurred, false (=no error) otherwise
+ * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
*/
protected function transformCustom( $image, $params ) {
# Use a custom convert command
*
* @param File|FSFile $image
* @param string $filename
- * @return array
+ * @return array|false
*/
function getImageSize( $image, $filename ) {
$f = fopen( $filename, 'rb' );
/**
* Return data in the style of getimagesize()
- * @return array|bool Array or false on failure
+ * @return array|false Array or false on failure
*/
public function getImageSize() {
$data = $this->getInfo();
*
* @param File|FSFile $image
* @param string $path
- * @return array
+ * @return array|false
*/
function getImageSize( $image, $path ) {
$gis = parent::getImageSize( $image, $path );
* @param File|FSFile $image The image object, or false if there isn't one.
* Warning, FSFile::getPropsFromPath might pass an FSFile instead of File (!)
* @param string $path The filename
- * @return array|bool Follow the format of PHP getimagesize() internal function.
+ * @return array|false Follow the format of PHP getimagesize() internal function.
* See https://www.php.net/getimagesize. MediaWiki will only ever use the
* first two array keys (the width and height), and the 'bits' associative
* key. All other array keys are ignored. Returning a 'bits' key is optional
/**
* @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();
* @param File|FSFile $file
* @param string $path Unused
* @param bool|array $metadata
- * @return array
+ * @return array|false
*/
function getImageSize( $file, $path, $metadata = false ) {
if ( $metadata === false && $file instanceof File ) {
*/
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'] = [
*
* @param File|FSFile $image
* @param string $filename
- * @return array
+ * @return array|false
*/
function getImageSize( $image, $filename ) {
$header = self::getXCFMetaData( $filename );
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 );
}
}
*
* @return string HTML
*/
- function getNavigationBar() {
+ public function getNavigationBar() {
if ( !$this->isNavigationBarShown() ) {
return '';
}
* @ingroup Pager
*/
interface Pager {
- function getNavigationBar();
+ public function getNavigationBar();
- function getBody();
+ public function getBody();
}
*/
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 string[] */
+ private static $speculativeFields = [
+ 'speculativePageIdUsed',
+ 'mSpeculativeRevId',
+ 'revisionTimestampUsed'
+ ];
+
/** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */
private $mSpeculativeRevId;
-
+ /** @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;
return $this->mSpeculativeRevId;
}
+ /**
+ * @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;
+ }
+
/**
* @param string $timestamp TS_MW timestamp
* @since 1.34
$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;
--- /dev/null
+<?php
+
+/**
+ * BaseSearchResultSet is the base class that must be extended by SearchEngine
+ * search result set implementations.
+ *
+ * This base class is meant to hold B/C behaviors and to be useful it must never:
+ * - be used as type hints (ISearchResultSet must be used for this)
+ * - implement a constructor
+ * - declare utility methods
+ *
+ * @ingroup Search
+ */
+abstract class BaseSearchResultSet implements ISearchResultSet {
+
+ /**
+ * @var ArrayIterator|null Iterator supporting BC iteration methods
+ */
+ private $bcIterator;
+
+ /**
+ * Fetches next search result, or false.
+ * @deprecated since 1.32; Use self::extractResults() or foreach
+ * @return SearchResult|false
+ */
+ public function next() {
+ wfDeprecated( __METHOD__, '1.32' );
+ $it = $this->bcIterator();
+ $searchResult = $it->current();
+ $it->next();
+ return $searchResult ?? false;
+ }
+
+ /**
+ * Rewind result set back to beginning
+ * @deprecated since 1.32; Use self::extractResults() or foreach
+ */
+ public function rewind() {
+ wfDeprecated( __METHOD__, '1.32' );
+ $this->bcIterator()->rewind();
+ }
+
+ private function bcIterator() {
+ if ( $this->bcIterator === null ) {
+ $this->bcIterator = 'RECURSION';
+ $this->bcIterator = $this->getIterator();
+ } elseif ( $this->bcIterator === 'RECURSION' ) {
+ // Either next/rewind or extractResults must be implemented. This
+ // class was potentially instantiated directly. It should be
+ // abstract with abstract methods to enforce this but that's a
+ // breaking change...
+ wfDeprecated( static::class . ' without implementing extractResults', '1.32' );
+ $this->bcIterator = new ArrayIterator( [] );
+ }
+ return $this->bcIterator;
+ }
+
+ /**
+ * Fetch an array of regular expression fragments for matching
+ * the search terms as parsed by this engine in a text extract.
+ * STUB
+ *
+ * @return string[]
+ * @deprecated since 1.34 (use SqlSearchResult)
+ */
+ public function termMatches() {
+ return [];
+ }
+
+ /**
+ * Frees the result set, if applicable.
+ * @deprecated noop since 1.34
+ */
+ public function free() {
+ }
+}
<?php
/**
+ * A set of SearchEngine results.
+ * Must not be directly implemented by extension, please extend BaseSearchResultSet instead.
+ * This interface must only be used for type hinting.
+ *
+ * @see BaseSearchResultSet
* @ingroup Search
*/
interface ISearchResultSet extends \Countable, \IteratorAggregate {
/**
* @return int
*/
- function numRows();
+ public function numRows();
/**
* Some search modes return a total hit count for the query
*
* @return int|null
*/
- function getTotalHits();
+ public function getTotalHits();
/**
* Some search modes will run an alternative query that it thinks gives
*
* @return bool
*/
- function hasRewrittenQuery();
+ public function hasRewrittenQuery();
/**
* @return string|null The search the query was internally rewritten to,
* or null when the result of the original query was returned.
*/
- function getQueryAfterRewrite();
+ public function getQueryAfterRewrite();
/**
* @return string|null Same as self::getQueryAfterRewrite(), but in HTML
* and with changes highlighted. Null when the query was not rewritten.
*/
- function getQueryAfterRewriteSnippet();
+ public function getQueryAfterRewriteSnippet();
/**
* Some search modes return a suggested alternate term if there are
*
* @return bool
*/
- function hasSuggestion();
+ public function hasSuggestion();
/**
* @return string|null Suggested query, null if none
*/
- function getSuggestionQuery();
+ public function getSuggestionQuery();
/**
* @return string HTML highlighted suggested query, '' if none
*/
- function getSuggestionSnippet();
+ public function getSuggestionSnippet();
/**
* Return a result set of hits on other (multiple) wikis associated with this one
* @param int $type
* @return ISearchResultSet[]
*/
- function getInterwikiResults( $type = self::SECONDARY_RESULTS );
+ public function getInterwikiResults( $type = self::SECONDARY_RESULTS );
/**
* Check if there are results on other wikis
* @param int $type
* @return bool
*/
- function hasInterwikiResults( $type = self::SECONDARY_RESULTS );
+ public function hasInterwikiResults( $type = self::SECONDARY_RESULTS );
/**
* Did the search contain search syntax? If so, Special:Search won't offer
$setAugmentors[$name] = new PerRowAugmentor( $row );
}
+ /**
+ * @var string $name
+ * @var ResultSetAugmentor $augmentor
+ */
foreach ( $setAugmentors as $name => $augmentor ) {
$data = $augmentor->augmentAll( $resultSet );
if ( $data ) {
*
* @return bool
*/
- function isBrokenTitle() {
+ public function isBrokenTitle() {
return is_null( $this->mTitle );
}
*
* @return bool
*/
- function isMissingRevision() {
+ public function isMissingRevision() {
return !$this->mRevision && !$this->mImage;
}
/**
* @return Title
*/
- function getTitle() {
+ public function getTitle() {
return $this->mTitle;
}
* Get the file for this page, if one exists
* @return File|null
*/
- function getFile() {
+ public function getFile() {
return $this->mImage;
}
* @param string[] $terms Terms to highlight (this parameter is deprecated and ignored)
* @return string Highlighted text snippet, null (and not '') if not supported
*/
- function getTextSnippet( $terms = [] ) {
+ public function getTextSnippet( $terms = [] ) {
return '';
}
/**
* @return string Highlighted title, '' if not supported
*/
- function getTitleSnippet() {
+ public function getTitleSnippet() {
return '';
}
/**
* @return string Highlighted redirect name (redirect to this page), '' if none or not supported
*/
- function getRedirectSnippet() {
+ public function getRedirectSnippet() {
return '';
}
/**
* @return Title|null Title object for the redirect to this page, null if none or not supported
*/
- function getRedirectTitle() {
+ public function getRedirectTitle() {
return null;
}
/**
* @return string Highlighted relevant section name, null if none or not supported
*/
- function getSectionSnippet() {
+ public function getSectionSnippet() {
return '';
}
* @return Title|null Title object (pagename+fragment) for the section,
* null if none or not supported
*/
- function getSectionTitle() {
+ public function getSectionTitle() {
return null;
}
/**
* @return string Timestamp
*/
- function getTimestamp() {
+ public function getTimestamp() {
if ( $this->mRevision ) {
return $this->mRevision->getTimestamp();
} elseif ( $this->mImage ) {
/**
* @return int Number of words
*/
- function getWordCount() {
+ public function getWordCount() {
$this->initText();
return str_word_count( $this->mText );
}
/**
* @return int Size in bytes
*/
- function getByteSize() {
+ public function getByteSize() {
$this->initText();
return strlen( $this->mText );
}
/**
* @return string Interwiki prefix of the title (return iw even if title is broken)
*/
- function getInterwikiPrefix() {
+ public function getInterwikiPrefix() {
return '';
}
/**
* @return string Interwiki namespace of the title (since we likely can't resolve it locally)
*/
- function getInterwikiNamespaceText() {
+ public function getInterwikiNamespaceText() {
return '';
}
* Did this match file contents (eg: PDF/DJVU)?
* @return bool
*/
- function isFileMatch() {
+ public function isFileMatch() {
return false;
}
/**
* @ingroup Search
*/
-class SearchResultSet implements ISearchResultSet {
+class SearchResultSet extends BaseSearchResultSet {
+ use SearchResultSetTrait;
protected $containedSyntax = false;
*/
protected $results;
- /**
- * Set of result's extra data, indexed per result id
- * and then per data item name.
- * The structure is:
- * PAGE_ID => [ augmentor name => data, ... ]
- * @var array[]
- */
- protected $extraData = [];
-
/**
* @var boolean True when there are more pages of search results available.
*/
private $hasMoreResults;
- /**
- * @var ArrayIterator|null Iterator supporting BC iteration methods
- */
- private $bcIterator;
-
/**
* @param bool $containedSyntax True when query is not requesting a simple
* term match
$this->hasMoreResults = $hasMoreResults;
}
- /**
- * Fetch an array of regular expression fragments for matching
- * the search terms as parsed by this engine in a text extract.
- * STUB
- *
- * @return string[]
- * @deprecated since 1.34 (use SqlSearchResult)
- */
- function termMatches() {
- return [];
- }
-
- function numRows() {
+ public function numRows() {
return $this->count();
}
*
* @return int
*/
- function getTotalHits() {
+ public function getTotalHits() {
return null;
}
*
* @return bool
*/
- function hasRewrittenQuery() {
+ public function hasRewrittenQuery() {
return false;
}
* @return string|null The search the query was internally rewritten to,
* or null when the result of the original query was returned.
*/
- function getQueryAfterRewrite() {
+ public function getQueryAfterRewrite() {
return null;
}
* @return string|null Same as self::getQueryAfterRewrite(), but in HTML
* and with changes highlighted. Null when the query was not rewritten.
*/
- function getQueryAfterRewriteSnippet() {
+ public function getQueryAfterRewriteSnippet() {
return null;
}
*
* @return bool
*/
- function hasSuggestion() {
+ public function hasSuggestion() {
return false;
}
/**
* @return string|null Suggested query, null if none
*/
- function getSuggestionQuery() {
+ public function getSuggestionQuery() {
return null;
}
/**
* @return string HTML highlighted suggested query, '' if none
*/
- function getSuggestionSnippet() {
+ public function getSuggestionSnippet() {
return '';
}
* @param int $type
* @return ISearchResultSet[]
*/
- function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
+ public function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
return null;
}
* @param int $type
* @return bool
*/
- function hasInterwikiResults( $type = self::SECONDARY_RESULTS ) {
+ public function hasInterwikiResults( $type = self::SECONDARY_RESULTS ) {
return false;
}
- /**
- * Fetches next search result, or false.
- * @deprecated since 1.32; Use self::extractResults() or foreach
- * @return SearchResult|false
- */
- public function next() {
- wfDeprecated( __METHOD__, '1.32' );
- $it = $this->bcIterator();
- $searchResult = $it->current();
- $it->next();
- return $searchResult ?? false;
- }
-
- /**
- * Rewind result set back to beginning
- * @deprecated since 1.32; Use self::extractResults() or foreach
- */
- public function rewind() {
- wfDeprecated( __METHOD__, '1.32' );
- $this->bcIterator()->rewind();
- }
-
- private function bcIterator() {
- if ( $this->bcIterator === null ) {
- $this->bcIterator = 'RECURSION';
- $this->bcIterator = $this->getIterator();
- } elseif ( $this->bcIterator === 'RECURSION' ) {
- // Either next/rewind or extractResults must be implemented. This
- // class was potentially instantiated directly. It should be
- // abstract with abstract methods to enforce this but that's a
- // breaking change...
- wfDeprecated( static::class . ' without implementing extractResults', '1.32' );
- $this->bcIterator = new ArrayIterator( [] );
- }
- return $this->bcIterator;
- }
-
- /**
- * Frees the result set, if applicable.
- * @deprecated noop since 1.34
- */
- function free() {
- }
-
/**
* Did the search contain search syntax? If so, Special:Search won't offer
* the user a link to a create a page named by the search string because the
}
return $this->titles;
}
-
- /**
- * Sets augmented data for result set.
- * @param string $name Extra data item name
- * @param array[] $data Extra data as PAGEID => data
- */
- public function setAugmentedData( $name, $data ) {
- foreach ( $data as $id => $resultData ) {
- $this->extraData[$id][$name] = $resultData;
- }
- }
-
- /**
- * Returns extra data for specific result and store it in SearchResult object.
- * @param SearchResult $result
- */
- public function augmentResult( SearchResult $result ) {
- $id = $result->getTitle()->getArticleID();
- if ( $id === -1 ) {
- return;
- }
- $result->setExtensionData( function () use ( $id ) {
- return $this->extraData[$id] ?? [];
- } );
- }
-
- /**
- * @return int|null The offset the current page starts at. Typically
- * this should be null to allow the UI to decide on its own, but in
- * special cases like interleaved AB tests specifying explicitly is
- * necessary.
- */
- public function getOffset() {
- return null;
- }
-
- final public function getIterator() {
- return new ArrayIterator( $this->extractResults() );
- }
}
--- /dev/null
+<?php
+
+/**
+ * Trait useful for SearchResultSet implementations.
+ * It holds the functions that are rarely needed to be overridden.
+ *
+ * This trait can be used directly by extensions providing a SearchEngine.
+ *
+ * @ingroup Search
+ */
+trait SearchResultSetTrait {
+ /**
+ * Set of result's extra data, indexed per result id
+ * and then per data item name.
+ * The structure is:
+ * PAGE_ID => [ augmentor name => data, ... ]
+ * @var array[]
+ */
+ private $extraData = [];
+
+ /**
+ * Sets augmented data for result set.
+ * @param string $name Extra data item name
+ * @param array[] $data Extra data as PAGEID => data
+ */
+ public function setAugmentedData( $name, $data ) {
+ foreach ( $data as $id => $resultData ) {
+ $this->extraData[$id][$name] = $resultData;
+ }
+ }
+
+ /**
+ * Returns extra data for specific result and store it in SearchResult object.
+ * @param SearchResult $result
+ */
+ public function augmentResult( SearchResult $result ) {
+ $id = $result->getTitle()->getArticleID();
+ if ( $id === -1 ) {
+ return;
+ }
+ $result->setExtensionData( function () use ( $id ) {
+ return $this->extraData[$id] ?? [];
+ } );
+ }
+
+ /**
+ * @return int|null The offset the current page starts at. Typically
+ * this should be null to allow the UI to decide on its own, but in
+ * special cases like interleaved AB tests specifying explicitly is
+ * necessary.
+ */
+ public function getOffset() {
+ return null;
+ }
+
+ final public function getIterator() {
+ return new ArrayIterator( $this->extractResults() );
+ }
+}
// 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 {
$num = $res->numRows();
# Fetch results
$vals = [];
- foreach ( $res as $row ) {
+ foreach ( $res as $i => $row ) {
if ( isset( $row->value ) ) {
if ( $this->usesTimestamps() ) {
$value = wfTimestamp( TS_UNIX,
$value = intval( $row->value ); // T16414
}
} else {
- $value = 0;
+ $value = $i;
}
$vals[] = [
* @return bool|array
*/
public static function processForm( array $data, IContextSource $context ) {
- global $wgBlockAllowsUTEdit, $wgHideUserContribLimit;
-
$performer = $context->getUser();
$enablePartialBlocks = $context->getConfig()->get( 'EnablePartialBlocks' );
$isPartialBlock = $enablePartialBlocks &&
}
# Recheck params here...
+ $hideUserContribLimit = $context->getConfig()->get( 'HideUserContribLimit' );
if ( $type != DatabaseBlock::TYPE_USER ) {
$data['HideUser'] = false; # IP users should not be hidden
} elseif ( !wfIsInfinity( $data['Expiry'] ) ) {
# Bad expiry.
return [ 'ipb_expiry_temp' ];
- } elseif ( $wgHideUserContribLimit !== false
- && $user->getEditCount() > $wgHideUserContribLimit
+ } elseif ( $hideUserContribLimit !== false
+ && $user->getEditCount() > $hideUserContribLimit
) {
# Typically, the user should have a handful of edits.
# Disallow hiding users with many edits for performance.
return [ [ 'ipb_hide_invalid',
- Message::numParam( $wgHideUserContribLimit ) ] ];
+ Message::numParam( $hideUserContribLimit ) ] ];
} elseif ( !$data['Confirm'] ) {
return [ 'ipb-confirmhideuser', 'ipb-confirmaction' ];
}
}
+ $blockAllowsUTEdit = $context->getConfig()->get( 'BlockAllowsUTEdit' );
+ $userTalkEditAllowed = !$blockAllowsUTEdit || !$data['DisableUTEdit'];
+ if ( !$userTalkEditAllowed &&
+ $isPartialBlock &&
+ !in_array( NS_USER_TALK, explode( "\n", $data['NamespaceRestrictions'] ) )
+ ) {
+ return [ 'ipb-prevent-user-talk-edit' ];
+ }
+
# Create block object.
$block = new DatabaseBlock();
$block->setTarget( $target );
$block->setReason( $data['Reason'][0] );
$block->setExpiry( $expiryTime );
$block->isCreateAccountBlocked( $data['CreateAccount'] );
- $block->isUsertalkEditAllowed( !$wgBlockAllowsUTEdit || !$data['DisableUTEdit'] );
+ $block->isUsertalkEditAllowed( $userTalkEditAllowed );
$block->isEmailBlocked( $data['DisableEmail'] );
$block->isHardblock( $data['HardBlock'] );
$block->isAutoblocking( $data['AutoBlock'] );
if ( $performer->getBlock() ) {
if ( $target instanceof User && $target->getId() == $performer->getId() ) {
# User is trying to unblock themselves
- // @TODO Ensure that the block does not apply to the `unblockself`
- // right.
if ( $performer->isAllowed( 'unblockself' ) ) {
return true;
# User blocked themselves and is now trying to reverse it
public function execute( $subPage ) {
$this->setHeaders();
$this->loadAuth( $subPage );
+
+ if ( !$this->isActionAllowed( $this->authAction ) ) {
+ if ( $this->authAction === AuthManager::ACTION_UNLINK ) {
+ // Looks like there are no linked accounts to unlink
+ $titleMessage = $this->msg( 'cannotunlink-no-provider-title' );
+ $errorMessage = $this->msg( 'cannotunlink-no-provider' );
+ throw new ErrorPageError( $titleMessage, $errorMessage );
+ } else {
+ // user probably back-button-navigated into an auth session that no longer exists
+ // FIXME would be nice to show a message
+ $this->getOutput()->redirect( $this->getPageTitle()->getFullURL( '', false, PROTO_HTTPS ) );
+ return;
+ }
+ }
+
$this->outputHeader();
$status = $this->trySubmit();
<td class="mw-changeslist-line-inner">{{!
}}{{# rev-deleted-event }}<span class="history-deleted">{{{ . }}}</span>{{/ rev-deleted-event }}{{!
}}{{{ articleLink }}}{{{ languageDirMark }}}{{{ logText }}}{{!
- }}<span class="mw-changeslist-separator"></span>{{!
- }}{{# charDifference }}{{{ . }}} <span class="mw-changeslist-separator"></span>{{/ charDifference }}{{!
+ }} <span class="mw-changeslist-separator"></span> {{!
+ }}{{# charDifference }}{{{ . }}} <span class="mw-changeslist-separator"></span> {{/ charDifference }}{{!
}}<span class="changedby">{{{ users }}}</span>{{!
}}{{ numberofWatchingusers }}{{!
}}</td>
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
*/
* - array $config['dropdowninput'] Configuration for the DropdownInputWidget
* - bool $config['or'] Configuration for whether the widget is dropdown AND input
* or dropdown OR input
+ * - bool $config['required'] Configuration for whether the widget is a required input.
*/
public function __construct( array $config = [] ) {
// Configuration initialization
[
'textinput' => [],
'dropdowninput' => [],
- 'or' => false
+ 'or' => false,
+ 'required' => false,
],
$config
);
$config['dropdowninput']['disabled'] = true;
}
+ $config['textinput']['required'] = $config['or'] ? false : $config['required'];
+ $config['dropdowninput']['required'] = $config['required'];
+
parent::__construct( $config );
// Properties
$config['dropdowninput'] = $this->config['dropdowninput'];
$config['dropdowninput']['dropdown']['$overlay'] = true;
$config['or'] = $this->config['or'];
+ $config['required'] = $this->config['required'];
return parent::getConfig( $config );
}
}
"Elbasyouny",
"Omar Ghrida",
"AHmed Khaled",
- "البراء صالح"
+ "البراء صالح",
+ "Dyolf77 (WMF)"
]
},
"tog-underline": "سطر تحت الوصلات:",
"password-change-forbidden": "لا يمكنك تغيير كلمات السر على هذا الويكي.",
"externaldberror": "هناك إما خطأ في دخول قاعدة البيانات الخارجية أو أنه غير مسموح لك بتحديث حسابك الخارجي.",
"login": "تسجيل الدخول",
- "login-security": "توكيد هويتك",
+ "login-security": "تأكيد هويتك",
"nav-login-createaccount": "دخول / إنشاء حساب",
"logout": "تسجيل الخروج",
"userlogout": "خروج",
"passwordreset-invalidemail": "عنوان بريد إلكتروني غير صالح",
"passwordreset-nodata": "لا اسم مستخدم ولا عنوان بريد الإلكتروي تم توفيره",
"changeemail": "تغيير أو إزالة عنوان البريد الإلكتروني",
- "changeemail-header": "Ø¥Ù\83Ù\85اÙ\84 Ù\87ذا اÙ\84Ù\86Ù\85Ù\88ذج Ù\84تغÙ\8aÙ\8aر عÙ\86Ù\88اÙ\86 اÙ\84برÙ\8aد اÙ\84Ø¥Ù\84Ù\83ترÙ\88Ù\86Ù\8a اÙ\84خاص بÙ\83. إذا Ù\83Ù\86ت ترغب Ù\81Ù\8a إزاÙ\84Ø© جÙ\85عÙ\8aØ© Ø£Ù\8a عÙ\86Ù\88اÙ\86 اÙ\84برÙ\8aد اÙ\84Ø¥Ù\84Ù\83ترÙ\88Ù\86Ù\8a Ù\85Ù\86 ØسابÙ\83Ø\8c Ù\88ترÙ\83 اÙ\84Ù\81راغ عÙ\86Ù\88اÙ\86 اÙ\84برÙ\8aد اÙ\84Ø¥Ù\84Ù\83ترÙ\88Ù\86Ù\8a اÙ\84جدÙ\8aد عÙ\86د تÙ\82دÙ\8aÙ\85 اÙ\84Ù\86Ù\85Ù\88ذج",
+ "changeemail-header": "Ø£Ù\83Ù\85Ù\84 Ù\87Ø°Ù\87 اÙ\84استÙ\85ارة Ù\84تغÙ\8aÙ\8aر عÙ\86Ù\88اÙ\86 اÙ\84برÙ\8aد اÙ\84Ø¥Ù\84Ù\83ترÙ\88Ù\86Ù\8a اÙ\84خاص بÙ\83. إذا Ù\83Ù\86ت ترغب Ù\81Ù\8a إزاÙ\84Ø© Ø£Ù\8a عÙ\86Ù\88اÙ\86 برÙ\8aد Ø¥Ù\84Ù\83ترÙ\88Ù\86Ù\8a Ù\85Ù\82ترÙ\86 Ù\85ع ØسابÙ\83Ø\8c اترÙ\83 ØÙ\82Ù\84 عÙ\86Ù\88اÙ\86 اÙ\84برÙ\8aد اÙ\84Ø¥Ù\84Ù\83ترÙ\88Ù\86Ù\8a اÙ\84جدÙ\8aد Ù\81ارغا عÙ\86د تÙ\82دÙ\8aÙ\85 اÙ\84استÙ\85ارة.",
"changeemail-no-info": "يجب تسجيل الدخول للوصول إلى هذه الصفحة مباشرة.",
"changeemail-oldemail": "عنوان البريد الإلكتروني الحالي:",
"changeemail-newemail": "عنوان البريد الإلكتروني الجديد:",
- "changeemail-newemail-help": "هذا الحقل ينبغي أن يترك فارغا في حالة لو كنت تريد إزالة عنوان البريد الإلكتروني الخاص بك. أنت لن تكون قادرا على إعادة ضبط كلمة سر ضائعة ولن تتلقى رسئل بريد إلكتروني من هذه الويكي لو أزيل عنوان البريد الإلكتروني.",
+ "changeemail-newemail-help": "هذا الحقل يجب أن يترك فارغا لو كنت تريد إزالة عنوان البريد الإلكتروني الخاص بك. لو أزيل عنوان البريد الإلكتروني لن تكون قادرا على إعادة ضبط كلمة سر ضائعة ولن تتلقى رسائل بريد إلكتروني من هذه الويكي.",
"changeemail-none": "(لا شيء)",
"changeemail-password": "كلمة سر {{SITENAME}} الخاصة بك:",
"changeemail-submit": "غيّر البريد الإلكتروني",
"linkaccounts": "ربط الحسابات",
"linkaccounts-success-text": "الحساب تم وصله.",
"linkaccounts-submit": "اربط الحسابات",
+ "cannotunlink-no-provider-title": "لا توجد حسابات مرتبطة لإلغاء الربط",
+ "cannotunlink-no-provider": "لا توجد حسابات مرتبطة يمكن إلغاء ربطها.",
"unlinkaccounts": "إزالة ربط الحسابات",
"unlinkaccounts-success": "الحساب تم فك وصله.",
"authenticationdatachange-ignored": "تغيير بيانات التحقق لم يتم التعامل معه. ربما لم يتم ضبط موفر؟",
"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|সৃষ্টি কৰা হ'ল}}",
"category-file-count-limited": "جهلیگین {{PLURAL:$1|فایل|$1 فایلان}} ته هنوکین دسته اینت",
"listingcontinuesabbrev": "ادامه.",
"index-category": "سرتاک بوتگێن پێجان",
- "noindex-category": "سرتاک نبوتگین پیجان",
+ "noindex-category": "سرتاک نبݔتگݔں تاکاں",
"broken-file-category": "پیج گون پرشتگین لینک فایل",
"about": "بارہئا",
"article": "محتوا صفحه",
"search-interwiki-more": "(گݔشتِر)",
"search-interwiki-more-results": "گݔشترݔں نتیجہ",
"search-relatedarticle": "مربوطین",
+ "search-invalid-sort-order": "$1ء ترتیپ شرّ نئں پݔسری ترتیپ پدا سپتءَ بئ اَنت.\nھنیگݔں ترتیپ: $2",
+ "search-unknown-profile": "$1ء شۏھاز نابزانت اِنت،پݔسری شۏھازی سپتءَ بئ اَنت۔",
"searchrelated": "مربوط",
"searchall": "کل",
"showingresults": "جهل پیش دارگنت تا {{PLURAL:$1|'''1'''نتیجه|'''$1''' نتایج}} شروع بنت گون #'''$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.",
"Matma Rex",
"Trizek (WMF)",
"Dishual",
- "Fitoschido"
+ "Fitoschido",
+ "Huñvreüs"
]
},
"tog-underline": "Liammoù islinennet",
"history": "Istor ar bajenn",
"history_short": "Istor",
"history_small": "istor",
- "updatedmarker": "kemmet abaoe ma zaol-sell diwezhañ",
+ "updatedmarker": "kemmet abaoe ho kwel diwezhañ",
"printableversion": "Stumm da voullañ",
"permalink": "Chomlec'h ar stumm-mañ",
"print": "Moullañ",
"defaultmessagetext": "Testenn dre ziouer",
"content-failed-to-parse": "C'hwitet eo dielfennadur endalc'had $2 evit ar patrom $1: $3",
"invalid-content-data": "n'eo ket mat roadennoù an endalc'had",
- "content-not-allowed-here": "N'eo ket aotreet an endalc'had \"$1\" er bajenn [[:$2]]",
+ "content-not-allowed-here": "N'eo ket aotreet an endalc'had \"$1\" er bajenn [[:$2]] e lec’h \"$3\"",
"editwarning-warning": "Mar kuitait ar bajenn-mañ e c'hallit koll ar c'hemmoù degaset ganeoc'h.\nMa'z oc'h kevreet e c'hallit diweredekaat ar c'hemenn-diwall-mañ e rann \"{{int:prefs-editing}}\" en ho penndibaboù.",
"editpage-invalidcontentmodel-title": "N'eo ket skoret ar patrom danvez",
"editpage-invalidcontentmodel-text": "N'eo ket skoret ar patrom danvez \"$1\".",
"stub-threshold-disabled": "Diweredekaet",
"recentchangesdays": "Niver a zevezhioù da ziskouez er c'hemmoù diwezhañ :",
"recentchangesdays-max": "D'ar muiañ $1 {{PLURAL:$1|deiz|deiz}}",
- "recentchangescount": "Niver a gemmoù da ziskouez dre ziouer",
- "prefs-help-recentchangescount": "Kemer a ra an dra-mañ e kont ar c'hemmoù diwezhañ, istor ar pajennoù hag ar marilhoù.",
+ "recentchangescount": "Niver a gemmoù da ziskouez e kemmoù diwezhañ, istorioù ha marilhoù, dre ziouer :",
+ "prefs-help-recentchangescount": "Niver brasañ : 1000",
"prefs-help-watchlist-token2": "Homañ zo an alc'hwez kuzh d'ho roll-evezhiañ davit boued war ar web. Forzh piv a anavez anezhañ a c'hall lenn ho roll-evezhiañ, setu na lavarit grit diwar e benn. M'ho pez ezhomm, e c'hallit [[Special:ResetTokens|assevel anezhañ]].",
"savedprefs": "Enrollet eo bet ar penndibaboù.",
"savedrights": "Enrollet eo bet strolladoù implijer {{GENDER:$1|$1}}.",
"timezoneregion-europe": "Europa",
"timezoneregion-indian": "Meurvor Indez",
"timezoneregion-pacific": "Meurvor Habask",
- "allowemail": "Aotren ar posteloù a-berzh implijerien all",
+ "allowemail": "Aotreañ implijerien all da gas din posteloù",
"email-allow-new-users-label": "Aotreañ e-bostoù a-berzh an implijerien nevez",
"email-blacklist-label": "Difenn d'an implijerien-se da gas un e-bost din:",
"prefs-searchoptions": "Klask",
"grouppage-autoconfirmed": "{{ns:project}}: Implijerien bet kadarnaet ent emgefre",
"grouppage-bot": "{{ns:project}}:Botoù",
"grouppage-sysop": "{{ns:project}}:Merourien",
- "grouppage-interface-admin": "{{ns:project}}:Merourien etrefas",
+ "grouppage-interface-admin": "{{ns:project}}:Merourien an etrefas",
"grouppage-bureaucrat": "{{ns:project}}: Burevidi",
"grouppage-suppress": "{{ns:project}}: Diverkerien",
"right-read": "Lenn ar pajennoù",
"rcfilters-exclude-button-off": "Skarzhañ ar re dibabet",
"rcfilters-exclude-button-on": "Re dibabet skarzhet",
"rcfilters-view-tags": "Kemmoù merket",
+ "rcfilters-liveupdates-button": "Nevesaat war-eeun",
"rcfilters-filter-showlinkedfrom-label": "Diskouez ar c'hemmoù war ar bajennoù liammet",
"rcfilters-filter-showlinkedto-label": "Diskouez ar c'hemmoù war ar bajennoù liammet",
"rcfilters-target-page-placeholder": "Skrivañ anv ar bajenn (pe rummad)",
"ipbreason": "Abeg :",
"ipbreason-dropdown": "*Abegoù stankañ boutinañ\n** Degas titouroù faos\n** Tennañ danvez eus ar pajennoù\n** Degas liammoù Strobus war-du lec'hiennoù diavaez\n** Degas danvez diboell/dizoare er pajennoù\n** Emzalc'h hegazus/handeus betek re\n** Mont re bell gant implij meur a gont\n** Anv implijer n'eo ket aotreet",
"ipb-hardblock": "Mirout ouzh an implijerien kevreet da zegas kemmoù adalek ar chomlec'h IP-mañ",
- "ipbcreateaccount": "Mirout ouzh an implijer da grouiñ kontoù",
- "ipbemailban": "Mirout ouzh an implijer da gas posteloù",
+ "ipbcreateaccount": "Krouiñ kontoù",
+ "ipbemailban": "Kas posteloù",
"ipbenableautoblock": "Stankañ war-eeun ar chomlec'h IP diwezhañ implijet gant an den-mañ hag an holl chomlec'hioù a c'hallfe klask kemmañ traoù drezo diwezhatoc'h",
"ipbsubmit": "Stankañ an implijer-mañ",
"ipbother": "Prantad all",
"ipboptions": "2 eurvezh:2 hours,1 devezh:1 day,3 devezh:3 days,1 sizhunvezh:1 week,2 sizhunvezh:2 weeks,1 mizvezh:1 month,3 mizvezh:3 months,6 mizvezh:6 months,1 bloaz:1 year,da viken:infinite",
"ipbhidename": "Kuzhat anv an implijer er rolloù hag er c'hemmoù",
"ipbwatchuser": "Evezhiañ pajennoù implijer ha kaozeal an implijer-mañ",
- "ipb-disableusertalk": "Mirout ouzh an implijer-mañ da implijout e bajenn gaozeal dezhañ e-unan e-keit hag emañ stanket",
+ "ipb-disableusertalk": "Kemmañ e bajenn gaozeal dezhañ",
"ipb-change-block": "Adstankañ an implijer-mañ gant an hevelep arventennoù",
"ipb-confirm": "Kadarnaat ar stankadenn",
"ipb-pages-label": "Pajennoù",
"import-nonewrevisions": "N'eus bet enporzhiet degasadenn ebet (aze e oant dija, pe distaolet e oant bet abalamour da fazioù).",
"xml-error-string": "$1 war al linenn $2, bann $3 (okted $4) : $5",
"import-upload": "Enporzhiañ roadennoù XML",
- "import-token-mismatch": "Kollet eo bet roadennoù an dalc'h.\n\nMarteze oc'h bet digevreet. <strong>Gwiriit emaoc'h mat kevreet ha klaskit en-dro</strong>.\nMa ne'z a ket en-dro c'hoazh, klaskit [[Special:UserLogout|digevreañ]] hag adkevreañ en-dro, ha gwiriit mat ec'h asant ho merdeer degemer toupinoù digant al lec'hienn-mañ.",
+ "import-token-mismatch": "Kollet eo bet roadennoù an dalc'h.\n\nMarteze oc'h bet digevreet. '''Gwiriit emaoc'h kevreet mat ha klaskit en-dro'''.\nMa ne'z a ket en-dro c'hoazh, klaskit [[Special:UserLogout|digevreañ]] hag adkevreañ, ha gwiriit mat ec'h asant ho merdeer degemer toupinoù digant al lec'hienn-mañ.",
"import-invalid-interwiki": "Dibosupl enporzhiañ adal ar wiki spisaet.",
"import-error-edit": "N'eo ket bet enporzhiet ar bajenn \"$1\" peogwir n'oc'h ket aotreet da zegas kemmoù enni.",
"import-error-create": "N'eo ket bet enporzhiet ar bajenn \"$1\" peogwir n'oc'h ket aotreet da grouiñ anezhi.",
"authmanager-email-help": "Chomlec'h postel",
"authmanager-realname-label": "Anv gwir",
"authmanager-realname-help": "Anv gwir an implijer",
- "authmanager-provider-password": "Gwiriekadur diazezet war ur ger-termen",
+ "authmanager-provider-password": "Gwiriekadur diazezet war ur ger-tremen",
"authmanager-provider-password-domain": "Dilesadur diazezet war ur ger-tremen hag an domani",
"authmanager-provider-temporarypassword": "Ger-tremen da c'hortoz",
"authprovider-confirmlink-message": "Diwar an taolioù-esae diwezhañ graet ganeoc'h evit kevreañ e c'haller liammañ ar c'hontoù da heul ouzh ho kont wiki. Ur wech liammet e c'hallit kevreañ dre ar c'hontoù-se. Diuzit ar re zo da vezañ liammet.",
"autoblockedtext": "La vostra adreça IP ha estat blocada automàticament perquè va ser usada per un usuari actualment blocat. Aquest usuari va ser blocat per l'{{GENDER:$1|administrador|administradora}} $1. El motiu donat per al blocatge és aquest:\n\n:<em>$2</em>\n\n* Inici del blocatge: $8\n* Final del blocatge: $6\n* Usuari blocat: $7\n\nPodeu contactar l'usuari $1 o algun altre dels [[{{MediaWiki:Grouppage-sysop}}|administradors]] per a discutir el blocatge.\n\nRecordeu que per a poder usar l'opció «{{int:emailuser}}» haureu d'haver validat una adreça de correu electrònic a les vostres [[Special:Preferences|preferències]].\n\nEl número d'identificació de la vostra adreça IP és $3, i l'ID del blocatge és #$5. Si us plau, incloeu aquestes dades en totes les consultes que feu.",
"systemblockedtext": "El vostre nom d'usuari o adreça IP ha estat blocada automàticament pel MediaWiki.\nEl motiu donat és:\n\n:<em>$2</em>\n\n* Inici del blocatge: $8\n* Caducitat del blocatge: $6\n* Destinatari del blocatge: $7\n\nLa vostra adreça IP actual és $3.\nAfegiu les dades de més amunt en qualsevol consulta que feu al respecte.",
"blockednoreason": "no s'ha donat cap motiu",
+ "blockedtext-composite-no-ids": "La vostra adreça IP apareix en diferents llistes negres",
"blockedtext-composite-reason": "Hi ha diferents blocatges associats al vostre compte i/o adreça IP",
"whitelistedittext": "Heu de $1 per modificar pàgines.",
"confirmedittext": "Heu de confirmar la vostra adreça electrònica abans de poder modificar les pàgines. Definiu i valideu la vostra adreça electrònica a través de les vostres [[Special:Preferences|preferències d'usuari]].",
"edit-error-short": "Error: $1",
"edit-error-long": "Errors:\n\n$1",
"specialmute": "Silencia",
+ "specialmute-success": "S'han actualitzat les vostres preferències de silenciament. Vegeu tots els usuaris silenciats a les [[Special:Preferences|your preferències]].",
"specialmute-submit": "Confirma",
"specialmute-label-mute-email": "Silencia el correu electrònic d'aquest usuari",
+ "specialmute-header": "Seleccioneu les vostres preferències de silenciament de l'usuari <b>{{BIDI:[[User:$1|$1]]}}</b>.",
"specialmute-error-invalid-user": "No s’ha trobat el nom d’usuari que heu indicat.",
"specialmute-login-required": "Inicieu una sessió per canviar les preferències de silenciament.",
+ "mute-preferences": "Preferències de silenciament",
"revid": "revisió $1",
"pageid": "ID de pàgina $1",
"rawhtml-notallowed": "No és possible fer servir les etiquetes <html> fora de les pàgines normals.",
"lockmanager-fail-acquirelock": "«$1» блоктоха цатарло.",
"lockmanager-fail-openlock": "Блоктохаран «$1» файл схьаелла цаелира.",
"lockmanager-fail-releaselock": "\"$1\" блокдӀаяккха цаелира.",
+ "zip-file-open-error": "Архивна талламбархьама файл схьаеллаш гӀалат даьлла.",
+ "zip-wrong-format": "Билгалйина файл — ZIP-архив яц.",
+ "zip-unsupported": "ХӀокху ZIP-файло лелош ю таронаш, MediaWiki ловш йоцу.\nИза кхочуш талла еш яц.",
"uploadstash": "Къайлаха чуяккхар",
"uploadstash-clear": "ДӀацӀанъян къайла йолу файлаш",
"uploadstash-nofiles": "Хьан къайла файлаш яц.",
"allmessages-filter-unmodified": "Хийцан йоцурш",
"allmessages-filter-all": "Массо",
"allmessages-filter-modified": "Хийцнарш",
- "allmessages-prefix": "Ð\9bÑ\83Ñ\8cÑ\82Ñ\82Ñ\83Ñ\80г оÑ\86Ñ\83 деÑ\88аÑ\85Ñ\8cалÑ\85е:",
+ "allmessages-prefix": "Ð\9bÑ\83Ñ\8cÑ\82Ñ\82Ñ\83Ñ\80г пÑ\80еÑ\84икÑ\81Ñ\86а:",
"allmessages-language": "Мотт:",
"allmessages-filter-submit": "Дехьа гӀо",
"allmessages-filter-translate": "Гочйе",
"McDutchie",
"Johanna Strodt (WMDE)",
"Andi-3",
- "1233qwer1234qwer4"
+ "1233qwer1234qwer4",
+ "MarkusRost",
+ "Mcandri13"
]
},
"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",
"systemblockedtext": "Dein Benutzername oder deine IP-Adresse wurde von MediaWiki automatisch gesperrt.\nDer angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der Sperre: $6\n* Sperre betrifft: $7\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
"blockednoreason": "keine Begründung angegeben",
"blockedtext-composite": "<strong>Dein Benutzername oder deine IP-Adresse wurde gesperrt.</strong>\n\nDer Angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der längsten Sperre: $6\n\n* $5\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
+ "blockedtext-composite-no-ids": "Deine IP-Adresse taucht in mehreren Sperrlisten auf",
"blockedtext-composite-reason": "Es gibt mehrere Sperren gegen dein Benutzerkonto und/oder deine IP-Adresse",
"whitelistedittext": "Du musst dich $1, um Seiten bearbeiten zu können.",
"confirmedittext": "Du musst deine E-Mail-Adresse erst bestätigen, bevor du Bearbeitungen durchführen kannst. Bitte ergänze und bestätige deine E-Mail in den [[Special:Preferences|Einstellungen]].",
"rcfilters-clear-all-filters": "Clear all filters",
"rcfilters-show-new-changes": "View new changes since $1",
"rcfilters-search-placeholder": "Filter changes (use menu or search for filter name)",
+ "rcfilters-search-placeholder-mobile": "Filters",
"rcfilters-invalid-filter": "Invalid filter",
"rcfilters-empty-filter": "No active filters. All contributions are shown.",
"rcfilters-filterlist-title": "Filters",
"block-log-flags-angry-autoblock": "enhanced autoblock enabled",
"block-log-flags-hiddenname": "username hidden",
"range_block_disabled": "The administrator ability to create range blocks is disabled.",
+ "ipb-prevent-user-talk-edit": "Editing their own talk page must be allowed for a partial block, unless it includes a restriction on the User Talk namespace.",
"ipb_expiry_invalid": "Expiry time invalid.",
"ipb_expiry_old": "Expiry time is in the past.",
"ipb_expiry_temp": "Hidden username blocks must be permanent.",
"linkaccounts": "Link accounts",
"linkaccounts-success-text": "The account was linked.",
"linkaccounts-submit": "Link accounts",
+ "cannotunlink-no-provider-title": "There are no linked accounts to unlink",
+ "cannotunlink-no-provider": "There are no linked accounts that can be unlinked.",
"unlinkaccounts": "Unlink accounts",
"unlinkaccounts-success": "The account was unlinked.",
"authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?",
"svg-long-error": "SVG fitxategi ez baliagarria: $1",
"show-big-image": "Jatorrizko fitxategia",
"show-big-image-preview": "Aurreikuspen honen neurria: $1.",
- "show-big-image-preview-differ": "$2 fitxategi honen $3 aurreikuspenaren tamainia: $1.",
+ "show-big-image-preview-differ": "$2 fitxategi honen $3 aurreikuspenaren tamaina: $1.",
"show-big-image-other": "Bestelako {{PLURAL:$2|bereizmena|bereizmenak}}: $1.",
"show-big-image-size": "$1 × $2 pixel",
"file-info-gif-looped": "kiribildua",
"mediastatistics": "Media estatistikak",
"mediastatistics-summary": "Igotako fitxategien estatistikak. Hemen ikus daitekeena fitxategiaren azken bertsioa baino ez da. Fitxategiaren bertsio zahar edo ezabatuak kanpo daude.",
"mediastatistics-nbytes": "{{PLURAL:$1|$1 byte|$1 byte}} ($2; %$3)",
- "mediastatistics-bytespertype": "Atal honetarako fitxategi tamainia totala: {{PLURAL:$1|$1 byte}} ($2; $3%).",
- "mediastatistics-allbytes": "Fitxategi guztietarako fitxategi tamainia osoa: {{PLURAL:$1|$1 byte|$1 bytes}} ($2).",
+ "mediastatistics-bytespertype": "Atal honetako fitxategi tamaina, guztira: {{PLURAL:$1|$1 byte}} ($2; % $3).",
+ "mediastatistics-allbytes": "Fitxategi guztien tamaina, guztira: {{PLURAL:$1|$1 byte}} ($2).",
"mediastatistics-table-mimetype": "MIME mota",
"mediastatistics-table-extensions": "Luzapen posibleak",
"mediastatistics-table-count": "Fitxategi kopurua",
"authors": [
"Don Alessandro",
"Ильнар",
- "Рашат Якупов"
+ "Рашат Якупов",
+ "Ерней"
]
},
"exif-imagewidth": "Киңлек",
"exif-photometricinterpretation": "Төс моделе",
"exif-orientation": "Кадр куелышы",
"exif-samplesperpixel": "Төс өлешләре саны",
- "exif-xresolution": "Ð\93оÑ\80изонÑ\82алÑ\8c зÑ\83Ñ\80лык",
- "exif-yresolution": "Ð\92еÑ\80Ñ\82икалÑ\8c зÑ\83Ñ\80лык",
- "exif-datetime": "Файл үзгәртүләр датасы һәм вакыты",
- "exif-imagedescription": "Ð Ó\99Ñ\81емнең иÑ\81еме",
- "exif-make": "Камераның җитештерүчесе",
- "exif-model": "Камераның төре",
- "exif-software": "Ð\9fÑ\80огÑ\80аммалÑ\8b Ñ\82Ó\99Ñ\8dмин иÑ\82елеÑ\88",
+ "exif-xresolution": "ЯÑ\82ма аÑ\87Ñ\8bклык",
+ "exif-yresolution": "Ð\90Ñ\81ма аÑ\87Ñ\8bклык",
+ "exif-datetime": "Файл үзгәреше датасы һәм вакыты",
+ "exif-imagedescription": "СÑ\83Ñ\80Ó\99Ñ\82 аÑ\82амаÑ\81Ñ\8b",
+ "exif-make": "Камера җитештерүчесе",
+ "exif-model": "Камера төре",
+ "exif-software": "Ð\9aÑ\83лланÑ\8bлган пÑ\80огÑ\80амма",
"exif-artist": "Автор",
- "exif-copyright": "Автор хокуклары иясе",
+ "exif-copyright": "Авторлык хокукы иясе",
"exif-exifversion": "Exif юрамасы",
- "exif-flashpixversion": "FlashPix Ñ\8eÑ\80амаÑ\81Ñ\8bн Ñ\82Ó\99Ñ\8dмин иÑ\82Ò¯",
- "exif-colorspace": "Төсләр тирәлеге",
+ "exif-flashpixversion": "FlashPix Ñ\8fÑ\80аÑ\88лÑ\8b Ñ\8eÑ\80амаÑ\81Ñ\8b",
+ "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": "Чын вакыты",
"exif-subsectime": "Файлны үзгәртүнең өлешле секунд вакыты",
"exif-subsectimeoriginal": "Чын ясалу вакытының өлеш секунды",
"exif-subsectimedigitized": "Санлаштыру вакытының өлеш секунды",
- "exif-exposuretime": "ÐкÑ\81позиÑ\86иÑ\8f вакÑ\8bÑ\82ы",
+ "exif-exposuretime": "ÐкÑ\81позиÑ\86иÑ\8f озаклÑ\8bгы",
"exif-exposuretime-format": "$1 с ($2)",
"exif-fnumber": "Диафрагманың саны",
"exif-fnumber-format": "f/$1",
"exif-sharpness": "Ачыклыгы",
"exif-devicesettingdescription": "Камераның көйләүләр тасвирламасы",
"exif-subjectdistancerange": "Төшерү җисеменә кадәр ераклык",
- "exif-imageuniqueid": "Ð Ó\99Ñ\81емнең саны (ID)",
+ "exif-imageuniqueid": "СÑ\83Ñ\80Ó\99Ñ\82 саны (ID)",
"exif-gpsversionid": "GPS мәгълүматы блогының версиясе",
"exif-gpslatituderef": "Киңлек индексы",
"exif-gpslatitude": "Киңлек",
"exif-gpslongituderef": "Озынлык индексы",
"exif-gpslongitude": "Озынлык",
- "exif-gpsaltituderef": "Ð\91иеклек индексы",
- "exif-gpsaltitude": "Ð\91иеклек",
+ "exif-gpsaltituderef": "ЮгаÑ\80Ñ\8bлÑ\8bк индексы",
+ "exif-gpsaltitude": "ЮгаÑ\80Ñ\8bлÑ\8bк",
"exif-gpstimestamp": "UTC буенча вакыт",
"exif-gpssatellites": "Кулланылган иярченнәр тасвирламасы",
"exif-gpsstatus": "Алгычның статусы һәм төшерү вакыты",
"exif-languagecode": "Тел",
"exif-iimversion": "IIM юрамасы",
"exif-iimcategory": "Төркем",
+ "exif-iimsupplementalcategory": "Өстәмә төркемнәр",
"exif-identifier": "Идентификатор",
"exif-label": "Билгеләү",
- "exif-copyrighted": "Автор хокуклары халәте:",
- "exif-copyrightowner": "Автор хокуклары иясе",
+ "exif-copyrighted": "Авторлык хокукы халәте",
+ "exif-copyrightowner": "Авторлык хокукы иясе",
"exif-usageterms": "Куллану шартлары",
"exif-orientation-1": "Гадәти",
"exif-orientation-3": "180° ка борылган",
- "exif-componentsconfiguration-0": "юк",
+ "exif-componentsconfiguration-0": "барлыкта юк",
"exif-exposureprogram-0": "Билгесез",
"exif-exposureprogram-1": "Кулдан җайлау режимы",
"exif-exposureprogram-2": "Программалы режим (гади)",
"exif-meteringmode-6": "Өлешләтә",
"exif-meteringmode-255": "Башка",
"exif-lightsource-0": "Билгесез",
+ "exif-lightsource-1": "Көндезге яктылык",
"exif-lightsource-4": "Яктылык",
- "exif-lightsource-9": "Яхшы һава торышы",
+ "exif-lightsource-9": "Аяз",
+ "exif-lightsource-10": "Болытлы",
"exif-lightsource-11": "Күләгә",
"exif-flash-mode-3": "автоматик режим",
"exif-focalplaneresolutionunit-2": "дюйм",
"exif-gpsstatus-a": "Үлчәү тәмамланмаган",
"exif-gpsstatus-v": "Мәгълүматларны җибәрүгә әзер",
"exif-gpsspeed-k": "км/сәг",
- "exif-gpsspeed-m": "милÑ\8f/сәг",
- "exif-gpsspeed-n": "Төен",
+ "exif-gpsspeed-m": "милÑ\8c/сәг",
+ "exif-gpsspeed-n": "узел",
"exif-gpsdestdistance-k": "Километр",
"exif-gpsdestdistance-m": "Миль",
"exif-gpsdestdistance-n": "Диңгез миле",
"exif-gpsdop-fair": "Ярыйсы ($1)",
"exif-gpsdop-poor": "Начар ($1)",
"exif-dc-date": "Дата(лар)",
- "exif-dc-publisher": "Нәшрият",
+ "exif-dc-publisher": "Нәшир",
"exif-dc-relation": "Бәйле медиа",
"exif-dc-rights": "Хокуклар",
"exif-dc-source": "Чыганак медиа",
"exif-dc-type": "Медиа төре",
"exif-rating-rejected": "Кире кагылды",
- "exif-isospeedratings-overflow": "65535-тән күп",
+ "exif-isospeedratings-overflow": "65535 тән күбрәк",
"exif-iimcategory-hth": "Сәламәтлек",
"exif-iimcategory-lab": "Хезмәт",
- "exif-iimcategory-wea": "Һава тырышы",
+ "exif-iimcategory-wea": "Һава торышы",
"exif-urgency-normal": "Гадәти ($1)",
"exif-urgency-low": "Түбән ($1)",
"exif-urgency-high": "Югары ($1)"
"tags-edit-reason": "Syy:",
"tags-edit-revision-submit": "Toteuta muutokset {{PLURAL:$1|tähän versioon|$1 versioon}}",
"tags-edit-logentry-submit": "Lähetä muutoksesi {{PLURAL:$1|tähän lokimerkintään|$1 lokimerkintään}}",
- "tags-edit-success": "Muutokset on tehty.",
+ "tags-edit-success": "Muutokset toteutettiin.",
"tags-edit-failure": "Muutoksia ei voitu toteuttaa: $1",
"tags-edit-nooldid-title": "Kohdeversio ei ole kelvollinen",
"tags-edit-nooldid-text": "Et ole joko määrittänyt sitä kohdeversiota, johon tämä toimenpide kohdistuu, tai sitten määrättyä versiota ei ole olemassa.",
"linkaccounts": "Lier les comptes",
"linkaccounts-success-text": "Le compte a été lié.",
"linkaccounts-submit": "Lier les comptes",
+ "cannotunlink-no-provider-title": "Il n’y a pas de compte lier à délier",
+ "cannotunlink-no-provider": "Il n’y a pas de compte lié qui puisse être délié.",
"unlinkaccounts": "Dissocier les comptes",
"unlinkaccounts-success": "Le compte a été dissocié.",
"authenticationdatachange-ignored": "Les modifications de données d’authentification n’ont pas été gérées. Peut-être aucun fournisseur n’a-t-il été configuré ?",
"pool-queuefull": "Piha ke kiʻo kiū",
"pool-errorunknown": "Hewa ʻikeʻole",
"aboutsite": "No {{SITENAME}}",
- "aboutpage": "Project:No translatewiki.net",
"copyright": "Aia nā mealoko ma lalo o ka laikini $1 inā noka ʻole ia.",
"copyrightpage": "{{ns:project}}:Kūleana kope",
"currentevents": "Nūhou",
"search-interwiki-more": "(עוד)",
"search-interwiki-more-results": "תוצאות נוספות",
"search-relatedarticle": "קשור",
+ "search-invalid-sort-order": "סדר המיון של $1 אינו מוכר, יחול הסדר שמוגדר לפי ברירת המחדל. סדרי המיון התקינים הם: $2",
+ "search-unknown-profile": "פרופיל חיפוש של $1 אינו מוכר, יחול הסדר שמוגדר לפי ברירת המחדל.",
"searchrelated": "קשור",
"searchall": "הכול",
"showingresults": "{{PLURAL:$1|מוצגת תוצאה <strong>אחת</strong>|מוצגות עד <strong>$1</strong> תוצאות}} החל ממספר <strong>$2</strong>:",
"linkaccounts": "קישור חשבונות",
"linkaccounts-success-text": "החשבון קושר.",
"linkaccounts-submit": "קישור החשבונות",
+ "cannotunlink-no-provider-title": "אין חשבונות מקושרים שאפשר לבטל את הקישור שלהם",
+ "cannotunlink-no-provider": "אין חשבונות מקושרים שהקישור שלהם יכול להיות מבוטל.",
"unlinkaccounts": "ביטול הקישור בין חשבונות",
"unlinkaccounts-success": "קישור החשבון בוטל.",
"authenticationdatachange-ignored": "השינוי בנתוני האימות לא הצליח. ייתכן שלא הוגדר ספק.",
"search-result-category-size": "{{PLURAL:$1|անդամ}} ({{PLURAL:$2|ենթակատեգորիա}}, {{PLURAL:$3|նիշք}})",
"search-redirect": "(վերահղում $1 էջից)",
"search-section": "(բաժին $1)",
- "search-category": "(կատեգորիա $1)",
+ "search-category": "(կատեգորիա $1)",
"search-file-match": "(համապատասխանում է նիշքի բովանդակությանը)",
"search-suggest": "Գուցե նկատի ունե՞ք՝ $1",
"search-interwiki-caption": "Կից նախագծեր",
"december-date": "$1 Դեկտեմբեր",
"period-am": "Նախ Կէսօր",
"period-pm": "Կէսօրէն Յետոյ",
- "pagecategories": "{{PLURAL:$1|Կատեգորիա|Կատեգորիաներ}}",
+ "pagecategories": "{{PLURAL:$1|Ստորոգութիւն|Ստորոգութիւններ}}",
"category_header": "«$1» ստորոգութեան մէջ էջեր",
"subcategories": "Ենթակատեգորիաներ",
"category-media-header": "\"$1\" ստորոգութեան հաղորդամիջոց",
"category-empty": "<em>Այս ստորոգութիւնը ներկայիս դատարկ է։<em>",
- "hidden-categories": "{{PLURAL:$1|Թաքցուած կատեգորիա|Թաքցուած կատեգորիաներ}}",
+ "hidden-categories": "{{PLURAL:$1|Թաքուն ստորոգութիւն|Թաքուն ստորոգութիւններ}}",
"hidden-category-category": "Թաքցուած կատեգորիաներ",
"category-subcat-count": "{{PLURAL:$2|Այս կատեգորիան ունի միայն հետեւեալ ենթակատեգորիան։|Այս կատեգորիան ունի հետեւեալ {{PLURAL:$1|ենթակատեգորիա|ենթակատեգորիաներ}}ը՝ ընդհանուր $2էն։}}",
"category-subcat-count-limited": "Այս ստորոգութիւնը ունի հետեւեալ {{PLURAL:$1|ենթաստորոգութիւն|$1 ենթաստորոգութիւններ}}։",
"nstab-mediawiki": "Հաղորդագրութիւն",
"nstab-template": "Կաղապար",
"nstab-help": "Օգնութեան էջ",
- "nstab-category": "Կատեգորիա",
+ "nstab-category": "Ստորոգութիւն",
"mainpage-nstab": "Գլխաւոր Էջ",
"nosuchaction": "Այս գործողութիւնը չկայ",
"nosuchspecialpage": "Այդպիսի յատուկ էջ չկայ",
"search-result-category-size": "{{PLURAL:$1|1 անդամ|$1 անդամներ}} ({{PLURAL:$2|1 ենթաստորոգութիւն|$2 ենթաստորոգութիւններ}}, {{PLURAL:$3|1 նիշք|$3 նիշքեր}})",
"search-redirect": "(Վերայղուած է $1-էն)",
"search-section": "(բաժին $1)",
- "search-category": "(Õ¯Õ¡Õ¿Õ¥Õ£Õ¸Ö\80Õ«Õ¡ $1)",
+ "search-category": "(Õ½Õ¿Õ¸Ö\80Õ¸Õ£Õ¸Ö\82Õ©Õ«Ö\82Õ¶ $1)",
"search-file-match": "(համապատասխան է նիշքի բովանդակութեան)",
"search-suggest": "$1 Նկատի ունի՞ք",
"search-interwiki-default": "$1 արդիւնք.",
"upload-disallowed-here": "Այս նիշքը կարելի չէ ջնջել ու փոխարինել։",
"unusedtemplates": "Չօգտագործուող կաղապարներ",
"randompage": "Պատահական էջ",
+ "randomincategory-category": "Ստորոգութիւն:",
"statistics": "Վիճակագրութիւն",
"statistics-header-pages": "Էջերու վիճակագրութիւն",
"statistics-header-edits": "Խմբագրումներու վիճակագրութիւն",
"withoutinterwiki-submit": "Ցուցնել",
"fewestrevisions": "Նուազ վերաքաղուած էջեր",
"nbytes": "$1 {{PLURAL:$1|պայթ}}",
+ "ncategories": "$1 {{PLURAL:$1|ստորոգութիւն|ստորոգութիւններ}}",
"nmembers": "$1 {{PLURAL:$1|անդամ|անդամներ}}",
"uncategorizedpages": "Առանց ստորոգութիւններու էջեր",
"uncategorizedcategories": "Ենթաստորոգութիւն չունեցող ստորոգութիւններ",
"Pebaryan",
"Veracious",
"Mnam23",
- "Shirayuki"
+ "Shirayuki",
+ "ArlandGa"
]
},
"tog-underline": "Garis bawahi pranala:",
"history": "Riwayat halaman",
"history_short": "Versi terdahulu",
"history_small": "riwayat",
- "updatedmarker": "berubah sejak kunjungan terakhir saya",
+ "updatedmarker": "dimutakhirkan sejak kunjungan terakhir saya",
"printableversion": "Versi cetak",
"permalink": "Pranala permanen",
"print": "Cetak",
"autoblockedtext": "Alamat IP Anda telah terblokir secara otomatis karena digunakan oleh pengguna lain, yang diblokir oleh $1. Pemblokiran dilakukan dengan alasan:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAnda dapat menghubungi $1 atau [[{{MediaWiki:Grouppage-sysop}}|pengurus]] lainnya untuk membicarakan pemblokiran ini.\n\nAnda tidak dapat menggunakan fitur \"{{int:emailuser}}\" kecuali Anda telah memasukkan alamat surel yang sah di [[Special:Preferences|preferensi akun]] Anda dan Anda tidak diblokir untuk menggunakannya.\n\nAlamat IP Anda saat ini adalah $3, dan ID pemblokiran adalah #$5.\nTolong sertakan informasi-informasi ini dalam setiap pertanyaan Anda.",
"systemblockedtext": "Nama pengguna atau alamat IP Anda telah diblokir secara otomatis oleh MediaWiki.\nAlasan yang diberikan adalah:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAlamat IP Anda saat ini adalah $3\nMohon sertakan semua perincian di atas dalam setiap pertanyaan yang Anda ajukan.",
"blockednoreason": "tidak ada alasan yang diberikan",
+ "blockedtext-composite-no-ids": "Alamat IP Anda muncul dalam daftar hitam ganda",
"blockedtext-composite-reason": "Ada pemblokiran berganda terhadap akun Anda dan/atau alamat IP Anda.",
"whitelistedittext": "Anda harus $1 untuk dapat menyunting halaman.",
"confirmedittext": "Anda harus mengkonfirmasikan dulu alamat surel Anda sebelum menyunting halaman.\nHarap masukkan dan validasikan alamat surel Anda melalui [[Special:Preferences|halaman preferensi pengguna]] Anda.",
"mw-widgets-abandonedit-title": "Apakah Anda yakin?",
"mw-widgets-copytextlayout-copy": "Salin",
"mw-widgets-copytextlayout-copy-fail": "Gagal menyalin ke papan klip.",
- "mw-widgets-copytextlayout-copy-success": "Salin ke papan klip.",
+ "mw-widgets-copytextlayout-copy-success": "Disalin ke papan klip.",
"mw-widgets-dateinput-no-date": "Tanggal tidak ada yang terpilih",
"mw-widgets-dateinput-placeholder-day": "TTTT-BB-HH",
"mw-widgets-dateinput-placeholder-month": "TTTT-BB",
"linkaccounts": "Tautkan akun",
"linkaccounts-success-text": "Akun telah ditautkan.",
"linkaccounts-submit": "Tautkan akun",
+ "cannotunlink-no-provider": "Tidak ada akun yang tertaut yang dapat dibatalkan tautannya.",
"unlinkaccounts": "Lepastautkan akun",
"unlinkaccounts-success": "Akun berikut telah dilepastautkan.",
"authenticationdatachange-ignored": "Otentikasi perubahan data tidak dijalankan. Mungkin tidak ada provider yang diatur?",
"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",
"thumbnail-more": "Plugrandigar",
"thumbnail_error": "Ne sucesas krear imajeto: $1",
"import": "Importacar pagini",
+ "import-upload-filename": "Nomo dil arkivo:",
"import-comment": "Komento:",
"importtext": "Voluntez exportacar l' arkivo de la fonto-wikio per [[Special:Export|exportacilo]]. Registragar ol a vua komputero ed adkargar ol hike.",
"importfailed": "La importaco faliis: $1",
"redirect-page": "Identigo di la pagino",
"redirect-revision": "Revizo di la pagino",
"redirect-file": "Arkivo-nomo",
+ "fileduplicatesearch-filename": "Nomo dil arkivo:",
"fileduplicatesearch-submit": "Serchar",
"specialpages": "Specala pagini",
"specialpages-group-maintenance": "Raporti pri manteno",
"specialmute-error-invalid-user": "요청한 사용자 이름을 찾을 수 없습니다.",
"specialmute-email-footer": "{{BIDI:$2}}의 이메일 환경 설정을 관리하려면 <$1>을(를) 방문해 주십시오.",
"specialmute-login-required": "알림 미표시 환경 설정을 변경하려면 로그인해 주십시오.",
+ "mute-preferences": "알림 미표시 환경 설정",
"revid": "$1 판",
"pageid": "페이지 ID $1",
"interfaceadmin-info": "$1\n\n사이트 전체에 쓰이는 CSS/JS/JSON 파일의 편집 권한이 최근 <code>editinterface</code> 권한에서 분리되었습니다. 왜 이 오류가 발생하는지 이해가 되지 않는다면, [[mw:MediaWiki_1.32/interface-admin]]을 참고하십시오.",
"printableversion": "Verscion da stanpâ",
"permalink": "Ingancio fisso",
"print": "Stampa",
- "view": "Visualizza",
+ "view": "Vixoalìzza",
"view-foreign": "Véddi in sce $1",
"edit": "Modìfica",
"edit-local": "Modifica descrission locale",
"page_first": "prìmma",
"page_last": "ùrtima",
"histlegend": "Confronto tra verscioîn: selession-a e cascette corispondenti a-e verscioîn dexidiæ e schissa Invio oppû o pomello da basso.\n\nLegenda: (corr) = differense co-a verscion corrente, (prec) = differense co-a verscion precedente, '''m''' = modiffica minô",
- "history-fieldset-title": "Çerca de verscioin",
+ "history-fieldset-title": "Çerca e verscioin",
"history-show-deleted": "Solo verscioin scassæ",
"histfirst": "prìmma",
"histlast": "ùrtima",
"recentchanges-page-removed-from-category": "[[:$1]] rimosso da-a categoria",
"recentchanges-page-removed-from-category-bundled": "[[:$1]] rimossa da-a categoria, [[Special:WhatLinksHere/$1|questa pagina a l'è inclusa a l'interno di atre pagine]]",
"autochange-username": "Modiffica aotomattica MediaWiki",
- "upload": "Carrega 'n file",
+ "upload": "Càrega 'n file",
"uploadbtn": "Carreghilo",
"reuploaddesc": "Torna a-o moddulo pe-o caregamento.",
"upload-tryagain": "Invia a descrission do file modificou",
"imagelinks": "Utilìzzo do file",
"linkstoimage": "{{PLURAL:$1|A segoente pàgina a contegne|E segoenti $1 pàgine contegnan}} colegaménti a-o file:",
"linkstoimage-more": "Ciù de $1 {{PLURAL:$1|pagina aponta|pagine apontan}} a questo file.\nA seguente lista a mostra {{PLURAL:$1|a primma paggina ch'a l'aponta|e primme $1 paggine ch'apontan}} a sto file.\nL'è disponibile un [[Special:WhatLinksHere/$2|elenco completo]].",
- "nolinkstoimage": "No gh'è nisciun-a paggina inganciâ a sto file.",
+ "nolinkstoimage": "No gh'è nisciun-a pàgina ch'a contegne sto file.",
"morelinkstoimage": "Vixualizza [[Special:WhatLinksHere/$1|di atri inganci]] a questo file.",
"linkstoimage-redirect": "$1 (rendriççamento file) $2",
"duplicatesoffile": "{{PLURAL:$1|O seguente file o l'è un dupricou|I seguenti $1 file son di dupricæ}} de questo file ([[Special:FileDuplicateSearch/$2|urteioî detaggi]]):",
"tooltip-t-contributions": "Lista de contribûssioin de {{GENDER:$1|questo|questa}} utente",
"tooltip-t-emailuser": "Invia un messaggio email a {{GENDER:$1|questo|questa}} utente",
"tooltip-t-info": "Urteioî informaçioin insce questa pagina",
- "tooltip-t-upload": "Carrega di file murtimediali",
+ "tooltip-t-upload": "Càrega di files",
"tooltip-t-specialpages": "Lista de tùtte e pagine speçiâli",
"tooltip-t-print": "Verscion stanpabbile de sta paggina",
"tooltip-t-permalink": "Colegaménto fisso a sta revixión da pàgina",
"expansion-depth-exceeded-category-desc": "زؽر دٱسٱ سی بٱلگٱیایؽ کاْ د ڤنو پی یا ڤلٱ بیئن فرٱ پیشکرد کردٱ.",
"expansion-depth-exceeded-warning": "بٱلگٱ د پی یا ڤلٱ بیئن پیشکرد کرد",
"parser-unstrip-loop-warning": "گردۊلٱ د فرمونٱ Unstrip پاٛدا بیٱ",
- "unstrip-depth-warning": "د بؽشترÙ\88Ù\86Ù± د سرÚ\86Ø´Ù\85Ù± رٱتÙ\86 د دٱسدÛ\8aر Unstrip ڤارÛ\89تر رٱتؽتٱ($1)",
+ "unstrip-depth-warning": "د بؽشترÙ\88Ù\86Ù± د سرÚ\86Ø´Ù\85Ù± رٱتÙ\86 د دٱسدÛ\8aر Unstrip ڤارÛ\8fتر رٱتؽتٱ($1)",
"converter-manual-rule-error": "خٱتا د قانۊن ڤالٛرشتن دٱسی زڤوݩ",
"undo-success": "نمۊئٱ ڤیرایش ناْ ٱنجومشیو بٱکؽت.\nلوتفٱن اؽ فٱرخؽ کاْ ها د هار ناْ ڤارسی بٱکؽت تا یاٛ کارؽ کاْ مؽهایت ٱنجوم باٛیؽت،ۉ اۊساْ آلشتؽا هار ناْآمادٱ بٱکؽت سی یٱ کاْ خونسا کردن ڤیرایش ناْ ٱنجوم باٛیؽت.",
"undo-failure": "سی ری ڤ ری بیئن اؽ ڤیرایش ڤا ڤیرایشؽا مؽنجایی، نمۊئٱ اؽ ڤیرایش ناْ خونسا بٱکؽت.",
"difference-missing-revision": "{{PLURAL:$2|یاٛ ڤیرایش|$2 ڤیرایش}} د فٱرخ مؽنجا($1) {{PLURAL:$2|پاٛدا ناٛی|پاٛدا ناٛییٱ}}.\n\nشایٱد بانی جاڤٱنٱ ڤٱ ڤا یاٛ ڤیرگار ڤٱ هٱنگوم ناٛییٱ کاْ د یاٛ بٱلگٱ پاکسا بیٱ هوم پاٛڤٱن باٛئٱ بۊئٱ.\nشایٱد جۏزییات د [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log] پاٛدا مۊئٱن.",
"searchresults": "نتيجٱیا پاٛ جۊری",
"searchresults-title": "نٱتيجٱیا پاٛ جۊری سی \"$1\"",
- "titlematches": "داسون بلگه یکی بیه",
- "textmatches": "هومسازی نیسسٱ بلگٱ.",
+ "titlematches": "داسوݩ بٱلگٱ یٱکؽ بیٱ",
+ "textmatches": "Ù\87Ù\88Ù\85سازÛ\8c Ù\86Û\8cسسٱ بٱÙ\84Ú¯Ù±.",
"notextmatches": "نیسسٱ بٱلگٱ هومسازی نارٱ",
"prevn": "ڤادما {{PLURAL:$1|$1}}",
"nextn": "نوئایی {{PLURAL:$1|$1}}",
"next-page": "بٱلگٱ نوئایی",
"prevn-title": "زیتر $1 {{PLURAL:$1|نٱتیجٱ|نٱتيجٱيا}}",
"nextn-title": "دمایی $1 {{PLURAL:$1|نٱتيجٱ|نٱتيجؽا}}",
- "shown-title": "نشوݩ داٛین $1 {{PLURAL:$1|نتیجٱ|نتیجٱیا}} سی هٱر بٱلگٱ",
+ "shown-title": "نشوݩ داٛئن $1 {{PLURAL:$1|نتیجٱ|نتیجٱیا}} سی هٱر بٱلگٱ",
"viewprevnext": "ديئن ($1 {{int:pipe-separator}} $2) ($3)",
"searchmenu-exists": "'''ایچاْ بٱلگاٛیؽ هؽ ڤ نوم\"[[:$1]]\" کاْ ها د اؽ ڤیکی'''",
"searchmenu-new": "'''اؽ بٱلگٱ ناْ دۏرس كو \"[[:$1]]\" د اؽ ڤیکی!'''",
"searchprofile-articles": "بٱلگٱيا مؽنونٱ دار",
- "searchprofile-images": "ڤارسگرؽا خلکمٱن",
- "searchprofile-everything": "همٱ چی",
+ "searchprofile-images": "ڤارسگرؽا خٱÙ\84Ú©Ù\85Ù±Ù\86",
+ "searchprofile-everything": "Ù\87Ù±Ù\85Ù± Ú\86Û\8c",
"searchprofile-advanced": "پیشکردٱ",
"searchprofile-articles-tooltip": "بٱگٱرد مؽن $1",
- "searchprofile-images-tooltip": "جانؽایا ناْ پاٛ جۊری کو",
- "searchprofile-everything-tooltip": "همٱ مؽنونٱیا ناْ پاٛ جۊری كو (شامل بٱلگٱيا چٱک چنٱ)",
+ "searchprofile-images-tooltip": "جانؽایا ناْ پاٛجۊری کو",
+ "searchprofile-everything-tooltip": "همٱ مؽنونٱیا ناْ پاٛجۊری كو (شامل بٱلگٱيا چٱک چنٱ)",
"searchprofile-advanced-tooltip": "نوم جايا نوم دؽار بٱگٱرد",
"search-result-size": "$1 ({{PLURAL:$2|1 کلٱمٱ|$2 کلٱمٱیا}})",
"search-result-category-size": "{{PLURAL:$1|1 ٱندوم|$1 ٱندومؽا}} ({{PLURAL:$2|1 زؽردٱسٱ|$2 زؽردٱسٱیا}}، {{PLURAL:$3|1 جانؽا|$3 جانؽایا}}",
"search-redirect": "(ڤرگٱشتن سی $1)",
"search-section": "(بٱئرجا $1)",
- "search-category": "(دسه $1)",
+ "search-category": "(دٱسٱ $1)",
"search-file-match": "(یٱکؽ کردن مؽنونٱ جانؽا)",
"search-suggest": "مٱنزۊرت يٱ بی:$1",
- "search-rewritten": "Ù\86ئشÙ\88Ù\99 دأئÙ\86 Ù\86أتÛ\8cجÛ\95 Û\8cا سÛ\8c $1. سÛ\8c Ù\86ئÙ\85Ù\88Ù\99Ù\86Û\95 بأگأردÛ\8cت سی $2.",
+ "search-rewritten": "Ù\86Ø´Ù\88Ý© داÙ\9bئÙ\86 Ù\86ٱتÛ\8cجٱÛ\8cا سÛ\8c $1. سÛ\8c Ù\86Ù\85Û\8aÙ\86Ù± بٱگٱردؽت سی $2.",
"search-interwiki-caption": "پروجه یا خوئر",
- "search-interwiki-default": "$1 نتیجه یا:",
- "search-interwiki-more": "(بیشتر)",
- "search-relatedarticle": "مرتوط",
- "searchrelated": "مرتوط",
- "searchall": "همٱ",
+ "search-interwiki-default": "$1 نٱتیجٱیا:",
+ "search-interwiki-more": "(بؽشتر)",
+ "search-relatedarticle": "مورتٱبت",
+ "searchrelated": "مورتٱبت",
+ "searchall": "Ù\87Ù±Ù\85Ù±",
"showingresults": "نمایش بؽشترونٱ {{PLURAL:$1|'''۱''' نتیجٱ|'''$1''' نتیجٱ}} د هار، شرۊ د شمارٱ'''$2'''.",
- "showingresultsinrange": "نمایش بؽشترونٱ {{PLURAL:$1|'''۱''' نتیجٱ|'''$1''' نتیجٱ}} د هار، شرۊ د شمارٱ'''$2''' تا شمارٱ '''$3'''.",
+ "showingresultsinrange": "نمایش بؽشترونٱ {{PLURAL:$1|'''۱''' نٱتیجٱ|'''$1''' نٱتیجٱ}} د هار، شرۊ د شمارٱ'''$2''' تا شمارٱ '''$3'''.",
"search-showingresults": "{{PLURAL:$4|نٱتیجٱیا<strong>$1</strong> د <strong>$3</strong>|نٱتیجٱیا<strong>$1 - $2</strong د <strong>$3</strong>}}",
- "search-nonefound": "هیچ نتیجاٛیؽ ڤا پاٛجۊری تو یٱکؽ نؽ.",
- "powersearch-legend": "پی جوری پیشکرده",
- "powersearch-ns": "د نوم جايا نوم ديار پی جوری بک:",
- "powersearch-togglelabel": "ڤارئسÛ\8c کئردئن:",
- "powersearch-toggleall": "هأمە",
- "powersearch-togglenone": "هيش كوم",
- "powersearch-remember": "د ویر داشتن انتخاو سی پی جوریا نهایی",
- "search-external": "پی جوری د در",
- "searchdisabled": "Ù\85ئÙ\86 جÙ\88رÛ\8c د {{SITENAME}} Ú©Ù\86شتگر Ù\86ئ.\nÙ\85Ù\88Ù\82تاÙ\8b Ù\85Û\8c تÙ\88Ù\86Û\8cت Ù\85ئÙ\86 جÙ\88رÛ\8c Google Ù\86Ù\87 بÙ\88Ù\86Û\8cت Ù\88Ù\87 کار.\nد Ù\88Û\8cرتÙ\88 با Ú©Ù\87 Ù\86تÛ\8cجÙ\87 Û\8cاÛ\8cÛ\8c Ú©Ù\87 د Ù\85ئÙ\86 جÙ\88رÛ\8c Ù\88ا اÙ\88 رÙ\88شت Ù\88Ù\87 دست Ù\85Û\8cاÙ\86 شاÛ\8cت Ù\88Ù\87 رÙ\88ز Ù\86بان.",
- "search-error": "یه گل خطا سی اوسنی که پی جوری می کردیت اتفاق افتائه:$1",
- "preferences": "Ø®Ù\88صÙ\88Ù\99Û\8cات Ù\87Ø£نی",
+ "search-nonefound": "هیچ نٱتیجاٛیؽ ڤا پاٛجۊری تو یٱکؽ نؽ.",
+ "powersearch-legend": "پاٛجۊری پیشکردٱ",
+ "powersearch-ns": "د نوم جايا نوم دؽار پاٛجۊری کو:",
+ "powersearch-togglelabel": "ڤارسÛ\8c کردن:",
+ "powersearch-toggleall": "هٱمٱ",
+ "powersearch-togglenone": "هیچ کوم",
+ "powersearch-remember": "د ڤیر داشتن اْنتخاب سی پاٛجۊرؽا نهایی",
+ "search-external": "پاٛجۊری د دٱر",
+ "searchdisabled": "Ù\85ؽÙ\86 جÛ\8aرÛ\8c د {{SITENAME}} Ú©Ù\86شگٱر Ù\86ؽ.\nÙ\85Ù\88Ú¤Ù±Ù\82ٱتٱÙ\86 Ù\85ؽتÙ\88Ù\86ؽت Ù\85ؽÙ\86 جÛ\8aرÛ\8c Google Ù\86اÙ\92 بÙ\88Ù\86ؽت Ú¤ کار.\nد Ú¤Û\8cرتÙ\88 با کاÙ\92 Ù\86ٱتÛ\8cجٱÛ\8cاÛ\8cؽ کاÙ\92 د Ù\85ؽÙ\86 جÙ\88Ù¾Û\8aرÛ\8c ڤا اÙ\88 رٱڤش Ú¤ دٱس Ù\85ؽاÙ\86 شاÛ\8cٱد Ú¤ رÛ\8aز Ù\86Û\8aئٱن.",
+ "search-error": "یاٛ خٱتا سی اۊسنؽ کاْ پاٛجۊری مؽ کردؽت اْتفاق اوفتایٱ:$1",
+ "preferences": "Ø®Ù\88سÛ\8aسÛ\8cات Ù\87نی",
"mypreferences": "چیا هنی",
- "prefs-edits": "Ø´Ù\88Ù\85ارÛ\95 Ú¤Û\8cراÛ\8cئشتÛ\8cا:",
- "prefsnologintext2": "لطف بکیت بیایت وامین و ترجیحات خوتونه آلشت بئیت.",
- "prefs-skin": "پوس",
- "skin-preview": "Ù¾Û\8cØ´ سئÛ\8cÙ\84",
- "datedefault": "هیچ ترجیحات دش نئ",
- "prefs-labs": "گزینشتیا ازماشتی",
- "prefs-user-pages": "بألگە کاریار",
- "prefs-personal": "جانیاگە کاریار",
- "prefs-rc": "Ø¢Ù\84ئشتÛ\8cا ئÛ\8cسئنی",
- "prefs-watchlist": "سئÛ\8cÙ\84 بأرگ",
- "prefs-editwatchlist": "Ú¤Û\8cراÛ\8cئشت سئÛ\8cÙ\84 بأرگ",
- "prefs-editwatchlist-label": "دادÙ\87 Û\8cا Ù\86Ù\87 د سÛ\8cÙ\84 برگ Ø®Ù\88تÙ\88 Ù\88Û\8cراÛ\8cشت بکÛ\8cت:",
- "prefs-editwatchlist-edit": "داسÙ\88Ù\86اÙ\86Ù\87 سÛ\8cÙ\84 بکÛ\8cت Ù\88 Ù\88Ù\86Ù\88Ù\86Ù\87 د سÛ\8cÙ\84 برگ Ø®Ù\88تÙ\88 Ù\88ردارÛ\8cت",
- "prefs-editwatchlist-raw": "Ú¤Û\8cراÛ\8cئشتکارÛ\8c رأدÛ\8cÙ\81Û\8c سئÛ\8cÙ\84 بأرگ",
- "prefs-editwatchlist-clear": "سئÛ\8cÙ\84 بأرگئ تÙ\88Ù\99Ù\86Û\95 پاک بأکÛ\8cت",
- "prefs-watchlist-days": "روزیا نه د سیل برگ نشو دئه بو:",
- "prefs-watchlist-days-max": "$1 بیشترونه {{PLURAL:$1|روز|روزیا}}",
+ "prefs-edits": "Ø´Ù\85ارٱ Ú¤Û\8cراÛ\8cشؽا:",
+ "prefsnologintext2": "لوتف بٱکؽت بؽایؽت ڤامؽن ۉ تٱرجیهؽا خوتو ناْ آلشت بٱکؽت.",
+ "prefs-skin": "پۊس",
+ "skin-preview": "Ù¾Û\8cØ´ ساÙ\9bÙ\84Ù\9b",
+ "datedefault": "هیچ تٱرجیهاتؽ دش نؽ",
+ "prefs-labs": "گوزینشؽا آزمایشی",
+ "prefs-user-pages": "بٱلگٱ کاریار",
+ "prefs-personal": "جانؽاگٱ کاریار",
+ "prefs-rc": "Ø¢Ù\84شتؽا اÛ\8cسنی",
+ "prefs-watchlist": "ساÙ\9bÙ\84Ù\9b بٱرگ",
+ "prefs-editwatchlist": "Ú¤Û\8cراÛ\8cØ´ ساÙ\9bÙ\84Ù\9b بٱرگ",
+ "prefs-editwatchlist-label": "دادٱÛ\8cا Ù\86اÙ\92 د ساÙ\9bÙ\84Ù\9b بٱرگ Ø®Ù\88تÙ\88 Ú¤Û\8cراÛ\8cØ´ بٱکؽت:",
+ "prefs-editwatchlist-edit": "داسÙ\88Ù\86ؽا Ù\86اÙ\92 ساÙ\9bÙ\84Ù\9b بٱکؽت Û\89 Ú¤Ù\86Ù\88 Ù\86اÙ\92 د ساÙ\9bÙ\84Ù\9b بٱرگ Ø®Ù\88تÙ\88 ڤردارؽت",
+ "prefs-editwatchlist-raw": "Ú¤Û\8cراÛ\8cشتکارÛ\8c ردÛ\8cÙ\81Û\8c ساÙ\9bÙ\84Ù\9b بٱرگ",
+ "prefs-editwatchlist-clear": "ساÙ\9bÙ\84Ù\9b بٱرگ تÙ\88 Ù\86اÙ\92 پاک بٱکؽت",
+ "prefs-watchlist-days": "رۊزؽا د ساٛلٛ بٱرگ نشوݩ داٛئٱ بۊئٱن:",
+ "prefs-watchlist-days-max": "$1 بؽشترونٱ {{PLURAL:$1|رۊز|رۊزؽا}}",
"prefs-watchlist-edits": "بیشترونه انازه آلشتیایی که د سیل برگ گپ بیه نشو دئه بیه:",
- "prefs-watchlist-edits-max": "Ø´Ù\85ارÙ\87 بÛ\8cشترÙ\88Ù\86Ù\87:1000",
- "prefs-watchlist-token": "Ù\86Ø´Ù\88Ù\86Ù\87 سÛ\8cÙ\84 برگ:",
- "prefs-misc": "شیڤئسئن",
- "prefs-resetpass": "رازÛ\8cÙ\86Ù\87 Ú¯Ù\88اردÙ\86 Ù\86Ù\87 Ø¢Ù\84شت بÙ\83Ù\8aت",
- "prefs-changeemail": "تÛ\8cرÙ\86ئشÙ\88Ù\99Ù\86 Ø£Ù\86جÙ\88Ù\85اÙ\86اÙ\85Û\95 تÙ\88Ù\99Ù\86Û\95 Ø¢Ù\84ئشت بأکÛ\8cت",
- "prefs-setemail": "يه گل انجومانامه بنیت",
- "prefs-email": "Ú\86Û\8cا Ù\87Ù\86Û\8c اÙ\86جÙ\88Ù\85اÙ\86اÙ\85Ù\87",
- "prefs-rendering": "شیڤە",
- "saveprefs": "ئÙ\85اÛ\8cÛ\95 کئردئن",
- "restoreprefs": "د نو زنه کردن همه میزونکاریا پیش فرض(د همه جایا)",
- "prefs-editing": "د حال و بال ڤیرایئشت",
- "searchresultshead": "پئÛ\8c جÙ\88Ù\99ری",
+ "prefs-watchlist-edits-max": "Ø´Ù\85ارٱ بؽشترÙ\88Ù\86Ù±:1000",
+ "prefs-watchlist-token": "Ù\86Ø´Ù\88Ù\86Ù± ساÙ\9bÙ\84Ù\9b بٱرگ:",
+ "prefs-misc": "شؽڤسن",
+ "prefs-resetpass": "رازÛ\8cÙ\86Ù± Ú¯Ù\88ئارسÙ\86 Ù\86اÙ\92 Ø¢Ù\84شت بٱکؽت",
+ "prefs-changeemail": "تÛ\8cرÙ\86Ø´Ù\88Ý© Ù±Ù\86جÙ\88Ù\85اÙ\86اÙ\85Ù± تÙ\88 Ù\86اÙ\92 Ø¢Ù\84شت بٱکؽت",
+ "prefs-setemail": "یاٛ ٱنجومانامٱ بٱنؽت",
+ "prefs-email": "Ú\86Û\8cا Ù\87Ù\86Û\8c اÙ\86جÙ\88Ù\85اÙ\86اÙ\85Ù±",
+ "prefs-rendering": "شؽڤٱ",
+ "saveprefs": "Ø¢Ù\85ادٱ کردن",
+ "restoreprefs": "د نۊ زنٱ کردن هٱمٱ میزونکاریا پیش فٱرز(د هٱمٱ جایا)",
+ "prefs-editing": "د هال ۉ بال ڤیرایش",
+ "searchresultshead": "پاÙ\9bجÛ\8aری",
"stub-threshold": "آستوٙنە ڤیرایئشتکاریا د یأک دیسئسە <a href=\"#\" class=\"stub\">ناقئص</a> (بایت):",
- "stub-threshold-sample-link": "نئموٙنە",
- "stub-threshold-disabled": "Ù\86اکÙ\88Ù\86ئشتگأر بÛ\8cÛ\8cÛ\95",
- "recentchangesdays": "روزیا آلشتیا تازه باو نه نشو بیه:",
- "recentchangesdays-max": "$1 بیشترونه {{PLURAL:$1|روز|روزیا}}",
+ "stub-threshold-sample-link": "نمۊنٱ",
+ "stub-threshold-disabled": "Ù\86اکÙ\86شگٱر بÛ\8cÙ±",
+ "recentchangesdays": "رۊزؽا آلشتیا تازٱ بۊ ناْ نشوݩ باٛیٱ:",
+ "recentchangesdays-max": "$1 بؽشترونٱ {{PLURAL:$1|رۊز|رۊزؽا}}",
"recentchangescount": "انازه ویرایشتیایی که دیاری می که:",
"prefs-help-recentchangescount": "یه شامل آلشتیا تازه،ویرگاریا بلگه و پهرستنومه یا هئ.",
"prefs-help-watchlist-token2": "یه یه گل کلیت رازینه دار سی خوارک تیارگه سیل برگه شمانه.\nهر کسی که شما مئشناسیت می تونه سیل برگ شما نه بوحونه،په ونه هومبئری نکیت.[[Special:ResetTokens|ار لازمه ونه آلشت بئیت ایچه نه بپورنیت]].",
- "savedprefs": "ویجه گیا هنی تو اماییه بیه.",
- "timezonelegend": "گات راساگÙ\87",
- "localtime": "گات ولاتی:",
- "timezoneuseserverdefault": "ویکی پیش فرض($1) وه کار بونیت",
+ "savedprefs": "ویژگیا هنی تو آمادٱ بیٱ.",
+ "timezonelegend": "گات راساگٱ:",
+ "localtime": "گات ڤلاتی:",
+ "timezoneuseserverdefault": "ڤیکی پیش فٱرز($1) ڤ کار بونؽت",
"timezoneuseoffset": "هنی",
- "servertime": "گات رسینه جا:",
- "guesstimezone": "وا جاگرد پر بوئه",
- "timezoneregion-africa": "اÙ\81رقا",
- "timezoneregion-america": "اÙ\85رÙ\83ا",
- "timezoneregion-antarctica": "قطو هار ونه",
- "timezoneregion-arctic": "قطو شمال",
- "timezoneregion-asia": "آسيا",
- "timezoneregion-atlantic": "جهون آو آتلانتیک",
- "timezoneregion-australia": "استراليا",
- "timezoneregion-europe": "اوروپا",
- "timezoneregion-indian": "جهوناو هند",
- "timezoneregion-pacific": "جهوناو آروم",
+ "servertime": "گات رٱسینٱجا:",
+ "guesstimezone": "ڤا جاگرد پور بۊئٱ",
+ "timezoneregion-africa": "اÙ\92Ù\81رÛ\8cقا",
+ "timezoneregion-america": "اÙ\92Ù\85رÛ\8cÚ©ا",
+ "timezoneregion-antarctica": "قوتب هارگٱ",
+ "timezoneregion-arctic": "قوتب شمال",
+ "timezoneregion-asia": "آسؽا",
+ "timezoneregion-atlantic": "جهوݩ آو آتلانتیک",
+ "timezoneregion-australia": "اوسترالٛیا",
+ "timezoneregion-europe": "اورۊپا",
+ "timezoneregion-indian": "جهوݩ آو هند",
+ "timezoneregion-pacific": "جهوݩ آو آروم",
"allowemail": "انجومانامه نه سی کاریاریا هنی کنشتگر کو",
- "prefs-searchoptions": "پئÛ\8c جÙ\88Ù\99ری",
+ "prefs-searchoptions": "پاÙ\9bجÛ\8aری",
"prefs-namespaces": "نوم جایا",
- "default": "پيش فرض",
- "prefs-files": "جانیایا",
- "prefs-custom-css": "سی اس اس جاافتائه",
- "prefs-custom-js": "جاوا نیسسه جاافتائه",
+ "default": "پيش فٱرز",
+ "prefs-files": "جانؽایا",
+ "prefs-custom-css": "سی اْس اْس جا اوفتایٱ",
+ "prefs-custom-js": "جاڤا نیسسٱ جا اوفتایٱ",
"prefs-common-config": " سی اس اس/جاوا اسکریپت بهر بیه سی همه پوسه یا:",
- "prefs-reset-intro": "شما می تونیت ای بلگه سی د نو زنه کردن ترجیحات خوت وه شکل تیارگه پیش فرض وه کار بوونیت.\nیه ورئشت پذیر نئ.",
- "prefs-emailconfirm-label": "پش راست کردن انجومانامه:",
- "youremail": "أنجومانامە:",
+ "prefs-reset-intro": "شما مؽ تونؽت اؽ بٱلگٱ ناْ سی د نۊ زنٱ کردن تٱرجیهؽا خوت ڤ شکل تیارگٱ پیش فٱرز ڤ کار بونؽت.\nیٱ ورگٱشت پٱزیر نؽ.",
+ "prefs-emailconfirm-label": "تٱیید کردن ٱنجومانامٱ:",
+ "youremail": "ٱنجومانامٱ:",
"username": "{{GENDER:$1|نوم کاریاری}}:",
- "prefs-memberingroups": "{{GENDER:$2|أندوم}} {{PLURAL:$1|دأسە|دأسە یا}}:",
+ "prefs-memberingroups": "{{GENDER:$2|ٱندوم}} {{PLURAL:$1|دٱسٱ|دٱسٱیا}}:",
"prefs-memberingroups-type": "$1",
- "prefs-registration": "گات Ø«Ù\88ت Ù\86ام:",
+ "prefs-registration": "گات سٱبت Ù\86Ù\88م:",
"prefs-registration-date-time": "$1",
- "yourrealname": "نوم راستكی:",
- "yourlanguage": "زوٙن:",
+ "yourrealname": "نوم راسی:",
+ "yourlanguage": "زڤوݩ:",
"yourvariant": "مینونٱ آلشتگٱر زڤوݩ:",
- "prefs-help-variant": "قسٱ ڤری اْنتخاویی شما سی نمایش مینونٱ بٱلگٱیا د اؽ ڤیکی.",
- "yournick": "اÙ\85ضا تازÙ\87:",
- "prefs-help-signature": "ویر و باوریا نیسسه بیه د بلگه چک چنه باید وا«<nowiki>~~~~</nowiki>» امضا بان؛ ای نشون وه شکل خودانجومی وه امضا شما و مؤر ویرگار تبدیل بوئه.",
+ "prefs-help-variant": "قسٱ ڤری اْنتخابی شما سی نمایش مؽنونٱ بٱلگٱیا د اؽ ڤیکی.",
+ "yournick": "اÙ\92Ù\85زا تازٱ:",
+ "prefs-help-signature": "ڤویر ۉ باڤٱرؽا نیسسٱ بیٱ د بٱلگٱ چٱک چنٱ بایٱد ڤا«<nowiki>~~~~</nowiki>» اْمزا بۊئٱن؛ اؽ نشوݩ ڤ شکل خودٱنجومؽ ڤ اْمزا شما ۉ مۉئر ڤیرگار تٱبدیلٛ مۊئٱ.",
"badsig": "ئمضا خوم بی ئتئڤار.\nسأردیسیا ئچ تی ئم ئل نە ڤارئسی بأکیت.",
"badsiglength": "امضا شما فره گپه.\nدرازا امضا باید کمتر د $1 {{PLURAL:$1|نیسه}} بوئه.",
"yourgender": "شما بیشتر میهایت که چه جوری گوته بوئه؟",
"protectedpages-timestamp": "Laiko žyma",
"protectedpages-page": "Puslapis",
"protectedpages-expiry": "Galioja iki",
- "protectedpages-performer": "Užrakinantis naudotojas",
+ "protectedpages-performer": "Užrakinęs naudotojas",
"protectedpages-params": "Užrakinimo nuostatos",
"protectedpages-reason": "Priežastis",
"protectedpages-submit": "Rodyti puslapius",
"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'''.",
"rcfilters-clear-all-filters": "Title for the button that clears all filters",
"rcfilters-show-new-changes": "Label for the button to show new changes. Parameters:\n* $1 - timestamp from which new changes are available. It indicates that clicking the refresh link will bring changes newer than (or equal to) this timestamp. It is formatted according to the user's date, time and timezone preferences",
"rcfilters-search-placeholder": "Placeholder for the filter search input. The first \"Filter\" is a verb, and the second \"filter\" is a noun.",
+ "rcfilters-search-placeholder-mobile": "Placeholder for the filter search input for mobile devices.",
"rcfilters-invalid-filter": "A label for an invalid filter.",
"rcfilters-empty-filter": "Placeholder for the filter list when no filters were chosen.",
"rcfilters-filterlist-title": "Title for the filters list.\n{{Identical|Filter}}",
"block-log-flags-angry-autoblock": "Used as a block log flag in [[Special:Log/block]].\n{{Related|Block-log-flags}}",
"block-log-flags-hiddenname": "Used as a block log flag in [[Special:Log/block]] and in [[Special:Block]].\n\n{{Related|Block-log-flags}}",
"range_block_disabled": "Used as error message in [[Special:Block]].\n\nSee also:\n* {{msg-mw|Range block disabled}}\n* {{msg-mw|Ip range invalid}}\n* {{msg-mw|Ip range toolarge}}",
+ "ipb-prevent-user-talk-edit": "Used as error message in [[Special:Block]] if invalid options are selected regarding \"Edit own user talk\".\n\nSee also:\n{{msg-mw|ipb-disableusertalk}}",
"ipb_expiry_invalid": "Used as error message in [[Special:Block]].",
"ipb_expiry_old": "Used as error message in [[Special:Block]], if the expiry time is in the past.\n{{Identical|protect_expiry_old}}",
"ipb_expiry_temp": "Warning message displayed on [[Special:Block]] if the option \"hide username\" is selected but the expiry time is not infinite.",
"linkaccounts": "Title of the special page [[Special:LinkAccounts]] which allows the user to connect the local user accounts with external ones such as Google or Facebook.",
"linkaccounts-success-text": "Text shown on top of the form after a successful action.",
"linkaccounts-submit": "Text of the main submit button on [[Special:LinkAccounts]] (when there is one)",
+ "cannotunlink-no-provider-title": "Error page title shown when the user visits [[Special:UnlinkAccounts]] but there is no external account that could be unlinked.",
+ "cannotunlink-no-provider": "Error message shown when the user visits [[Special:UnlinkAccounts]] but there is no external account that could be unlinked.",
"unlinkaccounts": "Title of the special page [[Special:UnlinkAccounts]] which allows the user to remove linked remote accounts.",
"unlinkaccounts-success": "Account unlinking form success message",
"authenticationdatachange-ignored": "Shown when authentication data change was unsuccessful due to configuration problems.\n\nCf. e.g. {{msg-mw|Passwordreset-ignored}}.",
/* 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' => [
'styles/mw.rcfilters.ui.RclToOrFromWidget.less',
'styles/mw.rcfilters.ui.RclTargetPageWidget.less',
'styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less',
+ 'styles/mw.rcfilters.ui.FilterTagMultiselectWidgetMobile.less'
],
'skinStyles' => [
'vector' => [
'rcfilters-clear-all-filters',
'rcfilters-show-new-changes',
'rcfilters-search-placeholder',
+ 'rcfilters-search-placeholder-mobile',
'rcfilters-invalid-filter',
'rcfilters-empty-filter',
'rcfilters-filterlist-title',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-layout',
'oojs-ui.styles.icons-media',
+ 'oojs-ui-windows.icons'
],
],
'mediawiki.interface.helpers.styles' => [
background-image: e( '/* @embed */' ) url( @url );
}
-.vertical-gradient( @startColor: gray, @endColor: white, @startPos: 0, @endPos: 100% ) {
- background-color: @endColor;
- background-image: -webkit-linear-gradient( top, @startColor @startPos, @endColor @endPos ); // Safari 5.1+, Chrome 10+
- background-image: -moz-linear-gradient( top, @startColor @startPos, @endColor @endPos ); // Firefox 3.6+
- background-image: linear-gradient( @startColor @startPos, @endColor @endPos ); // Standard
+.horizontal-gradient( @startColor: #808080, @endColor: #fff, @startPos: 0, @endPos: 100% ) {
+ background-color: average( @startColor, @endColor );
+ background-image: -webkit-gradient( linear, left top, right top, color-stop( @startPos, @startColor ), color-stop( @endPos, @endColor ) );
+ background-image: -webkit-linear-gradient( left, @startColor @startPos, @endColor @endPos );
+ background-image: -moz-linear-gradient( left, @startColor @startPos, @endColor @endPos );
+ background-image: linear-gradient( to right, @startColor @startPos, @endColor @endPos );
+}
+
+.vertical-gradient( @startColor: #808080, @endColor: #fff, @startPos: 0, @endPos: 100% ) {
+ background-color: average( @startColor, @endColor );
+ background-image: -webkit-gradient( linear, right top, right bottom, color-stop( @startPos, @startColor ), color-stop( @endPos, @endColor ) );
+ background-image: -webkit-linear-gradient( top, @startColor @startPos, @endColor @endPos );
+ background-image: -moz-linear-gradient( top, @startColor @startPos, @endColor @endPos );
+ background-image: linear-gradient( to bottom, @startColor @startPos, @endColor @endPos );
}
// SVG support using a transparent gradient to guarantee cross-browser
}
&-results {
- width: 35em;
margin: 5em auto;
&-noresult,
.mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c2.mw-rcfilters-highlight-color-c3.mw-rcfilters-highlight-color-c4.mw-rcfilters-highlight-color-c5 {
.highlight-results( tint( mix( @highlight-c1, mix( @highlight-c2, mix( @highlight-c3, average( @highlight-c4, @highlight-c5 ), 20% ), 20% ), 20% ), 15% ) );
}
+
+@media screen and ( min-width: @width-breakpoint-tablet ) {
+ // center conflict message
+ // e.g. Special:RecentChanges?goodfaith=maybebad&hidepageedits=1&hidenewpages=1&hidecategorization=1&hideWikibase=1&limit=50&days=0.0833&enhanced=1&urlversion=2
+ // More context in https://phabricator.wikimedia.org/T223363#5374874
+ .mw-rcfilters-ui-changesListWrapperWidget {
+ &-results {
+ width: 35em;
+ }
+ }
+}
--- /dev/null
+@import 'mediawiki.mixins';
+@import 'mediawiki.ui/variables';
+
+.mw-rcfilters-ui-filterTagMultiselectWidget-mobile {
+
+ // Them mobile version of the search input is meant to function
+ // as a button, so styles are modified to that effect. See T224655 for details.
+ .oo-ui-tagMultiselectWidget-input {
+ & .oo-ui-iconElement-icon {
+ opacity: 1;
+ cursor: pointer;
+ }
+
+ &.oo-ui-textInputWidget input[ readonly ] {
+ background-color: @background-color-base;
+ font-weight: bold;
+ cursor: pointer;
+ .mixin-placeholder( { color: @colorText; } );
+ }
+ }
+
+ .mw-rcfilters-ui-filterTagMultiselectWidget-mobile-view {
+ width: 100%;
+ margin-top: -1px;
+
+ & .oo-ui-buttonOptionWidget {
+ width: 50%;
+
+ & .oo-ui-buttonElement-button {
+ width: 100%;
+ text-align: initial;
+ }
+ }
+ }
+}
}
&-bottom {
- .flex-display;
- .flex;
+ .flex-display();
+ .flex();
+ flex-wrap: wrap;
margin-top: 1em;
}
+
+ &-bottom-mobile {
+ .oo-ui-buttonElement {
+ margin-bottom: 1em;
+
+ &-button {
+ text-align: left;
+ }
+ }
+
+ .mw-rcfilters-ui-changesLimitAndDateButtonWidget {
+ order: 1;
+ }
+
+ .mw-rcfilters-ui-liveUpdateButtonWidget {
+ order: 2;
+ }
+
+ .mw-rcfilters-ui-filterWrapperWidget-showNewChanges {
+ order: 3;
+ font-size: 0.85em;
+
+ & > a {
+ white-space: normal;
+ /* stylelint-disable-next-line */
+ padding-top: 0 !important; //overrides .oo-ui-buttonElement-button
+ }
+ }
+ }
}
.addClass( 'mw-rcfilters-ui-changesLimitPopupWidget' )
.append(
this.valuePicker.$element,
- new OO.ui.FieldLayout(
- this.groupByPageCheckbox,
- {
- align: 'inline',
- label: mw.msg( 'rcfilters-group-results-by-page' )
- }
- ).$element
+ OO.ui.isMobile() ? undefined :
+ new OO.ui.FieldLayout(
+ this.groupByPageCheckbox,
+ {
+ align: 'inline',
+ label: mw.msg( 'rcfilters-group-results-by-page' )
+ }
+ ).$element
);
};
this.matchingQuery = null;
this.currentView = this.model.getCurrentView();
this.collapsed = false;
+ this.isMobile = config.isMobile;
// Parent
FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
filterFromInput: false,
hideWhenOutOfView: false,
hideOnChoose: false,
+ // Only set width and footers for desktop
+ isMobile: this.isMobile,
width: 650,
footers: [
{
}
]
},
+ /**
+ * In the presence of an onscreen keyboard (i.e. isMobile) the filter input should act as a button
+ * rather than a text input. Mobile screens are too small to accommodate both an
+ * onscreen keyboard and a popup-menu, so readyOnly is set to disable the keyboard.
+ * A different icon and shorter message is used for mobile as well. (See T224655 for details).
+ */
input: {
- icon: 'menu',
- placeholder: mw.msg( 'rcfilters-search-placeholder' )
+ icon: this.isMobile ? 'funnel' : 'menu',
+ placeholder: this.isMobile ? mw.msg( 'rcfilters-search-placeholder-mobile' ) : mw.msg( 'rcfilters-search-placeholder' ),
+ readOnly: !!this.isMobile,
+ classes: [ 'oo-ui-tagMultiselectWidget-input' ]
}
}, config ) );
this.model.connect( this, {
initialize: 'onModelInitialize',
update: 'onModelUpdate',
- searchChange: 'onModelSearchChange',
+ searchChange: this.isMobile ? function () {} : 'onModelSearchChange',
itemUpdate: 'onModelItemUpdate',
highlightChange: 'onModelHighlightChange'
} );
- this.input.connect( this, { change: 'onInputChange' } );
+
+ if ( !this.isMobile ) {
+ this.input.connect( this, { change: 'onInputChange' } );
+ }
// The filter list and button should appear side by side regardless of how
// wide the button is; the button also changes its width depending
}
// Add a selector at the right of the input
- this.viewsSelectWidget = new OO.ui.ButtonSelectWidget( {
- classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget' ],
- items: [
- new OO.ui.ButtonOptionWidget( {
- framed: false,
- data: 'namespaces',
- icon: 'article',
- label: mw.msg( 'namespaces' ),
- title: mw.msg( 'rcfilters-view-namespaces-tooltip' )
- } ),
- new OO.ui.ButtonOptionWidget( {
- framed: false,
- data: 'tags',
- icon: 'tag',
- label: mw.msg( 'tags-title' ),
- title: mw.msg( 'rcfilters-view-tags-tooltip' )
- } )
- ]
- } );
+ this.viewsSelectWidget = this.createViewsSelectWidget();
- // Rearrange the UI so the select widget is at the right of the input
- this.$element.append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
- .append( this.input.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell' )
- .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
- .append( this.viewsSelectWidget.$element )
- )
- )
- );
+ // change the layout of the viewsSelectWidget
+ this.restructureViewsSelectWidget();
// Event
this.viewsSelectWidget.connect( this, { choose: 'onViewsSelectWidgetChoose' } );
this.$element
.addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
+ if ( this.isMobile ) {
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-mobile' );
+ }
+
this.reevaluateResetRestoreState();
};
/* Methods */
+/**
+ * Create a OOUI ButtonSelectWidget. The buttons are framed and have additional CSS
+ * classes applied on mobile.
+ * @return {OO.ui.ButtonSelectWidget}
+ */
+FilterTagMultiselectWidget.prototype.createViewsSelectWidget = function () {
+ return new OO.ui.ButtonSelectWidget( {
+ classes: this.isMobile ?
+ [
+ 'mw-rcfilters-ui-table',
+ 'mw-rcfilters-ui-filterTagMultiselectWidget-mobile-view'
+ ] :
+ [
+ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget'
+ ],
+ items: [
+ new OO.ui.ButtonOptionWidget( {
+ framed: !!this.isMobile,
+ data: 'namespaces',
+ icon: 'article',
+ label: mw.msg( 'namespaces' ),
+ classes: this.isMobile ? [ 'mw-rcfilters-ui-cell' ] : []
+ } ),
+ new OO.ui.ButtonOptionWidget( {
+ framed: !!this.isMobile,
+ data: 'tags',
+ icon: 'tag',
+ label: mw.msg( 'tags-title' ),
+ title: mw.msg( 'rcfilters-view-tags-tooltip' ),
+ classes: this.isMobile ? [ 'mw-rcfilters-ui-cell' ] : []
+ } )
+ ]
+ } );
+};
+
+/**
+ * Rearrange the DOM structure of the viewsSelectWiget so that on the namespace & tags buttons
+ * are at the right of the input on desktop, and below the input on mobile.
+ */
+FilterTagMultiselectWidget.prototype.restructureViewsSelectWidget = function () {
+ if ( this.isMobile ) {
+ // On mobile, append the search input and the extra buttons below the search input.
+ this.$element.append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
+ .append( this.input.$element )
+ .append( this.viewsSelectWidget.$element )
+ );
+ } else {
+ // On desktop, rearrange the UI so the select widget is at the right of the input
+ this.$element.append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
+ .append( this.input.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell' )
+ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
+ .append( this.viewsSelectWidget.$element )
+ )
+ )
+ );
+ }
+};
+
/**
* Respond to view select widget choose event
*
FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
if ( isVisible ) {
- this.focus();
+ if ( !this.isMobile ) {
+ this.focus();
+ }
mw.hook( 'RcFilters.popup.open' ).fire();
this.blur();
}
- this.input.setIcon( isVisible ? 'search' : 'menu' );
+ if ( this.isMobile ) {
+ this.input.setIcon( isVisible ? 'close' : 'funnel' );
+ } else {
+ this.input.setIcon( isVisible ? 'search' : 'menu' );
+ }
};
/**
* @inheritdoc
*/
FilterTagMultiselectWidget.prototype.onInputFocus = function () {
- // Parent
- FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
+ var scrollToElement = this.isMobile ? this.input.$input : this.$element;
+
+ // treat the input as a menu toggle rather than a text field on mobile
+ if ( this.isMobile ) {
+ this.input.$input.trigger( 'blur' );
+ this.getMenu().toggle();
+ } else {
+ // Parent
+ FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
+ }
// Only scroll to top of the viewport if:
// - The widget is more than 20px from the top
// - The widget is not above the top of the viewport (do not scroll downwards)
// (This isn't represented because >20 is, anyways and always, bigger than 0)
- this.scrollToTop( this.$element, 0, { min: 20, max: Infinity } );
+ this.scrollToTop( scrollToElement, 0, { min: 20, max: Infinity } );
};
/**
// Select the tag if it exists, or reset selection otherwise
this.selectTag( this.findItemFromData( item.model.getName() ) );
- this.focus();
+ if ( !this.isMobile ) {
+ this.focus();
+ }
+
};
/**
{
$overlay: this.$overlay,
collapsed: config.collapsed,
- $wrapper: this.$wrapper
+ $wrapper: this.$wrapper,
+ isMobile: OO.ui.isMobile()
}
);
.addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' );
$bottom = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' )
+ .addClass( OO.ui.isMobile() ?
+ 'mw-rcfilters-ui-filterWrapperWidget-bottom ' +
+ 'mw-rcfilters-ui-filterWrapperWidget-bottom-mobile' :
+ 'mw-rcfilters-ui-filterWrapperWidget-bottom'
+ )
.append(
this.showNewChangesLink.$element,
this.numChangesAndDateWidget.$element
* @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FiltersViewModel} model View model
* @param {Object} [config] Configuration object
+ * @cfg {boolean} [isMobile] a boolean flag determining whether the menu
+ * should display a header or not (the header is omitted on mobile).
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
* @cfg {Object[]} [footers] An array of objects defining the footers for
* this menu, with a definition whether they appear per specific views.
// Parent
MenuSelectWidget.parent.call( this, $.extend( config, {
$autoCloseIgnore: this.$overlay,
- width: 650,
+ width: config.isMobile ? undefined : 650,
// Our filtering is done through the model
filterFromInput: false
} ) );
$( '<div>' )
.addClass( 'mw-rcfilters-ui-menuSelectWidget-group' )
);
- this.setClippableElement( this.$body );
- this.setClippableContainer( this.$element );
-
- header = new FilterMenuHeaderWidget(
- this.controller,
- this.model,
- {
- $overlay: this.$overlay
- }
- );
+
+ if ( !config.isMobile ) {
+ // When hiding the header (i.e. mobile mode) avoid problems
+ // with clippable and the menu's fixed width.
+ this.setClippableElement( this.$body );
+ this.setClippableContainer( this.$element );
+
+ header = new FilterMenuHeaderWidget(
+ this.controller,
+ this.model,
+ {
+ $overlay: this.$overlay
+ }
+ );
+ }
this.noResults = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-filterlist-noresults' ),
// Initialization
this.$element
.addClass( 'mw-rcfilters-ui-menuSelectWidget' )
- .append( header.$element )
+ .append( config.isMobile ? undefined : header.$element )
.append(
this.$body
.append( this.$group, this.noResults.$element )
// Append all footers; we will control their visibility
// based on view
- config.footers = config.footers || [];
+ config.footers = config.isMobile ? [] : config.footers || [];
config.footers.forEach( function ( footerData ) {
var isSticky = footerData.sticky === undefined ? true : !!footerData.sticky,
adjustedData = {
// 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();
}
// Events
this.button.connect( this, { click: 'onButtonClick' } );
- this.textInput.$input.on( 'click', this.onInputClick.bind( this ) );
+ this.textInput.$input.on( 'focus', this.onInputFocus.bind( this ) );
this.$element.addClass( 'mw-widget-copyTextLayout' );
};
};
/**
- * Handle button click events
+ * Handle text widget focus events
*/
- mw.widgets.CopyTextLayout.prototype.onInputClick = function () {
- this.selectText();
+ mw.widgets.CopyTextLayout.prototype.onInputFocus = function () {
+ if ( !this.selecting ) {
+ this.selectText();
+ }
};
/**
scrollTop = input.scrollTop,
scrollLeft = input.scrollLeft;
+ this.selecting = true;
this.textInput.select();
+ this.selecting = false;
// Restore scroll position
input.scrollTop = scrollTop;
* @cfg {Object} [textinput] Config for the text input
* @cfg {boolean} [or=false] Config for whether the widget is dropdown AND input
* or dropdown OR input
+ * @cfg {boolean} [required=false] Config for whether input is required
*/
mw.widgets.SelectWithInputWidget = function MwWidgetsSelectWithInputWidget( config ) {
// Config initialization
- config = $.extend( { or: false }, config );
+ config = $.extend( { or: false, required: false }, config );
// Properties
this.textinput = new OO.ui.TextInputWidget( config.textinput );
this.dropdowninput = new OO.ui.DropdownInputWidget( config.dropdowninput );
this.or = config.or;
+ this.required = config.required;
// Events
this.dropdowninput.on( 'change', this.onChange.bind( this ) );
// is required. However, validity is not checked for disabled fields, as these are not
// submitted with the form. So we should also disable fields when hiding them.
this.textinput.setDisabled( textinputIsHidden || disabled );
+ // If the widget is required, set the text field as required, but only if the widget is visible.
+ if ( this.required ) {
+ this.textinput.setRequired( !this.textinput.isDisabled() );
+ }
};
/**
*
* @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 ) {
'auto' => true,
'expiry' => 0
] );
- $this->user->mBlock->mTimestamp = 0;
+ $this->user->mBlock->setTimestamp( 0 );
$this->assertEquals( [ [ 'autoblockedtext',
"[[User:Useruser|\u{202A}Useruser\u{202C}]]", 'no reason given', '127.0.0.1',
"\u{202A}Useruser\u{202C}", null, 'infinite', '127.0.8.1',
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 ) {
'partial' => true,
'pagerestrictions' => $title,
'namespacerestrictions' => $namespace,
+ 'allowusertalk' => true,
] );
$block = DatabaseBlock::newFromTarget( $this->mUser->getName() );
];
}
- /**
- * @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" );
}
/**
* @covers WANObjectCache::getWithSetCallback()
* @covers WANObjectCache::fetchOrRegenerate()
*/
- public function testBusyValue() {
+ public function testBusyValueBasic() {
$cache = $this->cache;
$key = wfRandomString();
$value = wfRandomString();
$cache->setMockTime( $mockWallClock );
$calls = 0;
- $func = function () use ( &$calls, $value, $cache, $key ) {
+ $func = function () use ( &$calls, $value ) {
++$calls;
return $value;
};
$this->assertEquals( 3, $calls, 'Callback was not used; used interim' );
}
+ public function getBusyValues_Provider() {
+ $hash = new HashBagOStuff( [] );
+
+ return [
+ [
+ function () {
+ return "Saint Oliver Plunckett";
+ },
+ 'Saint Oliver Plunckett'
+ ],
+ [ 'strlen', 'strlen' ],
+ [ 'WANObjectCache::newEmpty', 'WANObjectCache::newEmpty' ],
+ [ [ 'WANObjectCache', 'newEmpty' ], [ 'WANObjectCache', 'newEmpty' ] ],
+ [ [ $hash, 'getLastError' ], [ $hash, 'getLastError' ] ],
+ [ [ 1, 2, 3 ], [ 1, 2, 3 ] ]
+ ];
+ }
+
+ /**
+ * @covers WANObjectCache::getWithSetCallback()
+ * @covers WANObjectCache::fetchOrRegenerate()
+ * @dataProvider getBusyValues_Provider
+ * @param mixed $busyValue
+ * @param mixed $expected
+ */
+ public function testBusyValueTypes( $busyValue, $expected ) {
+ $cache = $this->cache;
+ $key = wfRandomString();
+
+ $mockWallClock = 1549343530.2053;
+ $cache->setMockTime( $mockWallClock );
+
+ $calls = 0;
+ $func = function () use ( &$calls ) {
+ ++$calls;
+ return 418;
+ };
+
+ // Acquire a lock to verify that getWithSetCallback uses busyValue properly
+ $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
+
+ $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'busyValue' => $busyValue ] );
+ $this->assertSame( $expected, $ret, 'busyValue used as expected' );
+ $this->assertSame( 0, $calls, 'busyValue was used' );
+ }
+
/**
* @covers WANObjectCache::getMulti()
*/
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 ],
+ ];
+ }
+}
$this->assertSame( $time, $po->getCacheTime() );
}
+ public static function provideOldSerialized() {
+ return [
+ // phpcs:ignore Generic.Files.LineLength
+ '1.34.0-wmf.15' => [ 'O:12:"ParserOutput":43:{s:5:"mText";s:0:"";s:14:"mLanguageLinks";a:0:{}s:11:"mCategories";a:0:{}s:11:"mIndicators";a:0:{}s:10:"mTitleText";s:0:"";s:6:"mLinks";a:0:{}s:10:"mTemplates";a:0:{}s:12:"mTemplateIds";a:0:{}s:7:"mImages";a:0:{}s:18:"mFileSearchOptions";a:0:{}s:14:"mExternalLinks";a:0:{}s:15:"mInterwikiLinks";a:0:{}s:11:"mNewSection";b:0;s:15:"mHideNewSection";b:0;s:10:"mNoGallery";b:0;s:10:"mHeadItems";a:0:{}s:8:"mModules";a:0:{}s:13:"mModuleStyles";a:0:{}s:13:"mJsConfigVars";a:0:{}s:12:"mOutputHooks";a:0:{}s:9:"mWarnings";a:0:{}s:9:"mSections";a:0:{}s:11:"mProperties";a:0:{}s:8:"mTOCHTML";s:0:"";s:10:"mTimestamp";N;s:11:"mEnableOOUI";b:0;s:26:"\\000ParserOutput\\000mIndexPolicy";s:0:"";s:30:"\\000ParserOutput\\000mAccessedOptions";a:0:{}s:28:"\\000ParserOutput\\000mExtensionData";a:0:{}s:30:"\\000ParserOutput\\000mLimitReportData";a:0:{}s:32:"\\000ParserOutput\\000mLimitReportJSData";a:0:{}s:34:"\\000ParserOutput\\000mPreventClickjacking";b:0;s:20:"\\000ParserOutput\\000mFlags";a:0:{}s:31:"\\000ParserOutput\\000mSpeculativeRevId";N;s:35:"\\000ParserOutput\\000revisionTimestampUsed";N;s:36:"\\000ParserOutput\\000revisionUsedSha1Base36";N;s:32:"\\000ParserOutput\\000mWrapperDivClasses";a:0:{}s:32:"\\000ParserOutput\\000mMaxAdaptiveExpiry";d:INF;s:12:"mUsedOptions";N;s:8:"mVersion";s:5:"1.6.4";s:10:"mCacheTime";s:0:"";s:12:"mCacheExpiry";N;s:16:"mCacheRevisionId";N;}' ]
+ ];
+ }
+
+ /**
+ * Ensure that old ParserOutput objects can be unserialized and reserialized without an error
+ * (T229366).
+ *
+ * @dataProvider provideOldSerialized
+ * @covers ParserOutput::__sleep()
+ */
+ public function testOldSerialized( $serialized ) {
+ $po = unserialize( stripcslashes( $serialized ) );
+ $reserialized = serialize( $po );
+ $this->assertStringStartsWith( 'O:', $reserialized );
+ }
+
}
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 ];
class SearchResultSetTest extends MediaWikiTestCase {
/**
* @covers SearchResultSet::getIterator
- * @covers SearchResultSet::next
- * @covers SearchResultSet::rewind
+ * @covers BaseSearchResultSet::next
+ * @covers BaseSearchResultSet::rewind
*/
public function testIterate() {
$result = SearchResult::newFromTitle( Title::newMainPage() );
}
$this->assertEquals( 1, $count );
- $this->hideDeprecated( 'SearchResultSet::rewind' );
- $this->hideDeprecated( 'SearchResultSet::next' );
+ $this->hideDeprecated( 'BaseSearchResultSet::rewind' );
+ $this->hideDeprecated( 'BaseSearchResultSet::next' );
$resultSet->rewind();
$count = 0;
while ( ( $iterResult = $resultSet->next() ) !== false ) {
}
/**
- * @covers SearchResultSet::augmentResult
- * @covers SearchResultSet::setAugmentedData
+ * @covers SearchResultSetTrait::augmentResult
+ * @covers SearchResultSetTrait::setAugmentedData
*/
public function testDelayedResultAugment() {
$result = SearchResult::newFromTitle( Title::newMainPage() );
$this->assertSame( 0, $count );
}
+ /**
+ * @dataProvider provideProcessFormErrors
+ * @covers ::processForm()
+ */
+ public function testProcessFormErrors( $data, $expected, $config = [] ) {
+ $defaultConfig = [
+ 'wgEnablePartialBlocks' => true,
+ 'wgBlockAllowsUTEdit' => true,
+ ];
+
+ $this->setMwGlobals( array_merge( $defaultConfig, $config ) );
+
+ $defaultData = [
+ 'Target' => '1.2.3.4',
+ 'Expiry' => 'infinity',
+ 'Reason' => [ 'bad reason' ],
+ 'Confirm' => false,
+ 'PageRestrictions' => '',
+ 'NamespaceRestrictions' => '',
+ ];
+
+ $context = RequestContext::getMain();
+ $page = $this->newSpecialPage();
+ $result = $page->processForm( array_merge( $defaultData, $data ), $context );
+
+ $this->assertEquals( $result[0], $expected );
+ }
+
+ public function provideProcessFormErrors() {
+ return [
+ 'Invalid expiry' => [
+ [
+ 'Expiry' => 'invalid',
+ ],
+ 'ipb_expiry_invalid',
+ ],
+ 'Expiry is in the past' => [
+ [
+ 'Expiry' => 'yesterday',
+ ],
+ 'ipb_expiry_old',
+ ],
+ 'HideUser with wrong permissions' => [
+ [
+ 'HideUser' => 1,
+ ],
+ 'badaccess-group0',
+ ],
+ 'Bad ip address' => [
+ [
+ 'Target' => '1.2.3.4/1234',
+ ],
+ 'badipaddress',
+ ],
+ 'Edit user talk page invalid with no restrictions' => [
+ [
+ 'EditingRestriction' => 'partial',
+ 'DisableUTEdit' => 1,
+ ],
+ 'ipb-prevent-user-talk-edit',
+ ],
+ 'Edit user talk page invalid with namespace restriction != NS_USER_TALK ' => [
+ [
+ 'EditingRestriction' => 'partial',
+ 'DisableUTEdit' => 1,
+ 'NamespaceRestrictions' => NS_USER
+ ],
+ 'ipb-prevent-user-talk-edit',
+ ],
+ ];
+ }
+
/**
* @dataProvider provideCheckUnblockSelf
* @covers ::checkUnblockSelf
]
);
$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;