Merge "objectcache: Fix grammar error in BagOStuff::getWithToken doc"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 22 Oct 2015 02:33:16 +0000 (02:33 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 22 Oct 2015 02:33:16 +0000 (02:33 +0000)
39 files changed:
RELEASE-NOTES-1.27
includes/DefaultSettings.php
includes/EditPage.php
includes/Hooks.php
includes/MediaWiki.php
includes/TemplateParser.php
includes/api/ApiQueryRevisionsBase.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/cache/MessageBlobStore.php
includes/db/loadbalancer/LoadMonitorMySQL.php
includes/deferred/DeferredUpdates.php
includes/filebackend/SwiftFileBackend.php
includes/libs/objectcache/APCBagOStuff.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/EmptyBagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/WinCacheBagOStuff.php
includes/libs/objectcache/XCacheBagOStuff.php
includes/objectcache/MemcachedBagOStuff.php
includes/objectcache/MemcachedPeclBagOStuff.php
includes/objectcache/MultiWriteBagOStuff.php
includes/objectcache/RedisBagOStuff.php
includes/objectcache/SqlBagOStuff.php
includes/page/Article.php
includes/specials/SpecialExpandTemplates.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/doMaintenance.php
resources/Resources.php
resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js
resources/src/mediawiki/api.js
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css [new file with mode: 0644]
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
tests/parser/parserTests.txt
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/objectcache/MultiWriteBagOStuffTest.php

index 7d4ac3d..617af0c 100644 (file)
@@ -76,6 +76,8 @@ production.
   into the redirect destination.
 * prop=imageinfo&iiprop=uploadwarning will no longer include the possibility of
   "was-deleted" warning.
+* Added difftotextpst to query=revisions which preforms a pre-save transform on
+  the text before diffing it.
 
 === Action API internal changes in 1.27 ===
 * ApiQueryORM removed.
index 5eaeffa..71fe83d 100644 (file)
@@ -3365,10 +3365,11 @@ $wgEdititis = false;
 $wgSend404Code = true;
 
 /**
- * The $wgShowRollbackEditCount variable is used to show how many edits will be
- * rollback. The numeric value of the variable are the limit up to are counted.
- * If the value is false or 0, the edits are not counted. Disabling this will
- * furthermore prevent MediaWiki from hiding some useless rollback links.
+ * The $wgShowRollbackEditCount variable is used to show how many edits can be rolled back.
+ * The numeric value of the variable controls how many edits MediaWiki will look back to
+ * determine whether a rollback is allowed (by checking that they are all from the same author).
+ * If the value is false or 0, the edits are not counted. Disabling this will prevent MediaWiki
+ * from hiding some useless rollback links.
  *
  * @since 1.20
  */
index 8571cd7..81f35f9 100644 (file)
@@ -537,6 +537,20 @@ class EditPage {
                        return;
                }
 
+               $revision = $this->mArticle->getRevisionFetched();
+               // Disallow editing revisions with content models different from the current one
+               if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
+                       $this->displayViewSourcePage(
+                               $this->getContentObject(),
+                               wfMessage(
+                                       'contentmodelediterror',
+                                       $revision->getContentModel(),
+                                       $this->contentModel
+                               )->plain()
+                       );
+                       return;
+               }
+
                $this->isConflict = false;
                // css / js subpages of user pages get a special treatment
                $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
@@ -647,6 +661,20 @@ class EditPage {
                        throw new PermissionsError( $action, $permErrors );
                }
 
+               $this->displayViewSourcePage(
+                       $content,
+                       $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
+               );
+       }
+
+       /**
+        * Display a read-only View Source page
+        * @param Content $content content object
+        * @param string $errorMessage additional wikitext error message to display
+        */
+       protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
+               global $wgOut;
+
                Hooks::run( 'EditPage::showReadOnlyForm:initial', array( $this, &$wgOut ) );
 
                $wgOut->setRobotPolicy( 'noindex,nofollow' );
@@ -658,8 +686,10 @@ class EditPage {
                $wgOut->addHTML( $this->editFormPageTop );
                $wgOut->addHTML( $this->editFormTextTop );
 
-               $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) );
-               $wgOut->addHTML( "<hr />\n" );
+               if ( $errorMessage !== '' ) {
+                       $wgOut->addWikiText( $errorMessage );
+                       $wgOut->addHTML( "<hr />\n" );
+               }
 
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
@@ -667,7 +697,13 @@ class EditPage {
                        $text = $this->textbox1;
                        $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
-                       $text = $this->toEditText( $content );
+                       try {
+                               $text = $this->toEditText( $content );
+                       } catch ( MWException $e ) {
+                               # Serialize using the default format if the content model is not supported
+                               # (e.g. for an old revision with a different model)
+                               $text = $content->serialize();
+                       }
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
 
index a414562..980d350 100644 (file)
@@ -232,8 +232,11 @@ class Hooks {
 
        /**
         * Handle PHP errors issued inside a hook. Catch errors that have to do
-        * with a function expecting a reference, and pass all others through to
-        * MWExceptionHandler::handleError() for default processing.
+        * with a function expecting a reference, missing arguments, or wrong argument
+        * types. Pass all others through to to the default error handler.
+        *
+        * This is useful for throwing errors for major callback invocation errors
+        * (with regard to parameter signature) which PHP just gives warnings for.
         *
         * @since 1.18
         *
@@ -243,13 +246,14 @@ class Hooks {
         * @return bool
         */
        public static function hookErrorHandler( $errno, $errstr ) {
-               if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) {
+               if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false
+                       || strpos( $errstr, 'Missing argument ' ) !== false
+                       || strpos( $errstr, ' expects parameter ' ) !== false
+               ) {
                        throw new MWHookException( $errstr, $errno );
                }
 
-               // Delegate unhandled errors to the default MW handler
-               return call_user_func_array(
-                       'MWExceptionHandler::handleError', func_get_args()
-               );
+               // Delegate unhandled errors to the default handlers
+               return false;
        }
 }
index 3399790..676108c 100644 (file)
@@ -515,6 +515,7 @@ class MediaWiki {
                if ( $factory->laggedSlaveUsed() ) {
                        $maxAge = $this->config->get( 'CdnMaxageLagged' );
                        $this->context->getOutput()->lowerCdnMaxage( $maxAge );
+                       $request->response()->header( "X-Database-Lagged: true" );
                        wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
                }
        }
