Merge "Migrate PurgeJobUtils callback to AutoCommitUpdate"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 17 Apr 2018 23:12:44 +0000 (23:12 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 17 Apr 2018 23:12:44 +0000 (23:12 +0000)
58 files changed:
RELEASE-NOTES-1.31
RELEASE-NOTES-1.32 [new file with mode: 0644]
StartProfiler.sample [deleted file]
autoload.php
includes/DefaultSettings.php
includes/PHPVersionCheck.php
includes/Setup.php
includes/WebStart.php
includes/api/i18n/es.json
includes/api/i18n/pt.json
includes/api/i18n/sv.json
includes/deferred/TransactionRoundDefiningUpdate.php
includes/installer/Installer.php
includes/installer/WebInstaller.php
includes/installer/WebInstallerOptions.php
includes/installer/i18n/en.json
includes/installer/i18n/pt.json
includes/installer/i18n/qqq.json
includes/jobqueue/Job.php
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/RecentChangesUpdateJob.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/rdbms/loadmonitor/LoadMonitor.php
includes/registration/ExtensionDependencyError.php [new file with mode: 0644]
includes/registration/ExtensionRegistry.php
includes/registration/VersionChecker.php
includes/resourceloader/ResourceLoaderModule.php
includes/specials/SpecialStatistics.php
languages/i18n/be-tarask.json
languages/i18n/bn.json
languages/i18n/cs.json
languages/i18n/diq.json
languages/i18n/en.json
languages/i18n/he.json
languages/i18n/ko.json
languages/i18n/li.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/sv.json
languages/i18n/szl.json
languages/i18n/th.json
languages/i18n/tr.json
languages/i18n/vec.json
mw-config/config.js
resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js
resources/src/mediawiki.special/mediawiki.special.upload.js
resources/src/mediawiki.widgets/mw.widgets.UsersMultiselectWidget.js
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
tests/phpunit/autoload.ide.php
tests/phpunit/includes/db/LoadBalancerTest.php
tests/phpunit/includes/libs/CSSMinTest.php
tests/phpunit/includes/registration/VersionCheckerTest.php
tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php
tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js

index 3de0402..21b29cb 100644 (file)
@@ -257,6 +257,8 @@ changes to languages because of Phabricator reports.
   $wgValidateAllHtml configuration option is removed and will be ignored.
 * Execution of external programs using MediaWiki\Shell\Command now applies RESTRICT_DEFAULT
   Firejail restriction by default.
+* The ResourceLoaderModule::getHashMtime() and ::getDefinitionMtime() methods,
+  deprecated in 1.26, were removed.
 
 === Deprecations in 1.31 ===
 * The Revision class was deprecated in favor of RevisionStore, BlobStore, and
diff --git a/RELEASE-NOTES-1.32 b/RELEASE-NOTES-1.32
new file mode 100644 (file)
index 0000000..de41aa4
--- /dev/null
@@ -0,0 +1,102 @@
+== MediaWiki 1.32 ==
+
+THIS IS NOT A RELEASE YET
+
+MediaWiki 1.32 is an alpha-quality branch and is not recommended for use in
+production.
+
+=== Configuration changes in 1.32 ===
+* …
+
+=== New features in 1.32 ===
+* …
+
+=== External library changes in 1.32 ===
+* …
+
+==== Upgraded external libraries ====
+* …
+
+==== New external libraries ====
+* …
+
+==== Removed and replaced external libraries ====
+* …
+
+=== Bug fixes in 1.32 ===
+* …
+
+=== Action API changes in 1.32 ===
+* …
+
+=== Action API internal changes in 1.32 ===
+* …
+
+=== Languages updated in 1.32 ===
+MediaWiki supports over 350 languages. Many localisations are updated
+regularly. Below only new and removed languages are listed, as well as
+changes to languages because of Phabricator reports.
+
+* …
+
+=== Breaking changes in 1.32 ===
+* $wgRequestTime (deprecated in 1.25) was removed.
+  Use $_SERVER['REQUEST_TIME_FLOAT'] or WebRequest::getElapsedTime() instead.
+
+=== Deprecations in 1.32 ===
+* Use of a StartProfiler.php file is deprecated in favour of placing
+  configuration in LocalSettings.php.
+
+=== Other changes in 1.32 ===
+* …
+
+== Compatibility ==
+MediaWiki 1.32 requires PHP 5.5.9 or later. Although HHVM 3.18.5 or later is supported,
+it is generally advised to use PHP 5.5.9 or later for long term support.
+
+MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used,
+but support for them is somewhat less mature. There is experimental support for
+Oracle and Microsoft SQL Server.
+
+The supported versions are:
+
+* MySQL 5.0.3 or later
+* PostgreSQL 9.2 or later
+* SQLite 3.3.7 or later
+* Oracle 9.0.1 or later
+* Microsoft SQL Server 2005 (9.00.1399)
+
+== Upgrading ==
+1.32 has several database changes since 1.31, and will not work without schema
+updates. Note that due to changes to some very large tables like the revision
+table, the schema update may take quite long (minutes on a medium sized site,
+many hours on a large site).
+
+Don't forget to always back up your database before upgrading!
+
+See the file UPGRADE for more detailed upgrade instructions, including
+important information when upgrading from versions prior to 1.11.
+
+For notes on 1.31.x and older releases, see HISTORY.
+
+== Online documentation ==
+Documentation for both end-users and site administrators is available on
+MediaWiki.org, and is covered under the GNU Free Documentation License (except
+for pages that explicitly state that their contents are in the public domain):
+
+       https://www.mediawiki.org/wiki/Special:MyLanguage/Documentation
+
+== Mailing list ==
+A mailing list is available for MediaWiki user support and discussion:
+
+       https://lists.wikimedia.org/mailman/listinfo/mediawiki-l
+
+A low-traffic announcements-only list is also available:
+
+       https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce
+
+It's highly recommended that you sign up for one of these lists if you're
+going to run a public MediaWiki, so you can be notified of security fixes.
+
+== IRC help ==
+There's usually someone online in #mediawiki on irc.freenode.net.
diff --git a/StartProfiler.sample b/StartProfiler.sample
deleted file mode 100644 (file)
index bdf2139..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-/**
- * To use a profiler, copy this file to StartProfiler.php and add:
- *  $wgProfiler['class'] = 'ProfilerXhprof';
- *
- * For output, set the 'output' key to an array of class names, one for each
- * output type you want the profiler to generate. For example:
- *  $wgProfiler['output'] = array( 'ProfilerOutputText' );
- *
- * The output classes available to you by default are ProfilerOutputDb,
- * ProfilerOutputDump, ProfilerOutputStats, ProfilerOutputText, and
- * ProfilerOutputUdp.
- *
- * ProfilerOutputStats outputs profiling data as StatsD metrics. It expects
- * that you have set the $wgStatsdServer configuration variable to the host (or
- * host:port) of your statsd server.
- *
- * ProfilerOutputText will output profiling data in the page body as a comment.
- * You can make the profiling data in HTML render as part of the page content
- * by setting the 'visible' configuration flag:
- *  $wgProfiler['visible'] = true;
- *
- * 'ProfilerOutputDb' expects a database table that can be created by applying
- * maintenance/archives/patch-profiling.sql to your database.
- *
- * 'ProfilerOutputDump' expects a $wgProfiler['outputDir'] telling it where to
- * write dump files. The files produced are compatible with the XHProf gui.
- * For a rudimentary sampling profiler:
- *   $wgProfiler['class'] = 'ProfilerXhprof';
- *   $wgProfiler['output'] = array( 'ProfilerOutputDb' );
- *   $wgProfiler['sampling'] = 50; // one every 50 requests
- * This will use ProfilerStub for non-sampled cases.
- *
- * For performance, the profiler is always disabled for CLI scripts as they
- * could be long running and the data would accumulate. Use the '--profiler'
- * parameter of maintenance scripts to override this.
- */
index fc610cf..0e1b30f 100644 (file)
@@ -456,6 +456,7 @@ $wgAutoloadLocalClasses = [
        'ExplodeIterator' => __DIR__ . '/includes/libs/ExplodeIterator.php',
        'ExportProgressFilter' => __DIR__ . '/includes/export/ExportProgressFilter.php',
        'ExportSites' => __DIR__ . '/maintenance/exportSites.php',
+       'ExtensionDependencyError' => __DIR__ . '/includes/registration/ExtensionDependencyError.php',
        'ExtensionJsonValidationError' => __DIR__ . '/includes/registration/ExtensionJsonValidationError.php',
        'ExtensionJsonValidator' => __DIR__ . '/includes/registration/ExtensionJsonValidator.php',
        'ExtensionLanguages' => __DIR__ . '/maintenance/language/languages.inc',
index c12f2f2..6d7962c 100644 (file)
@@ -71,7 +71,7 @@ $wgConfigRegistry = [
  * MediaWiki version number
  * @since 1.2
  */
-$wgVersion = '1.31.0-alpha';
+$wgVersion = '1.32.0-alpha';
 
 /**
  * Name of the site. It must be changed in LocalSettings.php
@@ -6061,7 +6061,7 @@ $wgUseTeX = false;
 /************************************************************************//**
  * @name   Profiling, testing and debugging
  *
- * To enable profiling, edit StartProfiler.php
+ * See $wgProfiler for how to enable profiling.
  *
  * @{
  */
@@ -6306,6 +6306,66 @@ $wgDevelopmentWarnings = false;
  */
 $wgDeprecationReleaseLimit = false;
 
+/**
+ * Profiler configuration.
+ *
+ * To use a profiler, set $wgProfiler in LocalSetings.php.
+ * For backwards-compatibility, it is also allowed to set the variable from
+ * a separate file called StartProfiler.php, which MediaWiki will include.
+ *
+ * Example:
+ *
+ * @code
+ *  $wgProfiler['class'] = ProfilerXhprof::class;
+ * @endcode
+ *
+ * For output, set the 'output' key to an array of class names, one for each
+ * output type you want the profiler to generate. For example:
+ *
+ * @code
+ *  $wgProfiler['output'] = [ ProfilerOutputText::class ];
+ * @endcode
+ *
+ * The output classes available to you by default are ProfilerOutputDb,
+ * ProfilerOutputDump, ProfilerOutputStats, ProfilerOutputText, and
+ * ProfilerOutputUdp.
+ *
+ * ProfilerOutputStats outputs profiling data as StatsD metrics. It expects
+ * that you have set the $wgStatsdServer configuration variable to the host (or
+ * host:port) of your statsd server.
+ *
+ * ProfilerOutputText will output profiling data in the page body as a comment.
+ * You can make the profiling data in HTML render as part of the page content
+ * by setting the 'visible' configuration flag:
+ *
+ * @code
+ *  $wgProfiler['visible'] = true;
+ * @endcode
+ *
+ * 'ProfilerOutputDb' expects a database table that can be created by applying
+ * maintenance/archives/patch-profiling.sql to your database.
+ *
+ * 'ProfilerOutputDump' expects a $wgProfiler['outputDir'] telling it where to
+ * write dump files. The files produced are compatible with the XHProf gui.
+ * For a rudimentary sampling profiler:
+ *
+ * @code
+ *   $wgProfiler['class'] = 'ProfilerXhprof';
+ *   $wgProfiler['output'] = array( 'ProfilerOutputDb' );
+ *   $wgProfiler['sampling'] = 50; // one every 50 requests
+ * @endcode
+ *
+ * When using the built-in `sampling` option, the `class` will changed to
+ * ProfilerStub for non-sampled cases.
+ *
+ * For performance, the profiler is always disabled for CLI scripts as they
+ * could be long running and the data would accumulate. Use the '--profiler'
+ * parameter of maintenance scripts to override this.
+ *
+ * @since 1.17.0
+ */
+$wgProfiler = [];
+
 /**
  * Only record profiling info for pages that took longer than this
  * @deprecated since 1.25: set $wgProfiler['threshold'] instead.
index 37d4632..7c28b39 100644 (file)
@@ -27,7 +27,7 @@
  */
 class PHPVersionCheck {
        /* @var string The number of the MediaWiki version used */
-       var $mwVersion = '1.31';
+       var $mwVersion = '1.32';
        var $functionsExtensionsMapping = array(
                'mb_substr'   => 'mbstring',
                'utf8_encode' => 'xml',
index cc6915a..6513cdd 100644 (file)
@@ -37,12 +37,6 @@ if ( !defined( 'MEDIAWIKI' ) ) {
  * Pre-config setup: Before loading LocalSettings.php
  */
 
-// Get profiler configuraton
-$wgProfiler = [];
-if ( file_exists( "$IP/StartProfiler.php" ) ) {
-       require "$IP/StartProfiler.php";
-}
-
 // Start the autoloader, so that extensions can derive classes from core files
 require_once "$IP/includes/AutoLoader.php";
 
@@ -85,6 +79,11 @@ MediaWiki\HeaderCallback::register();
  * Load LocalSettings.php
  */
 
+if ( is_readable( "$IP/StartProfiler.php" ) ) {
+       // @deprecated since 1.32: Use LocalSettings.php instead.
+       require "$IP/StartProfiler.php";
+}
+
 if ( defined( 'MW_CONFIG_CALLBACK' ) ) {
        call_user_func( MW_CONFIG_CALLBACK );
 } else {
index c9aecce..b24ff7a 100644 (file)
@@ -34,15 +34,6 @@ if ( ini_get( 'mbstring.func_overload' ) ) {
 # points and when $wgOut gets disabled or overridden.
 header( 'X-Content-Type-Options: nosniff' );
 
-/**
- * @var float Request start time as fractional seconds since epoch
- * @deprecated since 1.25; use $_SERVER['REQUEST_TIME_FLOAT'] or
- *   WebRequest::getElapsedTime() instead.
- */
-$wgRequestTime = $_SERVER['REQUEST_TIME_FLOAT'];
-
-unset( $IP );
-
 # Valid web server entry point, enable includes.
 # Please don't move this line to includes/Defines.php. This line essentially
 # defines a valid entry point. If you put it in includes/Defines.php, then
index 0a17df2..0816ed7 100644 (file)
        "apierror-promised-nonwrite-api": "La cabecera HTTP <code>Promise-Non-Write-API-Action</code> no se puede enviar a módulos de la API en modo escritura.",
        "apierror-protect-invalidaction": "Tipo de protección «$1» no válido.",
        "apierror-protect-invalidlevel": "Nivel de protección «$1» no válido.",
+       "apierror-ratelimited": "Has excedido tu límite de frecuencia. Aguarda unos minutos y vuelve a intentarlo.",
        "apierror-readapidenied": "Necesitas permiso de lectura para utilizar este módulo.",
        "apierror-readonly": "El wiki está actualmente en modo de solo lectura.",
        "apierror-reauthenticate": "No te has autentificado recientemente en esta sesión. Por favor, vuelve a autentificarte.",
index 6288830..2a81d29 100644 (file)
        "apihelp-query+alllinks-example-B": "Listar os títulos para os quais existem ligações, incluindo títulos em falta, com os identificadores das páginas que contêm as respetivas ligações, começando pela letra <kbd>B</kbd>.",
        "apihelp-query+alllinks-example-unique": "Listar os títulos únicos para os quais existem hiperligações.",
        "apihelp-query+alllinks-example-unique-generator": "Obtém todos os títulos para os quais existem hiperligações, marcando aqueles em falta.",
-       "apihelp-query+alllinks-example-generator": "Obtém as páginas que contêm as ligações.",
+       "apihelp-query+alllinks-example-generator": "Obtém as páginas que contêm as hiperligações.",
        "apihelp-query+allmessages-summary": "Devolver as mensagens deste sítio.",
        "apihelp-query+allmessages-param-messages": "Mensagens a serem produzidas no resultado. <kbd>*</kbd> (o valor por omissão) significa todas as mensagens.",
        "apihelp-query+allmessages-param-prop": "As propriedades a serem obtidas:",
        "apihelp-query+imageusage-param-dir": "A direção de listagem.",
        "apihelp-query+imageusage-param-filterredir": "Como filtrar redirecionamentos. Se definido como <kbd>nonredirects</kbd> quando <var>$1redirect</var> está ativado, isto só é aplicado ao segundo nível.",
        "apihelp-query+imageusage-param-limit": "O número total de páginas a serem devolvidas. Se <var>$1redirect</var> estiver ativado, o nível aplica-se a cada nível em separado (o que significa que até 2 * <var>$1limit</var> resultados podem ser devolvidos).",
-       "apihelp-query+imageusage-param-redirect": "Se a página que contém a ligação é um redirecionamento, procurar também todas as páginas que contêm ligações para esse redirecionamento. O limite máximo é reduzido para metade.",
+       "apihelp-query+imageusage-param-redirect": "Se a página que contém a hiperligação é um redirecionamento, procurar também todas as páginas que contêm hiperligações para esse redirecionamento. O limite máximo é reduzido para metade.",
        "apihelp-query+imageusage-example-simple": "Mostrar as páginas que usam [[:File:Albert Einstein Head.jpg]].",
        "apihelp-query+imageusage-example-generator": "Obter informações sobre as páginas que usam o ficheiro [[:File:Albert Einstein Head.jpg]].",
        "apihelp-query+info-summary": "Obter a informação básica da página.",
index ebb0b45..5c0a12e 100644 (file)
        "apihelp-query+imageusage-example-simple": "Visa sidor med hjälp av [[:File:Albert Einstein Head.jpg]].",
        "apihelp-query+imageusage-example-generator": "Hämta information om sidor med hjälp av [[:File:Albert Einstein Head.jpg]].",
        "apihelp-query+info-summary": "Få grundläggande sidinformation.",
+       "apihelp-query+info-paramvalue-prop-varianttitles": "Ger visningstiteln i alla variationer på webbplatsens innehållsspråk.",
        "apihelp-query+iwbacklinks-param-limit": "Hur många sidor att returnera totalt.",
        "apihelp-query+iwbacklinks-param-dir": "Riktningen att lista mot.",
        "apihelp-query+iwlinks-param-dir": "Riktningen att lista mot.",
index 65baec5..a32d4a0 100644 (file)
@@ -1,8 +1,7 @@
 <?php
 
 /**
- * Deferrable update for closure/callback updates that need LBFactory and Database
- * to be outside any active transaction round.
+ * Deferrable update that must run outside of any explicit LBFactory transaction round
  *
  * @since 1.31
  */
index 94a5a5a..1efe5d6 100644 (file)
@@ -1301,7 +1301,15 @@ abstract class Installer {
                        if ( !is_dir( "$extDir/$file" ) ) {
                                continue;
                        }
-                       if ( file_exists( "$extDir/$file/$jsonFile" ) || file_exists( "$extDir/$file/$file.php" ) ) {
+                       $fullJsonFile = "$extDir/$file/$jsonFile";
+                       $isJson = file_exists( $fullJsonFile );
+                       $isPhp = false;
+                       if ( !$isJson ) {
+                               // Only fallback to PHP file if JSON doesn't exist
+                               $fullPhpFile = "$extDir/$file/$file.php";
+                               $isPhp = file_exists( $fullPhpFile );
+                       }
+                       if ( $isJson || $isPhp ) {
                                // Extension exists. Now see if there are screenshots
                                $exts[$file] = [];
                                if ( is_dir( "$extDir/$file/screenshots" ) ) {
@@ -1312,6 +1320,13 @@ abstract class Installer {
 
                                }
                        }
+                       if ( $isJson ) {
+                               $info = $this->readExtension( $fullJsonFile );
+                               if ( $info === false ) {
+                                       continue;
+                               }
+                               $exts[$file] += $info;
+                       }
                }
                closedir( $dh );
                uksort( $exts, 'strnatcasecmp' );
@@ -1319,6 +1334,82 @@ abstract class Installer {
                return $exts;
        }
 
+       /**
+        * @param string $fullJsonFile
+        * @param array $extDeps
+        * @param array $skinDeps
+        *
+        * @return array|bool False if this extension can't be loaded
+        */
+       private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
+               $load = [
+                       $fullJsonFile => 1
+               ];
+               if ( $extDeps ) {
+                       $extDir = $this->getVar( 'IP' ) . '/extensions';
+                       foreach ( $extDeps as $dep ) {
+                               $fname = "$extDir/$dep/extension.json";
+                               if ( !file_exists( $fname ) ) {
+                                       return false;
+                               }
+                               $load[$fname] = 1;
+                       }
+               }
+               if ( $skinDeps ) {
+                       $skinDir = $this->getVar( 'IP' ) . '/skins';
+                       foreach ( $skinDeps as $dep ) {
+                               $fname = "$skinDir/$dep/skin.json";
+                               if ( !file_exists( $fname ) ) {
+                                       return false;
+                               }
+                               $load[$fname] = 1;
+                       }
+               }
+               $registry = new ExtensionRegistry();
+               try {
+                       $info = $registry->readFromQueue( $load );
+               } catch ( ExtensionDependencyError $e ) {
+                       if ( $e->incompatibleCore || $e->incompatibleSkins
+                               || $e->incompatibleExtensions
+                       ) {
+                               // If something is incompatible with a dependency, we have no real
+                               // option besides skipping it
+                               return false;
+                       } elseif ( $e->missingExtensions || $e->missingSkins ) {
+                               // There's an extension missing in the dependency tree,
+                               // so add those to the dependency list and try again
+                               return $this->readExtension(
+                                       $fullJsonFile,
+                                       array_merge( $extDeps, $e->missingExtensions ),
+                                       array_merge( $skinDeps, $e->missingSkins )
+                               );
+                       }
+                       // Some other kind of dependency error?
+                       return false;
+               }
+               $ret = [];
+               // The order of credits will be the order of $load,
+               // so the first extension is the one we want to load,
+               // everything else is a dependency
+               $i = 0;
+               foreach ( $info['credits'] as $name => $credit ) {
+                       $i++;
+                       if ( $i == 1 ) {
+                               // Extension we want to load
+                               continue;
+                       }
+                       $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
+                       $ret['requires'][$type][] = $credit['name'];
+               }
+               $credits = array_values( $info['credits'] )[0];
+               if ( isset( $credits['url'] ) ) {
+                       $ret['url'] = $credits['url'];
+               }
+               $ret['type'] = $credits['type'];
+
+               return $ret;
+       }
+
        /**
         * Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
         * but will fall back to another if the default skin is missing and some other one is present
index 9d7e051..8fb9807 100644 (file)
@@ -915,6 +915,7 @@ class WebInstaller extends Installer {
         *    Parameters are:
         *      var:         The variable to be configured (required)
         *      label:       The message name for the label (required)
+        *      labelAttribs:Additional attributes for the label element (optional)
         *      attribs:     Additional attributes for the input element (optional)
         *      controlName: The name for the input element (optional)
         *      value:       The current value of the variable (optional)
@@ -937,6 +938,9 @@ class WebInstaller extends Installer {
                if ( !isset( $params['help'] ) ) {
                        $params['help'] = "";
                }
+               if ( !isset( $params['labelAttribs'] ) ) {
+                       $params['labelAttribs'] = [];
+               }
                if ( isset( $params['rawtext'] ) ) {
                        $labelText = $params['rawtext'];
                } else {
@@ -945,17 +949,19 @@ class WebInstaller extends Installer {
 
                return "<div class=\"config-input-check\">\n" .
                        $params['help'] .
-                       "<label>\n" .
-                       Xml::check(
-                               $params['controlName'],
-                               $params['value'],
-                               $params['attribs'] + [
-                                       'id' => $params['controlName'],
-                                       'tabindex' => $this->nextTabIndex(),
-                               ]
-                       ) .
-                       $labelText . "\n" .
-                       "</label>\n" .
+                       Html::rawElement(
+                               'label',
+                               $params['labelAttribs'],
+                               Xml::check(
+                                       $params['controlName'],
+                                       $params['value'],
+                                       $params['attribs'] + [
+                                               'id' => $params['controlName'],
+                                               'tabindex' => $this->nextTabIndex(),
+                                       ]
+                               ) .
+                               $labelText . "\n"
+                               ) .
                        "</div>\n";
        }
 
index 07378ab..c62eb65 100644 (file)
@@ -25,6 +25,8 @@ class WebInstallerOptions extends WebInstallerPage {
         * @return string|null
         */
        public function execute() {
+               global $wgLang;
+
                if ( $this->getVar( '_SkipOptional' ) == 'skip' ) {
                        $this->submitSkins();
                        return 'skip';
@@ -145,20 +147,90 @@ class WebInstallerOptions extends WebInstallerPage {
                $this->addHTML( $skinHtml );
 
                $extensions = $this->parent->findExtensions();
+               $dependencyMap = [];
 
                if ( $extensions ) {
                        $extHtml = $this->getFieldsetStart( 'config-extensions' );
 
+                       $extByType = [];
+                       $types = SpecialVersion::getExtensionTypes();
+                       // Sort by type first
                        foreach ( $extensions as $ext => $info ) {
-                               $extHtml .= $this->parent->getCheckBox( [
-                                       'var' => "ext-$ext",
-                                       'rawtext' => $ext,
-                               ] );
+                               if ( !isset( $info['type'] ) || !isset( $types[$info['type']] ) ) {
+                                       // We let extensions normally define custom types, but
+                                       // since we aren't loading extensions, we'll have to
+                                       // categorize them under other
+                                       $info['type'] = 'other';
+                               }
+                               $extByType[$info['type']][$ext] = $info;
+                       }
+
+                       foreach ( $types as $type => $message ) {
+                               if ( !isset( $extByType[$type] ) ) {
+                                       continue;
+                               }
+                               $extHtml .= Html::element( 'h2', [], $message );
+                               foreach ( $extByType[$type] as $ext => $info ) {
+                                       $urlText = '';
+                                       if ( isset( $info['url'] ) ) {
+                                               $urlText = ' ' . Html::element( 'a', [ 'href' => $info['url'] ], '(more information)' );
+                                       }
+                                       $attribs = [ 'data-name' => $ext ];
+                                       $labelAttribs = [];
+                                       $fullDepList = [];
+                                       if ( isset( $info['requires']['extensions'] ) ) {
+                                               $dependencyMap[$ext]['extensions'] = $info['requires']['extensions'];
+                                               $labelAttribs['class'] = 'mw-ext-with-dependencies';
+                                       }
+                                       if ( isset( $info['requires']['skins'] ) ) {
+                                               $dependencyMap[$ext]['skins'] = $info['requires']['skins'];
+                                               $labelAttribs['class'] = 'mw-ext-with-dependencies';
+                                       }
+                                       if ( isset( $dependencyMap[$ext] ) ) {
+                                               $links = [];
+                                               // For each dependency, link to the checkbox for each
+                                               // extension/skin that is required
+                                               if ( isset( $dependencyMap[$ext]['extensions'] ) ) {
+                                                       foreach ( $dependencyMap[$ext]['extensions'] as $name ) {
+                                                               $links[] = Html::element(
+                                                                       'a',
+                                                                       [ 'href' => "#config_ext-$name" ],
+                                                                       $name
+                                                               );
+                                                       }
+                                               }
+                                               if ( isset( $dependencyMap[$ext]['skins'] ) ) {
+                                                       foreach ( $dependencyMap[$ext]['skins'] as $name ) {
+                                                               $links[] = Html::element(
+                                                                       'a',
+                                                                       [ 'href' => "#config_skin-$name" ],
+                                                                       $name
+                                                               );
+                                                       }
+                                               }
+
+                                               $text = wfMessage( 'config-extensions-requires' )
+                                                       ->rawParams( $ext, $wgLang->commaList( $links ) )
+                                                       ->escaped();
+                                       } else {
+                                               $text = $ext;
+                                       }
+                                       $extHtml .= $this->parent->getCheckBox( [
+                                               'var' => "ext-$ext",
+                                               'rawtext' => $text,
+                                               'attribs' => $attribs,
+                                               'labelAttribs' => $labelAttribs,
+                                       ] );
+                               }
                        }
 
                        $extHtml .= $this->parent->getHelpBox( 'config-extensions-help' ) .
                                $this->getFieldsetEnd();
                        $this->addHTML( $extHtml );
+                       // Push the dependency map to the client side
+                       $this->addHTML( Html::inlineScript(
+                               'var extDependencyMap = ' . Xml::encodeJsVar( $dependencyMap )
+                       ) );
                }
 
                // Having / in paths in Windows looks funny :)
index f1b7080..d1a3b83 100644 (file)
        "config-extension-link": "Did you know that your wiki supports [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions]?\n\nYou can browse [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category] or the [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] to see the full list of extensions.",
        "config-skins-screenshots": "$1 (screenshots: $2)",
        "config-skins-screenshot": "$1 ($2)",
+       "config-extensions-requires": "$1 (requires $2)",
        "config-screenshot": "screenshot",
        "mainpagetext": "<strong>MediaWiki has been installed.</strong>",
        "mainpagedocfooter": "Consult the [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Learn how to combat spam on your wiki]"
index 433b567..21410c8 100644 (file)
        "config-install-extension-tables": "A criar as tabelas das extensões ativadas",
        "config-install-mainpage-failed": "Não foi possível inserir a página principal: $1",
        "config-install-done": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório de raiz da sua instalação (o mesmo diretório onde está o ficheiro index.php). Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando na hiperligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não o descarregar agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
-       "config-install-done-path": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório <code>$4</code>. Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando na ligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não fizer o descarregamento agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
+       "config-install-done-path": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório <code>$4</code>. Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando a hiperligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não fizer o descarregamento agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
        "config-install-success": "O MediaWiki foi instalado. Já pode visitar <$1$2> para ver a sua wiki.\nSe tiver dúvidas, veja a nossa lista de perguntas frequentes,\n<https://www.mediawiki.org/wiki/Manual:FAQ/pt>, ou utilize um dos fóruns de suporte indicados nessa página.",
        "config-download-localsettings": "Descarregar <code>LocalSettings.php</code>",
        "config-help": "ajuda",
index d82c74b..751b42a 100644 (file)
        "config-extension-link": "Shown on last page of installation to inform about possible extensions.\n{{Identical|Did you know}}",
        "config-skins-screenshots": "Radio button text, $1 is the skin name, and $2 is a list of links to screenshots of that skin",
        "config-skins-screenshot": "Radio button text, $1 is the skin name, and $2 is a link to a screenshot of that skin, where the link text is {{msg-mw|config-screenshot}}.",
+       "config-extensions-requires": "Radio button text, $1 is the extension name, and $2 are links to other extensions that this one requires.",
        "config-screenshot": "Link text for the link in {{msg-mw|config-skins-screenshot}}\n{{Identical|Screenshot}}",
        "mainpagetext": "Along with {{msg-mw|mainpagedocfooter}}, the text you will see on the Main Page when your wiki is installed.",
        "mainpagedocfooter": "Along with {{msg-mw|mainpagetext}}, the text you will see on the Main Page when your wiki is installed.\nThis might be a good place to put information about <nowiki>{{GRAMMAR:}}</nowiki>. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/fi]] for an example. For languages having grammatical distinctions and not having an appropriate <nowiki>{{GRAMMAR:}}</nowiki> software available, a suggestion to check and possibly amend the messages having <nowiki>{{SITENAME}}</nowiki> may be valuable. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/ksh]] for an example."
index 8508861..f9c416f 100644 (file)
@@ -50,6 +50,12 @@ abstract class Job implements IJobSpecification {
        /** @var callable[] */
        protected $teardownCallbacks = [];
 
+       /** @var int Bitfield of JOB_* class constants */
+       protected $executionFlags = 0;
+
+       /** @var int Job must not be wrapped in the usual explicit LBFactory transaction round */
+       const JOB_NO_EXPLICIT_TRX_ROUND = 1;
+
        /**
         * Run the job
         * @return bool Success
@@ -108,6 +114,15 @@ abstract class Job implements IJobSpecification {
                }
        }
 
+       /**
+        * @param int $flag JOB_* class constant
+        * @return bool
+        * @since 1.31
+        */
+       public function hasExecutionFlag( $flag ) {
+               return ( $this->executionFlags && $flag ) === $flag;
+       }
+
        /**
         * Batch-insert a group of jobs into the queue.
         * This will be wrapped in a transaction with a forced commit.
index fa7d605..977fbda 100644 (file)
@@ -290,7 +290,9 @@ class JobRunner implements LoggerAwareInterface {
                $jobStartTime = microtime( true );
                try {
                        $fnameTrxOwner = get_class( $job ) . '::run'; // give run() outer scope
-                       $lbFactory->beginMasterChanges( $fnameTrxOwner );
+                       if ( !$job->hasExecutionFlag( $job::JOB_NO_EXPLICIT_TRX_ROUND ) ) {
+                               $lbFactory->beginMasterChanges( $fnameTrxOwner );
+                       }
                        $status = $job->run();
                        $error = $job->getLastError();
                        $this->commitMasterChanges( $lbFactory, $job, $fnameTrxOwner );
index 77daca7..8f50828 100644 (file)
@@ -35,6 +35,7 @@ class RecentChangesUpdateJob extends Job {
                        throw new Exception( "Missing 'type' parameter." );
                }
 
+               $this->executionFlags |= self::JOB_NO_EXPLICIT_TRX_ROUND;
                $this->removeDuplicates = true;
        }
 
@@ -127,124 +128,118 @@ class RecentChangesUpdateJob extends Job {
                $window = $wgActiveUserDays * 86400;
 
                $dbw = wfGetDB( DB_MASTER );
-               // 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 = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
-                               $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
-                               // Avoid disconnect/ping() cycle that makes locks fall off
-                               $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
-
-                               $lockKey = wfWikiID() . '-activeusers';
-                               if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) {
-                                       // Exclusive update (avoids duplicate entries)… it's usually fine to just drop out here,
-                                       // if the Job is already running.
-                                       return;
-                               }
+               $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
 
-                               $nowUnix = time();
-                               // Get the last-updated timestamp for the cache
-                               $cTime = $dbw->selectField( 'querycache_info',
-                                       'qci_timestamp',
-                                       [ 'qci_type' => 'activeusers' ]
-                               );
-                               $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
-
-                               // Pick the date range to fetch from. This is normally from the last
-                               // update to till the present time, but has a limited window for sanity.
-                               // If the window is limited, multiple runs are need to fully populate it.
-                               $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 );
-                               $eTimestamp = min( $sTimestamp + $window, $nowUnix );
-
-                               // Get all the users active since the last update
-                               $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
-                               $res = $dbw->select(
-                                       [ 'recentchanges' ] + $actorQuery['tables'],
-                                       [
-                                               'rc_user_text' => $actorQuery['fields']['rc_user_text'],
-                                               'lastedittime' => 'MAX(rc_timestamp)'
-                                       ],
-                                       [
-                                               $actorQuery['fields']['rc_user'] . ' > 0', // actual accounts
-                                               'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
-                                               'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
-                                               'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
-                                               'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
-                                       ],
-                                       __METHOD__,
-                                       [
-                                               'GROUP BY' => [ 'rc_user_text' ],
-                                               'ORDER BY' => 'NULL' // avoid filesort
-                                       ],
-                                       $actorQuery['joins']
-                               );
-                               $names = [];
-                               foreach ( $res as $row ) {
-                                       $names[$row->rc_user_text] = $row->lastedittime;
-                               }
+               $lockKey = wfWikiID() . '-activeusers';
+               if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) {
+                       // Exclusive update (avoids duplicate entries)… it's usually fine to just
+                       // drop out here, if the Job is already running.
+                       return;
+               }
 
-                               // Find which of the recently active users are already accounted for
-                               if ( count( $names ) ) {
-                                       $res = $dbw->select( 'querycachetwo',
-                                               [ 'user_name' => 'qcc_title' ],
-                                               [
-                                                       'qcc_type' => 'activeusers',
-                                                       'qcc_namespace' => NS_USER,
-                                                       'qcc_title' => array_keys( $names ),
-                                                       'qcc_value >= ' . $dbw->addQuotes( $nowUnix - $days * 86400 ), // TS_UNIX
-                                                ],
-                                               __METHOD__
-                                       );
-                                       // Note: In order for this to be actually consistent, we would need
-                                       // to update these rows with the new lastedittime.
-                                       foreach ( $res as $row ) {
-                                               unset( $names[$row->user_name] );
-                                       }
-                               }
+               // Long-running queries expected
+               $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
 
-                               // Insert the users that need to be added to the list
-                               if ( count( $names ) ) {
-                                       $newRows = [];
-                                       foreach ( $names as $name => $lastEditTime ) {
-                                               $newRows[] = [
-                                                       'qcc_type' => 'activeusers',
-                                                       'qcc_namespace' => NS_USER,
-                                                       'qcc_title' => $name,
-                                                       'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
-                                                       'qcc_namespacetwo' => 0, // unused
-                                                       'qcc_titletwo' => '' // unused
-                                               ];
-                                       }
-                                       foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
-                                               $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
-                                               $factory->commitAndWaitForReplication( __METHOD__, $ticket );
-                                       }
-                               }
+               $nowUnix = time();
+               // Get the last-updated timestamp for the cache
+               $cTime = $dbw->selectField( 'querycache_info',
+                       'qci_timestamp',
+                       [ 'qci_type' => 'activeusers' ]
+               );
+               $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
+
+               // Pick the date range to fetch from. This is normally from the last
+               // update to till the present time, but has a limited window for sanity.
+               // If the window is limited, multiple runs are need to fully populate it.
+               $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 );
+               $eTimestamp = min( $sTimestamp + $window, $nowUnix );
+
+               // Get all the users active since the last update
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+               $res = $dbw->select(
+                       [ 'recentchanges' ] + $actorQuery['tables'],
+                       [
+                               'rc_user_text' => $actorQuery['fields']['rc_user_text'],
+                               'lastedittime' => 'MAX(rc_timestamp)'
+                       ],
+                       [
+                               $actorQuery['fields']['rc_user'] . ' > 0', // actual accounts
+                               'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
+                               'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
+                               'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
+                               'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
+                       ],
+                       __METHOD__,
+                       [
+                               'GROUP BY' => [ 'rc_user_text' ],
+                               'ORDER BY' => 'NULL' // avoid filesort
+                       ],
+                       $actorQuery['joins']
+               );
+               $names = [];
+               foreach ( $res as $row ) {
+                       $names[$row->rc_user_text] = $row->lastedittime;
+               }
+
+               // Find which of the recently active users are already accounted for
+               if ( count( $names ) ) {
+                       $res = $dbw->select( 'querycachetwo',
+                               [ 'user_name' => 'qcc_title' ],
+                               [
+                                       'qcc_type' => 'activeusers',
+                                       'qcc_namespace' => NS_USER,
+                                       'qcc_title' => array_keys( $names ),
+                                       'qcc_value >= ' . $dbw->addQuotes( $nowUnix - $days * 86400 ), // TS_UNIX
+                                ],
+                               __METHOD__
+                       );
+                       // Note: In order for this to be actually consistent, we would need
+                       // to update these rows with the new lastedittime.
+                       foreach ( $res as $row ) {
+                               unset( $names[$row->user_name] );
+                       }
+               }
+
+               // Insert the users that need to be added to the list
+               if ( count( $names ) ) {
+                       $newRows = [];
+                       foreach ( $names as $name => $lastEditTime ) {
+                               $newRows[] = [
+                                       'qcc_type' => 'activeusers',
+                                       'qcc_namespace' => NS_USER,
+                                       'qcc_title' => $name,
+                                       'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
+                                       'qcc_namespacetwo' => 0, // unused
+                                       'qcc_titletwo' => '' // unused
+                               ];
+                       }
+                       foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
+                               $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
+                               $factory->commitAndWaitForReplication( __METHOD__, $ticket );
+                       }
+               }
+
+               // If a transaction was already started, it might have an old
+               // snapshot, so kludge the timestamp range back as needed.
+               $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() );
+
+               // Touch the data freshness timestamp
+               $dbw->replace( 'querycache_info',
+                       [ 'qci_type' ],
+                       [ 'qci_type' => 'activeusers',
+                               'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ) ], // not always $now
+                       __METHOD__
+               );
+
+               $dbw->unlock( $lockKey, __METHOD__ );
 
-                               // If a transaction was already started, it might have an old
-                               // snapshot, so kludge the timestamp range back as needed.
-                               $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() );
-
-                               // Touch the data freshness timestamp
-                               $dbw->replace( 'querycache_info',
-                                       [ 'qci_type' ],
-                                       [ 'qci_type' => 'activeusers',
-                                               'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ) ], // not always $now
-                                       __METHOD__
-                               );
-
-                               $dbw->unlock( $lockKey, __METHOD__ );
-
-                               // Rotate out users that have not edited in too long (according to old data set)
-                               $dbw->delete( 'querycachetwo',
-                                       [
-                                               'qcc_type' => 'activeusers',
-                                               'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX
-                                       ],
-                                       __METHOD__
-                               );
-                       },
+               // Rotate out users that have not edited in too long (according to old data set)
+               $dbw->delete( 'querycachetwo',
+                       [
+                               'qcc_type' => 'activeusers',
+                               'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX
+                       ],
                        __METHOD__
                );
        }
index a699b23..2f493c7 100644 (file)
@@ -167,10 +167,13 @@ interface ILoadBalancer {
        /**
         * Get any open connection to a given server index, local or foreign
         *
+        * Use CONN_TRX_AUTOCOMMIT to only look for connections opened with that flag
+        *
         * @param int $i Server index or DB_MASTER/DB_REPLICA
+        * @param int $flags Bitfield of CONN_* class constants
         * @return Database|bool False if no such connection is open
         */
-       public function getAnyOpenConnection( $i );
+       public function getAnyOpenConnection( $i, $flags = 0 );
 
        /**
         * Get a connection handle by server index
index 96ea949..01589ae 100644 (file)
@@ -369,7 +369,7 @@ class LoadBalancer implements ILoadBalancer {
                // Scale the configured load ratios according to each server's load and state
                $this->getLoadMonitor()->scaleLoads( $loads, $domain );
 
-               // Pick a server to use, accounting for weights, load, lag, and mWaitForPos
+               // Pick a server to use, accounting for weights, load, lag, and "waitForPos"
                list( $i, $laggedReplicaMode ) = $this->pickReaderIndex( $loads, $domain );
                if ( $i === false ) {
                        // Replica DB connection unsuccessful
@@ -379,7 +379,7 @@ class LoadBalancer implements ILoadBalancer {
                if ( $this->waitForPos && $i != $this->getWriterIndex() ) {
                        // Before any data queries are run, wait for the server to catch up to the
                        // specified position. This is used to improve session consistency. Note that
-                       // when LoadBalancer::waitFor() sets mWaitForPos, the waiting triggers here,
+                       // when LoadBalancer::waitFor() sets "waitForPos", the waiting triggers here,
                        // so update laggedReplicaMode as needed for consistency.
                        if ( !$this->doWait( $i ) ) {
                                $laggedReplicaMode = true;
@@ -424,7 +424,7 @@ class LoadBalancer implements ILoadBalancer {
                        } else {
                                $i = false;
                                if ( $this->waitForPos && $this->waitForPos->asOfTime() ) {
-                                       // ChronologyProtecter sets mWaitForPos for session consistency.
+                                       // ChronologyProtecter sets "waitForPos" for session consistency.
                                        // This triggers doWait() after connect, so it's especially good to
                                        // avoid lagged servers so as to avoid excessive delay in that method.
                                        $ago = microtime( true ) - $this->waitForPos->asOfTime();
@@ -566,16 +566,17 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * @param int $i
-        * @return IDatabase|bool
-        */
-       public function getAnyOpenConnection( $i ) {
+       public function getAnyOpenConnection( $i, $flags = 0 ) {
+               $autocommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
                foreach ( $this->conns as $connsByServer ) {
-                       if ( !empty( $connsByServer[$i] ) ) {
-                               /** @var IDatabase[] $serverConns */
-                               $serverConns = $connsByServer[$i];
-                               return reset( $serverConns );
+                       if ( !isset( $connsByServer[$i] ) ) {
+                               continue;
+                       }
+
+                       foreach ( $connsByServer[$i] as $conn ) {
+                               if ( !$autocommit || $conn->getLBInfo( 'autoCommitOnly' ) ) {
+                                       return $conn;
+                               }
                        }
                }
 
@@ -1139,7 +1140,7 @@ class LoadBalancer implements ILoadBalancer {
                                $context
                        );
 
-                       // If all servers were busy, mLastError will contain something sensible
+                       // If all servers were busy, "lastError" will contain something sensible
                        throw new DBConnectionError( null, $this->lastError );
                }
        }
index 3372839..c7f807a 100644 (file)
@@ -156,13 +156,14 @@ class LoadMonitor implements ILoadMonitor {
                                continue;
                        }
 
-                       $conn = $this->parent->getAnyOpenConnection( $i );
-                       if ( $conn && !$conn->trxLevel() ) {
-                               # Handles with open transactions are avoided since they might be subject
-                               # to REPEATABLE-READ snapshots, which could affect the lag estimate query.
+                       # Handles with open transactions are avoided since they might be subject
+                       # to REPEATABLE-READ snapshots, which could affect the lag estimate query.
+                       $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
+                       $conn = $this->parent->getAnyOpenConnection( $i, $flags );
+                       if ( $conn ) {
                                $close = false; // already open
                        } else {
-                               $conn = $this->parent->openConnection( $i, '' );
+                               $conn = $this->parent->openConnection( $i, ILoadBalancer::DOMAIN_ANY, $flags );
                                $close = true; // new connection
                        }
 
diff --git a/includes/registration/ExtensionDependencyError.php b/includes/registration/ExtensionDependencyError.php
new file mode 100644 (file)
index 0000000..d380d07
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+/**
+ * @since 1.31
+ */
+class ExtensionDependencyError extends Exception {
+
+       /**
+        * @var string[]
+        */
+       public $missingExtensions = [];
+
+       /**
+        * @var string[]
+        */
+       public $missingSkins = [];
+
+       /**
+        * @var string[]
+        */
+       public $incompatibleExtensions = [];
+
+       /**
+        * @var string[]
+        */
+       public $incompatibleSkins = [];
+
+       /**
+        * @var bool
+        */
+       public $incompatibleCore = false;
+
+       /**
+        * @param array $errors Each error has a 'msg' and 'type' key at minimum
+        */
+       public function __construct( array $errors ) {
+               $msg = '';
+               foreach ( $errors as $info ) {
+                       $msg .= $info['msg'] . "\n";
+                       switch ( $info['type'] ) {
+                               case 'incompatible-core':
+                                       $this->incompatibleCore = true;
+                                       break;
+                               case 'missing-skins':
+                                       $this->missingSkins[] = $info['missing'];
+                                       break;
+                               case 'missing-extensions':
+                                       $this->missingExtensions[] = $info['missing'];
+                                       break;
+                               case 'incompatible-skins':
+                                       $this->incompatibleSkins[] = $info['incompatible'];
+                                       break;
+                               case 'incompatible-extensions':
+                                       $this->incompatibleExtensions[] = $info['incompatible'];
+                                       break;
+                               // default: continue
+                       }
+               }
+
+               parent::__construct( $msg );
+       }
+
+}
index 1876645..b000dc1 100644 (file)
@@ -202,6 +202,7 @@ class ExtensionRegistry {
         * @param array $queue keys are filenames, values are ignored
         * @return array extracted info
         * @throws Exception
+        * @throws ExtensionDependencyError
         */
        public function readFromQueue( array $queue ) {
                global $wgVersion;
@@ -273,11 +274,7 @@ class ExtensionRegistry {
                );
 
                if ( $incompatible ) {
-                       if ( count( $incompatible ) === 1 ) {
-                               throw new Exception( $incompatible[0] );
-                       } else {
-                               throw new Exception( implode( "\n", $incompatible ) );
-                       }
+                       throw new ExtensionDependencyError( $incompatible );
                }
 
                // Need to set this so we can += to it later
index 02e3a7c..9c673bc 100644 (file)
@@ -110,13 +110,18 @@ class VersionChecker {
                                        case ExtensionRegistry::MEDIAWIKI_CORE:
                                                $mwError = $this->handleMediaWikiDependency( $values, $extension );
                                                if ( $mwError !== false ) {
-                                                       $errors[] = $mwError;
+                                                       $errors[] = [
+                                                               'msg' => $mwError,
+                                                               'type' => 'incompatible-core',
+                                                       ];
                                                }
                                                break;
                                        case 'extensions':
                                        case 'skin':
                                                foreach ( $values as $dependency => $constraint ) {
-                                                       $extError = $this->handleExtensionDependency( $dependency, $constraint, $extension );
+                                                       $extError = $this->handleExtensionDependency(
+                                                               $dependency, $constraint, $extension, $dependencyType
+                                                       );
                                                        if ( $extError !== false ) {
                                                                $errors[] = $extError;
                                                        }
@@ -164,12 +169,19 @@ class VersionChecker {
         * @param string $dependencyName The name of the dependency
         * @param string $constraint The required version constraint for this dependency
         * @param string $checkedExt The Extension, which depends on this dependency
-        * @return bool|string false for no errors, or a string message
+        * @param string $type Either 'extension' or 'skin'
+        * @return bool|array false for no errors, or an array of info
         */
-       private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt ) {
+       private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt,
+               $type
+       ) {
                // Check if the dependency is even installed
                if ( !isset( $this->loaded[$dependencyName] ) ) {
-                       return "{$checkedExt} requires {$dependencyName} to be installed.";
+                       return [
+                               'msg' => "{$checkedExt} requires {$dependencyName} to be installed.",
+                               'type' => "missing-$type",
+                               'missing' => $dependencyName,
+                       ];
                }
                // Check if the dependency has specified a version
                if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
@@ -180,8 +192,13 @@ class VersionChecker {
                                return false;
                        } else {
                                // Otherwise, mark it as incompatible.
-                               return "{$dependencyName} does not expose its version, but {$checkedExt}"
+                               $msg = "{$dependencyName} does not expose its version, but {$checkedExt}"
                                        . " requires: {$constraint}.";
+                               return [
+                                       'msg' => $msg,
+                                       'type' => "incompatible-$type",
+                                       'incompatible' => $checkedExt,
+                               ];
                        }
                } else {
                        // Try to get a constraint for the dependency version
@@ -193,16 +210,24 @@ class VersionChecker {
                        } catch ( UnexpectedValueException $e ) {
                                // Non-parsable version, output an error message that the version
                                // string is invalid
-                               return "$dependencyName does not have a valid version string.";
+                               return [
+                                       'msg' => "$dependencyName does not have a valid version string.",
+                                       'type' => 'invalid-version',
+                               ];
                        }
                        // Check if the constraint actually matches...
                        if (
                                !$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
                        ) {
-                               return "{$checkedExt} is not compatible with the current "
+                               $msg = "{$checkedExt} is not compatible with the current "
                                        . "installed version of {$dependencyName} "
                                        . "({$this->loaded[$dependencyName]['version']}), "
                                        . "it requires: " . $constraint . '.';
+                               return [
+                                       'msg' => $msg,
+                                       'type' => "incompatible-$type",
+                                       'incompatible' => $checkedExt,
+                               ];
                        }
                }
 
index 8bf7170..6d1529b 100644 (file)
@@ -937,41 +937,6 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
                return null;
        }
 
-       /**
-        * Back-compat dummy for old subclass implementations of getModifiedTime().
-        *
-        * This method used to use ObjectCache to track when a hash was first seen. That principle
-        * stems from a time that ResourceLoader could only identify module versions by timestamp.
-        * That is no longer the case. Use getDefinitionSummary() directly.
-        *
-        * @deprecated since 1.26 Superseded by getVersionHash()
-        * @param ResourceLoaderContext $context
-        * @return int UNIX timestamp
-        */
-       public function getHashMtime( ResourceLoaderContext $context ) {
-               if ( !is_string( $this->getModifiedHash( $context ) ) ) {
-                       return 1;
-               }
-               // Dummy that is > 1
-               return 2;
-       }
-
-       /**
-        * Back-compat dummy for old subclass implementations of getModifiedTime().
-        *
-        * @since 1.23
-        * @deprecated since 1.26 Superseded by getVersionHash()
-        * @param ResourceLoaderContext $context
-        * @return int UNIX timestamp
-        */
-       public function getDefinitionMtime( ResourceLoaderContext $context ) {
-               if ( $this->getDefinitionSummary( $context ) === null ) {
-                       return 1;
-               }
-               // Dummy that is > 1
-               return 2;
-       }
-
        /**
         * Check whether this module is known to be empty. If a child class
         * has an easy and cheap way to determine that this module is
index a60549b..146e6e7 100644 (file)
@@ -168,7 +168,11 @@ class SpecialStatistics extends SpecialPage {
                        Xml::tags( 'th', [ 'colspan' => '2' ],
                                $this->msg( 'statistics-header-users' )->parse() ) .
                        Xml::closeElement( 'tr' ) .
-                       $this->formatRow( $this->msg( 'statistics-users' )->parse(),
+                       $this->formatRow( $this->msg( 'statistics-users' )->parse() . ' ' .
+                               $this->getLinkRenderer()->makeKnownLink(
+                                       SpecialPage::getTitleFor( 'ListUsers' ),
+                                       $this->msg( 'listgrouprights-members' )->text()
+                               ),
                                $this->getLanguage()->formatNum( $this->users ),
                                [ 'class' => 'mw-statistics-users' ]
                        ) .
index c66fbc1..15b54ab 100644 (file)
        "rcfilters-filtergroup-reviewstatus": "Статус праверкі",
        "rcfilters-filter-reviewstatus-unpatrolled-description": "Рэдагаваньні, якія не былі пазначаныя як патруляваныя, уручную ці аўтаматычна.",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "Неправераныя",
+       "rcfilters-filter-reviewstatus-manual-description": "Рэдагаваньні, уручную пазначаныя як патруляваныя.",
        "rcfilters-filtergroup-significance": "Значэньне",
        "rcfilters-filter-minor-label": "Дробныя праўкі",
        "rcfilters-filter-minor-description": "Праўкі, якія аўтар пазначыў як дробныя.",
index da0544f..9385e48 100644 (file)
        "emailnotarget": "প্রাপকের ব্যবহারকারী নাম সঠিক নয় অথবা এই নামের ব্যবহারকারী নেই।",
        "emailtarget": "ব্যবহারকারী নাম অথবা প্রাপকের নাম প্রবেশ করান",
        "emailusername": "ব্যবহারকারী নাম:",
-       "emailusernamesubmit": "à¦\9cমা à¦¦à¦¾à¦\93",
+       "emailusernamesubmit": "à¦\9cমা à¦¦à¦¿à¦¨",
        "email-legend": "অন্য একজন {{SITENAME}} ব্যবহারকারীকে ই-মেইল পাঠান",
        "emailfrom": "প্রেরক:",
        "emailto": "প্রাপক:",
        "feedback-error4": "ত্রুটি: প্রতিক্রিয়ায় দেয়া শিরোনামে প্রকাশ অক্ষম",
        "feedback-message": "বার্তা:",
        "feedback-subject": "বিষয়:",
-       "feedback-submit": "à¦\9cমা à¦¦à¦¾à¦\93",
+       "feedback-submit": "à¦\9cমা à¦¦à¦¿à¦¨",
        "feedback-terms": "আমি বুঝেছি যে আমার প্রতিক্রিয়ার পাশাপাশি আমার যথাযথ ব্রাউজার এবং অপারেটিং সিস্টেম সংস্করণের তথ্যসহ ব্যবহারকারী এজেন্ট সর্বজনীনভাবে ভাগ করা হবে।",
        "feedback-termsofuse": "আমি ব্যবহারের শর্তাবলী অনুসারে প্রতিক্রিয়া জানাতে সম্মত।",
        "feedback-thanks": "ধন্যবাদ! আপনার প্রতিক্রিয়া \"[$2 $1]\" পাতায় পোস্ট করা হয়েছে।",
index 45c2c31..013b8e2 100644 (file)
        "uploadstash-bad-path": "Cesta neexistuje.",
        "uploadstash-bad-path-invalid": "Cesta není platná.",
        "uploadstash-bad-path-unknown-type": "Neznámý typ „$1“.",
-       "uploadstash-bad-path-bad-format": "Klíč \"$1\" není správně naformátovaný.",
+       "uploadstash-bad-path-bad-format": "Klíč „$1‟ nemá správný formát.",
        "uploadstash-file-not-found-no-thumb": "Nepodařilo se získat náhled.",
        "uploadstash-file-not-found-no-object": "Nepodařilo se vytvořit objekt lokálního souboru pro náhled.",
        "uploadstash-file-not-found-no-remote-thumb": "Načtení náhledu se nepodařilo: $1\nURL = $2",
index 74ee879..dab8908 100644 (file)
        "pool-errorunknown": "Xeta nêzanıtiye",
        "pool-servererror": "Amordoğa xızmeti ya istifade nëbena $1",
        "poolcounter-usage-error": "Xırab karyayış:$1",
-       "aboutsite": "Heqa {{SITENAME}} dı",
+       "aboutsite": "Heqa {{SITENAME}} de",
        "aboutpage": "Project:Heqa",
        "copyright": "Zerrekacı $1 bındı not biya.",
        "copyrightpage": "{{ns:project}}:Heqa telifi",
index 168ce07..350c348 100644 (file)
        "statistics-files": "Uploaded files",
        "statistics-edits": "Page edits since {{SITENAME}} was set up",
        "statistics-edits-average": "Average edits per page",
-       "statistics-users": "Registered [[Special:ListUsers|users]]",
+       "statistics-users": "Registered users",
        "statistics-users-active": "Active users",
        "statistics-users-active-desc": "Users who have performed an action in the last {{PLURAL:$1|day|$1 days}}",
        "statistics-footer": "",
index d7bb1f8..f854dce 100644 (file)
        "revdelete-edit-reasonlist": "עריכת סיבות ההסתרה",
        "revdelete-offender": "מחבר הגרסה:",
        "suppressionlog": "יומן העלמות",
-       "suppressionlogtext": "×\9c×\94×\9c×\9f ×¨×©×\99×\9eת ×\94×\9e×\97×\99ק×\95ת ×\95×\94×\97ס×\99×\9e×\95ת ×\94×\9b×\95×\9c×\9c×\95ת ×ª×\95×\9b×\9f ×\94×\9e×\95סתר ×\9e×\9eפע×\99×\9c×\99 ×\94×\9eער×\9bת.\nר×\90×\95 ×\90ת [[Special:BlockList|רשימת החסומים]] לרשימת החסימות הפעילות כעת.",
+       "suppressionlogtext": "×\9c×\94×\9c×\9f ×¨×©×\99×\9eת ×\94×\9e×\97×\99ק×\95ת ×\95×\94×\97ס×\99×\9e×\95ת ×\94×\9b×\95×\9c×\9c×\95ת ×ª×\95×\9b×\9f ×\94×\9e×\95סתר ×\9e×\9eפע×\99×\9c×\99 ×\94×\9eער×\9bת.\n× ×\99ת×\9f ×\9c×¢×\99×\99×\9f ×\91[[Special:BlockList|רשימת החסומים]] לרשימת החסימות הפעילות כעת.",
        "mergehistory": "מיזוג גרסאות של דפים",
        "mergehistory-header": "דף זה מאפשר לך למזג גרסאות מתוך היסטוריית גרסאות של דף אחד (דף המקור) לתוך דף חדש.\nיש לוודא שהמיזוג לא יפגע בהמשכיות השינויים בדף הישן.",
        "mergehistory-box": "מיזוג גרסאות של שני דפים:",
        "mergehistory-from": "דף המקור:",
        "mergehistory-into": "דף היעד:",
        "mergehistory-list": "היסטוריית עריכות בת מיזוג",
-       "mergehistory-merge": "× ×\99ת×\9f ×\9c×\9e×\96×\92 ×\90ת ×\94×\92רס×\90×\95ת ×\94×\91×\90×\95ת ×©×\9c [[:$1]] ×\9cת×\95×\9a [[:$2]].\n×\99ש ×\9c×\94שת×\9eש ×\91×\9c×\97צנ×\99 ×\94×\90פשר×\95ת ×\9b×\93×\99 ×\9c×\91×\97×\95ר ×\96×\9e×\9f ×©×¨×§ ×\92רס×\90×\95ת ×©× ×\95צר×\95 ×\91×\95 ×\95×\9cפנ×\99×\95 ×\99×\9e×\95×\96×\92×\95.\nשימוש בקישורי הניווט יאפס עמודה זו.",
+       "mergehistory-merge": "× ×\99ת×\9f ×\9c×\9e×\96×\92 ×\90ת ×\94×\92רס×\90×\95ת ×\94×\91×\90×\95ת ×©×\9c [[:$1]] ×\9cת×\95×\9a [[:$2]].\n×\99ש ×\9c×\94שת×\9eש ×\91×\9c×\97צנ×\99 ×\94×\90פשר×\95ת ×\9b×\93×\99 ×\9c×\91×\97×\95ר ×\96×\9e×\9f ×©×¨×§ ×\92רס×\90×\95ת ×©× ×\95צר×\95 ×\91×\95 ×\95×\9cפנ×\99×\95 ×ª×\9e×\95×\96×\92× ×\94.\nשימוש בקישורי הניווט יאפס עמודה זו.",
        "mergehistory-go": "הצגת עריכות בנות מיזוג",
-       "mergehistory-submit": "מיזוג",
+       "mergehistory-submit": "מיזוג הגרסאות",
        "mergehistory-empty": "אין גרסאות למיזוג.",
-       "mergehistory-done": "{{PLURAL:$3|×\92רס×\94 ×\90×\97ת|$3 ×\92רס×\90×\95ת}} ×©×\9c $1 {{PLURAL:$3|×\9e×\95×\96×\92×\94\9e×\95×\96×\92×\95}} ×\91×\94צ×\9c×\97×\94 ×\9cת×\95×\9a [[:$2]].",
+       "mergehistory-done": "{{PLURAL:$3|גרסה אחת|$3 גרסאות}} של $1 {{PLURAL:$3|מוזגה|מוזגו}} לתוך [[:$2]].",
        "mergehistory-fail": "לא ניתן לבצע את מיזוג הגרסאות, יש לבדוק שנית את הגדרות הדף והזמן.",
        "mergehistory-fail-bad-timestamp": "התאריך והשעה אינם תקינים.",
        "mergehistory-fail-invalid-source": "דף המקור אינו תקין.",
        "mergehistory-invalid-destination": "דף היעד חייב להיות בעל כותרת תקינה.",
        "mergehistory-autocomment": "הדף [[:$1]] מוזג לתוך הדף [[:$2]]",
        "mergehistory-comment": "הדף [[:$1]] מוזג לתוך הדף [[:$2]]: $3",
-       "mergehistory-same-destination": "×\93פ×\99 ×\94×\9eק×\95ר ×\95×\94×\99×¢×\93 זהים",
+       "mergehistory-same-destination": "×\93×£ ×\94×\9eק×\95ר ×\95×\93×£ ×\94×\99×¢×\93 ×\9c×\90 ×\99×\9b×\95×\9c×\99×\9d ×\9c×\94×\99×\95ת זהים",
        "mergehistory-reason": "סיבה:",
        "mergelog": "יומן מיזוגים",
        "revertmerge": "ביטול המיזוג",
index 4c31b99..14c1709 100644 (file)
        "tag-mw-blank": "비우기",
        "tag-mw-blank-description": "문서를 비운 편집",
        "tag-mw-replace": "대체됨",
-       "tag-mw-replace-description": "문서 내용 중 90% 보다 많은 내용을 제거한 편집",
+       "tag-mw-replace-description": "문서 내용의 90% 이상을 제거한 편집",
        "tag-mw-rollback": "되돌리기",
        "tag-mw-rollback-description": "되돌리기 링크를 사용하여 이전 편집으로 전부 되돌린 편집",
        "tag-mw-undo": "편집 취소",
index 5ec8fb6..f713b72 100644 (file)
        "tags-delete-submit": "Haol dit label eweg zónger det 't kan waere trögkgedrejd",
        "tags-delete-not-allowed": "Labeles die waere bepaoldj door 'n oetbreijing kónne neet waeren eweggehaold, behauve wen de oetbreijing dit specefiek tousteit.",
        "tags-delete-not-found": "'t Label \"$1\" besteit neet.",
+       "tags-delete-no-permission": "Doe höbs gein rechte veur verangeringslabels eweg te haole.",
+       "tags-activate-title": "Aktiveer label",
+       "tags-activate-question": "Doe geis zo 't label \"$1\" aktivere.",
+       "tags-activate-reason": "Raeje:",
+       "tags-activate-not-allowed": "'t Is neet meugelik 't label \"$1\" te aktivere.",
+       "tags-activate-not-found": "'t Label \"$1\" besteit neet.",
+       "tags-activate-submit": "Aktiveer",
+       "tags-deactivate-title": "Deaktiveer label",
+       "tags-deactivate-question": "Doe geis zo 't label \"$1\" deaktivere.",
        "tags-deactivate-reason": "Raeje:",
+       "tags-deactivate-not-allowed": "'t Is neet meugelik 't label \"$1\" te deaktivere.",
        "tags-deactivate-submit": "Deaktiveer",
+       "tags-apply-no-permission": "Doe höbs gein rechte veur verangeringslabels tou te veuge aan dien verangeringe.",
        "comparepages": "Vergeliek pazjena's",
        "compare-page1": "Paasj 1",
        "compare-page2": "Paasj 2",
index 789eaeb..f1bf26e 100644 (file)
        "linksearch-ns": "Domínio:",
        "linksearch-ok": "Pesquisar",
        "linksearch-text": "É possível usar caracteres de substituição ''(wildcards)'', tais como \"*.wikipedia.org\".\nÉ necessário, pelo menos, um domínio de topo, por exemplo \"*.org\".<br />\n{{PLURAL:$2|Protocolo suportado|Protocolos suportados}}: $1 (será utilizado http:// se não for especificado um protocolo).",
-       "linksearch-line": "Ligação para $1 na página $2",
+       "linksearch-line": "Hiperligação para $1 na página $2",
        "linksearch-error": "Caracteres de substituição ''(wildcards)'' só podem ser usados no início do endereço.",
        "listusersfrom": "Mostrar utilizadores começados por:",
        "listusers-submit": "Mostrar",
        "sp-contributions-submit": "Pesquisar",
        "sp-contributions-outofrange": "Não é possível mostrar nenhum resultado. A gama de endereços IP pedida é maior do que o limite /$1 do intervalo CIDR.",
        "whatlinkshere": "Páginas afluentes",
-       "whatlinkshere-title": "Páginas com ligações para \"$1\"",
+       "whatlinkshere-title": "Páginas com hiperligações para \"$1\"",
        "whatlinkshere-page": "Página:",
        "linkshere": "As seguintes páginas têm hiperligações para <strong>[[:$1]]</strong>:",
        "nolinkshere": "Não existem afluentes para <strong>[[:$1]]</strong> com as condições especificadas.",
        "whatlinkshere-hideredirs": "$1 redirecionamentos",
        "whatlinkshere-hidetrans": "$1 transclusões",
        "whatlinkshere-hidelinks": "$1 hiperligações",
-       "whatlinkshere-hideimages": "$1 ligações para ficheiros",
+       "whatlinkshere-hideimages": "$1 hiperligações para ficheiros",
        "whatlinkshere-filters": "Filtros",
        "whatlinkshere-submit": "Continuar",
        "autoblockid": "Bloqueio automático nº $1",
        "tooltip-n-recentchanges": "Uma lista de mudanças recentes nesta wiki",
        "tooltip-n-randompage": "Carregar página aleatória",
        "tooltip-n-help": "Um local reservado para auxílio.",
-       "tooltip-t-whatlinkshere": "Lista de todas as páginas que contêm ligações para esta",
+       "tooltip-t-whatlinkshere": "Lista de todas as páginas que contêm hiperligações para esta",
        "tooltip-t-recentchangeslinked": "Mudanças recentes nas páginas para as quais esta contém hiperligações",
        "tooltip-feed-rss": "''Feed'' RSS desta página",
        "tooltip-feed-atom": "''Feed'' Atom desta página",
        "spamprotectiontext": "O texto que pretendia gravar foi bloqueado pelo filtro de spam.\nEste bloqueio foi provavelmente causado por uma hiperligação para um sítio externo que está na lista negra.",
        "spamprotectionmatch": "O seguinte texto ativou o filtro de <i>spam</i>: $1",
        "spambot_username": "MediaWiki limpeza de spam",
-       "spam_reverting": "A reverter para a última revisão que não contém ligação para $1",
+       "spam_reverting": "A reverter para a última revisão que não contém hiperligações para $1",
        "spam_blanking": "Todas as revisões continham hiperligações para $1; a esvaziar",
        "spam_deleting": "Todas as revisões continham hiperligações para $1; a eliminar",
        "simpleantispam-label": "Verificação contra spam.\n<strong>Não</strong> preencha isto!",
index e7da4c6..c07c1fa 100644 (file)
        "statistics-files": "Used in [[Special:Statistics]].\n{{Identical|Uploaded file}}",
        "statistics-edits": "Used in [[Special:Statistics]]",
        "statistics-edits-average": "Used in [[Special:Statistics]]",
-       "statistics-users": "{{doc-important|Do not translate \"Special:ListUsers\"}}\nUsed in [[Special:Statistics]].",
+       "statistics-users": "Used in [[Special:Statistics]].",
        "statistics-users-active": "Used in [[Special:Statistics]]",
        "statistics-users-active-desc": "Description shown beneath ''Active users'' in [[Special:Statistics]]. Parameters:\n* $1 - Value of <code>$wgRCMaxAge</code> in days",
        "statistics-footer": "{{notranslate}}",
index 7f8f65d..1887c6e 100644 (file)
        "tog-watchlisthideminor": "Dölj mindre ändringar i bevakningslistan",
        "tog-watchlisthideliu": "Visa inte redigeringar av inloggade användare i bevakningslistan",
        "tog-watchlistreloadautomatically": "Uppdatera bevakningslistan automatiskt när ett filter ändras (JavaScript krävs)",
-       "tog-watchlistunwatchlinks": "Lägg till länkar för att bevaka och sluta bevaka artiklar i bevakningslistan (JavaScript krävs)",
+       "tog-watchlistunwatchlinks": "Lägg till länkar för att bevaka och sluta bevaka ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) artiklar i bevakningslistan (JavaScript krävs)",
        "tog-watchlisthideanons": "Dölj redigeringar av anonyma användare i bevakningslistan",
        "tog-watchlisthidepatrolled": "Dölj patrullerade redigeringar i bevakningslistan",
        "tog-watchlisthidecategorization": "Dölj kategorisering av sidor",
        "rcfilters-filter-humans-label": "Mänskliga (inte bot)",
        "rcfilters-filter-humans-description": "Redigeringar gjorda av mänskliga redigerare.",
        "rcfilters-filtergroup-reviewstatus": "Granskningsstatus",
+       "rcfilters-filter-reviewstatus-unpatrolled-description": "Redigeringar som inte är manuellt eller automatiskt märkta som patrullerade.",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "Inte patrullerade",
+       "rcfilters-filter-reviewstatus-manual-description": "Redigeringar som är manuellt markerade som patrullerade.",
+       "rcfilters-filter-reviewstatus-manual-label": "Patrullerad manuellt",
+       "rcfilters-filter-reviewstatus-auto-description": "Redigeringar av avancerade användare vars verk är automatiskt märkta som patrullerade.",
        "rcfilters-filter-reviewstatus-auto-label": "Autopatrullerad",
        "rcfilters-filtergroup-significance": "Betydelse",
        "rcfilters-filter-minor-label": "Mindre redigeringar",
index 2fa107f..5441099 100644 (file)
@@ -18,7 +18,8 @@
                        "Timpul",
                        "아라",
                        "Macofe",
-                       "Uostofchuodnego"
+                       "Uostofchuodnego",
+                       "Przem(1)s"
                ]
        },
        "tog-underline": "Podsztrychńyńcy linkůw:",
        "faq": "FAQ",
        "actions": "Akcyje",
        "namespaces": "Raumy mjan",
-       "variants": "Warjanty",
+       "variants": "Ôpcyje",
        "navigation-heading": "Menu nawigacyje",
        "errorpagetitle": "Feler",
        "returnto": "Nazod do zajty $1.",
        "tagline": "Ze {{GRAMMAR:D.lp|{{SITENAME}}}}",
        "help": "Půmoc",
-       "search": "Sznupej",
-       "searchbutton": "Sznupej",
+       "search": "Szukej",
+       "searchbutton": "Szukej",
        "go": "Przyńdź",
-       "searcharticle": "Przyńdź",
+       "searcharticle": "Pōdź",
        "history": "Gyszichta zajty",
        "history_short": "Gyszichta",
        "updatedmarker": "pomjyńane uod uostatńij wizyty",
-       "printableversion": "Wersyjo do druku",
+       "printableversion": "Wersyjŏ do durku",
        "permalink": "Link do tyj wersyje zajty",
        "print": "Drukuj",
        "view": "Podglůnd",
        "view-foreign": "Uobejrzij we {{grammar:MS.lp|$1}}",
-       "edit": "Sprowjej",
+       "edit": "Edytuj",
        "create": "Stwůrz",
        "create-local": "Wkludź lokalny uopis",
        "delete": "Wyćep",
        "talkpagelinktext": "dyskusyjŏ",
        "specialpage": "Szpecyjolno zajta",
        "personaltools": "Perzōnŏlne",
-       "talk": "Dyskusyjo",
-       "views": "Ôbocz",
-       "toolbox": "Werkcojg",
+       "talk": "Dyskusyjŏ",
+       "views": "Ôbŏcz",
+       "toolbox": "Nŏczynia",
        "imagepage": "Uobejrz zajta pliku",
        "mediawikipage": "Zajta komuńikata",
        "templatepage": "Zajta mustra",
        "lastmodifiedat": "Ta zajta bůła uostatńo sprowjano $2, $1.",
        "viewcount": "W ta zajta filowano {{PLURAL:$1|tylko roz|$1 rozůw}}.",
        "protectedpage": "Zajta zawarto",
-       "jumpto": "Przyńdź do:",
-       "jumptonavigation": "nawigacyjo",
-       "jumptosearch": "sznupaniy",
+       "jumpto": "Pōdź do:",
+       "jumptonavigation": "nawigacyjŏ",
+       "jumptosearch": "szukej",
        "view-pool-error": "Felerńe, syrwyry sům przećůnżone.\n\n$1",
        "pool-timeout": "Za dugi czas uoczekiwańo na blokada",
        "pool-queuefull": "Kolyjność zadań je pełno",
        "site-atom-feed": "Kanŏł Atom {{GRAMMAR:D.lp|$1}}",
        "page-rss-feed": "Kanoł RSS \"$1\"",
        "page-atom-feed": "Kanoł Atom \"$1\"",
-       "red-link-title": "$1 (ńy ma zajty)",
+       "red-link-title": "$1 (niy ma zajty)",
        "sort-descending": "Sortuj pomńijszajůnco",
        "sort-ascending": "Sortuj rosnůnco",
        "nstab-main": "Zajta",
        "createacct-another-realname-tip": "Wszkryflańy twojigo mjana a nazwiska ńy je końyczne.\nKej bydźesz chćoł je podoć, bydům użyte, coby dokůmyntowoć Twoje autorstwo.",
        "pt-login": "Zaloguj śe",
        "pt-login-button": "Zaloguj śe",
-       "pt-createaccount": "Twōrz nowe konto",
+       "pt-createaccount": "Twōrz nowe kōnto",
        "pt-userlogout": "Uodloguj śe",
        "php-mail-error-unknown": "Ńyznany feler we funkcyji mail()",
        "user-mail-no-addy": "Průba posłańo e‐brifa bez adresu uodbjorcy",
        "newpageletter": "N",
        "boteditletter": "b",
        "number_of_watching_users_pageview": "[$1 {{PLURAL:$1|dowajůncy pozůr užytkowńik|dowajůncych pozůr užytkowńikůw}}]",
-       "rc-change-size-new": "$1 {{PLURAL:$1|bajt|bajty|bajtůw}} po půmjyÅ\84\84u",
+       "rc-change-size-new": "$1 {{PLURAL:$1|bajt|bajty|bajtÅ\8dw}} po pÅ\8dmianie",
        "newsectionsummary": "/* $1 */ nowo tajla",
        "rc-enhanced-expand": "Pokoż szczygůły",
        "rc-enhanced-hide": "Schrůń detajle",
        "blocklink": "blokuj",
        "unblocklink": "uodymknij",
        "change-blocklink": "půmjyń zawarće uod sprowjyń",
-       "contribslink": "ajnzace",
+       "contribslink": "wkłŏd",
        "autoblocker": "Zawarto Ci sprowjyńo autůmatyczńy, bez tůż co używosz tygo samygo adresu IP, co używocz „[[User:$1|$1]]”.\nPowůd zawarća $1 to: „$2”",
        "blocklogpage": "Gyszichta zawjyrańo",
        "blocklogentry": "zawarto [[$1]], bydźe uodymkńynty: $2 $3",
        "tooltip-ca-watch": "Przidej artikel na pozůrlista",
        "tooltip-ca-unwatch": "Wyciep tyn artikel ze pozůrlisty",
        "tooltip-search": "Sznupej we serwisie {{SITENAME}}",
-       "tooltip-search-go": "Przyńdź na zajtã ô gynał takij titli, eli sam je",
+       "tooltip-search-go": "Pōdź na zajtã ô gynau takij titli, eli sam je",
        "tooltip-search-fulltext": "Sznupej wciepany tekst na zajcie",
-       "tooltip-p-logo": "Przodnio zajta",
+       "tooltip-p-logo": "Przodniŏ zajta",
        "tooltip-n-mainpage": "Przelyź na przodńo zajta",
        "tooltip-n-mainpage-description": "Przelyź na przodńo zajta",
        "tooltip-n-portal": "Uo projekće, co mogesz robić, kaj mogesz nolyźć informacyje",
        "tooltip-n-currentevents": "Informacyje uo aktualnych przitrefjyńach",
-       "tooltip-n-recentchanges": "Lista ńydowno půmjyńanych we wiki",
+       "tooltip-n-recentchanges": "Spisek niydŏwnych pōmian we wiki",
        "tooltip-n-randompage": "Ukoż cufalno zajta",
        "tooltip-n-help": "Sam śe mogesz moc przewjedźeć",
        "tooltip-t-whatlinkshere": "Ukoż zajty, kere sam linkujům",
        "tooltip-t-contributions": "Ukoż ajnzace tygo używocza",
        "tooltip-t-emailuser": "Wyślij e-brif do tygo użytkowńika",
        "tooltip-t-upload": "Wćepej plik na serwer",
-       "tooltip-t-specialpages": "Lista wszeckich ekstra zajt",
+       "tooltip-t-specialpages": "Spisek wszyjskich szpecyjalnych zajt",
        "tooltip-t-print": "Wersyjo do durku",
        "tooltip-t-permalink": "Pewny link do tyj wersyje zajty",
        "tooltip-ca-nstab-main": "Uobźyrej zajta artikla",
index ea6a295..a1bda46 100644 (file)
        "filereadonlyerror": "ไม่สามารถแก้ไขไฟล์ \"$1\" เพราะที่เก็บไฟล์ \"$2\" อยู่ในภาวะอ่านอย่างเดียว\n\nผู้ดูแลระบบที่ล็อกให้คำอธิบายว่า: \"$3\"",
        "invalidtitle-knownnamespace": "ชื่อเรื่องที่มีเนมสเปซ \"$2\" กับข้อความ \"$3\" ไม่ถูกต้อง",
        "invalidtitle-unknownnamespace": "ชื่อเรื่องที่ไม่ทราบเนมสเปซหมายเลข $1 กับข้อความ \"$2\" ไม่ถูกต้อง",
-       "exception-nologin": "ยัà¸\87à¹\84มà¹\88ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89า",
+       "exception-nologin": "ยัà¸\87à¹\84มà¹\88à¹\84à¸\94à¹\89à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9a",
        "exception-nologin-text": "โปรดล็อกอินเพื่อสามารถเข้าถึงหน้าหรือปฏิบัติการนี้",
        "exception-nologin-text-manual": "โปรด$1เพื่อสามารถเข้าถึงหน้าหรือปฏิบัติการนี้",
        "virus-badscanner": "โครงแบบผิดพลาด: ไม่รู้จักตัวสแกนไวรัส: <em>$1</em>",
        "yourdomainname": "โดเมนของคุณ:",
        "password-change-forbidden": "คุณไม่สามารถเปลี่ยนรหัสผ่านบนวิกินี้",
        "externaldberror": "มีข้อผิดพลาดของฐานข้อมูลการพิสูจน์ตัวจริง หรือคุณไม่ได้รับอนุญาตให้ปรับบัญชีภายนอกของคุณ",
-       "login": "ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89า",
+       "login": "à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9a",
        "login-security": "ยืนยันตัวตนของคุณ",
        "nav-login-createaccount": "ล็อกอิน / สร้างบัญชี",
-       "logout": "ลà¸\87à¸\8aืà¹\88อออà¸\81",
-       "userlogout": "ลà¸\87à¸\8aืà¹\88อออà¸\81",
-       "notloggedin": "ยัà¸\87à¹\84มà¹\88ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89า",
+       "logout": "ออà¸\81à¸\88าà¸\81ระà¸\9aà¸\9a",
+       "userlogout": "ออà¸\81à¸\88าà¸\81ระà¸\9aà¸\9a",
+       "notloggedin": "ยัà¸\87à¹\84มà¹\88à¹\84à¸\94à¹\89à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9a",
        "userlogin-noaccount": "ไม่มีบัญชีหรือ",
        "userlogin-joinproject": "เข้าร่วมกับ{{SITENAME}}",
        "createaccount": "สร้างบัญชี",
        "nocookiesfornew": "บัญชีผู้ใช้ไม่ถูกสร้าง เนื่องจากเราไม่สามารถยืนยันต้นทาง\nกรุณาทำให้แน่ใจว่าคุณได้เปิดใช้งานคุกกี้ โหลดหน้านี้ใหม่และลองอีกครั้ง",
        "createacct-loginerror": "บัญชีผู้ใช้ถูกสร้างสำเร็จแล้ว แต่คุณไม่สามารถเข้าสู่ระบบได้โดยอัตโนมัติ โปรด[[Special:UserLogin|เข้าสู่ระบบด้วยตนเอง]]",
        "noname": "คุณไม่ได้ใส่ชื่อผู้ใช้ที่ถูกต้อง",
-       "loginsuccesstitle": "ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89าแล้ว",
+       "loginsuccesstitle": "à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9aแล้ว",
        "loginsuccess": "<strong>ขณะนี้คุณล็อกอินสู่ {{SITENAME}} ในชื่อ \"$1\"</strong>",
        "nosuchuser": "ไม่มีผู้ใช้ชื่อ \"$1\"\nชื่อผู้ใช้นั้นไวต่ออักษรใหญ่เล็ก\nกรุณาตรวจการสะกดอีกครั้ง หรือ[[Special:CreateAccount|สร้างบัญชีใหม่]]",
        "nosuchusershort": "ไม่มีผู้ใช้ชื่อ \"$1\" \nกรุณาตรวจสอบการสะกด",
        "loginlanguagelabel": "ภาษา: $1",
        "suspicious-userlogout": "คำขอล็อกเอาต์ของคุณถูกปฏิเสธเพราะดูเหมือนส่งมาจากเบราว์เซอร์หรือพร็อกซีแคชที่เสีย",
        "createacct-another-realname-tip": "ไม่จำเป็นต้องใส่ชื่อจริง\nหากคุณเลือกใส่ชื่อจริง จะใช้เพื่อแสดงที่มาสำหรับงานของตน",
-       "pt-login": "ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89า",
-       "pt-login-button": "ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89า",
-       "pt-login-continue-button": "ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89าต่อ",
+       "pt-login": "à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9a",
+       "pt-login-button": "à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9a",
+       "pt-login-continue-button": "à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9aต่อ",
        "pt-createaccount": "สร้างบัญชี",
-       "pt-userlogout": "ลà¸\87à¸\8aืà¹\88อออà¸\81",
+       "pt-userlogout": "ออà¸\81à¸\88าà¸\81ระà¸\9aà¸\9a",
        "php-mail-error-unknown": "เกิดข้อผิดพลาดไม่ทราบสาเหตุในฟังก์ชัน mail() ของพีเอชพี",
        "user-mail-no-addy": "พยายามส่งอีเมลโดยไม่มีที่อยู่อีเมล",
        "user-mail-no-body": "พยายามส่งอีเมลที่มีเนื้อหาว่างหรือสั้นอย่างไร้เหตุผล",
        "nosuchsectiontitle": "ไม่พบส่วน",
        "nosuchsectiontext": "คุณพยายามแก้ไขส่วนที่ไม่มีอยู่ \nส่วนดังกล่าวอาจถูกย้ายหรือลบขณะที่คุณดูหน้าอยู่",
        "loginreqtitle": "ต้องล็อกอิน",
-       "loginreqlink": "ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89า",
+       "loginreqlink": "à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9a",
        "loginreqpagetext": "กรุณา$1เพื่อดูหน้าอื่น",
        "accmailtitle": "ส่งรหัสผ่านแล้ว",
        "accmailtext": "ส่งรหัสผ่านแบบสุ่มของ [[User talk:$1|$1]] ไป $2 แล้ว สามารถเปลี่ยนรหัสผ่านในหน้า<em>[[Special:ChangePassword|เปลี่ยนรหัสผ่าน]]</em> หลังล็อกอิน",
        "reuploaddesc": "ยกเลิกการอัปโหลดและกลับไปแบบอัปโหลด",
        "upload-tryagain": "ส่งคำอธิบายไฟล์ที่ดัดแปรแล้ว",
        "upload-tryagain-nostash": "ส่งไฟล์ที่อัปโหลดใหม่และคำอธิบายที่ดัดแปรแล้ว",
-       "uploadnologin": "ยัà¸\87à¹\84มà¹\88ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89า",
+       "uploadnologin": "ยัà¸\87à¹\84มà¹\88à¹\84à¸\94à¹\89à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9a",
        "uploadnologintext": "โปรด$1เพื่ออัปโหลดไฟล์",
        "upload_directory_missing": "สารบบอัปโหลด ($1) หาย และเว็บเซิร์ฟเวอร์ไม่สามารถสร้างได้",
        "upload_directory_read_only": "เว็บเซิร์ฟเวอร์ไม่สามารถเขียนสารบบอัปโหลด ($1)",
        "watchlistfor2": "สำหรับ $1 $2",
        "nowatchlist": "ไม่มีรายการในรายการเฝ้าดูของคุณ",
        "watchlistanontext": "โปรดลงชื่อเข้าเพื่อดูหรือแก้ไขรายการในรายการเฝ้าดูของคุณ",
-       "watchnologin": "ยัà¸\87à¹\84มà¹\88ลà¸\87à¸\8aืà¹\88อà¹\80à¸\82à¹\89า",
+       "watchnologin": "ยัà¸\87à¹\84มà¹\88à¹\84à¸\94à¹\89à¹\80à¸\82à¹\89าสูà¹\88ระà¸\9aà¸\9a",
        "addwatch": "เพิ่มเข้ารายการเฝ้าดู",
        "addedwatchtext": "เพิ่มหน้า \"[[:$1]]\" และหน้าอภิปรายเข้า[[Special:Watchlist|รายการเฝ้าดู]]ของคุณแล้ว",
        "addedwatchtext-talk": "เพิ่ม \"[[:$1]]\" และหน้าที่สัมพันธ์เข้า[[Special:Watchlist|รายการเฝ้าดู]]ของคุณแล้ว",
        "tooltip-pt-anoncontribs": "รายการการแก้ไขจากเลขที่อยู่ไอพีนี้",
        "tooltip-pt-login": "สนับสนุนให้คุณล็อกอิน แต่ไม่บังคับ",
        "tooltip-pt-login-private": "คุณต้องล็อกอินจึงจะใช้วิกินี้ได้",
-       "tooltip-pt-logout": "ลà¸\87à¸\8aืà¹\88อออà¸\81",
+       "tooltip-pt-logout": "ออà¸\81à¸\88าà¸\81ระà¸\9aà¸\9a",
        "tooltip-pt-createaccount": "สนับสนุนให้คุณสร้างบัญชีและล็อกอิน แต่ไม่บังคับ",
        "tooltip-ca-talk": "อภิปรายเกี่ยวกับหน้าเนื้อหา",
        "tooltip-ca-edit": "แก้ไขหน้านี้",
index 3f08748..c6c018e 100644 (file)
        "tag-mw-new-redirect": "Yeni yönlendirme",
        "tag-mw-removed-redirect": "Yönlendirme kaldırıldı",
        "tag-mw-changed-redirect-target": "Yönlendirme hedefi değiştirildi",
+       "tag-mw-blank": "Boşaltma",
+       "tag-mw-replace": "Değiştirme",
        "tag-mw-rollback": "Geri döndürme",
        "tag-mw-undo": "Geri alma",
        "tags-title": "Etiketler",
index 5c6403f..4c953f1 100644 (file)
                        "Macofe",
                        "Matma Rex",
                        "V6rg",
-                       "C.R."
+                       "C.R.",
+                       "Cusolotto"
                ]
        },
        "tog-underline": "Sotołinea i cołegamenti:",
        "tog-hideminor": "Scondi i canbiamenti picenini in tei \"Ultimi canbiamenti\"",
        "tog-hidepatrolled": "Scondi i canbiamenti verificà in tei \"Ultimi canbiamenti\"",
        "tog-newpageshidepatrolled": "Scondi łe pajine verifegae da l'elenco de łe pajine pì resenti",
+       "tog-hidecategorization": "Nascondi le categorixasion de la pagina",
        "tog-extendwatchlist": "Mostra tute łe modifeghe a i oservai spesałi, no soło l'ultima",
        "tog-usenewrc": "Ragrupa ƚe modifeghe par pàgina inte i ultemi canbiamenti e inte ƚe tegnùe d'òcio",
        "tog-numberheadings": "Numerasion automatega de i titołi de sesion",
@@ -38,6 +40,8 @@
        "tog-watchdefault": "Xonta łe pàjine e i file modifegai a łe tegnùe d'ocio",
        "tog-watchmoves": "Xonta łe pàjine e i file spostai a łe tegnùe d'ocio",
        "tog-watchdeletion": "Xonta łe pàjine e i file scansełai a łe tegnùe d'ocio",
+       "tog-watchuploads": "Xonta en novo file che cargo a i oservatori speciali",
+       "tog-watchrollback": "Xonta a i vardatori speciali le pagine ndo go fato na modifica su na mia watchlist",
        "tog-minordefault": "Segna ogni canbiamento come picenin (solo come predefinìo)",
        "tog-previewontop": "Mostra l'anteprima sora ła caseła de modifega e no soto",
        "tog-previewonfirst": "Mostra l'anteprima par ła prima modifega",
        "tog-enotifminoredits": "Avìxeme par e-mail anca pa' i canbiamenti picenini de pàjine e file",
        "tog-enotifrevealaddr": "Fà védar el me indirisso e-mail in tei messagi de aviso",
        "tog-shownumberswatching": "Mostra el numaro de utenti che i ga ła pajina en oservasion",
-       "tog-oldsig": "Anteprima de ła firma:",
+       "tog-oldsig": "La to firma:",
        "tog-fancysig": "Interpreta i comandi wiki in te la firma (sensa colegamento automatego)",
-       "tog-uselivepreview": "Intaca l'anteprima dal vivo",
+       "tog-uselivepreview": "Varda anteprima sensa ricargar la pagina",
        "tog-forceeditsummary": "Chiedi conferma se l'ozeto de ła modifega el xé vodo",
        "tog-watchlisthideown": "Scondi łe me modifeghe ne i oservai spesałi",
        "tog-watchlisthidebots": "Scondi łe modifeghe de i bot ne i oservai spesałi",
        "tog-watchlisthideminor": "Scondi łe modifeghe picenine ne i oservai spesałi",
        "tog-watchlisthideliu": "Scondi łe modifeghe de i utenti rejistrà ne i oservai spesałi",
+       "tog-watchlistreloadautomatically": "Ricarga en automatico l'elenco dei vardatori speciali ogni olta che se modifica en filtro (serve JavaScript)",
        "tog-watchlisthideanons": "Scondi łe modifeghe de i utenti anonimi ne i oservai spesałi",
        "tog-watchlisthidepatrolled": "Scondi łe modifeghe verifegae ne i oservai spesałi",
+       "tog-watchlisthidecategorization": "Nascondi le categorixasion de la pagina",
        "tog-ccmeonemails": "Inviame na copia de i mesaji spedii a i altri utenti",
        "tog-diffonly": "No visuałisar el contenuo de ła pajina dopo el confronto tra version",
        "tog-showhiddencats": "Mostra łe categorie sconte",
-       "tog-norollbackdiff": "No mostrare el confronto tra version dopo aver efetuà on rollback",
+       "tog-norollbackdiff": "No mostrare el confronto tra version dopo aver efetuà el rollback",
        "tog-useeditwarning": "Dime se sto lassando na pagina de modifica sensa aver salvà",
+       "tog-prefershttps": "Utilixa senpre na conesion sicura cuqndo te entre",
        "underline-always": "Senpre",
        "underline-never": "Mai",
        "underline-default": "Mantien łe inpostasion del browser o de ła skin",
        "october-date": "$1 de otobre",
        "november-date": "$1 de novenbre",
        "december-date": "$1 de desenbre",
+       "period-am": "Matina",
+       "period-pm": "Pomerigio",
        "pagecategories": "{{PLURAL:$1|Categoria|Categorie}}",
        "category_header": "Pagine in te la categoria \"$1\"",
        "subcategories": "Sotocategorie",
        "newwindow": "(se verze in te na finestra nova)",
        "cancel": "Lassa star",
        "moredotdotdot": "Altro...",
-       "morenotlisted": "Sta lista no xe conpleta.",
+       "morenotlisted": "Sta lista no l'è conpleta.",
        "mypage": "Pàjina",
        "mytalk": "Discussion",
        "anontalk": "Discussion",
        "tagline": "Da {{SITENAME}}",
        "help": "Ajuto",
        "search": "Serca",
+       "search-ignored-headings": " #<!-- l'asa sta riga come te l'è catà --> <pre>\n# Elenco de le intestasioni che le sarà ignorè da la ricerca.\n# Chel che te modifiche su sta pagijna le se vedarà subi.\n# Te pol sforsar la re-indicizazion di una pagina effettuando una modifica nula.\n# Te dovreste scriver cosita:\n#   * Tuto dal carattere \"#\" a la fine de la riga l'è en comento\n#   * Tute le righe non ude iè le intestasioni juste da no vardar, maiuscolo/minuscolo e tuto\nNote\nVoci corelate\nColegamenti foraia\n #</pre> <!-- lasa sta riga come te l'è catà -->",
        "searchbutton": "Serca",
        "go": "Va",
        "searcharticle": "Và",
        "history": "Storia de la pagina",
        "history_short": "Storia",
+       "history_small": "Storia",
        "updatedmarker": "modifegà da ła me ultema visita",
        "printableversion": "Version par la stanpa",
        "permalink": "Link parmanente",
        "talk": "Discussion",
        "views": "Visite",
        "toolbox": "Strumenti",
+       "tool-link-userrights": "Modifica grupi {{GENDER:$1|utente}}",
+       "tool-link-userrights-readonly": "Visualisa {{GENDER:$1|}} grupi",
+       "tool-link-emailuser": "Scrivi na e-mail a {{GENDER:$1|sto|sta}} utente",
        "imagepage": "Varda la pagina del file",
        "mediawikipage": "Varda el mesajo",
        "templatepage": "Varda el modeło",
        "pool-timeout": "Timeout durante l'atesa de lo sbloco",
        "pool-queuefull": "La cóa de laorassion la xe piena",
        "pool-errorunknown": "Eror sconossùo",
+       "poolcounter-usage-error": "Erore de l'utilixo: $1",
        "aboutsite": "Se parla de {{SITENAME}}",
        "aboutpage": "Project:Se parla de",
-       "copyright": "Contenui sojeti a licensa d'uso $1.",
+       "copyright": "Contenui sogeti a licensa d'uso $1.",
        "copyrightpage": "{{ns:project}}:Copyright",
        "currentevents": "Atuałità",
        "currentevents-url": "Project:Atuałità",
        "nospecialpagetext": "<strong>La pagina speciale che te serchi no la ghe xe.</strong>\n\nL'elenco de le pagine speciali te lo cati su [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Erore",
        "databaseerror": "Erore del database",
+       "databaseerror-text": "Sè verificà en eror en del database.\nEl podaria eser en bug del software.",
+       "databaseerror-textcl": "Sè verificà en eror en del database.",
+       "databaseerror-query": "Query: $1",
        "databaseerror-function": "Funsion: $1",
        "databaseerror-error": "Eror: $1",
+       "transaction-duration-limit-exceeded": "Par evitar n'alto ritardo de replica, sta operasion l'è sta interrota parché ea durata del tenpo de scritura ($1) l'ha superà el limite de $2 {{PLURAL:$2|secondo|secondi}}.\nSe te se drio modificar tanti elementi en na sola olta, proa a far più operasioni co pochi elementi.",
        "laggedslavemode": "'''Atension:''' ła pajina podaria no riportare i azornamenti pì resenti.",
        "readonly": "Database blocà",
        "enterlockreason": "Indicare el motivo del bloco, spesifegando el momento in cui xè presumibiłe che el venga rimoso",
        "readonly_lag": "El database xè sta blocà automaticamente par consentire a i server co i database slave de sincronizarse con el master",
        "internalerror": "Erore interno",
        "internalerror_info": "Erore interno: $1",
+       "internalerror-fatal-exception": "Bruto error de tipo $1",
        "filecopyerror": "Inposibiłe copiare el file \"$1\" en \"$2\".",
        "filerenameerror": "Inposibiłe rinominare el file \"$1\" en \"$2\".",
        "filedeleteerror": "Inposibiłe scansełare el file \"$1\".",
        "directorycreateerror": "Inposibiłe creare ła directory \"$1\".",
+       "directoryreadonlyerror": "L'elenco \"$1\" te pol solo lexerlo.",
+       "directorynotreadableerror": "L'elenco \"$1\" no l'è mia legibile.",
        "filenotfound": "File \"$1\" no trovà.",
        "unexpected": "Vałore inprevisto: \"$1\"=\"$2\".",
        "formerror": "Erore: inposibiłe inviare el moduło",
        "cannotdelete": "No xè sta posibiłe scansełare el file \"$1\".\nPodaria esare sta zà scansełà da qualcun altro.",
        "cannotdelete-title": "Inposibiłe ełiminare ła pajina \"$1\"",
        "delete-hook-aborted": "Modifega abortìa da parte del hook.\nNo xe stà dà nisuna spiegasion in merito.",
+       "no-null-revision": "Non l'è mia sta posibile crear na version nula par la pagina $1.",
        "badtitle": "Titoło mia justo",
        "badtitletext": "El titoło de ła pajina richiesta xè vodo, erà o con carateri no amesi opure el deriva da n'erore ne i cołegamenti tra siti wiki diversi o version en łengue diverse de ło steso sito.",
+       "title-invalid-empty": "El titolo de la pagina che te serche l'è udo o el contiene solo el nome.",
+       "title-invalid-utf8": "El titolo de la pagina el contenie na sequensa UTF-8 mia valida.",
+       "title-invalid-interwiki": "El titolo de la pagina che se serche la contenie en colegamente interwiki che nol pol mia eser meso nei titoli.",
+       "title-invalid-talk-namespace": "El titolo de la pagina che se serche la fa riferimento a na pagina de discusion che no la ghe.",
+       "title-invalid-characters": "El titolo de la pagina che te serche la contenie carateri mia validi: \"$1\".",
+       "title-invalid-relative": "El titolo che tè indicà el contenie en percorso relativo (., ../). Sti titoli no iè mia validi parchè no iè mia speso ragiungibili quando gestii da browser dell'utente.",
+       "title-invalid-magic-tilde": "El titolo de la pagina che te serchè no l'è mia valido parchè la ga la sucesion special di tilde (<nowiki>~~~</nowiki>).",
        "perfcached": "Sti dati vien tiradi fora da na copia \"cache\" del database e łi podarìa no esare ajornadi. Inte ła cache xe {{PLURAL:$1|disponibiłe un rixultado|xe disponibiłi $1 rixultadi}}.",
        "perfcachedts": "Sti dati cua xe stà ajornadi l'ultima volta el $1. Inte ła cache xe disponibiłe al masimo {{PLURAL:$4|on rexultado|$4 rexultadi}}.",
        "querypage-no-updates": "L'azornamento periodico de sta pagina el xe sospeso. \nI dati che ghè qua drento no i xe azornà.",
        "password-login-forbidden": "Sto nome utente e/o password i xè stai proibìi",
        "mailmypassword": "Rinposta ła password",
        "passwordremindertitle": "Servisio Password Reminder de {{SITENAME}}",
-       "passwordremindertext": "Qualcheduni (probabilmente ti, da l'indirizo IP $1) el gà domandà che ghe vegna mandà na nova password par {{SITENAME}} ($4).\nNa password tenporànea par l'utente \"$2\" la xe stà creà e inpostà a \"$3\".\nSe xe questo che te voléi far, desso te podi entrar co' sta password tenporanea e inpostar na password nova.\nLa to password tenporànea la scade in {{PLURAL:$5|un zorno|$5 zorni}}.\n\nSe no te sì mìa stà ti a far la domanda, opure t'è vegnù in mente la password e no te vol più canbiarla, te pol ignorar sto mesagio e continuar a doparar la vecia password.",
+       "passwordremindertext": "Cualcheduni (probabilmente ti, da l'indirizo IP $1) el gà domandà che ghe vegna mandà na nova password par {{SITENAME}} ($4).\nNa password tenporànea par l'utente \"$2\" la xe stà creà e inpostà a \"$3\".\nSe xe questo che te voléi far, deso te podi entrar co' sta password tenporanea e inpostar na password nova.\nLa to password tenporànea la scade in {{PLURAL:$5|en dì|$5 dì}}.\n\nSe no te sì mìa stà ti a far la domanda, opure t'è vegnù in mente la password e no te vol più canbiarla, te pol ignorar sto mesagio e continuar a doparar la vecia password.",
        "noemail": "Nissuna casela e-mail la risulta registrà par l'Utente \"$1\".",
        "noemailcreate": "Te ghè da fornir un indirisso e-mail vàlido",
        "passwordsent": "Na password nova la xe stà mandà a la casela e-mail registrà per l'Utente \"$1\".\nPar piaser, fà subito un login 'pena che la te riva.",
        "retypenew": "Riscrivi la password nova:",
        "resetpass_submit": "Inposta la password e acedi al sito",
        "changepassword-success": "La password la xe stà modificà!",
+       "botpasswords-label-appid": "Nome bot:",
+       "botpasswords-label-create": "Crea",
+       "botpasswords-label-update": "Agiorna",
+       "botpasswords-label-cancel": "Anùla",
+       "botpasswords-label-delete": "Scanseła",
+       "botpasswords-label-resetpassword": "Rinposta ła password",
+       "botpasswords-label-grants": "Asegnasioni aplicabili:",
+       "botpasswords-label-grants-column": "Asegnasioni",
+       "botpasswords-created-title": "Password bot creata",
        "resetpass_forbidden": "No se pol modificar le password",
        "resetpass-no-info": "Te ghè da ver fato l'acesso per poder entrar in sta pàxena.",
        "resetpass-submit-loggedin": "Cànbia password",
        "longpageerror": "'''Erore: el testo invià xe grando {{PLURAL:$1|1|$1}} kilobyte, che xe de pì deła dimension masima consentìa de {{PLURAL:$2|1|$2}} kilobyte.'''\nEl testo no połe esare salvà.",
        "readonlywarning": "'''OCIO: El database el xe stà blocà par manutension, cuindi nó se pol salvar łe modifeghe in sto momento.'''\nPar nó pèrdarle, te pol copiar tuto chel che te ghè inserìo fin deso inte ła caxeła de modifega, incołarlo inte un programa de elaborasion de testi e salvarlo, intanto che te speti che i sbloca el database.\n\nL'aministrador che gà blocà el database el gà dato ła seguente spiegasion: $1",
        "protectedpagewarning": "'''Ocio:''' Sta pajina ła xe sta proteta in maniera che soło i aministradori i posa canbiarla. Sta qua ła xe l'ultima operasion catà sul registro de ła pajina:",
-       "semiprotectedpagewarning": "'''Ocio:''' Sta pajina ła xe stà proteta in maniera che soło i utenti rexistrài i posa canbiarla. Sta qua ła xe l'ultima operasion catà sul registro de ła pajina:",
+       "semiprotectedpagewarning": "<strong>Ocio:</strong> Sta pajina ła xe stà proteta in maniera che soło i utenti rexistrài i poda canbiarla. Sta qua ła xe l'ultima operasion catà sul registro de ła pajina:",
        "cascadeprotectedwarning": "'''Ocio:''' Sta pajina ła xe stà proteta in maniera che soło i utenti co priviłegi de aministrador i posa canbiarla. Questo sucede parché ła pajina ła xe inclusa {{PLURAL:$1|'nte ła pajina indicà de seguito, che ła xe stà proteta|ne le pagine indicà de seguito, che łe xe stae protete}} sełesionando ła protesion \"ricorsiva\":",
        "titleprotectedwarning": "'''Ocio:''' Sta pajina ła xe stà proteta in modo che soło i utenti co [[Special:ListGroupRights|serti privilègi]] i ła posa crear. Sta qua ła xe l'ultima operasion catà sul registro de ła pajina:",
        "templatesused": "{{PLURAL:$1|Modèl doparà|Modèi doparà}} su sta pagina:",
        "recentchanges-label-plusminus": "La dimension de la pagina la xe canbià de sto nùmaro de byte",
        "recentchanges-legend-heading": "<strong>Legenda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (varda anca [[Special:NewPages|l'elenco de le pagine nove]])",
+       "rcfilters-filter-reviewstatus-manual-label": "Verificà manualmente",
+       "rcfilters-filter-reviewstatus-auto-label": "Verificà en automatico.",
        "rcnotefrom": "Qui di seguito sono elencate le modifiche da '''$2''' (fino a '''$1''').",
        "rclistfrom": "Mostra i canbiamenti fati da ƚe $2 del $3",
        "rcshowhideminor": "$1 i canbiamenti picenini",
        "suppress": "Supervision",
        "querypage-disabled": "Sta pàjina speciałe ła xe dixativà par motivi de prestasion.",
        "apihelp-no-such-module": "Modulo \"$1\" mia catà.",
+       "apisandbox-add-multi": "Xonta",
        "booksources": "Fonti librarie",
        "booksources-search-legend": "Riserca de fonti librarie",
        "booksources-search": "Serca",
        "version-specialpages": "Pagine speciali",
        "version-parserhooks": "Hook del parser",
        "version-variables": "Variabili",
+       "version-editors": "Contributor",
        "version-antispam": "Prevension del spam",
        "version-other": "Altro",
        "version-mediahandlers": "Gestori de contenuti multimediài",
index acb9664..ab57b7b 100644 (file)
@@ -1,3 +1,4 @@
+/* global extDependencyMap */
 ( function ( $ ) {
        $( function () {
                var $label, labelText;
                                $memc.hide( 'slow' );
                        }
                } );
+
+               function areReqsSatisfied( name ) {
+                       var i, ext, skin, node;
+                       if ( !extDependencyMap[ name ] ) {
+                               return true;
+                       }
+
+                       if ( extDependencyMap[ name ].extensions ) {
+                               for ( i in extDependencyMap[ name ].extensions ) {
+                                       ext = extDependencyMap[ name ].extensions[ i ];
+                                       node = document.getElementById( 'config_ext-' + ext );
+                                       if ( !node || !node.checked ) {
+                                               return false;
+                                       }
+                               }
+                       }
+                       if ( extDependencyMap[ name ].skins ) {
+                               for ( i in extDependencyMap[ name ].skins ) {
+                                       skin = extDependencyMap[ name ].skins[ i ];
+                                       node = document.getElementById( 'config_skin-' + skin );
+                                       if ( !node || !node.checked ) {
+                                               return false;
+                                       }
+                               }
+                       }
+
+                       return true;
+               }
+
+               // Disable checkboxes if the extension has dependencies
+               $( '.mw-ext-with-dependencies input' ).prop( 'disabled', true );
+               $( 'input[data-name]' ).change( function () {
+                       $( '.mw-ext-with-dependencies input' ).each( function () {
+                               var $this = $( this ),
+                                       name = $this.data( 'name' );
+                               if ( areReqsSatisfied( name ) ) {
+                                       // Un-disable it!
+                                       $this.prop( 'disabled', false );
+                               } else {
+                                       // Disable the checkbox, and uncheck it if it is checked
+                                       $this.prop( 'disabled', true );
+                                       if ( $this.prop( 'checked' ) ) {
+                                               $this.prop( 'checked', false );
+                                       }
+                               }
+                       } );
+               } );
        } );
 }( jQuery ) );
index 67460ad..d837420 100644 (file)
@@ -7,6 +7,7 @@
  * Changes:
  * - Add closure.
  * - Add this.JpegMeta assignment to expose it as global.
+ * - Add export as module.
  * - Add mw.libs.jpegmeta wrapper.
  */
 
            }
        };
 
-       // MediaWiki: Add mw.libs wrapper
-       mw.libs.jpegmeta = function( fileReaderResult, fileName ) {
+       // MediaWiki: Export as module
+       module.exports = function( fileReaderResult, fileName ) {
                return new JpegMeta.JpegFile( fileReaderResult, fileName );
        };
 
+       // MediaWiki: Add mw.libs wrapper
+       // @deprecated since 1.31
+       mw.log.deprecate( mw.libs, 'jpegmeta', module.exports );
+
 }( mediaWiki ) );
index ca9b252..d4ce55d 100644 (file)
@@ -28,6 +28,8 @@
                padding-left: 0.5em;
 
                &:not( .mw-recentchanges-toplinks-collapsed ) {
+                       margin-top: -1px; // Make up for `border` to prevent link movement
+                       margin-left: -1px;
                        margin-bottom: 0.5em;
                        border: 1px solid @colorGray12; // Same as the legend
                        padding: 0 0.5em 0.5em 0.5em;
index 1b8d6d9..1476241 100644 (file)
@@ -26,7 +26,8 @@
                                                        return true;
                                                }
                                        }
-                               } else if ( $input.is( 'input' ) ) { // <input> has defaultValue or defaultChecked
+                               } else if ( $input.is( 'input' ) || $input.is( 'textarea' ) ) {
+                                       // <input> has defaultValue or defaultChecked
                                        inputType = input.type;
                                        if ( inputType === 'radio' || inputType === 'checkbox' ) {
                                                if ( input.checked !== input.defaultChecked ) {
index de5ab87..9120e2a 100644 (file)
                                };
                                img.src = dataURL;
                        }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
+                               var jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
                                try {
-                                       meta = mw.libs.jpegmeta( data, file.fileName );
+                                       meta = jpegmeta( data, file.fileName );
                                        // eslint-disable-next-line no-underscore-dangle, camelcase
                                        meta._binary_data = null;
                                } catch ( e ) {
index d250ad8..f0ad056 100644 (file)
                this.limit = config.limit;
 
                if ( 'name' in config ) {
-                       // If used inside HTML form, then create hidden input, which will store
-                       // the results.
-                       this.hiddenInput = $( '<input>' )
-                               .attr( 'type', 'hidden' )
+                       // Use this instead of <input type="hidden">, because hidden inputs do not have separate
+                       // 'value' and 'defaultValue' properties. The script on Special:Preferences
+                       // (mw.special.preferences.confirmClose) checks this property to see if a field was changed.
+                       this.hiddenInput = $( '<textarea>' )
+                               .addClass( 'oo-ui-element-hidden' )
                                .attr( 'name', config.name )
                                .appendTo( this.$element );
-
                        // Update with preset values
                        this.updateHiddenInput();
+                       // Set the default value (it might be different from just being empty)
+                       this.hiddenInput.prop( 'defaultValue', this.getSelectedUsernames().join( '\n' ) );
                }
 
                this.menu = this.getMenu();
        mw.widgets.UsersMultiselectWidget.prototype.updateHiddenInput = function () {
                if ( 'hiddenInput' in this ) {
                        this.hiddenInput.val( this.getSelectedUsernames().join( '\n' ) );
-                       // Hidden inputs do not trigger onChange.
-                       // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
+                       // Trigger a 'change' event as if a user edited the text
+                       // (it is not triggered when changing the value from JS code).
                        this.hiddenInput.trigger( 'change' );
                }
        };
index 2efacc9..7d4ed53 100644 (file)
                if ( file && file.type === 'image/jpeg' ) {
                        fileReader = new FileReader();
                        fileReader.onload = function () {
-                               var fileStr, arr, i, metadata;
+                               var fileStr, arr, i, metadata,
+                                       jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
 
                                if ( typeof fileReader.result === 'string' ) {
                                        fileStr = fileReader.result;
                                }
 
                                try {
-                                       metadata = mw.libs.jpegmeta( fileStr, file.name );
+                                       metadata = jpegmeta( fileStr, file.name );
                                } catch ( e ) {
                                        metadata = null;
                                }
index 4b0b187..f8a745e 100644 (file)
@@ -37,12 +37,6 @@ $maintenance->setup();
 // We used to call this variable $self, but it was moved
 // to $maintenance->mSelf. Keep that here for b/c
 $self = $maintenance->getName();
-global $IP;
-# Get profiler configuraton
-$wgProfiler = [];
-if ( file_exists( "$IP/StartProfiler.php" ) ) {
-       require "$IP/StartProfiler.php";
-}
 # Start the autoloader, so that extensions can derive classes from core files
 require_once "$IP/includes/AutoLoader.php";
 
index 88cf0e0..7e58555 100644 (file)
@@ -32,25 +32,27 @@ use Wikimedia\Rdbms\LoadMonitorNull;
  * @covers \Wikimedia\Rdbms\LoadBalancer
  */
 class LoadBalancerTest extends MediaWikiTestCase {
-       public function testWithoutReplica() {
+       private function makeServerConfig() {
                global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
 
-               $servers = [
-                       [
-                               'host'        => $wgDBserver,
-                               'dbname'      => $wgDBname,
-                               'tablePrefix' => $this->dbPrefix(),
-                               'user'        => $wgDBuser,
-                               'password'    => $wgDBpassword,
-                               'type'        => $wgDBtype,
-                               'dbDirectory' => $wgSQLiteDataDir,
-                               'load'        => 0,
-                               'flags'       => DBO_TRX // REPEATABLE-READ for consistency
-                       ],
+               return [
+                       'host' => $wgDBserver,
+                       'dbname' => $wgDBname,
+                       'tablePrefix' => $this->dbPrefix(),
+                       'user' => $wgDBuser,
+                       'password' => $wgDBpassword,
+                       'type' => $wgDBtype,
+                       'dbDirectory' => $wgSQLiteDataDir,
+                       'load' => 0,
+                       'flags' => DBO_TRX // REPEATABLE-READ for consistency
                ];
+       }
+
+       public function testWithoutReplica() {
+               global $wgDBname;
 
                $lb = new LoadBalancer( [
-                       'servers' => $servers,
+                       'servers' => [ $this->makeServerConfig() ],
                        'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
                        'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
                ] );
@@ -259,4 +261,39 @@ class LoadBalancerTest extends MediaWikiTestCase {
 
                $this->assertFalse( $lb->getServerAttributes( 1 )[Database::ATTR_DB_LEVEL_LOCKING] );
        }
+
+       /**
+        * @covers LoadBalancer::openConnection()
+        * @covers LoadBalancer::getAnyOpenConnection()
+        */
+       function testOpenConnection() {
+               global $wgDBname;
+
+               $lb = new LoadBalancer( [
+                       'servers' => [ $this->makeServerConfig() ],
+                       'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
+               ] );
+
+               $i = $lb->getWriterIndex();
+               $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) );
+               $conn1 = $lb->getConnection( $i );
+               $this->assertNotEquals( null, $conn1 );
+               $this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) );
+               $conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
+               $this->assertNotEquals( null, $conn2 );
+               if ( $lb->getServerAttributes( $i )[Database::ATTR_DB_LEVEL_LOCKING] ) {
+                       $this->assertEquals( null,
+                               $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
+                       $this->assertEquals( $conn1,
+                               $lb->getConnection(
+                                       $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ), $lb::CONN_TRX_AUTOCOMMIT );
+               } else {
+                       $this->assertEquals( $conn2,
+                               $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
+                       $this->assertEquals( $conn2,
+                               $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ) );
+               }
+
+               $lb->closeAll();
+       }
 }
index 59a1c7c..02b3549 100644 (file)
@@ -1,9 +1,6 @@
 <?php
-/**
- * This file test the CSSMin library shipped with Mediawiki.
- *
- * @author Timo Tijhof
- */
+
+use Wikimedia\TestingAccessWrapper;
 
 /**
  * @group ResourceLoader
@@ -14,8 +11,8 @@ class CSSMinTest extends MediaWikiTestCase {
        protected function setUp() {
                parent::setUp();
 
-               $server = 'http://doc.example.org';
-
+               // For wfExpandUrl
+               $server = 'https://expand.example';
                $this->setMwGlobals( [
                        'wgServer' => $server,
                        'wgCanonicalServer' => $server,
@@ -23,7 +20,7 @@ class CSSMinTest extends MediaWikiTestCase {
        }
 
        /**
-        * @dataProvider serializeStringValueProvider
+        * @dataProvider provideSerializeStringValue
         * @covers CSSMin::serializeStringValue
         */
        public function testSerializeStringValue( $input, $expected ) {
@@ -35,7 +32,7 @@ class CSSMinTest extends MediaWikiTestCase {
                );
        }
 
-       public function serializeStringValueProvider() {
+       public static function provideSerializeStringValue() {
                return [
                        [ 'Hello World!', '"Hello World!"' ],
                        [ "Null\0Null", "\"Null\\fffd Null\"" ],
@@ -60,7 +57,7 @@ class CSSMinTest extends MediaWikiTestCase {
        }
 
        /**
-        * @dataProvider mimeTypeProvider
+        * @dataProvider provideMimeType
         * @covers CSSMin::getMimeType
         */
        public function testGetMimeType( $fileContents, $fileExtension, $expected ) {
@@ -71,7 +68,7 @@ class CSSMinTest extends MediaWikiTestCase {
                $this->assertSame( $expected, CSSMin::getMimeType( $fileName ) );
        }
 
-       public function mimeTypeProvider() {
+       public static function provideMimeType() {
                return [
                        'JPEG with short extension' => [
                                "\xFF\xD8\xFF",
@@ -214,7 +211,8 @@ class CSSMinTest extends MediaWikiTestCase {
         * @covers CSSMin::isRemoteUrl
         */
        public function testIsRemoteUrl( $expect, $url ) {
-               $this->assertEquals( CSSMinTestable::isRemoteUrl( $url ), $expect );
+               $class = TestingAccessWrapper::newFromClass( CSSMin::class );
+               $this->assertEquals( $class->isRemoteUrl( $url ), $expect );
        }
 
        public static function provideIsLocalUrls() {
@@ -231,7 +229,8 @@ class CSSMinTest extends MediaWikiTestCase {
         * @covers CSSMin::isLocalUrl
         */
        public function testIsLocalUrl( $expect, $url ) {
-               $this->assertEquals( CSSMinTestable::isLocalUrl( $url ), $expect );
+               $class = TestingAccessWrapper::newFromClass( CSSMin::class );
+               $this->assertEquals( $class->isLocalUrl( $url ), $expect );
        }
 
        /**
@@ -280,7 +279,7 @@ class CSSMinTest extends MediaWikiTestCase {
                        [
                                'Expand absolute paths',
                                [ 'foo { prop: url(/w/skin/images/bar.png); }', false, 'http://example.org/quux', false ],
-                               'foo { prop: url(http://doc.example.org/w/skin/images/bar.png); }',
+                               'foo { prop: url(https://expand.example/w/skin/images/bar.png); }',
                        ],
                        [
                                "Don't barf at behavior: url(#default#behaviorName) - T162973",
@@ -384,12 +383,12 @@ class CSSMinTest extends MediaWikiTestCase {
                        [
                                'Domain-relative URL',
                                'foo { background: url(/static/foo.png); }',
-                               'foo { background: url(http://doc.example.org/static/foo.png); }',
+                               'foo { background: url(https://expand.example/static/foo.png); }',
                        ],
                        [
                                'Domain-relative URL with query',
                                'foo { background: url(/static/foo.png?query=yes); }',
-                               'foo { background: url(http://doc.example.org/static/foo.png?query=yes); }',
+                               'foo { background: url(https://expand.example/static/foo.png?query=yes); }',
                        ],
                        [
                                'Remote URL (unnecessary quotes not preserved)',
@@ -493,12 +492,12 @@ class CSSMinTest extends MediaWikiTestCase {
                        [
                                '@import rule to local file (should we remap this?)',
                                '@import url(/styles.css)',
-                               '@import url(http://doc.example.org/styles.css)',
+                               '@import url(https://expand.example/styles.css)',
                        ],
                        [
                                '@import rule to local file (should we remap this?)',
                                '@import url(/styles.css)',
-                               '@import url(http://doc.example.org/styles.css)',
+                               '@import url(https://expand.example/styles.css)',
                        ],
                        [
                                '@import rule to URL',
@@ -634,13 +633,3 @@ class CSSMinTest extends MediaWikiTestCase {
                ];
        }
 }
-
-class CSSMinTestable extends CSSMin {
-       // Make some protected methods public
-       public static function isRemoteUrl( $maybeUrl ) {
-               return parent::isRemoteUrl( $maybeUrl );
-       }
-       public static function isLocalUrl( $maybeUrl ) {
-               return parent::isLocalUrl( $maybeUrl );
-       }
-}
index 5dc7a96..35d4ea0 100644 (file)
@@ -94,7 +94,11 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
                                                'NoVersionGiven' => '1.0',
                                        ]
                                ],
-                               [ 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.' ],
+                               [ [
+                                       'incompatible' => 'FakeExtension',
+                                       'type' => 'incompatible-extensions',
+                                       'msg' => 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.'
+                               ] ],
                        ],
                        [
                                [
@@ -102,7 +106,11 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
                                                'Missing' => '*',
                                        ]
                                ],
-                               [ 'FakeExtension requires Missing to be installed.' ],
+                               [ [
+                                       'missing' => 'Missing',
+                                       'type' => 'missing-extensions',
+                                       'msg' => 'FakeExtension requires Missing to be installed.',
+                               ] ],
                        ],
                        [
                                [
@@ -110,8 +118,12 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
                                                'FakeDependency' => '2.0.0',
                                        ]
                                ],
-                               // phpcs:ignore Generic.Files.LineLength.TooLong
-                               [ 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.' ],
+                               [ [
+                                       'incompatible' => 'FakeExtension',
+                                       'type' => 'incompatible-extensions',
+                                       // phpcs:ignore Generic.Files.LineLength.TooLong
+                                       'msg' => 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.'
+                               ] ],
                        ]
                ];
        }
@@ -128,7 +140,11 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
                                        'version' => 'not really valid',
                                ],
                        ] );
-               $this->assertEquals( [ "FakeDependency does not have a valid version string." ],
+               $this->assertEquals(
+                       [ [
+                               'type' => 'invalid-version',
+                               'msg' => "FakeDependency does not have a valid version string."
+                       ] ],
                        $checker->checkArray( [
                                'FakeExtension' => [
                                        'extensions' => [
index fa4d804..4d98773 100644 (file)
@@ -103,6 +103,11 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( '{"foo":"Example"}', $blob, 'Generated blob' );
        }
 
+       /**
+        * Seems to fail sometimes (T176097).
+        *
+        * @group Broken
+        */
        public function testGetBlobCached() {
                $module = $this->makeModule( [ 'example' ] );
                $rl = new ResourceLoader();
index 0ea4e2b..c917882 100644 (file)
@@ -4,6 +4,8 @@ class ResourceLoaderModuleTest extends ResourceLoaderTestCase {
 
        /**
         * @covers ResourceLoaderModule::getVersionHash
+        * @covers ResourceLoaderModule::getModifiedTime
+        * @covers ResourceLoaderModule::getModifiedHash
         */
        public function testGetVersionHash() {
                $context = $this->getResourceLoaderContext();
index 0b05ac1..42bc0a7 100644 (file)
@@ -1,5 +1,5 @@
 ( function ( mw, $ ) {
-       QUnit.module( 'mediawiki (mw.loader)', QUnit.newMwEnvironment( {
+       QUnit.module( 'mediawiki.loader', QUnit.newMwEnvironment( {
                setup: function ( assert ) {
                        mw.loader.store.enabled = false;
 
                        }
                        // Remove any remaining temporary statics
                        // exposed for cross-file mocks.
-                       if ( 'testCallback' in mw.loader ) {
-                               delete mw.loader.testCallback;
-                       }
-                       if ( 'testFail' in mw.loader ) {
-                               delete mw.loader.testFail;
-                       }
+                       delete mw.loader.testCallback;
+                       delete mw.loader.testFail;
                }
        } ) );
 
                );
        }
 
-       QUnit.test( 'Basic', function ( assert ) {
-               var isAwesomeDone;
-
+       QUnit.test( '.using( .., Function callback ) Promise', function ( assert ) {
+               var script = 0, callback = 0;
                mw.loader.testCallback = function () {
-                       assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
-                       isAwesomeDone = true;
+                       script++;
                };
+               mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ] );
 
-               mw.loader.implement( 'test.callback', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ] );
-
-               return mw.loader.using( 'test.callback', function () {
-                       assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' );
-               }, function () {
-                       assert.ok( false, 'Error callback fired while loader.using "test.callback" module' );
+               return mw.loader.using( 'test.promise', function () {
+                       callback++;
+               } ).then( function () {
+                       assert.strictEqual( script, 1, 'module script ran' );
+                       assert.strictEqual( callback, 1, 'using() callback ran' );
                } );
        } );
 
-       QUnit.test( 'Object method as module name', function ( assert ) {
-               var isAwesomeDone;
-
+       QUnit.test( 'Prototype method as module name', function ( assert ) {
+               var call = 0;
                mw.loader.testCallback = function () {
-                       assert.strictEqual( isAwesomeDone, undefined, 'Implementing module hasOwnProperty: isAwesomeDone should still be undefined' );
-                       isAwesomeDone = true;
+                       call++;
                };
-
                mw.loader.implement( 'hasOwnProperty', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ], {}, {} );
 
                return mw.loader.using( 'hasOwnProperty', function () {
-                       assert.strictEqual( isAwesomeDone, true, 'hasOwnProperty module should\'ve caused isAwesomeDone to be true' );
-               }, function () {
-                       assert.ok( false, 'Error callback fired while loader.using "hasOwnProperty" module' );
+                       assert.strictEqual( call, 1, 'module script ran' );
                } );
        } );
 
-       QUnit.test( '.using( .. ) Promise', 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/mwLoaderTestCallback.js' ) ] );
-
-               return mw.loader.using( 'test.promise' )
-                       .done( function () {
-                               assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
-                       } )
-                       .fail( function () {
-                               assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
-                       } );
-       } );
-
        // Covers mw.loader#sortDependencies (with native Set if available)
-       QUnit.test( '.using() Error: Circular dependency [StringSet default]', function ( assert ) {
+       QUnit.test( '.using() Error: Circular dependency [StringSet default]', function ( assert ) {
                var done = assert.async();
 
                mw.loader.register( [
        } );
 
        // @covers mw.loader#sortDependencies (with fallback shim)
-       QUnit.test( '.using() Error: Circular dependency [StringSet shim]', function ( assert ) {
+       QUnit.test( '.using() Error: Circular dependency [StringSet shim]', function ( assert ) {
                var done = assert.async();
 
                if ( !window.Set ) {
                mw.loader.load( 'test.implement.d' );
        } );
 
+       QUnit.test( '.implement( messages before script )', function ( assert ) {
+               mw.loader.implement(
+                       'test.implement.order',
+                       function () {
+                               assert.equal( mw.loader.getState( 'test.implement.order' ), 'executing', 'state during script execution' );
+                               assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' );
+                       },
+                       {},
+                       {
+                               'test-foobar': 'Hello Foobar, $1!'
+                       }
+               );
+
+               return mw.loader.using( 'test.implement.order' ).then( function () {
+                       assert.equal( mw.loader.getState( 'test.implement.order' ), 'ready', 'final success state' );
+               } );
+       } );
+
        // @import (T33676)
-       QUnit.test( '.implement( styles has @import )', function ( assert ) {
-               var isJsExecuted, $element,
+       QUnit.test( '.implement( styles with @import )', function ( assert ) {
+               var $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'
                                                + '\');\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' );
-               } );
+               return mw.loader.using( 'test.implement.import' );
        } );
 
        QUnit.test( '.implement( dependency with styles )', function ( assert ) {
 
                return mw.loader.using( 'test.implement.msgs', function () {
                        assert.ok( mw.messages.exists( 'T31107' ), 'T31107: messages-only module should implement ok' );
-               }, function () {
-                       assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' );
                } );
        } );
 
                ] );
 
                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"' );
+                       assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module "testMissing" state' );
+                       assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module "testUsesMissing" state' );
+                       assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module "testUsesNestedMissing" state' );
                }
 
                mw.loader.using( [ 'testUsesNestedMissing' ],
                        [ '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' ],
+               return mw.loader.using( [ 'testUsesSkippable' ] ).then(
                        function () {
-                               assert.ok( true, 'Success handler should be invoked.' );
-                               assert.ok( true ); // Dummy to match error handler and reach QUnit expect()
-
-                               verifyModuleStates();
+                               assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Skipped module' );
+                               assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Regular module' );
+                               assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Regular module with skippable dependency' );
                        },
                        function ( e, badmodules ) {
-                               assert.ok( false, 'Error handler should not be invoked.' );
-                               assert.deepEqual( badmodules, [], 'Bad modules as expected.' );
-
-                               verifyModuleStates();
+                               // Should not fail and QUnit would already catch this,
+                               // but add a handler anyway to report details from 'badmodules
+                               assert.deepEqual( badmodules, [], 'Bad modules' );
                        }
                );
        } );
        } );
 
        QUnit.test( 'Stale response caching - backcompat', function ( assert ) {
-               var count = 0;
+               var script = 0;
                mw.loader.store.enabled = true;
                mw.loader.register( 'test.stalebc', 'v2' );
                assert.strictEqual( mw.loader.store.get( 'test.stalebc' ), false, 'Not in store' );
 
                mw.loader.implement( 'test.stalebc', function () {
-                       count++;
+                       script++;
                } );
 
                return mw.loader.using( 'test.stalebc' )
                        .then( function () {
-                               assert.strictEqual( count, 1 );
+                               assert.strictEqual( script, 1, 'module script ran' );
                                assert.strictEqual( mw.loader.getState( 'test.stalebc' ), 'ready' );
                                assert.ok( mw.loader.store.get( 'test.stalebc' ), 'In store' );
                        } )
                        } catch ( e ) {
                                assert.equal( null, String( e ), 'require works asynchrously in debug mode' );
                        }
-               }, function () {
-                       assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
                } );
        } );
 
        QUnit.test( 'Implicit dependencies', function ( assert ) {
-               var ranUser = false,
-                       userSeesSite = false,
-                       ranSite = false;
+               var user = 0,
+                       site = 0,
+                       siteFromUser = 0;
 
                mw.loader.implement(
                        'site',
                        function () {
-                               ranSite = true;
+                               site++;
                        }
                );
                mw.loader.implement(
                        'user',
                        function () {
-                               userSeesSite = ranSite;
-                               ranUser = true;
+                               user++;
+                               siteFromUser = site;
                        }
                );
 
-               assert.strictEqual( ranSite, false, 'verify site module not yet loaded' );
-               assert.strictEqual( ranUser, false, 'verify user module not yet loaded' );
                return mw.loader.using( 'user', function () {
-                       assert.strictEqual( ranSite, true, 'ran site module' );
-                       assert.strictEqual( ranUser, true, 'ran user module' );
-                       assert.strictEqual( userSeesSite, true, 'ran site before user module' );
-
+                       assert.strictEqual( site, 1, 'site module' );
+                       assert.strictEqual( user, 1, 'user module' );
+                       assert.strictEqual( siteFromUser, 1, 'site ran before user' );
+               } ).always( function () {
                        // Reset
                        mw.loader.moduleRegistry[ 'site' ].state = 'registered';
                        mw.loader.moduleRegistry[ 'user' ].state = 'registered';