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 ====
* …
* …
=== Bug fixes in 1.34 ===
-* …
+* (T222529) If a log entry or page revision is recorded in the database with an
+ empty username, attempting to display it will log an error and return a "no
+ username available" to the user instead of silently displaying nothing or
+ invalid links.
=== Action API changes in 1.34 ===
* The 'recenteditcount' response property from action=query list=allusers,
* wfArrayFilter() and wfArrayFilterByKey(), deprecated in 1.32, have been
removed.
* wfMakeUrlIndexes() function, deprecated in 1.33, have been removed.
+* Method signatures in WatchedItemQueryServiceExtension have changed from taking
+ User objects to taking UserIdentity objects. Extensions implementing this
+ interface need to be changed accordingly.
* User::getGroupPage() and ::makeGroupLinkHTML(), deprecated in 1.29, have been
removed. Use UserGroupMembership::getGroupPage and ::getLink instead.
* User::makeGroupLinkWiki(), deprecated in 1.29, has been removed. Use
* …
=== 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',
'UserNamePrefixSearch' => __DIR__ . '/includes/user/UserNamePrefixSearch.php',
'UserNotLoggedIn' => __DIR__ . '/includes/exception/UserNotLoggedIn.php',
'UserOptionsMaintenance' => __DIR__ . '/maintenance/userOptions.php',
+ 'UserOptionsUpdateJob' => __DIR__ . '/includes/jobqueue/jobs/UserOptionsUpdateJob.php',
'UserPasswordPolicy' => __DIR__ . '/includes/password/UserPasswordPolicy.php',
'UserRightsProxy' => __DIR__ . '/includes/user/UserRightsProxy.php',
'UserrightsPage' => __DIR__ . '/includes/specials/SpecialUserrights.php',
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\Block\AbstractBlock;
use MediaWiki\Block\BlockRestrictionStore;
use MediaWiki\Block\Restriction\Restriction;
use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\MediaWikiServices;
-class Block {
+class Block extends AbstractBlock {
/** @var string */
public $mReason;
if ( $block->getType() == self::TYPE_RANGE ) {
# This is the number of bits that are allowed to vary in the block, give
# or take some floating point errors
- $end = Wikimedia\base_convert( $block->getRangeEnd(), 16, 10 );
- $start = Wikimedia\base_convert( $block->getRangeStart(), 16, 10 );
+ $prefix = 'v6-';
+ $end = Wikimedia\base_convert( ltrim( $block->getRangeEnd(), $prefix ), 16, 10 );
+ $start = Wikimedia\base_convert( ltrim( $block->getRangeStart(), $prefix ), 16, 10 );
$size = log( $end - $start + 1, 2 );
# Rank a range block covering a single IP equally with a single-IP block
* 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,
* can add to this to provide custom jobs.
* A job handler should either be a class name to be instantiated,
* or (since 1.30) a callback to use for creating the job object.
+ * The callback takes (Title, array map of parameters) as arguments.
*/
$wgJobClasses = [
'deletePage' => DeletePageJob::class,
'cdnPurge' => CdnPurgeJob::class,
'userGroupExpiry' => UserGroupExpiryJob::class,
'clearWatchlistNotifications' => ClearWatchlistNotificationsJob::class,
+ 'userOptionsUpdate' => UserOptionsUpdateJob::class,
'enqueue' => EnqueueJob::class, // local queue for multi-DC setups
'null' => NullJob::class,
];
/**
* Proxy to use for CURL requests.
*/
-$wgHTTPProxy = false;
+$wgHTTPProxy = '';
/**
* Local virtual hosts.
LogEventsList::showLogExtract(
$out,
'block',
- MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
+ MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
'',
[
'lim' => 1,
protected function addPageProtectionWarningHeaders() {
$out = $this->context->getOutput();
if ( $this->mTitle->isProtected( 'edit' ) &&
- MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+ $this->mTitle->getNamespace()
+ ) !== [ '' ]
) {
# Is the title semi-protected?
if ( $this->mTitle->isSemiProtected() ) {
/**
* 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 );
}
/**
*/
public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
// First we check whether the namespace exists or not.
- if ( MWNamespace::exists( $namespace ) ) {
+ if ( MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace ) ) {
if ( $namespace == NS_MAIN ) {
$name = $context->msg( 'blanknamespace' )->text();
} else {
* @since 1.16.3. $altUserName was added in 1.19.
*/
public static function userLink( $userId, $userName, $altUserName = false ) {
+ if ( $userName === '' ) {
+ wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+ 'that need to be fixed?' );
+ return wfMessage( 'empty-username' )->parse();
+ }
+
$classes = 'mw-userlink';
$page = null;
if ( $userId == 0 ) {
$userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
$useParentheses = true
) {
+ if ( $userText === '' ) {
+ wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+ 'that need to be fixed?' );
+ return ' ' . wfMessage( 'empty-username' )->parse();
+ }
+
global $wgUser, $wgDisableAnonTalk, $wgLang;
$talkable = !( $wgDisableAnonTalk && $userId == 0 );
$blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
* @return string HTML fragment with user talk link
*/
public static function userTalkLink( $userId, $userText ) {
+ if ( $userText === '' ) {
+ wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+ 'that need to be fixed?' );
+ return wfMessage( 'empty-username' )->parse();
+ }
+
$userTalkPage = new TitleValue( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
$moreLinkAttribs['class'] = 'mw-usertoollinks-talk';
* @return string HTML fragment with block link
*/
public static function blockLink( $userId, $userText ) {
+ if ( $userText === '' ) {
+ wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+ 'that need to be fixed?' );
+ return wfMessage( 'empty-username' )->parse();
+ }
+
$blockPage = SpecialPage::getTitleFor( 'Block', $userText );
$moreLinkAttribs['class'] = 'mw-usertoollinks-block';
* @return string HTML fragment with e-mail user link
*/
public static function emailLink( $userId, $userText ) {
+ if ( $userText === '' ) {
+ wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+ 'that need to be fixed?' );
+ return wfMessage( 'empty-username' )->parse();
+ }
+
$emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
$moreLinkAttribs['class'] = 'mw-usertoollinks-mail';
return self::link( $emailPage,
([^[]*) # 3. link trail (the text up until the next link)
/x',
function ( $match ) use ( $title, $local, $wikiId ) {
- $medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
+ $services = MediaWikiServices::getInstance();
+
+ $medians = '(?:';
+ $medians .= preg_quote(
+ $services->getNamespaceInfo()->getCanonicalName( NS_MEDIA ), '/' );
+ $medians .= '|';
$medians .= preg_quote(
MediaWikiServices::getInstance()->getContentLanguage()->getNsText( NS_MEDIA ),
'/'
$wikiId,
$linkTarget->getNamespace() === 0
? $linkTarget->getDBkey()
- : MWNamespace::getCanonicalName( $linkTarget->getNamespace() ) . ':'
- . $linkTarget->getDBkey(),
+ : MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( $linkTarget->getNamespace() ) .
+ ':' . $linkTarget->getDBkey(),
$linkTarget->getFragment()
),
$text,
# Some namespaces don't allow subpages,
# so only perform processing if subpages are allowed
- if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
+ if (
+ $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $contextTitle->getNamespace() )
+ ) {
$hash = strpos( $target, '#' );
if ( $hash !== false ) {
$suffix = substr( $target, $hash );
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();
[ 'cl_from' => $pageid ],
__METHOD__
);
- $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ $services = MediaWikiServices::getInstance();
+ $type = $services->getNamespaceInfo()->
getCategoryLinkType( $this->newTitle->getNamespace() );
foreach ( $prefixes as $prefixRow ) {
$prefix = $prefixRow->cl_sortkey_prefix;
# Update watchlists
$oldtitle = $this->oldTitle->getDBkey();
$newtitle = $this->newTitle->getDBkey();
- $oldsnamespace = MWNamespace::getSubject( $this->oldTitle->getNamespace() );
- $newsnamespace = MWNamespace::getSubject( $this->newTitle->getNamespace() );
+ $oldsnamespace = $services->getNamespaceInfo()->
+ getSubject( $this->oldTitle->getNamespace() );
+ $newsnamespace = $services->getNamespaceInfo()->
+ getSubject( $this->newTitle->getNamespace() );
if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
- $store = MediaWikiServices::getInstance()->getWatchedItemStore();
- $store->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
+ $services->getWatchedItemStore()->duplicateAllAssociatedEntries(
+ $this->oldTitle, $this->newTitle );
}
// If it is a file then move it last.
$title = $this->getTitle();
$ns = $title->getNamespace();
- $canonicalNamespace = MWNamespace::exists( $ns )
- ? MWNamespace::getCanonicalName( $ns )
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+ $canonicalNamespace = $nsInfo->exists( $ns )
+ ? $nsInfo->getCanonicalName( $ns )
: $title->getNsText();
$sk = $this->getSkin();
/** @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;
}
/**
* Loads the current state of protection into the object.
*/
function loadData() {
- $levels = MWNamespace::getRestrictionLevels(
+ $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
$this->mTitle->getNamespace(), $this->mContext->getUser()
);
$this->mCascade = $this->mTitle->areRestrictionsCascading();
* Main entry point for action=protect and action=unprotect
*/
function execute() {
- if ( MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) === [ '' ] ) {
+ if (
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+ $this->mTitle->getNamespace()
+ ) === [ '' ]
+ ) {
throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
}
function buildSelector( $action, $selected ) {
// If the form is disabled, display all relevant levels. Otherwise,
// just show the ones this user can use.
- $levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(),
+ $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+ $this->mTitle->getNamespace(),
$this->disabled ? null : $this->mContext->getUser()
);
* @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()
);
},
'LinkRendererFactory' => function ( MediaWikiServices $services ) : LinkRendererFactory {
return new LinkRendererFactory(
$services->getTitleFormatter(),
- $services->getLinkCache()
+ $services->getLinkCache(),
+ $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() );
* @return int
*/
public function articles() {
- $config = MediaWikiServices::getInstance()->getMainConfig();
+ $services = MediaWikiServices::getInstance();
$tables = [ 'page' ];
$conds = [
- 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_namespace' => $services->getNamespaceInfo()->getContentNamespaces(),
'page_is_redirect' => 0,
];
- if ( $config->get( 'ArticleCountMethod' ) == 'link' ) {
+ if ( $services->getMainConfig()->get( 'ArticleCountMethod' ) == 'link' ) {
$tables[] = 'pagelinks';
$conds[] = 'pl_from=page_id';
}
// NOTE: ideally, this would just call makeTitle() and then isValid(),
// but presently, that means more overhead on a potential performance hotspot.
- if ( !MWNamespace::exists( $ns ) ) {
+ if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) {
return null;
}
$canonicalNamespace = false
) {
if ( $canonicalNamespace ) {
- $namespace = MWNamespace::getCanonicalName( $ns );
+ $namespace = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( $ns );
} else {
$namespace = MediaWikiServices::getInstance()->getContentLanguage()->getNsText( $ns );
}
* @return bool
*/
public function isValid() {
- if ( !MWNamespace::exists( $this->mNamespace ) ) {
+ $services = MediaWikiServices::getInstance();
+ if ( !$services->getNamespaceInfo()->exists( $this->mNamespace ) ) {
return false;
}
try {
- $parser = MediaWikiServices::getInstance()->getTitleParser();
- $parser->parseTitle( $this->mDbkeyform, $this->mNamespace );
+ $services->getTitleParser()->parseTitle( $this->mDbkeyform, $this->mNamespace );
return true;
} catch ( MalformedTitleException $ex ) {
return false;
if ( $this->isExternal() ) {
// This probably shouldn't even happen, except for interwiki transclusion.
// If possible, use the canonical name for the foreign namespace.
- $nsText = MWNamespace::getCanonicalName( $this->mNamespace );
+ $nsText = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( $this->mNamespace );
if ( $nsText !== false ) {
return $nsText;
}
* @return string Namespace text
*/
public function getSubjectNsText() {
- return MediaWikiServices::getInstance()->getContentLanguage()->
- getNsText( MWNamespace::getSubject( $this->mNamespace ) );
+ $services = MediaWikiServices::getInstance();
+ return $services->getContentLanguage()->
+ getNsText( $services->getNamespaceInfo()->getSubject( $this->mNamespace ) );
}
/**
* @return string Namespace text
*/
public function getTalkNsText() {
- return MediaWikiServices::getInstance()->getContentLanguage()->
- getNsText( MWNamespace::getTalk( $this->mNamespace ) );
+ $services = MediaWikiServices::getInstance();
+ return $services->getContentLanguage()->
+ getNsText( $services->getNamespaceInfo()->getTalk( $this->mNamespace ) );
}
/**
* Can this title have a corresponding talk page?
*
- * @see MWNamespace::hasTalkNamespace
+ * @see NamespaceInfo::hasTalkNamespace
* @since 1.30
*
* @return bool True if this title either is a talk page or can have a talk page associated.
*/
public function canHaveTalkPage() {
- return MWNamespace::hasTalkNamespace( $this->mNamespace );
+ return MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasTalkNamespace( $this->mNamespace );
}
/**
* @return bool
*/
public function isWatchable() {
- return !$this->isExternal() && MWNamespace::isWatchable( $this->mNamespace );
+ return !$this->isExternal() && MediaWikiServices::getInstance()->getNamespaceInfo()->
+ isWatchable( $this->mNamespace );
}
/**
* @since 1.19
*/
public function inNamespace( $ns ) {
- return MWNamespace::equals( $this->mNamespace, $ns );
+ return MediaWikiServices::getInstance()->getNamespaceInfo()->
+ equals( $this->mNamespace, $ns );
}
/**
* @return bool
*/
public function hasSubjectNamespace( $ns ) {
- return MWNamespace::subjectEquals( $this->mNamespace, $ns );
+ return MediaWikiServices::getInstance()->getNamespaceInfo()->
+ subjectEquals( $this->mNamespace, $ns );
}
/**
* @return bool
*/
public function isContentPage() {
- return MWNamespace::isContent( $this->mNamespace );
+ return MediaWikiServices::getInstance()->getNamespaceInfo()->
+ isContent( $this->mNamespace );
}
/**
* @return bool
*/
public function isMovable() {
- if ( !MWNamespace::isMovable( $this->mNamespace ) || $this->isExternal() ) {
+ if (
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ isMovable( $this->mNamespace ) || $this->isExternal()
+ ) {
// Interwiki title or immovable namespace. Hooks don't get to override here
return false;
}
* @return bool
*/
public function isSubpage() {
- return MWNamespace::hasSubpages( $this->mNamespace )
+ return MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $this->mNamespace )
? strpos( $this->getText(), '/' ) !== false
: false;
}
* @return bool
*/
public function isTalkPage() {
- return MWNamespace::isTalk( $this->mNamespace );
+ return MediaWikiServices::getInstance()->getNamespaceInfo()->
+ isTalk( $this->mNamespace );
}
/**
* 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 ) );
}
/**
* @since 1.20
*/
public function getRootText() {
- if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ if (
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $this->mNamespace )
+ ) {
return $this->getText();
}
*/
public function getBaseText() {
$text = $this->getText();
- if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ if (
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $this->mNamespace )
+ ) {
return $text;
}
* @return string Subpage name
*/
public function getSubpageText() {
- if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ if (
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $this->mNamespace )
+ ) {
return $this->mTextform;
}
$parts = explode( '/', $this->mTextform );
* @return bool
*/
public function hasSubpages() {
- if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ if (
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $this->mNamespace )
+ ) {
# Duh
return false;
}
* doesn't allow subpages
*/
public function getSubpages( $limit = -1 ) {
- if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ if (
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $this->mNamespace )
+ ) {
return [];
}
* @return string Containing capitalized title
*/
public static function capitalize( $text, $ns = NS_MAIN ) {
- if ( MWNamespace::isCapitalized( $ns ) ) {
+ $services = MediaWikiServices::getInstance();
+ if ( $services->getNamespaceInfo()->isCapitalized( $ns ) ) {
return MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $text );
} else {
return $text;
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 {
];
}
// Do the source and target namespaces support subpages?
- if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+ if ( !$nsInfo->hasSubpages( $this->mNamespace ) ) {
return [
- [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $this->mNamespace ) ],
+ [ 'namespace-nosubpages', $nsInfo->getCanonicalName( $this->mNamespace ) ],
];
}
- if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
+ if ( !$nsInfo->hasSubpages( $nt->getNamespace() ) ) {
return [
- [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $nt->getNamespace() ) ],
+ [ 'namespace-nosubpages', $nsInfo->getCanonicalName( $nt->getNamespace() ) ],
];
}
* @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();
}
/**
*/
public function getNamespaceKey( $prepend = 'nstab-' ) {
// Gets the subject namespace of this title
- $subjectNS = MWNamespace::getSubject( $this->mNamespace );
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+ $subjectNS = $nsInfo->getSubject( $this->mNamespace );
// Prefer canonical namespace name for HTML IDs
- $namespaceKey = MWNamespace::getCanonicalName( $subjectNS );
+ $namespaceKey = $nsInfo->getCanonicalName( $subjectNS );
if ( $namespaceKey === false ) {
// Fallback to localised text
$namespaceKey = $this->getSubjectNsText();
public function canUseNoindex() {
global $wgExemptFromUserRobotsControl;
- $bannedNamespaces = $wgExemptFromUserRobotsControl ?? MWNamespace::getContentNamespaces();
+ $bannedNamespaces = $wgExemptFromUserRobotsControl ??
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces();
return !in_array( $this->mNamespace, $bannedNamespaces );
}
}
}
- if ( MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ if (
+ MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $this->mNamespace )
+ ) {
// Optional notice for page itself and any parent page
$editnotice_base = $editnotice_ns;
foreach ( explode( '/', $this->mDbkeyform ) as $part ) {
* @ingroup Categories
*/
+use MediaWiki\MediaWikiServices;
+
/**
* This class performs some operations related to tracking categories, such as creating
* a list of all such categories.
}
$trackingCategories = [];
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
foreach ( $categories as $catMsg ) {
/*
* Check if the tracking category varies by namespace
// Match things like {{NAMESPACE}} and {{NAMESPACENUMBER}}.
// False positives are ok, this is just an efficiency shortcut
if ( strpos( $msgObj->plain(), '{{' ) !== false ) {
- $ns = MWNamespace::getValidNamespaces();
+ $ns = $nsInfo->getValidNamespaces();
foreach ( $ns as $namesp ) {
$tempTitle = Title::makeTitleSafe( $namesp, $catMsg );
if ( !$tempTitle ) {
}
// Subpages of this page, if subpages are enabled for the current NS
- if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+ if ( $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() ) ) {
$prefixIndex = SpecialPage::getTitleFor(
'Prefixindex', $title->getPrefixedText() . '/' );
$pageInfo['header-basic'][] = [
protected function pageCounts( Page $page ) {
$fname = __METHOD__;
$config = $this->context->getConfig();
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ $services = MediaWikiServices::getInstance();
+ $cache = $services->getMainWANObjectCache();
return $cache->getWithSetCallback(
self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
WANObjectCache::TTL_WEEK,
- function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
+ function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname, $services ) {
global $wgActorTableSchemaMigrationStage;
$title = $page->getTitle();
$joins = [];
}
- $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
+ $watchedItemStore = $services->getWatchedItemStore();
$result = [];
$result['watchers'] = $watchedItemStore->countWatchers( $title );
);
// Subpages (if enabled)
- if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+ if ( $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() ) ) {
$conds = [ 'page_namespace' => $title->getNamespace() ];
$conds[] = 'page_title ' .
$dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
* @file
*/
+use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IDatabase;
/**
*/
abstract class ApiBase extends ContextSource {
+ use ApiBlockInfoTrait;
+
/**
* @name Constants for ::getAllowedParams() arrays
* These constants are keys in the arrays returned by ::getAllowedParams()
$provided = $this->getMain()->getCheck( $encParamName );
if ( isset( $value ) && $type == 'namespace' ) {
- $type = MWNamespace::getValidNamespaces();
+ $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getValidNamespaces();
if ( isset( $paramSettings[self::PARAM_EXTRA_NAMESPACES] ) &&
is_array( $paramSettings[self::PARAM_EXTRA_NAMESPACES] )
) {
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;
+ }
+
+}
break;
case 'namespace':
- $namespaces = MWNamespace::getValidNamespaces();
+ $namespaces = MediaWikiServices::getInstance()->
+ getNamespaceInfo()->getValidNamespaces();
if ( isset( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ) &&
is_array( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] )
) {
ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' );
}
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+
$usernames = [];
if ( $res ) {
foreach ( $res as $row ) {
$this->processDbRow( $row );
// Need gender information
- if ( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) {
+ if ( $nsInfo->hasGenderDistinction( $row->page_namespace ) ) {
$usernames[] = $row->page_title;
}
}
$this->mTitles[] = $title;
// need gender information
- if ( MWNamespace::hasGenderDistinction( $ns ) ) {
+ if ( $nsInfo->hasGenderDistinction( $ns ) ) {
$usernames[] = $dbkey;
}
}
}
// Need gender information
- if ( MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) {
+ if (
+ MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasGenderDistinction( $titleObj->getNamespace() )
+ ) {
$usernames[] = $titleObj->getText();
}
}
$user = $this->getUser();
$db = $this->getDB();
$params = $this->extractRequestParams( false );
- $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+ $services = MediaWikiServices::getInstance();
+ $revisionStore = $services->getRevisionStore();
$result = $this->getResult();
$miser_ns = null;
if ( $mode == 'all' ) {
- $namespaces = $params['namespace'] ?? MWNamespace::getValidNamespaces();
+ $namespaces = $params['namespace'] ??
+ $services->getNamespaceInfo()->getValidNamespaces();
$this->addWhereFld( 'ar_namespace', $namespaces );
// For from/to/prefix, we have to consider the potential
$res = $this->select( __METHOD__ );
// Get gender information
- if ( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) {
+ $services = MediaWikiServices::getInstance();
+ if ( $services->getNamespaceInfo()->hasGenderDistinction( $params['namespace'] ) ) {
$users = [];
foreach ( $res as $row ) {
$users[] = $row->page_title;
}
- MediaWikiServices::getInstance()->getGenderCache()->doQuery( $users, __METHOD__ );
+ $services->getGenderCache()->doQuery( $users, __METHOD__ );
$res->rewind(); // reset
}
$db = $this->getDB();
$params = $this->extractRequestParams( false );
- $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+ $services = MediaWikiServices::getInstance();
+ $revisionStore = $services->getRevisionStore();
$result = $this->getResult();
if ( $params['namespace'] !== null ) {
$params['namespace'] = array_unique( $params['namespace'] );
sort( $params['namespace'] );
- if ( $params['namespace'] != MWNamespace::getValidNamespaces() ) {
+ if ( $params['namespace'] != $services->getNamespaceInfo()->getValidNamespaces() ) {
$needPageTable = true;
if ( $this->getConfig()->get( 'MiserMode' ) ) {
$miser_ns = $params['namespace'];
*/
private function getTSIDs() {
$getTitles = $this->talkids = $this->subjectids = [];
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
/** @var Title $t */
foreach ( $this->everything as $t ) {
- if ( MWNamespace::isTalk( $t->getNamespace() ) ) {
+ if ( $nsInfo->isTalk( $t->getNamespace() ) ) {
if ( $this->fld_subjectid ) {
$getTitles[] = $t->getSubjectPage();
}
$this->addWhere( $lb->constructSet( 'page', $db ) );
$res = $this->select( __METHOD__ );
foreach ( $res as $row ) {
- if ( MWNamespace::isTalk( $row->page_namespace ) ) {
- $this->talkids[MWNamespace::getSubject( $row->page_namespace )][$row->page_title] =
- (int)$row->page_id;
+ if ( $nsInfo->isTalk( $row->page_namespace ) ) {
+ $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
+ (int)( $row->page_id );
} else {
- $this->subjectids[MWNamespace::getTalk( $row->page_namespace )][$row->page_title] =
- (int)$row->page_id;
+ $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
+ (int)( $row->page_id );
}
}
}
$search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] );
$search->setFeatureData( 'interwiki', (bool)$interwiki );
- $nquery = $search->transformSearchTerm( $query );
- if ( $nquery !== $query ) {
- $query = $nquery;
- wfDeprecated( 'SearchEngine::transformSearchTerm() (overridden by ' .
- get_class( $search ) . ')', '1.32' );
- }
-
$nquery = $search->replacePrefixes( $query );
if ( $nquery !== $query ) {
$query = $nquery;
$data = [
ApiResult::META_TYPE => 'assoc',
];
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
foreach (
MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
as $ns => $title
) {
$data[$ns] = [
'id' => (int)$ns,
- 'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
+ 'case' => $nsInfo->isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
];
ApiResult::setContentValue( $data[$ns], 'name', $title );
- $canonical = MWNamespace::getCanonicalName( $ns );
+ $canonical = $nsInfo->getCanonicalName( $ns );
- $data[$ns]['subpages'] = MWNamespace::hasSubpages( $ns );
+ $data[$ns]['subpages'] = $nsInfo->hasSubpages( $ns );
if ( $canonical ) {
$data[$ns]['canonical'] = strtr( $canonical, '_', ' ' );
}
- $data[$ns]['content'] = MWNamespace::isContent( $ns );
- $data[$ns]['nonincludable'] = MWNamespace::isNonincludable( $ns );
+ $data[$ns]['content'] = $nsInfo->isContent( $ns );
+ $data[$ns]['nonincludable'] = $nsInfo->isNonincludable( $ns );
- $contentmodel = MWNamespace::getNamespaceContentModel( $ns );
+ $contentmodel = $nsInfo->getNamespaceContentModel( $ns );
if ( $contentmodel ) {
$data[$ns]['defaultcontentmodel'] = $contentmodel;
}
*/
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]].",
--- /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
+ */
+
+namespace MediaWiki\Block;
+
+/**
+ * @since 1.34
+ */
+abstract class AbstractBlock {
+}
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 ) {
$this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
// Purge the messages in the message blob store and fire any hook handlers
- $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader();
- $blobStore = $resourceloader->getMessageBlobStore();
+ $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
foreach ( $replacements as list( $title, $msg ) ) {
$blobStore->updateMessage( $this->contLang->lcfirst( $msg ) );
Hooks::run( 'MessageCacheReplace', [ $title, $newTextByTitle[$title] ] );
# HACK: If using a null (i.e. disabled) storage backend, we
# can't write to the MessageBlobStore either
if ( $purgeBlobs && !$this->store instanceof LCStoreNull ) {
- $blobStore = new MessageBlobStore(
- MediaWikiServices::getInstance()->getResourceLoader()
- );
+ $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
$blobStore->clear();
}
}
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Feed to Special:RecentChanges and Special:RecentChangesLinked.
*
}
}
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
foreach ( $sorted as $obj ) {
$title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
- $talkpage = MWNamespace::hasTalkNamespace( $obj->rc_namespace )
+ $talkpage = $nsInfo->hasTalkNamespace( $obj->rc_namespace )
? $title->getTalkPage()->getFullURL()
: '';
<?php
+
/**
* Base class for content handling.
*
namespace MediaWiki\EditPage;
-use MWNamespace;
+use MediaWiki\MediaWikiServices;
use Sanitizer;
use Title;
use User;
public function getTextboxProtectionCSSClasses( Title $title ) {
$classes = []; // Textarea CSS
if ( $title->isProtected( 'edit' ) &&
- MWNamespace::getRestrictionLevels( $title->getNamespace() ) !== [ '' ]
+ MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getRestrictionLevels( $title->getNamespace() ) !== [ '' ]
) {
# Is the title semi-protected?
if ( $title->isSemiProtected() ) {
* Install handlers with PHP.
*/
public static function installHandler() {
+ // This catches:
+ // * Exception objects that were explicitly thrown but not
+ // caught anywhere in the application. This is rare given those
+ // would normally be caught at a high-level like MediaWiki::run (index.php),
+ // api.php, or ResourceLoader::respond (load.php). These high-level
+ // catch clauses would then call MWExceptionHandler::logException
+ // or MWExceptionHandler::handleException.
+ // If they are not caught, then they are handled here.
+ // * Error objects (on PHP 7+), for issues that would historically
+ // cause fatal errors but may now be caught as Throwable (not Exception).
+ // Same as previous case, but more common to bubble to here instead of
+ // caught locally because they tend to not be safe to recover from.
+ // (e.g. argument TypeErorr, devision by zero, etc.)
set_exception_handler( 'MWExceptionHandler::handleUncaughtException' );
+
+ // This catches:
+ // * Non-fatal errors (e.g. PHP Notice, PHP Warning, PHP Error) that do not
+ // interrupt execution in any way. We log these in the background and then
+ // continue execution.
+ // * Fatal errors (on HHVM in PHP5 mode) where PHP 7 would throw Throwable.
set_error_handler( 'MWExceptionHandler::handleError' );
+ // This catches:
+ // * Fatal error for which no Throwable is thrown (PHP 7), and no Error emitted (HHVM).
+ // This includes Out-Of-Memory and Timeout fatals.
+ //
// Reserve 16k of memory so we can report OOM fatals
self::$reservedMemory = str_repeat( ' ', 16384 );
register_shutdown_function( 'MWExceptionHandler::handleFatalError' );
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* @ingroup Dump
*/
* @return bool
*/
protected function pass( $page ) {
- return !MWNamespace::isTalk( $page->page_namespace );
+ return !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ isTalk( $page->page_namespace );
}
}
*/
function namespaces() {
$spaces = "<namespaces>\n";
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
foreach (
MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
as $ns => $title
Xml::element( 'namespace',
[
'key' => $ns,
- 'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
+ 'case' => $nsInfo->isCapitalized( $ns )
+ ? 'first-letter' : 'case-sensitive',
], $title ) . "\n";
}
$spaces .= " </namespaces>";
* @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 ) {
}
// Optional settings that have a default
- $this->initialCapital = $info['initialCapital'] ?? MWNamespace::isCapitalized( NS_FILE );
+ $this->initialCapital = $info['initialCapital'] ??
+ MediaWikiServices::getInstance()->getNamespaceInfo()->isCapitalized( NS_FILE );
$this->url = $info['url'] ?? false; // a subclass may set the URL (e.g. ForeignAPIRepo)
if ( isset( $info['thumbUrl'] ) ) {
$this->thumbUrl = $info['thumbUrl'];
* @return string
*/
public function getNameFromTitle( Title $title ) {
- if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
+ if (
+ $this->initialCapital !=
+ MediaWikiServices::getInstance()->getNamespaceInfo()->isCapitalized( NS_FILE )
+ ) {
$name = $title->getUserCaseDBKey();
if ( $this->initialCapital ) {
$name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
}
/**
- * 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;
}
// @todo Code is incomplete.
// $linkTarget = Title::newFromText( MediaWikiServices::getInstance()->
- // getContentLanguage()->getNsText( MWNamespace::getUser() ) . ":{$ut}" );
+ // getContentLanguage()->getNsText( MediaWikiServices::getInstance()->
+ // getNamespaceInfo()->getUser() ) . ":{$ut}" );
// $ul = Linker::link( $linkTarget, $ut );
$meta = [];
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 );
return true;
} elseif (
$namespace >= 0 &&
- MWNamespace::exists( intval( $namespace ) )
+ MediaWikiServices::getInstance()->getNamespaceInfo()->exists( intval( $namespace ) )
) {
$namespace = intval( $namespace );
$this->setImportTitleFactory( new NamespaceImportTitleFactory( $namespace ) );
if ( !$title || $title->isExternal() ) {
$status->fatal( 'import-rootpage-invalid' );
- } elseif ( !MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+ } elseif (
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $title->getNamespace() )
+ ) {
$displayNSText = $title->getNamespace() == NS_MAIN
? wfMessage( 'blanknamespace' )->text()
: MediaWikiServices::getInstance()->getContentLanguage()->
}
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 ] );
}
--- /dev/null
+<?php
+/**
+ * Job that updates a user's preferences.
+ *
+ * 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 3 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 JobQueue
+ */
+
+/**
+ * Job that updates a user's preferences
+ *
+ * The following job parameters are required:
+ * - userId: the user ID
+ * - options: a map of (option => value)
+ *
+ * @since 1.33
+ */
+class UserOptionsUpdateJob extends Job implements GenericParameterJob {
+ public function __construct( array $params ) {
+ parent::__construct( 'userOptionsUpdate', $params );
+ $this->removeDuplicates = true;
+ }
+
+ public function run() {
+ if ( !$this->params['options'] ) {
+ return true; // nothing to do
+ }
+
+ $user = User::newFromId( $this->params['userId'] );
+ $user->load( $user::READ_EXCLUSIVE );
+ if ( !$user->getId() ) {
+ return true;
+ }
+
+ foreach ( $this->params['options'] as $name => $value ) {
+ $user->setOption( $name, $value );
+ }
+
+ $user->saveSettings();
+
+ return true;
+ }
+}
* This method should only be called internally
*/
public static function purgeAllOnShutdown() {
- foreach ( self::$pathsCollect as $path ) {
+ foreach ( self::$pathsCollect as $path => $unused ) {
Wikimedia\suppressWarnings();
unlink( $path );
Wikimedia\restoreWarnings();
use LinkCache;
use Linker;
use MediaWiki\MediaWikiServices;
-use MWNamespace;
+use NamespaceInfo;
use Sanitizer;
use Title;
use TitleFormatter;
*/
private $linkCache;
+ /**
+ * @var NamespaceInfo
+ */
+ private $nsInfo;
+
/**
* Whether to run the legacy Linker hooks
*
/**
* @param TitleFormatter $titleFormatter
* @param LinkCache $linkCache
+ * @param NamespaceInfo $nsInfo
*/
- public function __construct( TitleFormatter $titleFormatter, LinkCache $linkCache ) {
+ public function __construct(
+ TitleFormatter $titleFormatter, LinkCache $linkCache, NamespaceInfo $nsInfo
+ ) {
$this->titleFormatter = $titleFormatter;
$this->linkCache = $linkCache;
+ $this->nsInfo = $nsInfo;
}
/**
if ( $this->linkCache->getGoodLinkFieldObj( $target, 'redirect' ) ) {
# Page is a redirect
return 'mw-redirect';
- } elseif ( $this->stubThreshold > 0 && MWNamespace::isContent( $target->getNamespace() )
- && $this->linkCache->getGoodLinkFieldObj( $target, 'length' ) < $this->stubThreshold
+ } elseif (
+ $this->stubThreshold > 0 && $this->nsInfo->isContent( $target->getNamespace() ) &&
+ $this->linkCache->getGoodLinkFieldObj( $target, 'length' ) < $this->stubThreshold
) {
# Page is a stub
return 'stub';
namespace MediaWiki\Linker;
use LinkCache;
+use NamespaceInfo;
use TitleFormatter;
use User;
*/
private $linkCache;
+ /**
+ * @var NamespaceInfo
+ */
+ private $nsInfo;
+
/**
* @param TitleFormatter $titleFormatter
* @param LinkCache $linkCache
+ * @param NamespaceInfo $nsInfo
*/
- public function __construct( TitleFormatter $titleFormatter, LinkCache $linkCache ) {
+ public function __construct(
+ TitleFormatter $titleFormatter, LinkCache $linkCache, NamespaceInfo $nsInfo
+ ) {
$this->titleFormatter = $titleFormatter;
$this->linkCache = $linkCache;
+ $this->nsInfo = $nsInfo;
}
/**
* @return LinkRenderer
*/
public function create() {
- return new LinkRenderer( $this->titleFormatter, $this->linkCache );
+ return new LinkRenderer( $this->titleFormatter, $this->linkCache, $this->nsInfo );
}
/**
$title = $this->getTitle();
+ $services = MediaWikiServices::getInstance();
+
# Show info in user (talk) namespace. Does the user exist? Is he blocked?
if ( $title->getNamespace() == NS_USER
|| $title->getNamespace() == NS_USER_TALK
LogEventsList::showLogExtract(
$outputPage,
'block',
- MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
+ $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
+ $block->getTarget(),
'',
[
'lim' => 1,
# Show delete and move logs if there were any such events.
# The logging query can DOS the site when bots/crawlers cause 404 floods,
# so be careful showing this. 404 pages must be cheap as they are hard to cache.
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
+ $cache = $services->getMainObjectStash();
$key = $cache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
$loggedIn = $this->getContext()->getUser()->isLoggedIn();
$sessionExists = $this->getContext()->getRequest()->getSession()->isPersistent();
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
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
$namespaceIds = $contLang->getNamespaceIds();
$caseSensitiveNamespaces = [];
- foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+ foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
$namespaceIds[$contLang->lc( $name )] = $index;
- if ( !MWNamespace::isCapitalized( $index ) ) {
+ if ( !$nsInfo->isCapitalized( $index ) ) {
$caseSensitiveNamespaces[] = $index;
}
}
'wgEnableWriteAPI' => true, // Deprecated since MW 1.32
'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
'wgNamespaceIds' => $namespaceIds,
- 'wgContentNamespaces' => MWNamespace::getContentNamespaces(),
+ 'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
'wgSiteName' => $conf->get( 'Sitename' ),
'wgDBname' => $conf->get( 'DBname' ),
'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
* @return array Array of strings
*/
public static function titleSearch( $search, $limit, $namespaces = [], $offset = 0 ) {
+ wfDeprecated( __METHOD__, '1.34' );
$prefixSearch = new StringPrefixSearch;
return $prefixSearch->search( $search, $limit, $namespaces, $offset );
}
* search engine
*/
public function transformSearchTerm( $term ) {
+ wfDeprecated( __METHOD__, '1.34' );
return $term;
}
return $subpages;
}
- if ( $out->isArticle() && MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+ if (
+ $out->isArticle() && MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $title->getNamespace() )
+ ) {
$ptext = $title->getPrefixedText();
if ( strpos( $ptext, '/' ) !== false ) {
$links = explode( '/', $ptext );
}
}
- $linkClass = MediaWikiServices::getInstance()->getLinkRenderer()->getLinkClasses( $title );
+ $services = MediaWikiServices::getInstance();
+ $linkClass = $services->getLinkRenderer()->getLinkClasses( $title );
// wfMessageFallback will nicely accept $message as an array of fallbacks
// or just a single key
if ( $msg->exists() ) {
$text = $msg->text();
} else {
- $text = MediaWikiServices::getInstance()->getContentLanguage()->getConverter()->
- convertNamespace( MWNamespace::getSubject( $title->getNamespace() ) );
+ $text = $services->getContentLanguage()->getConverter()->
+ convertNamespace( $services->getNamespaceInfo()->
+ getSubject( $title->getNamespace() ) );
}
// Avoid PHP 7.1 warning of passing $this by reference
}
if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() &&
- MWNamespace::getRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
+ MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
) {
$mode = $title->isProtected() ? 'unprotect' : 'protect';
$content_navigation['actions'][$mode] = [
* @file
* @ingroup SpecialPage
*/
+
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\DBQueryTimeoutError;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
-use MediaWiki\MediaWikiServices;
/**
* Special page which uses a ChangesList to show query results.
if ( $opts[ 'associated' ] ) {
$associatedNamespaces = array_map(
function ( $ns ) {
- return MWNamespace::getAssociated( $ns );
+ return MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getAssociated( $ns );
},
$namespaces
);
public function getQueryInfo() {
$tables = [ 'page', 'revision' ];
$conds = [
- 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_namespace' =>
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces(),
'page_is_redirect' => 0
];
$joinConds = [
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
use MediaWiki\Widget\DateInputWidget;
/**
if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
if ( $block->getType() == Block::TYPE_RANGE ) {
- $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
+ $nt = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( NS_USER ) . ':' . $block->getTarget();
}
$out = $this->getOutput(); // showLogExtract() wants first parameter by reference
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* A special page that list pages that contain no link to other pages
*
],
'conds' => [
'pl_from IS NULL',
- 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces(),
'page_is_redirect' => 0
],
'join_conds' => [
function getOrderFields() {
// For some crazy reason ordering by a constant
// causes a filesort
- if ( count( MWNamespace::getContentNamespaces() ) > 1 ) {
+ if ( count( MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces() ) > 1
+ ) {
return [ 'page_namespace', 'page_title' ];
} else {
return [ 'page_title' ];
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Implements Special:DeletedContributions to display archived revisions
* @ingroup SpecialPage
$block = Block::newFromTarget( $userObj, $userObj );
if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
if ( $block->getType() == Block::TYPE_RANGE ) {
- $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
+ $nt = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( NS_USER ) . ':' . $block->getTarget();
}
// LogEventsList::showLogExtract() wants the first parameter by ref
*/
protected function getWatchlistInfo() {
$titles = [];
+ $services = MediaWikiServices::getInstance();
- $watchedItems = MediaWikiServices::getInstance()->getWatchedItemStore()
+ $watchedItems = $services->getWatchedItemStore()
->getWatchedItemsForUser( $this->getUser(), [ 'sort' => WatchedItemStore::SORT_ASC ] );
$lb = new LinkBatch();
$namespace = $watchedItem->getLinkTarget()->getNamespace();
$dbKey = $watchedItem->getLinkTarget()->getDBkey();
$lb->add( $namespace, $dbKey );
- if ( !MWNamespace::isTalk( $namespace ) ) {
+ if ( !$services->getNamespaceInfo()->isTalk( $namespace ) ) {
$titles[$namespace][$dbKey] = 1;
}
}
*/
private function getExpandedTargets( array $targets ) {
$expandedTargets = [];
+ $services = MediaWikiServices::getInstance();
foreach ( $targets as $target ) {
if ( !$target instanceof LinkTarget ) {
try {
$ns = $target->getNamespace();
$dbKey = $target->getDBkey();
- $expandedTargets[] = new TitleValue( MWNamespace::getSubject( $ns ), $dbKey );
- $expandedTargets[] = new TitleValue( MWNamespace::getTalk( $ns ), $dbKey );
+ $expandedTargets[] =
+ new TitleValue( $services->getNamespaceInfo()->getSubject( $ns ), $dbKey );
+ $expandedTargets[] =
+ new TitleValue( $services->getNamespaceInfo()->getTalk( $ns ), $dbKey );
}
return $expandedTargets;
}
'redirect' => 'page_is_redirect'
],
'conds' => [
- 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces(),
'page_id = rev_page' ],
'options' => [
'GROUP BY' => [ 'page_namespace', 'page_title', 'page_is_redirect' ]
$groupnameLocalized = UserGroupMembership::getGroupName( $groupname );
$grouppageLocalizedTitle = UserGroupMembership::getGroupPage( $groupname )
- ?: Title::newFromText( MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname );
+ ?: Title::newFromText( MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( NS_PROJECT ) . ':' . $groupname );
if ( $group == '*' || !$grouppageLocalizedTitle ) {
// Do not make a link for the generic * group or group with invalid group page
);
$linkRenderer = $this->getLinkRenderer();
ksort( $namespaceProtection );
- $validNamespaces = MWNamespace::getValidNamespaces();
+ $validNamespaces =
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces();
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
foreach ( $namespaceProtection as $namespace => $rights ) {
if ( !in_array( $namespace, $validNamespaces ) ) {
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* A special page looking for articles with no article linking to them,
* thus being lonely.
$tables = [ 'page', 'pagelinks', 'templatelinks' ];
$conds = [
'pl_namespace IS NULL',
- 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces(),
'page_is_redirect' => 0,
'tl_namespace IS NULL'
];
function getOrderFields() {
// For some crazy reason ordering by a constant
// causes a filesort in MySQL 5
- if ( count( MWNamespace::getContentNamespaces() ) > 1 ) {
+ if ( count( MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces() ) > 1
+ ) {
return [ 'page_namespace', 'page_title' ];
} else {
return [ 'page_title' ];
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
*/
+use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
'title' => 'page_title',
'value' => 'COUNT(*)'
],
- 'conds' => [ 'page_namespace' => MWNamespace::getContentNamespaces() ],
+ 'conds' => [ 'page_namespace' =>
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces() ],
'options' => [
'HAVING' => 'COUNT(*) > 1',
'GROUP BY' => [ 'page_namespace', 'page_title' ]
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
'title' => 'page_title',
'value' => 'COUNT(*)'
], 'conds' => [
- 'page_namespace' => MWNamespace::getContentNamespaces()
+ 'page_namespace' =>
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces()
], 'options' => [
'HAVING' => 'COUNT(*) > 1',
'GROUP BY' => [
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* A special page that allows users to change page titles
*
$immovableNamespaces = [];
foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) {
- if ( !MWNamespace::isMovable( $nsId ) ) {
+ if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->isMovable( $nsId ) ) {
$immovableNamespaces[] = $nsId;
}
}
# 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;
}
*/
// @todo FIXME: Use Title::moveSubpages() here
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
$dbr = wfGetDB( DB_MASTER );
if ( $this->moveSubpages && (
- MWNamespace::hasSubpages( $nt->getNamespace() ) || (
+ $nsInfo->hasSubpages( $nt->getNamespace() ) || (
$this->moveTalk
- && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+ && $nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() )
)
) ) {
$conds = [
. ' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
];
$conds['page_namespace'] = [];
- if ( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
+ if ( $nsInfo->hasSubpages( $nt->getNamespace() ) ) {
$conds['page_namespace'][] = $ot->getNamespace();
}
if ( $this->moveTalk &&
- MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+ $nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() )
) {
$conds['page_namespace'][] = $ot->getTalkPage()->getNamespace();
}
* @param Title $title Page being moved.
*/
function showSubpages( $title ) {
- $nsHasSubpages = MWNamespace::hasSubpages( $title->getNamespace() );
+ $nsHasSubpages = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $title->getNamespace() );
$subpages = $title->getSubpages();
$count = $subpages instanceof TitleArray ? $subpages->count() : 0;
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* This special page lists the defined password policies for user groups.
* See also @ref $wgPasswordPolicy.
$groupnameLocalized = UserGroupMembership::getGroupName( $group );
$grouppageLocalizedTitle = UserGroupMembership::getGroupPage( $group )
- ?: Title::newFromText( MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $group );
+ ?: Title::newFromText( MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( NS_PROJECT ) . ':' . $group );
$grouppage = $linkRenderer->makeLink(
$grouppageLocalizedTitle,
protected $extra = []; // Extra SQL statements
public function __construct( $name = 'Randompage' ) {
- $this->namespaces = MWNamespace::getContentNamespaces();
+ $this->namespaces = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces();
parent::__construct( $name );
}
$showSuggestion = $title === null || !$title->isKnown();
$search->setShowSuggestion( $showSuggestion );
- $rewritten = $search->transformSearchTerm( $term );
- if ( $rewritten !== $term ) {
- $term = $rewritten;
- wfDeprecated( 'SearchEngine::transformSearchTerm() (overridden by ' .
- get_class( $search ) . ')', '1.32' );
- }
-
$rewritten = $search->replacePrefixes( $term );
if ( $rewritten !== $term ) {
wfDeprecated( 'SearchEngine::replacePrefixes() (overridden by ' .
) {
// Reset namespace preferences: namespaces are not searched
// when they're not mentioned in the URL parameters.
- foreach ( MWNamespace::getValidNamespaces() as $n ) {
+ foreach ( MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces()
+ as $n
+ ) {
$user->setOption( 'searchNs' . $n, false );
}
// The request parameters include all the namespaces to be searched.
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
$blacklist = $config->get( 'ShortPagesNamespaceBlacklist' );
$tables = [ 'page' ];
$conds = [
- 'page_namespace' => array_diff( MWNamespace::getContentNamespaces(), $blacklist ),
+ 'page_namespace' => array_diff(
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces(),
+ $blacklist
+ ),
'page_is_redirect' => 0
];
$joinConds = [];
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Special page lists various statistics, including the contents of
* `site_stats`, plus page view details if enabled
}
$msg = $this->msg( 'grouppage-' . $groupname )->inContentLanguage();
if ( $msg->isBlank() ) {
- $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) .
- ':' . $groupname;
+ $grouppageLocalized = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( NS_PROJECT ) . ':' . $groupname;
} else {
$grouppageLocalized = $msg->text();
}
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* A special page looking for page without any category.
*
'cl_from IS NULL',
'page_namespace' => $this->requestedNamespace !== false
? $this->requestedNamespace
- : MWNamespace::getContentNamespaces(),
+ : MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces(),
'page_is_redirect' => 0
],
'join_conds' => [
function getOrderFields() {
// For some crazy reason ordering by a constant
// causes a filesort
- if ( $this->requestedNamespace === false && count( MWNamespace::getContentNamespaces() ) > 1 ) {
+ if ( $this->requestedNamespace === false &&
+ count( MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces() ) > 1
+ ) {
return [ 'page_namespace', 'page_title' ];
}
'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(),
* @author Rob Church <robchur@gmail.com>
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Special page lists pages without language links
*
],
'conds' => [
'll_title IS NULL',
- 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces(),
'page_is_redirect' => 0
],
'join_conds' => [ 'langlinks' => [ 'LEFT JOIN', 'll_from = page_id' ] ]
'wgCheckFileExtensions' => $config->get( 'CheckFileExtensions' ),
'wgStrictFileExtensions' => $config->get( 'StrictFileExtensions' ),
'wgFileExtensions' => array_values( array_unique( $config->get( 'FileExtensions' ) ) ),
- 'wgCapitalizeUploads' => MWNamespace::isCapitalized( NS_FILE ),
+ 'wgCapitalizeUploads' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+ isCapitalized( NS_FILE ),
'wgMaxUploadSize' => $this->mMaxUploadSize,
'wgFileCanRotate' => SpecialUpload::rotationEnabled(),
];
}
$associatedNS = $this->mDb->addQuotes(
- MWNamespace::getAssociated( $this->namespace )
+ MediaWikiServices::getInstance()->getAssociated( $this->namespace )
);
return [
// For built-in namespaces (0 <= ID < 100), we try to find a local NS with
// the same namespace ID
- if ( $foreignNs < 100 && MWNamespace::exists( $foreignNs ) ) {
+ if (
+ $foreignNs < 100 &&
+ MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $foreignNs )
+ ) {
return Title::makeTitleSafe( $foreignNs, $foreignTitle->getText() );
}
}
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* A class to convert page titles on a foreign wiki (ForeignTitle objects) into
* page titles on the local wiki (Title objects), placing all pages in a fixed
* @param int $ns The namespace to use for all pages
*/
public function __construct( $ns ) {
- if ( !MWNamespace::exists( $ns ) ) {
+ if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) {
throw new MWException( "Namespace $ns doesn't exist on this wiki" );
}
$this->ns = $ns;
* @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
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* A class to convert page titles on a foreign wiki (ForeignTitle objects) into
* page titles on the local wiki (Title objects), placing all pages as subpages
* created
*/
public function __construct( Title $rootPage ) {
- if ( !MWNamespace::hasSubpages( $rootPage->getNamespace() ) ) {
+ if (
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->
+ hasSubpages( $rootPage->getNamespace() )
+ ) {
throw new MWException( "The root page you specified, $rootPage, is in a " .
"namespace where subpages are not allowed" );
}
if ( $pos !== false ) {
$iw = explode( ':', substr( $userName, 0, $pos ) );
$firstIw = array_shift( $iw );
- $interwikiLookup = MediaWikiServices::getInstance()->getInterwikiLookup();
+ $services = MediaWikiServices::getInstance();
+ $interwikiLookup = $services->getInterwikiLookup();
if ( $interwikiLookup->isValidInterwiki( $firstIw ) ) {
- $title = MWNamespace::getCanonicalName( NS_USER ) . ':' . substr( $userName, $pos + 1 );
+ $title = $services->getNamespaceInfo()->getCanonicalName( NS_USER ) .
+ ':' . substr( $userName, $pos + 1 );
if ( $iw ) {
$title = implode( ':', $iw ) . ':' . $title;
}
* @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';
}
if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
- return self::$idCacheByName[$name];
+ return is_null( self::$idCacheByName[$name] ) ? null : (int)self::$idCacheByName[$name];
}
list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
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 );
}
use Html;
use MediaWiki\MediaWikiServices;
use MediaWiki\Widget\SearchInputWidget;
-use MWNamespace;
use SearchEngineConfig;
use SpecialSearch;
use Xml;
$activeNamespaces = $this->specialSearch->getNamespaces();
$langConverter = $this->specialSearch->getLanguage();
foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
- $subject = MWNamespace::getSubject( $namespace );
+ $subject = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getSubject( $namespace );
if ( !isset( $rows[$subject] ) ) {
$rows[$subject] = "";
}
*/
use CLDRPluralRuleParser\Evaluator;
+use MediaWiki\MediaWikiServices;
use Wikimedia\Assert\Assert;
/**
if ( is_null( $this->namespaceNames ) ) {
global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
- $validNamespaces = MWNamespace::getCanonicalNamespaces();
+ $validNamespaces = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalNamespaces();
$this->namespaceNames = $wgExtraNamespaces +
self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
*/
public function getNsIndex( $text ) {
$lctext = $this->lc( $text );
- $ns = MWNamespace::getCanonicalIndex( $lctext );
+ $ns = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalIndex( $lctext );
if ( $ns !== null ) {
return $ns;
}
"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": "Паказаць удзельнікаў ад:",
"blocklink": "block",
"unblocklink": "unblock",
"change-blocklink": "change block",
+ "empty-username": "(no username available)",
"contribslink": "contribs",
"emaillink": "send email",
"autoblocker": "Autoblocked because your IP address has been recently used by \"[[User:$1|$1]]\".\nThe reason given for $1's block is \"$2\"",
"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": "د دوتنې مېنځپانگه پټول",
"blocklink": "Display name for a link that, when selected, leads to a form where a user can be blocked. Used in page history and recent changes pages. Example: \"''UserName (Talk | contribs | '''block''')''\".\n\nUsed as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]].\n\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|Change-blocklink}}\n* {{msg-mw|Unblocklink}}\n* {{msg-mw|Sp-contributions-blocklog}}\n* {{msg-mw|Sp-contributions-uploads}}\n* {{msg-mw|Sp-contributions-logs}}\n* {{msg-mw|Sp-contributions-deleted}}\n* {{msg-mw|Sp-contributions-userrights}}\n{{Identical|Block}}",
"unblocklink": "Used as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]].\n\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|change-blocklink}}\n* {{msg-mw|blocklink}}\n* {{msg-mw|sp-contributions-blocklog}}\n* {{msg-mw|sp-contributions-uploads}}\n* {{msg-mw|sp-contributions-logs}}\n* {{msg-mw|sp-contributions-deleted}}\n* {{msg-mw|sp-contributions-userrights}}\n{{Identical|Unblock}}",
"change-blocklink": "Used to name the link on [[Special:Log]].\n\nAlso used as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]].\n\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|unblocklink}}\n* {{msg-mw|blocklink}}\n* {{msg-mw|sp-contributions-blocklog}}\n* {{msg-mw|sp-contributions-uploads}}\n* {{msg-mw|sp-contributions-logs}}\n* {{msg-mw|sp-contributions-deleted}}\n* {{msg-mw|sp-contributions-userrights}}",
+ "empty-username": "If no username is available to display for a log or history entry, such as because of an incorrect database entry, this message is displayed in place of the links to the user page/talk/contribs/etc.",
"contribslink": "Short for \"contributions\". Used as display name for a link to user contributions on history pages, [[Special:RecentChanges]], [[Special:Watchlist]], etc.\n{{Identical|Contribution}}",
"emaillink": "Used as display name for a link to send an e-mail to a user in the user tool links. Example: \"(Talk | contribs | block | send e-mail)\".\n\n{{Identical|E-mail}}",
"autoblocker": "Used in [[Special:Block]].\n* $1 - target username\n* $2 - reason",
"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
$this->namespace = intval( $this->getOption( 'namespace', 0 ) );
- if ( MWNamespace::isCapitalized( $this->namespace ) ) {
+ if (
+ MediaWikiServices::getInstance()->getNamespaceInfo()->
+ isCapitalized( $this->namespace )
+ ) {
$this->output( "Will be moving pages to first letter capitalized titles" );
$callback = 'processRowToUppercase';
} else {
# Namespace which no longer exists. Put the page in the main namespace
# since we don't have any idea of the old namespace name. See T70501.
- if ( !MWNamespace::exists( $ns ) ) {
+ if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) {
$ns = 0;
}
$id = 0;
if ( $this->assign ) {
- $id = (int)User::idFromName( $name );
+ $id = User::idFromName( $name );
if ( !$id ) {
// See if any extension wants to create it.
if ( !isset( $this->triedCreations[$name] ) ) {
$this->triedCreations[$name] = true;
if ( !Hooks::run( 'ImportHandleUnknownUser', [ $name ] ) ) {
- $id = (int)User::idFromName( $name, User::READ_LATEST );
+ $id = User::idFromName( $name, User::READ_LATEST );
}
}
}
* @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__
* @return string
*/
function guessPriority( $namespace ) {
- return MWNamespace::isSubject( $namespace )
+ return MediaWikiServices::getInstance()->getNamespaceInfo()->isSubject( $namespace )
? $this->priorities[self::GS_MAIN]
: $this->priorities[self::GS_TALK];
}
* @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;
}
// Now pull in all canonical and alias namespaces...
- foreach ( MWNamespace::getCanonicalNamespaces() as $ns => $name ) {
+ foreach (
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getCanonicalNamespaces()
+ as $ns => $name
+ ) {
// This includes $wgExtraNamespaces
if ( $name !== '' ) {
$spaces[$name] = $ns;
* @return ResultWrapper
*/
private function getTargetList( $ns, $name, $options ) {
- if ( $options['move-talk'] && MWNamespace::isSubject( $ns ) ) {
+ if (
+ $options['move-talk'] &&
+ MediaWikiServices::getInstance()->getNamespaceInfo()->isSubject( $ns )
+ ) {
$checkNamespaces = [ NS_MAIN, NS_TALK ];
} else {
$checkNamespaces = NS_MAIN;
$dbk = "$name-" . $dbk;
}
$destNS = $ns;
- if ( $sourceNs == NS_TALK && MWNamespace::isSubject( $ns ) ) {
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+ if ( $sourceNs == NS_TALK && $nsInfo->isSubject( $ns ) ) {
// This is an associated talk page moved with the --move-talk feature.
- $destNS = MWNamespace::getTalk( $destNS );
+ $destNS = $nsInfo->getTalk( $destNS );
}
$newTitle = Title::makeTitleSafe( $destNS, $dbk );
if ( !$newTitle || !$newTitle->canExist() ) {
$url = rtrim( $this->source, '?' ) . '?' . $url;
}
- $json = Http::get( $url );
+ $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get( $url );
$data = json_decode( $json, true );
if ( is_array( $data ) ) {
* @ingroup Maintenance
*/
+use MediaWiki\MediaWikiServices;
+
require_once __DIR__ . '/Maintenance.php';
/**
// Get the pages
$res = $dbr->select( 'page',
[ 'page_namespace', 'page_title', 'page_id' ],
- [ 'page_namespace' => MWNamespace::getContentNamespaces(),
+ [ 'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getContentNamespaces(),
"page_id BETWEEN " . (int)$blockStart . " AND " . (int)$blockEnd ],
__METHOD__,
[ 'ORDER BY' => 'page_id ASC', 'USE INDEX' => 'PRIMARY' ]
"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' => [
#pagehistory li.selected {
background-color: #f8f9fa;
color: #222;
- border: 1px dashed #a2a9b1;
+ border-color: #f8f9fa;
+ outline: 1px dashed #a2a9b1;
}
.mw-history-revisionactions {
--- /dev/null
+// Common button mixins for MediaWiki
+//
+// Helper mixins used to create button styles. this file is importable
+// by all less files via `@import 'mediawiki.mixins.buttons';`.
+
+/* stylelint-disable selector-class-pattern */
+
+// Primary buttons mixin
+.button-colors-primary( @bgColor, @highlightColor, @activeColor ) {
+ background-color: @bgColor;
+ color: #fff;
+ // border of the same color as background so that light background and
+ // dark background buttons are the same height and width
+ border: 1px solid @bgColor;
+
+ &:hover {
+ background-color: @highlightColor;
+ border-color: @highlightColor;
+ }
+
+ &:focus {
+ box-shadow: inset 0 0 0 1px @bgColor, inset 0 0 0 2px #fff;
+ }
+
+ &:active,
+ &.is-on {
+ background-color: @activeColor;
+ border-color: @activeColor;
+ box-shadow: none;
+ }
+
+ &:disabled {
+ background-color: @colorGray12;
+ color: #fff;
+ border-color: @colorGray12;
+
+ // Make sure disabled buttons don't have hover and active states
+ &:hover,
+ &:active {
+ background-color: @colorGray12;
+ color: #fff;
+ border-color: @colorGray12;
+ box-shadow: none;
+ }
+ }
+}
@import 'mediawiki.mixins';
+@import 'mediawiki.ui/mixins.buttons';
@import 'mediawiki.ui/variables';
/* stylelint-disable selector-class-pattern */
-// Buttons
-// Helper mixins
-// Primary buttons mixin
-.button-colors-primary( @bgColor, @highlightColor, @activeColor ) {
- background-color: @bgColor;
- color: #fff;
- // border of the same color as background so that light background and
- // dark background buttons are the same height and width
- border: 1px solid @bgColor;
-
- &:hover {
- background-color: @highlightColor;
- border-color: @highlightColor;
- }
-
- &:focus {
- box-shadow: inset 0 0 0 1px @bgColor, inset 0 0 0 2px #fff;
- }
-
- &:active,
- &.is-on {
- background-color: @activeColor;
- border-color: @activeColor;
- box-shadow: none;
- }
-
- &:disabled {
- background-color: @colorGray12;
- color: #fff;
- border-color: @colorGray12;
-
- // Make sure disabled buttons don't have hover and active states
- &:hover,
- &:active {
- background-color: @colorGray12;
- color: #fff;
- border-color: @colorGray12;
- box-shadow: none;
- }
- }
-}
-
// All buttons start with `.mw-ui-button` class, modified by other classes.
// It can be any element. Due to a lack of a CSS reset, the exact styling of
// the button depends on what type of element is used.
<?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' );
}
/**
'wgLanguageCode' => $langCode,
'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ),
'wgNamespacesWithSubpages' => array_fill_keys(
- MWNamespace::getValidNamespaces(), isset( $opts['subpage'] )
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
+ isset( $opts['subpage'] )
),
'wgMaxTocLevel' => $maxtoclevel,
'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ),
* @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;
}
}
// NOTE: prefer content namespaces
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
$namespaces = array_unique( array_merge(
- MWNamespace::getContentNamespaces(),
+ $nsInfo->getContentNamespaces(),
[ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
- MWNamespace::getValidNamespaces()
+ $nsInfo->getValidNamespaces()
) );
$namespaces = array_diff( $namespaces, [
NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
] );
- $talk = array_filter( $namespaces, function ( $ns ) {
- return MWNamespace::isTalk( $ns );
+ $talk = array_filter( $namespaces, function ( $ns ) use ( $nsInfo ) {
+ return $nsInfo->isTalk( $ns );
} );
// prefer non-talk pages
'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 )
+ ) );
+ }
}
];
}
+ /**
+ * @dataProvider provideNewFromTargetRangeBlocks
+ * @covers Block::newFromTarget
+ */
+ public function testNewFromTargetRangeBlocks( $targets, $ip, $expectedTarget ) {
+ $blocker = $this->getTestSysop()->getUser();
+
+ foreach ( $targets as $target ) {
+ $block = new Block();
+ $block->setTarget( $target );
+ $block->setBlocker( $blocker );
+ $block->insert();
+ }
+
+ // Should find the block with the narrowest range
+ $blockTarget = Block::newFromTarget( $this->getTestUser()->getUser(), $ip )->getTarget();
+ $this->assertSame(
+ $blockTarget instanceof User ? $blockTarget->getName() : $blockTarget,
+ $expectedTarget
+ );
+
+ foreach ( $targets as $target ) {
+ $block = Block::newFromTarget( $target );
+ $block->delete();
+ }
+ }
+
+ function provideNewFromTargetRangeBlocks() {
+ return [
+ 'Blocks to IPv4 ranges' => [
+ [ '0.0.0.0/20', '0.0.0.0/30', '0.0.0.0/25' ],
+ '0.0.0.0',
+ '0.0.0.0/30'
+ ],
+ 'Blocks to IPv6 ranges' => [
+ [ '0:0:0:0:0:0:0:0/20', '0:0:0:0:0:0:0:0/30', '0:0:0:0:0:0:0:0/25' ],
+ '0:0:0:0:0:0:0:0',
+ '0:0:0:0:0:0:0:0/30'
+ ],
+ 'Blocks to wide IPv4 range and IP' => [
+ [ '0.0.0.0/16', '0.0.0.0' ],
+ '0.0.0.0',
+ '0.0.0.0'
+ ],
+ 'Blocks to wide IPv6 range and IP' => [
+ [ '0:0:0:0:0:0:0:0/19', '0:0:0:0:0:0:0:0' ],
+ '0:0:0:0:0:0:0:0',
+ '0:0:0:0:0:0:0:0'
+ ],
+ 'Blocks to narrow IPv4 range and IP' => [
+ [ '0.0.0.0/31', '0.0.0.0' ],
+ '0.0.0.0',
+ '0.0.0.0'
+ ],
+ 'Blocks to narrow IPv6 range and IP' => [
+ [ '0:0:0:0:0:0:0:0/127', '0:0:0:0:0:0:0:0' ],
+ '0:0:0:0:0:0:0:0',
+ '0:0:0:0:0:0:0:0'
+ ],
+ ];
+ }
+
/**
* @covers Block::appliesToRight
*/
// 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' );
}
/**
'wgArticlePath' => '/wiki/$1',
] );
- $this->assertEquals(
- $expected,
- Linker::userLink( $userId, $userName, $altUserName ),
- $msg
- );
+ // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+ if ( $userName === '' ) {
+ Wikimedia\suppressWarnings();
+ }
+ $actual = Linker::userLink( $userId, $userName, $altUserName );
+ if ( $userName === '' ) {
+ Wikimedia\restoreWarnings();
+ }
+
+ $this->assertEquals( $expected, $actual, $msg );
}
public static function provideCasesForUserLink() {
# - optional altUserName
# - optional message
return [
+ # Empty name (T222529)
+ 'Empty username, userid 0' => [ '(no username available)', 0, '' ],
+ 'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
# ## ANONYMOUS USER ########################################
[
];
}
+ /**
+ * @dataProvider provideUserToolLinks
+ * @covers Linker::userToolLinks
+ * @param string $expected
+ * @param int $userId
+ * @param string $userText
+ */
+ public function testUserToolLinks( $expected, $userId, $userText ) {
+ // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+ if ( $userText === '' ) {
+ Wikimedia\suppressWarnings();
+ }
+ $actual = Linker::userToolLinks( $userId, $userText );
+ if ( $userText === '' ) {
+ Wikimedia\restoreWarnings();
+ }
+
+ $this->assertSame( $expected, $actual );
+ }
+
+ public static function provideUserToolLinks() {
+ return [
+ // Empty name (T222529)
+ 'Empty username, userid 0' => [ ' (no username available)', 0, '' ],
+ 'Empty username, userid > 0' => [ ' (no username available)', 73, '' ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideUserTalkLink
+ * @covers Linker::userTalkLink
+ * @param string $expected
+ * @param int $userId
+ * @param string $userText
+ */
+ public function testUserTalkLink( $expected, $userId, $userText ) {
+ // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+ if ( $userText === '' ) {
+ Wikimedia\suppressWarnings();
+ }
+ $actual = Linker::userTalkLink( $userId, $userText );
+ if ( $userText === '' ) {
+ Wikimedia\restoreWarnings();
+ }
+
+ $this->assertSame( $expected, $actual );
+ }
+
+ public static function provideUserTalkLink() {
+ return [
+ // Empty name (T222529)
+ 'Empty username, userid 0' => [ '(no username available)', 0, '' ],
+ 'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideBlockLink
+ * @covers Linker::blockLink
+ * @param string $expected
+ * @param int $userId
+ * @param string $userText
+ */
+ public function testBlockLink( $expected, $userId, $userText ) {
+ // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+ if ( $userText === '' ) {
+ Wikimedia\suppressWarnings();
+ }
+ $actual = Linker::blockLink( $userId, $userText );
+ if ( $userText === '' ) {
+ Wikimedia\restoreWarnings();
+ }
+
+ $this->assertSame( $expected, $actual );
+ }
+
+ public static function provideBlockLink() {
+ return [
+ // Empty name (T222529)
+ 'Empty username, userid 0' => [ '(no username available)', 0, '' ],
+ 'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideEmailLink
+ * @covers Linker::emailLink
+ * @param string $expected
+ * @param int $userId
+ * @param string $userText
+ */
+ public function testEmailLink( $expected, $userId, $userText ) {
+ // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+ if ( $userText === '' ) {
+ Wikimedia\suppressWarnings();
+ }
+ $actual = Linker::emailLink( $userId, $userText );
+ if ( $userText === '' ) {
+ Wikimedia\restoreWarnings();
+ }
+
+ $this->assertSame( $expected, $actual );
+ }
+
+ public static function provideEmailLink() {
+ return [
+ // Empty name (T222529)
+ 'Empty username, userid 0' => [ '(no username available)', 0, '' ],
+ 'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
+ ];
+ }
+
/**
* @dataProvider provideCasesForFormatComment
* @covers Linker::formatComment
<?php
+use MediaWiki\MediaWikiServices;
+
/**
* @covers PageProps
*
( $model === null || $model === CONTENT_MODEL_WIKITEXT )
) {
$ns = $this->getDefaultWikitextNS();
- $page = MWNamespace::getCanonicalName( $ns ) . ':' . $page;
+ $page = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( $ns ) . ':' . $page;
}
$page = Title::newFromText( $page );
->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 );
}
( $model === null || $model === CONTENT_MODEL_WIKITEXT )
) {
$ns = $this->getDefaultWikitextNS();
- $titleString = MWNamespace::getCanonicalName( $ns ) . ':' . $titleString;
+ $titleString = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getCanonicalName( $ns ) . ':' . $titleString;
}
$title = Title::newFromText( $titleString );
$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
<?php
+use MediaWiki\MediaWikiServices;
use Wikimedia\TestingAccessWrapper;
/**
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'namespace',
],
- MWNamespace::getValidNamespaces(),
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
[],
],
// PARAM_ALL is ignored with namespace types.
ApiBase::PARAM_TYPE => 'namespace',
ApiBase::PARAM_ALL => false,
],
- MWNamespace::getValidNamespaces(),
+ MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
[],
],
'Namespace with wildcard "x"' => [
'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' ) );
use Wikimedia\Rdbms\LBFactoryMulti;
use Wikimedia\Rdbms\LoadBalancer;
use Wikimedia\Rdbms\ChronologyProtector;
-use Wikimedia\Rdbms\DatabaseMysqli;
use Wikimedia\Rdbms\MySQLMasterPos;
use Wikimedia\Rdbms\DatabaseDomain;
* @dataProvider getLBFactoryClassProvider
*/
public function testGetLBFactoryClass( $expected, $deprecated ) {
- $mockDB = $this->getMockBuilder( DatabaseMysqli::class )
+ $mockDB = $this->getMockBuilder( IDatabase::class )
->disableOriginalConstructor()
->getMock();
$m2Pos = new MySQLMasterPos( 'db1064-bin.002400/794074907', $now );
// Master DB 1
- $mockDB1 = $this->getMockBuilder( DatabaseMysqli::class )
+ $mockDB1 = $this->getMockBuilder( IDatabase::class )
->disableOriginalConstructor()
->getMock();
$mockDB1->method( 'writesOrCallbacksPending' )->willReturn( true );
$lb1->method( 'getMasterPos' )->willReturn( $m1Pos );
$lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
// Master DB 2
- $mockDB2 = $this->getMockBuilder( DatabaseMysqli::class )
+ $mockDB2 = $this->getMockBuilder( IDatabase::class )
->disableOriginalConstructor()
->getMock();
$mockDB2->method( 'writesOrCallbacksPending' )->willReturn( true );
$expected
) {
$this->setMwGlobals( [
- // set to trick MWNamespace::getRestrictionLevels
+ // set to trick NamespaceInfo::getRestrictionLevels
'wgRestrictionLevels' => $restrictionLevels
] );
<?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
]
}
protected function getMocks() {
- $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class )
+ $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
->disableOriginalClone()
->disableOriginalConstructor()
->getMock();
]
] );
- $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class )
+ $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
->disableOriginalConstructor()
->getMock();
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 ) );
}
}
*/
private $linkCache;
+ /**
+ * @var NamespaceInfo
+ */
+ private $nsInfo;
+
public function setUp() {
parent::setUp();
- $this->titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
- $this->linkCache = MediaWikiServices::getInstance()->getLinkCache();
+
+ $services = MediaWikiServices::getInstance();
+ $this->titleFormatter = $services->getTitleFormatter();
+ $this->linkCache = $services->getLinkCache();
+ $this->nsInfo = $services->getNamespaceInfo();
}
public static function provideCreateFromLegacyOptions() {
* @dataProvider provideCreateFromLegacyOptions
*/
public function testCreateFromLegacyOptions( $options, $func, $val ) {
- $factory = new LinkRendererFactory( $this->titleFormatter, $this->linkCache );
+ $factory =
+ new LinkRendererFactory( $this->titleFormatter, $this->linkCache, $this->nsInfo );
$linkRenderer = $factory->createFromLegacyOptions(
$options
);
}
public function testCreate() {
- $factory = new LinkRendererFactory( $this->titleFormatter, $this->linkCache );
+ $factory =
+ new LinkRendererFactory( $this->titleFormatter, $this->linkCache, $this->nsInfo );
$this->assertInstanceOf( LinkRenderer::class, $factory->create() );
}
$user->expects( $this->once() )
->method( 'getStubThreshold' )
->willReturn( 15 );
- $factory = new LinkRendererFactory( $this->titleFormatter, $this->linkCache );
+ $factory =
+ new LinkRendererFactory( $this->titleFormatter, $this->linkCache, $this->nsInfo );
$linkRenderer = $factory->createForUser( $user );
$this->assertInstanceOf( LinkRenderer::class, $linkRenderer );
$this->assertEquals( 15, $linkRenderer->getStubThreshold() );
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' );
0 // redir
);
- $linkRenderer = new LinkRenderer( $titleFormatter, $linkCache );
+ $linkRenderer = new LinkRenderer( $titleFormatter, $linkCache, $nsInfo );
$linkRenderer->setStubThreshold( 0 );
$this->assertEquals(
'',
* @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 ) {
+ }
}
/**
$updater->setContent( 'main', $content );
return $updater->saveRevision( CommentStoreComment::newUnsavedComment( $comment ) );
}
+
+ /**
+ * @covers User::idFromName
+ */
+ public function testExistingIdFromName() {
+ $this->assertTrue(
+ array_key_exists( $this->user->getName(), User::$idCacheByName ),
+ 'Test user should already be in the id cache.'
+ );
+ $this->assertSame(
+ $this->user->getId(), User::idFromName( $this->user->getName() ),
+ 'Id is correctly retreived from the cache.'
+ );
+ $this->assertSame(
+ $this->user->getId(), User::idFromName( $this->user->getName(), User::READ_LATEST ),
+ 'Id is correctly retreived from the database.'
+ );
+ }
+
+ /**
+ * @covers User::idFromName
+ */
+ public function testNonExistingIdFromName() {
+ $this->assertFalse(
+ array_key_exists( 'NotExisitngUser', User::$idCacheByName ),
+ 'Non exisitng user should not be in the id cache.'
+ );
+ $this->assertSame( null, User::idFromName( 'NotExisitngUser' ) );
+ $this->assertTrue(
+ array_key_exists( 'NotExisitngUser', User::$idCacheByName ),
+ 'Username will be cached when requested once.'
+ );
+ $this->assertSame( null, User::idFromName( 'NotExisitngUser' ) );
+ }
}
<?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;
use Wikimedia\TestingAccessWrapper;
/**
break;
case 'namespace':
- $validValues = MWNamespace::getValidNamespaces();
+ $validValues = MediaWikiServices::getInstance()->getNamespaceInfo()->
+ getValidNamespaces();
if (
isset( $config[ApiBase::PARAM_EXTRA_NAMESPACES] ) &&
is_array( $config[ApiBase::PARAM_EXTRA_NAMESPACES] )
<?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 () {
assert.strictEqual( HistoryPage.heading.getText(), 'Revision history of "' + name + '"' );
} );
- it( 'should perform rollbacks after confirming intention', function () {
+ it.skip( 'should perform rollbacks after confirming intention', function () {
HistoryPage.rollback.click();
HistoryPage.rollbackConfirmableYes.waitForVisible( 5000 );
// 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();
// ==================
// Test Files
+ // FIXME: The non-core patterns to be removed once T199116 is fixed.
// ==================
specs: [
relPath( './tests/selenium/wdio-mediawiki/specs/*.js' ),
],
// Patterns to exclude
exclude: [
- relPath( './extensions/CirrusSearch/tests/selenium/specs/**/*.js' )
+ relPath( './extensions/CirrusSearch/tests/selenium/specs/**/*.js' ),
+ // Disabled per T222517
+ relPath( './skins/MinervaNeue/tests/selenium/specs/**/*.js' )
],
// ============