Merge "protect.js: Simplify by removing object ProtectionForm"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 3 Apr 2019 20:26:15 +0000 (20:26 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 3 Apr 2019 20:26:15 +0000 (20:26 +0000)
38 files changed:
.fresnel.yml
RELEASE-NOTES-1.33
autoload.php
includes/Block.php
includes/DefaultSettings.php
includes/Linker.php
includes/Revision/RevisionStore.php
includes/ServiceWiring.php
includes/changes/ChangesList.php
includes/changes/EnhancedChangesList.php
includes/changes/RCCacheEntryFactory.php
includes/installer/SqliteInstaller.php
includes/jobqueue/Job.php
includes/jobqueue/JobQueue.php
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobQueueFederated.php
includes/jobqueue/JobQueueGroup.php
includes/jobqueue/JobQueueMemory.php
includes/jobqueue/JobQueueRedis.php
includes/jobqueue/aggregator/JobQueueAggregator.php [deleted file]
includes/jobqueue/aggregator/JobQueueAggregatorNull.php [deleted file]
includes/jobqueue/aggregator/JobQueueAggregatorRedis.php [deleted file]
includes/logging/LogEventsList.php
includes/logging/LogFormatter.php
includes/parser/Parser.php
includes/specialpage/QueryPage.php
includes/specialpage/SpecialPage.php
includes/specials/SpecialSearch.php
includes/templates/EnhancedChangesListGroup.mustache
languages/Language.php
maintenance/includes/MigrateActors.php
maintenance/purgeChangedPages.php
resources/src/mediawiki.ui/components/checkbox.less
tests/phpunit/includes/api/ApiMoveTest.php
tests/phpunit/includes/changes/EnhancedChangesListTest.php
tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php
tests/phpunit/includes/specialpage/SpecialPageTest.php
tests/phpunit/includes/user/PasswordResetTest.php

index e694294..2f71e4b 100644 (file)
@@ -3,8 +3,8 @@ runs: 5
 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:
index 7513334..f455b95 100644 (file)
@@ -408,6 +408,8 @@ because of Phabricator reports.
   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.
index 95dd2ae..bb1b3b2 100644 (file)
@@ -705,9 +705,6 @@ $wgAutoloadLocalClasses = [
        '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',
index 58ef448..c6b9482 100644 (file)
@@ -2146,7 +2146,7 @@ class Block {
         * 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() ) {
@@ -2159,7 +2159,7 @@ class Block {
                        case 'wgSoftBlockRanges':
                                return false;
                        default:
-                               return false;
+                               return true;
                }
        }
 
index f16b8cb..cedba70 100644 (file)
@@ -7571,14 +7571,6 @@ $wgJobTypeConf = [
        '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.
index 17dc037..4f0ab6a 100644 (file)
@@ -1001,7 +1001,7 @@ class Linker {
         * @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 );
        }
 
        /**
index 7dd4eea..9af2458 100644 (file)
@@ -1508,9 +1508,11 @@ class RevisionStore
         * @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.
@@ -1519,7 +1521,7 @@ class RevisionStore
                        // 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
@@ -1529,7 +1531,7 @@ class RevisionStore
                        $db = $this->getDBConnectionRefForQueryFlags( $flags );
 
                        $conds[] = 'rev_id=page_latest';
-                       $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
+                       $rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
 
                        return $rev;
                }
index b1cdc81..e121898 100644 (file)
@@ -203,12 +203,12 @@ return [
        },
 
        '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()
+                       );
                }
        },
 
index 2389997..184a2c1 100644 (file)
@@ -617,7 +617,13 @@ class ChangesList extends ContextSource {
                        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 );
                }
        }
 
index 3e98f65..8186059 100644 (file)
@@ -393,7 +393,7 @@ class EnhancedChangesList extends ChangesList {
                }
                $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,
@@ -556,19 +556,22 @@ class EnhancedChangesList extends ChangesList {
                                $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' ],
@@ -576,7 +579,8 @@ class EnhancedChangesList extends ChangesList {
                                                                'diff' => $currentRevision,
                                                                'oldid' => $unvisitedOldid,
                                                        ]
-                                               );
+                                               )
+                                       );
                                }
                        }
                }
@@ -585,17 +589,19 @@ class EnhancedChangesList extends ChangesList {
                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
@@ -606,8 +612,8 @@ class EnhancedChangesList extends ChangesList {
                        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;
        }
 
@@ -653,10 +659,9 @@ class EnhancedChangesList extends ChangesList {
                        $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 );
                }
@@ -664,16 +669,16 @@ class EnhancedChangesList extends ChangesList {
                # 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> ';
                        }
                }
 
@@ -686,7 +691,7 @@ class EnhancedChangesList extends ChangesList {
                        $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 );
                }
@@ -744,7 +749,11 @@ class EnhancedChangesList extends ChangesList {
                ] );
 
                // 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";
 
@@ -759,9 +768,10 @@ class EnhancedChangesList extends ChangesList {
         *
         * @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!
@@ -773,15 +783,23 @@ class EnhancedChangesList extends ChangesList {
                        }
                }
 
-               $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;
        }
 
        /**
index e8c3a99..2d60ca2 100644 (file)
@@ -82,7 +82,15 @@ class RCCacheEntryFactory {
                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
                        );
                }
 
index a8abba9..fa0d2a5 100644 (file)
@@ -231,6 +231,7 @@ class SqliteInstaller extends DatabaseInstaller {
                $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;
                }
@@ -283,6 +284,39 @@ EOT;
                        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();
        }
@@ -340,7 +374,9 @@ EOT;
         */
        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] = [
@@ -350,7 +386,9 @@ EOT;
                'type' => 'sqlite',
                'dbname' => 'wikicache',
                'tablePrefix' => '',
+               'variables' => [ 'synchronous' => 'NORMAL' ],
                'dbDirectory' => \$wgSQLiteDataDir,
+               'trxMode' => 'IMMEDIATE',
                'flags' => 0
        ]
 ];
