Merge "Issue 301 redirects for Special:Search/searchterm"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 18 Aug 2016 22:48:13 +0000 (22:48 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 18 Aug 2016 22:48:13 +0000 (22:48 +0000)
109 files changed:
Gemfile
Gemfile.lock
RELEASE-NOTES-1.28
composer.json
includes/EditPage.php
includes/GlobalFunctions.php
includes/MovePage.php
includes/OutputPage.php
includes/StubObject.php
includes/WatchedItemStore.php
includes/api/ApiAuthManagerHelper.php
includes/api/ApiBase.php
includes/api/ApiLogin.php
includes/api/ApiMain.php
includes/api/ApiQueryBacklinksprop.php
includes/api/ApiUpload.php
includes/api/i18n/fr.json
includes/api/i18n/zh-hans.json
includes/auth/AuthManager.php
includes/auth/AuthenticationRequest.php
includes/auth/PasswordAuthenticationRequest.php
includes/auth/PrimaryAuthenticationProvider.php
includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php
includes/content/ContentHandler.php
includes/content/WikitextContentHandler.php
includes/db/DBConnRef.php
includes/db/Database.php
includes/db/DatabaseMysqlBase.php
includes/db/DatabasePostgres.php
includes/db/IDatabase.php
includes/db/loadbalancer/LBFactory.php
includes/db/loadbalancer/LBFactoryMulti.php
includes/db/loadbalancer/LBFactorySimple.php
includes/db/loadbalancer/LoadBalancer.php
includes/deferred/DataUpdate.php
includes/deferred/LinksDeletionUpdate.php
includes/deferred/LinksUpdate.php
includes/htmlform/fields/HTMLFormFieldCloner.php
includes/installer/Installer.php
includes/jobqueue/jobs/CategoryMembershipChangeJob.php
includes/jobqueue/jobs/DeleteLinksJob.php
includes/jobqueue/jobs/HTMLCacheUpdateJob.php
includes/jobqueue/jobs/RecentChangesUpdateJob.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/objectcache/SqlBagOStuff.php
includes/resourceloader/ResourceLoaderClientHtml.php
includes/resourceloader/ResourceLoaderImageModule.php
includes/skins/SkinTemplate.php
includes/upload/UploadBase.php
includes/upload/UploadFromChunks.php
includes/upload/UploadFromStash.php
includes/user/User.php
languages/Language.php
languages/i18n/az.json
languages/i18n/bn.json
languages/i18n/ckb.json
languages/i18n/cs.json
languages/i18n/diq.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/got.json
languages/i18n/he.json
languages/i18n/it.json
languages/i18n/kk-cyrl.json
languages/i18n/ko.json
languages/i18n/mr.json
languages/i18n/nap.json
languages/i18n/pt.json
languages/i18n/sl.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/uz.json
languages/i18n/zh-hans.json
maintenance/cleanupCaps.php
resources/Resources.php
resources/ResourcesOOUI.php
resources/assets/licenses/README [new file with mode: 0644]
resources/lib/oojs-ui/i18n/ckb.json
resources/lib/oojs-ui/i18n/da.json
resources/lib/oojs-ui/i18n/diq.json
resources/lib/oojs-ui/oojs-ui-apex.js
resources/lib/oojs-ui/oojs-ui-core-apex.css
resources/lib/oojs-ui/oojs-ui-core-mediawiki.css
resources/lib/oojs-ui/oojs-ui-core.js
resources/lib/oojs-ui/oojs-ui-mediawiki.js
resources/lib/oojs-ui/oojs-ui-toolbars-apex.css
resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css
resources/lib/oojs-ui/oojs-ui-toolbars.js
resources/lib/oojs-ui/oojs-ui-widgets-apex.css
resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css
resources/lib/oojs-ui/oojs-ui-widgets.js
resources/lib/oojs-ui/oojs-ui-windows-apex.css
resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css
resources/lib/oojs-ui/oojs-ui-windows.js
resources/src/mediawiki.skinning/content.css
resources/src/mediawiki/mediawiki.raggett.css [deleted file]
resources/src/oojs-ui-styles-skip.js [deleted file]
tests/browser/environments.yml
tests/parser/parserTest.inc
tests/phpunit/includes/auth/AuthManagerTest.php
tests/phpunit/includes/auth/AuthenticationRequestTest.php
tests/phpunit/includes/auth/AuthenticationRequestTestCase.php
tests/phpunit/includes/content/ContentHandlerTest.php
tests/phpunit/includes/db/DatabaseTest.php
tests/phpunit/includes/libs/ObjectFactoryTest.php
tests/phpunit/structure/ResourcesTest.php
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.test.js

diff --git a/Gemfile b/Gemfile
index 19d2f52..8a349bf 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -1,5 +1,5 @@
 source 'https://rubygems.org'
 
-gem 'mediawiki_selenium', '~> 1.7', '>= 1.7.1'
+gem 'mediawiki_selenium', '~> 1.7', '>= 1.7.2'
 gem 'rake', '~> 11.1', '>= 11.1.1'
 gem 'rubocop', '~> 0.32.1', require: false
index 2d6e655..982619a 100644 (file)
@@ -17,16 +17,18 @@ GEM
       faker (>= 1.1.2)
       yml_reader (>= 0.6)
     diff-lcs (1.2.5)
-    domain_name (0.5.20160310)
+    domain_name (0.5.20160615)
       unf (>= 0.0.5, < 1.0.0)
-    faker (1.6.3)
+    faker (1.6.6)
       i18n (~> 0.5)
     faraday (0.9.2)
       multipart-post (>= 1.2, < 3)
     faraday-cookie_jar (0.0.6)
       faraday (>= 0.7.4)
       http-cookie (~> 1.0.0)
-    ffi (1.9.10)
+    faraday_middleware (0.10.0)
+      faraday (>= 0.7.4, < 0.10)
+    ffi (1.9.14)
     gherkin (2.12.2)
       multi_json (~> 1.3)
     headless (2.2.3)
@@ -34,14 +36,15 @@ GEM
       domain_name (~> 0.5)
     i18n (0.7.0)
     json (1.8.3)
-    mediawiki_api (0.6.0)
+    mediawiki_api (0.7.0)
       faraday (~> 0.9, >= 0.9.0)
       faraday-cookie_jar (~> 0.0, >= 0.0.6)
-    mediawiki_selenium (1.7.1)
+      faraday_middleware (~> 0.10, >= 0.10.0)
+    mediawiki_selenium (1.7.2)
       cucumber (~> 1.3, >= 1.3.20)
       headless (~> 2.0, >= 2.1.0)
       json (~> 1.8, >= 1.8.1)
-      mediawiki_api (~> 0.6, >= 0.6.0)
+      mediawiki_api (~> 0.7, >= 0.7.0)
       page-object (~> 1.0)
       rest-client (~> 1.6, >= 1.6.7)
       rspec-core (~> 2.14, >= 2.14.4)
@@ -53,7 +56,7 @@ GEM
     multi_test (0.1.2)
     multipart-post (2.0.0)
     netrc (0.11.0)
-    page-object (1.1.1)
+    page-object (1.2.0)
       page_navigation (>= 0.9)
       selenium-webdriver (>= 2.44.0)
       watir-webdriver (>= 0.6.11)
@@ -79,7 +82,7 @@ GEM
       ruby-progressbar (~> 1.4)
     ruby-progressbar (1.7.5)
     rubyzip (1.2.0)
-    selenium-webdriver (2.53.1)
+    selenium-webdriver (2.53.4)
       childprocess (~> 0.5)
       rubyzip (~> 1.0)
       websocket (~> 1.0)
@@ -88,7 +91,7 @@ GEM
     unf (0.1.4)
       unf_ext
     unf_ext (0.0.7.2)
-    watir-webdriver (0.9.1)
+    watir-webdriver (0.9.3)
       selenium-webdriver (>= 2.46.2)
     websocket (1.2.3)
     yml_reader (0.7)
@@ -97,9 +100,6 @@ PLATFORMS
   ruby
 
 DEPENDENCIES
-  mediawiki_selenium (~> 1.7, >= 1.7.1)
+  mediawiki_selenium (~> 1.7, >= 1.7.2)
   rake (~> 11.1, >= 11.1.1)
   rubocop (~> 0.32.1)
-
-BUNDLED WITH
-   1.10.6
index 80166ad..f6c3530 100644 (file)
@@ -106,6 +106,11 @@ changes to languages because of Phabricator reports.
 * DifferenceEngine::generateDiffBody() was removed (deprecated since 1.21).
 * UploadBase::stashFileGetKey() and UploadBase::stashSession() were deprecated.
   Use ...->stashFile()->getFileKey() instead.
+* "Public domain" was removed as a wiki license option from the installer, in
+  favour of CC-0.
+* AuthenticationRequest::$required is now changed from REQUIRED to PRIMARY_REQUIRED
+  on requests needed by primary providers even if all primaries need them.
+  Primary providers are discouraged from returning multiple REQUIRED requests.
 
 == Compatibility ==
 
index 90f7063..956739d 100644 (file)
@@ -25,7 +25,7 @@
                "ext-xml": "*",
                "liuggio/statsd-php-client": "1.0.18",
                "mediawiki/at-ease": "1.1.0",
-               "oojs/oojs-ui": "0.17.7",
+               "oojs/oojs-ui": "0.17.8",
                "oyejorge/less.php": "1.7.0.10",
                "php": ">=5.5.9",
                "psr/log": "1.0.0",
index ee06993..9b862b9 100644 (file)
@@ -1474,7 +1474,7 @@ class EditPage {
 
                        case self::AS_CANNOT_USE_CUSTOM_MODEL:
                        case self::AS_PARSE_ERROR:
-                               $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
+                               $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
                                return true;
 
                        case self::AS_SUCCESS_NEW_ARTICLE:
@@ -1551,7 +1551,7 @@ class EditPage {
                                // is if an extension hook aborted from inside ArticleSave.
                                // Render the status object into $this->hookError
                                // FIXME this sucks, we should just use the Status object throughout
-                               $this->hookError = '<div class="error">' . $status->getWikiText() .
+                               $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
                                        '</div>';
                                return true;
                }
index 7117f4c..0d66908 100644 (file)
@@ -811,7 +811,7 @@ function wfUrlProtocolsWithoutProtRel() {
  * 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2)).
  *
  * @param string $url A URL to parse
- * @return string[] Bits of the URL in an associative array, per PHP docs
+ * @return string[]|bool Bits of the URL in an associative array, per PHP docs, false on failure
  */
 function wfParseUrl( $url ) {
        global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php
index 70b6738..bc3305a 100644 (file)
@@ -256,7 +256,7 @@ class MovePage {
                $pageid = $this->oldTitle->getArticleID( Title::GAID_FOR_UPDATE );
                $protected = $this->oldTitle->isProtected();
 
-               // Do the actual move
+               // Do the actual move; if this fails, it will throw an MWException(!)
                $nullRevision = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect );
 
                // Refresh the sortkey for this row.  Be careful to avoid resetting
@@ -412,7 +412,7 @@ class MovePage {
         *
         * @fixme This was basically directly moved from Title, it should be split into smaller functions
         * @param User $user the User doing the move
-        * @param Title $nt The page to move to, which should be a redirect or nonexistent
+        * @param Title $nt The page to move to, which should be a redirect or non-existent
         * @param string $reason The reason for the move
         * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
         *   if the user has the suppressredirect right
@@ -430,6 +430,29 @@ class MovePage {
                        $logType = 'move';
                }
 
+               if ( $moveOverRedirect ) {
+                       $overwriteMessage = wfMessage(
+                                       'delete_and_move_reason',
+                                       $this->oldTitle->getPrefixedText()
+                               )->text();
+                       $newpage = WikiPage::factory( $nt );
+                       $errs = [];
+                       $status = $newpage->doDeleteArticleReal(
+                               $overwriteMessage,
+                               /* $suppress */ false,
+                               $nt->getArticleId(),
+                               /* $commit */ false,
+                               $errs,
+                               $user
+                       );
+
+                       if ( !$status->isGood() ) {
+                               throw new MWException( 'Failed to delete page-move revision: ' . $status );
+                       }
+
+                       $nt->resetArticleID( false );
+               }
+
                if ( $createRedirect ) {
                        if ( $this->oldTitle->getNamespace() == NS_CATEGORY
                                && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
@@ -484,19 +507,6 @@ class MovePage {
 
                $newpage = WikiPage::factory( $nt );
 
-               if ( $moveOverRedirect ) {
-                       $newid = $nt->getArticleID();
-                       $newcontent = $newpage->getContent();
-
-                       # Delete the old redirect. We don't save it to history since
-                       # by definition if we've got here it's rather uninteresting.
-                       # We have to remove it so that the next step doesn't trigger
-                       # a conflict on the unique namespace+title index...
-                       $dbw->delete( 'page', [ 'page_id' => $newid ], __METHOD__ );
-
-                       $newpage->doDeleteUpdates( $newid, $newcontent );
-               }
-
                # Save a null revision in the page's history notifying of the move
                $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $user );
                if ( !is_object( $nullRevision ) ) {
@@ -542,9 +552,7 @@ class MovePage {
                        );
                }
 
-               if ( !$moveOverRedirect ) {
-                       WikiPage::onArticleCreate( $nt );
-               }
+               WikiPage::onArticleCreate( $nt );
 
                # Recreate the redirect, this time in the other direction.
                if ( $redirectContent ) {
index cc377bc..374e7af 100644 (file)
@@ -2671,7 +2671,7 @@ class OutputPage extends ContextSource {
                                'user.options',
                                'user.tokens',
                        ] );
-                       $rlClient = new ResourceLoaderClientHtml( $context );
+                       $rlClient = new ResourceLoaderClientHtml( $context, $this->getTarget() );
                        $rlClient->setConfig( $this->getJSVars() );
                        $rlClient->setModules( $this->getModules( /*filter*/ true ) );
                        $rlClient->setModuleStyles( $this->getModuleStyles( /*filter*/ true ) );
@@ -2790,9 +2790,12 @@ class OutputPage extends ContextSource {
         * @return string|WrappedStringList HTML
         */
        public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
+               // Apply 'target' and 'origin' filters
+               $modules = $this->filterModules( (array)$modules, null, $only );
+
                return ResourceLoaderClientHtml::makeLoad(
                        $this->getRlClientContext(),
-                       (array)$modules,
+                       $modules,
                        $only,
                        $extraQuery
                );
@@ -3810,9 +3813,6 @@ class OutputPage extends ContextSource {
                        'oojs-ui.styles.textures',
                        'mediawiki.widgets.styles',
                ] );
-               // Used by 'skipFunction' of the four 'oojs-ui.styles.*' modules. Please don't treat this as a
-               // public API or you'll be severely disappointed when T87871 is fixed and it disappears.
-               $this->addMeta( 'X-OOUI-PHP', '1' );
        }
 
        /**
index 0b4d048..0210ed9 100644 (file)
@@ -48,6 +48,9 @@ class StubObject {
        /** @var null|string */
        protected $class;
 
+       /** @var null|callable */
+       protected $factory;
+
        /** @var array */
        protected $params;
 
@@ -55,12 +58,17 @@ class StubObject {
         * Constructor.
         *
         * @param string $global Name of the global variable.
-        * @param string $class Name of the class of the real object.
+        * @param string|callable $class Name of the class of the real object
+        *                               or a factory function to call
         * @param array $params Parameters to pass to constructor of the real object.
         */
        public function __construct( $global = null, $class = null, $params = [] ) {
                $this->global = $global;
-               $this->class = $class;
+               if ( is_callable( $class ) ) {
+                       $this->factory = $class;
+               } else {
+                       $this->class = $class;
+               }
                $this->params = $params;
        }
 
@@ -110,8 +118,10 @@ class StubObject {
         * @return object
         */
        public function _newObject() {
-               return ObjectFactory::getObjectFromSpec( [
-                       'class' => $this->class,
+               $params = $this->factory
+                       ? [ 'factory' => $this->factory ]
+                       : [ 'class' => $this->class ];
+               return ObjectFactory::getObjectFromSpec( $params + [
                        'args' => $this->params,
                        'closure_expansion' => false,
                ] );
index b8a47fa..a13609b 100644 (file)
@@ -741,6 +741,8 @@ class WatchedItemStore implements StatsdAwareInterface {
                                        global $wgUpdateRowsPerQuery;
 
                                        $dbw = $this->getConnection( DB_MASTER );
+                                       $factory = wfGetLBFactory();
+                                       $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
 
                                        $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery );
                                        foreach ( $watchersChunks as $watchersChunk ) {
@@ -754,8 +756,9 @@ class WatchedItemStore implements StatsdAwareInterface {
                                                        ], $fname
                                                );
                                                if ( count( $watchersChunks ) > 1 ) {
-                                                       $dbw->commit( __METHOD__, 'flush' );
-                                                       wfGetLBFactory()->waitForReplication( [ 'wiki' => $dbw->getWikiID() ] );
+                                                       $factory->commitAndWaitForReplication(
+                                                               __METHOD__, $ticket, [ 'wiki' => $dbw->getWikiID() ]
+                                                       );
                                                }
                                        }
                                        $this->uncacheLinkTarget( $target );
index c970f93..d330862 100644 (file)
@@ -326,6 +326,7 @@ class ApiAuthManagerHelper {
                        $this->formatMessage( $ret, 'label', $field['label'] );
                        $this->formatMessage( $ret, 'help', $field['help'] );
                        $ret['optional'] = !empty( $field['optional'] );
+                       $ret['sensitive'] = !empty( $field['sensitive'] );
 
                        $retFields[$name] = $ret;
                }
index b45eacb..4a1a520 100644 (file)
@@ -2458,6 +2458,7 @@ abstract class ApiBase extends ContextSource {
 
                // Build map of extension directories to extension info
                if ( self::$extensionInfo === null ) {
+                       $extDir = $this->getConfig()->get( 'ExtensionDirectory' );
                        self::$extensionInfo = [
                                realpath( __DIR__ ) ?: __DIR__ => [
                                        'path' => $IP,
@@ -2465,6 +2466,7 @@ abstract class ApiBase extends ContextSource {
                                        'license-name' => 'GPL-2.0+',
                                ],
                                realpath( "$IP/extensions" ) ?: "$IP/extensions" => null,
+                               realpath( $extDir ) ?: $extDir => null,
                        ];
                        $keep = [
                                'path' => null,
index 851252c..28937f7 100644 (file)
@@ -155,10 +155,14 @@ class ApiLogin extends ApiBase {
                                        $authRes = 'Failed';
                                        $message = $res->message;
                                        \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
-                                               ->info( __METHOD__ . ': Authentication failed: ' . $message->plain() );
+                                               ->info( __METHOD__ . ': Authentication failed: '
+                                               . $message->inLanguage( 'en' )->plain() );
                                        break;
 
                                default:
+                                       \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                               ->info( __METHOD__ . ': Authentication failed due to unsupported response type: '
+                                               . $res->status, $this->getAuthenticationResponseLogData( $res ) );
                                        $authRes = 'Aborted';
                                        break;
                        }
@@ -273,4 +277,32 @@ class ApiLogin extends ApiBase {
        public function getHelpUrls() {
                return 'https://www.mediawiki.org/wiki/API:Login';
        }
+
+       /**
+        * Turns an AuthenticationResponse into a hash suitable for passing to Logger
+        * @param AuthenticationResponse $response
+        * @return array
+        */
+       protected function getAuthenticationResponseLogData( AuthenticationResponse $response ) {
+               $ret = [
+                       'status' => $response->status,
+               ];
+               if ( $response->message ) {
+                       $ret['message'] = $response->message->inLanguage( 'en' )->plain();
+               };
+               $reqs = [
+                       'neededRequests' => $response->neededRequests,
+                       'createRequest' => $response->createRequest,
+                       'linkRequest' => $response->linkRequest,
+               ];
+               foreach ( $reqs as $k => $v ) {
+                       if ( $v ) {
+                               $v = is_array( $v ) ? $v : [ $v ];
+                               $reqClasses = array_unique( array_map( 'get_class', $v ) );
+                               sort( $reqClasses );
+                               $ret[$k] = implode( ', ', $reqClasses );
+                       }
+               }
+               return $ret;
+       }
 }
index 0478027..565e829 100644 (file)
@@ -25,6 +25,8 @@
  * @defgroup API API
  */
 
+use MediaWiki\Logger\LoggerFactory;
+
 /**
  * This is the main API class, used for both external and internal processing.
  * When executed, it will create the requested formatter object,
@@ -206,7 +208,7 @@ class ApiMain extends ApiBase {
                                        $config->get( 'CrossSiteAJAXdomainExceptions' )
                                )
                        ) ) {
-                               MediaWiki\Logger\LoggerFactory::getInstance( 'cors' )->warning(
+                               LoggerFactory::getInstance( 'cors' )->warning(
                                        'Non-whitelisted CORS request with session cookies', [
                                                'origin' => $originHeader,
                                                'cookies' => $sessionCookies,
@@ -1453,6 +1455,7 @@ class ApiMain extends ApiBase {
        protected function setRequestExpectations( ApiBase $module ) {
                $limits = $this->getConfig()->get( 'TrxProfilerLimits' );
                $trxProfiler = Profiler::instance()->getTransactionProfiler();
+               $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
                if ( $this->getRequest()->hasSafeMethod() ) {
                        $trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
                } elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
index 3810e90..236fb9e 100644 (file)
@@ -53,6 +53,7 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
                        'code' => 'lh',
                        'prefix' => 'pl',
                        'linktable' => 'pagelinks',
+                       'indexes' => [ 'pl_namespace', 'pl_backlinks_namespace' ],
                        'from_namespace' => true,
                        'showredirects' => true,
                ],
@@ -60,6 +61,7 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
                        'code' => 'ti',
                        'prefix' => 'tl',
                        'linktable' => 'templatelinks',
+                       'indexes' => [ 'tl_namespace', 'tl_backlinks_namespace' ],
                        'from_namespace' => true,
                        'showredirects' => true,
                ],
@@ -67,6 +69,7 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
                        'code' => 'fu',
                        'prefix' => 'il',
                        'linktable' => 'imagelinks',
+                       'indexes' => [ 'il_to', 'il_backlinks_namespace' ],
                        'from_namespace' => true,
                        'to_namespace' => NS_FILE,
                        'exampletitle' => 'File:Example.jpg',
@@ -249,6 +252,18 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
                // Override any ORDER BY from above with what we calculated earlier.
                $this->addOption( 'ORDER BY', array_keys( $sortby ) );
 
+               // MySQL's optimizer chokes if we have too many values in "$bl_title IN
+               // (...)" and chooses the wrong index, so specify the correct index to
+               // use for the query. See T139056 for details.
+               if ( !empty( $settings['indexes'] ) ) {
+                       list( $idxNoFromNS, $idxWithFromNS ) = $settings['indexes'];
+                       if ( $params['namespace'] !== null && !empty( $settings['from_namespace'] ) ) {
+                               $this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxWithFromNS ] );
+                       } else {
+                               $this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxNoFromNS ] );
+                       }
+               }
+
                $this->addOption( 'LIMIT', $params['limit'] + 1 );
 
                $res = $this->select( __METHOD__ );
index 89d9af8..fc2fd59 100644 (file)
@@ -240,7 +240,7 @@ class ApiUpload extends ApiBase {
                                        'offset' => $this->mUpload->getOffset(),
                                ];
 
-                               $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'stashfailed', 0, $extradata );
+                               $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
                        }
                }
 
@@ -271,7 +271,7 @@ class ApiUpload extends ApiBase {
                                                $filekey,
                                                [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
                                        );
-                                       $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'stashfailed' );
+                                       $this->dieStatusWithCode( $status, 'stashfailed' );
                                }
 
                                // The fully concatenated file has a new filekey. So remove
@@ -315,8 +315,9 @@ class ApiUpload extends ApiBase {
         * @return string|null File key
         */
        private function performStash( $failureMode, &$data = null ) {
+               $isPartial = (bool)$this->mParams['chunk'];
                try {
-                       $status = $this->mUpload->tryStashFile( $this->getUser() );
+                       $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
 
                        if ( $status->isGood() && !$status->getValue() ) {
                                // Not actually a 'good' status...
@@ -381,6 +382,28 @@ class ApiUpload extends ApiBase {
                $this->dieUsage( $parsed['info'], $parsed['code'], 0, $data );
        }
 
+       /**
+        * Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from
+        * IApiMessage.
+        *
+        * @param Status $status
+        * @param string $overrideCode Error code to use if there isn't one from IApiMessage
+        * @param array|null $moreExtraData
+        * @throws UsageException
+        */
+       public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
+               $extraData = null;
+               list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData );
+               $errors = $status->getErrorsByType( 'error' ) ?: $status->getErrorsByType( 'warning' );
+               if ( !( $errors[0]['message'] instanceof IApiMessage ) ) {
+                       $code = $overrideCode;
+               }
+               if ( $moreExtraData ) {
+                       $extraData += $moreExtraData;
+               }
+               $this->dieUsage( $msg, $code, 0, $extraData );
+       }
+
        /**
         * Select an upload module and set it to mUpload. Dies on failure. If the
         * request was a status request and not a true upload, returns false;
@@ -403,7 +426,7 @@ class ApiUpload extends ApiBase {
                        if ( !$progress ) {
                                $this->dieUsage( 'No result in status data', 'missingresult' );
                        } elseif ( !$progress['status']->isGood() ) {
-                               $this->dieUsage( $progress['status']->getWikiText( false, false, 'en' ), 'stashfailed' );
+                               $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
                        }
                        if ( isset( $progress['status']->value['verification'] ) ) {
                                $this->checkVerification( $progress['status']->value['verification'] );
@@ -421,7 +444,7 @@ class ApiUpload extends ApiBase {
 
                if ( $this->mParams['chunk'] ) {
                        // Chunk upload
-                       $this->mUpload = new UploadFromChunks();
+                       $this->mUpload = new UploadFromChunks( $this->getUser() );
                        if ( isset( $this->mParams['filekey'] ) ) {
                                if ( $this->mParams['offset'] === 0 ) {
                                        $this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' );
index 80eec50..4b42964 100644 (file)
@@ -25,7 +25,8 @@
                        "Lbayle",
                        "Verdy p",
                        "Yasten",
-                       "Trial"
+                       "Trial",
+                       "Pols12"
                ]
        },
        "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentation]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> Toutes les fonctionnalités affichées sur cette page devraient fonctionner, mais l’API est encore en cours de développement et peut changer à tout moment. Inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un en-tête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet en-tête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Test :</strong> Pour faciliter le test des requêtes de l’API, voyez [[Special:ApiSandbox]].",
        "apihelp-stashedit-param-section": "Numéro de section. <kbd>0</kbd> pour la section du haut, <kbd>new</kbd> pour une nouvelle section.",
        "apihelp-stashedit-param-sectiontitle": "Le titre pour une nouvelle section.",
        "apihelp-stashedit-param-text": "Contenu de la page.",
-       "apihelp-stashedit-param-stashedtexthash": "A substituer par le hachage du contenu de la page venant d’une réserve antérieure.",
+       "apihelp-stashedit-param-stashedtexthash": "Empreinte du contenu de la page venant d’une réserve préalable à utiliser à la place.",
        "apihelp-stashedit-param-contentmodel": "Modèle de contenu du nouveau contenu.",
        "apihelp-stashedit-param-contentformat": "Format de sérialisation de contenu utilisé pour le texte saisi.",
        "apihelp-stashedit-param-baserevid": "ID de révision de la révision de base.",
index 037aff4..860b7a7 100644 (file)
        "apihelp-query+search-paramvalue-prop-snippet": "Adds a parsed snippet of the page.",
        "apihelp-query+search-paramvalue-prop-titlesnippet": "Adds a parsed snippet of the page title.",
        "apihelp-query+search-paramvalue-prop-redirectsnippet": "添加被解析的重定向标题的片段。",
-       "apihelp-query+search-paramvalue-prop-redirecttitle": "Adds the title of the matching redirect.",
+       "apihelp-query+search-paramvalue-prop-redirecttitle": "添加匹配的重定向的标题。",
        "apihelp-query+search-paramvalue-prop-sectionsnippet": "Adds a parsed snippet of the matching section title.",
        "apihelp-query+search-paramvalue-prop-sectiontitle": "Adds the title of the matching section.",
        "apihelp-query+search-paramvalue-prop-categorysnippet": "Adds a parsed snippet of the matching category.",
        "apihelp-query+search-param-enablerewrites": "启用内部查询重写。一些搜索后端可以重写查询到它认为会给出更好结果的地方,例如纠正拼写错误。",
        "apihelp-query+search-example-simple": "搜索<kbd>meaning</kbd>。",
        "apihelp-query+search-example-text": "搜索文本<kbd>meaning</kbd>。",
-       "apihelp-query+search-example-generator": "è\8e·å¾\97有关搜索<kbd>meaning</kbd>返回页面的页面信息。",
+       "apihelp-query+search-example-generator": "è\8e·å\8f\96有关搜索<kbd>meaning</kbd>返回页面的页面信息。",
        "apihelp-query+siteinfo-description": "返回有关网站的一般信息。",
        "apihelp-query+siteinfo-param-prop": "要获取的信息:",
        "apihelp-query+siteinfo-paramvalue-prop-general": "全部系统信息。",
index 50e370e..acdc01b 100644 (file)
@@ -1688,13 +1688,9 @@ class AuthManager implements LoggerAwareInterface {
                        $logEntry->setParameters( [
                                '4::userid' => $user->getId(),
                        ] );
-                       $logid = $logEntry->insert();
+                       $logEntry->insert();
                }
 
-               // Commit database changes, so even if something else later blows up
-               // the newly-created user doesn't get lost.
-               wfGetLBFactory()->commitMasterChanges( __METHOD__ );
-
                $trxProfiler->setSilenced( false );
 
                if ( $login ) {
@@ -2026,37 +2022,26 @@ class AuthManager implements LoggerAwareInterface {
 
                // Query them and merge results
                $reqs = [];
-               $allPrimaryRequired = null;
                foreach ( $providers as $provider ) {
                        $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
-                       $thisRequired = [];
                        foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
                                $id = $req->getUniqueId();
 
-                               // If it's from a Primary, mark it as "primary-required" but
-                               // track it for later.
+                               // If a required request if from a Primary, mark it as "primary-required" instead
                                if ( $isPrimary ) {
                                        if ( $req->required ) {
-                                               $thisRequired[$id] = true;
                                                $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
                                        }
                                }
 
-                               if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
+                               if (
+                                       !isset( $reqs[$id] )
+                                       || $req->required === AuthenticationRequest::REQUIRED
+                                       || $reqs[$id] === AuthenticationRequest::OPTIONAL
+                               ) {
                                        $reqs[$id] = $req;
                                }
                        }
-
-                       // Track which requests are required by all primaries
-                       if ( $isPrimary ) {
-                               $allPrimaryRequired = $allPrimaryRequired === null
-                                       ? $thisRequired
-                                       : array_intersect_key( $allPrimaryRequired, $thisRequired );
-                       }
-               }
-               // Any requests that were required by all primaries are required.
-               foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
-                       $reqs[$id]->required = AuthenticationRequest::REQUIRED;
                }
 
                // AuthManager has its own req for some actions
index ff4d52e..2474b8b 100644 (file)
@@ -43,7 +43,8 @@ abstract class AuthenticationRequest {
        const REQUIRED = 1;
 
        /** Indicates that the request is required by a primary authentication
-        * provdier, but other primary authentication providers do not require it. */
+        * provdier. Since the user can choose which primary to authenticate with,
+        * the request might or might not end up being actually required. */
        const PRIMARY_REQUIRED = 2;
 
        /** @var string|null The AuthManager::ACTION_* constant this request was
@@ -101,6 +102,8 @@ abstract class AuthenticationRequest {
         *  - label: (Message) Text suitable for a label in an HTML form
         *  - help: (Message) Text suitable as a description of what the field is
         *  - optional: (bool) If set and truthy, the field may be left empty
+        *  - sensitive: (bool) If set and truthy, the field is considered sensitive. Code using the
+        *      request should avoid exposing the value of the field.
         *
         * @return array As above
         */
@@ -314,6 +317,8 @@ abstract class AuthenticationRequest {
                                        $options['optional'] = !empty( $options['optional'] );
                                }
 
+                               $options['sensitive'] = !empty( $options['sensitive'] );
+
                                if ( !array_key_exists( $name, $merged ) ) {
                                        $merged[$name] = $options;
                                } elseif ( $merged[$name]['type'] !== $options['type'] ) {
@@ -332,6 +337,7 @@ abstract class AuthenticationRequest {
                                        }
 
                                        $merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
+                                       $merged[$name]['sensitive'] = $merged[$name]['sensitive'] || $options['sensitive'];
 
                                        // No way to merge 'value', 'image', 'help', or 'label', so just use
                                        // the value from the first request.
index 187c29a..8550f3e 100644 (file)
@@ -53,6 +53,7 @@ class PasswordAuthenticationRequest extends AuthenticationRequest {
                                'type' => 'password',
                                'label' => wfMessage( $passwordLabel ),
                                'help' => wfMessage( 'authmanager-password-help' ),
+                               'sensitive' => true,
                        ],
                ];
 
@@ -68,6 +69,7 @@ class PasswordAuthenticationRequest extends AuthenticationRequest {
                                'type' => 'password',
                                'label' => wfMessage( $retypeLabel ),
                                'help' => wfMessage( 'authmanager-retype-help' ),
+                               'sensitive' => true,
                        ];
                }
 
index c44c8fc..35f3287 100644 (file)
@@ -57,6 +57,14 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
        /** Provider cannot create or link to accounts */
        const TYPE_NONE = 'none';
 
+       /**
+        * {@inheritdoc}
+        *
+        * Of the requests returned by this method, exactly one should have
+        * {@link AuthenticationRequest::$required} set to REQUIRED.
+        */
+       public function getAuthenticationRequests( $action, array $options );
+
        /**
         * Start an authentication flow
         *
index 46cbab5..ed94c1a 100644 (file)
@@ -303,7 +303,11 @@ class TemporaryPasswordPrimaryAuthenticationProvider
                );
 
                if ( $sendMail ) {
-                       $this->sendPasswordResetEmail( $req );
+                       // Send email after DB commit
+                       $dbw->onTransactionIdle( function () use ( $req ) {
+                               /** @var TemporaryPasswordAuthenticationRequest $req */
+                               $this->sendPasswordResetEmail( $req );
+                       } );
                }
        }
 
@@ -370,7 +374,10 @@ class TemporaryPasswordPrimaryAuthenticationProvider
                $this->providerChangeAuthenticationData( $req );
 
                if ( $mailpassword ) {
-                       $this->sendNewAccountEmail( $user, $creator, $req->password );
+                       // Send email after DB commit
+                       wfGetDB( DB_MASTER )->onTransactionIdle( function () use ( $user, $creator, $req ) {
+                               $this->sendNewAccountEmail( $user, $creator, $req->password );
+                       } );
                }
 
                return $mailpassword ? 'byemail' : null;
index 7184980..5a5c0d8 100644 (file)
@@ -1156,62 +1156,19 @@ abstract class ContentHandler {
         *
         * @param string $event Event name
         * @param array $args Parameters passed to hook functions
-        * @param bool $warn Whether to log a warning.
-        *                    Default to self::$enableDeprecationWarnings.
-        *                    May be set to false for testing.
+        * @param string|null $deprecatedVersion Emit a deprecation notice
+        *   when the hook is run for the provided version
         *
         * @return bool True if no handler aborted the hook
-        *
-        * @see ContentHandler::$enableDeprecationWarnings
         */
        public static function runLegacyHooks( $event, $args = [],
-               $warn = null
+               $deprecatedVersion = null
        ) {
 
-               if ( $warn === null ) {
-                       $warn = self::$enableDeprecationWarnings;
-               }
-
                if ( !Hooks::isRegistered( $event ) ) {
                        return true; // nothing to do here
                }
 
-               if ( $warn ) {
-                       // Log information about which handlers are registered for the legacy hook,
-                       // so we can find and fix them.
-
-                       $handlers = Hooks::getHandlers( $event );
-                       $handlerInfo = [];
-
-                       MediaWiki\suppressWarnings();
-
-                       foreach ( $handlers as $handler ) {
-                               if ( is_array( $handler ) ) {
-                                       if ( is_object( $handler[0] ) ) {
-                                               $info = get_class( $handler[0] );
-                                       } else {
-                                               $info = $handler[0];
-                                       }
-
-                                       if ( isset( $handler[1] ) ) {
-                                               $info .= '::' . $handler[1];
-                                       }
-                               } elseif ( is_object( $handler ) ) {
-                                       $info = get_class( $handler[0] );
-                                       $info .= '::on' . $event;
-                               } else {
-                                       $info = $handler;
-                               }
-
-                               $handlerInfo[] = $info;
-                       }
-
-                       MediaWiki\restoreWarnings();
-
-                       wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " .
-                               implode( ', ', $handlerInfo ), 2 );
-               }
-
                // convert Content objects to text
                $contentObjects = [];
                $contentTexts = [];
@@ -1229,7 +1186,7 @@ abstract class ContentHandler {
                }
 
                // call the hook functions
-               $ok = Hooks::run( $event, $args );
+               $ok = Hooks::run( $event, $args, $deprecatedVersion );
 
                // see if the hook changed the text
                foreach ( $contentTexts as $k => $orig ) {
index 9baf643..3ad7665 100644 (file)
@@ -154,7 +154,11 @@ class WikitextContentHandler extends TextContentHandler {
        protected function getFileText( Title $title ) {
                $file = wfLocalFile( $title );
                if ( $file && $file->exists() ) {
-                       return $file->getHandler()->getEntireText( $file );
+                       $handler = $file->getHandler();
+                       if ( !$handler ) {
+                               return null;
+                       }
+                       return $handler->getEntireText( $file );
                }
 
                return null;
index 53862b9..4a78363 100644 (file)
@@ -445,7 +445,7 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function begin( $fname = __METHOD__ ) {
+       public function begin( $fname = __METHOD__, $mode = IDatabase::TRANSACTION_EXPLICIT ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
index 5023acd..933bea6 100644 (file)
@@ -703,7 +703,7 @@ abstract class DatabaseBase implements IDatabase {
                                                " performing implicit commit before closing connection!" );
                                }
 
-                               $this->commit( __METHOD__, 'flush' );
+                               $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
                        }
 
                        $closed = $this->closeConnection();
@@ -815,7 +815,7 @@ abstract class DatabaseBase implements IDatabase {
                if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
                        && $this->isTransactableQuery( $sql )
                ) {
-                       $this->begin( __METHOD__ . " ($fname)" );
+                       $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
                        $this->mTrxAutomatic = true;
                }
 
@@ -2203,7 +2203,7 @@ abstract class DatabaseBase implements IDatabase {
 
                $useTrx = !$this->mTrxLevel;
                if ( $useTrx ) {
-                       $this->begin( $fname );
+                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
                }
                try {
                        # Update any existing conflicting row(s)
@@ -2221,7 +2221,7 @@ abstract class DatabaseBase implements IDatabase {
                        throw $e;
                }
                if ( $useTrx ) {
-                       $this->commit( $fname );
+                       $this->commit( $fname, self::TRANSACTION_INTERNAL );
                }
 
                return $ok;
@@ -2520,7 +2520,7 @@ abstract class DatabaseBase implements IDatabase {
                        $this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
                } else {
                        // If no transaction is active, then make one for this callback
-                       $this->begin( __METHOD__ );
+                       $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
                        try {
                                call_user_func( $callback );
                                $this->commit( __METHOD__ );
@@ -2628,7 +2628,7 @@ abstract class DatabaseBase implements IDatabase {
 
        final public function startAtomic( $fname = __METHOD__ ) {
                if ( !$this->mTrxLevel ) {
-                       $this->begin( $fname );
+                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
                        $this->mTrxAutomatic = true;
                        // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
                        // in all changes being in one transaction to keep requests transactional.
@@ -2651,58 +2651,43 @@ abstract class DatabaseBase implements IDatabase {
                }
 
                if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
-                       $this->commit( $fname, 'flush' );
+                       $this->commit( $fname, self::FLUSHING_INTERNAL );
                }
        }
 
        final public function doAtomicSection( $fname, callable $callback ) {
                $this->startAtomic( $fname );
                try {
-                       call_user_func_array( $callback, [ $this, $fname ] );
+                       $res = call_user_func_array( $callback, [ $this, $fname ] );
                } catch ( Exception $e ) {
                        $this->rollback( $fname );
                        throw $e;
                }
                $this->endAtomic( $fname );
+
+               return $res;
        }
 
-       final public function begin( $fname = __METHOD__ ) {
-               if ( $this->mTrxLevel ) { // implicit commit
+       final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
+               // Protect against mismatched atomic section, transaction nesting, and snapshot loss
+               if ( $this->mTrxLevel ) {
                        if ( $this->mTrxAtomicLevels ) {
-                               // If the current transaction was an automatic atomic one, then we definitely have
-                               // a problem. Same if there is any unclosed atomic level.
                                $levels = implode( ', ', $this->mTrxAtomicLevels );
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "Got explicit BEGIN from $fname while atomic section(s) $levels are open."
-                               );
+                               $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
+                               throw new DBUnexpectedError( $this, $msg );
                        } elseif ( !$this->mTrxAutomatic ) {
-                               // We want to warn about inadvertently nested begin/commit pairs, but not about
-                               // auto-committing implicit transactions that were started by query() via DBO_TRX
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "$fname: Transaction already in progress (from {$this->mTrxFname}), " .
-                                               " performing implicit commit!"
-                               );
-                       } elseif ( $this->mTrxDoneWrites ) {
-                               // The transaction was automatic and has done write operations
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "$fname: Automatic transaction with writes in progress" .
-                                               " (from {$this->mTrxFname}), performing implicit commit!\n"
-                               );
-                       }
-
-                       $this->runOnTransactionPreCommitCallbacks();
-                       $writeTime = $this->pendingWriteQueryDuration();
-                       $this->doCommit( $fname );
-                       if ( $this->mTrxDoneWrites ) {
-                               $this->mDoneWrites = microtime( true );
-                               $this->getTransactionProfiler()->transactionWritingOut(
-                                       $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
+                               $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
+                               throw new DBUnexpectedError( $this, $msg );
+                       } else {
+                               // @TODO: make this an exception at some point
+                               $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
+                               wfLogDBError( $msg );
+                               return; // join the main transaction set
                        }
-
-                       $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
+               } elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
+                       // @TODO: make this an exception at some point
+                       wfLogDBError( "$fname: Implicit transaction expected (DBO_TRX set)." );
+                       return; // let any writes be in the main transaction
                }
 
                // Avoid fatals if close() was called
@@ -2742,28 +2727,27 @@ abstract class DatabaseBase implements IDatabase {
                        $levels = implode( ', ', $this->mTrxAtomicLevels );
                        throw new DBUnexpectedError(
                                $this,
-                               "Got COMMIT while atomic sections $levels are still open"
+                               "$fname: Got COMMIT while atomic sections $levels are still open."
                        );
                }
 
-               if ( $flush === 'flush' ) {
+               if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
                        if ( !$this->mTrxLevel ) {
                                return; // nothing to do
                        } elseif ( !$this->mTrxAutomatic ) {
                                throw new DBUnexpectedError(
                                        $this,
-                                       "$fname: Flushing an explicit transaction, getting out of sync!"
+                                       "$fname: Flushing an explicit transaction, getting out of sync."
                                );
                        }
                } else {
                        if ( !$this->mTrxLevel ) {
-                               wfWarn( "$fname: No transaction to commit, something got out of sync!" );
+                               wfWarn( "$fname: No transaction to commit, something got out of sync." );
                                return; // nothing to do
                        } elseif ( $this->mTrxAutomatic ) {
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "$fname: Explicit commit of implicit transaction."
-                               );
+                               // @TODO: make this an exception at some point
+                               wfLogDBError( "$fname: Explicit commit of implicit transaction." );
+                               return; // wait for the main transaction set commit round
                        }
                }
 
@@ -2796,14 +2780,19 @@ abstract class DatabaseBase implements IDatabase {
        }
 
        final public function rollback( $fname = __METHOD__, $flush = '' ) {
-               if ( $flush !== 'flush' ) {
+               if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
                        if ( !$this->mTrxLevel ) {
-                               wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
                                return; // nothing to do
                        }
                } else {
                        if ( !$this->mTrxLevel ) {
+                               wfWarn( "$fname: No transaction to rollback, something got out of sync." );
                                return; // nothing to do
+                       } elseif ( $this->getFlag( DBO_TRX ) ) {
+                               throw new DBUnexpectedError(
+                                       $this,
+                                       "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
+                               );
                        }
                }
 
@@ -3297,16 +3286,32 @@ abstract class DatabaseBase implements IDatabase {
        }
 
        public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
+               if ( $this->writesOrCallbacksPending() ) {
+                       // This only flushes transactions to clear snapshots, not to write data
+                       throw new DBUnexpectedError(
+                               $this,
+                               "$fname: Cannot COMMIT to clear snapshot because writes are pending."
+                       );
+               }
+
                if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
                        return null;
                }
 
                $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
-                       $this->commit( __METHOD__, 'flush' );
-                       $this->unlock( $lockKey, $fname );
+                       if ( $this->trxLevel() ) {
+                               // There is a good chance an exception was thrown, causing any early return
+                               // from the caller. Let any error handler get a chance to issue rollback().
+                               // If there isn't one, let the error bubble up and trigger server-side rollback.
+                               $this->onTransactionResolution( function () use ( $lockKey, $fname ) {
+                                       $this->unlock( $lockKey, $fname );
+                               } );
+                       } else {
+                               $this->unlock( $lockKey, $fname );
+                       }
                } );
 
-               $this->commit( __METHOD__, 'flush' );
+               $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
 
                return $unlocker;
        }
index 6380fe7..d1ebe62 100644 (file)
@@ -767,9 +767,6 @@ abstract class DatabaseMysqlBase extends Database {
                        return 0; // already reached this point for sure
                }
 
-               // Commit any open transactions
-               $this->commit( __METHOD__, 'flush' );
-
                // Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
                if ( $this->useGTIDs && $pos->gtids ) {
                        // Wait on the GTID set (MariaDB only)
index 867aeb8..1ecdd26 100644 (file)
@@ -149,7 +149,7 @@ class SavepointPostgres {
                $this->didbegin = false;
                /* If we are not in a transaction, we need to be for savepoint trickery */
                if ( !$dbw->trxLevel() ) {
-                       $dbw->begin( "FOR SAVEPOINT" );
+                       $dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL );
                        $this->didbegin = true;
                }
        }
@@ -1207,7 +1207,7 @@ __INDEXATTR__;
         * @param string $desiredSchema
         */
        function determineCoreSchema( $desiredSchema ) {
-               $this->begin( __METHOD__ );
+               $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
                if ( $this->schemaExists( $desiredSchema ) ) {
                        if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
                                $this->mCoreSchema = $desiredSchema;
index 43592e2..bdab09e 100644 (file)
  * @ingroup Database
  */
 interface IDatabase {
-       /* Constants to onTransactionResolution() callbacks */
+       /** @var int Callback triggered immediately due to no active transaction */
        const TRIGGER_IDLE = 1;
+       /** @var int Callback triggered by commit */
        const TRIGGER_COMMIT = 2;
+       /** @var int Callback triggered by rollback */
        const TRIGGER_ROLLBACK = 3;
 
+       /** @var string Transaction is requested by regular caller outside of the DB layer */
+       const TRANSACTION_EXPLICIT = '';
+       /** @var string Transaction is requested interally via DBO_TRX/startAtomic() */
+       const TRANSACTION_INTERNAL = 'implicit';
+
+       /** @var string Transaction operation comes from service managing all DBs */
+       const FLUSHING_ALL_PEERS = 'flush';
+       /** @var string Transaction operation comes from the database class internally */
+       const FLUSHING_INTERNAL = 'flush';
+
        /**
         * A string describing the current software version, and possibly
         * other details in a user-friendly way. Will be listed on Special:Version, etc.
@@ -1335,6 +1347,7 @@ interface IDatabase {
         *
         * @param string $fname Caller name (usually __METHOD__)
         * @param callable $callback Callback that issues DB updates
+        * @return mixed $res Result of the callback (since 1.28)
         * @throws DBError
         * @throws RuntimeException
         * @throws UnexpectedValueException
@@ -1346,6 +1359,10 @@ interface IDatabase {
         * Begin a transaction. If a transaction is already in progress,
         * that transaction will be committed before the new transaction is started.
         *
+        * Only call this from code with outer transcation scope.
+        * See https://www.mediawiki.org/wiki/Database_transactions for details.
+        * Nesting of transactions is not supported.
+        *
         * Note that when the DBO_TRX flag is set (which is usually the case for web
         * requests, but not for maintenance scripts), any previous database query
         * will have started a transaction automatically.
@@ -1355,20 +1372,23 @@ interface IDatabase {
         * automatically because of the DBO_TRX flag.
         *
         * @param string $fname
+        * @param string $mode A situationally valid IDatabase::TRANSACTION_* constant [optional]
         * @throws DBError
         */
-       public function begin( $fname = __METHOD__ );
+       public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT );
 
        /**
         * Commits a transaction previously started using begin().
         * If no transaction is in progress, a warning is issued.
         *
+        * Only call this from code with outer transcation scope.
+        * See https://www.mediawiki.org/wiki/Database_transactions for details.
         * Nesting of transactions is not supported.
         *
         * @param string $fname
-        * @param string $flush Flush flag, set to 'flush' to disable warnings about
-        *   explicitly committing implicit transactions, or calling commit when no
-        *   transaction is in progress.
+        * @param string $flush Flush flag, set to situationally valid IDatabase::FLUSHING_*
+        *   constant to disable warnings about explicitly committing implicit transactions,
+        *   or calling commit when no transaction is in progress.
         *
         *   This will trigger an exception if there is an ongoing explicit transaction.
         *
@@ -1383,13 +1403,17 @@ interface IDatabase {
         * Rollback a transaction previously started using begin().
         * If no transaction is in progress, a warning is issued.
         *
-        * No-op on non-transactional databases.
+        * Only call this from code with outer transcation scope.
+        * See https://www.mediawiki.org/wiki/Database_transactions for details.
+        * Nesting of transactions is not supported. If a serious unexpected error occurs,
+        * throwing an Exception is preferrable, using a pre-installed error handler to trigger
+        * rollback (in any case, failure to issue COMMIT will cause rollback server-side).
         *
         * @param string $fname
-        * @param string $flush Flush flag, set to 'flush' to disable warnings about
-        *   calling rollback when no transaction is in progress. This will silently
-        *   break any ongoing explicit transaction. Only set the flush flag if you
-        *   are sure that it is safe to ignore these warnings in your context.
+        * @param string $flush Flush flag, set to a situationally valid IDatabase::FLUSHING_*
+        *   constant to disable warnings about calling rollback when no transaction is in
+        *   progress. This will silently break any ongoing explicit transaction. Only set the
+        *   flush flag if you are sure that it is safe to ignore these warnings in your context.
         * @throws DBUnexpectedError
         * @since 1.23 Added $flush parameter
         */
@@ -1554,10 +1578,14 @@ interface IDatabase {
        /**
         * Acquire a named lock, flush any transaction, and return an RAII style unlocker object
         *
+        * Only call this from outer transcation scope and when only one DB will be affected.
+        * See https://www.mediawiki.org/wiki/Database_transactions for details.
+        *
         * This is suitiable for transactions that need to be serialized using cooperative locks,
         * where each transaction can see each others' changes. Any transaction is flushed to clear
         * out stale REPEATABLE-READ snapshot data. Once the returned object falls out of PHP scope,
-        * any transaction will be committed and the lock will be released.
+        * the lock will be released unless a transaction is active. If one is active, then the lock
+        * will be released when it either commits or rolls back.
         *
         * If the lock acquisition failed, then no transaction flush happens, and null is returned.
         *
index 4078a39..efc6148 100644 (file)
@@ -42,6 +42,8 @@ abstract class LBFactory implements DestructibleService {
        /** @var WANObjectCache */
        protected $wanCache;
 
+       /** @var mixed */
+       protected $ticket;
        /** @var string|bool Reason all LBs are read-only or false if not */
        protected $readOnlyReason = false;
 
@@ -72,6 +74,7 @@ abstract class LBFactory implements DestructibleService {
                        $this->wanCache = WANObjectCache::newEmpty();
                }
                $this->trxLogger = LoggerFactory::getInstance( 'DBTransaction' );
+               $this->ticket = mt_rand();
        }
 
        /**
@@ -404,6 +407,44 @@ abstract class LBFactory implements DestructibleService {
                }
        }
 
+       /**
+        * Get a token asserting that no transaction writes are active
+        *
+        * @param string $fname Caller name (e.g. __METHOD__)
+        * @return mixed A value to pass to commitAndWaitForReplication()
+        * @since 1.28
+        */
+       public function getEmptyTransactionTicket( $fname ) {
+               if ( $this->hasMasterChanges() ) {
+                       $this->trxLogger->error( __METHOD__ . ": $fname does not have outer scope." );
+                       return null;
+               }
+
+               return $this->ticket;
+       }
+
+       /**
+        * Convenience method for safely running commitMasterChanges()/waitForReplication()
+        *
+        * This will commit and wait unless $ticket indicates it is unsafe to do so
+        *
+        * @param string $fname Caller name (e.g. __METHOD__)
+        * @param mixed $ticket Result of getOuterTransactionScopeTicket()
+        * @param array $opts Options to waitForReplication()
+        * @throws DBReplicationWaitError
+        * @since 1.28
+        */
+       public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
+               if ( $ticket !== $this->ticket ) {
+                       $logger = LoggerFactory::getInstance( 'DBPerformance' );
+                       $logger->error( __METHOD__ . ": cannot commit; $fname does not have outer scope." );
+                       return;
+               }
+
+               $this->commitMasterChanges( $fname );
+               $this->waitForReplication( $opts );
+       }
+
        /**
         * Disable the ChronologyProtector for all load balancers
         *
index 0f3ca93..4b9cccc 100644 (file)
@@ -364,6 +364,8 @@ class LBFactoryMulti extends LBFactory {
                        }
                        $serverInfo['hostName'] = $serverName;
                        $serverInfo['load'] = $load;
+                       $serverInfo += [ 'flags' => DBO_DEFAULT ];
+
                        $servers[] = $serverInfo;
                }
 
index 14baf2e..3702c8b 100644 (file)
@@ -56,6 +56,7 @@ class LBFactorySimple extends LBFactory {
                                } else {
                                        $server['slave'] = true;
                                }
+                               $server += [ 'flags' => DBO_DEFAULT ];
                        }
                } else {
                        global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
index a7c486c..d08172a 100644 (file)
@@ -1062,7 +1062,7 @@ class LoadBalancer {
         */
        public function commitAll( $fname = __METHOD__ ) {
                $this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( $fname ) {
-                       $conn->commit( $fname, 'flush' );
+                       $conn->commit( $fname, IDatabase::FLUSHING_ALL_PEERS );
                } );
        }
 
@@ -1109,7 +1109,7 @@ class LoadBalancer {
        public function commitMasterChanges( $fname = __METHOD__ ) {
                $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $fname ) {
                        if ( $conn->writesOrCallbacksPending() ) {
-                               $conn->commit( $fname, 'flush' );
+                               $conn->commit( $fname, IDatabase::FLUSHING_ALL_PEERS );
                        }
                } );
        }
@@ -1143,7 +1143,7 @@ class LoadBalancer {
                        foreach ( $conns2[$masterIndex] as $conn ) {
                                if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
                                        try {
-                                               $conn->rollback( $fname, 'flush' );
+                                               $conn->rollback( $fname, IDatabase::FLUSHING_ALL_PEERS );
                                        } catch ( DBError $e ) {
                                                MWExceptionHandler::logException( $e );
                                                $failedServers[] = $conn->getServer();
index 2865461..5b84ca9 100644 (file)
  *       subclasses can override the beginTransaction() and commitTransaction() methods.
  */
 abstract class DataUpdate implements DeferrableUpdate {
+       /** @var mixed Result from LBFactory::getEmptyTransactionTicket() */
+       protected $ticket;
+
        public function __construct() {
                // noop
        }
 
+       /**
+        * @param mixed $ticket Result of getEmptyTransactionTicket()
+        * @since 1.28
+        */
+       public function setTransactionTicket( $ticket ) {
+               $this->ticket = $ticket;
+       }
+
        /**
         * Begin an appropriate transaction, if any.
         * This default implementation does nothing.
index 0009781..47f2b21 100644 (file)
@@ -54,6 +54,7 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
        public function doUpdate() {
                $config = RequestContext::getMain()->getConfig();
                $batchSize = $config->get( 'UpdateRowsPerQuery' );
+               $factory = wfGetLBFactory();
 
                // Page may already be deleted, so don't just getId()
                $id = $this->pageId;
@@ -77,8 +78,9 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
                foreach ( $catBatches as $catBatch ) {
                        $this->page->updateCategoryCounts( [], $catBatch, $id );
                        if ( count( $catBatches ) > 1 ) {
-                               $this->mDb->commit( __METHOD__, 'flush' );
-                               wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] );
+                               $factory->commitAndWaitForReplication(
+                                       __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+                               );
                        }
                }
 
@@ -173,8 +175,9 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
                        foreach ( $rcIdBatches as $rcIdBatch ) {
                                $this->mDb->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ );
                                if ( count( $rcIdBatches ) > 1 ) {
-                                       $this->mDb->commit( __METHOD__, 'flush' );
-                                       wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] );
+                                       $factory->commitAndWaitForReplication(
+                                               __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+                                       );
                                }
                        }
                }
@@ -185,6 +188,7 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
 
        private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) {
                $dbw = $this->mDb; // convenience
+               $factory = wfGetLBFactory();
                $res = $dbw->select( $table, $pk, $conds, __METHOD__ );
 
                $pkDeleteConds = [];
@@ -192,8 +196,9 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
                        $pkDeleteConds[] = $this->mDb->makeList( (array)$row, LIST_AND );
                        if ( count( $pkDeleteConds ) >= $bSize ) {
                                $dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ );
-                               $dbw->commit( __METHOD__, 'flush' );
-                               wfGetLBFactory()->waitForReplication( [ 'wiki' => $dbw->getWikiID() ] );
+                               $factory->commitAndWaitForReplication(
+                                       __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+                               );
                                $pkDeleteConds = [];
                        }
                }
index 22944eb..4f40c38 100644 (file)
@@ -335,6 +335,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         */
        private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
                $bSize = RequestContext::getMain()->getConfig()->get( 'UpdateRowsPerQuery' );
+               $factory = wfGetLBFactory();
 
                if ( $table === 'page_props' ) {
                        $fromField = 'pp_page';
@@ -386,15 +387,17 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
 
                foreach ( $deleteWheres as $deleteWhere ) {
                        $this->mDb->delete( $table, $deleteWhere, __METHOD__ );
-                       $this->mDb->commit( __METHOD__, 'flush' );
-                       wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] );
+                       $factory->commitAndWaitForReplication(
+                               __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+                       );
                }
 
                $insertBatches = array_chunk( $insertions, $bSize );
                foreach ( $insertBatches as $insertBatch ) {
                        $this->mDb->insert( $table, $insertBatch, __METHOD__, 'IGNORE' );
-                       $this->mDb->commit( __METHOD__, 'flush' );
-                       wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] );
+                       $factory->commitAndWaitForReplication(
+                               __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+                       );
                }
 
                if ( count( $insertions ) ) {
index f39f54b..5d8f491 100644 (file)
@@ -257,7 +257,7 @@ class HTMLFormFieldCloner extends HTMLFormField {
         * @param array $values
         * @return string
         */
-       protected function getInputHTMLForKey( $key, $values ) {
+       protected function getInputHTMLForKey( $key, array $values ) {
                $displayFormat = isset( $this->mParams['format'] )
                        ? $this->mParams['format']
                        : $this->mParent->getDisplayFormat();
index 5e3758d..d508d76 100644 (file)
@@ -294,10 +294,6 @@ abstract class Installer {
                        'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
                        'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
                ],
-               'pd' => [
-                       'url' => '',
-                       'icon' => '$wgResourceBasePath/resources/assets/licenses/public-domain.png',
-               ],
                'gfdl' => [
                        'url' => 'https://www.gnu.org/copyleft/fdl.html',
                        'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
index 57e69b4..b561021 100644 (file)
@@ -64,7 +64,7 @@ class CategoryMembershipChangeJob extends Job {
                        return false;
                }
                // Clear any stale REPEATABLE-READ snapshot
-               $dbr->commit( __METHOD__, 'flush' );
+               wfGetLBFactory()->commitAll( __METHOD__ );
 
                $cutoffUnix = wfTimestamp( TS_UNIX, $this->params['revTimestamp'] );
                // Using ENQUEUE_FUDGE_SEC handles jobs inserted out of revision order due to the delay
@@ -157,6 +157,9 @@ class CategoryMembershipChangeJob extends Job {
                }
 
                $dbw = wfGetDB( DB_MASTER );
+               $factory = wfGetLBFactory();
+               $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
+
                $catMembChange = new CategoryMembershipChange( $title, $newRev );
                $catMembChange->checkTemplateLinks();
 
@@ -167,8 +170,7 @@ class CategoryMembershipChangeJob extends Job {
                        $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName );
                        $catMembChange->triggerCategoryAddedNotification( $categoryTitle );
                        if ( $insertCount++ && ( $insertCount % $batchSize ) == 0 ) {
-                               $dbw->commit( __METHOD__, 'flush' );
-                               wfGetLBFactory()->waitForReplication();
+                               $factory->commitAndWaitForReplication( __METHOD__, $ticket );
                        }
                }
 
@@ -176,8 +178,7 @@ class CategoryMembershipChangeJob extends Job {
                        $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName );
                        $catMembChange->triggerCategoryRemovedNotification( $categoryTitle );
                        if ( $insertCount++ && ( $insertCount++ % $batchSize ) == 0 ) {
-                               $dbw->commit( __METHOD__, 'flush' );
-                               wfGetLBFactory()->waitForReplication();
+                               $factory->commitAndWaitForReplication( __METHOD__, $ticket );
                        }
                }
        }
index f39f8fd..8d565bd 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @ingroup JobQueue
  */
+use \MediaWiki\MediaWikiServices;
 
 /**
  * Job to prune link tables for pages that were deleted
@@ -52,10 +53,12 @@ class DeleteLinksJob extends Job {
                        return false;
                }
 
+               $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                $timestamp = isset( $this->params['timestamp'] ) ? $this->params['timestamp'] : null;
-
                $page = WikiPage::factory( $this->title ); // title when deleted
+
                $update = new LinksDeletionUpdate( $page, $pageId, $timestamp );
+               $update->setTransactionTicket( $factory->getEmptyTransactionTicket( __METHOD__ ) );
                DataUpdate::runUpdates( [ $update ] );
 
                return true;
index a14cdd7..f09ba57 100644 (file)
@@ -113,11 +113,12 @@ class HTMLCacheUpdateJob extends Job {
                $touchTimestamp = wfTimestampNow();
 
                $dbw = wfGetDB( DB_MASTER );
+               $factory = wfGetLBFactory();
+               $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
                // Update page_touched (skipping pages already touched since the root job).
                // Check $wgUpdateRowsPerQuery for sanity; batch jobs are sized by that already.
                foreach ( array_chunk( $pageIds, $wgUpdateRowsPerQuery ) as $batch ) {
-                       $dbw->commit( __METHOD__, 'flush' );
-                       wfGetLBFactory()->waitForReplication();
+                       $factory->commitAndWaitForReplication( __METHOD__, $ticket );
 
                        $dbw->update( 'page',
                                [ 'page_touched' => $dbw->timestamp( $touchTimestamp ) ],
index fbc1572..2fd3899 100644 (file)
@@ -81,6 +81,8 @@ class RecentChangesUpdateJob extends Job {
                        return; // already in progress
                }
 
+               $factory = wfGetLBFactory();
+               $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
                $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
                do {
                        $rcIds = $dbw->selectFieldValues( 'recentchanges',
@@ -91,14 +93,11 @@ class RecentChangesUpdateJob extends Job {
                        );
                        if ( $rcIds ) {
                                $dbw->delete( 'recentchanges', [ 'rc_id' => $rcIds ], __METHOD__ );
-                       }
-                       // Commit in chunks to avoid slave lag
-                       $dbw->commit( __METHOD__, 'flush' );
-
-                       if ( count( $rcIds ) === $wgUpdateRowsPerQuery ) {
                                // There might be more, so try waiting for slaves
                                try {
-                                       wfGetLBFactory()->waitForReplication( [ 'timeout' => 3 ] );
+                                       $factory->commitAndWaitForReplication(
+                                               __METHOD__, $ticket, [ 'timeout' => 3 ]
+                                       );
                                } catch ( DBReplicationWaitError $e ) {
                                        // Another job will continue anyway
                                        break;
@@ -121,6 +120,8 @@ class RecentChangesUpdateJob extends Job {
                // JobRunner uses DBO_TRX, but doesn't call begin/commit itself;
                // onTransactionIdle() will run immediately since there is no trx.
                $dbw->onTransactionIdle( function() use ( $dbw, $days, $window ) {
+                       $factory = wfGetLBFactory();
+                       $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
                        // Avoid disconnect/ping() cycle that makes locks fall off
                        $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
 
@@ -204,7 +205,7 @@ class RecentChangesUpdateJob extends Job {
                                }
                                foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
                                        $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
-                                       wfGetLBFactory()->waitForReplication();
+                                       $factory->commitAndWaitForReplication( __METHOD__, $ticket );
                                }
                        }
 
index 8fba728..9cdb161 100644 (file)
@@ -241,7 +241,10 @@ class RefreshLinksJob extends Job {
                        $parserOutput
                );
 
+               $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
                foreach ( $updates as $key => $update ) {
+                       $update->setTransactionTicket( $ticket );
                        // FIXME: This code probably shouldn't be here?
                        // Needed by things like Echo notifications which need
                        // to know which user caused the links update
index c48880f..5556dd8 100644 (file)
@@ -471,6 +471,27 @@ class SqlBagOStuff extends BagOStuff {
                return $ok;
        }
 
+       public function changeTTL( $key, $expiry = 0 ) {
+               list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               try {
+                       $db = $this->getDB( $serverIndex );
+                       $db->update(
+                               $tableName,
+                               [ 'exptime' => $db->timestamp( $this->convertExpiry( $expiry ) ) ],
+                               [ 'keyname' => $key, 'exptime > ' . $db->addQuotes( $db->timestamp( time() ) ) ],
+                               __METHOD__
+                       );
+                       if ( $db->affectedRows() == 0 ) {
+                               return false;
+                       }
+               } catch ( DBError $e ) {
+                       $this->handleWriteError( $e, $serverIndex );
+                       return false;
+               }
+
+               return true;
+       }
+
        /**
         * @param IDatabase $db
         * @param string $exptime
index 3093cde..dc70af4 100644 (file)
@@ -33,6 +33,9 @@ class ResourceLoaderClientHtml {
        /** @var ResourceLoader */
        private $resourceLoader;
 
+       /** @var string|null */
+       private $target;
+
        /** @var array */
        private $config = [];
 
@@ -53,10 +56,12 @@ class ResourceLoaderClientHtml {
 
        /**
         * @param ResourceLoaderContext $context
+        * @param aray $target [optional] Custom 'target' parameter for the startup module
         */
-       public function __construct( ResourceLoaderContext $context ) {
+       public function __construct( ResourceLoaderContext $context, $target = null ) {
                $this->context = $context;
                $this->resourceLoader = $context->getResourceLoader();
+               $this->target = $target;
        }
 
        /**
@@ -316,7 +321,13 @@ class ResourceLoaderClientHtml {
                }
 
                // Async scripts. Once the startup is loaded, inline RLQ scripts will run.
-               $chunks[] = $this->getLoad( 'startup', ResourceLoaderModule::TYPE_SCRIPTS );
+               // Pass-through a custom target from OutputPage (T143066).
+               $startupQuery = $this->target ? [ 'target' => $this->target ] : [];
+               $chunks[] = $this->getLoad(
+                       'startup',
+                       ResourceLoaderModule::TYPE_SCRIPTS,
+                       $startupQuery
+               );
 
                return WrappedStringList::join( "\n", $chunks );
        }
@@ -358,8 +369,8 @@ class ResourceLoaderClientHtml {
                return self::makeContext( $this->context, $group, $type );
        }
 
-       private function getLoad( $modules, $only ) {
-               return self::makeLoad( $this->context, (array)$modules, $only );
+       private function getLoad( $modules, $only, array $extraQuery = [] ) {
+               return self::makeLoad( $this->context, (array)$modules, $only, $extraQuery );
        }
 
        private static function makeContext( ResourceLoaderContext $mainContext, $group, $type,
index 3b3bdf7..6cdab1b 100644 (file)
@@ -455,4 +455,11 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
                $this->loadFromDefinition();
                return $this->position;
        }
+
+       /**
+        * @return string
+        */
+       public function getType() {
+               return self::LOAD_STYLES;
+       }
 }
index 749a686..b4be461 100644 (file)
@@ -478,6 +478,9 @@ class SkinTemplate extends Skin {
                $tpl->set( 'sidebar', $this->buildSidebar() );
                $tpl->set( 'nav_urls', $this->buildNavUrls() );
 
+               // Do this last in case hooks above add bottom scripts
+               $tpl->set( 'bottomscripts', $this->bottomScripts() );
+
                // Set the head scripts near the end, in case the above actions resulted in added scripts
                $tpl->set( 'headelement', $out->headElement( $this ) );
 
@@ -508,9 +511,6 @@ class SkinTemplate extends Skin {
                // See Skin::afterContentHook() for further documentation.
                $tpl->set( 'dataAfterContent', $this->afterContentHook() );
 
-               // Do this last in case hooks above add bottom scripts
-               $tpl->set( 'bottomscripts', $this->bottomScripts() );
-
                return $tpl;
        }
 
index 9f534d2..ae16f70 100644 (file)
@@ -924,23 +924,22 @@ abstract class UploadBase {
        }
 
        /**
-        * Like stashFile(), but respects extensions' wishes to prevent the stashing.
+        * Like stashFile(), but respects extensions' wishes to prevent the stashing. verifyUpload() must
+        * be called before calling this method (unless $isPartial is true).
         *
         * Upload stash exceptions are also caught and converted to an error status.
         *
         * @since 1.28
         * @param User $user
+        * @param bool $isPartial Pass `true` if this is a part of a chunked upload (not a complete file).
         * @return Status If successful, value is an UploadStashFile instance
         */
-       public function tryStashFile( User $user ) {
-               $props = $this->mFileProps;
-               $error = null;
-               Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] );
-               if ( $error ) {
-                       if ( !is_array( $error ) ) {
-                               $error = [ $error ];
+       public function tryStashFile( User $user, $isPartial = false ) {
+               if ( !$isPartial ) {
+                       $error = $this->runUploadStashFileHook( $user );
+                       if ( $error ) {
+                               return call_user_func_array( 'Status::newFatal', $error );
                        }
-                       return call_user_func_array( 'Status::newFatal', $error );
                }
                try {
                        $file = $this->doStashFile( $user );
@@ -950,6 +949,22 @@ abstract class UploadBase {
                }
        }
 
+       /**
+        * @param User $user
+        * @return array|null Error message and parameters, null if there's no error
+        */
+       protected function runUploadStashFileHook( User $user ) {
+               $props = $this->mFileProps;
+               $error = null;
+               Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] );
+               if ( $error ) {
+                       if ( !is_array( $error ) ) {
+                               $error = [ $error ];
+                       }
+               }
+               return $error;
+       }
+
        /**
         * If the user does not supply all necessary information in the first upload
         * form submission (either by accident or by design) then we may want to
index cc1d698..6368db8 100644 (file)
@@ -38,12 +38,11 @@ class UploadFromChunks extends UploadFromFile {
        /**
         * Setup local pointers to stash, repo and user (similar to UploadFromStash)
         *
-        * @param User|null $user Default: null
+        * @param User $user
         * @param UploadStash|bool $stash Default: false
         * @param FileRepo|bool $repo Default: false
         */
-       public function __construct( $user = null, $stash = false, $repo = false ) {
-               // user object. sometimes this won't exist, as when running from cron.
+       public function __construct( User $user, $stash = false, $repo = false ) {
                $this->user = $user;
 
                if ( $repo ) {
@@ -162,7 +161,20 @@ class UploadFromChunks extends UploadFromFile {
                // Update the mTempPath and mLocalFile
                // (for FileUpload or normal Stash to take over)
                $tStart = microtime( true );
-               $this->mLocalFile = parent::doStashFile( $this->user );
+               // This is a re-implementation of UploadBase::tryStashFile(), we can't call it because we
+               // override doStashFile() with completely different functionality in this class...
+               $error = $this->runUploadStashFileHook( $this->user );
+               if ( $error ) {
+                       call_user_func_array( [ $status, 'fatal' ], $error );
+                       return $status;
+               }
+               try {
+                       $this->mLocalFile = parent::doStashFile( $this->user );
+               } catch ( UploadStashException $e ) {
+                       $status->fatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
+                       return $status;
+               }
+
                $tAmount = microtime( true ) - $tStart;
                $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
                wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." );
@@ -235,8 +247,6 @@ class UploadFromChunks extends UploadFromFile {
                        $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" );
 
                $dbw = $this->repo->getMasterDB();
-               // Use a quick transaction since we will upload the full temp file into shared
-               // storage, which takes time for large files. We don't want to hold locks then.
                $dbw->update(
                        'uploadstash',
                        [
@@ -247,7 +257,6 @@ class UploadFromChunks extends UploadFromFile {
                        [ 'us_key' => $this->mFileKey ],
                        __METHOD__
                );
-               $dbw->commit( __METHOD__, 'flush' );
        }
 
        /**
index 50bcbc4..1fbdb7d 100644 (file)
@@ -143,24 +143,6 @@ class UploadFromStash extends UploadBase {
                return $this->mFileProps['sha1'];
        }
 
-       /*
-        * protected function verifyFile() inherited
-        */
-
-       /**
-        * Stash the file.
-        *
-        * @param User $user
-        * @return UploadStashFile
-        */
-       protected function doStashFile( User $user = null ) {
-               // replace mLocalFile with an instance of UploadStashFile, which adds some methods
-               // that are useful for stashed files.
-               $this->mLocalFile = parent::doStashFile( $user );
-
-               return $this->mLocalFile;
-       }
-
        /**
         * Remove a temporarily kept file stashed by saveTempUploadedFile().
         * @return bool Success
index ab665a4..83cfa40 100644 (file)
@@ -4190,6 +4190,8 @@ class User implements IDBAccessObject {
         * login credentials aren't being hijacked with a foreign form
         * submission.
         *
+        * The $salt for 'edit' and 'csrf' tokens is the default (empty string).
+        *
         * @since 1.19
         * @param string|array $salt Array of Strings Optional function-specific data for hashing
         * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
index d96710a..8b39d77 100644 (file)
  * @defgroup Language Language
  */
 
-if ( !defined( 'MEDIAWIKI' ) ) {
-       echo "This file is part of MediaWiki, it is not a valid entry point.\n";
-       exit( 1 );
-}
-
 use CLDRPluralRuleParser\Evaluator;
 
 /**
index 026c3f8..3432c23 100644 (file)
        "alllogstext": "{{SITENAME}} üçün bütün mövcud qeydlərin birgə göstərişi.\nQeyd növü, istifadəçi adı və ya təsir edilmiş səhifəni seçməklə daha spesifik ola bilərsiniz.",
        "logempty": "Jurnalda uyğun qeyd tapılmadı.",
        "log-title-wildcard": "Bu mətnlə başlayan başlıqları axtar",
+       "checkbox-select": "Seçin: $1",
+       "checkbox-all": "Hamısı",
+       "checkbox-none": "Heç biri",
+       "checkbox-invert": "Çevir",
        "allpages": "Bütün səhifələr",
        "nextpage": "Sonrakı səhifə ($1)",
        "prevpage": "Əvvəlki səhifə ($1)",
index 4921d09..aa81ac2 100644 (file)
        "right-changetags": "নির্দিষ্ট সংস্করণ এবং দীর্ঘ সম্পাদনাগুলোতে [[Special:Tags|ট্যাগ]] সংযোজন ও অপসারণ করুন",
        "right-deletechangetags": "ডাটাবেজ থেকে [[Special:Tags|ট্যাগ]] অপসারণ করা",
        "grant-group-email": "ইমেইল পাঠান",
+       "grant-group-private-information": "আপনার সম্পর্কিত ব্যক্তিগত তথ্যে প্রবেশাধিকার পায়",
        "grant-group-other": "বিবিধ কার্যকলাপ",
        "grant-createaccount": "অ্যাকাউন্ট তৈরি করুন",
        "grant-createeditmovepage": "পাতা তৈরি, সম্পাদনা এবং স্থানান্তর করুন",
        "grant-editmyoptions": "আপনার ব্যবহারকারী পছন্দসমূহ সম্পাদনা করুন",
        "grant-editmywatchlist": "আপনার নজরতালিকা সম্পাদনা করুন",
        "grant-editprotected": "সংরক্ষিত পাতা সম্পাদনা করুন",
+       "grant-privateinfo": "ব্যক্তিগত তথ্যে প্রবেশাধিকার",
        "grant-sendemail": "অন্য ব্যবহারকারীকে ইমেইল পাঠান",
        "grant-uploadfile": "নতুন ফাইল আপলোড করুন",
        "grant-basic": "মৌলিক অধিকার",
index 052c3ce..1fb2401 100644 (file)
        "editlink": "دەستکاری",
        "viewsourcelink": "بینینی سەرچاوە",
        "editsectionhint": "دەستکاریکردنی بەش: $1",
-       "toc": "Ù\86اÙ\88Û\95Ú\95Û\86Ú©",
+       "toc": "Ù¾Û\8eرست",
        "showtoc": "نیشانیبدە",
        "hidetoc": "بیشارەوە",
        "collapsible-collapse": "کۆی بکەوە",
index 6e74495..7e9eecf 100644 (file)
        "grant-group-high-volume": "Velkoobjemové činnosti",
        "grant-group-customization": "Nastavení a přizpůsobení",
        "grant-group-administration": "Provádění správcovských činností",
-       "grant-group-private-information": "Zpřístupnit soukromá data o vás",
+       "grant-group-private-information": "Přístup k soukromým údajům o vás",
        "grant-group-other": "Různé činnosti",
        "grant-blockusers": "Blokovat a odblokovávat uživatele",
        "grant-createaccount": "Zakládat účty",
        "grant-highvolume": "Hromadné editace",
        "grant-oversight": "Skrývat uživatele a utajovat revize",
        "grant-patrol": "Patrolovat změny stránek",
+       "grant-privateinfo": "Přístup k soukromým údajům",
        "grant-protect": "Zamykat a odemykat stránky",
        "grant-rollback": "Vracet editace zpět",
        "grant-sendemail": "Posílat e-maily ostatním uživatelům",
        "uploadstash-errclear": "Soubory se nepodařilo vymazat.",
        "uploadstash-refresh": "Aktualizovat seznam souborů",
        "uploadstash-thumbnail": "zobrazit náhled",
+       "uploadstash-exception": "Načtený soubor se nepodařilo uložit do skrýše ($1): „$2“.",
        "invalid-chunk-offset": "Neplatný posun bloku",
        "img-auth-accessdenied": "Přístup odepřen",
        "img-auth-nopathinfo": "Chybí PATH_INFO.\nVáš server není nastaven tak, aby tuto informaci poskytoval.\nMožná funguje pomocí CGI a img_auth na něm nemůže fungovat.\nVizte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "trackingcategories-name": "Název hlášení",
        "trackingcategories-desc": "Kritéria pro vložení do kategorie",
        "restricted-displaytitle-ignored": "Stránky s ignorovanými zobrazovanými názvy",
-       "restricted-displaytitle-ignored-desc": "Stránky obsahující příkaz <code><nowiki>{{DISPLAYTITLE}}</nowiki></code>, který se ignoruje, neboť není ekvivalentní skutečnému názvu stránky.",
+       "restricted-displaytitle-ignored-desc": "Stránka obsahuje příkaz <code><nowiki>{{DISPLAYTITLE}}</nowiki></code>, který se ignoruje, neboť není ekvivalentní skutečnému názvu stránky.",
        "noindex-category-desc": "Stránka není indexována roboty, protože obsahuje kouzelné slovo <code><nowiki>__NOINDEX__</nowiki></code> a je ve jmenném prostoru, ve kterém je tento příznak dovolen.",
        "index-category-desc": "Stránka obsahuje kouzelné slovo <code><nowiki>__INDEX__</nowiki></code> (a je ve jmenném prostoru, ve kterém je tento příznak dovolen), takže je indexována roboty, přestože by normálně nebyla.",
        "post-expand-template-inclusion-category-desc": "Stránka je po rozbalení všech šablon větší než <code>$wgMaxArticleSize</code>, takže některé šablony rozbaleny nebyly.",
index 0b39d6e..7ca8358 100644 (file)
        "anontalk": "Werênayış",
        "navigation": "Pusula",
        "and": "&#32;u",
-       "qbfind": "Bıvêne",
+       "qbfind": "Bıvin",
        "qbbrowse": "Çım ra viyarne",
        "qbedit": "Bıvurne",
        "qbpageoptions": "Ena pele",
        "view-foreign": "$1 de bıvêne",
        "edit": "Bıvurne",
        "edit-local": "Şınasnayışê lokali bıvurne",
-       "create": "Vıraze",
+       "create": "Bıvıraz",
        "create-local": "Şınasnayışê lokali cı ke",
        "editthispage": "Ena pele bıvurne",
        "create-this-page": "Na pele bınuse",
        "talkpagelinktext": "werênayış",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê şexsiy",
-       "articlepage": "Pela zerreki bıvêne",
+       "articlepage": "Pera zerreki bıvin",
        "talk": "Werênayış",
        "views": "Asayışi",
        "toolbox": "Haceti",
        "imagepage": "Pera dosya bıasne",
        "mediawikipage": "Pera mesaci bıasne",
        "templatepage": "Pera şabloni bıasne",
-       "viewhelppage": "Pela peşti bıvêne",
+       "viewhelppage": "Pera peşti bıvin",
        "categorypage": "Pela kategoriya bıasne",
        "viewtalkpage": "Werênayışi bıvêne",
        "otherlanguages": "Zıwananê binan de",
        "redirectedfrom": "($1 ra kırışı yê)",
        "redirectpagesub": "Pela berdışi",
        "redirectto": "Beno hetê:",
-       "lastmodifiedat": "Ena pele tewr peyên roca $1, saeta $2 de biye rocane.",
+       "lastmodifiedat": "Per roca $1, sehat $2 de biye neye.",
        "viewcount": "Ena pele {{PLURAL:$1|rae|$1 rey}} vêniya.",
        "protectedpage": "Pela pawıtiye",
        "jumpto": "Şo be:",
        "confirmable-yes": "Eya",
        "confirmable-no": "Nê",
        "thisisdeleted": "Bıvêne ya zi $1 peyser biya?",
-       "viewdeleted": "$1 bıvêne?",
+       "viewdeleted": "$1 bıvin?",
        "restorelink": "{{PLURAL:$1|jew vurnayış besteriya|$1 vurnayışi besteriyaye}}",
        "feedlinks": "Warikerdış:",
        "feed-invalid": "Qeydey cıresnayışê  beğşi nêvêreno.",
        "perfcached": "Datay cı ver hazır biye. No semedê ra nıkayin niyo! tewr zaf {{PLURAL:$1|netice|$1 netice}} debêno de",
        "perfcachedts": "Cêr de malumatê nımıteyi esti, demdê newe kerdışo peyın: $1. Tewr zaf {{PLURAL:$4|netice|$4 neticey cı}} debyayo de",
        "querypage-no-updates": "Rocanebiyayışê na pele nıka cadayiyê.\nDayiyi tiya nıka newe nêbenê.",
-       "viewsource": "Çımey bıvêne",
+       "viewsource": "Çemi bıvin",
        "viewsource-title": "Cı geyrayışê $1'i bıvin",
        "actionthrottled": "Kerden peysnaya",
        "actionthrottledtext": "Riyê tedbirê anti-spami ra,  wextê do kılmek de şıma nê fealiyeti nêşkenê zaf zêde bıkerê, şıma ki no hedi viyarna ra.\nÇend deqey ra tepeya reyna bıcerrebnên.",
        "nologinlink": "Yew hesab ake",
        "createaccount": "Hesab vıraze",
        "gotaccount": "Hesabê şıma esto? '''$1'''.",
-       "gotaccountlink": "Cı kewe",
+       "gotaccountlink": "Cıkewtış",
        "userlogin-resetlink": "Melumatê cıkewtışi xo vira kerdê?",
        "userlogin-resetpassword-link": "Parola xo kerda xo vira?",
        "userlogin-helplink2": "Heqa qeydbiyayışi de peşti bıgêrên",
        "botpasswords-label-appid": "Nameyê boti:",
        "botpasswords-label-create": "Vıraze",
        "botpasswords-label-update": "Rocane ke",
-       "botpasswords-label-cancel": "Bıtexelne",
+       "botpasswords-label-cancel": "İbtal ke",
        "botpasswords-label-delete": "Bestere",
        "botpasswords-label-resetpassword": "Parola raçarne",
        "botpasswords-label-grants-column": "Dayen",
        "resetpass_forbidden": "parolayi nêvuryayi",
        "resetpass-no-info": "şıma gani hesab akere u hona bıeşke bırese cı",
        "resetpass-submit-loggedin": "Parola bıvurne",
-       "resetpass-submit-cancel": "Bıtexelne",
+       "resetpass-submit-cancel": "İbtal ke",
        "resetpass-wrong-oldpass": "parolayo parola maqbul niyo.\nşıma ya parolaye xo vurnayo ya zi parolayo muwaqqat waşto.",
        "resetpass-recycled": "Parolaya şımaya newiye wa paroloya şımaya verêne ra ferqıne bo.",
        "resetpass-temp-emailed": "E postaya rışyayê yubkoda şıma ronıştış akerdo.  Ronıştışi xo temammkerdışi rê yu parolaya newi lazım a",
        "hr_tip": "Xeta verardiye (teserrufın bıgureyne/bıxebetne)",
        "summary": "Xulasa:",
        "subject": "Mewzu:",
-       "minoredit": "No yew vurnayışo werdiyo",
-       "watchthis": "Ena pele seyr ke",
-       "savearticle": "Pele qeyd ke",
+       "minoredit": "Vurriyayışo werdiyo",
+       "watchthis": "Seyr kı",
+       "savearticle": "Qeyd kı",
        "savechanges": "Vurnayışan qeyd ke",
        "publishpage": "Perer bıhesırne",
        "publishchanges": "Vurnayışa vıla ke",
        "accmailtext": "[[User talk:$1|$1]] parolayo ke raşt ameyo şırawiyo na adres $2.\n\nQey na hesabê newe parola, cıkewtış dıma şıma eşkeni na qısım de ''[[Special:ChangePassword|parola bıvurn]]'' bıvurni.",
        "newarticle": "(Newe)",
        "newarticletext": "To yew gıre tıkna be ra yew pela ke hewna çıniya.\nSeba afernayışê pele ra, qutiya metnê cêrêni bıgurene (seba melumati qaytê [$1 pela peşti] ke).\nEke be ğeletine ameya tiya, wa gocega <strong>peyser</strong>i programê xo de bıtıkne.",
-       "anontalkpagetext": "----''No pel, pel o karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres şuxulneni û ney IP adresan herkes eşkeno bıvino. Eke şıma qayil niye ina bo xo ri [[Special:CreateAccount|yew hesab bıvıraze]] veyaxut [[Special:UserLogin|hesab akere]].''",
+       "anontalkpagetext": "----''Na per, perêk kı karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres xebetneno û ney IP adresan herkes nêşeno bıvino. Eke şıma qayil niye ina bo xorê [[Special:CreateAccount|yew hesab bıvıraze]] veya xut [[Special:UserLogin|hesab akere]].''",
        "noarticletext": "Ena pele de hewna theba çıniyo.\nTı şenê zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|qandê  sernameyê ena pele cı geyre]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre],\nya zi [{{fullurl:{{FULLPAGENAME}}|action=edit}} ena pele vıraze]</span>.{{MediaWiki mesaca pera newi}}",
        "noarticletext-nopermission": "Ena pele de hewna theba çıniyo.\nTı şenay zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|seba sernameyê na pele cı geyre]], ya zi <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre]</span>, ema destur çıniyo ke na pele vırazê.",
        "missing-revision": "Rewizyonê name dê pela da #$1 \"{{FULLPAGENAME}}\" dı çıniyo.\n\nNo normal de tarix dê pelanê besterneyan dı ena xırabin asena.\nDetayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} tiya dı] aseno.",
        "next-page": "Pela peyên",
        "prevn-title": "$1o verên  {{PLURAL:$1|netice|neticeyan}}",
        "nextn-title": "$1o ke yeno {{PLURAL:$1|netice|neticey}}",
-       "shown-title": "bimocne $1î  {{PLURAL:$1|netice|neticeyan}} ser her pel",
+       "shown-title": "Herg per sero $1 {{PLURAL:$1|netici|netica}} bıasne",
        "viewprevnext": "($1 {{int:pipe-separator}} $2) ($3) bıvênên",
        "searchmenu-exists": "''Ena 'Wikipediya de ser \"[[:$1]]\" yew pel esto'''",
        "searchmenu-new": "<strong>Na wiki de pela \"[[:$1]]\" vıraze!</strong> {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi neticanê cıgeyrayışê xo bıvênê.}}",
        "rightslogtext": "Ena listeyê loganê ke heqqa karbaranî mucneno.",
        "action-read": "ena pela wanayış",
        "action-edit": "ena pela bıvurnê",
-       "action-createpage": "na pele vıraze",
+       "action-createpage": "na perer bıvıraz",
        "action-createtalk": "pelanê werênayışi bıvıraze",
        "action-createaccount": "hesabê nê karberi bıvıraze",
        "action-autocreateaccount": "nê hesabê karberiyê teberi otomatik vıraze",
        "enhancedrc-history": "tarix",
        "recentchanges": "Vurriyayışê peyêni",
        "recentchanges-legend": "Tercihê vurnayışanê peyênan",
-       "recentchanges-summary": "Ena pele de wiki sero vurriyayışanê peyênan teqib ke.",
+       "recentchanges-summary": "Wiki sero vurriyayışê peyêni asenê.",
        "recentchanges-noresult": "Goreyê kriteranê kıfşkerdeyan ra qet yew vurnayış nêvêniya.",
        "recentchanges-feed-description": "Ena feed dı vurnayişanê tewr peniyan teqip bık.",
        "recentchanges-label-newpage": "Enê vurnayışi ra yew pela newiye vıraziye",
-       "recentchanges-label-minor": "No yew vurnayışo werdiyo",
+       "recentchanges-label-minor": "Vurriyayışo werdiyo",
        "recentchanges-label-bot": "Eno vurnayış terefê yew boti ra vıraziyo",
        "recentchanges-label-unpatrolled": "Eno vurnayış hewna dewriya nêbiyo",
        "recentchanges-label-plusminus": "Ebadê pele de bazê bayti de vayeyê cı",
        "rcshowhidecategorization": "kategorizasyonê pele $1",
        "rcshowhidecategorization-show": "Bıasne",
        "rcshowhidecategorization-hide": "Bınımne",
-       "rclinks": "Peyniya $2 rocan de $1 vurriyayışan bımocne <br />$3",
+       "rclinks": "Peyniya $2 rocan de $1 vurriyayışê <br />$3 asenê",
        "diff": "ferq",
        "hist": "verên",
        "hide": "Bınımne",
        "removedwatchtext": "Ena pela \"[[:$1]]\" biya wedariya [[Special:Watchlist|listeyê seyr-kerdışi şıma]].",
        "removedwatchtext-short": "Pera $1`i listeya seyran de şıma ra wedari yê",
        "watch": "Seyr ke",
-       "watchthispage": "Ena pele seyr ke",
+       "watchthispage": "Seyr kı",
        "unwatch": "Teqib meke",
        "unwatchthispage": "temaşa kerdışê peli vındarn.",
        "notanarticle": "mebhesê peli niyo",
        "pagesize": "(bitî)",
        "restriction-edit": "Bıvurne",
        "restriction-move": "Bıkırış",
-       "restriction-create": "Vıraze",
+       "restriction-create": "Bıvıraz",
        "restriction-upload": "Bar ke",
        "restriction-level-sysop": "tam pawiyayo",
        "restriction-level-autoconfirmed": "nêm pawiyayo",
        "undeleterevision-missing": "revizyonê nemeqbul u vindbiyayeyi.\nRevizyoni ya hewn a biyê ya arşiw ra veciyayê ya zi cıresayişê şımayi şaş o.",
        "undelete-nodiff": "revizyonê verıni nidiya",
        "undeletebtn": "Timar bike",
-       "undeletelink": "bıvêne/peyser biya",
+       "undeletelink": "bıewni/peyser biya",
        "undeleteviewlink": "bıvin",
        "undeleteinvert": "Weçinayışi dimlaşt ke",
        "undeletecomment": "Sebeb:",
        "unblocked-id": "Blokê $1î wedariyayo",
        "blocklist": "Karberê kılitbiyayey",
        "ipblocklist": "Karberê kılitbiyayey",
-       "ipblocklist-legend": "Yew karberê kılitbiyayey bıvêne",
+       "ipblocklist-legend": "Karberê kılit biyayey bıvin",
        "blocklist-userblocks": "Kılitkerdışê hesaban bınımne",
        "blocklist-tempblocks": "Kılitkerdışan mıweqet bınımne",
        "blocklist-addressblocks": "Tenya kılitkerdışanê IPy bınımne",
        "articleexists": "Ena nameyê pela database ma dı esta ya zi tı raşt nınuşt. .\nYewna name bınus.",
        "cantmove-titleprotected": "şıma nêşkeni yew peli bıhewelnê tiya çunke pawıyeno",
        "movetalk": "Pela werênayışiê elaqedare bere",
-       "move-subpages": "Pelanê bınênan bıkırışe (heta pela $1)",
-       "move-talk-subpages": "Pelanê werênayışiyê bınênan bıkırışe (heta pela $1)",
+       "move-subpages": "Peranê bınênan bıkırış (hetana $1)",
+       "move-talk-subpages": "Bın peranê peranê vatena bıkırış (hetana $1)",
        "movepage-page-exists": "maddeya $1i ca ra esta u newe ra otomatikmen nênusyena.",
        "movepage-page-moved": "pelê $1i kırışiya pelê $2i.",
        "movepage-page-unmoved": "pelê $1i nêkırışiyeno sernameyê $2i.",
        "fileduplicatesearch-result-1": "Dosyayê ''$1î'' de hem-kopya çini yo.",
        "fileduplicatesearch-result-n": "Dosyayê ''$1î'' de {{PLURAL:$2|1 hem-kopya|$2 hem-kopyayî'}} esto.",
        "fileduplicatesearch-noresults": "Ebe namey \"$1\" ra dosya nêdiyayê.",
-       "specialpages": "Pelê xısusiyi",
+       "specialpages": "Page bağsey",
        "specialpages-note-top": "Kıtabek",
        "specialpages-note": "* Pelê xasê normali.\n* <span class=\"mw-specialpagerestricted\">Pelê xasê nımıtey.</span>",
        "specialpages-group-maintenance": "Raporê pawıtışi",
        "feedback-bugcheck": "Harika! Sadece [xırabina ke $1 ] çınyayışê cı kontrol keno.",
        "feedback-bugnew": "Mı qontrol ke. Xetaya newi xeber ke",
        "feedback-bugornote": "Jew mersela teferruato teknik esta şıma reca malumatê şıma hazıro se [ $1  jew xırab rapor] bıvinê.Zewbi zi, formê cerê xo rê şenê karfiyê. Vatışê xo pela da \"[ $3  $2 ]\", namey karber dê xoya piya u wasteriya karfiye.",
-       "feedback-cancel": "Bıtexelne",
+       "feedback-cancel": "İbtal kı",
        "feedback-close": "Biya star",
        "feedback-error1": "Xeta: API ra neticey ne vıcyay",
        "feedback-error2": "Xeta: Timar kerdış nebı",
index 13cbfb7..f145ca9 100644 (file)
        "nolinkstoimage": "Aucune page n'utilise ce fichier.",
        "morelinkstoimage": "Voir [[Special:WhatLinksHere/$1|plus de liens]] vers ce fichier.",
        "linkstoimage-redirect": "$1 (redirection de fichier) $2",
-       "duplicatesoffile": "{{PLURAL:$1|Le fichier suivant est un duplicata|Les fichiers suivants sont des duplicatas}} de celui-ci ([[Special:FileDuplicateSearch/$2|plus de détails]]) :",
+       "duplicatesoffile": "{{PLURAL:$1|Le fichier suivant est un doublon|Les $1 fichiers suivants sont des doublons}} de celui-ci ([[Special:FileDuplicateSearch/$2|plus de détails]]) :",
        "sharedupload": "Ce fichier provient de : $1. Il peut être utilisé par d'autres projets.",
        "sharedupload-desc-there": "Ce fichier provient de : $1. Il peut être utilisé par d'autres projets.\nVeuillez consulter [$2 sa page de description] pour plus d'informations.",
        "sharedupload-desc-here": "Ce fichier provient de $1. Il peut être utilisé par d'autres projets.\nSa description sur sa [$2 page de description] est affichée ci-dessous.",
        "filedelete-intro-old": "Vous êtes en train d'effacer la version de '''[[Media:$1|$1]]''' du [$4 $2 à $3].",
        "filedelete-comment": "Motif :",
        "filedelete-submit": "Supprimer",
-       "filedelete-success": "'''$1''' a été supprimé.",
-       "filedelete-success-old": "La version de '''[[Media:$1|$1]]''' du $2 à $3 a été supprimée.",
-       "filedelete-nofile": "'''$1''' n'existe pas.",
-       "filedelete-nofile-old": "Il n'existe aucune version archivée de '''$1''' avec les attributs indiqués.",
+       "filedelete-success": "<strong>$1</strong> a été supprimé.",
+       "filedelete-success-old": "La version de <strong>[[Media:$1|$1]]</strong> du $2 à $3 a été supprimée.",
+       "filedelete-nofile": "<strong>$1</strong> n'existe pas.",
+       "filedelete-nofile-old": "Il n'existe aucune version archivée de <strong>$1</strong> avec les attributs indiqués.",
        "filedelete-otherreason": "Motif autre / supplémentaire :",
        "filedelete-reason-otherlist": "Autre motif",
        "filedelete-reason-dropdown": "* Motifs fréquents de suppression de fichiers\n** Violation du droit d'auteur\n** Fichier dupliqué",
-       "filedelete-edit-reasonlist": "Modifier les motifs fréquents de suppression",
-       "filedelete-maintenance": "La suppression et restauration de fichiers est temporairement désactivée durant la maintenance.",
+       "filedelete-edit-reasonlist": "Modifier les motifs de suppression",
+       "filedelete-maintenance": "La suppression et la restauration de fichiers sont  temporairement désactivées durant la maintenance.",
        "filedelete-maintenance-title": "Impossible de supprimer le fichier",
        "mimesearch": "Recherche par type de contenu MIME",
        "mimesearch-summary": "Cette page vous permet de filtrer les fichiers par leur type de contenu MIME.\nEntrée : type_de_contenu/sous-type ou type_de_contenu/*, par ex. <code>image/jpeg</code>.",
        "randompage-nopages": "Il n'y a aucune page dans {{PLURAL:$2|l'espace de noms|les espaces de noms}} : $1.",
        "randomincategory": "Page au hasard dans la catégorie",
        "randomincategory-invalidcategory": "« $1 » n’est pas un nom de catégorie valide.",
-       "randomincategory-nopages": "Il n’y a pas de page dans [[:Category:$1]].",
+       "randomincategory-nopages": "Il n’y a pas de pages dans la catégorie [[:Category:$1|$1]].",
        "randomincategory-category": "Catégorie :",
        "randomincategory-legend": "Page aléatoire dans la catégorie",
        "randomincategory-submit": "Lancer",
index d2548aa..f76e5b8 100644 (file)
        "undeletehistorynoadmin": "Esta páxina foi borrada.\nO motivo do borrado consta no resumo que aparece a continuación, xunto cos detalles dos usuarios que editaron esta páxina antes da súa eliminación.\nO texto destas revisións eliminadas só está á disposición dos administradores.",
        "undelete-revision": "Revisión eliminada de \"$1\" (o $4 ás $5) feita por $3:",
        "undeleterevision-missing": "Revisión non válida ou inexistente. Pode que a ligazón conteña un erro ou que a revisión se restaurase ou eliminase do arquivo.",
+       "undeleterevision-duplicate-revid": "{{PLURAL:$1|Unha revisión non pode ser restaurada|$1 revisións non poden ser restauradas}} porque {{PLURAL:$1|o seu|os seus}}  <code>rev_id</code> xa {{PLURAL:$1|está|están}} en uso.",
        "undelete-nodiff": "Non se atopou ningunha revisión anterior.",
        "undeletebtn": "Restaurar",
        "undeletelink": "ver/restaurar",
        "undeletedrevisions": "{{PLURAL:$1|Restaurouse $1 revisión|Restauráronse $1 revisións}}",
        "undeletedrevisions-files": "Restauráronse $1 {{PLURAL:$1|revisión|revisións}} e $2 {{PLURAL:$2|ficheiro|ficheiros}}",
        "undeletedfiles": "{{PLURAL:$1|Restaurouse $1 ficheiro|Restauráronse $1 ficheiros}}",
-       "cannotundelete": "Houbo un erro durante a restauración:\n$1",
+       "cannotundelete": "Algunhas ou todas as restauracións fallaronː\n$1",
        "undeletedpage": "'''A páxina \"$1\" foi restaurada'''\n\nComprobe o [[Special:Log/delete|rexistro de borrados]] para ver as entradas recentes no rexistro de páxinas eliminadas e restauradas.",
        "undelete-header": "Consulte [[Special:Log/delete|no rexistro de borrados]] as páxinas borradas recentemente.",
        "undelete-search-title": "Procurar páxinas borradas",
        "sp-contributions-newbies-sub": "Contribucións dos usuarios novos",
        "sp-contributions-newbies-title": "Contribucións dos usuarios novos",
        "sp-contributions-blocklog": "rexistro de bloqueos",
-       "sp-contributions-suppresslog": "contribucións borradas do usuario",
-       "sp-contributions-deleted": "contribucións borradas do usuario",
+       "sp-contributions-suppresslog": "contribucións {{GENDER:$1|do usuario|da usuaria}} suprimidas",
+       "sp-contributions-deleted": "contribucións {{GENDER:$1|do usuario|da usuaria}} borradas",
        "sp-contributions-uploads": "cargas",
        "sp-contributions-logs": "rexistros",
        "sp-contributions-talk": "conversa",
index 090585a..bda094a 100644 (file)
        "mainpage-nstab": "𐌰𐌽𐌰𐍃𐍄𐍉𐌳𐌴𐌹𐌽𐌹𐌻𐌰𐌿𐍆𐍃",
        "error": "𐌰𐌹𐍂𐌶𐌴𐌹",
        "databaseerror-error": "𐌰𐌹𐍂𐌶𐌴𐌹: $1",
-       "missing-article": "ð\90\8d\83ð\90\8c° ð\90\8c³ð\90\8c°ð\90\8d\84ð\90\8c°ð\90\8c±ð\90\8c¿ð\90\8d\83 ð\90\8c½ð\90\8c¹ ð\90\8c²ð\90\8c°ð\90\8c½ð\90\8c°ð\90\8c¼ ð\90\8c¸ð\90\8c°ð\90\8c½ð\90\8c° ð\90\8c±ð\90\8d\89ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8c°ð\90\8c¿ð\90\8d\82ð\90\8c³ð\90\8c°ð\90\8c½ ð\90\8c´ð\90\8c¹ ð\90\8c¹ð\90\8d\84ð\90\8c° ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8c» ð\90\8c±ð\90\8c¹ð\90\8c²ð\90\8c¹ð\90\8d\84ð\90\8c°ð\90\8c½: \"$1\" $2\n\n(The data base did not find the text of a page that it should have found, named \"$1\" $2.\n\nThis is usually caused by following an outdated diff or history link to a page that has been deleted.\n\nIf this is not the case, you may have found a bug in the software.\nPlease report this to an [[Special:ListUsers/sysop|administrator]], making note of the URL.)",
+       "missing-article": "ð\90\8c³ð\90\8c°ð\90\8d\84ð\90\8c°ð\90\8c±ð\90\8c´ð\90\8d\83 ð\90\8c½ð\90\8c¹ ð\90\8c±ð\90\8c¹ð\90\8c²ð\90\8c°ð\90\8d\84 ð\90\8c±ð\90\8d\89ð\90\8cºð\90\8d\89ð\90\8d\83 ð\90\8c»ð\90\8c°ð\90\8c¿ð\90\8c±ð\90\8c¹ð\90\8d\83 ð\90\8c¸ð\90\8c¹ð\90\8c¶ð\90\8c´ð\90\8c¹ ð\90\8d\83ð\90\8cºð\90\8c¿ð\90\8c»ð\90\8c³ð\90\8c´ð\90\8c³ð\90\8c¹ ð\90\8c±ð\90\8c¹ð\90\8c²ð\90\8c¹ð\90\8d\84ð\90\8c°ð\90\8c½, ð\90\8c·ð\90\8c°ð\90\8c¹ð\90\8d\84ð\90\8c°ð\90\8c½ð\90\8d\83 \"$1\" $2. \n\nð\90\8c¸ð\90\8c°ð\90\8d\84ð\90\8c° ð\90\8c¿ð\90\8d\86ð\90\8d\84ð\90\8c° ð\90\8d\85ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c¸ð\90\8c¹ð\90\8c¸ ð\90\8c¾ð\90\8c°ð\90\8c±ð\90\8c°ð\90\8c¹ ð\90\8c»ð\90\8c°ð\90\8c¹ð\90\8d\83ð\90\8d\84ð\90\8c¾ð\90\8c°ð\90\8c³ð\90\8c° ð\90\8d\86ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c½ð\90\8c¾ð\90\8c° ð\90\8c³ð\90\8c¹ð\90\8d\86ð\90\8d\86 ð\90\8c¸ð\90\8c°ð\90\8c¿ ð\90\8d\83ð\90\8d\80ð\90\8c¹ð\90\8c»ð\90\8c»ð\90\8c°ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹ð\90\8d\83ð\90\8d\83 ð\90\8d\83ð\90\8c´ð\90\8c¹ ð\90\8d\86ð\90\8d\82ð\90\8c°ð\90\8cµð\90\8c¹ð\90\8d\83ð\90\8d\84ð\90\8c¹ð\90\8c³ð\90\8c° ð\90\8c¹ð\90\8d\83ð\90\8d\84. ð\90\8c½ð\90\8c¹ð\90\8c±ð\90\8c°ð\90\8c¹ ð\90\8c¹ð\90\8d\83ð\90\8d\84, ð\90\8c¼ð\90\8c°ð\90\8c·ð\90\8d\84ð\90\8d\83 ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c´ð\90\8c¹ ð\90\8c±ð\90\8c¹ð\90\8c²ð\90\8c´ð\90\8d\84ð\90\8c´ð\90\8c¹ð\90\8d\83 ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c¶ð\90\8c´ð\90\8c¹ð\90\8c½ ð\90\8c¹ð\90\8c½ ð\90\8d\83ð\90\8c°ð\90\8c¿ð\90\8d\86ð\90\8d\84ð\90\8d\85ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c°. \n\nð\90\8c±ð\90\8c¹ð\90\8c³ð\90\8c¾ð\90\8c°ð\90\8c¼ ð\90\8c¸ð\90\8c¿ð\90\8cº, ð\90\8c¼ð\90\8c´ð\90\8d\82ð\90\8c´ð\90\8c¹ ð\90\8c¸ð\90\8c°ð\90\8d\84ð\90\8c° ð\90\8c³ð\90\8c¿ [[Special:ListUsers/sysop\n|ð\90\8d\82ð\90\8c´ð\90\8c¹ð\90\8cº]] ð\90\8c²ð\90\8c¹ð\90\8d\86ð\90\8c¿ð\90\8c· ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹ð\90\8d\83ð\90\8d\83.",
        "badtitle": "𐌿𐌽𐍂𐌰𐌹𐌷𐍄𐌰𐍄𐌰 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹",
        "badtitletext": "𐍆𐍂𐌰𐌹𐌷𐌰𐌽𐍃 𐌻𐌰𐌿𐍆𐍃 𐍅𐌰𐍃 𐌿𐌽𐌲𐌰𐌼𐌰𐌲𐌰𐌽𐌳𐍃, 𐌻𐌰𐌿𐍃, 𐌰𐌹𐌸𐌸𐌰𐌿 𐌿𐌽𐍂𐌰𐌹𐌷𐍄𐌰𐌱𐌰 𐌲𐌰𐍅𐌹𐌳𐌰𐌽𐍃 𐌼𐌹𐌸𐍂𐌰𐌶𐌳𐌰 𐌸𐌰𐌿 𐌼𐌹𐌸-𐍅𐌹𐌺𐌹 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹. 𐌼𐌰𐌲𐌹 𐌷𐌰𐌱𐌰𐌽 𐌰𐌹𐌽𐌰 𐌸𐌰𐌿 𐌼𐌰𐌽𐌰𐌲𐌹𐌶𐍉𐍃 𐌱𐍉𐌺𐍉𐍃 𐌱𐍂𐌿𐌺𐌹𐌳𐍉𐍃 𐌹𐌽 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌾𐌰𐌼.",
        "viewsource": "𐍃𐌰𐌹𐍈 𐌱𐍂𐌿𐌽𐌽𐌰𐌽",
index a99ca8f..7f73d57 100644 (file)
        "datedefault": "ברירת המחדל",
        "prefs-labs": "אפשרויות מעבדה",
        "prefs-user-pages": "דפי משתמש",
-       "prefs-personal": "פרטי המשתמש",
+       "prefs-personal": "פרטי ה{{GENDER:|משתמש|משתמשת}}",
        "prefs-rc": "שינויים אחרונים",
        "prefs-watchlist": "רשימת המעקב",
        "prefs-editwatchlist": "עריכת רשימת המעקב",
index 528968e..b807488 100644 (file)
        "undeletehistorynoadmin": "Questa pagina è stata cancellata.\nIl motivo della cancellazione è mostrato qui sotto, assieme ai dettagli dell'utente che ha modificato questa pagina prima della cancellazione.\nIl testo contenuto nelle versioni cancellate è disponibile solo agli amministratori.",
        "undelete-revision": "Versione cancellata della pagina $1, inserita il $4 alle $5 da $3:",
        "undeleterevision-missing": "Versione errata o mancante. Il collegamento è errato oppure la versione è stata già ripristinata o eliminata dall'archivio.",
+       "undeleterevision-duplicate-revid": "{{PLURAL:$1|Una versione non può essere ripristinata|$1 versioni non possono essere ripristinate}}, poiché {{PLURAL:$1|il suo|i loro}} <code>rev_id</code> {{PLURAL:$1|è già utilizzato|sono già utilizzati}}.",
        "undelete-nodiff": "Non è stata trovata nessuna versione precedente.",
        "undeletebtn": "Ripristina",
        "undeletelink": "visualizza/ripristina",
index c634c56..cc3b52e 100644 (file)
        "sp-contributions-username": "IP-мекенжайы немесе қатысушы аты:",
        "sp-contributions-toponly": "Өңдемелердің тек соңғы нұсқаларын көрсету",
        "sp-contributions-newonly": "Бет бастау өңдемелерін ғана көрсету",
+       "sp-contributions-hideminor": "Шағын өңдемелерді жасыру",
        "sp-contributions-submit": "Іздеу",
        "whatlinkshere": "Мұнда сілтейтін беттер",
        "whatlinkshere-title": "$1 дегенге сілтейтін беттер",
index ac2fb40..8552d01 100644 (file)
        "acct_creation_throttle_hit": "당신의 IP 주소를 이용한 방문자가 이전에 이미 {{PLURAL:$1|계정 $1개}}를 만들어, 계정 만들기 한도를 초과하였습니다.\n따라서 지금은 이 IP 주소로는 더 이상 계정을 만들 수 없습니다.",
        "emailauthenticated": "이메일 주소가 $2 $3에 인증되었습니다.",
        "emailnotauthenticated": "이메일 주소를 인증하지 않았습니다.\n이메일 확인 절차를 거치지 않으면 다음 이메일 기능을 사용할 수 없습니다.",
-       "noemailprefs": "이 기능을 사용하기 위해서는 사용자 환경 설정에서 이메일 주소를 설정해야 합니다.",
+       "noemailprefs": "이 기능을 사용하려면 사용자 환경 설정에서 이메일 주소를 지정하세요.",
        "emailconfirmlink": "이메일 주소 확인",
        "invalidemailaddress": "이메일 주소의 형식이 잘못되어 인식할 수 없습니다.\n정상적인 형식의 이메일을 입력하거나 칸을 비워 주세요.",
        "cannotchangeemail": "이 위키에서는 계정의 이메일 주소를 바꿀 수 없습니다.",
        "rev-showdeleted": "보이기",
        "revisiondelete": "판 삭제/되살리기",
        "revdelete-nooldid-title": "대상 판이 잘못되었습니다.",
-       "revdelete-nooldid-text": "이 기능을 수행할 특정 판을 제시하지 않았거나 해당 판이 없습니다. 또는 현재 판을 숨기려 하고 있을 수도 있습니다.",
+       "revdelete-nooldid-text": "이 기능을 수행할 대상 판을 지정하지 않았거나 해당 판이 존재하지 않습니다. 아니면 현재 판을 숨기려 하고 있을 수도 있습니다.",
        "revdelete-no-file": "해당 파일이 존재하지 않습니다.",
        "revdelete-show-file-confirm": "정말 \"<nowiki>$1</nowiki>\" 파일의 삭제된 $2 $3 버전을 보시겠습니까?",
        "revdelete-show-file-submit": "예",
        "tags-edit-success": "바뀜이 적용되었습니다.",
        "tags-edit-failure": "수정 사항이 적용될 수 없습니다: $1",
        "tags-edit-nooldid-title": "대상 판이 잘못되었습니다",
-       "tags-edit-nooldid-text": "이 기능을 수행할 특정 판을 제시하지 않았거나 해당 판이 없습니다.",
+       "tags-edit-nooldid-text": "이 기능을 수행할 대상 판을 지정하지 않았거나 해당 판이 존재하지 않습니다.",
        "tags-edit-none-selected": "추가하거나 제거할 최소 하나 이상의 태그를 선택하세요.",
        "comparepages": "문서 비교",
        "compare-page1": "첫 번째 문서",
index ebb6a50..d8bf6e3 100644 (file)
        "passwordreset-emailelement": "सदस्यनाव: \n$1\n\nअस्थायी परवलीचा शब्द: \n$2",
        "passwordreset-emailsentemail": "जर हा विपत्रपत्ता आपल्या खात्याशी संलग्न असेल तर, परवलीच्या शब्दाच्या पुनर्स्थापनेबाबत एक विपत्र पाठवण्यात येईल.",
        "passwordreset-emailsentusername": "जर या सदस्यनावाशी संलग्न विपत्रपत्ता असेल तर, परवलीचा शब्द पुनर्स्थापनाबाबत विपत्र पाठविल्या जाईल.",
-       "passwordreset-emailsent-capture": "'परवलीचा शब्द' पुनर्स्थापनेबाबत एक विपत्र पाठवण्यात आले आहे जे खाली दर्शविण्यात आले आहे.",
-       "passwordreset-emailerror-capture": "'परवलीचा शब्द' पुनर्स्थापनेबाबत एक विपत्र निर्माण करण्यात आले, जे खाली दर्शविण्यात आले आहे.परंतु,{{GENDER:$2|सदस्य}}ला पाठविणे असफल झाले: $1",
        "changeemail": "विपत्रपत्ता बदला किंवा हटवा",
        "changeemail-header": "आपला विपत्रपत्ता बदलण्यास हे आवेदन पूर्ण करा.जर आपणास आपल्या खात्याशी संलग्न कोणताही विपत्रपत्ता हटवायचा असेल तर,आवेदन सादर करण्यापूर्वी, नविन विपत्रपत्त्यासाठी असलेली जागा कोरी ठेवा.",
-       "changeemail-passwordrequired": "हे बदल नक्की करण्यासाठी आपणास आपला परवलीचा शब्द टाकावा लागेल.",
        "changeemail-no-info": "हे पान थेट बघण्यासठी तुम्हाला सनोंद-प्रवेशित असावे लागेल.",
        "changeemail-oldemail": "सध्याचा ईमेल पत्ता :",
        "changeemail-newemail": "नवा ईमेल पत्ता:",
        "undo-nochange": "असे दिसते कि हे संपादन पूर्ववत केल्या गेले आहे.",
        "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|चर्चा]])यांची आवृत्ती $1 परतवली.",
        "undo-summary-username-hidden": "अज्ञात सदस्याची $1 आवृत्ती परतवा",
-       "cantcreateaccounttitle": "खाते उघडू शकत नाही",
        "cantcreateaccount-text": "('''$1''')या आंतरजाल अंकपत्त्याकडूनच्या खाते निर्मितीस [[User:$3|$3]]ने अटकाव केला आहे.\n\n$3ने ''$2'' कारण दिले आहे.",
        "cantcreateaccount-range-text": "<strong>$1</strong>आवाक्यातील आंतरजाल अंकपत्ते,ज्यात आपल्या (<strong>$4</strong>) या अंकपत्त्याचा समावेश आहे, [[User:$3|$3]] ने त्यांच्या खाते निर्मितीस प्रतिबंध केला आहे.\n\n$3 ने <em>$2</em>कारण दिले आहे.",
        "viewpagelogs": "या पानाच्या नोंदी पहा",
        "undeletedrevisions": "{{PLURAL:$1|1 आवर्तन|$1 आवर्तने}} पुनर्स्थापित",
        "undeletedrevisions-files": "{{PLURAL:$1|1 आवर्तन|$1 आवर्तने}}आणि {{PLURAL:$2|1 संचिका|$2 संचिका}} पुनर्स्थापित",
        "undeletedfiles": "{{PLURAL:$1|1 संचिका|$1 संचिका}} पुनर्स्थापित",
-       "cannotundelete": "उलटवणे फसले:$1",
+       "cannotundelete": "à¤\95ाहà¥\80 à¤\95िà¤\82वा à¤¸à¤°à¥\8dवà¤\9a à¤\89लà¤\9fवणà¥\87 à¤«à¤¸à¤²à¥\87:$1",
        "undeletedpage": "<strong>$1ला पुनर्स्थापित केले</strong>\n\nअलिकडिल वगळलेल्या आणि पुनर्स्थापितांच्या नोंदीकरिता [[Special:Log/delete|वगळल्याच्या नोंदी]] पहा .",
        "undelete-header": "अलीकडील वगळलेल्या पानांकरिता [[Special:Log/delete|वगळलेल्या नोंदी]] पहा.",
        "undelete-search-title": "वगळलेली पाने शोधा",
        "sp-contributions-newbies-sub": "नवशिक्यांसाठी",
        "sp-contributions-newbies-title": "नवीन खात्यांसाठी सदस्य योगदान",
        "sp-contributions-blocklog": "रोध नोंदी",
-       "sp-contributions-suppresslog": "सदस्य योगदानाचे दमन केले",
-       "sp-contributions-deleted": "वगळलेली सदस्य संपादने",
+       "sp-contributions-suppresslog": "{{GENDER:$1|सदस्य}} योगदानाचे दमन केले",
+       "sp-contributions-deleted": "वगळलेली {{GENDER:$1|सदस्य}} संपादने",
        "sp-contributions-uploads": "अपभारणे",
        "sp-contributions-logs": "नोंदी",
        "sp-contributions-talk": "चर्चा",
index c756f47..b13f5f9 100644 (file)
        "action-applychangetags": "appreca tag pe' tramente ca se fanno 'e cagnamiente vuoste",
        "action-changetags": "azzecca o lèva tag a caso dint'a verziune nnividuale e riggistre 'e log",
        "action-deletechangetags": "scancellare 'e tag d' 'o database",
+       "action-purge": "agghiuorna sta paggena",
        "nchanges": "$1 {{PLURAL:$1|cagnamiento|cagnamiente}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|'a ll'urdema visita}}",
        "enhancedrc-history": "cronologgia",
        "uploadstash-errclear": "'A pulezzia d' 'e file scassaje.",
        "uploadstash-refresh": "Agghiuorna l'elenco d' 'e file",
        "uploadstash-thumbnail": "vide miniatura",
+       "uploadstash-exception": "Nun s'è pututo sarvà 'a càrreca dint' 'a stash ($1): \"$2\".",
        "invalid-chunk-offset": "Distanza d' 'a parte nun valida",
        "img-auth-accessdenied": "Acciesso negato",
        "img-auth-nopathinfo": "PATH_INFO mancante.\n'O server nun è mpustato pe' passà sta nfurmazione.\nPuò darse ca, essenno basato ncopp'a CGI, nun putesse suppurtà img_auth.\nVide https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization",
        "watchnologin": "Acciesso nun affettuato",
        "addwatch": "Miette dint' 'a l'elenco 'e paggene cuntrullate",
        "addedwatchtext": "'A paggena \"[[:$1]]\" e 'a paggena 'e chiacchiera è stata azzeccata dint'a l'elenco 'e [[Special:Watchlist|paggene cuntrullate]].",
+       "addedwatchtext-talk": "\"[[:$1]]\" e 'a paggena 'e chiacchiera suòccia è stata azzeccata dint'a l'elenco 'e [[Special:Watchlist|paggene cuntrullate]] vuosto.",
        "addedwatchtext-short": "Chista paggena \"$1\" è stata azzeccata a l'elenco 'e paggene cuntrullate.",
        "removewatch": "Leva 'a l'elenco 'e paggene cuntrullate",
-       "removedwatchtext": "\"[[:$1]]\" 'e 'a paggena 'e chiacchiera soja so' state scancellata 'a l'elenco [[Special:Watchlist|'e paggene cuntrullate]] vuosto.",
+       "removedwatchtext": "\"[[:$1]]\" e 'a paggena 'e chiacchiera soja so' state scancellate 'a l'elenco [[Special:Watchlist|'e paggene cuntrullate]] vuosto.",
+       "removedwatchtext-talk": "\"[[:$1]]\" e 'a paggena 'e chiacchiera soja so' state luvate 'a l'elenco [[Special:Watchlist|'e paggene cuntrullate]] vuosto.",
        "removedwatchtext-short": "Chista paggena \"$1\" è stata luvata a l'elenco 'e paggene cuntrullate.",
        "watch": "Secuta",
        "watchthispage": "Tiene d'uocchio sta paggena",
index 85894af..8108c7a 100644 (file)
        "grant-group-high-volume": "Realizar actividades em grande quantidade",
        "grant-group-customization": "Personalização e preferências",
        "grant-group-administration": "Executar acções administrativas",
+       "grant-group-private-information": "Aceder aos seus dados privados",
        "grant-group-other": "Actividade diversa",
        "grant-blockusers": "Bloquear e desbloquear utilizadores",
        "grant-createaccount": "Criar contas",
index d1f7ea4..7ef457b 100644 (file)
        "grant-group-high-volume": "Izvajanje visokoobsežnih dejavnosti",
        "grant-group-customization": "Prilagoditve in nastavitve",
        "grant-group-administration": "Izvajanje administrativnih dejanj",
+       "grant-group-private-information": "Dostop do zasebnih podatkov o vas",
        "grant-group-other": "Druga dejavnost",
        "grant-blockusers": "Blokiranje in odblokiranje uporabnikov",
        "grant-createaccount": "Ustvarjanje računov",
        "grant-highvolume": "Visokoobsežno urejanje",
        "grant-oversight": "Skrivanje uporabnikov in zatiranje redakcij",
        "grant-patrol": "Nadzor sprememb strani",
+       "grant-privateinfo": "Dostop do zasebnih podatkov",
        "grant-protect": "Zaščita in odstranitev zaščite strani",
        "grant-rollback": "Razveljavitev sprememb strani",
        "grant-sendemail": "Pošiljanje e-pošte drugim uporabnikom",
index d156e70..f81a37c 100644 (file)
        "watchthis": "Спостерігати за цією сторінкою",
        "savearticle": "Зберегти сторінку",
        "savechanges": "Зберегти зміни",
-       "publishpage": "Ð\9eпÑ\83блÑ\96кÑ\83вати сторінку",
-       "publishchanges": "Ð\9eпÑ\83блÑ\96кÑ\83вати зміни",
+       "publishpage": "Ð\97беÑ\80егти сторінку",
+       "publishchanges": "Ð\97беÑ\80егти зміни",
        "preview": "Попередній перегляд",
        "showpreview": "Попередній перегляд",
        "showdiff": "Показати зміни",
index b00ef27..7503b40 100644 (file)
        "shortpages": "چھوٹے صفحات",
        "longpages": "طویل ترین صفحات",
        "deadendpages": "مردہ صفحات",
-       "protectedpages": "محفوظ شدہ صفحات",
+       "protectedpages": "محفوظ کردہ صفحات",
        "protectedpages-noredirect": "رجوع مکررات چھپائیں",
        "protectedpages-timestamp": "وقت کی مہر",
        "protectedpages-page": "صفحہ",
index 7b604e7..71b389f 100644 (file)
        "minoredit": "Bu kichik tahrir",
        "watchthis": "Sahifani kuzatish",
        "savearticle": "Saqla",
+       "publishpage": "Sahifani chop et",
+       "publishchanges": "Oʻzgarishlarni chop et",
        "preview": "Ko‘rib chiqish",
        "showpreview": "Koʻrib chiqish",
        "showdiff": "Kiritilgan o‘zgarishlar",
        "undo-success": "Tahrirni bekor qilish imkoniyati bor. Iltimos, solishtirish oynasini koʻrib chiqib, aynan shu oʻzgarishlarni bekor qilmoqchiligingizga ishonch hosil qiling va undan keyin «Saqla» tugmasini bosing.",
        "undo-failure": "Keyingi tahrirlar bilan chalkashib ketgani sababli, ushbu tahrirni alohida oʻzini bekor qilishni iloji yoʻq.",
        "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|mun.]]) tomonidan qilingan $1-sonli tahrir qaytarildi",
-       "cantcreateaccounttitle": "Ro‘yxatdan o‘tib bo‘lmadi",
        "cantcreateaccount-text": "[[User:$3|$3]] ushbu IP manzil (<strong>$1</strong>) orqali ro‘yxatdan o‘tishni bloklab qo‘ygan.\n\n$3 <em>$2</em>ni sabab qilib ko‘rsatdi",
        "cantcreateaccount-range-text": "[[User:$3|$3]] <strong>$1</strong> sohaga tegishli IP manzillar, shu jumladan sizning IP manzilingiz (<strong>$4</strong>), orqali ro‘yxatdan o‘tishni bloklab qo‘ygan.\n\n$3 <em>$2</em>ni sabab qilib ko‘rsatdi",
        "viewpagelogs": "Ushbu sahifaga doir qaydlarni koʻrsat",
index 4e0e2a5..3956e0d 100644 (file)
        "revdelete-uname-unhid": "公开用户名",
        "revdelete-restricted": "应用对管理员的限制",
        "revdelete-unrestricted": "删除对管理员的限制",
-       "logentry-block-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},期限$5 $6",
+       "logentry-block-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},期限$5 $6",
        "logentry-block-unblock": "$1{{GENDER:$2|解封了}}{{GENDER:$4|$3}}",
        "logentry-block-reblock": "$1将{{GENDER:$4|$3}}的封禁设置{{GENDER:$2|更改为}}持续时间$5 $6",
        "logentry-suppress-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},持续时间$5 $6",
        "log-action-filter-upload-upload": "新上传",
        "log-action-filter-upload-overwrite": "重新上传",
        "authmanager-authn-not-in-progress": "身份验证尚未进行,或会话数据丢失。请从头重新开始。",
-       "authmanager-authn-no-primary": "提供的证书不能被验证。",
+       "authmanager-authn-no-primary": "提供的凭据不能通过验证。",
        "authmanager-authn-no-local-user": "提供的证书没有与该wiki上的任何用户相关联。",
        "authmanager-authn-no-local-user-link": "提供的证书有效,但没有与该wiki上的任何用户相关联。请通过不同方式登录,或创建一个新用户,然后您将拥有一个把您之前的证书链接到对应账户的选项。",
        "authmanager-authn-autocreate-failed": "所有账户的自动创建失败:$1",
index 6931259..2da45ca 100644 (file)
 require_once __DIR__ . '/cleanupTable.inc';
 
 /**
- * Maintenance script to clean up broken page links when somebody turns on $wgCapitalLinks.
+ * Maintenance script to clean up broken page links when somebody turns
+ * on or off $wgCapitalLinks.
  *
  * @ingroup Maintenance
  */
 class CapsCleanup extends TableCleanup {
 
        private $user;
+       private $namespace;
 
        public function __construct() {
                parent::__construct();
@@ -47,25 +49,66 @@ class CapsCleanup extends TableCleanup {
        }
 
        public function execute() {
-               global $wgCapitalLinks;
-
-               if ( $wgCapitalLinks ) {
-                       $this->error( "\$wgCapitalLinks is on -- no need for caps links cleanup.", true );
-               }
-
                $this->user = User::newSystemUser( 'Conversion script', [ 'steal' => true ] );
 
                $this->namespace = intval( $this->getOption( 'namespace', 0 ) );
+
+               if ( MWNamespace::isCapitalized( $this->namespace ) ) {
+                       $this->output( "Will be moving pages to first letter capitalized titles" );
+                       $callback = 'processRowToUppercase';
+               } else {
+                       $this->output( "Will be moving pages to first letter lowercase titles" );
+                       $callback = 'processRowToLowercase';
+               }
+
                $this->dryrun = $this->hasOption( 'dry-run' );
 
                $this->runTable( [
                        'table' => 'page',
                        'conds' => [ 'page_namespace' => $this->namespace ],
                        'index' => 'page_id',
-                       'callback' => 'processRow' ] );
+                       'callback' => $callback ] );
        }
 
-       protected function processRow( $row ) {
+       protected function processRowToUppercase( $row ) {
+               global $wgContLang;
+
+               $current = Title::makeTitle( $row->page_namespace, $row->page_title );
+               $display = $current->getPrefixedText();
+               $lower = $row->page_title;
+               $upper = $wgContLang->ucfirst( $row->page_title );
+               if ( $upper == $lower ) {
+                       $this->output( "\"$display\" already uppercase.\n" );
+
+                       return $this->progress( 0 );
+               }
+
+               $target = Title::makeTitle( $row->page_namespace, $upper );
+               if ( $target->exists() ) {
+                       // Prefix "CapsCleanup" to bypass the conflict
+                       $target = Title::newFromText( __CLASS__ . '/' . $display );
+               }
+               $ok = $this->movePage(
+                       $current,
+                       $target,
+                       'Converting page title to first-letter uppercase',
+                       false
+               );
+               if ( $ok ) {
+                       $this->progress( 1 );
+                       if ( $row->page_namespace == $this->namespace ) {
+                               $talk = $target->getTalkPage();
+                               $row->page_namespace = $talk->getNamespace();
+                               if ( $talk->exists() ) {
+                                       return $this->processRowToUppercase( $row );
+                               }
+                       }
+               }
+
+               return $this->progress( 0 );
+       }
+
+       protected function processRowToLowercase( $row ) {
                global $wgContLang;
 
                $current = Title::makeTitle( $row->page_namespace, $row->page_title );
@@ -79,35 +122,51 @@ class CapsCleanup extends TableCleanup {
                }
 
                $target = Title::makeTitle( $row->page_namespace, $lower );
-               $targetDisplay = $target->getPrefixedText();
                if ( $target->exists() ) {
+                       $targetDisplay = $target->getPrefixedText();
                        $this->output( "\"$display\" skipped; \"$targetDisplay\" already exists\n" );
 
                        return $this->progress( 0 );
                }
 
-               if ( $this->dryrun ) {
-                       $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" );
-                       $ok = true;
-               } else {
-                       $mp = new MovePage( $current, $target );
-                       $status = $mp->move( $this->user, 'Converting page titles to lowercase', true );
-                       $ok = $status->isOK() ? 'OK' : $status->getWikiText( false, false, 'en' );
-                       $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" );
-               }
+               $ok = $this->movePage( $current, $target, 'Converting page titles to lowercase', true );
                if ( $ok === true ) {
                        $this->progress( 1 );
                        if ( $row->page_namespace == $this->namespace ) {
                                $talk = $target->getTalkPage();
                                $row->page_namespace = $talk->getNamespace();
                                if ( $talk->exists() ) {
-                                       return $this->processRow( $row );
+                                       return $this->processRowToLowercase( $row );
                                }
                        }
                }
 
                return $this->progress( 0 );
        }
+
+       /**
+        * @param Title $current
+        * @param Title $target
+        * @param string $reason
+        * @param bool $createRedirect
+        * @return bool Success
+        */
+       private function movePage( Title $current, Title $target, $reason, $createRedirect ) {
+               $display = $current->getPrefixedText();
+               $targetDisplay = $target->getPrefixedText();
+
+               if ( $this->dryrun ) {
+                       $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" );
+                       $ok = 'OK';
+               } else {
+                       $mp = new MovePage( $current, $target );
+                       $status = $mp->move( $this->user, $reason, $createRedirect );
+                       $ok = $status->isOK() ? 'OK' : $status->getWikiText( false, false, 'en' );
+                       $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" );
+               }
+
+               return $ok === 'OK';
+       }
 }
 
 $maintClass = "CapsCleanup";
index 0d8cfd6..d2ee1cd 100644 (file)
@@ -1429,10 +1429,6 @@ return [
                'scripts' => 'resources/src/mediawiki/mediawiki.experiments.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
-       'mediawiki.raggett' => [
-               'styles' => 'resources/src/mediawiki/mediawiki.raggett.css',
-               'targets' => [ 'desktop', 'mobile' ],
-       ],
 
        /* MediaWiki Action */
 
index b31fe82..c3a287d 100644 (file)
@@ -68,6 +68,9 @@ return call_user_func( function () {
                        'es5-shim',
                        'oojs',
                        'oojs-ui-core.styles',
+                       'oojs-ui.styles.icons',
+                       'oojs-ui.styles.indicators',
+                       'oojs-ui.styles.textures',
                        'mediawiki.language',
                ],
                'targets' => [ 'desktop', 'mobile' ],
@@ -78,14 +81,6 @@ return call_user_func( function () {
                'styles' => 'resources/src/oojs-ui-local.css', // HACK, see inside the file
                'skinStyles' => $getSkinSpecific( 'core' ),
                'targets' => [ 'desktop', 'mobile' ],
-               // ResourceLoaderImageModule doesn't support 'skipFunction', so instead we set this up so that
-               // this module is skipped together with its dependencies. Nothing else depends on these modules.
-               'dependencies' => [
-                       'oojs-ui.styles.icons',
-                       'oojs-ui.styles.indicators',
-                       'oojs-ui.styles.textures',
-               ],
-               'skipFunction' => 'resources/src/oojs-ui-styles-skip.js',
        ];
 
        // Additional widgets and layouts module.
diff --git a/resources/assets/licenses/README b/resources/assets/licenses/README
new file mode 100644 (file)
index 0000000..dae2549
--- /dev/null
@@ -0,0 +1,4 @@
+These license icons are used in LocalSettings.php files that are generated by
+the installer. Although "Public domain" has been removed from the installer as
+an option, the public-domain.png image needs to remain here to support older
+installations that refer to it in LocalSettings.php.
index d2a998c..999fae0 100644 (file)
@@ -4,7 +4,8 @@
                        "Calak",
                        "Muhammed taha",
                        "Serwan",
-                       "Pirehelokan"
+                       "Pirehelokan",
+                       "Sarchia"
                ]
        },
        "ooui-toolbar-more": "زیاتر",
@@ -16,5 +17,7 @@
        "ooui-dialog-process-dismiss": "لێگەڕان",
        "ooui-dialog-process-retry": "دیسان ھەوڵ بدە",
        "ooui-dialog-process-continue": "درێژە بدە",
-       "ooui-selectfile-placeholder": "ھیچ فایلێک ھەڵنەبژێراوە"
+       "ooui-selectfile-button-select": "پەڕگەیەک دەستنیشان بکە",
+       "ooui-selectfile-placeholder": "ھیچ فایلێک ھەڵنەبژێراوە",
+       "ooui-selectfile-dragdrop-placeholder": "پەڕگەکان بخەرە ئێرە"
 }
index 30e3efa..e193fb0 100644 (file)
@@ -8,12 +8,18 @@
                        "Palnatoke",
                        "Simeondahl",
                        "Tehnix",
-                       "Macofe"
+                       "Macofe",
+                       "Peter Alberti"
                ]
        },
        "ooui-outline-control-move-down": "Flyt ned",
        "ooui-outline-control-move-up": "Flyt op",
        "ooui-toolbar-more": "Mere",
        "ooui-toolgroup-expand": "Mere",
+       "ooui-toolgroup-collapse": "Færre",
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Afbryd",
+       "ooui-dialog-process-error": "Noget gik galt",
+       "ooui-dialog-process-retry": "Prøv igen",
        "ooui-dialog-process-continue": "Fortsæt"
 }
index 881ff67..bf6b087 100644 (file)
@@ -6,11 +6,24 @@
                        "Kghbln",
                        "Marmase",
                        "Mirzali",
-                       "Se4598"
+                       "Se4598",
+                       "Kumkumuk"
                ]
        },
        "ooui-outline-control-move-down": "Bendi bere cêr",
        "ooui-outline-control-move-up": "Bendi bere cor",
        "ooui-outline-control-remove": "Obcey wedare",
-       "ooui-toolbar-more": "Zewbi"
+       "ooui-toolbar-more": "Zewbi",
+       "ooui-toolgroup-expand": "Dehana",
+       "ooui-toolgroup-collapse": "Deha tayn",
+       "ooui-dialog-message-accept": "TEMAM",
+       "ooui-dialog-message-reject": "Bıtexelne",
+       "ooui-dialog-process-error": "Tayê çi ğelet şi...",
+       "ooui-dialog-process-dismiss": "Racın",
+       "ooui-dialog-process-retry": "Fına bıcerbın",
+       "ooui-dialog-process-continue": "Dewam ke",
+       "ooui-selectfile-button-select": "Yu dosya weçinê",
+       "ooui-selectfile-not-supported": "Dosya weçinayış desteg nêvine na",
+       "ooui-selectfile-placeholder": "Dosya nêwçineya",
+       "ooui-selectfile-dragdrop-placeholder": "Dosya tiyara ake"
 }
index 6a38d0d..b272331 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:22Z
+ * Date: 2016-08-16T21:13:48Z
  */
 ( function ( OO ) {
 
index 72591cc..c13ac7a 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:27Z
+ * Date: 2016-08-16T21:13:53Z
  */
 .oo-ui-element-hidden {
        display: none !important;
@@ -16,7 +16,8 @@
        cursor: pointer;
        display: inline-block;
        vertical-align: middle;
-       font: inherit;
+       font-family: inherit;
+       font-size: inherit;
        line-height: normal;
        white-space: nowrap;
        -webkit-touch-callout: none;
        color: #d45353;
 }
 .oo-ui-fieldLayout-messages .oo-ui-labelWidget {
-       padding: 0;
-       line-height: 1.875em;
+       padding: 0.1em 0;
+       line-height: 1.5em;
        vertical-align: middle;
 }
 .oo-ui-actionFieldLayout {
 }
 .oo-ui-menuSelectWidget {
        position: absolute;
+       width: 100%;
+       z-index: 4;
        background-color: #ffffff;
        margin-top: -1px;
        border: 1px solid #cccccc;
 .oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
        cursor: pointer;
 }
-.oo-ui-dropdownWidget > .oo-ui-menuSelectWidget {
-       z-index: 1;
-       width: 100%;
-}
 .oo-ui-dropdownWidget:last-child {
        margin-right: 0;
 }
        max-width: 50em;
        margin-right: 0.5em;
 }
-.oo-ui-comboBoxInputWidget > .oo-ui-menuSelectWidget {
-       z-index: 1;
-       width: 100%;
-}
 .oo-ui-comboBoxInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
        cursor: pointer;
 }
 .oo-ui-checkboxMultioptionWidget .oo-ui-checkboxInputWidget {
        margin-right: 0;
 }
+.oo-ui-progressBarWidget {
+       max-width: 50em;
+       background-color: #ffffff;
+       border: 1px solid #cccccc;
+       border-radius: 0.25em;
+       overflow: hidden;
+}
+.oo-ui-progressBarWidget-bar {
+       height: 1em;
+       border-right: 1px solid #cccccc;
+       -webkit-transition: width 250ms ease, margin-left 250ms ease;
+          -moz-transition: width 250ms ease, margin-left 250ms ease;
+               transition: width 250ms ease, margin-left 250ms ease;
+       background-color: #cde7f4;
+       background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #eaf4fa), color-stop(100%, #b0d9ee));
+       background-image: -webkit-linear-gradient(top, #eaf4fa 0, #b0d9ee 100%);
+       background-image:    -moz-linear-gradient(top, #eaf4fa 0, #b0d9ee 100%);
+       background-image:         linear-gradient(to bottom, #eaf4fa 0, #b0d9ee 100%);
+       -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffeaf4fa', endColorstr='#ffb0d9ee' )";
+}
+.oo-ui-progressBarWidget-indeterminate .oo-ui-progressBarWidget-bar {
+       -webkit-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+          -moz-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+               animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+       width: 40%;
+       margin-left: -10%;
+       border-left: 1px solid #a6cee1;
+}
+.oo-ui-progressBarWidget.oo-ui-widget-disabled {
+       opacity: 0.6;
+}
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+       from {
+               margin-left: -40%;
+       }
+       to {
+               margin-left: 100%;
+       }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+       from {
+               margin-left: -40%;
+       }
+       to {
+               margin-left: 100%;
+       }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+       from {
+               margin-left: -40%;
+       }
+       to {
+               margin-left: 100%;
+       }
+}
index 23ecccd..4e8f65c 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:27Z
+ * Date: 2016-08-16T21:13:53Z
  */
 .oo-ui-element-hidden {
        display: none !important;
@@ -16,7 +16,8 @@
        cursor: pointer;
        display: inline-block;
        vertical-align: middle;
-       font: inherit;
+       font-family: inherit;
+       font-size: inherit;
        line-height: normal;
        white-space: nowrap;
        -webkit-touch-callout: none;
@@ -62,6 +63,7 @@
        margin-left: 0;
 }
 .oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+       margin-right: 0.25em;
        margin-left: 0.46875em;
 }
 .oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button .oo-ui-indicatorElement-indicator {
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
        background-color: #d9d9d9;
        border-color: #d9d9d9;
-       box-shadow: none;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
        background-color: #999999;
        color: #ffffff;
+       border-color: #999999;
+       z-index: 3;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button:focus {
+       border-color: #347bff;
 }
 .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button {
        color: #347bff;
 }
 .oo-ui-fieldLayout-messages .oo-ui-labelWidget {
        display: table-cell;
-       padding: 0;
-       line-height: 1.875;
+       padding: 0.1em 0;
+       line-height: 1.5;
        vertical-align: middle;
 }
 .oo-ui-actionFieldLayout {
        border-bottom-right-radius: 2px;
        border-top-right-radius: 2px;
 }
+.oo-ui-buttonGroupWidget.oo-ui-widget-enabled .oo-ui-buttonElement .oo-ui-buttonElement-button:focus {
+       border-color: #347bff;
+       z-index: 3;
+}
 .oo-ui-popupWidget {
        position: absolute;
        /* @noflip */
 }
 .oo-ui-menuSelectWidget {
        position: absolute;
+       width: 100%;
+       z-index: 4;
        background-color: #ffffff;
        margin-top: -1px;
        border: 1px solid #aaaaaa;
        position: relative;
        width: 100%;
        max-width: 50em;
-       background-color: #ffffff;
        margin-right: 0.5em;
 }
 .oo-ui-dropdownWidget-handle {
 .oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
        cursor: pointer;
 }
-.oo-ui-dropdownWidget > .oo-ui-menuSelectWidget {
-       z-index: 1;
-       width: 100%;
-}
 .oo-ui-dropdownWidget:last-child {
        margin-right: 0;
 }
 .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
        margin: 0 1em;
 }
-.oo-ui-dropdownWidget:hover .oo-ui-dropdownWidget-handle {
+.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
+       background-color: #ffffff;
+       -webkit-transition: border-color 100ms;
+          -moz-transition: border-color 100ms;
+               transition: border-color 100ms;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-enabled:hover .oo-ui-dropdownWidget-handle {
        border-color: #aaaaaa;
 }
 .oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle {
        max-width: 50em;
        margin-right: 0.5em;
 }
-.oo-ui-comboBoxInputWidget > .oo-ui-menuSelectWidget {
-       z-index: 1;
-       width: 100%;
-}
 .oo-ui-comboBoxInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
        cursor: pointer;
 }
 .oo-ui-checkboxMultioptionWidget .oo-ui-checkboxInputWidget {
        margin-right: 0;
 }
+.oo-ui-progressBarWidget {
+       max-width: 50em;
+       background-color: #ffffff;
+       border: 1px solid #cccccc;
+       border-radius: 2px;
+       overflow: hidden;
+}
+.oo-ui-progressBarWidget-bar {
+       background-color: #dddddd;
+       height: 1em;
+       -webkit-transition: width 200ms, margin-left 200ms;
+          -moz-transition: width 200ms, margin-left 200ms;
+               transition: width 200ms, margin-left 200ms;
+}
+.oo-ui-progressBarWidget-indeterminate .oo-ui-progressBarWidget-bar {
+       -webkit-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+          -moz-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+               animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+       width: 40%;
+       margin-left: -10%;
+       border-left-width: 1px;
+}
+.oo-ui-progressBarWidget.oo-ui-widget-disabled {
+       opacity: 0.6;
+}
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+       from {
+               margin-left: -40%;
+       }
+       to {
+               margin-left: 100%;
+       }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+       from {
+               margin-left: -40%;
+       }
+       to {
+               margin-left: 100%;
+       }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+       from {
+               margin-left: -40%;
+       }
+       to {
+               margin-left: 100%;
+       }
+}
index 4d32961..cd1a3de 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:22Z
+ * Date: 2016-08-16T21:13:48Z
  */
 ( function ( OO ) {
 
@@ -2026,6 +2026,7 @@ OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
        this.active = !!value;
        this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
+       this.updateThemeClasses();
        return this;
 };
 
@@ -7325,6 +7326,103 @@ OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
        return this;
 };
 
+/**
+ * Progress bars visually display the status of an operation, such as a download,
+ * and can be either determinate or indeterminate:
+ *
+ * - **determinate** process bars show the percent of an operation that is complete.
+ *
+ * - **indeterminate** process bars use a visual display of motion to indicate that an operation
+ *   is taking place. Because the extent of an indeterminate operation is unknown, the bar does
+ *   not use percentages.
+ *
+ * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
+ *
+ *     @example
+ *     // Examples of determinate and indeterminate progress bars.
+ *     var progressBar1 = new OO.ui.ProgressBarWidget( {
+ *         progress: 33
+ *     } );
+ *     var progressBar2 = new OO.ui.ProgressBarWidget();
+ *
+ *     // Create a FieldsetLayout to layout progress bars
+ *     var fieldset = new OO.ui.FieldsetLayout;
+ *     fieldset.addItems( [
+ *        new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
+ *        new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
+ *     ] );
+ *     $( 'body' ).append( fieldset.$element );
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
+ *  To create a determinate progress bar, specify a number that reflects the initial percent complete.
+ *  By default, the progress bar is indeterminate.
+ */
+OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.ProgressBarWidget.parent.call( this, config );
+
+       // Properties
+       this.$bar = $( '<div>' );
+       this.progress = null;
+
+       // Initialization
+       this.setProgress( config.progress !== undefined ? config.progress : false );
+       this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
+       this.$element
+               .attr( {
+                       role: 'progressbar',
+                       'aria-valuemin': 0,
+                       'aria-valuemax': 100
+               } )
+               .addClass( 'oo-ui-progressBarWidget' )
+               .append( this.$bar );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
+
+/* Static Properties */
+
+OO.ui.ProgressBarWidget.static.tagName = 'div';
+
+/* Methods */
+
+/**
+ * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
+ *
+ * @return {number|boolean} Progress percent
+ */
+OO.ui.ProgressBarWidget.prototype.getProgress = function () {
+       return this.progress;
+};
+
+/**
+ * Set the percent of the process completed or `false` for an indeterminate process.
+ *
+ * @param {number|boolean} progress Progress percent or `false` for indeterminate
+ */
+OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
+       this.progress = progress;
+
+       if ( progress !== false ) {
+               this.$bar.css( 'width', this.progress + '%' );
+               this.$element.attr( 'aria-valuenow', this.progress );
+       } else {
+               this.$bar.css( 'width', '' );
+               this.$element.removeAttr( 'aria-valuenow' );
+       }
+       this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress === false );
+};
+
 /**
  * InputWidget is the base class for all input widgets, which
  * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
@@ -8590,7 +8688,8 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        // Events
        this.$input.on( {
                keypress: this.onKeyPress.bind( this ),
-               blur: this.onBlur.bind( this )
+               blur: this.onBlur.bind( this ),
+               focus: this.onFocus.bind( this )
        } );
        this.$input.one( {
                focus: this.onElementAttach.bind( this )
@@ -8602,6 +8701,7 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
                change: 'onChange',
                disable: 'onDisable'
        } );
+       this.on( 'change', OO.ui.debounce( this.onDebouncedChange.bind( this ), 250 ) );
 
        // Initialization
        this.$element
@@ -8744,6 +8844,16 @@ OO.ui.TextInputWidget.prototype.onBlur = function () {
        this.setValidityFlag();
 };
 
+/**
+ * Handle focus events.
+ *
+ * @private
+ * @param {jQuery.Event} e Focus event
+ */
+OO.ui.TextInputWidget.prototype.onFocus = function () {
+       this.setValidityFlag( true );
+};
+
 /**
  * Handle element attach events.
  *
@@ -8765,10 +8875,19 @@ OO.ui.TextInputWidget.prototype.onElementAttach = function () {
  */
 OO.ui.TextInputWidget.prototype.onChange = function () {
        this.updateSearchIndicator();
-       this.setValidityFlag();
        this.adjustSize();
 };
 
+/**
+ * Handle debounced change events.
+ *
+ * @param {string} value
+ * @private
+ */
+OO.ui.TextInputWidget.prototype.onDebouncedChange = function () {
+       this.setValidityFlag();
+};
+
 /**
  * Handle disable events.
  *
index ff18605..ab1e9ea 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:22Z
+ * Date: 2016-08-16T21:13:48Z
  */
 ( function ( OO ) {
 
@@ -48,9 +48,7 @@ OO.ui.MediaWikiTheme.prototype.getElementClasses = function ( element ) {
        if ( element.supports( [ 'hasFlag' ] ) ) {
                isFramed = element.supports( [ 'isFramed' ] ) && element.isFramed();
                isActive = element.supports( [ 'isActive' ] ) && element.isActive();
-               if (
-                       ( isFramed && ( isActive || element.isDisabled() || element.hasFlag( 'primary' ) ) )
-               ) {
+               if ( isFramed && ( isActive || element.isDisabled() || element.hasFlag( 'primary' ) ) ) {
                        // Button with a dark background, use white icon
                        variants.invert = true;
                } else if ( !isFramed && element.isDisabled() ) {
index d69ebfa..75fd654 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:27Z
+ * Date: 2016-08-16T21:13:53Z
  */
 .oo-ui-popupTool .oo-ui-popupWidget-popup,
 .oo-ui-popupTool .oo-ui-popupWidget-anchor {
index 5b60528..0b55308 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:27Z
+ * Date: 2016-08-16T21:13:53Z
  */
 .oo-ui-popupTool .oo-ui-popupWidget-popup,
 .oo-ui-popupTool .oo-ui-popupWidget-anchor {
        line-height: 2.1;
        padding: 0 0.4em;
 }
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-title {
+       color: #555555;
+}
 .oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover {
        background-color: #eeeeee;
 }
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-tool-title {
-       color: #555555;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled {
-       box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled.oo-ui-tool-active {
        background-color: #e5e5e5;
+       box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
 }
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled:hover {
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled.oo-ui-tool-active:hover {
        background-color: #eeeeee;
 }
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-tool-title {
-       color: #cccccc;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-iconElement-icon {
-       opacity: 0.2;
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled.oo-ui-tool-active:active {
+       background-color: #e5e5e5;
 }
 .oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled > .oo-ui-tool-link .oo-ui-iconElement-icon {
        opacity: 0.7;
 .oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover > .oo-ui-tool-link .oo-ui-iconElement-icon {
        opacity: 0.9;
 }
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:active {
-       background-color: #e7e7e7;
-}
-.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-tool-title {
+.oo-ui-barToolGroup.oo-ui-widget-enabled .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-tool-title,
+.oo-ui-barToolGroup.oo-ui-widget-disabled .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-title {
        color: #cccccc;
 }
-.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-iconElement-icon {
+.oo-ui-barToolGroup.oo-ui-widget-enabled .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-iconElement-icon,
+.oo-ui-barToolGroup.oo-ui-widget-disabled .oo-ui-tool > .oo-ui-tool-link .oo-ui-iconElement-icon {
        opacity: 0.2;
 }
 .oo-ui-popupToolGroup {
 .oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
        margin-right: 1.75em;
 }
-.oo-ui-popupToolGroup.oo-ui-widget-enabled .oo-ui-popupToolGroup-handle:hover {
-       background-color: #eeeeee;
-}
-.oo-ui-popupToolGroup.oo-ui-widget-enabled .oo-ui-popupToolGroup-handle:active {
-       background-color: #e5e5e5;
-}
 .oo-ui-popupToolGroup-handle {
        padding: 0.3125em;
        height: 2.5em;
 .oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
        left: 0;
 }
+.oo-ui-popupToolGroup.oo-ui-widget-enabled .oo-ui-popupToolGroup-handle:hover {
+       background-color: #eeeeee;
+}
+.oo-ui-popupToolGroup.oo-ui-widget-enabled .oo-ui-popupToolGroup-handle:active {
+       background-color: #e5e5e5;
+}
 .oo-ui-popupToolGroup-header {
        line-height: 2.6;
        margin: 0 0.6em;
 .oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
        background-color: #eeeeee;
 }
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:active {
-       background-color: #e7e7e7;
-}
 .oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover .oo-ui-tool-link .oo-ui-iconElement-icon {
        opacity: 0.9;
 }
 .oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled:hover {
        background-color: #eeeeee;
 }
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
-       color: #cccccc;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-accel {
-       color: #dddddd;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
-       opacity: 0.2;
-}
-.oo-ui-listToolGroup.oo-ui-widget-disabled {
+.oo-ui-listToolGroup.oo-ui-widget-disabled,
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-title,
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-accel {
        color: #cccccc;
 }
 .oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
-.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
+.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-iconElement-icon {
        opacity: 0.2;
 }
 .oo-ui-menuToolGroup .oo-ui-tool {
 .oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
        background-color: #eeeeee;
 }
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
-       color: #cccccc;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
-       opacity: 0.2;
-}
-.oo-ui-menuToolGroup.oo-ui-widget-disabled {
+.oo-ui-menuToolGroup.oo-ui-widget-disabled,
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-title {
        color: #cccccc;
 }
 .oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
-.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
+.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-iconElement-icon {
        opacity: 0.2;
 }
 .oo-ui-toolbar {
index 5254c76..18fda57 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:22Z
+ * Date: 2016-08-16T21:13:48Z
  */
 ( function ( OO ) {
 
index a267b7f..b0e87af 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:27Z
+ * Date: 2016-08-16T21:13:53Z
  */
 .oo-ui-draggableElement-handle,
 .oo-ui-draggableElement-handle.oo-ui-widget {
 .oo-ui-draggableGroupElement-horizontal .oo-ui-draggableElement {
        display: inline-block;
 }
-.oo-ui-lookupElement > .oo-ui-menuSelectWidget {
-       z-index: 1;
-       width: 100%;
-}
 .oo-ui-bookletLayout-stackLayout.oo-ui-stackLayout-continuous > .oo-ui-panelLayout-scrollable {
        overflow-y: hidden;
 }
        padding: 0;
        background-color: transparent;
 }
-.oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
-       position: relative;
-}
 .oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
 .oo-ui-buttonOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
        position: static;
        left: 2.25em;
        margin-left: -2px;
 }
-.oo-ui-progressBarWidget {
-       max-width: 50em;
-       background-color: #ffffff;
-       border: 1px solid #cccccc;
-       border-radius: 0.25em;
-       overflow: hidden;
-}
-.oo-ui-progressBarWidget-bar {
-       height: 1em;
-       border-right: 1px solid #cccccc;
-       -webkit-transition: width 250ms ease, margin-left 250ms ease;
-          -moz-transition: width 250ms ease, margin-left 250ms ease;
-               transition: width 250ms ease, margin-left 250ms ease;
-       background-color: #cde7f4;
-       background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #eaf4fa), color-stop(100%, #b0d9ee));
-       background-image: -webkit-linear-gradient(top, #eaf4fa 0, #b0d9ee 100%);
-       background-image:    -moz-linear-gradient(top, #eaf4fa 0, #b0d9ee 100%);
-       background-image:         linear-gradient(to bottom, #eaf4fa 0, #b0d9ee 100%);
-       -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffeaf4fa', endColorstr='#ffb0d9ee' )";
-}
-.oo-ui-progressBarWidget-indeterminate .oo-ui-progressBarWidget-bar {
-       -webkit-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
-          -moz-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
-               animation: oo-ui-progressBarWidget-slide 2s infinite linear;
-       width: 40%;
-       margin-left: -10%;
-       border-left: 1px solid #a6cee1;
-}
-.oo-ui-progressBarWidget.oo-ui-widget-disabled {
-       opacity: 0.6;
-}
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
-       from {
-               margin-left: -40%;
-       }
-       to {
-               margin-left: 100%;
-       }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
-       from {
-               margin-left: -40%;
-       }
-       to {
-               margin-left: 100%;
-       }
-}
-@keyframes oo-ui-progressBarWidget-slide {
-       from {
-               margin-left: -40%;
-       }
-       to {
-               margin-left: 100%;
-       }
-}
 .oo-ui-selectFileWidget {
        display: inline-block;
        vertical-align: middle;
           -moz-box-sizing: border-box;
                box-sizing: border-box;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
-       position: absolute;
-       top: 0;
-       bottom: 0;
-       left: 0;
-       right: 0;
-       text-overflow: ellipsis;
-}
-.oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-fileType {
-       float: right;
-}
 .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
-.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
        position: absolute;
 }
 .oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
            -ms-user-select: none;
                user-select: none;
 }
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-clearButton {
+.oo-ui-selectFileWidget-label {
+       position: absolute;
+       top: 0;
+       bottom: 0;
+       left: 0;
+       right: 0;
+       text-overflow: ellipsis;
+}
+.oo-ui-selectFileWidget-fileType {
+       display: none;
+}
+.oo-ui-selectFileWidget-clearButton {
+       position: absolute;
        z-index: 2;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget {
+.oo-ui-selectFileWidget-dropTarget {
+       position: relative;
        cursor: default;
-       height: 5.5em;
-       padding: 0;
+       height: 8.815em;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-dropLabel,
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-dropLabel,
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton,
+.oo-ui-selectFileWidget-dropTarget .oo-ui-iconElement-icon {
        display: none;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail {
-       height: 5.5em;
-       width: 5.5em;
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail {
+       width: 7.815em;
        position: absolute;
-       background-size: cover;
+       top: 0.5em;
+       bottom: 0.5em;
+       left: 0.5em;
        background-position: center center;
+       background-repeat: no-repeat;
+       background-size: contain;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail.oo-ui-pendingElement-pending {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail.oo-ui-pendingElement-pending {
+       background-repeat: repeat;
        background-size: auto;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail > .oo-ui-selectFileWidget-noThumbnail-icon {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail > .oo-ui-selectFileWidget-noThumbnail-icon {
        opacity: 0.4;
-       background-color: #cccccc;
-       height: 5.5em;
-       width: 5.5em;
+       height: 7.815em;
+       width: 7.815em;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info {
-       border: 0;
-       background: none;
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info {
        display: block;
        height: 100%;
        width: auto;
-       margin-left: 5.5em;
+       margin-left: 8.815em;
+       border: 0;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-label {
+       display: block;
        position: relative;
+       top: inherit;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-fileName {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-fileName {
        display: block;
+       padding-top: 0.5em;
        padding-right: 2.375em;
-       overflow: hidden;
-       text-overflow: ellipsis;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-fileType {
-       display: block;
-       float: none;
-}
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
-       position: absolute;
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-clearButton {
        right: 0.5em;
 }
 .oo-ui-selectFileWidget-empty.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail,
 }
 .oo-ui-selectFileWidget-empty.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton {
        display: block;
-       margin: 0.7em;
+       margin: 2.2em 1em 1em;
 }
 .oo-ui-selectFileWidget-empty.oo-ui-selectFileWidget-dropTarget,
 .oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget {
 .oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget .oo-ui-buttonElement-button {
        cursor: no-drop;
 }
+.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget {
+       height: auto;
+}
+.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-label {
+       padding: 1em;
+}
 .oo-ui-selectFileWidget:last-child {
        margin-right: 0;
 }
        border-radius: 0.25em 0 0 0.25em;
        border-width: 1px 0 1px 1px;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
-       top: 0;
-       right: 0;
-       height: 2.3em;
-       margin-right: 0.775em;
-}
 .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
        top: 0;
        left: 0;
        height: 2.3em;
        margin-left: 0.3em;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+       top: 0;
+       right: 0;
+       height: 2.3em;
+       margin-right: 0.775em;
+}
+.oo-ui-selectFileWidget-label {
+       -webkit-box-sizing: border-box;
+          -moz-box-sizing: border-box;
+               box-sizing: border-box;
+       left: 0.5em;
+       right: 2.175em;
        line-height: 2.3em;
        margin: 0;
        overflow: hidden;
        white-space: nowrap;
-       -webkit-box-sizing: border-box;
-          -moz-box-sizing: border-box;
-               box-sizing: border-box;
        text-overflow: ellipsis;
-       left: 0.5em;
-       right: 0.5em;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
+.oo-ui-selectFileWidget-fileType {
        color: #888888;
+       display: block;
+       margin-top: 0.25em;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+.oo-ui-selectFileWidget-clearButton {
        top: 0;
+       right: 0;
        width: 1.875em;
        margin-right: 0;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+.oo-ui-selectFileWidget-clearButton .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
        height: 2.3em;
 }
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
-       color: #cccccc;
-       text-shadow: 0 1px 1px #ffffff;
-       border-color: #dddddd;
-       background-color: #f3f3f3;
-}
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
-       opacity: 0.2;
-}
 .oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-label {
        color: #cccccc;
 }
-.oo-ui-selectFileWidget.oo-ui-iconElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget.oo-ui-iconElement .oo-ui-selectFileWidget-label {
        left: 2.475em;
 }
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
-       right: 2.175em;
-}
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
-       right: 0;
-}
-.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-label {
        right: 4.2625em;
 }
-.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
+.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-clearButton {
        right: 2.0875em;
 }
-.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-label {
        right: 0.5em;
 }
-.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
-.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-label {
        right: 2em;
 }
 .oo-ui-selectFileWidget-supported.oo-ui-widget-enabled.oo-ui-selectFileWidget-canDrop.oo-ui-selectFileWidget-dropTarget {
        background-color: #e1f3ff;
 }
-.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled.oo-ui-selectFileWidget-dropTarget,
-.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget {
-       color: #cccccc;
-       text-shadow: 0 1px 1px #ffffff;
-       border-color: #dddddd;
-       background-color: #f3f3f3;
-}
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget {
+.oo-ui-selectFileWidget-dropTarget {
        background-color: #ffffff;
        border: 1px solid #aaaaaa;
-       margin-bottom: 0.5em;
        vertical-align: middle;
        border-radius: 0.25em;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
        border-radius: 0.25em;
 }
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-label {
+       line-height: 1.4;
+       overflow: inherit;
+       white-space: normal;
+}
 .oo-ui-selectFileWidget-empty.oo-ui-selectFileWidget-dropTarget {
        border-style: dashed;
 }
+.oo-ui-selectFileWidget.oo-ui-widget-disabled.oo-ui-selectFileWidget-dropTarget,
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled.oo-ui-selectFileWidget-dropTarget,
+.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget {
+       background-color: #f3f3f3;
+       color: #cccccc;
+       border-color: #dddddd;
+       text-shadow: 0 1px 1px #ffffff;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info,
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-info,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info {
+       background-color: #f3f3f3;
+       color: #cccccc;
+       border-color: #dddddd;
+       text-shadow: 0 1px 1px #ffffff;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+       opacity: 0.2;
+}
 .oo-ui-outlineOptionWidget {
        position: relative;
        cursor: pointer;
 .oo-ui-capsuleMultiselectWidget-group {
        display: inline;
 }
-.oo-ui-capsuleMultiselectWidget > .oo-ui-menuSelectWidget {
-       z-index: 1;
-       width: 100%;
-}
 .oo-ui-capsuleMultiselectWidget-handle {
        background-color: #ffffff;
        cursor: text;
index 8145ef9..9632bac 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:27Z
+ * Date: 2016-08-16T21:13:53Z
  */
 .oo-ui-draggableElement-handle,
 .oo-ui-draggableElement-handle.oo-ui-widget {
 .oo-ui-draggableGroupElement-horizontal .oo-ui-draggableElement {
        display: inline-block;
 }
-.oo-ui-lookupElement > .oo-ui-menuSelectWidget {
-       z-index: 1;
-       width: 100%;
-}
 .oo-ui-bookletLayout-stackLayout.oo-ui-stackLayout-continuous > .oo-ui-panelLayout-scrollable {
        overflow-y: hidden;
 }
 .oo-ui-buttonSelectWidget:focus {
        outline: 0;
 }
-.oo-ui-buttonSelectWidget:focus .oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected .oo-ui-buttonElement-button {
-       border-color: #347bff;
-       box-shadow: inset 0 0 0 1px #347bff;
-       z-index: 2;
-}
 .oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
        border-radius: 0;
        margin-left: -1px;
        border-bottom-right-radius: 2px;
        border-top-right-radius: 2px;
 }
+.oo-ui-buttonSelectWidget.oo-ui-widget-enabled:focus .oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected .oo-ui-buttonElement-button {
+       border-color: #347bff;
+       box-shadow: inset 0 0 0 1px #347bff;
+}
 .oo-ui-buttonOptionWidget {
        display: inline-block;
        padding: 0;
 }
-.oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
-       position: relative;
-}
 .oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
 .oo-ui-buttonOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
        position: static;
           -moz-transform: translateZ(0);
            -ms-transform: translateZ(0);
                transform: translateZ(0);
-       height: 2em;
        width: 3.5em;
+       min-height: 26px;
+       height: 2em;
        border: 1px solid #767676;
        border-radius: 1em;
        background-color: #ffffff;
                transition: border-color 100ms;
 }
 .oo-ui-toggleSwitchWidget-grip {
-       top: 0.35em;
+       top: 0.3125em;
        min-width: 16px;
-       width: 1.2em;
+       width: 1.25em;
        min-height: 16px;
-       height: 1.2em;
-       border-radius: 1.2em;
+       height: 1.25em;
+       border-radius: 1.25em;
        -webkit-transition: left 100ms, margin-left 100ms;
           -moz-transition: left 100ms, margin-left 100ms;
                transition: left 100ms, margin-left 100ms;
 .oo-ui-toggleSwitchWidget.oo-ui-widget-disabled .oo-ui-toggleSwitchWidget-grip {
        background-color: #ffffff;
 }
-.oo-ui-progressBarWidget {
-       max-width: 50em;
-       background-color: #ffffff;
-       border: 1px solid #cccccc;
-       border-radius: 2px;
-       overflow: hidden;
-}
-.oo-ui-progressBarWidget-bar {
-       background-color: #dddddd;
-       height: 1em;
-       -webkit-transition: width 200ms, margin-left 200ms;
-          -moz-transition: width 200ms, margin-left 200ms;
-               transition: width 200ms, margin-left 200ms;
-}
-.oo-ui-progressBarWidget-indeterminate .oo-ui-progressBarWidget-bar {
-       -webkit-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
-          -moz-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
-               animation: oo-ui-progressBarWidget-slide 2s infinite linear;
-       width: 40%;
-       margin-left: -10%;
-       border-left-width: 1px;
-}
-.oo-ui-progressBarWidget.oo-ui-widget-disabled {
-       opacity: 0.6;
-}
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
-       from {
-               margin-left: -40%;
-       }
-       to {
-               margin-left: 100%;
-       }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
-       from {
-               margin-left: -40%;
-       }
-       to {
-               margin-left: 100%;
-       }
-}
-@keyframes oo-ui-progressBarWidget-slide {
-       from {
-               margin-left: -40%;
-       }
-       to {
-               margin-left: 100%;
-       }
-}
 .oo-ui-selectFileWidget {
        display: inline-block;
        vertical-align: middle;
           -moz-box-sizing: border-box;
                box-sizing: border-box;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
-       position: absolute;
-       top: 0;
-       bottom: 0;
-       left: 0;
-       right: 0;
-       text-overflow: ellipsis;
-}
-.oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-fileType {
-       float: right;
-}
 .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
-.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
        position: absolute;
 }
 .oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
            -ms-user-select: none;
                user-select: none;
 }
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-clearButton {
+.oo-ui-selectFileWidget-label {
+       position: absolute;
+       top: 0;
+       bottom: 0;
+       left: 0;
+       right: 0;
+       text-overflow: ellipsis;
+}
+.oo-ui-selectFileWidget-fileType {
+       display: none;
+}
+.oo-ui-selectFileWidget-clearButton {
+       position: absolute;
        z-index: 2;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget {
+.oo-ui-selectFileWidget-dropTarget {
+       position: relative;
        cursor: default;
-       height: 5.5em;
-       padding: 0;
+       height: 8.815em;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-dropLabel,
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-dropLabel,
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton,
+.oo-ui-selectFileWidget-dropTarget .oo-ui-iconElement-icon {
        display: none;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail {
-       height: 5.5em;
-       width: 5.5em;
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail {
+       width: 7.815em;
        position: absolute;
-       background-size: cover;
+       top: 0.5em;
+       bottom: 0.5em;
+       left: 0.5em;
        background-position: center center;
+       background-repeat: no-repeat;
+       background-size: contain;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail.oo-ui-pendingElement-pending {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail.oo-ui-pendingElement-pending {
+       background-repeat: repeat;
        background-size: auto;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail > .oo-ui-selectFileWidget-noThumbnail-icon {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail > .oo-ui-selectFileWidget-noThumbnail-icon {
        opacity: 0.4;
-       background-color: #cccccc;
-       height: 5.5em;
-       width: 5.5em;
+       height: 7.815em;
+       width: 7.815em;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info {
-       border: 0;
-       background: none;
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info {
        display: block;
        height: 100%;
        width: auto;
-       margin-left: 5.5em;
+       margin-left: 8.815em;
+       border: 0;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-label {
+       display: block;
        position: relative;
+       top: inherit;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-fileName {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-fileName {
        display: block;
+       padding-top: 0.5em;
        padding-right: 2.375em;
-       overflow: hidden;
-       text-overflow: ellipsis;
-}
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-fileType {
-       display: block;
-       float: none;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
-       position: absolute;
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-clearButton {
        right: 0.5em;
 }
 .oo-ui-selectFileWidget-empty.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-thumbnail,
 }
 .oo-ui-selectFileWidget-empty.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton {
        display: block;
-       margin: 0.7em;
+       margin: 2.2em 1em 1em;
 }
 .oo-ui-selectFileWidget-empty.oo-ui-selectFileWidget-dropTarget,
 .oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget {
 .oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget .oo-ui-buttonElement-button {
        cursor: no-drop;
 }
+.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget {
+       height: auto;
+}
+.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-label {
+       padding: 1em;
+}
 .oo-ui-selectFileWidget:last-child {
        margin-right: 0;
 }
        height: 2.3em;
        margin-left: 0.5em;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+       top: 0;
+       right: 0;
+       height: 2.3em;
+       margin-right: 0.775em;
+}
+.oo-ui-selectFileWidget-label {
        -webkit-box-sizing: border-box;
           -moz-box-sizing: border-box;
                box-sizing: border-box;
        display: block;
+       right: 2.375em;
        line-height: 2.3;
        margin: 0;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
-       left: 0;
-       right: 0;
        padding-left: 0.5em;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
+.oo-ui-selectFileWidget-fileType {
        color: #888888;
+       display: block;
+       margin-top: 0.25em;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+.oo-ui-selectFileWidget-clearButton {
        top: 0;
        right: 0;
-       height: 2.3em;
-       margin-right: 0.775em;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
-       top: 0;
        min-width: 24px;
        width: 1.875em;
        margin-right: 0;
 }
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+.oo-ui-selectFileWidget-clearButton .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
        height: 2.3em;
 }
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info,
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
-       background-color: #f3f3f3;
-       color: #cccccc;
-       text-shadow: 0 1px 1px #ffffff;
-       border-color: #dddddd;
-       cursor: default;
-}
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
-       opacity: 0.2;
-}
 .oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-label {
        color: #cccccc;
 }
-.oo-ui-selectFileWidget.oo-ui-iconElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget.oo-ui-iconElement .oo-ui-selectFileWidget-label {
        left: 2.875em;
 }
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
-       right: 2.375em;
-}
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
-       right: 0;
-}
-.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-label {
        right: 4.4625em;
        padding-left: 0;
 }
-.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
+.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-clearButton {
        right: 2.0875em;
 }
-.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
-       right: 0.5em;
-}
-.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
-.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-label {
        right: 2em;
-       padding-left: 0;
 }
 .oo-ui-selectFileWidget-supported.oo-ui-widget-enabled.oo-ui-selectFileWidget-canDrop.oo-ui-selectFileWidget-dropTarget {
        background-color: #ebf2ff;
 }
-.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled.oo-ui-selectFileWidget-dropTarget,
-.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget {
-       border-color: #dddddd;
-       background-color: #f3f3f3;
-}
-.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info,
-.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-info,
-.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-dropLabel,
-.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-dropLabel {
-       color: #cccccc;
-       text-shadow: 0 1px 1px #ffffff;
-}
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget {
+.oo-ui-selectFileWidget-dropTarget {
        background-color: #ffffff;
        border: 1px solid #cccccc;
-       margin-bottom: 0.5em;
        vertical-align: middle;
        overflow: hidden;
        border-radius: 2px;
 }
-.oo-ui-selectFileWidget.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
        border-radius: 2px;
 }
+.oo-ui-selectFileWidget-dropTarget .oo-ui-selectFileWidget-label {
+       line-height: 1.4;
+       overflow: inherit;
+       white-space: normal;
+}
 .oo-ui-selectFileWidget-empty.oo-ui-selectFileWidget-dropTarget {
        background-color: #eeeeee;
        border-style: dashed;
 }
+.oo-ui-selectFileWidget.oo-ui-widget-disabled.oo-ui-selectFileWidget-dropTarget,
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled.oo-ui-selectFileWidget-dropTarget,
+.oo-ui-selectFileWidget-notsupported.oo-ui-selectFileWidget-dropTarget {
+       background-color: #f3f3f3;
+       border-color: #dddddd;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info,
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-info,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info {
+       background-color: #f3f3f3;
+       color: #cccccc;
+       border-color: #dddddd;
+       text-shadow: 0 1px 1px #ffffff;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
+.oo-ui-selectFileWidget-empty.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+       opacity: 0.2;
+}
 .oo-ui-widget-disabled .oo-ui-selectFileWidget-dropLabel {
        display: none;
 }
 .oo-ui-capsuleMultiselectWidget-group {
        display: inline;
 }
-.oo-ui-capsuleMultiselectWidget > .oo-ui-menuSelectWidget {
-       z-index: 1;
-       width: 100%;
-}
 .oo-ui-capsuleMultiselectWidget-handle {
-       background-color: #ffffff;
-       cursor: text;
        min-height: 2.4em;
        margin-right: 0.5em;
        padding: 0.15em 0.25em;
        top: 0;
        margin: 0.3em;
 }
-.oo-ui-capsuleMultiselectWidget:hover .oo-ui-capsuleMultiselectWidget-handle {
+.oo-ui-capsuleMultiselectWidget.oo-ui-widget-enabled .oo-ui-capsuleMultiselectWidget-handle {
+       background-color: #ffffff;
+       cursor: text;
+       -webkit-transition: border-color 100ms;
+          -moz-transition: border-color 100ms;
+               transition: border-color 100ms;
+}
+.oo-ui-capsuleMultiselectWidget.oo-ui-widget-enabled:hover .oo-ui-capsuleMultiselectWidget-handle {
        border-color: #aaaaaa;
 }
 .oo-ui-capsuleMultiselectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiselectWidget-handle {
        text-shadow: 0 1px 1px #ffffff;
        border-color: #dddddd;
        background-color: #f3f3f3;
-       cursor: default;
 }
 .oo-ui-capsuleMultiselectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-iconElement-icon,
 .oo-ui-capsuleMultiselectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-indicatorElement-indicator {
index cd62339..7a38633 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:22Z
+ * Date: 2016-08-16T21:13:48Z
  */
 ( function ( OO ) {
 
@@ -4770,103 +4770,6 @@ OO.ui.SelectFileWidget.prototype.setDisabled = function ( disabled ) {
        return this;
 };
 
-/**
- * Progress bars visually display the status of an operation, such as a download,
- * and can be either determinate or indeterminate:
- *
- * - **determinate** process bars show the percent of an operation that is complete.
- *
- * - **indeterminate** process bars use a visual display of motion to indicate that an operation
- *   is taking place. Because the extent of an indeterminate operation is unknown, the bar does
- *   not use percentages.
- *
- * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
- *
- *     @example
- *     // Examples of determinate and indeterminate progress bars.
- *     var progressBar1 = new OO.ui.ProgressBarWidget( {
- *         progress: 33
- *     } );
- *     var progressBar2 = new OO.ui.ProgressBarWidget();
- *
- *     // Create a FieldsetLayout to layout progress bars
- *     var fieldset = new OO.ui.FieldsetLayout;
- *     fieldset.addItems( [
- *        new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
- *        new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
- *     ] );
- *     $( 'body' ).append( fieldset.$element );
- *
- * @class
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
- *  To create a determinate progress bar, specify a number that reflects the initial percent complete.
- *  By default, the progress bar is indeterminate.
- */
-OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
-       // Configuration initialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.ProgressBarWidget.parent.call( this, config );
-
-       // Properties
-       this.$bar = $( '<div>' );
-       this.progress = null;
-
-       // Initialization
-       this.setProgress( config.progress !== undefined ? config.progress : false );
-       this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
-       this.$element
-               .attr( {
-                       role: 'progressbar',
-                       'aria-valuemin': 0,
-                       'aria-valuemax': 100
-               } )
-               .addClass( 'oo-ui-progressBarWidget' )
-               .append( this.$bar );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
-
-/* Static Properties */
-
-OO.ui.ProgressBarWidget.static.tagName = 'div';
-
-/* Methods */
-
-/**
- * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
- *
- * @return {number|boolean} Progress percent
- */
-OO.ui.ProgressBarWidget.prototype.getProgress = function () {
-       return this.progress;
-};
-
-/**
- * Set the percent of the process completed or `false` for an indeterminate process.
- *
- * @param {number|boolean} progress Progress percent or `false` for indeterminate
- */
-OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
-       this.progress = progress;
-
-       if ( progress !== false ) {
-               this.$bar.css( 'width', this.progress + '%' );
-               this.$element.attr( 'aria-valuenow', this.progress );
-       } else {
-               this.$bar.css( 'width', '' );
-               this.$element.removeAttr( 'aria-valuenow' );
-       }
-       this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
-};
-
 /**
  * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
  * and a menu of search results, which is displayed beneath the query
index 55f891a..2f6c1a0 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:27Z
+ * Date: 2016-08-16T21:13:53Z
  */
 .oo-ui-actionWidget.oo-ui-pendingElement-pending {
        background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
        width: 0;
        height: 0;
        overflow: hidden;
+       z-index: 4;
 }
 .oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-active {
        width: auto;
index 0d3976f..465e17b 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:27Z
+ * Date: 2016-08-16T21:13:53Z
  */
 .oo-ui-window {
        background: transparent;
        width: 0;
        height: 0;
        overflow: hidden;
+       z-index: 4;
 }
 .oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-active {
        width: auto;
index 0e9dbf1..510399d 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.7
+ * OOjs UI v0.17.8
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-08-03T16:38:22Z
+ * Date: 2016-08-16T21:13:48Z
  */
 ( function ( OO ) {
 
index 563db00..d9cdf5a 100644 (file)
@@ -254,8 +254,9 @@ div.tleft {
        margin: .5em 1.4em 1.3em 0;
 }
 
-/* Hide elements that are marked as "empty" according to legacy Tidy rules
+/* Hide elements that are marked as "empty" according to legacy Tidy rules,
+ * except if a client script removes the mw-hide-empty-elt class from the body
  */
-.mw-empty-elt, .mw-empty-li {
+body.mw-hide-empty-elt .mw-empty-elt {
        display: none;
 }
diff --git a/resources/src/mediawiki/mediawiki.raggett.css b/resources/src/mediawiki/mediawiki.raggett.css
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/resources/src/oojs-ui-styles-skip.js b/resources/src/oojs-ui-styles-skip.js
deleted file mode 100644 (file)
index 57c905a..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-/*!
- * Skip function for OOjs UI PHP style modules.
- *
- * The `<meta name="X-OOUI-PHP" />` is added to pages by OutputPage::enableOOUI().
- *
- * Looking for elements in the DOM might be expensive, but it's probably better than double-loading
- * 200 KB of CSS with embedded images because of bug T87871.
- */
-return !!jQuery( 'meta[name="X-OOUI-PHP"]' ).length;
index 35eb153..8740e5d 100644 (file)
@@ -23,12 +23,12 @@ mw-vagrant-guest:
   mediawiki_url: http://127.0.0.1/wiki/
 
 beta:
-  mediawiki_url: http://en.wikipedia.beta.wmflabs.org/wiki/
+  mediawiki_url: https://en.wikipedia.beta.wmflabs.org/wiki/
   mediawiki_user: Selenium_user
   # mediawiki_password: SET THIS IN THE ENVIRONMENT!
 
 test2:
-  mediawiki_url: http://test2.wikipedia.org/wiki/
+  mediawiki_url: https://test2.wikipedia.org/wiki/
   mediawiki_user: Selenium_user
   # mediawiki_password: SET THIS IN THE ENVIRONMENT!
 
index 9b85d3f..e965e2d 100644 (file)
@@ -174,10 +174,6 @@ class ParserTest {
                        echo "Warning: tidy is not installed, skipping some tests\n";
                }
 
-               if ( !extension_loaded( 'gd' ) ) {
-                       echo "Warning: GD extension is not present, thumbnailing tests will probably fail\n";
-               }
-
                $this->hooks = [];
                $this->functionHooks = [];
                $this->transparentHooks = [];
index 99b9029..788d304 100644 (file)
@@ -3087,7 +3087,7 @@ class AuthManagerTest extends \MediaWikiTestCase {
                $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
                $expected = [
                        $rememberReq,
-                       $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
@@ -3107,10 +3107,10 @@ class AuthManagerTest extends \MediaWikiTestCase {
                $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
                $expected = [
                        $rememberReq,
-                       $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
-                       $makeReq( "required", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
+                       $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
-                       $makeReq( "foo", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "bar", AuthenticationRequest::REQUIRED ),
                        $makeReq( "baz", AuthenticationRequest::REQUIRED ),
                ];
index a7df221..7d2ba8d 100644 (file)
@@ -172,6 +172,7 @@ class AuthenticationRequestTest extends \MediaWikiTestCase {
                                'type' => 'string',
                                'label' => $msg,
                                'help' => $msg,
+                               'sensitive' => true,
                        ],
                        'string3' => [
                                'type' => 'string',
@@ -206,6 +207,7 @@ class AuthenticationRequestTest extends \MediaWikiTestCase {
                $expect = $req1->getFieldInfo();
                foreach ( $expect as $name => &$options ) {
                        $options['optional'] = !empty( $options['optional'] );
+                       $options['sensitive'] = !empty( $options['sensitive'] );
                }
                unset( $options );
                $this->assertEquals( $expect, $fields );
@@ -225,8 +227,10 @@ class AuthenticationRequestTest extends \MediaWikiTestCase {
 
                $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
                $expect += $req2->getFieldInfo();
+               $expect['string1']['sensitive'] = true;
                $expect['string2']['optional'] = false;
                $expect['string3']['optional'] = false;
+               $expect['string3']['sensitive'] = false;
                $expect['select']['options']['bar'] = $msg;
                $this->assertEquals( $expect, $fields );
 
@@ -237,6 +241,7 @@ class AuthenticationRequestTest extends \MediaWikiTestCase {
                $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
                $expect += $req2->getFieldInfo();
                $expect['string1']['optional'] = false;
+               $expect['string1']['sensitive'] = true;
                $expect['string3']['optional'] = false;
                $expect['select']['optional'] = false;
                $expect['select']['options']['bar'] = $msg;
@@ -246,7 +251,11 @@ class AuthenticationRequestTest extends \MediaWikiTestCase {
 
                $fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
                $expect = $req1->getFieldInfo() + $req2->getFieldInfo();
+               foreach ( $expect as $name => &$options ) {
+                       $options['sensitive'] = !empty( $options['sensitive'] );
+               }
                $expect['string1']['optional'] = false;
+               $expect['string1']['sensitive'] = true;
                $expect['string2']['optional'] = true;
                $expect['string3']['optional'] = true;
                $expect['select']['optional'] = false;
index aa0e3c7..b5c8a36 100644 (file)
@@ -32,6 +32,13 @@ abstract class AuthenticationRequestTestCase extends \MediaWikiTestCase {
                        if ( isset( $data['image'] ) ) {
                                $this->assertType( 'string', $data['image'], "Field $field, image" );
                        }
+                       if ( isset( $data['sensitive'] ) ) {
+                               $this->assertType( 'bool', $data['sensitive'], "Field $field, sensitive" );
+                       }
+                       if ( $data['type'] === 'password' ) {
+                               $this->assertTrue( !empty( $data['sensitive'] ),
+                                       "Field $field, password field must be sensitive" );
+                       }
 
                        switch ( $data['type'] ) {
                                case 'string':
index bb9050f..6168182 100644 (file)
@@ -376,8 +376,7 @@ class ContentHandlerTest extends MediaWikiTestCase {
                $content = new WikitextContent( 'test text' );
                $ok = ContentHandler::runLegacyHooks(
                        'testRunLegacyHooks',
-                       [ 'foo', &$content, 'bar' ],
-                       false
+                       [ 'foo', &$content, 'bar' ]
                );
 
                $this->assertTrue( $ok, "runLegacyHooks should have returned true" );
index 723f1c3..7e55a73 100644 (file)
@@ -285,8 +285,42 @@ class DatabaseTest extends MediaWikiTestCase {
                        $called = true;
                        $db->setFlag( DBO_TRX );
                } );
-               $db->rollback( __METHOD__ );
+               $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
                $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
                $this->assertTrue( $called, 'Callback reached' );
        }
+
+       public function testGetScopedLock() {
+               $db = $this->db;
+
+               $db->setFlag( DBO_TRX );
+               try {
+                       $this->badLockingMethodImplicit( $db );
+               } catch ( RunTimeException $e ) {
+                       $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
+               }
+               $db->clearFlag( DBO_TRX );
+               $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+               $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
+
+               try {
+                       $this->badLockingMethodExplicit( $db );
+               } catch ( RunTimeException $e ) {
+                       $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
+               }
+               $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+               $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
+       }
+
+       private function badLockingMethodImplicit( IDatabase $db ) {
+               $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
+               $db->query( "SELECT 1" ); // trigger DBO_TRX
+               throw new RunTimeException( "Uh oh!" );
+       }
+
+       private function badLockingMethodExplicit( IDatabase $db ) {
+               $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
+               $db->begin( __METHOD__ );
+               throw new RunTimeException( "Uh oh!" );
+       }
 }
index a4871a6..f8dda6f 100644 (file)
@@ -44,6 +44,7 @@ class ObjectFactoryTest extends PHPUnit_Framework_TestCase {
 
        /**
         * @covers ObjectFactory::getObjectFromSpec
+        * @covers ObjectFactory::expandClosures
         */
        public function testClosureExpansionEnabled() {
                $obj = ObjectFactory::getObjectFromSpec( [
@@ -80,6 +81,44 @@ class ObjectFactoryTest extends PHPUnit_Framework_TestCase {
                $this->assertSame( 'unwrapped', $obj->setterArgs[0] );
        }
 
+       /**
+        * @covers ObjectFactory::getObjectFromSpec
+        */
+       public function testGetObjectFromFactory() {
+               $args = [ 'a', 'b' ];
+               $obj = ObjectFactory::getObjectFromSpec( [
+                       'factory' => function ( $a, $b ) {
+                               return new ObjectFactoryTestFixture( $a, $b );
+                       },
+                       'args' => $args,
+               ] );
+               $this->assertSame( $args, $obj->args );
+       }
+
+       /**
+        * @covers ObjectFactory::getObjectFromSpec
+        * @expectedException InvalidArgumentException
+        */
+       public function testGetObjectFromInvalid() {
+               $args = [ 'a', 'b' ];
+               $obj = ObjectFactory::getObjectFromSpec( [
+                       // Missing 'class' or 'factory'
+                       'args' => $args,
+               ] );
+       }
+
+       /**
+        * @covers ObjectFactory::getObjectFromSpec
+        * @dataProvider provideConstructClassInstance
+        */
+       public function testGetObjectFromClass( $args ) {
+               $obj = ObjectFactory::getObjectFromSpec( [
+                       'class' => 'ObjectFactoryTestFixture',
+                       'args' => $args,
+               ] );
+               $this->assertSame( $args, $obj->args );
+       }
+
        /**
         * @covers ObjectFactory::constructClassInstance
         * @dataProvider provideConstructClassInstance
@@ -91,7 +130,7 @@ class ObjectFactoryTest extends PHPUnit_Framework_TestCase {
                $this->assertSame( $args, $obj->args );
        }
 
-       public function provideConstructClassInstance() {
+       public static function provideConstructClassInstance() {
                // These args go to 11. I thought about making 10 one louder, but 11!
                return [
                        '0 args' => [ [] ],
@@ -110,6 +149,7 @@ class ObjectFactoryTest extends PHPUnit_Framework_TestCase {
        }
 
        /**
+        * @covers ObjectFactory::constructClassInstance
         * @expectedException InvalidArgumentException
         */
        public function testNamedArgs() {
index 6446416..86ce53f 100644 (file)
@@ -86,6 +86,25 @@ class ResourcesTest extends MediaWikiTestCase {
                }
        }
 
+       /**
+        * Verify that all specified messages actually exist.
+        */
+       public function testMissingMessages() {
+               $data = self::getAllModules();
+               $validDeps = array_keys( $data['modules'] );
+               $lang = Language::factory( 'en' );
+
+               /** @var ResourceLoaderModule $module */
+               foreach ( $data['modules'] as $moduleName => $module ) {
+                       foreach ( $module->getMessages() as $msgKey ) {
+                               $this->assertTrue(
+                                       wfMessage( $msgKey )->useDatabase( false )->inLanguage( $lang )->exists(),
+                                       "Message '$msgKey' required by '$moduleName' must exist"
+                               );
+                       }
+               }
+       }
+
        /**
         * Verify that all dependencies of all modules are always satisfiable with the 'targets' defined
         * for the involved modules.
index 95f28c8..e30088d 100644 (file)
@@ -74,6 +74,7 @@ return [
                        'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.template.mustache.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.html.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js',
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js
new file mode 100644 (file)
index 0000000..41d800a
--- /dev/null
@@ -0,0 +1,685 @@
+( function ( mw, $ ) {
+       QUnit.module( 'mediawiki (mw.loader)' );
+
+       mw.loader.addSource(
+               'testloader',
+               QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' )
+       );
+
+       /**
+        * The sync style load test (for @import). This is, in a way, also an open bug for
+        * ResourceLoader ("execute js after styles are loaded"), but browsers don't offer a
+        * way to get a callback from when a stylesheet is loaded (that is, including any
+        * `@import` rules inside). To work around this, we'll have a little time loop to check
+        * if the styles apply.
+        *
+        * Note: This test originally used new Image() and onerror to get a callback
+        * when the url is loaded, but that is fragile since it doesn't monitor the
+        * same request as the css @import, and Safari 4 has issues with
+        * onerror/onload not being fired at all in weird cases like this.
+        */
+       function assertStyleAsync( assert, $element, prop, val, fn ) {
+               var styleTestStart,
+                       el = $element.get( 0 ),
+                       styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
+
+               function isCssImportApplied() {
+                       // Trigger reflow, repaint, redraw, whatever (cross-browser)
+                       var x = $element.css( 'height' );
+                       x = el.innerHTML;
+                       el.className = el.className;
+                       x = document.documentElement.clientHeight;
+
+                       return $element.css( prop ) === val;
+               }
+
+               function styleTestLoop() {
+                       var styleTestSince = new Date().getTime() - styleTestStart;
+                       // If it is passing or if we timed out, run the real test and stop the loop
+                       if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) {
+                               assert.equal( $element.css( prop ), val,
+                                       'style "' + prop + ': ' + val + '" from url is applied (after ' + styleTestSince + 'ms)'
+                               );
+
+                               if ( fn ) {
+                                       fn();
+                               }
+
+                               return;
+                       }
+                       // Otherwise, keep polling
+                       setTimeout( styleTestLoop );
+               }
+
+               // Start the loop
+               styleTestStart = new Date().getTime();
+               styleTestLoop();
+       }
+
+       function urlStyleTest( selector, prop, val ) {
+               return QUnit.fixurl(
+                       mw.config.get( 'wgScriptPath' ) +
+                               '/tests/qunit/data/styleTest.css.php?' +
+                               $.param( {
+                                       selector: selector,
+                                       prop: prop,
+                                       val: val
+                               } )
+               );
+       }
+
+       QUnit.test( 'Basic', 2, function ( assert ) {
+               var isAwesomeDone;
+
+               mw.loader.testCallback = function () {
+                       assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
+                       isAwesomeDone = true;
+               };
+
+               mw.loader.implement( 'test.callback', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
+
+               return mw.loader.using( 'test.callback', function () {
+                       assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' );
+                       delete mw.loader.testCallback;
+
+               }, function () {
+                       assert.ok( false, 'Error callback fired while loader.using "test.callback" module' );
+               } );
+       } );
+
+       QUnit.test( 'Object method as module name', 2, function ( assert ) {
+               var isAwesomeDone;
+
+               mw.loader.testCallback = function () {
+                       assert.strictEqual( isAwesomeDone, undefined, 'Implementing module hasOwnProperty: isAwesomeDone should still be undefined' );
+                       isAwesomeDone = true;
+               };
+
+               mw.loader.implement( 'hasOwnProperty', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ], {}, {} );
+
+               return mw.loader.using( 'hasOwnProperty', function () {
+                       assert.strictEqual( isAwesomeDone, true, 'hasOwnProperty module should\'ve caused isAwesomeDone to be true' );
+                       delete mw.loader.testCallback;
+
+               }, function () {
+                       assert.ok( false, 'Error callback fired while loader.using "hasOwnProperty" module' );
+               } );
+       } );
+
+       QUnit.test( '.using( .. ) Promise', 2, function ( assert ) {
+               var isAwesomeDone;
+
+               mw.loader.testCallback = function () {
+                       assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
+                       isAwesomeDone = true;
+               };
+
+               mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
+
+               return mw.loader.using( 'test.promise' )
+               .done( function () {
+                       assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
+                       delete mw.loader.testCallback;
+
+               } )
+               .fail( function () {
+                       assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
+               } );
+       } );
+
+       QUnit.test( '.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
+               var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
+
+               assert.notEqual(
+                       $element.css( 'float' ),
+                       'right',
+                       'style is clear'
+               );
+
+               mw.loader.implement(
+                       'test.implement.a',
+                       function () {
+                               assert.equal(
+                                       $element.css( 'float' ),
+                                       'right',
+                                       'style is applied'
+                               );
+                       },
+                       {
+                               all: '.mw-test-implement-a { float: right; }'
+                       }
+               );
+
+               return mw.loader.using( 'test.implement.a' );
+       } );
+
+       QUnit.test( '.implement( styles={ "url": { <media>: [url, ..] } } )', 7, function ( assert ) {
+               var $element1 = $( '<div class="mw-test-implement-b1"></div>' ).appendTo( '#qunit-fixture' ),
+                       $element2 = $( '<div class="mw-test-implement-b2"></div>' ).appendTo( '#qunit-fixture' ),
+                       $element3 = $( '<div class="mw-test-implement-b3"></div>' ).appendTo( '#qunit-fixture' ),
+                       done = assert.async();
+
+               assert.notEqual(
+                       $element1.css( 'text-align' ),
+                       'center',
+                       'style is clear'
+               );
+               assert.notEqual(
+                       $element2.css( 'float' ),
+                       'left',
+                       'style is clear'
+               );
+               assert.notEqual(
+                       $element3.css( 'text-align' ),
+                       'right',
+                       'style is clear'
+               );
+
+               mw.loader.implement(
+                       'test.implement.b',
+                       function () {
+                               // Note: done() must only be called when the entire test is
+                               // complete. So, make sure that we don't start until *both*
+                               // assertStyleAsync calls have completed.
+                               var pending = 2;
+                               assertStyleAsync( assert, $element2, 'float', 'left', function () {
+                                       assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
+
+                                       pending--;
+                                       if ( pending === 0 ) {
+                                               done();
+                                       }
+                               } );
+                               assertStyleAsync( assert, $element3, 'float', 'right', function () {
+                                       assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
+
+                                       pending--;
+                                       if ( pending === 0 ) {
+                                               done();
+                                       }
+                               } );
+                       },
+                       {
+                               url: {
+                                       print: [ urlStyleTest( '.mw-test-implement-b1', 'text-align', 'center' ) ],
+                                       screen: [
+                                               // bug 40834: Make sure it actually works with more than 1 stylesheet reference
+                                               urlStyleTest( '.mw-test-implement-b2', 'float', 'left' ),
+                                               urlStyleTest( '.mw-test-implement-b3', 'float', 'right' )
+                                       ]
+                               }
+                       }
+               );
+
+               mw.loader.load( 'test.implement.b' );
+       } );
+
+       // Backwards compatibility
+       QUnit.test( '.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
+               var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
+
+               assert.notEqual(
+                       $element.css( 'float' ),
+                       'right',
+                       'style is clear'
+               );
+
+               mw.loader.implement(
+                       'test.implement.c',
+                       function () {
+                               assert.equal(
+                                       $element.css( 'float' ),
+                                       'right',
+                                       'style is applied'
+                               );
+                       },
+                       {
+                               all: '.mw-test-implement-c { float: right; }'
+                       }
+               );
+
+               return mw.loader.using( 'test.implement.c' );
+       } );
+
+       // Backwards compatibility
+       QUnit.test( '.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) {
+               var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
+                       $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' ),
+                       done = assert.async();
+
+               assert.notEqual(
+                       $element.css( 'float' ),
+                       'right',
+                       'style is clear'
+               );
+               assert.notEqual(
+                       $element2.css( 'text-align' ),
+                       'center',
+                       'style is clear'
+               );
+
+               mw.loader.implement(
+                       'test.implement.d',
+                       function () {
+                               assertStyleAsync( assert, $element, 'float', 'right', function () {
+                                       assert.notEqual( $element2.css( 'text-align' ), 'center', 'print style is not applied (bug 40500)' );
+                                       done();
+                               } );
+                       },
+                       {
+                               all: [ urlStyleTest( '.mw-test-implement-d', 'float', 'right' ) ],
+                               print: [ urlStyleTest( '.mw-test-implement-d2', 'text-align', 'center' ) ]
+                       }
+               );
+
+               mw.loader.load( 'test.implement.d' );
+       } );
+
+       // @import (bug 31676)
+       QUnit.test( '.implement( styles has @import )', 7, function ( assert ) {
+               var isJsExecuted, $element,
+                       done = assert.async();
+
+               mw.loader.implement(
+                       'test.implement.import',
+                       function () {
+                               assert.strictEqual( isJsExecuted, undefined, 'script not executed multiple times' );
+                               isJsExecuted = true;
+
+                               assert.equal( mw.loader.getState( 'test.implement.import' ), 'executing', 'module state during implement() script execution' );
+
+                               $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' );
+
+                               assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' );
+
+                               assertStyleAsync( assert, $element, 'float', 'right', function () {
+                                       assert.equal( $element.css( 'text-align' ), 'center',
+                                               'CSS styles after the @import rule are working'
+                                       );
+
+                                       done();
+                               } );
+                       },
+                       {
+                               css: [
+                                       '@import url(\''
+                                               + urlStyleTest( '.mw-test-implement-import', 'float', 'right' )
+                                               + '\');\n'
+                                               + '.mw-test-implement-import { text-align: center; }'
+                               ]
+                       },
+                       {
+                               'test-foobar': 'Hello Foobar, $1!'
+                       }
+               );
+
+               mw.loader.using( 'test.implement.import' ).always( function () {
+                       assert.strictEqual( isJsExecuted, true, 'script executed' );
+                       assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state after script execution' );
+               } );
+       } );
+
+       QUnit.test( '.implement( dependency with styles )', 4, function ( assert ) {
+               var $element = $( '<div class="mw-test-implement-e"></div>' ).appendTo( '#qunit-fixture' ),
+                       $element2 = $( '<div class="mw-test-implement-e2"></div>' ).appendTo( '#qunit-fixture' );
+
+               assert.notEqual(
+                       $element.css( 'float' ),
+                       'right',
+                       'style is clear'
+               );
+               assert.notEqual(
+                       $element2.css( 'float' ),
+                       'left',
+                       'style is clear'
+               );
+
+               mw.loader.register( [
+                       [ 'test.implement.e', '0', [ 'test.implement.e2' ] ],
+                       [ 'test.implement.e2', '0' ]
+               ] );
+
+               mw.loader.implement(
+                       'test.implement.e',
+                       function () {
+                               assert.equal(
+                                       $element.css( 'float' ),
+                                       'right',
+                                       'Depending module\'s style is applied'
+                               );
+                       },
+                       {
+                               all: '.mw-test-implement-e { float: right; }'
+                       }
+               );
+
+               mw.loader.implement(
+                       'test.implement.e2',
+                       function () {
+                               assert.equal(
+                                       $element2.css( 'float' ),
+                                       'left',
+                                       'Dependency\'s style is applied'
+                               );
+                       },
+                       {
+                               all: '.mw-test-implement-e2 { float: left; }'
+                       }
+               );
+
+               return mw.loader.using( 'test.implement.e' );
+       } );
+
+       QUnit.test( '.implement( only scripts )', 1, function ( assert ) {
+               mw.loader.implement( 'test.onlyscripts', function () {} );
+               assert.strictEqual( mw.loader.getState( 'test.onlyscripts' ), 'ready' );
+       } );
+
+       QUnit.test( '.implement( only messages )', 2, function ( assert ) {
+               assert.assertFalse( mw.messages.exists( 'bug_29107' ), 'Verify that the test message doesn\'t exist yet' );
+
+               // jscs: disable requireCamelCaseOrUpperCaseIdentifiers
+               mw.loader.implement( 'test.implement.msgs', [], {}, { bug_29107: 'loaded' } );
+               // jscs: enable requireCamelCaseOrUpperCaseIdentifiers
+
+               return mw.loader.using( 'test.implement.msgs', function () {
+                       assert.ok( mw.messages.exists( 'bug_29107' ), 'Bug 29107: messages-only module should implement ok' );
+               }, function () {
+                       assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' );
+               } );
+       } );
+
+       QUnit.test( '.implement( empty )', 1, function ( assert ) {
+               mw.loader.implement( 'test.empty' );
+               assert.strictEqual( mw.loader.getState( 'test.empty' ), 'ready' );
+       } );
+
+       QUnit.test( 'Broken indirect dependency', 4, function ( assert ) {
+               // don't emit an error event
+               this.sandbox.stub( mw, 'track' );
+
+               mw.loader.register( [
+                       [ 'test.module1', '0' ],
+                       [ 'test.module2', '0', [ 'test.module1' ] ],
+                       [ 'test.module3', '0', [ 'test.module2' ] ]
+               ] );
+               mw.loader.implement( 'test.module1', function () {
+                       throw new Error( 'expected' );
+               }, {}, {} );
+               assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
+               assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
+               assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
+
+               assert.strictEqual( mw.track.callCount, 1 );
+       } );
+
+       QUnit.test( 'Circular dependency', 1, function ( assert ) {
+               mw.loader.register( [
+                       [ 'test.circle1', '0', [ 'test.circle2' ] ],
+                       [ 'test.circle2', '0', [ 'test.circle3' ] ],
+                       [ 'test.circle3', '0', [ 'test.circle1' ] ]
+               ] );
+               assert.throws( function () {
+                       mw.loader.using( 'test.circle3' );
+               }, /Circular/, 'Detect circular dependency' );
+       } );
+
+       QUnit.test( 'Out-of-order implementation', 9, function ( assert ) {
+               mw.loader.register( [
+                       [ 'test.module4', '0' ],
+                       [ 'test.module5', '0', [ 'test.module4' ] ],
+                       [ 'test.module6', '0', [ 'test.module5' ] ]
+               ] );
+               mw.loader.implement( 'test.module4', function () {} );
+               assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+               assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
+               assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
+               mw.loader.implement( 'test.module6', function () {} );
+               assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+               assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
+               assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
+               mw.loader.implement( 'test.module5', function () {} );
+               assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+               assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
+               assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
+       } );
+
+       QUnit.test( 'Missing dependency', 13, function ( assert ) {
+               mw.loader.register( [
+                       [ 'test.module7', '0' ],
+                       [ 'test.module8', '0', [ 'test.module7' ] ],
+                       [ 'test.module9', '0', [ 'test.module8' ] ]
+               ] );
+               mw.loader.implement( 'test.module8', function () {} );
+               assert.strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' );
+               assert.strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' );
+               assert.strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' );
+               mw.loader.state( 'test.module7', 'missing' );
+               assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
+               assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
+               assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
+               mw.loader.implement( 'test.module9', function () {} );
+               assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
+               assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
+               assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
+               mw.loader.using(
+                       [ 'test.module7' ],
+                       function () {
+                               assert.ok( false, 'Success fired despite missing dependency' );
+                               assert.ok( true, 'QUnit expected() count dummy' );
+                       },
+                       function ( e, dependencies ) {
+                               assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
+                               assert.deepEqual( dependencies, [ 'test.module7' ], 'Error callback called with module test.module7' );
+                       }
+               );
+               mw.loader.using(
+                       [ 'test.module9' ],
+                       function () {
+                               assert.ok( false, 'Success fired despite missing dependency' );
+                               assert.ok( true, 'QUnit expected() count dummy' );
+                       },
+                       function ( e, dependencies ) {
+                               assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
+                               dependencies.sort();
+                               assert.deepEqual(
+                                       dependencies,
+                                       [ 'test.module7', 'test.module8', 'test.module9' ],
+                                       'Error callback called with all three modules as dependencies'
+                               );
+                       }
+               );
+       } );
+
+       QUnit.test( 'Dependency handling', 5, function ( assert ) {
+               var done = assert.async();
+               mw.loader.register( [
+                       // [module, version, dependencies, group, source]
+                       [ 'testMissing', '1', [], null, 'testloader' ],
+                       [ 'testUsesMissing', '1', [ 'testMissing' ], null, 'testloader' ],
+                       [ 'testUsesNestedMissing', '1', [ 'testUsesMissing' ], null, 'testloader' ]
+               ] );
+
+               function verifyModuleStates() {
+                       assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' );
+                       assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' );
+                       assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' );
+               }
+
+               mw.loader.using( [ 'testUsesNestedMissing' ],
+                       function () {
+                               assert.ok( false, 'Error handler should be invoked.' );
+                               assert.ok( true ); // Dummy to reach QUnit expect()
+
+                               verifyModuleStates();
+
+                               done();
+                       },
+                       function ( e, badmodules ) {
+                               assert.ok( true, 'Error handler should be invoked.' );
+                               // As soon as server spits out state('testMissing', 'missing');
+                               // it will bubble up and trigger the error callback.
+                               // Therefor the badmodules array is not testUsesMissing or testUsesNestedMissing.
+                               assert.deepEqual( badmodules, [ 'testMissing' ], 'Bad modules as expected.' );
+
+                               verifyModuleStates();
+
+                               done();
+                       }
+               );
+       } );
+
+       QUnit.test( 'Skip-function handling', 5, function ( assert ) {
+               mw.loader.register( [
+                       // [module, version, dependencies, group, source, skip]
+                       [ 'testSkipped', '1', [], null, 'testloader', 'return true;' ],
+                       [ 'testNotSkipped', '1', [], null, 'testloader', 'return false;' ],
+                       [ 'testUsesSkippable', '1', [ 'testSkipped', 'testNotSkipped' ], null, 'testloader' ]
+               ] );
+
+               function verifyModuleStates() {
+                       assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Module is ready when skipped' );
+                       assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Module is ready when not skipped but loaded' );
+                       assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Module is ready when skippable dependencies are ready' );
+               }
+
+               return mw.loader.using( [ 'testUsesSkippable' ],
+                       function () {
+                               assert.ok( true, 'Success handler should be invoked.' );
+                               assert.ok( true ); // Dummy to match error handler and reach QUnit expect()
+
+                               verifyModuleStates();
+                       },
+                       function ( e, badmodules ) {
+                               assert.ok( false, 'Error handler should not be invoked.' );
+                               assert.deepEqual( badmodules, [], 'Bad modules as expected.' );
+
+                               verifyModuleStates();
+                       }
+               );
+       } );
+
+       QUnit.asyncTest( '.load( "//protocol-relative" ) - T32825', 2, function ( assert ) {
+               // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
+               // Test is for regressions!
+
+               // Forge a URL to the test callback script
+               var target = QUnit.fixurl(
+                       mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
+               );
+
+               // Confirm that mw.loader.load() works with protocol-relative URLs
+               target = target.replace( /https?:/, '' );
+
+               assert.equal( target.slice( 0, 2 ), '//',
+                       'URL must be relative to test relative URLs!'
+               );
+
+               // Async!
+               // The target calls QUnit.start
+               mw.loader.load( target );
+       } );
+
+       QUnit.asyncTest( '.load( "/absolute-path" )', 2, function ( assert ) {
+               // Forge a URL to the test callback script
+               var target = QUnit.fixurl(
+                       mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
+               );
+
+               // Confirm that mw.loader.load() works with absolute-paths (relative to current hostname)
+               assert.equal( target.slice( 0, 1 ), '/', 'URL is relative to document root' );
+
+               // Async!
+               // The target calls QUnit.start
+               mw.loader.load( target );
+       } );
+
+       QUnit.test( 'Executing race - T112232', 2, function ( assert ) {
+               var done = false;
+
+               // The red herring schedules its CSS buffer first. In T112232, a bug in the
+               // state machine would cause the job for testRaceLoadMe to run with an earlier job.
+               mw.loader.implement(
+                       'testRaceRedHerring',
+                       function () {},
+                       { css: [ '.mw-testRaceRedHerring {}' ] }
+               );
+               mw.loader.implement(
+                       'testRaceLoadMe',
+                       function () {
+                               done = true;
+                       },
+                       { css: [ '.mw-testRaceLoadMe { float: left; }' ] }
+               );
+
+               mw.loader.load( [ 'testRaceRedHerring', 'testRaceLoadMe' ] );
+               return mw.loader.using( 'testRaceLoadMe', function () {
+                       assert.strictEqual( done, true, 'script ran' );
+                       assert.strictEqual( mw.loader.getState( 'testRaceLoadMe' ), 'ready', 'state' );
+               } );
+       } );
+
+       QUnit.test( 'require()', 6, function ( assert ) {
+               mw.loader.register( [
+                       [ 'test.require1', '0' ],
+                       [ 'test.require2', '0' ],
+                       [ 'test.require3', '0' ],
+                       [ 'test.require4', '0', [ 'test.require3' ] ]
+               ] );
+               mw.loader.implement( 'test.require1', function () {} );
+               mw.loader.implement( 'test.require2', function ( $, jQuery, require, module ) {
+                       module.exports = 1;
+               } );
+               mw.loader.implement( 'test.require3', function ( $, jQuery, require, module ) {
+                       module.exports = function () {
+                               return 'hello world';
+                       };
+               } );
+               mw.loader.implement( 'test.require4', function ( $, jQuery, require, module ) {
+                       var other = require( 'test.require3' );
+                       module.exports = {
+                               pizza: function () {
+                                       return other();
+                               }
+                       };
+               } );
+               return mw.loader.using( [ 'test.require1', 'test.require2', 'test.require3', 'test.require4' ] )
+               .then( function ( require ) {
+                       var module1, module2, module3, module4;
+
+                       module1 = require( 'test.require1' );
+                       module2 = require( 'test.require2' );
+                       module3 = require( 'test.require3' );
+                       module4 = require( 'test.require4' );
+
+                       assert.strictEqual( typeof module1, 'object', 'export of module with no export' );
+                       assert.strictEqual( module2, 1, 'export a number' );
+                       assert.strictEqual( module3(), 'hello world', 'export a function' );
+                       assert.strictEqual( typeof module4.pizza, 'function', 'export an object' );
+                       assert.strictEqual( module4.pizza(), 'hello world', 'module can require other modules' );
+
+                       assert.throws( function () {
+                               require( '_badmodule' );
+                       }, /is not loaded/, 'Requesting non-existent modules throws error.' );
+               } );
+       } );
+
+       QUnit.test( 'require() in debug mode', 1, function ( assert ) {
+               var path = mw.config.get( 'wgScriptPath' );
+               mw.loader.register( [
+                       [ 'test.require.define', '0' ],
+                       [ 'test.require.callback', '0', [ 'test.require.define' ] ]
+               ] );
+               mw.loader.implement( 'test.require.callback', [ QUnit.fixurl( path + '/tests/qunit/data/requireCallMwLoaderTestCallback.js' ) ] );
+               mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
+
+               return mw.loader.using( 'test.require.callback' ).then( function ( require ) {
+                       var exported = require( 'test.require.callback' );
+                       assert.strictEqual( exported, 'Require worked.Define worked.',
+                               'module.exports worked in debug mode' );
+               }, function () {
+                       assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );
index baec37c..ab463a9 100644 (file)
@@ -1,5 +1,5 @@
 /*jshint -W024 */
-( function ( mw, $ ) {
+( function ( mw ) {
        var specialCharactersPageName,
                // Can't mock SITENAME since jqueryMsg caches it at load
                siteName = mw.config.get( 'wgSiteName' );
                }
        } ) );
 
-       mw.loader.addSource(
-               'testloader',
-               QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' )
-       );
-
        QUnit.test( 'Initial check', 8, function ( assert ) {
                assert.ok( window.jQuery, 'jQuery defined' );
                assert.ok( window.$, '$ defined' );
                assert.equal( mw.msg( 'int-msg' ), 'Some Other Message', 'int is resolved' );
        } );
 
-       /**
-        * The sync style load test (for @import). This is, in a way, also an open bug for
-        * ResourceLoader ("execute js after styles are loaded"), but browsers don't offer a
-        * way to get a callback from when a stylesheet is loaded (that is, including any
-        * `@import` rules inside). To work around this, we'll have a little time loop to check
-        * if the styles apply.
-        *
-        * Note: This test originally used new Image() and onerror to get a callback
-        * when the url is loaded, but that is fragile since it doesn't monitor the
-        * same request as the css @import, and Safari 4 has issues with
-        * onerror/onload not being fired at all in weird cases like this.
-        */
-       function assertStyleAsync( assert, $element, prop, val, fn ) {
-               var styleTestStart,
-                       el = $element.get( 0 ),
-                       styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
-
-               function isCssImportApplied() {
-                       // Trigger reflow, repaint, redraw, whatever (cross-browser)
-                       var x = $element.css( 'height' );
-                       x = el.innerHTML;
-                       el.className = el.className;
-                       x = document.documentElement.clientHeight;
-
-                       return $element.css( prop ) === val;
-               }
-
-               function styleTestLoop() {
-                       var styleTestSince = new Date().getTime() - styleTestStart;
-                       // If it is passing or if we timed out, run the real test and stop the loop
-                       if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) {
-                               assert.equal( $element.css( prop ), val,
-                                       'style "' + prop + ': ' + val + '" from url is applied (after ' + styleTestSince + 'ms)'
-                               );
-
-                               if ( fn ) {
-                                       fn();
-                               }
-
-                               return;
-                       }
-                       // Otherwise, keep polling
-                       setTimeout( styleTestLoop );
-               }
-
-               // Start the loop
-               styleTestStart = new Date().getTime();
-               styleTestLoop();
-       }
-
-       function urlStyleTest( selector, prop, val ) {
-               return QUnit.fixurl(
-                       mw.config.get( 'wgScriptPath' ) +
-                               '/tests/qunit/data/styleTest.css.php?' +
-                               $.param( {
-                                       selector: selector,
-                                       prop: prop,
-                                       val: val
-                               } )
-               );
-       }
-
-       QUnit.test( 'mw.loader', 2, function ( assert ) {
-               var isAwesomeDone;
-
-               mw.loader.testCallback = function () {
-                       assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
-                       isAwesomeDone = true;
-               };
-
-               mw.loader.implement( 'test.callback', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
-
-               return mw.loader.using( 'test.callback', function () {
-                       assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' );
-                       delete mw.loader.testCallback;
-
-               }, function () {
-                       assert.ok( false, 'Error callback fired while loader.using "test.callback" module' );
-               } );
-       } );
-
-       QUnit.test( 'mw.loader with Object method as module name', 2, function ( assert ) {
-               var isAwesomeDone;
-
-               mw.loader.testCallback = function () {
-                       assert.strictEqual( isAwesomeDone, undefined, 'Implementing module hasOwnProperty: isAwesomeDone should still be undefined' );
-                       isAwesomeDone = true;
-               };
-
-               mw.loader.implement( 'hasOwnProperty', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ], {}, {} );
-
-               return mw.loader.using( 'hasOwnProperty', function () {
-                       assert.strictEqual( isAwesomeDone, true, 'hasOwnProperty module should\'ve caused isAwesomeDone to be true' );
-                       delete mw.loader.testCallback;
-
-               }, function () {
-                       assert.ok( false, 'Error callback fired while loader.using "hasOwnProperty" module' );
-               } );
-       } );
-
-       QUnit.test( 'mw.loader.using( .. ) Promise', 2, function ( assert ) {
-               var isAwesomeDone;
-
-               mw.loader.testCallback = function () {
-                       assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
-                       isAwesomeDone = true;
-               };
-
-               mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
-
-               return mw.loader.using( 'test.promise' )
-               .done( function () {
-                       assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
-                       delete mw.loader.testCallback;
-
-               } )
-               .fail( function () {
-                       assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
-               } );
-       } );
-
-       QUnit.test( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
-               var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
-
-               assert.notEqual(
-                       $element.css( 'float' ),
-                       'right',
-                       'style is clear'
-               );
-
-               mw.loader.implement(
-                       'test.implement.a',
-                       function () {
-                               assert.equal(
-                                       $element.css( 'float' ),
-                                       'right',
-                                       'style is applied'
-                               );
-                       },
-                       {
-                               all: '.mw-test-implement-a { float: right; }'
-                       }
-               );
-
-               return mw.loader.using( 'test.implement.a' );
-       } );
-
-       QUnit.test( 'mw.loader.implement( styles={ "url": { <media>: [url, ..] } } )', 7, function ( assert ) {
-               var $element1 = $( '<div class="mw-test-implement-b1"></div>' ).appendTo( '#qunit-fixture' ),
-                       $element2 = $( '<div class="mw-test-implement-b2"></div>' ).appendTo( '#qunit-fixture' ),
-                       $element3 = $( '<div class="mw-test-implement-b3"></div>' ).appendTo( '#qunit-fixture' ),
-                       done = assert.async();
-
-               assert.notEqual(
-                       $element1.css( 'text-align' ),
-                       'center',
-                       'style is clear'
-               );
-               assert.notEqual(
-                       $element2.css( 'float' ),
-                       'left',
-                       'style is clear'
-               );
-               assert.notEqual(
-                       $element3.css( 'text-align' ),
-                       'right',
-                       'style is clear'
-               );
-
-               mw.loader.implement(
-                       'test.implement.b',
-                       function () {
-                               // Note: done() must only be called when the entire test is
-                               // complete. So, make sure that we don't start until *both*
-                               // assertStyleAsync calls have completed.
-                               var pending = 2;
-                               assertStyleAsync( assert, $element2, 'float', 'left', function () {
-                                       assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
-
-                                       pending--;
-                                       if ( pending === 0 ) {
-                                               done();
-                                       }
-                               } );
-                               assertStyleAsync( assert, $element3, 'float', 'right', function () {
-                                       assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
-
-                                       pending--;
-                                       if ( pending === 0 ) {
-                                               done();
-                                       }
-                               } );
-                       },
-                       {
-                               url: {
-                                       print: [ urlStyleTest( '.mw-test-implement-b1', 'text-align', 'center' ) ],
-                                       screen: [
-                                               // bug 40834: Make sure it actually works with more than 1 stylesheet reference
-                                               urlStyleTest( '.mw-test-implement-b2', 'float', 'left' ),
-                                               urlStyleTest( '.mw-test-implement-b3', 'float', 'right' )
-                                       ]
-                               }
-                       }
-               );
-
-               mw.loader.load( 'test.implement.b' );
-       } );
-
-       // Backwards compatibility
-       QUnit.test( 'mw.loader.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
-               var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
-
-               assert.notEqual(
-                       $element.css( 'float' ),
-                       'right',
-                       'style is clear'
-               );
-
-               mw.loader.implement(
-                       'test.implement.c',
-                       function () {
-                               assert.equal(
-                                       $element.css( 'float' ),
-                                       'right',
-                                       'style is applied'
-                               );
-                       },
-                       {
-                               all: '.mw-test-implement-c { float: right; }'
-                       }
-               );
-
-               return mw.loader.using( 'test.implement.c' );
-       } );
-
-       // Backwards compatibility
-       QUnit.test( 'mw.loader.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) {
-               var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
-                       $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' ),
-                       done = assert.async();
-
-               assert.notEqual(
-                       $element.css( 'float' ),
-                       'right',
-                       'style is clear'
-               );
-               assert.notEqual(
-                       $element2.css( 'text-align' ),
-                       'center',
-                       'style is clear'
-               );
-
-               mw.loader.implement(
-                       'test.implement.d',
-                       function () {
-                               assertStyleAsync( assert, $element, 'float', 'right', function () {
-                                       assert.notEqual( $element2.css( 'text-align' ), 'center', 'print style is not applied (bug 40500)' );
-                                       done();
-                               } );
-                       },
-                       {
-                               all: [ urlStyleTest( '.mw-test-implement-d', 'float', 'right' ) ],
-                               print: [ urlStyleTest( '.mw-test-implement-d2', 'text-align', 'center' ) ]
-                       }
-               );
-
-               mw.loader.load( 'test.implement.d' );
-       } );
-
-       // @import (bug 31676)
-       QUnit.test( 'mw.loader.implement( styles has @import )', 7, function ( assert ) {
-               var isJsExecuted, $element,
-                       done = assert.async();
-
-               mw.loader.implement(
-                       'test.implement.import',
-                       function () {
-                               assert.strictEqual( isJsExecuted, undefined, 'script not executed multiple times' );
-                               isJsExecuted = true;
-
-                               assert.equal( mw.loader.getState( 'test.implement.import' ), 'executing', 'module state during implement() script execution' );
-
-                               $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' );
-
-                               assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' );
-
-                               assertStyleAsync( assert, $element, 'float', 'right', function () {
-                                       assert.equal( $element.css( 'text-align' ), 'center',
-                                               'CSS styles after the @import rule are working'
-                                       );
-
-                                       done();
-                               } );
-                       },
-                       {
-                               css: [
-                                       '@import url(\''
-                                               + urlStyleTest( '.mw-test-implement-import', 'float', 'right' )
-                                               + '\');\n'
-                                               + '.mw-test-implement-import { text-align: center; }'
-                               ]
-                       },
-                       {
-                               'test-foobar': 'Hello Foobar, $1!'
-                       }
-               );
-
-               mw.loader.using( 'test.implement.import' ).always( function () {
-                       assert.strictEqual( isJsExecuted, true, 'script executed' );
-                       assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state after script execution' );
-               } );
-       } );
-
-       QUnit.test( 'mw.loader.implement( dependency with styles )', 4, function ( assert ) {
-               var $element = $( '<div class="mw-test-implement-e"></div>' ).appendTo( '#qunit-fixture' ),
-                       $element2 = $( '<div class="mw-test-implement-e2"></div>' ).appendTo( '#qunit-fixture' );
-
-               assert.notEqual(
-                       $element.css( 'float' ),
-                       'right',
-                       'style is clear'
-               );
-               assert.notEqual(
-                       $element2.css( 'float' ),
-                       'left',
-                       'style is clear'
-               );
-
-               mw.loader.register( [
-                       [ 'test.implement.e', '0', [ 'test.implement.e2' ] ],
-                       [ 'test.implement.e2', '0' ]
-               ] );
-
-               mw.loader.implement(
-                       'test.implement.e',
-                       function () {
-                               assert.equal(
-                                       $element.css( 'float' ),
-                                       'right',
-                                       'Depending module\'s style is applied'
-                               );
-                       },
-                       {
-                               all: '.mw-test-implement-e { float: right; }'
-                       }
-               );
-
-               mw.loader.implement(
-                       'test.implement.e2',
-                       function () {
-                               assert.equal(
-                                       $element2.css( 'float' ),
-                                       'left',
-                                       'Dependency\'s style is applied'
-                               );
-                       },
-                       {
-                               all: '.mw-test-implement-e2 { float: left; }'
-                       }
-               );
-
-               return mw.loader.using( 'test.implement.e' );
-       } );
-
-       QUnit.test( 'mw.loader.implement( only scripts )', 1, function ( assert ) {
-               mw.loader.implement( 'test.onlyscripts', function () {} );
-               assert.strictEqual( mw.loader.getState( 'test.onlyscripts' ), 'ready' );
-       } );
-
-       QUnit.test( 'mw.loader.implement( only messages )', 2, function ( assert ) {
-               assert.assertFalse( mw.messages.exists( 'bug_29107' ), 'Verify that the test message doesn\'t exist yet' );
-
-               // jscs: disable requireCamelCaseOrUpperCaseIdentifiers
-               mw.loader.implement( 'test.implement.msgs', [], {}, { bug_29107: 'loaded' } );
-               // jscs: enable requireCamelCaseOrUpperCaseIdentifiers
-
-               return mw.loader.using( 'test.implement.msgs', function () {
-                       assert.ok( mw.messages.exists( 'bug_29107' ), 'Bug 29107: messages-only module should implement ok' );
-               }, function () {
-                       assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' );
-               } );
-       } );
-
-       QUnit.test( 'mw.loader.implement( empty )', 1, function ( assert ) {
-               mw.loader.implement( 'test.empty' );
-               assert.strictEqual( mw.loader.getState( 'test.empty' ), 'ready' );
-       } );
-
-       QUnit.test( 'mw.loader with broken indirect dependency', 4, function ( assert ) {
-               // don't emit an error event
-               this.sandbox.stub( mw, 'track' );
-
-               mw.loader.register( [
-                       [ 'test.module1', '0' ],
-                       [ 'test.module2', '0', [ 'test.module1' ] ],
-                       [ 'test.module3', '0', [ 'test.module2' ] ]
-               ] );
-               mw.loader.implement( 'test.module1', function () {
-                       throw new Error( 'expected' );
-               }, {}, {} );
-               assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
-               assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
-               assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
-
-               assert.strictEqual( mw.track.callCount, 1 );
-       } );
-
-       QUnit.test( 'mw.loader with circular dependency', 1, function ( assert ) {
-               mw.loader.register( [
-                       [ 'test.circle1', '0', [ 'test.circle2' ] ],
-                       [ 'test.circle2', '0', [ 'test.circle3' ] ],
-                       [ 'test.circle3', '0', [ 'test.circle1' ] ]
-               ] );
-               assert.throws( function () {
-                       mw.loader.using( 'test.circle3' );
-               }, /Circular/, 'Detect circular dependency' );
-       } );
-
-       QUnit.test( 'mw.loader out-of-order implementation', 9, function ( assert ) {
-               mw.loader.register( [
-                       [ 'test.module4', '0' ],
-                       [ 'test.module5', '0', [ 'test.module4' ] ],
-                       [ 'test.module6', '0', [ 'test.module5' ] ]
-               ] );
-               mw.loader.implement( 'test.module4', function () {} );
-               assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
-               assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
-               assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
-               mw.loader.implement( 'test.module6', function () {} );
-               assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
-               assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
-               assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
-               mw.loader.implement( 'test.module5', function () {} );
-               assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
-               assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
-               assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
-       } );
-
-       QUnit.test( 'mw.loader missing dependency', 13, function ( assert ) {
-               mw.loader.register( [
-                       [ 'test.module7', '0' ],
-                       [ 'test.module8', '0', [ 'test.module7' ] ],
-                       [ 'test.module9', '0', [ 'test.module8' ] ]
-               ] );
-               mw.loader.implement( 'test.module8', function () {} );
-               assert.strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' );
-               assert.strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' );
-               assert.strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' );
-               mw.loader.state( 'test.module7', 'missing' );
-               assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
-               assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
-               assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
-               mw.loader.implement( 'test.module9', function () {} );
-               assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
-               assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
-               assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
-               mw.loader.using(
-                       [ 'test.module7' ],
-                       function () {
-                               assert.ok( false, 'Success fired despite missing dependency' );
-                               assert.ok( true, 'QUnit expected() count dummy' );
-                       },
-                       function ( e, dependencies ) {
-                               assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
-                               assert.deepEqual( dependencies, [ 'test.module7' ], 'Error callback called with module test.module7' );
-                       }
-               );
-               mw.loader.using(
-                       [ 'test.module9' ],
-                       function () {
-                               assert.ok( false, 'Success fired despite missing dependency' );
-                               assert.ok( true, 'QUnit expected() count dummy' );
-                       },
-                       function ( e, dependencies ) {
-                               assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
-                               dependencies.sort();
-                               assert.deepEqual(
-                                       dependencies,
-                                       [ 'test.module7', 'test.module8', 'test.module9' ],
-                                       'Error callback called with all three modules as dependencies'
-                               );
-                       }
-               );
-       } );
-
-       QUnit.test( 'mw.loader dependency handling', 5, function ( assert ) {
-               var done = assert.async();
-               mw.loader.register( [
-                       // [module, version, dependencies, group, source]
-                       [ 'testMissing', '1', [], null, 'testloader' ],
-                       [ 'testUsesMissing', '1', [ 'testMissing' ], null, 'testloader' ],
-                       [ 'testUsesNestedMissing', '1', [ 'testUsesMissing' ], null, 'testloader' ]
-               ] );
-
-               function verifyModuleStates() {
-                       assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' );
-                       assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' );
-                       assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' );
-               }
-
-               mw.loader.using( [ 'testUsesNestedMissing' ],
-                       function () {
-                               assert.ok( false, 'Error handler should be invoked.' );
-                               assert.ok( true ); // Dummy to reach QUnit expect()
-
-                               verifyModuleStates();
-
-                               done();
-                       },
-                       function ( e, badmodules ) {
-                               assert.ok( true, 'Error handler should be invoked.' );
-                               // As soon as server spits out state('testMissing', 'missing');
-                               // it will bubble up and trigger the error callback.
-                               // Therefor the badmodules array is not testUsesMissing or testUsesNestedMissing.
-                               assert.deepEqual( badmodules, [ 'testMissing' ], 'Bad modules as expected.' );
-
-                               verifyModuleStates();
-
-                               done();
-                       }
-               );
-       } );
-
-       QUnit.test( 'mw.loader skin-function handling', 5, function ( assert ) {
-               mw.loader.register( [
-                       // [module, version, dependencies, group, source, skip]
-                       [ 'testSkipped', '1', [], null, 'testloader', 'return true;' ],
-                       [ 'testNotSkipped', '1', [], null, 'testloader', 'return false;' ],
-                       [ 'testUsesSkippable', '1', [ 'testSkipped', 'testNotSkipped' ], null, 'testloader' ]
-               ] );
-
-               function verifyModuleStates() {
-                       assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Module is ready when skipped' );
-                       assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Module is ready when not skipped but loaded' );
-                       assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Module is ready when skippable dependencies are ready' );
-               }
-
-               return mw.loader.using( [ 'testUsesSkippable' ],
-                       function () {
-                               assert.ok( true, 'Success handler should be invoked.' );
-                               assert.ok( true ); // Dummy to match error handler and reach QUnit expect()
-
-                               verifyModuleStates();
-                       },
-                       function ( e, badmodules ) {
-                               assert.ok( false, 'Error handler should not be invoked.' );
-                               assert.deepEqual( badmodules, [], 'Bad modules as expected.' );
-
-                               verifyModuleStates();
-                       }
-               );
-       } );
-
-       QUnit.asyncTest( 'mw.loader( "//protocol-relative" ) (bug 30825)', 2, function ( assert ) {
-               // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
-               // Test is for regressions!
-
-               // Forge a URL to the test callback script
-               var target = QUnit.fixurl(
-                       mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
-               );
-
-               // Confirm that mw.loader.load() works with protocol-relative URLs
-               target = target.replace( /https?:/, '' );
-
-               assert.equal( target.slice( 0, 2 ), '//',
-                       'URL must be relative to test relative URLs!'
-               );
-
-               // Async!
-               // The target calls QUnit.start
-               mw.loader.load( target );
-       } );
-
-       QUnit.asyncTest( 'mw.loader( "/absolute-path" )', 2, function ( assert ) {
-               // Forge a URL to the test callback script
-               var target = QUnit.fixurl(
-                       mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
-               );
-
-               // Confirm that mw.loader.load() works with absolute-paths (relative to current hostname)
-               assert.equal( target.slice( 0, 1 ), '/', 'URL is relative to document root' );
-
-               // Async!
-               // The target calls QUnit.start
-               mw.loader.load( target );
-       } );
-
-       QUnit.test( 'mw.loader() executing race (T112232)', 2, function ( assert ) {
-               var done = false;
-
-               // The red herring schedules its CSS buffer first. In T112232, a bug in the
-               // state machine would cause the job for testRaceLoadMe to run with an earlier job.
-               mw.loader.implement(
-                       'testRaceRedHerring',
-                       function () {},
-                       { css: [ '.mw-testRaceRedHerring {}' ] }
-               );
-               mw.loader.implement(
-                       'testRaceLoadMe',
-                       function () {
-                               done = true;
-                       },
-                       { css: [ '.mw-testRaceLoadMe { float: left; }' ] }
-               );
-
-               mw.loader.load( [ 'testRaceRedHerring', 'testRaceLoadMe' ] );
-               return mw.loader.using( 'testRaceLoadMe', function () {
-                       assert.strictEqual( done, true, 'script ran' );
-                       assert.strictEqual( mw.loader.getState( 'testRaceLoadMe' ), 'ready', 'state' );
-               } );
-       } );
-
        QUnit.test( 'mw.hook', 13, function ( assert ) {
                var hook, add, fire, chars, callback;
 
                );
        } );
 
-       QUnit.test( 'mw.loader require()', 6, function ( assert ) {
-               mw.loader.register( [
-                       [ 'test.require1', '0' ],
-                       [ 'test.require2', '0' ],
-                       [ 'test.require3', '0' ],
-                       [ 'test.require4', '0', [ 'test.require3' ] ]
-               ] );
-               mw.loader.implement( 'test.require1', function () {} );
-               mw.loader.implement( 'test.require2', function ( $, jQuery, require, module ) {
-                       module.exports = 1;
-               } );
-               mw.loader.implement( 'test.require3', function ( $, jQuery, require, module ) {
-                       module.exports = function () {
-                               return 'hello world';
-                       };
-               } );
-               mw.loader.implement( 'test.require4', function ( $, jQuery, require, module ) {
-                       var other = require( 'test.require3' );
-                       module.exports = {
-                               pizza: function () {
-                                       return other();
-                               }
-                       };
-               } );
-               return mw.loader.using( [ 'test.require1', 'test.require2', 'test.require3', 'test.require4' ] )
-               .then( function ( require ) {
-                       var module1, module2, module3, module4;
-
-                       module1 = require( 'test.require1' );
-                       module2 = require( 'test.require2' );
-                       module3 = require( 'test.require3' );
-                       module4 = require( 'test.require4' );
-
-                       assert.strictEqual( typeof module1, 'object', 'export of module with no export' );
-                       assert.strictEqual( module2, 1, 'export a number' );
-                       assert.strictEqual( module3(), 'hello world', 'export a function' );
-                       assert.strictEqual( typeof module4.pizza, 'function', 'export an object' );
-                       assert.strictEqual( module4.pizza(), 'hello world', 'module can require other modules' );
-
-                       assert.throws( function () {
-                               require( '_badmodule' );
-                       }, /is not loaded/, 'Requesting non-existent modules throws error.' );
-               } );
-       } );
-
-       QUnit.test( 'mw.loader require() in debug mode', 1, function ( assert ) {
-               var path = mw.config.get( 'wgScriptPath' );
-               mw.loader.register( [
-                       [ 'test.require.define', '0' ],
-                       [ 'test.require.callback', '0', [ 'test.require.define' ] ]
-               ] );
-               mw.loader.implement( 'test.require.callback', [ QUnit.fixurl( path + '/tests/qunit/data/requireCallMwLoaderTestCallback.js' ) ] );
-               mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
-
-               return mw.loader.using( 'test.require.callback' ).then( function ( require ) {
-                       var exported = require( 'test.require.callback' );
-                       assert.strictEqual( exported, 'Require worked.Define worked.',
-                               'module.exports worked in debug mode' );
-               }, function () {
-                       assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
-               } );
-       } );
-
-}( mediaWiki, jQuery ) );
+}( mediaWiki ) );