From: jenkins-bot Date: Mon, 6 May 2019 19:59:46 +0000 (+0000) Subject: Merge "Consolidate duplicated unseen change logic and fix inconsistent code" X-Git-Tag: 1.34.0-rc.0~1790 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/membres/fiche.php?a=commitdiff_plain;h=57609f4ded4c9ac529d7fbfd30d3585372fcc0ce;hp=03d37f283b91f5a981ce131ac99a1a11151db53b;p=lhc%2Fweb%2Fwiklou.git Merge "Consolidate duplicated unseen change logic and fix inconsistent code" --- diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index 5d46edd8c2..b58c2694b7 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -25,6 +25,7 @@ Some specific notes for MediaWiki 1.34 upgrades are below: For notes on 1.33.x and older releases, see HISTORY. === Configuration changes for system administrators in 1.34 === + ==== New configuration ==== * … @@ -41,6 +42,7 @@ For notes on 1.33.x and older releases, see HISTORY. * … === External library changes in 1.34 === + ==== New external libraries ==== * … @@ -114,7 +116,7 @@ because of Phabricator reports. * … === Deprecations in 1.34 === -* The MWNamespace class is deprecated. Use MediaWikiServices::getNamespaceInfo. +* The MWNamespace class is deprecated. Use NamespaceInfo. * ExtensionRegistry->load() is deprecated, as it breaks dependency checking. Instead, use ->queue(). * User::isBlocked() is deprecated since it does not tell you if the user is @@ -126,6 +128,25 @@ because of Phabricator reports. instead. * The Config argument to ChangesListSpecialPage::checkStructuredFilterUiEnabled is deprecated. Pass only the User argument. +* WatchedItem::getUser is deprecated. Use getUserIdentity. +* Passing a Title as the first parameter to the getTimestampById method of + RevisionStore is deprecated. Omit it, passing only the remaining parameters. +* Title::getPreviousRevisionId and Title::getNextRevisionId are deprecated. Use + RevisionLookup::getPreviousRevision and RevisionLookup::getNextRevision. +* The Title parameter to RevisionLookup::getPreviousRevision and + RevisionLookup::getNextRevision is deprecated and should be omitted. +* MWHttpRequest::factory is deprecated. Use HttpRequestFactory. +* The Http class is deprecated. For the request, get, and post methods, use + HttpRequestFactory. For isValidURI, use MWHttpRequest::isValidURI. For + getProxy, use (string)$wgHTTPProxy. For createMultiClient, construct a + MultiHttpClient directly. +* Http::$httpEngine is deprecated and has no replacement. The default 'guzzle' + engine will eventually be made the only engine for HTTP requests. +* RepoGroup::singleton(), RepoGroup::destroySingleton(), + RepoGroup::setSingleton(), wfFindFile(), and wfLocalFile() are all + deprecated. Use MediaWikiServices instead. +* The getSubjectPage, getTalkPage, and getOtherPage of Title are deprecated. + Use NamespaceInfo's getSubjectPage, getTalkPage, and getAssociatedPage. === Other changes in 1.34 === * … diff --git a/autoload.php b/autoload.php index f8e90b73c1..5d3e578b20 100644 --- a/autoload.php +++ b/autoload.php @@ -27,6 +27,7 @@ $wgAutoloadLocalClasses = [ 'ApiAuthManagerHelper' => __DIR__ . '/includes/api/ApiAuthManagerHelper.php', 'ApiBase' => __DIR__ . '/includes/api/ApiBase.php', 'ApiBlock' => __DIR__ . '/includes/api/ApiBlock.php', + 'ApiBlockInfoTrait' => __DIR__ . '/includes/api/ApiBlockInfoTrait.php', 'ApiCSPReport' => __DIR__ . '/includes/api/ApiCSPReport.php', 'ApiChangeAuthenticationData' => __DIR__ . '/includes/api/ApiChangeAuthenticationData.php', 'ApiCheckToken' => __DIR__ . '/includes/api/ApiCheckToken.php', @@ -1564,6 +1565,7 @@ $wgAutoloadLocalClasses = [ 'UserNamePrefixSearch' => __DIR__ . '/includes/user/UserNamePrefixSearch.php', 'UserNotLoggedIn' => __DIR__ . '/includes/exception/UserNotLoggedIn.php', 'UserOptionsMaintenance' => __DIR__ . '/maintenance/userOptions.php', + 'UserOptionsUpdateJob' => __DIR__ . '/includes/jobqueue/jobs/UserOptionsUpdateJob.php', 'UserPasswordPolicy' => __DIR__ . '/includes/password/UserPasswordPolicy.php', 'UserRightsProxy' => __DIR__ . '/includes/user/UserRightsProxy.php', 'UserrightsPage' => __DIR__ . '/includes/specials/SpecialUserrights.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index b40d33b17e..4ba18369ea 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -6852,7 +6852,7 @@ $wgRCLinkDays = [ 1, 3, 7, 14, 30 ]; * FormattedRCFeed-specific options: * - 'uri' -- [required] The address to which the messages are sent. * The uri scheme of this string will be looked up in $wgRCEngines - * to determine which RCFeedEngine class to use. + * to determine which FormattedRCFeed class to use. * - 'formatter' -- [required] The class (implementing RCFeedFormatter) which will * produce the text to send. This can also be an object of the class. * Formatters available by default: JSONRCFeedFormatter, XMLRCFeedFormatter, @@ -7506,6 +7506,7 @@ $wgServiceWiringFiles = [ * can add to this to provide custom jobs. * A job handler should either be a class name to be instantiated, * or (since 1.30) a callback to use for creating the job object. + * The callback takes (Title, array map of parameters) as arguments. */ $wgJobClasses = [ 'deletePage' => DeletePageJob::class, @@ -7530,6 +7531,7 @@ $wgJobClasses = [ 'cdnPurge' => CdnPurgeJob::class, 'userGroupExpiry' => UserGroupExpiryJob::class, 'clearWatchlistNotifications' => ClearWatchlistNotificationsJob::class, + 'userOptionsUpdate' => UserOptionsUpdateJob::class, 'enqueue' => EnqueueJob::class, // local queue for multi-DC setups 'null' => NullJob::class, ]; @@ -8397,7 +8399,7 @@ $wgAsyncHTTPTimeout = 25; /** * Proxy to use for CURL requests. */ -$wgHTTPProxy = false; +$wgHTTPProxy = ''; /** * Local virtual hosts. diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index c7a45c72b3..66a4d9a34f 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -2633,25 +2633,25 @@ function wfGetLBFactory() { /** * Find a file. - * Shortcut for RepoGroup::singleton()->findFile() - * + * @deprecated since 1.34, use MediaWikiServices * @param string|LinkTarget $title String or LinkTarget object * @param array $options Associative array of options (see RepoGroup::findFile) * @return File|bool File, or false if the file does not exist */ function wfFindFile( $title, $options = [] ) { - return RepoGroup::singleton()->findFile( $title, $options ); + return MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title, $options ); } /** * Get an object referring to a locally registered file. * Returns a valid placeholder object if the file does not exist. * + * @deprecated since 1.34, use MediaWikiServices * @param Title|string $title * @return LocalFile|null A File, or null if passed an invalid Title */ function wfLocalFile( $title ) { - return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); + return MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $title ); } /** diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index c374a62523..d6f50bf8aa 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -48,6 +48,7 @@ use ParserCache; use ParserFactory; use PasswordFactory; use ProxyLookup; +use RepoGroup; use ResourceLoader; use SearchEngine; use SearchEngineConfig; @@ -789,6 +790,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'ReadOnlyMode' ); } + /** + * @since 1.34 + * @return RepoGroup + */ + public function getRepoGroup() : RepoGroup { + return $this->getService( 'RepoGroup' ); + } + /** * @since 1.33 * @return ResourceLoader diff --git a/includes/MovePage.php b/includes/MovePage.php index 24178acdad..e49398a0f7 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -233,14 +233,69 @@ class MovePage { } /** + * Move a page without taking user permissions into account. Only checks if the move is itself + * invalid, e.g., trying to move a special page or trying to move a page onto one that already + * exists. + * + * @param User $user + * @param string|null $reason + * @param bool|null $createRedirect + * @param string[] $changeTags Change tags to apply to the entry in the move log + * @return Status + */ + public function move( + User $user, $reason = null, $createRedirect = true, array $changeTags = [] + ) { + $status = $this->isValidMove(); + if ( !$status->isOK() ) { + return $status; + } + + return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags ); + } + + /** + * Same as move(), but with permissions checks. + * + * @param User $user + * @param string|null $reason + * @param bool|null $createRedirect Ignored if user doesn't have suppressredirect permission + * @param string[] $changeTags Change tags to apply to the entry in the move log + * @return Status + */ + public function moveIfAllowed( + User $user, $reason = null, $createRedirect = true, array $changeTags = [] + ) { + $status = $this->isValidMove(); + $status->merge( $this->checkPermissions( $user, $reason ) ); + if ( $changeTags ) { + $status->merge( ChangeTags::canAddTagsAccompanyingChange( $changeTags, $user ) ); + } + + if ( !$status->isOK() ) { + // Auto-block user's IP if the account was "hard" blocked + $user->spreadAnyEditBlock(); + return $status; + } + + // Check suppressredirect permission + if ( !$user->isAllowed( 'suppressredirect' ) ) { + $createRedirect = true; + } + + return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags ); + } + + /** + * Moves *without* any sort of safety or sanity checks. Hooks can still fail the move, however. + * * @param User $user * @param string $reason * @param bool $createRedirect - * @param string[] $changeTags Change tags to apply to the entry in the move log. Caller - * should perform permission checks with ChangeTags::canAddTagsAccompanyingChange + * @param string[] $changeTags Change tags to apply to the entry in the move log * @return Status */ - public function move( User $user, $reason, $createRedirect, array $changeTags = [] ) { + private function moveUnsafe( User $user, $reason, $createRedirect, array $changeTags ) { global $wgCategoryCollation; $status = Status::newGood(); diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php index 549b7ba45b..e44380328a 100644 --- a/includes/Permissions/PermissionManager.php +++ b/includes/Permissions/PermissionManager.php @@ -66,12 +66,16 @@ class PermissionManager { /** @var bool If set to true, blocked users will no longer be allowed to log in */ private $blockDisablesLogin; + /** @var NamespaceInfo */ + private $nsInfo; + /** * @param SpecialPageFactory $specialPageFactory * @param string[] $whitelistRead * @param string[] $whitelistReadRegexp * @param bool $emailConfirmToEdit * @param bool $blockDisablesLogin + * @param NamespaceInfo $nsInfo */ public function __construct( SpecialPageFactory $specialPageFactory, diff --git a/includes/Pingback.php b/includes/Pingback.php index 8d7c3b6e4d..f4e85adfdb 100644 --- a/includes/Pingback.php +++ b/includes/Pingback.php @@ -22,6 +22,7 @@ use Psr\Log\LoggerInterface; use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; /** * Send information about this MediaWiki instance to MediaWiki.org. @@ -229,7 +230,7 @@ class Pingback { $json = FormatJson::encode( $data ); $queryString = rawurlencode( str_replace( ' ', '\u0020', $json ) ) . ';'; $url = 'https://www.mediawiki.org/beacon/event?' . $queryString; - return Http::post( $url ) !== false; + return MediaWikiServices::getInstance()->getHttpRequestFactory()->post( $url ) !== null; } /** diff --git a/includes/Revision.php b/includes/Revision.php index cbaff90d69..de3c2998b5 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -1008,9 +1008,8 @@ class Revision implements IDBAccessObject { * @return Revision|null */ public function getPrevious() { - $title = $this->getTitle(); - $rec = self::getRevisionLookup()->getPreviousRevision( $this->mRecord, $title ); - return $rec ? new Revision( $rec, self::READ_NORMAL, $title ) : null; + $rec = self::getRevisionLookup()->getPreviousRevision( $this->mRecord ); + return $rec ? new Revision( $rec, self::READ_NORMAL, $this->getTitle() ) : null; } /** @@ -1019,9 +1018,8 @@ class Revision implements IDBAccessObject { * @return Revision|null */ public function getNext() { - $title = $this->getTitle(); - $rec = self::getRevisionLookup()->getNextRevision( $this->mRecord, $title ); - return $rec ? new Revision( $rec, self::READ_NORMAL, $title ) : null; + $rec = self::getRevisionLookup()->getNextRevision( $this->mRecord ); + return $rec ? new Revision( $rec, self::READ_NORMAL, $this->getTitle() ) : null; } /** @@ -1256,13 +1254,13 @@ class Revision implements IDBAccessObject { /** * Get rev_timestamp from rev_id, without loading the rest of the row * - * @param Title $title + * @param Title $title (ignored since 1.34) * @param int $id * @param int $flags * @return string|bool False if not found */ static function getTimestampFromId( $title, $id, $flags = 0 ) { - return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags ); + return self::getRevisionStore()->getTimestampFromId( $id, $flags ); } /** diff --git a/includes/Revision/RevisionLookup.php b/includes/Revision/RevisionLookup.php index db6c7c30b4..17cafc6a5d 100644 --- a/includes/Revision/RevisionLookup.php +++ b/includes/Revision/RevisionLookup.php @@ -85,11 +85,12 @@ interface RevisionLookup extends IDBAccessObject { * MCR migration note: this replaces Revision::getPrevious * * @param RevisionRecord $rev - * @param Title|null $title if known (optional) + * @param int $flags (optional) $flags include: + * IDBAccessObject::READ_LATEST: Select the data from the master * * @return RevisionRecord|null */ - public function getPreviousRevision( RevisionRecord $rev, Title $title = null ); + public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ); /** * Get next revision for this title @@ -97,11 +98,24 @@ interface RevisionLookup extends IDBAccessObject { * MCR migration note: this replaces Revision::getNext * * @param RevisionRecord $rev - * @param Title|null $title if known (optional) + * @param int $flags (optional) $flags include: + * IDBAccessObject::READ_LATEST: Select the data from the master * * @return RevisionRecord|null */ - public function getNextRevision( RevisionRecord $rev, Title $title = null ); + public function getNextRevision( RevisionRecord $rev, $flags = 0 ); + + /** + * Get rev_timestamp from rev_id, without loading the rest of the row. + * + * MCR migration note: this replaces Revision::getTimestampFromId + * + * @param int $id + * @param int $flags + * @return string|bool False if not found + * @since 1.34 (present earlier in RevisionStore) + */ + public function getTimestampFromId( $id, $flags = 0 ); /** * Load a revision based on a known page ID and current revision ID from the DB diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index 0329bd1587..ea4cf88825 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -278,12 +278,13 @@ class RevisionStore /** * @param int $mode DB_MASTER or DB_REPLICA + * @param array $groups * * @return IDatabase */ - private function getDBConnection( $mode ) { + private function getDBConnection( $mode, $groups = [] ) { $lb = $this->getDBLoadBalancer(); - return $lb->getConnection( $mode, [], $this->wikiId ); + return $lb->getConnection( $mode, $groups, $this->wikiId ); } /** @@ -1739,7 +1740,8 @@ class RevisionStore $user = User::newFromAnyId( $row->ar_user ?? null, $row->ar_user_text ?? null, - $row->ar_actor ?? null + $row->ar_actor ?? null, + $this->wikiId ); } catch ( InvalidArgumentException $ex ) { wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() ); @@ -1793,7 +1795,8 @@ class RevisionStore $user = User::newFromAnyId( $row->rev_user ?? null, $row->rev_user_text ?? null, - $row->rev_actor ?? null + $row->rev_actor ?? null, + $this->wikiId ); } catch ( InvalidArgumentException $ex ) { wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() ); @@ -1931,14 +1934,21 @@ class RevisionStore /** @var UserIdentity $user */ $user = null; - if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) { + // If a user is passed in, use it if possible. We cannot use a user from a + // remote wiki with unsuppressed ids, due to issues described in T222212. + if ( isset( $fields['user'] ) && + ( $fields['user'] instanceof UserIdentity ) && + ( $this->wikiId === false || + ( !$fields['user']->getId() && !$fields['user']->getActorId() ) ) + ) { $user = $fields['user']; } else { try { $user = User::newFromAnyId( $fields['user'] ?? null, $fields['user_text'] ?? null, - $fields['actor'] ?? null + $fields['actor'] ?? null, + $this->wikiId ); } catch ( InvalidArgumentException $ex ) { $user = null; @@ -2548,20 +2558,17 @@ class RevisionStore } /** - * Get the revision before $rev in the page's history, if any. - * Will return null for the first revision but also for deleted or unsaved revisions. - * - * MCR migration note: this replaces Revision::getPrevious - * - * @see Title::getPreviousRevisionID - * @see PageArchive::getPreviousRevision + * Implementation of getPreviousRevision and getNextRevision. * * @param RevisionRecord $rev - * @param Title|null $title if known (optional) - * + * @param int $flags + * @param string $dir 'next' or 'prev' * @return RevisionRecord|null */ - public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) { + private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) { + $op = $dir === 'next' ? '>' : '<'; + $sort = $dir === 'next' ? 'ASC' : 'DESC'; + if ( !$rev->getId() || !$rev->getPageId() ) { // revision is unsaved or otherwise incomplete return null; @@ -2572,54 +2579,86 @@ class RevisionStore return null; } - if ( $title === null ) { - // this would fail for deleted revisions - $title = $this->getTitle( $rev->getPageId(), $rev->getId() ); + list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags ); + $db = $this->getDBConnection( $dbType, [ 'contributions' ] ); + + $ts = $this->getTimestampFromId( $rev->getId(), $flags ); + if ( $ts === false ) { + // XXX Should this be moved into getTimestampFromId? + $ts = $db->selectField( 'archive', 'ar_timestamp', + [ 'ar_rev_id' => $rev->getId() ], __METHOD__ ); + if ( $ts === false ) { + // XXX Is this reachable? How can we have a page id but no timestamp? + return null; + } } + $ts = $db->addQuotes( $db->timestamp( $ts ) ); - $prev = $title->getPreviousRevisionID( $rev->getId() ); - if ( !$prev ) { + $revId = $db->selectField( 'revision', 'rev_id', + [ + 'rev_page' => $rev->getPageId(), + "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})" + ], + __METHOD__, + [ + 'ORDER BY' => "rev_timestamp $sort, rev_id $sort", + 'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319 + ] + ); + + if ( $revId === false ) { return null; } - return $this->getRevisionByTitle( $title, $prev ); + return $this->getRevisionById( intval( $revId ) ); } /** - * Get the revision after $rev in the page's history, if any. - * Will return null for the latest revision but also for deleted or unsaved revisions. + * Get the revision before $rev in the page's history, if any. + * Will return null for the first revision but also for deleted or unsaved revisions. * - * MCR migration note: this replaces Revision::getNext + * MCR migration note: this replaces Revision::getPrevious * - * @see Title::getNextRevisionID + * @see Title::getPreviousRevisionID + * @see PageArchive::getPreviousRevision * * @param RevisionRecord $rev - * @param Title|null $title if known (optional) + * @param int $flags (optional) $flags include: + * IDBAccessObject::READ_LATEST: Select the data from the master * * @return RevisionRecord|null */ - public function getNextRevision( RevisionRecord $rev, Title $title = null ) { - if ( !$rev->getId() || !$rev->getPageId() ) { - // revision is unsaved or otherwise incomplete - return null; - } - - if ( $rev instanceof RevisionArchiveRecord ) { - // revision is deleted, so it's not part of the page history - return null; + public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) { + if ( $flags instanceof Title ) { + // Old calling convention, we don't use Title here anymore + wfDeprecated( __METHOD__ . ' with Title', '1.34' ); + $flags = 0; } - if ( $title === null ) { - // this would fail for deleted revisions - $title = $this->getTitle( $rev->getPageId(), $rev->getId() ); - } + return $this->getRelativeRevision( $rev, $flags, 'prev' ); + } - $next = $title->getNextRevisionID( $rev->getId() ); - if ( !$next ) { - return null; + /** + * Get the revision after $rev in the page's history, if any. + * Will return null for the latest revision but also for deleted or unsaved revisions. + * + * MCR migration note: this replaces Revision::getNext + * + * @see Title::getNextRevisionID + * + * @param RevisionRecord $rev + * @param int $flags (optional) $flags include: + * IDBAccessObject::READ_LATEST: Select the data from the master + * @return RevisionRecord|null + */ + public function getNextRevision( RevisionRecord $rev, $flags = 0 ) { + if ( $flags instanceof Title ) { + // Old calling convention, we don't use Title here anymore + wfDeprecated( __METHOD__ . ' with Title', '1.34' ); + $flags = 0; } - return $this->getRevisionByTitle( $title, $next ); + return $this->getRelativeRevision( $rev, $flags, 'next' ); } /** @@ -2658,21 +2697,27 @@ class RevisionStore } /** - * Get rev_timestamp from rev_id, without loading the rest of the row + * Get rev_timestamp from rev_id, without loading the rest of the row. + * + * Historically, there was an extra Title parameter that was passed before $id. This is no + * longer needed and is deprecated in 1.34. * * MCR migration note: this replaces Revision::getTimestampFromId * - * @param Title $title * @param int $id * @param int $flags * @return string|bool False if not found */ - public function getTimestampFromId( $title, $id, $flags = 0 ) { + public function getTimestampFromId( $id, $flags = 0 ) { + if ( $id instanceof Title ) { + // Old deprecated calling convention supported for backwards compatibility + $id = $flags; + $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0; + } $db = $this->getDBConnectionRefForQueryFlags( $flags ); - $conds = [ 'rev_id' => $id ]; - $conds['rev_page'] = $title->getArticleID(); - $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); + $timestamp = + $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ ); return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false; } diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index bf722c38b2..f74ba793e9 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -208,7 +208,7 @@ return [ }, 'GenderCache' => function ( MediaWikiServices $services ) : GenderCache { - return new GenderCache(); + return new GenderCache( $services->getNamespaceInfo() ); }, 'HttpRequestFactory' => @@ -231,7 +231,8 @@ return [ 'LinkCache' => function ( MediaWikiServices $services ) : LinkCache { return new LinkCache( $services->getTitleFormatter(), - $services->getMainWANObjectCache() + $services->getMainWANObjectCache(), + $services->getNamespaceInfo() ); }, @@ -363,7 +364,8 @@ return [ }, 'NamespaceInfo' => function ( MediaWikiServices $services ) : NamespaceInfo { - return new NamespaceInfo( $services->getMainConfig() ); + return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions, + $services->getMainConfig() ) ); }, 'NameTableStoreFactory' => function ( MediaWikiServices $services ) : NameTableStoreFactory { @@ -460,7 +462,8 @@ return [ DefaultPreferencesFactory::$constructorOptions, $services->getMainConfig() ), $services->getContentLanguage(), AuthManager::singleton(), - $services->getLinkRendererFactory()->create() + $services->getLinkRendererFactory()->create(), + $services->getNamespaceInfo() ); $factory->setLogger( LoggerFactory::getInstance( 'preferences' ) ); @@ -482,6 +485,15 @@ return [ ); }, + 'RepoGroup' => function ( MediaWikiServices $services ) : RepoGroup { + $config = $services->getMainConfig(); + return new RepoGroup( + $config->get( 'LocalFileRepo' ), + $config->get( 'ForeignFileRepos' ), + $services->getMainWANObjectCache() + ); + }, + 'ResourceLoader' => function ( MediaWikiServices $services ) : ResourceLoader { // @todo This should not take a Config object, but it's not so easy to remove because it // exposes it in a getter, which is actually used. @@ -696,7 +708,9 @@ return [ $services->getMainObjectStash(), new HashBagOStuff( [ 'maxKeys' => 100 ] ), $services->getReadOnlyMode(), - $services->getMainConfig()->get( 'UpdateRowsPerQuery' ) + $services->getMainConfig()->get( 'UpdateRowsPerQuery' ), + $services->getNamespaceInfo(), + $services->getRevisionLookup() ); $store->setStatsdDataFactory( $services->getStatsdDataFactory() ); diff --git a/includes/Title.php b/includes/Title.php index 27baeb2a7e..ad6c167937 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1501,10 +1501,12 @@ class Title implements LinkTarget, IDBAccessObject { /** * Get a Title object associated with the talk page of this article * + * @deprecated since 1.34, use NamespaceInfo::getTalkPage * @return Title The object for the talk page */ public function getTalkPage() { - return self::makeTitle( MWNamespace::getTalk( $this->mNamespace ), $this->mDbkeyform ); + return self::castFromLinkTarget( + MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) ); } /** @@ -1528,37 +1530,26 @@ class Title implements LinkTarget, IDBAccessObject { * Get a title object associated with the subject page of this * talk page * + * @deprecated since 1.34, use NamespaceInfo::getSubjectPage * @return Title The object for the subject page */ public function getSubjectPage() { - // Is this the same title? - $subjectNS = MWNamespace::getSubject( $this->mNamespace ); - if ( $this->mNamespace == $subjectNS ) { - return $this; - } - return self::makeTitle( $subjectNS, $this->mDbkeyform ); + return self::castFromLinkTarget( + MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) ); } /** * Get the other title for this page, if this is a subject page * get the talk page, if it is a subject page get the talk page * + * @deprecated since 1.34, use NamespaceInfo::getAssociatedPage * @since 1.25 * @throws MWException If the page doesn't have an other page * @return Title */ public function getOtherPage() { - if ( $this->isSpecialPage() ) { - throw new MWException( 'Special pages cannot have other pages' ); - } - if ( $this->isTalkPage() ) { - return $this->getSubjectPage(); - } else { - if ( !$this->canHaveTalkPage() ) { - throw new MWException( "{$this->getPrefixedText()} does not have an other page" ); - } - return $this->getTalkPage(); - } + return self::castFromLinkTarget( + MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) ); } /** @@ -3445,19 +3436,10 @@ class Title implements LinkTarget, IDBAccessObject { array $changeTags = [] ) { global $wgUser; - $err = $this->isValidMoveOperation( $nt, $auth, $reason ); - if ( is_array( $err ) ) { - // Auto-block user's IP if the account was "hard" blocked - $wgUser->spreadAnyEditBlock(); - return $err; - } - // Check suppressredirect permission - if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) { - $createRedirect = true; - } $mp = new MovePage( $this, $nt ); - $status = $mp->move( $wgUser, $reason, $createRedirect, $changeTags ); + $method = $auth ? 'moveIfAllowed' : 'move'; + $status = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags ); if ( $status->isOK() ) { return true; } else { @@ -3730,57 +3712,25 @@ class Title implements LinkTarget, IDBAccessObject { * @return int|bool New revision ID, or false if none exists */ private function getRelativeRevisionID( $revId, $flags, $dir ) { - $revId = (int)$revId; - if ( $dir === 'next' ) { - $op = '>'; - $sort = 'ASC'; - } elseif ( $dir === 'prev' ) { - $op = '<'; - $sort = 'DESC'; - } else { - throw new InvalidArgumentException( '$dir must be "next" or "prev"' ); - } - - if ( $flags & self::GAID_FOR_UPDATE ) { - $db = wfGetDB( DB_MASTER ); - } else { - $db = wfGetDB( DB_REPLICA, 'contributions' ); - } - - // Intentionally not caring if the specified revision belongs to this - // page. We only care about the timestamp. - $ts = $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $revId ], __METHOD__ ); - if ( $ts === false ) { - $ts = $db->selectField( 'archive', 'ar_timestamp', [ 'ar_rev_id' => $revId ], __METHOD__ ); - if ( $ts === false ) { - // Or should this throw an InvalidArgumentException or something? - return false; - } + $rl = MediaWikiServices::getInstance()->getRevisionLookup(); + $rlFlags = $flags === self::GAID_FOR_UPDATE ? IDBAccessObject::READ_LATEST : 0; + $rev = $rl->getRevisionById( $revId, $rlFlags ); + if ( !$rev ) { + return false; } - $ts = $db->addQuotes( $ts ); - - $revId = $db->selectField( 'revision', 'rev_id', - [ - 'rev_page' => $this->getArticleID( $flags ), - "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op $revId)" - ], - __METHOD__, - [ - 'ORDER BY' => "rev_timestamp $sort, rev_id $sort", - 'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319 - ] - ); - - if ( $revId === false ) { + $oldRev = $dir === 'next' + ? $rl->getNextRevision( $rev, $rlFlags ) + : $rl->getPreviousRevision( $rev, $rlFlags ); + if ( !$oldRev ) { return false; - } else { - return intval( $revId ); } + return $oldRev->getId(); } /** * Get the revision ID of the previous revision * + * @deprecated since 1.34, use RevisionLookup::getPreviousRevision * @param int $revId Revision ID. Get the revision that was before this one. * @param int $flags Title::GAID_FOR_UPDATE * @return int|bool Old revision ID, or false if none exists @@ -3792,6 +3742,7 @@ class Title implements LinkTarget, IDBAccessObject { /** * Get the revision ID of the next revision * + * @deprecated since 1.34, use RevisionLookup::getNextRevision * @param int $revId Revision ID. Get the revision that was after this one. * @param int $flags Title::GAID_FOR_UPDATE * @return int|bool Next revision ID, or false if none exists @@ -4031,14 +3982,14 @@ class Title implements LinkTarget, IDBAccessObject { /** * Compare with another title. * - * @param Title $title + * @param LinkTarget $title * @return bool */ - public function equals( Title $title ) { + public function equals( LinkTarget $title ) { // Note: === is necessary for proper matching of number-like titles. - return $this->mInterwiki === $title->mInterwiki - && $this->mNamespace == $title->mNamespace - && $this->mDbkeyform === $title->mDbkeyform; + return $this->mInterwiki === $title->getInterwiki() + && $this->mNamespace == $title->getNamespace() + && $this->mDbkeyform === $title->getDBkey(); } /** diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 8ab92aff4b..19d84f74b8 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -36,6 +36,8 @@ use Wikimedia\Rdbms\IDatabase; */ abstract class ApiBase extends ContextSource { + use ApiBlockInfoTrait; + /** * @name Constants for ::getAllowedParams() arrays * These constants are keys in the arrays returned by ::getAllowedParams() @@ -1811,7 +1813,7 @@ abstract class ApiBase extends ContextSource { if ( is_string( $error[0] ) && isset( self::$blockMsgMap[$error[0]] ) && $user->getBlock() ) { list( $msg, $code ) = self::$blockMsgMap[$error[0]]; $status->fatal( ApiMessage::create( $msg, $code, - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] + [ 'blockinfo' => $this->getBlockInfo( $user->getBlock() ) ] ) ); } else { $status->fatal( ...$error ); @@ -1834,7 +1836,7 @@ abstract class ApiBase extends ContextSource { foreach ( self::$blockMsgMap as $msg => list( $apiMsg, $code ) ) { if ( $status->hasMessage( $msg ) && $user->getBlock() ) { $status->replaceMessage( $msg, ApiMessage::create( $apiMsg, $code, - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] + [ 'blockinfo' => $this->getBlockInfo( $user->getBlock() ) ] ) ); } } @@ -2033,19 +2035,19 @@ abstract class ApiBase extends ContextSource { $this->dieWithError( 'apierror-autoblocked', 'autoblocked', - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ] + [ 'blockinfo' => $this->getBlockInfo( $block ) ] ); } elseif ( !$block->isSitewide() ) { $this->dieWithError( 'apierror-blocked-partial', 'blocked', - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ] + [ 'blockinfo' => $this->getBlockInfo( $block ) ] ); } else { $this->dieWithError( 'apierror-blocked', 'blocked', - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ] + [ 'blockinfo' => $this->getBlockInfo( $block ) ] ); } } diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index b5d51aae8c..336943d054 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -28,6 +28,8 @@ */ class ApiBlock extends ApiBase { + use ApiBlockInfoTrait; + /** * Blocks the user specified in the parameters for the given expiry, with the * given reason, and with all other settings provided in the params. If the block @@ -50,7 +52,7 @@ class ApiBlock extends ApiBase { $this->dieWithError( $status, null, - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ] + [ 'blockinfo' => $this->getBlockInfo( $block ) ] ); } } diff --git a/includes/api/ApiBlockInfoTrait.php b/includes/api/ApiBlockInfoTrait.php new file mode 100644 index 0000000000..26634854de --- /dev/null +++ b/includes/api/ApiBlockInfoTrait.php @@ -0,0 +1,53 @@ +getId(); + $vals['blockedby'] = $block->getByName(); + $vals['blockedbyid'] = $block->getBy(); + $vals['blockreason'] = $block->getReason(); + $vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->getTimestamp() ); + $vals['blockexpiry'] = ApiResult::formatExpiry( $block->getExpiry(), 'infinite' ); + $vals['blockpartial'] = !$block->isSitewide(); + if ( $block->getSystemBlockType() !== null ) { + $vals['systemblocktype'] = $block->getSystemBlockType(); + } + return $vals; + } + +} diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index 00d7d84de8..c495c6db13 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -29,6 +29,8 @@ use MediaWiki\MediaWikiServices; */ class ApiQueryUserInfo extends ApiQueryBase { + use ApiBlockInfoTrait; + const WL_UNREAD_LIMIT = 1000; private $params = []; @@ -50,33 +52,6 @@ class ApiQueryUserInfo extends ApiQueryBase { $result->addValue( 'query', $this->getModuleName(), $r ); } - /** - * Get basic info about a given block - * @param Block $block - * @return array Array containing several keys: - * - blockid - ID of the block - * - blockedby - username of the blocker - * - blockedbyid - user ID of the blocker - * - blockreason - reason provided for the block - * - blockedtimestamp - timestamp for when the block was placed/modified - * - blockexpiry - expiry time of the block - * - systemblocktype - system block type, if any - */ - public static function getBlockInfo( Block $block ) { - $vals = []; - $vals['blockid'] = $block->getId(); - $vals['blockedby'] = $block->getByName(); - $vals['blockedbyid'] = $block->getBy(); - $vals['blockreason'] = $block->getReason(); - $vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->getTimestamp() ); - $vals['blockexpiry'] = ApiResult::formatExpiry( $block->getExpiry(), 'infinite' ); - $vals['blockpartial'] = !$block->isSitewide(); - if ( $block->getSystemBlockType() !== null ) { - $vals['systemblocktype'] = $block->getSystemBlockType(); - } - return $vals; - } - /** * Get central user info * @param Config $config @@ -129,7 +104,7 @@ class ApiQueryUserInfo extends ApiQueryBase { if ( isset( $this->prop['blockinfo'] ) ) { $block = $user->getBlock(); if ( $block ) { - $vals = array_merge( $vals, self::getBlockInfo( $block ) ); + $vals = array_merge( $vals, $this->getBlockInfo( $block ) ); } } diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index ba4c6e8321..d2bbe7bb88 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -77,8 +77,9 @@ class ApiSetNotificationTimestamp extends ApiBase { $titles = $pageSet->getGoodTitles(); $title = reset( $titles ); if ( $title ) { + // XXX $title isn't actually used, can we just get rid of the previous six lines? $timestamp = MediaWikiServices::getInstance()->getRevisionStore() - ->getTimestampFromId( $title, $params['torevid'], IDBAccessObject::READ_LATEST ); + ->getTimestampFromId( $params['torevid'], IDBAccessObject::READ_LATEST ); if ( $timestamp ) { $timestamp = $dbw->timestamp( $timestamp ); } else { diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index 3aad8f4199..f038b9683b 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -28,6 +28,8 @@ */ class ApiUnblock extends ApiBase { + use ApiBlockInfoTrait; + /** * Unblocks the specified user or provides the reason the unblock failed. */ @@ -48,7 +50,7 @@ class ApiUnblock extends ApiBase { $this->dieWithError( $status, null, - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ] + [ 'blockinfo' => $this->getBlockInfo( $block ) ] ); } } diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index 9497d8df96..ea76a45533 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -128,7 +128,7 @@ "apihelp-edit-param-text": "문서 내용.", "apihelp-edit-param-summary": "편집 요약. 또한 $1section=new 및 $1sectiontitle이 설정되어 있지 않을 때 문단 제목.", "apihelp-edit-param-tags": "이 판에 적용할 태그를 변경합니다.", - "apihelp-edit-param-minor": "사소한 편집.", + "apihelp-edit-param-minor": "이 편집을 사소한 편집으로 표시합니다.", "apihelp-edit-param-notminor": "사소하지 않은 편집.", "apihelp-edit-param-bot": "이 편집을 봇 편집으로 표시.", "apihelp-edit-param-basetimestamp": "기본 판의 타임스탬프이며, 편집 충돌을 발견하기 위해 사용됩니다. [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]를 통해 가져올 수 있습니다.", diff --git a/includes/api/i18n/lb.json b/includes/api/i18n/lb.json index 615f71e43d..cfca2ee512 100644 --- a/includes/api/i18n/lb.json +++ b/includes/api/i18n/lb.json @@ -28,8 +28,8 @@ "apihelp-edit-summary": "Säiten uleeën an änneren.", "apihelp-edit-param-sectiontitle": "Den Titel fir en neien Abschnitt.", "apihelp-edit-param-text": "Säiteninhalt.", - "apihelp-edit-param-minor": "Kleng Ännerung.", - "apihelp-edit-param-notminor": "Keng kleng Ännerung", + "apihelp-edit-param-minor": "Dës Ännerung als kleng Ännerung markéieren.", + "apihelp-edit-param-notminor": "Dës Ännerung net als keng kleng Ännerung markéieren esouguer wann d'Benotzerastellung \"{{int:tog-minordefault}}\" agestallt ass.", "apihelp-edit-param-bot": "Dës Ännerung als eng Bot-Ännerung markéieren.", "apihelp-edit-param-createonly": "D'Säit net ännere wann et se scho gëtt.", "apihelp-edit-param-watch": "D'Säit op dem aktuelle Benotzer seng Iwwerwaachungslëscht dobäisetzen.", diff --git a/includes/api/i18n/nl.json b/includes/api/i18n/nl.json index 84eef7263b..15bc80267b 100644 --- a/includes/api/i18n/nl.json +++ b/includes/api/i18n/nl.json @@ -17,7 +17,8 @@ "Hex", "Mainframe98", "Southparkfan", - "Elroy" + "Elroy", + "Rots61" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Documentatie]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-maillijst]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-aankondigingen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & verzoeken]\n
\nStatus: De MediaWiki API is een stabiele interface die actief ondersteund en verbeterd wordt. Hoewel we het proberen te voorkomen, is het mogelijk dat er soms wijzigingen worden aangebracht die bepaalde API-verzoek kunnen verhinderen; abonneer u op de [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-maillijst mediawiki-api-announce] voor meldingen over wijzigingen.\n\nFoutieve verzoeken: als de API foutieve verzoeken ontvangt, wordt er geantwoord met een HTTP-header met de sleutel \"MediaWiki-API-Error\" en daarna worden de waarde van de header en de foutcode op dezelfde waarde ingesteld. Zie [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Foutmeldingen en waarschuwingen]] voor meer informatie.\n\n

Testen: u kunt [[Special:ApiSandbox|eenvoudig API-verzoeken testen]].

", @@ -87,7 +88,7 @@ "apihelp-edit-param-sectiontitle": "De naam van een nieuwe sectie.", "apihelp-edit-param-text": "Pagina-inhoud.", "apihelp-edit-param-tags": "De labels voor de revisie wijzigen.", - "apihelp-edit-param-minor": "Kleine bewerking.", + "apihelp-edit-param-minor": "Mankeer deze bewerking als een kleine bewerking.", "apihelp-edit-param-notminor": "Niet-kleine bewerking.", "apihelp-edit-param-bot": "Deze bewerking markeren als een botbewerking.", "apihelp-edit-param-createonly": "De pagina niet bewerken als die al bestaat.", diff --git a/includes/api/i18n/pl.json b/includes/api/i18n/pl.json index 2d4fc693d6..d36e4ea3c1 100644 --- a/includes/api/i18n/pl.json +++ b/includes/api/i18n/pl.json @@ -16,7 +16,8 @@ "Woytecr", "InternerowyGołąb", "CiaPan", - "Vlad5250" + "Vlad5250", + "Railfail536" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentacja]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista dyskusyjna]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Ogłoszenia dotyczące API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Błędy i propozycje]\n
\nStan: Wszystkie funkcje opisane na tej stronie powinny działać, ale API nadal jest aktywnie rozwijane i mogą się zmienić w dowolnym czasie. Subskrybuj [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ listę dyskusyjną mediawiki-api-announce], aby móc na bieżąco dowiadywać się o aktualizacjach.\n\nBłędne żądania: Gdy zostanie wysłane błędne żądanie do API, zostanie wysłany w odpowiedzi nagłówek HTTP z kluczem \"MediaWiki-API-Error\" i zarówno jego wartość jak i wartość kodu błędu wysłanego w odpowiedzi będą miały taką samą wartość. Aby uzyskać więcej informacji, zobacz [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Błędy i ostrzeżenia]].\n\nTestowanie: Aby łatwo testować żądania API, zobacz [[Special:ApiSandbox]].", @@ -81,7 +82,7 @@ "apihelp-edit-param-text": "Zawartość strony.", "apihelp-edit-param-summary": "Opis edycji. Także tytuł sekcji gdy użyto $1section=new, a nie ustawiono $1sectiontitle.", "apihelp-edit-param-tags": "Znaczniki zmian do zastosowania w tej edycji.", - "apihelp-edit-param-minor": "Drobna zmiana.", + "apihelp-edit-param-minor": "Oznacz tą zmianę jako drobną zmianę.", "apihelp-edit-param-notminor": "Nie oznaczaj tej zmiany jako drobną.", "apihelp-edit-param-bot": "Oznacz tę edycję jako edycję bota.", "apihelp-edit-param-basetimestamp": "Czas wersji, która jest edytowana. Służy do wykrywania konfliktów edycji. Można pobrać poprzez [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].", diff --git a/includes/api/i18n/zh-hant.json b/includes/api/i18n/zh-hant.json index 1026e2d2c0..e565b71855 100644 --- a/includes/api/i18n/zh-hant.json +++ b/includes/api/i18n/zh-hant.json @@ -135,8 +135,8 @@ "apihelp-edit-param-text": "頁面內容。", "apihelp-edit-param-summary": "編輯摘要。 當未設定 $1section=new 與 $1sectiontitle 時也會當做章節標題。", "apihelp-edit-param-tags": "更改套用到修訂的標籤。", - "apihelp-edit-param-minor": "小編輯。", - "apihelp-edit-param-notminor": "非小編輯。", + "apihelp-edit-param-minor": "標記此編輯為小編輯。", + "apihelp-edit-param-notminor": "不要標記此編輯為小編輯,即使有設定到「{{int:tog-minordefault}}」使用者偏好設定。", "apihelp-edit-param-bot": "標記此編輯為機器人編輯。", "apihelp-edit-param-basetimestamp": "基於修訂的時間戳記,用來檢測編輯衝突。也许可以取得[[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]認可。", "apihelp-edit-param-starttimestamp": "當編輯程序開始的時間戳記,用於偵測編輯衝突。當編輯程序開始時(例如:當載入要編輯的頁面內容),使用 [[Special:ApiHelp/main|curtimestamp]] 可以取得一個適當值。", diff --git a/includes/cache/CacheHelper.php b/includes/cache/CacheHelper.php index ec6ce04000..d798ddbcb3 100644 --- a/includes/cache/CacheHelper.php +++ b/includes/cache/CacheHelper.php @@ -288,7 +288,9 @@ class CacheHelper implements ICacheHelper { throw new MWException( 'No cache key set, so cannot obtain or save the CacheHelper values.' ); } - return wfMemcKey( ...array_values( $this->cacheKey ) ); + return ObjectCache::getLocalClusterInstance()->makeKey( + ...array_values( $this->cacheKey ) + ); } /** diff --git a/includes/cache/GenderCache.php b/includes/cache/GenderCache.php index 7228814d47..eedc3c6f04 100644 --- a/includes/cache/GenderCache.php +++ b/includes/cache/GenderCache.php @@ -34,6 +34,13 @@ class GenderCache { protected $misses = 0; protected $missLimit = 1000; + /** @var NamespaceInfo */ + private $nsInfo; + + public function __construct( NamespaceInfo $nsInfo = null ) { + $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo(); + } + /** * @deprecated in 1.28 see MediaWikiServices::getInstance()->getGenderCache() * @return GenderCache @@ -97,7 +104,7 @@ class GenderCache { public function doLinkBatch( $data, $caller = '' ) { $users = []; foreach ( $data as $ns => $pagenames ) { - if ( !MWNamespace::hasGenderDistinction( $ns ) ) { + if ( !$this->nsInfo->hasGenderDistinction( $ns ) ) { continue; } foreach ( array_keys( $pagenames ) as $username ) { @@ -122,7 +129,7 @@ class GenderCache { if ( !$titleObj ) { continue; } - if ( !MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) { + if ( !$this->nsInfo->hasGenderDistinction( $titleObj->getNamespace() ) ) { continue; } $users[] = $titleObj->getText(); diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index c13f95e1ed..1bcf948d2d 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -45,17 +45,29 @@ class LinkCache { /** @var TitleFormatter */ private $titleFormatter; + /** @var NamespaceInfo */ + private $nsInfo; + /** * How many Titles to store. There are two caches, so the amount actually * stored in memory can be up to twice this. */ const MAX_SIZE = 10000; - public function __construct( TitleFormatter $titleFormatter, WANObjectCache $cache ) { + public function __construct( + TitleFormatter $titleFormatter, + WANObjectCache $cache, + NamespaceInfo $nsInfo = null + ) { + if ( !$nsInfo ) { + wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' ); + $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); + } $this->goodLinks = new MapCacheLRU( self::MAX_SIZE ); $this->badLinks = new MapCacheLRU( self::MAX_SIZE ); $this->wanCache = $cache; $this->titleFormatter = $titleFormatter; + $this->nsInfo = $nsInfo; } /** @@ -231,9 +243,7 @@ class LinkCache { */ public function addLinkObj( LinkTarget $nt ) { $key = $this->titleFormatter->getPrefixedDBkey( $nt ); - if ( $this->isBadLink( $key ) || $nt->isExternal() - || $nt->inNamespace( NS_SPECIAL ) - ) { + if ( $this->isBadLink( $key ) || $nt->isExternal() || $nt->getNamespace() < 0 ) { return 0; } $id = $this->getGoodLinkID( $key ); @@ -300,11 +310,11 @@ class LinkCache { return true; } // Focus on transcluded pages more than the main content - if ( MWNamespace::isContent( $ns ) ) { + if ( $this->nsInfo->isContent( $ns ) ) { return false; } // Non-talk extension namespaces (e.g. NS_MODULE) - return ( $ns >= 100 && MWNamespace::isSubject( $ns ) ); + return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) ); } private function fetchPageRow( IDatabase $db, LinkTarget $nt ) { diff --git a/includes/externalstore/ExternalStoreHttp.php b/includes/externalstore/ExternalStoreHttp.php index 879686f724..a723557786 100644 --- a/includes/externalstore/ExternalStoreHttp.php +++ b/includes/externalstore/ExternalStoreHttp.php @@ -20,6 +20,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * Example class for HTTP accessible external objects. * Only supports reading, not storing. @@ -28,7 +30,8 @@ */ class ExternalStoreHttp extends ExternalStoreMedium { public function fetchFromURL( $url ) { - return Http::get( $url, [], __METHOD__ ); + return MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( $url, [], __METHOD__ ); } public function store( $location, $data ) { diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index 346ec8ec15..2c6f29632f 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -502,8 +502,9 @@ class ForeignAPIRepo extends FileRepo { } /** - * Like a Http:get request, but with custom User-Agent. - * @see Http::get + * Like a HttpRequestFactory::get request, but with custom User-Agent. + * @see HttpRequestFactory::get + * @todo Can this use HttpRequestFactory::get() but just pass the 'userAgent' option? * @param string $url * @param string $timeout * @param array $options diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index b6c70ab65e..8047835212 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -35,6 +35,9 @@ class RepoGroup { /** @var FileRepo[] */ protected $foreignRepos; + /** @var WANObjectCache */ + protected $wanCache; + /** @var bool */ protected $reposInitialised = false; @@ -47,66 +50,60 @@ class RepoGroup { /** @var ProcessCacheLRU */ protected $cache; - /** @var RepoGroup */ - protected static $instance; - /** Maximum number of cache items */ const MAX_CACHE_SIZE = 500; /** - * Get a RepoGroup instance. At present only one instance of RepoGroup is - * needed in a MediaWiki invocation, this may change in the future. + * @deprecated since 1.34, use MediaWikiServices::getRepoGroup * @return RepoGroup */ static function singleton() { - if ( self::$instance ) { - return self::$instance; - } - global $wgLocalFileRepo, $wgForeignFileRepos; - /** @var array $wgLocalFileRepo */ - self::$instance = new RepoGroup( $wgLocalFileRepo, $wgForeignFileRepos ); - - return self::$instance; + return MediaWikiServices::getInstance()->getRepoGroup(); } /** - * Destroy the singleton instance, so that a new one will be created next - * time singleton() is called. + * @deprecated since 1.34, use MediaWikiTestCase::overrideMwServices() or similar. This will + * cause bugs if you don't reset all other services that depend on this one at the same time. */ static function destroySingleton() { - self::$instance = null; + MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' ); } /** - * Set the singleton instance to a given object - * Used by extensions which hook into the Repo chain. - * It's not enough to just create a superclass ... you have - * to get people to call into it even though all they know is RepoGroup::singleton() - * + * @deprecated since 1.34, use MediaWikiTestCase::setService, this can mess up state of other + * tests * @param RepoGroup $instance */ static function setSingleton( $instance ) { - self::$instance = $instance; + $services = MediaWikiServices::getInstance(); + $services->disableService( 'RepoGroup' ); + $services->redefineService( 'RepoGroup', + function () use ( $instance ) { + return $instance; + } + ); } /** - * Construct a group of file repositories. + * Construct a group of file repositories. Do not call this -- use + * MediaWikiServices::getRepoGroup. * * @param array $localInfo Associative array for local repo's info * @param array $foreignInfo Array of repository info arrays. * Each info array is an associative array with the 'class' member * giving the class name. The entire array is passed to the repository * constructor as the first parameter. + * @param WANObjectCache $wanCache */ - function __construct( $localInfo, $foreignInfo ) { + function __construct( $localInfo, $foreignInfo, $wanCache ) { $this->localInfo = $localInfo; $this->foreignInfo = $foreignInfo; $this->cache = new MapCacheLRU( self::MAX_CACHE_SIZE ); + $this->wanCache = $wanCache; } /** * Search repositories for an image. - * You can also use wfFindFile() to do this. * * @param Title|string $title Title object or string * @param array $options Associative array of options: @@ -419,8 +416,7 @@ class RepoGroup { protected function newRepo( $info ) { $class = $info['class']; - $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); - $info['wanCache'] = $cache; + $info['wanCache'] = $this->wanCache; return new $class( $info ); } diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index 7d4f4dfe85..92be7d4ec9 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -2070,7 +2070,8 @@ abstract class File implements IDBAccessObject { $this->repo->descriptionCacheExpiry ?: $cache::TTL_UNCACHEABLE, function ( $oldValue, &$ttl, array &$setOpts ) use ( $renderUrl, $fname ) { wfDebug( "Fetching shared description from $renderUrl\n" ); - $res = Http::get( $renderUrl, [], $fname ); + $res = MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( $renderUrl, [], $fname ); if ( !$res ) { $ttl = WANObjectCache::TTL_UNCACHEABLE; } diff --git a/includes/filerepo/file/ForeignDBFile.php b/includes/filerepo/file/ForeignDBFile.php index 3438a6388b..e083a4e4e0 100644 --- a/includes/filerepo/file/ForeignDBFile.php +++ b/includes/filerepo/file/ForeignDBFile.php @@ -165,7 +165,8 @@ class ForeignDBFile extends LocalFile { $this->repo->descriptionCacheExpiry ?: $cache::TTL_UNCACHEABLE, function ( $oldValue, &$ttl, array &$setOpts ) use ( $renderUrl, $fname ) { wfDebug( "Fetching shared description from $renderUrl\n" ); - $res = Http::get( $renderUrl, [], $fname ); + $res = MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( $renderUrl, [], $fname ); if ( !$res ) { $ttl = WANObjectCache::TTL_UNCACHEABLE; } diff --git a/includes/http/CurlHttpRequest.php b/includes/http/CurlHttpRequest.php index 8ef9cc226b..5130e36fb5 100644 --- a/includes/http/CurlHttpRequest.php +++ b/includes/http/CurlHttpRequest.php @@ -27,6 +27,18 @@ class CurlHttpRequest extends MWHttpRequest { protected $curlOptions = []; protected $headerText = ""; + /** + * @throws RuntimeException + */ + public function __construct() { + if ( !function_exists( 'curl_init' ) ) { + throw new RuntimeException( + __METHOD__ . ': curl (https://www.php.net/curl) is not installed' ); + } + + parent::__construct( ...func_get_args() ); + } + /** * @param resource $fh * @param string $content diff --git a/includes/http/GuzzleHttpRequest.php b/includes/http/GuzzleHttpRequest.php index e6b289206a..3af7f56a5d 100644 --- a/includes/http/GuzzleHttpRequest.php +++ b/includes/http/GuzzleHttpRequest.php @@ -45,7 +45,7 @@ class GuzzleHttpRequest extends MWHttpRequest { /** * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL - * @param array $options (optional) extra params to pass (see Http::request()) + * @param array $options (optional) extra params to pass (see HttpRequestFactory::create()) * @param string $caller The method making this request, for profiling * @param Profiler|null $profiler An instance of the profiler for profiling, or null * @throws Exception diff --git a/includes/http/Http.php b/includes/http/Http.php index f0972dcf24..9596169b45 100644 --- a/includes/http/Http.php +++ b/includes/http/Http.php @@ -19,74 +19,40 @@ */ use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; /** * Various HTTP related functions + * @deprecated since 1.34 * @ingroup HTTP */ class Http { - public static $httpEngine = false; + /** @deprecated since 1.34, just use the default engine */ + public static $httpEngine = null; /** * Perform an HTTP request * + * @deprecated since 1.34, use HttpRequestFactory::request() + * * @param string $method HTTP method. Usually GET/POST * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// URL - * @param array $options Options to pass to MWHttpRequest object. - * Possible keys for the array: - * - timeout Timeout length in seconds - * - connectTimeout Timeout for connection, in seconds (curl only) - * - postData An array of key-value pairs or a url-encoded form data - * - proxy The proxy to use. - * Otherwise it will use $wgHTTPProxy (if set) - * Otherwise it will use the environment variable "http_proxy" (if set) - * - noProxy Don't use any proxy at all. Takes precedence over proxy value(s). - * - sslVerifyHost Verify hostname against certificate - * - sslVerifyCert Verify SSL certificate - * - caInfo Provide CA information - * - maxRedirects Maximum number of redirects to follow (defaults to 5) - * - followRedirects Whether to follow redirects (defaults to false). - * Note: this should only be used when the target URL is trusted, - * to avoid attacks on intranet services accessible by HTTP. - * - userAgent A user agent, if you want to override the default - * MediaWiki/$wgVersion - * - logger A \Psr\Logger\LoggerInterface instance for debug logging - * - username Username for HTTP Basic Authentication - * - password Password for HTTP Basic Authentication - * - originalRequest Information about the original request (as a WebRequest object or - * an associative array with 'ip' and 'userAgent'). + * @param array $options Options to pass to MWHttpRequest object. See HttpRequestFactory::create + * docs * @param string $caller The method making this request, for profiling * @return string|bool (bool)false on failure or a string on success */ public static function request( $method, $url, array $options = [], $caller = __METHOD__ ) { - $logger = LoggerFactory::getInstance( 'http' ); - $logger->debug( "$method: $url" ); - - $options['method'] = strtoupper( $method ); - - if ( !isset( $options['timeout'] ) ) { - $options['timeout'] = 'default'; - } - if ( !isset( $options['connectTimeout'] ) ) { - $options['connectTimeout'] = 'default'; - } - - $req = MWHttpRequest::factory( $url, $options, $caller ); - $status = $req->execute(); - - if ( $status->isOK() ) { - return $req->getContent(); - } else { - $errors = $status->getErrorsByType( 'error' ); - $logger->warning( Status::wrap( $status )->getWikiText( false, false, 'en' ), - [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] ); - return false; - } + $ret = MediaWikiServices::getInstance()->getHttpRequestFactory()->request( + $method, $url, $options, $caller ); + return is_string( $ret ) ? $ret : false; } /** * Simple wrapper for Http::request( 'GET' ) - * @see Http::request() + * + * @deprecated since 1.34, use HttpRequestFactory::get() + * * @since 1.25 Second parameter $timeout removed. Second parameter * is now $options which can be given a 'timeout' * @@ -111,7 +77,8 @@ class Http { /** * Simple wrapper for Http::request( 'POST' ) - * @see Http::request() + * + * @deprecated since 1.34, use HttpRequestFactory::post() * * @param string $url * @param array $options @@ -124,11 +91,12 @@ class Http { /** * A standard user-agent we can use for external requests. + * + * @deprecated since 1.34, use HttpRequestFactory::getUserAgent() * @return string */ public static function userAgent() { - global $wgVersion; - return "MediaWiki/$wgVersion"; + return MediaWikiServices::getInstance()->getHttpRequestFactory()->getUserAgent(); } /** @@ -143,37 +111,37 @@ class Http { * * @todo FIXME this is wildly inaccurate and fails to actually check most stuff * + * @deprecated since 1.34, use MWHttpRequest::isValidURI * @param string $uri URI to check for validity * @return bool */ public static function isValidURI( $uri ) { - return (bool)preg_match( - '/^https?:\/\/[^\/\s]\S*$/D', - $uri - ); + return MWHttpRequest::isValidURI( $uri ); } /** * Gets the relevant proxy from $wgHTTPProxy * - * @return mixed The proxy address or an empty string if not set. + * @deprecated since 1.34, use $wgHTTPProxy directly + * @return string The proxy address or an empty string if not set. */ public static function getProxy() { - global $wgHTTPProxy; + wfDeprecated( __METHOD__, '1.34' ); - if ( $wgHTTPProxy ) { - return $wgHTTPProxy; - } - - return ""; + global $wgHTTPProxy; + return (string)$wgHTTPProxy; } /** * Get a configured MultiHttpClient + * + * @deprecated since 1.34, construct it directly * @param array $options * @return MultiHttpClient */ public static function createMultiClient( array $options = [] ) { + wfDeprecated( __METHOD__, '1.34' ); + global $wgHTTPConnectTimeout, $wgHTTPTimeout, $wgHTTPProxy; return new MultiHttpClient( $options + [ diff --git a/includes/http/HttpRequestFactory.php b/includes/http/HttpRequestFactory.php index f15534816b..08520b765a 100644 --- a/includes/http/HttpRequestFactory.php +++ b/includes/http/HttpRequestFactory.php @@ -20,34 +20,52 @@ namespace MediaWiki\Http; use CurlHttpRequest; -use DomainException; +use GuzzleHttpRequest; use Http; use MediaWiki\Logger\LoggerFactory; use MWHttpRequest; use PhpHttpRequest; use Profiler; -use GuzzleHttpRequest; +use RuntimeException; +use Status; /** * Factory creating MWHttpRequest objects. */ class HttpRequestFactory { - /** * Generate a new MWHttpRequest object * @param string $url Url to use - * @param array $options (optional) extra params to pass (see Http::request()) + * @param array $options Possible keys for the array: + * - timeout Timeout length in seconds + * - connectTimeout Timeout for connection, in seconds (curl only) + * - postData An array of key-value pairs or a url-encoded form data + * - proxy The proxy to use. + * Otherwise it will use $wgHTTPProxy (if set) + * Otherwise it will use the environment variable "http_proxy" (if set) + * - noProxy Don't use any proxy at all. Takes precedence over proxy value(s). + * - sslVerifyHost Verify hostname against certificate + * - sslVerifyCert Verify SSL certificate + * - caInfo Provide CA information + * - maxRedirects Maximum number of redirects to follow (defaults to 5) + * - followRedirects Whether to follow redirects (defaults to false). + * Note: this should only be used when the target URL is trusted, + * to avoid attacks on intranet services accessible by HTTP. + * - userAgent A user agent, if you want to override the default + * MediaWiki/$wgVersion + * - logger A \Psr\Logger\LoggerInterface instance for debug logging + * - username Username for HTTP Basic Authentication + * - password Password for HTTP Basic Authentication + * - originalRequest Information about the original request (as a WebRequest object or + * an associative array with 'ip' and 'userAgent'). * @param string $caller The method making this request, for profiling - * @throws DomainException + * @throws RuntimeException * @return MWHttpRequest * @see MWHttpRequest::__construct */ public function create( $url, array $options = [], $caller = __METHOD__ ) { if ( !Http::$httpEngine ) { Http::$httpEngine = 'guzzle'; - } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { - throw new DomainException( __METHOD__ . ': curl (https://www.php.net/curl) is not ' . - 'installed, but Http::$httpEngine is set to "curl"' ); } if ( !isset( $options['logger'] ) ) { @@ -60,16 +78,9 @@ class HttpRequestFactory { case 'curl': return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() ); case 'php': - if ( !wfIniGetBool( 'allow_url_fopen' ) ) { - throw new DomainException( __METHOD__ . ': allow_url_fopen ' . - 'needs to be enabled for pure PHP http requests to ' . - 'work. If possible, curl should be used instead. See ' . - 'https://www.php.net/curl.' - ); - } return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() ); default: - throw new DomainException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); + throw new RuntimeException( __METHOD__ . ': The requested engine is not valid.' ); } } @@ -82,4 +93,75 @@ class HttpRequestFactory { return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' ); } + /** + * Perform an HTTP request + * + * @since 1.34 + * @param string $method HTTP method. Usually GET/POST + * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// + * URL + * @param array $options See HttpRequestFactory::create + * @param string $caller The method making this request, for profiling + * @return string|null null on failure or a string on success + */ + public function request( $method, $url, array $options = [], $caller = __METHOD__ ) { + $logger = LoggerFactory::getInstance( 'http' ); + $logger->debug( "$method: $url" ); + + $options['method'] = strtoupper( $method ); + + if ( !isset( $options['timeout'] ) ) { + $options['timeout'] = 'default'; + } + if ( !isset( $options['connectTimeout'] ) ) { + $options['connectTimeout'] = 'default'; + } + + $req = $this->create( $url, $options, $caller ); + $status = $req->execute(); + + if ( $status->isOK() ) { + return $req->getContent(); + } else { + $errors = $status->getErrorsByType( 'error' ); + $logger->warning( Status::wrap( $status )->getWikiText( false, false, 'en' ), + [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] ); + return null; + } + } + + /** + * Simple wrapper for request( 'GET' ), parameters have same meaning as for request() + * + * @since 1.34 + * @param string $url + * @param array $options + * @param string $caller + * @return string|null + */ + public function get( $url, array $options = [], $caller = __METHOD__ ) { + $this->request( 'GET', $url, $options, $caller ); + } + + /** + * Simple wrapper for request( 'POST' ), parameters have same meaning as for request() + * + * @since 1.34 + * @param string $url + * @param array $options + * @param string $caller + * @return string|null + */ + public function post( $url, array $options = [], $caller = __METHOD__ ) { + $this->request( 'POST', $url, $options, $caller ); + } + + /** + * @return string + */ + public function getUserAgent() { + global $wgVersion; + + return "MediaWiki/$wgVersion"; + } } diff --git a/includes/http/MWHttpRequest.php b/includes/http/MWHttpRequest.php index b4ac9a750e..41ea1dce35 100644 --- a/includes/http/MWHttpRequest.php +++ b/includes/http/MWHttpRequest.php @@ -85,7 +85,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface { /** * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL - * @param array $options (optional) extra params to pass (see Http::request()) + * @param array $options (optional) extra params to pass (see HttpRequestFactory::create()) * @param string $caller The method making this request, for profiling * @param Profiler|null $profiler An instance of the profiler for profiling, or null * @throws Exception @@ -172,9 +172,9 @@ abstract class MWHttpRequest implements LoggerAwareInterface { /** * Generate a new request object - * Deprecated: @see HttpRequestFactory::create + * @deprecated since 1.34, use HttpRequestFactory instead * @param string $url Url to use - * @param array|null $options (optional) extra params to pass (see Http::request()) + * @param array|null $options (optional) extra params to pass (see HttpRequestFactory::create()) * @param string $caller The method making this request, for profiling * @throws DomainException * @return MWHttpRequest @@ -224,7 +224,8 @@ abstract class MWHttpRequest implements LoggerAwareInterface { if ( self::isLocalURL( $this->url ) || $this->noProxy ) { $this->proxy = ''; } else { - $this->proxy = Http::getProxy(); + global $wgHTTPProxy; + $this->proxy = (string)$wgHTTPProxy; } } @@ -662,4 +663,27 @@ abstract class MWHttpRequest implements LoggerAwareInterface { $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip']; $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent']; } + + /** + * Check that the given URI is a valid one. + * + * This hardcodes a small set of protocols only, because we want to + * deterministically reject protocols not supported by all HTTP-transport + * methods. + * + * "file://" specifically must not be allowed, for security reasons + * (see ). + * + * @todo FIXME this is wildly inaccurate and fails to actually check most stuff + * + * @since 1.34 + * @param string $uri URI to check for validity + * @return bool + */ + public static function isValidURI( $uri ) { + return (bool)preg_match( + '/^https?:\/\/[^\/\s]\S*$/D', + $uri + ); + } } diff --git a/includes/http/PhpHttpRequest.php b/includes/http/PhpHttpRequest.php index d2af8c8568..c987c62b1c 100644 --- a/includes/http/PhpHttpRequest.php +++ b/includes/http/PhpHttpRequest.php @@ -22,6 +22,17 @@ class PhpHttpRequest extends MWHttpRequest { private $fopenErrors = []; + public function __construct() { + if ( !wfIniGetBool( 'allow_url_fopen' ) ) { + throw new RuntimeException( __METHOD__ . ': allow_url_fopen needs to be enabled for ' . + 'pure PHP http requests to work. If possible, curl should be used instead. See ' . + 'https://www.php.net/curl.' + ); + } + + parent::__construct( ...func_get_args() ); + } + /** * @param string $url * @return string diff --git a/includes/import/ImportStreamSource.php b/includes/import/ImportStreamSource.php index ebac200a4a..e6936cb2e3 100644 --- a/includes/import/ImportStreamSource.php +++ b/includes/import/ImportStreamSource.php @@ -112,7 +112,7 @@ class ImportStreamSource implements ImportSource { # quicker and sorts out user-agent problems which might # otherwise prevent importing from large sites, such # as the Wikimedia cluster, etc. - $data = Http::request( + $data = MediaWikiServices::getInstance()->getHttpRequestFactory()->request( $method, $url, [ diff --git a/includes/import/ImportableUploadRevisionImporter.php b/includes/import/ImportableUploadRevisionImporter.php index 4b378c1ec4..f1ac42c013 100644 --- a/includes/import/ImportableUploadRevisionImporter.php +++ b/includes/import/ImportableUploadRevisionImporter.php @@ -1,5 +1,6 @@ getSrc(); - $data = Http::get( $src, [], __METHOD__ ); + $data = MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( $src, [], __METHOD__ ); if ( !$data ) { $this->logger->debug( "IMPORT: couldn't fetch source $src\n" ); fclose( $f ); diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index 9053f8d195..c2312883c5 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -1203,9 +1203,11 @@ abstract class Installer { } try { - $text = Http::get( $url . $file, [ 'timeout' => 3 ], __METHOD__ ); + $text = MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( $url . $file, [ 'timeout' => 3 ], __METHOD__ ); } catch ( Exception $e ) { - // Http::get throws with allow_url_fopen = false and no curl extension. + // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl + // extension. $text = null; } unlink( $dir . $file ); diff --git a/includes/installer/i18n/ia.json b/includes/installer/i18n/ia.json index e6936f6977..8bf48d8afd 100644 --- a/includes/installer/i18n/ia.json +++ b/includes/installer/i18n/ia.json @@ -223,7 +223,7 @@ "config-license-help": "Multe wikis public pone tote le contributiones sub un [https://freedomdefined.org/Definition/Ia?uselang=ia licentia libere].\nIsto adjuta a crear un senso de proprietate communitari e incoragia le contribution in longe termino.\nIsto non es generalmente necessari pro un wiki private o de interprisa.\n\nSi tu vole poter usar texto de Wikipedia, e si tu vole que Wikipedia pote acceptar texto copiate de tu wiki, tu debe seliger {{int:config-license-cc-by-sa}}.\n\nWikipedia usava anteriormente le Licentia GNU pro Documentation Libere (GFDL).\nIste es un licentia valide, ma es difficile a comprender.\nIl es anque difficile reusar le contento licentiate sub GFDL.", "config-email-settings": "Configuration de e-mail", "config-enable-email": "Activar le e-mail sortiente", - "config-enable-email-help": "Si tu vole que e-mail functiona, [Config-dbsupport-oracle/manual/en/mail.configuration.php le optiones de e-mail de PHP] debe esser configurate correctemente.\nSi tu non vole functiones de e-mail, tu pote disactivar los hic.", + "config-enable-email-help": "Si tu vole que e-mail functiona, [https://www.php.net/manual/en/mail.configuration.php le optiones de e-mail de PHP] debe esser configurate correctemente.\nSi tu non vole functiones de e-mail, tu pote disactivar los hic.", "config-email-user": "Activar le e-mail de usator a usator", "config-email-user-help": "Permitter a tote le usatores de inviar e-mail inter se, si illes lo ha activate in lor preferentias.", "config-email-usertalk": "Activar notification de cambios in paginas de discussion de usatores", diff --git a/includes/jobqueue/jobs/ActivityUpdateJob.php b/includes/jobqueue/jobs/ActivityUpdateJob.php index 8cc14e51e8..9b085108be 100644 --- a/includes/jobqueue/jobs/ActivityUpdateJob.php +++ b/includes/jobqueue/jobs/ActivityUpdateJob.php @@ -19,6 +19,8 @@ * @ingroup JobQueue */ +use MediaWiki\Linker\LinkTarget; + /** * Job for updating user activity like "last viewed" timestamps * @@ -32,7 +34,9 @@ * @since 1.26 */ class ActivityUpdateJob extends Job { - function __construct( Title $title, array $params ) { + function __construct( LinkTarget $title, array $params ) { + $title = Title::newFromLinkTarget( $title ); + parent::__construct( 'activityUpdateJob', $title, $params ); static $required = [ 'type', 'userid', 'notifTime', 'curTime' ]; diff --git a/includes/jobqueue/jobs/ClearUserWatchlistJob.php b/includes/jobqueue/jobs/ClearUserWatchlistJob.php index 01fa46c002..0cb1a52d69 100644 --- a/includes/jobqueue/jobs/ClearUserWatchlistJob.php +++ b/includes/jobqueue/jobs/ClearUserWatchlistJob.php @@ -1,6 +1,7 @@ $user->getId(), 'maxWatchlistId' => $maxWatchlistId ] ); } diff --git a/includes/jobqueue/jobs/UserOptionsUpdateJob.php b/includes/jobqueue/jobs/UserOptionsUpdateJob.php new file mode 100644 index 0000000000..0e8b19f2da --- /dev/null +++ b/includes/jobqueue/jobs/UserOptionsUpdateJob.php @@ -0,0 +1,58 @@ + value) + * + * @since 1.33 + */ +class UserOptionsUpdateJob extends Job implements GenericParameterJob { + public function __construct( array $params ) { + parent::__construct( 'userOptionsUpdate', $params ); + $this->removeDuplicates = true; + } + + public function run() { + if ( !$this->params['options'] ) { + return true; // nothing to do + } + + $user = User::newFromId( $this->params['userId'] ); + $user->load( $user::READ_EXCLUSIVE ); + if ( !$user->getId() ) { + return true; + } + + foreach ( $this->params['options'] as $name => $value ) { + $user->setOption( $name, $value ); + } + + $user->saveSettings(); + + return true; + } +} diff --git a/includes/poolcounter/PoolWorkArticleView.php b/includes/poolcounter/PoolWorkArticleView.php index 2c9fbc8775..0abe1a5dde 100644 --- a/includes/poolcounter/PoolWorkArticleView.php +++ b/includes/poolcounter/PoolWorkArticleView.php @@ -114,7 +114,9 @@ class PoolWorkArticleView extends PoolCounterWork { $this->revision = $revision; $this->audience = $audience; $this->cacheKey = $this->parserCache->getKey( $page, $parserOptions ); - $keyPrefix = $this->cacheKey ?: wfMemcKey( 'articleview', 'missingcachekey' ); + $keyPrefix = $this->cacheKey ?: ObjectCache::getLocalClusterInstance()->makeKey( + 'articleview', 'missingcachekey' + ); parent::__construct( 'ArticleView', $keyPrefix . ':revid:' . $revid ); } diff --git a/includes/preferences/DefaultPreferencesFactory.php b/includes/preferences/DefaultPreferencesFactory.php index a5c8064a2d..1f21c1bbbc 100644 --- a/includes/preferences/DefaultPreferencesFactory.php +++ b/includes/preferences/DefaultPreferencesFactory.php @@ -39,8 +39,8 @@ use MediaWiki\Linker\LinkRenderer; use MediaWiki\MediaWikiServices; use MessageLocalizer; use MWException; -use MWNamespace; use MWTimestamp; +use NamespaceInfo; use OutputPage; use Parser; use ParserOptions; @@ -74,6 +74,9 @@ class DefaultPreferencesFactory implements PreferencesFactory { /** @var LinkRenderer */ protected $linkRenderer; + /** @var NamespaceInfo */ + protected $nsInfo; + /** * TODO Make this a const when we drop HHVM support (T192166) * @@ -108,16 +111,20 @@ class DefaultPreferencesFactory implements PreferencesFactory { ]; /** + * Do not call this directly. Get it from MediaWikiServices. + * * @param array|Config $options Config accepted for backwards compatibility * @param Language $contLang * @param AuthManager $authManager * @param LinkRenderer $linkRenderer + * @param NamespaceInfo|null $nsInfo */ public function __construct( $options, Language $contLang, AuthManager $authManager, - LinkRenderer $linkRenderer + LinkRenderer $linkRenderer, + NamespaceInfo $nsInfo = null ) { if ( $options instanceof Config ) { wfDeprecated( __METHOD__ . ' with Config parameter', '1.34' ); @@ -126,10 +133,15 @@ class DefaultPreferencesFactory implements PreferencesFactory { $options->assertRequiredOptions( self::$constructorOptions ); + if ( !$nsInfo ) { + wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' ); + $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); + } $this->options = $options; $this->contLang = $contLang; $this->authManager = $authManager; $this->linkRenderer = $linkRenderer; + $this->nsInfo = $nsInfo; $this->logger = new NullLogger(); } @@ -1262,7 +1274,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { * @param array &$defaultPreferences */ protected function searchPreferences( &$defaultPreferences ) { - foreach ( MWNamespace::getValidNamespaces() as $n ) { + foreach ( $this->nsInfo->getValidNamespaces() as $n ) { $defaultPreferences['searchNs' . $n] = [ 'type' => 'api', ]; diff --git a/includes/rcfeed/UDPRCFeedEngine.php b/includes/rcfeed/UDPRCFeedEngine.php index 7e69a02572..d142c48dce 100644 --- a/includes/rcfeed/UDPRCFeedEngine.php +++ b/includes/rcfeed/UDPRCFeedEngine.php @@ -24,7 +24,7 @@ */ class UDPRCFeedEngine extends RCFeedEngine { /** - * @see RCFeedEngine::send + * @see FormattedRCFeed::send * @param array $feed * @param string $line * @return bool diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 8b5562f9c7..39976c0d20 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -591,21 +591,12 @@ class MovePageForm extends UnlistedSpecialPage { # Do the actual move. $mp = new MovePage( $ot, $nt ); - $valid = $mp->isValidMove(); - if ( !$valid->isOK() ) { - $this->showForm( $valid->getErrorsArray() ); - return; - } - $permStatus = $mp->checkPermissions( $user, $this->reason ); - if ( !$permStatus->isOK() ) { - $this->showForm( $permStatus->getErrorsArray(), true ); - return; - } + $userPermitted = $mp->checkPermissions( $user, $this->reason )->isOK(); - $status = $mp->move( $user, $this->reason, $createRedirect ); + $status = $mp->moveIfAllowed( $user, $this->reason, $createRedirect ); if ( !$status->isOK() ) { - $this->showForm( $status->getErrorsArray() ); + $this->showForm( $status->getErrorsArray(), !$userPermitted ); return; } diff --git a/includes/title/NamespaceInfo.php b/includes/title/NamespaceInfo.php index f9cab24458..7cfadc0144 100644 --- a/includes/title/NamespaceInfo.php +++ b/includes/title/NamespaceInfo.php @@ -20,6 +20,9 @@ * @file */ +use MediaWiki\Config\ServiceOptions; +use MediaWiki\Linker\LinkTarget; + /** * This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of * them based on index. The textual names of the namespaces are handled by Language.php. @@ -44,14 +47,36 @@ class NamespaceInfo { /** @var int[]|null Valid namespaces cache */ private $validNamespaces = null; - /** @var Config */ - private $config; + /** @var ServiceOptions */ + private $options; + + /** + * TODO Make this const when HHVM support is dropped (T192166) + * + * @since 1.34 + * @var array + */ + public static $constructorOptions = [ + 'AllowImageMoving', + 'CanonicalNamespaceNames', + 'CapitalLinkOverrides', + 'CapitalLinks', + 'ContentNamespaces', + 'ExtraNamespaces', + 'ExtraSignatureNamespaces', + 'NamespaceContentModels', + 'NamespaceProtection', + 'NamespacesWithSubpages', + 'NonincludableNamespaces', + 'RestrictionLevels', + ]; /** - * @param Config $config + * @param ServiceOptions $options */ - public function __construct( Config $config ) { - $this->config = $config; + public function __construct( ServiceOptions $options ) { + $options->assertRequiredOptions( self::$constructorOptions ); + $this->options = $options; } /** @@ -80,8 +105,8 @@ class NamespaceInfo { * @return bool */ public function isMovable( $index ) { - $result = !( $index < NS_MAIN || - ( $index == NS_FILE && !$this->config->get( 'AllowImageMoving' ) ) ); + $result = $index >= NS_MAIN && + ( $index != NS_FILE || $this->options->get( 'AllowImageMoving' ) ); /** * @since 1.20 @@ -125,6 +150,18 @@ class NamespaceInfo { : $index + 1; } + /** + * @param LinkTarget $target + * @return LinkTarget Talk page for $target + * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL) + */ + public function getTalkPage( LinkTarget $target ) : LinkTarget { + if ( $this->isTalk( $target->getNamespace() ) ) { + return $target; + } + return new TitleValue( $this->getTalk( $target->getNamespace() ), $target->getDbKey() ); + } + /** * Get the subject namespace index for a given namespace * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject. @@ -143,24 +180,44 @@ class NamespaceInfo { : $index; } + /** + * @param LinkTarget $target + * @return LinkTarget Subject page for $target + */ + public function getSubjectPage( LinkTarget $target ) : LinkTarget { + if ( $this->isSubject( $target->getNamespace() ) ) { + return $target; + } + return new TitleValue( $this->getSubject( $target->getNamespace() ), $target->getDbKey() ); + } + /** * Get the associated namespace. * For talk namespaces, returns the subject (non-talk) namespace * For subject (non-talk) namespaces, returns the talk namespace * * @param int $index Namespace index - * @return int|null If no associated namespace could be found + * @return int + * @throws MWException if called on a namespace that has no talk pages (e.g., NS_SPECIAL) */ public function getAssociated( $index ) { $this->isMethodValidFor( $index, __METHOD__ ); if ( $this->isSubject( $index ) ) { return $this->getTalk( $index ); - } elseif ( $this->isTalk( $index ) ) { - return $this->getSubject( $index ); - } else { - return null; } + return $this->getSubject( $index ); + } + + /** + * @param LinkTarget $target + * @return LinkTarget Talk page for $target if it's a subject page, subject page if it's a talk + * page + * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL) + */ + public function getAssociatedPage( LinkTarget $target ) : LinkTarget { + return new TitleValue( + $this->getAssociated( $target->getNamespace() ), $target->getDbKey() ); } /** @@ -215,11 +272,11 @@ class NamespaceInfo { public function getCanonicalNamespaces() { if ( $this->canonicalNamespaces === null ) { $this->canonicalNamespaces = - [ NS_MAIN => '' ] + $this->config->get( 'CanonicalNamespaceNames' ); + [ NS_MAIN => '' ] + $this->options->get( 'CanonicalNamespaceNames' ); $this->canonicalNamespaces += ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' ); - if ( is_array( $this->config->get( 'ExtraNamespaces' ) ) ) { - $this->canonicalNamespaces += $this->config->get( 'ExtraNamespaces' ); + if ( is_array( $this->options->get( 'ExtraNamespaces' ) ) ) { + $this->canonicalNamespaces += $this->options->get( 'ExtraNamespaces' ); } Hooks::run( 'CanonicalNamespaces', [ &$this->canonicalNamespaces ] ); } @@ -242,7 +299,7 @@ class NamespaceInfo { * The input *must* be converted to lower case first * * @param string $name Namespace name - * @return int + * @return int|null */ public function getCanonicalIndex( $name ) { if ( $this->namespaceIndexes === false ) { @@ -259,8 +316,8 @@ class NamespaceInfo { } /** - * Returns an array of the namespaces (by integer id) that exist on the - * wiki. Used primarily by the api in help documentation. + * Returns an array of the namespaces (by integer id) that exist on the wiki. Used primarily by + * the API in help documentation. The array is sorted numerically and omits negative namespaces. * @return array */ public function getValidNamespaces() { @@ -297,7 +354,7 @@ class NamespaceInfo { * @return bool */ public function isContent( $index ) { - return $index == NS_MAIN || in_array( $index, $this->config->get( 'ContentNamespaces' ) ); + return $index == NS_MAIN || in_array( $index, $this->options->get( 'ContentNamespaces' ) ); } /** @@ -309,7 +366,7 @@ class NamespaceInfo { */ public function wantSignatures( $index ) { return $this->isTalk( $index ) || - in_array( $index, $this->config->get( 'ExtraSignatureNamespaces' ) ); + in_array( $index, $this->options->get( 'ExtraSignatureNamespaces' ) ); } /** @@ -329,7 +386,7 @@ class NamespaceInfo { * @return bool */ public function hasSubpages( $index ) { - return !empty( $this->config->get( 'NamespacesWithSubpages' )[$index] ); + return !empty( $this->options->get( 'NamespacesWithSubpages' )[$index] ); } /** @@ -337,7 +394,7 @@ class NamespaceInfo { * @return array Array of namespace indices */ public function getContentNamespaces() { - $contentNamespaces = $this->config->get( 'ContentNamespaces' ); + $contentNamespaces = $this->options->get( 'ContentNamespaces' ); if ( !is_array( $contentNamespaces ) || $contentNamespaces === [] ) { return [ NS_MAIN ]; } elseif ( !in_array( NS_MAIN, $contentNamespaces ) ) { @@ -391,13 +448,13 @@ class NamespaceInfo { if ( in_array( $index, $this->alwaysCapitalizedNamespaces ) ) { return true; } - $overrides = $this->config->get( 'CapitalLinkOverrides' ); + $overrides = $this->options->get( 'CapitalLinkOverrides' ); if ( isset( $overrides[$index] ) ) { // CapitalLinkOverrides is explicitly set return $overrides[$index]; } // Default to the global setting - return $this->config->get( 'CapitalLinks' ); + return $this->options->get( 'CapitalLinks' ); } /** @@ -418,7 +475,7 @@ class NamespaceInfo { * @return bool */ public function isNonincludable( $index ) { - $namespaces = $this->config->get( 'NonincludableNamespaces' ); + $namespaces = $this->options->get( 'NonincludableNamespaces' ); return $namespaces && in_array( $index, $namespaces ); } @@ -433,22 +490,25 @@ class NamespaceInfo { * @return null|string Default model name for the given namespace, if set */ public function getNamespaceContentModel( $index ) { - return $this->config->get( 'NamespaceContentModels' )[$index] ?? null; + return $this->options->get( 'NamespaceContentModels' )[$index] ?? null; } /** * Determine which restriction levels it makes sense to use in a namespace, * optionally filtered by a user's rights. * + * @todo Move this to PermissionManager and remove the dependency here on permissions-related + * config settings. + * * @param int $index Index to check * @param User|null $user User to check * @return array */ public function getRestrictionLevels( $index, User $user = null ) { - if ( !isset( $this->config->get( 'NamespaceProtection' )[$index] ) ) { + if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) { // All levels are valid if there's no namespace restriction. // But still filter by user, if necessary - $levels = $this->config->get( 'RestrictionLevels' ); + $levels = $this->options->get( 'RestrictionLevels' ); if ( $user ) { $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) { $right = $level; @@ -467,7 +527,7 @@ class NamespaceInfo { // First, get the list of groups that can edit this namespace. $namespaceGroups = []; $combine = 'array_merge'; - foreach ( (array)$this->config->get( 'NamespaceProtection' )[$index] as $right ) { + foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) { if ( $right == 'sysop' ) { $right = 'editprotected'; // BC } @@ -485,7 +545,7 @@ class NamespaceInfo { // group that can edit the namespace but would be blocked by the // restriction. $usableLevels = [ '' ]; - foreach ( $this->config->get( 'RestrictionLevels' ) as $level ) { + foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) { $right = $level; if ( $right == 'sysop' ) { $right = 'editprotected'; // BC diff --git a/includes/user/User.php b/includes/user/User.php index cdbbcc583b..57c5130ccd 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -673,11 +673,20 @@ class User implements IDBAccessObject, UserIdentity { * @param int|null $userId User ID, if known * @param string|null $userName User name, if known * @param int|null $actorId Actor ID, if known + * @param bool|string $wikiId remote wiki to which the User/Actor ID applies, or false if none * @return User */ - public static function newFromAnyId( $userId, $userName, $actorId ) { + public static function newFromAnyId( $userId, $userName, $actorId, $wikiId = false ) { global $wgActorTableSchemaMigrationStage; + // Stop-gap solution for the problem described in T222212. + // Force the User ID and Actor ID to zero for users loaded from the database + // of another wiki, to prevent subtle data corruption and confusing failure modes. + if ( $wikiId !== false ) { + $userId = 0; + $actorId = 0; + } + $user = new User; $user->mFrom = 'defaults'; @@ -3665,12 +3674,25 @@ class User implements IDBAccessObject, UserIdentity { return true; } + /** + * Alias of isLoggedIn() with a name that describes its actual functionality. UserIdentity has + * only this new name and not the old isLoggedIn() variant. + * + * @return bool True if user is registered on this wiki, i.e., has a user ID. False if user is + * anonymous or has no local account (which can happen when importing). This is equivalent to + * getId() != 0 and is provided for code readability. + * @since 1.34 + */ + public function isRegistered() { + return $this->getId() != 0; + } + /** * Get whether the user is logged in * @return bool */ public function isLoggedIn() { - return $this->getId() != 0; + return $this->isRegistered(); } /** @@ -3678,7 +3700,7 @@ class User implements IDBAccessObject, UserIdentity { * @return bool */ public function isAnon() { - return !$this->isLoggedIn(); + return !$this->isRegistered(); } /** diff --git a/includes/user/UserIdentity.php b/includes/user/UserIdentity.php index ac9bbec3f1..64c61fe294 100644 --- a/includes/user/UserIdentity.php +++ b/includes/user/UserIdentity.php @@ -62,4 +62,12 @@ interface UserIdentity { */ public function equals( UserIdentity $user ); + /** + * @since 1.34 + * + * @return bool True if user is registered on this wiki, i.e., has a user ID. False if user is + * anonymous or has no local account (which can happen when importing). This must be + * equivalent to getId() != 0 and is provided for code readability. + */ + public function isRegistered(); } diff --git a/includes/user/UserIdentityValue.php b/includes/user/UserIdentityValue.php index d1fd19de52..800ac760a5 100644 --- a/includes/user/UserIdentityValue.php +++ b/includes/user/UserIdentityValue.php @@ -93,4 +93,14 @@ class UserIdentityValue implements UserIdentity { return $this->getName() === $user->getName(); } + /** + * @since 1.34 + * + * @return bool True if user is registered on this wiki, i.e., has a user ID. False if user is + * anonymous or has no local account (which can happen when importing). This is equivalent to + * getId() != 0 and is provided for code readability. + */ + public function isRegistered() { + return $this->getId() != 0; + } } diff --git a/includes/watcheditem/NoWriteWatchedItemStore.php b/includes/watcheditem/NoWriteWatchedItemStore.php index fc95ebc46e..72f6086dfb 100644 --- a/includes/watcheditem/NoWriteWatchedItemStore.php +++ b/includes/watcheditem/NoWriteWatchedItemStore.php @@ -18,7 +18,9 @@ * @file * @ingroup Watchlist */ + use MediaWiki\Linker\LinkTarget; +use MediaWiki\User\UserIdentity; use Wikimedia\Rdbms\DBReadOnlyError; /** @@ -42,7 +44,7 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface { $this->actualStore = $actualStore; } - public function countWatchedItems( User $user ) { + public function countWatchedItems( UserIdentity $user ) { return $this->actualStore->countWatchedItems( $user ); } @@ -68,27 +70,27 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface { ); } - public function getWatchedItem( User $user, LinkTarget $target ) { + public function getWatchedItem( UserIdentity $user, LinkTarget $target ) { return $this->actualStore->getWatchedItem( $user, $target ); } - public function loadWatchedItem( User $user, LinkTarget $target ) { + public function loadWatchedItem( UserIdentity $user, LinkTarget $target ) { return $this->actualStore->loadWatchedItem( $user, $target ); } - public function getWatchedItemsForUser( User $user, array $options = [] ) { + public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) { return $this->actualStore->getWatchedItemsForUser( $user, $options ); } - public function isWatched( User $user, LinkTarget $target ) { + public function isWatched( UserIdentity $user, LinkTarget $target ) { return $this->actualStore->isWatched( $user, $target ); } - public function getNotificationTimestampsBatch( User $user, array $targets ) { + public function getNotificationTimestampsBatch( UserIdentity $user, array $targets ) { return $this->actualStore->getNotificationTimestampsBatch( $user, $targets ); } - public function countUnreadNotifications( User $user, $unreadLimit = null ) { + public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null ) { return $this->actualStore->countUnreadNotifications( $user, $unreadLimit ); } @@ -100,56 +102,60 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function addWatch( User $user, LinkTarget $target ) { + public function addWatch( UserIdentity $user, LinkTarget $target ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function addWatchBatchForUser( User $user, array $targets ) { + public function addWatchBatchForUser( UserIdentity $user, array $targets ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function removeWatch( User $user, LinkTarget $target ) { + public function removeWatch( UserIdentity $user, LinkTarget $target ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } public function setNotificationTimestampsForUser( - User $user, + UserIdentity $user, $timestamp, array $targets = [] ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) { + public function updateNotificationTimestamp( + UserIdentity $editor, LinkTarget $target, $timestamp + ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function resetAllNotificationTimestampsForUser( User $user ) { + public function resetAllNotificationTimestampsForUser( UserIdentity $user ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } public function resetNotificationTimestamp( - User $user, - Title $title, + UserIdentity $user, + LinkTarget $title, $force = '', $oldid = 0 ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function clearUserWatchedItems( User $user ) { + public function clearUserWatchedItems( UserIdentity $user ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function clearUserWatchedItemsUsingJobQueue( User $user ) { + public function clearUserWatchedItemsUsingJobQueue( UserIdentity $user ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function removeWatchBatchForUser( User $user, array $titles ) { + public function removeWatchBatchForUser( UserIdentity $user, array $titles ) { throw new DBReadOnlyError( null, self::DB_READONLY_ERROR ); } - public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target ) { + public function getLatestNotificationTimestamp( + $timestamp, UserIdentity $user, LinkTarget $target + ) { return wfTimestampOrNull( TS_MW, $timestamp ); } } diff --git a/includes/watcheditem/WatchedItem.php b/includes/watcheditem/WatchedItem.php index 43a9c4e536..4bf7f0c300 100644 --- a/includes/watcheditem/WatchedItem.php +++ b/includes/watcheditem/WatchedItem.php @@ -20,6 +20,7 @@ */ use MediaWiki\Linker\LinkTarget; +use MediaWiki\User\UserIdentity; /** * Representation of a pair of user and title for watchlist entries. @@ -36,7 +37,7 @@ class WatchedItem { private $linkTarget; /** - * @var User + * @var UserIdentity */ private $user; @@ -46,12 +47,12 @@ class WatchedItem { private $notificationTimestamp; /** - * @param User $user + * @param UserIdentity $user * @param LinkTarget $linkTarget * @param null|string $notificationTimestamp the value of the wl_notificationtimestamp field */ public function __construct( - User $user, + UserIdentity $user, LinkTarget $linkTarget, $notificationTimestamp ) { @@ -61,9 +62,17 @@ class WatchedItem { } /** + * @deprecated since 1.34, use getUserIdentity() * @return User */ public function getUser() { + return User::newFromIdentity( $this->user ); + } + + /** + * @return UserIdentity + */ + public function getUserIdentity() { return $this->user; } diff --git a/includes/watcheditem/WatchedItemQueryService.php b/includes/watcheditem/WatchedItemQueryService.php index 6094f41721..30e3cbee0e 100644 --- a/includes/watcheditem/WatchedItemQueryService.php +++ b/includes/watcheditem/WatchedItemQueryService.php @@ -1,8 +1,9 @@ string (format accepted by wfTimestamp) requires 'dir' option, * timestamp to end enumerating * 'watchlistOwner' => User user whose watchlist items should be listed if different - * than the one specified with $user param, - * requires 'watchlistOwnerToken' option + * than the one specified with $user param, requires + * 'watchlistOwnerToken' option * 'watchlistOwnerToken' => string a watchlist token used to access another user's * watchlist, used with 'watchlistOwnerToken' option * 'limit' => int maximum numbers of items to return @@ -256,7 +257,7 @@ class WatchedItemQueryService { /** * For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser * - * @param User $user + * @param UserIdentity $user * @param array $options Allowed keys: * 'sort' => string optional sorting by namespace ID and title * one of the self::SORT_* constants @@ -272,8 +273,8 @@ class WatchedItemQueryService { * specified using the form option * @return WatchedItem[] */ - public function getWatchedItemsForUser( User $user, array $options = [] ) { - if ( $user->isAnon() ) { + public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) { + if ( !$user->isRegistered() ) { // TODO: should this just return an empty array or rather complain loud at this point // as e.g. ApiBase::getWatchlistUser does? return []; @@ -460,11 +461,12 @@ class WatchedItemQueryService { return $conds; } - private function getWatchlistOwnerId( User $user, array $options ) { + private function getWatchlistOwnerId( UserIdentity $user, array $options ) { if ( array_key_exists( 'watchlistOwner', $options ) ) { /** @var User $watchlistOwner */ $watchlistOwner = $options['watchlistOwner']; - $ownersToken = $watchlistOwner->getOption( 'watchlisttoken' ); + $ownersToken = + $watchlistOwner->getOption( 'watchlisttoken' ); $token = $options['watchlistOwnerToken']; if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) { throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' ); @@ -613,7 +615,9 @@ class WatchedItemQueryService { ); } - private function getWatchedItemsForUserQueryConds( IDatabase $db, User $user, array $options ) { + private function getWatchedItemsForUserQueryConds( + IDatabase $db, UserIdentity $user, array $options + ) { $conds = [ 'wl_user' => $user->getId() ]; if ( $options['namespaceIds'] ) { $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] ); diff --git a/includes/watcheditem/WatchedItemQueryServiceExtension.php b/includes/watcheditem/WatchedItemQueryServiceExtension.php index a0e64c5d12..00770ea79d 100644 --- a/includes/watcheditem/WatchedItemQueryServiceExtension.php +++ b/includes/watcheditem/WatchedItemQueryServiceExtension.php @@ -1,5 +1,6 @@ lbFactory = $lbFactory; $this->loadBalancer = $lbFactory->getMainLB(); @@ -106,9 +117,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac $this->stats = new NullStatsdDataFactory(); $this->deferredUpdatesAddCallableUpdateCallback = [ DeferredUpdates::class, 'addCallableUpdate' ]; - $this->revisionGetTimestampFromIdCallback = - [ Revision::class, 'getTimestampFromId' ]; $this->updateRowsPerQuery = $updateRowsPerQuery; + $this->nsInfo = $nsInfo; + $this->revisionLookup = $revisionLookup; $this->latestUpdateCache = new HashBagOStuff( [ 'maxKeys' => 3 ] ); } @@ -144,30 +155,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } ); } - /** - * Overrides the Revision::getTimestampFromId callback - * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined. - * - * @param callable $callback - * @see Revision::getTimestampFromId for callback signiture - * - * @return ScopedCallback to reset the overridden value - * @throws MWException - */ - public function overrideRevisionGetTimestampFromIdCallback( callable $callback ) { - if ( !defined( 'MW_PHPUNIT_TEST' ) ) { - throw new MWException( - 'Cannot override Revision::getTimestampFromId callback in operation.' - ); - } - $previousValue = $this->revisionGetTimestampFromIdCallback; - $this->revisionGetTimestampFromIdCallback = $callback; - return new ScopedCallback( function () use ( $previousValue ) { - $this->revisionGetTimestampFromIdCallback = $previousValue; - } ); - } - - private function getCacheKey( User $user, LinkTarget $target ) { + private function getCacheKey( UserIdentity $user, LinkTarget $target ) { return $this->cache->makeKey( (string)$target->getNamespace(), $target->getDBkey(), @@ -176,7 +164,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } private function cache( WatchedItem $item ) { - $user = $item->getUser(); + $user = $item->getUserIdentity(); $target = $item->getLinkTarget(); $key = $this->getCacheKey( $user, $target ); $this->cache->set( $key, $item ); @@ -184,7 +172,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac $this->stats->increment( 'WatchedItemStore.cache' ); } - private function uncache( User $user, LinkTarget $target ) { + private function uncache( UserIdentity $user, LinkTarget $target ) { $this->cache->delete( $this->getCacheKey( $user, $target ) ); unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] ); $this->stats->increment( 'WatchedItemStore.uncache' ); @@ -201,7 +189,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } } - private function uncacheUser( User $user ) { + private function uncacheUser( UserIdentity $user ) { $this->stats->increment( 'WatchedItemStore.uncacheUser' ); foreach ( $this->cacheIndex as $ns => $dbKeyArray ) { foreach ( $dbKeyArray as $dbKey => $userArray ) { @@ -218,12 +206,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } /** - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * * @return WatchedItem|false */ - private function getCached( User $user, LinkTarget $target ) { + private function getCached( UserIdentity $user, LinkTarget $target ) { return $this->cache->get( $this->getCacheKey( $user, $target ) ); } @@ -231,12 +219,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac * Return an array of conditions to select or update the appropriate database * row. * - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * * @return array */ - private function dbCond( User $user, LinkTarget $target ) { + private function dbCond( UserIdentity $user, LinkTarget $target ) { return [ 'wl_user' => $user->getId(), 'wl_namespace' => $target->getNamespace(), @@ -260,11 +248,11 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac * * @since 1.30 * - * @param User $user + * @param UserIdentity $user * * @return bool true on success, false when too many items are watched */ - public function clearUserWatchedItems( User $user ) { + public function clearUserWatchedItems( UserIdentity $user ) { if ( $this->countWatchedItems( $user ) > $this->updateRowsPerQuery ) { return false; } @@ -280,7 +268,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac return true; } - private function uncacheAllItemsForUser( User $user ) { + private function uncacheAllItemsForUser( UserIdentity $user ) { $userId = $user->getId(); foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) { foreach ( $dbKeyIndex as $dbKey => $userIndex ) { @@ -309,9 +297,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac * * @since 1.31 * - * @param User $user + * @param UserIdentity $user */ - public function clearUserWatchedItemsUsingJobQueue( User $user ) { + public function clearUserWatchedItemsUsingJobQueue( UserIdentity $user ) { $job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() ); $this->queueGroup->push( $job ); } @@ -332,10 +320,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.31 - * @param User $user + * @param UserIdentity $user * @return int */ - public function countWatchedItems( User $user ) { + public function countWatchedItems( UserIdentity $user ) { $dbr = $this->getConnectionRef( DB_REPLICA ); $return = (int)$dbr->selectField( 'watchlist', @@ -394,16 +382,16 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } /** - * @param User $user + * @param UserIdentity $user * @param TitleValue[] $titles * @return bool * @throws MWException */ - public function removeWatchBatchForUser( User $user, array $titles ) { + public function removeWatchBatchForUser( UserIdentity $user, array $titles ) { if ( $this->readOnlyMode->isReadOnly() ) { return false; } - if ( $user->isAnon() ) { + if ( !$user->isRegistered() ) { return false; } if ( !$titles ) { @@ -563,12 +551,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * @return bool */ - public function getWatchedItem( User $user, LinkTarget $target ) { - if ( $user->isAnon() ) { + public function getWatchedItem( UserIdentity $user, LinkTarget $target ) { + if ( !$user->isRegistered() ) { return false; } @@ -583,13 +571,13 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * @return WatchedItem|bool */ - public function loadWatchedItem( User $user, LinkTarget $target ) { - // Only loggedin user can have a watchlist - if ( $user->isAnon() ) { + public function loadWatchedItem( UserIdentity $user, LinkTarget $target ) { + // Only registered user can have a watchlist + if ( !$user->isRegistered() ) { return false; } @@ -618,11 +606,11 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param array $options * @return WatchedItem[] */ - public function getWatchedItemsForUser( User $user, array $options = [] ) { + public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) { $options += [ 'forWrite' => false ]; $dbOptions = []; @@ -664,27 +652,27 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * @return bool */ - public function isWatched( User $user, LinkTarget $target ) { + public function isWatched( UserIdentity $user, LinkTarget $target ) { return (bool)$this->getWatchedItem( $user, $target ); } /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param LinkTarget[] $targets * @return array */ - public function getNotificationTimestampsBatch( User $user, array $targets ) { + public function getNotificationTimestampsBatch( UserIdentity $user, array $targets ) { $timestamps = []; foreach ( $targets as $target ) { $timestamps[$target->getNamespace()][$target->getDBkey()] = false; } - if ( $user->isAnon() ) { + if ( !$user->isRegistered() ) { return $timestamps; } @@ -728,27 +716,27 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * @throws MWException */ - public function addWatch( User $user, LinkTarget $target ) { + public function addWatch( UserIdentity $user, LinkTarget $target ) { $this->addWatchBatchForUser( $user, [ $target ] ); } /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param LinkTarget[] $targets * @return bool * @throws MWException */ - public function addWatchBatchForUser( User $user, array $targets ) { + public function addWatchBatchForUser( UserIdentity $user, array $targets ) { if ( $this->readOnlyMode->isReadOnly() ) { return false; } - // Only logged-in user can have a watchlist - if ( $user->isAnon() ) { + // Only registered user can have a watchlist + if ( !$user->isRegistered() ) { return false; } @@ -799,12 +787,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * @return bool * @throws MWException */ - public function removeWatch( User $user, LinkTarget $target ) { + public function removeWatch( UserIdentity $user, LinkTarget $target ) { return $this->removeWatchBatchForUser( $user, [ $target ] ); } @@ -820,14 +808,16 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac * only the specified titles will be updated, and this will be done immediately (not deferred). * * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param string|int $timestamp Value to set the "last viewed" timestamp to (null to clear) * @param LinkTarget[] $targets Titles to set the timestamp for; [] means the entire watchlist * @return bool */ - public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) { - // Only loggedin user can have a watchlist - if ( $user->isAnon() || $this->readOnlyMode->isReadOnly() ) { + public function setNotificationTimestampsForUser( + UserIdentity $user, $timestamp, array $targets = [] + ) { + // Only registered user can have a watchlist + if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) { return false; } @@ -873,7 +863,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac return true; } - public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target ) { + public function getLatestNotificationTimestamp( + $timestamp, UserIdentity $user, LinkTarget $target + ) { $timestamp = wfTimestampOrNull( TS_MW, $timestamp ); if ( $timestamp === null ) { return null; // no notification @@ -894,12 +886,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * Schedule a DeferredUpdate that sets all of the "last viewed" timestamps for a given user * to the same value. - * @param User $user + * @param UserIdentity $user * @param string|int|null $timestamp Value to set all timestamps to, null to clear them */ - public function resetAllNotificationTimestampsForUser( User $user, $timestamp = null ) { - // Only loggedin user can have a watchlist - if ( $user->isAnon() ) { + public function resetAllNotificationTimestampsForUser( UserIdentity $user, $timestamp = null ) { + // Only registered user can have a watchlist + if ( !$user->isRegistered() ) { return; } @@ -920,12 +912,14 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $editor + * @param UserIdentity $editor * @param LinkTarget $target * @param string|int $timestamp * @return int[] */ - public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) { + public function updateNotificationTimestamp( + UserIdentity $editor, LinkTarget $target, $timestamp + ) { $dbw = $this->getConnectionRef( DB_MASTER ); $uids = $dbw->selectFieldValues( 'watchlist', @@ -977,23 +971,36 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $user - * @param Title $title + * @param UserIdentity $user + * @param LinkTarget $title * @param string $force * @param int $oldid * @return bool */ - public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) { + public function resetNotificationTimestamp( + UserIdentity $user, LinkTarget $title, $force = '', $oldid = 0 + ) { $time = time(); - // Only loggedin user can have a watchlist - if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) { + // Only registered user can have a watchlist + if ( $this->readOnlyMode->isReadOnly() || !$user->isRegistered() ) { return false; } - if ( !Hooks::run( 'BeforeResetNotificationTimestamp', [ &$user, &$title, $force, &$oldid ] ) ) { + // Hook expects User and Title, not UserIdentity and LinkTarget + $userObj = User::newFromId( $user->getId() ); + $titleObj = Title::castFromLinkTarget( $title ); + if ( !Hooks::run( 'BeforeResetNotificationTimestamp', + [ &$userObj, &$titleObj, $force, &$oldid ] ) + ) { return false; } + if ( !$userObj->equals( $user ) ) { + $user = $userObj; + } + if ( !$titleObj->equals( $title ) ) { + $title = $titleObj; + } $item = null; if ( $force != 'force' ) { @@ -1004,11 +1011,19 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } // Get the timestamp (TS_MW) of this revision to track the latest one seen - $seenTime = call_user_func( - $this->revisionGetTimestampFromIdCallback, - $title, - $oldid ?: $title->getLatestRevID() - ); + $id = $oldid; + $seenTime = null; + if ( !$id ) { + $latestRev = $this->revisionLookup->getRevisionByTitle( $title ); + if ( $latestRev ) { + $id = $latestRev->getId(); + // Save a DB query + $seenTime = $latestRev->getTimestamp(); + } + } + if ( $seenTime === null ) { + $seenTime = $this->revisionLookup->getTimestampFromId( $id ); + } // Mark the item as read immediately in lightweight storage $this->stash->merge( @@ -1053,10 +1068,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } /** - * @param User $user + * @param UserIdentity $user * @return MapCacheLRU|null The map contains prefixed title keys and TS_MW values */ - private function getPageSeenTimestamps( User $user ) { + private function getPageSeenTimestamps( UserIdentity $user ) { $key = $this->getPageSeenTimestampsKey( $user ); return $this->latestUpdateCache->getWithSetCallback( @@ -1069,10 +1084,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } /** - * @param User $user + * @param UserIdentity $user * @return string */ - private function getPageSeenTimestampsKey( User $user ) { + private function getPageSeenTimestampsKey( UserIdentity $user ) { return $this->stash->makeGlobalKey( 'watchlist-recent-updates', $this->lbFactory->getLocalDomainID(), @@ -1088,13 +1103,16 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac return "{$target->getNamespace()}:{$target->getDBkey()}"; } - private function getNotificationTimestamp( User $user, Title $title, $item, $force, $oldid ) { + private function getNotificationTimestamp( + UserIdentity $user, LinkTarget $title, $item, $force, $oldid + ) { if ( !$oldid ) { // No oldid given, assuming latest revision; clear the timestamp. return null; } - if ( !$title->getNextRevisionID( $oldid ) ) { + $oldRev = $this->revisionLookup->getRevisionById( $oldid ); + if ( !$this->revisionLookup->getNextRevision( $oldRev, $title ) ) { // Oldid given and is the latest revision for this title; clear the timestamp. return null; } @@ -1110,12 +1128,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac // Oldid given and isn't the latest; update the timestamp. // This will result in no further notification emails being sent! - // Calls Revision::getTimestampFromId in normal operation - $notificationTimestamp = call_user_func( - $this->revisionGetTimestampFromIdCallback, - $title, - $oldid - ); + $notificationTimestamp = $this->revisionLookup->getTimestampFromId( $oldid ); // We need to go one second to the future because of various strict comparisons // throughout the codebase @@ -1137,11 +1150,11 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @since 1.27 - * @param User $user + * @param UserIdentity $user * @param int|null $unreadLimit * @return int|bool */ - public function countUnreadNotifications( User $user, $unreadLimit = null ) { + public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null ) { $dbr = $this->getConnectionRef( DB_REPLICA ); $queryOptions = []; @@ -1174,11 +1187,15 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac * @param LinkTarget $newTarget */ public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) { - $oldTarget = Title::newFromLinkTarget( $oldTarget ); - $newTarget = Title::newFromLinkTarget( $newTarget ); - - $this->duplicateEntry( $oldTarget->getSubjectPage(), $newTarget->getSubjectPage() ); - $this->duplicateEntry( $oldTarget->getTalkPage(), $newTarget->getTalkPage() ); + // Duplicate first the subject page, then the talk page + $this->duplicateEntry( + $this->nsInfo->getSubjectPage( $oldTarget ), + $this->nsInfo->getSubjectPage( $newTarget ) + ); + $this->duplicateEntry( + $this->nsInfo->getTalkPage( $oldTarget ), + $this->nsInfo->getTalkPage( $newTarget ) + ); } /** @@ -1241,10 +1258,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac } /** - * @param User $user - * @param Title[] $titles + * @param UserIdentity $user + * @param LinkTarget[] $titles */ - private function uncacheTitlesForUser( User $user, array $titles ) { + private function uncacheTitlesForUser( UserIdentity $user, array $titles ) { foreach ( $titles as $title ) { $this->uncache( $user, $title ); } diff --git a/includes/watcheditem/WatchedItemStoreInterface.php b/includes/watcheditem/WatchedItemStoreInterface.php index b6d7b68035..5ff29d0d5d 100644 --- a/includes/watcheditem/WatchedItemStoreInterface.php +++ b/includes/watcheditem/WatchedItemStoreInterface.php @@ -18,7 +18,9 @@ * @file * @ingroup Watchlist */ + use MediaWiki\Linker\LinkTarget; +use MediaWiki\User\UserIdentity; use Wikimedia\Rdbms\DBUnexpectedError; /** @@ -43,11 +45,11 @@ interface WatchedItemStoreInterface { * * @since 1.31 * - * @param User $user + * @param UserIdentity $user * * @return int */ - public function countWatchedItems( User $user ); + public function countWatchedItems( UserIdentity $user ); /** * @since 1.31 @@ -115,29 +117,29 @@ interface WatchedItemStoreInterface { * * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * * @return WatchedItem|false */ - public function getWatchedItem( User $user, LinkTarget $target ); + public function getWatchedItem( UserIdentity $user, LinkTarget $target ); /** * Loads an item from the db * * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * * @return WatchedItem|false */ - public function loadWatchedItem( User $user, LinkTarget $target ); + public function loadWatchedItem( UserIdentity $user, LinkTarget $target ); /** * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param array $options Allowed keys: * 'forWrite' => bool defaults to false * 'sort' => string optional sorting by namespace ID and title @@ -145,24 +147,24 @@ interface WatchedItemStoreInterface { * * @return WatchedItem[] */ - public function getWatchedItemsForUser( User $user, array $options = [] ); + public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ); /** * Must be called separately for Subject & Talk namespaces * * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * * @return bool */ - public function isWatched( User $user, LinkTarget $target ); + public function isWatched( UserIdentity $user, LinkTarget $target ); /** * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param LinkTarget[] $targets * * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp, @@ -170,54 +172,54 @@ interface WatchedItemStoreInterface { * - string|null value of wl_notificationtimestamp, * - false if $target is not watched by $user. */ - public function getNotificationTimestampsBatch( User $user, array $targets ); + public function getNotificationTimestampsBatch( UserIdentity $user, array $targets ); /** * Must be called separately for Subject & Talk namespaces * * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target */ - public function addWatch( User $user, LinkTarget $target ); + public function addWatch( UserIdentity $user, LinkTarget $target ); /** * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param LinkTarget[] $targets * * @return bool success */ - public function addWatchBatchForUser( User $user, array $targets ); + public function addWatchBatchForUser( UserIdentity $user, array $targets ); /** - * Removes an entry for the User watching the LinkTarget + * Removes an entry for the UserIdentity watching the LinkTarget * Must be called separately for Subject & Talk namespaces * * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param LinkTarget $target * * @return bool success * @throws DBUnexpectedError * @throws MWException */ - public function removeWatch( User $user, LinkTarget $target ); + public function removeWatch( UserIdentity $user, LinkTarget $target ); /** * @since 1.31 * - * @param User $user The user to set the timestamps for + * @param UserIdentity $user The user to set the timestamps for * @param string|null $timestamp Set the update timestamp to this value * @param LinkTarget[] $targets List of targets to update. Default to all targets * * @return bool success */ public function setNotificationTimestampsForUser( - User $user, + UserIdentity $user, $timestamp, array $targets = [] ); @@ -227,29 +229,30 @@ interface WatchedItemStoreInterface { * * @since 1.31 * - * @param User $user The user to reset the timestamps for + * @param UserIdentity $user The user to reset the timestamps for */ - public function resetAllNotificationTimestampsForUser( User $user ); + public function resetAllNotificationTimestampsForUser( UserIdentity $user ); /** * @since 1.31 * - * @param User $editor The editor that triggered the update. Their notification + * @param UserIdentity $editor The editor that triggered the update. Their notification * timestamp will not be updated(they have already seen it) * @param LinkTarget $target The target to update timestamps for * @param string $timestamp Set the update timestamp to this value * * @return int[] Array of user IDs the timestamp has been updated for */ - public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ); + public function updateNotificationTimestamp( + UserIdentity $editor, LinkTarget $target, $timestamp ); /** * Reset the notification timestamp of this entry * * @since 1.31 * - * @param User $user - * @param Title $title + * @param UserIdentity $user + * @param LinkTarget $title * @param string $force Whether to force the write query to be executed even if the * page is not watched or the notification timestamp is already NULL. * 'force' in order to force @@ -258,18 +261,19 @@ interface WatchedItemStoreInterface { * * @return bool success Whether a job was enqueued */ - public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ); + public function resetNotificationTimestamp( + UserIdentity $user, LinkTarget $title, $force = '', $oldid = 0 ); /** * @since 1.31 * - * @param User $user + * @param UserIdentity $user * @param int|null $unreadLimit * * @return int|bool The number of unread notifications * true if greater than or equal to $unreadLimit */ - public function countUnreadNotifications( User $user, $unreadLimit = null ); + public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null ); /** * Check if the given title already is watched by the user, and if so @@ -303,28 +307,28 @@ interface WatchedItemStoreInterface { * * @since 1.31 * - * @param User $user + * @param UserIdentity $user */ - public function clearUserWatchedItems( User $user ); + public function clearUserWatchedItems( UserIdentity $user ); /** * Queues a job that will clear the users watchlist using the Job Queue. * * @since 1.31 * - * @param User $user + * @param UserIdentity $user */ - public function clearUserWatchedItemsUsingJobQueue( User $user ); + public function clearUserWatchedItemsUsingJobQueue( UserIdentity $user ); /** * @since 1.32 * - * @param User $user + * @param UserIdentity $user * @param LinkTarget[] $targets * * @return bool success */ - public function removeWatchBatchForUser( User $user, array $targets ); + public function removeWatchBatchForUser( UserIdentity $user, array $targets ); /** * Convert $timestamp to TS_MW or return null if the page was visited since then by $user @@ -335,9 +339,10 @@ interface WatchedItemStoreInterface { * 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 UserIdentity $user * @param LinkTarget $target * @return string|null TS_MW timestamp or null if all revision were seen */ - public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target ); + public function getLatestNotificationTimestamp( + $timestamp, UserIdentity $user, LinkTarget $target ); } diff --git a/languages/i18n/ang.json b/languages/i18n/ang.json index 91191b7344..b3df9ec5ab 100644 --- a/languages/i18n/ang.json +++ b/languages/i18n/ang.json @@ -294,6 +294,8 @@ "error": "Wōh", "databaseerror": "Cȳþþuhordes wōh", "databaseerror-textcl": "Gecyþneshordfræge misgedwild belamp", + "databaseerror-query": "Æsce: $1", + "databaseerror-function": "Wice: $1", "databaseerror-error": "Wōg: $1", "laggedslavemode": "'''Warnung:''' Wēnunga næbbe se tramet nīwlīca nīwunga.", "readonly": "Ġifhord locen", diff --git a/languages/i18n/ar.json b/languages/i18n/ar.json index bcabcc8fcb..137e7ea4fd 100644 --- a/languages/i18n/ar.json +++ b/languages/i18n/ar.json @@ -589,7 +589,7 @@ "botpasswords-editexisting": "تعديل كلمة سر موجودة للبوت", "botpasswords-label-needsreset": "(تحتاج كلمة المرور إلى إعادة الضبط)", "botpasswords-label-appid": "اسم البوت:", - "botpasswords-label-create": "أنشأ", + "botpasswords-label-create": "إنشاء", "botpasswords-label-update": "تحديث", "botpasswords-label-cancel": "ألغ", "botpasswords-label-delete": "احذف", @@ -3113,7 +3113,7 @@ "confirmemail_pending": "تم إرسال كود التأكيد إلى بريدك الإلكتروني مؤخراً؛\nإذا كنت قد أنشأت حسابك للتو، من الأفضل أن تنتظر بضع دقائق قبل أن تطلب كوداً آخر.", "confirmemail_send": "أرسل كود تأكيد", "confirmemail_sent": "تم إرسال رسالة التأكيد، شكرا لك.", - "confirmemail_oncreate": "تم إرسال كود تأكيد إلى عنوان بريدك الإلكتروني.\nالكود غير مطلوب للدخول إلى الموسوعة باسمك، ولكن يجب إدخاله قبل استخدامك أياً من خواص البريد الإلكتروني المستخدمة هنا في الويكي.", + "confirmemail_oncreate": "تم إرسال كود تأكيد إلى عنوان بريدك الإلكتروني.\nالكود غير مطلوب للدخول، ولكن يجب إدخاله قبل استخدامك أيًّا من خواص البريد الإلكتروني المستخدمة هنا في الويكي.", "confirmemail_sendfailed": "لم يتمكن {{SITENAME}} من إرسال رسالة التأكيد إليك.\nمن فضلك تأكد من عنوان بريدك الإلكتروني بحثاً عن حروف غير صحيحة.\n\nأرجع خادم البريد: $1", "confirmemail_invalid": "كود تأكيد غير صحيح.\nربما انتهت فترة صلاحيته.", "confirmemail_needlogin": "يجب عليك $1 لتأكيد بريدك الإلكتروني.", diff --git a/languages/i18n/ban.json b/languages/i18n/ban.json index 2995d00ec8..522ac693e2 100644 --- a/languages/i18n/ban.json +++ b/languages/i18n/ban.json @@ -18,6 +18,7 @@ "tog-hideminor": "engkebang suntingan ring gentosan sane pinih anyar", "tog-hidepatrolled": "engkebang suntingan mapatrol ring gentosan sane pinih anyar", "tog-newpageshidepatrolled": "engkebang lembar mapatrol saking saking kepahan lembar anyar", + "tog-hidecategorization": "Engkebang kacané", "tog-extendwatchlist": "kembangang kepahan pangiwasan antuk nampilang samian panguwahan, nenten sane anyar kewanten", "tog-usenewrc": "aniang suntingan ring tampilan pagentosan sane pinih anyar lan kepahan pangiwasan manutin lembar", "tog-numberheadings": "isinin nomor murda anggen cara otomatis", @@ -35,9 +36,9 @@ "tog-enotifminoredits": "taler kirimang titiang email ring panguwahan alit", "tog-enotifrevealaddr": "kirimang titiang alamat email ring catetan email", "tog-shownumberswatching": "tampilang akehnyane sane ngiwasin", - "tog-oldsig": "tanda tangan mangkin", + "tog-oldsig": "Tanda tangan mangkin", "tog-fancysig": "dadosang tanda tangan dados teks wiki (nenten pranala otomatis)", - "tog-uselivepreview": "anggen pratayang langsung(experimental)", + "tog-uselivepreview": "Anggen pratayang langsung ten anggen kaca sane malunan", "tog-forceeditsummary": "elingang titiang yening kotak ringkesan suntingan kari kosong", "tog-watchlisthideown": "engkebang panguwahan titiang saking kepahan pangiwasan", "tog-watchlisthidebots": "engkebang panguwahan bot ring kepahan pangiwasan", @@ -48,9 +49,9 @@ "tog-ccmeonemails": "kirimang titiang salinan email sane kirimang titiang ring anak lianan", "tog-diffonly": "sampunang katampilang daging lembar ring ungkur binanne suntingan", "tog-showhiddencats": "tampilang golongan sane kaengkebang", - "tog-norollbackdiff": "sampunang tampilang binanne sesampun ngewaliang", + "tog-norollbackdiff": "Sampunang tampilang binanne sesampun ngewaliang", "tog-useeditwarning": "elingang titiang yening ngalahin lembar panyuntingan sadurung nyimpen pagentosan", - "tog-prefershttps": "setata nganggen sambungan sane aman rikala malebu log", + "tog-prefershttps": "Setata nganggen sambungan sane aman rikala malebu log", "underline-always": "Setata", "underline-never": "Nénten naénin", "underline-default": "kulit utawi penjelajah paaban", @@ -128,6 +129,7 @@ "category-media-header": "lembar ring golongan \"$1\"", "category-empty": "\"mangkin, nenten madaging lembar utawi pekakas ring golongan puniki\"", "hidden-categories": "{{plural:$1|punduhan sane kaengkebang| punduhan sane kaengkebang}}", + "hidden-category-category": "Kategori mengkeb", "category-subcat-count": "{{PLURAL:$2| golongan puniki madue {{PLURAL:$1|$1 subkategori}} puniki, saking genepan $2.}}", "category-article-count": "{{PLURAL:$2|golongan puniki madue{{PLURAL:$1|$1 lembar}}, saking total $2.}}", "category-file-count": "{{PLURAL:$2|golongan puniki madue{{PLURAL:$1|$1 lembar}}, saking total $2.}}", @@ -137,6 +139,7 @@ "about": "Indik", "newwindow": "(bukak ring jendela anyar)", "cancel": "Buwung", + "mypage": "Kaca", "mytalk": "Wicara", "anontalk": "Wicara", "navigation": "Pengarah", @@ -145,16 +148,18 @@ "actions": "Parilaksana", "namespaces": "Genah pesengan", "variants": "kawentenan sane lianan", - "navigation-heading": "menu navigasi", + "navigation-heading": "Menu navigasi", "errorpagetitle": "kaluputan", "returnto": "mabalik ring $1", "tagline": "Saka {{SITENAME}}", "help": "Tulung", + "help-mediawiki": "Pitulung MediaWiki", "search": "Rereh", "searchbutton": "Rereh", "searcharticle": "lanturang", "history": "sejarah pupulan", "history_short": "kawentenan sane lawas", + "history_small": "babad", "printableversion": "kawentenan lian sane macetak", "permalink": "Pranala ajeg", "view": "cingakin", @@ -165,8 +170,9 @@ "protect_change": "gentos", "newpage": "Lembar Anyar", "talkpagelinktext": "Wicara", + "specialpage": "Lembar sane kautamayang", "personaltools": "pekakas pribadi", - "talk": "rembug\n\nngarembug (kata kerja)", + "talk": "Rembug", "views": "Pekantenan", "toolbox": "Pekakas", "viewhelppage": "cingak lembar pamitutlung", @@ -184,14 +190,18 @@ "disclaimers": "nungkas", "disclaimerpage": "Project:Pengelidan lumrah", "edithelp": "pamitulung panguwahan", + "helppage-top-gethelp": "Tulung", "mainpage": "Kaca Utama", "mainpage-description": "Lembar Utama", "portal": "Pintu nuju sekha", "portal-url": "Project:pamedal sekha", "privacy": "kawicaksanaan padewekan", "privacypage": "Project:kawicaksanan tanpaiket", + "ok": "OK", "retrievedfrom": "kapolihang saking \"$1\"", "youhavenewmessages": "{{PLURAL:$3|ida dane maduwe}} $1 ($2)", + "youhavenewmessagesfromusers": "{{PLURAL:$4|You have}} $1 ring {{PLURAL:$3|another user|$3 users}} ($2).", + "youhavenewmessagesmanyusers": "Ida dane ngelah $1 saking liyane ($2).", "editsection": "gentos", "editold": "mecikang", "viewsourceold": "cingak witnyane", @@ -199,6 +209,12 @@ "viewsourcelink": "cingak witnyane", "editsectionhint": "ubah kepahan$1", "toc": "kepahan dagingnyane", + "showtoc": "edengang", + "hidetoc": "engkebang", + "collapsible-expand": "buka", + "confirmable-confirm": "{{GENDER:$1|Ida}} dane yakin?", + "confirmable-yes": "Inggih", + "confirmable-no": "Nénten", "site-atom-feed": "$1 \"atom feed\"", "page-atom-feed": "$1 \"atom feed\"", "red-link-title": "$1 (kaca tan wénten)", @@ -212,6 +228,8 @@ "nstab-category": "golongan", "mainpage-nstab": "Kaca Utama", "nosuchspecialpage": "Ten wenten lembar spesial", + "error": "kaluputan", + "databaseerror": "Database kaluputan", "missing-article": "data utama nenten prasida nemu tulisan saking lembar sane sepatutne wenten, inggih punika $1, $2\n\nindike puniki biasane keranayang olih pranala kaon nuju pabenahan sane dumun lembar sane sampun kaicalang\n\nyening nenten puniki sane ngranayang, ida dane minab sampun manggihin kaiwangang ring sajeroning piranti lunak.\nDurus sadokang indik puniki rin silih sinunggil anak \n\n[[Special:ListUsers/sysop|Pengurus]], antuk ngetik alamat URL sane katuju", "missingarticle-rev": "(pabenahan#:$1)", "badtitle": "murda sane nenten manut", @@ -226,13 +244,22 @@ "yourpasswordagain": "jumunin kruna sandi", "login": "Ngranjing log", "nav-login-createaccount": "malebu log / ngawe pepalihan", + "logout": "Medal Log", "userlogout": "medal saking Log", + "notloggedin": "Konden masuk log", + "userlogin-noaccount": "Durung madue akun?", + "userlogin-joinproject": "Indik {{SITENAME}}", "createaccount": "ngajuang akun anyar", "mailmypassword": "nyumu ngaryanin kruna sandi", "loginlanguagelabel": "Basa: $1", "pt-login": "Ngranjing log", + "pt-login-button": "Ngranjing log", "pt-createaccount": "Ngajuang akun anyar", "pt-userlogout": "Medal Log", + "botpasswords-label-create": "Ngae", + "botpasswords-label-cancel": "Buungan", + "botpasswords-label-delete": "Apus", + "botpasswords-label-resetpassword": "Nyumu kruna sandi", "passwordreset": "Nyumu kruna sandi", "bold_sample": "teks puniki mesurat tebel", "bold_tip": "teks puniki mesurat tebel", @@ -256,7 +283,7 @@ "savearticle": "simpen lembar", "preview": "tayangan sadurungnyane", "showpreview": "cingak sane lintang", - "showdiff": "cingak pagentosan", + "showdiff": "Cingak pagentosan", "anoneditwarning": "Pingetan: Ida dané nénten kacatet ngranjing. Alamat IP ida dané jagi kacatet ring sejarah (indik sané dumunan) ring lembar puniki. Yening ida dane [$1 log in] utawi [$2 create an account], your edits will be attributed to your username, along with other benefits.", "newarticle": "(Anyar)", "newarticletext": "ida dane ngiring pranala nuju lembar sane durung wenten. yening jagi ngaryanang lembar punika, ketik daging lembar ring kotak sane wenten ring beten puniki. (cingak [$1 lembar wantuan] anggen wacana salanturnyane). yening ida dane nenten nyelapang neked ring lembar puniki, klik tombol \"back\" ring \"penjelajah web\" ida dane.", @@ -271,7 +298,7 @@ "hiddencategories": "lembar niki inggih punika krama saking {{PLURAL:$1|1 golongan sane mengkeb|$1 golongan sane mengkeb}}", "permissionserrorstext-withaction": "ida dané nénten madué kuasa ngranjing anggén $2, riantukan {{PLURAL:$1|alasan}} ring sor puniki:", "recreate-moveddeleted-warn": "\"pingetan\" ida dane ngawe malih lembar sane naenin maapus.'''\n\nmangda kayunin malih napike pantes lanturang suntingan ida dane. puniki log pengapusan lan pangisidan saking lembar puniki:", - "moveddeleted-notice": "lembar puniki sampun kaapus. anggen pewarah, puniki log pangapus lan pengisidan lembar puniki", + "moveddeleted-notice": "Lembar puniki sampun kaapus.\nAnggen pewarah, proteksi, lan pengisidan log saking lembar puniki cingakin pustaka beten.", "content-model-wikitext": "tulisan wiki", "post-expand-template-inclusion-warning": "pinget: ukuran templat sane keanggen kalangkung ageng. wenten templat sane kacampahang", "post-expand-template-inclusion-category": "lembar sane maukuran templat sane nglangkungin wates", @@ -335,14 +362,14 @@ "action-edit": "benahang lembar puniki", "nchanges": "$1{{PLURAL:$1|panguwahan|uwah-uwahan}}", "enhancedrc-history": "babad", - "recentchanges": "pagentosan sane anyar", + "recentchanges": "Pagentosan anyar", "recentchanges-legend": "pilihan panguwahan sane anyar", "recentchanges-feed-description": "molihang pagentosan anyar ring wiki ring \"umpan\" puniki", "recentchanges-label-newpage": "panguwahan puniki ngaryanin lembar anyar", "recentchanges-label-minor": "niki panguwahan kidik", "recentchanges-label-bot": "penguwahan puniki kalaksanayang antuk bot", "recentchanges-label-unpatrolled": "panguwahan puniki durung kapatroli", - "rcnotefrom": "Ring beten puniki inggih punika {{PLURAL:$5|panguwahan|panguwahan}} saking $3, $4 (kaedengang ngantos $1 panguwahan).", + "rcnotefrom": "Ring beten puniki inggih punika {{PLURAL:$5|panguwahan}} saking $3, $4 (kaedengang ngantos $1 panguwahan).", "rclistfrom": "edengang penguwahan sane anyar wit saking $3 $2", "rcshowhideminor": "$1 uwahan kidik", "rcshowhideminor-show": "Edengang", @@ -439,7 +466,7 @@ "namespace": "Genah pesengan", "invert": "uliang pilihan", "tooltip-invert": "Centang kotak puniki mangdané ngengkebang lembar sané kauwah ring genah wastan sané kapilih (miwah genah wastan sané mapaiketan yéning kacentang)", - "blanknamespace": "utama", + "blanknamespace": "(Utama)", "contributions": "kawigunan {{GENDER:$1|penganggo}}", "contributions-title": "Kontribusi pangangge anggen $1", "mycontris": "kawigunan", @@ -520,12 +547,12 @@ "tooltip-n-randompage": "edengang polah-palih lembar", "tooltip-n-help": "genah anggen ngarereh", "tooltip-t-whatlinkshere": "kepahan sami lembar wiki sane maduwe pranala nuju lembar puniki", - "tooltip-t-recentchangeslinked": "pagentosan sane anyar lembar-lembar sane maduwe pranala nuju lembar puniki", + "tooltip-t-recentchangeslinked": "Pagentosan anyar lembar sane maduwe pranala nuju lembar puniki", "tooltip-feed-atom": "\"atom feed\" anggen lembar puniki", - "tooltip-t-contributions": "Daptar kepahan kawigunan {{GENDER:$1|penganggo niki}", + "tooltip-t-contributions": "Daptar kepahan kawigunan {{GENDER:$1|penganggo niki}}", "tooltip-t-emailuser": "Ngirim surel majeng ring {{GENDER:$1|penganggo puniki}}", "tooltip-t-upload": "ngunggahang file", - "tooltip-t-specialpages": "kepahan sami lembar istimewa", + "tooltip-t-specialpages": "Kepahan sami lembar istimewa", "tooltip-t-print": "kawentenan lian sane macetak ring lembar puniki", "tooltip-t-permalink": "Pranala ajeg kaanggen ngubah lembar puniki", "tooltip-ca-nstab-main": "cingak dagingnyane lembar puniki", @@ -537,9 +564,9 @@ "tooltip-ca-nstab-help": "cingak lembar pamitutlung", "tooltip-ca-nstab-category": "cingak lembar kategori", "tooltip-minoredit": "pingetin puniki dados panguwahan kidik", - "tooltip-save": "simpen pagentosan ida dane", - "tooltip-preview": "pagentosan sane dumun duwen ida dane, mangda anggen niki sadurung jagi nyimpen!", - "tooltip-diff": "cingak pagentosan sane sampun ida dane laksanayang", + "tooltip-save": "Nyimpen pagentosan ida dane", + "tooltip-preview": "Pagentosan sane dumun duwen ida dane, mangda anggen niki sadurung jagi nyimpen!", + "tooltip-diff": "Cingak pagentosan sane sampun ida dane laksanayang", "tooltip-compareselectedversions": "cingak binane makekalih kepahan lembar sane kasudi", "tooltip-watch": "imbuhin lembar niki ring daftar paninjoan ida dane", "tooltip-rollback": "\"nguliang\" muwungan jagi ngabecikang ring lembar puniki nuju haturan sane untat ngangge apisan klik", diff --git a/languages/i18n/be-tarask.json b/languages/i18n/be-tarask.json index d99c4cd19c..63a465948f 100644 --- a/languages/i18n/be-tarask.json +++ b/languages/i18n/be-tarask.json @@ -1347,6 +1347,10 @@ "action-editmyuserjs": "рэдагаваньне вашых уласных JavaScript-файлаў", "action-viewsuppressed": "прагляд вэрсіяў, схаваных ад усіх удзельнікаў", "action-hideuser": "блякаваньне імя ўдзельніка і яго хаваньне", + "action-ipblock-exempt": "абыход блякаваньняў IP-адрасоў, аўтаблякаваньняў і блякаваньняў дыяпазонаў", + "action-unblockself": "разблякаваньне самога сябе", + "action-noratelimit": "адсутнасьць абмежаваньня хуткасьці", + "action-reupload-own": "перазапіс уласных існых файлаў", "nchanges": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|з апошняга візыту}}", "enhancedrc-history": "гісторыя", @@ -2106,7 +2110,7 @@ "linksearch-pat": "Узор для пошуку:", "linksearch-ns": "Прастора назваў:", "linksearch-ok": "Шукаць", - "linksearch-text": "Можна ўжываць сымбалі падстаноўкі, напрыклад, «*.wikipedia.org».\nНеабходны дамэн першага ўзроўню, напрыклад, «*.org».
\n{{PLURAL:$2|1=Пратакол, які падтрымліваецца|Пратаколы, якія падтрымліваюцца}}: $1 (дапомна http://, калі пратакол не пазначаны).", + "linksearch-text": "Можна ўжываць сымбалі падстаноўкі, напрыклад, «*.wikipedia.org».\nНеабходны дамэн першага ўзроўню, напрыклад, «*.org».
\n{{PLURAL:$2|1=Пратакол, які падтрымліваецца|Пратаколы, якія падтрымліваюцца}}: $1 (па змоўчаньні http://, калі пратакол не пазначаны).", "linksearch-line": "Спасылка на $1 з $2", "linksearch-error": "Сымбалі падстаноўкі могуць ужывацца толькі ў пачатку адрасоў.", "listusersfrom": "Паказаць удзельнікаў ад:", diff --git a/languages/i18n/bjn.json b/languages/i18n/bjn.json index eaddbeae92..478ecef33e 100644 --- a/languages/i18n/bjn.json +++ b/languages/i18n/bjn.json @@ -116,7 +116,7 @@ "category-subcat-count-limited": "Tumbung ini baisi {{PLURAL:$1|sub-tumbung|$1 sub-tutumbung}} barikut.", "category-article-count": "{{PLURAL:$2|Tumbung ni baisi asa tungkaran barikut haja.|Tutumbung ngini baisi {{PLURAL:$1|tungkaran|$1 tutungkaran}}, matan $2 sabarataan.}}", "category-article-count-limited": "Tumbung ini baisi {{PLURAL:$1|asa tungkaran|$1 tutungkaran}} barikut.", - "category-file-count": "{{PLURAL:$2|Tumbung ngini wastu baisi satu barakas barikut.|Tumbung ngini baisi {{PLURAL:$1|barakas|$1 babarakas}} barikut, matan $2 sabarataan.}}", + "category-file-count": "{{PLURAL:$2|Tumbung ngini baisi {{PLURAL:$1|$1 barakas}}, matan jumlah $2.}}", "category-file-count-limited": "Tumbung ngini baisi {{PLURAL:$1|barakas|$1 barakas}} barikut.", "listingcontinuesabbrev": "samb.", "index-category": "Tungkaran tasusun bapadalakan kata", @@ -291,7 +291,7 @@ "actionthrottled": "Kalakuan dikiripi", "actionthrottledtext": "Sawagai sabuting takaran anti-spam, Pian dibabatasi hagan balalaku kababanyakan dalam parhatan handap, wan Pian sudah limpuari batasan ngini.\nMuhun cubai pulang dalam babarapa minit.", "protectedpagetext": "Tungkaran ngini sudah dilindungi hagan mancagah babakan.", - "viewsourcetext": "Pian kawa maniringi wan manyalin asal mula tungkaran ngini:", + "viewsourcetext": "Pian kawa maniringi wan manyalin asal-mula tungkaran ngini.", "viewyourtext": "Pian kawa maniringi wan salain asalmula matan '''babakan pian''' ka tungkaran ngini:", "protectedinterface": "Tungkaran ini manyadiakan naskah antarmuha gasan parangkat lunak, wan dilindungi hagan mancagah tasalah puruk.", "editinginterface": "'''Paringatan:''' Pian mambabak sabuting tungkaran nang dipuruk hagan manyadiakan naskah antarmuha gasan parangkat lunak.\nPaubahan ka tungkaran ngini akan bapangaruh matan tampaian antarmuha gasan pamakai lain.\nGasan tarjamahan, muhun pakai [https://translatewiki.net/wiki/Main_Page?setlang=bjn translatewiki.net], rangka gawian palokalan MediaWiki.", @@ -317,10 +317,12 @@ "userlogin-yourname-ph": "Masukakan ngaran pamakai Pian", "yourpassword": "Katasunduk:", "userlogin-yourpassword": "Kata sandi", + "userlogin-yourpassword-ph": "Masukakan kata sandi", "createacct-yourpassword-ph": "Masukakan kata sandi", "yourpasswordagain": "Katik pulang katasunduk:", "createacct-yourpasswordagain": "Konfirmasi kata sandi", "createacct-yourpasswordagain-ph": "Masukakan pulang kata sandi", + "userlogin-remembermypassword": "Biarakan ulun tatap babuat", "yourdomainname": "Domain Pian:", "password-change-forbidden": "Pian kada kawa ma-ubah kata sunduk pada wiki ngini.", "externaldberror": "Ada kasalahan apakah kacucukan basis data atawa Pian kada bulih mamutakhirakan akun luar.", @@ -329,7 +331,11 @@ "logout": "Kaluar", "userlogout": "Kaluar", "notloggedin": "Balum babuat log", + "userlogin-noaccount": "Balum baisi akun?", + "userlogin-joinproject": "Gabung {{SITENAME}}", "createaccount": "Ulah akun", + "userlogin-resetpassword-link": "Lupa kata sandi?", + "userlogin-helplink2": "Patulung babuat log", "createacct-emailoptional": "Alamat surél/email (bagusnya diisi)", "createacct-email-ph": "Masukakan alamat email Pian", "createaccountmail": "Malalui suril", @@ -384,6 +390,7 @@ "loginlanguagelabel": "Basa: $1", "suspicious-userlogout": "Pamintaan Pian hagan kaluar log kada ditarima marga nangkaya dikirim matan panjalajah web rakai atawa tatangkap proxy.", "pt-login": "Babuat log", + "pt-login-button": "Babuat log", "pt-createaccount": "Ulah akun", "pt-userlogout": "Kaluar", "php-mail-error-unknown": "Kasalahan kada dipinandui dalam pungsi surat () PHP", @@ -508,7 +515,7 @@ "semiprotectedpagewarning": "'''Catatan:''' Tungkaran ngini sudah dilindungi nang akibatnya pamakai tadaptar haja nang kawa mambabak.\nLog masuk pauncitnya disadiakan di bawah gasan rujukan:", "cascadeprotectedwarning": "'''Paringatan:''' Tungkaran ngini sudah dilindungi nang akibatnya pamakai awan hak istimiwa pambakal haja nang kawa mambabak, sualnya ngini tamasuk dalam baumpat parlindungan barénténg {{PLURAL:$1|tungkaran|tutungkaran}}:", "titleprotectedwarning": "'''Paringatan: Tungkaran ngini sudah dilindungi nang akibatnya [[Special:ListGroupRights|hak khas]] diparluakan hagan maulah ngini.'''\nLog masuk pauncitnya disadiakan di bawah gasan rujukan:", - "templatesused": "{{PLURAL:$1|Citakan|Citakan}} nang digunakan di tungkaran ngini:", + "templatesused": "{{PLURAL:$1|Citakan|Citakan}} nang dipakai di tungkaran ngini:", "templatesusedpreview": "{{PLURAL:$1|Citakan|Citakan}} nang digunakan di titilikan ngini:", "templatesusedsection": "{{PLURAL:$1|Citakan|Cicitakan}} nang diguna'akan di hagian ini:", "template-protected": "(dilindungi)", @@ -522,7 +529,7 @@ "permissionserrorstext": "Pian kada baisi ijin gasan malakuakan itu, karana {{PLURAL:$1|alasan|alasan}} ini:", "permissionserrorstext-withaction": "Pian kada baisi ijin gasan $2, karana {{PLURAL:$1|alasan|alasan}} ini:", "recreate-moveddeleted-warn": "'''Paringatan: Pian maulah pulang sabuah tungkaran nang sabalumnya dihapus.'''\n\nPian partimbangakan dahulu sasuaikah hagan manarusakan pambabakan tungkaran ini.\nLog pahapusan wan paugahan gasan tungkaran ini disadiakan di sia:", - "moveddeleted-notice": "Tungkaran ini sudah dihapus.\nLog pahapusan wan paugahan gasan tungkaran ini disadiakan di bawah ini gasan rujukan.", + "moveddeleted-notice": "Tungkaran ini sudah dihapus.\nLog pahapusan, palindungan, wan pamindahan matan tungkaran itu tasadia di bawah ini sabagai rujukan.", "log-fulllog": "Tiringi samunyaan log", "edit-hook-aborted": "Babakan ditinggalakan ulih kakait parser.\nIni kadada panjalasan.", "edit-gone-missing": "Kada kawa mamutakhirakan tungkaran ini.\nIni cungul pinanya sudah tahapus.", @@ -561,7 +568,7 @@ "currentrev": "Ralatan pahabisannya", "currentrev-asof": "Ralatan pahanyarnya pada $1", "revisionasof": "Ralatan matan $1", - "revision-info": "Ralatan pada $1 ulih $2", + "revision-info": "Ralatan par $1 ulih {{GENDER:$6|$2}}$7", "previousrevision": "←Ralatan talawas", "nextrevision": "Ralatan salanjutnya→", "currentrevisionlink": "Ralatan wayahini", @@ -571,10 +578,10 @@ "page_first": "Panambaian", "page_last": "Pauncitan", "histlegend": "Pilihan mananding: tandai kutak-kutak radiu ralatan-ralatan nang handak ditanding wan picik enter atawa picikan di bawah.
Legend: '''({{int:cur}})''' =lainnya awan ralatan pahanyarnya, '''({{int:last}})''' = lainnya awan ralatan sabalumnya, '''{{int:minoreditletter}}''' = babakan sapalih.", - "history-fieldset-title": "Tangadahi halam", + "history-fieldset-title": "Ralatan nang disaring", "history-show-deleted": "Nang dihapus haja", - "histfirst": "Palawasnya", - "histlast": "Pahanyarnya", + "histfirst": "palawasnya", + "histlast": "pahanyarnya", "historysize": "($1 {{PLURAL:$1|bita|bibita}})", "historyempty": "(kusung)", "history-feed-title": "Ralatan halam", @@ -665,7 +672,7 @@ "mergelog": "Log panggabungan", "revertmerge": "Walang panggabungan", "mergelogpagetext": "Di bawah adalah daptar nang paling hanyar panggabungan matan sabuah tungkaran halam ka dalam nang lain.", - "history-title": "Ralatan halam matan ''$1''", + "history-title": "Sajarah ralatan matan \"$1\"", "difference-title": "$1: Pabidaan ralatan", "difference-multipage": "(Nang balain antar tungkaran-tungkaran)", "lineno": "Baris $1:", @@ -673,6 +680,7 @@ "showhideselectedversions": "Tampaiakan/sungkupakan ralatan-ralatan", "editundo": "walangi", "diff-empty": "(Kadada bida)", + "diff-multi-sameuser": "({{PLURAL:$1|$1 ralatan antara}} ulih pamakai nang sama kada ditampaiakan)", "diff-multi-manyusers": "({{PLURAL:$1|Asa ralatan tangah|$1 raralatan tangah}} ulih labih pada $2 {{PLURAL:$2|pamuruk|papamuruk}} kada ditampaiakan)", "searchresults": "Kulihan panggagaian", "searchresults-title": "Kulihan gagai gasan \"$1\"", @@ -699,6 +707,7 @@ "search-result-category-size": "{{PLURAL:$1|1 angguta|$1 aangguta}} ({{PLURAL:$2|1 subtumbung|$2 subtutumbung}}, {{PLURAL:$3|1 barakas|$3 babarakas}})", "search-redirect": "(Diugahakan matan $1)", "search-section": "(hagian $1)", + "search-file-match": "(rasuk lawan isi barakas)", "search-suggest": "Nginikah maksud Pian: $1", "search-interwiki-caption": "Dingsanak rangka gawian", "search-interwiki-default": "Kulihan $1", @@ -946,6 +955,7 @@ "recentchanges": "Paubahan pahanyarnya", "recentchanges-legend": "Pilihan paubahan pahanyarnya", "recentchanges-summary": "Jajak paubahan wiki pahanyarnya pada tungkaran ngini", + "recentchanges-noresult": "Kadada paubahan dalam rantang waktu ngini nang rasuk lawan syarat.", "recentchanges-feed-description": "Susuri paubahan pahanyarnya dalam wiki di kitihan ini", "recentchanges-label-newpage": "Babakan ngini maulah sabuting tungkaran hanyar", "recentchanges-label-minor": "Ngini sabuting babakan sapalih", @@ -954,18 +964,23 @@ "recentchanges-label-plusminus": "Paubahan ukuran tungkaran dalam bita", "recentchanges-legend-heading": "Katarangan:", "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (tiringi jua [[Special:NewPages|daptar tungkaran hanyar]])", - "rcnotefrom": "Di bawah ngini paubahan tumatan '''$2''' (ditampaiakan sampai '''$1''' paubahan)", + "rcnotefrom": "Di bawah ngini adalah {{PLURAL:$5|paubahan}} tumatan $3, $4 (ditampaiakan sampai $1 paubahan).", "rclistfrom": "Tampaiakan paubahan pahanyarnya matan $3 $2", "rcshowhideminor": "$1 pambabakan sapalih", + "rcshowhideminor-show": "Tampaiakan", "rcshowhideminor-hide": "Sungkupakan", "rcshowhidebots": "$1 bot", "rcshowhidebots-show": "Tampaiakan", + "rcshowhidebots-hide": "Sungkupakan", "rcshowhideliu": "$1 pamakai tadaptar", + "rcshowhideliu-show": "Tampaiakan", "rcshowhideliu-hide": "Sungkupakan", "rcshowhideanons": "$1 pamakai kada bangaran", + "rcshowhideanons-show": "Tampaiakan", "rcshowhideanons-hide": "Sungkupakan", "rcshowhidepatr": "$1 babakan ta'awasi", "rcshowhidemine": "$1 babakan ulun", + "rcshowhidemine-show": "Tampaiakan", "rcshowhidemine-hide": "Sungkupakan", "rclinks": "Tampaiakan $1 paubahan pahanyarnya dalam $2 hari tauncit", "diff": "bida", @@ -1311,6 +1326,7 @@ "querypage-disabled": "Tungkaran istimiwa ngini dikada-kawakan gasan alasan ginawi.", "booksources": "Buku bamula", "booksources-search-legend": "Gagai gasan buku asal mula", + "booksources-search": "Gagai", "booksources-text": "Di bawah adalah sabuah daptar tautan ka situs lain nang manjual bubuku hanyar wan bakas, wan jua baisi panjalasan labih pasal bubuku nang Pian ugai:", "booksources-invalid-isbn": "ISBN nang dibari mancungul kada sah; pariksa kalua-ai tasalah marekap matan asal-mula aslinya.", "specialloguserlabel": "Pamakai:", @@ -1466,6 +1482,7 @@ "delete-warning-toobig": "Tungkaran ngini baisi halam babakan ganal, labih pada $1 {{PLURAL:$1|ralatan|raralatan}}.\nMahapus ngini kawa mangaruhi databasis oparasi {{SITENAME}};\njalanakan awan ba-a-awas.", "rollback": "Gulung bulik babakan", "rollbacklink": "bulikakan", + "rollbacklinkcount": "bulikakan $1 {{PLURAL:$1|babakan}}", "rollbackfailed": "Guling-bulik luput", "cantrollback": "Kada kawa mambalikakan babakan;\npanyumbang tauncit adalah asa-asanya panulis tungkaran ngini.", "alreadyrolled": "Kada kawa malakukan pambulikan ka ralatan tauncit [[:$1]] ulih [[User:$2|$2]] ([[User talk:$2|pandir]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\npamuruk lain sudah mambabak atawa malakukan pambulikan lawan tungkaran ini.\n\nBabakan tauncit dilakukan ulih [[User:$3|$3]] ([[User talk:$3|pandir]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).", @@ -1575,9 +1592,9 @@ "contributions-title": "Sumbangan pamakai gasan $1", "mycontris": "Sumbangan", "anoncontribs": "Sumbangan", - "contribsub2": "Gasan $1 ($2)", + "contribsub2": "Gasan {{GENDER:$3|$1}} ($2)", "nocontribs": "Kadada paubahan nang rasuk lawan syarat itu.", - "uctop": " atas", + "uctop": "wayah ini", "month": "Matan bulan (wan sabalumnya):", "year": "Matan tahun (wan sabalumnya):", "sp-contributions-newbies": "Tampaiakan sumbangan papamakai hanyar haja", @@ -1594,6 +1611,7 @@ "sp-contributions-search": "Gagai gasan sumbangan", "sp-contributions-username": "Alamat IP atawa ngaran-pamakai:", "sp-contributions-toponly": "Tampaiakan wastu ralatan nang paling atas (pauncitnya)", + "sp-contributions-newonly": "Hanya tampaiakan babakan nang barupa paulahan tungkaran", "sp-contributions-submit": "Gagai", "whatlinkshere": "Tautan apa di sia", "whatlinkshere-title": "Tungkaran-tungkaran nang batautan ka ''$1''", @@ -2229,8 +2247,8 @@ "revdelete-uname-unhid": "ngaran-pamuruk kada tasungkup", "revdelete-restricted": "Talamar pambatasan hagan pambakal-pambakal", "revdelete-unrestricted": "Buang pambatasan gasan pambakal-pambakal", - "logentry-move-move": "$1 mamindahakan tungkaran $3 ka $4", - "logentry-move-move-noredirect": "$1 diugah tungkaran $3 ka $4 awan-kada maninggalakan sabuah paugahan", + "logentry-move-move": "$1 {{GENDER:$2|mamindahakan}} tungkaran $3 ka $4", + "logentry-move-move-noredirect": "$1 {{GENDER:$2|mamindahakan}} tungkaran $3 ka $4 kada pakai maulah paugahan", "logentry-move-move_redir": "$1 diugah tungkaran $3 ka $4 lung paugahan", "logentry-move-move_redir-noredirect": "$1 diugah tungkaran $3 ka $4 lung sabuah paugahan awan-kada maninggalakan sabuah paugahan", "logentry-patrol-patrol": "$1 diciri'i ralatan $4 matan tungkaran $3 taawasi", @@ -2239,6 +2257,7 @@ "logentry-newusers-create": "$1 {{GENDER:$2|maulah}} akun pamakai", "logentry-newusers-create2": "$1 ma-ulah sabuting akun pamakai $3", "logentry-newusers-autocreate": "Akun $1 utumatis diulah", + "logentry-upload-upload": "$1 {{GENDER:$2|ma-unggah}} $3", "rightsnone": "(kadada)", "feedback-adding": "Manambahi kitihanbalik ka tungkaran...", "feedback-bugcheck": "Harat! hanyar dipariksa bahwasa ngini lainan salah asa [$1 bug nang dipinandui].", diff --git a/languages/i18n/bn.json b/languages/i18n/bn.json index c48df85b7c..725b8e9bfa 100644 --- a/languages/i18n/bn.json +++ b/languages/i18n/bn.json @@ -757,7 +757,7 @@ "edit-gone-missing": "পাতাটি হালনাগাদ হয়নি।\nসম্ভবতঃ পাতাটি মুছে ফেলা হয়েছে।", "edit-conflict": "সম্পাদনা সংঘাত।", "edit-no-change": "আপনার সম্পাদনাটি উপেক্ষা করা হয়েছে, কারণ লেখাতে কোনো পরিবর্তন করা হয়নি।", - "edit-slots-cannot-add": "নিচের {{PLURAL:$1|পাতাটি|পাতাসমূহ}} এখানে সমর্থিত নয়: $2।", + "edit-slots-cannot-add": "নিচের {{PLURAL:$1|স্লটটি|স্লটসমূহ}} এখানে সমর্থিত নয়: $2।", "edit-slots-cannot-remove": "নিচের {{PLURAL:$1|স্লট|স্লটসমূহ}} প্রয়োজন এবং বাদ দেওয়া যাবে না: $2।", "edit-slots-missing": "নিচের {{PLURAL:$1|স্লট|স্লটসমূহ}} পাওয়া যায়নি: $2।", "postedit-confirmation-created": "পাতাটি তৈরি করা হয়েছে।", @@ -1351,34 +1351,34 @@ "action-changetags": "নির্দিষ্ট সংস্করণ এবং লগ ভুক্তিগুলিতে যথেচ্ছভাবে ট্যাগ সংযোজন ও অপসারণ করা", "action-deletechangetags": "ডাটাবেজ থেকে ট্যাগ অপসরণ করার", "action-purge": "এই পাতাটি শোধন করুন", - "action-apihighlimits": "API কোয়েরি হিসাবে আরও উচ্চ লিমিট ব্যবহার করুন", - "action-autoconfirmed": "আইপি-ভিত্তিক রেট সীমানা দ্বারা প্রভাবিত নয়।", - "action-bigdelete": "বিশাল ইতিহাস সম্বলিত পাতা মুছে ফেলো", - "action-blockemail": "ই-মেইল পাঠাতে কোনো ব্যবহারকারীকে বাঁধা দাও", - "action-bot": "সয়ংক্রিয় পদ্ধতি হিসাবে চিহ্নিত করণ", + "action-apihighlimits": "API কোয়েরিতে আরো উচ্চতর সীমা ব্যবহার করার", + "action-autoconfirmed": "আইপি-ভিত্তিক রেট সীমার দ্বারা প্রভাবিত না হবার", + "action-bigdelete": "বিশাল ইতিহাস সম্বলিত পাতা অপসারণ করার", + "action-blockemail": "কোনো ব্যবহারকারীকে ই-মেইল পাঠানো থেকে বাধা দেয়ার", + "action-bot": "স্বয়ংক্রিয় পদ্ধতি হিসাবে চিহ্নিতকরণ করার", "action-editprotected": "\"{{int:protect-level-sysop}}\" হিসেবে সুরক্ষিত পাতা সম্পাদনা করার", "action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" হিসেবে সুরক্ষিত পাতা সম্পাদনা করার", - "action-editinterface": "ব্যবহারকারী ইন্টারফেস সম্পাদনা", - "action-editusercss": "অন্য ব্যবহারকারীগণের CSS ফাইল সম্পাদনা", - "action-edituserjson": "অন্য ব্যবহারকারীগণের JSON ফাইল সম্পাদনা", - "action-edituserjs": "অন্য ব্যবহারকারীগণের জাভাস্ক্রিপ্ট ফাইল সম্পাদনা", + "action-editinterface": "ব্যবহারকারী ইন্টারফেস সম্পাদনা করার", + "action-editusercss": "অন্য ব্যবহারকারীগণের CSS ফাইল সম্পাদনা করার", + "action-edituserjson": "অন্য ব্যবহারকারীগণের JSON ফাইল সম্পাদনা করার", + "action-edituserjs": "অন্য ব্যবহারকারীগণের জাভাস্ক্রিপ্ট ফাইল সম্পাদনা করার", "action-editsitecss": "সাইটব্যাপী CSS সম্পাদনা করার", "action-editsitejson": "সাইটব্যাপী JSON সম্পাদনা করার", "action-editsitejs": "সাইটব্যাপী জাভাস্ক্রিপ্ট সম্পাদনা করার", "action-editmyusercss": "স্ব ব্যবহারকারীর CSS ফাইল সম্পাদনা করার", - "action-editmyuserjson": "আপনার নিজস্ব ব্যবহারকারী JSON ফাইল সম্পাদনা করা", - "action-editmyuserjs": "আপনার নিজস্ব ব্যবহারকারী জাভাস্ক্রিপ্ট ফাইল সম্পাদনা করুন", - "action-viewsuppressed": "যেকোন ব্যবহারকারীর কাছ থেকে লুকানো সংস্করণগুলি দেখুন", - "action-hideuser": "ব্যবহারকারীকে বাধা দিন, এবং সর্বসাধারণের দৃষ্টিসীমা থেকে সরিয়ে নিন", - "action-ipblock-exempt": "আইপি বাধা, স্বয়ংক্রিয় বাধা ও পরিসীমার বাধা এড়ানো", + "action-editmyuserjson": "স্ব ব্যবহারকারী JSON ফাইল সম্পাদনা করার", + "action-editmyuserjs": "স্ব ব্যবহারকারী জাভাস্ক্রিপ্ট ফাইল সম্পাদনা করার", + "action-viewsuppressed": "যেকোন ব্যবহারকারীর কাছ থেকে লুকানো সংস্করণগুলি দেখার", + "action-hideuser": "ব্যবহারকারীকে বাধা দেয়ার, এবং তা সর্বসাধারণের দৃষ্টিসীমা থেকে লুকানোর", + "action-ipblock-exempt": "আইপি বাধা, স্বয়ংক্রিয় বাধা ও পরিসীমার বাধা এড়ানোর", "action-unblockself": "নিজেকে বাধামুক্ত করার", - "action-noratelimit": "রেট লিমিটের ভিত্তিতে পরিবর্তন হবে না", - "action-reupload-own": "নিজের দ্বারা আপলোডকৃত ফাইল যা ইতিমধ্যেই বিদ্যমান, সেটি মুছে পুনরায় নতুন করে আপলোড করা", - "action-nominornewtalk": "বার্তা লেখার মত আলাপ পাতায় কোনো অনুল্লেখ্য সম্পাদনা নেই", - "action-markbotedits": "ফেরত আনা সম্পাদনাসমূহকে বট সম্পাদনা হিসেবে চিহ্নিত করে", - "action-patrolmarks": "সাম্প্রতিক পরিবর্তনের পরীক্ষিত চিহ্ন দেখাও", - "action-override-export-depth": "লিংকসহ পাতা যার গভীরতা ৫ এর মধ্যে সেগুলো রপ্তানি করুন", - "action-suppressredirect": "পাতা স্থানান্তরের সময় মূল পাতা থেকে পুনর্নির্দেশ তৈরী করছে না", + "action-noratelimit": "রেট সীমার দ্বারা প্রভাবিত না হবার", + "action-reupload-own": "নিজের দ্বারা আপলোডকৃত ফাইল পুনর্লিখনের", + "action-nominornewtalk": "আলোচনার পৃষ্ঠাগুলিতে অনুল্লেখ্য সম্পাদনা নেই নতুন বার্তা প্রম্পট ট্রিগার করার", + "action-markbotedits": "ফেরত আনা সম্পাদনাসমূহকে বট সম্পাদনা হিসেবে চিহ্নিত করার", + "action-patrolmarks": "সাম্প্রতিক পরিবর্তনের পরীক্ষণের চিহ্ন দেখার", + "action-override-export-depth": "৫-এর গভীরতা পর্যন্ত সংযোগকৃত পাতাসহ পাতাগুলি রপ্তানি করার", + "action-suppressredirect": "পাতা স্থানান্তর করার সময় উৎস পাতা থেকে পুনর্নির্দেশ তৈরী করার", "nchanges": "$1টি {{PLURAL:$1|পরিবর্তন}}", "enhancedrc-since-last-visit": "{{PLURAL:$1|সর্বশেষ প্রদর্শনের পর}} $1টি", "enhancedrc-history": "ইতিহাস", @@ -1490,9 +1490,9 @@ "rcfilters-filter-watchlist-notwatched-description": "আপনার নজরতালিকায় থাকা পাতাগুলি ব্যতীয় সবকিছু।", "rcfilters-filtergroup-watchlistactivity": "নজরতালিকার কার্যক্রম", "rcfilters-filter-watchlistactivity-unseen-label": "অদেখা পরিবর্তন", - "rcfilters-filter-watchlistactivity-unseen-description": "আপনার নজরতালিকায় থাকা পাতাগুলিতে পরিবর্তন যেগুলিতে আপনি সম্পাদনা করার পর আর যাননি।", + "rcfilters-filter-watchlistactivity-unseen-description": "পাতাসমূহের পরিবর্তন ঘটার পর থেকে আপনি যেসব পাতা পরিদর্শন করেননি।", "rcfilters-filter-watchlistactivity-seen-label": "দেখা পরিবর্তন", - "rcfilters-filter-watchlistactivity-seen-description": "আপনার নজরতালিকায় থাকা পাতাগুলিতে পরিবর্তন যেগুলিতে আপনি সম্পাদনা করার পর আর যাননি।", + "rcfilters-filter-watchlistactivity-seen-description": "পাতাসমূহের পরিবর্তন ঘটার পর থেকে আপনি যেসব পাতা পরিদর্শন করেছেন।", "rcfilters-filtergroup-changetype": "পরিবর্তনের ধরন", "rcfilters-filter-pageedits-label": "পাতার সম্পাদনা", "rcfilters-filter-pageedits-description": "উইকি বিষয়বস্তু, আলোচনা, বিষয়শ্রেণীর বিবরণ... ইত্যাদিতে সম্পাদনা", @@ -1529,10 +1529,10 @@ "rcfilters-preference-help": "ছাঁকনিগুলি অনুসন্ধান বা আলোকপাতকরণ কার্যকারিতা ছাড়া সাম্প্রতিক পরিবর্তন লোড করে", "rcfilters-watchlist-preference-label": "জাভাস্ক্রিপ্টহীন ইন্টারফেস ব্যবহার করুন", "rcfilters-watchlist-preference-help": "ছাঁকনি অনুসন্ধান বা আলোকপাতকরণ বৈশিষ্ট্য ছাড়া নজরতালিকা লোড করে।", - "rcfilters-filter-showlinkedfrom-label": "লিংক করা এমন পাতাগুলোর পরিবর্তন দেখান", - "rcfilters-filter-showlinkedfrom-option-label": "নির্বাচিত পাতা থেকে পাতা লিংক করা", - "rcfilters-filter-showlinkedto-label": "পাতা লিংক করা এমন পাতাসমূহের পরিবর্তন দেখান", - "rcfilters-filter-showlinkedto-option-label": "নির্বাচিত পাতা থেকে পাতা লিংক করা", + "rcfilters-filter-showlinkedfrom-label": "এটি থেকে সংযোগকারী পাতাসমূহের পরিবর্তন দেখান", + "rcfilters-filter-showlinkedfrom-option-label": "নির্বাচিত পাতাটি থেকে সংযোগকারী পাতাসমূহ", + "rcfilters-filter-showlinkedto-label": "এটিতে সংযোগকারী পাতাসমূহের পরিবর্তন দেখান", + "rcfilters-filter-showlinkedto-option-label": "নির্বাচিত পাতাটিতে সংযোগকারী পাতাসমূহ", "rcfilters-target-page-placeholder": "একটি পাতার নাম (বা বিষয়শ্রেণী) লিখুন", "rcnotefrom": "$2টা থেকে সংঘটিত পরিবর্তনগুলি (সর্বোচ্চ $1টি দেখানো হয়েছে)।", "rclistfromreset": "তারিখ নির্বাচন পুনঃস্থাপন করুন", @@ -1660,7 +1660,7 @@ "uploaded-script-svg": "আপলোডকৃত SVG ফাইলে স্ক্রিপ্টযোগ্য উপাদান \"$1\" পাওয়া গেছে।", "uploaded-hostile-svg": "আপলোড করা SVG ফাইলের শৈলী উপাদানে অনিরাপদ সিএসএস পাওয়া গেছে।", "uploaded-event-handler-on-svg": "এসভিজি ফাইলের জন্য $1=\"$2\" ইভেন্ট-হ্যান্ডলার বৈশিষ্ট্যটি নির্ধারণ করা অনুমোদিত নয়।", - "uploaded-href-attribute-svg": "এসভিজি ফাইলের href বৈশিষ্ট্যগুলির জন্য কেবলমাত্র http:// বা https:// লক্ষ্যগুলি অনুমোদিত। অন্য বিষয় যেমন, , শুধুমাত্র উপাত্ত ও বৈশিষ্ঠগুলো গ্রহণযোগ্য। <$1 $2=\"$3\"> পাওয়া গেছে।", + "uploaded-href-attribute-svg": " উপাদান শুধুমাত্র উপাত্তে সংযোগ (href) করা যাবে: (এম্বেড করা ফাইল), http:// বা https://, বা খণ্ডিত (#, একই-নথি) লক্ষ্যগুলি। অন্যান্য উপাদানের জন্য, যেমন , কেবলমাত্র উপাত্ত: ও খণ্ড অনুমোদিত। আপনার এসভিজি রপ্তানি করার সময় ছবি এম্বেড করার চেষ্টা করুন। <$1 $2=\"$3\"> পাওয়া গেছে।", "uploaded-href-unsafe-target-svg": "অনিরাপদ উপাত্তে href পাওয়া গেছে: আপলোডকৃত SVG ফাইলে URI লক্ষ্য ছিল <$1 $2=\"$3\">।", "uploaded-animate-svg": "\"animate\" ট্যাগটি পাওয়া গেছে যা আপলোডকৃত এসভিজি ফাইলের <$1 $2=\"$3\"> - এই \"from\" অ্যাট্রিবিউটটি ব্যবহার করে href পরিবর্তন করতে পারে।", "uploaded-setting-event-handler-svg": "ইভেন্ট-হ্যান্ডলার অ্যাট্রিবিউট নির্ধারণ করতে বাধা দেওয়া হয়েছে। আপলোডকৃত এসভিজি ফাইলে <$1 $2=\"$3\"> খুঁজে পাওয়া গেছে।", @@ -2594,8 +2594,8 @@ "blocklogpage": "বাধা দানের লগ", "blocklog-showlog": "এই ব্যবহারকারীকে পূর্বেও বাধা প্রদান করা হয়েছিলো।\nতথ্যসূত্র হিসেবে তাই পূর্বের বাধাদানের লগটি নিচে প্রদর্শন করা হচ্ছে:", "blocklog-showsuppresslog": "এই ব্যবহারকারীকে পূর্বেও বাধা প্রদান ও লুকানো হয়েছিলো।\nতথ্যসূত্র হিসেবে তাই পূর্বের অপসারণ লগটি নিচে প্রদর্শন করা হচ্ছে:", - "blocklogentry": "[[$1]] কে $2 মেয়াদের জন্য বাধাদান করেছেন $3", - "reblock-logentry": "[[$1]] এর ব্লক সেটিং পরিবর্তন করা হয়েছে যেটি শেষ হবে $2 $3 সময়ে", + "blocklogentry": "[[$1]] কে $2 সময়ের জন্য বাধাদান করেছেন $3", + "reblock-logentry": "[[$1]]-এর বাধাদান সেটিং পরিবর্তন করেছেন যেটি শেষ হবার মেয়াদ $2 $3", "blocklogtext": "এটি ব্যবহারকারীদেরকে বাধা দানের বা বাধা তুলে নেওয়ার লগ।\nস্বয়ংক্রিয়ভাবে বাধাদানকৃত আইপি ঠিকানাগুলি এখানে তালিকাবদ্ধ করা হয়নি।\nবর্তমানে সক্রিয় নিষিদ্ধকরণ ও বাধাদানের তালিকার জন্য [[Special:BlockList| বাধাদান তালিকা]] দেখুন।", "unblocklogentry": "$1-এর উপর বাধা তুলে নেয়া হয়েছে", "block-log-flags-anononly": "কেবল বেনামী ব্যবহারকারীরা", @@ -2610,7 +2610,7 @@ "ipb_expiry_old": "মেয়াদোত্তীর্ণের সময় অতীত হয়েছে।", "ipb_expiry_temp": "লুকানো ব্যবহারকারীনাম বাধা চিরস্থায়ী হতে হবে।", "ipb_hide_invalid": "এই অ্যাকাউন্ট বাধা দেয়া সম্ভব নয়; এটি {{PLURAL:$1|একের অধিক|$1টি}} সম্পাদনা করেছে।", - "ipb_hide_partial": "লুকায়িত ব্যবহারকারী নামের বাধাদান অবশ্যই সাইটওয়াইড হতে হবে।", + "ipb_hide_partial": "লুকানো ব্যবহারকারী নামের বাধাদান অবশ্যই সাইটব্যপী হতে হবে।", "ipb_already_blocked": "\"$1\" ইতিমধ্যে বাধাপ্রাপ্ত।", "ipb-needreblock": "$1 ইতিমধ্যেই বাধাপ্রাপ্ত আছেন। আপনি কি সেটিংস পরিবর্তন করতে চান?", "ipb-otherblocks-header": "অন্যান্য {{PLURAL:$1|বাধা|বাধাসমূহ}}", @@ -3502,9 +3502,15 @@ "revdelete-unrestricted": "এই সীমাবদ্ধতা প্রশাসকের ক্ষেত্রে তুলে নাও", "logentry-block-block": "$1 {{GENDER:$4|$3}} কে $5 মেয়াদের জন্য {{GENDER:$2|বাধাদান}} করেছেন $6", "logentry-block-unblock": "$1 {{GENDER:$4|$3}}-এর উপর থেকে বাধা তুলে {{GENDER:$2|নিয়েছেন}}", - "logentry-block-reblock": "$1 {{GENDER:$4|$3}}-এর জন্য বাধাদান সেটিং $5 সময়ের জন্য {{GENDER:$2|পরিবর্তন}} করেছেন $6", + "logentry-block-reblock": "$1 {{GENDER:$4|$3}}-এর বাধাদান সেটিং {{GENDER:$2|পরিবর্তন করেছেন}} যেটি শেষ হবার মেয়াদ $5 $6", + "logentry-partialblock-block-page": "$2 {{PLURAL:$1|পাতাটি|পাতাগুলি}}", + "logentry-partialblock-block-ns": "$2 {{PLURAL:$1|নামস্থানটি|নামস্থানগুলি}}", + "logentry-partialblock-block": "$1 {{GENDER:$4|$3}} কে $7 সম্পাদনা করা থেকে $5 সময়ের জন্য {{GENDER:$2|বাধাদান করেছেন}} $6", + "logentry-partialblock-reblock": "$1 $7তে সম্পাদনা করা প্রতিরোধ করে {{GENDER:$4|$3}}-এর বাধাদান সেটিং {{GENDER:$2|পরিবর্তন করেছেন}} যেটি শেষ হবার মেয়াদ $5 $6", + "logentry-non-editing-block-block": "$1 {{GENDER:$4|$3}} কে সম্পাদনা-ছাড়া নির্দিষ্ট কর্ম করা থেকে $5 সময়ের জন্য {{GENDER:$2|বাধাদান করেছেন}} $6", + "logentry-non-editing-block-reblock": "$1 সম্পাদনা-ছাড়া নির্দিষ্ট কর্মের জন্য {{GENDER:$4|$3}}-এর বাধাদান সেটিং {{GENDER:$2|পরিবর্তন করেছেন}} যেটি শেষ হবার মেয়াদ $5 $6", "logentry-suppress-block": "$1 {{GENDER:$4|$3}} কে $5 মেয়াদের জন্য {{GENDER:$2|বাধাদান}} করেছেন $6", - "logentry-suppress-reblock": "$1 {{GENDER:$4|$3}}-এর জন্য বাধাদান সেটিং $5 সময়ের জন্য {{GENDER:$2|পরিবর্তন}} করেছেন $6", + "logentry-suppress-reblock": "$1 {{GENDER:$4|$3}}-এর বাধাদান সেটিং {{GENDER:$2|পরিবর্তন করেছেন}} যেটি শেষ হবার মেয়াদ $5 $6", "logentry-import-upload": "$1 ফাইল আপলোড দ্বারা $3 {{GENDER:$2|আমদানি করেছেন}}", "logentry-import-upload-details": "$1 ফাইল আপলোড দ্বারা $3 {{GENDER:$2|আমদানি করেছেন}} ($4টি {{PLURAL:$4|সংশোধন}})", "logentry-import-interwiki": "$1 অন্য একটি উইকিতে থেকে $3 {{GENDER:$2|আমদানি করেছে}}", diff --git a/languages/i18n/ce.json b/languages/i18n/ce.json index 55910ac7cc..388e5839db 100644 --- a/languages/i18n/ce.json +++ b/languages/i18n/ce.json @@ -2636,6 +2636,7 @@ "tag-mw-removed-redirect": "дӀаяьккхина дӀасхьажорг", "tag-mw-changed-redirect-target": "хийцаран бахьна ду дӀасахьажорг", "tag-mw-blank": "цӀанъяр", + "tag-mw-replace": "хийцар", "tag-mw-rollback": "Юхаяккха", "tag-mw-undo": "юхаяккхар", "tags-title": "Билгалонаш", diff --git a/languages/i18n/da.json b/languages/i18n/da.json index f529959d7f..53b0054f43 100644 --- a/languages/i18n/da.json +++ b/languages/i18n/da.json @@ -512,7 +512,7 @@ "badretype": "De indtastede adgangskoder er ikke ens.", "usernameinprogress": "En oprettelse af konto for dette brugernavn er allerede i gang.\nVent venligst.", "userexists": "Det brugernavn, du har valgt, er allerede i brug.\nVælg venligst et andet brugernavn.", - "createacct-normalization": "Dit brugernavn vil blive ændret til «$2» på grund af tekniske begrænsninger.", + "createacct-normalization": "Dit brugernavn vil blive ændret til \"$2\" på grund af tekniske begrænsninger.", "loginerror": "Logon mislykket", "createacct-error": "Fejl ved kontooprettelse", "createaccounterror": "Kunne ikke oprette brugerkonto: $1", @@ -826,7 +826,7 @@ "content-json-empty-array": "Tomt matrix", "deprecated-self-close-category": "Sider, der bruger ugyldige, selvlukkende HTML-tags", "deprecated-self-close-category-desc": "Siden bruger ugyldige selvlukkende HTML tags, som <b/> eller <span/>. De vil snart blive ændret i overensstemmelse med HTML5-specifikationen, så de ikke kan bruges i wikitext.", - "duplicate-args-warning": "Advarsel: [[:$1]] kaldes [[:$2]] med flere end en værdi for \"$3\"-parameteren. Bare den sidst angitte værdien vil bruges.", + "duplicate-args-warning": "Advarsel: [[:$1]] kalder [[:$2]] med mere end en værdi for \"$3\"-parameteren. Kun den sidst angivne værdi vil blive brugt.", "duplicate-args-category": "Sider der bruger samme argument mere end en gang i en skabelon", "duplicate-args-category-desc": "Siden indeholder en skabelon hvor et argument er brugt mere end en gang, som {{foo|bar=1|bar=2}} eller {{foo|bar|1=baz}}.", "expensive-parserfunction-warning": "Advarsel: Der er for mange beregningstunge oversætter-funktionskald på denne side.\n\nDer bør være færre end {{PLURAL:$2|$2 kald}}, lige nu er der {{PLURAL:$1|$1 kald}}.", @@ -837,7 +837,7 @@ "post-expand-template-argument-category": "Sider med udeladte skabelonparametre", "parser-template-loop-warning": "Skabelonløkke fundet: [[$1]]", "template-loop-category": "Sider med skabelonløkker", - "template-loop-category-desc": "Siden indeholder en malløkke, altså en skabelon som kalder sig selv rekursivt.", + "template-loop-category-desc": "Siden indeholder en skabelonløkke, det vil sige en skabelon som kalder sig selv rekursivt.", "parser-template-recursion-depth-warning": "En skabelon er rekursivt inkluderet for mange gange ($1)", "language-converter-depth-warning": "Dybdegrænse for sprogkonvertering overskredet ($1)", "node-count-exceeded-category": "Sider hvor antal noder er overskredet", @@ -872,7 +872,7 @@ "page_first": "Starten", "page_last": "Enden", "histlegend": "Forklaring: (nuværende) = forskel til den nuværende\nversion, (forrige) = forskel til den forrige version, M = mindre ændring", - "history-fieldset-title": "Søg efter versioner", + "history-fieldset-title": "Filtrer versioner", "history-show-deleted": "Kun slettede revisioner", "histfirst": "ældste", "histlast": "nyeste", @@ -1382,6 +1382,8 @@ "action-changetags": "tilføje og fjerne vilkårlige tags for enkelte versioner og logposter", "action-deletechangetags": "slette tags fra databasen", "action-purge": "rense denne side", + "action-bigdelete": "slet sider med store historikker", + "action-blockemail": "bloker en bruger fra at sende e-mails", "action-bot": "blive behandlet som en automatiseret proces", "nchanges": "$1 {{PLURAL:$1|ændring|ændringer}}", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|siden sidste besøg}}", @@ -1436,7 +1438,7 @@ "rcfilters-savedqueries-add-new-title": "Gem nuværende filterindstillinger", "rcfilters-restore-default-filters": "Gendan standardfiltre", "rcfilters-clear-all-filters": "Ryd alle filtre", - "rcfilters-show-new-changes": "Vis seneste ændringer", + "rcfilters-show-new-changes": "Vis seneste ændringer siden $1", "rcfilters-search-placeholder": "Filtrer ændringer (brug menuen eller søg på filternavn)", "rcfilters-invalid-filter": "Ugyldigt filter", "rcfilters-empty-filter": "Ingen aktive filtre. All bidrag vises.", @@ -2228,7 +2230,7 @@ "delete-confirm": "Slet \"$1\"", "delete-legend": "Slet", "historywarning": "Advarsel: Siden du er ved at slette har en historie med $1 {{PLURAL:$1|version|versioner}}:", - "historyaction-submit": "Vis", + "historyaction-submit": "Vis revisioner", "confirmdeletetext": "Du er ved at slette en side sammen med hele dens tilhørende historik.\nBekræft venligst at du virkelig vil gøre dette, at du forstår konsekvenserne, og at du gør det i overensstemmelse med [[{{MediaWiki:Policy-url}}|retningslinjerne]].", "actioncomplete": "Gennemført", "actionfailed": "Handlingen mislykkedes", diff --git a/languages/i18n/de.json b/languages/i18n/de.json index 656d41b9c3..047c448940 100644 --- a/languages/i18n/de.json +++ b/languages/i18n/de.json @@ -96,7 +96,8 @@ "PerfektesChaos", "Kurt Jansson", "McDutchie", - "Johanna Strodt (WMDE)" + "Johanna Strodt (WMDE)", + "Andi-3" ] }, "tog-underline": "Links unterstreichen:", @@ -104,7 +105,7 @@ "tog-hidepatrolled": "Kontrollierte Änderungen in den „Letzten Änderungen“ ausblenden", "tog-newpageshidepatrolled": "Kontrollierte Seiten bei den „Neuen Seiten“ ausblenden", "tog-hidecategorization": "Kategorisierungen von Seiten ausblenden", - "tog-extendwatchlist": "Alle Änderungen in der Beobachtungsliste anzeigen, nicht nur die aktuellsten", + "tog-extendwatchlist": "Alle Änderungen in der Beobachtungsliste anzeigen, nicht nur die letzten", "tog-usenewrc": "Änderungen auf „Letzte Änderungen“ und der Beobachtungsliste nach Seite gruppieren", "tog-numberheadings": "Überschriften automatisch nummerieren", "tog-editondblclick": "Seiten mit Doppelklick bearbeiten", @@ -766,7 +767,7 @@ "previewnote": "'''Dies ist nur eine Vorschau.'''\nDie Seite wurde noch nicht gespeichert!", "continue-editing": "Zum Bearbeitungsfeld gehen", "previewconflict": "Diese Vorschau gibt den Inhalt des oberen Textfeldes wieder. So wird die Seite aussehen, wenn du jetzt speicherst.", - "session_fail_preview": "Entschuldigung! Wir konnten deine Bearbeitung nicht verarbeiten, da Sitzungsdaten verloren gegangen sind.\n\nDu wurdest eventuell abgemeldet. Bitte verifiziere, dass du noch angemeldet bist und versuche es erneut.\nFalls dies nicht funktioniert, versuche dich [[Special:UserLogout|abzumelden]] und anschließend wieder anzumelden und überprüfe, ob dein Browser Cookies von dieser Website akzeptiert.", + "session_fail_preview": "Entschuldigung! Wir konnten deine Bearbeitung nicht verarbeiten, da Sitzungsdaten verloren gegangen sind.\n\nDu wurdest eventuell abgemeldet. Bitte stelle sicher, dass du noch angemeldet bist, und versuche es erneut.\nFalls dies nicht funktioniert, versuche dich [[Special:UserLogout|abzumelden]] und anschließend wieder anzumelden und überprüfe, ob dein Browser Cookies von dieser Website akzeptiert.", "session_fail_preview_html": "Deine Bearbeitung konnte nicht gespeichert werden, da Sitzungsdaten verloren gegangen sind.\n\nDa in {{SITENAME}} das Speichern von reinem HTML aktiviert ist, wurde die Vorschau ausgeblendet, um JavaScript-Attacken vorzubeugen.\n\nBitte versuche es erneut, indem du unter der folgenden Textvorschau nochmals auf „Seite speichern“ klickst.\nSollte das Problem bestehen bleiben, [[Special:UserLogout|melde dich ab]] und danach wieder an. Überprüfe, ob dein Browser Cookies von dieser Website akzeptiert.", "token_suffix_mismatch": "'''Deine Bearbeitung wurde zurückgewiesen, da dein Browser Zeichen im Bearbeiten-Token verstümmelt hat.\nEine Speicherung kann den Seiteninhalt zerstören. Dies geschieht bisweilen durch die Benutzung eines anonymen Proxy-Dienstes, der fehlerhaft arbeitet.'''", "edit_form_incomplete": "'''Der Inhalt des Bearbeitungsformulars hat den Server nicht vollständig erreicht. Bitte prüfe deine Bearbeitungen auf Vollständigkeit und versuche es erneut.'''", diff --git a/languages/i18n/diq.json b/languages/i18n/diq.json index 996bd3655f..542c69724e 100644 --- a/languages/i18n/diq.json +++ b/languages/i18n/diq.json @@ -1769,7 +1769,7 @@ "ncategories": "$1 {{PLURAL:$1|Kategori|Kategoriy}}", "ninterwikis": "$1 {{PLURAL:$1|interwiki|interwikiy}}", "nlinks": "$1 {{PLURAL:$1|link|linkî}}", - "nmembers": "$1 {{PLURAL:$1|eza|ezayan}}", + "nmembers": "$1 {{PLURAL:$1|eza|ezayi}}", "nmemberschanged": "$1 → $2 {{PLURAL:$1|eza|ezayan}}", "nrevisions": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}", "nimagelinks": "$1 {{PLURAL:$1|pele de|pelan de}} gureyeno", diff --git a/languages/i18n/eo.json b/languages/i18n/eo.json index 21a3ef0de7..1a6b65b16c 100644 --- a/languages/i18n/eo.json +++ b/languages/i18n/eo.json @@ -55,7 +55,8 @@ "Joao Xavier", "Surfo", "YvesNevelsteen", - "Vlad5250" + "Vlad5250", + "Mirin" ] }, "tog-underline": "Substrekado de ligiloj:", @@ -845,7 +846,7 @@ "histfirst": "plej malnova", "histlast": "plej nova", "historysize": "({{PLURAL:$1|1 bajto|$1 bajtoj}})", - "historyempty": "(malplena)", + "historyempty": "malplena", "history-feed-title": "Historio de redaktoj", "history-feed-description": "Revizia historio por ĉi tiu paĝo en la vikio", "history-feed-item-nocomment": "$1 ĉe $2", @@ -1475,6 +1476,7 @@ "rcfilters-watchlist-markseen-button": "Marku ĉiujn ŝanĝojn viditaj", "rcfilters-watchlist-edit-watchlist-button": "Redakti vian atentaron", "rcfilters-watchlist-showupdated": "Ŝanĝoj en paĝoj, kiujn vi ne vizitis post la ŝanĝo, aperas grase, kun plenigitaj buletoj.", + "rcfilters-watchlist-preference-label": "Uzi fasadon ne uzantan JavaScript", "rcfilters-target-page-placeholder": "Enigu nomon de paĝo (aŭ kategorio)", "rcnotefrom": "Malsupre estas la {{PLURAL:$5|ŝanĝo|ŝanĝoj}} ekde $3, $4 (montrante ĝis $1).", "rclistfrom": "Montri novajn ŝanĝojn ekde \"$3 $2\"", @@ -1707,6 +1709,7 @@ "uploadstash-thumbnail": "Vidi bildeton", "uploadstash-exception": "Ne eblas alŝuti en kaŝkonservejon ($1): \"$2\".", "uploadstash-bad-path-unrecognized-thumb-name": "Nerekonita miniatura nomo.", + "uploadstash-zero-length": "Longo de dosiero estas nul.", "invalid-chunk-offset": "Malvalida deŝovo de dosierpeco", "img-auth-accessdenied": "Atingo malpermisita", "img-auth-nopathinfo": "Mankas informo pri vojo.\nVia servilo estu agordita por sendi la variablojn REQUEST_URI kaj/aŭ PATH_INFO.\nSe ĝi jam estas, provu aktivigon de $wgUsePathInfo.\nVidu https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.", @@ -2009,6 +2012,7 @@ "speciallogtitlelabel": "Celo (titolo aŭ {{ns:user}}:salutnomo por uzanto):", "log": "Protokoloj", "logeventslist-submit": "Montri", + "logeventslist-tag-log": "Protokolo de etikedoj", "all-logs-page": "Ĉiuj publikaj protokoloj", "alllogstext": "Suma kompilaĵo de ĉiuj protokoloj de {{SITENAME}}.\nVi povas plistrikti la mendon per selektado de protokola speco, la salutnomo (inkluzivante uskladon) aŭ la efika paĝo (ankaŭ inkluzivas uskladon).", "logempty": "Neniaj artikoloj en la protokolo.", @@ -2212,6 +2216,9 @@ "deleteprotected": "Vi ne povas forigi ĉi tiun paĝon ĉar ĝi estis protektita.", "deleting-backlinks-warning": "Atentigo:\n[[Special:WhatLinksHere/{{FULLPAGENAME}}|Aliaj paĝoj]] ligas al aŭ transkludas tiun ĉi forigotan paĝon.", "rollback": "Restarigi antaŭan redakton", + "rollback-confirmation-confirm": "Bonvolu konfirmi:", + "rollback-confirmation-yes": "Amasmalfari", + "rollback-confirmation-no": "Nuligi", "rollbacklink": "malfari", "rollbacklinkcount": "nuligi $1 {{PLURAL:$1|redakton|redaktojn}}", "rollbacklinkcount-morethan": "nuligi pli ol $1 {{PLURAL:$1|redakton|redaktojn}}", @@ -2415,6 +2422,7 @@ "ipb-sitewide": "Tutreteja", "ipb-partial": "Parta", "ipb-pages-label": "Paĝoj", + "ipb-namespaces-label": "Nomspacoj", "badipaddress": "Neniu uzanto, aŭ la IP-adreso estas misformita.", "blockipsuccesssub": "Forbaro sukcesis.", "blockipsuccesstext": "[[Special:Contributions/$1|$1]] estas forbarita.
\nVidu la [[Special:BlockList|liston de forbaroj]] por kontroli.", @@ -2428,6 +2436,8 @@ "ipb-blocklist-contribs": "Kontribuoj de {{GENDER:$1|$1}}", "ipb-blocklist-duration-left": "$1 restas", "block-expiry": "Blokdaŭro", + "block-prevent-edit": "Redaktado", + "block-reason": "Kialo:", "unblockip": "Malforbari IP-adreson/nomon", "unblockiptext": "Per la jena formulo vi povas repovigi al iu\nforbarita IP-adreso/nomo la povon enskribi en la vikio.", "ipusubmit": "Forigi ĉi tiun forbaron", @@ -2442,6 +2452,7 @@ "blocklist-userblocks": "Kaŝi konto-forbarojn", "blocklist-tempblocks": "Kaŝi provizorajn forbarojn", "blocklist-addressblocks": "Kaŝi unuopajn IP-adresajn forbarojn", + "blocklist-type": "Tipo:", "blocklist-rangeblocks": "Kaŝi blokojn de intervalo", "blocklist-timestamp": "Tempindiko", "blocklist-target": "Celo", @@ -2790,6 +2801,7 @@ "pageinfo-display-title": "Montrita titolo", "pageinfo-default-sort": "Pravaloro de ordiga ŝlosilo", "pageinfo-length": "Paĝgrandeco (en bajtoj)", + "pageinfo-namespace": "Nomspaco", "pageinfo-article-id": "Paĝa identigo", "pageinfo-language": "Lingvo de paĝa enhavo", "pageinfo-language-change": "ŝanĝi", @@ -2959,6 +2971,8 @@ "confirm-unwatch-top": "Ĉu forigi tiun ĉi paĝon el via atentaro?", "confirm-rollback-button": "Bone", "confirm-rollback-top": "Malfaru redaktojn al ĉi tiu paĝo?", + "confirm-mcrundo-title": "Malfari ŝanĝon", + "mcrundofailed": "Malfaro malsukcesis", "quotation-marks": "„$1“", "imgmultipageprev": "← antaŭa paĝo", "imgmultipagenext": "sekva paĝo →", @@ -3158,6 +3172,7 @@ "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Etikedo|Etikedoj}}]]: $2", "tag-mw-contentmodelchange": "ŝanĝo de enhavomodelo", "tag-mw-contentmodelchange-description": "Redaktoj kiuj [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel ŝanĝas la enhavmodelon] de paĝo", + "tag-mw-undo": "Malfari", "tags-title": "Etikedoj", "tags-intro": "Ĉi tiu paĝo montras la etikedojn kun kiuj la programaro markus redakton, kaj iliaj signifoj.", "tags-tag": "Etikeda nomo", @@ -3255,7 +3270,10 @@ "compare-title-not-exists": "La titolo kiun vi specifis ne ekzistas.", "compare-revision-not-exists": "La revizio kiun vi specifis ne ekzistas.", "diff-form": "Malsamoj", + "diff-form-submit": "Montri diferencojn", "permanentlink": "Konstanta ligilo", + "permanentlink-revid": "Identigilo de revizio", + "permanentlink-submit": "Iri al revizio", "dberr-problems": "Bedaŭrinde, ĉi tiu retejo suferas pro teknikaj problemoj.", "dberr-again": "Bonvolu atendi kelkajn minutojn kaj reŝargi.", "dberr-info": "(Ne eblas konekti la datumbazon: $1)", @@ -3501,6 +3519,7 @@ "special-characters-group-thai": "Taja", "special-characters-group-lao": "laŭa", "special-characters-group-khmer": "kmera", + "special-characters-group-canadianaboriginal": "Kanada Indiĝena", "special-characters-title-endash": "mallonga streketo", "special-characters-title-emdash": "longa streketo", "special-characters-title-minus": "minus-signo", @@ -3518,6 +3537,8 @@ "mw-widgets-categoryselector-add-category-placeholder": "Aldoni kategorion", "mw-widgets-usersmultiselect-placeholder": "Aldoni pliajn...", "mw-widgets-titlesmultiselect-placeholder": "Aldoni pliajn...", + "date-range-from": "De dato:", + "date-range-to": "Ĝis dato:", "sessionmanager-tie": "Kombini diversajn tipojn de ensaluta peto ne estas permisita: $1.", "sessionprovider-generic": "$1 seancoj", "sessionprovider-mediawiki-session-cookiesessionprovider": "kuketaj seancoj", @@ -3573,6 +3594,7 @@ "log-action-filter-suppress-reblock": "Forigi uzanton per reforbari", "log-action-filter-upload-upload": "Novalŝuta", "log-action-filter-upload-overwrite": "Realŝuta", + "log-action-filter-upload-revert": "Restarigi", "authmanager-authn-not-in-progress": "Aŭtentigado ne estas progresanta aŭ la seancaj datumoj perdiĝis. Bonvolu provi denove ekde la komenco.", "authmanager-authn-no-primary": "La provizita legitimaĵo ne povus esti aŭtentikigita.", "authmanager-authn-no-local-user": "La provizitaj legitimaĵoj ne estas asociitaj kun ajna uzanto de ĉi tiu vikio.", @@ -3650,6 +3672,8 @@ "revid": "revizio $1", "pageid": "Identigilo de paĝo $1", "pagedata-title": "Paĝaj datumoj", + "pagedata-bad-title": "Nevalida titolo: \"$1\".", + "passwordpolicies": "Reguloj pri pasvortoj", "passwordpolicies-group": "Grupo", "passwordpolicies-policies": "Politiko", "passwordpolicies-policy-minimalpasswordlength": "Pasvortoj devas esti longaj almenaŭ $1 {{PLURAL:$1|1 signon|$1 signojn}}.", diff --git a/languages/i18n/hr.json b/languages/i18n/hr.json index ff0c96d930..028ebae868 100644 --- a/languages/i18n/hr.json +++ b/languages/i18n/hr.json @@ -460,7 +460,7 @@ "createacct-reason": "Razlog", "createacct-reason-ph": "Zašto stvarate još jedan račun?", "createacct-reason-help": "Poruka koja se prikazuje u evidenciji stvaranja suradničkih računa", - "createacct-submit": "Stvorite svoj suradnički račun", + "createacct-submit": "Stvori svoj suradnički račun", "createacct-another-submit": "Otvori račun", "createacct-continue-submit": "Pritisni za stvaranje računa", "createacct-another-continue-submit": "Nastavi za stvaranje računa", @@ -2293,6 +2293,7 @@ "sp-contributions-newonly": "Pokaži samo stranice koje je suradnik započeo", "sp-contributions-hideminor": "Sakrij manje izmjene", "sp-contributions-submit": "Traži", + "sp-contributions-outofrange": "Nije moguće pokazati rezultate. Traženi raspon IP adresa veći je od CIDR limita /$1.", "whatlinkshere": "Što vodi ovamo", "whatlinkshere-title": "Stranice koje vode na »$1«", "whatlinkshere-page": "Stranica:", diff --git a/languages/i18n/hu.json b/languages/i18n/hu.json index cf1c6fc99c..4200f17c3c 100644 --- a/languages/i18n/hu.json +++ b/languages/i18n/hu.json @@ -1377,13 +1377,17 @@ "action-editmyusercss": "saját szerkesztői CSS-fájlok szerkesztése", "action-editmyuserjson": "saját szerkesztői JSON-fájlok szerkesztése", "action-editmyuserjs": "saját szerkesztői JavaScript-fájlok szerkesztése", + "action-viewsuppressed": "minden felhasználó elől elrejtett változtatások megtekintése", + "action-hideuser": "felhasználói név blokkolása és elrejtése a külvilág elől", "action-ipblock-exempt": "IP-, auto- és tartományblokkok megkerülése", "action-unblockself": "saját felhasználói fiók blokkjának feloldása", "action-noratelimit": "sebességkorlát figyelmen kívül hagyása", "action-reupload-own": "a saját maga által feltöltött fájlok felülírása", + "action-nominornewtalk": "vitalapok apró szerkesztése új üzenetről való értesítés kiküldése nélkül", "action-markbotedits": "visszaállított szerkesztések botként való jelölése", "action-patrolmarks": "járőrök jelzéseinek megtekintése a friss változásokban", "action-override-export-depth": "lapok exportálása a hivatkozott lapokkal együtt, legfeljebb 5-ös mélységig", + "action-suppressredirect": "átirányítások készítésének kihagyása a lapok régi nevén átnevezéskor", "nchanges": "$1 változtatás", "enhancedrc-since-last-visit": "$1 az utolsó látogatás óta", "enhancedrc-history": "történet", @@ -1438,7 +1442,7 @@ "rcfilters-savedqueries-already-saved": "Ezek a szűrők már el lettek mentve. Módosítsd a beállításokat egy új mentett szűrő készítéséhez.", "rcfilters-restore-default-filters": "Alapértelmezett szűrők visszaállítása", "rcfilters-clear-all-filters": "Összes szűrő kikapcsolása", - "rcfilters-show-new-changes": "Legfrissebb változtatások megtekintése", + "rcfilters-show-new-changes": "$1-óta történt friss változtatások megtekintése", "rcfilters-search-placeholder": "Változtatások szűrése (használd a menüt vagy keress szűrőkre)", "rcfilters-invalid-filter": "Érvénytelen szűrő", "rcfilters-empty-filter": "Nincs aktív szűrő. Minden közreműködés látható.", @@ -3811,5 +3815,7 @@ "passwordpolicies-policy-passwordnotinlargeblacklist": "A jelszó nem szerepelhet a 100 000 leggyakrabban használt jelszó listáján .", "passwordpolicies-policyflag-forcechange": "lecserélés követelése bejelentkezéskor", "passwordpolicies-policyflag-suggestchangeonlogin": "lecserélés ajánlása bejelentkezéskor", - "unprotected-js": "Biztonsági okokból JavaScript nem tölthető be védtelen lapokról. Kérlek egyedül a MediaWiki névtérben készíts JavaScriptet, vagy szerkesztői allapként." + "unprotected-js": "Biztonsági okokból JavaScript nem tölthető be védtelen lapokról. Kérlek egyedül a MediaWiki névtérben készíts JavaScriptet, vagy szerkesztői allapként.", + "userlogout-continue": "Amennyiben ki szeretnél jelentkezni, [$1 használd a kijelentkezési oldalt].", + "userlogout-sessionerror": "Sikertelen kijelentkezés munkamenethiba miatt. Kérlek [$1 próbáld újra]." } diff --git a/languages/i18n/hyw.json b/languages/i18n/hyw.json index 17a347060d..06256b9d65 100644 --- a/languages/i18n/hyw.json +++ b/languages/i18n/hyw.json @@ -197,7 +197,7 @@ "badaccess-group0": "Արտունութիւն չունիք այս գործողութիւնը կատարել:", "badaccess-groups": "Տուեալ գործողութիւնը միայն $1 {{PLURAL:$2|խումբի|խումբերի}} մասնակիցները կ՛րնան կատարել։", "ok": "Լաւ", - "pagetitle": "Միացէ՛ք {{SITENAME}} նախագիծին", + "pagetitle": "", "retrievedfrom": "Վերցուած է «$1» էջէն", "youhavenewmessages": "{{PLURAL:$3|Դուք ունիք}} $1 ($2)։", "youhavenewmessagesfromusers": "{{PLURAL:$4|Դուք ունիք}} $1 {{PLURAL:$3|այլ մասնակից|$3 մասնակիցէն}} ($2):", diff --git a/languages/i18n/ia.json b/languages/i18n/ia.json index 7378e8a51b..9e36b9f63b 100644 --- a/languages/i18n/ia.json +++ b/languages/i18n/ia.json @@ -2531,7 +2531,7 @@ "blocklist-editing-page": "paginas", "blocklist-editing-ns": "spatios de nomines", "ipblocklist-empty": "Le lista de blocadas es vacue.", - "ipblocklist-no-results": "Le adresse IP o nomine de usator que tu requestava non es blocate.", + "ipblocklist-no-results": "Nulle blocadas trovate que corresponde al adresse IP o nomine de usator requestate.", "blocklink": "blocar", "unblocklink": "disblocar", "change-blocklink": "cambiar blocada", diff --git a/languages/i18n/jv.json b/languages/i18n/jv.json index 643fb5fe92..a348240ee4 100644 --- a/languages/i18n/jv.json +++ b/languages/i18n/jv.json @@ -1660,7 +1660,7 @@ "download": "undhuh", "unwatchedpages": "Kaca kang ora ingawasan", "listredirects": "Pratélan alihan", - "unusedtemplates": "Cithakan kang ora kanggo", + "unusedtemplates": "Cithakan kang ora kaanggo", "unusedtemplatestext": "Kaca iki isi kabèh kaca ing mandala aran {{ns:template}} kang ora kaanggo ing kaca liya.\nAja lali mesthèkaké ana-orané pranala liya kang ngener cithakané sadurungé panjenengan mbusek.", "unusedtemplateswlh": "pranala liya-liyané", "randompage": "Kaca sembarang", @@ -1707,7 +1707,7 @@ "withoutinterwiki-summary": "Kaca-kaca ing ngisor iki ora nggayut menyang vèrsi basa liyané.", "withoutinterwiki-legend": "Préfiks", "withoutinterwiki-submit": "Tuduhna", - "fewestrevisions": "Artikel kang owahé sithik dhéwé", + "fewestrevisions": "Artikel kang owahé sathithik dhéwé", "nbytes": "$1 {{PLURAL:$1|bét|bét}}", "ncategories": "$1 {{PLURAL:$1|kategori|kategori}}", "ninterwikis": "$1 {{PLURAL:$1|interwiki|interwiki}}", @@ -1724,8 +1724,8 @@ "uncategorizedcategories": "Kategori kang tanpa kategori", "uncategorizedimages": "Barkas kang tanpa kategori", "uncategorizedtemplates": "Cithakan kang durung kawènèhan kategori", - "unusedcategories": "Kategori kang ora kanggo", - "unusedimages": "Barkas kang ora kanggo", + "unusedcategories": "Kategori kang ora kaanggo", + "unusedimages": "Barkas kang ora kaanggo", "wantedcategories": "Kategori kang kapéngini", "wantedpages": "Kaca kang kapéngini", "wantedpages-badtitle": "Sesirah ora sah ing omboyakan kasil: $1", diff --git a/languages/i18n/ml.json b/languages/i18n/ml.json index f0f16cfea4..e6e991ba9b 100644 --- a/languages/i18n/ml.json +++ b/languages/i18n/ml.json @@ -1047,7 +1047,7 @@ "prefs-files": "പ്രമാണങ്ങൾ", "prefs-custom-css": "സ്വന്തം സി.എസ്.എസ്.", "prefs-custom-json": "ഐച്ഛിക ജെസൺ", - "prefs-custom-js": "സ്വന്തം ജെ.എസ്.", + "prefs-custom-js": "സ്വന്തം ജാവാസ്ക്രിപ്റ്റ്", "prefs-common-config": "എല്ലാ ദൃശ്യരൂപങ്ങൾക്കുമായി പങ്ക് വെയ്ക്കപ്പെട്ട സി.എസ്.എസ്./ജെസൺ/ജാവാസ്ക്രിപ്റ്റ്:", "prefs-reset-intro": "സൈറ്റിൽ സ്വതേയുണ്ടാവേണ്ട ക്രമീകരണങ്ങൾ പുനഃക്രമീകരിക്കാൻ താങ്കൾക്ക് ഈ താൾ ഉപയോഗിക്കാവുന്നതാണ്.\nഇത് തിരിച്ചു ചെയ്യാൻ സാദ്ധ്യമല്ല.", "prefs-emailconfirm-label": "ഇമെയിൽ സ്ഥിരീകരണം:", diff --git a/languages/i18n/my.json b/languages/i18n/my.json index a1a29db6a4..f06eb44f0c 100644 --- a/languages/i18n/my.json +++ b/languages/i18n/my.json @@ -1574,6 +1574,7 @@ "allpages-hide-redirects": "ပြန်ညွှန်းများအား ဝှက်ရန်", "cachedspecial-viewing-cached-ttl": "သင်သည် $1 အချိန်ကြာသွားနိုင်သော ဤစာမျက်နှာ၏ cached ဗားရှင်းကို ကြည့်ရှုနေခြင်း ဖြစ်ပါသည်။", "cachedspecial-viewing-cached-ts": "သင်သည် ဤစာမျက်နှာ၏ အမှန်တကယ်မဟုတ်နိုင်သော cached ဗားရှင်းကို ကြည့်ရှုနေခြင်းဖြစ်သည်။", + "cachedspecial-refresh-now": "နောက်ဆုံးကို ကြည့်ရှုရန်။", "categories": "ကဏ္ဍများ", "categories-submit": "ပြသရန်", "categoriespagetext": "အောက်ပါ {{PLURAL:$1|ကဏ္ဍ|ကဏ္ဍများ}}သည် ဤဝီကီတွင် အသုံးပြု သို့မဟုတ် အသုံးမပြုထားခြင်း ဖြစ်နိုင်သည်။ [[Special:WantedCategories|အလိုရှိသော ကဏ္ဍများ]]ကိုလည်း ကြည့်ပါ။", @@ -1837,6 +1838,7 @@ "mycontris": "ဆောင်ရွက်ချက်များ", "anoncontribs": "ဆောင်ရွက်ချက်များ", "contribsub2": "{{GENDER:$3|$1}}အတွက် ($2)", + "contributions-subtitle": "{{GENDER:$3|$1}} အတွက်", "contributions-userdoesnotexist": "အသုံးပြုသူအကောင့် \"$1\" သည် မှတ်ပုံမတင်ထားပါ။", "nocontribs": "ဤသတ်မှတ်ချက်များနှင့် ကိုက်ညီသည့် ပြောင်းလဲမှုများ မရှိပါ။", "uctop": "လက်ရှိ", @@ -1954,6 +1956,9 @@ "createaccountblock": "အကောင့်ဖန်တီးခြင်းကို ပိတ်ထားသည်", "emailblock": "အီးမေးကို ပိတ်ပင်ထားသည်", "blocklist-nousertalk": "မိမိ၏ဆွေးနွေးချက်စာမျက်နှာကို တည်းဖြတ်မရနိုင်ပါ", + "blocklist-editing": "တည်းဖြတ်ခြင်း", + "blocklist-editing-page": "စာမျက်နှာများ", + "blocklist-editing-ns": "အမည်ညွှန်းများ", "ipblocklist-empty": "ပိတ်ပင်ထားမှုစာရင်းသည် ဗလာဖြစ်နေသည်။", "ipblocklist-no-results": "တောင်းဆိုလိုက်သော အိုင်ပီလိပ်စာ သို့မဟုတ် အသုံးပြုသူအမည်ကို မပိတ်ပင်ထားပါ။", "blocklink": "ပိတ်ပင်", @@ -2156,6 +2161,7 @@ "pageinfo-display-title": "ပြသခေါင်းစဉ်", "pageinfo-default-sort": "ပုံမှန် စာလုံးစီကီး", "pageinfo-length": "စာမျက်နှာ အလျား (ဘိုက်ဖြင့်)", + "pageinfo-namespace": "အမည်ညွှန်း", "pageinfo-article-id": "စာမျက်နှာ အိုင်ဒီ", "pageinfo-language": "စာမျက်နှာ စာကိုယ် ဘာသာစကား", "pageinfo-language-change": "ပြောင်းလဲရန်", @@ -2607,6 +2613,7 @@ "log-action-filter-protect-protect": "ကာကွယ်မှု", "log-action-filter-rights-rights": "လူဖြင့် ပြောင်းလဲမှု", "log-action-filter-rights-autopromote": "အလိုအလျောက် ပြောင်းလဲမှု", + "log-action-filter-upload-revert": "ပြန်ပြောင်းရန်", "authmanager-create-disabled": "အကောင့်ဖန်တီးခြင်းကို ပိတ်ထားသည်။", "authmanager-autocreate-noperm": "အလိုအလျာက် အကောင့်ဖန်တီးခြင်းကို ခွင့်မပြုပါ။", "authmanager-autocreate-exception": "ရှေ့ကအမှားများကြောင့် အလိုအလျာက် အကောင့်ဖန်တီးခြင်းကို ယာယီပိတ်ထားသည်။", @@ -2618,6 +2625,7 @@ "authmanager-realname-help": "အသုံးပြုသူ၏ အမည်ရင်း", "authmanager-provider-temporarypassword": "ယာယီစကားဝှက်", "authprovider-resetpass-skip-label": "ကျော်ရန်", + "specialpage-securitylevel-not-allowed-title": "ခွင့်မပြုပါ", "cannotauth-not-allowed-title": "ခွင့်ပြုချက် ငြင်းပယ်လိုက်သည်", "cannotauth-not-allowed": "သင်သည် ဤစာမျက်နှာကို အသုံးပြုခွင့်မရှိပါ", "userjsispublic": "ကျေးဇူးပြု၍ မှတ်သားပါ- JavaScript စာမျက်နှာခွဲများတွင် အခြားအသုံးပြုသူများ ကြည့်ရှုနိုင်သော လျို့ဝှက်အပ်သည့်အချက်အလက် မပါဝင်သင့်ပါ။", diff --git a/languages/i18n/nb.json b/languages/i18n/nb.json index 9bd432df2c..85bea6f93c 100644 --- a/languages/i18n/nb.json +++ b/languages/i18n/nb.json @@ -1448,7 +1448,7 @@ "rcfilters-savedqueries-already-saved": "Disse filtrene er allerede lagret. Endre innstillingene dine for å opprette et nytt lagret filter.", "rcfilters-restore-default-filters": "Gjenopprett standardfiltre", "rcfilters-clear-all-filters": "Nullstill alle filtre", - "rcfilters-show-new-changes": "Vis nye endringer siden $1", + "rcfilters-show-new-changes": "Vis nye endringer etter $1", "rcfilters-search-placeholder": "Filtrer endringer (bruk menyen eller søk etter et filternavn)", "rcfilters-invalid-filter": "Ugyldig filter", "rcfilters-empty-filter": "Ingen aktive filtre. Alle bidrag vises.", diff --git a/languages/i18n/nn.json b/languages/i18n/nn.json index b8857e641e..d75881d54d 100644 --- a/languages/i18n/nn.json +++ b/languages/i18n/nn.json @@ -3035,5 +3035,6 @@ "revid": "versjon $1", "interfaceadmin-info": "$1\n\nLøyva for endring av CSS/JS/JSON-filer som gjeld heile nettstaden vart nyleg skilde ut frå editinterface-retten. Om du ikkje skjøner kvifor du får denne feilmeldinga, sjå [[mw:MediaWiki_1.32/interface-admin]].", "passwordpolicies-policy-passwordcannotmatchusername": "Passordet kan ikkje vera det same som brukarnamnet", - "passwordpolicies-policy-passwordcannotmatchblacklist": "Passordet kan ikkje passa med svartelista passord" + "passwordpolicies-policy-passwordcannotmatchblacklist": "Passordet kan ikkje passa med svartelista passord", + "userlogout-sessionerror": "Utlogging gjekk ikkje grunna ein øktfeil. [$1 Freist om att]." } diff --git a/languages/i18n/nqo.json b/languages/i18n/nqo.json index 176a3bb3ae..063bae30df 100644 --- a/languages/i18n/nqo.json +++ b/languages/i18n/nqo.json @@ -4,7 +4,9 @@ "Babamamadidianee", "Lancine.kounfantoh.fofana", "Lanciné.kounfantoh.fofana", - "Youssoufkadialy" + "Youssoufkadialy", + "Amire80", + "Nafadji Mory Diané" ] }, "sunday": "ߞߊ߯ߙߌߟߏ߲", @@ -75,12 +77,12 @@ "hidden-categories": "{{PLURAL:$1|ߦߌߟߡߊ߫ ߘߏ߲߰ߣߍ߲ |ߦߌߟߡߊ߫ ߘߏ߲߰ߣߍ߲ ߠߎ߬}}", "category-subcat-count": "{{PLURAL:$2|ߦߟߊߡߊߙߋ߲ ߣߌ߲߬ ߠߎ߫ ߜߊ߲߰ߛߊ߲ ߠߋ߫ ߦߋ߫ ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫.|ߦߌߟߡߊ ߣߊ߬ߕߐ ߟߎ߬ ߘߐ߫߸ {{PLURAL:$1|ߦߌߟߡߊߙߋ߲|$1 ߦߌߟߡߊߙߋ߲ ߠߎ߬}} ߟߋ߬ ߦߴߊ߬ ߘߐ߫߸ ߞߙߎߞߙߍ ߟߎ߬ ߞߐߞߊ߲߬ $2}}", "category-article-count": "{{PLURAL:$2|ߞߐߜߍ ߣߌ߲߬ ߘߐߙߐ߲߫ ߠߋ߬ ߦߋ߫ ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫.|ߖߡߊ߬ߦߊ߫ ߕߐ߮ ߣߊ߬ߕߊ {{PLURAL:$1|ߞߐߜߍ ߦߋ߫|$1 ߞߐߜߍ ߦߋ߫}} ߟߋ߬ ߦߋ߫ ߦߌߟߡߊ߫ ߘߌ߫߸ ߞߙߎߞߙߍ $2 ߞߐߞߊ߲߬}}", - "category-file-count": "{{:$2|ߞߐߕߐ߮ ߣߌ߲߬ ߜߊ߲߰ߛߊ߲ ߠߋ߫ ߦߋ߫ ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫.|ߡߍ߲ ߠߎ߬ ߦߋ߫ ߣߌ߲߬ {{PLURAL:$1|ߞߐߕߐ߮ ߦߋ߫|$1 ߞߐߕߐ߮ ߟߎ߬ ߦߋ߫}} ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫߸ ߞߙߎߞߙߍ ߣߌ߲߬ $2 ߕߴߊ߬ ߘߐ߫.}}", + "category-file-count": "{{PLURAL:$2|ߞߐߕߐ߮ ߣߌ߲߬ ߜߊ߲߰ߛߊ߲ ߠߋ߫ ߦߋ߫ ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫.|ߡߍ߲ ߠߎ߬ ߦߋ߫ ߣߌ߲߬ {{PLURAL:$1|ߞߐߕߐ߮ ߦߋ߫|$1 ߞߐߕߐ߮ ߟߎ߬ ߦߋ߫}} ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫߸ ߞߙߎߞߙߍ ߣߌ߲߬ $2 ߕߴߊ߬ ߘߐ߫.}}", "listingcontinuesabbrev": "ߖߊ߬ߕߋ߬ߘߊ", "index-category": "ߞߐߜߍ߫ ߓߊߕߐ߲ߛߐ߲ ߠߎ߬", "noindex-category": "ߞߐߜߍ߫ ߘߐߕߐ߲ߛߐ߲ߦߊߓߊߟߌ ߟߎ߬", "about": "ߡߊ߬ߘߎ߮", - "newwindow": "ߊ߬ ߟߊߞߊ߬ ߝߢߐߘߊ߫ ߞߎߘߊ߫ ߟߊ߫", + "newwindow": "(ߊ߬ ߟߊߞߊ߬ ߝߢߐߘߊ߫ ߞߎߘߊ߫ ߟߊ߫)", "cancel": "ߊ߬ ߘߐߛߊ߬", "moredotdotdot": "ߡߊߞߊ߬ߝߏ߬...", "morenotlisted": "ߛߙߍߘߍ ߣߌ߲߬ ߘߝߊߓߊߟߌ߫ ߓߍ߫ ߞߍ߫.", @@ -95,7 +97,7 @@ "navigation-heading": "ߛߏ߲߯ߓߊߟߌ߫ ߓߏߟߏ߲ߘߊ", "errorpagetitle": "ߝߎ߬ߕߎ߲߬ߕߌ", "returnto": "ߌ ߞߐߛߊ߬ߦߌ߲߬ ߦߊ߲߬ ߡߊ߬$1", - "tagline": "ߞߊ߬ ߝߘߊ߫", + "tagline": "ߞߊ߬ ߝߘߊ߫{{SITENAMEP}}", "help": "ߘߍ߬ߡߍ߲߬ߠߌ", "help-mediawiki": "ߘߍ߬ߡߍ߲߬ߠߌ߲ ߞߊ߬ ߓߍ߲߬ ߥߞߌ-ߟߊߛߋߢߊߥߙߍ ߡߊ߬", "search": "ߢߌߣߌ߲ߠߌ", @@ -110,11 +112,13 @@ "print": "ߜߌ߬ߙߌ߲߬ߘߌ߬ߟߌ", "view": "ߊ߬ ߘߐߜߍ߫", "view-foreign": "ߊ߬ ߦߋ߫ ߦߊ߲߬ $1", - "edit": "ߊ߬ ߡߊߝߊ߬ߟߋ߲߬", + "edit": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߬", "create": "ߟߊ߬ߘߊ߲߬ߠߌ", "create-local": "ߕߌ߲߬ߞߎߘߎ߲ ߞߊ߲߬ߛߓߍ߬ߟߌ ߟߊߘߏ߲߬", "delete": "ߊ߬ ߖߐ߬ߛߌ߬", "undelete_short": "ߟߊ߬ߛߊ߬ߦߌ߲߬ߠߌ {{PLURAL:$1|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߋߟߋ߲߫|$1 ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߠߎ߬}}", + "protect": "ߊ߬ ߟߊߞߊ߲ߘߊ߫", + "protect_change": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߫", "unprotect": "ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ ߡߊߝߊ߬ߟߋ߲߬ߠߌ", "newpage": "ߘߐߜߍ߫ ߞߎߘߊ", "talkpagelinktext": "ߓߊ߬ߘߏ߬ߟߌ", @@ -123,13 +127,24 @@ "talk": "ߓߊ߬ߘߏ߬ߓߊ߬ߘߌߦߊ", "views": "ߦߌ߬ߘߊ߬ߟߌ", "toolbox": "ߖߐ߯ߙߊ߲ ߠߎ߬", + "tool-link-emailuser": "ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ߫ ߟߊߕߊ߯ {{GENDER:$1|ߟߊߓߊ߯ߙߟߊ ߣߌ߲߬ ߡߊ߬ }}", + "imagepage": "ߞߐߕߐ߮ ߞߐߜߍ ߘߐߜߍ߫", + "mediawikipage": "ߗߋߛߓߍ ߞߐߜߍ ߘߐߜߍ߫", + "templatepage": "ߞߙߊߞߏ ߞߐߜߍ ߘߐߜߍ߫", + "viewhelppage": "ߡߊ߬ߘߍ߬ߡߍ߲߬ߠߌ߲ ߞߐߜߍ ߘߐߜߍ߫", + "categorypage": "ߦߌߟߡߊ ߞߐߜߍ ߘߐߜߍ߫", + "viewtalkpage": "ߢߊߝߐߞߣߍ ߞߐߜߍ ߘߐߜߍ߫", "otherlanguages": "ߞߊ߲ ߜߘߍ߫ ߟߎ߫ ߘߐ߫", "redirectedfrom": "(ߌ ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߫ ߞߊ߬ ߓߐ߫ $1)", "redirectto": "ߌ ߓߘߊ߫ ߟߊߞߎ߲߬ߛߌ߲߫ ߦߊ߲߬ ߠߊ߫:", - "lastmodifiedat": "ߞߐߜߍ ߣߌ߲߬ ߡߊߝߊߟߋ߲߫ ߟߊߓߊ߲ ߞߍ߫ ߘߊ߫ $1߸ $2", - "jumpto": "ߊ߬ ߕߌߙߌ߲߫", + "lastmodifiedat": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߟߊߓߊ߲ ߞߍ߫ ߘߊ߫ $1߸ $2", + "protectedpage": "ߞߐߜߍ߫ ߡߊߞߊ߲ߞߊ߲ߣߍ߲", + "jumpto": "ߊ߬ ߕߌߙߌ߲߫:", "jumptonavigation": "ߛߏ߲߯ߓߊߟߌ", "jumptosearch": "ߊ߬ ߕߌߙߌ߲߫", + "pool-timeout": "ߘߊߕߎ߲߯ߠߌ߲ ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲߬ ߕߎߡߊ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬", + "pool-errorunknown": "ߝߌ߬ߟߌ߬ ߛߎ߲߫ ߟߐ߲ߓߊߟߌ", + "poolcounter-usage-error": "ߟߊߓߊ߯ߙߊߟߌ߫ ߝߟߌ $1", "aboutsite": "ߞߊ߬ ߓߍ߲߬ {{SITENAME}}", "aboutpage": "Project:About", "copyrightpage": "{{ns:project}}: ߛߓߍߦߟߊ ߤߊߞߍ", @@ -138,12 +153,15 @@ "disclaimers": "ߖߊ߲߬ߘߐ߬ߓߌ߬ߟߊ߬ߟߌ ߟߎ߬", "disclaimerpage": "Project: ߖߊ߲߬ߘߐ߬ߓߌ߬ߟߊ߬ߟߌ ߡߎ߰ߡߍ", "edithelp": "ߡߊ߬ߦߟߍ߬ߢߊ߲߬ߠߌ߲ ߘߍ߬ߡߍ߲߬ߠߌ߲", + "helppage-top-gethelp": "ߘߍ߬ߡߍ߲߬ߠߌ", "mainpage": "ߓߏ߬ߟߏ߲߬ߘߊ", "mainpage-description": "ߓߏ߬ߟߏ߲߬ߘߊ", + "policy-url": "ߣߕߊ߬ߘߐ߬ߛߌ߮: ߕߐ߲ ߠߎ߬", "portal": "ߟߊ߬ߛߣߍ߬ߟߌ ߓߏ߬ߟߏ߲߬ߘߊ", "portal-url": "Project:ߟߊ߬ߛߣߍ߬ߟߌ ߓߏ߬ߟߏ߲߬ߘߊ", "privacy": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߤߊߞߍ", "privacypage": "Project:ߞߊ߬ ߓߍ߲߬ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߤߊߞߍ ߡߊ߬", + "ok": "ߏ߬ߞߍ߫", "retrievedfrom": "ߊ߬ ߡߊߝߍߣߍ߲߫ ߦߊ߲߬ ߓߊ߫$1", "youhavenewmessages": "{{PLURAL:$3|ߌ ߓߘߊ߫ ߗߋߛߓߍ߫ ߞߎߘߊ ߛߐ߬ߘߐ߲߬$1 $2 }}", "editsection": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߫", @@ -151,11 +169,18 @@ "viewsourceold": "ߊ߬ ߛߎ߲ ߘߐߜߍ߫", "editlink": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߬", "viewsourcelink": "ߊ߬ ߛߎ߲ ߠߊߓߊ߯ߙߊ߫", - "editsectionhint": "$1:ߟߊߖߍ߲ߛߍ߲ߠߌ߫ ߦߙߐ", + "editsectionhint": "ߦߌߟߡߊ ߡߊߝߊ߬ߟߋ߲߬ߠߌ:$1", "toc": "ߞߣߐߘߐ", + "showtoc": "ߦߌ߬ߘߊ߬ߟߌ", + "hidetoc": "ߢߡߊߘߏ߲߯ߠߌ", + "confirmable-confirm": "ߌ ߛߍ߬ߓߍ߫ ߓߊ߬ {{GENDER:$1|}}؟", + "confirmable-yes": "ߐ߲߬ߤߐ߲߫", + "confirmable-no": "ߍ߲߬ߍ߲߫", + "thisisdeleted": "ߦߊ߯ߟߊ߫ ߦߴߊ߬ ߝߍ߬ ߞߵߊ߬ ߦߌ߬ߘߊ߬ ߥߟߊ߫ ߞߵߊ߬ ߟߊߛߊߦߌ߲߬ ߞߎߘߊߞߍ߫ ߓߊ߬ $1؟", + "viewdeleted": "ߦߌ߬ߘߊ߬ߟߌ ߓߊ߬ $1؟", "site-atom-feed": "$1 ߝߕߌ ߓߊߟߏ", "page-atom-feed": "$1 ߝߕߌ ߓߊߟߏ", - "red-link-title": "$1(ߞߐߜߍ ߏ߬ ߡߊ߫ ߟߊߘߊ߲߫ ߝߟߐ߫)", + "red-link-title": "ߞߐߜߍ߫ ߕߍ߫ ߦߋ߲߬ $1", "nstab-main": "ߞߐߜߍ", "nstab-user": "ߞߐߜߍ߫ ߟߊߓߊ߯ߙߕߊ", "nstab-special": "ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲", @@ -166,6 +191,7 @@ "nstab-category": "ߦߌߟߡߊ", "mainpage-nstab": "ߓߏ߬ߟߏ߲߬ߘߊ", "nosuchspecialpage": "ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߬ ߛߎ߮ ߏ߬ ߝߋ߲߫ ߕߍ߫ ߦߊ߲߬", + "nospecialpagetext": "ߊߟߎ߫ ߓߘߊ߫ ߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߘߏ߫ ߢߌߣߌ߲߫ ߡߍ߲ ߕߺߴߦߋ߲߬.\nߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߫ ߓߘߍ߬ߡߊ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫ ߢߌ߲߬ ߠߋ߫ ߞߊ߲߬ [[Special:SpecialPages|{{int:specialpages}}]].", "badtitle": "ߞߎ߲߬ߕߐ߰ ߖߎ߮", "viewsource": "ߊ߬ ߛߎ߲ ߘߐߜߍ߫", "viewsource-title": "ߣߌ߲߬ $1 ߛߎ߲ ߘߐߜߍ߫", @@ -190,7 +216,7 @@ "createacct-benefit-heading": "ߛߌ߲ߘߌߣߍ߲߫ ߦߴߌ ߢߐ߲߭ ߡߐ߱ ߟߎ߬ ߟߋ߬ ߓߟߏ߫", "createacct-benefit-body1": "{{PLURAL:$1|ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߬|ߊ߬ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬}}", "createacct-benefit-body2": "$1 {{PLURAL:$1|ߘߐߜߍ|ߞߐߜߍ ߟߎ߬}}", - "createacct-benefit-body3": "ߕߊ߬ߡߌ߲߬ߣߍ߲߬ ߞߎߘߊ {{plural:$1|ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ߠߊ|ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ߠߊ ߟߎ߬}}", + "createacct-benefit-body3": "ߕߊ߬ߡߌ߲߬ߣߍ߲߬ ߞߎߘߊ {{PLURAL:$1|ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ߠߊ|ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ߠߊ ߟߎ߬}}", "loginlanguagelabel": "ߞߊ߲ $1", "pt-login": "ߌ ߜߊ߲߬ߞߎ߲߬", "pt-login-button": "ߌ ߜߊ߲߬ߞߎ߲߬", @@ -212,7 +238,7 @@ "image_tip": "ߞߐߕߐ߮ ߘߐߘߏ߲߬ߣߍ߲", "media_tip": "ߞߐߕߐ߮ ߛߘߌ߬ߜߋ߲", "sig_tip": "ߌ ߟߊ߫ ߞߟߊ߬ߣߐ ߕߎ߬ߡߊ߬ߘߊ ߓߊ߬ߘߌ߬ߟߊ߲߬ߡߊ", - "summary": "ߟߊ߬ߘߛߏ߬ߟߌ", + "summary": "ߟߊ߬ߘߛߏ߬ߟߌ:", "minoredit": "ߣߌ߲߬ ߦߋ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲ ߘߏ߫ ߟߋ߬ ߘߌ߫", "watchthis": "ߘߐߜߍ ߣߌ߲߬ ߘߐߜߍ߫", "savearticle": "ߊ߬ ߟߊߞߎ߲߬ߘߎ߬", @@ -224,14 +250,14 @@ "newarticletext": "ߌ ߓߘߊ߫ ߛߘߌ߬ߜߋ߲ ߘߏ߫ ߟߊߓߊ߬ߕߏ߬ ߞߐߜߍ ߘߏ߫ ߘߐ߫߸ ߡߍ߲ ߕߴߦߋ߲߬ ߡߎߣߎ߲߬.\nߣߵߌ ߦߴߊ߬ ߝߍ߫ ߞߊ߬ ߞߐߜߍ ߘߏ߫ ߟߊߘߊ߲߫߸ ߛߓߍߟߌ ߘߊߡߌ߬ߣߊ߬ ߘߎ߰ߟߊ ߘߐ߫ (ߞߊ߬ [$1 ߘߍ߬ߡߍ߲߬ߠߌ߲ ߞߐߜߍ] ߦߋ߫߸ ߖߐ߲߬ߛߊ߬ ߌ ߘߌ߫ ߞߌ߬ߓߊ߬ߙߏ߬ ߖߐ߲ߖߐ߲ ߛߐ߬ߘߐ߲߬). ߣߵߌ ߘߏ߲߬ ߞߍ߫ ߘߊ߫ ߦߊ߲߬ ߝߎ߬ߕߎ߲߬ߕߌ߬ ߓߟߏߡߊ߬߸ ߌ ߟߊ߫ ߛߏ߲߯ߓߊߟߊ߲ back ߛߐ߲߬ߞߌ߲߫.", "noarticletext": "ߛߓߍߟߌ߫ ߛߌ߫ ߕߍ߫ ߞߐߜߍ ߣߌ߲߭ ߞߊ߲߬ ߕߋ߲߫. ߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ [[Special:Search/{{PAGENAME}}|search for this page title]] ߕߐ߮ ߢߌߣߌ߲߫ ߠߊ߫ ߞߐߜߍ ߕߐ߭ ߟߎ߬ ߘߐ߫߸ [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]߸ ߥߟߊ߫ [{{fullurl:{{FULLPAGENAME}}|action=edit}} create this page].", "noarticletext-nopermission": "ߛߓߍߟߌ߫ ߛߌ߫ ߕߍ߫ ߞߐߜߍ ߣߌ߲߭ ߞߊ߲߬ ߕߋ߲߫.\nߌ ߘߌ߫ ߛߋ߫ [[Special:Search/{{PAGENAME}}|search for this page title]] ߢߌߣߌ߲߫ ߠߊ߫ ߞߐߜߍ ߕߐ߭ ߟߎ߬ ߘߐ߫߸ ߥߟߊ߫ [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs] ߞߏ߬ߣߌ߲߬ ߘߌ߬ߢߍ߬ ߞߍߣߍ߲߫ ߕߴߌ ߡߊ߬ ߞߐߜߍ߫ ߣߌ߲߬ ߠߊߞߊ߭ ߘߐ߫.", - "userpage-userdoesnotexist-view": "ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ \"$1\" ߛߙߍߘߍߦߊߣߍ߲߫ ߕߍ߫", + "userpage-userdoesnotexist-view": "ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ \"$1\" ߛߙߍߘߍߦߊߣߍ߲߫ ߕߍ߫.", "previewnote": "ߌ ߖߊ߲߬ߓߌ߬ߟߊ߬ ߞߏ߫ ߣߌ߲߬ ߦߋ߫ ߢߍߝߟߍߟߌ ߘߐߙߐ߲߫ ߠߋ߬ ߘߌ߫. ߌ ߟߊ߫ ߡߝߊ߬ߟߋ߲߬ߠߌ ߟߎ߫ ߡߊ߫ ߟߊߞߎ߲߬ߘߎ߬ ߝߟߐ߫ ߘߋ߬ ߹", "continue-editing": "ߥߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߞߣߍ ߞߊ߲߬", "editing": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ $1", "creating": "$1 ߛߌ߲ߘߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫", "editingsection": "(ߛߌ߰ߘߊ߬)$1 ߡߊߦߟߍ߬ߡߊ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫", "templatesused": "{{PLURAL:$1|ߞߙߊߞߏ|ߞߙߊߞߏ ߟߎ߫}} ߟߎ߫ ߟߊߓߊ߯ߙߊ߫ ߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫", - "template-protected": "ߊ߬ ߟߊߞߊ߲ߘߊߣߍ߲ ߠߋ߬", + "template-protected": "(ߊ߬ ߟߊߞߊ߲ߘߊߣߍ߲ ߠߋ߬)", "template-semiprotected": "(ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ-ߝߊ߲߬ߞߋ߬ߟߋ߲߬ߡߊ)", "hiddencategories": "ߞߐߜߍ ߣߌ߲߬ ߦߋ߫ ߢߌ߲߬ ߠߎ߫ ߛߌ߲߬ߝߏ߲ ߠߋ߬ ߘߌ߫{{PLURAL:$1|}}", "permissionserrors": "ߝߌ߬ߟߌ߫ ߘߌ߬ߢߍ߬ߒߧߋ", @@ -239,20 +265,22 @@ "content-model-wikitext": "ߥߞߌ߫ ߞߟߏߜߍ", "viewpagelogs": "ߞߐߜߍ ߣߌ߲߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߠߎ߬ ߦߋ߫", "currentrev-asof": "$1 ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲", - "revisionasof": "ߊ߬ ߡߊߛߊ߬ߦߌ߲ ߦߊ߲߬ ߓߊ߫", - "revision-info": "{{ߞߊ߬ߘߌ߬ߛߊ߬:$6|$2}} ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ $2", - "previousrevision": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߞߘߐ߬ߡߊ߲", + "revisionasof": "ߊ߬ ߡߊߛߊ߬ߦߌ߲ ߦߊ߲߬ ߓߊ߫ 1$", + "revision-info": "{{GENDER:$6|$2}} ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ $2", + "previousrevision": "→ ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߞߘߐ߬ߡߊ߲", "nextrevision": "ߡߊ߬ߛߋ߬ߦߌ߲߬ߣߍ߲߬ ߞߎߘߊ →", "currentrevisionlink": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲", "cur": "ߞߍߞߎߘߊ", "last": "ߢߍߕߊ", + "history-fieldset-title": "ߣߐ߬ߡߊ߬ߛߊߦߌ߲ ߠߎ߬ ߛߍ߲ߛߍ߲߫", "histfirst": "ߞߘߐ߬ߡߊ߲ ߠߎ߬", "histlast": "ߞߎߘߊ ߟߎ߬", "history-feed-title": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߘߐ߬ߝߐ", "history-feed-description": "ߞߐߜߍ ߣߌ߲߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߘߐ߬ߝߐ߸ ߥߞߌ ߘߐ߫", "rev-delundel": "ߊ߬ ߦߋߢߊ ߡߊߦߟߍ߬ߡߊ߲߫", "history-title": "$1 ߡߛߊ߬ߦߌ߲߬ߠߌ߲ ߘߐ߬ߝߐ", - "lineno": "$1: ߛߌ߬ߕߊߙߌ", + "lineno": "$1 ߛߌ߬ߕߊߙߌ", + "compareselectedversions": "ߘߟߊߡߌߘߊ߫ ߛߎߥߊ߲ߘߌߣߍ߲ ߠߎ߬ ߟߊߢߐ߲߯ߡߊ߫", "editundo": "ߊ߬ ߘߐߛߊ߬߸ ߊ߬ ߓߟߏߞߊ߬߸ ߊ߬ ߓߙߐߕߐ߫", "diff-empty": "ߝߊߙߊ߲ߝߊ߯ߛߌ߫ ߕߴߊ߬ߟߎ߬ ߕߍ߫", "searchresults": "ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ ߟߎ߬", @@ -270,13 +298,14 @@ "searchprofile-images-tooltip": "ߞߐߕߐ߮ ߟߎ߬ ߢߌߣߌ߲߫", "searchprofile-everything-tooltip": "ߊ߬ ߞߣߐߘߐ ߓߍ߯ ߢߌߣߌ߲߫ (ߤߊߟߌ߬ ߞߎߡߊߢߐ߲߯ߦߊ߫ ߞߐߜߍ ߟߎ߬)", "searchprofile-advanced-tooltip": "ߊ߬ ߢߌߣߌ߲߫ ߛߊ߲߬ߠߌ߲߬ߢߐ߲߮ ߠߎ߬ ߕߐ߮ ߞߣߍ ߘߐ߫", - "search-result-size": "$1 ({{PLURAL:$2|1 ߞߎߡߊߘߋ߲ |$2 ߞߎߡߊߘߋ߲ ߠߎ߬ }})", - "search-redirect": "ߌ ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߫ ߞߊ߬ ߓߐ߫ ߦߊ߲߬ $1", + "search-result-size": "$1 ({{PLURAL:$2|1 ߞߎߡߊߘߋ߲|$2 ߞߎߡߊߘߋ߲ ߠߎ߬}})", + "search-redirect": "(ߌ ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߫ ߞߊ߬ ߓߐ߫ ߦߊ߲߬ $1)", "search-section": "(ߕߍߕߍ߮ $1)", "search-suggest": "ߌ ߞߊ߲߫ ߦߋ߫ ߣߌ߲߬ ߠߋ߬ ߡߊ߬ $1", "searchall": "ߊ߬ ߓߍ߯", - "search-nonefound": "ߖߋ߬ߓߟߌ߬ ߛߌ߫ ߕߍ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߣߌ߲߫ ߞߊ߲߬", + "search-nonefound": "ߖߋ߬ߓߟߌ߬ ߛߌ߫ ߕߍ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߣߌ߲߫ ߞߊ߲߬.", "mypreferences": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ", + "group-sysop": "ߡߙߊ߬ߟߌ߬ߟߊ", "right-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫", "newuserlogpage": "ߖߊ߬ߕߋ߬ߘߊ߬ ߓߘߊ߫ ߟߊߞߊ߬ ߌ ߜߊ߲߬ߞߎ߲߬", "action-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬", @@ -284,6 +313,8 @@ "enhancedrc-history": "ߕߊ߬ߡߌ߲߬ߣߍ߲", "recentchanges": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߫ ߞߎߘߊ", "recentchanges-legend": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߟߊ߬ߓߍ߲߬ߢߐ߰ߡߦߊ߬ߘߊ", + "recentchanges-summary": "ߥߞߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎ߲ߓߊ ߡߍ߲ ߠߎ߬ ߞߍߣߍ߲߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߏ߬ ߟߎ߫ ߣߐ߬ߣߐ߬.", + "recentchanges-noresult": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߛߎߡߊ߲ߡߕߊ ߢߌ߲߬ ߠߎ߫ ߡߊ߬ ߕߎ߬ߡߊ߬ ߟߊߕߍ߰ߣߍ߲ ߦߌ߬ߘߊ ߘߐ߫.", "recentchanges-label-newpage": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߓߘߊ߫ ߘߐߜߍ߫ ߞߎߘߊ ߟߊߘߊ߲߫", "recentchanges-label-minor": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲ ߠߋ߫ ߦߋ߫", "recentchanges-label-bot": "ߡߐ߰ߡߐ߮ ߟߋ߫ ߣߐ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߣߌ߲߬ ߞߍ߫ ߟߊ߫", @@ -319,14 +350,15 @@ "recentchangeslinked-toolbox": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߫ ߓߌ߬ߟߊ߬ߢߐ߲߰ߡߊ", "recentchangeslinked-title": "ߊ߬ ߟߌ߬ߤߟߊ ߡߊߦߟߍ߬ߡߊ߲߫ ߦߊ߲߬$1", "recentchangeslinked-summary": "ߞߐߜߍ ߕߐ߮ ߟߊߘߏ߲߬߸ ߦߟߍ߬ߡߊ߲ ߡߍ߲ ߠߎ߬ ߘߏ߲߬ߣߍ߲߬ ߦߋ߫ ߞߐߜߍ ߟߎ߬ ߘߐ߫߸ ߥߟߊ߫ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫߸ ߞߵߏ߬ ߦߋ߫. (ߖߐ߲߬ߛߊ߬ ߌ ߘߌ߫ ߦߌߟߡߊ ߛߌ߲߬ߝߏ߲ ߠߎ߬ ߦߋ߫߸ {{ns:category}}: ߦߌߟߡߊ ߕߐ߮ ߟߊߘߏ߲߬).ߞߵߊ߬ ߦߟߍ߬ߡߊ߲߬ ߞߐߜߍ ߣߌ߲߬ [[Special:Watchlist|your Watchlist]] ߘߌ߫߸ ߏ߬ ߦߋ߫ ߛߓߍߘߋ߲߫ ߞߎ߲ߓߊ", - "recentchangeslinked-page": "ߘߐߜߍ ߕߐ߮", + "recentchangeslinked-page": "ߘߐߜߍ ߕߐ߮:", "upload": "ߞߐߕߐ߮ ߟߊߦߟߍ߬", "filedesc": "ߟߊߘߛߏߣߍ߲", + "license-header": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߴߌ ߘߐ߫", "imgfile": "ߞߐߕߐ߮", "listfiles": "ߞߐߕߐ߮ ߛߙߍߘߍ", "file-anchor-link": "ߞߐߕߐ߮", "filehist": "ߞߐߕߐ߮ ߟߊ߫ ߘߐ߬ߝߐ", - "filehist-help": "ߕߎ߬ߡߊ߬ߘߊ/ߕߎ߬ߡߊ ߛߐ߲߬ߞߌ߲߬ ߓߊ߫߸ ߞߊ߬ ߕߎ߬ߡߊ߬ߘߊ ߞߐߕߐ߮ ߟߎ߬ ߦߋ߫", + "filehist-help": "ߕߎ߬ߡߊ߬ߘߊ/ߕߎ߬ߡߊ ߛߐ߲߬ߞߌ߲߬ ߓߊ߫߸ ߞߊ߬ ߕߎ߬ߡߊ߬ߘߊ ߞߐߕߐ߮ ߟߎ߬ ߦߋ߫.", "filehist-current": "ߞߍߛߊ߲ߞߏ", "filehist-datetime": "ߕߎ߬ߡߊ߬ߘߊ/ߕߎ߬ߡߊ߬ߟߊ߲", "filehist-thumb": "ߞߝߊ߬ߟߋ߲ߛߋ߲", @@ -334,14 +366,15 @@ "filehist-dimensions": "ߛߎߡߊ߲ߘߐ", "filehist-comment": "ߞߊ߲߬ߝߐߟߌ", "imagelinks": "ߞߐߕߐ߮ ߟߊߓߊ߯ߙߊ", - "linkstoimage": "ߞߐߕߐ߮ ߣߌ߲߬ {{plural:$1|ߞߐߜߍ ߟߎ߬|$1 ߞߐߜߍ ߟߎ߬}}", + "linkstoimage": "ߞߐߕߐ߮ ߣߌ߲߬ {{PLURAL:$1|ߞߐߜߍ ߟߎ߬|$1 ߞߐߜߍ ߟߎ߬}}:", "nolinkstoimage": " ߞߐߜߍ߫ ߛߌ߫ ߡߊ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߓߊ߯ߙߊ߫ ߡߎߣߎ߲߬", "sharedupload-desc-here": "ߘߐ߬ߛߙߋ ߣߌ߲߬ ߦߋ߫ ߦߊ߲߬ ߠߋ߫ $1 ߖߊ߬ߕߋ߬ߘߐ߬ߛߌ߮ ߕߐ߭ ߟߎ߬ ߞߏ߬ߣߌ߲ ߘߌ߫ ߛߴߊ߬ ߟߊߓߊ߯ߙߊ߫ ߟߊ߫. ߊ߬ ߕߐ߯ ߛߓߍߟߌ ߦߙߐ $2 ߟߋ߬ ߦߋ߫ ߘߎ߰ߟߊ ߘߐ߫ ߣߌ߲߬.", "filepage-nofile": "ߕߐ߮ ߣߌ߲߬ ߞߐߕߐ߯ ߛߎ߯ ߕߍ߫ ߦߋ߲߬", - "upload-disallowed-here": "ߌ ߕߍߣߊ߬ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬ߛߓߍ߫ ߟߊ߫", + "upload-disallowed-here": "ߌ ߕߍߣߊ߬ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬ߛߓߍ߫ ߟߊ߫.", "randompage": "ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ", "statistics": "ߖߊ߬ߕߋ߬ߛߎ߬ߓߐ ߟߎ߬", "nbytes": "$1 {{PLURAL:$1|byte|bytes}}", + "nmembers": "$1 {{PLURAL:$1|ߛߌ߲߬ߝߏ߲ |members}}", "prefixindex": "ߞߐߜߍ߫ ߡߍ߲ ߠߎ߬ ߓߍ߯ ߟߊߝߟߐߣߍ߲߫...", "listusers": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߛߙߍߘߍ", "newpages": "ߘߐߜߍ߫ ߞߎߘߊ", @@ -352,6 +385,7 @@ "booksources-search": "ߢߌߣߌ߲ߠߌ߲", "specialloguserlabel": "ߞߍߓߊ߮ :", "log": "ߘߏ߲߬", + "logempty": "ߦߙߍߞߍߟߌ߫ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߝߐ߰ߓߍ ߟߎ߬ ߘߐ߫", "allpages": "ߞߐߜߍ ߟߎ߬ ߓߍ߯", "allarticles": "ߞߐߜߍ ߟߎ߬ ߓߍ߯", "allpagessubmit": "ߥߊ߫", @@ -373,32 +407,33 @@ "namespace": "ߕߐ߯ ߛߓߍ ߞߣߍ", "tooltip-invert": "ߞߏ߲߬ߘߏ ߣߌ߲߬ ߘߐߜߍ߫߸ ߞߊ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬ ߢߡߊߘߏ߲߰ ߞߐߜߍ ߟߎ߬ ߕߐ߯ ߞߣߍ߫ ߓߊߓߌ߬ߟߊ߬ߣߍ߲ ߘߐ߫ (ߊ߬ ߣߌ߫ ߕߐ߯ ߞߣߍ߫ ߓߟߏߘߏ߲߬ߣߍ߲ ߘߐߜߍߣߍ߲ ߠߎ߬)", "namespace_association": "ߕߐ߯ ߓߟߏߘߏ߲߬ߣߍ߲߫ ߢߐ߲߰ߓߟߏ", - "blanknamespace": "ߓߊߖߎߟߞߊ", - "contributions": "{{ߟߊߓߊ߯ߙߟߊ:$1|ߞߊ߬ߘߌ߬ߛߊ߬}} ߓߟߏߡߊߜߍ߲", + "blanknamespace": "ߓߊߖߎ", + "contributions": "{{GENDER:$1|ߞߊ߬ߘߌ߬ߛߊ߬}} ߓߟߏߡߊߜߍ߲", "contributions-title": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߡߍ߲ ߦߋ߫$1", "mycontris": "ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲", "anoncontribs": "ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߠߎ߬", - "contribsub2": " {{ߞߊ߬ߘߌ߬ߛߊ߬:$3|$1}} ߕߊ ($2)", + "contribsub2": "{{GENDER:$3|$1}} ߕߊ ($2)", "month": "ߞߵߊ߬ ߕߊ߬ ߞߊߙߏ ߡߊ߬ (ߊ߬ ߣߌ߫ ߞߊߙߏ ߞߎ߲߬ߝߟߐ ߘߐ߫)", "year": "ߞߵߊ߬ ߕߊ߬ ߞߊߙߏ ߡߊ߬ (ߊ߬ ߣߌ߫ ߞߊߙߏ ߞߎ߲߬ߝߟߐ ߡߊ߬)", "sp-contributions-newbies": "ߖߊ߬ߕߋ߬ߘߊ߬ ߞߎߘߊ ߟߎ߫ ߘߐߙߐ߲߫ ߠߊ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߦߌ߬ߘߊ߫ ߕߋ߲߬", "sp-contributions-uploads": "ߟߊ߬ߦߟߍ߬ߟߌ ߟߎ߬", "sp-contributions-talk": "ߞߎߡߊߢߐ߲߯ߦߊ", "sp-contributions-search": "ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߘߏ߫ ߢߌߣߌ߲߫", - "sp-contributions-username": "IP ߛߊ߲߬ߓߊ߬ߕߐ߮:ߥߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߕߐ߮", + "sp-contributions-username": "IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߥߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߕߐ߮:", "sp-contributions-newonly": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߍ߲ ߣߊ߬ ߞߐߜߍ߫ ߟߊߘߊ߲ ߘߌ߫߸ ߏ߬ ߟߎ߫ ߘߐߙߐ߲߫ ߦߌ߬ߘߊ߬", "sp-contributions-submit": "ߢߌߣߌ߲ߠߌ߲", "whatlinkshere": "ߛߘߌ߬ߜߋ߲ ߢߎ߬ߡߊ߲߬ ߦߋ߫ ߦߊ߲߬", "whatlinkshere-title": "ߞߐߜߍ ߡߍ߲ ߠߎ߫ ߛߘߌ߬ߣߍ߲߫ ߝߊ߲߭ ߣߌ߲߬ $1 ߡߊ߬", "whatlinkshere-page": "ߘߐߜߍ:", "linkshere": "ߞߐߜߍ ߟߎ߬ ߛߘߌ߬ߜߋ߲ ߡߍ߲ ߠߎ߬ ߦߋ߫ ߦߊ߲߬ $2:", - "nolinkshere": "ߞߐߜߍ߫ ߛߌ߫ ߟߎ߫ ߛߘߌ߬ߜߋ߲߬ ߕߍ߫ ߦߋ߲߬ $2", + "nolinkshere": "ߞߐߜߍ߫ ߛߌ߫ ߟߎ߫ ߛߘߌ߬ߜߋ߲߬ ߕߍ߫ ߦߋ߲߬ $2.", "isredirect": "ߞߎ߲߬ߕߋ߬ߟߋ߲߬ ߞߎߘߊ ߞߐߜߍ", "isimage": "ߞߐߕߐ߮ ߛߘߌ߬ߜߋ߲", "whatlinkshere-prev": "{{PLURAL:$1|ߢߝߍߕߊ ߟߎ߬|ߢߝߍߕߊ ߟߎ߬ $1}}", "whatlinkshere-next": "{{PLURAL:$1|ߢߍߕߊ|ߢߍߕߊ $1}}", - "whatlinkshere-links": "ߛߘߌ߬ߜߋ߲", + "whatlinkshere-links": "→ ߛߘߌ߬ߜߋ߲", "whatlinkshere-hideredirs": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߠߎ߬ $1", + "whatlinkshere-hidetrans": "ߟߊ߬ߘߏ߲߬ߘߐ߬ߟߌ ߓߊ߲ߓߊ߲ߣߍ߲", "whatlinkshere-hidelinks": "ߛߘߌ߬ߜߋ߲$1", "whatlinkshere-hideimages": "ߞߐߕߐ߮ ߛߘߌ߬ߜߋ߲$1", "whatlinkshere-filters": "ߢߡߊߘߏ߲߰ߣߍ߲", @@ -409,15 +444,15 @@ "export": "ߞߐߜߍ ߟߎ߬ ߟߊߝߏ߬ߦߌ߬", "thumbnail-more": "ߊ߬ ߟߊߞߎ߲߬ߓߦߊ߬", "tooltip-pt-userpage": "{{GENDER:|ߌ ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬}} ߞߐߜߍ", - "tooltip-pt-mytalk": "{{ߖߊ߲߬ߕߌ߮:|ߟߊ߫}} ߞߎߡߊ߫ ߞߐߜߍ", - "tooltip-pt-preferences": "{{ߞߊ߬ߘߌ߬ߛߊ߬:|ߌ}} ߤߣߍߕߊ ߟߎ߬", + "tooltip-pt-mytalk": "{{GENDER:|ߟߊ߫}} ߞߎߡߊ߫ ߞߐߜߍ", + "tooltip-pt-preferences": "{{GENDER:|ߌ}} ߤߣߍߕߊ ߟߎ߬", "tooltip-pt-watchlist": "ߌ ߟߊ߫ ߞߐߜߍ߫ ߡߊߦߟߍ߬ߡߊ߲߬ߕߊ ߜߋ߬ߟߎ߲߬ߣߍ߲ ߠߎ߬ ߛߙߍߘߍ", - "tooltip-pt-mycontris": "{{ߖߊ߲߬ߕߌ߮:| ߟߊ߫}} ߓߟߏߡߊߜߍ߲ ߠߎ߬", + "tooltip-pt-mycontris": "{{GENDER:|ߟߊ߫}} ߓߟߏߡߊߜߍ߲ ߠߎ߬", "tooltip-pt-login": "ߌ ߡߊߘߌߦߊߣߍ߲߫ ߦߴߌ ߜߊ߲߬ߞߎ߲߫ ߸ ߘߌߦߊߜߏߦߊ߫ ߞߏ߬ߣߌ߲߬ ߕߍ߫", "tooltip-pt-logout": "ߌ ߜߊ߲߬ߞߎ߲߬ ߓߐ߫", "tooltip-pt-createaccount": "ߌ ߡߊߘߌߦߊߣߍ߲߫ ߦߋ߫ ߖߊ߬ߕߋ߬ߘߊ߫ ߟߊߞߊ߬ ߞߵߌ ߜߊ߲߬ߞߎ߲߫ ߸ ߓߊ߬ߙߌ߬ ߌ ߘߌߦߊߜߏߦߊߣߍ߲߫ ߕߍ߫", "tooltip-ca-talk": "ߘߐ߬ߞߕߌ߬ߟߌ ߞߊ߬ ߓߍ߲߬ ߞߐߜߍ ߞߣߐߘߐ ߡߊ߬", - "tooltip-ca-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߝߊ߬ߟߋ߲߬", + "tooltip-ca-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬", "tooltip-ca-addsection": "ߛߌ߰ߘߊ߬ ߞߎߘߊ߫ ߘߊߡߌ߬ߣߊ߬", "tooltip-ca-viewsource": "ߞߐߜߍ ߣߌ߲߬ ߠߊߞߊ߲ߘߊߣߍ߲߫ ߠߋ߬.\nߌ ߘߌ߫ ߛߴߊ߬ ߛߎ߲ ߘߐߜߍ߫ ߟߊ߫", "tooltip-ca-history": "ߞߐߜߍ ߣߌ߲߬ ߛߊߞߍߟߌ߫ ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߫ ߘߐߜߍ߫", @@ -425,7 +460,7 @@ "tooltip-ca-delete": "ߞߐߜߍ ߣߌ߲߬ ߖߏ߰ߛߌ߫", "tooltip-ca-move": "ߘߐߜߍ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫", "tooltip-ca-watch": "ߞߐߜߍ ߣߌ߲߬ ߝߙߊ߬ ߌ ߟߊ߫ ߟߊߞߙߐ߬ߛߌ߬ߕߊ߬ ߛߙߍߘߍ ߟߎ߫ ߞߊ߲߬", - "tooltip-search": " {{ߞߍߦߙߐ ߕߐ߮}} ߊ߬ ߢߌߣߌ߲߫", + "tooltip-search": "ߊ߬ ߢߌߣߌ߲߫ {{SITENAME}} ߘߐ߫", "tooltip-search-go": "ߕߐ߮ ߣߌ߲߬ ߢߌߣߌ߲߫ ߞߐߜߍ߫ ߞߣߐ߫ ߣߴߊ߬ ߞߍ߫ ߘߊ߫ ߦߋ߲߬", "tooltip-search-fulltext": "ߞߎߡߊߘߋ߲߫ ߣߌ߲߬ ߞߐߜߍ߫ ߟߎ߫ ߢߌߣߌ߲߫", "tooltip-p-logo": "ߞߐߜߍ߫ ߓߏߟߏ߲ߘߊ ߡߊߝߍߣߍ߲߫", @@ -439,14 +474,15 @@ "tooltip-t-whatlinkshere": "ߥߞߌ߫ ߞߐߜߍ ߓߍ߯ ߛߘߌ߬ߜߋ߲ ߠߋ߬ ߦߋ߫ ߦߊ߲߬", "tooltip-t-recentchangeslinked": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߞߎߘߊ ߟߎ߬ ߞߐߜߍ߫ ߘߐ߫ ߡߍ߲ ߣߌ߫ ߞߐߜߍ ߣߌ߲߬ ߕߎ߲߰ߣߍ߲߫", "tooltip-feed-atom": "ߞߐߜߍ ߣߌ߲߬ ߝߕߌ߫ ߓߊߟߏ", - "tooltip-t-contributions": "{{ߞߊ߬ߘߌ߬ߛߊ߬:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ ߛߙߍߘߍ", + "tooltip-t-contributions": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ ߛߙߍߘߍ", + "tooltip-t-emailuser": " ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߟߊߕߊ߯ ߟߊߓߊ߯ߙߟߊ ߣߌ߲߬ ߡߊ߬{{GENDER:$1|ߟߊߓߊ߯ߙߟߊ(ߡߏ߬ߛߏ) }}", "tooltip-t-upload": "ߞߐߕߐ߮ ߟߎ߫ ߟߊߦߟߍ߬", "tooltip-t-specialpages": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߐߜߍ߫ ߟߎ߫ ߛߙߍߘߍ", "tooltip-t-print": " ߞߐߜߍ ߣߌ߲߬ ߜߌ߬ߙߌ߲߬ߘߌ߬ߕߊ߬ߡߊ ߛߎ߮", "tooltip-t-permalink": "ߞߐߜߍ ߣߌ߲߬ ߡߛߊ߬ߦߌ߲߬ߠߌ߲߬ ߛߘߌ߬ߜߋ߲߬ ߓߟߏߕߍ߰ߓߊߟߌ", "tooltip-ca-nstab-main": "ߞߐߜߍ ߞߣߐߘߐ ߘߐߜߍ߫", "tooltip-ca-nstab-user": "ߞߐߜߍ߫ ߟߊߓߊ߯ߙߕߊ ߘߐߜߍ߫", - "tooltip-ca-nstab-special": "ߣߌ߲߬ ߦߋ߫ ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߠߋ߬ ߘߌ߫߸ ߊ߬ ߕߍ߫ ߛߋ߫ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫.", + "tooltip-ca-nstab-special": "ߣߌ߲߬ ߦߋ߫ ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߠߋ߬ ߘߌ߫߸ ߊ߬ ߕߍ߫ ߛߋ߫ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫", "tooltip-ca-nstab-project": "ߖߊ߬ߕߋ߬ߘߐ߬ߛߌ߰ ߞߐߜߍ ߘߐߜߍ߫", "tooltip-ca-nstab-image": "ߞߐߕߐ߮ ߞߐߜߍ ߟߎ߫ ߘߐߜߍ߫", "tooltip-ca-nstab-mediawiki": "ߞߊ߲ߞߋ ߗߋߛߓߍ ߘߐߜߍ߫", @@ -484,15 +520,16 @@ "file-nohires": "ߢߊߓߐߣߍ߲ ߛߊ߲ߘߐߕߊ߫ ߜߘߍ߫ ߕߍ߫ ߦߋ߲߬", "show-big-image": "ߞߐߕߐ߮ ߓߊߛߎ߲", "show-big-image-preview": "ߊ߬ ߢߍߦߋߟߌ ߢߊ߲ߞߊ߲$1", - "show-big-image-other": "{{PLURAL:$2|ߢߊߓߐߟߌ|ߢߊߓߐߟߌ ߟߎ߬}} ߕߐ߬ߡߊ $1", + "show-big-image-other": "{{PLURAL:$2|ߢߊߓߐߟߌ|ߢߊߓߐߟߌ ߟߎ߬}} ߕߐ߬ߡߊ $1.", "show-big-image-size": "$1 × $2 ߖߌ߬ߦߊ߬ߘߊ߲ߕߊ", "metadata": "ߡߋߕߊߘߊ߯ߕߊ߫", "metadata-fields": "Image metadata fields listed in this message will be included on image page display when the metadata table is collapsed.\nOthers will be hidden by default.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude", - "namespacesall": "ߓߍ߯", + "namespacesall": "ߊ߬ ߓߍ߯", "monthsall": "ߡߎ߰ߡߍ", "imgmultipagenext": "ߞߐߜߍ ߢߍߕߊ", "imgmultigo": "ߥߊ߫", "imgmultigoto": "ߥߊ߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬$1", + "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|talk]])", "redirect-submit": "ߕߊ߯", "redirect-lookup": "ߊ߬ ߘߐߜߍ߫", "redirect-value": "ߡߐ߬ߟߐ߲", @@ -501,18 +538,18 @@ "redirect-revision": "ߞߐߜߍ ߣߐ߬ߡߊ߬ߛߊ߬ߦߌ߬ ߝߙߍߕߍ", "redirect-file": "ߞߐߕߐ߯ ߕߐ߮", "specialpages": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߘߐߜߍ", - "tag-filter": "[[Special:Tags|Tag]] ߢߡߊߘߏ߲߰ߣߍ߲", - "tag-list-wrapper": "[[ߛߐ߲߬ߞߌ߲߬ߠߌ߲߬: ߓߟߏߡߊߞߊ߬ߣߍ߲|{{PLURAL:$1|ߛߐ߲߬ߞߌ߲߬ߠߌ߲|ߛߐ߲߬ߞߌ߲߬ߠߌ߲ ߠߎ߬}}]]: $2", + "tag-filter": "[[Special:Tags|Tag]] ߢߡߊߘߏ߲߰ߣߍ߲:", + "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Tag|Tags}}]]: $2", "tags-active-yes": "ߐ߲߬ߐ߲߬ߐ߲߫", "tags-active-no": "ߍ߲߬ߍ߲ߍ߲߬", - "tags-hitcount": "$1{{PLURAL:$1|ߦߟߍ߬ߡߊ߲߬ߠߌ|ߦߟߍ߬ߡߊ߲߬ߠߌ ߠߎ߬ }}", - "logentry-delete-delete": "$1 {{ߞߊ߬ߘߌ߬ߛߊ߫:$2|ߖߏ߰ߛߌ߬ߣߍ߲}} ߞߐߜߍ$3", + "tags-hitcount": "$1 {{PLURAL:$1|ߦߟߍ߬ߡߊ߲߬ߠߌ|ߦߟߍ߬ߡߊ߲߬ߠߌ ߠߎ߬}}", + "logentry-delete-delete": "$1 {{GENDER:$2|ߖߏ߰ߛߌ߬ߣߍ߲}} ߞߐߜߍ$3", "revdelete-content-hid": "ߞߣߐߘߐ ߘߐ߲߰ߣߍ߲߫ ߠߋ߬", - "logentry-move-move": "$1 {{ߞߊ߬ߘߌ߬ߛߊ߬:$2|ߓߘߊ߫ ߞߐߜߍ}} ߓߐ߫ ߦߊ߲߬ $3 ߞߴߊ߬ ߟߊߕߊ߯ $4", - "logentry-move-move-noredirect": "$1 {{ߞߊ߬ߘߌ߬ߛߊ߬:$1|ߓߘߴߊ߬ ߓߐ߫ ߦߋ߲߬}} ߞߐߜߍ ߣߌ߲߬ $3 ߞߊ߬ ߥߴߊ߬ ߘߌ߫ $4 ߞߵߊ߬ ߕߘߍ߬ ߊ߬ ߡߴߊ߬ ߟߊߞߎ߲߬ߛߌ߲߫", - "logentry-newusers-create": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ $1 ߕߘߍ߬ ߦߋ߫ {{ߞߊ߬ߘߌ߬ߛߊ߬:$2|ߕߊ ߟߋ߬ ߘߌ߫}}", + "logentry-move-move": "$1 {{GENDER:$2|ߓߘߊ߫ ߞߐߜߍ}} ߓߐ߫ ߦߊ߲߬ $3 ߞߴߊ߬ ߟߊߕߊ߯ $4", + "logentry-move-move-noredirect": "$1 {{GENDER:$1|ߓߘߴߊ߬ ߓߐ߫ ߦߋ߲߬}} ߞߐߜߍ ߣߌ߲߬ $3 ߞߊ߬ ߥߴߊ߬ ߘߌ߫ $4 ߞߵߊ߬ ߕߘߍ߬ ߊ߬ ߡߴߊ߬ ߟߊߞߎ߲߬ߛߌ߲߫", + "logentry-newusers-create": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ $1 ߕߘߍ߬ ߦߋ߫ {{GENDER:$2|ߕߊ ߟߋ߬ ߘߌ߫}}", "logentry-newusers-autocreate": "ߟߊߓߊ߯ߙߊߟߊ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ $1{{GENDER:$2|ߟߊߘߊ߲߫ ߘߊ߫ }} ߞߍߒߖߘߍߦߋ߫ ߓߟߏߡߊ߬", - "logentry-upload-upload": "$1 {{ߞߊ߬ߘߌ߬ߛߊ߫:$2|ߟߊ߬ߦߟߍ߬ߟߌ߬ߣߐ ߟߋ߬}} $3", - "searchsuggest-search": " {{SITENAME}} ߊ߬ ߢߌߣߌ߲߫", + "logentry-upload-upload": "$1 {{GENDER:$2|ߟߊ߬ߦߟߍ߬ߟߌ߬ߣߐ ߟߋ߬}} $3", + "searchsuggest-search": "{{SITENAME}} ߊ߬ ߢߌߣߌ߲߫", "duration-days": "$1 {{PLURAL:$1|ߟߏ߲|ߟߏ߲ ߠߎ߬}}" } diff --git a/languages/i18n/ps.json b/languages/i18n/ps.json index 7148e91dcb..712ce2266e 100644 --- a/languages/i18n/ps.json +++ b/languages/i18n/ps.json @@ -788,7 +788,7 @@ "revdelete-text-file": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.", "logdelete-text": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.", "revdelete-text-others": "نور پازوالان به لا هم د پټ راز محتوياتو ته لاسرسی ومومي او دا یې له منځه یوسي، مګر که نه بل ډول مشخص شوی.", - "revdelete-confirm": "لطفا دا تایید کړئ چې تاسو دا کار کول غواړئ، دا چې تاسو پایلې په پام کې لرئ او تاسو یې سره مطابقت کوئ[[{{MediaWiki:Policy-url}}|پالیسۍ]].", + "revdelete-confirm": "لطفا دا تایید کړئ چې تاسو دا کار کول غواړئ، تاسو پایلې په پام کې لرئ او [[{{MediaWiki:Policy-url}}|پالیسۍ]] ته مو هم فکر دی.", "revdelete-legend": "د ښکارېدنې محدوديتونه ټاکل", "revdelete-hide-text": "د مخکتنې متن", "revdelete-hide-image": "د دوتنې مېنځپانگه پټول", diff --git a/languages/i18n/sah.json b/languages/i18n/sah.json index 0ad8cb89aa..2fc69e26df 100644 --- a/languages/i18n/sah.json +++ b/languages/i18n/sah.json @@ -64,6 +64,7 @@ "tog-norollbackdiff": "Төннөрүү кэнниттэн барыллар уратыларын көрдөрүмэ", "tog-useeditwarning": "Уларытыыларбын бигэргэппэккэ сирэйтэн тахсаары гыннахпына сэрэтээр", "tog-prefershttps": "Манна киирэргэ куруук көмүскэллээх холбонууну туттарга", + "tog-showrollbackconfirmation": "Сигэни баттаатахха дьайыыга бигэргэтиини көрдөр", "underline-always": "Куруук", "underline-never": "Аннынан тардыма", "underline-default": "Браузер туруоруутунан", @@ -448,6 +449,7 @@ "badretype": "Аһарыктарыҥ сөп түбэспэтилэр.", "usernameinprogress": "Бу аатынан бэлиэтэнии бара турар.\nБука диэн кэтэһэ түс.", "userexists": "Суруйбут аатыҥ бэлиэр баар.\nБука диэн, атын аатта тал.", + "createacct-normalization": "Эн бэлиэтэммит аатыҥ техника хааччаҕын учуоттаан маннык буолуо «$2».", "loginerror": "Ааккын система билбэтэ", "createacct-error": "Бэлиэтэнии кэмигэр алҕас таҕыста", "createaccounterror": "Саҥа аат бэлиэтиир кыах суох: $1", @@ -559,7 +561,8 @@ "resetpass-abort-generic": "Аһарыгы уларытыыны кэҥэтии тохтотто.", "resetpass-expired": "Аһарыгыҥ болдьоҕо ааспыт эбит. Бука диэн, саҥа аһарыкта туруорун.", "resetpass-expired-soft": "Аһарыгыҥ болдьоҕо бүппүт, онон уларытыллыахтаах эбит. Бука диэн атын аһарыкта суруй эбэтэр маны баттаан кэлин киллэрээр \"{{int:authprovider-resetpass-skip-label}}\".", - "resetpass-validity-soft": "Аһарыгыҥ алҕастаах: $1\n\nБука диэн саҥа аһарыкта суруй эбэтэр кэлин киллэриэххин баҕарар буоллаххына маны баттаа \"{{int:authprovider-resetpass-skip-label}}\"", + "resetpass-validity": "Аһарыгыҥ алҕастаах: $1\n\nСаҥа аһарыкта туруорун дуу.", + "resetpass-validity-soft": "Аһарыгыҥ алҕастаах: $1\n\nБука диэн саҥа аһарыкта суруй эбэтэр кэлин суруйуоххун баҕарар буоллаххына маны баттаа \n\"{{int:authprovider-resetpass-skip-label}}\"", "passwordreset": "Аһарыгы саҥаттан", "passwordreset-text-one": "Урукку аһарыгы уларытарга бу форманы толор.", "passwordreset-text-many": "{{PLURAL:$1|Быстах аһарыгы электрон почтаҕар ыыттарарга түннүктэртэн биирдэстэригэр суруй.}}", @@ -923,6 +926,7 @@ "diff-paragraph-moved-toold": "Параграф көһөрүллүбүт. Баттаан урукку сиригэр көс.", "difference-missing-revision": "$2 барыл бу тэҥнээһиҥҥэ ($1) көстүбэтэ.\n\nБу үксүн хайыы-үйэ сотуллубут сирэйи кытта тэҥнээри эргэрбит сигэнэн кэллэххэ баар буолааччы.\nСиһилии баҕар [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} сотуу сурунаалыгар] баара буолуо.", "searchresults": "Булулунна", + "search-filter-title-prefix": "Мантан саҕаланар «$1» сирэйдэри эрэ көрдөө", "search-filter-title-prefix-reset": "Сирэйдэри барытын көрдөөһүн", "searchresults-title": "Көрдөөһүн түмүгэ \"$1\"", "titlematches": "Ыстатыйалар ааттара хоһулаһар", @@ -1014,8 +1018,8 @@ "stub-threshold-disabled": "Арахсыбыт", "recentchangesdays": "Хас хонук иһинэн уларытыылары көрдөрөргө:", "recentchangesdays-max": "(улааппыта $1 күн)", - "recentchangescount": "Саҥа уларытыылар көрдөрүллэр ахсааннара:", - "prefs-help-recentchangescount": "Бу саҥа көннөрүүлэри, сирэй устуоруйаларын уонна сурунааллары көрдөрөр.", + "recentchangescount": "Саҥа уларытыылар испииһэктэригэр, сирэй устуоруйатыгар уонна сурунаалларга көрдөрүллэр уларытыылар ахсааннара:", + "prefs-help-recentchangescount": "Улааппыта: 1000", "prefs-help-watchlist-token2": "Бу кэтиир испииһэгиҥ ситим-ханаалын кистэлэҥ күлүүһэ.\nБу күлүүһүнэн ким баҕарар эн испииһэккин көрүөн сөп, онон кимиэхэ да биэримэ. Хаһан баҕарар [[Special:ResetTokens|маны баттаан уларытыаххын]] сөп.", "savedprefs": "Эн туруорууларыҥ олохтоннулар.", "savedrights": "{{GENDER:$1|$1}} кыттааччы бөлөҕө бигэргэннэ.", @@ -1043,6 +1047,7 @@ "default": "чопчу ыйыллыбатаҕына маннык", "prefs-files": "Билэлэр", "prefs-custom-css": "Бэйэ CSS", + "prefs-custom-json": "Тус бэйэ JSON-а", "prefs-custom-js": "Бэйэ JS", "prefs-common-config": "Бары тиэмэлэргэ биир CSS/JS", "prefs-reset-intro": "Бу сирэй көмөтүнэн туруорууларгын саҥаттан туруорар турукка төннөрүөххүн сөп.\nМаны бигэргэттэххинэ билигин баар туруоруулары дэбигис сөргүппэккин.", @@ -1088,6 +1093,7 @@ "prefs-displaywatchlist": "Көстүүтүн туруоруулара", "prefs-changesrc": "Көстүбүт уларытыылар", "prefs-changeswatchlist": "Көрдөр;ллэр уларытыылар", + "prefs-pageswatchlist": "Кэтэбилгэ сылдьар сирэйдэр", "prefs-tokenwatchlist": "Токен", "prefs-diffs": "Уратылара", "prefs-help-prefershttps": "Аныгыскы киириигэр үлэлиир буолуо.", @@ -1125,6 +1131,7 @@ "group-autoconfirmed": "Аптамаатынан бигэргэтиллибит кыттааччылар", "group-bot": "Роботтар", "group-sysop": "Дьаһабыллар", + "group-interface-admin": "Алтыһаан дьаһабыллара", "group-bureaucrat": "Бюрокрааттар", "group-suppress": "Ревизордар", "group-all": "(бары)", @@ -1132,16 +1139,18 @@ "group-autoconfirmed-member": "{{GENDER:$1|аптамаатынан бигэргэтиллибит кыттааччы}}", "group-bot-member": "{{GENDER:$1|робот}}", "group-sysop-member": "{{GENDER:$1|дьаһабыл}}", + "group-interface-admin-member": "{{GENDER:$1|алтыһаан дьаһабыла}}", "group-bureaucrat-member": "{{GENDER:$1|бүрэкирээт}}", "group-suppress-member": "{{GENDER:$1|ревизор}}", "grouppage-user": "{{ns:project}}:Кыттааччылар", "grouppage-autoconfirmed": "{{ns:project}}:Аптамаатынан бигэргэммит кыттааччылар", "grouppage-bot": "{{ns:project}}:Роботтар", "grouppage-sysop": "{{ns:project}}:Дьаһабыллар", + "grouppage-interface-admin": "{{ns:project}}:Алтыһаан дьаһабыллара", "grouppage-bureaucrat": "{{ns:project}}:Бюрокрааттар", "grouppage-suppress": "{{ns:project}}:Ревизордар", "right-read": "Сирэйдэри көрүү", - "right-edit": "Сирэйдэри уларытыы", + "right-edit": "Уларытыы", "right-createpage": "Сирэйдэри оҥоруу (ырытыы сирэйдэриттэн ураты)", "right-createtalk": "Ырытыы сирэйдэрин оҥоруу", "right-createaccount": "Саҥа кыттааччыны бэлиэтээһин", @@ -1158,7 +1167,7 @@ "right-reupload-own": "Билэлэри суруттарбыт киһи бэйэтэ иккистээн суруттарыыта", "right-reupload-shared": "Уопсай ыскылаат билэлэрин локальнай ыскылаат билэлэринэн уларытыы", "right-upload_by_url": "URL аадырыстан билэлэри киллэрии", - "right-purge": "Кээһи бигэргэтэр сирэйэ суох ыраастааһын", + "right-purge": "Сирэй кээһин ыраастааһын", "right-autoconfirmed": "IP түргэнигэр олоҕурбут хааччахтан тутулуктаныма", "right-bot": "аптамаат быһыытынан ааҕыллар", "right-nominornewtalk": "Ырытыы сирэйдэригэр кыра көннөрүүлэр суох буоллахтарына саҥа этии эрэсиимэ холбонор", @@ -1188,7 +1197,11 @@ "right-editusercss": "Атын кыттааччылар CSS-билэлэрин уларытыы", "right-edituserjson": "Атын кыттааччылар JSON-билэлэрин уларытыы", "right-edituserjs": "Атын кыттааччылар JS-билэлэрин уларытыы", + "right-editsitecss": "CSS-билэлэри уларытыы", + "right-editsitejson": "JSON-билэлэри уларытыы", + "right-editsitejs": "JavaScript-билэлэри уларытыы", "right-editmyusercss": "Кыттааччы CSS-билэтин уларытыы", + "right-editmyuserjson": "Тус бэйэ JSON-билэлэрин уларытыы", "right-editmyuserjs": "Бэйэ JavaScript-билэлэрин уларытыы", "right-viewmywatchlist": "Бэйэ кэтиир тиһигин көрүү", "right-editmywatchlist": "Бэйэ кэтиир тиһигин уларытыы. Болҕой, сорох дьайыыларыҥ бу быраабы биэрбэтэҕиҥ да иһин сирэйдэри тиһиккэ эбиэхтэрин сөп.", @@ -1302,6 +1315,14 @@ "action-changetags": "ханнык баҕарар тиэктэри сурунаал биирдиилээн уларытыыларыгар уонна суруктарыгар эбэри уонна сотору көҥүллээ", "action-deletechangetags": "тиэктэри билии олоҕуттан сотуу", "action-purge": "сирэй кээһин ыраастааһын", + "action-bigdelete": "уһун устуоруйалаах сирэйдэри сотуу", + "action-blockemail": "эл. суругу ыытары бобуу", + "action-bot": "аптамаат быһыытынан ааҕыллар", + "action-editinterface": "кыттааччы алтыһаанын уларытыы", + "action-editusercss": "атын кыттааччылар CSS-билэлэрин уларытыы", + "action-edituserjson": "атын кыттааччылар JSON-билэлэрин уларытыы", + "action-edituserjs": "Атын кыттааччылар JavaScript-билэлэрин уларытыы", + "action-editsitecss": "ситим-сир CSS-билэлэрин уларытыы", "nchanges": "$1 {{PLURAL:$1|уларытыы|уларытыылар}}", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|тиһэх сылдьыыгыттан}}", "enhancedrc-history": "устуоруйата", diff --git a/languages/i18n/sr-ec.json b/languages/i18n/sr-ec.json index 995667cf47..c262c7081d 100644 --- a/languages/i18n/sr-ec.json +++ b/languages/i18n/sr-ec.json @@ -429,7 +429,7 @@ "yourpasswordagain": "Поново унеси лозинку:", "createacct-yourpasswordagain": "Потврдите лозинку", "createacct-yourpasswordagain-ph": "Поново унесите лозинку", - "userlogin-remembermypassword": "Остави ме пријављеног/у", + "userlogin-remembermypassword": "Не одјављуј ме", "userlogin-signwithsecure": "Користите безбедну везу", "cannotlogin-title": "Пријава није могућа", "cannotlogin-text": "Пријава није могућа", @@ -726,7 +726,7 @@ "copyrightwarning": "Имајте на уму да се сви доприноси на овом викију сматрају као објављени под лиценцом $2 (више на $1).\nАко не желите да се ваши текстови мењају и размењују без ограничења, онда их не шаљите овде.
\nИсто тако обећавате да сте Ви аутор текста, или да сте га умножили са извора који је у јавном власништву.\nНе шаљите радове заштићене ауторским правима без дозволе!", "copyrightwarning2": "Имајте на уму да се сви доприноси на овом викију могу мењати, враћати или брисати од других корисника.\nАко не желите да се ваши текстови слободно мењају и расподељују, не шаљите их овде.
\nИсто тако обећавате да сте ви аутор текста, или да сте га умножили с извора који је у јавном власништву (више на $1).\nНе шаљите радове заштићене ауторским правима без дозволе!", "editpage-cannot-use-custom-model": "Модел садржаја ове странице се не може променити.", - "longpageerror": "Грешка: текст који сте унели је величине {{PLURAL:$1|један килобајт|$1 килобајта}}, што је веће од {{PLURAL:$2|дозвољеног једног килобајта|дозвољена $2 килобајта|дозвољених $2 килобајта}}.\nСтраница не може бити сачувана.", + "longpageerror": "Грешка: текст који сте проследили је величине {{PLURAL:$1|један килобајт|$1 килобајта}}, што је веће од {{PLURAL:$2|дозвољеног једног килобајта|дозвољена $2 килобајта|дозвољених $2 килобајта}}.\nСтраница не може бити сачувана.", "readonlywarning": "Упозорење: база података је закључана ради одржавања, тако да тренутно нећете моћи да сачувате измене.\nМожда бисте желели сачувати текст за касније у некој текстуалној датотеци.\n\nСистемски администратор је навео следеће објашњење: $1", "protectedpagewarning": "Упозорење: Ова страница је заштићена, тако да само корисници са администраторским овлашћењима могу да је уређују.\nНајновији унос у дневнику је наведен испод као референца:", "semiprotectedpagewarning": "Напомена: Ова страница је заштићена, тако да само аутоматски потврђени корисници могу да је уређују.\nНајновији унос у дневнику је наведен испод као референца:", @@ -1566,7 +1566,7 @@ "upload": "Отпремање датотеке", "uploadbtn": "Отпреми датотеку", "reuploaddesc": "Назад на образац за отпремање", - "upload-tryagain": "Пошаљи измењени опис датотеке", + "upload-tryagain": "Проследи измењени опис датотеке", "upload-tryagain-nostash": "Пошаљите ре-отпремљену датотеку и измењен опис", "uploadnologin": "Нисте пријављени", "uploadnologintext": "$1 да бисте отпремали датотеке.", @@ -1598,7 +1598,7 @@ "filetype-unwanted-type": "„.$1“ је непожељан тип датотеке.\n{{PLURAL:$3|Пожељан тип датотеке је|Пожељни типови датотека су}} $2.", "filetype-banned-type": "„.$1“ {{PLURAL:$4|није допуштен тип датотеке|нису допуштени типови датотека}}.\n{{PLURAL:$3|Дозвољен тип датотеке је|Дозвољени типови датотека су}} $2.", "filetype-missing": "Ова датотека нема проширење (нпр. „.jpg“).", - "empty-file": "Послата датотека је празна.", + "empty-file": "Датотека коју сте проследили је празна.", "file-too-large": "Послата датотека је превелика.", "filename-tooshort": "Назив датотеке је прекратак.", "filetype-banned": "Овај тип датотеке је забрањен.", @@ -2162,7 +2162,7 @@ "emailnotarget": "Непостојеће или наважеће корисничко име примаоца.", "emailtarget": "Унос корисничког имена примаоца", "emailusername": "Корисничко име:", - "emailusernamesubmit": "Пошаљи", + "emailusernamesubmit": "Проследи", "email-legend": "Слање е-поруке кориснику/ци пројекта {{SITENAME}}", "emailfrom": "Од:", "emailto": "За:", @@ -2267,6 +2267,8 @@ "deleting-subpages-warning": "Упозорење: Страница коју желите избрисати има [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|подстраницу|$1 подстранице|$1 подстраница|51=преко 50 подстраница}}]].", "rollback": "Врати измене", "rollback-confirmation-confirm": "Потврдите:", + "rollback-confirmation-yes": "Врати", + "rollback-confirmation-no": "Откажи", "rollbacklink": "врати", "rollbacklinkcount": "врати $1 {{PLURAL:$1|измену|измене|измена}}", "rollbacklinkcount-morethan": "врати више од $1 {{PLURAL:$1|измене|измене|измена}}", @@ -2409,7 +2411,9 @@ "mycontris": "Доприноси", "anoncontribs": "Доприноси", "contribsub2": "За {{GENDER:$3|$1}} ($2)", + "contributions-subtitle": "За {{GENDER:$3|$1}}", "contributions-userdoesnotexist": "Кориснички налог „$1“ није отворен.", + "negative-namespace-not-supported": "Именски простори са негативним вредностима нису подржани.", "nocontribs": "Нису пронађене промене које одговарају овим критеријумима.", "uctop": "тренутна", "month": "од месеца (и раније):", @@ -2513,7 +2517,9 @@ "blocklist-userblocks": "Сакриј блокаде налога", "blocklist-tempblocks": "Сакриј привремене блокаде", "blocklist-addressblocks": "Сакриј појединачне блокаде IP-а", - "blocklist-type-opt-sitewide": "На новоу сајта", + "blocklist-type": "Тип:", + "blocklist-type-opt-all": "Све", + "blocklist-type-opt-sitewide": "На нивоу сајта", "blocklist-type-opt-partial": "Делимично", "blocklist-rangeblocks": "Сакриј блокаде опсега", "blocklist-timestamp": "Временска ознака", @@ -3176,7 +3182,7 @@ "watchlistedit-normal-done": "{{PLURAL:$1|1=Једна страница је уклоњена|$1 странице су уклоњене|$1 страница је уклоњено}} с вашег списка надгледања:", "watchlistedit-raw-title": "Уређивање необрађеног списка надгледања", "watchlistedit-raw-legend": "Уређивање необрађеног списка надгледања", - "watchlistedit-raw-explain": "Наслови са списка надгледања су приказани испод и могу се уређивати додавањем или уклањањем ставки са списка;\nједан наслов по реду.\nКада завршите, кликните на „{{int:Watchlistedit-raw-submit}}“.\nМожете да [[Special:EditWatchlist|користите и обичан уређивач]].", + "watchlistedit-raw-explain": "Наслови са списка надгледања су приказани испод и могу се уређивати додавањем или уклањањем ставки са списка;\nједан наслов по реду.\nКада завршите, кликните на „{{int:Watchlistedit-raw-submit}}”.\nМожете да [[Special:EditWatchlist|користите и стандардни уређивач]].", "watchlistedit-raw-titles": "Наслови:", "watchlistedit-raw-submit": "Ажурирај списак", "watchlistedit-raw-done": "Ваш списак надгледања је ажуриран.", @@ -3463,7 +3469,7 @@ "htmlform-int-toolow": "Наведена вредност је испод минимума од $1", "htmlform-int-toohigh": "Наведена вредност је изнад максимума од $1", "htmlform-required": "Ова вредност је обавезна.", - "htmlform-submit": "Постави", + "htmlform-submit": "Проследи", "htmlform-reset": "Врати промене", "htmlform-selectorother-other": "Друго", "htmlform-no": "Не", diff --git a/languages/i18n/sw.json b/languages/i18n/sw.json index 7857deff10..8f8dfd94b0 100644 --- a/languages/i18n/sw.json +++ b/languages/i18n/sw.json @@ -21,7 +21,8 @@ "Muddyb", "Fitoschido", "Rance", - "Vlad5250" + "Vlad5250", + "Yasen igra" ] }, "tog-underline": "Wekea mstari viungo:", @@ -46,7 +47,7 @@ "tog-enotifminoredits": "Pia nitumie barua pale mabadiliko ya ukurasa yanapokuwa madogo tu.", "tog-enotifrevealaddr": "Onyesha anwani ya barua pepe yangu katika barua pepe za taarifa", "tog-shownumberswatching": "Onyesha idadi ya watumiaji waangalizi", - "tog-oldsig": "Sahihi iliyopo:", + "tog-oldsig": "Sahihi iliyopo yenu:", "tog-fancysig": "Weka sahihi tu (bila kujiweka kiungo yenyewe)", "tog-uselivepreview": "Tumia kihakikio cha papohapo", "tog-forceeditsummary": "Nishtue pale ninapoingiza muhtasari mtupu wa kuhariri", @@ -287,6 +288,7 @@ "nstab-template": "Kigezo", "nstab-help": "Msaada", "nstab-category": "Jamii", + "mainpage-nstab": "Mwanzo", "nosuchaction": "Kitendo hiki hakipo", "nosuchactiontext": "Haiwezikani kutenda kitendo kilichoandikwa kwenye KISARA.\nLabda ulikosea kuandika KISARA, au kiungo ulichofuata ina kasoro.\nAu labda kuna hitilafu kwenye programu inayotumika na {{SITENAME}}.", "nosuchspecialpage": "Ukurasa maalum huu hakuna", @@ -536,6 +538,7 @@ "minoredit": "Haya ni mabadiliko madogo", "watchthis": "Fuatilia ukurasa huu", "savearticle": "Hifadhi ukurasa", + "savechanges": "Hifadhi mabadiliko", "preview": "Hakiki", "showpreview": "Onyesha hakikisho la mabadiliko", "showdiff": "Onyesha mabadiliko", @@ -1103,6 +1106,7 @@ "recentchanges-label-plusminus": "Ukubwa ukurasa kubadilishwa na hii idadi ya baiti", "recentchanges-legend-heading": "Simulizi:", "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (pia tazama [[Special:NewPages|orodha ya kurasa mpya]])", + "rcfilters-filter-editsbyself-description": "Michango yenu.", "rcnotefrom": "Hapo chini {{PLURAL:$5|is the change|yaonekana mabadiliko}} tangu $3,$4 (hadi $1tunaonyesha).", "rclistfrom": "Onyesha mabadiliko mapya kuanzia $3 $2", "rcshowhideminor": "$1 mabadiliko madogo", @@ -1741,6 +1745,7 @@ "contributions": "Michango ya {{GENDER:$1|mtumiaji}}", "contributions-title": "Michango ya mtumiaji $1", "mycontris": "Michango", + "anoncontribs": "Michango", "contribsub2": "Kwa {{GENDER:$3|$1}} ($2)", "nocontribs": "Mabadiliko yanayolingana na vigezo vilivyoulizwa hayakupatikana.", "uctop": "ya kisasa", @@ -1778,6 +1783,7 @@ "whatlinkshere-hidelinks": "$1 viungo", "whatlinkshere-hideimages": "Viungo vya faili $1", "whatlinkshere-filters": "Machujio", + "whatlinkshere-submit": "Nenda", "block": "Kumzuia mtumiaji", "unblock": "Kuacha kumzuia mtumiaji", "blockip": "Zuia mtumiaji", @@ -2121,6 +2127,7 @@ "imgmultipagenext": "ukurasa ujao →", "imgmultigo": "Nenda!", "imgmultigoto": "Uende kwenye ukurasa wa $1", + "img-lang-go": "Enda", "ascending_abbrev": "pand", "descending_abbrev": "shuk", "table_pager_next": "Ukurasa ujao", @@ -2171,6 +2178,7 @@ "version-software-version": "Toleo", "version-entrypoints-header-url": "KISARA Kioneshi Sanifu Raslimali", "redirect-submit": "Nenda", + "redirect-file": "Jina la faili", "fileduplicatesearch": "Tafuta mafaili ya nakili", "fileduplicatesearch-summary": "Kutafuta mafaili ya nakili kwa kuzingatia thamani za reli.", "fileduplicatesearch-filename": "Jina la faili:", @@ -2200,6 +2208,8 @@ "tag-filter-submit": "Chuja", "tags-title": "Tagi", "tags-description-header": "Maelezo kamili ya maana", + "tags-active-yes": "Ndiyo", + "tags-active-no": "Siyo", "tags-edit": "hariri", "tags-hitcount": "{{PLURAL:$1|badiliko|mabadiliko}} $1", "comparepages": "Linganisha kurasa", diff --git a/languages/i18n/tr.json b/languages/i18n/tr.json index 73731503cf..ff452eebeb 100644 --- a/languages/i18n/tr.json +++ b/languages/i18n/tr.json @@ -897,12 +897,12 @@ "page_first": "ilk", "page_last": "son", "histlegend": "Fark seçimi: Karşılaştırmayı istediğiniz 2 sürümün önündeki daireleri işaretleyip, \"{{int:Compareselectedversions}}\" düğmesine basın.
\nTanımlar: '''({{int:cur}})''' = son revizyon ile arasındaki fark, '''({{int:last}})''' = bir önceki revizyon ile arasındaki fark, '''{{int:minoreditletter}}''' = küçük değişiklik.", - "history-fieldset-title": "Geçmişe gözat", + "history-fieldset-title": "Revizyonları filtrele", "history-show-deleted": "Sadece silinen sürümler", "histfirst": "en eski", "histlast": "en yeni", "historysize": "({{PLURAL:$1|1 bayt|$1 bayt}})", - "historyempty": "(boş)", + "historyempty": "boş", "history-feed-title": "Değişiklik geçmişi", "history-feed-description": "Viki üzerindeki bu sayfanın değişiklik geçmişi.", "history-feed-item-nocomment": "$1, $2'de", @@ -1268,7 +1268,7 @@ "right-reupload-own": "Kendisinin yüklediği bir dosyanın üzerine yaz", "right-reupload-shared": "Paylaşılan ortam deposundaki dosyaları yerel olarak geçersiz kıl", "right-upload_by_url": "Bir URL adresinden dosya yükle", - "right-purge": "Doğrulama yapmadan bir sayfa için site belleğini temizle", + "right-purge": "Bir sayfa için site önbelleğini temizle", "right-autoconfirmed": "IP-tabanlı hız limitleri etkilenme", "right-bot": "Otomatik bir işlem gibi muamele gör", "right-nominornewtalk": "Kullanıcı tartışma sayfalarında yaptığı küçük değişiklikler kullanıcıya yeni mesaj bildirimiyle bildirilmez", @@ -1344,7 +1344,7 @@ "grant-delete": "Sayfaları, sürümleri ve günlük girdileri sil", "grant-editinterface": "MediaWiki alanadını, sitewide'ı ve kullanıcı JSON'unu düzenle", "grant-editmycssjs": "Kullanıcı CSS/JSON/JavaScript'ini düzenle", - "grant-editmyoptions": "Kullanıcı tercihlerini Düzenle", + "grant-editmyoptions": "Kullanıcı tercihlerinizi ve JSON yapılandırmanızı düzenleyin", "grant-editmywatchlist": "İzleme listeni düzenle", "grant-editsiteconfig": "Sitewide ve kullanıcı CSS/JS değiştir", "grant-editpage": "Mevcut sayfaları düzenle", @@ -1471,7 +1471,7 @@ "rcfilters-savedqueries-already-saved": "Bu filtreler zaten kaydedildi. Yeni bir Kayıtlı Filtre oluşturmak için ayarlarınızı değiştirin.", "rcfilters-restore-default-filters": "Varsayılan süzgeçleri geri getir", "rcfilters-clear-all-filters": "Tüm süzgeçleri temizle", - "rcfilters-show-new-changes": "Yeni değişiklikleri görüntüle", + "rcfilters-show-new-changes": "$1 tarihinden bu yana yapılan yeni değişiklikleri görüntüleyin", "rcfilters-search-placeholder": "Son değişiklikleri filtrele (menüyü kullanın veya süzgeç adını arayın)", "rcfilters-invalid-filter": "Geçersiz süzgeç", "rcfilters-empty-filter": "Etkin süzgeç bulunmuyor. Tüm katkıları gösteriliyor.", @@ -1557,10 +1557,10 @@ "rcfilters-watchlist-markseen-button": "Tüm değişiklikleri görüldü olarak işaretle", "rcfilters-watchlist-edit-watchlist-button": "İzlenen sayfaların listesini düzenle", "rcfilters-watchlist-showupdated": "Gerçekleştirilen değişikliklerden bu yana ziyaret etmediğiniz sayfalarda yapılan değişiklikler koyu renktedir.", - "rcfilters-preference-label": "Son değişikliklerin geliştirilmiş sürümünü gizle", - "rcfilters-preference-help": "2017 arayüz tasarımını ve bu andan sonra eklenen tüm araçları geri alır.", - "rcfilters-watchlist-preference-label": "İzleme listesinin geliştirilmiş sürümünü gizle", - "rcfilters-watchlist-preference-help": "2017 arayüz tasarımını ve bu andan sonra eklenen tüm araçları geri alır.", + "rcfilters-preference-label": "JavaScript olmayan bir arayüz kullanın", + "rcfilters-preference-help": "Filtre olmadan arama yapma veya işlevselliği vurgulamadan SonDeğişiklikler'i yükler.", + "rcfilters-watchlist-preference-label": "JavaScript olmayan bir arayüz kullanın", + "rcfilters-watchlist-preference-help": "Filtre Listesini arama olmadan veya işlevselliği vurgulayarak İzleme Listesi'ni yükler.", "rcfilters-target-page-placeholder": "Bir sayfa (ya da kategori) adı girin", "rcnotefrom": "$3, $4 tarihinden itibaren yapılan {{PLURAL:$5|değişiklik|değişiklik}} aşağıdadır ($1 tarhine kadar olanlar gösterilmektedir).", "rclistfromreset": "Tarih seçimini sıfırla", @@ -2052,7 +2052,7 @@ "apisandbox-loading-results": "API sonuçları alınıyor...", "apisandbox-results-error": "API sorgusu yanıtı yüklenirken bir hata oluştu: $1.", "apisandbox-request-url-label": "İstek URL:", - "apisandbox-request-time": "İstek zamanı: $1", + "apisandbox-request-time": "İstek zamanı: {{PLURAL:$1|$1 ms}}", "apisandbox-continue": "Devam et", "apisandbox-continue-clear": "Temizle", "apisandbox-multivalue-all-namespaces": "$1 (Tüm isim alanları)", @@ -2235,7 +2235,7 @@ "enotif_body_intro_moved": "{{SITENAME}} sayfası $1, $2 tarafından $PAGEEDITDATE tarihinde {{GENDER:$2|taşındı}}, mevcut revizyon için bakınız: $3.", "enotif_body_intro_restored": "{{SITENAME}} sayfası $1, $2 tarafından $PAGEEDITDATE tarihinde {{GENDER:$2|geri getirildi}}, mevcut revizyon için bakınız: $3.", "enotif_body_intro_changed": "{{SITENAME}} sayfası $1, $2 tarafından $PAGEEDITDATE tarihinde {{GENDER:$2|değiştirildi}}, mevcut revizyon için bakınız: $3.", - "enotif_lastvisited": "Son ziyaretinizden bu yana olan tüm değişiklikleri görmek için $1'e bakın.", + "enotif_lastvisited": "Son ziyaretinizden bu yana yapılan tüm değişiklikler için bakınız: $1", "enotif_lastdiff": "Bu değişikliği görmek için, $1 sayfasına bakınız.", "enotif_anon_editor": "anonim kullanıcı $1", "enotif_body": "Sayın $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nEditörün girdiği özet: $PAGESUMMARY $PAGEMINOREDIT\n\nEditörün iletişim bilgileri:\ne-posta: $PAGEEDITOR_EMAIL\nviki: $PAGEEDITOR_WIKI\n\nBahsi geçen sayfayı oturum açarak ziyaret edinceye kadar sayfayla ilgili başka bildirim gönderilmeyecektir. Ayrıca izleme listenizdeki tüm sayfaların bildirim durumlarını sıfırlayabilirsiniz.\n\n{{SITENAME}} bildirim sistemi\n\n--\nE-posta bildirim ayarlarınızı değiştirmek için aşağıdaki sayfayı ziyaret ediniz:\n{{canonicalurl:{{#special:Preferences}}}}\n\nİzleme listesi ayarlarınızı değiştirmek için aşağıdaki sayfayı ziyaret ediniz:\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nSayfayı izleme listenizden silmek için aşağıdaki sayfayı ziyaret ediniz:\n$UNWATCHURL\n\nGeri bildirim ve daha fazla yardım için:\n$HELPPAGE", @@ -2245,12 +2245,12 @@ "deletepage": "Sayfayı sil", "confirm": "Onayla", "excontent": "eski içerik: '$1'", - "excontentauthor": "eski içerik: '$1' ('[[Special:Contributions/$2|$2]]' katkıda bulunmuş olan tek kullanıcı)", + "excontentauthor": "eski içerik: '$1' ve katkıda bulunmuş olan tek kullanıcı \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|mesaj]])", "exbeforeblank": "Silinmeden önceki içerik: '$1'", "delete-confirm": "\"$1\" sayfasını sil", "delete-legend": "Sil", "historywarning": "Uyarı: Silmek üzere olduğunuz sayfanın yaklaşık olarak $1 sürüme sahip bir geçmişi var:", - "historyaction-submit": "Göster", + "historyaction-submit": "Revizyonları göster", "confirmdeletetext": "Bu sayfayı veya dosyayı tüm geçmişi ile birlikte veritabanından kalıcı olarak silmek üzeresiniz.\nBu işlemden kaynaklı doğabilecek sonuçların farkında iseniz ve işlemin [[{{MediaWiki:Policy-url}}|Silme kurallarına]] uygun olduğuna eminseniz, işlemi onaylayın.", "actioncomplete": "İşlem tamamlandı", "actionfailed": "İşlem başarısız oldu", @@ -2401,6 +2401,7 @@ "mycontris": "Katkılar", "anoncontribs": "Katkılar", "contribsub2": "{{GENDER:$3|$1}} ($2) tarafından", + "contributions-subtitle": "{{GENDER:$3|$1}} için", "contributions-userdoesnotexist": "\"$1\" kullanıcı hesabı kayıtlı değil.", "nocontribs": "Bu kriterlere uyan değişiklik bulunamadı", "uctop": "güncel", @@ -2411,8 +2412,8 @@ "sp-contributions-newbies-sub": "Yeni kullanıcılar için", "sp-contributions-newbies-title": "Yeni hesaplar için kullanıcı katkıları", "sp-contributions-blocklog": "engelleme günlüğü", - "sp-contributions-suppresslog": "kullanıcının silinen katkıları", - "sp-contributions-deleted": "kullanıcının silinen katkıları", + "sp-contributions-suppresslog": "{{GENDER:$1|kullanıcının}} baskılanmış katkıları", + "sp-contributions-deleted": "{{GENDER:$1|kullanıcının}} silinen katkıları", "sp-contributions-uploads": "yüklenenler", "sp-contributions-logs": "günlükler", "sp-contributions-talk": "mesaj", diff --git a/languages/i18n/yue.json b/languages/i18n/yue.json index e40a5f9ec1..b7dc9c4f95 100644 --- a/languages/i18n/yue.json +++ b/languages/i18n/yue.json @@ -35,7 +35,8 @@ "Hello903hello", "Fitoschido", "Kanashimi", - "Roy17" + "Roy17", + "Tang891228" ] }, "tog-underline": "連結加底線:", @@ -359,7 +360,7 @@ "title-invalid-talk-namespace": "所請求嘅版面標題指去未開嘅討論版。", "title-invalid-characters": "所請求嘅版面標題有「$1」呢個無效字符。", "title-invalid-relative": "標題有相對路徑。因為用戶嘅瀏覽器經常處理唔到相對路徑(./, ../),所以相對路徑無效。", - "title-invalid-magic-tilde": "所請求嘅版面標題有無效嘅波浪線魔法字(~~~)。", + "title-invalid-magic-tilde": "所請求嘅版面標題有無效嘅波浪線魔術字(~~~)。", "title-invalid-too-long": "所請求嘅版面標題太長。標題用UTF-8編碼嗰時嘅長度唔應該超過 $1 {{PLURAL:$1|字節}}", "title-invalid-leading-colon": "所請求嘅版面標題開頭有無效冒號。", "perfcached": "以下嘅資料係嚟自快取,可能唔係最新嘅。 最多有{{PLURAL:$1|一個結果|$1個結果}}響快取度。", diff --git a/languages/i18n/zh-hant.json b/languages/i18n/zh-hant.json index 1eab155788..1542edaa57 100644 --- a/languages/i18n/zh-hant.json +++ b/languages/i18n/zh-hant.json @@ -102,7 +102,8 @@ "Hello903hello", "Luuva", "Davidzdh", - "WQL" + "WQL", + "Tang891228" ] }, "tog-underline": "底線標示連結:", @@ -2151,12 +2152,12 @@ "booksources-search": "搜尋", "booksources-text": "下列清單包含其他銷售新書籍或二手書籍的網站連結,可會有你想尋找書籍的進一部資訊:", "booksources-invalid-isbn": "您提供的 ISBN 不正確,請檢查複製的來源是否有誤。", - "magiclink-tracking-rfc": "使用 RFC 魔法連結的頁面", - "magiclink-tracking-rfc-desc": "此頁面使用 RFC 魔法連結的頁面,請參考 [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] 的如何遷移。", - "magiclink-tracking-pmid": "使用 PMID 魔法連結的頁面", - "magiclink-tracking-pmid-desc": "此頁面使用 PMID 魔法連結的頁面,請參考 [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] 的如何遷移。", - "magiclink-tracking-isbn": "使用 ISBN 魔法連結的頁面", - "magiclink-tracking-isbn-desc": "此頁面使用 ISBN 魔法連結的頁面,請參考 [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] 的如何遷移。", + "magiclink-tracking-rfc": "使用 RFC 魔術連結的頁面", + "magiclink-tracking-rfc-desc": "此頁面使用RFC魔術連結,請參考[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org]以了解如何遷移。", + "magiclink-tracking-pmid": "使用 PMID 魔術連結的頁面", + "magiclink-tracking-pmid-desc": "此頁面使用PMID魔術連結,請參考[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org]以了解如何遷移。", + "magiclink-tracking-isbn": "使用 ISBN 魔術連結的頁面", + "magiclink-tracking-isbn-desc": "此頁面使用ISBN魔術連結,請參考[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org]以了解如何遷移。", "specialloguserlabel": "執行者:", "speciallogtitlelabel": "目標(標題或以 {{ns:user}}:使用者名稱 表示使用者):", "log": "日誌", diff --git a/maintenance/benchmarks/bench_HTTP_HTTPS.php b/maintenance/benchmarks/bench_HTTP_HTTPS.php index 5e1feb739b..b7d584ad2d 100644 --- a/maintenance/benchmarks/bench_HTTP_HTTPS.php +++ b/maintenance/benchmarks/bench_HTTP_HTTPS.php @@ -24,6 +24,8 @@ * @author Platonides */ +use MediaWiki\MediaWikiServices; + require_once __DIR__ . '/Benchmarker.php'; /** @@ -45,7 +47,8 @@ class BenchHttpHttps extends Benchmarker { } private function doRequest( $proto ) { - Http::get( "$proto://localhost/", [], __METHOD__ ); + MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( "$proto://localhost/", [], __METHOD__ ); } // bench function 1 diff --git a/maintenance/findHooks.php b/maintenance/findHooks.php index 900752fe4f..b57db8f18c 100644 --- a/maintenance/findHooks.php +++ b/maintenance/findHooks.php @@ -34,6 +34,8 @@ * @author Antoine Musso */ +use MediaWiki\MediaWikiServices; + require_once __DIR__ . '/Maintenance.php'; /** @@ -216,7 +218,7 @@ class FindHooks extends Maintenance { $retval = []; while ( true ) { - $json = Http::get( + $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get( wfAppendQuery( 'https://www.mediawiki.org/w/api.php', $params ), [], __METHOD__ diff --git a/maintenance/importSiteScripts.php b/maintenance/importSiteScripts.php index e60e776328..1d4b496e4c 100644 --- a/maintenance/importSiteScripts.php +++ b/maintenance/importSiteScripts.php @@ -21,6 +21,8 @@ * @ingroup Maintenance */ +use MediaWiki\MediaWikiServices; + require_once __DIR__ . '/Maintenance.php'; /** @@ -64,7 +66,8 @@ class ImportSiteScripts extends Maintenance { $url = wfAppendQuery( $baseUrl, [ 'action' => 'raw', 'title' => "MediaWiki:{$page}" ] ); - $text = Http::get( $url, [], __METHOD__ ); + $text = MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( $url, [], __METHOD__ ); $wikiPage = WikiPage::factory( $title ); $content = ContentHandler::makeContent( $text, $wikiPage->getTitle() ); @@ -86,7 +89,8 @@ class ImportSiteScripts extends Maintenance { while ( true ) { $url = wfAppendQuery( $baseUrl, $data ); - $strResult = Http::get( $url, [], __METHOD__ ); + $strResult = MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( $url, [], __METHOD__ ); $result = FormatJson::decode( $strResult, true ); $page = null; diff --git a/maintenance/populateInterwiki.php b/maintenance/populateInterwiki.php index acc66c5199..a654a1fc95 100644 --- a/maintenance/populateInterwiki.php +++ b/maintenance/populateInterwiki.php @@ -86,7 +86,7 @@ TEXT $url = rtrim( $this->source, '?' ) . '?' . $url; } - $json = Http::get( $url ); + $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get( $url ); $data = json_decode( $json, true ); if ( is_array( $data ) ) { diff --git a/tests/integration/includes/http/MWHttpRequestTestCase.php b/tests/integration/includes/http/MWHttpRequestTestCase.php index 603f4c26f2..f7a4cc479c 100644 --- a/tests/integration/includes/http/MWHttpRequestTestCase.php +++ b/tests/integration/includes/http/MWHttpRequestTestCase.php @@ -1,19 +1,25 @@ oldHttpEngine = Http::$httpEngine; Http::$httpEngine = static::$httpEngine; + $this->factory = MediaWikiServices::getInstance()->getHttpRequestFactory(); + try { - $request = MWHttpRequest::factory( 'null:' ); - } catch ( DomainException $e ) { + $request = $factory->create( 'null:' ); + } catch ( RuntimeException $e ) { $this->markTestSkipped( static::$httpEngine . ' engine not supported' ); } @@ -32,19 +38,19 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { // -------------------- public function testIsRedirect() { - $request = MWHttpRequest::factory( 'http://httpbin.org/get' ); + $request = $this->factory->create( 'http://httpbin.org/get' ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); $this->assertFalse( $request->isRedirect() ); - $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/1' ); + $request = $this->factory->create( 'http://httpbin.org/redirect/1' ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); $this->assertTrue( $request->isRedirect() ); } public function testgetFinalUrl() { - $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3' ); + $request = $this->factory->create( 'http://httpbin.org/redirect/3' ); if ( !$request->canFollowRedirects() ) { $this->markTestSkipped( 'cannot follow redirects' ); } @@ -52,14 +58,14 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { $this->assertTrue( $status->isGood() ); $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() ); - $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects' => true ] ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() ); $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request ); - $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects' => true ] ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); @@ -71,7 +77,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { return; } - $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects' => true, 'maxRedirects' => 1 ] ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); @@ -79,7 +85,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { } public function testSetCookie() { - $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' ); + $request = $this->factory->create( 'http://httpbin.org/cookies' ); $request->setCookie( 'foo', 'bar' ); $request->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] ); $status = $request->execute(); @@ -88,7 +94,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { } public function testSetCookieJar() { - $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' ); + $request = $this->factory->create( 'http://httpbin.org/cookies' ); $cookieJar = new CookieJar(); $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] ); $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] ); @@ -97,7 +103,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { $this->assertTrue( $status->isGood() ); $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request ); - $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/set?foo=bar' ); + $request = $this->factory->create( 'http://httpbin.org/cookies/set?foo=bar' ); $cookieJar = new CookieJar(); $request->setCookieJar( $cookieJar ); $status = $request->execute(); @@ -106,7 +112,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { $this->markTestIncomplete( 'CookieJar does not handle deletion' ); - // $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/delete?foo' ); + // $request = $this->factory->create( 'http://httpbin.org/cookies/delete?foo' ); // $cookieJar = new CookieJar(); // $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] ); // $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'httpbin.org' ] ); @@ -118,7 +124,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { } public function testGetResponseHeaders() { - $request = MWHttpRequest::factory( 'http://httpbin.org/response-headers?Foo=bar' ); + $request = $this->factory->create( 'http://httpbin.org/response-headers?Foo=bar' ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); $headers = array_change_key_case( $request->getResponseHeaders(), CASE_LOWER ); @@ -127,7 +133,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { } public function testSetHeader() { - $request = MWHttpRequest::factory( 'http://httpbin.org/headers' ); + $request = $this->factory->create( 'http://httpbin.org/headers' ); $request->setHeader( 'Foo', 'bar' ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); @@ -135,14 +141,14 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { } public function testGetStatus() { - $request = MWHttpRequest::factory( 'http://httpbin.org/status/418' ); + $request = $this->factory->create( 'http://httpbin.org/status/418' ); $status = $request->execute(); $this->assertFalse( $status->isOK() ); $this->assertSame( $request->getStatus(), 418 ); } public function testSetUserAgent() { - $request = MWHttpRequest::factory( 'http://httpbin.org/user-agent' ); + $request = $this->factory->create( 'http://httpbin.org/user-agent' ); $request->setUserAgent( 'foo' ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); @@ -150,7 +156,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { } public function testSetData() { - $request = MWHttpRequest::factory( 'http://httpbin.org/post', [ 'method' => 'POST' ] ); + $request = $this->factory->create( 'http://httpbin.org/post', [ 'method' => 'POST' ] ); $request->setData( [ 'foo' => 'bar', 'foo2' => 'bar2' ] ); $status = $request->execute(); $this->assertTrue( $status->isGood() ); @@ -163,7 +169,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { return; } - $request = MWHttpRequest::factory( 'http://httpbin.org/ip' ); + $request = $this->factory->create( 'http://httpbin.org/ip' ); $data = ''; $request->setCallback( function ( $fh, $content ) use ( &$data ) { $data .= $content; @@ -177,7 +183,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { } public function testBasicAuthentication() { - $request = MWHttpRequest::factory( 'http://httpbin.org/basic-auth/user/pass', [ + $request = $this->factory->create( 'http://httpbin.org/basic-auth/user/pass', [ 'username' => 'user', 'password' => 'pass', ] ); @@ -185,7 +191,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { $this->assertTrue( $status->isGood() ); $this->assertResponseFieldValue( 'authenticated', true, $request ); - $request = MWHttpRequest::factory( 'http://httpbin.org/basic-auth/user/pass', [ + $request = $this->factory->create( 'http://httpbin.org/basic-auth/user/pass', [ 'username' => 'user', 'password' => 'wrongpass', ] ); @@ -195,7 +201,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { } public function testFactoryDefaults() { - $request = MWHttpRequest::factory( 'http://acme.test' ); + $request = $this->factory->create( 'http://acme.test' ); $this->assertInstanceOf( MWHttpRequest::class, $request ); } diff --git a/tests/parser/ParserTestPrinter.php b/tests/parser/ParserTestPrinter.php index fddee3d28f..34f8cd50d3 100644 --- a/tests/parser/ParserTestPrinter.php +++ b/tests/parser/ParserTestPrinter.php @@ -168,14 +168,10 @@ class ParserTestPrinter extends TestRecorder { $output = strtr( $output, $pairs ); } - # Windows, or at least the fc utility, is retarded - $slash = wfIsWindows() ? '\\' : '/'; - $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand(); - - $infile = "$prefix-$inFileTail"; + $infile = tempnam( wfTempDir(), "mwParser-$inFileTail" ); $this->dumpToFile( $input, $infile ); - $outfile = "$prefix-$outFileTail"; + $outfile = tempnam( wfTempDir(), "mwParser-$outFileTail" ); $this->dumpToFile( $output, $outfile ); global $wgDiff3; diff --git a/tests/parser/ParserTestRunner.php b/tests/parser/ParserTestRunner.php index 3eb25a9c95..df897d9a1e 100644 --- a/tests/parser/ParserTestRunner.php +++ b/tests/parser/ParserTestRunner.php @@ -289,9 +289,14 @@ class ParserTestRunner { // All FileRepo changes should be done here by injecting services, // there should be no need to change global variables. - RepoGroup::setSingleton( $this->createRepoGroup() ); + MediaWikiServices::getInstance()->disableService( 'RepoGroup' ); + MediaWikiServices::getInstance()->redefineService( 'RepoGroup', + function () { + return $this->createRepoGroup(); + } + ); $teardown[] = function () { - RepoGroup::destroySingleton(); + MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' ); }; // Set up null lock managers @@ -449,7 +454,8 @@ class ParserTestRunner { 'transformVia404' => false, 'backend' => $backend ], - [] + [], + MediaWikiServices::getInstance()->getMainWANObjectCache() ); } @@ -635,6 +641,8 @@ class ParserTestRunner { /** * Reset the Title-related services that need resetting * for each test + * + * @todo We need to reset all services on every test */ private function resetTitleServices() { $services = MediaWikiServices::getInstance(); @@ -643,6 +651,7 @@ class ParserTestRunner { $services->resetServiceForTesting( '_MediaWikiTitleCodec' ); $services->resetServiceForTesting( 'LinkRenderer' ); $services->resetServiceForTesting( 'LinkRendererFactory' ); + $services->resetServiceForTesting( 'NamespaceInfo' ); } /** diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index ebc3b79c3f..ec61c23634 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -472,7 +472,17 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * @return string Absolute name of the temporary file */ protected function getNewTempFile() { - $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' ); + $fileName = tempnam( + wfTempDir(), + // Avoid backslashes here as they result in inconsistent results + // between Windows and other OS, as well as between functions + // that try to normalise these in one or both directions. + // For example, tempnam rejects directory separators in the prefix which + // means it rejects any namespaced class on Windows. + // And then there is, wfMkdirParents which normalises paths always + // whereas most other PHP and MW functions do not. + 'MW_PHPUnit_' . strtr( static::class, [ '\\' => '_' ] ) . '_' + ); $this->tmpFiles[] = $fileName; return $fileName; @@ -489,14 +499,15 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * @return string Absolute name of the temporary directory */ protected function getNewTempDirectory() { - // Starting of with a temporary /file/. + // Starting of with a temporary *file*. $fileName = $this->getNewTempFile(); - // Converting the temporary /file/ to a /directory/ + // Converting the temporary file to a *directory*. // The following is not atomic, but at least we now have a single place, - // where temporary directory creation is bundled and can be improved + // where temporary directory creation is bundled and can be improved. unlink( $fileName ); - $this->assertTrue( wfMkdirParents( $fileName ) ); + // If this fails for some reason, PHP will warn and fail the test. + mkdir( $fileName, 0777, /* recursive = */ true ); return $fileName; } @@ -2408,4 +2419,18 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { 'comment' => $comment, ] ); } + + /** + * Returns a PHPUnit constraint that matches anything other than a fixed set of values. This can + * be used to whitelist values, e.g. + * $mock->expects( $this->never() )->method( $this->anythingBut( 'foo', 'bar' ) ); + * which will throw if any unexpected method is called. + * + * @param mixed ...$values Values that are not matched + */ + protected function anythingBut( ...$values ) { + return $this->logicalNot( $this->logicalOr( + ...array_map( [ $this, 'matches' ], $values ) + ) ); + } } diff --git a/tests/phpunit/includes/ContentSecurityPolicyTest.php b/tests/phpunit/includes/ContentSecurityPolicyTest.php index 5f0200d09d..a758f990c9 100644 --- a/tests/phpunit/includes/ContentSecurityPolicyTest.php +++ b/tests/phpunit/includes/ContentSecurityPolicyTest.php @@ -37,7 +37,7 @@ class ContentSecurityPolicyTest extends MediaWikiTestCase { // Note, there are some obscure globals which // could affect the results which aren't included above. - RepoGroup::destroySingleton(); + $this->overrideMwServices(); $context = RequestContext::getMain(); $resp = $context->getRequest()->response(); $conf = $context->getConfig(); diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php index 9443b19e06..1210a507db 100644 --- a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php +++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php @@ -74,12 +74,8 @@ class GlobalTest extends MediaWikiTestCase { $this->assertFalse( wfRandomString() == wfRandomString() ); - $this->assertEquals( - strlen( wfRandomString( 10 ) ), 10 - ); - $this->assertTrue( - preg_match( '/^[0-9a-f]+$/i', wfRandomString() ) === 1 - ); + $this->assertSame( 10, strlen( wfRandomString( 10 ) ), 'length' ); + $this->assertSame( 1, preg_match( '/^[0-9a-f]+$/i', wfRandomString() ), 'pattern' ); } /** diff --git a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php index b183fcab47..3467153a55 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php +++ b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php @@ -1416,10 +1416,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { ->value['revision']; $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->getTimestampFromId( - $page->getTitle(), - $rev->getId() - ); + $result = $store->getTimestampFromId( $rev->getId() ); $this->assertSame( $rev->getTimestamp(), $result ); } @@ -1434,10 +1431,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { ->value['revision']; $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->getTimestampFromId( - $page->getTitle(), - $rev->getId() + 1 - ); + $result = $store->getTimestampFromId( $rev->getId() + 1 ); $this->assertFalse( $result ); } diff --git a/tests/phpunit/includes/RevisionDbTestBase.php b/tests/phpunit/includes/RevisionDbTestBase.php index 13ddffac14..96e27668ec 100644 --- a/tests/phpunit/includes/RevisionDbTestBase.php +++ b/tests/phpunit/includes/RevisionDbTestBase.php @@ -625,6 +625,34 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { $this->assertEquals( $latestRevision, $newRevision->getPrevious()->getId() ); } + /** + * @covers Title::getPreviousRevisionID + * @covers Title::getRelativeRevisionID + * @covers MediaWiki\Revision\RevisionStore::getPreviousRevision + * @covers MediaWiki\Revision\RevisionStore::getRelativeRevision + */ + public function testTitleGetPreviousRevisionID() { + $oldestId = $this->testPage->getOldestRevision()->getId(); + $latestId = $this->testPage->getLatest(); + + $title = $this->testPage->getTitle(); + + $this->assertFalse( $title->getPreviousRevisionID( $oldestId ) ); + + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $newId = $this->testPage->getRevision()->getId(); + + $this->assertEquals( $latestId, $title->getPreviousRevisionID( $newId ) ); + } + + /** + * @covers Title::getPreviousRevisionID + * @covers Title::getRelativeRevisionID + */ + public function testTitleGetPreviousRevisionID_invalid() { + $this->assertFalse( $this->testPage->getTitle()->getPreviousRevisionID( 123456789 ) ); + } + /** * @covers Revision::getNext */ @@ -640,6 +668,33 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() ); } + /** + * @covers Title::getNextRevisionID + * @covers Title::getRelativeRevisionID + * @covers MediaWiki\Revision\RevisionStore::getNextRevision + * @covers MediaWiki\Revision\RevisionStore::getRelativeRevision + */ + public function testTitleGetNextRevisionID() { + $title = $this->testPage->getTitle(); + + $origId = $this->testPage->getLatest(); + + $this->assertFalse( $title->getNextRevisionID( $origId ) ); + + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $newId = $this->testPage->getLatest(); + + $this->assertSame( $this->testPage->getLatest(), $title->getNextRevisionID( $origId ) ); + } + + /** + * @covers Title::getNextRevisionID + * @covers Title::getRelativeRevisionID + */ + public function testTitleGetNextRevisionID_invalid() { + $this->assertFalse( $this->testPage->getTitle()->getNextRevisionID( 123456789 ) ); + } + /** * @covers Revision::newNullRevision */ diff --git a/tests/phpunit/includes/TestUserRegistry.php b/tests/phpunit/includes/TestUserRegistry.php index 3064a3d316..40a5dc5c31 100644 --- a/tests/phpunit/includes/TestUserRegistry.php +++ b/tests/phpunit/includes/TestUserRegistry.php @@ -33,7 +33,7 @@ class TestUserRegistry { */ public static function getMutableTestUser( $testName, $groups = [] ) { $id = self::getNextId(); - $password = wfRandomString( 20 ); + $password = "password_for_test_user_id_{$id}"; $testUser = new TestUser( "TestUser $testName $id", // username "Name $id", // real name @@ -75,7 +75,7 @@ class TestUserRegistry { $password = 'UTSysopPassword'; } else { $username = "TestUser $id"; - $password = wfRandomString( 20 ); + $password = "password_for_test_user_id_{$id}"; } self::$testUsers[$key] = $testUser = new TestUser( $username, // username diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index c0de1bfbc0..c46f69b2ef 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -157,6 +157,7 @@ class TitleTest extends MediaWikiTestCase { ] ] ); + // Reset services since we modified $wgLocalInterwikis $this->overrideMwServices(); } @@ -785,19 +786,6 @@ class TitleTest extends MediaWikiTestCase { ]; } - /** - * @dataProvider provideGetTalkPage_good - * @covers Title::getTalkPage - */ - public function testGetTalkPage_good( Title $title, Title $expected ) { - $talk = $title->getTalkPage(); - $this->assertSame( - $expected->getPrefixedDBKey(), - $talk->getPrefixedDBKey(), - $title->getPrefixedDBKey() - ); - } - /** * @dataProvider provideGetTalkPage_good * @covers Title::getTalkPageIfDefined diff --git a/tests/phpunit/includes/api/ApiBaseTest.php b/tests/phpunit/includes/api/ApiBaseTest.php index 0dc64df87f..e02e8a4984 100644 --- a/tests/phpunit/includes/api/ApiBaseTest.php +++ b/tests/phpunit/includes/api/ApiBaseTest.php @@ -1332,7 +1332,10 @@ class ApiBaseTest extends ApiTestCase { 'expiry' => time() + 100500, ] ); $block->insert(); - $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]; + $userInfoTrait = TestingAccessWrapper::newFromObject( + $this->getMockForTrait( ApiBlockInfoTrait::class ) + ); + $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockInfo( $block ) ]; $expect = Status::newGood(); $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) ); @@ -1387,7 +1390,10 @@ class ApiBaseTest extends ApiTestCase { 'expiry' => time() + 100500, ] ); $block->insert(); - $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]; + $userInfoTrait = TestingAccessWrapper::newFromObject( + $this->getObjectForTrait( ApiBlockInfoTrait::class ) + ); + $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockInfo( $block ) ]; $expect = Status::newGood(); $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) ); diff --git a/tests/phpunit/includes/api/ApiBlockInfoTraitTest.php b/tests/phpunit/includes/api/ApiBlockInfoTraitTest.php new file mode 100644 index 0000000000..f05cfbcdc1 --- /dev/null +++ b/tests/phpunit/includes/api/ApiBlockInfoTraitTest.php @@ -0,0 +1,43 @@ +getMockForTrait( ApiBlockInfoTrait::class ); + $info = TestingAccessWrapper::newFromObject( $mock )->getBlockInfo( $block ); + $subset = [ + 'blockid' => null, + 'blockedby' => '', + 'blockedbyid' => 0, + 'blockreason' => '', + 'blockexpiry' => 'infinite', + 'blockpartial' => false, + ]; + $this->assertArraySubset( $subset, $info ); + } + + public function testGetBlockInfoPartial() { + $mock = $this->getMockForTrait( ApiBlockInfoTrait::class ); + + $block = new Block( [ + 'sitewide' => false, + ] ); + $info = TestingAccessWrapper::newFromObject( $mock )->getBlockInfo( $block ); + $subset = [ + 'blockid' => null, + 'blockedby' => '', + 'blockedbyid' => 0, + 'blockreason' => '', + 'blockexpiry' => 'infinite', + 'blockpartial' => true, + ]; + $this->assertArraySubset( $subset, $info ); + } + +} diff --git a/tests/phpunit/includes/api/ApiQueryUserInfoTest.php b/tests/phpunit/includes/api/ApiQueryUserInfoTest.php deleted file mode 100644 index 7dcb75c486..0000000000 --- a/tests/phpunit/includes/api/ApiQueryUserInfoTest.php +++ /dev/null @@ -1,47 +0,0 @@ -apiContext ), 'userinfo' ), - 'userinfo' - ); - - $block = new Block(); - $info = $apiQueryUserInfo->getBlockInfo( $block ); - $subset = [ - 'blockid' => null, - 'blockedby' => '', - 'blockedbyid' => 0, - 'blockreason' => '', - 'blockexpiry' => 'infinite', - 'blockpartial' => false, - ]; - $this->assertArraySubset( $subset, $info ); - } - - public function testGetBlockInfoPartial() { - $apiQueryUserInfo = new ApiQueryUserInfo( - new ApiQuery( new ApiMain( $this->apiContext ), 'userinfo' ), - 'userinfo' - ); - - $block = new Block( [ - 'sitewide' => false, - ] ); - $info = $apiQueryUserInfo->getBlockInfo( $block ); - $subset = [ - 'blockid' => null, - 'blockedby' => '', - 'blockedbyid' => 0, - 'blockreason' => '', - 'blockexpiry' => 'infinite', - 'blockpartial' => true, - ]; - $this->assertArraySubset( $subset, $info ); - } -} diff --git a/tests/phpunit/includes/auth/AuthManagerTest.php b/tests/phpunit/includes/auth/AuthManagerTest.php index 209ed55fd4..5cf93c9e2b 100644 --- a/tests/phpunit/includes/auth/AuthManagerTest.php +++ b/tests/phpunit/includes/auth/AuthManagerTest.php @@ -2670,7 +2670,7 @@ class AuthManagerTest extends \MediaWikiTestCase { // Test backoff $cache = \ObjectCache::getLocalClusterInstance(); - $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) ); + $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) ); $cache->set( $backoffKey, true ); $session->clear(); $user = \User::newFromName( $username ); @@ -2709,7 +2709,7 @@ class AuthManagerTest extends \MediaWikiTestCase { // Test addToDatabase throws an exception $cache = \ObjectCache::getLocalClusterInstance(); - $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) ); + $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) ); $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' ); $session->clear(); $user = $this->getMockBuilder( \User::class ) diff --git a/tests/phpunit/includes/config/GlobalVarConfigTest.php b/tests/phpunit/includes/config/GlobalVarConfigTest.php index ec443e7a8a..591f27dd66 100644 --- a/tests/phpunit/includes/config/GlobalVarConfigTest.php +++ b/tests/phpunit/includes/config/GlobalVarConfigTest.php @@ -19,11 +19,10 @@ class GlobalVarConfigTest extends MediaWikiTestCase { */ public function testConstructor( $prefix ) { $var = $prefix . 'GlobalVarConfigTest'; - $rand = wfRandomString(); - $this->setMwGlobals( $var, $rand ); + $this->setMwGlobals( $var, 'testvalue' ); $config = new GlobalVarConfig( $prefix ); $this->assertInstanceOf( GlobalVarConfig::class, $config ); - $this->assertEquals( $rand, $config->get( 'GlobalVarConfigTest' ) ); + $this->assertEquals( 'testvalue', $config->get( 'GlobalVarConfigTest' ) ); } public static function provideConstructor() { @@ -41,7 +40,7 @@ class GlobalVarConfigTest extends MediaWikiTestCase { * @covers GlobalVarConfig::hasWithPrefix */ public function testHas() { - $this->setMwGlobals( 'wgGlobalVarConfigTestHas', wfRandomString() ); + $this->setMwGlobals( 'wgGlobalVarConfigTestHas', 'testvalue' ); $config = new GlobalVarConfig(); $this->assertTrue( $config->has( 'GlobalVarConfigTestHas' ) ); $this->assertFalse( $config->has( 'GlobalVarConfigTestNotHas' ) ); diff --git a/tests/phpunit/includes/db/LBFactoryTest.php b/tests/phpunit/includes/db/LBFactoryTest.php index b79cdf3896..106a13bc77 100644 --- a/tests/phpunit/includes/db/LBFactoryTest.php +++ b/tests/phpunit/includes/db/LBFactoryTest.php @@ -30,7 +30,6 @@ use Wikimedia\Rdbms\LBFactorySimple; use Wikimedia\Rdbms\LBFactoryMulti; use Wikimedia\Rdbms\LoadBalancer; use Wikimedia\Rdbms\ChronologyProtector; -use Wikimedia\Rdbms\DatabaseMysqli; use Wikimedia\Rdbms\MySQLMasterPos; use Wikimedia\Rdbms\DatabaseDomain; @@ -47,7 +46,7 @@ class LBFactoryTest extends MediaWikiTestCase { * @dataProvider getLBFactoryClassProvider */ public function testGetLBFactoryClass( $expected, $deprecated ) { - $mockDB = $this->getMockBuilder( DatabaseMysqli::class ) + $mockDB = $this->getMockBuilder( IDatabase::class ) ->disableOriginalConstructor() ->getMock(); @@ -291,7 +290,7 @@ class LBFactoryTest extends MediaWikiTestCase { $m2Pos = new MySQLMasterPos( 'db1064-bin.002400/794074907', $now ); // Master DB 1 - $mockDB1 = $this->getMockBuilder( DatabaseMysqli::class ) + $mockDB1 = $this->getMockBuilder( IDatabase::class ) ->disableOriginalConstructor() ->getMock(); $mockDB1->method( 'writesOrCallbacksPending' )->willReturn( true ); @@ -316,7 +315,7 @@ class LBFactoryTest extends MediaWikiTestCase { $lb1->method( 'getMasterPos' )->willReturn( $m1Pos ); $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' ); // Master DB 2 - $mockDB2 = $this->getMockBuilder( DatabaseMysqli::class ) + $mockDB2 = $this->getMockBuilder( IDatabase::class ) ->disableOriginalConstructor() ->getMock(); $mockDB2->method( 'writesOrCallbacksPending' )->willReturn( true ); diff --git a/tests/phpunit/includes/filebackend/FileBackendTest.php b/tests/phpunit/includes/filebackend/FileBackendTest.php index 4dc2f9e0ca..8548fdeb5b 100644 --- a/tests/phpunit/includes/filebackend/FileBackendTest.php +++ b/tests/phpunit/includes/filebackend/FileBackendTest.php @@ -1,5 +1,6 @@ 'localtesting', 'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ), 'parallelize' => 'implicit', - 'wikiId' => wfWikiID() . wfRandomString(), + 'wikiId' => 'testdb', 'backends' => [ [ 'name' => 'localmultitesting1', @@ -1538,7 +1539,8 @@ class FileBackendTest extends MediaWikiTestCase { $url = $this->backend->getFileHttpUrl( [ 'src' => $source ] ); if ( $url !== null ) { // supported - $data = Http::request( "GET", $url, [], __METHOD__ ); + $data = MediaWikiServices::getInstance()->getHttpRequestFactory()-> + get( $url, [], __METHOD__ ); $this->assertEquals( $content, $data, "HTTP GET of URL has right contents ($backendName)." ); } @@ -2567,11 +2569,9 @@ class FileBackendTest extends MediaWikiTestCase { 'wikiId' => wfWikiID() ] ) ); - $name = wfRandomString( 300 ); - $input = [ 'headers' => [ - 'content-Disposition' => FileBackend::makeContentDisposition( 'inline', $name ), + 'content-Disposition' => FileBackend::makeContentDisposition( 'inline', 'name' ), 'Content-dUration' => 25.6, 'X-LONG-VALUE' => str_pad( '0', 300 ), 'CONTENT-LENGTH' => 855055, @@ -2579,7 +2579,7 @@ class FileBackendTest extends MediaWikiTestCase { ]; $expected = [ 'headers' => [ - 'content-disposition' => FileBackend::makeContentDisposition( 'inline', $name ), + 'content-disposition' => FileBackend::makeContentDisposition( 'inline', 'name' ), 'content-duration' => 25.6, 'content-length' => 855055 ] diff --git a/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php index 4c9855b074..346be7afa3 100644 --- a/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php +++ b/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php @@ -112,7 +112,7 @@ class FileBackendDBRepoWrapperTest extends MediaWikiTestCase { } protected function getMocks() { - $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class ) + $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class ) ->disableOriginalClone() ->disableOriginalConstructor() ->getMock(); diff --git a/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php b/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php index 9beea5b630..0c78c2bd86 100644 --- a/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php +++ b/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php @@ -28,7 +28,7 @@ class MigrateFileRepoLayoutTest extends MediaWikiTestCase { ] ] ); - $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class ) + $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class ) ->disableOriginalConstructor() ->getMock(); diff --git a/tests/phpunit/includes/filerepo/RepoGroupTest.php b/tests/phpunit/includes/filerepo/RepoGroupTest.php index 5a343f6529..67de6982e6 100644 --- a/tests/phpunit/includes/filerepo/RepoGroupTest.php +++ b/tests/phpunit/includes/filerepo/RepoGroupTest.php @@ -7,7 +7,7 @@ class RepoGroupTest extends MediaWikiTestCase { function testHasForeignRepoNegative() { $this->setMwGlobals( 'wgForeignFileRepos', [] ); - RepoGroup::destroySingleton(); + $this->overrideMwServices(); FileBackendGroup::destroySingleton(); $this->assertFalse( RepoGroup::singleton()->hasForeignRepos() ); } @@ -27,7 +27,7 @@ class RepoGroupTest extends MediaWikiTestCase { function testForEachForeignRepoNone() { $this->setMwGlobals( 'wgForeignFileRepos', [] ); - RepoGroup::destroySingleton(); + $this->overrideMwServices(); FileBackendGroup::destroySingleton(); $fakeCallback = $this->createMock( RepoGroupTestHelper::class ); $fakeCallback->expects( $this->never() )->method( 'callback' ); @@ -48,7 +48,7 @@ class RepoGroupTest extends MediaWikiTestCase { 'apiThumbCacheExpiry' => 86400, 'directory' => $wgUploadDirectory ] ] ); - RepoGroup::destroySingleton(); + $this->overrideMwServices(); FileBackendGroup::destroySingleton(); } } diff --git a/tests/phpunit/includes/http/HttpTest.php b/tests/phpunit/includes/http/HttpTest.php index eee4296281..a8c53d9112 100644 --- a/tests/phpunit/includes/http/HttpTest.php +++ b/tests/phpunit/includes/http/HttpTest.php @@ -67,6 +67,8 @@ class HttpTest extends MediaWikiTestCase { * @covers Http::getProxy */ public function testGetProxy() { + $this->hideDeprecated( 'Http::getProxy' ); + $this->setMwGlobals( 'wgHTTPProxy', false ); $this->assertEquals( '', diff --git a/tests/phpunit/includes/jobqueue/JobQueueTest.php b/tests/phpunit/includes/jobqueue/JobQueueTest.php index 1baaa5489e..ce07f78bab 100644 --- a/tests/phpunit/includes/jobqueue/JobQueueTest.php +++ b/tests/phpunit/includes/jobqueue/JobQueueTest.php @@ -259,8 +259,7 @@ class JobQueueTest extends MediaWikiTestCase { $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" ); - $id = wfRandomString( 32 ); - $root1 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp + $root1 = Job::newRootJobParams( "nulljobspam:testId" ); // task ID/timestamp for ( $i = 0; $i < 5; ++$i ) { $this->assertNull( $queue->push( $this->newJob( 0, $root1 ) ), "Push worked ($desc)" ); } diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php index ef333f9420..4c937899db 100644 --- a/tests/phpunit/includes/libs/CSSMinTest.php +++ b/tests/phpunit/includes/libs/CSSMinTest.php @@ -85,9 +85,9 @@ class CSSMinTest extends MediaWikiTestCase { * @covers CSSMin::getMimeType */ public function testGetMimeType( $fileContents, $fileExtension, $expected ) { - $fileName = wfTempDir() . DIRECTORY_SEPARATOR . uniqid( 'MW_PHPUnit_CSSMinTest_' ) . '.' - . $fileExtension; - $this->addTmpFiles( $fileName ); + // Automatically removed when it falls out of scope (including if the test fails) + $file = TempFSFile::factory( 'PHPUnit_CSSMinTest_', $fileExtension, wfTempDir() ); + $fileName = $file->getPath(); file_put_contents( $fileName, $fileContents ); $this->assertSame( $expected, CSSMin::getMimeType( $fileName ) ); } diff --git a/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php index 0376803f41..9f88474e7b 100644 --- a/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php +++ b/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php @@ -28,8 +28,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase { * @covers MultiWriteBagOStuff::doWrite */ public function testSetImmediate() { - $key = wfRandomString(); - $value = wfRandomString(); + $key = 'key'; + $value = 'value'; $this->cache->set( $key, $value ); // Set in tier 1 @@ -42,8 +42,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase { * @covers MultiWriteBagOStuff */ public function testSyncMerge() { - $key = wfRandomString(); - $value = wfRandomString(); + $key = 'keyA'; + $value = 'value'; $func = function () use ( $value ) { return $value; }; @@ -56,14 +56,14 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase { // Set in tier 1 $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); // Not yet set in tier 2 - $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' ); + $this->assertFalse( $this->cache2->get( $key ), 'Not written to tier 2' ); $dbw->commit(); // Set in tier 2 $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); - $key = wfRandomString(); + $key = 'keyB'; $dbw->begin(); $this->cache->merge( $key, $func, 0, 1, BagOStuff::WRITE_SYNC ); @@ -80,8 +80,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase { * @covers MultiWriteBagOStuff::set */ public function testSetDelayed() { - $key = wfRandomString(); - $value = (object)[ 'v' => wfRandomString() ]; + $key = 'key'; + $value = (object)[ 'v' => 'saved value' ]; $expectValue = clone $value; // XXX: DeferredUpdates bound to transactions in CLI mode @@ -90,12 +90,12 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase { $this->cache->set( $key, $value ); // Test that later changes to $value don't affect the saved value (e.g. T168040) - $value->v = 'bogus'; + $value->v = 'other value'; // Set in tier 1 $this->assertEquals( $expectValue, $this->cache1->get( $key ), 'Written to tier 1' ); // Not yet set in tier 2 - $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' ); + $this->assertFalse( $this->cache2->get( $key ), 'Not written to tier 2' ); $dbw->commit(); diff --git a/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php index b7f22ece94..550ec0bd09 100644 --- a/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php +++ b/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php @@ -23,40 +23,40 @@ class ReplicatedBagOStuffTest extends MediaWikiTestCase { * @covers ReplicatedBagOStuff::set */ public function testSet() { - $key = wfRandomString(); - $value = wfRandomString(); + $key = 'a key'; + $value = 'a value'; $this->cache->set( $key, $value ); // Write to master. - $this->assertEquals( $this->writeCache->get( $key ), $value ); + $this->assertEquals( $value, $this->writeCache->get( $key ) ); // Don't write to replica. Replication is deferred to backend. - $this->assertEquals( $this->readCache->get( $key ), false ); + $this->assertFalse( $this->readCache->get( $key ) ); } /** * @covers ReplicatedBagOStuff::get */ public function testGet() { - $key = wfRandomString(); + $key = 'a key'; - $write = wfRandomString(); + $write = 'one value'; $this->writeCache->set( $key, $write ); - $read = wfRandomString(); + $read = 'another value'; $this->readCache->set( $key, $read ); // Read from replica. - $this->assertEquals( $this->cache->get( $key ), $read ); + $this->assertEquals( $read, $this->cache->get( $key ) ); } /** * @covers ReplicatedBagOStuff::get */ public function testGetAbsent() { - $key = wfRandomString(); - $value = wfRandomString(); + $key = 'a key'; + $value = 'a value'; $this->writeCache->set( $key, $value ); // Don't read from master. No failover if value is absent. - $this->assertEquals( $this->cache->get( $key ), false ); + $this->assertFalse( $this->cache->get( $key ) ); } } diff --git a/tests/phpunit/includes/linker/LinkRendererTest.php b/tests/phpunit/includes/linker/LinkRendererTest.php index 91ee276550..e90577c907 100644 --- a/tests/phpunit/includes/linker/LinkRendererTest.php +++ b/tests/phpunit/includes/linker/LinkRendererTest.php @@ -140,7 +140,8 @@ class LinkRendererTest extends MediaWikiLangTestCase { public function testGetLinkClasses() { $wanCache = ObjectCache::getMainWANInstance(); $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter(); - $linkCache = new LinkCache( $titleFormatter, $wanCache ); + $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); + $linkCache = new LinkCache( $titleFormatter, $wanCache, $nsInfo ); $foobarTitle = new TitleValue( NS_MAIN, 'FooBar' ); $redirectTitle = new TitleValue( NS_MAIN, 'Redirect' ); $userTitle = new TitleValue( NS_USER, 'Someuser' ); diff --git a/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php b/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php index bcd5c376c4..a00eb3fcd0 100644 --- a/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php +++ b/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php @@ -52,11 +52,19 @@ class DefaultPreferencesFactoryTest extends \MediaWikiTestCase { * @return DefaultPreferencesFactory */ protected function getPreferencesFactory() { + $mockNsInfo = $this->createMock( NamespaceInfo::class ); + $mockNsInfo->method( 'getValidNamespaces' )->willReturn( [ + NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK + ] ); + $mockNsInfo->expects( $this->never() ) + ->method( $this->anythingBut( 'getValidNamespaces', '__destruct' ) ); + return new DefaultPreferencesFactory( new ServiceOptions( DefaultPreferencesFactory::$constructorOptions, $this->config ), new Language(), AuthManager::singleton(), - MediaWikiServices::getInstance()->getLinkRenderer() + MediaWikiServices::getInstance()->getLinkRenderer(), + $mockNsInfo ); } diff --git a/tests/phpunit/includes/title/NamespaceInfoTest.php b/tests/phpunit/includes/title/NamespaceInfoTest.php index 21b64682d4..556c640bd6 100644 --- a/tests/phpunit/includes/title/NamespaceInfoTest.php +++ b/tests/phpunit/includes/title/NamespaceInfoTest.php @@ -5,218 +5,246 @@ * @file */ -use MediaWiki\MediaWikiServices; +use MediaWiki\Config\ServiceOptions; class NamespaceInfoTest extends MediaWikiTestCase { + /********************************************************************************************** + * Shared code + * %{ + */ + private $scopedCallback; - /** @var NamespaceInfo */ - private $obj; - - protected function setUp() { + public function setUp() { parent::setUp(); - $this->setMwGlobals( [ - 'wgContentNamespaces' => [ NS_MAIN ], - 'wgNamespacesWithSubpages' => [ - NS_TALK => true, - NS_USER => true, - NS_USER_TALK => true, - ], - 'wgCapitalLinks' => true, - 'wgCapitalLinkOverrides' => [], - 'wgNonincludableNamespaces' => [], - ] ); - - $this->obj = MediaWikiServices::getInstance()->getNamespaceInfo(); - } + // Boo, there's still some global state in the class :( + global $wgHooks; + $hooks = $wgHooks; + unset( $hooks['CanonicalNamespaces'] ); + $this->setMwGlobals( 'wgHooks', $hooks ); - /** - * @todo Write more texts, handle $wgAllowImageMoving setting - * @covers NamespaceInfo::isMovable - */ - public function testIsMovable() { - $this->assertFalse( $this->obj->isMovable( NS_SPECIAL ) ); + $this->scopedCallback = + ExtensionRegistry::getInstance()->setAttributeForTest( 'ExtensionNamespaces', [] ); } - private function assertIsSubject( $ns ) { - $this->assertTrue( $this->obj->isSubject( $ns ) ); - } + public function tearDown() { + $this->scopedCallback = null; - private function assertIsNotSubject( $ns ) { - $this->assertFalse( $this->obj->isSubject( $ns ) ); + parent::tearDown(); } /** - * Please make sure to change testIsTalk() if you change the assertions below - * @covers NamespaceInfo::isSubject + * TODO Make this a const once HHVM support is dropped (T192166) */ - public function testIsSubject() { - // Special namespaces - $this->assertIsSubject( NS_MEDIA ); - $this->assertIsSubject( NS_SPECIAL ); - - // Subject pages - $this->assertIsSubject( NS_MAIN ); - $this->assertIsSubject( NS_USER ); - $this->assertIsSubject( 100 ); # user defined - - // Talk pages - $this->assertIsNotSubject( NS_TALK ); - $this->assertIsNotSubject( NS_USER_TALK ); - $this->assertIsNotSubject( 101 ); # user defined + private static $defaultOptions = [ + 'AllowImageMoving' => true, + 'CanonicalNamespaceNames' => [ + NS_TALK => 'Talk', + NS_USER => 'User', + NS_USER_TALK => 'User_talk', + NS_SPECIAL => 'Special', + NS_MEDIA => 'Media', + ], + 'CapitalLinkOverrides' => [], + 'CapitalLinks' => true, + 'ContentNamespaces' => [ NS_MAIN ], + 'ExtraNamespaces' => [], + 'ExtraSignatureNamespaces' => [], + 'NamespaceContentModels' => [], + 'NamespaceProtection' => [], + 'NamespacesWithSubpages' => [ + NS_TALK => true, + NS_USER => true, + NS_USER_TALK => true, + ], + 'NonincludableNamespaces' => [], + 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ], + ]; + + private function newObj( array $options = [] ) : NamespaceInfo { + return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions, + $options, self::$defaultOptions ) ); } - private function assertIsTalk( $ns ) { - $this->assertTrue( $this->obj->isTalk( $ns ) ); - } + // %} End shared code - private function assertIsNotTalk( $ns ) { - $this->assertFalse( $this->obj->isTalk( $ns ) ); - } + /********************************************************************************************** + * Basic methods + * %{ + */ /** - * Reverse of testIsSubject(). - * Please update testIsSubject() if you change assertions below - * @covers NamespaceInfo::isTalk + * @covers NamespaceInfo::__construct + * @dataProvider provideConstructor + * @param ServiceOptions $options + * @param string|null $expectedExceptionText */ - public function testIsTalk() { - // Special namespaces - $this->assertIsNotTalk( NS_MEDIA ); - $this->assertIsNotTalk( NS_SPECIAL ); - - // Subject pages - $this->assertIsNotTalk( NS_MAIN ); - $this->assertIsNotTalk( NS_USER ); - $this->assertIsNotTalk( 100 ); # user defined + public function testConstructor( ServiceOptions $options, $expectedExceptionText = null ) { + if ( $expectedExceptionText !== null ) { + $this->setExpectedException( \Wikimedia\Assert\PreconditionException::class, + $expectedExceptionText ); + } + new NamespaceInfo( $options ); + $this->assertTrue( true ); + } - // Talk pages - $this->assertIsTalk( NS_TALK ); - $this->assertIsTalk( NS_USER_TALK ); - $this->assertIsTalk( 101 ); # user defined + public function provideConstructor() { + return [ + [ new ServiceOptions( NamespaceInfo::$constructorOptions, self::$defaultOptions ) ], + [ new ServiceOptions( [], [] ), 'Required options missing: ' ], + [ new ServiceOptions( + array_merge( NamespaceInfo::$constructorOptions, [ 'invalid' ] ), + self::$defaultOptions, + [ 'invalid' => '' ] + ), 'Unsupported options passed: invalid' ], + ]; } /** - * @covers NamespaceInfo::getSubject + * @dataProvider provideIsMovable + * @covers NamespaceInfo::isMovable + * + * @param bool $expected + * @param int $ns + * @param bool $allowImageMoving */ - public function testGetSubject() { - // Special namespaces are their own subjects - $this->assertEquals( NS_MEDIA, $this->obj->getSubject( NS_MEDIA ) ); - $this->assertEquals( NS_SPECIAL, $this->obj->getSubject( NS_SPECIAL ) ); - - $this->assertEquals( NS_MAIN, $this->obj->getSubject( NS_TALK ) ); - $this->assertEquals( NS_USER, $this->obj->getSubject( NS_USER_TALK ) ); + public function testIsMovable( $expected, $ns, $allowImageMoving = true ) { + $obj = $this->newObj( [ 'AllowImageMoving' => $allowImageMoving ] ); + $this->assertSame( $expected, $obj->isMovable( $ns ) ); } - /** - * Regular getTalk() calls - * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in - * the function testGetTalkExceptions() - * @covers NamespaceInfo::getTalk - */ - public function testGetTalk() { - $this->assertEquals( NS_TALK, $this->obj->getTalk( NS_MAIN ) ); - $this->assertEquals( NS_TALK, $this->obj->getTalk( NS_TALK ) ); - $this->assertEquals( NS_USER_TALK, $this->obj->getTalk( NS_USER ) ); - $this->assertEquals( NS_USER_TALK, $this->obj->getTalk( NS_USER_TALK ) ); + public function provideIsMovable() { + return [ + 'Main' => [ true, NS_MAIN ], + 'Talk' => [ true, NS_TALK ], + 'Special' => [ false, NS_SPECIAL ], + 'Nonexistent even namespace' => [ true, 1234 ], + 'Nonexistent odd namespace' => [ true, 12345 ], + + 'Media with image moving' => [ false, NS_MEDIA, true ], + 'Media with no image moving' => [ false, NS_MEDIA, false ], + 'File with image moving' => [ true, NS_FILE, true ], + 'File with no image moving' => [ false, NS_FILE, false ], + ]; } /** - * Exceptions with getTalk() - * NS_MEDIA does not have talk pages. MediaWiki raise an exception for them. - * @expectedException MWException - * @covers NamespaceInfo::getTalk + * @param int $ns + * @param bool $expected + * @dataProvider provideIsSubject + * @covers NamespaceInfo::isSubject */ - public function testGetTalkExceptionsForNsMedia() { - $this->assertNull( $this->obj->getTalk( NS_MEDIA ) ); + public function testIsSubject( $ns, $expected ) { + $this->assertSame( $expected, $this->newObj()->isSubject( $ns ) ); } /** - * Exceptions with getTalk() - * NS_SPECIAL does not have talk pages. MediaWiki raise an exception for them. - * @expectedException MWException - * @covers NamespaceInfo::getTalk + * @param int $ns + * @param bool $expected + * @dataProvider provideIsSubject + * @covers NamespaceInfo::isTalk */ - public function testGetTalkExceptionsForNsSpecial() { - $this->assertNull( $this->obj->getTalk( NS_SPECIAL ) ); + public function testIsTalk( $ns, $expected ) { + $this->assertSame( !$expected, $this->newObj()->isTalk( $ns ) ); } - /** - * Regular getAssociated() calls - * Namespaces without an associated page (NS_MEDIA, NS_SPECIAL) are tested in - * the function testGetAssociatedExceptions() - * @covers NamespaceInfo::getAssociated - */ - public function testGetAssociated() { - $this->assertEquals( NS_TALK, $this->obj->getAssociated( NS_MAIN ) ); - $this->assertEquals( NS_MAIN, $this->obj->getAssociated( NS_TALK ) ); + public function provideIsSubject() { + return [ + // Special namespaces + [ NS_MEDIA, true ], + [ NS_SPECIAL, true ], + + // Subject pages + [ NS_MAIN, true ], + [ NS_USER, true ], + [ 100, true ], + + // Talk pages + [ NS_TALK, false ], + [ NS_USER_TALK, false ], + [ 101, false ], + ]; } - # ## Exceptions with getAssociated() - # ## NS_MEDIA and NS_SPECIAL do not have talk pages. MediaWiki raises - # ## an exception for them. /** - * @expectedException MWException - * @covers NamespaceInfo::getAssociated + * @covers NamespaceInfo::exists + * @dataProvider provideExists + * @param int $ns + * @param bool $expected */ - public function testGetAssociatedExceptionsForNsMedia() { - $this->assertNull( $this->obj->getAssociated( NS_MEDIA ) ); + public function testExists( $ns, $expected ) { + $this->assertSame( $expected, $this->newObj()->exists( $ns ) ); } - /** - * @expectedException MWException - * @covers NamespaceInfo::getAssociated - */ - public function testGetAssociatedExceptionsForNsSpecial() { - $this->assertNull( $this->obj->getAssociated( NS_SPECIAL ) ); + public function provideExists() { + return [ + 'Main' => [ NS_MAIN, true ], + 'Talk' => [ NS_TALK, true ], + 'Media' => [ NS_MEDIA, true ], + 'Special' => [ NS_SPECIAL, true ], + 'Nonexistent' => [ 12345, false ], + 'Negative nonexistent' => [ -12345, false ], + ]; } /** * Note if we add a namespace registration system with keys like 'MAIN' - * we should add tests here for equivilance on things like 'MAIN' == 0 + * we should add tests here for equivalence on things like 'MAIN' == 0 * and 'MAIN' == NS_MAIN. * @covers NamespaceInfo::equals */ public function testEquals() { - $this->assertTrue( $this->obj->equals( NS_MAIN, NS_MAIN ) ); - $this->assertTrue( $this->obj->equals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN' - $this->assertTrue( $this->obj->equals( NS_USER, NS_USER ) ); - $this->assertTrue( $this->obj->equals( NS_USER, 2 ) ); - $this->assertTrue( $this->obj->equals( NS_USER_TALK, NS_USER_TALK ) ); - $this->assertTrue( $this->obj->equals( NS_SPECIAL, NS_SPECIAL ) ); - $this->assertFalse( $this->obj->equals( NS_MAIN, NS_TALK ) ); - $this->assertFalse( $this->obj->equals( NS_USER, NS_USER_TALK ) ); - $this->assertFalse( $this->obj->equals( NS_PROJECT, NS_TEMPLATE ) ); + $obj = $this->newObj(); + $this->assertTrue( $obj->equals( NS_MAIN, NS_MAIN ) ); + $this->assertTrue( $obj->equals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN' + $this->assertTrue( $obj->equals( NS_USER, NS_USER ) ); + $this->assertTrue( $obj->equals( NS_USER, 2 ) ); + $this->assertTrue( $obj->equals( NS_USER_TALK, NS_USER_TALK ) ); + $this->assertTrue( $obj->equals( NS_SPECIAL, NS_SPECIAL ) ); + $this->assertFalse( $obj->equals( NS_MAIN, NS_TALK ) ); + $this->assertFalse( $obj->equals( NS_USER, NS_USER_TALK ) ); + $this->assertFalse( $obj->equals( NS_PROJECT, NS_TEMPLATE ) ); } /** + * @param int $ns1 + * @param int $ns2 + * @param bool $expected + * @dataProvider provideSubjectEquals * @covers NamespaceInfo::subjectEquals */ - public function testSubjectEquals() { - $this->assertSameSubject( NS_MAIN, NS_MAIN ); - $this->assertSameSubject( NS_MAIN, 0 ); // In case we make NS_MAIN 'MAIN' - $this->assertSameSubject( NS_USER, NS_USER ); - $this->assertSameSubject( NS_USER, 2 ); - $this->assertSameSubject( NS_USER_TALK, NS_USER_TALK ); - $this->assertSameSubject( NS_SPECIAL, NS_SPECIAL ); - $this->assertSameSubject( NS_MAIN, NS_TALK ); - $this->assertSameSubject( NS_USER, NS_USER_TALK ); + public function testSubjectEquals( $ns1, $ns2, $expected ) { + $this->assertSame( $expected, $this->newObj()->subjectEquals( $ns1, $ns2 ) ); + } - $this->assertDifferentSubject( NS_PROJECT, NS_TEMPLATE ); - $this->assertDifferentSubject( NS_SPECIAL, NS_MAIN ); + public function provideSubjectEquals() { + return [ + [ NS_MAIN, NS_MAIN, true ], + // In case we make NS_MAIN 'MAIN' + [ NS_MAIN, 0, true ], + [ NS_USER, NS_USER, true ], + [ NS_USER, 2, true ], + [ NS_USER_TALK, NS_USER_TALK, true ], + [ NS_SPECIAL, NS_SPECIAL, true ], + [ NS_MAIN, NS_TALK, true ], + [ NS_USER, NS_USER_TALK, true ], + + [ NS_PROJECT, NS_TEMPLATE, false ], + [ NS_SPECIAL, NS_MAIN, false ], + [ NS_MEDIA, NS_SPECIAL, false ], + [ NS_SPECIAL, NS_MEDIA, false ], + ]; } /** - * @covers NamespaceInfo::subjectEquals + * @dataProvider provideHasTalkNamespace + * @covers NamespaceInfo::hasTalkNamespace + * + * @param int $ns + * @param bool $expected */ - public function testSpecialAndMediaAreDifferentSubjects() { - $this->assertDifferentSubject( - NS_MEDIA, NS_SPECIAL, - "NS_MEDIA and NS_SPECIAL are different subject namespaces" - ); - $this->assertDifferentSubject( - NS_SPECIAL, NS_MEDIA, - "NS_SPECIAL and NS_MEDIA are different subject namespaces" - ); + public function testHasTalkNamespace( $ns, $expected ) { + $this->assertSame( $expected, $this->newObj()->hasTalkNamespace( $ns ) ); } public function provideHasTalkNamespace() { @@ -235,178 +263,180 @@ class NamespaceInfoTest extends MediaWikiTestCase { } /** - * @dataProvider provideHasTalkNamespace - * @covers NamespaceInfo::hasTalkNamespace - * - * @param int $index + * @param int $ns * @param bool $expected + * @param array $contentNamespaces + * @covers NamespaceInfo::isContent + * @dataProvider provideIsContent */ - public function testHasTalkNamespace( $index, $expected ) { - $actual = $this->obj->hasTalkNamespace( $index ); - $this->assertSame( $actual, $expected, "NS $index" ); - } - - private function assertIsContent( $ns ) { - $this->assertTrue( $this->obj->isContent( $ns ) ); + public function testIsContent( $ns, $expected, $contentNamespaces = [ NS_MAIN ] ) { + $obj = $this->newObj( [ 'ContentNamespaces' => $contentNamespaces ] ); + $this->assertSame( $expected, $obj->isContent( $ns ) ); } - private function assertIsNotContent( $ns ) { - $this->assertFalse( $this->obj->isContent( $ns ) ); + public function provideIsContent() { + return [ + [ NS_MAIN, true ], + [ NS_MEDIA, false ], + [ NS_SPECIAL, false ], + [ NS_TALK, false ], + [ NS_USER, false ], + [ NS_CATEGORY, false ], + [ 100, false ], + [ 100, true, [ NS_MAIN, 100, 252 ] ], + [ 252, true, [ NS_MAIN, 100, 252 ] ], + [ NS_MAIN, true, [ NS_MAIN, 100, 252 ] ], + // NS_MAIN is always content + [ NS_MAIN, true, [] ], + ]; } /** - * @covers NamespaceInfo::isContent + * @dataProvider provideWantSignatures + * @covers NamespaceInfo::wantSignatures + * + * @param int $index + * @param bool $expected */ - public function testIsContent() { - // NS_MAIN is a content namespace per DefaultSettings.php - // and per function definition. - - $this->assertIsContent( NS_MAIN ); - - // Other namespaces which are not expected to be content + public function testWantSignatures( $index, $expected ) { + $this->assertSame( $expected, $this->newObj()->wantSignatures( $index ) ); + } - $this->assertIsNotContent( NS_MEDIA ); - $this->assertIsNotContent( NS_SPECIAL ); - $this->assertIsNotContent( NS_TALK ); - $this->assertIsNotContent( NS_USER ); - $this->assertIsNotContent( NS_CATEGORY ); - $this->assertIsNotContent( 100 ); + public function provideWantSignatures() { + return [ + 'Main' => [ NS_MAIN, false ], + 'Talk' => [ NS_TALK, true ], + 'User' => [ NS_USER, false ], + 'User talk' => [ NS_USER_TALK, true ], + 'Special' => [ NS_SPECIAL, false ], + 'Media' => [ NS_MEDIA, false ], + 'Nonexistent talk' => [ 12345, true ], + 'Nonexistent subject' => [ 123456, false ], + 'Nonexistent negative odd' => [ -12345, false ], + ]; } /** - * Similar to testIsContent() but alters the $wgContentNamespaces - * global variable. - * @covers NamespaceInfo::isContent + * @dataProvider provideWantSignatures_ExtraSignatureNamespaces + * @covers NamespaceInfo::wantSignatures + * + * @param int $index + * @param int $expected */ - public function testIsContentAdvanced() { - global $wgContentNamespaces; - - // Test that user defined namespace #252 is not content - $this->assertIsNotContent( 252 ); - - // Bless namespace # 252 as a content namespace - $wgContentNamespaces[] = 252; - - $this->assertIsContent( 252 ); - - // Makes sure NS_MAIN was not impacted - $this->assertIsContent( NS_MAIN ); + public function testWantSignatures_ExtraSignatureNamespaces( $index, $expected ) { + $obj = $this->newObj( [ 'ExtraSignatureNamespaces' => + [ NS_MAIN, NS_USER, NS_SPECIAL, NS_MEDIA, 123456, -12345 ] ] ); + $this->assertSame( $expected, $obj->wantSignatures( $index ) ); } - private function assertIsWatchable( $ns ) { - $this->assertTrue( $this->obj->isWatchable( $ns ) ); - } + public function provideWantSignatures_ExtraSignatureNamespaces() { + $ret = array_map( + function ( $arr ) { + // We've added all these as extra signature namespaces, so expect true + return [ $arr[0], true ]; + }, + self::provideWantSignatures() + ); - private function assertIsNotWatchable( $ns ) { - $this->assertFalse( $this->obj->isWatchable( $ns ) ); + // Add one more that's false + $ret['Another nonexistent subject'] = [ 12345678, false ]; + return $ret; } /** + * @param int $ns + * @param bool $expected * @covers NamespaceInfo::isWatchable + * @dataProvider provideIsWatchable */ - public function testIsWatchable() { - // Specials namespaces are not watchable - $this->assertIsNotWatchable( NS_MEDIA ); - $this->assertIsNotWatchable( NS_SPECIAL ); - - // Core defined namespaces are watchables - $this->assertIsWatchable( NS_MAIN ); - $this->assertIsWatchable( NS_TALK ); - - // Additional, user defined namespaces are watchables - $this->assertIsWatchable( 100 ); - $this->assertIsWatchable( 101 ); + public function testIsWatchable( $ns, $expected ) { + $this->assertSame( $expected, $this->newObj()->isWatchable( $ns ) ); } - private function assertHasSubpages( $ns ) { - $this->assertTrue( $this->obj->hasSubpages( $ns ) ); - } + public function provideIsWatchable() { + return [ + // Specials namespaces are not watchable + [ NS_MEDIA, false ], + [ NS_SPECIAL, false ], - private function assertHasNotSubpages( $ns ) { - $this->assertFalse( $this->obj->hasSubpages( $ns ) ); + // Core defined namespaces are watchables + [ NS_MAIN, true ], + [ NS_TALK, true ], + + // Additional, user defined namespaces are watchables + [ 100, true ], + [ 101, true ], + ]; } /** + * @param int $ns + * @param int $expected + * @param array|null $namespacesWithSubpages To pass to constructor * @covers NamespaceInfo::hasSubpages + * @dataProvider provideHasSubpages */ - public function testHasSubpages() { - global $wgNamespacesWithSubpages; - - // Special namespaces: - $this->assertHasNotSubpages( NS_MEDIA ); - $this->assertHasNotSubpages( NS_SPECIAL ); - - // Namespaces without subpages - $this->assertHasNotSubpages( NS_MAIN ); + public function testHasSubpages( $ns, $expected, array $namespacesWithSubpages = null ) { + $obj = $this->newObj( $namespacesWithSubpages + ? [ 'NamespacesWithSubpages' => $namespacesWithSubpages ] + : [] ); + $this->assertSame( $expected, $obj->hasSubpages( $ns ) ); + } - $wgNamespacesWithSubpages[NS_MAIN] = true; - $this->assertHasSubpages( NS_MAIN ); + public function provideHasSubpages() { + return [ + // Special namespaces: + [ NS_MEDIA, false ], + [ NS_SPECIAL, false ], - $wgNamespacesWithSubpages[NS_MAIN] = false; - $this->assertHasNotSubpages( NS_MAIN ); + // Namespaces without subpages + [ NS_MAIN, false ], + [ NS_MAIN, true, [ NS_MAIN => true ] ], + [ NS_MAIN, false, [ NS_MAIN => false ] ], - // Some namespaces with subpages - $this->assertHasSubpages( NS_TALK ); - $this->assertHasSubpages( NS_USER ); - $this->assertHasSubpages( NS_USER_TALK ); + // Some namespaces with subpages + [ NS_TALK, true ], + [ NS_USER, true ], + [ NS_USER_TALK, true ], + ]; } /** + * @param $contentNamespaces To pass to constructor + * @param array $expected + * @dataProvider provideGetContentNamespaces * @covers NamespaceInfo::getContentNamespaces */ - public function testGetContentNamespaces() { - global $wgContentNamespaces; - - $this->assertEquals( - [ NS_MAIN ], - $this->obj->getContentNamespaces(), - '$wgContentNamespaces is an array with only NS_MAIN by default' - ); - - # test !is_array( $wgcontentNamespaces ) - $wgContentNamespaces = ''; - $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() ); - - $wgContentNamespaces = false; - $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() ); - - $wgContentNamespaces = null; - $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() ); + public function testGetContentNamespaces( $contentNamespaces, array $expected ) { + $obj = $this->newObj( [ 'ContentNamespaces' => $contentNamespaces ] ); + $this->assertSame( $expected, $obj->getContentNamespaces() ); + } - $wgContentNamespaces = 5; - $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() ); + public function provideGetContentNamespaces() { + return [ + // Non-array + [ '', [ NS_MAIN ] ], + [ false, [ NS_MAIN ] ], + [ null, [ NS_MAIN ] ], + [ 5, [ NS_MAIN ] ], - # test $wgContentNamespaces === [] - $wgContentNamespaces = []; - $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() ); + // Empty array + [ [], [ NS_MAIN ] ], - # test !in_array( NS_MAIN, $wgContentNamespaces ) - $wgContentNamespaces = [ NS_USER, NS_CATEGORY ]; - $this->assertEquals( - [ NS_MAIN, NS_USER, NS_CATEGORY ], - $this->obj->getContentNamespaces(), - 'NS_MAIN is forced in $wgContentNamespaces even if unwanted' - ); + // NS_MAIN is forced to be content even if unwanted + [ [ NS_USER, NS_CATEGORY ], [ NS_MAIN, NS_USER, NS_CATEGORY ] ], - # test other cases, return $wgcontentNamespaces as is - $wgContentNamespaces = [ NS_MAIN ]; - $this->assertEquals( - [ NS_MAIN ], - $this->obj->getContentNamespaces() - ); - - $wgContentNamespaces = [ NS_MAIN, NS_USER, NS_CATEGORY ]; - $this->assertEquals( - [ NS_MAIN, NS_USER, NS_CATEGORY ], - $this->obj->getContentNamespaces() - ); + // In other cases, return as-is + [ [ NS_MAIN ], [ NS_MAIN ] ], + [ [ NS_MAIN, NS_USER, NS_CATEGORY ], [ NS_MAIN, NS_USER, NS_CATEGORY ] ], + ]; } /** * @covers NamespaceInfo::getSubjectNamespaces */ public function testGetSubjectNamespaces() { - $subjectsNS = $this->obj->getSubjectNamespaces(); + $subjectsNS = $this->newObj()->getSubjectNamespaces(); $this->assertContains( NS_MAIN, $subjectsNS, "Talk namespaces should have NS_MAIN" ); $this->assertNotContains( NS_TALK, $subjectsNS, @@ -422,7 +452,7 @@ class NamespaceInfoTest extends MediaWikiTestCase { * @covers NamespaceInfo::getTalkNamespaces */ public function testGetTalkNamespaces() { - $talkNS = $this->obj->getTalkNamespaces(); + $talkNS = $this->newObj()->getTalkNamespaces(); $this->assertContains( NS_TALK, $talkNS, "Subject namespaces should have NS_TALK" ); $this->assertNotContains( NS_MAIN, $talkNS, @@ -434,167 +464,870 @@ class NamespaceInfoTest extends MediaWikiTestCase { "Subject namespaces should not have NS_SPECIAL" ); } - private function assertIsCapitalized( $ns ) { - $this->assertTrue( $this->obj->isCapitalized( $ns ) ); + /** + * @param int $ns + * @param bool $expected + * @param bool $capitalLinks To pass to constructor + * @param array $capitalLinkOverrides To pass to constructor + * @dataProvider provideIsCapitalized + * @covers NamespaceInfo::isCapitalized + */ + public function testIsCapitalized( + $ns, $expected, $capitalLinks = true, array $capitalLinkOverrides = [] + ) { + $obj = $this->newObj( [ + 'CapitalLinks' => $capitalLinks, + 'CapitalLinkOverrides' => $capitalLinkOverrides, + ] ); + $this->assertSame( $expected, $obj->isCapitalized( $ns ) ); } - private function assertIsNotCapitalized( $ns ) { - $this->assertFalse( $this->obj->isCapitalized( $ns ) ); + public function provideIsCapitalized() { + return [ + // Test default settings + [ NS_PROJECT, true ], + [ NS_PROJECT_TALK, true ], + [ NS_MEDIA, true ], + [ NS_FILE, true ], + + // Always capitalized no matter what + [ NS_SPECIAL, true, false ], + [ NS_USER, true, false ], + [ NS_MEDIAWIKI, true, false ], + + // Even with an override too + [ NS_SPECIAL, true, false, [ NS_SPECIAL => false ] ], + [ NS_USER, true, false, [ NS_USER => false ] ], + [ NS_MEDIAWIKI, true, false, [ NS_MEDIAWIKI => false ] ], + + // Overrides work for other namespaces + [ NS_PROJECT, false, true, [ NS_PROJECT => false ] ], + [ NS_PROJECT, true, false, [ NS_PROJECT => true ] ], + + // NS_MEDIA is treated like NS_FILE, and ignores NS_MEDIA overrides + [ NS_MEDIA, false, true, [ NS_FILE => false, NS_MEDIA => true ] ], + [ NS_MEDIA, true, false, [ NS_FILE => true, NS_MEDIA => false ] ], + [ NS_FILE, false, true, [ NS_FILE => false, NS_MEDIA => true ] ], + [ NS_FILE, true, false, [ NS_FILE => true, NS_MEDIA => false ] ], + ]; } /** - * Some namespaces are always capitalized per code definition - * in NamespaceInfo::$alwaysCapitalizedNamespaces - * @covers NamespaceInfo::isCapitalized + * @covers NamespaceInfo::hasGenderDistinction */ - public function testIsCapitalizedHardcodedAssertions() { - // NS_MEDIA and NS_FILE are treated the same - $this->assertEquals( - $this->obj->isCapitalized( NS_MEDIA ), - $this->obj->isCapitalized( NS_FILE ), - 'NS_MEDIA and NS_FILE have same capitalization rendering' - ); + public function testHasGenderDistinction() { + $obj = $this->newObj(); - // Boths are capitalized by default - $this->assertIsCapitalized( NS_MEDIA ); - $this->assertIsCapitalized( NS_FILE ); + // Namespaces with gender distinctions + $this->assertTrue( $obj->hasGenderDistinction( NS_USER ) ); + $this->assertTrue( $obj->hasGenderDistinction( NS_USER_TALK ) ); + + // Other ones, "genderless" + $this->assertFalse( $obj->hasGenderDistinction( NS_MEDIA ) ); + $this->assertFalse( $obj->hasGenderDistinction( NS_SPECIAL ) ); + $this->assertFalse( $obj->hasGenderDistinction( NS_MAIN ) ); + $this->assertFalse( $obj->hasGenderDistinction( NS_TALK ) ); + } - // Always capitalized namespaces - // @see NamespaceInfo::$alwaysCapitalizedNamespaces - $this->assertIsCapitalized( NS_SPECIAL ); - $this->assertIsCapitalized( NS_USER ); - $this->assertIsCapitalized( NS_MEDIAWIKI ); + /** + * @covers NamespaceInfo::isNonincludable + */ + public function testIsNonincludable() { + $obj = $this->newObj( [ 'NonincludableNamespaces' => [ NS_USER ] ] ); + $this->assertTrue( $obj->isNonincludable( NS_USER ) ); + $this->assertFalse( $obj->isNonincludable( NS_TEMPLATE ) ); } /** - * Follows up for testIsCapitalizedHardcodedAssertions() but alter the - * global $wgCapitalLink setting to have extended coverage. + * @dataProvider provideGetNamespaceContentModel + * @covers NamespaceInfo::getNamespaceContentModel * - * NamespaceInfo::isCapitalized() rely on two global settings: - * $wgCapitalLinkOverrides = []; by default - * $wgCapitalLinks = true; by default - * This function test $wgCapitalLinks + * @param int $ns + * @param string $expected + */ + public function testGetNamespaceContentModel( $ns, $expected ) { + $obj = $this->newObj( [ 'NamespaceContentModels' => + [ NS_USER => CONTENT_MODEL_WIKITEXT, 123 => CONTENT_MODEL_JSON, 1234 => 'abcdef' ], + ] ); + $this->assertSame( $expected, $obj->getNamespaceContentModel( $ns ) ); + } + + public function provideGetNamespaceContentModel() { + return [ + [ NS_MAIN, null ], + [ NS_TALK, null ], + [ NS_USER, CONTENT_MODEL_WIKITEXT ], + [ NS_USER_TALK, null ], + [ NS_SPECIAL, null ], + [ 122, null ], + [ 123, CONTENT_MODEL_JSON ], + [ 1234, 'abcdef' ], + [ 1235, null ], + ]; + } + + /** + * @dataProvider provideGetCategoryLinkType + * @covers NamespaceInfo::getCategoryLinkType * - * Global setting correctness is tested against the NS_PROJECT and - * NS_PROJECT_TALK namespaces since they are not hardcoded nor specials - * @covers NamespaceInfo::isCapitalized + * @param int $ns + * @param string $expected */ - public function testIsCapitalizedWithWgCapitalLinks() { - $this->assertIsCapitalized( NS_PROJECT ); - $this->assertIsCapitalized( NS_PROJECT_TALK ); + public function testGetCategoryLinkType( $ns, $expected ) { + $this->assertSame( $expected, $this->newObj()->getCategoryLinkType( $ns ) ); + } - $this->setMwGlobals( 'wgCapitalLinks', false ); + public function provideGetCategoryLinkType() { + return [ + [ NS_MAIN, 'page' ], + [ NS_TALK, 'page' ], + [ NS_USER, 'page' ], + [ NS_USER_TALK, 'page' ], + + [ NS_FILE, 'file' ], + [ NS_FILE_TALK, 'page' ], - // hardcoded namespaces (see above function) are still capitalized: - $this->assertIsCapitalized( NS_SPECIAL ); - $this->assertIsCapitalized( NS_USER ); - $this->assertIsCapitalized( NS_MEDIAWIKI ); + [ NS_CATEGORY, 'subcat' ], + [ NS_CATEGORY_TALK, 'page' ], - // setting is correctly applied - $this->assertIsNotCapitalized( NS_PROJECT ); - $this->assertIsNotCapitalized( NS_PROJECT_TALK ); + [ 100, 'page' ], + [ 101, 'page' ], + ]; } + // %} End basic methods + + /********************************************************************************************** + * getSubject/Talk/Associated + * %{ + */ /** - * Counter part for NamespaceInfo::testIsCapitalizedWithWgCapitalLinks() now - * testing the $wgCapitalLinkOverrides global. + * @dataProvider provideSubjectTalk + * @covers NamespaceInfo::getSubject + * @covers NamespaceInfo::getSubjectPage + * @covers NamespaceInfo::isMethodValidFor + * @covers Title::getSubjectPage * - * @todo split groups of assertions in autonomous testing functions - * @covers NamespaceInfo::isCapitalized + * @param int $subject + * @param int $talk */ - public function testIsCapitalizedWithWgCapitalLinkOverrides() { - global $wgCapitalLinkOverrides; + public function testGetSubject( $subject, $talk ) { + $obj = $this->newObj(); + $this->assertSame( $subject, $obj->getSubject( $subject ) ); + $this->assertSame( $subject, $obj->getSubject( $talk ) ); + + $subjectTitleVal = new TitleValue( $subject, 'A' ); + $talkTitleVal = new TitleValue( $talk, 'A' ); + // Object will be the same one passed in if it's a subject, different but equal object if + // it's talk + $this->assertSame( $subjectTitleVal, $obj->getSubjectPage( $subjectTitleVal ) ); + $this->assertEquals( $subjectTitleVal, $obj->getSubjectPage( $talkTitleVal ) ); + + $subjectTitle = Title::makeTitle( $subject, 'A' ); + $talkTitle = Title::makeTitle( $talk, 'A' ); + $this->assertSame( $subjectTitle, $subjectTitle->getSubjectPage() ); + $this->assertEquals( $subjectTitle, $talkTitle->getSubjectPage() ); + } - // Test default settings - $this->assertIsCapitalized( NS_PROJECT ); - $this->assertIsCapitalized( NS_PROJECT_TALK ); + /** + * @dataProvider provideSpecialNamespaces + * @covers NamespaceInfo::getSubject + * @covers NamespaceInfo::getSubjectPage + * + * @param int $ns + */ + public function testGetSubject_special( $ns ) { + $obj = $this->newObj(); + $this->assertSame( $ns, $obj->getSubject( $ns ) ); - // hardcoded namespaces (see above function) are capitalized: - $this->assertIsCapitalized( NS_SPECIAL ); - $this->assertIsCapitalized( NS_USER ); - $this->assertIsCapitalized( NS_MEDIAWIKI ); + $title = new TitleValue( $ns, 'A' ); + $this->assertSame( $title, $obj->getSubjectPage( $title ) ); + } - // Hardcoded namespaces remains capitalized - $wgCapitalLinkOverrides[NS_SPECIAL] = false; - $wgCapitalLinkOverrides[NS_USER] = false; - $wgCapitalLinkOverrides[NS_MEDIAWIKI] = false; + /** + * @dataProvider provideSubjectTalk + * @covers NamespaceInfo::getTalk + * @covers NamespaceInfo::getTalkPage + * @covers NamespaceInfo::isMethodValidFor + * @covers Title::getTalkPage + * + * @param int $subject + * @param int $talk + */ + public function testGetTalk( $subject, $talk ) { + $obj = $this->newObj(); + $this->assertSame( $talk, $obj->getTalk( $subject ) ); + $this->assertSame( $talk, $obj->getTalk( $talk ) ); + + $subjectTitleVal = new TitleValue( $subject, 'A' ); + $talkTitleVal = new TitleValue( $talk, 'A' ); + // Object will be the same one passed in if it's a talk, different but equal object if it's + // subject + $this->assertEquals( $talkTitleVal, $obj->getTalkPage( $subjectTitleVal ) ); + $this->assertSame( $talkTitleVal, $obj->getTalkPage( $talkTitleVal ) ); + + $subjectTitle = Title::makeTitle( $subject, 'A' ); + $talkTitle = Title::makeTitle( $talk, 'A' ); + $this->assertEquals( $talkTitle, $subjectTitle->getTalkPage() ); + $this->assertSame( $talkTitle, $talkTitle->getTalkPage() ); + } - $this->assertIsCapitalized( NS_SPECIAL ); - $this->assertIsCapitalized( NS_USER ); - $this->assertIsCapitalized( NS_MEDIAWIKI ); + /** + * @dataProvider provideSpecialNamespaces + * @covers NamespaceInfo::getTalk + * @covers NamespaceInfo::isMethodValidFor + * + * @param int $ns + */ + public function testGetTalk_special( $ns ) { + $this->setExpectedException( MWException::class, + "NamespaceInfo::getTalk does not make any sense for given namespace $ns" ); + $this->newObj()->getTalk( $ns ); + } - $wgCapitalLinkOverrides[NS_PROJECT] = false; - $this->assertIsNotCapitalized( NS_PROJECT ); + /** + * @dataProvider provideSpecialNamespaces + * @covers NamespaceInfo::getTalk + * @covers NamespaceInfo::getTalkPage + * @covers NamespaceInfo::isMethodValidFor + * + * @param int $ns + */ + public function testGetTalkPage_special( $ns ) { + $this->setExpectedException( MWException::class, + "NamespaceInfo::getTalk does not make any sense for given namespace $ns" ); + $this->newObj()->getTalkPage( new TitleValue( $ns, 'A' ) ); + } - $wgCapitalLinkOverrides[NS_PROJECT] = true; - $this->assertIsCapitalized( NS_PROJECT ); + /** + * @dataProvider provideSpecialNamespaces + * @covers NamespaceInfo::getTalk + * @covers NamespaceInfo::getTalkPage + * @covers NamespaceInfo::isMethodValidFor + * @covers Title::getTalkPage + * + * @param int $ns + */ + public function testTitleGetTalkPage_special( $ns ) { + $this->setExpectedException( MWException::class, + "NamespaceInfo::getTalk does not make any sense for given namespace $ns" ); + Title::makeTitle( $ns, 'A' )->getTalkPage(); + } - unset( $wgCapitalLinkOverrides[NS_PROJECT] ); - $this->assertIsCapitalized( NS_PROJECT ); + /** + * @dataProvider provideSpecialNamespaces + * @covers NamespaceInfo::getAssociated + * @covers NamespaceInfo::isMethodValidFor + * + * @param int $ns + */ + public function testGetAssociated_special( $ns ) { + $this->setExpectedException( MWException::class, + "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" ); + $this->newObj()->getAssociated( $ns ); } /** - * @covers NamespaceInfo::hasGenderDistinction + * @dataProvider provideSpecialNamespaces + * @covers NamespaceInfo::getAssociated + * @covers NamespaceInfo::getAssociatedPage + * @covers NamespaceInfo::isMethodValidFor + * + * @param int $ns */ - public function testHasGenderDistinction() { - // Namespaces with gender distinctions - $this->assertTrue( $this->obj->hasGenderDistinction( NS_USER ) ); - $this->assertTrue( $this->obj->hasGenderDistinction( NS_USER_TALK ) ); + public function testGetAssociatedPage_special( $ns ) { + $this->setExpectedException( MWException::class, + "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" ); + $this->newObj()->getAssociatedPage( new TitleValue( $ns, 'A' ) ); + } - // Other ones, "genderless" - $this->assertFalse( $this->obj->hasGenderDistinction( NS_MEDIA ) ); - $this->assertFalse( $this->obj->hasGenderDistinction( NS_SPECIAL ) ); - $this->assertFalse( $this->obj->hasGenderDistinction( NS_MAIN ) ); - $this->assertFalse( $this->obj->hasGenderDistinction( NS_TALK ) ); + /** + * @dataProvider provideSpecialNamespaces + * @covers NamespaceInfo::getAssociated + * @covers NamespaceInfo::getAssociatedPage + * @covers NamespaceInfo::isMethodValidFor + * @covers Title::getOtherPage + * + * @param int $ns + */ + public function testTitleGetOtherPage_special( $ns ) { + $this->setExpectedException( MWException::class, + "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" ); + Title::makeTitle( $ns, 'A' )->getOtherPage(); } /** - * @covers NamespaceInfo::isNonincludable + * @dataProvider provideSubjectTalk + * @covers NamespaceInfo::getAssociated + * @covers NamespaceInfo::getAssociatedPage + * @covers Title::getOtherPage + * + * @param int $subject + * @param int $talk */ - public function testIsNonincludable() { - global $wgNonincludableNamespaces; + public function testGetAssociated( $subject, $talk ) { + $obj = $this->newObj(); + $this->assertSame( $talk, $obj->getAssociated( $subject ) ); + $this->assertSame( $subject, $obj->getAssociated( $talk ) ); + + $subjectTitle = new TitleValue( $subject, 'A' ); + $talkTitle = new TitleValue( $talk, 'A' ); + // Object will not be the same + $this->assertEquals( $talkTitle, $obj->getAssociatedPage( $subjectTitle ) ); + $this->assertEquals( $subjectTitle, $obj->getAssociatedPage( $talkTitle ) ); + + $subjectTitle = Title::makeTitle( $subject, 'A' ); + $talkTitle = Title::makeTitle( $talk, 'A' ); + $this->assertEquals( $talkTitle, $subjectTitle->getOtherPage() ); + $this->assertEquals( $subjectTitle, $talkTitle->getOtherPage() ); + } + + public static function provideSubjectTalk() { + return [ + // Format: [ subject, talk ] + 'Main/talk' => [ NS_MAIN, NS_TALK ], + 'User/user talk' => [ NS_USER, NS_USER_TALK ], + 'Unknown namespaces also supported' => [ 106, 107 ], + ]; + } + + public static function provideSpecialNamespaces() { + return [ + 'Special' => [ NS_SPECIAL ], + 'Media' => [ NS_MEDIA ], + 'Unknown negative index' => [ -613 ], + ]; + } - $wgNonincludableNamespaces = [ NS_USER ]; + // %} End getSubject/Talk/Associated - $this->assertTrue( $this->obj->isNonincludable( NS_USER ) ); - $this->assertFalse( $this->obj->isNonincludable( NS_TEMPLATE ) ); + /********************************************************************************************** + * Canonical namespaces + * %{ + */ + + // Default canonical namespaces + // %{ + private function getDefaultNamespaces() { + return [ NS_MAIN => '' ] + self::$defaultOptions['CanonicalNamespaceNames']; } - private function assertSameSubject( $ns1, $ns2, $msg = '' ) { - $this->assertTrue( $this->obj->subjectEquals( $ns1, $ns2 ), $msg ); + /** + * @covers NamespaceInfo::getCanonicalNamespaces + */ + public function testGetCanonicalNamespaces() { + $this->assertSame( + $this->getDefaultNamespaces(), + $this->newObj()->getCanonicalNamespaces() + ); } - private function assertDifferentSubject( $ns1, $ns2, $msg = '' ) { - $this->assertFalse( $this->obj->subjectEquals( $ns1, $ns2 ), $msg ); + /** + * @dataProvider provideGetCanonicalName + * @covers NamespaceInfo::getCanonicalName + * + * @param int $index + * @param string|bool $expected + */ + public function testGetCanonicalName( $index, $expected ) { + $this->assertSame( $expected, $this->newObj()->getCanonicalName( $index ) ); } - public function provideGetCategoryLinkType() { + public function provideGetCanonicalName() { return [ - [ NS_MAIN, 'page' ], - [ NS_TALK, 'page' ], - [ NS_USER, 'page' ], - [ NS_USER_TALK, 'page' ], + 'Main' => [ NS_MAIN, '' ], + 'Talk' => [ NS_TALK, 'Talk' ], + 'With underscore not space' => [ NS_USER_TALK, 'User_talk' ], + 'Special' => [ NS_SPECIAL, 'Special' ], + 'Nonexistent' => [ 12345, false ], + 'Nonexistent negative' => [ -12345, false ], + ]; + } - [ NS_FILE, 'file' ], - [ NS_FILE_TALK, 'page' ], + /** + * @dataProvider provideGetCanonicalIndex + * @covers NamespaceInfo::getCanonicalIndex + * + * @param string $name + * @param int|null $expected + */ + public function testGetCanonicalIndex( $name, $expected ) { + $this->assertSame( $expected, $this->newObj()->getCanonicalIndex( $name ) ); + } - [ NS_CATEGORY, 'subcat' ], - [ NS_CATEGORY_TALK, 'page' ], + public function provideGetCanonicalIndex() { + return [ + 'Main' => [ '', NS_MAIN ], + 'Talk' => [ 'talk', NS_TALK ], + 'Not lowercase' => [ 'Talk', null ], + 'With underscore' => [ 'user_talk', NS_USER_TALK ], + 'Space is not recognized for underscore' => [ 'user talk', null ], + '0' => [ '0', null ], + ]; + } - [ 100, 'page' ], - [ 101, 'page' ], + /** + * @covers NamespaceInfo::getValidNamespaces + */ + public function testGetValidNamespaces() { + $this->assertSame( + [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK ], + $this->newObj()->getValidNamespaces() + ); + } + + // %} End default canonical namespaces + + // No canonical namespace names + // %{ + /** + * @covers NamespaceInfo::getCanonicalNamespaces + */ + public function testGetCanonicalNamespaces_NoCanonicalNamespaceNames() { + $obj = $this->newObj( [ 'CanonicalNamespaceNames' => [] ] ); + + $this->assertSame( [ NS_MAIN => '' ], $obj->getCanonicalNamespaces() ); + } + + /** + * @covers NamespaceInfo::getCanonicalName + */ + public function testGetCanonicalName_NoCanonicalNamespaceNames() { + $obj = $this->newObj( [ 'CanonicalNamespaceNames' => [] ] ); + + $this->assertSame( '', $obj->getCanonicalName( NS_MAIN ) ); + $this->assertFalse( $obj->getCanonicalName( NS_TALK ) ); + } + + /** + * @covers NamespaceInfo::getCanonicalIndex + */ + public function testGetCanonicalIndex_NoCanonicalNamespaceNames() { + $obj = $this->newObj( [ 'CanonicalNamespaceNames' => [] ] ); + + $this->assertSame( NS_MAIN, $obj->getCanonicalIndex( '' ) ); + $this->assertNull( $obj->getCanonicalIndex( 'talk' ) ); + } + + /** + * @covers NamespaceInfo::getValidNamespaces + */ + public function testGetValidNamespaces_NoCanonicalNamespaceNames() { + $obj = $this->newObj( [ 'CanonicalNamespaceNames' => [] ] ); + + $this->assertSame( [ NS_MAIN ], $obj->getValidNamespaces() ); + } + + // %} End no canonical namespace names + + // Test extension namespaces + // %{ + private function setupExtensionNamespaces() { + $this->scopedCallback = null; + $this->scopedCallback = ExtensionRegistry::getInstance()->setAttributeForTest( + 'ExtensionNamespaces', + [ NS_MAIN => 'No effect', NS_TALK => 'No effect', 12345 => 'Extended' ] + ); + } + + /** + * @covers NamespaceInfo::getCanonicalNamespaces + */ + public function testGetCanonicalNamespaces_ExtensionNamespaces() { + $this->setupExtensionNamespaces(); + + $this->assertSame( + $this->getDefaultNamespaces() + [ 12345 => 'Extended' ], + $this->newObj()->getCanonicalNamespaces() + ); + } + + /** + * @covers NamespaceInfo::getCanonicalName + */ + public function testGetCanonicalName_ExtensionNamespaces() { + $this->setupExtensionNamespaces(); + $obj = $this->newObj(); + + $this->assertSame( '', $obj->getCanonicalName( NS_MAIN ) ); + $this->assertSame( 'Talk', $obj->getCanonicalName( NS_TALK ) ); + $this->assertSame( 'Extended', $obj->getCanonicalName( 12345 ) ); + } + + /** + * @covers NamespaceInfo::getCanonicalIndex + */ + public function testGetCanonicalIndex_ExtensionNamespaces() { + $this->setupExtensionNamespaces(); + $obj = $this->newObj(); + + $this->assertSame( NS_MAIN, $obj->getCanonicalIndex( '' ) ); + $this->assertSame( NS_TALK, $obj->getCanonicalIndex( 'talk' ) ); + $this->assertSame( 12345, $obj->getCanonicalIndex( 'extended' ) ); + } + + /** + * @covers NamespaceInfo::getValidNamespaces + */ + public function testGetValidNamespaces_ExtensionNamespaces() { + $this->setupExtensionNamespaces(); + + $this->assertSame( + [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK, 12345 ], + $this->newObj()->getValidNamespaces() + ); + } + + // %} End extension namespaces + + // Hook namespaces + // %{ + /** + * @return array Expected canonical namespaces + */ + private function setupHookNamespaces() { + $callback = + function ( &$canonicalNamespaces ) { + $canonicalNamespaces[NS_MAIN] = 'Main'; + unset( $canonicalNamespaces[NS_MEDIA] ); + $canonicalNamespaces[123456] = 'Hooked'; + }; + $this->setTemporaryHook( 'CanonicalNamespaces', $callback ); + $expected = $this->getDefaultNamespaces(); + ( $callback )( $expected ); + return $expected; + } + + /** + * @covers NamespaceInfo::getCanonicalNamespaces + */ + public function testGetCanonicalNamespaces_HookNamespaces() { + $expected = $this->setupHookNamespaces(); + + $this->assertSame( $expected, $this->newObj()->getCanonicalNamespaces() ); + } + + /** + * @covers NamespaceInfo::getCanonicalName + */ + public function testGetCanonicalName_HookNamespaces() { + $this->setupHookNamespaces(); + $obj = $this->newObj(); + + $this->assertSame( 'Main', $obj->getCanonicalName( NS_MAIN ) ); + $this->assertFalse( $obj->getCanonicalName( NS_MEDIA ) ); + $this->assertSame( 'Hooked', $obj->getCanonicalName( 123456 ) ); + } + + /** + * @covers NamespaceInfo::getCanonicalIndex + */ + public function testGetCanonicalIndex_HookNamespaces() { + $this->setupHookNamespaces(); + $obj = $this->newObj(); + + $this->assertSame( NS_MAIN, $obj->getCanonicalIndex( 'main' ) ); + $this->assertNull( $obj->getCanonicalIndex( 'media' ) ); + $this->assertSame( 123456, $obj->getCanonicalIndex( 'hooked' ) ); + } + + /** + * @covers NamespaceInfo::getValidNamespaces + */ + public function testGetValidNamespaces_HookNamespaces() { + $this->setupHookNamespaces(); + + $this->assertSame( + [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK, 123456 ], + $this->newObj()->getValidNamespaces() + ); + } + + // %} End hook namespaces + + // Extra namespaces + // %{ + /** + * @return NamespaceInfo + */ + private function setupExtraNamespaces() { + return $this->newObj( [ 'ExtraNamespaces' => + [ NS_MAIN => 'No effect', NS_TALK => 'No effect', 1234567 => 'Extra' ] + ] ); + } + + /** + * @covers NamespaceInfo::getCanonicalNamespaces + */ + public function testGetCanonicalNamespaces_ExtraNamespaces() { + $this->assertSame( + $this->getDefaultNamespaces() + [ 1234567 => 'Extra' ], + $this->setupExtraNamespaces()->getCanonicalNamespaces() + ); + } + + /** + * @covers NamespaceInfo::getCanonicalName + */ + public function testGetCanonicalName_ExtraNamespaces() { + $obj = $this->setupExtraNamespaces(); + + $this->assertSame( '', $obj->getCanonicalName( NS_MAIN ) ); + $this->assertSame( 'Talk', $obj->getCanonicalName( NS_TALK ) ); + $this->assertSame( 'Extra', $obj->getCanonicalName( 1234567 ) ); + } + + /** + * @covers NamespaceInfo::getCanonicalIndex + */ + public function testGetCanonicalIndex_ExtraNamespaces() { + $obj = $this->setupExtraNamespaces(); + + $this->assertNull( $obj->getCanonicalIndex( 'no effect' ) ); + $this->assertNull( $obj->getCanonicalIndex( 'no_effect' ) ); + $this->assertSame( 1234567, $obj->getCanonicalIndex( 'extra' ) ); + } + + /** + * @covers NamespaceInfo::getValidNamespaces + */ + public function testGetValidNamespaces_ExtraNamespaces() { + $this->assertSame( + [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK, 1234567 ], + $this->setupExtraNamespaces()->getValidNamespaces() + ); + } + + // %} End extra namespaces + + // Canonical namespace caching + // %{ + /** + * @covers NamespaceInfo::getCanonicalNamespaces + */ + public function testGetCanonicalNamespaces_caching() { + $obj = $this->newObj(); + + // This should cache the values + $obj->getCanonicalNamespaces(); + + // Now try to alter them through nefarious means + $this->setupExtensionNamespaces(); + $this->setupHookNamespaces(); + + // Should have no effect + $this->assertSame( $this->getDefaultNamespaces(), $obj->getCanonicalNamespaces() ); + } + + /** + * @covers NamespaceInfo::getCanonicalName + */ + public function testGetCanonicalName_caching() { + $obj = $this->newObj(); + + // This should cache the values + $obj->getCanonicalName( NS_MAIN ); + + // Now try to alter them through nefarious means + $this->setupExtensionNamespaces(); + $this->setupHookNamespaces(); + + // Should have no effect + $this->assertSame( '', $obj->getCanonicalName( NS_MAIN ) ); + $this->assertSame( 'Media', $obj->getCanonicalName( NS_MEDIA ) ); + $this->assertFalse( $obj->getCanonicalName( 12345 ) ); + $this->assertFalse( $obj->getCanonicalName( 123456 ) ); + } + + /** + * @covers NamespaceInfo::getCanonicalIndex + */ + public function testGetCanonicalIndex_caching() { + $obj = $this->newObj(); + + // This should cache the values + $obj->getCanonicalIndex( '' ); + + // Now try to alter them through nefarious means + $this->setupExtensionNamespaces(); + $this->setupHookNamespaces(); + + // Should have no effect + $this->assertSame( NS_MAIN, $obj->getCanonicalIndex( '' ) ); + $this->assertSame( NS_MEDIA, $obj->getCanonicalIndex( 'media' ) ); + $this->assertNull( $obj->getCanonicalIndex( 'extended' ) ); + $this->assertNull( $obj->getCanonicalIndex( 'hooked' ) ); + } + + /** + * @covers NamespaceInfo::getValidNamespaces + */ + public function testGetValidNamespaces_caching() { + $obj = $this->newObj(); + + // This should cache the values + $obj->getValidNamespaces(); + + // Now try to alter through nefarious means + $this->setupExtensionNamespaces(); + $this->setupHookNamespaces(); + + // Should have no effect + $this->assertSame( + [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK ], + $obj->getValidNamespaces() + ); + } + + // %} End canonical namespace caching + + // Miscellaneous + // %{ + + /** + * @dataProvider provideGetValidNamespaces_misc + * @covers NamespaceInfo::getValidNamespaces + * + * @param array $namespaces List of namespace indices to return from getCanonicalNamespaces() + * (list is overwritten by a hook, so NS_MAIN doesn't have to be present) + * @param array $expected + */ + public function testGetValidNamespaces_misc( array $namespaces, array $expected ) { + // Each namespace's name is just its index + $this->setTemporaryHook( 'CanonicalNamespaces', + function ( &$canonicalNamespaces ) use ( $namespaces ) { + $canonicalNamespaces = array_combine( $namespaces, $namespaces ); + } + ); + $this->assertSame( $expected, $this->newObj()->getValidNamespaces() ); + } + + public function provideGetValidNamespaces_misc() { + return [ + 'Out of order (T109137)' => [ [ 1, 0 ], [ 0, 1 ] ], + 'Alphabetical order' => [ [ 10, 2 ], [ 2, 10 ] ], + 'Negative' => [ [ -1000, -500, -2, 0 ], [ 0 ] ], ]; } + // %} End miscellaneous + // %} End canonical namespaces + + /********************************************************************************************** + * Restriction levels + * %{ + */ + + /** + * This mock user can only have isAllowed() called on it. + * + * @param array $groups Groups for the mock user to have + * @return User + */ + private function getMockUser( array $groups = [] ) : User { + $groups[] = '*'; + + $mock = $this->createMock( User::class ); + $mock->method( 'isAllowed' )->will( $this->returnCallback( + function ( $action ) use ( $groups ) { + global $wgGroupPermissions, $wgRevokePermissions; + if ( $action == '' ) { + return true; + } + foreach ( $wgRevokePermissions as $group => $rights ) { + if ( !in_array( $group, $groups ) ) { + continue; + } + if ( isset( $rights[$action] ) && $rights[$action] ) { + return false; + } + } + foreach ( $wgGroupPermissions as $group => $rights ) { + if ( !in_array( $group, $groups ) ) { + continue; + } + if ( isset( $rights[$action] ) && $rights[$action] ) { + return true; + } + } + return false; + } + ) ); + $mock->expects( $this->never() )->method( $this->anythingBut( 'isAllowed' ) ); + return $mock; + } + /** - * @dataProvider provideGetCategoryLinkType - * @covers NamespaceInfo::getCategoryLinkType + * @dataProvider provideGetRestrictionLevels + * @covers NamespaceInfo::getRestrictionLevels * - * @param int $index - * @param string $expected + * @param array $expected + * @param int $ns + * @param User|null $user */ - public function testGetCategoryLinkType( $index, $expected ) { - $actual = $this->obj->getCategoryLinkType( $index ); - $this->assertSame( $expected, $actual, "NS $index" ); + public function testGetRestrictionLevels( array $expected, $ns, User $user = null ) { + $this->setMwGlobals( [ + 'wgGroupPermissions' => [ + '*' => [ 'edit' => true ], + 'autoconfirmed' => [ 'editsemiprotected' => true ], + 'sysop' => [ + 'editsemiprotected' => true, + 'editprotected' => true, + ], + 'privileged' => [ 'privileged' => true ], + ], + 'wgRevokePermissions' => [ + 'noeditsemiprotected' => [ 'editsemiprotected' => true ], + ], + ] ); + $obj = $this->newObj( [ + 'NamespaceProtection' => [ + NS_MAIN => 'autoconfirmed', + NS_USER => 'sysop', + 101 => [ 'editsemiprotected', 'privileged' ], + ], + ] ); + $this->assertSame( $expected, $obj->getRestrictionLevels( $ns, $user ) ); } + + public function provideGetRestrictionLevels() { + return [ + 'No namespace restriction' => [ [ '', 'autoconfirmed', 'sysop' ], NS_TALK ], + 'Restricted to autoconfirmed' => [ [ '', 'sysop' ], NS_MAIN ], + 'Restricted to sysop' => [ [ '' ], NS_USER ], + // @todo Bug -- 'sysop' protection should be allowed in this case. Someone who's + // autoconfirmed and also privileged can edit this namespace, and would be blocked by + // the sysop protection. + 'Restricted to someone in two groups' => [ [ '' ], 101 ], + + 'No special permissions' => [ [ '' ], NS_TALK, $this->getMockUser() ], + 'autoconfirmed' => [ + [ '', 'autoconfirmed' ], + NS_TALK, + $this->getMockUser( [ 'autoconfirmed' ] ) + ], + 'autoconfirmed revoked' => [ + [ '' ], + NS_TALK, + $this->getMockUser( [ 'autoconfirmed', 'noeditsemiprotected' ] ) + ], + 'sysop' => [ + [ '', 'autoconfirmed', 'sysop' ], + NS_TALK, + $this->getMockUser( [ 'sysop' ] ) + ], + 'sysop with autoconfirmed revoked (a bit silly)' => [ + [ '', 'sysop' ], + NS_TALK, + $this->getMockUser( [ 'sysop', 'noeditsemiprotected' ] ) + ], + ]; + } + + // %} End restriction levels } + +/** + * For really cool vim folding this needs to be at the end: + * vim: foldmarker=%{,%} foldmethod=marker + */ diff --git a/tests/phpunit/includes/user/UserTest.php b/tests/phpunit/includes/user/UserTest.php index 481da75af6..474decf2b0 100644 --- a/tests/phpunit/includes/user/UserTest.php +++ b/tests/phpunit/includes/user/UserTest.php @@ -504,20 +504,24 @@ class UserTest extends MediaWikiTestCase { } /** + * @covers User::isRegistered * @covers User::isLoggedIn * @covers User::isAnon */ public function testLoggedIn() { $user = $this->getMutableTestUser()->getUser(); + $this->assertTrue( $user->isRegistered() ); $this->assertTrue( $user->isLoggedIn() ); $this->assertFalse( $user->isAnon() ); // Non-existent users are perceived as anonymous $user = User::newFromName( 'UTNonexistent' ); + $this->assertFalse( $user->isRegistered() ); $this->assertFalse( $user->isLoggedIn() ); $this->assertTrue( $user->isAnon() ); $user = new User; + $this->assertFalse( $user->isRegistered() ); $this->assertFalse( $user->isLoggedIn() ); $this->assertTrue( $user->isAnon() ); } @@ -1201,6 +1205,15 @@ class UserTest extends MediaWikiTestCase { $this->assertSame( 'Bogus', $test->getName() ); $this->assertSame( 654321, $test->getActorId() ); + // Loading remote user by name from remote wiki should succeed + $test = User::newFromAnyId( null, 'Bogus', null, 'foo' ); + $this->assertSame( 0, $test->getId() ); + $this->assertSame( 'Bogus', $test->getName() ); + $this->assertSame( 0, $test->getActorId() ); + $test = User::newFromAnyId( 123456, 'Bogus', 654321, 'foo' ); + $this->assertSame( 0, $test->getId() ); + $this->assertSame( 0, $test->getActorId() ); + // Exceptional cases try { User::newFromAnyId( null, null, null ); @@ -1212,6 +1225,13 @@ class UserTest extends MediaWikiTestCase { $this->fail( 'Expected exception not thrown' ); } catch ( InvalidArgumentException $ex ) { } + + // Loading remote user by id from remote wiki should fail + try { + User::newFromAnyId( 123456, null, 654321, 'foo' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + } } /** diff --git a/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php b/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php index a8761e39e9..f424b21b3e 100644 --- a/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php +++ b/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php @@ -1,5 +1,7 @@ setExpectedException( DBReadOnlyError::class ); - $noWriteService->addWatch( $this->getTestSysop()->getUser(), new TitleValue( 0, 'Foo' ) ); + $noWriteService->addWatch( + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) ); } public function testAddWatchBatchForUser() { @@ -24,7 +27,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $this->setExpectedException( DBReadOnlyError::class ); - $noWriteService->addWatchBatchForUser( $this->getTestSysop()->getUser(), [] ); + $noWriteService->addWatchBatchForUser( new UserIdentityValue( 1, 'MockUser', 0 ), [] ); } public function testRemoveWatch() { @@ -34,7 +37,8 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $this->setExpectedException( DBReadOnlyError::class ); - $noWriteService->removeWatch( $this->getTestSysop()->getUser(), new TitleValue( 0, 'Foo' ) ); + $noWriteService->removeWatch( + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) ); } public function testSetNotificationTimestampsForUser() { @@ -45,7 +49,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $this->setExpectedException( DBReadOnlyError::class ); $noWriteService->setNotificationTimestampsForUser( - $this->getTestSysop()->getUser(), + new UserIdentityValue( 1, 'MockUser', 0 ), 'timestamp', [] ); @@ -59,7 +63,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $this->setExpectedException( DBReadOnlyError::class ); $noWriteService->updateNotificationTimestamp( - $this->getTestSysop()->getUser(), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ), 'timestamp' ); @@ -73,8 +77,8 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $this->setExpectedException( DBReadOnlyError::class ); $noWriteService->resetNotificationTimestamp( - $this->getTestSysop()->getUser(), - Title::newFromText( 'Foo' ) + new UserIdentityValue( 1, 'MockUser', 0 ), + new TitleValue( 0, 'Foo' ) ); } @@ -85,7 +89,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $return = $noWriteService->countWatchedItems( - $this->getTestSysop()->getUser() + new UserIdentityValue( 1, 'MockUser', 0 ) ); $this->assertEquals( __METHOD__, $return ); } @@ -154,7 +158,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $return = $noWriteService->getWatchedItem( - $this->getTestSysop()->getUser(), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) ); $this->assertEquals( __METHOD__, $return ); @@ -167,7 +171,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $return = $noWriteService->loadWatchedItem( - $this->getTestSysop()->getUser(), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) ); $this->assertEquals( __METHOD__, $return ); @@ -182,7 +186,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $return = $noWriteService->getWatchedItemsForUser( - $this->getTestSysop()->getUser(), + new UserIdentityValue( 1, 'MockUser', 0 ), [] ); $this->assertEquals( __METHOD__, $return ); @@ -195,7 +199,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $return = $noWriteService->isWatched( - $this->getTestSysop()->getUser(), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) ); $this->assertEquals( __METHOD__, $return ); @@ -210,7 +214,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $return = $noWriteService->getNotificationTimestampsBatch( - $this->getTestSysop()->getUser(), + new UserIdentityValue( 1, 'MockUser', 0 ), [ new TitleValue( 0, 'Foo' ) ] ); $this->assertEquals( __METHOD__, $return ); @@ -225,7 +229,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { $noWriteService = new NoWriteWatchedItemStore( $innerService ); $return = $noWriteService->countUnreadNotifications( - $this->getTestSysop()->getUser(), + new UserIdentityValue( 1, 'MockUser', 0 ), 88 ); $this->assertEquals( __METHOD__, $return ); diff --git a/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php b/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php index b22b7f8141..3ba8773c29 100644 --- a/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php +++ b/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php @@ -1,5 +1,6 @@ getMockBuilder( User::class )->getMock(); - $mock->expects( $this->any() ) - ->method( 'isAnon' ) - ->will( $this->returnValue( false ) ); - $mock->expects( $this->any() ) - ->method( 'getId' ) - ->will( $this->returnValue( $id ) ); + $mock->method( 'isRegistered' )->willReturn( true ); + $mock->method( 'getId' )->willReturn( $id ); + $methods = array_merge( [ + 'isRegistered', + 'getId', + ], $extraMethods ); + $mock->expects( $this->never() )->method( $this->anythingBut( ...$methods ) ); return $mock; } /** * @param int $id + * @param string[] $extraMethods Extra methods that are expected might be called * @return PHPUnit_Framework_MockObject_MockObject|User */ - private function getMockUnrestrictedNonAnonUserWithId( $id ) { - $mock = $this->getMockNonAnonUserWithId( $id ); - $mock->expects( $this->any() ) - ->method( 'isAllowed' ) - ->will( $this->returnValue( true ) ); - $mock->expects( $this->any() ) - ->method( 'isAllowedAny' ) - ->will( $this->returnValue( true ) ); - $mock->expects( $this->any() ) - ->method( 'useRCPatrol' ) - ->will( $this->returnValue( true ) ); + private function getMockUnrestrictedNonAnonUserWithId( $id, array $extraMethods = [] ) { + $mock = $this->getMockNonAnonUserWithId( $id, + array_merge( [ 'isAllowed', 'isAllowedAny', 'useRCPatrol' ], $extraMethods ) ); + $mock->method( 'isAllowed' )->willReturn( true ); + $mock->method( 'isAllowedAny' )->willReturn( true ); + $mock->method( 'useRCPatrol' )->willReturn( true ); return $mock; } @@ -193,18 +192,19 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { * @return PHPUnit_Framework_MockObject_MockObject|User */ private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) { - $mock = $this->getMockNonAnonUserWithId( $id ); + $mock = $this->getMockNonAnonUserWithId( $id, + [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] ); - $mock->expects( $this->any() ) - ->method( 'isAllowed' ) + $mock->method( 'isAllowed' ) ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) { return $action !== $notAllowedAction; } ) ); - $mock->expects( $this->any() ) - ->method( 'isAllowedAny' ) + $mock->method( 'isAllowedAny' ) ->will( $this->returnCallback( function ( ...$actions ) use ( $notAllowedAction ) { return !in_array( $notAllowedAction, $actions ); } ) ); + $mock->method( 'useRCPatrol' )->willReturn( false ); + $mock->method( 'useNPPatrol' )->willReturn( false ); return $mock; } @@ -214,7 +214,8 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { * @return PHPUnit_Framework_MockObject_MockObject|User */ private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) { - $mock = $this->getMockNonAnonUserWithId( $id ); + $mock = $this->getMockNonAnonUserWithId( $id, + [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] ); $mock->expects( $this->any() ) ->method( 'isAllowed' ) @@ -233,14 +234,6 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { return $mock; } - private function getMockAnonUser() { - $mock = $this->getMockBuilder( User::class )->getMock(); - $mock->expects( $this->any() ) - ->method( 'isAnon' ) - ->will( $this->returnValue( true ) ); - return $mock; - } - private function getFakeRow( array $rowValues ) { $fakeRow = new stdClass(); foreach ( $rowValues as $valueName => $value ) { @@ -1382,7 +1375,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); - $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 ); + $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] ); $otherUser->expects( $this->once() ) ->method( 'getOption' ) ->with( 'watchlisttoken' ) @@ -1413,7 +1406,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); - $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 ); + $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] ); $otherUser->expects( $this->once() ) ->method( 'getOption' ) ->with( 'watchlisttoken' ) @@ -1713,7 +1706,8 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { $queryService = $this->newService( $mockDb ); - $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() ); + $items = $queryService->getWatchedItemsForUser( + new UserIdentityValue( 0, 'AnonUser', 0 ) ); $this->assertEmpty( $items ); } diff --git a/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php b/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php index 2f95688548..82308de4ea 100644 --- a/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php +++ b/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php @@ -1,8 +1,10 @@ createMock( User::class ); - $mock->expects( $this->any() ) - ->method( 'isAnon' ) - ->will( $this->returnValue( false ) ); - $mock->expects( $this->any() ) - ->method( 'getId' ) - ->will( $this->returnValue( $id ) ); - $mock->expects( $this->any() ) - ->method( 'getUserPage' ) - ->will( $this->returnValue( Title::makeTitle( NS_USER, 'MockUser' ) ) ); + private function getMockNsInfo() : NamespaceInfo { + $mock = $this->createMock( NamespaceInfo::class ); + $mock->method( 'getSubjectPage' )->will( $this->returnArgument( 0 ) ); + $mock->method( 'getTalkPage' )->will( $this->returnCallback( + function ( $target ) { + return new TitleValue( 1, $target->getDbKey() ); + } + ) ); + $mock->expects( $this->never() ) + ->method( $this->anythingBut( 'getSubjectPage', 'getTalkPage' ) ); return $mock; } /** - * @return User + * No methods may be called except provided callbacks, if any. + * + * @param array $callbacks Keys are method names, values are callbacks + * @param array $counts Keys are method names, values are expected number of times to be called + * (default is any number is okay) */ - private function getAnonUser() { - return User::newFromName( 'Anon_User' ); + private function getMockRevisionLookup( + array $callbacks = [], array $counts = [] + ) : RevisionLookup { + $mock = $this->createMock( RevisionLookup::class ); + foreach ( $callbacks as $method => $callback ) { + $count = isset( $counts[$method] ) ? $this->exactly( $counts[$method] ) : $this->any(); + $mock->expects( $count ) + ->method( $method ) + ->will( $this->returnCallback( $callbacks[$method] ) ); + } + $mock->expects( $this->never() ) + ->method( $this->anythingBut( ...array_keys( $callbacks ) ) ); + return $mock; } private function getFakeRow( array $rowValues ) { @@ -141,24 +157,33 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { return $fakeRow; } - private function newWatchedItemStore( - LBFactory $lbFactory, - JobQueueGroup $queueGroup, - HashBagOStuff $cache, - ReadOnlyMode $readOnlyMode - ) { + /** + * @param array $mocks Associative array providing mocks to use when constructing the + * WatchedItemStore. Anything not provided will fall back to a default. Valid keys: + * * lbFactory + * * db + * * queueGroup + * * cache + * * readOnlyMode + * * nsInfo + * * revisionLookup + */ + private function newWatchedItemStore( array $mocks = [] ) : WatchedItemStore { return new WatchedItemStore( - $lbFactory, - $queueGroup, + $mocks['lbFactory'] ?? + $this->getMockLBFactory( $mocks['db'] ?? $this->getMockDb() ), + $mocks['queueGroup'] ?? $this->getMockJobQueueGroup(), new HashBagOStuff(), - $cache, - $readOnlyMode, - 1000 + $mocks['cache'] ?? $this->getMockCache(), + $mocks['readOnlyMode'] ?? $this->getMockReadOnlyMode(), + 1000, + $mocks['nsInfo'] ?? $this->getMockNsInfo(), + $mocks['revisionLookup'] ?? $this->getMockRevisionLookup() ); } public function testClearWatchedItems() { - $user = $this->getMockNonAnonUserWithId( 7 ); + $user = new UserIdentityValue( 7, 'MockUser', 0 ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->once() ) @@ -187,12 +212,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( 'RM-KEY' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); TestingAccessWrapper::newFromObject( $store ) ->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ]; @@ -200,7 +220,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testClearWatchedItems_tooManyItemsWatched() { - $user = $this->getMockNonAnonUserWithId( 7 ); + $user = new UserIdentityValue( 7, 'MockUser', 0 ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->once() ) @@ -220,18 +240,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->clearUserWatchedItems( $user ) ); } public function testCountWatchedItems() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->exactly( 1 ) ) @@ -251,12 +266,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( 12, $store->countWatchedItems( $user ) ); } @@ -283,12 +293,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( 7, $store->countWatchers( $titleValue ) ); } @@ -336,12 +341,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $expected = [ 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], @@ -404,12 +404,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $expected = [ 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], @@ -454,12 +449,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) ); } @@ -537,12 +527,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $expected = [ 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], @@ -643,12 +628,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $expected = [ 0 => [ @@ -698,12 +678,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $expected = [ 0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ], @@ -716,7 +691,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testCountUnreadNotifications() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->exactly( 1 ) ) @@ -737,12 +712,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( 9, $store->countUnreadNotifications( $user ) ); } @@ -751,7 +721,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { * @dataProvider provideIntWithDbUnsafeVersion */ public function testCountUnreadNotifications_withUnreadLimit_overLimit( $limit ) { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->exactly( 1 ) ) @@ -773,12 +743,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertSame( true, @@ -790,7 +755,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { * @dataProvider provideIntWithDbUnsafeVersion */ public function testCountUnreadNotifications_withUnreadLimit_underLimit( $limit ) { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->exactly( 1 ) ) @@ -812,12 +777,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( 9, @@ -844,16 +804,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ) ->will( $this->returnValue( new FakeResultWrapper( [] ) ) ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $this->getMockCache(), - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb ] ); $store->duplicateEntry( - Title::newFromText( 'Old_Title' ), - Title::newFromText( 'New_Title' ) + new TitleValue( 0, 'Old_Title' ), + new TitleValue( 0, 'New_Title' ) ); } @@ -904,16 +859,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $store->duplicateEntry( - Title::newFromText( 'Old_Title' ), - Title::newFromText( 'New_Title' ) + new TitleValue( 0, 'Old_Title' ), + new TitleValue( 0, 'New_Title' ) ); } @@ -952,22 +902,17 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $store->duplicateAllAssociatedEntries( - Title::newFromText( 'Old_Title' ), - Title::newFromText( 'New_Title' ) + new TitleValue( 0, 'Old_Title' ), + new TitleValue( 0, 'New_Title' ) ); } public function provideLinkTargetPairs() { return [ - [ Title::newFromText( 'Old_Title' ), Title::newFromText( 'New_Title' ) ], + [ new TitleValue( 0, 'Old_Title' ), new TitleValue( 0, 'New_Title' ) ], [ new TitleValue( 0, 'Old_Title' ), new TitleValue( 0, 'New_Title' ) ], ]; } @@ -1047,12 +992,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $store->duplicateAllAssociatedEntries( $oldTarget, @@ -1081,16 +1021,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:Some_Page:1' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $store->addWatch( - $this->getMockNonAnonUserWithId( 1 ), - Title::newFromText( 'Some_Page' ) + new UserIdentityValue( 1, 'MockUser', 0 ), + new TitleValue( 0, 'Some_Page' ) ); } @@ -1103,30 +1038,21 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() ) ->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $store->addWatch( - $this->getAnonUser(), - Title::newFromText( 'Some_Page' ) + new UserIdentityValue( 0, 'AnonUser', 0 ), + new TitleValue( 0, 'Some_Page' ) ); } public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() { $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $this->getMockDb() ), - $this->getMockJobQueueGroup(), - $this->getMockCache(), - $this->getMockReadOnlyMode( true ) - ); + [ 'readOnlyMode' => $this->getMockReadOnlyMode( true ) ] ); $this->assertFalse( $store->addWatchBatchForUser( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ] ) ); @@ -1168,14 +1094,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '1:Some_Page:1' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); - $mockUser = $this->getMockNonAnonUserWithId( 1 ); + $mockUser = new UserIdentityValue( 1, 'MockUser', 0 ); $this->assertTrue( $store->addWatchBatchForUser( @@ -1194,23 +1115,18 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() ) ->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->addWatchBatchForUser( - $this->getAnonUser(), + new UserIdentityValue( 0, 'AnonUser', 0 ), [ new TitleValue( 0, 'Other_Page' ) ] ) ); } public function testAddWatchBatchReturnsTrue_whenGivenEmptyList() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->never() ) ->method( 'insert' ); @@ -1219,12 +1135,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() ) ->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertTrue( $store->addWatchBatchForUser( $user, [] ) @@ -1255,15 +1166,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { '0:SomeDbKey:1' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $watchedItem = $store->loadWatchedItem( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ); $this->assertInstanceOf( WatchedItem::class, $watchedItem ); @@ -1291,16 +1197,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->loadWatchedItem( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ) ); @@ -1315,16 +1216,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->loadWatchedItem( - $this->getAnonUser(), + new UserIdentityValue( 0, 'AnonUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ) ); @@ -1365,18 +1261,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { [ '1:SomeDbKey:1' ] ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); - $titleValue = new TitleValue( 0, 'SomeDbKey' ); $this->assertTrue( $store->removeWatch( - $this->getMockNonAnonUserWithId( 1 ), - Title::newFromTitleValue( $titleValue ) + new UserIdentityValue( 1, 'MockUser', 0 ), + new TitleValue( 0, 'SomeDbKey' ) ) ); } @@ -1417,18 +1307,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { [ '1:SomeDbKey:1' ] ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); - $titleValue = new TitleValue( 0, 'SomeDbKey' ); $this->assertFalse( $store->removeWatch( - $this->getMockNonAnonUserWithId( 1 ), - Title::newFromTitleValue( $titleValue ) + new UserIdentityValue( 1, 'MockUser', 0 ), + new TitleValue( 0, 'SomeDbKey' ) ) ); } @@ -1443,16 +1327,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() ) ->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->removeWatch( - $this->getAnonUser(), + new UserIdentityValue( 0, 'AnonUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ) ); @@ -1489,15 +1368,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { '0:SomeDbKey:1' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $watchedItem = $store->getWatchedItem( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ); $this->assertInstanceOf( WatchedItem::class, $watchedItem ); @@ -1511,7 +1385,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockDb->expects( $this->never() ) ->method( 'selectRow' ); - $mockUser = $this->getMockNonAnonUserWithId( 1 ); + $mockUser = new UserIdentityValue( 1, 'MockUser', 0 ); $linkTarget = new TitleValue( 0, 'SomeDbKey' ); $cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' ); @@ -1525,12 +1399,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ) ->will( $this->returnValue( $cachedItem ) ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( $cachedItem, @@ -1564,16 +1433,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( '0:SomeDbKey:1' ) ->will( $this->returnValue( false ) ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->getWatchedItem( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ) ); @@ -1589,16 +1453,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->getWatchedItem( - $this->getAnonUser(), + new UserIdentityValue( 0, 'AnonUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ) ); @@ -1631,13 +1490,8 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'set' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); - $user = $this->getMockNonAnonUserWithId( 1 ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $watchedItems = $store->getWatchedItemsForUser( $user ); @@ -1670,7 +1524,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockDb = $this->getMockDb(); $mockCache = $this->getMockCache(); $mockLoadBalancer = $this->getMockLBFactory( $mockDb, $dbType ); - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $mockDb->expects( $this->once() ) ->method( 'select' ) @@ -1684,11 +1538,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->will( $this->returnValue( [] ) ); $store = $this->newWatchedItemStore( - $mockLoadBalancer, - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + [ 'lbFactory' => $mockLoadBalancer, 'cache' => $mockCache ] ); $watchedItems = $store->getWatchedItemsForUser( $user, @@ -1698,16 +1548,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testGetWatchedItemsForUser_badSortOptionThrowsException() { - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $this->getMockDb() ), - $this->getMockJobQueueGroup(), - $this->getMockCache(), - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore(); $this->setExpectedException( InvalidArgumentException::class ); $store->getWatchedItemsForUser( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), [ 'sort' => 'foo' ] ); } @@ -1741,16 +1586,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { '0:SomeDbKey:1' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertTrue( $store->isWatched( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ) ); @@ -1779,16 +1619,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( '0:SomeDbKey:1' ) ->will( $this->returnValue( false ) ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->isWatched( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ) ); @@ -1804,16 +1639,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->isWatched( - $this->getAnonUser(), + new UserIdentityValue( 0, 'AnonUser', 0 ), new TitleValue( 0, 'SomeDbKey' ) ) ); @@ -1873,19 +1703,15 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( [ 0 => [ 'SomeDbKey' => '20151212010101', ], 1 => [ 'AnotherDbKey' => null, ], ], - $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets ) + $store->getNotificationTimestampsBatch( + new UserIdentityValue( 1, 'MockUser', 0 ), $targets ) ); } @@ -1925,18 +1751,14 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( [ 0 => [ 'OtherDbKey' => false, ], ], - $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets ) + $store->getNotificationTimestampsBatch( + new UserIdentityValue( 1, 'MockUser', 0 ), $targets ) ); } @@ -1946,7 +1768,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { new TitleValue( 1, 'AnotherDbKey' ), ]; - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' ); $mockDb = $this->getMockDb(); @@ -1988,12 +1810,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( [ @@ -2010,7 +1827,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { new TitleValue( 1, 'AnotherDbKey' ), ]; - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $cachedItems = [ new WatchedItem( $user, $targets[0], '20151212010101' ), new WatchedItem( $user, $targets[1], null ), @@ -2030,12 +1847,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( [ @@ -2058,19 +1870,15 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache = $this->getMockCache(); $mockCache->expects( $this->never() )->method( $this->anything() ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( [ 0 => [ 'SomeDbKey' => false, ], 1 => [ 'AnotherDbKey' => false, ], ], - $store->getNotificationTimestampsBatch( $this->getAnonUser(), $targets ) + $store->getNotificationTimestampsBatch( + new UserIdentityValue( 0, 'AnonUser', 0 ), $targets ) ); } @@ -2084,17 +1892,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->resetNotificationTimestamp( - $this->getAnonUser(), - Title::newFromText( 'SomeDbKey' ) + new UserIdentityValue( 0, 'AnonUser', 0 ), + new TitleValue( 0, 'SomeDbKey' ) ) ); } @@ -2119,24 +1922,19 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'set' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertFalse( $store->resetNotificationTimestamp( - $this->getMockNonAnonUserWithId( 1 ), - Title::newFromText( 'SomeDbKey' ) + new UserIdentityValue( 1, 'MockUser', 0 ), + new TitleValue( 0, 'SomeDbKey' ) ) ); } public function testResetNotificationTimestamp_item() { - $user = $this->getMockNonAnonUserWithId( 1 ); - $title = Title::newFromText( 'SomeDbKey' ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); + $title = new TitleValue( 0, 'SomeDbKey' ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->once() ) @@ -2173,12 +1971,22 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { // don't run } ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $mockQueueGroup, - $mockCache, - $this->getMockReadOnlyMode() - ); + // We don't care if these methods actually do anything here + $mockRevisionLookup = $this->getMockRevisionLookup( [ + 'getRevisionByTitle' => function () { + return null; + }, + 'getTimestampFromId' => function () { + return '00000000000000'; + }, + ] ); + + $store = $this->newWatchedItemStore( [ + 'db' => $mockDb, + 'queueGroup' => $mockQueueGroup, + 'cache' => $mockCache, + 'revisionLookup' => $mockRevisionLookup, + ] ); $this->assertTrue( $store->resetNotificationTimestamp( @@ -2189,8 +1997,8 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testResetNotificationTimestamp_noItemForced() { - $user = $this->getMockNonAnonUserWithId( 1 ); - $title = Title::newFromText( 'SomeDbKey' ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); + $title = new TitleValue( 0, 'SomeDbKey' ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->never() ) @@ -2204,12 +2012,23 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( '0:SomeDbKey:1' ); $mockQueueGroup = $this->getMockJobQueueGroup(); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $mockQueueGroup, - $mockCache, - $this->getMockReadOnlyMode() - ); + + // We don't care if these methods actually do anything here + $mockRevisionLookup = $this->getMockRevisionLookup( [ + 'getRevisionByTitle' => function () { + return null; + }, + 'getTimestampFromId' => function () { + return '00000000000000'; + }, + ] ); + + $store = $this->newWatchedItemStore( [ + 'db' => $mockDb, + 'queueGroup' => $mockQueueGroup, + 'cache' => $mockCache, + 'revisionLookup' => $mockRevisionLookup, + ] ); $mockQueueGroup->expects( $this->any() ) ->method( 'lazyPush' ) @@ -2226,26 +2045,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ); } - /** - * @param string $text - * @param int $ns - * - * @return PHPUnit_Framework_MockObject_MockObject|Title - */ - private function getMockTitle( $text, $ns = 0 ) { - $title = $this->createMock( Title::class ); - $title->expects( $this->any() ) - ->method( 'getText' ) - ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) ); - $title->expects( $this->any() ) - ->method( 'getDbKey' ) - ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) ); - $title->expects( $this->any() ) - ->method( 'getNamespace' ) - ->will( $this->returnValue( $ns ) ); - return $title; - } - private function verifyCallbackJob( ActivityUpdateJob $job, LinkTarget $expectedTitle, @@ -2265,13 +2064,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $oldid = 22; - $title = $this->getMockTitle( 'SomeTitle' ); - $title->expects( $this->once() ) - ->method( 'getNextRevisionID' ) - ->with( $oldid ) - ->will( $this->returnValue( false ) ); + $title = new TitleValue( 0, 'SomeTitle' ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->never() ) @@ -2285,12 +2080,35 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( '0:SomeTitle:1' ); $mockQueueGroup = $this->getMockJobQueueGroup(); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $mockQueueGroup, - $mockCache, - $this->getMockReadOnlyMode() - ); + + $mockRevisionRecord = $this->createMock( RevisionRecord::class ); + $mockRevisionRecord->expects( $this->never() )->method( $this->anything() ); + + $mockRevisionLookup = $this->getMockRevisionLookup( [ + 'getTimestampFromId' => function () { + return '00000000000000'; + }, + 'getRevisionById' => function ( $id, $flags ) use ( $oldid, $mockRevisionRecord ) { + $this->assertSame( $oldid, $id ); + $this->assertSame( 0, $flags ); + return $mockRevisionRecord; + }, + 'getNextRevision' => + function ( $oldRev, $titleArg ) use ( $mockRevisionRecord, $title ) { + $this->assertSame( $mockRevisionRecord, $oldRev ); + $this->assertSame( $title, $titleArg ); + return false; + }, + ], [ + 'getNextRevision' => 1, + ] ); + + $store = $this->newWatchedItemStore( [ + 'db' => $mockDb, + 'queueGroup' => $mockQueueGroup, + 'cache' => $mockCache, + 'revisionLookup' => $mockRevisionLookup, + ] ); $mockQueueGroup->expects( $this->any() ) ->method( 'lazyPush' ) @@ -2318,13 +2136,15 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $oldid = 22; - $title = $this->getMockTitle( 'SomeDbKey' ); - $title->expects( $this->once() ) - ->method( 'getNextRevisionID' ) - ->with( $oldid ) - ->will( $this->returnValue( 33 ) ); + $title = new TitleValue( 0, 'SomeDbKey' ); + + $mockRevision = $this->createMock( RevisionRecord::class ); + $mockRevision->expects( $this->never() )->method( $this->anything() ); + + $mockNextRevision = $this->createMock( RevisionRecord::class ); + $mockNextRevision->expects( $this->never() )->method( $this->anything() ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->once() ) @@ -2352,12 +2172,34 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( '0:SomeDbKey:1' ); $mockQueueGroup = $this->getMockJobQueueGroup(); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $mockQueueGroup, - $mockCache, - $this->getMockReadOnlyMode() - ); + + $mockRevisionLookup = $this->getMockRevisionLookup( + [ + 'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) { + $this->assertSame( $oldid, $oldidParam ); + }, + 'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) { + $this->assertSame( $oldid, $id ); + return $mockRevision; + }, + 'getNextRevision' => + function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) { + $this->assertSame( $mockRevision, $rev ); + return $mockNextRevision; + }, + ], + [ + 'getTimestampFromId' => 2, + 'getRevisionById' => 1, + 'getNextRevision' => 1, + ] + ); + $store = $this->newWatchedItemStore( [ + 'db' => $mockDb, + 'queueGroup' => $mockQueueGroup, + 'cache' => $mockCache, + 'revisionLookup' => $mockRevisionLookup, + ] ); $mockQueueGroup->expects( $this->any() ) ->method( 'lazyPush' ) @@ -2374,15 +2216,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } ) ); - $getTimestampCallCounter = 0; - $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( - function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) { - $getTimestampCallCounter++; - $this->assertEquals( $title, $titleParam ); - $this->assertEquals( $oldid, $oldidParam ); - } - ); - $this->assertTrue( $store->resetNotificationTimestamp( $user, @@ -2391,19 +2224,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $oldid ) ); - $this->assertEquals( 2, $getTimestampCallCounter ); - - ScopedCallback::consume( $scopedOverrideRevision ); } public function testResetNotificationTimestamp_notWatchedPageForced() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $oldid = 22; - $title = $this->getMockTitle( 'SomeDbKey' ); - $title->expects( $this->once() ) - ->method( 'getNextRevisionID' ) - ->with( $oldid ) - ->will( $this->returnValue( 33 ) ); + $title = new TitleValue( 0, 'SomeDbKey' ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->once() ) @@ -2427,13 +2253,42 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( '0:SomeDbKey:1' ); $mockQueueGroup = $this->getMockJobQueueGroup(); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $mockQueueGroup, - $mockCache, - $this->getMockReadOnlyMode() + + $mockRevision = $this->createMock( RevisionRecord::class ); + $mockRevision->expects( $this->never() )->method( $this->anything() ); + + $mockNextRevision = $this->createMock( RevisionRecord::class ); + $mockNextRevision->expects( $this->never() )->method( $this->anything() ); + + $mockRevisionLookup = $this->getMockRevisionLookup( + [ + 'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) { + $this->assertSame( $oldid, $oldidParam ); + }, + 'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) { + $this->assertSame( $oldid, $id ); + return $mockRevision; + }, + 'getNextRevision' => + function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) { + $this->assertSame( $mockRevision, $rev ); + return $mockNextRevision; + }, + ], + [ + 'getTimestampFromId' => 1, + 'getRevisionById' => 1, + 'getNextRevision' => 1, + ] ); + $store = $this->newWatchedItemStore( [ + 'db' => $mockDb, + 'queueGroup' => $mockQueueGroup, + 'cache' => $mockCache, + 'revisionLookup' => $mockRevisionLookup, + ] ); + $mockQueueGroup->expects( $this->any() ) ->method( 'lazyPush' ) ->will( $this->returnCallback( @@ -2460,13 +2315,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testResetNotificationTimestamp_futureNotificationTimestampForced() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $oldid = 22; - $title = $this->getMockTitle( 'SomeDbKey' ); - $title->expects( $this->once() ) - ->method( 'getNextRevisionID' ) - ->with( $oldid ) - ->will( $this->returnValue( 33 ) ); + $title = new TitleValue( 0, 'SomeDbKey' ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->once() ) @@ -2494,13 +2345,42 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( '0:SomeDbKey:1' ); $mockQueueGroup = $this->getMockJobQueueGroup(); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $mockQueueGroup, - $mockCache, - $this->getMockReadOnlyMode() + + $mockRevision = $this->createMock( RevisionRecord::class ); + $mockRevision->expects( $this->never() )->method( $this->anything() ); + + $mockNextRevision = $this->createMock( RevisionRecord::class ); + $mockNextRevision->expects( $this->never() )->method( $this->anything() ); + + $mockRevisionLookup = $this->getMockRevisionLookup( + [ + 'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) { + $this->assertEquals( $oldid, $oldidParam ); + }, + 'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) { + $this->assertSame( $oldid, $id ); + return $mockRevision; + }, + 'getNextRevision' => + function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) { + $this->assertSame( $mockRevision, $rev ); + return $mockNextRevision; + }, + ], + [ + 'getTimestampFromId' => 2, + 'getRevisionById' => 1, + 'getNextRevision' => 1, + ] ); + $store = $this->newWatchedItemStore( [ + 'db' => $mockDb, + 'queueGroup' => $mockQueueGroup, + 'cache' => $mockCache, + 'revisionLookup' => $mockRevisionLookup, + ] ); + $mockQueueGroup->expects( $this->any() ) ->method( 'lazyPush' ) ->will( $this->returnCallback( @@ -2516,15 +2396,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } ) ); - $getTimestampCallCounter = 0; - $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( - function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) { - $getTimestampCallCounter++; - $this->assertEquals( $title, $titleParam ); - $this->assertEquals( $oldid, $oldidParam ); - } - ); - $this->assertTrue( $store->resetNotificationTimestamp( $user, @@ -2533,19 +2404,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $oldid ) ); - $this->assertEquals( 2, $getTimestampCallCounter ); - - ScopedCallback::consume( $scopedOverrideRevision ); } public function testResetNotificationTimestamp_futureNotificationTimestampNotForced() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $oldid = 22; - $title = $this->getMockTitle( 'SomeDbKey' ); - $title->expects( $this->once() ) - ->method( 'getNextRevisionID' ) - ->with( $oldid ) - ->will( $this->returnValue( 33 ) ); + $title = new TitleValue( 0, 'SomeDbKey' ); $mockDb = $this->getMockDb(); $mockDb->expects( $this->once() ) @@ -2573,12 +2437,40 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->with( '0:SomeDbKey:1' ); $mockQueueGroup = $this->getMockJobQueueGroup(); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $mockQueueGroup, - $mockCache, - $this->getMockReadOnlyMode() - ); + + $mockRevision = $this->createMock( RevisionRecord::class ); + $mockRevision->expects( $this->never() )->method( $this->anything() ); + + $mockNextRevision = $this->createMock( RevisionRecord::class ); + $mockNextRevision->expects( $this->never() )->method( $this->anything() ); + + $mockRevisionLookup = $this->getMockRevisionLookup( + [ + 'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) { + $this->assertEquals( $oldid, $oldidParam ); + }, + 'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) { + $this->assertSame( $oldid, $id ); + return $mockRevision; + }, + 'getNextRevision' => + function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) { + $this->assertSame( $mockRevision, $rev ); + return $mockNextRevision; + }, + ], + [ + 'getTimestampFromId' => 2, + 'getRevisionById' => 1, + 'getNextRevision' => 1, + ] + ); + $store = $this->newWatchedItemStore( [ + 'db' => $mockDb, + 'queueGroup' => $mockQueueGroup, + 'cache' => $mockCache, + 'revisionLookup' => $mockRevisionLookup, + ] ); $mockQueueGroup->expects( $this->any() ) ->method( 'lazyPush' ) @@ -2595,15 +2487,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } ) ); - $getTimestampCallCounter = 0; - $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( - function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) { - $getTimestampCallCounter++; - $this->assertEquals( $title, $titleParam ); - $this->assertEquals( $oldid, $oldidParam ); - } - ); - $this->assertTrue( $store->resetNotificationTimestamp( $user, @@ -2612,31 +2495,19 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $oldid ) ); - $this->assertEquals( 2, $getTimestampCallCounter ); - - ScopedCallback::consume( $scopedOverrideRevision ); } public function testSetNotificationTimestampsForUser_anonUser() { - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $this->getMockDb() ), - $this->getMockJobQueueGroup(), - $this->getMockCache(), - $this->getMockReadOnlyMode() - ); - $this->assertFalse( $store->setNotificationTimestampsForUser( $this->getAnonUser(), '' ) ); + $store = $this->newWatchedItemStore(); + $this->assertFalse( $store->setNotificationTimestampsForUser( + new UserIdentityValue( 0, 'AnonUser', 0 ), '' ) ); } public function testSetNotificationTimestampsForUser_allRows() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $timestamp = '20100101010101'; - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $this->getMockDb() ), - $this->getMockJobQueueGroup(), - $this->getMockCache(), - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore(); // Note: This does not actually assert the job is correct $callableCallCounter = 0; @@ -2653,15 +2524,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testSetNotificationTimestampsForUser_nullTimestamp() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $timestamp = null; - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $this->getMockDb() ), - $this->getMockJobQueueGroup(), - $this->getMockCache(), - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore(); // Note: This does not actually assert the job is correct $callableCallCounter = 0; @@ -2677,7 +2543,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testSetNotificationTimestampsForUser_specificTargets() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $timestamp = '20100101010101'; $targets = [ new TitleValue( 0, 'Foo' ), new TitleValue( 0, 'Bar' ) ]; @@ -2699,12 +2565,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'affectedRows' ) ->will( $this->returnValue( 2 ) ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $this->getMockCache(), - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb ] ); $this->assertTrue( $store->setNotificationTimestampsForUser( $user, $timestamp, $targets ) @@ -2743,17 +2604,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $this->assertEquals( [ 2, 3 ], $store->updateNotificationTimestamp( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'SomeDbKey' ), '20151212010101' ) @@ -2785,15 +2641,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockCache->expects( $this->never() )->method( 'get' ); $mockCache->expects( $this->never() )->method( 'delete' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); $watchers = $store->updateNotificationTimestamp( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'SomeDbKey' ), '20151212010101' ); @@ -2802,7 +2653,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { } public function testUpdateNotificationTimestamp_clearsCachedItems() { - $user = $this->getMockNonAnonUserWithId( 1 ); + $user = new UserIdentityValue( 1, 'MockUser', 0 ); $titleValue = new TitleValue( 0, 'SomeDbKey' ); $mockDb = $this->getMockDb(); @@ -2830,18 +2681,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'delete' ) ->with( '0:SomeDbKey:1' ); - $store = $this->newWatchedItemStore( - $this->getMockLBFactory( $mockDb ), - $this->getMockJobQueueGroup(), - $mockCache, - $this->getMockReadOnlyMode() - ); + $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); // This will add the item to the cache $store->getWatchedItem( $user, $titleValue ); $store->updateNotificationTimestamp( - $this->getMockNonAnonUserWithId( 1 ), + new UserIdentityValue( 1, 'MockUser', 0 ), $titleValue, '20151212010101' ); diff --git a/tests/phpunit/mocks/filebackend/MockFileBackend.php b/tests/phpunit/mocks/filebackend/MockFileBackend.php index 0a0499305a..a1bdbadb53 100644 --- a/tests/phpunit/mocks/filebackend/MockFileBackend.php +++ b/tests/phpunit/mocks/filebackend/MockFileBackend.php @@ -32,7 +32,7 @@ class MockFileBackend extends MemoryFileBackend { protected function doGetLocalCopyMulti( array $params ) { $tmpFiles = []; // (path => MockFSFile) foreach ( $params['srcs'] as $src ) { - $tmpFiles[$src] = new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) ); + $tmpFiles[$src] = new MockFSFile( "Fake path for $src" ); } return $tmpFiles; } diff --git a/tests/phpunit/mocks/filerepo/MockLocalRepo.php b/tests/phpunit/mocks/filerepo/MockLocalRepo.php index eeaf05a0aa..b2c51ca49d 100644 --- a/tests/phpunit/mocks/filerepo/MockLocalRepo.php +++ b/tests/phpunit/mocks/filerepo/MockLocalRepo.php @@ -7,17 +7,16 @@ * @since 1.28 */ class MockLocalRepo extends LocalRepo { - function getLocalCopy( $virtualUrl ) { - return new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) ); + public function getLocalCopy( $virtualUrl ) { + return new MockFSFile( "Fake path for $virtualUrl" ); } - function getLocalReference( $virtualUrl ) { - return new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) ); + public function getLocalReference( $virtualUrl ) { + return new MockFSFile( "Fake path for $virtualUrl" ); } - function getFileProps( $virtualUrl ) { + public function getFileProps( $virtualUrl ) { $fsFile = $this->getLocalReference( $virtualUrl ); - return $fsFile->getProps(); } } diff --git a/tests/phpunit/suites/UploadFromUrlTestSuite.php b/tests/phpunit/suites/UploadFromUrlTestSuite.php index 3b6d6f219b..d340221b8b 100644 --- a/tests/phpunit/suites/UploadFromUrlTestSuite.php +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -1,5 +1,7 @@ resetServiceForTesting( 'RepoGroup' ); FileBackendGroup::destroySingleton(); } @@ -80,7 +82,7 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite { $GLOBALS[$var] = $val; } // Restore backends - RepoGroup::destroySingleton(); + MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' ); FileBackendGroup::destroySingleton(); parent::tearDown(); diff --git a/tests/selenium/pageobjects/history.page.js b/tests/selenium/pageobjects/history.page.js index da5e90961e..3f75243c8f 100644 --- a/tests/selenium/pageobjects/history.page.js +++ b/tests/selenium/pageobjects/history.page.js @@ -1,5 +1,6 @@ const Page = require( 'wdio-mediawiki/Page' ), - Api = require( 'wdio-mediawiki/Api' ); + Api = require( 'wdio-mediawiki/Api' ), + Util = require( 'wdio-mediawiki/Util' ); class HistoryPage extends Page { get heading() { return browser.element( '#firstHeading' ); } @@ -17,6 +18,16 @@ class HistoryPage extends Page { super.openTitle( title, { action: 'history' } ); } + toggleRollbackConfirmationSetting( enable ) { + Util.waitForModuleState( 'mediawiki.api', 'ready', 5000 ); + return browser.execute( function ( enable ) { + return new mw.Api().saveOption( + 'showrollbackconfirmation', + enable ? '1' : '0' + ); + }, enable ); + } + vandalizePage( name, content ) { let vandalUsername = 'Evil_' + browser.options.username; diff --git a/tests/selenium/specs/rollback.js b/tests/selenium/specs/rollback.js index 51a1fc6f9a..383b372fe8 100644 --- a/tests/selenium/specs/rollback.js +++ b/tests/selenium/specs/rollback.js @@ -16,14 +16,7 @@ describe( 'Rollback with confirmation', function () { // Enable rollback confirmation for admin user // Requires user to log in again, handled by deleteCookie() call in beforeEach function UserLoginPage.loginAdmin(); - - UserLoginPage.waitForScriptsToBeReady(); - browser.execute( function () { - return ( new mw.Api() ).saveOption( - 'showrollbackconfirmation', - '1' - ); - } ); + HistoryPage.toggleRollbackConfirmationSetting( true ); } ); beforeEach( function () { @@ -103,14 +96,7 @@ describe( 'Rollback without confirmation', function () { // Disable rollback confirmation for admin user // Requires user to log in again, handled by deleteCookie() call in beforeEach function UserLoginPage.loginAdmin(); - - UserLoginPage.waitForScriptsToBeReady(); - browser.execute( function () { - return ( new mw.Api() ).saveOption( - 'showrollbackconfirmation', - '0' - ); - } ); + HistoryPage.toggleRollbackConfirmationSetting( false ); } ); beforeEach( function () { diff --git a/tests/selenium/wdio-mediawiki/LoginPage.js b/tests/selenium/wdio-mediawiki/LoginPage.js index 60855f8ff1..8838530586 100644 --- a/tests/selenium/wdio-mediawiki/LoginPage.js +++ b/tests/selenium/wdio-mediawiki/LoginPage.js @@ -1,5 +1,4 @@ -const Page = require( './Page' ), - Util = require( 'wdio-mediawiki/Util' ); +const Page = require( './Page' ); class LoginPage extends Page { get username() { return browser.element( '#wpName1' ); } @@ -21,10 +20,6 @@ class LoginPage extends Page { loginAdmin() { this.login( browser.options.username, browser.options.password ); } - - waitForScriptsToBeReady() { - Util.waitForModuleState( 'mediawiki.api' ); - } } module.exports = new LoginPage();