@@ -358,8 +396,22 @@ EOT;
        '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
+       ]
 ];";
        }
 }
index 24fc473..22ff446 100644 (file)
@@ -156,6 +156,36 @@ abstract class Job implements IJobSpecification {
                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
index cb5cd82..8cfed3b 100644 (file)
@@ -29,7 +29,7 @@ use MediaWiki\MediaWikiServices;
  * @since 1.21
  */
 abstract class JobQueue {
-       /** @var string Wiki ID */
+       /** @var string DB domain ID */
        protected $domain;
        /** @var string Job type */
        protected $type;
@@ -44,8 +44,6 @@ abstract class JobQueue {
 
        /** @var BagOStuff */
        protected $dupCache;
-       /** @var JobQueueAggregator */
-       protected $aggr;
 
        const QOS_ATOMIC = 1; // integer; "all-or-nothing" job insertions
 
@@ -53,7 +51,7 @@ abstract class JobQueue {
 
        /**
         * @param array $params
-        * @throws MWException
+        * @throws JobQueueError
         */
        protected function __construct( array $params ) {
                $this->domain = $params['domain'] ?? $params['wiki']; // b/c
@@ -66,10 +64,9 @@ abstract class JobQueue {
                        $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;
        }
 
@@ -99,16 +96,16 @@ abstract class JobQueue {
         *
         * @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;
@@ -318,7 +315,7 @@ abstract class JobQueue {
         * @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();
@@ -329,16 +326,15 @@ abstract class JobQueue {
 
                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() ) {
@@ -359,7 +355,7 @@ abstract class JobQueue {
         * 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() {
@@ -367,19 +363,15 @@ abstract class JobQueue {
 
                $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 ) ) {
@@ -407,12 +399,12 @@ abstract class JobQueue {
         *
         * @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 );
@@ -452,13 +444,13 @@ abstract class JobQueue {
         * 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 );
@@ -467,12 +459,12 @@ abstract class JobQueue {
        /**
         * @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();
 
@@ -495,12 +487,12 @@ abstract class JobQueue {
         * 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 );
 
@@ -555,10 +547,10 @@ abstract class JobQueue {
 
        /**
         * @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." );
        }
 
        /**
@@ -659,7 +651,7 @@ abstract class JobQueue {
         *
         * @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 ) {
@@ -682,7 +674,7 @@ abstract class JobQueue {
         *
         * @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 ) {
index 74a6559..7aecfe9 100644 (file)
@@ -317,8 +317,8 @@ class JobQueueDB extends JobQueue {
                                $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 );
 
@@ -484,7 +484,8 @@ class JobQueueDB extends JobQueue {
         * @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." );
                }
 
@@ -493,8 +494,11 @@ class JobQueueDB extends JobQueue {
                $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 ) {
@@ -617,8 +621,8 @@ class JobQueueDB extends JobQueue {
                                                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;
                                }
@@ -724,7 +728,6 @@ class JobQueueDB extends JobQueue {
                                        $affected = $dbw->affectedRows();
                                        $count += $affected;
                                        JobQueue::incrStats( 'recycles', $this->type, $affected );
-                                       $this->aggr->notifyQueueNonEmpty( $this->domain, $this->type );
                                }
                        }
 
index 2025bf7..30ab7e7 100644 (file)
@@ -287,7 +287,7 @@ class JobQueueFederated extends JobQueue {
                                $job = false;
                        }
                        if ( $job ) {
-                               $job->metadata['QueuePartition'] = $partition;
+                               $job->setMetadata( 'QueuePartition', $partition );
 
                                return $job;
                        } else {
@@ -300,11 +300,12 @@ class JobQueueFederated extends JobQueue {
        }
 
        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 ) {
index 1311149..b9c4157 100644 (file)
@@ -114,7 +114,6 @@ class JobQueueGroup {
                } else {
                        $conf = $conf + $wgJobTypeConf['default'];
                }
-               $conf['aggregator'] = JobQueueAggregator::singleton();
                if ( !isset( $conf['readOnlyReason'] ) ) {
                        $conf['readOnlyReason'] = $this->readOnlyReason;
                }
@@ -353,12 +352,14 @@ class JobQueueGroup {
        /**
         * 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
@@ -376,12 +377,14 @@ class JobQueueGroup {
        /**
         * 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
@@ -395,7 +398,7 @@ class JobQueueGroup {
        }
 
        /**
-        * @return array
+        * @return JobQueue[]
         */
        protected function getCoalescedQueues() {
                global $wgJobTypeConf;
index 9b1fbdf..6c45e96 100644 (file)
@@ -132,7 +132,7 @@ class JobQueueMemory extends JobQueue {
                $job = $this->jobFromSpecInternal( $spec );
 
                end( $claimed );
-               $job->metadata['claimId'] = key( $claimed );
+               $job->setMetadata( 'claimId', key( $claimed ) );
 
                return $job;
        }
@@ -148,7 +148,7 @@ class JobQueueMemory extends JobQueue {
                }
 
                $claimed =& $this->getQueueData( 'claimed' );
-               unset( $claimed[$job->metadata['claimId']] );
+               $job->setMetadata( 'claimId', null );
        }
 
        /**
index 5e7a115..4d07a09 100644 (file)
@@ -385,11 +385,11 @@ LUA;
         * @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 =
@@ -643,10 +643,11 @@ LUA;
                        }
                        $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 ) {
@@ -704,8 +705,8 @@ LUA;
        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;
        }
diff --git a/includes/jobqueue/aggregator/JobQueueAggregator.php b/includes/jobqueue/aggregator/JobQueueAggregator.php
deleted file mode 100644 (file)
index b44fc45..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-<?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;
-       }
-}
diff --git a/includes/jobqueue/aggregator/JobQueueAggregatorNull.php b/includes/jobqueue/aggregator/JobQueueAggregatorNull.php
deleted file mode 100644 (file)
index c44d70e..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<?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;
-       }
-}
diff --git a/includes/jobqueue/aggregator/JobQueueAggregatorRedis.php b/includes/jobqueue/aggregator/JobQueueAggregatorRedis.php
deleted file mode 100644 (file)
index 7d0e1e6..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-<?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 ) ];
-       }
-}
index c66aa59..3fd52af 100644 (file)
@@ -692,6 +692,8 @@ class LogEventsList extends ContextSource {
                        $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() );
index 6d45ed5..b9bb70c 100644 (file)
@@ -762,7 +762,9 @@ class LogFormatter {
                                        $user->getName(),
                                        true, // redContribsWhenNoEdits
                                        $toolFlags,
-                                       $user->getEditCount()
+                                       $user->getEditCount(),
+                                       // do not render parenthesises in the HTML markup (CSS will provide)
+                                       false
                                );
                        }
                }
@@ -815,6 +817,11 @@ class LogFormatter {
                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;
                        }
index 0440e89..a1a784b 100644 (file)
@@ -2587,6 +2587,18 @@ class Parser {
                $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 ) {
index f0cb7e5..46873b1 100644 (file)
@@ -658,8 +658,8 @@ abstract class QueryPage extends SpecialPage {
                                $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.
index a9bbb8a..bd0e24f 100644 (file)
@@ -920,4 +920,70 @@ class SpecialPage implements MessageLocalizer {
        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 );
+       }
 }
index b60a2ad..a520396 100644 (file)
@@ -471,8 +471,7 @@ class SpecialSearch extends SpecialPage {
                                $offset = $this->offset;
                        }
 
-                       $prevnext = $this->getLanguage()->viewPrevNext(
-                               $this->getPageTitle(),
+                       $prevnext = $this->buildPrevNextNavigation(
                                $offset,
                                $this->limit,
                                $this->powerSearchOptions() + [ 'search' => $term ],
index 6d9d6b0..5a5986f 100644 (file)
@@ -16,8 +16,8 @@
                <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>
index 2cdcea9..f72ac1a 100644 (file)
@@ -3556,12 +3556,8 @@ class Language {
         * @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
@@ -4854,6 +4850,8 @@ class Language {
         * @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
index d9c2072..aff6758 100644 (file)
@@ -478,14 +478,14 @@ class MigrateActors extends LoggedUpdateMaintenance {
                        [ '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();
                }
 
@@ -504,7 +504,7 @@ class MigrateActors extends LoggedUpdateMaintenance {
                                        '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;
index 0ef2a99..81fb5e3 100644 (file)
@@ -170,23 +170,31 @@ class PurgeChangedPages extends Maintenance {
         */
        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 ];
        }
 }
 
index 08612d0..92b4b8a 100644 (file)
@@ -74,7 +74,7 @@
                        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
@@ -90,7 +90,7 @@
                // 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 {
index b9c49b1..d437a52 100644 (file)
@@ -17,6 +17,7 @@ class ApiMoveTest extends ApiTestCase {
        protected function assertMoved( $from, $to, $id, $opts = null ) {
                $opts = (array)$opts;
 
+               Title::clearCaches();
                $fromTitle = Title::newFromText( $from );
                $toTitle = Title::newFromText( $to );
 
index eff2c85..1511d46 100644 (file)
@@ -124,14 +124,14 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase {
                $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 ) );
        }
 
        /**
index b1857cc..8f914b7 100644 (file)
@@ -156,14 +156,15 @@ class RCCacheEntryFactoryTest extends MediaWikiLangTestCase {
 
                $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'
                );
index 2eddb01..ec4bf0f 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use Wikimedia\TestingAccessWrapper;
+
 /**
  * @covers SpecialPage
  *
@@ -101,4 +103,82 @@ class SpecialPageTest extends MediaWikiTestCase {
                $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 . '&amp;offset=' . max( 0, $offset - $limit ) . '&amp;',
+                               $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 . '&amp;offset=' . ( $offset + $limit ) . '&amp;',
+                               $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&amp;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&amp;offset=' . $offset, $links[$i] );
+               $this->assertContains( 'title="(shown-title: 500)"', $links[$i] );
+               $this->assertContains( 'class="mw-numlink"', $links[$i] );
+               $this->assertContains( '>500<', $links[$i] );
+       }
+
 }
index 4978b72..e8334d6 100644 (file)
@@ -133,6 +133,15 @@ class PasswordResetTest extends MediaWikiTestCase {
                                '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,