grunt.loadNpmTasks( 'grunt-banana-checker' );
grunt.loadNpmTasks( 'grunt-contrib-copy' );
grunt.loadNpmTasks( 'grunt-eslint' );
- grunt.loadNpmTasks( 'grunt-jsonlint' );
grunt.loadNpmTasks( 'grunt-karma' );
grunt.loadNpmTasks( 'grunt-stylelint' );
grunt.loadNpmTasks( 'grunt-svgmin' );
eslint: {
options: {
reportUnusedDisableDirectives: true,
+ extensions: [ '.js', '.json' ],
cache: true
},
all: [
- '**/*.js',
+ '**/*.js{,on}',
'!docs/**',
'!node_modules/**',
'!resources/lib/**',
'!tests/coverage/**',
'!vendor/**',
// Explicitly say "**/*.js" here in case of symlinks
- '!extensions/**/*.js',
- '!skins/**/*.js'
- ]
- },
- jsonlint: {
- all: [
- '**/*.json',
- '!{docs/js,extensions,node_modules,skins,vendor}/**'
+ '!extensions/**/*.js{,on}',
+ '!skins/**/*.js{,on}'
]
},
banana: {
For notes on 1.33.x and older releases, see HISTORY.
=== Configuration changes for system administrators in 1.34 ===
+
==== New configuration ====
* …
* …
=== External library changes in 1.34 ===
+
==== New external libraries ====
* …
* …
=== 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
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 ===
* …
'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',
* 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,
/**
* Proxy to use for CURL requests.
*/
-$wgHTTPProxy = false;
+$wgHTTPProxy = '';
/**
* Local virtual hosts.
/**
* 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 );
}
/**
use ParserFactory;
use PasswordFactory;
use ProxyLookup;
+use RepoGroup;
use ResourceLoader;
use SearchEngine;
use SearchEngineConfig;
return $this->getService( 'ReadOnlyMode' );
}
+ /**
+ * @since 1.34
+ * @return RepoGroup
+ */
+ public function getRepoGroup() : RepoGroup {
+ return $this->getService( 'RepoGroup' );
+ }
+
/**
* @since 1.33
* @return ResourceLoader
}
/**
+ * 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();
/** @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,
use Psr\Log\LoggerInterface;
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
/**
* Send information about this MediaWiki instance to MediaWiki.org.
$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;
}
/**
* @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;
}
/**
* @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;
}
/**
/**
* 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 );
}
/**
* 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
* 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
/**
* @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 );
}
/**
$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() );
$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() );
/** @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;
}
/**
- * 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;
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' );
}
/**
}
/**
- * 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;
}
},
'GenderCache' => function ( MediaWikiServices $services ) : GenderCache {
- return new GenderCache();
+ return new GenderCache( $services->getNamespaceInfo() );
},
'HttpRequestFactory' =>
'LinkCache' => function ( MediaWikiServices $services ) : LinkCache {
return new LinkCache(
$services->getTitleFormatter(),
- $services->getMainWANObjectCache()
+ $services->getMainWANObjectCache(),
+ $services->getNamespaceInfo()
);
},
},
'NamespaceInfo' => function ( MediaWikiServices $services ) : NamespaceInfo {
- return new NamespaceInfo( $services->getMainConfig() );
+ return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions,
+ $services->getMainConfig() ) );
},
'NameTableStoreFactory' => function ( MediaWikiServices $services ) : NameTableStoreFactory {
DefaultPreferencesFactory::$constructorOptions, $services->getMainConfig() ),
$services->getContentLanguage(),
AuthManager::singleton(),
- $services->getLinkRendererFactory()->create()
+ $services->getLinkRendererFactory()->create(),
+ $services->getNamespaceInfo()
);
$factory->setLogger( LoggerFactory::getInstance( 'preferences' ) );
);
},
+ '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.
$services->getMainObjectStash(),
new HashBagOStuff( [ 'maxKeys' => 100 ] ),
$services->getReadOnlyMode(),
- $services->getMainConfig()->get( 'UpdateRowsPerQuery' )
+ $services->getMainConfig()->get( 'UpdateRowsPerQuery' ),
+ $services->getNamespaceInfo(),
+ $services->getRevisionLookup()
);
$store->setStatsdDataFactory( $services->getStatsdDataFactory() );
/**
* 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 ) );
}
/**
* 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 ) );
}
/**
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 {
* @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
/**
* 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
/**
* 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();
}
/**
*/
abstract class ApiBase extends ContextSource {
+ use ApiBlockInfoTrait;
+
/**
* @name Constants for ::getAllowedParams() arrays
* These constants are keys in the arrays returned by ::getAllowedParams()
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 );
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() ) ]
) );
}
}
$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 ) ]
);
}
}
*/
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
$this->dieWithError(
$status,
null,
- [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
+ [ 'blockinfo' => $this->getBlockInfo( $block ) ]
);
}
}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup API
+ */
+trait ApiBlockInfoTrait {
+
+ /**
+ * 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
+ */
+ private 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;
+ }
+
+}
*/
class ApiQueryUserInfo extends ApiQueryBase {
+ use ApiBlockInfoTrait;
+
const WL_UNREAD_LIMIT = 1000;
private $params = [];
$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
if ( isset( $this->prop['blockinfo'] ) ) {
$block = $user->getBlock();
if ( $block ) {
- $vals = array_merge( $vals, self::getBlockInfo( $block ) );
+ $vals = array_merge( $vals, $this->getBlockInfo( $block ) );
}
}
$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 {
*/
class ApiUnblock extends ApiBase {
+ use ApiBlockInfoTrait;
+
/**
* Unblocks the specified user or provides the reason the unblock failed.
*/
$this->dieWithError(
$status,
null,
- [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
+ [ 'blockinfo' => $this->getBlockInfo( $block ) ]
);
}
}
"apihelp-edit-param-text": "문서 내용.",
"apihelp-edit-param-summary": "편집 요약. 또한 $1section=new 및 $1sectiontitle이 설정되어 있지 않을 때 문단 제목.",
"apihelp-edit-param-tags": "이 판에 적용할 태그를 변경합니다.",
- "apihelp-edit-param-minor": "ì\82¬ì\86\8cí\95\9c í\8e¸ì§\91.",
+ "apihelp-edit-param-minor": "ì\9d´ í\8e¸ì§\91ì\9d\84 ì\82¬ì\86\8cí\95\9c í\8e¸ì§\91ì\9c¼ë¡\9c í\91\9cì\8b\9cí\95©ë\8b\88ë\8b¤.",
"apihelp-edit-param-notminor": "사소하지 않은 편집.",
"apihelp-edit-param-bot": "이 편집을 봇 편집으로 표시.",
"apihelp-edit-param-basetimestamp": "기본 판의 타임스탬프이며, 편집 충돌을 발견하기 위해 사용됩니다. [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]를 통해 가져올 수 있습니다.",
"Hex",
"Mainframe98",
"Southparkfan",
- "Elroy"
+ "Elroy",
+ "Rots61"
]
},
"apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>Status:</strong> 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\n<strong>Foutieve verzoeken:</strong> 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<p class=\"mw-apisandbox-link\"><strong>Testen:</strong> u kunt [[Special:ApiSandbox|eenvoudig API-verzoeken testen]].</p>",
"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.",
"Woytecr",
"InternerowyGołąb",
"CiaPan",
- "Vlad5250"
+ "Vlad5250",
+ "Railfail536"
]
},
"apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\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</div>\n<strong>Stan:</strong> 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\n<strong>Błędne żądania:</strong> 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\n<strong>Testowanie:</strong> Aby łatwo testować żądania API, zobacz [[Special:ApiSandbox]].",
"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]].",
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
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 ) {
if ( !$titleObj ) {
continue;
}
- if ( !MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) {
+ if ( !$this->nsInfo->hasGenderDistinction( $titleObj->getNamespace() ) ) {
continue;
}
$users[] = $titleObj->getText();
/** @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;
}
/**
*/
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 );
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 ) {
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Example class for HTTP accessible external objects.
* Only supports reading, not storing.
*/
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 ) {
}
/**
- * 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
/** @var FileRepo[] */
protected $foreignRepos;
+ /** @var WANObjectCache */
+ protected $wanCache;
+
/** @var bool */
protected $reposInitialised = false;
/** @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:
protected function newRepo( $info ) {
$class = $info['class'];
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- $info['wanCache'] = $cache;
+ $info['wanCache'] = $this->wanCache;
return new $class( $info );
}
$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;
}
$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;
}
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
/**
* @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
*/
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'
*
/**
* Simple wrapper for Http::request( 'POST' )
- * @see Http::request()
+ *
+ * @deprecated since 1.34, use HttpRequestFactory::post()
*
* @param string $url
* @param array $options
/**
* 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();
}
/**
*
* @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 + [
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'] ) ) {
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.' );
}
}
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";
+ }
}
/**
* @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
/**
* 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
if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
$this->proxy = '';
} else {
- $this->proxy = Http::getProxy();
+ global $wgHTTPProxy;
+ $this->proxy = (string)$wgHTTPProxy;
}
}
$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 <https://www.mediawiki.org/wiki/Special:Code/MediaWiki/r67684>).
+ *
+ * @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
+ );
+ }
}
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
# 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,
[
<?php
+use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerInterface;
/**
// @todo FIXME!
$src = $wikiRevision->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 );
}
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 );
* @ingroup JobQueue
*/
+use MediaWiki\Linker\LinkTarget;
+
/**
* Job for updating user activity like "last viewed" timestamps
*
* @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' ];
<?php
use MediaWiki\MediaWikiServices;
+use MediaWiki\User\UserIdentity;
/**
* Job to clear a users watchlist in batches.
}
/**
- * @param User $user User to clear the watchlist for.
+ * @param UserIdentity $user User to clear the watchlist for.
* @param int $maxWatchlistId The maximum wl_id at the time the job was first created.
*
* @return ClearUserWatchlistJob
*/
- public static function newForUser( User $user, $maxWatchlistId ) {
+ public static function newForUser( UserIdentity $user, $maxWatchlistId ) {
return new self( [ 'userId' => $user->getId(), 'maxWatchlistId' => $maxWatchlistId ] );
}
use MediaWiki\MediaWikiServices;
use MessageLocalizer;
use MWException;
-use MWNamespace;
use MWTimestamp;
+use NamespaceInfo;
use OutputPage;
use Parser;
use ParserOptions;
/** @var LinkRenderer */
protected $linkRenderer;
+ /** @var NamespaceInfo */
+ protected $nsInfo;
+
/**
* TODO Make this a const when we drop HHVM support (T192166)
*
];
/**
+ * 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' );
$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();
}
* @param array &$defaultPreferences
*/
protected function searchPreferences( &$defaultPreferences ) {
- foreach ( MWNamespace::getValidNamespaces() as $n ) {
+ foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
$defaultPreferences['searchNs' . $n] = [
'type' => 'api',
];
*/
class UDPRCFeedEngine extends RCFeedEngine {
/**
- * @see RCFeedEngine::send
+ * @see FormattedRCFeed::send
* @param array $feed
* @param string $line
* @return bool
# 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;
}
'description' => 'rcfilters-filter-watchlistactivity-unseen-description',
'cssClassSuffix' => 'watchedunseen',
'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
- $changeTs = $rc->getAttribute( 'rc_timestamp' );
- $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
-
- return $lastVisitTs !== null && $changeTs >= $lastVisitTs;
+ return !$this->isChangeEffectivelySeen( $rc );
},
],
[
'description' => 'rcfilters-filter-watchlistactivity-seen-description',
'cssClassSuffix' => 'watchedseen',
'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
- $changeTs = $rc->getAttribute( 'rc_timestamp' );
- $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
-
- return $lastVisitTs === null || $changeTs < $lastVisitTs;
+ return $this->isChangeEffectivelySeen( $rc );
}
],
],
$rc->counter = $counter++;
if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
- $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
- $updated = ( $lastVisitTs > $rc->getAttribute( 'timestamp' ) );
+ $unseen = !$this->isChangeEffectivelySeen( $rc );
} else {
- $updated = false;
+ $unseen = false;
}
if ( isset( $watchedItemStore ) ) {
$rc->numberofWatchingusers = 0;
}
- $changeLine = $list->recentChangesLine( $rc, $updated, $counter );
+ $changeLine = $list->recentChangesLine( $rc, $unseen, $counter );
if ( $changeLine !== false ) {
$s .= $changeLine;
}
/**
* @param RecentChange $rc
- * @return string TS_MW timestamp
+ * @return bool User viewed the revision or a newer one
+ */
+ protected function isChangeEffectivelySeen( RecentChange $rc ) {
+ $lastVisitTs = $this->getLatestSeenTimestampIfHasUnseen( $rc );
+
+ return $lastVisitTs === null || $lastVisitTs > $rc->getAttribute( 'rc_timestamp' );
+ }
+
+ /**
+ * @param RecentChange $rc
+ * @return string|null TS_MW timestamp or null if all revision were seen
*/
- protected function getLatestSeenTimestamp( RecentChange $rc ) {
+ private function getLatestSeenTimestampIfHasUnseen( RecentChange $rc ) {
return $this->watchStore->getLatestNotificationTimestamp(
$rc->getAttribute( 'wl_notificationtimestamp' ),
$rc->getPerformer(),
* @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.
/** @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;
}
/**
* @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
: $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.
: $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() );
}
/**
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 ] );
}
* 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 ) {
}
/**
- * 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() {
* @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' ) );
}
/**
*/
public function wantSignatures( $index ) {
return $this->isTalk( $index ) ||
- in_array( $index, $this->config->get( 'ExtraSignatureNamespaces' ) );
+ in_array( $index, $this->options->get( 'ExtraSignatureNamespaces' ) );
}
/**
* @return bool
*/
public function hasSubpages( $index ) {
- return !empty( $this->config->get( 'NamespacesWithSubpages' )[$index] );
+ return !empty( $this->options->get( 'NamespacesWithSubpages' )[$index] );
}
/**
* @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 ) ) {
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' );
}
/**
* @return bool
*/
public function isNonincludable( $index ) {
- $namespaces = $this->config->get( 'NonincludableNamespaces' );
+ $namespaces = $this->options->get( 'NonincludableNamespaces' );
return $namespaces && in_array( $index, $namespaces );
}
* @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;
// 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
}
// 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
* @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';
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();
}
/**
* @return bool
*/
public function isAnon() {
- return !$this->isLoggedIn();
+ return !$this->isRegistered();
}
/**
*/
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();
}
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;
+ }
}
* @file
* @ingroup Watchlist
*/
+
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
use Wikimedia\Rdbms\DBReadOnlyError;
/**
$this->actualStore = $actualStore;
}
- public function countWatchedItems( User $user ) {
+ public function countWatchedItems( UserIdentity $user ) {
return $this->actualStore->countWatchedItems( $user );
}
);
}
- 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 );
}
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 );
}
}
*/
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
/**
* Representation of a pair of user and title for watchlist entries.
private $linkTarget;
/**
- * @var User
+ * @var UserIdentity
*/
private $user;
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
) {
}
/**
+ * @deprecated since 1.34, use getUserIdentity()
* @return User
*/
public function getUser() {
+ return User::newFromIdentity( $this->user );
+ }
+
+ /**
+ * @return UserIdentity
+ */
+ public function getUserIdentity() {
return $this->user;
}
<?php
-use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LoadBalancer;
/**
* 'end' => 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
/**
* 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
* 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 [];
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' );
);
}
- 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'] );
<?php
+use MediaWiki\User\UserIdentity;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
*
* @warning Any joins added *must* join on a unique key of the target table
* unless you really know what you're doing.
- * @param User $user
+ * @param UserIdentity $user
* @param array $options Options from
* WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
* @param IDatabase $db Database connection being used for the query
* @param array &$dbOptions Options for Database::select()
* @param array &$joinConds Join conditions for Database::select()
*/
- public function modifyWatchedItemsWithRCInfoQuery( User $user, array $options, IDatabase $db,
- array &$tables, array &$fields, array &$conds, array &$dbOptions, array &$joinConds
+ public function modifyWatchedItemsWithRCInfoQuery( UserIdentity $user, array $options,
+ IDatabase $db, array &$tables, array &$fields, array &$conds, array &$dbOptions,
+ array &$joinConds
);
/**
* Modify the results from WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
* before they're returned.
*
- * @param User $user
+ * @param UserIdentity $user
* @param array $options Options from
* WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
* @param IDatabase $db Database connection being used for the query
* [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ] from the first item
* removed.
*/
- public function modifyWatchedItemsWithRCInfo( User $user, array $options, IDatabase $db,
+ public function modifyWatchedItemsWithRCInfo( UserIdentity $user, array $options, IDatabase $db,
array &$items, $res, &$startFrom
);
<?php
-use Wikimedia\Rdbms\IDatabase;
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\User\UserIdentity;
use Wikimedia\Assert\Assert;
-use Wikimedia\ScopedCallback;
+use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILBFactory;
use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\ScopedCallback;
/**
* Storage layer class for WatchedItems.
private $deferredUpdatesAddCallableUpdateCallback;
/**
- * @var callable|null
+ * @var int
*/
- private $revisionGetTimestampFromIdCallback;
+ private $updateRowsPerQuery;
/**
- * @var int
+ * @var NamespaceInfo
*/
- private $updateRowsPerQuery;
+ private $nsInfo;
+
+ /**
+ * @var RevisionLookup
+ */
+ private $revisionLookup;
/**
* @var StatsdDataFactoryInterface
* @param HashBagOStuff $cache
* @param ReadOnlyMode $readOnlyMode
* @param int $updateRowsPerQuery
+ * @param NamespaceInfo $nsInfo
+ * @param RevisionLookup $revisionLookup
*/
public function __construct(
ILBFactory $lbFactory,
BagOStuff $stash,
HashBagOStuff $cache,
ReadOnlyMode $readOnlyMode,
- $updateRowsPerQuery
+ $updateRowsPerQuery,
+ NamespaceInfo $nsInfo,
+ RevisionLookup $revisionLookup
) {
$this->lbFactory = $lbFactory;
$this->loadBalancer = $lbFactory->getMainLB();
$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 ] );
}
} );
}
- /**
- * 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(),
}
private function cache( WatchedItem $item ) {
- $user = $item->getUser();
+ $user = $item->getUserIdentity();
$target = $item->getLinkTarget();
$key = $this->getCacheKey( $user, $target );
$this->cache->set( $key, $item );
$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' );
}
}
- 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 ) {
}
/**
- * @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 ) );
}
* 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(),
*
* @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;
}
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 ) {
*
* @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 );
}
/**
* @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',
}
/**
- * @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 ) {
/**
* @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;
}
/**
* @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;
}
/**
* @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 = [];
/**
* @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;
}
/**
* @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;
}
/**
* @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 ] );
}
* 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;
}
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
/**
* 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;
}
/**
* @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',
/**
* @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' ) {
}
// 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(
}
/**
- * @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(
}
/**
- * @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(),
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;
}
// 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
/**
* @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 = [];
* @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 )
+ );
}
/**
}
/**
- * @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 );
}
* @file
* @ingroup Watchlist
*/
+
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
use Wikimedia\Rdbms\DBUnexpectedError;
/**
*
* @since 1.31
*
- * @param User $user
+ * @param UserIdentity $user
*
* @return int
*/
- public function countWatchedItems( User $user );
+ public function countWatchedItems( UserIdentity $user );
/**
* @since 1.31
*
* @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
*
* @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,
* - 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 = []
);
*
* @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
*
* @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
*
* @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
* 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 TS_MW timestamp or null
+ * @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 );
}
"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",
"botpasswords-editexisting": "تعديل كلمة سر موجودة للبوت",
"botpasswords-label-needsreset": "(تحتاج كلمة المرور إلى إعادة الضبط)",
"botpasswords-label-appid": "اسم البوت:",
- "botpasswords-label-create": "Ø£Ù\86شأ",
+ "botpasswords-label-create": "Ø¥Ù\86شاء",
"botpasswords-label-update": "تحديث",
"botpasswords-label-cancel": "ألغ",
"botpasswords-label-delete": "احذف",
"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}}",
"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",
"savearticle": "simpen lembar",
"preview": "tayangan sadurungnyane",
"showpreview": "cingak sane lintang",
- "showdiff": "cingak pagentosan",
+ "showdiff": "Cingak pagentosan",
"anoneditwarning": "<strong>Pingetan:</strong> Ida dané nénten kacatet ngranjing. Alamat IP ida dané jagi kacatet ring sejarah (indik sané dumunan) ring lembar puniki. Yening ida dane <strong>[$1 log in]</strong> utawi <strong>[$2 create an account]</strong>, 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.",
"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 <strong>$3, $4</strong> (kaedengang ngantos <strong>$1</strong> panguwahan).",
+ "rcnotefrom": "Ring beten puniki inggih punika {{PLURAL:$5|panguwahan}} saking <strong>$3, $4</strong> (kaedengang ngantos <strong>$1</strong> panguwahan).",
"rclistfrom": "edengang penguwahan sane anyar wit saking $3 $2",
"rcshowhideminor": "$1 uwahan kidik",
"rcshowhideminor-show": "Edengang",
"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",
"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",
"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",
"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": "гісторыя",
"linksearch-pat": "Узор для пошуку:",
"linksearch-ns": "Прастора назваў:",
"linksearch-ok": "Шукаць",
- "linksearch-text": "Ð\9cожна Ñ\9eжÑ\8bваÑ\86Ñ\8c Ñ\81Ñ\8bмбалÑ\96 падÑ\81Ñ\82аноÑ\9eкÑ\96, напÑ\80Ñ\8bклад, «*.wikipedia.org».\nÐ\9dеабÑ\85однÑ\8b дамÑ\8dн пеÑ\80Ñ\88ага Ñ\9eзÑ\80оÑ\9eнÑ\8e, напÑ\80Ñ\8bклад, «*.org».<br />\n{{PLURAL:$2|1=Ð\9fÑ\80аÑ\82акол, Ñ\8fкÑ\96 падÑ\82Ñ\80Ñ\8bмлÑ\96ваеÑ\86Ñ\86а|Ð\9fÑ\80аÑ\82аколÑ\8b, Ñ\8fкÑ\96Ñ\8f падÑ\82Ñ\80Ñ\8bмлÑ\96ваÑ\8eÑ\86Ñ\86а}}: $1 (дапомна http://, калі пратакол не пазначаны).",
+ "linksearch-text": "Ð\9cожна Ñ\9eжÑ\8bваÑ\86Ñ\8c Ñ\81Ñ\8bмбалÑ\96 падÑ\81Ñ\82аноÑ\9eкÑ\96, напÑ\80Ñ\8bклад, «*.wikipedia.org».\nÐ\9dеабÑ\85однÑ\8b дамÑ\8dн пеÑ\80Ñ\88ага Ñ\9eзÑ\80оÑ\9eнÑ\8e, напÑ\80Ñ\8bклад, «*.org».<br />\n{{PLURAL:$2|1=Ð\9fÑ\80аÑ\82акол, Ñ\8fкÑ\96 падÑ\82Ñ\80Ñ\8bмлÑ\96ваеÑ\86Ñ\86а|Ð\9fÑ\80аÑ\82аколÑ\8b, Ñ\8fкÑ\96Ñ\8f падÑ\82Ñ\80Ñ\8bмлÑ\96ваÑ\8eÑ\86Ñ\86а}}: $1 (па змоÑ\9eÑ\87анÑ\8cнÑ\96 http://, калі пратакол не пазначаны).",
"linksearch-line": "Спасылка на $1 з $2",
"linksearch-error": "Сымбалі падстаноўкі могуць ужывацца толькі ў пачатку адрасоў.",
"listusersfrom": "Паказаць удзельнікаў ад:",
"Joao Xavier",
"Surfo",
"YvesNevelsteen",
- "Vlad5250"
+ "Vlad5250",
+ "Mirin"
]
},
"tog-underline": "Substrekado de ligiloj:",
"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",
"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 <strong>grase</strong>, 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 <strong>$3, $4</strong> (montrante ĝis <strong>$1</strong>).",
"rclistfrom": "Montri novajn ŝanĝojn ekde \"$3 $2\"",
"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.",
"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.",
"deleteprotected": "Vi ne povas forigi ĉi tiun paĝon ĉar ĝi estis protektita.",
"deleting-backlinks-warning": "<strong>Atentigo:</strong>\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}}",
"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. <br />\nVidu la [[Special:BlockList|liston de forbaroj]] por kontroli.",
"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",
"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",
"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",
"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 →",
"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",
"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)",
"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",
"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",
"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.",
"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}}.",
"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",
"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:",
"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",
"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.",
- "userlogout-continue": "Amennyiben ki szeretnél jelentkezni, [$1 használd a kijelentkezési oldalt]."
+ "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]."
}
"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):",
"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",
"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}}",
"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",
"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.",
"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å <code>editinterface</code>-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]."
}
"Lancine.kounfantoh.fofana",
"Lanciné.kounfantoh.fofana",
"Youssoufkadialy",
- "Amire80"
+ "Amire80",
+ "Nafadji Mory Diané"
]
},
"sunday": "ߞߊ߯ߙߌߟߏ߲",
"navigation-heading": "ߛߏ߲߯ߓߊߟߌ߫ ߓߏߟߏ߲ߘߊ",
"errorpagetitle": "ߝߎ߬ߕߎ߲߬ߕߌ",
"returnto": "ߌ ߞߐߛߊ߬ߦߌ߲߬ ߦߊ߲߬ ߡߊ߬$1",
- "tagline": "ߞߊ߬ ߝߘߊ߫ {{SITENAMEP}}",
+ "tagline": "ߞߊ߬ ߝߘߊ߫{{SITENAMEP}}",
"help": "ߘߍ߬ߡߍ߲߬ߠߌ",
"help-mediawiki": "ߘߍ߬ߡߍ߲߬ߠߌ߲ ߞߊ߬ ߓߍ߲߬ ߥߞߌ-ߟߊߛߋߢߊߥߙߍ ߡߊ߬",
"search": "ߢߌߣߌ߲ߠߌ",
"nstab-category": "ߦߌߟߡߊ",
"mainpage-nstab": "ߓߏ߬ߟߏ߲߬ߘߊ",
"nosuchspecialpage": "ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߬ ߛߎ߮ ߏ߬ ߝߋ߲߫ ߕߍ߫ ߦߊ߲߬",
+ "nospecialpagetext": "<strong>ߊߟߎ߫ ߓߘߊ߫ ߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߘߏ߫ ߢߌߣߌ߲߫ ߡߍ߲ ߕߺߴߦߋ߲߬.</strong>\nߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߫ ߓߘߍ߬ߡߊ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫ ߢߌ߲߬ ߠߋ߫ ߞߊ߲߬ [[Special:SpecialPages|{{int:specialpages}}]].",
"badtitle": "ߞߎ߲߬ߕߐ߰ ߖߎ߮",
"viewsource": "ߊ߬ ߛߎ߲ ߘߐߜߍ߫",
"viewsource-title": "ߣߌ߲߬ $1 ߛߎ߲ ߘߐߜߍ߫",
"content-model-wikitext": "ߥߞߌ߫ ߞߟߏߜߍ",
"viewpagelogs": "ߞߐߜߍ ߣߌ߲߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߠߎ߬ ߦߋ߫",
"currentrev-asof": "$1 ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲",
- "revisionasof": "ߊ߬ ߡߊߛߊ߬ߦߌ߲ ߦߊ߲߬ ߓߊ߫ $1",
+ "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": "ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ ߟߎ߬",
"searchall": "ߊ߬ ߓߍ߯",
"search-nonefound": "ߖߋ߬ߓߟߌ߬ ߛߌ߫ ߕߍ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߣߌ߲߫ ߞߊ߲߬.",
"mypreferences": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ",
+ "group-sysop": "ߡߙߊ߬ߟߌ߬ߟߊ",
"right-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫",
"newuserlogpage": "ߖߊ߬ߕߋ߬ߘߊ߬ ߓߘߊ߫ ߟߊߞߊ߬ ߌ ߜߊ߲߬ߞߎ߲߬",
"action-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬",
"recentchanges": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߫ ߞߎߘߊ",
"recentchanges-legend": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߟߊ߬ߓߍ߲߬ߢߐ߰ߡߦߊ߬ߘߊ",
"recentchanges-summary": "ߥߞߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎ߲ߓߊ ߡߍ߲ ߠߎ߬ ߞߍߣߍ߲߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߏ߬ ߟߎ߫ ߣߐ߬ߣߐ߬.",
+ "recentchanges-noresult": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߛߎߡߊ߲ߡߕߊ ߢߌ߲߬ ߠߎ߫ ߡߊ߬ ߕߎ߬ߡߊ߬ ߟߊߕߍ߰ߣߍ߲ ߦߌ߬ߘߊ ߘߐ߫.",
"recentchanges-label-newpage": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߓߘߊ߫ ߘߐߜߍ߫ ߞߎߘߊ ߟߊߘߊ߲߫",
"recentchanges-label-minor": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲ ߠߋ߫ ߦߋ߫",
"recentchanges-label-bot": "ߡߐ߰ߡߐ߮ ߟߋ߫ ߣߐ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߣߌ߲߬ ߞߍ߫ ߟߊ߫",
"randompage": "ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ",
"statistics": "ߖߊ߬ߕߋ߬ߛߎ߬ߓߐ ߟߎ߬",
"nbytes": "$1 {{PLURAL:$1|byte|bytes}}",
+ "nmembers": "$1 {{PLURAL:$1|ߛߌ߲߬ߝߏ߲ |members}}",
"prefixindex": "ߞߐߜߍ߫ ߡߍ߲ ߠߎ߬ ߓߍ߯ ߟߊߝߟߐߣߍ߲߫...",
"listusers": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߛߙߍߘߍ",
"newpages": "ߘߐߜߍ߫ ߞߎߘߊ",
"booksources-search": "ߢߌߣߌ߲ߠߌ߲",
"specialloguserlabel": "ߞߍߓߊ߮ :",
"log": "ߘߏ߲߬",
+ "logempty": "ߦߙߍߞߍߟߌ߫ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߝߐ߰ߓߍ ߟߎ߬ ߘߐ߫",
"allpages": "ߞߐߜߍ ߟߎ߬ ߓߍ߯",
"allarticles": "ߞߐߜߍ ߟߎ߬ ߓߍ߯",
"allpagessubmit": "ߥߊ߫",
"tooltip-t-recentchangeslinked": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߞߎߘߊ ߟߎ߬ ߞߐߜߍ߫ ߘߐ߫ ߡߍ߲ ߣߌ߫ ߞߐߜߍ ߣߌ߲߬ ߕߎ߲߰ߣߍ߲߫",
"tooltip-feed-atom": "ߞߐߜߍ ߣߌ߲߬ ߝߕߌ߫ ߓߊߟߏ",
"tooltip-t-contributions": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ ߛߙߍߘߍ",
+ "tooltip-t-emailuser": " ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߟߊߕߊ߯ ߟߊߓߊ߯ߙߟߊ ߣߌ߲߬ ߡߊ߬{{GENDER:$1|ߟߊߓߊ߯ߙߟߊ(ߡߏ߬ߛߏ) }}",
"tooltip-t-upload": "ߞߐߕߐ߮ ߟߎ߫ ߟߊߦߟߍ߬",
"tooltip-t-specialpages": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߐߜߍ߫ ߟߎ߫ ߛߙߍߘߍ",
"tooltip-t-print": " ߞߐߜߍ ߣߌ߲߬ ߜߌ߬ߙߌ߲߬ߘߌ߬ߕߊ߬ߡߊ ߛߎ߮",
"revdelete-text-file": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.",
"logdelete-text": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.",
"revdelete-text-others": "نور پازوالان به لا هم د پټ راز محتوياتو ته لاسرسی ومومي او دا یې له منځه یوسي، مګر که نه بل ډول مشخص شوی.",
- "revdelete-confirm": "Ù\84Ø·Ù\81ا دا تاÛ\8cÛ\8cد Ú©Ú\93ئ Ú\86Û\90 تاسÙ\88 دا کار Ú©Ù\88Ù\84 غÙ\88اÚ\93ئØ\8c دا Ú\86Û\90 تاسÙ\88 پاÛ\8cÙ\84Û\90 Ù¾Ù\87 پاÙ\85 Ú©Û\90 Ù\84رئ اÙ\88 تاسÙ\88 Û\8cÛ\90 سرÙ\87 Ù\85طابÙ\82ت Ú©Ù\88ئ[[{{MediaWiki:Policy-url}}|پاÙ\84Û\8cسÛ\8d]].",
+ "revdelete-confirm": "Ù\84Ø·Ù\81ا دا تاÛ\8cÛ\8cد Ú©Ú\93ئ Ú\86Û\90 تاسÙ\88 دا کار Ú©Ù\88Ù\84 غÙ\88اÚ\93ئØ\8c تاسÙ\88 پاÛ\8cÙ\84Û\90 Ù¾Ù\87 پاÙ\85 Ú©Û\90 Ù\84رئ اÙ\88 [[{{MediaWiki:Policy-url}}|پاÙ\84Û\8cسÛ\8d]] تÙ\87 Ù\85Ù\88 Ù\87Ù\85 Ù\81کر دÛ\8c.",
"revdelete-legend": "د ښکارېدنې محدوديتونه ټاکل",
"revdelete-hide-text": "د مخکتنې متن",
"revdelete-hide-image": "د دوتنې مېنځپانگه پټول",
"tog-norollbackdiff": "Төннөрүү кэнниттэн барыллар уратыларын көрдөрүмэ",
"tog-useeditwarning": "Уларытыыларбын бигэргэппэккэ сирэйтэн тахсаары гыннахпына сэрэтээр",
"tog-prefershttps": "Манна киирэргэ куруук көмүскэллээх холбонууну туттарга",
+ "tog-showrollbackconfirmation": "Сигэни баттаатахха дьайыыга бигэргэтиини көрдөр",
"underline-always": "Куруук",
"underline-never": "Аннынан тардыма",
"underline-default": "Браузер туруоруутунан",
"badretype": "Аһарыктарыҥ сөп түбэспэтилэр.",
"usernameinprogress": "Бу аатынан бэлиэтэнии бара турар.\nБука диэн кэтэһэ түс.",
"userexists": "Суруйбут аатыҥ бэлиэр баар.\nБука диэн, атын аатта тал.",
+ "createacct-normalization": "Эн бэлиэтэммит аатыҥ техника хааччаҕын учуоттаан маннык буолуо «$2».",
"loginerror": "Ааккын система билбэтэ",
"createacct-error": "Бэлиэтэнии кэмигэр алҕас таҕыста",
"createaccounterror": "Саҥа аат бэлиэтиир кыах суох: $1",
"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|Быстах аһарыгы электрон почтаҕар ыыттарарга түннүктэртэн биирдэстэригэр суруй.}}",
"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": "Ыстатыйалар ааттара хоһулаһар",
"stub-threshold-disabled": "Арахсыбыт",
"recentchangesdays": "Хас хонук иһинэн уларытыылары көрдөрөргө:",
"recentchangesdays-max": "(улааппыта $1 күн)",
- "recentchangescount": "Саҥа Ñ\83лаÑ\80Ñ\8bÑ\82Ñ\8bÑ\8bлаÑ\80 көÑ\80дөÑ\80үллÑ\8dр ахсааннара:",
- "prefs-help-recentchangescount": "Ð\91Ñ\83 Ñ\81аҥа көннөÑ\80үүлÑ\8dÑ\80и, Ñ\81иÑ\80Ñ\8dй Ñ\83Ñ\81Ñ\82Ñ\83оÑ\80Ñ\83йалаÑ\80Ñ\8bн Ñ\83онна Ñ\81Ñ\83Ñ\80Ñ\83нааллаÑ\80Ñ\8b көÑ\80дөÑ\80Ó©Ñ\80.",
+ "recentchangescount": "Саҥа Ñ\83лаÑ\80Ñ\8bÑ\82Ñ\8bÑ\8bлаÑ\80 иÑ\81пииһÑ\8dкÑ\82Ñ\8dÑ\80игÑ\8dÑ\80, Ñ\81иÑ\80Ñ\8dй Ñ\83Ñ\81Ñ\82Ñ\83оÑ\80Ñ\83йаÑ\82Ñ\8bгаÑ\80 Ñ\83онна Ñ\81Ñ\83Ñ\80Ñ\83нааллаÑ\80га көÑ\80дөÑ\80үллÑ\8dÑ\80 Ñ\83лаÑ\80Ñ\8bÑ\82Ñ\8bÑ\8bлар ахсааннара:",
+ "prefs-help-recentchangescount": "УлааппÑ\8bÑ\82а: 1000",
"prefs-help-watchlist-token2": "Бу кэтиир испииһэгиҥ ситим-ханаалын кистэлэҥ күлүүһэ.\nБу күлүүһүнэн ким баҕарар эн испииһэккин көрүөн сөп, онон кимиэхэ да биэримэ. Хаһан баҕарар [[Special:ResetTokens|маны баттаан уларытыаххын]] сөп.",
"savedprefs": "Эн туруорууларыҥ олохтоннулар.",
"savedrights": "{{GENDER:$1|$1}} кыттааччы бөлөҕө бигэргэннэ.",
"default": "чопчу ыйыллыбатаҕына маннык",
"prefs-files": "Билэлэр",
"prefs-custom-css": "Бэйэ CSS",
+ "prefs-custom-json": "Тус бэйэ JSON-а",
"prefs-custom-js": "Бэйэ JS",
"prefs-common-config": "Бары тиэмэлэргэ биир CSS/JS",
"prefs-reset-intro": "Бу сирэй көмөтүнэн туруорууларгын саҥаттан туруорар турукка төннөрүөххүн сөп.\nМаны бигэргэттэххинэ билигин баар туруоруулары дэбигис сөргүппэккин.",
"prefs-displaywatchlist": "Көстүүтүн туруоруулара",
"prefs-changesrc": "Көстүбүт уларытыылар",
"prefs-changeswatchlist": "Көрдөр;ллэр уларытыылар",
+ "prefs-pageswatchlist": "Кэтэбилгэ сылдьар сирэйдэр",
"prefs-tokenwatchlist": "Токен",
"prefs-diffs": "Уратылара",
"prefs-help-prefershttps": "Аныгыскы киириигэр үлэлиир буолуо.",
"group-autoconfirmed": "Аптамаатынан бигэргэтиллибит кыттааччылар",
"group-bot": "Роботтар",
"group-sysop": "Дьаһабыллар",
+ "group-interface-admin": "Алтыһаан дьаһабыллара",
"group-bureaucrat": "Бюрокрааттар",
"group-suppress": "Ревизордар",
"group-all": "(бары)",
"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": "СиÑ\80Ñ\8dйдÑ\8dÑ\80и Ñ\83ларытыы",
+ "right-edit": "Уларытыы",
"right-createpage": "Сирэйдэри оҥоруу (ырытыы сирэйдэриттэн ураты)",
"right-createtalk": "Ырытыы сирэйдэрин оҥоруу",
"right-createaccount": "Саҥа кыттааччыны бэлиэтээһин",
"right-reupload-own": "Билэлэри суруттарбыт киһи бэйэтэ иккистээн суруттарыыта",
"right-reupload-shared": "Уопсай ыскылаат билэлэрин локальнай ыскылаат билэлэринэн уларытыы",
"right-upload_by_url": "URL аадырыстан билэлэри киллэрии",
- "right-purge": "Ð\9aÑ\8dÑ\8dһи бигÑ\8dÑ\80гÑ\8dÑ\82Ñ\8dÑ\80 Ñ\81иÑ\80Ñ\8dйÑ\8d Ñ\81Ñ\83оÑ\85 ыраастааһын",
+ "right-purge": "СиÑ\80Ñ\8dй кÑ\8dÑ\8dһин ыраастааһын",
"right-autoconfirmed": "IP түргэнигэр олоҕурбут хааччахтан тутулуктаныма",
"right-bot": "аптамаат быһыытынан ааҕыллар",
"right-nominornewtalk": "Ырытыы сирэйдэригэр кыра көннөрүүлэр суох буоллахтарына саҥа этии эрэсиимэ холбонор",
"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": "Бэйэ кэтиир тиһигин уларытыы. Болҕой, сорох дьайыыларыҥ бу быраабы биэрбэтэҕиҥ да иһин сирэйдэри тиһиккэ эбиэхтэрин сөп.",
"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": "устуоруйата",
"yourpasswordagain": "Поново унеси лозинку:",
"createacct-yourpasswordagain": "Потврдите лозинку",
"createacct-yourpasswordagain-ph": "Поново унесите лозинку",
- "userlogin-remembermypassword": "Ð\9eÑ\81Ñ\82ави ме пÑ\80иÑ\98авÑ\99еног/Ñ\83",
+ "userlogin-remembermypassword": "Ð\9dе одÑ\98авÑ\99Ñ\83Ñ\98 ме",
"userlogin-signwithsecure": "Користите безбедну везу",
"cannotlogin-title": "Пријава није могућа",
"cannotlogin-text": "Пријава није могућа",
"copyrightwarning": "Имајте на уму да се сви доприноси на овом викију сматрају као објављени под лиценцом $2 (више на $1).\nАко не желите да се ваши текстови мењају и размењују без ограничења, онда их не шаљите овде.<br />\nИсто тако обећавате да сте Ви аутор текста, или да сте га умножили са извора који је у јавном власништву.\n<strong>Не шаљите радове заштићене ауторским правима без дозволе!</strong>",
"copyrightwarning2": "Имајте на уму да се сви доприноси на овом викију могу мењати, враћати или брисати од других корисника.\nАко не желите да се ваши текстови слободно мењају и расподељују, не шаљите их овде.<br />\nИсто тако обећавате да сте ви аутор текста, или да сте га умножили с извора који је у јавном власништву (више на $1).\n<strong>Не шаљите радове заштићене ауторским правима без дозволе!</strong>",
"editpage-cannot-use-custom-model": "Модел садржаја ове странице се не може променити.",
- "longpageerror": "<strong>Грешка: текст који сте унели је величине {{PLURAL:$1|један килобајт|$1 килобајта}}, што је веће од {{PLURAL:$2|дозвољеног једног килобајта|дозвољена $2 килобајта|дозвољених $2 килобајта}}.</strong>\nСтраница не може бити сачувана.",
+ "longpageerror": "<strong>Грешка: текст који сте проследили је величине {{PLURAL:$1|један килобајт|$1 килобајта}}, што је веће од {{PLURAL:$2|дозвољеног једног килобајта|дозвољена $2 килобајта|дозвољених $2 килобајта}}.</strong>\nСтраница не може бити сачувана.",
"readonlywarning": "<strong>Упозорење: база података је закључана ради одржавања, тако да тренутно нећете моћи да сачувате измене.</strong>\nМожда бисте желели сачувати текст за касније у некој текстуалној датотеци.\n\nСистемски администратор је навео следеће објашњење: $1",
"protectedpagewarning": "<strong>Упозорење: Ова страница је заштићена, тако да само корисници са администраторским овлашћењима могу да је уређују.</strong>\nНајновији унос у дневнику је наведен испод као референца:",
"semiprotectedpagewarning": "<strong>Напомена:</strong> Ова страница је заштићена, тако да само аутоматски потврђени корисници могу да је уређују.\nНајновији унос у дневнику је наведен испод као референца:",
"upload": "Отпремање датотеке",
"uploadbtn": "Отпреми датотеку",
"reuploaddesc": "Назад на образац за отпремање",
- "upload-tryagain": "Пошаљи измењени опис датотеке",
+ "upload-tryagain": "Проследи измењени опис датотеке",
"upload-tryagain-nostash": "Пошаљите ре-отпремљену датотеку и измењен опис",
"uploadnologin": "Нисте пријављени",
"uploadnologintext": "$1 да бисте отпремали датотеке.",
"filetype-unwanted-type": "<strong>„.$1“</strong> је непожељан тип датотеке.\n{{PLURAL:$3|Пожељан тип датотеке је|Пожељни типови датотека су}} $2.",
"filetype-banned-type": "<strong>„.$1“</strong> {{PLURAL:$4|није допуштен тип датотеке|нису допуштени типови датотека}}.\n{{PLURAL:$3|Дозвољен тип датотеке је|Дозвољени типови датотека су}} $2.",
"filetype-missing": "Ова датотека нема проширење (нпр. „.jpg“).",
- "empty-file": "Ð\9fоÑ\81лаÑ\82а даÑ\82оÑ\82ека је празна.",
+ "empty-file": "Ð\94аÑ\82оÑ\82ека коÑ\98Ñ\83 Ñ\81Ñ\82е пÑ\80оÑ\81ледили је празна.",
"file-too-large": "Послата датотека је превелика.",
"filename-tooshort": "Назив датотеке је прекратак.",
"filetype-banned": "Овај тип датотеке је забрањен.",
"emailnotarget": "Непостојеће или наважеће корисничко име примаоца.",
"emailtarget": "Унос корисничког имена примаоца",
"emailusername": "Корисничко име:",
- "emailusernamesubmit": "Пошаљи",
+ "emailusernamesubmit": "Проследи",
"email-legend": "Слање е-поруке кориснику/ци пројекта {{SITENAME}}",
"emailfrom": "Од:",
"emailto": "За:",
"deleting-subpages-warning": "<strong>Упозорење:</strong> Страница коју желите избрисати има [[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|измене|измене|измена}}",
"mycontris": "Доприноси",
"anoncontribs": "Доприноси",
"contribsub2": "За {{GENDER:$3|$1}} ($2)",
+ "contributions-subtitle": "За {{GENDER:$3|$1}}",
"contributions-userdoesnotexist": "Кориснички налог „$1“ није отворен.",
+ "negative-namespace-not-supported": "Именски простори са негативним вредностима нису подржани.",
"nocontribs": "Нису пронађене промене које одговарају овим критеријумима.",
"uctop": "тренутна",
"month": "од месеца (и раније):",
"blocklist-userblocks": "Сакриј блокаде налога",
"blocklist-tempblocks": "Сакриј привремене блокаде",
"blocklist-addressblocks": "Сакриј појединачне блокаде IP-а",
+ "blocklist-type": "Тип:",
+ "blocklist-type-opt-all": "Све",
"blocklist-type-opt-sitewide": "На нивоу сајта",
"blocklist-type-opt-partial": "Делимично",
"blocklist-rangeblocks": "Сакриј блокаде опсега",
"watchlistedit-normal-done": "{{PLURAL:$1|1=Једна страница је уклоњена|$1 странице су уклоњене|$1 страница је уклоњено}} с вашег списка надгледања:",
"watchlistedit-raw-title": "Уређивање необрађеног списка надгледања",
"watchlistedit-raw-legend": "Уређивање необрађеног списка надгледања",
- "watchlistedit-raw-explain": "Ð\9dаÑ\81лови Ñ\81а Ñ\81пиÑ\81ка надгледаÑ\9aа Ñ\81Ñ\83 пÑ\80иказани иÑ\81под и могÑ\83 Ñ\81е Ñ\83Ñ\80еÑ\92иваÑ\82и додаваÑ\9aем или Ñ\83клаÑ\9aаÑ\9aем Ñ\81Ñ\82авки Ñ\81а Ñ\81пиÑ\81ка;\nÑ\98едан наÑ\81лов по Ñ\80едÑ\83.\nÐ\9aада завÑ\80Ñ\88иÑ\82е, кликниÑ\82е на â\80\9e{{int:Watchlistedit-raw-submit}}â\80\9c.\nÐ\9cожеÑ\82е да [[Special:EditWatchlist|коÑ\80иÑ\81Ñ\82иÑ\82е и обиÑ\87ан уређивач]].",
+ "watchlistedit-raw-explain": "Ð\9dаÑ\81лови Ñ\81а Ñ\81пиÑ\81ка надгледаÑ\9aа Ñ\81Ñ\83 пÑ\80иказани иÑ\81под и могÑ\83 Ñ\81е Ñ\83Ñ\80еÑ\92иваÑ\82и додаваÑ\9aем или Ñ\83клаÑ\9aаÑ\9aем Ñ\81Ñ\82авки Ñ\81а Ñ\81пиÑ\81ка;\nÑ\98едан наÑ\81лов по Ñ\80едÑ\83.\nÐ\9aада завÑ\80Ñ\88иÑ\82е, кликниÑ\82е на â\80\9e{{int:Watchlistedit-raw-submit}}â\80\9d.\nÐ\9cожеÑ\82е да [[Special:EditWatchlist|коÑ\80иÑ\81Ñ\82иÑ\82е и Ñ\81Ñ\82андаÑ\80дни уређивач]].",
"watchlistedit-raw-titles": "Наслови:",
"watchlistedit-raw-submit": "Ажурирај списак",
"watchlistedit-raw-done": "Ваш списак надгледања је ажуриран.",
"htmlform-int-toolow": "Наведена вредност је испод минимума од $1",
"htmlform-int-toohigh": "Наведена вредност је изнад максимума од $1",
"htmlform-required": "Ова вредност је обавезна.",
- "htmlform-submit": "Постави",
+ "htmlform-submit": "Проследи",
"htmlform-reset": "Врати промене",
"htmlform-selectorother-other": "Друго",
"htmlform-no": "Не",
"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.<br />\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",
"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",
"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",
"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.",
"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 <strong>koyu</strong> 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": "<strong>$3, $4</strong> tarihinden itibaren yapılan {{PLURAL:$5|değişiklik|değişiklik}} aşağıdadır (<strong>$1</strong> tarhine kadar olanlar gösterilmektedir).",
"rclistfromreset": "Tarih seçimini sıfırla",
"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ı)",
"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",
"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": "<strong>Uyarı:</strong> 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",
"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",
"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",
"Hello903hello",
"Fitoschido",
"Kanashimi",
- "Roy17"
+ "Roy17",
+ "Tang891228"
]
},
"tog-underline": "連結加底線:",
"title-invalid-talk-namespace": "所請求嘅版面標題指去未開嘅討論版。",
"title-invalid-characters": "所請求嘅版面標題有「$1」呢個無效字符。",
"title-invalid-relative": "標題有相對路徑。因為用戶嘅瀏覽器經常處理唔到相對路徑(./, ../),所以相對路徑無效。",
- "title-invalid-magic-tilde": "所請求嘅版面標題有無效嘅波浪線魔法字(<nowiki>~~~</nowiki>)。",
+ "title-invalid-magic-tilde": "所請求嘅版面標題有無效嘅波浪線魔術字(<nowiki>~~~</nowiki>)。",
"title-invalid-too-long": "所請求嘅版面標題太長。標題用UTF-8編碼嗰時嘅長度唔應該超過 $1 {{PLURAL:$1|字節}}",
"title-invalid-leading-colon": "所請求嘅版面標題開頭有無效冒號。",
"perfcached": "以下嘅資料係嚟自快取,可能唔係最新嘅。 最多有{{PLURAL:$1|一個結果|$1個結果}}響快取度。",
"Hello903hello",
"Luuva",
"Davidzdh",
- "WQL"
+ "WQL",
+ "Tang891228"
]
},
"tog-underline": "底線標示連結:",
"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": "日誌",
* @author Platonides
*/
+use MediaWiki\MediaWikiServices;
+
require_once __DIR__ . '/Benchmarker.php';
/**
}
private function doRequest( $proto ) {
- Http::get( "$proto://localhost/", [], __METHOD__ );
+ MediaWikiServices::getInstance()->getHttpRequestFactory()->
+ get( "$proto://localhost/", [], __METHOD__ );
}
// bench function 1
* @author Antoine Musso <hashar at free dot fr>
*/
+use MediaWiki\MediaWikiServices;
+
require_once __DIR__ . '/Maintenance.php';
/**
$retval = [];
while ( true ) {
- $json = Http::get(
+ $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get(
wfAppendQuery( 'https://www.mediawiki.org/w/api.php', $params ),
[],
__METHOD__
* @ingroup Maintenance
*/
+use MediaWiki\MediaWikiServices;
+
require_once __DIR__ . '/Maintenance.php';
/**
$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() );
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;
$url = rtrim( $this->source, '?' ) . '?' . $url;
}
- $json = Http::get( $url );
+ $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get( $url );
$data = json_decode( $json, true );
if ( is_array( $data ) ) {
"selenium-test": "wdio ./tests/selenium/wdio.conf.js"
},
"devDependencies": {
- "eslint-config-wikimedia": "0.11.0",
+ "eslint-config-wikimedia": "0.12.0",
"grunt": "1.0.4",
"grunt-banana-checker": "0.7.0",
"grunt-contrib-copy": "1.0.0",
"grunt-contrib-watch": "1.1.0",
"grunt-eslint": "21.0.0",
- "grunt-jsonlint": "1.1.0",
"grunt-karma": "3.0.0",
"grunt-stylelint": "0.10.1",
"grunt-svgmin": "5.0.0",
'dependencies' => [ 'jquery.makeCollapsible' ],
'scripts' => 'resources/src/mediawiki.action/mediawiki.action.history.js',
'styles' => 'resources/src/mediawiki.action/mediawiki.action.history.css',
+ 'targets' => [ 'desktop', 'mobile' ]
],
'mediawiki.action.history.styles' => [
'skinStyles' => [
<?php
+use MediaWiki\MediaWikiServices;
use Wikimedia\TestingAccessWrapper;
abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
protected static $httpEngine;
protected $oldHttpEngine;
+ /** @var HttpRequestFactory */
+ private $factory;
+
public function setUp() {
parent::setUp();
$this->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' );
}
// --------------------
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' );
}
$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() );
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() );
}
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();
}
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' ] );
$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();
$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' ] );
}
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 );
}
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() );
}
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() );
}
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() );
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;
}
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',
] );
$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',
] );
}
public function testFactoryDefaults() {
- $request = MWHttpRequest::factory( 'http://acme.test' );
+ $request = $this->factory->create( 'http://acme.test' );
$this->assertInstanceOf( MWHttpRequest::class, $request );
}
$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;
// 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
'transformVia404' => false,
'backend' => $backend
],
- []
+ [],
+ MediaWikiServices::getInstance()->getMainWANObjectCache()
);
}
/**
* 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();
$services->resetServiceForTesting( '_MediaWikiTitleCodec' );
$services->resetServiceForTesting( 'LinkRenderer' );
$services->resetServiceForTesting( 'LinkRendererFactory' );
+ $services->resetServiceForTesting( 'NamespaceInfo' );
}
/**
* @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;
* @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;
}
'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 )
+ ) );
+ }
}
// 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();
$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' );
}
/**
->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->getTimestampFromId(
- $page->getTitle(),
- $rev->getId()
- );
+ $result = $store->getTimestampFromId( $rev->getId() );
$this->assertSame( $rev->getTimestamp(), $result );
}
->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->getTimestampFromId(
- $page->getTitle(),
- $rev->getId() + 1
- );
+ $result = $store->getTimestampFromId( $rev->getId() + 1 );
$this->assertFalse( $result );
}
$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
*/
$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
*/
*/
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
$password = 'UTSysopPassword';
} else {
$username = "TestUser $id";
- $password = wfRandomString( 20 );
+ $password = "password_for_test_user_id_{$id}";
}
self::$testUsers[$key] = $testUser = new TestUser(
$username, // username
]
] );
+ // Reset services since we modified $wgLocalInterwikis
$this->overrideMwServices();
}
];
}
- /**
- * @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
'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 ) );
'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 ) );
--- /dev/null
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers ApiBlockInfoTrait
+ */
+class ApiBlockInfoTraitTest extends MediaWikiTestCase {
+
+ public function testGetBlockInfo() {
+ $block = new Block();
+ $mock = $this->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 );
+ }
+
+}
+++ /dev/null
-<?php
-
-/**
- * @group medium
- * @covers ApiQueryUserInfo
- */
-class ApiQueryUserInfoTest extends ApiTestCase {
- public function testGetBlockInfo() {
- $apiQueryUserInfo = new ApiQueryUserInfo(
- new ApiQuery( new ApiMain( $this->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 );
- }
-}
*/
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() {
* @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' ) );
<?php
+use MediaWiki\MediaWikiServices;
use Wikimedia\TestingAccessWrapper;
/**
'name' => 'localtesting',
'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ),
'parallelize' => 'implicit',
- 'wikiId' => wfWikiID() . wfRandomString(),
+ 'wikiId' => 'testdb',
'backends' => [
[
'name' => 'localmultitesting1',
$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)." );
}
'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,
];
$expected = [
'headers' => [
- 'content-disposition' => FileBackend::makeContentDisposition( 'inline', $name ),
+ 'content-disposition' => FileBackend::makeContentDisposition( 'inline', 'name' ),
'content-duration' => 25.6,
'content-length' => 855055
]
function testHasForeignRepoNegative() {
$this->setMwGlobals( 'wgForeignFileRepos', [] );
- RepoGroup::destroySingleton();
+ $this->overrideMwServices();
FileBackendGroup::destroySingleton();
$this->assertFalse( RepoGroup::singleton()->hasForeignRepos() );
}
function testForEachForeignRepoNone() {
$this->setMwGlobals( 'wgForeignFileRepos', [] );
- RepoGroup::destroySingleton();
+ $this->overrideMwServices();
FileBackendGroup::destroySingleton();
$fakeCallback = $this->createMock( RepoGroupTestHelper::class );
$fakeCallback->expects( $this->never() )->method( 'callback' );
'apiThumbCacheExpiry' => 86400,
'directory' => $wgUploadDirectory
] ] );
- RepoGroup::destroySingleton();
+ $this->overrideMwServices();
FileBackendGroup::destroySingleton();
}
}
* @covers Http::getProxy
*/
public function testGetProxy() {
+ $this->hideDeprecated( 'Http::getProxy' );
+
$this->setMwGlobals( 'wgHTTPProxy', false );
$this->assertEquals(
'',
$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)" );
}
* @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 ) );
}
* @covers MultiWriteBagOStuff::doWrite
*/
public function testSetImmediate() {
- $key = wfRandomString();
- $value = wfRandomString();
+ $key = 'key';
+ $value = 'value';
$this->cache->set( $key, $value );
// Set in tier 1
* @covers MultiWriteBagOStuff
*/
public function testSyncMerge() {
- $key = wfRandomString();
- $value = wfRandomString();
+ $key = 'keyA';
+ $value = 'value';
$func = function () use ( $value ) {
return $value;
};
// 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 );
* @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
$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();
* @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 ) );
}
}
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' );
* @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
);
}
* @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() {
}
/**
- * @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,
* @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,
"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
+ */
}
/**
+ * @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() );
}
$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 );
$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 ) {
+ }
}
/**
<?php
+use MediaWiki\User\UserIdentityValue;
+
/**
* @author Addshore
*
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$this->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() {
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$this->setExpectedException( DBReadOnlyError::class );
- $noWriteService->addWatchBatchForUser( $this->getTestSysop()->getUser(), [] );
+ $noWriteService->addWatchBatchForUser( new UserIdentityValue( 1, 'MockUser', 0 ), [] );
}
public function testRemoveWatch() {
$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() {
$this->setExpectedException( DBReadOnlyError::class );
$noWriteService->setNotificationTimestampsForUser(
- $this->getTestSysop()->getUser(),
+ new UserIdentityValue( 1, 'MockUser', 0 ),
'timestamp',
[]
);
$this->setExpectedException( DBReadOnlyError::class );
$noWriteService->updateNotificationTimestamp(
- $this->getTestSysop()->getUser(),
+ new UserIdentityValue( 1, 'MockUser', 0 ),
new TitleValue( 0, 'Foo' ),
'timestamp'
);
$this->setExpectedException( DBReadOnlyError::class );
$noWriteService->resetNotificationTimestamp(
- $this->getTestSysop()->getUser(),
- Title::newFromText( 'Foo' )
+ new UserIdentityValue( 1, 'MockUser', 0 ),
+ new TitleValue( 0, 'Foo' )
);
}
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$return = $noWriteService->countWatchedItems(
- $this->getTestSysop()->getUser()
+ new UserIdentityValue( 1, 'MockUser', 0 )
);
$this->assertEquals( __METHOD__, $return );
}
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$return = $noWriteService->getWatchedItem(
- $this->getTestSysop()->getUser(),
+ new UserIdentityValue( 1, 'MockUser', 0 ),
new TitleValue( 0, 'Foo' )
);
$this->assertEquals( __METHOD__, $return );
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$return = $noWriteService->loadWatchedItem(
- $this->getTestSysop()->getUser(),
+ new UserIdentityValue( 1, 'MockUser', 0 ),
new TitleValue( 0, 'Foo' )
);
$this->assertEquals( __METHOD__, $return );
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$return = $noWriteService->getWatchedItemsForUser(
- $this->getTestSysop()->getUser(),
+ new UserIdentityValue( 1, 'MockUser', 0 ),
[]
);
$this->assertEquals( __METHOD__, $return );
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$return = $noWriteService->isWatched(
- $this->getTestSysop()->getUser(),
+ new UserIdentityValue( 1, 'MockUser', 0 ),
new TitleValue( 0, 'Foo' )
);
$this->assertEquals( __METHOD__, $return );
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$return = $noWriteService->getNotificationTimestampsBatch(
- $this->getTestSysop()->getUser(),
+ new UserIdentityValue( 1, 'MockUser', 0 ),
[ new TitleValue( 0, 'Foo' ) ]
);
$this->assertEquals( __METHOD__, $return );
$noWriteService = new NoWriteWatchedItemStore( $innerService );
$return = $noWriteService->countUnreadNotifications(
- $this->getTestSysop()->getUser(),
+ new UserIdentityValue( 1, 'MockUser', 0 ),
88
);
$this->assertEquals( __METHOD__, $return );
<?php
+use MediaWiki\User\UserIdentityValue;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LoadBalancer;
use Wikimedia\TestingAccessWrapper;
/**
* @param int $id
+ * @param string[] $extraMethods Extra methods that are expected might be called
* @return PHPUnit_Framework_MockObject_MockObject|User
*/
- private function getMockNonAnonUserWithId( $id ) {
+ private function getMockNonAnonUserWithId( $id, array $extraMethods = [] ) {
$mock = $this->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;
}
* @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;
}
* @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' )
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 ) {
$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' )
$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' )
$queryService = $this->newService( $mockDb );
- $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
+ $items = $queryService->getWatchedItemsForUser(
+ new UserIdentityValue( 0, 'AnonUser', 0 ) );
$this->assertEmpty( $items );
}
<?php
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\User\UserIdentityValue;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\ScopedCallback;
use Wikimedia\TestingAccessWrapper;
/**
}
/**
- * @param int $id
- * @return PHPUnit_Framework_MockObject_MockObject|User
+ * Assumes that only getSubjectPage and getTalkPage will ever be called, and everything passed
+ * to them will have namespace 0.
*/
- private function getMockNonAnonUserWithId( $id ) {
- $mock = $this->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 ) {
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() )
->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' ] ] ];
}
public function testClearWatchedItems_tooManyItemsWatched() {
- $user = $this->getMockNonAnonUserWithId( 7 );
+ $user = new UserIdentityValue( 7, 'MockUser', 0 );
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
$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 ) )
$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 ) );
}
$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 ) );
}
$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 ],
$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 ],
$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' ) );
}
$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 ],
$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 => [
$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 ],
}
public function testCountUnreadNotifications() {
- $user = $this->getMockNonAnonUserWithId( 1 );
+ $user = new UserIdentityValue( 1, 'MockUser', 0 );
$mockDb = $this->getMockDb();
$mockDb->expects( $this->exactly( 1 ) )
$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 ) );
}
* @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 ) )
$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,
* @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 ) )
$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,
)
->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' )
);
}
$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' )
);
}
$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' ) ],
];
}
$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,
->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' )
);
}
$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' ) ]
)
);
->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(
$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' );
$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, [] )
'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 );
$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' )
)
);
$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' )
)
);
[ '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' )
)
);
}
[ '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' )
)
);
}
$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' )
)
);
'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 );
$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' );
)
->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,
->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' )
)
);
$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' )
)
);
$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 );
$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' )
->will( $this->returnValue( [] ) );
$store = $this->newWatchedItemStore(
- $mockLoadBalancer,
- $this->getMockJobQueueGroup(),
- $mockCache,
- $this->getMockReadOnlyMode()
- );
+ [ 'lbFactory' => $mockLoadBalancer, 'cache' => $mockCache ] );
$watchedItems = $store->getWatchedItemsForUser(
$user,
}
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' ]
);
}
'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' )
)
);
->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' )
)
);
$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' )
)
);
$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 )
);
}
$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 )
);
}
new TitleValue( 1, 'AnotherDbKey' ),
];
- $user = $this->getMockNonAnonUserWithId( 1 );
+ $user = new UserIdentityValue( 1, 'MockUser', 0 );
$cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' );
$mockDb = $this->getMockDb();
$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(
[
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 ),
$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(
[
$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 )
);
}
$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' )
)
);
}
$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() )
// 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(
}
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() )
->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' )
);
}
- /**
- * @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,
}
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() )
->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' )
}
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() )
->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' )
}
) );
- $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,
$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() )
->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(
}
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() )
->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(
}
) );
- $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,
$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() )
->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' )
}
) );
- $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,
$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;
}
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;
}
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' ) ];
->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 )
$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'
)
$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'
);
}
public function testUpdateNotificationTimestamp_clearsCachedItems() {
- $user = $this->getMockNonAnonUserWithId( 1 );
+ $user = new UserIdentityValue( 1, 'MockUser', 0 );
$titleValue = new TitleValue( 0, 'SomeDbKey' );
$mockDb = $this->getMockDb();
->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'
);
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;
}
* @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();
}
}
<?php
+use MediaWiki\MediaWikiServices;
+
require_once dirname( __DIR__ ) . '/includes/upload/UploadFromUrlTest.php';
class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
$wgStyleDirectory = "$IP/skins";
}
- RepoGroup::destroySingleton();
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' );
FileBackendGroup::destroySingleton();
}
$GLOBALS[$var] = $val;
}
// Restore backends
- RepoGroup::destroySingleton();
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' );
FileBackendGroup::destroySingleton();
parent::tearDown();
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' ); }
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;
// 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 () {
// 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 () {
-const Page = require( './Page' ),
- Util = require( 'wdio-mediawiki/Util' );
+const Page = require( './Page' );
class LoginPage extends Page {
get username() { return browser.element( '#wpName1' ); }
loginAdmin() {
this.login( browser.options.username, browser.options.password );
}
-
- waitForScriptsToBeReady() {
- Util.waitForModuleState( 'mediawiki.api' );
- }
}
module.exports = new LoginPage();