* The transitional wrapper classes AuthPluginPrimaryAuthenticationProvider,
AuthManagerAuthPlugin, and AuthManagerAuthPluginUser.
* The $wgAuth configuration setting and its use in Setup.php and unit tests
+* (T217772) The 'wgAvailableSkins' mw.config key in JavaScript, was removed.
=== Deprecations in 1.33 ===
* The configuration option $wgUseESI has been deprecated, and is expected
check block behaviour.
* The api-feature-usage log channel now has log context. The text message is
deprecated and will be removed in the future.
-* The "stream" request option in MultiHttpClient has been deprecated.
- Use the new "sink" option instead.
=== Other changes in 1.33 ===
* (T201747) Html::openElement() warns if given an element name with a space
'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey',
],
[
- 'categorylinks' => [ 'INNER JOIN', 'cl_from = page_id' ],
+ 'categorylinks' => [ 'JOIN', 'cl_from = page_id' ],
'category' => [ 'LEFT JOIN', [
'cat_title = page_title',
'page_namespace' => NS_CATEGORY
// Prevent caching of responses with cookies (T127993)
$headers = [];
foreach ( headers_list() as $header ) {
- list( $name, $value ) = explode( ':', $header, 2 );
- $headers[strtolower( trim( $name ) )][] = trim( $value );
+ $header = explode( ':', $header, 2 );
+
+ // Note: The code below (currently) does not care about value-less headers
+ if ( isset( $header[1] ) ) {
+ $headers[ strtolower( trim( $header[0] ) ) ][] = trim( $header[1] );
+ }
}
if ( isset( $headers['set-cookie'] ) ) {
* @return bool True if this namespace either is or has a corresponding talk namespace.
*/
public static function canTalk( $index ) {
+ wfDeprecated( __METHOD__, '1.30' );
return self::hasTalkNamespace( $index );
}
*/
public static function pageJoinCond() {
wfDeprecated( __METHOD__, '1.31' );
- return [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
+ return [ 'JOIN', [ 'page_id = rev_page' ] ];
}
/**
'page_is_redirect',
'page_len',
] );
- $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
+ $ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
}
if ( in_array( 'user', $options, true ) ) {
'old_text',
'old_flags'
] );
- $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
+ $ret['joins']['text'] = [ 'JOIN', [ 'rev_text_id=old_id' ] ];
}
return $ret;
'content_address',
'content_model',
] );
- $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ];
+ $ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
if ( in_array( 'model', $options, true ) ) {
// Use left join to attach model name, so we still find the revision row even
return new WatchedItemQueryService(
$services->getDBLoadBalancer(),
$services->getCommentStore(),
- $services->getActorMigration()
+ $services->getActorMigration(),
+ $services->getWatchedItemStore()
);
},
'WatchedItemStore' => function ( MediaWikiServices $services ) : WatchedItemStore {
$store = new WatchedItemStore(
$services->getDBLoadBalancerFactory(),
+ JobQueueGroup::singleton(),
+ $services->getMainObjectStash(),
new HashBagOStuff( [ 'maxKeys' => 100 ] ),
$services->getReadOnlyMode(),
$services->getMainConfig()->get( 'UpdateRowsPerQuery' )
* and does not rely on global state or the database.
*/
class Title implements LinkTarget, IDBAccessObject {
- /** @var MapCacheLRU */
+ /** @var MapCacheLRU|null */
private static $titleCache = null;
/**
* Only public to share cache with TitleFormatter
*
* @private
- * @var string
+ * @var string|null
*/
public $prefixedText = null;
* the database or false if not loaded, yet. */
private $mDbPageLanguage = false;
- /** @var TitleValue A corresponding TitleValue object */
+ /** @var TitleValue|null A corresponding TitleValue object */
private $mTitleValue = null;
- /** @var bool Would deleting this page be a big deletion? */
+ /** @var bool|null Would deleting this page be a big deletion? */
private $mIsBigDeletion = null;
// @}
* @return Title|null Title, or null on an error
*/
public static function newFromDBkey( $key ) {
- $t = new Title();
+ $t = new self();
$t->mDbkeyform = $key;
try {
}
try {
- return self::newFromTextThrow( strval( $text ), $defaultNamespace );
+ return self::newFromTextThrow( (string)$text, $defaultNamespace );
} catch ( MalformedTitleException $ex ) {
return null;
}
$t = new Title();
$t->mDbkeyform = strtr( $filteredText, ' ', '_' );
- $t->mDefaultNamespace = intval( $defaultNamespace );
+ $t->mDefaultNamespace = (int)$defaultNamespace;
$t->secureAndSplit();
if ( $defaultNamespace == NS_MAIN ) {
* @return MapCacheLRU
*/
private static function getTitleCache() {
- if ( self::$titleCache == null ) {
+ if ( self::$titleCache === null ) {
self::$titleCache = new MapCacheLRU( self::CACHE_MAX );
}
return self::$titleCache;
$this->mLatestID = (int)$row->page_latest;
}
if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) {
- $this->mContentModel = strval( $row->page_content_model );
+ $this->mContentModel = (string)$row->page_content_model;
} elseif ( !$this->mForcedContentModel ) {
$this->mContentModel = false; # initialized lazily in getContentModel()
}
$t = new Title();
$t->mInterwiki = $interwiki;
$t->mFragment = $fragment;
- $t->mNamespace = $ns = intval( $ns );
+ $t->mNamespace = $ns = (int)$ns;
$t->mDbkeyform = strtr( $title, ' ', '_' );
$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
$t->mUrlform = wfUrlencode( $t->mDbkeyform );
if ( !is_null( $params['tag'] ) ) {
$this->addTables( 'change_tag' );
$this->addJoinConds(
- [ 'change_tag' => [ 'INNER JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
+ [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
);
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
try {
if ( $needPageTable ) {
$revQuery['tables'][] = 'page';
- $revQuery['joins']['page'] = [ 'INNER JOIN', [ "$pageField = page_id" ] ];
+ $revQuery['joins']['page'] = [ 'JOIN', [ "$pageField = page_id" ] ];
if ( (bool)$miser_ns ) {
$revQuery['fields'][] = 'page_namespace';
}
$this->addTables( 'user_groups', 'ug1' );
$this->addJoinConds( [
'ug1' => [
- 'INNER JOIN',
+ 'JOIN',
[
'ug1.ug_user=user_id',
'ug1.ug_group' => $params['group'],
// There shouldn't be any duplicate rows in querycachetwo here.
$this->addTables( 'querycachetwo' );
$this->addJoinConds( [ 'querycachetwo' => [
- 'INNER JOIN', [
+ 'JOIN', [
'qcc_type' => 'activeusers',
'qcc_namespace' => NS_USER,
'qcc_title=user_name',
$limitGroups = array_unique( $limitGroups );
$this->addTables( 'user_groups' );
$this->addJoinConds( [ 'user_groups' => [
- $excludeGroups ? 'LEFT OUTER JOIN' : 'INNER JOIN',
+ $excludeGroups ? 'LEFT OUTER JOIN' : 'JOIN',
[
'ug_user=' . $revQuery['fields']['rev_user'],
'ug_group' => $limitGroups,
if ( !is_null( $params['tag'] ) ) {
$this->addTables( 'change_tag' );
$this->addJoinConds(
- [ 'change_tag' => [ 'INNER JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
+ [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
);
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
try {
if ( !is_null( $params['tag'] ) ) {
$this->addTables( 'change_tag' );
$this->addJoinConds(
- [ 'change_tag' => [ 'INNER JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
+ [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
);
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
try {
if ( !is_null( $params['tag'] ) ) {
$this->addTables( 'change_tag' );
- $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN',
+ $this->addJoinConds( [ 'change_tag' => [ 'JOIN',
[ 'log_id=ct_log_id' ] ] ] );
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
try {
if ( !is_null( $params['tag'] ) ) {
$this->addTables( 'change_tag' );
- $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ 'rc_id=ct_rc_id' ] ] ] );
+ $this->addJoinConds( [ 'change_tag' => [ 'JOIN', [ 'rc_id=ct_rc_id' ] ] ] );
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
try {
$this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
// Always join 'page' so orphaned revisions are filtered out
$this->addTables( [ 'revision', 'page' ] );
$this->addJoinConds(
- [ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ] ]
+ [ 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ] ]
);
$this->addFields( [
'rev_id' => $idField, 'rev_timestamp' => $tsField, 'rev_page' => $pageField
if ( $params['tag'] !== null ) {
$this->addTables( 'change_tag' );
$this->addJoinConds(
- [ 'change_tag' => [ 'INNER JOIN', [ 'rev_id=ct_rev_id' ] ] ]
+ [ 'change_tag' => [ 'JOIN', [ 'rev_id=ct_rev_id' ] ] ]
);
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
try {
if ( isset( $this->params['tag'] ) ) {
$this->addTables( 'change_tag' );
$this->addJoinConds(
- [ 'change_tag' => [ 'INNER JOIN', [ $idField . ' = ct_rev_id' ] ] ]
+ [ 'change_tag' => [ 'JOIN', [ $idField . ' = ct_rev_id' ] ] ]
);
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
try {
}
$this->addTables( 'user_groups' );
- $this->addJoinConds( [ 'user_groups' => [ 'INNER JOIN', 'ug_user=user_id' ] ] );
+ $this->addJoinConds( [ 'user_groups' => [ 'JOIN', 'ug_user=user_id' ] ] );
$this->addFields( [ 'user_name' ] );
$this->addFields( UserGroupMembership::selectFields() );
$this->addWhere( 'ug_expiry IS NULL OR ug_expiry >= ' .
foreach ( $sorted as $obj ) {
$title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
- $talkpage = MWNamespace::canTalk( $obj->rc_namespace )
+ $talkpage = MWNamespace::hasTalkNamespace( $obj->rc_namespace )
? $title->getTalkPage()->getFullURL()
: '';
// Add an INNER JOIN on change_tag
$tables[] = 'change_tag';
- $join_conds['change_tag'] = [ 'INNER JOIN', $join_cond ];
+ $join_conds['change_tag'] = [ 'JOIN', $join_cond ];
$filterTagIds = [];
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
foreach ( (array)$filter_tag as $filterTagName ) {
}
$tagTables = [ 'change_tag', 'change_tag_def' ];
- $join_cond_ts_tags = [ 'change_tag_def' => [ 'INNER JOIN', 'ct_tag_id=ctd_id' ] ];
+ $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
$field = 'ctd_name';
return wfGetDB( DB_REPLICA )->buildGroupConcatField(
/**
* Do any deferred updates and clear the list
*
+ * If $stage is self::ALL then the queue of PRESEND updates will be resolved,
+ * followed by the queue of POSTSEND updates
+ *
* @param string $mode Use "enqueue" to use the job queue when possible [Default: "run"]
* @param int $stage DeferredUpdates constant (PRESEND, POSTSEND, or ALL) (since 1.27)
*/
* @param array $existing
* @return array
*/
- function getPropertyDeletions( $existing ) {
+ private function getPropertyDeletions( $existing ) {
return array_diff_assoc( $existing, $this->mProperties );
}
$opts[] = 'STRAIGHT_JOIN';
$opts['USE INDEX']['revision'] = 'rev_page_id';
unset( $join['revision'] );
- $join['page'] = [ 'INNER JOIN', 'rev_page=page_id' ];
+ $join['page'] = [ 'JOIN', 'rev_page=page_id' ];
}
} elseif ( $this->history & self::CURRENT ) {
# Latest revision dumps...
if ( $this->list_authors && $cond != '' ) { // List authors, if so desired
$this->do_list_authors( $cond );
}
- $join['revision'] = [ 'INNER JOIN', 'page_id=rev_page AND page_latest=rev_id' ];
+ $join['revision'] = [ 'JOIN', 'page_id=rev_page AND page_latest=rev_id' ];
} elseif ( $this->history & self::STABLE ) {
# "Stable" revision dumps...
# Default JOIN, to be overridden...
- $join['revision'] = [ 'INNER JOIN', 'page_id=rev_page AND page_latest=rev_id' ];
+ $join['revision'] = [ 'JOIN', 'page_id=rev_page AND page_latest=rev_id' ];
# One, and only one hook should set this, and return false
if ( Hooks::run( 'WikiExporter::dumpStableQuery', [ &$tables, &$opts, &$join ] ) ) {
throw new MWException( __METHOD__ . " given invalid history dump type." );
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
-use Psr\Http\Message\ResponseInterface;
-use GuzzleHttp\Client;
+use MediaWiki\MediaWikiServices;
/**
* Class to handle multiple HTTP requests
* PUT requests, and a field/value array for POST request;
* array bodies are encoded as multipart/form-data and strings
* use application/x-www-form-urlencoded (headers sent automatically)
- * - sink : resource to receive the HTTP response body (preferred over stream)
- * @since 1.33
* - stream : resource to stream the HTTP response body to
- * @deprecated since 1.33, use sink instead
* - proxy : HTTP proxy to use
* - flags : map of boolean flags which supports:
* - relayResponseHeaders : write out header via header()
* @since 1.23
*/
class MultiHttpClient implements LoggerAwareInterface {
- /** @var float connection timeout in seconds, zero to wait indefinitely*/
+ /** @var resource */
+ protected $multiHandle = null; // curl_multi handle
+ /** @var string|null SSL certificates path */
+ protected $caBundlePath;
+ /** @var float */
protected $connTimeout = 10;
- /** @var float request timeout in seconds, zero to wait indefinitely*/
+ /** @var float */
protected $reqTimeout = 300;
+ /** @var bool */
+ protected $usePipelining = false;
+ /** @var int */
+ protected $maxConnsPerHost = 50;
/** @var string|null proxy */
protected $proxy;
- /** @var int CURLMOPT_PIPELINING value, only effective if curl is available */
- protected $pipeliningMode = 0;
- /** @var int CURLMOPT_MAXCONNECTS value, only effective if curl is available */
- protected $maxConnsPerHost = 50;
/** @var string */
protected $userAgent = 'wikimedia/multi-http-client v1.0';
/** @var LoggerInterface */
protected $logger;
- /** @var string|null SSL certificates path */
- protected $caBundlePath;
+
+ // In PHP 7 due to https://bugs.php.net/bug.php?id=76480 the request/connect
+ // timeouts are periodically polled instead of being accurately respected.
+ // The select timeout is set to the minimum timeout multiplied by this factor.
+ const TIMEOUT_ACCURACY_FACTOR = 0.1;
/**
* @param array $options
* - connTimeout : default connection timeout (seconds)
* - reqTimeout : default request timeout (seconds)
* - proxy : HTTP proxy to use
- * - pipeliningMode : whether to use HTTP pipelining/multiplexing if possible (for all
- * hosts). The exact behavior is dependent on curl version.
+ * - usePipelining : whether to use HTTP pipelining if possible (for all hosts)
* - maxConnsPerHost : maximum number of concurrent connections (per host)
* - userAgent : The User-Agent header value to send
* - logger : a \Psr\Log\LoggerInterface instance for debug logging
* - caBundlePath : path to specific Certificate Authority bundle (if any)
* @throws Exception
- *
- * usePipelining is an alias for pipelining mode, retained for backward compatibility.
- * If both usePipelining and pipeliningMode are specified, pipeliningMode wins.
*/
public function __construct( array $options ) {
if ( isset( $options['caBundlePath'] ) ) {
$this->caBundlePath = $options['caBundlePath'];
if ( !file_exists( $this->caBundlePath ) ) {
- throw new Exception( "Cannot find CA bundle: {$this->caBundlePath}" );
+ throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath );
}
}
-
- // Backward compatibility. Defers to newer option naming if both are specified.
- if ( isset( $options['usePipelining'] ) ) {
- $this->pipeliningMode = $options['usePipelining'];
- }
-
static $opts = [
- 'connTimeout', 'reqTimeout', 'proxy', 'pipeliningMode', 'maxConnsPerHost',
- 'userAgent', 'logger'
+ 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost',
+ 'proxy', 'userAgent', 'logger'
];
foreach ( $opts as $key ) {
if ( isset( $options[$key] ) ) {
$this->$key = $options[$key];
}
}
-
if ( $this->logger === null ) {
$this->logger = new NullLogger;
}
* - code : HTTP response code or 0 if there was a serious error
* - reason : HTTP response reason (empty if there was a serious error)
* - headers : <header name/value associative array>
- * - body : HTTP response body
- * - error : Any error string
+ * - body : HTTP response body or resource (if "stream" was set)
+ * - error : Any error string
* The map also stores integer-indexed copies of these values. This lets callers do:
* @code
- * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $http->run( $req );
+ * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $http->run( $req );
* @endcode
* @param array $req HTTP request array
* @param array $opts
* - connTimeout : connection timeout per request (seconds)
* - reqTimeout : post-connection timeout per request (seconds)
- * - handler : optional custom handler
- * See http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html
* @return array Response array for request
- * @throws Exception
*/
public function run( array $req, array $opts = [] ) {
return $this->runMulti( [ $req ], $opts )[0]['response'];
* - code : HTTP response code or 0 if there was a serious error
* - reason : HTTP response reason (empty if there was a serious error)
* - headers : <header name/value associative array>
- * - body : HTTP response body
+ * - body : HTTP response body or resource (if "stream" was set)
* - error : Any error string
* The map also stores integer-indexed copies of these values. This lets callers do:
* @code
* @param array $opts
* - connTimeout : connection timeout per request (seconds)
* - reqTimeout : post-connection timeout per request (seconds)
- * - pipeliningMode : whether to use HTTP pipelining/multiplexing if possible (for all
- * hosts). The exact behavior is dependent on curl version.
+ * - usePipelining : whether to use HTTP pipelining if possible
* - maxConnsPerHost : maximum number of concurrent connections (per host)
- * - handler : optional custom handler.
- * See http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html
* @return array $reqs With response array populated for each
* @throws Exception
- *
- * usePipelining is an alias for pipelining mode, retained for backward compatibility.
- * If both usePipelining and pipeliningMode are specified, pipeliningMode wins.
*/
public function runMulti( array $reqs, array $opts = [] ) {
$this->normalizeRequests( $reqs );
- return $this->runMultiGuzzle( $reqs, $opts );
+ if ( $this->isCurlEnabled() ) {
+ return $this->runMultiCurl( $reqs, $opts );
+ } else {
+ return $this->runMultiHttp( $reqs, $opts );
+ }
}
/**
*
* @param array $reqs Map of HTTP request arrays
* @param array $opts
+ * - connTimeout : connection timeout per request (seconds)
+ * - reqTimeout : post-connection timeout per request (seconds)
+ * - usePipelining : whether to use HTTP pipelining if possible
+ * - maxConnsPerHost : maximum number of concurrent connections (per host)
* @return array $reqs With response array populated for each
* @throws Exception
*/
- private function runMultiGuzzle( array $reqs, array $opts = [] ) {
- $guzzleOptions = [
- 'timeout' => $opts['reqTimeout'] ?? $this->reqTimeout,
- 'connect_timeout' => $opts['connTimeout'] ?? $this->connTimeout,
- 'allow_redirects' => [
- 'max' => 4,
- ],
- ];
-
- if ( !is_null( $this->caBundlePath ) ) {
- $guzzleOptions['verify'] = $this->caBundlePath;
+ private function runMultiCurl( array $reqs, array $opts = [] ) {
+ $chm = $this->getCurlMulti();
+
+ $selectTimeout = $this->getSelectTimeout( $opts );
+
+ // Add all of the required cURL handles...
+ $handles = [];
+ foreach ( $reqs as $index => &$req ) {
+ $handles[$index] = $this->getCurlHandle( $req, $opts );
+ if ( count( $reqs ) > 1 ) {
+ // https://github.com/guzzle/guzzle/issues/349
+ curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true );
+ }
}
+ unset( $req ); // don't assign over this by accident
- // Include curl-specific option section only if curl is available.
- // Our defaults may differ from curl's defaults, depending on curl version.
- if ( $this->isCurlEnabled() ) {
- // Backward compatibility
- $optsPipeliningMode = $opts['pipeliningMode'] ?? ( $opts['usePipelining'] ?? null );
+ $indexes = array_keys( $reqs );
+ if ( isset( $opts['usePipelining'] ) ) {
+ curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
+ }
+ if ( isset( $opts['maxConnsPerHost'] ) ) {
+ // Keep these sockets around as they may be needed later in the request
+ curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
+ }
- // Per-request options override class-level options
- $pipeliningMode = $optsPipeliningMode ?? $this->pipeliningMode;
- $maxConnsPerHost = $opts['maxConnsPerHost'] ?? $this->maxConnsPerHost;
+ // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS)
+ $batches = array_chunk( $indexes, $this->maxConnsPerHost );
+ $infos = [];
- $guzzleOptions['curl'][CURLMOPT_PIPELINING] = (int)$pipeliningMode;
- $guzzleOptions['curl'][CURLMOPT_MAXCONNECTS] = (int)$maxConnsPerHost;
+ foreach ( $batches as $batch ) {
+ // Attach all cURL handles for this batch
+ foreach ( $batch as $index ) {
+ curl_multi_add_handle( $chm, $handles[$index] );
+ }
+ // Execute the cURL handles concurrently...
+ $active = null; // handles still being processed
+ do {
+ // Do any available work...
+ do {
+ $mrc = curl_multi_exec( $chm, $active );
+ $info = curl_multi_info_read( $chm );
+ if ( $info !== false ) {
+ $infos[(int)$info['handle']] = $info;
+ }
+ } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
+ // Wait (if possible) for available work...
+ if ( $active > 0 && $mrc == CURLM_OK ) {
+ if ( curl_multi_select( $chm, $selectTimeout ) == -1 ) {
+ // PHP bug 63411; https://curl.haxx.se/libcurl/c/curl_multi_fdset.html
+ usleep( 5000 ); // 5ms
+ }
+ }
+ } while ( $active > 0 && $mrc == CURLM_OK );
}
- if ( isset( $opts['handler'] ) ) {
- $guzzleOptions['handler'] = $opts['handler'];
- }
+ // Remove all of the added cURL handles and check for errors...
+ foreach ( $reqs as $index => &$req ) {
+ $ch = $handles[$index];
+ curl_multi_remove_handle( $chm, $ch );
+
+ if ( isset( $infos[(int)$ch] ) ) {
+ $info = $infos[(int)$ch];
+ $errno = $info['result'];
+ if ( $errno !== 0 ) {
+ $req['response']['error'] = "(curl error: $errno)";
+ if ( function_exists( 'curl_strerror' ) ) {
+ $req['response']['error'] .= " " . curl_strerror( $errno );
+ }
+ $this->logger->warning( "Error fetching URL \"{$req['url']}\": " .
+ $req['response']['error'] );
+ }
+ } else {
+ $req['response']['error'] = "(curl error: no status set)";
+ }
- $guzzleOptions['headers']['user-agent'] = $this->userAgent;
+ // For convenience with the list() operator
+ $req['response'][0] = $req['response']['code'];
+ $req['response'][1] = $req['response']['reason'];
+ $req['response'][2] = $req['response']['headers'];
+ $req['response'][3] = $req['response']['body'];
+ $req['response'][4] = $req['response']['error'];
+ curl_close( $ch );
+ // Close any string wrapper file handles
+ if ( isset( $req['_closeHandle'] ) ) {
+ fclose( $req['_closeHandle'] );
+ unset( $req['_closeHandle'] );
+ }
+ }
+ unset( $req ); // don't assign over this by accident
- $client = new Client( $guzzleOptions );
- $promises = [];
- foreach ( $reqs as $index => $req ) {
- $reqOptions = [
- 'proxy' => $req['proxy'] ?? $this->proxy,
- ];
+ // Restore the default settings
+ curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
+ curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
- if ( $req['method'] == 'POST' ) {
- $reqOptions['form_params'] = $req['body'];
+ return $reqs;
+ }
- // Suppress 'Expect: 100-continue' header, as some servers
- // will reject it with a 417 and Curl won't auto retry
- // with HTTP 1.0 fallback
- $reqOptions['expect'] = false;
- }
+ /**
+ * @param array &$req HTTP request map
+ * @param array $opts
+ * - connTimeout : default connection timeout
+ * - reqTimeout : default request timeout
+ * @return resource
+ * @throws Exception
+ */
+ protected function getCurlHandle( array &$req, array $opts = [] ) {
+ $ch = curl_init();
+
+ curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT_MS,
+ ( $opts['connTimeout'] ?? $this->connTimeout ) * 1000 );
+ curl_setopt( $ch, CURLOPT_PROXY, $req['proxy'] ?? $this->proxy );
+ curl_setopt( $ch, CURLOPT_TIMEOUT_MS,
+ ( $opts['reqTimeout'] ?? $this->reqTimeout ) * 1000 );
+ curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
+ curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
+ curl_setopt( $ch, CURLOPT_HEADER, 0 );
+ if ( !is_null( $this->caBundlePath ) ) {
+ curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
+ curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath );
+ }
+ curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
- if ( isset( $req['headers']['user-agent'] ) ) {
- $reqOptions['headers']['user-agent'] = $req['headers']['user-agent'];
- }
+ $url = $req['url'];
+ $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
+ if ( $query != '' ) {
+ $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
+ }
+ curl_setopt( $ch, CURLOPT_URL, $url );
- // Backward compatibility for pre-Guzzle naming
- if ( isset( $req['sink'] ) ) {
- $reqOptions['sink'] = $req['sink'];
- } elseif ( isset( $req['stream'] ) ) {
- $reqOptions['sink'] = $req['stream'];
- }
+ curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
+ if ( $req['method'] === 'HEAD' ) {
+ curl_setopt( $ch, CURLOPT_NOBODY, 1 );
+ }
- if ( !empty( $req['flags']['relayResponseHeaders'] ) ) {
- $reqOptions['on_headers'] = function ( ResponseInterface $response ) {
- foreach ( $response->getHeaders() as $name => $values ) {
- foreach ( $values as $value ) {
- header( $name . ': ' . $value . "\r\n" );
- }
- }
- };
+ if ( $req['method'] === 'PUT' ) {
+ curl_setopt( $ch, CURLOPT_PUT, 1 );
+ if ( is_resource( $req['body'] ) ) {
+ curl_setopt( $ch, CURLOPT_INFILE, $req['body'] );
+ if ( isset( $req['headers']['content-length'] ) ) {
+ curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] );
+ } elseif ( isset( $req['headers']['transfer-encoding'] ) &&
+ $req['headers']['transfer-encoding'] === 'chunks'
+ ) {
+ curl_setopt( $ch, CURLOPT_UPLOAD, true );
+ } else {
+ throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." );
+ }
+ } elseif ( $req['body'] !== '' ) {
+ $fp = fopen( "php://temp", "wb+" );
+ fwrite( $fp, $req['body'], strlen( $req['body'] ) );
+ rewind( $fp );
+ curl_setopt( $ch, CURLOPT_INFILE, $fp );
+ curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) );
+ $req['_closeHandle'] = $fp; // remember to close this later
+ } else {
+ curl_setopt( $ch, CURLOPT_INFILESIZE, 0 );
}
-
- $url = $req['url'];
- $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
- if ( $query != '' ) {
- $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
+ curl_setopt( $ch, CURLOPT_READFUNCTION,
+ function ( $ch, $fd, $length ) {
+ $data = fread( $fd, $length );
+ $len = strlen( $data );
+ return $data;
+ }
+ );
+ } elseif ( $req['method'] === 'POST' ) {
+ curl_setopt( $ch, CURLOPT_POST, 1 );
+ // Don't interpret POST parameters starting with '@' as file uploads, because this
+ // makes it impossible to POST plain values starting with '@' (and causes security
+ // issues potentially exposing the contents of local files).
+ curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
+ curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
+ } else {
+ if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
+ throw new Exception( "HTTP body specified for a non PUT/POST request." );
}
- $promises[$index] = $client->requestAsync( $req['method'], $url, $reqOptions );
+ $req['headers']['content-length'] = 0;
}
- $results = GuzzleHttp\Promise\settle( $promises )->wait();
+ if ( !isset( $req['headers']['user-agent'] ) ) {
+ $req['headers']['user-agent'] = $this->userAgent;
+ }
- foreach ( $results as $index => $result ) {
- if ( $result['state'] === 'fulfilled' ) {
- $this->guzzleHandleSuccess( $reqs[$index], $result['value'] );
- } elseif ( $result['state'] === 'rejected' ) {
- $this->guzzleHandleFailure( $reqs[$index], $result['reason'] );
- } else {
- // This should never happen, and exists only in case of changes to guzzle
- throw new UnexpectedValueException(
- "Unrecognized result state: {$result['state']}" );
+ $headers = [];
+ foreach ( $req['headers'] as $name => $value ) {
+ if ( strpos( $name, ': ' ) ) {
+ throw new Exception( "Headers cannot have ':' in the name." );
}
+ $headers[] = $name . ': ' . trim( $value );
}
+ curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
- foreach ( $reqs as &$req ) {
- $req['response'][0] = $req['response']['code'];
- $req['response'][1] = $req['response']['reason'];
- $req['response'][2] = $req['response']['headers'];
- $req['response'][3] = $req['response']['body'];
- $req['response'][4] = $req['response']['error'];
+ curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
+ function ( $ch, $header ) use ( &$req ) {
+ if ( !empty( $req['flags']['relayResponseHeaders'] ) && trim( $header ) !== '' ) {
+ header( $header );
+ }
+ $length = strlen( $header );
+ $matches = [];
+ if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) {
+ $req['response']['code'] = (int)$matches[2];
+ $req['response']['reason'] = trim( $matches[3] );
+ return $length;
+ }
+ if ( strpos( $header, ":" ) === false ) {
+ return $length;
+ }
+ list( $name, $value ) = explode( ":", $header, 2 );
+ $name = strtolower( $name );
+ $value = trim( $value );
+ if ( isset( $req['response']['headers'][$name] ) ) {
+ $req['response']['headers'][$name] .= ', ' . $value;
+ } else {
+ $req['response']['headers'][$name] = $value;
+ }
+ return $length;
+ }
+ );
+
+ if ( isset( $req['stream'] ) ) {
+ // Don't just use CURLOPT_FILE as that might give:
+ // curl_setopt(): cannot represent a stream of type Output as a STDIO FILE*
+ // The callback here handles both normal files and php://temp handles.
+ curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
+ function ( $ch, $data ) use ( &$req ) {
+ return fwrite( $req['stream'], $data );
+ }
+ );
+ } else {
+ curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
+ function ( $ch, $data ) use ( &$req ) {
+ $req['response']['body'] .= $data;
+ return strlen( $data );
+ }
+ );
}
- return $reqs;
+ return $ch;
}
/**
- * Called for successful requests
- *
- * @param array $req the original request
- * @param ResponseInterface $response
+ * @return resource
+ * @throws Exception
*/
- private function guzzleHandleSuccess( &$req, $response ) {
- $req['response'] = [
- 'code' => $response->getStatusCode(),
- 'reason' => $response->getReasonPhrase(),
- 'headers' => $this->parseHeaders( $response->getHeaders() ),
- 'body' => isset( $req['sink'] ) ? '' : $response->getBody()->getContents(),
- 'error' => '',
- ];
+ protected function getCurlMulti() {
+ if ( !$this->multiHandle ) {
+ if ( !function_exists( 'curl_multi_init' ) ) {
+ throw new Exception( "PHP cURL function curl_multi_init missing. " .
+ "Check https://www.mediawiki.org/wiki/Manual:CURL" );
+ }
+ $cmh = curl_multi_init();
+ curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
+ curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
+ $this->multiHandle = $cmh;
+ }
+ return $this->multiHandle;
}
/**
- * Called for failed requests
+ * Execute a set of HTTP(S) requests sequentially.
*
- * @param array $req the original request
- * @param Exception $reason
+ * @see MultiHttpClient::runMulti()
+ * @todo Remove dependency on MediaWikiServices: use a separate HTTP client
+ * library or copy code from PhpHttpRequest
+ * @param array $reqs Map of HTTP request arrays
+ * @param array $opts
+ * - connTimeout : connection timeout per request (seconds)
+ * - reqTimeout : post-connection timeout per request (seconds)
+ * @return array $reqs With response array populated for each
+ * @throws Exception
*/
- private function guzzleHandleFailure( &$req, $reason ) {
- $req['response'] = [
- 'code' => $reason->getCode(),
- 'reason' => '',
- 'headers' => [],
- 'body' => '',
- 'error' => $reason->getMessage(),
+ private function runMultiHttp( array $reqs, array $opts = [] ) {
+ $httpOptions = [
+ 'timeout' => $opts['reqTimeout'] ?? $this->reqTimeout,
+ 'connectTimeout' => $opts['connTimeout'] ?? $this->connTimeout,
+ 'logger' => $this->logger,
+ 'caInfo' => $this->caBundlePath,
];
+ foreach ( $reqs as &$req ) {
+ $reqOptions = $httpOptions + [
+ 'method' => $req['method'],
+ 'proxy' => $req['proxy'] ?? $this->proxy,
+ 'userAgent' => $req['headers']['user-agent'] ?? $this->userAgent,
+ 'postData' => $req['body'],
+ ];
- if (
- $reason instanceof GuzzleHttp\Exception\RequestException &&
- $reason->hasResponse()
- ) {
- $response = $reason->getResponse();
- if ( $response ) {
- $req['response']['reason'] = $response->getReasonPhrase();
- $req['response']['headers'] = $this->parseHeaders( $response->getHeaders() );
- $req['response']['body'] = $response->getBody()->getContents();
+ $url = $req['url'];
+ $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
+ if ( $query != '' ) {
+ $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
}
- }
- $this->logger->warning( "Error fetching URL \"{$req['url']}\": " .
- $req['response']['error'] );
- }
+ $httpRequest = MediaWikiServices::getInstance()->getHttpRequestFactory()->create(
+ $url, $reqOptions );
+ $sv = $httpRequest->execute()->getStatusValue();
- /**
- * Parses response headers.
- *
- * @param string[][] $guzzleHeaders
- * @return array
- */
- private function parseHeaders( $guzzleHeaders ) {
- $headers = [];
- foreach ( $guzzleHeaders as $name => $values ) {
- $headers[strtolower( $name )] = implode( ', ', $values );
+ $respHeaders = array_map(
+ function ( $v ) {
+ return implode( ', ', $v );
+ },
+ $httpRequest->getResponseHeaders() );
+
+ $req['response'] = [
+ 'code' => $httpRequest->getStatus(),
+ 'reason' => '',
+ 'headers' => $respHeaders,
+ 'body' => $httpRequest->getContent(),
+ 'error' => '',
+ ];
+
+ if ( !$sv->isOk() ) {
+ $svErrors = $sv->getErrors();
+ if ( isset( $svErrors[0] ) ) {
+ $req['response']['error'] = $svErrors[0]['message'];
+
+ // param values vary per failure type (ex. unknown host vs unknown page)
+ if ( isset( $svErrors[0]['params'][0] ) ) {
+ if ( is_numeric( $svErrors[0]['params'][0] ) ) {
+ if ( isset( $svErrors[0]['params'][1] ) ) {
+ $req['response']['reason'] = $svErrors[0]['params'][1];
+ }
+ } else {
+ $req['response']['reason'] = $svErrors[0]['params'][0];
+ }
+ }
+ }
+ }
+
+ $req['response'][0] = $req['response']['code'];
+ $req['response'][1] = $req['response']['reason'];
+ $req['response'][2] = $req['response']['headers'];
+ $req['response'][3] = $req['response']['body'];
+ $req['response'][4] = $req['response']['error'];
}
- return $headers;
+
+ return $reqs;
}
/**
* Normalize request information
*
* @param array $reqs the requests to normalize
- * @throws Exception
*/
private function normalizeRequests( array &$reqs ) {
foreach ( $reqs as &$req ) {
}
}
+ /**
+ * Get a suitable select timeout for the given options.
+ *
+ * @param array $opts
+ * @return float
+ */
+ private function getSelectTimeout( $opts ) {
+ $connTimeout = $opts['connTimeout'] ?? $this->connTimeout;
+ $reqTimeout = $opts['reqTimeout'] ?? $this->reqTimeout;
+ $timeouts = array_filter( [ $connTimeout, $reqTimeout ] );
+ if ( count( $timeouts ) === 0 ) {
+ return 1;
+ }
+
+ $selectTimeout = min( $timeouts ) * self::TIMEOUT_ACCURACY_FACTOR;
+ // Minimum 10us for sanity
+ if ( $selectTimeout < 10e-6 ) {
+ $selectTimeout = 10e-6;
+ }
+ return $selectTimeout;
+ }
+
/**
* Register a logger
*
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
+
+ function __destruct() {
+ if ( $this->multiHandle ) {
+ curl_multi_close( $this->multiHandle );
+ }
+ }
}
}
}
# Don't show duplicate rows when using log_search
- $joins['log_search'] = [ 'INNER JOIN', 'ls_log_id=log_id' ];
+ $joins['log_search'] = [ 'JOIN', 'ls_log_id=log_id' ];
$info = [
'tables' => $tables,
],
__METHOD__,
[],
- [ 'categorylinks' => [ 'INNER JOIN', 'page_id = cl_from' ] ]
+ [ 'categorylinks' => [ 'JOIN', 'page_id = cl_from' ] ]
);
return TitleArray::newFromResult( $res );
$localFileRefs = array_values( array_unique( $localFileRefs ) );
sort( $localFileRefs );
$localPaths = self::getRelativePaths( $localFileRefs );
-
$storedPaths = self::getRelativePaths( $this->getFileDependencies( $context ) );
- // If the list has been modified since last time we cached it, update the cache
- if ( $localPaths !== $storedPaths ) {
- $vary = $context->getSkin() . '|' . $context->getLanguage();
- $cache = ObjectCache::getLocalClusterInstance();
- $key = $cache->makeKey( __METHOD__, $this->getName(), $vary );
- $scopeLock = $cache->getScopedLock( $key, 0 );
- if ( !$scopeLock ) {
- return; // T124649; avoid write slams
- }
- // No needless escaping as this isn't HTML output.
- // Only stored in the database and parsed in PHP.
- $deps = json_encode( $localPaths, JSON_UNESCAPED_SLASHES );
- $dbw = wfGetDB( DB_MASTER );
- $dbw->upsert( 'module_deps',
- [
- 'md_module' => $this->getName(),
- 'md_skin' => $vary,
- 'md_deps' => $deps,
- ],
- [ 'md_module', 'md_skin' ],
- [
- 'md_deps' => $deps,
- ]
- );
+ if ( $localPaths === $storedPaths ) {
+ // Unchanged. Avoid needless database query (especially master conn!).
+ return;
+ }
- if ( $dbw->trxLevel() ) {
- $dbw->onTransactionResolution(
- function () use ( &$scopeLock ) {
- ScopedCallback::consume( $scopeLock ); // release after commit
- },
- __METHOD__
- );
- }
+ // The file deps list has changed, we want to update it.
+ $vary = $context->getSkin() . '|' . $context->getLanguage();
+ $cache = ObjectCache::getLocalClusterInstance();
+ $key = $cache->makeKey( __METHOD__, $this->getName(), $vary );
+ $scopeLock = $cache->getScopedLock( $key, 0 );
+ if ( !$scopeLock ) {
+ // Another request appears to be doing this update already.
+ // Avoid write slams (T124649).
+ return;
+ }
+
+ // No needless escaping as this isn't HTML output.
+ // Only stored in the database and parsed in PHP.
+ $deps = json_encode( $localPaths, JSON_UNESCAPED_SLASHES );
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->upsert( 'module_deps',
+ [
+ 'md_module' => $this->getName(),
+ 'md_skin' => $vary,
+ 'md_deps' => $deps,
+ ],
+ [ 'md_module', 'md_skin' ],
+ [
+ 'md_deps' => $deps,
+ ]
+ );
+
+ if ( $dbw->trxLevel() ) {
+ $dbw->onTransactionResolution(
+ function () use ( &$scopeLock ) {
+ ScopedCallback::consume( $scopeLock ); // release after commit
+ },
+ __METHOD__
+ );
}
} catch ( Exception $e ) {
+ // Probably a DB failure. Either the read query from getFileDependencies(),
+ // or the write query above.
wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" );
}
}
'wgSiteName' => $conf->get( 'Sitename' ),
'wgDBname' => $conf->get( 'DBname' ),
'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
- 'wgAvailableSkins' => Skin::getSkinNames(),
'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
// MediaWiki sets cookies to have this prefix by default
'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
'wgCookieDomain' => $conf->get( 'CookieDomain' ),
'wgCookiePath' => $conf->get( 'CookiePath' ),
'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
- 'wgResourceLoaderMaxQueryLength' => $conf->get( 'ResourceLoaderMaxQueryLength' ),
'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
*/
public function getScript( ResourceLoaderContext $context ) {
global $IP;
+ $conf = $this->getConfig();
+
if ( $context->getOnly() !== 'scripts' ) {
return '/* Requires only=script */';
}
if ( $context->getDebug() ) {
$mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/mediawiki.log.js" );
}
- if ( $this->getConfig()->get( 'ResourceLoaderEnableJSProfiler' ) ) {
+ if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
$mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/profiler.js" );
}
// Perform replacements for mediawiki.js
$mwLoaderPairs = [
'$VARS.baseModules' => ResourceLoader::encodeJsonForScript( $this->getBaseModules() ),
+ '$VARS.maxQueryLength' => ResourceLoader::encodeJsonForScript(
+ $conf->get( 'ResourceLoaderMaxQueryLength' )
+ ),
];
$profilerStubs = [
'$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
'$CODE.profileScriptStart();' => 'mw.loader.profiler.onScriptStart( module );',
'$CODE.profileScriptEnd();' => 'mw.loader.profiler.onScriptEnd( module );',
];
- if ( $this->getConfig()->get( 'ResourceLoaderEnableJSProfiler' ) ) {
+ if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
// When profiling is enabled, insert the calls.
$mwLoaderPairs += $profilerStubs;
} else {
// Perform string replacements for startup.js
$pairs = [
'$VARS.wgLegacyJavaScriptGlobals' => ResourceLoader::encodeJsonForScript(
- $this->getConfig()->get( 'LegacyJavaScriptGlobals' )
+ $conf->get( 'LegacyJavaScriptGlobals' )
),
'$VARS.configuration' => ResourceLoader::encodeJsonForScript(
$this->getConfigSettings( $context )
*/
public function extend( $name, $value ) {
if ( $this->haveData( $name ) ) {
- $this->data[$name] = $this->data[$name] . $value;
+ $this->data[$name] .= $value;
} else {
$this->data[$name] = $value;
}
];
$joinConds = [
'revision' => [
- 'INNER JOIN', [
+ 'JOIN', [
'page_latest = rev_id'
]
],
protected function getFormFields() {
global $wgBlockAllowsUTEdit;
+ $this->getOutput()->enableOOUI();
+
$user = $this->getUser();
$suggestedDurations = self::getSuggestedDurations();
'type' => 'radio',
'cssclass' => 'mw-block-editing-restriction',
'options' => [
- $this->msg( 'ipb-sitewide' )->escaped() => 'sitewide',
- $this->msg( 'ipb-partial' )->escaped() => 'partial',
+ $this->msg( 'ipb-sitewide' )->escaped() .
+ new \OOUI\LabelWidget( [
+ 'classes' => [ 'oo-ui-inline-help' ],
+ 'label' => $this->msg( 'ipb-sitewide-help' )->text(),
+ ] ) => 'sitewide',
+ $this->msg( 'ipb-partial' )->escaped() .
+ new \OOUI\LabelWidget( [
+ 'classes' => [ 'oo-ui-inline-help' ],
+ 'label' => $this->msg( 'ipb-partial-help' )->text(),
+ ] ) => 'partial',
],
'section' => 'actions',
];
'pp_propname' => $this->propName,
],
'join_conds' => [
- 'page' => [ 'INNER JOIN', 'page_id = pp_page' ]
+ 'page' => [ 'JOIN', 'page_id = pp_page' ]
],
'options' => []
];
'OFFSET' => $offset
],
'join_conds' => [
- 'page' => [ 'INNER JOIN', 'cl_from = page_id' ]
+ 'page' => [ 'JOIN', 'cl_from = page_id' ]
]
];
$conds + $subconds,
__METHOD__,
$order + $query_options,
- $join_conds + [ $link_table => [ 'INNER JOIN', $subjoin ] ]
+ $join_conds + [ $link_table => [ 'JOIN', $subjoin ] ]
);
if ( $dbr->unionSupportsOrderAndLimit() ) {
$join_conds = array_merge(
[
'watchlist' => [
- 'INNER JOIN',
+ 'JOIN',
[
'wl_user' => $user->getId(),
'wl_namespace=rc_namespace',
// Force JOIN order per T106682 to avoid large filesorts
[ 'ORDER BY' => $fromCol, 'LIMIT' => 2 * $queryLimit, 'STRAIGHT_JOIN' ],
[
- 'page' => [ 'INNER JOIN', "$fromCol = page_id" ],
+ 'page' => [ 'JOIN', "$fromCol = page_id" ],
'redirect' => [ 'LEFT JOIN', $on ]
]
);
[],
__CLASS__ . '::showIndirectLinks',
[ 'ORDER BY' => 'page_id', 'LIMIT' => $queryLimit ],
- [ 'page' => [ 'INNER JOIN', "$fromCol = page_id" ] ]
+ [ 'page' => [ 'JOIN', "$fromCol = page_id" ] ]
);
};
// Outer query to select the recent edit counts for the selected active users
$tables = [ 'qcc_users' => $subquery, 'recentchanges' ];
- $jconds = [ 'recentchanges' => [
- 'JOIN', $useActor ? 'rc_actor = actor_id' : 'rc_user_text = qcc_title',
- ] ];
- $conds = [
+ $jconds = [ 'recentchanges' => [ 'LEFT JOIN', [
+ $useActor ? 'rc_actor = actor_id' : 'rc_user_text = qcc_title',
'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata.
'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes.
'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ),
'rc_timestamp >= ' . $dbr->addQuotes( $timestamp ),
- ];
+ ] ] ];
+ $conds = [];
return [
'tables' => $tables,
'qcc_title',
'user_name' => 'qcc_title',
'user_id' => 'user_id',
- 'recentedits' => 'COUNT(*)'
+ 'recentedits' => 'COUNT(rc_id)'
],
'options' => [ 'GROUP BY' => [ 'qcc_title' ] ],
'conds' => $conds,
$sortColumns = array_merge( [ $this->mIndexField ], $this->mExtraSortFields );
if ( $descending ) {
+ $dir = 'ASC';
$orderBy = $sortColumns;
$operator = $this->mIncludeOffset ? '>=' : '>';
} else {
+ $dir = 'DESC';
$orderBy = [];
foreach ( $sortColumns as $col ) {
$orderBy[] = $col . ' DESC';
}
$info = $this->getQueryInfo( [
'limit' => intval( $limit ),
- 'order' => $descending ? 'DESC' : 'ASC',
+ 'order' => $dir,
'conds' =>
$offset != '' ? [ $this->mIndexField . $operator . $this->mDb->addQuotes( $offset ) ] : [],
] );
$jcond = $rcQuery['fields']['rc_user'] . ' = ' . $imgQuery['fields']['img_user'];
}
$jconds['recentchanges'] = [
- 'INNER JOIN',
+ 'JOIN',
[
'rc_title = img_name',
$jcond,
$fields = array_merge( $rcQuery['fields'], [
'length' => 'page_len', 'rev_id' => 'page_latest', 'page_namespace', 'page_title'
] );
- $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ] + $rcQuery['joins'];
+ $join_conds = [ 'page' => [ 'JOIN', 'page_id=rc_cur_id' ] ] + $rcQuery['joins'];
// Avoid PHP 7.1 warning from passing $this by reference
$pager = $this;
throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
}
+ public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target ) {
+ return wfTimestampOrNull( TS_MW, $timestamp );
+ }
}
/** @var ActorMigration */
private $actorMigration;
+ /** @var WatchedItemStoreInterface */
+ private $watchedItemStore;
+
public function __construct(
LoadBalancer $loadBalancer,
CommentStore $commentStore,
- ActorMigration $actorMigration
+ ActorMigration $actorMigration,
+ WatchedItemStoreInterface $watchedItemStore
) {
$this->loadBalancer = $loadBalancer;
$this->commentStore = $commentStore;
$this->actorMigration = $actorMigration;
+ $this->watchedItemStore = $watchedItemStore;
}
/**
break;
}
+ $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
$items[] = [
new WatchedItem(
$user,
- new TitleValue( (int)$row->rc_namespace, $row->rc_title ),
- $row->wl_notificationtimestamp
+ $target,
+ $this->watchedItemStore->getLatestNotificationTimestamp(
+ $row->wl_notificationtimestamp, $user, $target
+ )
),
$this->getRecentChangeFieldsFromRow( $row )
];
$watchedItems = [];
foreach ( $res as $row ) {
+ $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
// todo these could all be cached at some point?
$watchedItems[] = new WatchedItem(
$user,
- new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
- $row->wl_notificationtimestamp
+ $target,
+ $this->watchedItemStore->getLatestNotificationTimestamp(
+ $row->wl_notificationtimestamp, $user, $target
+ )
);
}
private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
$joinConds = [
- 'watchlist' => [ 'INNER JOIN',
+ 'watchlist' => [ 'JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
<?php
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
* @param IDatabase $db Database connection being used for the query
* @param array &$items array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ).
* May be truncated if necessary, in which case $startFrom must be updated.
- * @param ResultWrapper|bool $res Database query result
+ * @param IResultWrapper|bool $res Database query result
* @param array|null &$startFrom Continuation value. If you truncate $items, set this to
* [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ] from the first item
* removed.
*/
private $loadBalancer;
+ /**
+ * @var JobQueueGroup
+ */
+ private $queueGroup;
+
+ /**
+ * @var BagOStuff
+ */
+ private $stash;
+
/**
* @var ReadOnlyMode
*/
*/
private $cache;
+ /**
+ * @var HashBagOStuff
+ */
+ private $latestUpdateCache;
+
/**
* @var array[] Looks like $cacheIndex[Namespace ID][Target DB Key][User Id] => 'key'
* The index is needed so that on mass changes all relevant items can be un-cached.
/**
* @param ILBFactory $lbFactory
+ * @param JobQueueGroup $queueGroup
+ * @param BagOStuff $stash
* @param HashBagOStuff $cache
* @param ReadOnlyMode $readOnlyMode
* @param int $updateRowsPerQuery
*/
public function __construct(
ILBFactory $lbFactory,
+ JobQueueGroup $queueGroup,
+ BagOStuff $stash,
HashBagOStuff $cache,
ReadOnlyMode $readOnlyMode,
$updateRowsPerQuery
) {
$this->lbFactory = $lbFactory;
$this->loadBalancer = $lbFactory->getMainLB();
+ $this->queueGroup = $queueGroup;
+ $this->stash = $stash;
$this->cache = $cache;
$this->readOnlyMode = $readOnlyMode;
$this->stats = new NullStatsdDataFactory();
$this->revisionGetTimestampFromIdCallback =
[ Revision::class, 'getTimestampFromId' ];
$this->updateRowsPerQuery = $updateRowsPerQuery;
+
+ $this->latestUpdateCache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
}
/**
*/
public function clearUserWatchedItemsUsingJobQueue( User $user ) {
$job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() );
- // TODO inject me.
- JobQueueGroup::singleton()->push( $job );
+ $this->queueGroup->push( $job );
}
/**
}
$dbr = $this->getConnectionRef( DB_REPLICA );
+
$row = $dbr->selectRow(
'watchlist',
'wl_notificationtimestamp',
$item = new WatchedItem(
$user,
$target,
- wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp )
+ $this->getLatestNotificationTimestamp( $row->wl_notificationtimestamp, $user, $target )
);
$this->cache( $item );
$watchedItems = [];
foreach ( $res as $row ) {
+ $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
// @todo: Should we add these to the process cache?
$watchedItems[] = new WatchedItem(
$user,
new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
- $row->wl_notificationtimestamp
+ $this->getLatestNotificationTimestamp(
+ $row->wl_notificationtimestamp, $user, $target )
);
}
);
foreach ( $res as $row ) {
+ $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
$timestamps[$row->wl_namespace][$row->wl_title] =
- wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp );
+ $this->getLatestNotificationTimestamp(
+ $row->wl_notificationtimestamp, $user, $target );
}
return $timestamps;
$timestamp = $dbw->timestamp( $timestamp );
}
- $success = $dbw->update(
+ $dbw->update(
'watchlist',
[ 'wl_notificationtimestamp' => $timestamp ],
$conds,
$this->uncacheUser( $user );
- return $success;
+ return true;
+ }
+
+ public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target ) {
+ $timestamp = wfTimestampOrNull( TS_MW, $timestamp );
+ if ( $timestamp === null ) {
+ return null; // no notification
+ }
+
+ $seenTimestamps = $this->getPageSeenTimestamps( $user );
+ if (
+ $seenTimestamps &&
+ $seenTimestamps->get( $this->getPageSeenKey( $target ) ) >= $timestamp
+ ) {
+ // If a reset job did not yet run, then the "seen" timestamp will be higher
+ return null;
+ }
+
+ return $timestamp;
}
public function resetAllNotificationTimestampsForUser( User $user ) {
* @return bool
*/
public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
+ $time = time();
+
// Only loggedin user can have a watchlist
if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
return false;
}
}
+ // Mark the item as read immediately in lightweight storage
+ $this->stash->merge(
+ $this->getPageSeenTimestampsKey( $user ),
+ function ( $cache, $key, $current ) use ( $time, $title ) {
+ $value = $current ?: new MapCacheLRU( 300 );
+ $value->set( $this->getPageSeenKey( $title ), wfTimestamp( TS_MW, $time ) );
+
+ $this->latestUpdateCache->set( $key, $value, IExpiringStore::TTL_PROC_LONG );
+
+ return $value;
+ },
+ IExpiringStore::TTL_HOUR
+ );
+
// If the page is watched by the user (or may be watched), update the timestamp
$job = new ActivityUpdateJob(
$title,
'type' => 'updateWatchlistNotification',
'userid' => $user->getId(),
'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ),
- 'curTime' => time()
+ 'curTime' => $time
]
);
+ // Try to enqueue this post-send
+ $this->queueGroup->lazyPush( $job );
- // Try to run this post-send
- // Calls DeferredUpdates::addCallableUpdate in normal operation
- call_user_func(
- $this->deferredUpdatesAddCallableUpdateCallback,
- function () use ( $job ) {
- $job->run();
+ $this->uncache( $user, $title );
+
+ return true;
+ }
+
+ /**
+ * @param User $user
+ * @return MapCacheLRU|null
+ */
+ private function getPageSeenTimestamps( User $user ) {
+ $key = $this->getPageSeenTimestampsKey( $user );
+
+ return $this->latestUpdateCache->getWithSetCallback(
+ $key,
+ IExpiringStore::TTL_PROC_LONG,
+ function () use ( $key ) {
+ return $this->stash->get( $key ) ?: null;
}
);
+ }
- $this->uncache( $user, $title );
+ /**
+ * @param User $user
+ * @return string
+ */
+ private function getPageSeenTimestampsKey( User $user ) {
+ return $this->stash->makeGlobalKey(
+ 'watchlist-recent-updates',
+ $this->lbFactory->getLocalDomainID(),
+ $user->getId()
+ );
+ }
- return true;
+ /**
+ * @param LinkTarget $target
+ * @return string
+ */
+ private function getPageSeenKey( LinkTarget $target ) {
+ return "{$target->getNamespace()}:{$target->getDBkey()}";
}
private function getNotificationTimestamp( User $user, Title $title, $item, $force, $oldid ) {
* @return int|bool
*/
public function countUnreadNotifications( User $user, $unreadLimit = null ) {
+ $dbr = $this->getConnectionRef( DB_REPLICA );
+
$queryOptions = [];
if ( $unreadLimit !== null ) {
$unreadLimit = (int)$unreadLimit;
$queryOptions['LIMIT'] = $unreadLimit;
}
- $dbr = $this->getConnectionRef( DB_REPLICA );
- $rowCount = $dbr->selectRowCount(
- 'watchlist',
- '1',
- [
- 'wl_user' => $user->getId(),
- 'wl_notificationtimestamp IS NOT NULL',
- ],
- __METHOD__,
- $queryOptions
- );
+ $conds = [
+ 'wl_user' => $user->getId(),
+ 'wl_notificationtimestamp IS NOT NULL'
+ ];
+
+ $rowCount = $dbr->selectRowCount( 'watchlist', '1', $conds, __METHOD__, $queryOptions );
- if ( !isset( $unreadLimit ) ) {
+ if ( $unreadLimit === null ) {
return $rowCount;
}
*/
public function removeWatchBatchForUser( User $user, array $targets );
+ /**
+ * Convert $timestamp to TS_MW or return null if the page was visited since then by $user
+ *
+ * Use this only on single-user methods (having higher read-after-write expectations)
+ * and not in places involving arbitrary batches of different users
+ *
+ * Usage of this method should be limited to WatchedItem* classes
+ *
+ * @param string|null $timestamp Value of wl_notificationtimestamp from the DB
+ * @param User $user
+ * @param LinkTarget $target
+ * @return string TS_MW timestamp or null
+ */
+ public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target );
}
"formerror": "Error: No s'han pogut enviar les dades del formulari.",
"badarticleerror": "Aquesta operació no es pot dur a terme en aquesta pàgina.",
"cannotdelete": "No s'ha pogut suprimir la pàgina o fitxer «$1».\nPotser ja l'ha suprimit algú altre.",
- "cannotdelete-title": "No es pot suprimir la pàgina \" $1 \"",
+ "cannotdelete-title": "No es pot suprimir la pàgina «$1»",
"delete-scheduled": "S'ha programat la pàgina «$1» per ser eliminada.\nTingueu paciència.",
"delete-hook-aborted": "Un «hook» ha interromput la supressió.\nNo ha donat cap explicació.",
"no-null-revision": "No s'ha pogut crear una nova revisió nul·la de la pàgina «$1»",
"log-action-filter-suppress-delete": "Supressió de pàgines",
"log-action-filter-upload-upload": "Càrrega nova",
"log-action-filter-upload-overwrite": "Torna a carregar",
+ "log-action-filter-upload-revert": "Reverteix",
"authmanager-authn-not-in-progress": "L'autenticació no està en curs o les dades de sessió s'han perdut. Comenceu de nou des del principi.",
"authmanager-authn-no-primary": "Les dades credencials no s'han pogut autenticar.",
"authmanager-authn-autocreate-failed": "Ha fallat la creació automàtica d'un compte local: $1",
"unprotect": "Starnayışi bıvurne",
"newpage": "Perra newi",
"talkpagelinktext": "werênayış",
- "specialpage": "Perra xısusiye",
+ "specialpage": "Pela xısusiye",
"personaltools": "Hacetê şexsiy",
"talk": "Werênayış",
"views": "Asayışi",
"showtoc": "bımocne",
"hidetoc": "bınımne",
"collapsible-collapse": "Teng ke",
- "collapsible-expand": "Hera kerê",
+ "collapsible-expand": "Hira ke",
"confirmable-confirm": "{{GENDER:$1|Şıma}} pêbawerê?",
"confirmable-yes": "Eya",
"confirmable-no": "Nê",
"nstab-main": "Pele",
"nstab-user": "Pera karberi",
"nstab-media": "Pela medya",
- "nstab-special": "Perra xısusiye",
+ "nstab-special": "Pela xısusiye",
"nstab-project": "Perra proji",
"nstab-image": "Dosya",
"nstab-mediawiki": "Mesac",
"unstrip-depth-warning": "Sinorê newekerdışê ($1) viyarna ra",
"unstrip-size-warning": "Sinorê newekerdışê ($1) viyarna ra",
"converter-manual-rule-error": "Rehberê zıwan açarnayışi dı xırabin tesbit biya",
- "undo-success": "No vurnayiş tepeye geryeno. pêverronayişêyê cêrıni kontrol bıkeri.",
- "undo-failure": "Poxta pëverameyişa vurnayişan ra peyd grotışë kari në bı",
- "undo-norev": "Vurnayiş tepêya nêgeryeno çunke ya vere cû hewna biyo ya zi ca ra çino.",
- "undo-summary": "Vırnayışê $1'i [[Special:Contributions/$2|$2i]] ([[User talk:$2|Werênayış]]) peyser gırewt",
+ "undo-success": "Eno vurnayış şeno peyser bıgêriyo. Kerem ke, têvereştışê cêrêni kontrol ke, waştena nê vurnayışi kerdene rê bawer be û be qeydkerdışê pele ra vurnayışê vêrêni peyser bıgê.",
+ "undo-failure": "Seba vurnayışanê yewbininêgırewteyan ra vurnayış peyser nêgêriya.",
+ "undo-norev": "Vurnayış peyser nêgêriyeno, çıke çıniyo ya zi esteriyayo.",
+ "undo-nochange": "Vurnayış xora zey peysergırewte aseno.",
+ "undo-summary": "[[Special:Contributions/$2|$2]]i ([[User talk:$2|werênayış]]) vurnayışê $1i peyser gırewt",
"undo-summary-username-hidden": "Rewizyona veri $1'i hewada",
"cantcreateaccount-text": "Hesabvıraştışê na IP adrese ('''$1''') terefê [[User:$3|$3]] kılit biyo.\n\nSebebo ke terefê $3 ra diyao ''$2''",
"viewpagelogs": "Qeydanê na pele bımocne",
"lineno": "Xeta $1:",
"compareselectedversions": "Rewizyonanê weçineyan pêver ke",
"showhideselectedversions": "weçinaye revizyona bımotne/bınımne",
- "editundo": "Peyser bıgêre",
+ "editundo": "peyser bıgê",
"diff-empty": "(Babetna niyo)",
"diff-multi-sameuser": "(Terefê eyni karberi ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})",
"diff-multi-otherusers": "(Terefê {{PLURAL:$2|yew karberi|$2 karberan}} ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})",
"tooltip-t-emailuser": "{{GENDER:$1|Enê karberi}} rê yew e-poste bırışe",
"tooltip-t-info": "Derheqê ena pele de zêdêr melumat",
"tooltip-t-upload": "Dosyeyan bar ke",
- "tooltip-t-specialpages": "Listeya peranê hısusiyan hemın",
+ "tooltip-t-specialpages": "Yew lista pelanê xısusiyanê pêroyinan",
"tooltip-t-print": "Versiyono perre ro ke nuşterniyaye.",
"tooltip-t-permalink": "Gırêyo daimi be ena versiyonê pele",
"tooltip-ca-nstab-main": "Pela zerreki bıvêne",
"tooltip-recreate": "pel hewn a bışiyo zi tepiya biya",
"tooltip-upload": "Sergen de bari be",
"tooltip-rollback": "\"Peyser biya\" be yew tık pela iştıraqanê peyênan peyser ano",
- "tooltip-undo": "\"Undo\" ena vurnayışê newi iptal kena u vurnayışê verni a kena.\nTı eşkeno yew sebeb bınus.",
+ "tooltip-undo": "\"Peyser\" nê vurnayışi peyser ano û modusê verqayti de vurnayışê formi keno a. Têserkerdışê yew sebebi rê xulasa de imkan dano cı.",
"tooltip-preferences-save": "Terciha qeyd ke",
"tooltip-summary": "Xulasa kılmek bınuse",
"interlanguage-link-title": "$1 - $2",
"version": "Versiyon",
"version-extensions": "Ekstensiyonî ke ronaye",
"version-skins": "Bar kerde bejni",
- "version-specialpages": "Perê hısusiy",
+ "version-specialpages": "Pelê xısusiyi",
"version-parserhooks": "Çengelê Parserî",
"version-variables": "Vurnayeyî",
"version-editors": "Vurnayoği",
"tag-mw-blank-description": "Vengiya na pele bıvurne",
"tag-mw-replace": "Zerrek vurriya",
"tag-mw-rollback": "Peyserardış",
- "tag-mw-undo": "Peyser bıgêrê",
+ "tag-mw-undo": "Peyser bıgê",
"tags-title": "Etiketi",
"tags-intro": "Ena pele etiketê ke be vurnayışê nuşiyayışi ra nişan biyê û maneyê inan lista kena.",
"tags-tag": "Nameyê etiketi",
"htmlform-int-toohigh": "Ena değer ke ti spesife kerd maxsimumê $1î ra zafyer o.",
"htmlform-required": "Ena deger lazim o",
"htmlform-submit": "Bişirav",
- "htmlform-reset": "Vurnayişî reyna biyar",
+ "htmlform-reset": "Vurnayışan peyser bıgê",
"htmlform-selectorother-other": "Sewbi",
"htmlform-no": "Nê",
"htmlform-yes": "Eya",
"ipb-confirm": "Confirm block",
"ipb-sitewide": "Sitewide",
"ipb-partial": "Partial",
+ "ipb-sitewide-help": "Every page on the wiki and all other contribution actions.",
+ "ipb-partial-help": "Specific pages or namespaces.",
"ipb-pages-label": "Pages",
"ipb-namespaces-label": "Namespaces",
"badipaddress": "Invalid IP address",
"rcfilters-filter-pageedits-label": "Panacheo sompadonam",
"rcfilters-filter-categorization-label": "Vorgache bodol",
"rcfilters-filtergroup-lastRevision": "Akherchim uzollnnim",
+ "rcfilters-filter-lastrevision-label": "Sogleanvon novi uzollnni",
"rcfilters-tag-prefix-namespace-inverted": "$1 <strong>:nhoi</strong>",
"rcnotefrom": "Sokoil <strong>$3, $4<strong> savn {{PLURAL:$5|zalelem bodol dilam|zalelem bodol dileant}} (<strong>$1<strong> meren {{PLURAL:$5|dakhoilam|dakhoileant}}).",
"rclistfrom": "$3 $2 savn suru zatelim nove bodol dakhoi",
"anoncontribs": "Közreműködések",
"contribsub2": "$1 ($2)",
"contributions-userdoesnotexist": "Nincs regisztrálva „$1” szerkesztői azonosító.",
+ "negative-namespace-not-supported": "Negatív értékű névterek nem támogatottak.",
"nocontribs": "Nem található a feltételeknek megfelelő változtatás.",
"uctop": "aktuális",
"month": "E hónap végéig:",
"logentry-rights-autopromote": "$1 automatikusan előléptetve erről: $4 erre: $5",
"logentry-upload-upload": "$1 {{GENDER:$2|feltöltötte}} ezt: $3",
"logentry-upload-overwrite": "$1 $3 új verzióját {{GENDER:$2|töltötte}} fel",
- "logentry-upload-revert": "$1 {{GENDER:$2|feltöltötte}} $3-t",
+ "logentry-upload-revert": "$1 {{GENDER:$2|visszaállította}} „$3”-t egy régebbi változatra",
"log-name-managetags": "Címkekezelési napló",
"log-description-managetags": "Ez a lap a [[Special:Tags|címkék]] kezelésével kapcsolatos tevékenységeket listázza. A napló csak azokat a műveleteket tartalmazza, amelyet az adminisztrátorok kézzel hajtottak végre; a wikiszoftver képes naplóbejegyzés nélkül is létrehozni és törölni címkéket.",
"logentry-managetags-create": "$1 {{GENDER:$2|létrehozta}} a(z) „$4” címkét",
"log-action-filter-suppress-reblock": "Felhasználó elrejtést újra blokkolással",
"log-action-filter-upload-upload": "Új feltöltés",
"log-action-filter-upload-overwrite": "Újrafeltöltés",
+ "log-action-filter-upload-revert": "Visszaállítás",
"authmanager-authn-not-in-progress": "Hitelesítés nincs folyamatban, vagy a folyamat adatai elvesztek. Kérjük, indítsd újra az elejétől.",
"authmanager-authn-no-primary": "A megadott hitelesítő adatokkal nem lehet hitelesíteni.",
"authmanager-authn-no-local-user": "A megadott hitelesítő adatok nincsenek társítva egyetlen felhasználóval sem ezen a wikin.",
"anontalk": "Diskuto relatant ad ica IP",
"navigation": "Navigado",
"and": " ed",
- "faq": "Maxim komuna questioni",
+ "faq": "Dubi maxim frequa (FAQ)",
"actions": "Agi",
"namespaces": "Nomari",
"variants": "Varianti",
"tooltip-recreate": "Na pele esterıte bo ki, nae oncia bıaferne",
"tooltip-upload": "Dest be bar-kerdene ke",
"tooltip-rollback": "\"Peyser biya\" ebe jü tık pela iştırakunê peyênu peyser ano.",
- "tooltip-undo": "\"Peyser\" ni vurnaişi peyser ano u modusê verqayt de vurnaisê formi keno ra.\nTêser-kerdena jü sebebi rê xulasa de imkan dano cı.",
+ "tooltip-undo": "\"Peyser\" ni vurnayişi peyser ano u modusê verqayti de vurnayisê formi keno ra. Têserkerdena jü sebebi rê xulasa de imkan dano cı.",
"tooltip-summary": "Xulasê da kılme cı kuye",
"common.css": "/* CSSo ke itaro, serba çermu pêroine gurenino */",
"pageinfo-contentpage-yes": "Heya",
"Jay94ks",
"Ryuch",
"Delim",
- "Comjun04"
+ "Comjun04",
+ "Son77391"
]
},
"tog-underline": "링크에 밑줄 긋기:",
"blocklist-nousertalk": "자신의 토론 문서 편집 불가",
"blocklist-editing": "편집 중",
"blocklist-editing-sitewide": "편집 중 (사이트 전체)",
+ "blocklist-editing-page": "문서",
"blocklist-editing-ns": "이름공간",
"ipblocklist-empty": "차단 목록이 비어 있습니다.",
"ipblocklist-no-results": "요청한 IP 주소나 사용자는 차단되지 않았습니다.",
"monday": "دۏشٱمٱ",
"tuesday": "ساْ شٱمٱ",
"wednesday": "چارشٱمٱ",
- "thursday": "پٱن شمٱ",
+ "thursday": "پٱÙ\86 شٱÙ\85Ù±",
"friday": "جۏمٱ",
"saturday": "شٱمٱ",
"sun": "یاٛشٱمٱ",
"february-gen": "فڤریٱ",
"march-gen": "مارس",
"april-gen": "آڤریل",
- "may-gen": "Ù\85ئی",
- "june-gen": "جۊٱن",
+ "may-gen": "Ù\85اÙ\9bی",
+ "june-gen": "ژوئٱن",
"july-gen": "جۊلای",
"august-gen": "آگوست",
"september-gen": "سپتامر",
"category-subcat-count-limited": "ئی دأسە ها د {{PLURAL:$1|زیردأسە|$1 زیردأسە یا}} یی کئ ها ڤئ دومئشوٙ",
"category-article-count": "{{PLURAL:$2|اؽ دٱسٱ د ڤٱرگرتٱ بٱلگٱ نهاییٱ.| {{PLURAL:$1| بٱلگٱ هؽ|$1 بٱلگٱیا هؽسن}} د اؽ دٱسٱ، ڤ دٱر د $2 کولٛ.}}",
"category-article-count-limited": "نئها {{PLURAL:$1|بألگە هی|$1بألگە یا هئن}} د دأسە ئیسئنی.",
- "category-file-count": "{{PLURAL:$2|اÛ\8c دٱسٱ Ù\81Ù±Ù\82ٱت د ڤٱرگرتٱ جاÙ\86Û\8cا Ù\86ئÙ\87اÛ\8cÛ\8cÙ±.| Ù\86ئÙ\87اÛ\8cÛ\8c {{PLURAL:$1|جاÙ\86Û\8cا Ù\87Û\8c|$1 جاÙ\86Û\8cاÛ\8cا Ù\87Û\8cÙ\86}} د اÛ\8c دٱسٱØ\8c Ú¤ دٱر د Ú©Ù\88Ù\84 $2 .}}",
+ "category-file-count": "{{PLURAL:$2|اÛ\8c دٱسٱ Ù\81Ù\82ٱت د ڤٱرگرتٱ جاÙ\86ؽا Ù\86Ù\87اÛ\8cÛ\8cÙ±.| Ù\86Ù\87اÛ\8cÛ\8c {{PLURAL:$1|جاÙ\86ؽا Ù\87ؽ|$1 جاÙ\86Û\8cاÛ\8cا Ù\87ؽسÙ\86}} د اؽ دٱسٱØ\8c Ú¤ دٱر د Ú©Ù\88Ù\84Ù\9b $2 .}}",
"category-file-count-limited": " {{PLURAL:$1|[جانیا هی|1$جانیایا هین}} نئهایی هان د دأسە ئیسئنی.",
"listingcontinuesabbrev": "دومالٱ",
"index-category": "بألگە یا سیاە دار",
"noindex-category": "بلگٱیا بی سیائٱ",
"broken-file-category": "بألگە یایی کئ هوم پئیڤأند جانیایا ئشگئسئ نە دارئن",
"categoryviewer-pagedlinks": "($1) ($2)",
- "about": "دئبارە",
+ "about": "دٱربارٱ",
"article": "مینوٙنە یا بألگە",
"newwindow": "(د یاٛ نیمدری تازٱ ڤازش کو)",
"cancel": "ٱنجوم شیڤسن",
"viewsourceold": "سئیل د سأرچئشمە بأکیت",
"editlink": "ڤیرایش",
"viewsourcelink": "ساٛلٛ د سرچشمٱ بٱکؽت",
- "editsectionhint": "ڤیرایش یاٛ بٱرجا:$1",
+ "editsectionhint": "Ú¤Û\8cراÛ\8cØ´ Û\8cاÙ\9b بٱئرجا:$1",
"toc": "مؽنونٱیا",
"showtoc": "نئشوٙ دأئن",
"hidetoc": "قام کئردئن",
"nstab-user": "بٱلگٱ کاریار",
"nstab-media": "بألگە ڤارئسگأر",
"nstab-special": "بٱلگٱیا ڤیژٱ",
- "nstab-project": "بألگە پوروجە",
+ "nstab-project": "بٱلگٱ پرۉژٱ",
"nstab-image": "جانؽا",
"nstab-mediawiki": "پئیغوٙم",
"nstab-template": "چۊٱ",
"mainpage-nstab": "سرآسونٱ",
"nosuchaction": "چئنی کونئشتگأری نییئش",
"nosuchactiontext": "کاری کئ ڤا یوٙ آر ئل تیار بییە نادیارە.\nگاسی شوما یوٙ آر ئل نە دوروس نأنیسأنیتە، یا یئ گئل هوم پئیڤأند ئشتئڤا ڤارئد بییە.\nڤئ گاسی یئ گئل سیسئریک د نأرم أفزاز ڤئ کار گئرئتە بییە ڤا {{SITENAME}} ئشارە بأکە.",
- "nosuchspecialpage": "چئنی بألگە ڤیجە یی نییئش",
- "nospecialpagetext": "<strong>Ø´Ù\88Ù\85ا Û\8cئ گئÙ\84 بأÙ\84Ú¯Û\95 Ù\86ادÛ\8cار Ù\86Û\95 Ù\87استÛ\8cتÛ\95.</strong>\nگاسÛ\8c Û\8cئ گئÙ\84 Ù\86Ù\88Ù\85Ú¯Û\95 سÛ\8c دÛ\8cارÛ\8c دأئÙ\86 د بأÙ\84Ú¯Û\95 Û\8cا باÛ\8cأد د [[Special:SpecialPages|{{int:specialpages}}]] دÛ\8cارÛ\8c بأکÛ\95.",
+ "nosuchspecialpage": "چنی بٱلگاٛ ڤیژاٛیی نؽسش",
+ "nospecialpagetext": "<strong>Ø´Ù\85ا Û\8cاÙ\9b بٱÙ\84Ú¯Ù± Ù\86ادؽار Ù\86اÙ\92 Ù\87استؽتٱ.</strong>\nگاسÛ\8c Û\8cاÙ\9b Ù\86Ù\88Ù\85Ú¯Ù± سÛ\8c دؽارÛ\8c داÙ\9bئÙ\86 د بٱÙ\84Ú¯Ù±Û\8cا باÛ\8cٱد د [[Special:SpecialPages|{{int:specialpages}}]] دؽارÛ\8c بٱکٱ.",
"error": "خأطا",
"databaseerror": "خأطا د رئسینە گا",
"databaseerror-text": "یئ گئل خأطا جوست کاری د رئسینە گا دیاری کئردە.گاسی یە یئ گل سیسئریک د کار گئرئتئن نأرم أفزار راس بأکە.",
"cannotdelete-title": "نأبوٙە بألگە $1 پاکسا با",
"delete-hook-aborted": "پاکسا کاری ڤا قولاڤ نئها گئری بیە.\nهیچ توضیی سیش نی.",
"no-null-revision": "سی بألگە $1 ڤانیأری خومثا نە راس بأکیت",
- "badtitle": "داسوٙن گأن",
+ "badtitle": "داسوݩ گٱن",
"badtitletext": "داسوݩ بٱلگٱ هاستنی نادؽارٱ، یٱ یاٛ داسوݩ مؽنجا زڤونی یا مؽنجا ڤیکی اْشتبائٱ.\nگاسؽ یٱ د ڤٱر گرتٱ یاٛ کاراکتر یا چٱن تا کاراکتر با کاْ نمۊئٱ د داسونؽا ڤ کارشو گرت.",
"title-invalid-empty": "داسوٙن بألگە هاستئنی حالیە یا فأقأط مینوٙنە دار یئ گئل نوٙم یا نوٙم جاە.",
"title-invalid-utf8": "داسوٙن بألگە هاستئنی مینوٙنە دار یئ گئل نئماجا UTF-8 نادیارە.",
"perfcachedts": "رئسینە یا نئهایی د ڤیرگە قام بییە موٙکیس بینە و گاسی هأنی ڤئ هئنگوم سازی نأبینە.بیشتئروٙنە {{PLURAL:$4|یئ گئل نأتیجە|$4 یئ گئل نأتیجە}} د ڤیرگە قام بییە هان د دأسرئس.",
"querypage-no-updates": "نأبوٙە ئی بألگە ڤئ هئنگوم سازی با.\nرئسینە یا ئیچئ تازە کاری نأبینە.",
"viewsource": "ساٛلٛ د سرچشمٱ بٱکؽت",
- "viewsource-title": "سئÛ\8cÙ\84 د سأرÚ\86ئشÙ\85Û\95 $1 بأکÛ\8cت",
+ "viewsource-title": "ساÙ\9bÙ\84Ù\9b د سرÚ\86Ø´Ù\85Ù± $1 بٱکؽت",
"actionthrottled": "کونئشتکاری نئهاگئری بییە",
"actionthrottledtext": "سی نئهاگئری د دأرتیچ بییئن ئسپأم نأبوٙە کئ شوما چئنی کاری نە د یئ گاتی کوٙتا چأن گئل أنجوم بئییت.\nلوطف بأکیت د چأن دئیقە هأنی د نۊ تئلاش بأکیت.",
"protectedpagetext": "نأبوٙە د ئی بألگە ڤیرایئشت کاریا کاریاریا هأنی نە سئیل بأکیت.",
- "viewsourcetext": "Ø´Ù\88Ù\85ا Ù\85Û\8c تÛ\8aÙ\86Û\8cت سرÚ\86Ø´Ù\85Ù± اÛ\8c بÙ\84Ú¯Ù± Ù\86اÙ\9b ساÙ\9bÛ\8cÙ\84 بٱکÛ\8cت Ù\88 داÙ\9bØ´ Û\8bردارÛ\8cت:",
+ "viewsourcetext": "Ø´Ù\85ا Ù\85ؽ تÙ\88Ù\86ؽت سرÚ\86Ø´Ù\85Ù± اؽ بٱÙ\84Ú¯Ù± Ù\86اÙ\92 ساÙ\9bÙ\84Ù\9b بٱکؽت Û\89 دش ڤردارؽت:",
"viewyourtext": "شوما می توٙنیت سأرچئشمە ڤیرایئشتیا توٙنە د ئی بألگە سئیل بأکیت و دئشوٙ ڤئرداریت:",
"protectedinterface": "ئی بألگە سی نأرم أفزار کئ ها د ئی ڤیکی نیسئسە آمادە میکە،و ڤئ د موزاحئمە ت کاری پأر و پیم کاری بیە\nسی ئضاف کئردئن یا آلئشت دأئن د هأمە ڤیکی یا لوطف بأکیت [https://translatewiki.net/ translatewiki.net] نە ڤئ کار بئیریت، پوروجە ڤولات نئشین سازی ڤیکیمئدیا.",
"editinginterface": "<strong>ڤارئسکاری کئردئن:</strong> شوما داریت یئ گئل بألگە نە کئ سی یئ گئل نیسئسە یا نأرم أفزار پئیڤأندکار ڤئ کار گئرئتە بیە ڤیرایئشت میکیت.\nآلئشت دأئن ئی بألگە ری رئخت و بارت پئیڤأندکاری کئ کاریاری هأنی ڤئ نە ڤئ کار مئیرئن کارگئرایی دارە.",
"yourpassword": "رازینە گوڤاردئن:",
"userlogin-yourpassword": "رازینٱ گوڤاردن",
"userlogin-yourpassword-ph": "رازینٱ گوئارسناْ بٱزاْ",
- "createacct-yourpassword-ph": "رازینە گوڤاردئن نە بأزە",
+ "createacct-yourpassword-ph": "رازینٱ گوئاردن ناْ بٱزاْ",
"yourpasswordagain": "یئ گئل هأنی رازینە گوڤاردئن نە بأزە",
- "createacct-yourpasswordagain": "رازینە گوڤاردئن نە پوشت راس کو",
- "createacct-yourpasswordagain-ph": "یاٛ گاٛل هٱنی رازینٱ گوڤاردن بٱزٱ",
- "userlogin-remembermypassword": "مئنە د ساموٙنە ڤادار",
+ "createacct-yourpasswordagain": "رازینٱ گوئاردن ناْ پوشت دۏرس کو",
+ "createacct-yourpasswordagain-ph": "یاٛ گلٛ هنی رازینٱ گوئاردن بٱزٱ",
+ "userlogin-remembermypassword": "مناْ د سامونٱ ڤادار",
"userlogin-signwithsecure": "ڤأصل بییئن أمن نە ڤئ کار بئیر",
"yourdomainname": "پوشگئر شوما:",
"password-change-forbidden": "شوما نئمی توٙنیت رازینە گوڤاردئن خوتوٙنە د ئی ڤیکی آلئشت بأکیت.",
"logout": "د ساموٙنە دئرئوٙمائن",
"userlogout": "د ساموٙنە دئرئوٙمائن",
"notloggedin": "نأبوٙأ بیائیت ڤامین",
- "userlogin-noaccount": "Û\8cئ گئÙ\84 Øئساڤ Ù\86ارÛ\8cت؟",
- "userlogin-joinproject": "أندوم دیارگە {{SITENAME}} بوٙئیت",
+ "userlogin-noaccount": "Û\8cاÙ\9b Ù\87ساÙ\88 Ù\86ارؽت؟",
+ "userlogin-joinproject": "ٱندوم دؽارگٱ {{SITENAME}} بۊئؽت",
"createaccount": "هساو دۏرس بٱکؽت",
"userlogin-resetpassword-link": "رازینٱ گوئارسن تو د ڤیرتو رٱتٱ؟",
- "userlogin-helplink2": "Ù\87Ù\88Ù\85Û\8cارÛ\8c کئردئÙ\86 د طأرÛ\8cÙ\82 ڤاÙ\85Û\8cÙ\86 ئÙ\88Ù\99Ù\85ائن",
+ "userlogin-helplink2": "Ù\87Ù\88Ù\85Û\8cارÛ\8c کردÙ\86 د تٱرÛ\8cÙ\82 ڤاÙ\85ؽÙ\86 اÙ\88Ù\85اÛ\8cن",
"userlogin-loggedin": "شوما ئیسئ چی یئ گئل {{GENDER:$1|$1}} ئوٙمایتە ڤامین.نوم بألگە هاری نە سی ڤامین ئوٙمائن چی یئ گئل کاریار هأنی بلگه هاری سی وا مین اومائن چی یه گل کاریار هنی ڤئ کار بئیریت.",
"userlogin-createanother": "یئ گئل حئساڤ هأنی راس بأکیت",
"createacct-emailrequired": "تیرنئشوٙن أنجومانامە",
- "createacct-emailoptional": "تیرنشۊن ٱنجومانامٱ",
- "createacct-email-ph": "تیرنشون انجومانامه تونه وارد بكيت",
+ "createacct-emailoptional": "تیرنشوݩ ٱنجومانامٱ",
+ "createacct-email-ph": "تیرنشوݩ ٱنجومانامٱ توناْ ڤارد بٱكؽت",
"createacct-another-email-ph": "تیرنئشوٙن أنجومانامە توٙنە بأزأنیت",
"createaccountmail": "یئ گئل رازینە گوڤاردئن موڤأقأتینە ڤئ کار بئیریت و ڤئ نەسی یئ گئل تیرنئشوٙن أنجومانامە تیار بییە کئل بأکیت.",
"createacct-realname": "نوم راستأکی(مأژبوٙری نی)",
"createacct-reason": "دألیل",
"createacct-reason-ph": "سی چی شوما داریت یئ گئل حئساڤ هأنی راس میکید",
- "createacct-submit": "حئسأڤ خوتوٙنە راس بأکیت",
+ "createacct-submit": "هساو خوتوناْ دۏرس بٱکؽت",
"createacct-another-submit": "یئ گئل حئساڤ هأنی راس بأکیت",
- "createacct-benefit-heading": "{{SITENAME}} ڤئ دأس خألکی چی شوما رأڤأندیاری بییە.",
- "createacct-benefit-body1": "{{PLURAL:$1|Ú¤Û\8cراÛ\8cئشت|Ú¤Û\8cراÛ\8cئشتÛ\8cا}}",
- "createacct-benefit-body2": "{{PLURAL:$1|بألگە|بألگە یا}}",
- "createacct-benefit-body3": "تازە{{PLURAL:$1|هومیار|ھومیاریا}}",
+ "createacct-benefit-heading": "{{SITENAME}} ڤ دٱس کٱسؽایؽ چی شما رٱڤٱندؽاری بیٱ.",
+ "createacct-benefit-body1": "{{PLURAL:$1|Ú¤Û\8cراÛ\8cØ´|Ú¤Û\8cراÛ\8cشؽا}}",
+ "createacct-benefit-body2": "{{PLURAL:$1|بٱلگٱ|بٱلگٱیا}}",
+ "createacct-benefit-body3": "تازٱ{{PLURAL:$1|هومیار|ھومیاریا}}",
"badretype": "رازینە گوڤاردئنی کئ شمأ دأییتە هومدأنگی نارە.",
"usernameinprogress": "رأرڤأندیاری یئ گئل حئساڤ سی ئی نوم کاریاری ھا د پیشکئرد. یئ گوری آھئرە داری بأکیت.",
"userexists": "نوم کاریاری دە بییە ئیسئنی ڤئ کار گئرئتە بییە.\nلوطف بأکیت یئ گئل نوم هأنی نە ڤئرداریت.",
"suspicious-userlogout": "د حاست ڤئ دأر رأتئن شوما تیە پوشی بییە سی یە کئ ڤئ نأظأر یما کئ ڤئ سی یئ گئل دوڤارتە نیأر گأن یا یئ گئل پوروکسی کئ ها د ڤیرگە کأش کئل بییە.",
"createacct-another-realname-tip": "نوم راستأکی دئل ڤئ حاییە.\nأر شوما ڤئنە نئها ئمایە بأکیت، یە سی هوم نئسبأت دأئن کاریاری سی کاریاش ڤئ کار گئرئتئ بوٙە.",
"pt-login": "ڤا مؽن اوماین",
- "pt-login-button": "ڤامین ئوٙمائن",
+ "pt-login-button": "ڤامؽن اوماین",
"pt-createaccount": "هساو دۏرس بٱکؽت",
"pt-userlogout": "د سامونٱ دروماین",
"php-mail-error-unknown": "خأطا نادیار د آلئشتگئر PHP's mail()",
"resetpass-expired": "گات دیاری رازینە گوڤاردئن شوما تأموم بییە. لوطف بأکیت یئ گئل رازینە گوڤاردئن هأنی نە سی ڤامین ئوٙمائن میزوٙنکاری بأکیت.",
"resetpass-expired-soft": "گات دیاری رازینە گوڤاردئن شوما تأموم بییە و باس د نۊ زئنە با. لوطف بأکیت یئ گئل رازینە گوڤاردئن هأنی نە ئنتئخاڤ بأکیت، یا سی د نۊ زئنە کئردئن د نئهاتئر د ئیچئ \"{{int:authprovider-resetpass-skip-label}}\" بأپوٙرنیت.",
"resetpass-validity-soft": "رازینە گوڤاردئن توٙ نادیاره:$1\n\n لوطف بأکیت یئ گئل رازینە گوڤاردئن هأنی نە ئنتئخاڤ بأکیت، یا سی د نۊ زئنە کئردئن د نئهاتئر د ئیچئ \"{{int:authprovider-resetpass-skip-label}}\" بأپوٙرنیت.",
- "passwordreset": "د Ù\86Û\8a دأئÙ\86 رازÛ\8cÙ\86Û\95 Ú¯Ù\88ڤاردئن",
+ "passwordreset": "د Ù\86Û\8a داÙ\9bئÙ\86 رازÛ\8cÙ± Ú¯Ù\88ئاردن",
"passwordreset-text-one": "ئی نوم بألگە نە سی گئرئتئن یئ گئل رازینە گوڤاردئن موڤأقأت ڤا أنجومانامە توٙ پور بأکیت.",
"passwordreset-text-many": "{{PLURAL:$1|یئ گئل د جاگە یا نە سی گئرئتئن رازینە گوڤاردئن موڤأقأتی نە ڤا أنجومانامە گئرئتە بوٙأ پور بأکیت.}}",
"passwordreset-disabled": "نۊ کئردئن رازینە گوڤاردئن د ئی ڤیکی ناکونئشگأر بییە.",
"accmailtitle": "رازینە گوڤاردئن کئل بی",
"accmailtext": "یئ گئل رازینە گوڤاردئن شامسأکی سی[[User talk:$1|$1]] سی $2 کئل بییە.بوٙە ڤئنە د گات ڤئ کار گئرئتئن بألگە ڤامین ئوٙمائن <em>[[Special:آلئشت دأئن رازینە گوڤاردئن|آلئشت دأئن رازینە گوڤاردئن]]</em> آلئشت کاری با.",
"newarticle": "تازە",
- "newarticletext": "Ø´Ù\88Ù\85ا Ù\87اÛ\8cÛ\8cÙ\86 ڤا دئÙ\85ا Ù\87Ù\88Ù\85 پئÛ\8cڤأÙ\86دÛ\8c کئ Ú¤Ù\88جÙ\88Ù\99د Ù\86ارÛ\95.\nسÛ\8c رأڤأÙ\86دÛ\8cارÛ\8c بأÙ\84Ú¯Û\95.Ø´Ù\88رÙ\88Ù\99 بأکÛ\8cت Ù\85Û\8cÙ\86ئ جأڤÛ\95 Ù\87ارÛ\8c بأÙ\86Û\8cسÛ\8cت (سÛ\8c دÙ\88Ù\99Ù\86ئسئÙ\86 بÛ\8cشتئر سئÛ\8cÙ\84 [$1 ] بأکÛ\8cت).\nأر Ø´Ù\88Ù\85ا سÛ\8c ئشتئڤا کئردئÙ\86 Ù\87ائÛ\8cت ئÛ\8cÚ\86ئØ\8c رÛ\8c دÙ\88Ú¯Ù\85Û\95 ڤادئÙ\85ا رأتئÙ\86 دÙ\88ڤارتÛ\95 Ù\86Û\8cأر بأپÙ\88Ù\99رÙ\86Û\8cت.",
+ "newarticletext": "Ø´Ù\85ا Ù\87ائؽت ڤا دÙ\85ا Ù\87Ù\88Ù\85 پاÙ\9bÚ¤Ù±Ù\86ؽ اÙ\92 Ú¤Ù\88جÛ\8aد Ù\86ارٱ.\nسÛ\8c رٱڤٱÙ\86دؽارÛ\8c بٱÙ\84Ú¯Ù±.شرÛ\8a بٱکؽت Ù\85ؽÙ\86 جٱڤٱ Ù\87Ù\88رÛ\8c بٱÙ\86Û\8cسؽت (سÛ\8c دÙ\88Ù\86سÙ\86 بؽشتر ساÙ\9bÙ\84Ù\9b [$1 ] بٱکؽت).\nٱر Ø´Ù\85ا سÛ\8c اÙ\92شتبا کردÙ\86 Ù\87ائؽت اÛ\8cÚ\86اÙ\92Ø\8c رÛ\8c دÛ\8fÚ¯Ù\85Ù± ڤادÙ\85ا رٱتÙ\86 دÙ\88ئارتٱ Ù\86Û\8cٱر بٱپÛ\8aرÙ\86ؽت.",
"anontalkpagetext": "----",
"noarticletext": "د ایسنؽا اؽ بٱلگٱ نیسسٱ ڤجۊد ناشتٱ.\nشما مؽ تونؽت د[[Special:Search/{{PAGENAME}}|بٱگٱردؽد]] د اؽ بٱلگٱ اؽ د بٱلگٱ هنی یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} د نۊ پاٛجۊری بۊئٱ]</span>، <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}} یا ای بٱلگٱ ناْ ڤیرایش بٱکؽت]</span>.",
"noarticletext-nopermission": "د ایسنؽا اؽ بٱلگٱ نیسساٛیؽ ڤجۊد ناشتٱ.\nشما مؽ تونؽت د[[Special:Search/{{PAGENAME}}|بٱگردؽد]] د اؽ بٱلگٱیا د بٱلگٱ هنی یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} د نۊ پاٛجۊری بۊئٱ]</span>، <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}}</span>.ڤلی شما سلا یٱناْ کاْ اؽ بٱلگٱ ناْ دۏرس بٱکؽت نارؽت.",
"session_fail_preview_html": "<strong>د بأخت گأن سی یە کئ رئسینە یا نئشأسجا نە د دأس دأئیمە نئمی توٙنیم کار پأردازئشت ڤیرایئشت کاری شومانە أنجوم بئمینوٙ.</strong>\n\n\n<em>سی یە کئ {{SITENAME}} یئ گئل رأگ ئچ تی ئم ئل کونئشتکار بییە دارە، پیش سئیل سی یە کئ د دأس چول کاریا جاڤا ئسکئریپت لیز داشتوٙە نئھوٙ بییە..</em>\n\nلوطف بأکیت یئ گئل ھأنی تئلاش بأکیت.\nأر ھأنی ڤئ دوروس کار نأکئرد،[[Special:UserLogout|ئوٙمائن ڤئ دأر]] نە ئزمایئشت بأکیت و د نۊ بیائیت ڤامین.",
"token_suffix_mismatch": "<strong>ڤیرایئشتیا شوما سی یە کئ دوڤارتە نیأر شوما نیسئسە یا نوقطە نیائن نە د رازینە أمینیأتی ڤیرایئشت د یأک تیچئسە رأد میکە.</strong>\nڤیرایئشت سی یە کئ د خئراڤ بییئن نیسئسە بألگە نئھاگئری با رأد بییە.\nئی روخ ڤأن د گاتیایی پیش میا کئ شوما یئ گئل رئسینە جا پوروکسی نە ڤئ کار بئیریت.",
"edit_form_incomplete": "<strong>پارە یی د ڤیرایئشتیا ڤئ رئسینە جا نئمی رئسئن، د نۊ ڤارئسی بأکیت سی یە کئ د خوٙ بییئن ڤیرایئشتیا خوتوٙ ڤارئسیاری بأکیت و د نۊ تئلاش بأکیت.</strong>",
- "editing": "د حال و بال ڤیرایئشت $1",
- "creating": "راس کئردئن $1",
- "editingsection": "د حال و بال ڤیرایئشت $1 (بأرجا$1)",
+ "editing": "د هال ۉ بال ڤیرایش $1",
+ "creating": "دÛ\8fرس کردن $1",
+ "editingsection": "د هال ۉ بال ڤیرایش $1 (بٱرجا$1)",
"editingcomment": "د حال و بال ڤیرایئشت $1 (بأرجا تازە)",
"editconflict": "ری ڤئ ری کاری د ڤیرایئشت: $1",
"explainconflict": "د گاتی کئ شوما شوروٙ د ڤیرایئشت کاری د بألگە کئردیتە، یئ کأس ھأنی ئی بألگە نئ آلئشت دئە.\nراساگە ڤارو نیسئسە بألگە، نیسئسە نە چی یە کئ ڤوجوٙد داشتوٙە د ڤأر گئرئتە.\nآلئشتکاریا شوم د نیسئسە ھاری دیاری میکە.\nشوما بایأد آلئشت کاریاتوٙنە د نیسئسە یی کئ ھیش سأریأک بأکیت.\nفأقأط نیسئسە یی کئ ھا د ڤارو د گاتی کئ شوما\"$1\" نە گوزارئشت میکیت ئمایە بوٙە.",
"templatesusedsection": "{{PLURAL:$1|چوٙأ|چوٙأ یا}} ڤئ کار گئرئتە بییە د ئی بأرجا:",
"template-protected": "(پٱر ۉ پیم بیٱ)",
"template-semiprotected": "(نسم ۉ نیمٱ پٱر ۉ پیم بیٱ)",
- "hiddencategories": "اؽ بٱلگٱ یٱکؽ د ٱندومیائٱ {{PLURAL:$1|1 hidden category|$1 hidden categories}} :",
+ "hiddencategories": "اؽ بٱلگٱ یٱکؽ د ٱندومؽا ٱ {{PLURAL:$1|1 hidden category|$1 hidden categories}} :",
"edittools-upload": "-",
"nocreatetext": "{{SITENAME}} سی رأڤأندیاری بألگە یا تازە نئھاگئری بییە.\nشوما می توٙنیت روئیت ڤادئما و بألگە ئی کئ بییشە ڤیرایئشت کاری بأکیت،[[Special:ڤامین ئوٙمائن کاریار|بیائیت ڤامین یا یە کئ یئ گئل حئساڤ دوروس بأکیت]].",
"nocreate-loggedin": "شوما صئلا راس کئردئن بألگە تازە نە ناریت.",
"sectioneditnotsupported-text": "ڤیرایئشت بأرجایی د ئی بألگە نیئش.",
"permissionserrors": "خأطا صئلا دأئن",
"permissionserrorstext": "شوما حأق ناریت ڤئنە أنجوم بئیت، سی{{PLURAL:$1|دألیل|دألیلیا}} نئھایی:",
- "permissionserrorstext-withaction": "Ø´Ù\88Ù\85ا سÛ\8c $2 صئÙ\84ا \nÙ\86ئھاگئرÛ\8c Ù\86ارÛ\8cت {{PLURAL:$1|دأÙ\84Û\8cÙ\84|دأÙ\84Û\8cÙ\84Û\8cا}}:",
+ "permissionserrorstext-withaction": "Ø´Ù\85ا سÛ\8c $2 سÙ\84ا \nÙ\86ھاگرÛ\8c Ù\86ارؽت {{PLURAL:$1|دÙ\84Ù\9bÛ\8cÙ\84Ù\9b|دÙ\84Ù\9bÛ\8cÙ\84Ù\9bؽا}}:",
"recreate-moveddeleted-warn": "'''د ڤیرئتوٙ با:شوما بألگە یی کئ ھا ڤادئما و پاکسا بییە د نۊ راس کئردیتە.'''\nبایأد د ڤیرئتوٙ با کئ آیا ھأنی نئھاگئری ڤیرایئشت ئی بألگە خوٙأ.\nپاکسا کاری و جا ڤئ جا کاری ئی بألگە سی حال و بال آسایئشت شوما آمادە بییە:",
- "moveddeleted-notice": "ای بلگٱ پاکسا بیٱ.\nپاکسا کاری و جا ۋ جا کاری ای بلگٱ سی هال و بال آسایشت شوما آمادٱ بیٱ.",
+ "moveddeleted-notice": "اؽ بٱلگٱ پاکسا بیٱ.\nپاکسا کاری ۉ جا ڤ جا کاری اؽ بٱلگٱ سی هال ۉ بال پٱلٛٱمار شما آمادٱ بیٱ.",
"log-fulllog": "دیئن هأمە پئهئرستنوٙمە یا",
"edit-hook-aborted": "ڤیرایئشت ڤا قولاڤ نئھاگئری بییە.\nھیچ توضیی سیش نی.",
"edit-gone-missing": "نأبوٙە ئی بألگە نە ڤئ ھئنگوم بأکیت.\nچئنی ڤئ نأظأر میا کئ ڤئ پاکسا بییە.",
"undo-summary-username-hidden": "خومثی بیئن وانئری $1 وا یه گل کاریار قام بیه",
"cantcreateaccount-text": "حساو دروس بیه و ا ای تیرنشون آی پی(<strong>$1</strong>) وه دس ای [[کاریار:$3|$3]] قلف بیه.\n\n\nدلیل دئه بیه وا $3 ها د<em>$2</em>",
"cantcreateaccount-range-text": "حساو دروس بیه وا تیرنشون آی پی که د پوشینه <strong>$1</strong> ، که وه ئم مینونه دار تیرنشون آی پی شما ئم هئ(<strong>$4</strong>)، وه دس [[کاریار:$3|$3]]قلف بیه.\n\nدلیل دئه بیه وا $3، \"$2\" ئه.",
- "viewpagelogs": "سئÛ\8cÙ\84 پئرئستÙ\86Ù\88Ù\99Ù\85Û\95 Û\8cا ئÛ\8c بأÙ\84Ú¯Û\95 بأکÛ\8cت",
+ "viewpagelogs": "ساÙ\9bÙ\84Ù\9b Ù¾Ù\87رستÙ\86Ù\88Ù\85Ù±Û\8cا اب بٱÙ\84Ú¯Ù± بٱکؽت",
"nohistory": "هیچ ویرگار ویرایشتی د ای بلگه نئ.",
"currentrev": "آخرین دوواره دیئن",
- "currentrev-asof": "آخري وانئری چی $1",
+ "currentrev-asof": "آخری ڤانری چی $1",
"revisionasof": "دوئرٱ دیئن $1",
- "revision-info": "دوواره سیل بیه چی $1 وا $2",
+ "revision-info": "دوئارٱ ساٛلٛ بیٱ چی $1 ڤا $2",
"previousrevision": "ڤانیٱری زیتری ←",
- "nextrevision": "ڤانیٱری تازٱتر",
- "currentrevisionlink": "آخری ڤانیٱری",
+ "nextrevision": "ڤانؽٱری تازٱتر",
+ "currentrevisionlink": "آخری ڤانؽٱری",
"cur": "تازٱ باو",
"next": "نئهایی",
"last": "دمایی",
"page_first": "أڤئلی",
"page_last": "آخئر",
- "histlegend": "اÙ\86تخاÙ\88 Ù\81رخدار:جعÙ\88Û\8cا رادÛ\8cÙ\88 Ù\86Ù\87 سÛ\8c دÙ\88Ù\88ارÙ\87 دÛ\8cئÙ\86 Ù\88 Ù\88ارسÛ\8c Ù\86Ø´Ù\88 دار بکÛ\8cد Ù\88 Û\8cا رÛ\8c رئتÙ\86 Ú©Ù\84Û\8cÚ© بکÛ\8cد .<br />\nØ´Ø±Ø Ù\86Ù\88شتÙ\87: '''({{int:cur}})''' = Ù\88ا آخرÛ\8c دÙ\88Ù\88ارÙ\87 دÛ\8cئÙ\86 Ù\81رخ دارÙ\87 '''({{ int:last}})'''= Ù\88ا دÙ\88ارÙ\87 دÛ\8cئÙ\86 اÙ\86جÙ\88Ù\85 دئÙ\86Û\8c Ù\81رخ دارÙ\87 '''{{int:minoreditletter}}''' =Ù\88Û\8cراÛ\8cشت کؤچک.",
- "history-fieldset-title": "ڤیرگار دوڤارٱ نیٱری",
+ "histlegend": "اÙ\92Ù\86تخاب Ù\81ٱرخدار:جٱڤٱÛ\8cا رادÛ\8cÙ\88 Ù\86اÙ\92 سÛ\8c دÙ\88ئارٱ دÛ\8cئÙ\86 Û\89 ڤارسÛ\8c Ù\86Ø´Ù\88Ý© دار بٱکؽت Û\89 Û\8cا رÛ\8c رٱتÙ\86 Ú©Ù\84Ù\9bÛ\8cÚ© بٱکؽت .<br />\nØ´Ø±Ø Ù\86Ù\88شتÙ\87: '''({{int:cur}})''' = ڤا آخرÛ\8c دÙ\88ئارٱ دÛ\8cئÙ\86 Ù\81ٱرخ دارٱ '''({{ int:last}})'''= ڤا دÙ\88ئارٱ دÛ\8cئÙ\86 Ù±Ù\86جÙ\88Ù\85 داÙ\9bئÙ\86Û\8c Ù\81ٱرخ دارٱ '''{{int:minoreditletter}}''' =Ú¤Û\8cراÛ\8cØ´ Ú©Ù\88چک.",
+ "history-fieldset-title": "ڤیرگار دوئارٱ نیٱری",
"history-show-deleted": "فقط پاكسا بيه",
"histfirst": "قاٛیمی تریݩ",
"histlast": "ایسنی تریݩ",
"mergelog": "سریک سازی پهرستنومه",
"revertmerge": "بی لوئه",
"mergelogpagetext": "شما د هار نوم گه آخرین چیا وه یک شیوسن ویرگار یه بلگه نه د بلگه تر میئنیت.",
- "history-title": "دوڤارٱ دیاٛن ڤیرگار $1",
- "difference-title": "فرخ مینجا وانیریا \"$1\"",
+ "history-title": "دوئارٱ دیئن ڤیرگار $1",
+ "difference-title": "فٱرخ مؽنجا ڤانیرؽا \"$1\"",
"difference-title-multipage": "فرخ مینجا بلگه یا \"$1\" و \"$2\"",
"difference-multipage": "(فرخ مینجا بلگه یا)",
"lineno": "خٱت $1:",
"showhideselectedversions": "شلک دیئن وانیریا انتخاو بیه نه آلشت بکید",
"editundo": "ناٱنجومگر کردن",
"diff-empty": "(بی فرق)",
- "diff-multi-sameuser": "({{PLURAL:$1|یه گل نسقه مینجایی|$1 نسقه یا مینجایی}} وه دس{{PLURAL:$2|کاریاری تر|$2 کاریاریا}} نشو دئه نبیه)",
+ "diff-multi-sameuser": "({{PLURAL:$1|یاٛ نۏسخٱ مؽنجایی|$1 نۏسخٱیا مؽنجایی}} ڤ دٱس{{PLURAL:$2|کاریارؽ تر|$2 کاریارؽا}} نشوݩ داٛئٱ ناٛیٱ)",
"diff-multi-otherusers": "({{PLURAL:$1|یه گل نسقه مینجایی|$1 نسقه یا مینجایی}} وه دس{{PLURAL:$2|کاریاری تر|$2 کاریاریا}} نشو دئه نبیه)",
"diff-multi-manyusers": "({{PLURAL:$1|یه گل وانیری مینجاگرته|$1وانیریا مینجا گرته}} بیشتر د $2 {{PLURAL:$2|کاریار|کاریاریا}} نشو دئه نبیه)",
"difference-missing-revision": "{{PLURAL:$2|یه گل ویرایشت|$2 ویرایشت}} د فرق مینجا($1) {{PLURAL:$2|پیدا نبی|پیدا نبینه}}.\n\nشایت بانی جاونه وه وا یه گل ویرگار وه هنگوم نبیه که د یه گل بلگه پاکسا بیه هوم پیوند بیه بوئه.\nشایت جزئیات د [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log] پیدا بوئن.",
"search-section": "(بٱئرجا $1)",
"search-category": "(دسه $1)",
"search-file-match": "(یکی کردن مینونه جانیا)",
- "search-suggest": "Ù\85Ù\86ظÙ\88رت Ù\8aÙ\87 بی:$1",
+ "search-suggest": "Ù\85Ù±Ù\86زÛ\8aرت Ù\8aÙ± بی:$1",
"search-rewritten": "نئشوٙ دأئن نأتیجە یا سی $1. سی نئموٙنە بأگأردیت سی $2.",
"search-interwiki-caption": "پروجه یا خوئر",
"search-interwiki-default": "$1 نتیجه یا:",
"rightslog": "پهرستنومه حقوق کاریار",
"rightslogtext": "یه پهرستنومه آلشتیا حقوق کاریاره.",
"action-read": "ای بلگه نه بحو",
- "action-edit": "ای بلگه نه ويرايشت بكيد",
+ "action-edit": "اؽ بٱلگٱ ناْ ڤيرايش بٱكیت",
"action-createpage": "راس کردن بلگیا",
"action-createtalk": "بلگه یا چک چنه نه راس بکید",
"action-createaccount": "حساو ای کاریار نه راس بکید",
"enhancedrc-history": "ڤیرگار",
"recentchanges": "آلشتؽا ایسنی",
"recentchanges-legend": "گوزینٱیا آلشتؽا ایسنی",
- "recentchanges-summary": "دۏ بؽشتر آلشتؽا تازباو ناْ د ڤیکی ناْ د اؽ بٱلگٱ پاٛجۊری کو.",
- "recentchanges-noresult": "Ù\87Û\8cÚ\98 Ø¢Ù\84شتÛ\8c د درازا دÙ\88رÙ\87 دÛ\8cار بÛ\8cÙ\87 Ù\88ا اÛ\8c Ù\85عÛ\8cارÛ\8cا Û\8cÚ©Û\8c Ù\86بی.",
+ "recentchanges-summary": "دۏ بؽشتر آلشتؽا تازباو ناْ د ڤیکی د اؽ بٱلگٱ پاٛجۊری کو.",
+ "recentchanges-noresult": "Ù\87Û\8cÚ\86 Ø¢Ù\84شتؽ د درازا دÛ\89رٱ دؽار بÛ\8cÙ± ڤا اؽ Ù\85اÙ\9bعÛ\8cاؽا Û\8cÙ±Ú©Û\8c Ù\86اÙ\9bی.",
"recentchanges-feed-description": "دو بیشتر آلشتیا تازباو نه د ویکی که ها د هوال حون پیگری کو.",
"recentchanges-label-newpage": "اؽ ڤیرایش یاٛ بٱلگٱ تازٱ دۏرس کردٱ.",
"recentchanges-label-minor": "یٱ یاٛ ڤیرایش کوچکٱ",
"recentchanges-legend-heading": "<strong>میرات:</strong>",
"recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (همچنو باٛینؽت [[ڤیژٱ:بٱلگٱیا تازٱ|نوم گٱ بٱلگٱیا تازٱ]])",
"recentchanges-legend-plusminus": "(<em>±123</em>)",
- "rcnotefrom": "د هار آلشتیا د $2 هیئن(د بال د $1 نشون دئه بیه)",
+ "rcnotefrom": "د هار آلشتؽا د $2 هؽسن(د بال د $1 نشوݩ داٛئٱ بیٱ)",
"rclistfrom": "آلشتؽا تازاٛیؽ کاْ ڤا $3 $2 شرۊ بیٱ نشونش باٛیٱ",
"rcshowhideminor": "ڤیرایشؽا فرٱ کوچک $1",
- "rcshowhideminor-show": "نشو دئن",
+ "rcshowhideminor-show": "نشوݩ داٛئن",
"rcshowhideminor-hide": "قایم کردن",
"rcshowhidebots": "$1 روباتؽا یا بوتؽا",
"rcshowhidebots-show": "نشوݩ داٛین",
- "rcshowhidebots-hide": "قام کردن",
+ "rcshowhidebots-hide": "قایم کردن",
"rcshowhideliu": "$1 کاریاریا سٱبت نوم کردٱ",
- "rcshowhideliu-show": "نشۊ دٱئن",
+ "rcshowhideliu-show": "نشوݩ داٛئن",
"rcshowhideliu-hide": "قایم کردن",
"rcshowhideanons": "کاریار نادؽار $1",
- "rcshowhideanons-show": "Ù\86ئشÙ\88Ù\99 دأئن",
+ "rcshowhideanons-show": "Ù\86Ø´Ù\88Ý© داÙ\9bئن",
"rcshowhideanons-hide": "قایم کردن",
"rcshowhidepatr": "$1 ویرایشتیا تیه پرس بیه",
"rcshowhidepatr-show": "نئشوٙ دأئن",
"rcshowhidepatr-hide": "قام کئردئن",
"rcshowhidemine": "ڤیرایشؽا ماْ $1",
- "rcshowhidemine-show": "Ù\86ئشÙ\88Ù\99 دأئن",
+ "rcshowhidemine-show": "Ù\86Ø´Ù\88Ý© داÙ\9bئن",
"rcshowhidemine-hide": "قایم کردن",
"rcshowhidecategorization": "جأرغە کاری بألگە $1",
"rcshowhidecategorization-show": "نئشوٙ دأئن",
"upload-curl-error28": "تموم بیئن مئلت سی سوار کرد",
"upload-curl-error28-text": "ای دیارگه فره دیر دتو واکنشت نشو دئه.\nلطف بکیت سی یه که دیارگه کنشگتر و ری خطه یه گل وارسی بکیت، اوسه یه گر واستید و هنی تلاش بکیت.\nشایت بیتر با که د گات خلوتری هنی تلاش بکیت.",
"license": "ليانس دار بيئن",
- "license-header": "د هال ۉ بال للیسانس دار بیین",
+ "license-header": "د هال ۉ بال لیسانس دار بیئن",
"nolicense": "هیچی انتخاو نبیه",
"licenses-edit": "گزینه یا مجوز ویرایشت",
"license-nopreview": "(پیش سیل د دسرس نئ)",
"listfiles-summary": "ای بلگه یا ویجه همه جانیایا سوار بیه نه نشو می ئین.",
"listfiles_search_for": "پی جوری سی نوم رسانه:",
"listfiles-userdoesnotexist": "حساو کاریاری «$1» ثوت نام نبیه.",
- "imgfile": "جانیا",
+ "imgfile": "جانؽا",
"listfiles": "نومگە جانیا",
"listfiles_thumb": "بأن کئلئکی",
"listfiles_date": "گات",
"ncategories": "$1{{PLURAL:$1|دسه|دسه يا}}",
"ninterwikis": "$1 {{PLURAL:$1|مئن ویکی|مئن ویکیا}}",
"nlinks": "$1 {{PLURAL:$1|هوم پیوند|هوم پیوندیا}}",
- "nmembers": "$1 {{PLURAL:$1|اندوم|اندوميا}}",
+ "nmembers": "$1 {{PLURAL:$1|ٱندوم|ٱندومؽا}}",
"nmemberschanged": "$1 → $2 {{PLURAL:$2|اندوم|اندومیا}}",
"nrevisions": "$1 {{جمس:$1|وانئری|وانئریا}}",
"nimagelinks": "$1 {{PLURAL:$1|بلگه|بلگيا}} استفاده بیه",
"notargettext": "شما بلگه یا کاریاری مقصدی سی انجوم دئن ای کنشت ریش انتخاو نکردیته.",
"nopagetitle": "چنی بلگه ای نیئش",
"nopagetext": "بلگه حاستنی که شما دیاری کردیته وجود ناره.",
- "pager-newer-n": "{{PLURAL:$1|وانها تر 1وانها تر $1}}",
- "pager-older-n": "{{PLURAL:$1|گپساÙ\84تر 1|Ú¯پسالتر $1}}",
+ "pager-newer-n": "{{PLURAL:$1|ڤانوئاتر 1ڤانوئاتر $1}}",
+ "pager-older-n": "{{PLURAL:$1|گٱپساÙ\84تر 1|Ú¯Ù±پسالتر $1}}",
"suppress": "پائیئن",
"querypage-disabled": "ای بلگه ویجه سی دلیلیا انجومکاری ناکشتگر بیه.",
"apihelp": "هومیاری آی پی آی",
"apihelp-no-such-module": "ماجول \"$1\" پیدا نبی.",
"booksources": "سرچشمٱیا کتاو",
- "booksources-search-legend": "پاٛ جۊری سی سٱرچشمٱیا کتاو",
+ "booksources-search-legend": "پاٛ جۊری سی سرچشمٱیا کتاو",
"booksources-isbn": "آی اس بی ان:",
"booksources-search": "پاٛ جۊری",
"booksources-text": "د هار نومگه ای د هوم پیوندیا د دیارگه یا هنی اومائه که کتاویا نو و دس دوئم می فروشن، و همچنو شایت دونسمنیا بیشتری راجع وه کتاو حاستنی شما داشتوئن:",
"removewatch": "جا ڤئ جا کئردئن د سئیل بأرگ",
"removedwatchtext": "بألگە \"[[:$1]]\" د [[Special:سئیل بأرگ|سئیل بأرگ خوتوٙ]] جا ڤئ جا بییە.",
"removedwatchtext-short": "بلگه \"$1\" د سیل برگ جا وه جا بیه.",
- "watch": "سئÛ\8cÙ\84 کئردئن",
+ "watch": "ساÙ\9bÙ\84Ù\9b کردن",
"watchthispage": "دیئن ئی بألگە",
"unwatch": "دیە نأبییە",
"unwatchthispage": "نئھاگئری دیئن",
"actioncomplete": "عملكرد كامل بيه",
"actionfailed": "عملكرد شكست حرده",
"deletedtext": "«$1» پاکسا بیه.\nسی نهاتری پاکساگریا ایسنی وه $2 سرکشی بکیت.",
- "dellogpage": "پاکسا کردÙ\86 Ù¾Ù\87رستÙ\86Ù\88Ù\85Ù\87",
+ "dellogpage": "پاکسا کردÙ\86 Ù¾Ù\87رستÙ\86Ù\88Ù\85Ù±",
"dellogpagetext": "نومگه هاری یه گل نومگه د آخری چیا پاکسا بیه هئ.",
"deletionlog": "پهرستنومه پاک بیئن",
"reverted": "لرسه د نزیکترین وانئری",
"deleting-backlinks-warning": "''' هشدار:''' [[Special:WhatLinksHere/{{FULLPAGENAME}}|بلگه یا هنی]] ین که وه بلگه یی که شما د حال و بار پاکسا کردن ونیت پیوند دارن یا د وه پرگنجایشت کاری بیینه.",
"rollback": "چواشه کردن ویرایشتیا",
"rollbacklink": "ڤرگٱشتن",
- "rollbacklinkcount": "Ú\86Ù\88اشÙ\87 کردÙ\86 $1 {{PLURAL:$1|Ù\88Û\8cراÛ\8cشت|Ù\88Û\8cراÛ\8cشتÛ\8cا}}",
+ "rollbacklinkcount": "Ú\86Ù\88ئارشٱ کردÙ\86 $1 {{PLURAL:$1|Ú¤Û\8cراÛ\8cØ´|Ú¤Û\8cراÛ\8cشؽا}}",
"rollbacklinkcount-morethan": "چواشه کردن بیشتر د$1 {{PLURAL:$1|ویرایشت|ویرایشتیا}}",
"rollbackfailed": "چواشه کردن د خوئی انجوم نبی",
"cantrollback": "نبوئه ویرایشت نه پاکساگری بکیت:\nآخری هومیار تئنا نیسنه ای گوتاره.",
"changecontentmodel-success-title": "حال و بال مینوٙنە آلئشتکاری بی",
"logentry-contentmodel-change-revertlink": "لئرنیئن",
"logentry-contentmodel-change-revert": "لئرنیئن",
- "protectlogpage": "پأر و پیم کاری پئرئستنوٙمە",
+ "protectlogpage": "پٱر ۉ پیم کاری پهرستنومٱ",
"protectlogtext": "د ھار یئ گئل نومگە د آلئشتیا ریتئراز پأر و پیم کاری بألگە یا ئوٙماە.\n[[Special:ProtectedPages|نومگە بألگە یا پأر و پیم کاری بییە]] نە سی دیئن نومگە پأر و پیم کاری کارگئرا بألگە یا نە سئیل بأکیت.",
"protectedarticle": "پأر و پیم کاری بییە [[$1]]",
"modifiedarticleprotection": "ریتراز حفاظت د \"[[$1]]\" آلشت بیه",
"contribsub2": "سي {{جنسيت:$3|$1}} ($2)",
"contributions-userdoesnotexist": "کاریار \"$1\" ثوت نام نکرده.",
"nocontribs": "هیچ آلشتی وا ای مشقصات دیاری نکرد.",
- "uctop": "تازÙ\87 باÙ\88",
+ "uctop": "تازٱ بÛ\8a",
"month": "د ما(یا زیتر)",
"year": "د سال",
"sp-contributions-newbies": "فقٱت هومیارؽایؽ کاْ د هساو تازٱ بیٱ نشوݩ باٛیٱ",
"movepage-page-moved": "بلگه $1 د $2 جا وه جا بیه",
"movepage-page-unmoved": "نبوئه بلگه $1 د $2 جا وه جا بوئه",
"movepage-max-pages": "بیشترونه انازه بلگه یا شایت سی ($1 {{PLURAL:$1|بلگه|بلگه یا}}) یی که بوئه جا وه جاکاری بوئن، جا وه جاکاری بیه و بلگه یا هنی نه نبوئه و شکل خودانجوم جا وه جاکاری کرد.",
- "movelogpage": "جاوه جا کردن",
+ "movelogpage": "جا ڤ جا کردن",
"movelogpagetext": "د هار یه گل نوم گه د جا وه جایی یا بلگه هئ",
"movesubpage": "{{PLURAL:$1|زیر بلگه|زیر بلگه یا}}",
"movesubpagetext": "ای بلگه $1 زیربلگه داره که د زیر نشو {{PLURAL:|نشو دئه بیه|دئه بینه}}.",
"tooltip-ca-unprotect": "پر و پیم گیری د ای بلگه نه آلشت بکیت",
"tooltip-ca-delete": "ای بلگه نه پاکسا کو",
"tooltip-ca-undelete": "د نو زنه کردن ویرایشتیا ری ای بلگه دما یه که پاکساگری بان",
- "tooltip-ca-move": "ای بگله نه جا وه جا كو",
+ "tooltip-ca-move": "اؽ بٱلگٱ ناْ جا ڤ جا كو",
"tooltip-ca-watch": "اْزاف کردن اؽ بٱلگٱ ڤ نوم نڤشت پاٛگیریاتو",
"tooltip-ca-unwatch": "ورداشتن ای بلگه وه نوم نوشت پیگئریاتو",
"tooltip-search": "پاٛ جۊری {{SITENAME}}",
"tooltip-t-print": "نۏسخٱ پاٛلا بی ینی سی اؽ بٱلگٱ",
"tooltip-t-permalink": "هوم پاٛڤٱن همیشاٛیی سی دوئارٱ دیئن اؽ بٱلگٱ",
"tooltip-ca-nstab-main": "ديئن مؽنونٱ بٱلگٱ",
- "tooltip-ca-nstab-user": "دیین بٱلگٱ کاریار",
+ "tooltip-ca-nstab-user": "دیئن بٱلگٱ کاریار",
"tooltip-ca-nstab-media": "دیئن بلگه وارسگر",
"tooltip-ca-nstab-special": "یٱ یاٛ بٱلگٱ ڤیژٱ آ؛ نمۊئٱ ڤیرایشش بٱکؽت",
- "tooltip-ca-nstab-project": "دÙ\8aئÙ\86 بÙ\84Ú¯Ù\87 پرÙ\88جÙ\87",
+ "tooltip-ca-nstab-project": "دÙ\8aئÙ\86 بٱÙ\84Ú¯Ù± پرÛ\89Ú\98Ù±",
"tooltip-ca-nstab-image": "دیئن بٱلگٱ جانؽا",
"tooltip-ca-nstab-mediawiki": "دیاٛن پیغوم سامۊنٱ",
"tooltip-ca-nstab-template": "ديئن چۊٱ",
"tooltip-save": "آلشتؽا توناْ آمادٱ بٱکؽت",
"tooltip-preview": "پیش ساٛلٛ آلشتؽاتو، لوتف بٱکؽت ڤنوناْ دما د آمایٱ کاریشو ڤ کار باٛیرؽت!",
"tooltip-diff": "آلشتؽا ناْ کاْ شما د ای مٱتن دۏرس کردؽتٱ نشوݩ باٛیٱ",
- "tooltip-compareselectedversions": "فرخیا مینجا د تا د دو بار دیاٛن ای بلگٱ نٱ بۉنیت",
+ "tooltip-compareselectedversions": "فٱرخؽا مؽنجا د تا د دۏ بار دیئن اؽ بٱلگٱ ناْ بونؽت",
"tooltip-watch": "ای بلگه نه د سیل برگتو اضاف بکید",
"tooltip-watchlistedit-normal-submit": "ؤرداشتن سرونیا",
"tooltip-watchlistedit-raw-submit": "وه هنگوم سازی سیل برگ",
"filedelete-old-unregistered": "وانئری جانیا تیارکرده \"$1\" د رسینه جا وجود ناره.",
"filedelete-current-unregistered": "جانیا تیارکرده \"$1\" د رسینه جا نئیش.",
"filedelete-archive-read-only": "نشونگه مال دیارکردن ($1) د لا سرور قاول نیسنن نئ.",
- "previousdiff": "← ويرايشت كۈهنه تر",
- "nextdiff": "ويرايشت تازه تر",
+ "previousdiff": "← ڤیرایش کۏنٱتر",
+ "nextdiff": "ڤیرایش تازٱ تر",
"mediawarning": "'''هشدار''': شایت ای جانیا د خوش رازینه یا گن داشتوئه.\nشایت وا اجرا وه انجومیار شما آسیو دینه.",
"imagemaxsize": "انازه عسگ:<br /><em>(سی شرح جانیا بلگه یا)</em>",
"thumbsize": "انازه بن کلکی:",
"file-info-size": "$1 × $2 پیکسل, ٱندازٱ فایل: $3, MIME نوع: $4",
"file-info-size-pages": "$1 × $2 pixels, انازه جانیا: $3, MIME type: $4, $5 {{PLURAL:$5|بلگه|بلگه یا}}",
"file-nohires": "عٱسک ڤٱن بالاترؽ دش نؽ",
- "svg-long-desc": "جانیا اٛس ۋی جی, نومی $1 × $2 پیکسل, ٱنازٱ جانیا: $3",
+ "svg-long-desc": "جانؽا اْس ڤی جی, نومی $1 × $2 پیکسل, ٱندازٱ جانؽا: $3",
"svg-long-desc-animated": "جانیا جمشدار اس وی جی .نومنا $1 × $2 پيكسل،انازه جانیا:$3",
"svg-long-error": "جانیا اس وی جی نامعتور:$1",
"show-big-image": "جانؽا ٱسلی",
"logentry-rights-rights": "$1 اندوم بیین $3 نه د گرو $4 د $5 {{GENDER:$2|آلشت ده}}",
"logentry-rights-rights-legacy": "$1 اندوم بیین $3 د گرو نه {{GENDER:$2|آلشت ده}}",
"logentry-rights-autopromote": "$1 وه شکل خودانجوم $4 نه د $5 {{GENDER:$2|برد واروتر}}",
- "logentry-upload-upload": "$1 {{GENDER:$2|سوار کرده}} $3",
+ "logentry-upload-upload": "$1 {{GENDER:$2|سڤار کردٱ}} $3",
"logentry-upload-overwrite": "$1 یه گل نسقه تازه د $3 نه {{GENDER:$2|سوار کرد}}",
"logentry-upload-revert": "$1 $3 نه {{GENDER:$2|سوارکرد}}",
"log-name-managetags": "سردیس دیوونداری کردن پهرستنومه",
"watchnologin": "ലോഗിൻ ചെയ്തിട്ടില്ല",
"addwatch": "ശ്രദ്ധിക്കുന്ന താളുകളുടെ പട്ടികയിലേക്കു ചേർക്കുക",
"addedwatchtext": "താങ്കൾ [[Special:Watchlist|ശ്രദ്ധിക്കുന്ന താളുകളുടെ പട്ടികയിലേക്ക്]] \"[[:$1]]\" എന്ന ഈ താളും അതിന്റെ സംവാദത്താളും ചേർത്തിരിക്കുന്നു.",
+ "addedwatchtext-talk": "\"[[:$1]]\" ഒപ്പം ഇതിന്റെ ബന്ധപ്പെട്ട താളും താങ്കൾ [[Special:Watchlist|ശ്രദ്ധിക്കുന്നവയുടെ പട്ടികയിലേക്ക്]] ചേർത്തിരിക്കുന്നു.",
"addedwatchtext-short": "\"$1\" എന്ന താൾ താങ്കൾ ശ്രദ്ധിക്കുന്നവയുടെ പട്ടികയിലേക്ക് ചേർത്തു.",
"removewatch": "ശ്രദ്ധിക്കുന്ന താളുകളുടെ പട്ടികയിൽ നിന്നും ഒഴിവാക്കുക",
"removedwatchtext": "താങ്കൾ [[Special:Watchlist|ശ്രദ്ധിക്കുന്ന താളുകളുടെ പട്ടികയിൽ]] നിന്നും \"[[:$1]]\" എന്ന താളും അതിന്റെ സംവാദത്താളും നീക്കം ചെയ്തിരിക്കുന്നു.",
+ "removedwatchtext-talk": "\"[[:$1]]\" ഒപ്പം ഇതിന്റെ ബന്ധപ്പെട്ട താളും താങ്കൾ [[Special:Watchlist|ശ്രദ്ധിക്കുന്നവയുടെ പട്ടികയിൽ]] നിന്ന് ഒഴിവാക്കിയിരിക്കുന്നു.",
"removedwatchtext-short": "\"$1\" എന്ന താൾ താങ്കൾ ശ്രദ്ധിക്കുന്നവയുടെ പട്ടികയിൽ നിന്ന് നീക്കി.",
"watch": "മാറ്റങ്ങൾ ശ്രദ്ധിക്കുക",
"watchthispage": "ഈ താൾ ശ്രദ്ധിക്കുക",
"ipb-confirm": "Used as hidden field in the form on [[Special:Block]].",
"ipb-sitewide": "A type of block the user can select from on [[Special:Block]].",
"ipb-partial": "A type of block the user can select from on [[Special:Block]].",
+ "ipb-sitewide-help": "Help text describing the effects of a sitewide block on [[Special:Block]]",
+ "ipb-partial-help": "Help text describing the effects of a partial block on [[Special:Block]]",
"ipb-pages-label": "The label for an autocomplete text field to specify pages to block a user from editing on [[Special:Block]].",
"ipb-namespaces-label": "The label for an autocomplete text field to specify namespaces to block a user from editing on [[Special:Block]].",
"badipaddress": "An error message shown when one entered an invalid IP address in blocking page.",
"logentry-rights-autopromote": "$1 je {{GENDER:$2|bil samodejno povišan|bila samodejno povišana|bil(-a) samodejno povišan(-a)}} z $4 na $5",
"logentry-upload-upload": "$1 je {{GENDER:$2|naložil|naložila|naložil(-a)}} $3",
"logentry-upload-overwrite": "$1 je {{GENDER:$2|naložil|naložila|naložil(-a)}} novo različico $3",
- "logentry-upload-revert": "$1 je {{GENDER:$2|naložil|naložila|naložil(-a)}} $3",
+ "logentry-upload-revert": "$1 je {{GENDER:$2|vrnil|vrnila|vrnil(-a)}} $3 na starejšo različico",
"log-name-managetags": "Dnevnik upravljanja oznak",
"log-description-managetags": "Stran navaja opravila upravljanja, povezana z [[Special:Tags|oznakami]]. Dnevnik vsebuje samo dejanja, ki so jih ročno izvedli administratorji; oznake je lahko ustvarilo ali izbrisalo tudi programje wiki brez zabeleženega vnosa v tem dnevniku.",
"logentry-managetags-create": "$1 je {{GENDER:$2|ustvaril|ustvarila|ustvaril(-a)}} oznako »$4«",
"log-action-filter-suppress-reblock": "Zatrtje uporabnika s ponovno blokado",
"log-action-filter-upload-upload": "Novo nalaganje",
"log-action-filter-upload-overwrite": "Ponovno nalaganje",
+ "log-action-filter-upload-revert": "Vrni",
"authmanager-authn-not-in-progress": "Overjanje ni v teku ali pa smo izgubili podatke seje. Prosimo, pričnite znova od začetka.",
"authmanager-authn-no-primary": "Navedenih poverilnic nismo mogli overiti.",
"authmanager-authn-no-local-user": "Navedene poverilnice niso povezane z nobenim uporabnikom na wikiju.",
],
__METHOD__,
[ 'ORDER BY' => 'rev_timestamp DESC', 'LIMIT' => 1 ],
- [ 'revision' => [ 'INNER JOIN', 'rev_page=page_id' ] ]
+ [ 'revision' => [ 'JOIN', 'rev_page=page_id' ] ]
);
return $id;
'rc_type' => RC_LOG,
] );
$it->addJoinConditions( [
- 'page' => [ 'INNER JOIN', 'rc_cur_id = page_id' ],
+ 'page' => [ 'JOIN', 'rc_cur_id = page_id' ],
] );
$this->addIndex( $it );
return $it;
$joinConds = [];
if ( $mtime1 || $mtime2 ) {
$joinTables[] = 'page';
- $joinConds['page'] = [ 'INNER JOIN',
+ $joinConds['page'] = [ 'JOIN',
[ 'page_title = img_name', 'page_namespace' => NS_FILE ] ];
$joinTables[] = 'logging';
$on = [ 'log_page = page_id', 'log_type' => [ 'upload', 'move', 'delete' ] ];
if ( $mtime2 ) {
$on[] = "log_timestamp < {$dbr->addQuotes($mtime2)}";
}
- $joinConds['logging'] = [ 'INNER JOIN', $on ];
+ $joinConds['logging'] = [ 'JOIN', $on ];
}
do {
};
</script>
<script>
- // Mock startup.js
+ // Mock ResourceLoaderStartUpModule substitutions
window.$VARS = {
- baseModules: []
+ baseModules: [],
+ maxQueryLength: 2000
};
+ // Mock startup.js
window.RLQ = [];
</script>
<script src="modules/src/startup/mediawiki.js"></script>
} else { // revision
$selectTables = [ 'revision', 'page' ];
$fields = [ 'page_title', 'page_namespace' ];
- $join_conds = [ 'page' => [ 'INNER JOIN', 'rev_page=page_id' ] ];
+ $join_conds = [ 'page' => [ 'JOIN', 'rev_page=page_id' ] ];
$where = $ns === 'all' ? [] : [ 'page_namespace' => $ns ];
$page_id_column = 'rev_page';
$rev_id_column = 'rev_id';
__METHOD__,
[ 'ORDER BY' => 'rev_timestamp', 'LIMIT' => $bSize ],
[
- 'page' => [ 'INNER JOIN', 'rev_page=page_id' ],
+ 'page' => [ 'JOIN', 'rev_page=page_id' ],
]
);
[ 'ug_group' => $botgroups ],
__METHOD__,
[ 'DISTINCT' ],
- [ 'user_group' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
+ [ 'user_groups' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
);
$botusers = [];
[ 'ug_group' => $autopatrolgroups ],
__METHOD__,
[ 'DISTINCT' ],
- [ 'user_group' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
+ [ 'user_groups' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
);
foreach ( $res as $obj ) {
* @param {string[]} batch
*/
function batchRequest( batch ) {
- var reqBase, splits, maxQueryLength, b, bSource, bGroup,
+ var reqBase, splits, b, bSource, bGroup,
source, group, i, modules, sourceLoadScript,
currReqBase, currReqBaseLength, moduleMap, currReqModules, l,
lastDotIndex, prefix, suffix, bytesAdded;
lang: mw.config.get( 'wgUserLanguage' ),
debug: mw.config.get( 'debug' )
};
- maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
// Split module list by source and by group.
splits = Object.create( null );
modules[ i ].length + 3; // '%7C'.length == 3
// If the url would become too long, create a new one, but don't create empty requests
- if ( maxQueryLength > 0 && currReqModules.length && l + bytesAdded > maxQueryLength ) {
+ if ( currReqModules.length && l + bytesAdded > mw.loader.maxQueryLength ) {
// Dispatch what we've got...
doRequest();
// .. and start again.
moduleMap = Object.create( null );
currReqModules = [];
- mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
+ mw.track( 'resourceloader.splitRequest', { maxQueryLength: mw.loader.maxQueryLength } );
}
if ( !moduleMap[ prefix ] ) {
moduleMap[ prefix ] = [];
*/
moduleRegistry: registry,
+ /**
+ * Exposed for testing and debugging only.
+ *
+ * @see #batchRequest
+ * @property
+ * @private
+ */
+ maxQueryLength: $VARS.maxQueryLength,
+
/**
* @inheritdoc #newStyleTag
* @method
private $originalSpi;
/** @var Spi|null */
private $spi;
- /** @var array|null */
- private $lastTestLogs;
/**
* A test started.
LoggerFactory::registerProvider( $this->spi );
}
+ public function addRiskyTest( PHPUnit_Framework_Test $test, Exception $e, $time ) {
+ $this->augmentTestWithLogs( $test );
+ }
+
+ public function addIncompleteTest( PHPUnit_Framework_Test $test, Exception $e, $time ) {
+ $this->augmentTestWithLogs( $test );
+ }
+
+ public function addSkippedTest( PHPUnit_Framework_Test $test, Exception $e, $time ) {
+ $this->augmentTestWithLogs( $test );
+ }
+
+ public function addError( PHPUnit_Framework_Test $test, Exception $e, $time ) {
+ $this->augmentTestWithLogs( $test );
+ }
+
+ public function addWarning( PHPUnit_Framework_Test $test, PHPUnit\Framework\Warning $e, $time ) {
+ $this->augmentTestWithLogs( $test );
+ }
+
+ public function addFailure( PHPUnit_Framework_Test $test,
+ PHPUnit_Framework_AssertionFailedError $e, $time
+ ) {
+ $this->augmentTestWithLogs( $test );
+ }
+
+ private function augmentTestWithLogs( PHPUnit_Framework_Test $test ) {
+ if ( $this->spi ) {
+ $logs = $this->spi->getLogs();
+ $formatted = $this->formatLogs( $logs );
+ $test->_formattedMediaWikiLogs = $formatted;
+ }
+ }
+
/**
* A test ended.
*
* @param float $time
*/
public function endTest( PHPUnit_Framework_Test $test, $time ) {
- $this->lastTestLogs = $this->spi->getLogs();
LoggerFactory::registerProvider( $this->originalSpi );
$this->originalSpi = null;
$this->spi = null;
* Get string formatted logs generated during the last
* test to execute.
*
+ * @param array $logs
* @return string
*/
- public function getLog() {
- $logs = $this->lastTestLogs;
- if ( !$logs ) {
- return '';
- }
+ private function formatLogs( array $logs ) {
$message = [];
foreach ( $logs as $log ) {
$message[] = sprintf(
class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command {
private $cliArgs;
- private $logListener;
public function __construct( $ignorableOptions, $cliArgs ) {
$ignore = function ( $arg ) {
// Add our own listeners
$this->arguments['listeners'][] = new MediaWikiPHPUnitTestListener;
- $this->logListener = new MediaWikiLoggerPHPUnitTestListener;
- $this->arguments['listeners'][] = $this->logListener;
+ $this->arguments['listeners'][] = new MediaWikiLoggerPHPUnitTestListener;
// Output only to stderr to avoid "Headers already sent" problems
$this->arguments['stderr'] = true;
- // We could create a printer instance and avoid passing the
- // listener statically, but then we have to recreate the
- // appropriate arguments handling + defaults.
+ // Use a custom result printer that includes per-test logging output
+ // when nothing is provided.
if ( !isset( $this->arguments['printer'] ) ) {
$this->arguments['printer'] = MediaWikiPHPUnitResultPrinter::class;
}
}
protected function createRunner() {
- MediaWikiPHPUnitResultPrinter::setLogListener( $this->logListener );
$runner = new MediaWikiTestRunner;
$runner->setMwCliArgs( $this->cliArgs );
return $runner;
<?php
class MediaWikiPHPUnitResultPrinter extends PHPUnit_TextUI_ResultPrinter {
- /** @var MediaWikiLoggerPHPUnitTestListener */
- private static $logListener;
-
- public static function setLogListener( MediaWikiLoggerPHPUnitTestListener $logListener ) {
- self::$logListener = $logListener;
- }
-
protected function printDefectTrace( PHPUnit_Framework_TestFailure $defect ) {
- $log = self::$logListener->getLog();
- if ( $log ) {
- $this->write( "=== Logs generated by test case\n{$log}\n===\n" );
+ $test = $defect->failedTest();
+ if ( $test !== null && isset( $test->_formattedMediaWikiLogs ) ) {
+ $log = $test->_formattedMediaWikiLogs;
+ if ( $log ) {
+ $this->write( "=== Logs generated by test case\n{$log}\n===\n" );
+ }
}
parent::printDefectTrace( $defect );
}
* @param bool $expected
*/
public function testCanTalk( $index, $expected ) {
+ $this->hideDeprecated( 'MWNamespace::canTalk' );
$actual = MWNamespace::canTalk( $index );
$this->assertSame( $actual, $expected, "NS $index" );
}
--- /dev/null
+<?php
+
+/**
+ * The urls herein are not actually called, because we mock the return results.
+ *
+ * @covers MultiHttpClient
+ */
+class MultiHttpClientTest extends MediaWikiTestCase {
+ protected $client;
+
+ protected function setUp() {
+ parent::setUp();
+ $client = $this->getMockBuilder( MultiHttpClient::class )
+ ->setConstructorArgs( [ [] ] )
+ ->setMethods( [ 'isCurlEnabled' ] )->getMock();
+ $client->method( 'isCurlEnabled' )->willReturn( false );
+ $this->client = $client;
+ }
+
+ private function getHttpRequest( $statusValue, $statusCode, $headers = [] ) {
+ $httpRequest = $this->getMockBuilder( PhpHttpRequest::class )
+ ->setConstructorArgs( [ '', [] ] )
+ ->getMock();
+ $httpRequest->expects( $this->any() )
+ ->method( 'execute' )
+ ->willReturn( Status::wrap( $statusValue ) );
+ $httpRequest->expects( $this->any() )
+ ->method( 'getResponseHeaders' )
+ ->willReturn( $headers );
+ $httpRequest->expects( $this->any() )
+ ->method( 'getStatus' )
+ ->willReturn( $statusCode );
+ return $httpRequest;
+ }
+
+ private function mockHttpRequestFactory( $httpRequest ) {
+ $factory = $this->getMockBuilder( MediaWiki\Http\HttpRequestFactory::class )
+ ->getMock();
+ $factory->expects( $this->any() )
+ ->method( 'create' )
+ ->willReturn( $httpRequest );
+ return $factory;
+ }
+
+ /**
+ * Test call of a single url that should succeed
+ */
+ public function testMultiHttpClientSingleSuccess() {
+ // Mock success
+ $httpRequest = $this->getHttpRequest( StatusValue::newGood( 200 ), 200 );
+ $this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
+
+ list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $this->client->run( [
+ 'method' => 'GET',
+ 'url' => "http://example.test",
+ ] );
+
+ $this->assertEquals( 200, $rcode );
+ }
+
+ /**
+ * Test call of a single url that should not exist, and therefore fail
+ */
+ public function testMultiHttpClientSingleFailure() {
+ // Mock an invalid tld
+ $httpRequest = $this->getHttpRequest(
+ StatusValue::newFatal( 'http-invalid-url', 'http://www.example.test' ), 0 );
+ $this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
+
+ list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $this->client->run( [
+ 'method' => 'GET',
+ 'url' => "http://www.example.test",
+ ] );
+
+ $failure = $rcode < 200 || $rcode >= 400;
+ $this->assertTrue( $failure );
+ }
+
+ /**
+ * Test call of multiple urls that should all succeed
+ */
+ public function testMultiHttpClientMultipleSuccess() {
+ // Mock success
+ $httpRequest = $this->getHttpRequest( StatusValue::newGood( 200 ), 200 );
+ $this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
+
+ $reqs = [
+ [
+ 'method' => 'GET',
+ 'url' => 'http://example.test',
+ ],
+ [
+ 'method' => 'GET',
+ 'url' => 'https://get.test',
+ ],
+ ];
+ $responses = $this->client->runMulti( $reqs );
+ foreach ( $responses as $response ) {
+ list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $response['response'];
+ $this->assertEquals( 200, $rcode );
+ }
+ }
+
+ /**
+ * Test call of multiple urls that should all fail
+ */
+ public function testMultiHttpClientMultipleFailure() {
+ // Mock page not found
+ $httpRequest = $this->getHttpRequest(
+ StatusValue::newFatal( "http-bad-status", 404, 'Not Found' ), 404 );
+ $this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
+
+ $reqs = [
+ [
+ 'method' => 'GET',
+ 'url' => 'http://example.test/12345',
+ ],
+ [
+ 'method' => 'GET',
+ 'url' => 'http://example.test/67890' ,
+ ]
+ ];
+ $responses = $this->client->runMulti( $reqs );
+ foreach ( $responses as $response ) {
+ list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $response['response'];
+ $failure = $rcode < 200 || $rcode >= 400;
+ $this->assertTrue( $failure );
+ }
+ }
+
+ /**
+ * Test of response header handling
+ */
+ public function testMultiHttpClientHeaders() {
+ // Represenative headers for typical requests, per MWHttpRequest::getResponseHeaders()
+ $headers = [
+ 'content-type' => [
+ 'text/html; charset=utf-8',
+ ],
+ 'date' => [
+ 'Wed, 18 Jul 2018 14:52:41 GMT',
+ ],
+ 'set-cookie' => [
+ 'COUNTRY=NAe6; expires=Wed, 25-Jul-2018 14:52:41 GMT; path=/; domain=.example.test',
+ 'LAST_NEWS=1531925562; expires=Thu, 18-Jul-2019 14:52:41 GMT; path=/; domain=.example.test',
+ ]
+ ];
+
+ // Mock success with specific headers
+ $httpRequest = $this->getHttpRequest( StatusValue::newGood( 200 ), 200, $headers );
+ $this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
+
+ list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->client->run( [
+ 'method' => 'GET',
+ 'url' => 'http://example.test',
+ ] );
+
+ $this->assertEquals( 200, $rcode );
+ $this->assertEquals( count( $headers ), count( $rhdrs ) );
+ foreach ( $headers as $name => $values ) {
+ $value = implode( ', ', $values );
+ $this->assertArrayHasKey( $name, $rhdrs );
+ $this->assertEquals( $value, $rhdrs[$name] );
+ }
+ }
+}
[ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
[ [ 1 ] ],
[],
- [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+ [ 'text' => [ 'JOIN', [ 'rev_text_id = old_id' ] ] ]
);
parent::assertRevisionExistsInDatabase( $rev );
[ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
[ [ 1 ] ],
[],
- [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+ [ 'text' => [ 'JOIN', [ 'rev_text_id = old_id' ] ] ]
);
parent::assertRevisionExistsInDatabase( $rev );
]
),
'joins' => [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ],
],
]
];
]
),
'joins' => [
- 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
+ 'text' => [ 'JOIN', [ 'rev_text_id=old_id' ] ],
],
]
];
[ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
[ [ 1 ] ],
[],
- [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+ [ 'text' => [ 'JOIN', [ 'rev_text_id = old_id' ] ] ]
);
parent::assertRevisionExistsInDatabase( $rev );
$this->getNewCommentQueryFields( 'rev' )
),
'joins' => [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ],
'user' => [
'LEFT JOIN',
[ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ],
),
'joins' => array_merge(
[
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ],
'user' => [
'LEFT JOIN',
[
),
'joins' => array_merge(
[
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ],
'user' => [
'LEFT JOIN',
[
),
'joins' => array_merge(
[
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ],
'user' => [
'LEFT JOIN',
[
$this->getNewCommentQueryFields( 'rev' )
),
'joins' => [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'page' => [ 'JOIN', [ 'page_id = rev_page' ] ],
'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'comment_rev_comment'
$this->getNewCommentQueryFields( 'rev' )
),
'joins' => [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ], ],
+ 'page' => [ 'JOIN', [ 'page_id = rev_page' ], ],
'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'comment_rev_comment'
=> [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
$this->getNewCommentQueryFields( 'rev' )
),
'joins' => [
- 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
+ 'text' => [ 'JOIN', [ 'rev_text_id=old_id' ] ],
'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'comment_rev_comment'
=> [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
),
'joins' => [
'page' => [
- 'INNER JOIN',
+ 'JOIN',
[ 'page_id = rev_page' ],
],
'user' => [
],
],
'text' => [
- 'INNER JOIN',
+ 'JOIN',
[ 'rev_text_id=old_id' ],
],
'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'content_model',
],
'joins' => [
- 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+ 'content' => [ 'JOIN', [ 'slot_content_id = content_id' ] ],
],
]
];
'model_name',
],
'joins' => [
- 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+ 'content' => [ 'JOIN', [ 'slot_content_id = content_id' ] ],
'content_models' => [ 'LEFT JOIN', [ 'content_model = model_id' ] ],
],
]
public function testRevisionPageJoinCond() {
$this->hideDeprecated( 'Revision::pageJoinCond' );
$this->assertEquals(
- [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ [ 'JOIN', [ 'page_id = rev_page' ] ],
Revision::pageJoinCond()
);
}
[
'tables' => [ 'text' ],
'fields' => [ 'old_id', 'old_text', 'old_flags', 'rev_text_id' ],
- 'joins' => [ 'text' => [ 'INNER JOIN', 'old_id=rev_text_id' ] ]
+ 'joins' => [ 'text' => [ 'JOIN', 'old_id=rev_text_id' ] ]
]
];
}
__METHOD__,
[],
[
- 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ],
- 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ],
+ 'change_tag' => [ 'JOIN', 'ct_log_id = log_id' ],
+ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ],
]
) );
}
__METHOD__,
[],
[
- 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ],
- 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ]
+ 'change_tag' => [ 'JOIN', 'ct_log_id = log_id' ],
+ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ]
]
) );
}
'ctd_name',
[ 'ct_rev_id' => $revId ],
__METHOD__,
- [ 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ] ]
+ [ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ] ]
)
);
}
__METHOD__,
[],
[
- 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ],
- 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ],
+ 'change_tag' => [ 'JOIN', 'ct_log_id = log_id' ],
+ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ],
]
) );
}
'log_title' => strtr( $user->getName(), ' ', '_' )
],
__METHOD__,
- [ 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ] ]
+ [ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ] ]
)
);
}
// HACK if we call $dbr->buildGroupConcatField() now, it will return the wrong table names
// We have to have the test runner call it instead
$baseConcats = [ ',', [ 'change_tag', 'change_tag_def' ], 'ctd_name' ];
- $joinConds = [ 'change_tag_def' => [ 'INNER JOIN', 'ct_tag_id=ctd_id' ] ];
+ $joinConds = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
$groupConcats = [
'recentchanges' => array_merge( $baseConcats, [ 'ct_rc_id=rc_id', $joinConds ] ),
'logging' => array_merge( $baseConcats, [ 'ct_log_id=log_id', $joinConds ] ),
'tables' => [ 'recentchanges', 'change_tag' ],
'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
- 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
+ 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
]
],
'tables' => [ 'logging', 'change_tag' ],
'fields' => [ 'log_id', 'ts_tags' => $groupConcats['logging'] ],
'conds' => [ "log_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
- 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_log_id=log_id' ] ],
+ 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_log_id=log_id' ] ],
'options' => [ 'ORDER BY log_timestamp DESC' ],
]
],
'tables' => [ 'revision', 'change_tag' ],
'fields' => [ 'rev_id', 'rev_timestamp', 'ts_tags' => $groupConcats['revision'] ],
'conds' => [ "rev_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
- 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rev_id=rev_id' ] ],
+ 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rev_id=rev_id' ] ],
'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
]
],
'tables' => [ 'archive', 'change_tag' ],
'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
'conds' => [ "ar_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
- 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rev_id=ar_rev_id' ] ],
+ 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rev_id=ar_rev_id' ] ],
'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
]
],
'tables' => [ 'recentchanges', 'change_tag' ],
'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
- 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
+ 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
'options' => [ 'ORDER BY' => 'rc_timestamp DESC', 'DISTINCT' ],
]
],
'tables' => [ 'recentchanges', 'change_tag' ],
'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
- 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
+ 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
]
],
'tables' => [ 'recentchanges', 'change_tag' ],
'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
- 'join_conds' => [ 'change_tag' => [ 'INNER JOIN', 'ct_rc_id=rc_id' ] ],
+ 'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
'options' => [ 'ORDER BY rc_timestamp DESC', 'DISTINCT' ],
]
],
+++ /dev/null
-<?php
-
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Psr7\Response;
-
-/**
- * Tests for MultiHttpClient
- *
- * The urls herein are not actually called, because we mock the return results.
- *
- * @covers MultiHttpClient
- */
-class MultiHttpClientTest extends MediaWikiTestCase {
- private $successReqs = [
- [
- 'method' => 'GET',
- 'url' => 'http://example.test',
- ],
- [
- 'method' => 'GET',
- 'url' => 'https://get.test',
- ],
- [
- 'method' => 'POST',
- 'url' => 'http://example.test',
- 'body' => [ 'field' => 'value' ],
- ],
- ];
-
- private $failureReqs = [
- [
- 'method' => 'GET',
- 'url' => 'http://example.test',
- ],
- [
- 'method' => 'GET',
- 'url' => 'http://example.test/12345',
- ],
- [
- 'method' => 'POST',
- 'url' => 'http://example.test',
- 'body' => [ 'field' => 'value' ],
- ],
- ];
-
- private function makeHandler( array $rCodes ) {
- $queue = [];
- foreach ( $rCodes as $rCode ) {
- $queue[] = new Response( $rCode );
- }
- return HandlerStack::create( new MockHandler( $queue ) );
- }
-
- /**
- * Test call of a single url that should succeed
- */
- public function testSingleSuccess() {
- $handler = $this->makeHandler( [ 200 ] );
- $client = new MultiHttpClient( [] );
-
- list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $client->run(
- $this->successReqs[0],
- [ 'handler' => $handler ] );
-
- $this->assertEquals( 200, $rcode );
- }
-
- /**
- * Test call of a single url that should not exist, and therefore fail
- */
- public function testSingleFailure() {
- $handler = $this->makeHandler( [ 404 ] );
- $client = new MultiHttpClient( [] );
-
- list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $client->run(
- $this->failureReqs[0],
- [ 'handler' => $handler ] );
-
- $failure = $rcode < 200 || $rcode >= 400;
- $this->assertTrue( $failure );
- }
-
- /**
- * Test call of multiple urls that should all succeed
- */
- public function testMultipleSuccess() {
- $handler = $this->makeHandler( [ 200, 200, 200 ] );
- $client = new MultiHttpClient( [] );
- $responses = $client->runMulti( $this->successReqs, [ 'handler' => $handler ] );
-
- foreach ( $responses as $response ) {
- list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $response['response'];
- $this->assertEquals( 200, $rcode );
- }
- }
-
- /**
- * Test call of multiple urls that should all fail
- */
- public function testMultipleFailure() {
- $handler = $this->makeHandler( [ 404, 404, 404 ] );
- $client = new MultiHttpClient( [] );
- $responses = $client->runMulti( $this->failureReqs, [ 'handler' => $handler ] );
-
- foreach ( $responses as $response ) {
- list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $response['response'];
- $failure = $rcode < 200 || $rcode >= 400;
- $this->assertTrue( $failure );
- }
- }
-
- /**
- * Test call of multiple urls, some of which should succeed and some of which should fail
- */
- public function testMixedSuccessAndFailure() {
- $responseCodes = [ 200, 200, 200, 404, 404, 404 ];
- $handler = $this->makeHandler( $responseCodes );
- $client = new MultiHttpClient( [] );
-
- $responses = $client->runMulti(
- array_merge( $this->successReqs, $this->failureReqs ),
- [ 'handler' => $handler ] );
-
- foreach ( $responses as $index => $response ) {
- list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $response['response'];
- $this->assertEquals( $responseCodes[$index], $rcode );
- }
- }
-
- /**
- * Test of response header handling
- */
- public function testHeaders() {
- // Representative headers for typical requests, per MWHttpRequest::getResponseHeaders()
- $headers = [
- 'content-type' => [
- 'text/html; charset=utf-8',
- ],
- 'date' => [
- 'Wed, 18 Jul 2018 14:52:41 GMT',
- ],
- 'set-cookie' => [
- 'COUNTRY=NAe6; expires=Wed, 25-Jul-2018 14:52:41 GMT; path=/; domain=.example.test',
- 'LAST_NEWS=1531925562; expires=Thu, 18-Jul-2019 14:52:41 GMT; path=/; domain=.example.test',
- ]
- ];
-
- $handler = HandlerStack::create( new MockHandler( [
- new Response( 200, $headers ),
- ] ) );
-
- $client = new MultiHttpClient( [] );
-
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $client->run( [
- 'method' => 'GET',
- 'url' => "http://example.test",
- ],
- [ 'handler' => $handler ] );
- $this->assertEquals( 200, $rcode );
-
- $this->assertEquals( count( $headers ), count( $rhdrs ) );
- foreach ( $headers as $name => $values ) {
- $value = implode( ', ', $values );
- $this->assertArrayHasKey( $name, $rhdrs );
- $this->assertEquals( $value, $rhdrs[$name] );
- }
- }
-}
return new WatchedItemQueryService(
$this->getMockLoadBalancer( $mockDb ),
$this->getMockCommentStore(),
- $this->getMockActorMigration()
+ $this->getMockActorMigration(),
+ $this->getMockWatchedItemStore()
);
}
return $mock;
}
+ /**
+ * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
+ * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
+ */
+ private function getMockWatchedItemStore() {
+ $mock = $this->getMockBuilder( WatchedItemStore::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getLatestNotificationTimestamp' )
+ ->will( $this->returnCallback( function ( $timestamp ) {
+ return $timestamp;
+ } ) );
+ return $mock;
+ }
+
/**
* @param int $id
* @return PHPUnit_Framework_MockObject_MockObject|User
],
[
'watchlist' => [
- 'INNER JOIN',
+ 'JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
],
[
'watchlist' => [
- 'INNER JOIN',
+ 'JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
$expectedJoinConds = array_merge(
[
'watchlist' => [
- 'INNER JOIN',
+ 'JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
$this->isType( 'string' ),
$this->isType( 'array' ),
array_merge( [
- 'watchlist' => [ 'INNER JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
+ 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
], $expectedExtraJoins )
)
[],
[
'watchlist' => [
- 'INNER JOIN',
+ 'JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
[],
[
'watchlist' => [
- 'INNER JOIN',
+ 'JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
[],
[
'watchlist' => [
- 'INNER JOIN',
+ 'JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
[ $title->getNamespace() => [ $title->getDBkey() => null ] ],
$store->getNotificationTimestampsBatch( $user, [ $title ] )
);
+
+ // Run the job queue
+ JobQueueGroup::destroySingletons();
+ $jobs = new RunJobs;
+ $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null );
+ $jobs->execute();
+
$this->assertEquals(
$initialVisitingWatchers,
$store->countVisitingWatchers( $title, '20150202020202' )
return $mock;
}
+ /**
+ * @return PHPUnit_Framework_MockObject_MockObject|JobQueueGroup
+ */
+ private function getMockJobQueueGroup() {
+ $mock = $this->getMockBuilder( JobQueueGroup::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'push' )
+ ->will( $this->returnCallback( function ( Job $job ) {
+ $job->run();
+ } ) );
+ $mock->expects( $this->any() )
+ ->method( 'lazyPush' )
+ ->will( $this->returnCallback( function ( Job $job ) {
+ $job->run();
+ } ) );
+ return $mock;
+ }
+
/**
* @return PHPUnit_Framework_MockObject_MockObject|HashBagOStuff
*/
return $fakeRow;
}
- private function newWatchedItemStore( LBFactory $lbFactory, HashBagOStuff $cache,
+ private function newWatchedItemStore(
+ LBFactory $lbFactory,
+ JobQueueGroup $queueGroup,
+ HashBagOStuff $cache,
ReadOnlyMode $readOnlyMode
) {
return new WatchedItemStore(
$lbFactory,
+ $queueGroup,
+ new HashBagOStuff(),
$cache,
$readOnlyMode,
1000
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() {
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $this->getMockDb() ),
+ $this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode( true )
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$mockLoadBalancer,
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
public function testGetWatchedItemsForUser_badSortOptionThrowsException() {
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $this->getMockDb() ),
+ $this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
->method( 'delete' )
->with( '0:SomeDbKey:1' );
+ $mockQueueGroup = $this->getMockJobQueueGroup();
+ $mockQueueGroup->expects( $this->once() )
+ ->method( 'lazyPush' )
+ ->willReturnCallback( function ( ActivityUpdateJob $job ) {
+ // don't run
+ } );
+
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $mockQueueGroup,
$mockCache,
$this->getMockReadOnlyMode()
);
- // Note: This does not actually assert the job is correct
- $callableCallCounter = 0;
- $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
- $callableCallCounter++;
- $this->assertInternalType( 'callable', $callable );
- };
- $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
-
$this->assertTrue(
$store->resetNotificationTimestamp(
$user,
$title
)
);
- $this->assertEquals( 1, $callableCallCounter );
-
- ScopedCallback::consume( $scopedOverride );
}
public function testResetNotificationTimestamp_noItemForced() {
->method( 'delete' )
->with( '0:SomeDbKey:1' );
+ $mockQueueGroup = $this->getMockJobQueueGroup();
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $mockQueueGroup,
$mockCache,
$this->getMockReadOnlyMode()
);
- // Note: This does not actually assert the job is correct
- $callableCallCounter = 0;
- $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
- $callableCallCounter++;
- $this->assertInternalType( 'callable', $callable );
- };
- $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+ $mockQueueGroup->expects( $this->any() )
+ ->method( 'lazyPush' )
+ ->will( $this->returnCallback( function ( ActivityUpdateJob $job ) {
+ // don't run
+ } ) );
$this->assertTrue(
$store->resetNotificationTimestamp(
'force'
)
);
- $this->assertEquals( 1, $callableCallCounter );
-
- ScopedCallback::consume( $scopedOverride );
}
/**
}
private function verifyCallbackJob(
- $callback,
+ ActivityUpdateJob $job,
LinkTarget $expectedTitle,
$expectedUserId,
callable $notificationTimestampCondition
) {
- $this->assertInternalType( 'callable', $callback );
-
- $callbackReflector = new ReflectionFunction( $callback );
- $vars = $callbackReflector->getStaticVariables();
- $this->assertArrayHasKey( 'job', $vars );
- $this->assertInstanceOf( ActivityUpdateJob::class, $vars['job'] );
-
- /** @var ActivityUpdateJob $job */
- $job = $vars['job'];
$this->assertEquals( $expectedTitle->getDBkey(), $job->getTitle()->getDBkey() );
$this->assertEquals( $expectedTitle->getNamespace(), $job->getTitle()->getNamespace() );
->method( 'delete' )
->with( '0:SomeTitle:1' );
+ $mockQueueGroup = $this->getMockJobQueueGroup();
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $mockQueueGroup,
$mockCache,
$this->getMockReadOnlyMode()
);
- $callableCallCounter = 0;
- $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
- function ( $callable ) use ( &$callableCallCounter, $title, $user ) {
- $callableCallCounter++;
- $this->verifyCallbackJob(
- $callable,
- $title,
- $user->getId(),
- function ( $time ) {
- return $time === null;
- }
- );
- }
- );
+ $mockQueueGroup->expects( $this->any() )
+ ->method( 'lazyPush' )
+ ->will( $this->returnCallback(
+ function ( ActivityUpdateJob $job ) use ( $title, $user ) {
+ $this->verifyCallbackJob(
+ $job,
+ $title,
+ $user->getId(),
+ function ( $time ) {
+ return $time === null;
+ }
+ );
+ }
+ ) );
$this->assertTrue(
$store->resetNotificationTimestamp(
$oldid
)
);
- $this->assertEquals( 1, $callableCallCounter );
-
- ScopedCallback::consume( $scopedOverride );
}
public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() {
->method( 'delete' )
->with( '0:SomeDbKey:1' );
+ $mockQueueGroup = $this->getMockJobQueueGroup();
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $mockQueueGroup,
$mockCache,
$this->getMockReadOnlyMode()
);
- $addUpdateCallCounter = 0;
- $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
- function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
- $addUpdateCallCounter++;
- $this->verifyCallbackJob(
- $callable,
- $title,
- $user->getId(),
- function ( $time ) {
- return $time !== null && $time > '20151212010101';
- }
- );
- }
- );
+ $mockQueueGroup->expects( $this->any() )
+ ->method( 'lazyPush' )
+ ->will( $this->returnCallback(
+ function ( ActivityUpdateJob $job ) use ( $title, $user ) {
+ $this->verifyCallbackJob(
+ $job,
+ $title,
+ $user->getId(),
+ function ( $time ) {
+ return $time !== null && $time > '20151212010101';
+ }
+ );
+ }
+ ) );
$getTimestampCallCounter = 0;
$scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
$oldid
)
);
- $this->assertEquals( 1, $addUpdateCallCounter );
$this->assertEquals( 1, $getTimestampCallCounter );
- ScopedCallback::consume( $scopedOverrideDeferred );
ScopedCallback::consume( $scopedOverrideRevision );
}
->method( 'delete' )
->with( '0:SomeDbKey:1' );
+ $mockQueueGroup = $this->getMockJobQueueGroup();
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $mockQueueGroup,
$mockCache,
$this->getMockReadOnlyMode()
);
- $callableCallCounter = 0;
- $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
- function ( $callable ) use ( &$callableCallCounter, $title, $user ) {
- $callableCallCounter++;
- $this->verifyCallbackJob(
- $callable,
- $title,
- $user->getId(),
- function ( $time ) {
- return $time === null;
- }
- );
- }
- );
+ $mockQueueGroup->expects( $this->any() )
+ ->method( 'lazyPush' )
+ ->will( $this->returnCallback(
+ function ( ActivityUpdateJob $job ) use ( $title, $user ) {
+ $this->verifyCallbackJob(
+ $job,
+ $title,
+ $user->getId(),
+ function ( $time ) {
+ return $time === null;
+ }
+ );
+ }
+ ) );
$this->assertTrue(
$store->resetNotificationTimestamp(
$oldid
)
);
- $this->assertEquals( 1, $callableCallCounter );
-
- ScopedCallback::consume( $scopedOverride );
}
public function testResetNotificationTimestamp_futureNotificationTimestampForced() {
->method( 'delete' )
->with( '0:SomeDbKey:1' );
+ $mockQueueGroup = $this->getMockJobQueueGroup();
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $mockQueueGroup,
$mockCache,
$this->getMockReadOnlyMode()
);
- $addUpdateCallCounter = 0;
- $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
- function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
- $addUpdateCallCounter++;
- $this->verifyCallbackJob(
- $callable,
- $title,
- $user->getId(),
- function ( $time ) {
- return $time === '30151212010101';
- }
- );
- }
- );
+ $mockQueueGroup->expects( $this->any() )
+ ->method( 'lazyPush' )
+ ->will( $this->returnCallback(
+ function ( ActivityUpdateJob $job ) use ( $title, $user ) {
+ $this->verifyCallbackJob(
+ $job,
+ $title,
+ $user->getId(),
+ function ( $time ) {
+ return $time === '30151212010101';
+ }
+ );
+ }
+ ) );
$getTimestampCallCounter = 0;
$scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
$oldid
)
);
- $this->assertEquals( 1, $addUpdateCallCounter );
$this->assertEquals( 1, $getTimestampCallCounter );
- ScopedCallback::consume( $scopedOverrideDeferred );
ScopedCallback::consume( $scopedOverrideRevision );
}
->method( 'delete' )
->with( '0:SomeDbKey:1' );
+ $mockQueueGroup = $this->getMockJobQueueGroup();
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $mockQueueGroup,
$mockCache,
$this->getMockReadOnlyMode()
);
- $addUpdateCallCounter = 0;
- $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
- function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
- $addUpdateCallCounter++;
- $this->verifyCallbackJob(
- $callable,
- $title,
- $user->getId(),
- function ( $time ) {
- return $time === false;
- }
- );
- }
- );
+ $mockQueueGroup->expects( $this->any() )
+ ->method( 'lazyPush' )
+ ->will( $this->returnCallback(
+ function ( ActivityUpdateJob $job ) use ( $title, $user ) {
+ $this->verifyCallbackJob(
+ $job,
+ $title,
+ $user->getId(),
+ function ( $time ) {
+ return $time === false;
+ }
+ );
+ }
+ ) );
$getTimestampCallCounter = 0;
$scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
$oldid
)
);
- $this->assertEquals( 1, $addUpdateCallCounter );
$this->assertEquals( 1, $getTimestampCallCounter );
- ScopedCallback::consume( $scopedOverrideDeferred );
ScopedCallback::consume( $scopedOverrideRevision );
}
public function testSetNotificationTimestampsForUser_anonUser() {
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $this->getMockDb() ),
+ $this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$this->getMockCache(),
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
$store = $this->newWatchedItemStore(
$this->getMockLBFactory( $mockDb ),
+ $this->getMockJobQueueGroup(),
$mockCache,
$this->getMockReadOnlyMode()
);
};
},
teardown: function () {
+ mw.loader.maxQueryLength = 2000;
// Teardown for StringSet shim test
if ( this.nativeSet ) {
window.Set = this.nativeSet;
[ 'testUrlIncDump', 'dump', [], null, 'testloader' ]
] );
- mw.config.set( 'wgResourceLoaderMaxQueryLength', 10 );
+ mw.loader.maxQueryLength = 10;
return mw.loader.using( [ 'testUrlIncDump', 'testUrlInc' ] ).then( function ( require ) {
assert.propEqual(