Merge "Make Job::hasExecutionFlag() actually work"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 29 Mar 2019 22:54:58 +0000 (22:54 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 29 Mar 2019 22:54:58 +0000 (22:54 +0000)
58 files changed:
RELEASE-NOTES-1.33
docs/hooks.txt
includes/EditPage.php
includes/MediaWiki.php
includes/ServiceWiring.php
includes/Storage/DerivedPageDataUpdater.php
includes/Storage/NameTableStore.php
includes/WikiMap.php
includes/actions/HistoryAction.php
includes/api/ApiMain.php
includes/deferred/DeferredUpdates.php
includes/filerepo/file/LocalFile.php
includes/htmlform/HTMLForm.php
includes/htmlform/OOUIHTMLForm.php
includes/htmlform/fields/HTMLCheckField.php
includes/htmlform/fields/HTMLNamespacesMultiselectField.php
includes/jobqueue/Job.php
includes/jobqueue/JobQueue.php
includes/jobqueue/JobSpecification.php
includes/jobqueue/jobs/EnqueueJob.php
includes/libs/objectcache/APCBagOStuff.php
includes/libs/objectcache/APCUBagOStuff.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/domain/DatabaseDomain.php
includes/media/GIFHandler.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderClientHtml.php
includes/specials/SpecialSearch.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/Maintenance.php
maintenance/doMaintenance.php
maintenance/getConfiguration.php
maintenance/update.php
mw-config/index.php
resources/Resources.php
resources/src/mediawiki.special/apisandbox.css
resources/src/mediawiki.special/block.less
resources/src/mediawiki.special/comparepages.less
resources/src/mediawiki.special/edittags.css
resources/src/mediawiki.special/newpages.less
resources/src/mediawiki.special/pagesWithProp.css
resources/src/mediawiki.special/special.less
resources/src/mediawiki.special/upload.css
resources/src/mediawiki.special/userrights.css
resources/src/mediawiki.special/watchlist.css
tests/phpunit/ResourceLoaderTestCase.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/WikiMapTest.php
tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php
tests/phpunit/includes/jobqueue/JobQueueTest.php
tests/phpunit/includes/libs/objectcache/BagOStuffTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
tests/selenium/specs/rollback.js

index ddd6da9..6e5de2c 100644 (file)
@@ -109,6 +109,7 @@ For notes on 1.32.x and older releases, see HISTORY.
   Content::getNativeData() for text-based content models.
 * (T214706) LinksUpdate::getAddedExternalLinks() and
   LinksUpdate::getRemovedExternalLinks() were introduced.
+* (T213893) Added 'MaintenanceUpdateAddParams' hook
 
 === External library changes in 1.33 ===
 ==== New external libraries ====
index 139123d..e9ceb95 100644 (file)
@@ -2198,6 +2198,16 @@ Special:LonelyPages.
 'MagicWordwgVariableIDs': When defining new magic words IDs.
 &$variableIDs: array of strings
 
+'MaintenanceUpdateAddParams': allow extensions to add params to the update.php
+maintenance script.
+&$params: array to populate with the params to be added. Array elements are keyed by
+the param name. Each param is an associative array that must include the following keys:
+  - desc The description of the param to show on --help
+  - require Is the param required? Defaults to false if not set.
+  - withArg Is an argument required with this option?  Defaults to false if not set.
+  - shortName Character to use as short name, or false if none.  Defaults to false if not set.
+  - multiOccurrence Can this option be passed multiple times?  Defaults to false if not set.
+
 'MaintenanceRefreshLinksInit': before executing the refreshLinks.php maintenance
 script.
 $refreshLinks: RefreshLinks object
index 23cdc3b..d8fef17 100644 (file)
@@ -869,7 +869,7 @@ class EditPage {
                } elseif ( $this->section == 'new' ) {
                        // Nothing *to* preview for new sections
                        return false;
-               } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
+               } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
                        && $this->context->getUser()->getOption( 'previewonfirst' )
                ) {
                        // Standard preference behavior
@@ -975,7 +975,7 @@ class EditPage {
 
                        $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
 
-                       if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
+                       if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
                                // wpTextbox1 field is missing, possibly due to being "too big"
                                // according to some filter rules such as Suhosin's setting for
                                // suhosin.request.max_value_length (d'oh)
index 43512e1..8cb9152 100644 (file)
@@ -330,7 +330,7 @@ class MediaWiki {
 
                if ( $request->getVal( 'action', 'view' ) != 'view'
                        || $request->wasPosted()
-                       || ( $request->getVal( 'title' ) !== null
+                       || ( $request->getCheck( 'title' )
                                && $title->getPrefixedDBkey() == $request->getVal( 'title' ) )
                        || count( $request->getValueNames( [ 'action', 'title' ] ) )
                        || !Hooks::run( 'TestCanonicalRedirect', [ $request, $title, $output ] )
index 73e4543..b1cdc81 100644 (file)
@@ -417,10 +417,15 @@ return [
        },
 
        'ResourceLoader' => function ( MediaWikiServices $services ) : ResourceLoader {
-               return new ResourceLoader(
-                       $services->getMainConfig(),
+               $config = $services->getMainConfig();
+
+               $rl = new ResourceLoader(
+                       $config,
                        LoggerFactory::getInstance( 'resourceloader' )
                );
+               $rl->addSource( $config->get( 'ResourceLoaderSources' ) );
+
+               return $rl;
        },
 
        'RevisionFactory' => function ( MediaWikiServices $services ) : RevisionFactory {
index 9ce12b4..d5c1656 100644 (file)
@@ -1590,8 +1590,9 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                                $update->setRevision( $legacyRevision );
                                $update->setTriggeringUser( $triggeringUser );
                        }
+
                        if ( $options['defer'] === false ) {
-                               if ( $options['transactionTicket'] !== null ) {
+                               if ( $update instanceof DataUpdate && $options['transactionTicket'] !== null ) {
                                        $update->setTransactionTicket( $options['transactionTicket'] );
                                }
                                $update->doUpdate();
index 3016e99..bf88b20 100644 (file)
@@ -47,7 +47,7 @@ class NameTableStore {
        private $tableCache = null;
 
        /** @var bool|string */
-       private $wikiId = false;
+       private $domain = false;
 
        /** @var int */
        private $cacheTTL;
@@ -77,7 +77,7 @@ class NameTableStore {
         * @param string $nameField
         * @param callable|null $normalizationCallback Normalization to be applied to names before being
         * saved or queried. This should be a callback that accepts and returns a single string.
-        * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+        * @param bool|string $dbDomain Database domain ID. Use false for the local database domain.
         * @param callable|null $insertCallback Callback to change insert fields accordingly.
         * This parameter was introduced in 1.32
         */
@@ -89,7 +89,7 @@ class NameTableStore {
                $idField,
                $nameField,
                callable $normalizationCallback = null,
-               $wikiId = false,
+               $dbDomain = false,
                callable $insertCallback = null
        ) {
                $this->loadBalancer = $dbLoadBalancer;
@@ -99,7 +99,7 @@ class NameTableStore {
                $this->idField = $idField;
                $this->nameField = $nameField;
                $this->normalizationCallback = $normalizationCallback;
-               $this->wikiId = $wikiId;
+               $this->domain = $dbDomain;
                $this->cacheTTL = IExpiringStore::TTL_MONTH;
                $this->insertCallback = $insertCallback;
        }
@@ -111,7 +111,7 @@ class NameTableStore {
         * @return IDatabase
         */
        private function getDBConnection( $index, $flags = 0 ) {
-               return $this->loadBalancer->getConnection( $index, [], $this->wikiId, $flags );
+               return $this->loadBalancer->getConnection( $index, [], $this->domain, $flags );
        }
 
        /**
@@ -126,7 +126,7 @@ class NameTableStore {
                return $this->cache->makeGlobalKey(
                        'NameTableSqlStore',
                        $this->table,
-                       $this->loadBalancer->resolveDomainID( $this->wikiId )
+                       $this->loadBalancer->resolveDomainID( $this->domain )
                );
        }
 
index dbad4b0..8b000f2 100644 (file)
@@ -245,8 +245,12 @@ class WikiMap {
        /**
         * Get the wiki ID of a database domain
         *
-        * This is like DatabaseDomain::getId() without encoding (for legacy reasons)
-        * and without the schema if it merely set to the generic value "mediawiki"
+        * This is like DatabaseDomain::getId() without encoding (for legacy reasons) and
+        * without the schema if it is the generic installer default of "mediawiki"/"dbo"
+        *
+        * @see $wgDBmwschema
+        * @see PostgresInstaller
+        * @see MssqlInstaller
         *
         * @param string|DatabaseDomain $domain
         * @return string
@@ -254,15 +258,17 @@ class WikiMap {
         */
        public static function getWikiIdFromDbDomain( $domain ) {
                $domain = DatabaseDomain::newFromId( $domain );
-
+               // Since the schema was not always part of the wiki ID, try to maintain backwards
+               // compatibility with some common cases. Assume that if the DB domain schema is just
+               // the installer default then it is probably the case that the schema is the same for
+               // all wikis in the farm. Historically, any wiki farm had to make the database/prefix
+               // combination unique per wiki. Ommit the schema if it does not seem wiki specific.
                if ( !in_array( $domain->getSchema(), [ null, 'mediawiki', 'dbo' ], true ) ) {
-                       // Include the schema if it is set and is not the default placeholder.
                        // This means a site admin may have specifically taylored the schemas.
-                       // Domain IDs might use the form <DB>-<project>-<language>, meaning that
-                       // the schema portion must be accounted for to disambiguate wikis.
+                       // Domain IDs might use the form <DB>-<project>- or <DB>-<project>-<language>_,
+                       // meaning that the schema portion must be accounted for to disambiguate wikis.
                        return "{$domain->getDatabase()}-{$domain->getSchema()}-{$domain->getTablePrefix()}";
                }
-
                // Note that if this wiki ID is passed a a domain ID to LoadBalancer, then it can
                // handle the schema by assuming the generic "mediawiki" schema if needed.
                return strlen( $domain->getTablePrefix() )
@@ -291,30 +297,16 @@ class WikiMap {
 
        /**
         * @param DatabaseDomain|string $domain
-        * @return bool Whether $domain has the same DB/prefix as the current wiki
+        * @return bool Whether $domain matches the DB domain of the current wiki
         * @since 1.33
         */
        public static function isCurrentWikiDbDomain( $domain ) {
-               $domain = DatabaseDomain::newFromId( $domain );
-               $curDomain = self::getCurrentWikiDbDomain();
-
-               if ( !in_array( $curDomain->getSchema(), [ null, 'mediawiki', 'dbo' ], true ) ) {
-                       // Include the schema if it is set and is not the default placeholder.
-                       // This means a site admin may have specifically taylored the schemas.
-                       // Domain IDs might use the form <DB>-<project>-<language>, meaning that
-                       // the schema portion must be accounted for to disambiguate wikis.
-                       return $curDomain->equals( $domain );
-               }
-
-               return (
-                       $curDomain->getDatabase() === $domain->getDatabase() &&
-                       $curDomain->getTablePrefix() === $domain->getTablePrefix()
-               );
+               return self::getCurrentWikiDbDomain()->equals( DatabaseDomain::newFromId( $domain ) );
        }
 
        /**
         * @param string $wikiId
-        * @return bool Whether $wikiId has the same DB/prefix as the current wiki
+        * @return bool Whether $wikiId matches the wiki ID of the current wiki
         * @since 1.33
         */
        public static function isCurrentWikiId( $wikiId ) {
index e9f8b6f..06e214f 100644 (file)
@@ -229,7 +229,6 @@ class HistoryAction extends FormlessAction {
                }
 
                // Add the general form.
-               $action = htmlspecialchars( wfScript() );
                $fields = [
                        [
                                'name' => 'title',
@@ -268,9 +267,10 @@ class HistoryAction extends FormlessAction {
                $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
                $htmlForm
                        ->setMethod( 'get' )
-                       ->setAction( $action )
+                       ->setAction( wfScript() )
                        ->setId( 'mw-history-searchform' )
                        ->setSubmitText( $this->msg( 'historyaction-submit' )->text() )
+                       ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
                        ->setWrapperLegend( $this->msg( 'history-fieldset-title' )->text() );
                $htmlForm->loadData();
 
index 295d5d0..261e4a4 100644 (file)
@@ -321,7 +321,7 @@ class ApiMain extends ApiBase {
                $request = $this->getRequest();
 
                // JSONP mode
-               if ( $request->getVal( 'callback' ) !== null ) {
+               if ( $request->getCheck( 'callback' ) ) {
                        $this->lacksSameOriginSecurity = true;
                        return true;
                }
index 3043c10..3b7de9d 100644 (file)
@@ -336,7 +336,8 @@ class DeferredUpdates {
                foreach ( $updates as $update ) {
                        if ( $update instanceof EnqueueableDataUpdate ) {
                                $spec = $update->getAsJobSpecification();
-                               JobQueueGroup::singleton( $spec['wiki'] )->push( $spec['job'] );
+                               $domain = $spec['domain'] ?? $spec['wiki'];
+                               JobQueueGroup::singleton( $domain )->push( $spec['job'] );
                        } else {
                                $remaining[] = $update;
                        }
index 9b9e748..134a104 100644 (file)
@@ -634,7 +634,6 @@ class LocalFile extends File {
                }
 
                $this->fileExists = true;
-               $this->maybeUpgradeRow();
        }
 
        /**
@@ -659,7 +658,7 @@ class LocalFile extends File {
        /**
         * Upgrade a row if it needs it
         */
-       function maybeUpgradeRow() {
+       protected function maybeUpgradeRow() {
                global $wgUpdateCompatibleMetadata;
 
                if ( wfReadOnly() || $this->upgrading ) {
@@ -1028,6 +1027,7 @@ class LocalFile extends File {
         */
        function purgeCache( $options = [] ) {
                // Refresh metadata cache
+               $this->maybeUpgradeRow();
                $this->purgeMetadataCache();
 
                // Delete thumbnails
index ee0da7b..3c9b23c 100644 (file)
@@ -233,6 +233,7 @@ class HTMLForm extends ContextSource {
        protected $mButtons = [];
 
        protected $mWrapperLegend = false;
+       protected $mWrapperAttributes = [];
 
        /**
         * Salt for the edit token.
@@ -1089,7 +1090,7 @@ class HTMLForm extends ContextSource {
                # Include a <fieldset> wrapper for style, if requested.
                if ( $this->mWrapperLegend !== false ) {
                        $legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
-                       $html = Xml::fieldset( $legend, $html );
+                       $html = Xml::fieldset( $legend, $html, $this->mWrapperAttributes );
                }
 
                return Html::rawElement(
@@ -1534,6 +1535,19 @@ class HTMLForm extends ContextSource {
                return $this;
        }
 
+       /**
+        * For internal use only. Use is discouraged, and should only be used where
+        * support for gadgets/user scripts is warranted.
+        * @param array $attributes
+        * @internal
+        * @return HTMLForm $this for chaining calls
+        */
+       public function setWrapperAttributes( $attributes ) {
+               $this->mWrapperAttributes = $attributes;
+
+               return $this;
+       }
+
        /**
         * Prompt the whole form to be wrapped in a "<fieldset>", with
         * this message as its "<legend>" element.
index e7e3ff6..738db09 100644 (file)
@@ -284,7 +284,7 @@ class OOUIHTMLForm extends HTMLForm {
                                                'content' => new OOUI\HtmlSnippet( $html )
                                        ] ),
                                ],
-                       ] );
+                       ] + OOUI\Element::configFromHtmlAttributes( $this->mWrapperAttributes ) );
                } else {
                        $content = new OOUI\HtmlSnippet( $html );
                }
index aad9f6e..0a1024c 100644 (file)
@@ -119,7 +119,7 @@ class HTMLCheckField extends HTMLFormField {
                // Fetch the value in either one of the two following case:
                // - we have a valid submit attempt (form was just submitted)
                // - we have a value (an URL manually built by the user, or GET form with no wpFormIdentifier)
-               if ( $this->isSubmitAttempt( $request ) || $request->getVal( $this->mName ) !== null ) {
+               if ( $this->isSubmitAttempt( $request ) || $request->getCheck( $this->mName ) ) {
                        return $invert
                                ? !$request->getBool( $this->mName )
                                : $request->getBool( $this->mName );
index 5ad1a4d..bd492d1 100644 (file)
@@ -45,6 +45,10 @@ class HTMLNamespacesMultiselectField extends HTMLSelectNamespace {
                }
 
                foreach ( $namespaces as $namespace ) {
+                       if ( $namespace < 0 ) {
+                               return $this->msg( 'htmlform-select-badoption' );
+                       }
+
                        $result = parent::validate( $namespace, $alldata );
                        if ( $result !== true ) {
                                return $result;
index 19a2b66..24fc473 100644 (file)
@@ -41,7 +41,7 @@ abstract class Job implements IJobSpecification {
        protected $title;
 
        /** @var bool Expensive jobs may set this to true */
-       protected $removeDuplicates;
+       protected $removeDuplicates = false;
 
        /** @var string Text for error that occurred last */
        protected $error;
@@ -65,14 +65,22 @@ abstract class Job implements IJobSpecification {
         * Create the appropriate object to handle a specific job
         *
         * @param string $command Job command
-        * @param Title $title Associated title
         * @param array $params Job parameters
         * @throws InvalidArgumentException
         * @return Job
         */
-       public static function factory( $command, Title $title, $params = [] ) {
+       public static function factory( $command, $params = [] ) {
                global $wgJobClasses;
 
+               if ( $params instanceof Title ) {
+                       // Backwards compatibility for old signature ($command, $title, $params)
+                       $title = $params;
+                       $params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
+               } else {
+                       // Subclasses can override getTitle() to return something more meaningful
+                       $title = Title::makeTitle( NS_SPECIAL, 'Blankpage' );
+               }
+
                if ( isset( $wgJobClasses[$command] ) ) {
                        $handler = $wgJobClasses[$command];
 
@@ -86,9 +94,10 @@ abstract class Job implements IJobSpecification {
 
                        if ( $job instanceof Job ) {
                                $job->command = $command;
+
                                return $job;
                        } else {
-                               throw new InvalidArgumentException( "Cannot instantiate job '$command': bad spec!" );
+                               throw new InvalidArgumentException( "Could instantiate job '$command': bad spec!" );
                        }
                }
 
@@ -97,17 +106,21 @@ abstract class Job implements IJobSpecification {
 
        /**
         * @param string $command
-        * @param Title $title
-        * @param array|bool $params Can not be === true
+        * @param array $params
         */
-       public function __construct( $command, $title, $params = false ) {
+       public function __construct( $command, $params = [] ) {
+               if ( $params instanceof Title ) {
+                       // Backwards compatibility for old signature ($command, $title, $params)
+                       $title = $params;
+                       $params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
+               } else {
+                       // Subclasses can override getTitle() to return something more meaningful
+                       $title = Title::makeTitle( NS_SPECIAL, 'Blankpage' );
+               }
+
                $this->command = $command;
                $this->title = $title;
                $this->params = is_array( $params ) ? $params : []; // sanity
-
-               // expensive jobs may set this to true
-               $this->removeDuplicates = false;
-
                if ( !isset( $this->params['requestId'] ) ) {
                        $this->params['requestId'] = WebRequest::getRequestId();
                }
index 660352a..fdcd65c 100644 (file)
@@ -126,7 +126,7 @@ abstract class JobQueue {
         * @deprecated 1.33
         */
        final public function getWiki() {
-               return $this->domain;
+               return WikiMap::getWikiIdFromDbDomain( $this->domain );
        }
 
        /**
index 4abbc6d..b04aa83 100644 (file)
@@ -64,7 +64,7 @@ class JobSpecification implements IJobSpecification {
 
                $this->type = $type;
                $this->params = $params;
-               $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . static::class );
+               $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Blankpage' );
                $this->opts = $opts;
        }
 
index ea7a8d7..72923ce 100644 (file)
@@ -50,22 +50,24 @@ final class EnqueueJob extends Job {
        public static function newFromLocalJobs( $jobs ) {
                $jobs = is_array( $jobs ) ? $jobs : [ $jobs ];
 
-               return self::newFromJobsByWiki( [ wfWikiID() => $jobs ] );
+               return self::newFromJobsByDomain( [
+                       WikiMap::getCurrentWikiDbDomain()->getId() => $jobs
+               ] );
        }
 
        /**
-        * @param array $jobsByWiki Map of (wiki => JobSpecification list)
+        * @param array $jobsByDomain Map of (wiki => JobSpecification list)
         * @return EnqueueJob
         */
-       public static function newFromJobsByWiki( array $jobsByWiki ) {
+       public static function newFromJobsByDomain( array $jobsByDomain ) {
                $deduplicate = true;
 
-               $jobMapsByWiki = [];
-               foreach ( $jobsByWiki as $wiki => $jobs ) {
-                       $jobMapsByWiki[$wiki] = [];
+               $jobMapsByDomain = [];
+               foreach ( $jobsByDomain as $domain => $jobs ) {
+                       $jobMapsByDomain[$domain] = [];
                        foreach ( $jobs as $job ) {
                                if ( $job instanceof JobSpecification ) {
-                                       $jobMapsByWiki[$wiki][] = $job->toSerializableArray();
+                                       $jobMapsByDomain[$domain][] = $job->toSerializableArray();
                                } else {
                                        throw new InvalidArgumentException( "Jobs must be of type JobSpecification." );
                                }
@@ -75,7 +77,7 @@ final class EnqueueJob extends Job {
 
                $eJob = new self(
                        Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __CLASS__ ),
-                       [ 'jobsByWiki' => $jobMapsByWiki ]
+                       [ 'jobsByDomain' => $jobMapsByDomain ]
                );
                // If *all* jobs to be pushed are to be de-duplicated (a common case), then
                // de-duplicate this whole job itself to avoid build up in high traffic cases
@@ -84,13 +86,24 @@ final class EnqueueJob extends Job {
                return $eJob;
        }
 
+       /**
+        * @param array $jobsByWiki
+        * @return EnqueueJob
+        * @deprecated Since 1.33; use newFromJobsByDomain()
+        */
+       public static function newFromJobsByWiki( array $jobsByWiki ) {
+               return self::newFromJobsByDomain( $jobsByWiki );
+       }
+
        public function run() {
-               foreach ( $this->params['jobsByWiki'] as $wiki => $jobMaps ) {
+               $jobsByDomain = $this->params['jobsByDomain'] ?? $this->params['jobsByWiki']; // b/c
+
+               foreach ( $jobsByDomain as $domain => $jobMaps ) {
                        $jobSpecs = [];
                        foreach ( $jobMaps as $jobMap ) {
                                $jobSpecs[] = JobSpecification::newFromArray( $jobMap );
                        }
-                       JobQueueGroup::singleton( $wiki )->push( $jobSpecs );
+                       JobQueueGroup::singleton( $domain )->push( $jobSpecs );
                }
 
                return true;
index 902cd6a..9a5a433 100644 (file)
 /**
  * This is a wrapper for APC's shared memory functions
  *
+ * Use PHP serialization to avoid bugs and easily create CAS tokens.
+ * APCu has a memory corruption bug when the serializer is set to 'default'.
+ * See T120267, and upstream bug reports:
+ *  - https://github.com/krakjoe/apcu/issues/38
+ *  - https://github.com/krakjoe/apcu/issues/35
+ *  - https://github.com/krakjoe/apcu/issues/111
+ *
  * @ingroup Cache
  */
 class APCBagOStuff extends BagOStuff {
-
-       /**
-        * @var bool If true, trust the APC implementation to serialize and
-        * deserialize objects correctly. If false, (de-)serialize in PHP.
-        */
-       protected $nativeSerialize;
-
        /**
         * @var string String to append to each APC key. This may be changed
         *  whenever the handling of values is changed, to prevent existing code
         *  from encountering older values which it cannot handle.
         */
-       const KEY_SUFFIX = ':2';
-
-       /**
-        * Available parameters are:
-        *   - nativeSerialize:     If true, pass objects to apc_store(), and trust it
-        *                          to serialize them correctly. If false, serialize
-        *                          all values in PHP.
-        *
-        * @param array $params
-        */
-       public function __construct( array $params = [] ) {
-               parent::__construct( $params );
-
-               if ( isset( $params['nativeSerialize'] ) ) {
-                       $this->nativeSerialize = $params['nativeSerialize'];
-               } elseif ( extension_loaded( 'apcu' ) && ini_get( 'apc.serializer' ) === 'default' ) {
-                       // APCu has a memory corruption bug when the serializer is set to 'default'.
-                       // See T120267, and upstream bug reports:
-                       //  - https://github.com/krakjoe/apcu/issues/38
-                       //  - https://github.com/krakjoe/apcu/issues/35
-                       //  - https://github.com/krakjoe/apcu/issues/111
-                       $this->logger->warning(
-                               'The APCu extension is loaded and the apc.serializer INI setting ' .
-                               'is set to "default". This can cause memory corruption! ' .
-                               'You should change apc.serializer to "php" instead. ' .
-                               'See <https://github.com/krakjoe/apcu/issues/38>.'
-                       );
-                       $this->nativeSerialize = false;
-               } else {
-                       $this->nativeSerialize = true;
-               }
-       }
+       const KEY_SUFFIX = ':3';
 
        protected function doGet( $key, $flags = 0, &$casToken = null ) {
                $casToken = null;
@@ -117,18 +86,10 @@ class APCBagOStuff extends BagOStuff {
        }
 
        protected function serialize( $value ) {
-               if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) {
-                       $value = serialize( $value );
-               }
-               return $value;
+               return $this->isInteger( $value ) ? (int)$value : serialize( $value );
        }
 
        protected function unserialize( $value ) {
-               if ( is_string( $value ) && !$this->nativeSerialize ) {
-                       $value = $this->isInteger( $value )
-                               ? intval( $value )
-                               : unserialize( $value );
-               }
-               return $value;
+               return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
        }
 }
index da6544b..0483ee7 100644 (file)
 /**
  * This is a wrapper for APCU's shared memory functions
  *
+ * Use PHP serialization to avoid bugs and easily create CAS tokens.
+ * APCu has a memory corruption bug when the serializer is set to 'default'.
+ * See T120267, and upstream bug reports:
+ *  - https://github.com/krakjoe/apcu/issues/38
+ *  - https://github.com/krakjoe/apcu/issues/35
+ *  - https://github.com/krakjoe/apcu/issues/111
+ *
  * @ingroup Cache
  */
-class APCUBagOStuff extends APCBagOStuff {
+class APCUBagOStuff extends BagOStuff {
        /**
-        * Available parameters are:
-        *   - nativeSerialize:     If true, pass objects to apcu_store(), and trust it
-        *                          to serialize them correctly. If false, serialize
-        *                          all values in PHP.
-        *
-        * @param array $params
+        * @var string String to append to each APC key. This may be changed
+        *  whenever the handling of values is changed, to prevent existing code
+        *  from encountering older values which it cannot handle.
         */
-       public function __construct( array $params = [] ) {
-               parent::__construct( $params );
-       }
+       const KEY_SUFFIX = ':3';
 
        protected function doGet( $key, $flags = 0, &$casToken = null ) {
                $casToken = null;
@@ -100,4 +102,12 @@ class APCUBagOStuff extends APCBagOStuff {
                        return false;
                }
        }
+
+       protected function serialize( $value ) {
+               return $this->isInteger( $value ) ? (int)$value : serialize( $value );
+       }
+
+       protected function unserialize( $value ) {
+               return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
+       }
 }
index ae4b71a..18961bd 100644 (file)
@@ -617,6 +617,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function dbSchema( $schema = null ) {
+               if ( strlen( $schema ) && $this->getDBname() === null ) {
+                       throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set." );
+               }
+
                $old = $this->currentDomain->getSchema();
                if ( $schema !== null ) {
                        $this->currentDomain = new DatabaseDomain(
index ca57938..5dd4b49 100644 (file)
@@ -48,6 +48,8 @@ class DatabaseDomain {
                $this->database = $database;
                if ( $schema !== null && ( !is_string( $schema ) || $schema === '' ) ) {
                        throw new InvalidArgumentException( 'Schema must be null or a non-empty string.' );
+               } elseif ( $database === null && $schema !== null ) {
+                       throw new InvalidArgumentException( 'Schema must be null if database is null.' );
                }
                $this->schema = $schema;
                if ( !is_string( $prefix ) ) {
@@ -120,8 +122,8 @@ class DatabaseDomain {
         * Check whether the domain $other meets the specifications of this domain
         *
         * If this instance has a null database specifier, then $other can have any database
-        * specified, including the null, and likewise if the schema specifier is null. This
-        * is not transitive like equals() since a domain that explicitly wants a certain
+        * specifier, including null. This is likewise true if the schema specifier is null.
+        * This is not transitive like equals() since a domain that explicitly wants a certain
         * database or schema cannot be satisfied by one of another (nor null). If the prefix
         * is empty and the DB and schema are both null, then the entire domain is considered
         * unspecified, and any prefix of $other is considered compatible.
index 556e83c..9ef37fb 100644 (file)
@@ -86,8 +86,11 @@ class GIFHandler extends BitmapHandler {
                $ser = $image->getMetadata();
                if ( $ser ) {
                        $metadata = unserialize( $ser );
-
-                       return $image->getWidth() * $image->getHeight() * $metadata['frameCount'];
+                       if ( isset( $metadata['frameCount'] ) && $metadata['frameCount'] > 0 ) {
+                               return $image->getWidth() * $image->getHeight() * $metadata['frameCount'];
+                       } else {
+                               return $image->getWidth() * $image->getHeight();
+                       }
                } else {
                        return $image->getWidth() * $image->getHeight();
                }
@@ -101,7 +104,7 @@ class GIFHandler extends BitmapHandler {
                $ser = $image->getMetadata();
                if ( $ser ) {
                        $metadata = unserialize( $ser );
-                       if ( $metadata['frameCount'] > 1 ) {
+                       if ( isset( $metadata['frameCount'] ) && $metadata['frameCount'] > 1 ) {
                                return true;
                        }
                }
index 5712692..8ab7c0c 100644 (file)
@@ -254,9 +254,6 @@ class ResourceLoader implements LoggerAwareInterface {
                // Add 'local' source first
                $this->addSource( 'local', $config->get( 'LoadScript' ) );
 
-               // Add other sources
-               $this->addSource( $config->get( 'ResourceLoaderSources' ) );
-
                // Register core modules
                $this->register( include "$IP/resources/Resources.php" );
                // Register extension modules
index 2b3db22..84477ca 100644 (file)
@@ -22,7 +22,7 @@ use Wikimedia\WrappedString;
 use Wikimedia\WrappedStringList;
 
 /**
- * Bootstrap a ResourceLoader client on an HTML page.
+ * Load and configure a ResourceLoader client on an HTML page.
  *
  * @since 1.28
  */
index 171566b..b60a2ad 100644 (file)
@@ -129,7 +129,7 @@ class SpecialSearch extends SpecialPage {
                $this->load();
                // TODO: This performs database actions on GET request, which is going to
                // be a problem for our multi-datacenter work.
-               if ( !is_null( $request->getVal( 'nsRemember' ) ) ) {
+               if ( $request->getCheck( 'nsRemember' ) ) {
                        $this->saveNamespaces();
                        // Remove the token from the URL to prevent the user from inadvertently
                        // exposing it (e.g. by pasting it into a public wiki page) or undoing
@@ -141,10 +141,7 @@ class SpecialSearch extends SpecialPage {
                }
 
                $this->searchEngineType = $request->getVal( 'srbackend' );
-               if (
-                       !$request->getVal( 'fulltext' ) &&
-                       $request->getVal( 'offset' ) === null
-               ) {
+               if ( !$request->getVal( 'fulltext' ) && !$request->getCheck( 'offset' ) ) {
                        $url = $this->goResult( $term );
                        if ( $url !== null ) {
                                // successful 'go'
index 65b956e..c4c018b 100644 (file)
        "right-reupload-own": "Overwrite existing files uploaded by oneself",
        "right-reupload-shared": "Override files on the shared media repository locally",
        "right-upload_by_url": "Upload files from a URL",
-       "right-purge": "Purge the site cache for a page without confirmation",
+       "right-purge": "Purge the site cache for a page",
        "right-autoconfirmed": "Not be affected by IP-based rate limits",
        "right-bot": "Be treated as an automated process",
        "right-nominornewtalk": "Not have minor edits to discussion pages trigger the new messages prompt",
index f37b5c7..fa66817 100644 (file)
        "right-reupload-own": "{{doc-right|reupload-own}}\nRight to upload a file under a file name that already exists, and that the same user has uploaded.\n\nRelated messages:\n* {{msg-mw|right-upload}}\n* {{msg-mw|right-reupload}}",
        "right-reupload-shared": "{{doc-right|reupload-shared}}\nThe right to upload a file locally under a file name that already exists in a shared database (for example Commons).\n\nRelated messages:\n* {{msg-mw|right-upload}}\n* {{msg-mw|right-reupload}}",
        "right-upload_by_url": "{{doc-right|upload_by_url}}",
-       "right-purge": "{{doc-right|purge}}\nThe right to use <code>&action=purge</code> in the URL, without needing to confirm it (by default, anonymous users need to confirm it).",
+       "right-purge": "{{doc-right|purge}}\nThe right to use <code>&action=purge</code> in the URL.",
        "right-autoconfirmed": "{{doc-right|autoconfirmed}}\nIf your account is older than [[mw:Manual:$wgAutoConfirmAge|wgAutoConfirmAge]] and if you have at least [[mw:Manual:$wgAutoConfirmCount|$wgAutoConfirmCount]] edits, you are in the '''group \"autoconfirmed\"''' (note that you can't see this group at [[Special:ListUsers]]).\nIf you are in that group, you have (by default) the '''right \"autoconfirmed\"''', which exempts you from certain rate limits (those based on your IP address or otherwise intended solely for new users). Other rate limits may still apply; see {{msg-mw|right-noratelimit}}.",
        "right-bot": "{{doc-right|bot}}",
        "right-nominornewtalk": "{{doc-right|nominornewtalk}}\nIf someone with this right (bots by default) edits a user talk page and marks it as minor (requires {{msg-mw|right-minoredit}}), the user will not get a notification \"You have new messages\".",
index 3476a32..b3e958f 100644 (file)
@@ -54,6 +54,18 @@ use Wikimedia\Rdbms\IMaintainableDatabase;
  * is the execute() method. See docs/maintenance.txt for more info
  * and a quick demo of how to use it.
  *
+ * Terminology:
+ *   params: registry of named values that may be passed to the script
+ *   arg list: registry of positional values that may be passed to the script
+ *   options: passed param values
+ *   args: passed positional values
+ *
+ * In the command:
+ *   mwscript somescript.php --foo=bar baz
+ * foo is a param
+ * bar is the option value of the option for param foo
+ * baz is the arg value at index 0 in the arg list
+ *
  * @since 1.16
  * @ingroup Maintenance
  */
@@ -69,13 +81,13 @@ abstract class Maintenance {
        // Const for getStdin()
        const STDIN_ALL = 'all';
 
-       // This is the desired params
+       // Array of desired/allowed params
        protected $mParams = [];
 
        // Array of mapping short parameters to long ones
        protected $mShortParamsMap = [];
 
-       // Array of desired args
+       // Array of desired/allowed args
        protected $mArgList = [];
 
        // This is the list of options that were actually passed
@@ -738,7 +750,6 @@ abstract class Maintenance {
                }
 
                $this->loadParamsAndArgs();
-               $this->maybeHelp();
 
                # Set the memory limit
                # Note we need to set it again later in cache LocalSettings changed it
@@ -758,8 +769,6 @@ abstract class Maintenance {
                while ( ob_get_level() > 0 ) {
                        ob_end_flush();
                }
-
-               $this->validateParamsAndArgs();
        }
 
        /**
@@ -972,7 +981,7 @@ abstract class Maintenance {
        /**
         * Run some validation checks on the params, etc
         */
-       protected function validateParamsAndArgs() {
+       public function validateParamsAndArgs() {
                $die = false;
                # Check to make sure we've got all the required options
                foreach ( $this->mParams as $opt => $info ) {
@@ -998,9 +1007,7 @@ abstract class Maintenance {
                        }
                }
 
-               if ( $die ) {
-                       $this->maybeHelp( true );
-               }
+               $this->maybeHelp( $die );
        }
 
        /**
index 1f1a4c7..1c53fe8 100644 (file)
@@ -90,6 +90,8 @@ $maintenance->checkRequiredExtensions();
 // This avoids having long running scripts just OOM and lose all the updates.
 $maintenance->setAgentAndTriggers();
 
+$maintenance->validateParamsAndArgs();
+
 // Do the work
 $success = $maintenance->execute();
 
index de6e87a..f56729c 100644 (file)
@@ -56,7 +56,7 @@ class GetConfiguration extends Maintenance {
                $this->addOption( 'format', implode( ', ', self::$outFormats ), false, true );
        }
 
-       protected function validateParamsAndArgs() {
+       public function validateParamsAndArgs() {
                $error_out = false;
 
                # Get the format and make sure it is set to a valid default value
index 2a1feb4..50fb6dc 100755 (executable)
@@ -242,6 +242,24 @@ class UpdateMediaWiki extends Maintenance {
                        'manualRecache' => false,
                ];
        }
+
+       public function validateParamsAndArgs() {
+               // Allow extensions to add additional params.
+               $params = [];
+               Hooks::run( 'MaintenanceUpdateAddParams', [ &$params ] );
+               foreach ( $params as $name => $param ) {
+                       $this->addOption(
+                               $name,
+                               $param['desc'],
+                               $param['require'] ?? false,
+                               $param['withArg'] ?? false,
+                               $param['shortName'] ?? false,
+                               $param['multiOccurrence'] ?? false
+                       );
+               }
+
+               parent::validateParamsAndArgs();
+       }
 }
 
 $maintClass = UpdateMediaWiki::class;
index df3f6e5..b625c96 100644 (file)
@@ -63,7 +63,7 @@ function wfInstallerMain() {
                $session = array();
        }
 
-       if ( $request->getVal( 'uselang' ) !== null ) {
+       if ( $request->getCheck( 'uselang' ) ) {
                $langCode = $request->getVal( 'uselang' );
        } elseif ( isset( $session['settings']['_UserLang'] ) ) {
                $langCode = $session['settings']['_UserLang'];
index bfa80a8..af40b73 100644 (file)
@@ -1781,14 +1781,16 @@ return [
        /* MediaWiki Special pages */
 
        'mediawiki.rcfilters.filters.base.styles' => [
-               'styles' => [
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less',
+               'skinStyles' => [
+                       'default' => 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less',
                ],
        ],
        'mediawiki.rcfilters.highlightCircles.seenunseen.styles' => [
-               'styles' => [
-                       'resources/src/mediawiki.rcfilters/' .
-                       'styles/mw.rcfilters.ui.ChangesListWrapperWidget.highlightCircles.seenunseen.less',
+               'skinStyles' => [
+                       'default' => [
+                               'resources/src/mediawiki.rcfilters/' .
+                               'styles/mw.rcfilters.ui.ChangesListWrapperWidget.highlightCircles.seenunseen.less',
+                       ],
                ],
        ],
        'mediawiki.rcfilters.filters.dm' => [
index 4dc4c27..923b595 100644 (file)
@@ -1,3 +1,6 @@
+/*!
+ * Styles for Special:ApiSandbox
+ */
 .client-js .mw-apisandbox-nojs {
        display: none;
 }
index d7ee3da..6d12722 100644 (file)
@@ -1,10 +1,12 @@
-/* Special:Block styles */
+/*!
+ * Styles for Special:Block
+ */
 
-// OOUIHTMLForm styles
-@ooui-font-size-browser: 16; // Assumed browser default of `16px`
-@ooui-font-size-base: 0.875em; // Equals `14px` at browser default of `16px`
+// OOUIHTMLForm specifics
+@ooui-font-size-browser: 16; // Assumed browser default of `16px`.
+@ooui-font-size-base: 0.875em; // Equals `14px` at browser default of `16px`.
 
-@ooui-spacing-radio-label: 26 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `1.85714286em`≈`26px`
+@ooui-spacing-radio-label: 26 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `1.85714286em`≈`26px`.
 
 body.mw-special-Block {
        .mw-block-editing-restriction.oo-ui-fieldLayout {
index 87b7a8b..38b4078 100644 (file)
@@ -1,3 +1,6 @@
+/*!
+ * Styles for Special:ComparePages
+ */
 @import 'mediawiki.mixins';
 
 .mw-special-ComparePages .mw-htmlform-ooui-wrapper {
index 204009c..8964a13 100644 (file)
@@ -1,5 +1,5 @@
 /*!
- * Styling for Special:EditTags and action=editchangetags
+ * Styles for Special:EditTags and action=editchangetags
  */
 #mw-edittags-tags-selector td {
        vertical-align: top;
index 835cab8..8d72b11 100644 (file)
@@ -1,5 +1,5 @@
 /*!
- * Styling for Special:NewPages
+ * Styles for Special:NewPages
  */
 
 // OOUIHTMLForm styles
index 7ef75d0..7240bd4 100644 (file)
@@ -1,4 +1,8 @@
-/* Distinguish actual data from information about it being hidden visually */
+/*!
+ * Styles for Special:PagesWithProp
+ */
+
+/* Distinguish actual data from information about it being hidden visually. */
 .prop-value-hidden {
        font-style: italic;
 }
index 35071be..3798f1e 100644 (file)
@@ -1,6 +1,9 @@
-/* Special:AllMessages */
+/*!
+ * Styles shared across various special pages.
+ */
 @import 'mediawiki.mixins';
 
+/* Special:AllMessages */
 /* Visually hide repeating text, but leave in for better form navigation on screen readers */
 .mw-special-Allmessages .mw-htmlform-ooui .oo-ui-fieldsetLayout:first-child .oo-ui-fieldsetLayout-header {
        .mixin-screen-reader-text();
index 626a7e8..0a5796e 100644 (file)
@@ -1,5 +1,5 @@
 /*!
- * Styling for Special:Upload
+ * Styles for Special:Upload
  */
 .mw-destfile-warning {
        border: 1px solid #fde29b;
index 14ad695..06a74f5 100644 (file)
@@ -1,5 +1,5 @@
 /*!
- * Styling for Special:UserRights
+ * Styles for Special:UserRights
  */
 .mw-userrights-nested {
        margin-left: 1.2em;
index c9861c2..1660bd2 100644 (file)
@@ -1,5 +1,5 @@
 /*!
- * Styling for elements generated by JavaScript on Special:Watchlist
+ * Styles for elements generated by JavaScript on Special:Watchlist
  */
 .mw-changelist-line-inner-unwatched {
        text-decoration: line-through;
index ca5ff6c..c7fb48b 100644 (file)
@@ -60,9 +60,6 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase {
                        // Avoid influence from wgInvalidateCacheOnLocalSettingsChange
                        'CacheEpoch' => '20140101000000',
 
-                       // For ResourceLoader::__construct()
-                       'ResourceLoaderSources' => [],
-
                        // For wfScript()
                        'ScriptPath' => '/w',
                        'Script' => '/w/index.php',
index a17d21d..d7f4fd6 100644 (file)
@@ -26,6 +26,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        [
                                'page',
                                'revision',
+                               'comment',
                                'ip_changes',
                                'text',
                                'archive',
@@ -1396,6 +1397,9 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $this->setService( 'MainWANObjectCache', $cache );
                $db = wfGetDB( DB_MASTER );
 
+               $now = 1553893742;
+               $cache->setMockTime( $now );
+
                // Get a fresh revision to use during testing
                $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
                $rev = $this->testPage->getRevision();
@@ -1410,6 +1414,8 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
                $this->assertFalse( $cache->get( $key ) );
 
+               ++$now;
+
                // Get the new revision and make sure it is in the cache and correct
                $newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
                $this->assertRevEquals( $rev, $newRev );
index df05671..1608b9c 100644 (file)
@@ -260,16 +260,19 @@ class WikiMapTest extends MediaWikiLangTestCase {
         * @covers WikiMap::getCurrentWikiDbDomain()
         */
        public function testIsCurrentWikiDomain() {
-               $this->assertTrue( WikiMap::isCurrentWikiDbDomain( wfWikiID() ) );
+               $this->setMwGlobals( 'wgDBmwschema', 'mediawiki' );
 
-               $localDomain = DatabaseDomain::newFromId( wfWikiID() );
+               $localDomain = WikiMap::getCurrentWikiDbDomain()->getId();
+               $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $localDomain ) );
+
+               $localDomain = DatabaseDomain::newFromId( $localDomain );
                $domain1 = new DatabaseDomain(
                        $localDomain->getDatabase(), 'someschema', $localDomain->getTablePrefix() );
                $domain2 = new DatabaseDomain(
                        $localDomain->getDatabase(), null, $localDomain->getTablePrefix() );
 
-               $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $domain1 ), 'Schema ignored' );
-               $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $domain2 ), 'Schema ignored' );
+               $this->assertFalse( WikiMap::isCurrentWikiDbDomain( $domain1 ), 'Schema not ignored' );
+               $this->assertFalse( WikiMap::isCurrentWikiDbDomain( $domain2 ), 'Null schema not ignored' );
 
                $this->assertTrue( WikiMap::isCurrentWikiDbDomain( WikiMap::getCurrentWikiDbDomain() ) );
        }
index b2e7ea4..232b46a 100644 (file)
@@ -18,7 +18,7 @@ class JobQueueMemoryTest extends PHPUnit\Framework\TestCase {
        private function newJobQueue() {
                return JobQueue::factory( [
                        'class' => JobQueueMemory::class,
-                       'wiki' => wfWikiID(),
+                       'domain' => WikiMap::getCurrentWikiDbDomain()->getId(),
                        'type' => 'null',
                ] );
        }
index 0421fe7..d38c6c7 100644 (file)
@@ -31,7 +31,7 @@ class JobQueueTest extends MediaWikiTestCase {
                        $baseConfig = [ 'class' => JobQueueDBSingle::class ];
                }
                $baseConfig['type'] = 'null';
-               $baseConfig['wiki'] = wfWikiID();
+               $baseConfig['domain'] = WikiMap::getCurrentWikiDbDomain()->getId();
                $variants = [
                        'queueRand' => [ 'order' => 'random', 'claimTTL' => 0 ],
                        'queueRandTTL' => [ 'order' => 'random', 'claimTTL' => 10 ],
@@ -75,7 +75,10 @@ class JobQueueTest extends MediaWikiTestCase {
                        $this->markTestSkipped( $desc );
                }
                $this->assertEquals( wfWikiID(), $queue->getWiki(), "Proper wiki ID ($desc)" );
-               $this->assertEquals( wfWikiID(), $queue->getDomain(), "Proper wiki ID ($desc)" );
+               $this->assertEquals(
+                       WikiMap::getCurrentWikiDbDomain()->getId(),
+                       $queue->getDomain(),
+                       "Proper wiki ID ($desc)" );
        }
 
        /**
index e6b277b..4a09a2e 100644 (file)
@@ -107,77 +107,6 @@ class BagOStuffTest extends MediaWikiTestCase {
                $this->assertEquals( $n, $calls );
        }
 
-       /**
-        * @covers BagOStuff::merge
-        * @dataProvider provideTestMerge_fork
-        */
-       public function testMerge_fork( $exists, $childWins, $resCAS ) {
-               $key = $this->cache->makeKey( self::TEST_KEY );
-               $pCallback = function ( BagOStuff $cache, $key, $oldVal ) {
-                       return ( $oldVal === false ) ? 'init-parent' : $oldVal . '-merged-parent';
-               };
-               $cCallback = function ( BagOStuff $cache, $key, $oldVal ) {
-                       return ( $oldVal === false ) ? 'init-child' : $oldVal . '-merged-child';
-               };
-
-               if ( $exists ) {
-                       $this->cache->set( $key, 'x', 5 );
-               }
-
-               /*
-                * Test concurrent merges by forking this process, if:
-                * - not manually called with --use-bagostuff
-                * - pcntl_fork is supported by the system
-                * - cache type will correctly support calls over forks
-                */
-               $fork = (bool)$this->getCliArg( 'use-bagostuff' );
-               $fork &= function_exists( 'pcntl_fork' );
-               $fork &= !$this->cache instanceof HashBagOStuff;
-               $fork &= !$this->cache instanceof EmptyBagOStuff;
-               $fork &= !$this->cache instanceof MultiWriteBagOStuff;
-               if ( $fork ) {
-                       $pid = null;
-                       // Function to start merge(), run another merge() midway through, then finish
-                       $func = function ( $cache, $key, $cur ) use ( $pCallback, $cCallback, &$pid ) {
-                               $pid = pcntl_fork();
-                               if ( $pid == -1 ) {
-                                       return false;
-                               } elseif ( $pid ) {
-                                       pcntl_wait( $status );
-
-                                       return $pCallback( $cache, $key, $cur );
-                               } else {
-                                       $this->cache->merge( $key, $cCallback, 0, 1 );
-                                       // Bail out of the outer merge() in the child process since it does not
-                                       // need to attempt to write anything. Success is checked by the parent.
-                                       parent::tearDown(); // avoid phpunit notices
-                                       exit;
-                               }
-                       };
-
-                       // attempt a merge - this should fail
-                       $merged = $this->cache->merge( $key, $func, 0, 1 );
-
-                       if ( $pid == -1 ) {
-                               return; // can't fork, ignore this test...
-                       }
-
-                       // merge has failed because child process was merging (and we only attempted once)
-                       $this->assertEquals( !$childWins, $merged );
-                       $this->assertEquals( $this->cache->get( $key ), $resCAS );
-               } else {
-                       $this->markTestSkipped( 'No pcntl methods available' );
-               }
-       }
-
-       function provideTestMerge_fork() {
-               return [
-                       // (already exists, child wins CAS, result of CAS)
-                       [ false, true, 'init-child' ],
-                       [ true, true, 'x-merged-child' ]
-               ];
-       }
-
        /**
         * @covers BagOStuff::changeTTL
         */
index e188ba8..b1d4fad 100644 (file)
@@ -149,8 +149,6 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase {
                                [ '', null, null, '', true ],
                        'dontcaredb+dontcaredbschema+prefix' =>
                                [ 'mywiki-mediawiki-prefix_', null, null, 'prefix_', false ],
-                       'dontcaredb+schema+prefix' =>
-                               [ 'mywiki-schema-prefix_', null, 'schema', 'prefix_', false ],
                        'db+dontcareschema+prefix' =>
                                [ 'mywiki-schema-prefix_', 'mywiki', null, 'prefix_', false ],
                        'postgres-db-jobqueue' =>
@@ -181,8 +179,6 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase {
                                [ 'mywiki-schema-prefix_', 'thatwiki', 'schema', 'prefix_' ],
                        'dontcaredb+dontcaredbschema+prefix' =>
                                [ 'thatwiki-mediawiki-otherprefix_', null, null, 'prefix_' ],
-                       'dontcaredb+schema+prefix' =>
-                               [ 'mywiki-otherschema-prefix_', null, 'schema', 'prefix_' ],
                        'db+dontcareschema+prefix' =>
                                [ 'notmywiki-schema-prefix_', 'mywiki', null, 'prefix_' ],
                ];
@@ -202,6 +198,20 @@ class DatabaseDomainTest extends PHPUnit\Framework\TestCase {
                $this->assertFalse( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
        }
 
+       /**
+        * @expectedException InvalidArgumentException
+        */
+       public function testSchemaWithNoDB1() {
+               new DatabaseDomain( null, 'schema', '' );
+       }
+
+       /**
+        * @expectedException InvalidArgumentException
+        */
+       public function testSchemaWithNoDB2() {
+               DatabaseDomain::newFromId( '-schema-prefix' );
+       }
+
        /**
         * @covers Wikimedia\Rdbms\DatabaseDomain::isUnspecified
         */
index f81e9bb..8b24791 100644 (file)
@@ -13,6 +13,8 @@ use Wikimedia\Rdbms\DatabaseMssql;
 use Wikimedia\Rdbms\DBUnexpectedError;
 
 class DatabaseTest extends PHPUnit\Framework\TestCase {
+       /** @var DatabaseTestHelper */
+       private $db;
 
        use MediaWikiCoversValidator;
 
@@ -629,6 +631,10 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::dbSchema
         */
        public function testSchemaAndPrefixMutators() {
+               $ud = DatabaseDomain::newUnspecified();
+
+               $this->assertEquals( $ud->getId(), $this->db->getDomainID() );
+
                $old = $this->db->tablePrefix();
                $oldDomain = $this->db->getDomainId();
                $this->assertInternalType( 'string', $old, 'Prefix is string' );
@@ -643,11 +649,27 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
                $oldDomain = $this->db->getDomainId();
                $this->assertInternalType( 'string', $old, 'Schema is string' );
                $this->assertSame( $old, $this->db->dbSchema(), "Schema unchanged" );
+
+               $this->db->selectDB( 'y' );
                $this->assertSame( $old, $this->db->dbSchema( 'xxx' ) );
                $this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" );
                $this->db->dbSchema( $old );
                $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
-               $this->assertSame( $oldDomain, $this->db->getDomainId() );
+               $this->assertSame( "y", $this->db->getDomainId() );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::tablePrefix
+        * @covers Wikimedia\Rdbms\Database::dbSchema
+        * @expectedException DBUnexpectedError
+        */
+       public function testSchemaWithNoDB() {
+               $ud = DatabaseDomain::newUnspecified();
+
+               $this->assertEquals( $ud->getId(), $this->db->getDomainID() );
+               $this->assertSame( '', $this->db->dbSchema() );
+
+               $this->db->dbSchema( 'xxx' );
        }
 
        /**
index 50b9421..825c9b9 100644 (file)
@@ -4,110 +4,12 @@ use Wikimedia\TestingAccessWrapper;
 
 /**
  * @group ResourceLoader
+ * @covers ResourceLoaderClientHtml
  */
 class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase {
 
        use MediaWikiCoversValidator;
 
-       protected static function expandVariables( $text ) {
-               return strtr( $text, [
-                       '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
-               ] );
-       }
-
-       protected static function makeContext( $extraQuery = [] ) {
-               $conf = new HashConfig( [
-                       'ResourceLoaderSources' => [],
-                       'ResourceModuleSkinStyles' => [],
-                       'ResourceModules' => [],
-                       'EnableJavaScriptTest' => false,
-                       'LoadScript' => '/w/load.php',
-               ] );
-               return new ResourceLoaderContext(
-                       new ResourceLoader( $conf ),
-                       new FauxRequest( array_merge( [
-                               'lang' => 'nl',
-                               'skin' => 'fallback',
-                               'user' => 'Example',
-                               'target' => 'phpunit',
-                       ], $extraQuery ) )
-               );
-       }
-
-       protected static function makeModule( array $options = [] ) {
-               return new ResourceLoaderTestModule( $options );
-       }
-
-       protected static function makeSampleModules() {
-               $modules = [
-                       'test' => [],
-                       'test.private' => [ 'group' => 'private' ],
-                       'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
-                       'test.shouldembed' => [ 'shouldEmbed' => true ],
-                       'test.user' => [ 'group' => 'user' ],
-
-                       'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
-                       'test.styles.mixed' => [],
-                       'test.styles.noscript' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'group' => 'noscript',
-                       ],
-                       'test.styles.user' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'group' => 'user',
-                       ],
-                       'test.styles.user.empty' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'group' => 'user',
-                               'isKnownEmpty' => true,
-                       ],
-                       'test.styles.private' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'group' => 'private',
-                               'styles' => '.private{}',
-                       ],
-                       'test.styles.shouldembed' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'shouldEmbed' => true,
-                               'styles' => '.shouldembed{}',
-                       ],
-                       'test.styles.deprecated' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'deprecated' => 'Deprecation message.',
-                       ],
-
-                       'test.scripts' => [],
-                       'test.scripts.user' => [ 'group' => 'user' ],
-                       'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
-                       'test.scripts.raw' => [ 'isRaw' => true ],
-                       'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
-
-                       'test.ordering.a' => [ 'shouldEmbed' => false ],
-                       'test.ordering.b' => [ 'shouldEmbed' => false ],
-                       'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
-                       'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
-                       'test.ordering.e' => [ 'shouldEmbed' => false ],
-               ];
-               return array_map( function ( $options ) {
-                       return self::makeModule( $options );
-               }, $modules );
-       }
-
-       /**
-        * @covers ResourceLoaderClientHtml::getDocumentAttributes
-        */
-       public function testGetDocumentAttributes() {
-               $client = new ResourceLoaderClientHtml( self::makeContext() );
-               $this->assertInternalType( 'array', $client->getDocumentAttributes() );
-       }
-
-       /**
-        * @covers ResourceLoaderClientHtml::__construct
-        * @covers ResourceLoaderClientHtml::setModules
-        * @covers ResourceLoaderClientHtml::setModuleStyles
-        * @covers ResourceLoaderClientHtml::getData
-        * @covers ResourceLoaderClientHtml::getContext
-        */
        public function testGetData() {
                $context = self::makeContext();
                $context->getResourceLoader()->register( self::makeSampleModules() );
@@ -133,10 +35,22 @@ class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase {
 
                $expected = [
                        'states' => [
+                               // The below are NOT queued for loading via `mw.loader.load(Array)`.
+                               // Instead we tell the client to set their state to "loading" so that
+                               // if they are needed as dependencies, the client will not try to
+                               // load them on-demand, because the server is taking care of them already.
+                               // Either:
+                               // - Embedded as inline scripts in the HTML (e.g. user-private code, and
+                               //   previews). Once that script tag is reached, the state is "loaded".
+                               // - Loaded directly from the HTML with a dedicated HTTP request (e.g.
+                               //   user scripts, which vary by a 'user' and 'version' parameter that
+                               //   the static user-agnostic startup module won't have).
                                'test.private' => 'loading',
-                               'test.shouldembed.empty' => 'ready',
                                'test.shouldembed' => 'loading',
                                'test.user' => 'loading',
+                               // The below are known to the server to be empty scripts, or to be
+                               // synchronously loaded stylesheets. These start in the "ready" state.
+                               'test.shouldembed.empty' => 'ready',
                                'test.styles.pure' => 'ready',
                                'test.styles.user.empty' => 'ready',
                                'test.styles.private' => 'ready',
@@ -171,13 +85,6 @@ Deprecation message.' ]
                $this->assertEquals( $expected, $access->getData() );
        }
 
-       /**
-        * @covers ResourceLoaderClientHtml::setConfig
-        * @covers ResourceLoaderClientHtml::setExemptStates
-        * @covers ResourceLoaderClientHtml::getHeadHtml
-        * @covers ResourceLoaderClientHtml::getLoad
-        * @covers ResourceLoader::makeLoaderStateScript
-        */
        public function testGetHeadHtml() {
                $context = self::makeContext();
                $context->getResourceLoader()->register( self::makeSampleModules() );
@@ -218,8 +125,6 @@ Deprecation message.' ]
 
        /**
         * Confirm that 'target' is passed down to the startup module's load url.
-        *
-        * @covers ResourceLoaderClientHtml::getHeadHtml
         */
        public function testGetHeadHtmlWithTarget() {
                $client = new ResourceLoaderClientHtml(
@@ -237,8 +142,6 @@ Deprecation message.' ]
 
        /**
         * Confirm that 'safemode' is passed down to startup.
-        *
-        * @covers ResourceLoaderClientHtml::getHeadHtml
         */
        public function testGetHeadHtmlWithSafemode() {
                $client = new ResourceLoaderClientHtml(
@@ -256,8 +159,6 @@ Deprecation message.' ]
 
        /**
         * Confirm that a null 'target' is the same as no target.
-        *
-        * @covers ResourceLoaderClientHtml::getHeadHtml
         */
        public function testGetHeadHtmlWithNullTarget() {
                $client = new ResourceLoaderClientHtml(
@@ -273,10 +174,6 @@ Deprecation message.' ]
                $this->assertEquals( $expected, $client->getHeadHtml() );
        }
 
-       /**
-        * @covers ResourceLoaderClientHtml::getBodyHtml
-        * @covers ResourceLoaderClientHtml::getLoad
-        */
        public function testGetBodyHtml() {
                $context = self::makeContext();
                $context->getResourceLoader()->register( self::makeSampleModules() );
@@ -427,15 +324,9 @@ Deprecation message.' ]
 
        /**
         * @dataProvider provideMakeLoad
-        * @covers ResourceLoaderClientHtml::makeLoad
-        * @covers ResourceLoaderClientHtml::makeContext
-        * @covers ResourceLoader::makeModuleResponse
+        * @covers ResourceLoaderClientHtml
         * @covers ResourceLoaderModule::getModuleContent
-        * @covers ResourceLoader::getCombinedVersion
-        * @covers ResourceLoader::createLoaderURL
-        * @covers ResourceLoader::createLoaderQuery
-        * @covers ResourceLoader::makeLoaderQuery
-        * @covers ResourceLoader::makeInlineScript
+        * @covers ResourceLoader
         */
        public function testMakeLoad(
                array $contextQuery,
@@ -450,4 +341,92 @@ Deprecation message.' ]
                $expected = self::expandVariables( $expected );
                $this->assertEquals( $expected, (string)$actual );
        }
+
+       public function testGetDocumentAttributes() {
+               $client = new ResourceLoaderClientHtml( self::makeContext() );
+               $this->assertInternalType( 'array', $client->getDocumentAttributes() );
+       }
+
+       private static function expandVariables( $text ) {
+               return strtr( $text, [
+                       '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
+               ] );
+       }
+
+       private static function makeContext( $extraQuery = [] ) {
+               $conf = new HashConfig( [
+                       'ResourceModuleSkinStyles' => [],
+                       'ResourceModules' => [],
+                       'EnableJavaScriptTest' => false,
+                       'LoadScript' => '/w/load.php',
+               ] );
+               return new ResourceLoaderContext(
+                       new ResourceLoader( $conf ),
+                       new FauxRequest( array_merge( [
+                               'lang' => 'nl',
+                               'skin' => 'fallback',
+                               'user' => 'Example',
+                               'target' => 'phpunit',
+                       ], $extraQuery ) )
+               );
+       }
+
+       private static function makeModule( array $options = [] ) {
+               return new ResourceLoaderTestModule( $options );
+       }
+
+       private static function makeSampleModules() {
+               $modules = [
+                       'test' => [],
+                       'test.private' => [ 'group' => 'private' ],
+                       'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
+                       'test.shouldembed' => [ 'shouldEmbed' => true ],
+                       'test.user' => [ 'group' => 'user' ],
+
+                       'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
+                       'test.styles.mixed' => [],
+                       'test.styles.noscript' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'group' => 'noscript',
+                       ],
+                       'test.styles.user' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'group' => 'user',
+                       ],
+                       'test.styles.user.empty' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'group' => 'user',
+                               'isKnownEmpty' => true,
+                       ],
+                       'test.styles.private' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'group' => 'private',
+                               'styles' => '.private{}',
+                       ],
+                       'test.styles.shouldembed' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'shouldEmbed' => true,
+                               'styles' => '.shouldembed{}',
+                       ],
+                       'test.styles.deprecated' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'deprecated' => 'Deprecation message.',
+                       ],
+
+                       'test.scripts' => [],
+                       'test.scripts.user' => [ 'group' => 'user' ],
+                       'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
+                       'test.scripts.raw' => [ 'isRaw' => true ],
+                       'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
+
+                       'test.ordering.a' => [ 'shouldEmbed' => false ],
+                       'test.ordering.b' => [ 'shouldEmbed' => false ],
+                       'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
+                       'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
+                       'test.ordering.e' => [ 'shouldEmbed' => false ],
+               ];
+               return array_map( function ( $options ) {
+                       return self::makeModule( $options );
+               }, $modules );
+       }
 }
index 2ee85b5..d4b5ed6 100644 (file)
@@ -459,13 +459,12 @@ mw.loader.register( [
         * @covers ResourceLoader::makeLoaderRegisterScript
         */
        public function testGetModuleRegistrations( $case ) {
-               if ( isset( $case['sources'] ) ) {
-                       $this->setMwGlobals( 'wgResourceLoaderSources', $case['sources'] );
-               }
-
                $extraQuery = $case['extraQuery'] ?? [];
                $context = $this->getResourceLoaderContext( $extraQuery );
                $rl = $context->getResourceLoader();
+               if ( isset( $case['sources'] ) ) {
+                       $rl->addSource( $case['sources'] );
+               }
                $rl->register( $case['modules'] );
                $module = new ResourceLoaderStartUpModule();
                $out = ltrim( $case['out'], "\n" );
index 5941c6e..3f7925f 100644 (file)
@@ -624,7 +624,6 @@ END
         * @covers ResourceLoader::getLoadScript
         */
        public function testGetLoadScript() {
-               $this->setMwGlobals( 'wgResourceLoaderSources', [] );
                $rl = new ResourceLoader();
                $sources = self::fakeSources();
                $rl->addSource( $sources );
index d54641b..805b793 100644 (file)
@@ -50,6 +50,7 @@ describe( 'Rollback with confirmation', function () {
 
        it( 'should offer a way to cancel rollbacks', function () {
                HistoryPage.rollback.click();
+               browser.pause( 300 );
                HistoryPage.rollbackConfirmableNo.click();
 
                browser.pause( 500 );