@@ -689,7 +690,7 @@ class MediaWiki {
                Profiler::instance()->getTransactionProfiler()->resetExpectations();
 
                // Do any deferred jobs
-               DeferredUpdates::doUpdates( 'commit', 'enqueue' );
+               DeferredUpdates::doUpdates( 'enqueue' );
 
                // Make sure any lazy jobs are pushed
                JobQueueGroup::pushLazyJobs();
index 3c62c14..44d264d 100644 (file)
@@ -41,7 +41,7 @@ class TemplateParser {
         * @param boolean $forceRecompile
         */
        public function __construct( $templateDir = null, $forceRecompile = false ) {
-               $this->templateDir = $templateDir ? $templateDir : __DIR__ . '/templates';
+               $this->templateDir = $templateDir ?: __DIR__ . '/templates';
                $this->forceRecompile = $forceRecompile;
        }
 
@@ -49,7 +49,7 @@ class TemplateParser {
         * Constructs the location of the the source Mustache template
         * @param string $templateName The name of the template
         * @return string
-        * @throws UnexpectedValueException Disallows upwards directory traversal via $templateName
+        * @throws UnexpectedValueException If $templateName attempts upwards directory traversal
         */
        protected function getTemplateFilename( $templateName ) {
                // Prevent upwards directory traversal using same methods as Title::secureAndSplit
@@ -103,10 +103,8 @@ class TemplateParser {
 
                if ( $secretKey ) {
                        // See if the compiled PHP code is stored in cache.
-                       // CACHE_ACCEL throws an exception if no suitable object cache is present, so fall
-                       // back to CACHE_ANYTHING.
                        $cache = ObjectCache::newAccelerator( CACHE_ANYTHING );
-                       $key = wfMemcKey( 'template', $templateName, $fastHash );
+                       $key = $cache->makeKey( 'template', $templateName, $fastHash );
                        $code = $this->forceRecompile ? null : $cache->get( $key );
 
                        if ( !$code ) {
index f8dbf23..609efb0 100644 (file)
@@ -31,8 +31,8 @@
  */
 abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
 
-       protected $limit, $diffto, $difftotext, $expandTemplates, $generateXML, $section,
-               $parseContent, $fetchContent, $contentFormat, $setParsedLimit = true;
+       protected $limit, $diffto, $difftotext, $difftotextpst, $expandTemplates, $generateXML,
+               $section, $parseContent, $fetchContent, $contentFormat, $setParsedLimit = true;
 
        protected $fld_ids = false, $fld_flags = false, $fld_timestamp = false,
                $fld_size = false, $fld_sha1 = false, $fld_comment = false,
@@ -61,6 +61,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
        protected function parseParameters( $params ) {
                if ( !is_null( $params['difftotext'] ) ) {
                        $this->difftotext = $params['difftotext'];
+                       $this->difftotextpst = $params['difftotextpst'];
                } elseif ( !is_null( $params['diffto'] ) ) {
                        if ( $params['diffto'] == 'cur' ) {
                                $params['diffto'] = 0;
@@ -385,6 +386,11 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                                                        $this->contentFormat
                                                );
 
+                                               if ( $this->difftotextpst ) {
+                                                       $popts = ParserOptions::newFromContext( $this->getContext() );
+                                                       $difftocontent = $difftocontent->preSaveTransform( $title, $user, $popts );
+                                               }
+
                                                $engine = $handler->createDifferenceEngine( $context );
                                                $engine->setContent( $content, $difftocontent );
                                        }
@@ -490,6 +496,10 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                                ApiBase::PARAM_DFLT => null,
                                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotext',
                        ),
+                       'difftotextpst' => array(
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotextpst',
+                       ),
                        'contentformat' => array(
                                ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
                                ApiBase::PARAM_DFLT => null,
index 589e5fd..450d83a 100644 (file)
        "apihelp-query+revisions+base-param-section": "Only retrieve the content of this section number.",
        "apihelp-query+revisions+base-param-diffto": "Revision ID to diff each revision to. Use <kbd>prev</kbd>, <kbd>next</kbd> and <kbd>cur</kbd> for the previous, next and current revision respectively.",
        "apihelp-query+revisions+base-param-difftotext": "Text to diff each revision to. Only diffs a limited number of revisions. Overrides <var>$1diffto</var>. If <var>$1section</var> is set, only that section will be diffed against this text.",
+       "apihelp-query+revisions+base-param-difftotextpst": "Perform a pre-save transform on the text before diffing it. Only valid when used with <var>$1difftotext</var>.",
        "apihelp-query+revisions+base-param-contentformat": "Serialization format used for <var>$1difftotext</var> and expected for output of content.",
 
        "apihelp-query+search-description": "Perform a full text search.",
index ea4251c..947c89b 100644 (file)
        "apihelp-query+revisions+base-param-section": "{{doc-apihelp-param|query+revisions+base|section|description=the \"section\" parameter to revision querying modules|noseealso=1}}",
        "apihelp-query+revisions+base-param-diffto": "{{doc-apihelp-param|query+revisions+base|diffto|description=the \"diffto\" parameter to revision querying modules|noseealso=1}}",
        "apihelp-query+revisions+base-param-difftotext": "{{doc-apihelp-param|query+revisions+base|difftotext|description=the \"difftotext\" parameter to revision querying modules|noseealso=1}}",
+       "apihelp-query+revisions+base-param-difftotextpst": "{{doc-apihelp-param|query+revisions+base|difftotextpst|description=the \"difftotextpst\" parameter to revision querying modules|noseealso=1}}",
        "apihelp-query+revisions+base-param-contentformat": "{{doc-apihelp-param|query+revisions+base|contentformat|description=the \"contentformat\" parameter to revision querying modules|noseealso=1}}",
        "apihelp-query+search-description": "{{doc-apihelp-description|query+search}}",
        "apihelp-query+search-param-search": "{{doc-apihelp-param|query+search|search}}",
index 63d8c7e..b7c70c1 100644 (file)
@@ -374,7 +374,6 @@ class MessageBlobStore {
                        return array();
                }
 
-               $config = $resourceLoader->getConfig();
                $retval = array();
                $dbr = wfGetDB( DB_SLAVE );
                $res = $dbr->select( 'msg_resource',
@@ -390,13 +389,10 @@ class MessageBlobStore {
                                throw new MWException( __METHOD__ . ' passed an invalid module name' );
                        }
 
-                       // Update the module's blobs if the set of messages changed or if the blob is
-                       // older than the CacheEpoch setting
-                       $keys = array_keys( FormatJson::decode( $row->mr_blob, true ) );
-                       $values = array_values( array_unique( $module->getMessages() ) );
-                       if ( $keys !== $values
-                               || wfTimestamp( TS_MW, $row->mr_timestamp ) <= $config->get( 'CacheEpoch' )
-                       ) {
+                       // Update the module's blob if the list of messages changed
+                       $blobKeys = array_keys( FormatJson::decode( $row->mr_blob, true ) );
+                       $moduleMsgs = array_values( array_unique( $module->getMessages() ) );
+                       if ( $blobKeys !== $moduleMsgs ) {
                                $retval[$row->mr_resource] = $this->updateModule( $row->mr_resource, $module, $lang );
                        } else {
                                $retval[$row->mr_resource] = $row->mr_blob;
index 39ced1b..f49e965 100644 (file)
@@ -37,7 +37,7 @@ class LoadMonitorMySQL implements LoadMonitor {
                $this->parent = $parent;
 
                $this->srvCache = ObjectCache::newAccelerator( 'hash' );
-               $this->mainCache = wfGetMainCache();
+               $this->mainCache = ObjectCache::getLocalClusterInstance();
        }
 
        public function scaleLoads( &$loads, $group = false, $wiki = false ) {
index 8f8ed2e..0194a61 100644 (file)
@@ -94,11 +94,17 @@ class DeferredUpdates {
        /**
         * Do any deferred updates and clear the list
         *
-        * @param string $commit Set to 'commit' to commit after every update to
         * @param string $mode Use "enqueue" to use the job queue when possible [Default: run]
         *   prevent lock contention
+        * @param string $oldMode Unused
         */
-       public static function doUpdates( $commit = '', $mode = 'run' ) {
+       public static function doUpdates( $mode = 'run', $oldMode = '' ) {
+               // B/C for ( $commit, $mode ) args
+               $mode = $oldMode ?: $mode;
+               if ( $mode === 'commit' ) {
+                       $mode = 'run';
+               }
+
                $updates = self::$updates;
 
                while ( count( $updates ) ) {
@@ -121,9 +127,7 @@ class DeferredUpdates {
                        foreach ( $otherUpdates as $update ) {
                                try {
                                        $update->doUpdate();
-                                       if ( $commit === 'commit' ) {
-                                               wfGetLBFactory()->commitMasterChanges();
-                                       }
+                                       wfGetLBFactory()->commitMasterChanges();
                                } catch ( Exception $e ) {
                                        // We don't want exceptions thrown during deferred updates to
                                        // be reported to the user since the output is already sent.
index 408194f..e72d026 100644 (file)
@@ -136,9 +136,10 @@ class SwiftFileBackend extends FileBackendStore {
                // Cache auth token information to avoid RTTs
                if ( !empty( $config['cacheAuthInfo'] ) ) {
                        if ( PHP_SAPI === 'cli' ) {
-                               $this->srvCache = wfGetMainCache(); // preferrably memcached
+                               // Preferrably memcached
+                               $this->srvCache = ObjectCache::getLocalClusterInstance();
                        } else {
-                               // look for APC, XCache, WinCache, ect...
+                               // Look for APC, XCache, WinCache, ect...
                                $this->srvCache = ObjectCache::newAccelerator( CACHE_NONE );
                        }
                } else {
index 522c5d7..b5419b7 100644 (file)
@@ -40,7 +40,7 @@ class APCBagOStuff extends BagOStuff {
                return $val;
        }
 
-       public function set( $key, $value, $exptime = 0 ) {
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                apc_store( $key . self::KEY_SUFFIX, $value, $exptime );
 
                return true;
index fc75f56..5dfec6e 100644 (file)
@@ -67,6 +67,8 @@ abstract class BagOStuff implements LoggerAwareInterface {
        /** Bitfield constants for get()/getMulti() */
        const READ_LATEST = 1; // use latest data for replicated stores
        const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
+       /** Bitfield constants for set()/merge() */
+       const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
 
        public function __construct( array $params = array() ) {
                if ( isset( $params['logger'] ) ) {
@@ -158,6 +160,7 @@ abstract class BagOStuff implements LoggerAwareInterface {
         * @param mixed $casToken
         * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
         * @return mixed Returns false on failure and if the item does not exist
+        * @throws Exception
         */
        protected function getWithToken( $key, &$casToken, $flags = 0 ) {
                throw new Exception( __METHOD__ . ' not implemented.' );
@@ -169,9 +172,10 @@ abstract class BagOStuff implements LoggerAwareInterface {
         * @param string $key
         * @param mixed $value
         * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants
         * @return bool Success
         */
-       abstract public function set( $key, $value, $exptime = 0 );
+       abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
 
        /**
         * Delete an item
@@ -191,15 +195,16 @@ abstract class BagOStuff implements LoggerAwareInterface {
         * @param callable $callback Callback method to be executed
         * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
         * @param int $attempts The amount of times to attempt a merge in case of failure
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants
         * @return bool Success
         * @throws InvalidArgumentException
         */
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+       public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                if ( !is_callable( $callback ) ) {
                        throw new InvalidArgumentException( "Got invalid callback." );
                }
 
-               return $this->mergeViaLock( $key, $callback, $exptime, $attempts );
+               return $this->mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
        }
 
        /**
@@ -262,9 +267,10 @@ abstract class BagOStuff implements LoggerAwareInterface {
         * @param callable $callback Callback method to be executed
         * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
         * @param int $attempts The amount of times to attempt a merge in case of failure
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants
         * @return bool Success
         */
-       protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10 ) {
+       protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                if ( !$this->lock( $key, 6 ) ) {
                        return false;
                }
@@ -279,7 +285,7 @@ abstract class BagOStuff implements LoggerAwareInterface {
                        if ( $value === false ) {
                                $success = true; // do nothing
                        } else {
-                               $success = $this->set( $key, $value, $exptime ); // set the new value
+                               $success = $this->set( $key, $value, $exptime, $flags ); // set the new value
                        }
                }
 
index bef0456..4321b25 100644 (file)
@@ -31,7 +31,7 @@ class EmptyBagOStuff extends BagOStuff {
                return false;
        }
 
-       public function set( $key, $value, $exp = 0 ) {
+       public function set( $key, $value, $exp = 0, $flags = 0 ) {
                return true;
        }
 
@@ -39,7 +39,7 @@ class EmptyBagOStuff extends BagOStuff {
                return true;
        }
 
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+       public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                return true; // faster
        }
 }
index d4044e9..94bc95f 100644 (file)
@@ -60,7 +60,7 @@ class HashBagOStuff extends BagOStuff {
                return $this->bag[$key][0];
        }
 
-       public function set( $key, $value, $exptime = 0 ) {
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                $this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) );
                return true;
        }
index b98c982..40ac1bb 100644 (file)
@@ -84,8 +84,8 @@ class ReplicatedBagOStuff extends BagOStuff {
                        : $this->readStore->getMulti( $keys, $flags );
        }
 
-       public function set( $key, $value, $exptime = 0 ) {
-               return $this->writeStore->set( $key, $value, $exptime );
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+               return $this->writeStore->set( $key, $value, $exptime, $flags );
        }
 
        public function delete( $key ) {
@@ -112,8 +112,8 @@ class ReplicatedBagOStuff extends BagOStuff {
                return $this->writeStore->unlock( $key );
        }
 
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
-               return $this->writeStore->merge( $key, $callback, $exptime, $attempts );
+       public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
+               return $this->writeStore->merge( $key, $callback, $exptime, $attempts, $flags );
        }
 
        public function getLastError() {
index 592565f..8b01610 100644 (file)
@@ -46,7 +46,7 @@ class WinCacheBagOStuff extends BagOStuff {
                return $val;
        }
 
-       public function set( $key, $value, $expire = 0 ) {
+       public function set( $key, $value, $expire = 0, $flags = 0 ) {
                $result = wincache_ucache_set( $key, serialize( $value ), $expire );
 
                /* wincache_ucache_set returns an empty array on success if $value
@@ -64,7 +64,7 @@ class WinCacheBagOStuff extends BagOStuff {
                return true;
        }
 
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+       public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                if ( !is_callable( $callback ) ) {
                        throw new Exception( "Got invalid callback." );
                }
index dc34f55..47c2906 100644 (file)
@@ -44,7 +44,7 @@ class XCacheBagOStuff extends BagOStuff {
                return $val;
        }
 
-       public function set( $key, $value, $expire = 0 ) {
+       public function set( $key, $value, $expire = 0, $flags = 0 ) {
                if ( !$this->isInteger( $value ) ) {
                        $value = serialize( $value );
                }
index 412f017..95f5c8d 100644 (file)
@@ -27,6 +27,7 @@
  * @ingroup Cache
  */
 class MemcachedBagOStuff extends BagOStuff {
+       /** @var MWMemcached|Memcached */
        protected $client;
 
        /**
@@ -67,49 +68,26 @@ class MemcachedBagOStuff extends BagOStuff {
                return $this->client->get( $this->encodeKey( $key ), $casToken );
        }
 
-       /**
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @return bool
-        */
-       public function set( $key, $value, $exptime = 0 ) {
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                return $this->client->set( $this->encodeKey( $key ), $value,
                        $this->fixExpiry( $exptime ) );
        }
 
-       /**
-        * @param mixed $casToken
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @return bool
-        */
        protected function cas( $casToken, $key, $value, $exptime = 0 ) {
                return $this->client->cas( $casToken, $this->encodeKey( $key ),
                        $value, $this->fixExpiry( $exptime ) );
        }
 
-       /**
-        * @param string $key
-        * @return bool
-        */
        public function delete( $key ) {
                return $this->client->delete( $this->encodeKey( $key ) );
        }
 
-       /**
-        * @param string $key
-        * @param int $value
-        * @param int $exptime (default 0)
-        * @return mixed
-        */
        public function add( $key, $value, $exptime = 0 ) {
                return $this->client->add( $this->encodeKey( $key ), $value,
                        $this->fixExpiry( $exptime ) );
        }
 
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+       public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                if ( !is_callable( $callback ) ) {
                        throw new Exception( "Got invalid callback." );
                }
index a7b48a2..365236d 100644 (file)
@@ -122,33 +122,16 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $result;
        }
 
-       /**
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @return bool
-        */
-       public function set( $key, $value, $exptime = 0 ) {
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                $this->debugLog( "set($key)" );
                return $this->checkResult( $key, parent::set( $key, $value, $exptime ) );
        }
 
-       /**
-        * @param float $casToken
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @return bool
-        */
        protected function cas( $casToken, $key, $value, $exptime = 0 ) {
                $this->debugLog( "cas($key)" );
                return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
        }
 
-       /**
-        * @param string $key
-        * @return bool
-        */
        public function delete( $key ) {
                $this->debugLog( "delete($key)" );
                $result = parent::delete( $key );
@@ -160,33 +143,17 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                }
        }
 
-       /**
-        * @param string $key
-        * @param int $value
-        * @param int $exptime
-        * @return mixed
-        */
        public function add( $key, $value, $exptime = 0 ) {
                $this->debugLog( "add($key)" );
                return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
        }
 
-       /**
-        * @param string $key
-        * @param int $value
-        * @return mixed
-        */
        public function incr( $key, $value = 1 ) {
                $this->debugLog( "incr($key)" );
                $result = $this->client->increment( $key, $value );
                return $this->checkResult( $key, $result );
        }
 
-       /**
-        * @param string $key
-        * @param int $value
-        * @return mixed
-        */
        public function decr( $key, $value = 1 ) {
                $this->debugLog( "decr($key)" );
                $result = $this->client->decrement( $key, $value );
index 50dd215..73bdabd 100644 (file)
@@ -98,14 +98,20 @@ class MultiWriteBagOStuff extends BagOStuff {
                );
        }
 
-       /**
-        * @param bool $debug
-        */
        public function setDebug( $debug ) {
-               $this->doWrite( self::ALL, 'setDebug', $debug );
+               foreach ( $this->caches as $cache ) {
+                       $cache->setDebug( $debug );
+               }
        }
 
        protected function doGet( $key, $flags = 0 ) {
+               if ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) {
+                       // If the latest write was a delete(), we do NOT want to fallback
+                       // to the other tiers and possibly see the old value. Also, this
+                       // is used by mergeViaLock(), which only needs to hit the primary.
+                       return $this->caches[0]->get( $key, $flags );
+               }
+
                $misses = 0; // number backends checked
                $value = false;
                foreach ( $this->caches as $cache ) {
@@ -120,89 +126,46 @@ class MultiWriteBagOStuff extends BagOStuff {
                        && $misses > 0
                        && ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED
                ) {
-                       $this->doWrite( $misses, 'set', $key, $value, self::UPGRADE_TTL );
+                       $this->doWrite( $misses, $this->asyncWrites, 'set', $key, $value, self::UPGRADE_TTL );
                }
 
                return $value;
        }
 
-       /**
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @return bool
-        */
-       public function set( $key, $value, $exptime = 0 ) {
-               return $this->doWrite( self::ALL, 'set', $key, $value, $exptime );
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+               $asyncWrites = ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC )
+                       ? false
+                       : $this->asyncWrites;
+
+               return $this->doWrite( self::ALL, $asyncWrites, 'set', $key, $value, $exptime );
        }
 
-       /**
-        * @param string $key
-        * @return bool
-        */
        public function delete( $key ) {
-               return $this->doWrite( self::ALL, 'delete', $key );
+               return $this->doWrite( self::ALL, $this->asyncWrites, 'delete', $key );
        }
 
-       /**
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @return bool
-        */
        public function add( $key, $value, $exptime = 0 ) {
-               return $this->doWrite( self::ALL, 'add', $key, $value, $exptime );
+               return $this->doWrite( self::ALL, $this->asyncWrites, 'add', $key, $value, $exptime );
        }
 
-       /**
-        * @param string $key
-        * @param int $value
-        * @return bool|null
-        */
        public function incr( $key, $value = 1 ) {
-               return $this->doWrite( self::ALL, 'incr', $key, $value );
+               return $this->doWrite( self::ALL, $this->asyncWrites, 'incr', $key, $value );
        }
 
-       /**
-        * @param string $key
-        * @param int $value
-        * @return bool
-        */
        public function decr( $key, $value = 1 ) {
-               return $this->doWrite( self::ALL, 'decr', $key, $value );
+               return $this->doWrite( self::ALL, $this->asyncWrites, 'decr', $key, $value );
        }
 
-       /**
-        * @param string $key
-        * @param int $timeout
-        * @param int $expiry
-        * @param string $rclass
-        * @return bool
-        */
        public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
-               // Lock only the first cache, to avoid deadlocks
+               // Only need to lock the first cache; also avoids deadlocks
                return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass );
        }
 
-       /**
-        * @param string $key
-        * @return bool
-        */
        public function unlock( $key ) {
+               // Only the first cache is locked
                return $this->caches[0]->unlock( $key );
        }
 
-       /**
-        * @param string $key
-        * @param callable $callback Callback method to be executed
-        * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
-        * @param int $attempts The amount of times to attempt a merge in case of failure
-        * @return bool Success
-        */
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
-               return $this->doWrite( self::ALL, 'merge', $key, $callback, $exptime );
-       }
-
        public function getLastError() {
                return $this->caches[0]->getLastError();
        }
@@ -215,20 +178,21 @@ class MultiWriteBagOStuff extends BagOStuff {
         * Apply a write method to the first $count backing caches
         *
         * @param integer $count
+        * @param bool $asyncWrites
         * @param string $method
         * @param mixed ...
         * @return bool
         */
-       protected function doWrite( $count, $method /*, ... */ ) {
+       protected function doWrite( $count, $asyncWrites, $method /*, ... */ ) {
                $ret = true;
-               $args = array_slice( func_get_args(), 2 );
+               $args = array_slice( func_get_args(), 3 );
 
                foreach ( $this->caches as $i => $cache ) {
                        if ( $i >= $count ) {
                                break; // ignore the lower tiers
                        }
 
-                       if ( $i == 0 || !$this->asyncWrites ) {
+                       if ( $i == 0 || !$asyncWrites ) {
                                // First store or in sync mode: write now and get result
                                if ( !call_user_func_array( array( $cache, $method ), $args ) ) {
                                        $ret = false;
index e6b3f9e..2c12fdf 100644 (file)
@@ -102,7 +102,7 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function set( $key, $value, $expiry = 0 ) {
+       public function set( $key, $value, $expiry = 0, $flags = 0 ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
index c2e5bd7..1f384c7 100644 (file)
@@ -289,11 +289,6 @@ class SqlBagOStuff extends BagOStuff {
                return $values;
        }
 
-       /**
-        * @param array $data
-        * @param int $expiry
-        * @return bool
-        */
        public function setMulti( array $data, $expiry = 0 ) {
                $keysByTable = array();
                foreach ( $data as $key => $value ) {
@@ -353,23 +348,10 @@ class SqlBagOStuff extends BagOStuff {
                return $result;
        }
 
-       /**
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @return bool
-        */
-       public function set( $key, $value, $exptime = 0 ) {
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                return $this->setMulti( array( $key => $value ), $exptime );
        }
 
-       /**
-        * @param mixed $casToken
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @return bool
-        */
        protected function cas( $casToken, $key, $value, $exptime = 0 ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
                try {
@@ -410,10 +392,6 @@ class SqlBagOStuff extends BagOStuff {
                return (bool)$db->affectedRows();
        }
 
-       /**
-        * @param string $key
-        * @return bool
-        */
        public function delete( $key ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
                try {
@@ -430,11 +408,6 @@ class SqlBagOStuff extends BagOStuff {
                return true;
        }
 
-       /**
-        * @param string $key
-        * @param int $step
-        * @return int|null
-        */
        public function incr( $key, $step = 1 ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
                try {
@@ -479,7 +452,7 @@ class SqlBagOStuff extends BagOStuff {
                return $newValue;
        }
 
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+       public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                if ( !is_callable( $callback ) ) {
                        throw new Exception( "Got invalid callback." );
                }
index 4fdaea1..43b1a47 100644 (file)
@@ -1075,7 +1075,6 @@ class Article implements Page {
 
                $outputPage = $this->getContext()->getOutput();
                $user = $this->getContext()->getUser();
-               $cache = wfGetMainCache();
                $rc = false;
 
                if ( !$this->getTitle()->quickUserCan( 'patrol', $user )
@@ -1100,7 +1099,8 @@ class Article implements Page {
                }
 
                // Check for cached results
-               $key = wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() );
+               $key = wfMemcKey( 'unpatrollable-page', $this->getTitle()->getArticleID() );
+               $cache = ObjectCache::getMainWANInstance();
                if ( $cache->get( $key ) ) {
                        return false;
                }
index b7582e6..06eb276 100644 (file)
@@ -114,7 +114,7 @@ class SpecialExpandTemplates extends SpecialPage {
                        }
 
                        $config = $this->getConfig();
-                       if ( ( $config->get( 'UseTidy' ) && $options->getTidy() ) || $config->get( 'AlwaysUseTidy' ) ) {
+                       if ( $config->get( 'UseTidy' ) && $options->getTidy() ) {
                                $tmp = MWTidy::tidy( $tmp );
                        }
 
index 3a15388..3e545d2 100644 (file)
        "permissionserrors": "Permission error",
        "permissionserrorstext": "You do not have permission to do that, for the following {{PLURAL:$1|reason|reasons}}:",
        "permissionserrorstext-withaction": "You do not have permission to $2, for the following {{PLURAL:$1|reason|reasons}}:",
+       "contentmodelediterror": "You cannot edit this revision because its content model is <code>$1</code>, and the current content model of the page is <code>$2</code>.",
        "recreate-moveddeleted-warn": "<strong>Warning: You are recreating a page that was previously deleted.</strong>\n\nYou should consider whether it is appropriate to continue editing this page.\nThe deletion and move log for this page are provided here for convenience:",
        "moveddeleted-notice": "This page has been deleted.\nThe deletion and move log for the page are provided below for reference.",
        "moveddeleted-notice-recent": "Sorry, this page was recently deleted (within the last 24 hours).\nThe deletion and move log for the page are provided below for reference.",
index bc6e853..4bd1055 100644 (file)
        "permissionserrors": "Used as title of error message.\n\nSee also:\n* {{msg-mw|loginreqtitle}}\n{{Identical|Permission error}}",
        "permissionserrorstext": "This message is \"without action\" version of {{msg-mw|Permissionserrorstext-withaction}}.\n\nParameters:\n* $1 - the number of reasons that were found why ''the action'' cannot be performed",
        "permissionserrorstext-withaction": "This message is \"with action\" version of {{msg-mw|Permissionserrorstext}}.\n\nParameters:\n* $1 - the number of reasons that were found why the action cannot be performed\n* $2 - one of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.",
+       "contentmodelediterror": "Error message shown when trying to edit an old revision with a content model different from that of the current revision\n* $1 - content model of the old revision\n* $2 - content model of the current revision",
        "recreate-moveddeleted-warn": "Warning shown when creating a page which has already been deleted. See for example [[Test]].",
        "moveddeleted-notice": "Shown on top of a deleted page in normal view modus ([{{canonicalurl:Test}} example]).",
        "moveddeleted-notice-recent": "Shown on top of a recently deleted page in normal view modus ([{{canonicalurl:Test}} example]).",
index 4b9ad9c..e66b729 100644 (file)
@@ -106,7 +106,7 @@ $maintenance->execute();
 $maintenance->globals();
 
 // Perform deferred updates.
-DeferredUpdates::doUpdates( 'commit' );
+DeferredUpdates::doUpdates();
 
 // log profiling info
 wfLogProfilingData();
index fee1e7c..59ce155 100644 (file)
@@ -1238,6 +1238,7 @@ return array(
        ),
        'mediawiki.ForeignStructuredUpload.BookletLayout' => array(
                'scripts' => 'resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js',
+               'styles' => 'resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css',
                'dependencies' => array(
                        'mediawiki.ForeignStructuredUpload',
                        'mediawiki.Upload.BookletLayout',
@@ -2031,6 +2032,8 @@ return array(
                'dependencies' => array(
                        'oojs-ui',
                        'mediawiki.api',
+                       'mediawiki.ForeignApi',
+                       'mediawiki.Title',
                ),
                'targets' => array( 'desktop', 'mobile' ),
        ),
index f1c4f6f..24b0e72 100644 (file)
@@ -7,42 +7,45 @@
 ( function ( $, mw ) {
 
        /**
-        * @class mw.widgets.CategoryCapsuleItemWidget
+        * @class mw.widgets.PageExistenceCache
+        * @private
+        * @param {mw.Api} [api]
         */
-
-       var processExistenceCheckQueueDebounced,
-               api = new mw.Api(),
-               currentRequest = null,
-               existenceCache = {},
-               existenceCheckQueue = {};
-
-       // The existence checking code really could be refactored into a separate class.
+       function PageExistenceCache( api ) {
+               this.api = api || new mw.Api();
+               this.processExistenceCheckQueueDebounced = OO.ui.debounce( this.processExistenceCheckQueue );
+               this.currentRequest = null;
+               this.existenceCache = {};
+               this.existenceCheckQueue = {};
+       }
 
        /**
+        * Check for existence of pages in the queue.
+        *
         * @private
         */
-       function processExistenceCheckQueue() {
+       PageExistenceCache.prototype.processExistenceCheckQueue = function () {
                var queue, titles;
-               if ( currentRequest ) {
+               if ( this.currentRequest ) {
                        // Don't fire off a million requests at the same time
-                       currentRequest.always( function () {
-                               currentRequest = null;
-                               processExistenceCheckQueueDebounced();
-                       } );
+                       this.currentRequest.always( function () {
+                               this.currentRequest = null;
+                               this.processExistenceCheckQueueDebounced();
+                       }.bind( this ) );
                        return;
                }
-               queue = existenceCheckQueue;
-               existenceCheckQueue = {};
+               queue = this.existenceCheckQueue;
+               this.existenceCheckQueue = {};
                titles = Object.keys( queue ).filter( function ( title ) {
-                       if ( existenceCache.hasOwnProperty( title ) ) {
-                               queue[ title ].resolve( existenceCache[ title ] );
+                       if ( this.existenceCache.hasOwnProperty( title ) ) {
+                               queue[ title ].resolve( this.existenceCache[ title ] );
                        }
-                       return !existenceCache.hasOwnProperty( title );
-               } );
+                       return !this.existenceCache.hasOwnProperty( title );
+               }.bind( this ) );
                if ( !titles.length ) {
                        return;
                }
-               currentRequest = api.get( {
+               this.currentRequest = this.api.get( {
                        action: 'query',
                        prop: [ 'info' ],
                        titles: titles
                        var index, curr, title;
                        for ( index in response.query.pages ) {
                                curr = response.query.pages[ index ];
-                               title = mw.Title.newFromText( curr.title ).getPrefixedText();
-                               existenceCache[ title ] = curr.missing === undefined;
-                               queue[ title ].resolve( existenceCache[ title ] );
+                               title = new ForeignTitle( curr.title ).getPrefixedText();
+                               this.existenceCache[ title ] = curr.missing === undefined;
+                               queue[ title ].resolve( this.existenceCache[ title ] );
                        }
-               } );
-       }
-
-       processExistenceCheckQueueDebounced = OO.ui.debounce( processExistenceCheckQueue );
+               }.bind( this ) );
+       };
 
        /**
         * Register a request to check whether a page exists.
         * @param {mw.Title} title
         * @return {jQuery.Promise} Promise resolved with true if the page exists or false otherwise
         */
-       function checkPageExistence( title ) {
+       PageExistenceCache.prototype.checkPageExistence = function ( title ) {
                var key = title.getPrefixedText();
-               if ( !existenceCheckQueue[ key ] ) {
-                       existenceCheckQueue[ key ] = $.Deferred();
+               if ( !this.existenceCheckQueue[ key ] ) {
+                       this.existenceCheckQueue[ key ] = $.Deferred();
                }
-               processExistenceCheckQueueDebounced();
-               return existenceCheckQueue[ key ].promise();
+               this.processExistenceCheckQueueDebounced();
+               return this.existenceCheckQueue[ key ].promise();
+       };
+
+       /**
+        * @class mw.widgets.ForeignTitle
+        * @private
+        * @extends mw.Title
+        *
+        * @constructor
+        * @inheritdoc
+        */
+       function ForeignTitle() {
+               ForeignTitle.parent.apply( this, arguments );
        }
+       OO.inheritClass( ForeignTitle, mw.Title );
+       ForeignTitle.prototype.getNamespacePrefix = function () {
+               // We only need to handle categories here...
+               return 'Category:'; // HACK
+       };
 
        /**
+        * @class mw.widgets.CategoryCapsuleItemWidget
+        *
         * Category selector capsule item widget. Extends OO.ui.CapsuleItemWidget with the ability to link
         * to the given page, and to show its existence status (i.e., whether it is a redlink).
         *
         * @constructor
         * @param {Object} config Configuration options
         * @cfg {mw.Title} title Page title to use (required)
+        * @cfg {string} [apiUrl] API URL, if not the current wiki's API
         */
        mw.widgets.CategoryCapsuleItemWidget = function MWWCategoryCapsuleItemWidget( config ) {
                // Parent constructor
 
                // Properties
                this.title = config.title;
+               this.apiUrl = config.apiUrl || '';
                this.$link = $( '<a>' )
                        .text( this.label )
                        .attr( 'target', '_blank' )
                this.setMissing( false );
                this.$label.replaceWith( this.$link );
                this.setLabelElement( this.$link );
-               checkPageExistence( this.title ).done( function ( exists ) {
-                       this.setMissing( !exists );
-               }.bind( this ) );
+
+               /*jshint -W024*/
+               if ( !this.constructor.static.pageExistenceCaches[ this.apiUrl ] ) {
+                       this.constructor.static.pageExistenceCaches[ this.apiUrl ] =
+                               new PageExistenceCache( new mw.ForeignApi( this.apiUrl ) );
+               }
+               this.constructor.static.pageExistenceCaches[ this.apiUrl ]
+                       .checkPageExistence( new ForeignTitle( this.title.getPrefixedText() ) )
+                       .done( function ( exists ) {
+                               this.setMissing( !exists );
+                       }.bind( this ) );
+               /*jshint +W024*/
        };
 
        /* Setup */
 
        OO.inheritClass( mw.widgets.CategoryCapsuleItemWidget, OO.ui.CapsuleItemWidget );
 
+       /* Static Properties */
+
+       /*jshint -W024*/
+       /**
+        * Map of API URLs to PageExistenceCache objects.
+        *
+        * @static
+        * @inheritable
+        * @property {Object}
+        */
+       mw.widgets.CategoryCapsuleItemWidget.static.pageExistenceCaches = {
+               '': new PageExistenceCache()
+       };
+       /*jshint +W024*/
+
        /* Methods */
 
        /**
         * @param {boolean} missing Whether the page is missing (does not exist)
         */
        mw.widgets.CategoryCapsuleItemWidget.prototype.setMissing = function ( missing ) {
+               var
+                       title = new ForeignTitle( this.title.getPrefixedText() ), // HACK
+                       prefix = this.apiUrl.replace( '/w/api.php', '' ); // HACK
+
                if ( !missing ) {
                        this.$link
-                               .attr( 'href', this.title.getUrl() )
+                               .attr( 'href', prefix + title.getUrl() )
                                .removeClass( 'new' );
                } else {
                        this.$link
-                               .attr( 'href', this.title.getUrl( { action: 'edit', redlink: 1 } ) )
+                               .attr( 'href', prefix + title.getUrl( { action: 'edit', redlink: 1 } ) )
                                .addClass( 'new' );
                }
        };
index 89fcc0b..59f1d50 100644 (file)
@@ -30,6 +30,7 @@
         *
         * @constructor
         * @param {Object} [config] Configuration options
+        * @cfg {mw.Api} [api] Instance of mw.Api (or subclass thereof) to use for queries
         * @cfg {number} [limit=10] Maximum number of results to load
         * @cfg {mw.widgets.CategorySelector.SearchType[]} [searchTypes=[mw.widgets.CategorySelector.SearchType.OpenSearch]]
         *   Default search API to use when searching.
@@ -61,7 +62,7 @@
                this.$input.on( 'change input cut paste', OO.ui.debounce( this.updateMenuItems.bind( this ), 100 ) );
 
                // Initialize
-               this.api = new mw.Api();
+               this.api = config.api || new mw.Api();
        }
 
        /* Setup */
         */
        CSP.createItemWidget = function ( data ) {
                return new mw.widgets.CategoryCapsuleItemWidget( {
+                       apiUrl: this.api.apiUrl || undefined,
                        title: mw.Title.newFromText( data, NS_CATEGORY )
                } );
        };
index 79aba77..5f82b18 100644 (file)
@@ -67,7 +67,6 @@
         *  each individual request by passing them to #get or #post (or directly #ajax) later on.
         */
        mw.Api = function ( options ) {
-               // TODO: Share API objects with exact same config.
                options = options || {};
 
                // Force a string if we got a mw.Uri object
@@ -90,7 +89,9 @@
                 */
                abort: function () {
                        $.each( this.requests, function ( index, request ) {
-                               request.abort();
+                               if ( request ) {
+                                       request.abort();
+                               }
                        } );
                },
 
                /**
                 * Perform API post request
                 *
-                * TODO: Post actions for non-local hostnames will need proxy.
-                *
                 * @param {Object} parameters
                 * @param {Object} [ajaxOptions]
                 * @return {jQuery.Promise}
                 *  Fail: Error code
                 */
                ajax: function ( parameters, ajaxOptions ) {
-                       var token,
+                       var token, requestIndex,
+                               api = this,
                                apiDeferred = $.Deferred(),
                                xhr, key, formData;
 
                                        }
                                } );
 
+                       requestIndex = this.requests.length;
                        this.requests.push( xhr );
+                       xhr.always( function () {
+                               api.requests[ requestIndex ] = null;
+                       } );
                        // Return the Promise
                        return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
                                if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
diff --git a/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css b/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css
new file mode 100644 (file)
index 0000000..4143520
--- /dev/null
@@ -0,0 +1,5 @@
+.mw-foreignStructuredUpload-bookletLayout-license {
+       font-size: 90%;
+       line-height: 1.4em;
+       color: #555;
+}
index 5a7e62e..86fb91b 100644 (file)
 
        /* Uploading */
 
+       /**
+        * @inheritdoc
+        */
+       mw.ForeignStructuredUpload.BookletLayout.prototype.initialize = function () {
+               mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this );
+               // Point the CategorySelector to the right wiki as soon as we know what the right wiki is
+               this.upload.apiPromise.done( function ( api ) {
+                       // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
+                       if ( api.apiUrl ) {
+                               // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
+                               this.categoriesWidget.api = new mw.ForeignApi( api.apiUrl );
+                       }
+               }.bind( this ) );
+       };
+
        /**
         * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
         * with the {@link #cfg-target target} specified in config.
@@ -78,7 +93,8 @@
                        notOwnWorkLocal = mw.message( 'foreign-structured-upload-form-label-not-own-work-local-default' );
                }
 
-               $ownWorkMessage = $( '<p>' ).html( ownWorkMessage.parse() );
+               $ownWorkMessage = $( '<p>' ).html( ownWorkMessage.parse() )
+                       .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
                $notOwnWorkMessage = $( '<div>' ).append(
                        $( '<p>' ).html( notOwnWorkMessage.parse() ),
                        $( '<p>' ).html( notOwnWorkLocal.parse() )
                        label: $notOwnWorkMessage
                } );
                this.ownWorkCheckbox = new OO.ui.CheckboxInputWidget().on( 'change', function ( on ) {
-                       if ( on ) {
-                               layout.messageLabel.setLabel( $ownWorkMessage );
-                       } else {
-                               layout.messageLabel.setLabel( $notOwnWorkMessage );
-                       }
+                       layout.messageLabel.toggle( !on );
                } );
 
                fieldset = new OO.ui.FieldsetLayout();
                        } ),
                        new OO.ui.FieldLayout( this.ownWorkCheckbox, {
                                align: 'inline',
-                               label: mw.msg( 'foreign-structured-upload-form-label-own-work' )
+                               label: $( '<div>' ).append(
+                                       $( '<p>' ).text( mw.msg( 'foreign-structured-upload-form-label-own-work' ) ),
+                                       $ownWorkMessage
+                               )
                        } ),
-                       this.messageLabel
+                       new OO.ui.FieldLayout( this.messageLabel, {
+                               align: 'top'
+                       } )
                ] );
                this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
 
                        mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
                } );
                this.categoriesWidget = new mw.widgets.CategorySelector( {
+                       // Can't be done here because we don't know the target wiki yet... done in #initialize.
+                       // api: new mw.ForeignApi( ... ),
                        $overlay: this.$overlay
                } );
 
index b5d7a11..f245826 100644 (file)
@@ -14,7 +14,9 @@
 # Plus any combination of these:
 #
 # cat           add category links
+#               (ignored by Parsoid, since it emits <link>s)
 # ill           add inter-language links
+#               (ignored by Parsoid, since it emits <links>s)
 # subpage       enable subpages (disabled by default)
 # noxml         don't check for XML well-formedness
 # title=[[XXX]] run test using article title XXX
@@ -3328,14 +3330,18 @@ parsoid=wt2html,wt2wt
 <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":" [[Category:foo]]"}},"i":0}}]}'> </span><link rel="mw:PageProp/Category" href="./Category:Foo" about="#mwt1"> <!-- No pre&#x2D;wrapping -->
 !! end
 
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
 !! test
 7b. Indent-pre and category links
 !! options
-parsoid=wt2html,wt2wt
+parsoid=wt2html
 !! wikitext
  [[Category:foo]] a
  [[Category:foo]] {{echo|b}}
-!! html
+!! html/parsoid
 <pre><link rel="mw:PageProp/Category" href="./Category:Foo"> a
  <link rel="mw:PageProp/Category" href="./Category:Foo"> <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"b"}},"i":0}}]}'>b</span></pre>
 !! end
@@ -4687,12 +4693,43 @@ http://example.com/url_with_entity&#60;
 <a rel="mw:ExtLink" href="http://example.com">http://example.com</a>)
 <a rel="mw:ExtLink" href="http://example.com/url_with_(brackets)">http://example.com/url_with_(brackets)</a>
 (<a rel="mw:ExtLink" href="http://example.com/url_without_brackets">http://example.com/url_without_brackets</a>)
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity ">http://example.com/url_with_entity </a>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity ">http://example.com/url_with_entity </a>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity ">http://example.com/url_with_entity </a>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity">&lt;</span>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity&lt;">http://example.com/url_with_entity&lt;</a>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity&lt;">http://example.com/url_with_entity&lt;</a></p>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;nbsp;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#xA0;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#160;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;lt;","srcContent":"&lt;"}'>&lt;</span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#x3C;","srcContent":"&lt;"}'>&lt;</span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#60;","srcContent":"&lt;"}'>&lt;</span></p>
+!! end
+
+!! test
+External links: tricky Parsoid html2html case
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+http://example.com/url_with_entity&amp;amp;
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/url_with_entity&amp;amp">http://example.com/url_with_entity&amp;amp</a>;
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/url_with_entity&amp;amp">http://example.com/url_with_entity&amp;amp</a>;</p>
+!! end
+
+!! test
+External links: Free with trailing quotes (T113666)
+!! wikitext
+'''News:''' Stuff here
+
+news:'a'b''c''d e
+!! html/php
+<p><b>News:</b> Stuff here
+</p><p><a rel="nofollow" class="external free" href="news:'a'b">news:'a'b</a><i>c</i>d e
+</p>
+!! html/parsoid
+<p><b>News:</b> Stuff here</p>
+<p><a rel="mw:ExtLink" href="news:'a'b">news:'a'b</a><i>c</i>d e</p>
 !! end
 
 !! test
@@ -5076,6 +5113,8 @@ External links: link text with spaces
 
 !! test
 External links: wiki links within external link (Bug 3695)
+!! options
+parsoid=wt2html,html2html
 !! wikitext
 [http://example.com [[wikilink]] embedded in ext link]
 !! html/php
@@ -5596,7 +5635,7 @@ Parenthesis in external links, w/ transclusion or comment
 !! test
 Serialize <a> tags with invalid link targets as plain text
 !! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
 !! html/parsoid
 <a rel="mw:WikiLink" href="[[foo]]">text</a>
 <a rel="mw:WikiLink" href="[[foo]]">*text</a>
@@ -10088,6 +10127,23 @@ PMID
 1234</p>
 !! end
 
+# <nowiki> nodes shouldn't be inserted during html2wt by Parsoid,
+# since these are ExtLinkText, not MagicLinkText
+!! test
+Magic links: use appropriate serialization for "almost" magic links.
+!! wikitext
+X[[Special:BookSources/0978739256|foo]]
+
+X[//tools.ietf.org/html/rfc1234 foo]
+!! html/php
+<p>X<a href="/wiki/Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a>
+</p><p>X<a rel="nofollow" class="external text" href="//tools.ietf.org/html/rfc1234">foo</a>
+</p>
+!! html/parsoid
+<p>X<a rel="mw:WikiLink" href="./Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a></p>
+<p>X<a rel="mw:ExtLink" href="//tools.ietf.org/html/rfc1234">foo</a></p>
+!! end
+
 ###
 ### Templates
 ####
@@ -10809,8 +10865,14 @@ Un-closed <includeonly>
 !! html
 !! end
 
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize the include directives to serialize on their own line.
+## Selser will take care of preserving formatting in scenarios where they
+## intermingled with other wikitext.
 !! test
 Includes and comments at SOL
+!! options
+parsoid=wt2html,html2html
 !! wikitext
 <!-- comment --><noinclude><!-- comment --></noinclude><!-- comment -->== hu ==
 
@@ -10991,10 +11053,14 @@ parsoid=wt2html,wt2wt
 </tbody></table>
 !!end
 
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize the include directives to serialize on their own line.
+## Selser will take care of preserving formatting in scenarios where they
+## intermingled with other wikitext.
 !!test
 2. Table tag in SOL posn. should get reparsed correctly with valid TSR
 !!options
-parsoid=wt2html,wt2wt
+parsoid=wt2html
 !!wikitext
 <includeonly>a</includeonly>{| {{{b}}}
 |c
@@ -12913,13 +12979,14 @@ parsoid={
 [[Image:Foobar.jpg|<nowiki>|</nowiki>]]
 !! end
 
+# wgExternalLinkTarget not supported by Parsoid
 !! test
 Image with link parameter, wgExternalLinkTarget
 !! wikitext
 [[Image:foobar.jpg|link=http://example.com/]]
 !! config
 wgExternalLinkTarget='foobar'
-!! html
+!! html/php
 <p><a href="http://example.com/" target="foobar" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! end
@@ -12946,13 +13013,14 @@ wgNoFollowDomainExceptions='example.com'
 </p>
 !! end
 
+# wgExternalLinkTarget not supported by Parsoid
 !! test
 Image with link parameter, wgExternalLinkTarget, unnamed parameter
 !! wikitext
 [[Image:foobar.jpg|link=http://example.com/|Title]]
 !! config
 wgExternalLinkTarget='foobar'
-!! html
+!! html/php
 <p><a href="http://example.com/" title="Title" target="foobar" rel="nofollow"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
 </p>
 !! end
@@ -14207,15 +14275,22 @@ pst
 [[Category:Foo (bar)|Foo]]
 !! end
 
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
 !! test
 Category with link tail
 !! options
 cat
 pst
+parsoid=wt2html
 !! wikitext
 123[[Category:Foo]]456
-!! html
+!! html/php
 123[[Category:Foo]]456
+!! html/parsoid
+<p>123<link rel="mw:PageProp/Category" href="Category:Foo"/>456</p>
 !! end
 
 !! test
@@ -14251,8 +14326,14 @@ pst
 [[Category:{{echo|Foo}}|{{echo|Bar}}]]
 !! end
 
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
 !! test
 Category / paragraph interactions
+!! options
+parsoid=wt2html
 !! wikitext
 Foo [[Category:Baz]] Bar
 
@@ -14279,7 +14360,7 @@ Bar
 [[Category:Baz]]
  {{echo|[[Category:Baz]]}}
 [[Category:Baz]]
-!! html
+!! html/php
 <p>Foo Bar
 </p><p>Foo
 Bar
@@ -14289,20 +14370,32 @@ Bar
 </p><p>Foo
 Bar
 </p>
+!! html/parsoid
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar</p>
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar</p>
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar</p>
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar</p>
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[Category:Baz]]"}},"i":0}}]}'/></p>
+<link rel="mw:PageProp/Category" href="Category:Baz"/>
 !! end
 
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
+##
 ## The whitespace on the empty line is part of the test. Please do not delete
 !! test
 1. Categories and newlines: All preceding newlines should be suppressed (courtesy bug 87)
 !! options
-parsoid=wt2html,wt2wt
+parsoid=wt2html
 !! wikitext
 This
    
 [[Category:Foo]] and this should be part of same paragraph (not an indent-pre)
    
 {{echo|[[Category:Foo]] and so should this!}}
-!! html
+!! html/php
 <p>This and this should be part of same paragraph (not an indent-pre) and so should this!
 </p>
 !! html/parsoid
@@ -14400,8 +14493,14 @@ parsoid=wt2html
 <link rel="mw:PageProp/Category" href="./Category:Foo" data-parsoid='{"stx":"simple","a":{"href":"./Category:Foo"},"sa":{"href":"Category:Foo"}}'/>
 !! end
 
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
 !! test
 6. Categories and newlines: migrateTrailingCategories dom pass should not migrate categories not preceded by newlines
+!! options
+parsoid=wt2html
 !! wikitext
 * a [[Category:Foo]]
 !! html/parsoid
@@ -14452,13 +14551,20 @@ parsoid
 </p>
 !! end
 
-# html2wt localizes the "Category" namespace.
-# XXX the <link> element needs an empty data-parsoid attribute, or
-# else the html2html test fails because spaces are inserted.
+# We used to, but no longer wt2wt this test since the default serializer
+# will normalize all categories to serialize on their own line.
+# This wikitext usage is going to be fairly uncommon in production and
+# selser will take care of preventing whitespace insertion if this
+# occurs in an article.
+#
+# html2html disabled for the same reason (whitespace insertion between
+# x and y).
+#
+# html2wt disabled because it localizes the "Category" namespace.
 !! test
 Link prefix/suffixes aren't applied to category links
 !! options
-parsoid=wt2html,wt2wt,html2html
+parsoid=wt2html
 language=is
 !! wikitext
 x[[Category:Foo]]y
@@ -19158,17 +19264,25 @@ Category:分類
 blah
 !! endarticle
 
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
 !! test
 Don't convert blue categorylinks to another variant (bug 33210)
 !! options
-language=zh cat
+cat
+language=zh
+parsoid=wt2html
 !! wikitext
 [[A]][[Category:分类]]
-!! html
+!! html/php
 <a href="/wiki/Category:%E5%88%86%E7%B1%BB" title="Category:分类">分类</a>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="A" title="A">A</a></p>
+<link rel="mw:PageProp/Category" href="Category:分类"/>
 !! end
 
-
 !! test
 Stripping -{}- tags (language variants)
 !! options
@@ -23512,7 +23626,8 @@ parsoid=html2wt
 
  __TOC__ foo
 
-__TOC__ bar
+__TOC__
+ bar
 !! end
 
 #### --------------- HTML tags ---------------
@@ -25258,11 +25373,11 @@ parsoid=html2wt
 <p><b><a href="http://cscott.net">http://cscott.net</a>x</b></p>
 <p><a href="http://cscott.net">http://cscott.net</a>x</p>
 !! wikitext
-http://cscott.net<nowiki/>'''foo'''
+http://cscott.net'''foo'''
 
 http://cscott.net<b>foo</b>
 
-'''http://cscott.net<nowiki/>'''
+'''http://cscott.net'''
 
 '''http://cscott.net '''
 
@@ -25430,7 +25545,7 @@ parsoid=html2wt
 !! test
 T75121: Infer extension name from typeOf if data-mw is not present
 !! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
 !! html/parsoid
 <div typeOf="mw:Extension/foo"></div>
 !! wikitext
@@ -25533,7 +25648,7 @@ parsoid={
 !! test
 Never serialize a-tag as html, regardless of what data-parsoid has to say
 !! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
 !! html/parsoid
 <a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{"stx":"html"}'>Foo</a>
 !! wikitext
@@ -25547,7 +25662,7 @@ parsoid=html2wt
 !! test
 Never serialize a-tag as html, no matter what attributes it has
 !! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
 !! html/parsoid
 <a bad='true' href='http://boo.org'><img src='http://boohoo.org' /></a>
 !! wikitext
@@ -25571,6 +25686,26 @@ parsoid=html2wt
 # Tests spec'ing wikitext serialization norms |
 # --------------------------------------------
 
+!! test
+1. Categories should always be serialized on their own line
+!! options
+parsoid=html2wt
+!! html/parsoid
+foo<link rel="mw:PageProp/Category" href="./Category:Foo">bar
+!! wikitext
+foo
+[[Category:Foo]]
+bar
+!! end
+
+!! test
+2. Categories that are part of templates should not introduce a line break
+!! wikitext
+foo {{echo|<span>bar</span> [[Category:baz]]}} bar
+!! html/parsoid
+<p>foo <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;span>bar&lt;/span> [[Category:baz]]"}},"i":0}}]}'>bar</span><span about="#mwt1"> </span><link rel="mw:PageProp/Category" href="./Category:Baz" about="#mwt1" data-parsoid='{"stx":"simple","a":{"href":"./Category:Baz"},"sa":{"href":"Category:baz"}}'/> bar</p>
+!! end
+
 !! test
 Lists: Add space after bullets
 !! options
index 8af1428..9e4a984 100644 (file)
@@ -207,7 +207,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                if ( $this->needsDB() && $this->db ) {
                        // Clean up open transactions
                        while ( $this->db->trxLevel() > 0 ) {
-                               $this->db->rollback();
+                               $this->db->rollback( __METHOD__, 'flush' );
                        }
                }
 
@@ -241,7 +241,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                if ( $this->needsDB() && $this->db ) {
                        // Clean up open transactions
                        while ( $this->db->trxLevel() > 0 ) {
-                               $this->db->rollback();
+                               $this->db->rollback( __METHOD__, 'flush' );
                        }
                }
 
index ac52e12..1d8f43a 100644 (file)
@@ -34,6 +34,41 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
        }
 
+       public function testSyncMerge() {
+               $key = wfRandomString();
+               $value = wfRandomString();
+               $func = function () use ( $value ) {
+                       return $value;
+               };
+
+               // XXX: DeferredUpdates bound to transactions in CLI mode
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->begin();
+               $this->cache->merge( $key, $func );
+
+               // Set in tier 1
+               $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' );
+               // Not yet set in tier 2
+               $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' );
+
+               $dbw->commit();
+
+               // Set in tier 2
+               $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
+
+               $key = wfRandomString();
+
+               $dbw->begin();
+               $this->cache->merge( $key, $func, 0, 1, BagOStuff::WRITE_SYNC );
+
+               // Set in tier 1
+               $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' );
+               // Also set in tier 2
+               $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
+
+               $dbw->commit();
+       }
+
        public function testSetDelayed() {
                $key = wfRandomString();
                $value = wfRandomString();