scenarios:
Load a page:
# The only page that exists by default is the main page.
- # But, its actual name is configurable/unknown (T216791).
- # Omit 'title' to let MediaWiki show the defaul (which is the main page),
+ # But its actual name is configurable/unknown (T216791).
+ # Omit 'title' to let MediaWiki show the default (which is the main page),
# and a query string to prevent a normalization redirect.
url: "{MW_SERVER}{MW_SCRIPT_PATH}/index.php?noredirectplz"
viewport:
deprecated and will be removed in the future.
* The FileBasedSiteLookup class has been deprecated. For a cacheable SiteLookup
implementation, use CachingSiteStore instead.
+* Language::viewPrevNext function is deprecated, use
+ SpecialPage::buildPrevNextNavigation instead
* ManualLogEntry::setTags() is deprecated, use ManualLogEntry::addTags()
instead. The setTags() method was overriding the tags, addTags() doesn't
override, only adds new tags.
'JavaScriptMinifier' => __DIR__ . '/includes/libs/JavaScriptMinifier.php',
'Job' => __DIR__ . '/includes/jobqueue/Job.php',
'JobQueue' => __DIR__ . '/includes/jobqueue/JobQueue.php',
- 'JobQueueAggregator' => __DIR__ . '/includes/jobqueue/aggregator/JobQueueAggregator.php',
- 'JobQueueAggregatorNull' => __DIR__ . '/includes/jobqueue/aggregator/JobQueueAggregatorNull.php',
- 'JobQueueAggregatorRedis' => __DIR__ . '/includes/jobqueue/aggregator/JobQueueAggregatorRedis.php',
'JobQueueConnectionError' => __DIR__ . '/includes/jobqueue/exception/JobQueueConnectionError.php',
'JobQueueDB' => __DIR__ . '/includes/jobqueue/JobQueueDB.php',
'JobQueueEnqueueUpdate' => __DIR__ . '/includes/deferred/JobQueueEnqueueUpdate.php',
* Check if the block prevents a user from resetting their password
*
* @since 1.33
- * @return bool|null The block blocks password reset
+ * @return bool The block blocks password reset
*/
public function appliesToPasswordReset() {
switch ( $this->getSystemBlockType() ) {
case 'wgSoftBlockRanges':
return false;
default:
- return false;
+ return true;
}
}
'default' => [ 'class' => JobQueueDB::class, 'order' => 'random', 'claimTTL' => 3600 ],
];
-/**
- * Which aggregator to use for tracking which queues have jobs.
- * These settings should be global to all wikis.
- */
-$wgJobQueueAggregator = [
- 'class' => JobQueueAggregatorNull::class
-];
-
/**
* Whether to include the number of jobs that are queued
* for the API's maxlag parameter.
* @return string
*/
public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
- return self::userToolLinks( $userId, $userText, true, 0, $edits );
+ return self::userToolLinks( $userId, $userText, true, 0, $edits, false );
}
/**
* @return RevisionRecord|null
*/
public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
+ // TODO should not require Title in future (T206498)
+ $title = Title::newFromLinkTarget( $linkTarget );
$conds = [
- 'page_namespace' => $linkTarget->getNamespace(),
- 'page_title' => $linkTarget->getDBkey()
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
];
if ( $revId ) {
// Use the specified revision ID.
// Since the caller supplied a revision ID, we are pretty sure the revision is
// supposed to exist, so we should try hard to find it.
$conds['rev_id'] = $revId;
- return $this->newRevisionFromConds( $conds, $flags );
+ return $this->newRevisionFromConds( $conds, $flags, $title );
} else {
// Use a join to get the latest revision.
// Note that we don't use newRevisionFromConds here because we don't want to retry
$db = $this->getDBConnectionRefForQueryFlags( $flags );
$conds[] = 'rev_id=page_latest';
- $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
+ $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
return $rev;
}
},
'LinkRenderer' => function ( MediaWikiServices $services ) : LinkRenderer {
- global $wgUser;
-
if ( defined( 'MW_NO_SESSION' ) ) {
return $services->getLinkRendererFactory()->create();
} else {
- return $services->getLinkRendererFactory()->createForUser( $wgUser );
+ return $services->getLinkRendererFactory()->createForUser(
+ RequestContext::getMain()->getUser()
+ );
}
},
return ' <span class="history-deleted">' .
$this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
} else {
- return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
+ return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle(),
+ // Whether section links should refer to local page (using default false)
+ false,
+ // wikid to generate links for (using default null) */
+ null,
+ // whether parentheses should be rendered as part of the message
+ false );
}
}
}
$classes = array_merge( $classes, $this->getHTMLClasses( $rcObj, $rcObj->watched ) );
- $separator = ' <span class="mw-changeslist-separator">. .</span> ';
+ $separator = ' <span class="mw-changeslist-separator"></span> ';
$data['recentChangesFlags'] = [
'newpage' => $type == RC_NEW,
$isnew ||
$rcObj->mAttribs['rc_type'] == RC_CATEGORIZE
) {
- $links['total-changes'] = $nchanges[$n];
+ $links['total-changes'] = Html::rawElement( 'span', [], $nchanges[$n] );
} else {
- $links['total-changes'] = $this->linkRenderer->makeKnownLink(
- $block0->getTitle(),
- new HtmlArmor( $nchanges[$n] ),
- [ 'class' => 'mw-changeslist-groupdiff' ],
- $queryParams + [
- 'diff' => $currentRevision,
- 'oldid' => $last->mAttribs['rc_last_oldid'],
- ]
+ $links['total-changes'] = Html::rawElement( 'span', [],
+ $this->linkRenderer->makeKnownLink(
+ $block0->getTitle(),
+ new HtmlArmor( $nchanges[$n] ),
+ [ 'class' => 'mw-changeslist-groupdiff' ],
+ $queryParams + [
+ 'diff' => $currentRevision,
+ 'oldid' => $last->mAttribs['rc_last_oldid'],
+ ]
+ )
);
if ( $sinceLast > 0 && $sinceLast < $n ) {
- $links['total-changes-since-last'] = $this->linkRenderer->makeKnownLink(
+ $links['total-changes-since-last'] = Html::rawElement( 'span', [],
+ $this->linkRenderer->makeKnownLink(
$block0->getTitle(),
new HtmlArmor( $sinceLastVisitMsg[$sinceLast] ),
[ 'class' => 'mw-changeslist-groupdiff' ],
'diff' => $currentRevision,
'oldid' => $unvisitedOldid,
]
- );
+ )
+ );
}
}
}
if ( $allLogs || $rcObj->mAttribs['rc_type'] == RC_CATEGORIZE ) {
// don't show history link for logs
} elseif ( $namehidden || !$block0->getTitle()->exists() ) {
- $links['history'] = $this->message['enhancedrc-history'];
+ $links['history'] = Html::rawElement( 'span', [], $this->message['enhancedrc-history'] );
} else {
$params = $queryParams;
$params['action'] = 'history';
- $links['history'] = $this->linkRenderer->makeKnownLink(
+ $links['history'] = Html::rawElement( 'span', [],
+ $this->linkRenderer->makeKnownLink(
$block0->getTitle(),
new HtmlArmor( $this->message['enhancedrc-history'] ),
[ 'class' => 'mw-changeslist-history' ],
$params
- );
+ )
+ );
}
# Allow others to alter, remove or add to these links
return '';
}
- $logtext = implode( $this->message['pipe-separator'], $links );
- $logtext = $this->msg( 'parentheses' )->rawParams( $logtext )->escaped();
+ $logtext = Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ],
+ implode( ' ', $links ) );
return ' ' . $logtext;
}
$logPage = new LogPage( $logType );
$logTitle = SpecialPage::getTitleFor( 'Log', $logType );
$logName = $logPage->getName()->text();
- $data['logLink'] = $this->msg( 'parentheses' )
- ->rawParams(
- $this->linkRenderer->makeKnownLink( $logTitle, $logName )
- )->escaped();
+ $data['logLink'] = Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ],
+ $this->linkRenderer->makeKnownLink( $logTitle, $logName )
+ );
} else {
$data['articleLink'] = $this->getArticleLink( $rcObj, $rcObj->unpatrolled, $rcObj->watched );
}
# Diff and hist links
if ( $type != RC_LOG && $type != RC_CATEGORIZE ) {
$query['action'] = 'history';
- $data['historyLink'] = $this->getDiffHistLinks( $rcObj, $query );
+ $data['historyLink'] = $this->getDiffHistLinks( $rcObj, $query, false );
}
- $data['separatorAfterLinks'] = ' <span class="mw-changeslist-separator">. .</span> ';
+ $data['separatorAfterLinks'] = ' <span class="mw-changeslist-separator"></span> ';
# Character diff
if ( $this->getConfig()->get( 'RCShowChangedSize' ) ) {
$cd = $this->formatCharacterDifference( $rcObj );
if ( $cd !== '' ) {
$data['characterDiff'] = $cd;
- $data['separatorAftercharacterDiff'] = ' <span class="mw-changeslist-separator">. .</span> ';
+ $data['separatorAftercharacterDiff'] = ' <span class="mw-changeslist-separator"></span> ';
}
}
$data['userTalkLink'] = $rcObj->usertalklink;
$data['comment'] = $this->insertComment( $rcObj );
if ( $type == RC_CATEGORIZE ) {
- $data['historyLink'] = $this->getDiffHistLinks( $rcObj, $query );
+ $data['historyLink'] = $this->getDiffHistLinks( $rcObj, $query, false );
}
$data['rollback'] = $this->getRollback( $rcObj );
}
] );
// everything else: makes it easier for extensions to add or remove data
- $line .= implode( '', $data );
+ foreach ( $data as $key => $dataItem ) {
+ $line .= Html::rawElement( 'span', [
+ 'class' => 'mw-changeslist-line-inner-' . $key,
+ ], $dataItem );
+ }
$line .= "</td></tr></table>\n";
*
* @param RCCacheEntry $rc
* @param array $query array of key/value pairs to append as a query string
+ * @param bool $useParentheses (optional) Wrap comments in parentheses where needed
* @return string HTML
*/
- public function getDiffHistLinks( RCCacheEntry $rc, array $query ) {
+ public function getDiffHistLinks( RCCacheEntry $rc, array $query, $useParentheses = true ) {
$pageTitle = $rc->getTitle();
if ( $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE ) {
// For categorizations we must swap the category title with the page title!
}
}
- $retVal = ' ' . $this->msg( 'parentheses' )
- ->rawParams( $rc->difflink . $this->message['pipe-separator']
- . $this->linkRenderer->makeKnownLink(
- $pageTitle,
- new HtmlArmor( $this->message['hist'] ),
- [ 'class' => 'mw-changeslist-history' ],
- $query
- ) )->escaped();
- return $retVal;
+ $histLink = $this->linkRenderer->makeKnownLink(
+ $pageTitle,
+ new HtmlArmor( $this->message['hist'] ),
+ [ 'class' => 'mw-changeslist-history' ],
+ $query
+ );
+ if ( $useParentheses ) {
+ $retVal = $this->msg( 'parentheses' )
+ ->rawParams( $rc->difflink . $this->message['pipe-separator']
+ . $histLink )->escaped();
+ } else {
+ $retVal = Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ],
+ Html::rawElement( 'span', [], $rc->difflink ) .
+ Html::rawElement( 'span', [], $histLink )
+ );
+ }
+ return ' ' . $retVal;
}
/**
if ( !ChangesList::isDeleted( $cacheEntry, Revision::DELETED_USER ) ) {
$cacheEntry->usertalklink = Linker::userToolLinks(
$cacheEntry->mAttribs['rc_user'],
- $cacheEntry->mAttribs['rc_user_text']
+ $cacheEntry->mAttribs['rc_user_text'],
+ // Should the contributions link be red if the user has no edits (using default)
+ false,
+ // Customisation flags (using default 0)
+ 0,
+ // User edit count (using default )
+ null,
+ // do not wrap the message in parentheses
+ false
);
}
$status->merge( $this->makeStubDBFile( $dir, $db ) );
$status->merge( $this->makeStubDBFile( $dir, "wikicache" ) );
$status->merge( $this->makeStubDBFile( $dir, "{$db}_l10n_cache" ) );
+ $status->merge( $this->makeStubDBFile( $dir, "{$db}_jobqueue" ) );
if ( !$status->isOK() ) {
return $status;
}
return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
}
+ # Create the job queue DB
+ try {
+ $conn = Database::factory(
+ 'sqlite', [ 'dbname' => "{$db}_jobqueue", 'dbDirectory' => $dir ] );
+ # @todo: don't duplicate job definition, though it's very static
+ $sql =
+<<<EOT
+ CREATE TABLE job (
+ job_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ job_cmd BLOB NOT NULL default '',
+ job_namespace INTEGER NOT NULL,
+ job_title TEXT NOT NULL,
+ job_timestamp BLOB NULL default NULL,
+ job_params BLOB NOT NULL,
+ job_random integer NOT NULL default 0,
+ job_attempts integer NOT NULL default 0,
+ job_token BLOB NOT NULL default '',
+ job_token_timestamp BLOB NULL default NULL,
+ job_sha1 BLOB NOT NULL default ''
+ );
+ CREATE INDEX job_sha1 ON job (job_sha1);
+ CREATE INDEX job_cmd_token ON job (job_cmd,job_token,job_random);
+ CREATE INDEX job_cmd_token_id ON job (job_cmd,job_token,job_id);
+ CREATE INDEX job_cmd ON job (job_cmd, job_namespace, job_title, job_params);
+ CREATE INDEX job_timestamp ON job (job_timestamp);
+EOT;
+ $conn->query( $sql );
+ $conn->query( "PRAGMA journal_mode=WAL" ); // this is permanent
+ $conn->close();
+ } catch ( DBConnectionError $e ) {
+ return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
+ }
+
# Open the main DB
return $this->getConnection();
}
*/
public function getLocalSettings() {
$dir = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgSQLiteDataDir' ) );
-
+ // These tables have frequent writes and are thus split off from the main one.
+ // Since the code using these tables only uses transactions for writes then set
+ // them to using BEGIN IMMEDIATE. This avoids frequent lock errors on first write.
return "# SQLite-specific settings
\$wgSQLiteDataDir = \"{$dir}\";
\$wgObjectCaches[CACHE_DB] = [
'type' => 'sqlite',
'dbname' => 'wikicache',
'tablePrefix' => '',
+ 'variables' => [ 'synchronous' => 'NORMAL' ],
'dbDirectory' => \$wgSQLiteDataDir,
+ 'trxMode' => 'IMMEDIATE',
'flags' => 0
]
];
'type' => 'sqlite',
'dbname' => \"{\$wgDBname}_l10n_cache\",
'tablePrefix' => '',
+ 'variables' => [ 'synchronous' => 'NORMAL' ],
'dbDirectory' => \$wgSQLiteDataDir,
+ 'trxMode' => 'IMMEDIATE',
'flags' => 0
+];
+\$wgJobTypeConf['default'] = [
+ 'class' => 'JobQueueDB',
+ 'claimTTL' => 3600,
+ 'server' => [
+ 'type' => 'sqlite',
+ 'dbname' => \"{\$wgDBname}_jobqueue\",
+ 'tablePrefix' => '',
+ 'dbDirectory' => \$wgSQLiteDataDir,
+ 'trxMode' => 'IMMEDIATE',
+ 'flags' => 0
+ ]
];";
}
}
return $this->params;
}
+ /**
+ * @param string|null $field Metadata field or null to get all the metadata
+ * @return mixed|null Value; null if missing
+ * @since 1.33
+ */
+ public function getMetadata( $field = null ) {
+ if ( $field === null ) {
+ return $this->metadata;
+ }
+
+ return $this->metadata[$field] ?? null;
+ }
+
+ /**
+ * @param string $field Key name to set the value for
+ * @param mixed $value The value to set the field for
+ * @return mixed|null The prior field value; null if missing
+ * @since 1.33
+ */
+ public function setMetadata( $field, $value ) {
+ $old = $this->getMetadata( $field );
+ if ( $value === null ) {
+ unset( $this->metadata[$field] );
+ } else {
+ $this->metadata[$field] = $value;
+ }
+
+ return $old;
+ }
+
/**
* @return int|null UNIX timestamp to delay running this job until, otherwise null
* @since 1.22
* @since 1.21
*/
abstract class JobQueue {
- /** @var string Wiki ID */
+ /** @var string DB domain ID */
protected $domain;
/** @var string Job type */
protected $type;
/** @var BagOStuff */
protected $dupCache;
- /** @var JobQueueAggregator */
- protected $aggr;
const QOS_ATOMIC = 1; // integer; "all-or-nothing" job insertions
/**
* @param array $params
- * @throws MWException
+ * @throws JobQueueError
*/
protected function __construct( array $params ) {
$this->domain = $params['domain'] ?? $params['wiki']; // b/c
$this->order = $this->optimalOrder();
}
if ( !in_array( $this->order, $this->supportedOrders() ) ) {
- throw new MWException( __CLASS__ . " does not support '{$this->order}' order." );
+ throw new JobQueueError( __CLASS__ . " does not support '{$this->order}' order." );
}
$this->dupCache = wfGetCache( CACHE_ANYTHING );
- $this->aggr = $params['aggregator'] ?? new JobQueueAggregatorNull( [] );
$this->readOnlyReason = $params['readOnlyReason'] ?? false;
}
*
* @param array $params
* @return JobQueue
- * @throws MWException
+ * @throws JobQueueError
*/
final public static function factory( array $params ) {
$class = $params['class'];
if ( !class_exists( $class ) ) {
- throw new MWException( "Invalid job queue class '$class'." );
+ throw new JobQueueError( "Invalid job queue class '$class'." );
}
$obj = new $class( $params );
if ( !( $obj instanceof self ) ) {
- throw new MWException( "Class '$class' is not a " . __CLASS__ . " class." );
+ throw new JobQueueError( "Class '$class' is not a " . __CLASS__ . " class." );
}
return $obj;
* @param IJobSpecification[] $jobs
* @param int $flags Bitfield (supports JobQueue::QOS_ATOMIC)
* @return void
- * @throws MWException
+ * @throws JobQueueError
*/
final public function batchPush( array $jobs, $flags = 0 ) {
$this->assertNotReadOnly();
foreach ( $jobs as $job ) {
if ( $job->getType() !== $this->type ) {
- throw new MWException(
+ throw new JobQueueError(
"Got '{$job->getType()}' job; expected a '{$this->type}' job." );
} elseif ( $job->getReleaseTimestamp() && !$this->supportsDelayedJobs() ) {
- throw new MWException(
+ throw new JobQueueError(
"Got delayed '{$job->getType()}' job; delays are not supported." );
}
}
$this->doBatchPush( $jobs, $flags );
- $this->aggr->notifyQueueNonEmpty( $this->domain, $this->type );
foreach ( $jobs as $job ) {
if ( $job->isRootJob() ) {
* This requires $wgJobClasses to be set for the given job type.
* Outside callers should use JobQueueGroup::pop() instead of this function.
*
- * @throws MWException
+ * @throws JobQueueError
* @return Job|bool Returns false if there are no jobs
*/
final public function pop() {
$this->assertNotReadOnly();
if ( !WikiMap::isCurrentWikiDbDomain( $this->domain ) ) {
- throw new MWException(
+ throw new JobQueueError(
"Cannot pop '{$this->type}' job off foreign '{$this->domain}' wiki queue." );
} elseif ( !isset( $wgJobClasses[$this->type] ) ) {
// Do not pop jobs if there is no class for the queue type
- throw new MWException( "Unrecognized job type '{$this->type}'." );
+ throw new JobQueueError( "Unrecognized job type '{$this->type}'." );
}
$job = $this->doPop();
- if ( !$job ) {
- $this->aggr->notifyQueueEmpty( $this->domain, $this->type );
- }
-
// Flag this job as an old duplicate based on its "root" job...
try {
if ( $job && $this->isRootJobOldDuplicate( $job ) ) {
*
* @param Job $job
* @return void
- * @throws MWException
+ * @throws JobQueueError
*/
final public function ack( Job $job ) {
$this->assertNotReadOnly();
if ( $job->getType() !== $this->type ) {
- throw new MWException( "Got '{$job->getType()}' job; expected '{$this->type}'." );
+ throw new JobQueueError( "Got '{$job->getType()}' job; expected '{$this->type}'." );
}
$this->doAck( $job );
* This does nothing for certain queue classes.
*
* @param IJobSpecification $job
- * @throws MWException
+ * @throws JobQueueError
* @return bool
*/
final public function deduplicateRootJob( IJobSpecification $job ) {
$this->assertNotReadOnly();
if ( $job->getType() !== $this->type ) {
- throw new MWException( "Got '{$job->getType()}' job; expected '{$this->type}'." );
+ throw new JobQueueError( "Got '{$job->getType()}' job; expected '{$this->type}'." );
}
return $this->doDeduplicateRootJob( $job );
/**
* @see JobQueue::deduplicateRootJob()
* @param IJobSpecification $job
- * @throws MWException
+ * @throws JobQueueError
* @return bool
*/
protected function doDeduplicateRootJob( IJobSpecification $job ) {
if ( !$job->hasRootJobParams() ) {
- throw new MWException( "Cannot register root job; missing parameters." );
+ throw new JobQueueError( "Cannot register root job; missing parameters." );
}
$params = $job->getRootJobParams();
* Check if the "root" job of a given job has been superseded by a newer one
*
* @param Job $job
- * @throws MWException
+ * @throws JobQueueError
* @return bool
*/
final protected function isRootJobOldDuplicate( Job $job ) {
if ( $job->getType() !== $this->type ) {
- throw new MWException( "Got '{$job->getType()}' job; expected '{$this->type}'." );
+ throw new JobQueueError( "Got '{$job->getType()}' job; expected '{$this->type}'." );
}
$isDuplicate = $this->doIsRootJobOldDuplicate( $job );
/**
* @see JobQueue::delete()
- * @throws MWException
+ * @throws JobQueueError
*/
protected function doDelete() {
- throw new MWException( "This method is not implemented." );
+ throw new JobQueueError( "This method is not implemented." );
}
/**
*
* @param array $types List of queues types
* @return array|null (list of non-empty queue types) or null if unsupported
- * @throws MWException
+ * @throws JobQueueError
* @since 1.22
*/
final public function getSiblingQueuesWithJobs( array $types ) {
*
* @param array $types List of queues types
* @return array|null (job type => whether queue is empty) or null if unsupported
- * @throws MWException
+ * @throws JobQueueError
* @since 1.22
*/
final public function getSiblingQueueSizes( array $types ) {
$title = Title::makeTitle( $row->job_namespace, $row->job_title );
$job = Job::factory( $row->job_cmd, $title,
self::extractBlob( $row->job_params ) );
- $job->metadata['id'] = $row->job_id;
- $job->metadata['timestamp'] = $row->job_timestamp;
+ $job->setMetadata( 'id', $row->job_id );
+ $job->setMetadata( 'timestamp', $row->job_timestamp );
break; // done
} while ( true );
* @throws MWException
*/
protected function doAck( Job $job ) {
- if ( !isset( $job->metadata['id'] ) ) {
+ $id = $job->getMetadata( 'id' );
+ if ( $id === null ) {
throw new MWException( "Job of type '{$job->getType()}' has no ID." );
}
$scope = $this->getScopedNoTrxFlag( $dbw );
try {
// Delete a row with a single DELETE without holding row locks over RTTs...
- $dbw->delete( 'job',
- [ 'job_cmd' => $this->type, 'job_id' => $job->metadata['id'] ], __METHOD__ );
+ $dbw->delete(
+ 'job',
+ [ 'job_cmd' => $this->type, 'job_id' => $id ],
+ __METHOD__
+ );
JobQueue::incrStats( 'acks', $this->type );
} catch ( DBError $e ) {
Title::makeTitle( $row->job_namespace, $row->job_title ),
strlen( $row->job_params ) ? unserialize( $row->job_params ) : []
);
- $job->metadata['id'] = $row->job_id;
- $job->metadata['timestamp'] = $row->job_timestamp;
+ $job->setMetadata( 'id', $row->job_id );
+ $job->setMetadata( 'timestamp', $row->job_timestamp );
return $job;
}
$affected = $dbw->affectedRows();
$count += $affected;
JobQueue::incrStats( 'recycles', $this->type, $affected );
- $this->aggr->notifyQueueNonEmpty( $this->domain, $this->type );
}
}
$job = false;
}
if ( $job ) {
- $job->metadata['QueuePartition'] = $partition;
+ $job->setMetadata( 'QueuePartition', $partition );
return $job;
} else {
}
protected function doAck( Job $job ) {
- if ( !isset( $job->metadata['QueuePartition'] ) ) {
+ $partition = $job->getMetadata( 'QueuePartition' );
+ if ( $partition === null ) {
throw new MWException( "The given job has no defined partition name." );
}
- $this->partitionQueues[$job->metadata['QueuePartition']]->ack( $job );
+ $this->partitionQueues[$partition]->ack( $job );
}
protected function doIsRootJobOldDuplicate( Job $job ) {
} else {
$conf = $conf + $wgJobTypeConf['default'];
}
- $conf['aggregator'] = JobQueueAggregator::singleton();
if ( !isset( $conf['readOnlyReason'] ) ) {
$conf['readOnlyReason'] = $this->readOnlyReason;
}
/**
* Get the list of job types that have non-empty queues
*
- * @return array List of job types that have non-empty queues
+ * @return string[] List of job types that have non-empty queues
*/
public function getQueuesWithJobs() {
$types = [];
foreach ( $this->getCoalescedQueues() as $info ) {
- $nonEmpty = $info['queue']->getSiblingQueuesWithJobs( $this->getQueueTypes() );
+ /** @var JobQueue $queue */
+ $queue = $info['queue'];
+ $nonEmpty = $queue->getSiblingQueuesWithJobs( $this->getQueueTypes() );
if ( is_array( $nonEmpty ) ) { // batching features supported
$types = array_merge( $types, $nonEmpty );
} else { // we have to go through the queues in the bucket one-by-one
/**
* Get the size of the queus for a list of job types
*
- * @return array Map of (job type => size)
+ * @return int[] Map of (job type => size)
*/
public function getQueueSizes() {
$sizeMap = [];
foreach ( $this->getCoalescedQueues() as $info ) {
- $sizes = $info['queue']->getSiblingQueueSizes( $this->getQueueTypes() );
+ /** @var JobQueue $queue */
+ $queue = $info['queue'];
+ $sizes = $queue->getSiblingQueueSizes( $this->getQueueTypes() );
if ( is_array( $sizes ) ) { // batching features supported
$sizeMap = $sizeMap + $sizes;
} else { // we have to go through the queues in the bucket one-by-one
}
/**
- * @return array
+ * @return JobQueue[]
*/
protected function getCoalescedQueues() {
global $wgJobTypeConf;
$job = $this->jobFromSpecInternal( $spec );
end( $claimed );
- $job->metadata['claimId'] = key( $claimed );
+ $job->setMetadata( 'claimId', key( $claimed ) );
return $job;
}
}
$claimed =& $this->getQueueData( 'claimed' );
- unset( $claimed[$job->metadata['claimId']] );
+ $job->setMetadata( 'claimId', null );
}
/**
* @throws JobQueueError
*/
protected function doAck( Job $job ) {
- if ( !isset( $job->metadata['uuid'] ) ) {
+ $uuid = $job->getMetadata( 'uuid' );
+ if ( $uuid === null ) {
throw new UnexpectedValueException( "Job of type '{$job->getType()}' has no UUID." );
}
- $uuid = $job->metadata['uuid'];
$conn = $this->getConnection();
try {
static $script =
}
$title = Title::makeTitle( $item['namespace'], $item['title'] );
$job = Job::factory( $item['type'], $title, $item['params'] );
- $job->metadata['uuid'] = $item['uuid'];
- $job->metadata['timestamp'] = $item['timestamp'];
+ $job->setMetadata( 'uuid', $item['uuid'] );
+ $job->setMetadata( 'timestamp', $item['timestamp'] );
// Add in attempt count for debugging at showJobs.php
- $job->metadata['attempts'] = $conn->hGet( $this->getQueueKey( 'h-attempts' ), $uid );
+ $job->setMetadata( 'attempts',
+ $conn->hGet( $this->getQueueKey( 'h-attempts' ), $uid ) );
return $job;
} catch ( RedisException $e ) {
protected function getJobFromFields( array $fields ) {
$title = Title::makeTitle( $fields['namespace'], $fields['title'] );
$job = Job::factory( $fields['type'], $title, $fields['params'] );
- $job->metadata['uuid'] = $fields['uuid'];
- $job->metadata['timestamp'] = $fields['timestamp'];
+ $job->setMetadata( 'uuid', $fields['uuid'] );
+ $job->setMetadata( 'timestamp', $fields['timestamp'] );
return $job;
}
+++ /dev/null
-<?php
-/**
- * Job queue aggregator code.
- *
- * 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
- */
-
-/**
- * Class to handle tracking information about all queues
- *
- * @ingroup JobQueue
- * @since 1.21
- */
-abstract class JobQueueAggregator {
- /** @var JobQueueAggregator */
- protected static $instance = null;
-
- /**
- * @param array $params
- */
- public function __construct( array $params ) {
- }
-
- /**
- * @throws MWException
- * @return JobQueueAggregator
- */
- final public static function singleton() {
- global $wgJobQueueAggregator;
-
- if ( !isset( self::$instance ) ) {
- $class = $wgJobQueueAggregator['class'];
- $obj = new $class( $wgJobQueueAggregator );
- if ( !( $obj instanceof JobQueueAggregator ) ) {
- throw new MWException( "Class '$class' is not a JobQueueAggregator class." );
- }
- self::$instance = $obj;
- }
-
- return self::$instance;
- }
-
- /**
- * Destroy the singleton instance
- *
- * @return void
- */
- final public static function destroySingleton() {
- self::$instance = null;
- }
-
- /**
- * Mark a queue as being empty
- *
- * @param string $wiki
- * @param string $type
- * @return bool Success
- */
- final public function notifyQueueEmpty( $wiki, $type ) {
- $ok = $this->doNotifyQueueEmpty( $wiki, $type );
-
- return $ok;
- }
-
- /**
- * @see JobQueueAggregator::notifyQueueEmpty()
- * @param string $wiki
- * @param string $type
- * @return bool
- */
- abstract protected function doNotifyQueueEmpty( $wiki, $type );
-
- /**
- * Mark a queue as being non-empty
- *
- * @param string $wiki
- * @param string $type
- * @return bool Success
- */
- final public function notifyQueueNonEmpty( $wiki, $type ) {
- $ok = $this->doNotifyQueueNonEmpty( $wiki, $type );
-
- return $ok;
- }
-
- /**
- * @see JobQueueAggregator::notifyQueueNonEmpty()
- * @param string $wiki
- * @param string $type
- * @return bool
- */
- abstract protected function doNotifyQueueNonEmpty( $wiki, $type );
-
- /**
- * Get the list of all of the queues with jobs
- *
- * @return array (job type => (list of wiki IDs))
- */
- final public function getAllReadyWikiQueues() {
- $res = $this->doGetAllReadyWikiQueues();
-
- return $res;
- }
-
- /**
- * @see JobQueueAggregator::getAllReadyWikiQueues()
- */
- abstract protected function doGetAllReadyWikiQueues();
-
- /**
- * Purge all of the aggregator information
- *
- * @return bool Success
- */
- final public function purge() {
- $res = $this->doPurge();
-
- return $res;
- }
-
- /**
- * @see JobQueueAggregator::purge()
- */
- abstract protected function doPurge();
-
- /**
- * Get all databases that have a pending job.
- * This poll all the queues and is this expensive.
- *
- * @return array (job type => (list of wiki IDs))
- */
- protected function findPendingWikiQueues() {
- global $wgLocalDatabases;
-
- $pendingDBs = []; // (job type => (db list))
- foreach ( $wgLocalDatabases as $wikiId ) {
- foreach ( JobQueueGroup::singleton( $wikiId )->getQueuesWithJobs() as $type ) {
- $pendingDBs[$type][] = $wikiId;
- }
- }
-
- return $pendingDBs;
- }
-}
+++ /dev/null
-<?php
-/**
- * Job queue aggregator code.
- *
- * 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 JobQueue
- */
-class JobQueueAggregatorNull extends JobQueueAggregator {
- protected function doNotifyQueueEmpty( $wiki, $type ) {
- return true;
- }
-
- protected function doNotifyQueueNonEmpty( $wiki, $type ) {
- return true;
- }
-
- protected function doGetAllReadyWikiQueues() {
- return [];
- }
-
- protected function doPurge() {
- return true;
- }
-}
+++ /dev/null
-<?php
-/**
- * Job queue aggregator code that uses PhpRedis.
- *
- * 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
- */
-use Psr\Log\LoggerInterface;
-
-/**
- * Class to handle tracking information about all queues using PhpRedis
- *
- * The mediawiki/services/jobrunner background service must be set up and running.
- *
- * @ingroup JobQueue
- * @ingroup Redis
- * @since 1.21
- */
-class JobQueueAggregatorRedis extends JobQueueAggregator {
- /** @var RedisConnectionPool */
- protected $redisPool;
- /** @var LoggerInterface */
- protected $logger;
- /** @var array List of Redis server addresses */
- protected $servers;
-
- /**
- * @param array $params Possible keys:
- * - redisConfig : An array of parameters to RedisConnectionPool::__construct().
- * - redisServers : Array of server entries, the first being the primary and the
- * others being fallback servers. Each entry is either a hostname/port
- * combination or the absolute path of a UNIX socket.
- * If a hostname is specified but no port, the standard port number
- * 6379 will be used. Required.
- */
- public function __construct( array $params ) {
- parent::__construct( $params );
- $this->servers = $params['redisServers'] ?? [ $params['redisServer'] ]; // b/c
- $params['redisConfig']['serializer'] = 'none';
- $this->redisPool = RedisConnectionPool::singleton( $params['redisConfig'] );
- $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
- }
-
- protected function doNotifyQueueEmpty( $wiki, $type ) {
- return true; // managed by the service
- }
-
- protected function doNotifyQueueNonEmpty( $wiki, $type ) {
- return true; // managed by the service
- }
-
- protected function doGetAllReadyWikiQueues() {
- $conn = $this->getConnection();
- if ( !$conn ) {
- return [];
- }
- try {
- $map = $conn->hGetAll( $this->getReadyQueueKey() );
-
- if ( is_array( $map ) && isset( $map['_epoch'] ) ) {
- unset( $map['_epoch'] ); // ignore
- $pendingDBs = []; // (type => list of wikis)
- foreach ( $map as $key => $time ) {
- list( $type, $wiki ) = $this->decodeQueueName( $key );
- $pendingDBs[$type][] = $wiki;
- }
- } else {
- throw new UnexpectedValueException(
- "No queue listing found; make sure redisJobChronService is running."
- );
- }
-
- return $pendingDBs;
- } catch ( RedisException $e ) {
- $this->redisPool->handleError( $conn, $e );
-
- return [];
- }
- }
-
- protected function doPurge() {
- return true; // fully and only refreshed by the service
- }
-
- /**
- * Get a connection to the server that handles all sub-queues for this queue
- *
- * @return RedisConnRef|bool Returns false on failure
- * @throws MWException
- */
- protected function getConnection() {
- $conn = false;
- foreach ( $this->servers as $server ) {
- $conn = $this->redisPool->getConnection( $server, $this->logger );
- if ( $conn ) {
- break;
- }
- }
-
- return $conn;
- }
-
- /**
- * @return string
- */
- private function getReadyQueueKey() {
- return "jobqueue:aggregator:h-ready-queues:v2"; // global
- }
-
- /**
- * @param string $name
- * @return string[]
- */
- private function decodeQueueName( $name ) {
- list( $type, $wiki ) = explode( '/', $name, 2 );
-
- return [ rawurldecode( $type ), rawurldecode( $wiki ) ];
- }
-}
$s .= $loglist->beginLogEventsList() .
$logBody .
$loglist->endLogEventsList();
+ // add styles for change tags
+ $context->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' );
} elseif ( $showIfEmpty ) {
$s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ],
$context->msg( 'logempty' )->parse() );
$user->getName(),
true, // redContribsWhenNoEdits
$toolFlags,
- $user->getEditCount()
+ $user->getEditCount(),
+ // do not render parenthesises in the HTML markup (CSS will provide)
+ false
);
}
}
foreach ( $this->getParametersForApi() as $key => $value ) {
$vals = explode( ':', $key, 3 );
if ( count( $vals ) !== 3 ) {
+ if ( $value instanceof __PHP_Incomplete_Class ) {
+ wfLogWarning( 'Log entry of type ' . $this->entry->getFullType() .
+ ' contains unrecoverable extra parameters.' );
+ continue;
+ }
$logParams[$key] = $value;
continue;
}
$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
+ // In miser mode, disable words that always cause double-parses on page save (T137900)
+ static $slowRevWords = [ 'revisionid' => true ]; // @TODO: 'revisiontimestamp'
+ if (
+ isset( $slowRevWords[$index] ) &&
+ $this->siteConfig->get( 'MiserMode' ) &&
+ !$this->mOptions->getInterfaceMessage() &&
+ // @TODO: disallow this word on all namespaces
+ MWNamespace::isContent( $this->mTitle->getNamespace() )
+ ) {
+ return $this->mRevisionId ? '-' : '';
+ };
+
$pageLang = $this->getFunctionLang();
switch ( $index ) {
$miserMaxResults = $this->getConfig()->get( 'MiserMode' )
&& ( $this->offset + $this->limit >= $this->getMaxResults() );
$atEnd = ( $this->numRows <= $this->limit ) || $miserMaxResults;
- $paging = $this->getLanguage()->viewPrevNext( $this->getPageTitle( $par ), $this->offset,
- $this->limit, $this->linkParameters(), $atEnd );
+ $paging = $this->buildPrevNextNavigation( $this->offset,
+ $this->limit, $this->linkParameters(), $atEnd, $par );
$out->addHTML( '<p>' . $paging . '</p>' );
} else {
# No results to show, so don't bother with "showing X of Y" etc.
public function setLinkRenderer( LinkRenderer $linkRenderer ) {
$this->linkRenderer = $linkRenderer;
}
+
+ /**
+ * Generate (prev x| next x) (20|50|100...) type links for paging
+ *
+ * @param int $offset
+ * @param int $limit
+ * @param array $query Optional URL query parameter string
+ * @param bool $atend Optional param for specified if this is the last page
+ * @param string|bool $subpage Optional param for specifying subpage
+ * @return string
+ */
+ protected function buildPrevNextNavigation( $offset, $limit,
+ array $query = [], $atend = false, $subpage = false
+ ) {
+ $lang = $this->getLanguage();
+
+ # Make 'previous' link
+ $prev = $this->msg( 'prevn' )->numParams( $limit )->text();
+ if ( $offset > 0 ) {
+ $plink = $this->numLink( max( $offset - $limit, 0 ), $limit, $query,
+ $prev, 'prevn-title', 'mw-prevlink', $subpage );
+ } else {
+ $plink = htmlspecialchars( $prev );
+ }
+
+ # Make 'next' link
+ $next = $this->msg( 'nextn' )->numParams( $limit )->text();
+ if ( $atend ) {
+ $nlink = htmlspecialchars( $next );
+ } else {
+ $nlink = $this->numLink( $offset + $limit, $limit,
+ $query, $next, 'nextn-title', 'mw-nextlink', $subpage );
+ }
+
+ # Make links to set number of items per page
+ $numLinks = [];
+ foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
+ $numLinks[] = $this->numLink( $offset, $num, $query,
+ $lang->formatNum( $num ), 'shown-title', 'mw-numlink', $subpage );
+ }
+
+ return $this->msg( 'viewprevnext' )->rawParams( $plink, $nlink, $lang->pipeList( $numLinks ) )->
+ escaped();
+ }
+
+ /**
+ * Helper function for buildPrevNextNavigation() that generates links
+ *
+ * @param int $offset
+ * @param int $limit
+ * @param array $query Extra query parameters
+ * @param string $link Text to use for the link; will be escaped
+ * @param string $tooltipMsg Name of the message to use as tooltip
+ * @param string $class Value of the "class" attribute of the link
+ * @param string|bool $subpage Optional param for specifying subpage
+ * @return string HTML fragment
+ */
+ private function numLink( $offset, $limit, array $query, $link,
+ $tooltipMsg, $class, $subpage = false
+ ) {
+ $query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
+ $tooltip = $this->msg( $tooltipMsg )->numParams( $limit )->text();
+ $href = $this->getPageTitle( $subpage )->getLocalURL( $query );
+ return Html::element( 'a', [ 'href' => $href,
+ 'title' => $tooltip, 'class' => $class ], $link );
+ }
}
$offset = $this->offset;
}
- $prevnext = $this->getLanguage()->viewPrevNext(
- $this->getPageTitle(),
+ $prevnext = $this->buildPrevNextNavigation(
$offset,
$this->limit,
$this->powerSearchOptions() + [ 'search' => $term ],
<td class="mw-changeslist-line-inner">
{{# rev-deleted-event }}<span class="history-deleted">{{{ . }}}</span>{{/ rev-deleted-event }}
{{{ articleLink }}}{{{ languageDirMark }}}{{{ logText }}}
- <span class="mw-changeslist-separator">. .</span>
- {{# charDifference }}{{{ . }}} <span class="mw-changeslist-separator">. .</span>{{/ charDifference }}
+ <span class="mw-changeslist-separator"></span>
+ {{# charDifference }}{{{ . }}} <span class="mw-changeslist-separator"></span>{{/ charDifference }}
<span class="changedby">{{{ users }}}</span>
{{ numberofWatchingusers }}
</td>
* @return string
*/
private function truncateInternal(
- $string, $length, $ellipsis, $adjustLength, $measureLength, $getSubstring
+ $string, $length, $ellipsis, $adjustLength, callable $measureLength, callable $getSubstring
) {
- if ( !is_callable( $measureLength ) || !is_callable( $getSubstring ) ) {
- throw new InvalidArgumentException( 'Invalid callback provided' );
- }
-
# Check if there is no need to truncate
if ( $measureLength( $string ) <= abs( $length ) ) {
return $string; // no need to truncate
* @param array $query Optional URL query parameter string
* @param bool $atend Optional param for specified if this is the last page
* @return string
+ * @deprecated since 1.33, use SpecialPage::viewPrevNext()
+ * instead.
*/
public function viewPrevNext( Title $title, $offset, $limit,
array $query = [], $atend = false
[ 'LIMIT' => 1 ]
);
if ( $anyBad ) {
- $this->output( "... Deleting bogus rows due to T21552\n" );
+ $this->output( "... Deleting bogus rows due to T215525\n" );
$dbw->delete(
'log_search',
[ 'ls_field' => 'target_author_actor', 'ls_value' => '' ],
__METHOD__
);
$ct = $dbw->affectedRows();
- $this->output( "... Deleted $ct bogus row(s) from T21552\n" );
+ $this->output( "... Deleted $ct bogus row(s) from T215525\n" );
wfWaitForSlaves();
}
'ORDER BY' => $primaryKey,
'LIMIT' => $this->mBatchSize,
],
- [ 'actor' => [ 'LEFT JOIN', 'ls_value = ' . $dbw->buildStringCast( 'actor_user' ) ] ]
+ [ 'actor' => [ 'LEFT JOIN', 'actor_user = ' . $dbw->buildIntegerCast( 'ls_value' ) ] ]
);
if ( !$res->numRows() ) {
break;
*/
protected function pageableSortedRows( ResultWrapper $res, $column, $limit ) {
$rows = iterator_to_array( $res, false );
- $count = count( $rows );
- if ( !$count ) {
- return [ [], null ]; // nothing to do
- } elseif ( $count < $limit ) {
- return [ $rows, $rows[$count - 1]->$column ]; // no more rows left
+
+ // Nothing to do
+ if ( !$rows ) {
+ return [ [], null ];
+ }
+
+ $lastValue = end( $rows )->$column;
+ if ( count( $rows ) < $limit ) {
+ return [ $rows, $lastValue ];
}
- $lastValue = $rows[$count - 1]->$column; // should be the highest
- for ( $i = $count - 1; $i >= 0; --$i ) {
- if ( $rows[$i]->$column === $lastValue ) {
- unset( $rows[$i] );
- } else {
+
+ for ( $i = count( $rows ) - 1; $i >= 0; --$i ) {
+ if ( $rows[$i]->$column !== $lastValue ) {
break;
}
+
+ unset( $rows[$i] );
+ }
+
+ // No more rows left
+ if ( !$rows ) {
+ return [ [], null ];
}
- $lastValueLeft = count( $rows ) ? $rows[count( $rows ) - 1]->$column : null;
- return [ $rows, $lastValueLeft ];
+ return [ $rows, end( $rows )->$column ];
}
}
background-origin: border-box;
background-position: center center;
background-repeat: no-repeat;
- .background-size( 0, 0 );
+ background-size: 0 0;
.box-sizing( border-box );
position: absolute;
// Ensure alignment of checkbox to middle of the text in long labels, see T85241
// Apply a checkmark on the pseudo `:before` element when the input is checked
&:checked + label:before {
.background-image-svg( 'images/checkbox-checked.svg', 'images/checkbox-checked.png' );
- .background-size( 90%, 90% );
+ background-size: 90% 90%;
}
&:enabled {
protected function assertMoved( $from, $to, $id, $opts = null ) {
$opts = (array)$opts;
+ Title::clearCaches();
$fromTitle = Title::newFromText( $from );
$toTitle = Title::newFromText( $to );
$html = $this->createCategorizationLine(
$this->getCategorizationChange( '20150629191735', 0, 0 )
);
- $this->assertNotContains( '(diff | hist)', strip_tags( $html ) );
+ $this->assertNotContains( 'diffhist', strip_tags( $html ) );
}
public function testCategorizationLineFormattingWithRevision() {
$html = $this->createCategorizationLine(
$this->getCategorizationChange( '20150629191735', 1025, 1024 )
);
- $this->assertContains( '(diff | hist)', strip_tags( $html ) );
+ $this->assertContains( 'diffhist', strip_tags( $html ) );
}
/**
$this->assertValidHTML( $cacheEntry->usertalklink );
$this->assertRegExp(
- '#^ <span class="mw-usertoollinks">\(.*<a .+>talk</a>.*\)</span>#',
+ '#^ <span class="mw-usertoollinks mw-changeslist-links">.*<span><a .+>talk</a></span>.*</span>#',
$cacheEntry->usertalklink,
'verify user talk link'
);
$this->assertValidHTML( $cacheEntry->usertalklink );
$this->assertRegExp(
- '#^ <span class="mw-usertoollinks">\(.*<a .+>contribs</a>.*\)</span>$#',
+ '#^ <span class="mw-usertoollinks mw-changeslist-links">.*<span><a .+>' .
+ 'contribs</a></span>.*</span>$#',
$cacheEntry->usertalklink,
'verify user tool links'
);
<?php
+use Wikimedia\TestingAccessWrapper;
+
/**
* @covers SpecialPage
*
$this->assertTrue( true );
}
+ public function provideBuildPrevNextNavigation() {
+ yield [ 0, 20, false, false ];
+ yield [ 17, 20, false, false ];
+ yield [ 0, 17, false, false ];
+ yield [ 0, 20, true, 'Foo' ];
+ yield [ 17, 20, true, 'Föö_Bär' ];
+ }
+
+ /**
+ * @dataProvider provideBuildPrevNextNavigation
+ */
+ public function testBuildPrevNextNavigation( $offset, $limit, $atEnd, $subPage ) {
+ $this->setUserLang( Language::factory( 'qqx' ) ); // disable i18n
+
+ $specialPage = new SpecialPage( 'Watchlist' );
+ $specialPage = TestingAccessWrapper::newFromObject( $specialPage );
+
+ $html = $specialPage->buildPrevNextNavigation(
+ $offset,
+ $limit,
+ [ 'x' => 25 ],
+ $atEnd,
+ $subPage
+ );
+
+ $this->assertStringStartsWith( '(viewprevnext:', $html );
+
+ preg_match_all( '!<a.*?</a>!', $html, $m, PREG_PATTERN_ORDER );
+ $links = $m[0];
+
+ foreach ( $links as $a ) {
+ if ( $subPage ) {
+ $this->assertContains( 'Special:Watchlist/' . wfUrlencode( $subPage ), $a );
+ } else {
+ $this->assertContains( 'Special:Watchlist', $a );
+ $this->assertNotContains( 'Special:Watchlist/', $a );
+ }
+ $this->assertContains( 'x=25', $a );
+ }
+
+ $i = 0;
+
+ if ( $offset > 0 ) {
+ $this->assertContains(
+ 'limit=' . $limit . '&offset=' . max( 0, $offset - $limit ) . '&',
+ $links[ $i ]
+ );
+ $this->assertContains( 'title="(prevn-title: ' . $limit . ')"', $links[$i] );
+ $this->assertContains( 'class="mw-prevlink"', $links[$i] );
+ $this->assertContains( '>(prevn: ' . $limit . ')<', $links[$i] );
+ $i += 1;
+ }
+
+ if ( !$atEnd ) {
+ $this->assertContains(
+ 'limit=' . $limit . '&offset=' . ( $offset + $limit ) . '&',
+ $links[ $i ]
+ );
+ $this->assertContains( 'title="(nextn-title: ' . $limit . ')"', $links[$i] );
+ $this->assertContains( 'class="mw-nextlink"', $links[$i] );
+ $this->assertContains( '>(nextn: ' . $limit . ')<', $links[$i] );
+ $i += 1;
+ }
+
+ $this->assertCount( 5 + $i, $links );
+
+ $this->assertContains( 'limit=20&offset=' . $offset, $links[$i] );
+ $this->assertContains( 'title="(shown-title: 20)"', $links[$i] );
+ $this->assertContains( 'class="mw-numlink"', $links[$i] );
+ $this->assertContains( '>20<', $links[$i] );
+ $i += 4;
+
+ $this->assertContains( 'limit=500&offset=' . $offset, $links[$i] );
+ $this->assertContains( 'title="(shown-title: 500)"', $links[$i] );
+ $this->assertContains( 'class="mw-numlink"', $links[$i] );
+ $this->assertContains( '>500<', $links[$i] );
+ }
+
}
'globalBlock' => null,
'isAllowed' => true,
],
+ 'blocked with an unknown system block type' => [
+ 'passwordResetRoutes' => [ 'username' => true ],
+ 'enableEmail' => true,
+ 'allowsAuthenticationDataChange' => true,
+ 'canEditPrivate' => true,
+ 'block' => new Block( [ 'systemBlock' => 'unknown' ] ),
+ 'globalBlock' => null,
+ 'isAllowed' => false,
+ ],
'all OK' => [
'passwordResetRoutes' => [ 'username' => true ],
'enableEmail' => true,