Merge "Update weblinks in comments from HTTP to HTTPS"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 8 Nov 2016 21:32:00 +0000 (21:32 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 8 Nov 2016 21:32:00 +0000 (21:32 +0000)
30 files changed:
1  2 
includes/DefaultSettings.php
includes/FormOptions.php
includes/GlobalFunctions.php
includes/MediaWikiServices.php
includes/OutputPage.php
includes/api/ApiMain.php
includes/htmlform/fields/HTMLFloatField.php
includes/htmlform/fields/HTMLIntField.php
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobRunner.php
includes/libs/filebackend/FileBackend.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/page/WikiPage.php
includes/parser/Parser.php
includes/resourceloader/ResourceLoader.php
includes/user/User.php
includes/utils/UIDGenerator.php
resources/src/mediawiki.skinning/content.css
resources/src/mediawiki/mediawiki.js
tests/parser/parserTests.txt
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/upload/UploadBaseTest.php
tests/phpunit/suites/ParserTestTopLevelSuite.php

@@@ -75,7 -75,7 +75,7 @@@ $wgConfigRegistry = 
   * MediaWiki version number
   * @since 1.2
   */
 -$wgVersion = '1.28.0-alpha';
 +$wgVersion = '1.29.0-alpha';
  
  /**
   * Name of the site. It must be changed in LocalSettings.php
@@@ -311,7 -311,7 +311,7 @@@ $wgAppleTouchIcon = false
   * Value for the referrer policy meta tag.
   * One of 'never', 'default', 'origin', 'always'. Setting it to false just
   * prevents the meta tag from being output.
-  * See http://www.w3.org/TR/referrer-policy/ for details.
+  * See https://www.w3.org/TR/referrer-policy/ for details.
   *
   * @since 1.25
   */
@@@ -658,7 -658,7 +658,7 @@@ $wgLockManagers = []
  
  /**
   * Show Exif data, on by default if available.
-  * Requires PHP's Exif extension: http://www.php.net/manual/en/ref.exif.php
+  * Requires PHP's Exif extension: https://secure.php.net/manual/en/ref.exif.php
   *
   * @note FOR WINDOWS USERS:
   * To enable Exif functions, add the following line to the "Windows
@@@ -1511,7 -1511,7 +1511,7 @@@ $wgDjvuTxt = null
   * For now we recommend you use djvudump instead. The djvuxml output is
   * probably more stable, so we'll switch back to it as soon as they fix
   * the efficiency problem.
-  * http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583
+  * https://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583
   *
   * @par Example:
   * @code
@@@ -1973,7 -1973,7 +1973,7 @@@ $wgDBerrorLog = false
   * Defaults to the wiki timezone ($wgLocaltimezone).
   *
   * A list of usable timezones can found at:
-  * http://php.net/manual/en/timezones.php
+  * https://secure.php.net/manual/en/timezones.php
   *
   * @par Examples:
   * @code
@@@ -3110,7 -3110,7 +3110,7 @@@ $wgForceUIMsgAsContentMsg = []
   * timezone-nameinlowercase like timezone-utc.
   *
   * A list of usable timezones can found at:
-  * http://php.net/manual/en/timezones.php
+  * https://secure.php.net/manual/en/timezones.php
   *
   * @par Examples:
   * @code
@@@ -3178,7 -3178,7 +3178,7 @@@ $wgHtml5 = true
   *
   * If your wiki uses RDFa, set it to the correct value for RDFa+HTML5.
   * Correct current values are 'HTML+RDFa 1.0' or 'XHTML+RDFa 1.0'.
-  * See also http://www.w3.org/TR/rdfa-in-html/#document-conformance
+  * See also https://www.w3.org/TR/rdfa-in-html/#document-conformance
   * @since 1.16
   */
  $wgHtml5Version = null;
@@@ -4216,7 -4216,7 +4216,7 @@@ $wgAllowImageTag = false
  /**
   * Configuration for HTML postprocessing tool. Set this to a configuration
   * array to enable an external tool. Dave Raggett's "HTML Tidy" is typically
-  * used. See http://www.w3.org/People/Raggett/tidy/
+  * used. See https://www.w3.org/People/Raggett/tidy/
   *
   * If this is null and $wgUseTidy is true, the deprecated configuration
   * parameters will be used instead.
@@@ -4363,9 -4363,9 +4363,9 @@@ $wgTranscludeCacheExpiry = 3600
   * @since 1.28
   */
  $wgEnableMagicLinks = [
 -      'ISBN' => true,
 -      'PMID' => true,
 -      'RFC' => true
 +      'ISBN' => false,
 +      'PMID' => false,
 +      'RFC' => false
  ];
  
  /** @} */ # end of parser settings }
@@@ -5747,8 -5747,6 +5747,8 @@@ $wgGrantPermissions['viewdeleted']['bro
  $wgGrantPermissions['viewdeleted']['deletedhistory'] = true;
  $wgGrantPermissions['viewdeleted']['deletedtext'] = true;
  
 +$wgGrantPermissions['viewrestrictedlogs']['suppressionlog'] = true;
 +
  $wgGrantPermissions['delete'] = $wgGrantPermissions['editpage'] +
        $wgGrantPermissions['viewdeleted'];
  $wgGrantPermissions['delete']['delete'] = true;
@@@ -5799,7 -5797,6 +5799,7 @@@ $wgGrantPermissionGroups = 
        'blockusers'          => 'administration',
        'delete'              => 'administration',
        'viewdeleted'         => 'administration',
 +      'viewrestrictedlogs'  => 'administration',
        'protect'             => 'administration',
        'createaccount'       => 'administration',
  
@@@ -6375,9 -6372,9 +6375,9 @@@ $wgDisableInternalSearch = false
   * To forward to Google you'd have something like:
   * @code
   * $wgSearchForwardUrl =
-  *     'http://www.google.com/search?q=$1' .
-  *     '&domains=http://example.com' .
-  *     '&sitesearch=http://example.com' .
+  *     'https://www.google.com/search?q=$1' .
+  *     '&domains=https://example.com' .
+  *     '&sitesearch=https://example.com' .
   *     '&ie=utf-8&oe=utf-8';
   * @endcode
   */
@@@ -6710,9 -6707,9 +6710,9 @@@ $wgFeedDiffCutoff = 32768
   * Should be a format as key (either 'rss' or 'atom') and an URL to the feed
   * as value.
   * @par Example:
-  * Configure the 'atom' feed to http://example.com/somefeed.xml
+  * Configure the 'atom' feed to https://example.com/somefeed.xml
   * @code
-  * $wgSiteFeed['atom'] = "http://example.com/somefeed.xml";
+  * $wgSiteFeed['atom'] = "https://example.com/somefeed.xml";
   * @endcode
   */
  $wgOverrideSiteFeed = [];
@@@ -7121,7 -7118,7 +7121,7 @@@ $wgAutoloadAttemptLowercase = true
   *         'Foo Barstein',
   *     ],
   *     'version' => '1.9.0',
-  *     'url' => 'http://example.org/example-extension/',
+  *     'url' => 'https://example.org/example-extension/',
   *     'descriptionmsg' => 'exampleextension-desc',
   *     'license-name' => 'GPL-2.0+',
   * ];
   * - author: A string or an array of strings. Authors can be linked using
   *    the regular wikitext link syntax. To have an internationalized version of
   *    "and others" show, add an element "...". This element can also be linked,
-  *    for instance "[http://example ...]".
+  *    for instance "[https://example ...]".
   *
   * - descriptionmsg: A message key or an an array with message key and parameters:
   *    `'descriptionmsg' => 'exampleextension-desc',`
@@@ -7366,7 -7363,7 +7366,7 @@@ $wgCategoryPagingLimit = 200
   *     all languages in a mediocre way. However, it is better than "uppercase".
   *
   * To use the uca-default collation, you must have PHP's intl extension
-  * installed. See http://php.net/manual/en/intl.setup.php . The details of the
+  * installed. See https://secure.php.net/manual/en/intl.setup.php . The details of the
   * resulting collation will depend on the version of ICU installed on the
   * server.
   *
@@@ -8035,7 -8032,7 +8035,7 @@@ $wgShellCgroup = false
  $wgPhpCli = '/usr/bin/php';
  
  /**
-  * Locale for LC_CTYPE, to work around http://bugs.php.net/bug.php?id=45132
+  * Locale for LC_CTYPE, to work around https://bugs.php.net/bug.php?id=45132
   * For Unix-like operating systems, set this to to a locale that has a UTF-8
   * character set. Only the character set is relevant.
   */
diff --combined includes/FormOptions.php
@@@ -52,9 -52,6 +52,9 @@@ class FormOptions implements ArrayAcces
         * This is useful for the namespace selector.
         */
        const INTNULL = 3;
 +      /** Array type, maps guessType() to WebRequest::getArray()
 +       * @since 1.29 */
 +      const ARR = 5;
        /* @} */
  
        /**
                        return self::FLOAT;
                } elseif ( is_string( $data ) ) {
                        return self::STRING;
 +              } elseif ( is_array( $data ) ) {
 +                      return self::ARR;
                } else {
                        throw new MWException( 'Unsupported datatype' );
                }
                                        break;
                                case self::INTNULL:
                                        $value = $r->getIntOrNull( $name );
 +                                      break;
 +                              case self::ARR:
 +                                      $value = $r->getArray( $name );
                                        break;
                                default:
                                        throw new MWException( 'Unsupported datatype' );
  
        /** @name ArrayAccess functions
         * These functions implement the ArrayAccess PHP interface.
-        * @see http://php.net/manual/en/class.arrayaccess.php
+        * @see https://secure.php.net/manual/en/class.arrayaccess.php
         */
        /* @{ */
        /**
@@@ -40,7 -40,7 +40,7 @@@ use Wikimedia\ScopedCallback
   */
  
  // hash_equals function only exists in PHP >= 5.6.0
- // http://php.net/hash_equals
+ // https://secure.php.net/hash_equals
  if ( !function_exists( 'hash_equals' ) ) {
        /**
         * Check whether a user-provided string is equal to a fixed-length secret string
@@@ -1625,13 -1625,11 +1625,13 @@@ function wfShowingResults( $offset, $li
  }
  
  /**
 - * @todo document
 - * @todo FIXME: We may want to blacklist some broken browsers
 + * Whether the client accept gzip encoding
 + *
 + * Uses the Accept-Encoding header to check if the client supports gzip encoding.
 + * Use this when considering to send a gzip-encoded response to the client.
   *
 - * @param bool $force
 - * @return bool Whereas client accept gzip compression
 + * @param bool $force Forces another check even if we already have a cached result.
 + * @return bool
   */
  function wfClientAcceptsGzip( $force = false ) {
        static $result = null;
  function wfEscapeWikiText( $text ) {
        global $wgEnableMagicLinks;
        static $repl = null, $repl2 = null;
 -      if ( $repl === null ) {
 +      if ( $repl === null || defined( 'MW_PARSER_TEST' ) || defined( 'MW_PHPUNIT_TEST' ) ) {
 +              // Tests depend upon being able to change $wgEnableMagicLinks, so don't cache
 +              // in those situations
                $repl = [
                        '"' => '&#34;', '&' => '&#38;', "'" => '&#39;', '<' => '&#60;',
                        '=' => '&#61;', '>' => '&#62;', '[' => '&#91;', ']' => '&#93;',
@@@ -2138,7 -2134,7 +2138,7 @@@ function wfMkdirParents( $dir, $mode = 
   */
  function wfRecursiveRemoveDir( $dir ) {
        wfDebug( __FUNCTION__ . "( $dir )\n" );
-       // taken from http://de3.php.net/manual/en/function.rmdir.php#98622
+       // taken from https://secure.php.net/manual/en/function.rmdir.php#98622
        if ( is_dir( $dir ) ) {
                $objects = scandir( $dir );
                foreach ( $objects as $object ) {
@@@ -2231,8 -2227,8 +2231,8 @@@ function wfEscapeShellArg( /*...*/ ) 
                        // Escaping for an MSVC-style command line parser and CMD.EXE
                        // @codingStandardsIgnoreStart For long URLs
                        // Refs:
-                       //  * http://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html
-                       //  * http://technet.microsoft.com/en-us/library/cc723564.aspx
+                       //  * https://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html
+                       //  * https://technet.microsoft.com/en-us/library/cc723564.aspx
                        //  * T15518
                        //  * CR r63214
                        // Double the backslashes before any double quotes. Escape the double quotes.
@@@ -2332,7 -2328,7 +2332,7 @@@ function wfShellExec( $cmd, &$retval = 
                if ( wfIsWindows() ) {
                        /* Surrounding a set in quotes (method used by wfEscapeShellArg) makes the quotes themselves
                         * appear in the environment variable, so we must use carat escaping as documented in
-                        * http://technet.microsoft.com/en-us/library/cc723564.aspx
+                        * https://technet.microsoft.com/en-us/library/cc723564.aspx
                         * Note however that the quote isn't listed there, but is needed, and the parentheses
                         * are listed there but doesn't appear to need it.
                         */
@@@ -2550,7 -2546,7 +2550,7 @@@ function wfShellExecWithStderr( $cmd, &
  }
  
  /**
-  * Workaround for http://bugs.php.net/bug.php?id=45132
+  * Workaround for https://bugs.php.net/bug.php?id=45132
   * escapeshellarg() destroys non-ASCII characters if LANG is not a UTF-8 locale
   */
  function wfInitShellLocale() {
@@@ -2802,7 -2798,7 +2802,7 @@@ function wfUseMW( $req_ver ) 
  /**
   * Return the final portion of a pathname.
   * Reimplemented because PHP5's "basename()" is buggy with multibyte text.
-  * http://bugs.php.net/bug.php?id=33898
+  * https://bugs.php.net/bug.php?id=33898
   *
   * PHP's basename() only considers '\' a pathchar on Windows and Netware.
   * We'll consider it so always, as we don't want '\s' in our Unix paths either.
@@@ -3,7 -3,6 +3,7 @@@ namespace MediaWiki
  
  use Config;
  use ConfigFactory;
 +use CryptHKDF;
  use CryptRand;
  use EventRelayerGroup;
  use GenderCache;
@@@ -22,7 -21,6 +22,7 @@@ use MediaWiki\Services\NoSuchServiceExc
  use MWException;
  use MimeAnalyzer;
  use ObjectCache;
 +use Parser;
  use ProxyLookup;
  use SearchEngine;
  use SearchEngineConfig;
@@@ -184,7 -182,7 +184,7 @@@ class MediaWikiServices extends Service
  
                $oldInstance = self::$instance;
  
 -              self::$instance = self::newInstance( $bootstrapConfig );
 +              self::$instance = self::newInstance( $bootstrapConfig, 'load' );
                self::$instance->importWiring( $oldInstance, [ 'BootstrapConfig' ] );
  
                if ( $quick === 'quick' ) {
                } else {
                        $oldInstance->destroy();
                }
 -
        }
  
        /**
                self::resetGlobalInstance();
  
                // Child, reseed because there is no bug in PHP:
-               // http://bugs.php.net/bug.php?id=42465
+               // https://bugs.php.net/bug.php?id=42465
                mt_srand( getmypid() );
        }
  
                return $this->getService( 'CryptRand' );
        }
  
 +      /**
 +       * @since 1.28
 +       * @return CryptHKDF
 +       */
 +      public function getCryptHKDF() {
 +              return $this->getService( 'CryptHKDF' );
 +      }
 +
        /**
         * @since 1.28
         * @return MediaHandlerFactory
                return $this->getService( 'ProxyLookup' );
        }
  
 +      /**
 +       * @since 1.29
 +       * @return Parser
 +       */
 +      public function getParser() {
 +              return $this->getService( 'Parser' );
 +      }
 +
        /**
         * @since 1.28
         * @return GenderCache
diff --combined includes/OutputPage.php
@@@ -1214,8 -1214,8 +1214,8 @@@ class OutputPage extends ContextSource 
        /**
         * Add new language links
         *
 -       * @param array $newLinkArray Associative array mapping language code to the page
 -       *                      name
 +       * @param string[] $newLinkArray Array of interwiki-prefixed (non DB key) titles
 +       *                               (e.g. 'fr:Test page')
         */
        public function addLanguageLinks( array $newLinkArray ) {
                $this->mLanguageLinks += $newLinkArray;
        /**
         * Reset the language links and add new language links
         *
 -       * @param array $newLinkArray Associative array mapping language code to the page
 -       *                      name
 +       * @param string[] $newLinkArray Array of interwiki-prefixed (non DB key) titles
 +       *                               (e.g. 'fr:Test page')
         */
        public function setLanguageLinks( array $newLinkArray ) {
                $this->mLanguageLinks = $newLinkArray;
        /**
         * Get the list of language links
         *
 -       * @return array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page')
 +       * @return string[] Array of interwiki-prefixed (non DB key) titles (e.g. 'fr:Test page')
         */
        public function getLanguageLinks() {
                return $this->mLanguageLinks;
                $popts->setTidy( $oldTidy );
  
                $this->addParserOutput( $parserOutput );
 -
        }
  
        /**
                }
  
                // Include profiling data
 -              $this->setLimitReportData( $parserOutput->getLimitReportData() );
 +              if ( !$this->limitReportData ) {
 +                      $this->setLimitReportData( $parserOutput->getLimitReportData() );
 +              }
  
                // Link flags are ignored for now, but may in the future be
                // used to mark individual language links.
                $this->setCdnMaxage( $this->mCdnMaxage );
        }
  
 +      /**
 +       * Get TTL in [$minTTL,$maxTTL] in pass it to lowerCdnMaxage()
 +       *
 +       * This sets and returns $minTTL if $mtime is false or null. Otherwise,
 +       * the TTL is higher the older the $mtime timestamp is. Essentially, the
 +       * TTL is 90% of the age of the object, subject to the min and max.
 +       *
 +       * @param string|integer|float|bool|null $mtime Last-Modified timestamp
 +       * @param integer $minTTL Mimimum TTL in seconds [default: 1 minute]
 +       * @param integer $maxTTL Maximum TTL in seconds [default: $wgSquidMaxage]
 +       * @return integer TTL in seconds
 +       * @since 1.28
 +       */
 +      public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
 +              $minTTL = $minTTL ?: IExpiringStore::TTL_MINUTE;
 +              $maxTTL = $maxTTL ?: $this->getConfig()->get( 'SquidMaxage' );
 +
 +              if ( $mtime === null || $mtime === false ) {
 +                      return $minTTL; // entity does not exist
 +              }
 +
 +              $age = time() - wfTimestamp( TS_UNIX, $mtime );
 +              $adaptiveTTL = max( .9 * $age, $minTTL );
 +              $adaptiveTTL = min( $adaptiveTTL, $maxTTL );
 +
 +              $this->lowerCdnMaxage( (int)$adaptiveTTL );
 +
 +              return $adaptiveTTL;
 +      }
 +
        /**
         * Use enableClientCache(false) to force it to send nocache headers
         *
                                        # We'll purge the proxy cache explicitly, but require end user agents
                                        # to revalidate against the proxy on each visit.
                                        # Surrogate-Control controls our CDN, Cache-Control downstream caches
 -                                      wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **", 'private' );
 +                                      wfDebug( __METHOD__ .
 +                                              ": proxy caching with ESI; {$this->mLastModified} **", 'private' );
                                        # start with a shorter timeout for initial testing
                                        # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
 -                                      $response->header( 'Surrogate-Control: max-age=' . $config->get( 'SquidMaxage' )
 -                                              . '+' . $this->mCdnMaxage . ', content="ESI/1.0"' );
 +                                      $response->header(
 +                                              "Surrogate-Control: max-age={$config->get( 'SquidMaxage' )}" .
 +                                              "+{$this->mCdnMaxage}, content=\"ESI/1.0\""
 +                                      );
                                        $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
                                } else {
                                        # We'll purge the proxy cache for anons explicitly, but require end user agents
                                        # to revalidate against the proxy on each visit.
                                        # IMPORTANT! The CDN needs to replace the Cache-Control header with
                                        # Cache-Control: s-maxage=0, must-revalidate, max-age=0
 -                                      wfDebug( __METHOD__ . ": local proxy caching; {$this->mLastModified} **", 'private' );
 +                                      wfDebug( __METHOD__ .
 +                                              ": local proxy caching; {$this->mLastModified} **", 'private' );
                                        # start with a shorter timeout for initial testing
                                        # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
 -                                      $response->header( 'Cache-Control: s-maxage=' . $this->mCdnMaxage
 -                                              . ', must-revalidate, max-age=0' );
 +                                      $response->header( "Cache-Control: " .
 +                                              "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" );
                                }
                        } else {
                                # We do want clients to cache if they can, but they *must* check for updates
        /**
         * Output a standard permission error page
         *
 -       * @param array $errors Error message keys
 +       * @param array $errors Error message keys or [key, param...] arrays
         * @param string $action Action that was denied or null if unknown
         */
        public function showPermissionsErrorPage( array $errors, $action = null ) {
 +              foreach ( $errors as $key => $error ) {
 +                      $errors[$key] = (array)$error;
 +              }
 +
                // For some action (read, edit, create and upload), display a "login to do this action"
                // error if all of the following conditions are met:
                // 1. the user is not logged in
                        $exemptStates = [];
                        $moduleStyles = $this->getModuleStyles( /*filter*/ true );
  
 -                      // Batch preload getTitleInfo for isKnownEmpty() calls below
 -                      $exemptModules = array_filter( $moduleStyles,
 -                              function ( $name ) use ( $rl, &$exemptGroups ) {
 -                                      $module = $rl->getModule( $name );
 -                                      return $module && isset( $exemptGroups[ $module->getGroup() ] );
 -                              }
 -                      );
 -                      ResourceLoaderWikiModule::preloadTitleInfo(
 -                              $context, wfGetDB( DB_REPLICA ), $exemptModules );
 +                      // Preload getTitleInfo for isKnownEmpty calls below and in ResourceLoaderClientHtml
 +                      // Separate user-specific batch for improved cache-hit ratio.
 +                      $userBatch = [ 'user.styles', 'user' ];
 +                      $siteBatch = array_diff( $moduleStyles, $userBatch );
 +                      $dbr = wfGetDB( DB_REPLICA );
 +                      ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $siteBatch );
 +                      ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $userBatch );
  
                        // Filter out modules handled by buildExemptModules()
                        $moduleStyles = array_filter( $moduleStyles,
                        // The spec recommends defining XHTML5's charset using the XML declaration
                        // instead of meta.
                        // Our XML declaration is output by Html::htmlHeader.
-                       // http://www.whatwg.org/html/semantics.html#attr-meta-http-equiv-content-type
-                       // http://www.whatwg.org/html/semantics.html#charset
+                       // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-type
+                       // https://html.spec.whatwg.org/multipage/semantics.html#charset
                        $pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] );
                }
  
                        }
                }
  
 -              $chunks[] = ResourceLoader::makeInlineScript(
 -                      ResourceLoader::makeConfigSetScript(
 -                              [ 'wgPageParseReport' => $this->limitReportData ],
 -                              true
 -                      )
 -              );
 +              if ( $this->limitReportData ) {
 +                      $chunks[] = ResourceLoader::makeInlineScript(
 +                              ResourceLoader::makeConfigSetScript(
 +                                      [ 'wgPageParseReport' => $this->limitReportData ],
 +                                      true
 +                              )
 +                      );
 +              }
  
                return self::combineWrappedStrings( $chunks );
        }
        public static function transformCssMedia( $media ) {
                global $wgRequest;
  
-               // http://www.w3.org/TR/css3-mediaqueries/#syntax
+               // https://www.w3.org/TR/css3-mediaqueries/#syntax
                $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
  
                // Switch in on-screen display for media testing
diff --combined includes/api/ApiMain.php
@@@ -636,8 -636,8 +636,8 @@@ class ApiMain extends ApiBase 
         * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
         * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
         * headers are set.
-        * http://www.w3.org/TR/cors/#resource-requests
-        * http://www.w3.org/TR/cors/#resource-preflight-requests
+        * https://www.w3.org/TR/cors/#resource-requests
+        * https://www.w3.org/TR/cors/#resource-preflight-requests
         *
         * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
         */
  
                        $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
                        $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
-                       // http://www.w3.org/TR/resource-timing/#timing-allow-origin
+                       // https://www.w3.org/TR/resource-timing/#timing-allow-origin
                        if ( $allowTiming !== false ) {
                                $response->header( "Timing-Allow-Origin: $allowTiming" );
                        }
                        'ip' => $request->getIP(),
                        'userAgent' => $this->getUserAgent(),
                        'wiki' => wfWikiID(),
 -                      'timeSpentBackend' => (int) round( $time * 1000 ),
 +                      'timeSpentBackend' => (int)round( $time * 1000 ),
                        'hadError' => $e !== null,
                        'errorCodes' => [],
                        'params' => [],
@@@ -4,11 -4,11 +4,11 @@@
   * A field that will contain a numeric value
   */
  class HTMLFloatField extends HTMLTextField {
 -      function getSize() {
 +      public function getSize() {
                return isset( $this->mParams['size'] ) ? $this->mParams['size'] : 20;
        }
  
 -      function validate( $value, $alldata ) {
 +      public function validate( $value, $alldata ) {
                $p = parent::validate( $value, $alldata );
  
                if ( $p !== true ) {
@@@ -17,7 -17,7 +17,7 @@@
  
                $value = trim( $value );
  
-               # http://www.w3.org/TR/html5/infrastructure.html#floating-point-numbers
+               # https://www.w3.org/TR/html5/infrastructure.html#floating-point-numbers
                # with the addition that a leading '+' sign is ok.
                if ( !preg_match( '/^((\+|\-)?\d+(\.\d+)?(E(\+|\-)?\d+)?)?$/i', $value ) ) {
                        return $this->msg( 'htmlform-float-invalid' )->parseAsBlock();
@@@ -4,14 -4,14 +4,14 @@@
   * A field that must contain a number
   */
  class HTMLIntField extends HTMLFloatField {
 -      function validate( $value, $alldata ) {
 +      public function validate( $value, $alldata ) {
                $p = parent::validate( $value, $alldata );
  
                if ( $p !== true ) {
                        return $p;
                }
  
-               # http://www.w3.org/TR/html5/infrastructure.html#signed-integers
+               # https://www.w3.org/TR/html5/infrastructure.html#signed-integers
                # with the addition that a leading '+' sign is ok. Note that leading zeros
                # are fine, and will be left in the input, which is useful for things like
                # phone numbers when you know that they are integers (the HTML5 type=tel
@@@ -21,7 -21,6 +21,7 @@@
   * @author Aaron Schulz
   */
  use MediaWiki\MediaWikiServices;
 +use Wikimedia\ScopedCallback;
  
  /**
   * Class to handle job queues stored in the DB
@@@ -69,7 -68,7 +69,7 @@@ class JobQueueDB extends JobQueue 
         * @return bool
         */
        protected function doIsEmpty() {
 -              $dbr = $this->getSlaveDB();
 +              $dbr = $this->getReplicaDB();
                try {
                        $found = $dbr->selectField( // unclaimed job
                                'job', '1', [ 'job_cmd' => $this->type, 'job_token' => '' ], __METHOD__
@@@ -94,7 -93,7 +94,7 @@@
                }
  
                try {
 -                      $dbr = $this->getSlaveDB();
 +                      $dbr = $this->getReplicaDB();
                        $size = (int)$dbr->selectField( 'job', 'COUNT(*)',
                                [ 'job_cmd' => $this->type, 'job_token' => '' ],
                                __METHOD__
                        return $count;
                }
  
 -              $dbr = $this->getSlaveDB();
 +              $dbr = $this->getReplicaDB();
                try {
                        $count = (int)$dbr->selectField( 'job', 'COUNT(*)',
                                [ 'job_cmd' => $this->type, "job_token != {$dbr->addQuotes( '' )}" ],
                        return $count;
                }
  
 -              $dbr = $this->getSlaveDB();
 +              $dbr = $this->getReplicaDB();
                try {
                        $count = (int)$dbr->selectField( 'job', 'COUNT(*)',
                                [
                $invertedDirection = false; // whether one job_random direction was already scanned
                // This uses a replication safe method for acquiring jobs. One could use UPDATE+LIMIT
                // instead, but that either uses ORDER BY (in which case it deadlocks in MySQL) or is
-               // not replication safe. Due to http://bugs.mysql.com/bug.php?id=6980, subqueries cannot
+               // not replication safe. Due to https://bugs.mysql.com/bug.php?id=6980, subqueries cannot
                // be used here with MySQL.
                do {
                        if ( $tinyQueue ) { // queue has <= MAX_OFFSET rows
                $row = false; // the row acquired
                do {
                        if ( $dbw->getType() === 'mysql' ) {
-                               // Per http://bugs.mysql.com/bug.php?id=6980, we can't use subqueries on the
+                               // Per https://bugs.mysql.com/bug.php?id=6980, we can't use subqueries on the
                                // same table being changed in an UPDATE query in MySQL (gives Error: 1093).
                                // Oracle and Postgre have no such limitation. However, MySQL offers an
                                // alternative here by supporting ORDER BY + LIMIT for UPDATE queries.
         * @return Iterator
         */
        protected function getJobIterator( array $conds ) {
 -              $dbr = $this->getSlaveDB();
 +              $dbr = $this->getReplicaDB();
                try {
                        return new MappedIterator(
                                $dbr->select( 'job', self::selectFields(), $conds ),
        }
  
        protected function doGetSiblingQueuesWithJobs( array $types ) {
 -              $dbr = $this->getSlaveDB();
 +              $dbr = $this->getReplicaDB();
                // @note: this does not check whether the jobs are claimed or not.
                // This is useful so JobQueueGroup::pop() also sees queues that only
                // have stale jobs. This lets recycleAndDeleteStaleJobs() re-enqueue
        }
  
        protected function doGetSiblingQueueSizes( array $types ) {
 -              $dbr = $this->getSlaveDB();
 +              $dbr = $this->getReplicaDB();
                $res = $dbr->select( 'job', [ 'job_cmd', 'COUNT(*) AS count' ],
                        [ 'job_cmd' => $types ], __METHOD__, [ 'GROUP BY' => 'job_cmd' ] );
  
         * @throws JobQueueConnectionError
         * @return DBConnRef
         */
 -      protected function getSlaveDB() {
 +      protected function getReplicaDB() {
                try {
                        return $this->getDB( DB_REPLICA );
                } catch ( DBConnectionError $e ) {
@@@ -26,7 -26,6 +26,7 @@@ use MediaWiki\Logger\LoggerFactory
  use Liuggio\StatsdClient\Factory\StatsdDataFactory;
  use Psr\Log\LoggerAwareInterface;
  use Psr\Log\LoggerInterface;
 +use Wikimedia\ScopedCallback;
  
  /**
   * Job queue runner utility methods
@@@ -336,7 -335,7 +336,7 @@@ class JobRunner implements LoggerAwareI
         */
        private function getMaxRssKb() {
                $info = wfGetRusage() ?: [];
-               // see http://linux.die.net/man/2/getrusage
+               // see https://linux.die.net/man/2/getrusage
                return isset( $info['ru_maxrss'] ) ? (int)$info['ru_maxrss'] : null;
        }
  
@@@ -30,7 -30,6 +30,7 @@@
   */
  use Psr\Log\LoggerAwareInterface;
  use Psr\Log\LoggerInterface;
 +use Wikimedia\ScopedCallback;
  
  /**
   * @brief Base class for all file backend classes (including multi-write backends).
@@@ -928,7 -927,7 +928,7 @@@ abstract class FileBackend implements L
         * @return ScopedCallback|null
         */
        final protected function getScopedPHPBehaviorForOps() {
-               if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+               if ( PHP_SAPI != 'cli' ) { // https://bugs.php.net/bug.php?id=47540
                        $old = ignore_user_abort( true ); // avoid half-finished operations
                        return new ScopedCallback( function () use ( $old ) {
                                ignore_user_abort( $old );
@@@ -176,6 -176,7 +176,6 @@@ class SwiftFileBackend extends FileBack
                return isset( $params['headers'] )
                        ? $this->getCustomHeaders( $params['headers'] )
                        : [];
 -
        }
  
        /**
                                        $this->rgwS3SecretKey,
                                        true // raw
                                ) );
-                               // See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
+                               // See https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
                                // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
                                // Note: S3 API is the rgw default; remove the /swift/ URL bit.
                                return str_replace( '/swift/v1', '', $this->storageUrl( $auth ) . $spath ) .
        /**
         * Set read/write permissions for a Swift container.
         *
-        * @see http://swift.openstack.org/misc.html#acls
+        * @see http://docs.openstack.org/developer/swift/misc.html#acls
         *
         * In general, we don't allow listings to end-users. It's not useful, isn't well-defined
         * (lists are truncated to 10000 item with no way to page), and is just a performance risk.
@@@ -25,7 -25,6 +25,7 @@@
   */
  use Psr\Log\LoggerAwareInterface;
  use Psr\Log\LoggerInterface;
 +use Wikimedia\ScopedCallback;
  
  /**
   * Relational database abstraction object
@@@ -79,7 -78,7 +79,7 @@@ abstract class Database implements IDat
        /** @var callback Error logging callback */
        protected $errorLogger;
  
 -      /** @var resource Database connection */
 +      /** @var resource|null Database connection */
        protected $mConn = null;
        /** @var bool */
        protected $mOpened = false;
                        }
                        if ( !isset( $p['errorLogger'] ) ) {
                                $p['errorLogger'] = function ( Exception $e ) {
 -                                      trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
 +                                      trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
                                };
                        }
  
                return !!( $this->mFlags & $flag );
        }
  
 +      /**
 +       * @param string $name Class field name
 +       * @return mixed
 +       * @deprecated Since 1.28
 +       */
        public function getProperty( $name ) {
                return $this->$name;
        }
                if ( $this->htmlErrors !== false ) {
                        ini_set( 'html_errors', $this->htmlErrors );
                }
 +
 +              return $this->getLastPHPError();
 +      }
 +
 +      /**
 +       * @return string|bool Last PHP error for this DB (typically connection errors)
 +       */
 +      protected function getLastPHPError() {
                if ( $this->mPHPError ) {
                        $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
                        $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
  
                        return $error;
 -              } else {
 -                      return false;
                }
 +
 +              return false;
        }
  
        /**
         * @return bool
         */
        protected function isTransactableQuery( $sql ) {
 -              $verb = $this->getQueryVerb( $sql );
 -              return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
 +              return !in_array(
 +                      $this->getQueryVerb( $sql ),
 +                      [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET', 'CREATE', 'ALTER' ],
 +                      true
 +              );
        }
  
        /**
  
                if ( false === $ret ) {
                        # Deadlocks cause the entire transaction to abort, not just the statement.
-                       # http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
+                       # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
                        # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
                        if ( $this->wasDeadlock() ) {
                                if ( $this->explicitTrxActive() || $priorWritesPending ) {
                } elseif ( count( $dbDetails ) == 2 ) {
                        list( $database, $table ) = $dbDetails;
                        # We don't want any prefix added in this case
 +                      $prefix = '';
                        # In dbs that support it, $database may actually be the schema
                        # but that doesn't affect any of the functionality here
 -                      $prefix = '';
                        $schema = '';
                } else {
                        list( $table ) = $dbDetails;
                # Quote $table and apply the prefix if not quoted.
                # $tableName might be empty if this is called from Database::replaceVars()
                $tableName = "{$prefix}{$table}";
 -              if ( $format == 'quoted'
 -                      && !$this->isQuotedIdentifier( $tableName ) && $tableName !== ''
 +              if ( $format === 'quoted'
 +                      && !$this->isQuotedIdentifier( $tableName )
 +                      && $tableName !== ''
                ) {
                        $tableName = $this->addIdentifierQuotes( $tableName );
                }
  
 -              # Quote $schema and merge it with the table name if needed
 -              if ( strlen( $schema ) ) {
 -                      if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
 -                              $schema = $this->addIdentifierQuotes( $schema );
 -                      }
 -                      $tableName = $schema . '.' . $tableName;
 -              }
 +              # Quote $schema and $database and merge them with the table name if needed
 +              $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
 +              $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
 +
 +              return $tableName;
 +      }
  
 -              # Quote $database and merge it with the table name if needed
 -              if ( $database !== '' ) {
 -                      if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
 -                              $database = $this->addIdentifierQuotes( $database );
 +      /**
 +       * @param string|null $namespace Database or schema
 +       * @param string $relation Name of table, view, sequence, etc...
 +       * @param string $format One of (raw, quoted)
 +       * @return string Relation name with quoted and merged $namespace as needed
 +       */
 +      private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
 +              if ( strlen( $namespace ) ) {
 +                      if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
 +                              $namespace = $this->addIdentifierQuotes( $namespace );
                        }
 -                      $tableName = $database . '.' . $tableName;
 +                      $relation = $namespace . '.' . $relation;
                }
  
 -              return $tableName;
 +              return $relation;
        }
  
        public function tableNames() {
                        $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
                        throw new DBUnexpectedError(
                                $this,
 -                              "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
 +                              "$fname: Cannot flush snapshot because writes are pending ($fnames)."
                        );
                }
  
                        $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
                        throw new DBUnexpectedError(
                                $this,
 -                              "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
 +                              "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
                        );
                }
  
                return true;
        }
  
 +      /**
 +       * Get the underlying binding handle, mConn
 +       *
 +       * Makes sure that mConn is set (disconnects and ping() failure can unset it).
 +       * This catches broken callers than catch and ignore disconnection exceptions.
 +       * Unlike checking isOpen(), this is safe to call inside of open().
 +       *
 +       * @return resource|object
 +       * @throws DBUnexpectedError
 +       * @since 1.26
 +       */
 +      protected function getBindingHandle() {
 +              if ( !$this->mConn ) {
 +                      throw new DBUnexpectedError(
 +                              $this,
 +                              'DB connection was already closed or the connection dropped.'
 +                      );
 +              }
 +
 +              return $this->mConn;
 +      }
 +
        /**
         * @since 1.19
         * @return string
                }
  
                if ( $this->mConn ) {
 -                      // Avoid connection leaks for sanity
 +                      // Avoid connection leaks for sanity. Normally, resources close at script completion.
 +                      // The connection might already be closed in zend/hhvm by now, so suppress warnings.
 +                      \MediaWiki\suppressWarnings();
                        $this->closeConnection();
 +                      \MediaWiki\restoreWarnings();
                        $this->mConn = false;
                        $this->mOpened = false;
                }
@@@ -94,7 -94,7 +94,7 @@@ abstract class DatabaseMysqlBase extend
        /**
         * @return string
         */
 -      function getType() {
 +      public function getType() {
                return 'mysql';
        }
  
         * @throws Exception|DBConnectionError
         * @return bool
         */
 -      function open( $server, $user, $password, $dbName ) {
 +      public function open( $server, $user, $password, $dbName ) {
                # Close/unset connection handle
                $this->close();
  
         * @param ResultWrapper|resource $res
         * @throws DBUnexpectedError
         */
 -      function freeResult( $res ) {
 +      public function freeResult( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
         * @return stdClass|bool
         * @throws DBUnexpectedError
         */
 -      function fetchObject( $res ) {
 +      public function fetchObject( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
                // Unfortunately, mysql_fetch_object does not reset the last errno.
                // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
                // these are the only errors mysql_fetch_object can cause.
-               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+               // See https://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
                if ( $errno == 2000 || $errno == 2013 ) {
                        throw new DBUnexpectedError(
                                $this,
         * @return array|bool
         * @throws DBUnexpectedError
         */
 -      function fetchRow( $res ) {
 +      public function fetchRow( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
                // Unfortunately, mysql_fetch_array does not reset the last errno.
                // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
                // these are the only errors mysql_fetch_array can cause.
-               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+               // See https://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
                if ( $errno == 2000 || $errno == 2013 ) {
                        throw new DBUnexpectedError(
                                $this,
                // Unfortunately, mysql_num_rows does not reset the last errno.
                // We are not checking for any errors here, since
                // these are no errors mysql_num_rows can cause.
-               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+               // See https://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
                // See https://phabricator.wikimedia.org/T44430
                return $n;
        }
         * @param ResultWrapper|resource $res
         * @return int
         */
 -      function numFields( $res ) {
 +      public function numFields( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
         * @param int $n
         * @return string
         */
 -      function fieldName( $res, $n ) {
 +      public function fieldName( $res, $n ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
         * @param int $row
         * @return bool
         */
 -      function dataSeek( $res, $row ) {
 +      public function dataSeek( $res, $row ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
        /**
         * @return string
         */
 -      function lastError() {
 +      public function lastError() {
                if ( $this->mConn ) {
                        # Even if it's non-zero, it can still be invalid
                        MediaWiki\suppressWarnings();
         * @param string $fname
         * @return ResultWrapper
         */
 -      function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
 +      public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
                return $this->nativeReplace( $table, $rows, $fname );
        }
  
                return (int)$rows;
        }
  
 -      function tableExists( $table, $fname = __METHOD__ ) {
 +      public function tableExists( $table, $fname = __METHOD__ ) {
                $table = $this->tableName( $table, 'raw' );
                if ( isset( $this->mSessionTempTables[$table] ) ) {
                        return true; // already known to exist and won't show in SHOW TABLES anyway
         * @param string $field
         * @return bool|MySQLField
         */
 -      function fieldInfo( $table, $field ) {
 +      public function fieldInfo( $table, $field ) {
                $table = $this->tableName( $table );
                $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
                if ( !$res ) {
         * @param string $fname
         * @return bool|array|null False or null on failure
         */
 -      function indexInfo( $table, $index, $fname = __METHOD__ ) {
 +      public function indexInfo( $table, $index, $fname = __METHOD__ ) {
                # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
                # SHOW INDEX should work for 3.x and up:
-               # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
+               # https://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
                $table = $this->tableName( $table );
                $index = $this->indexName( $index );
  
         * @param string $s
         * @return string
         */
 -      function strencode( $s ) {
 +      public function strencode( $s ) {
                return $this->mysqlRealEscapeString( $s );
        }
  
                return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
        }
  
 -      function getLag() {
 +      public function getLag() {
                if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
                        return $this->getLagFromPtHeartbeat();
                } else {
                return [ $row ? $row->ts : null, microtime( true ) ];
        }
  
 -      public function getApproximateLagStatus() {
 +      protected function getApproximateLagStatus() {
                if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
                        // Disable caching since this is fast enough and we don't wan't
                        // to be *too* pessimistic by having both the cache TTL and the
                return $approxLag;
        }
  
 -      function masterPosWait( DBMasterPos $pos, $timeout ) {
 +      public function masterPosWait( DBMasterPos $pos, $timeout ) {
                if ( !( $pos instanceof MySQLMasterPos ) ) {
                        throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
                }
         *
         * @return MySQLMasterPos|bool
         */
 -      function getReplicaPos() {
 +      public function getReplicaPos() {
                $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
                $row = $this->fetchObject( $res );
  
         *
         * @return MySQLMasterPos|bool
         */
 -      function getMasterPos() {
 +      public function getMasterPos() {
                $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
                $row = $this->fetchObject( $res );
  
  
        /**
         * FROM MYSQL DOCS:
-        * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
+        * https://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
         * @param string $lockName
         * @param string $method
         * @return bool
        }
  
        private function makeLockName( $lockName ) {
-               // http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
+               // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
                // Newer version enforce a 64 char length limit.
                return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
        }
         * @throws DBUnexpectedError
         * @return bool|ResultWrapper
         */
 -      function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
 +      public function deleteJoin(
 +              $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
 +      ) {
                if ( !$conds ) {
                        throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
                }
         *
         * @return int
         */
 -      function getServerUptime() {
 +      public function getServerUptime() {
                $vars = $this->getMysqlStatus( 'Uptime' );
  
                return (int)$vars['Uptime'];
         *
         * @return bool
         */
 -      function wasDeadlock() {
 +      public function wasDeadlock() {
                return $this->lastErrno() == 1213;
        }
  
         *
         * @return bool
         */
 -      function wasLockTimeout() {
 +      public function wasLockTimeout() {
                return $this->lastErrno() == 1205;
        }
  
 -      function wasErrorReissuable() {
 +      public function wasErrorReissuable() {
                return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
        }
  
         *
         * @return bool
         */
 -      function wasReadOnlyError() {
 +      public function wasReadOnlyError() {
                return $this->lastErrno() == 1223 ||
                        ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
        }
  
 -      function wasConnectionError( $errno ) {
 +      public function wasConnectionError( $errno ) {
                return $errno == 2013 || $errno == 2006;
        }
  
 -      /**
 -       * Get the underlying binding handle, mConn
 -       *
 -       * Makes sure that mConn is set (disconnects and ping() failure can unset it).
 -       * This catches broken callers than catch and ignore disconnection exceptions.
 -       * Unlike checking isOpen(), this is safe to call inside of open().
 -       *
 -       * @return resource|object
 -       * @throws DBUnexpectedError
 -       * @since 1.26
 -       */
 -      protected function getBindingHandle() {
 -              if ( !$this->mConn ) {
 -                      throw new DBUnexpectedError(
 -                              $this,
 -                              'DB connection was already closed or the connection dropped.'
 -                      );
 -              }
 -
 -              return $this->mConn;
 -      }
 -
        /**
         * @param string $oldName
         * @param string $newName
         * @param string $fname
         * @return bool
         */
 -      function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
 +      public function duplicateTableStructure(
 +              $oldName, $newName, $temporary = false, $fname = __METHOD__
 +      ) {
                $tmp = $temporary ? 'TEMPORARY ' : '';
                $newName = $this->addIdentifierQuotes( $newName );
                $oldName = $this->addIdentifierQuotes( $oldName );
         * @param string $fname Calling function name
         * @return array
         */
 -      function listTables( $prefix = null, $fname = __METHOD__ ) {
 +      public function listTables( $prefix = null, $fname = __METHOD__ ) {
                $result = $this->query( "SHOW TABLES", $fname );
  
                $endArray = [];
         * @param string $which
         * @return array
         */
 -      function getMysqlStatus( $which = "%" ) {
 +      private function getMysqlStatus( $which = "%" ) {
                $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
                $status = [];
  
@@@ -48,31 -48,38 +48,31 @@@ class DatabasePostgres extends Databas
                parent::__construct( $params );
        }
  
 -      function getType() {
 +      public function getType() {
                return 'postgres';
        }
  
 -      function implicitGroupby() {
 +      public function implicitGroupby() {
                return false;
        }
  
 -      function implicitOrderby() {
 +      public function implicitOrderby() {
                return false;
        }
  
 -      function hasConstraint( $name ) {
 +      public function hasConstraint( $name ) {
 +              $conn = $this->getBindingHandle();
 +
                $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
                        "WHERE c.connamespace = n.oid AND conname = '" .
 -                      pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" .
 -                      pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
 +                      pg_escape_string( $conn, $name ) . "' AND n.nspname = '" .
 +                      pg_escape_string( $conn, $this->getCoreSchema() ) . "'";
                $res = $this->doQuery( $sql );
  
                return $this->numRows( $res );
        }
  
 -      /**
 -       * Usually aborts on failure
 -       * @param string $server
 -       * @param string $user
 -       * @param string $password
 -       * @param string $dbName
 -       * @throws DBConnectionError|Exception
 -       * @return resource|bool|null
 -       */
 -      function open( $server, $user, $password, $dbName ) {
 +      public function open( $server, $user, $password, $dbName ) {
                # Test for Postgres support, to avoid suppressed fatal error
                if ( !function_exists( 'pg_connect' ) ) {
                        throw new DBConnectionError(
                        );
                }
  
 -              if ( !strlen( $user ) ) { # e.g. the class is being loaded
 -                      return null;
 -              }
 -
                $this->mServer = $server;
                $this->mUser = $user;
                $this->mPassword = $password;
                $this->installErrorHandler();
  
                try {
 -                      $this->mConn = pg_connect( $this->connectString );
 +                      // Use new connections to let LoadBalancer/LBFactory handle reuse
 +                      $this->mConn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
                } catch ( Exception $ex ) {
                        $this->restoreErrorHandler();
                        throw $ex;
                $phpError = $this->restoreErrorHandler();
  
                if ( !$this->mConn ) {
 -                      $this->queryLogger->debug( "DB connection error\n" );
                        $this->queryLogger->debug(
 +                              "DB connection error\n" .
                                "Server: $server, Database: $dbName, User: $user, Password: " .
 -                              substr( $password, 0, 3 ) . "...\n" );
 +                              substr( $password, 0, 3 ) . "...\n"
 +                      );
                        $this->queryLogger->debug( $this->lastError() . "\n" );
                        throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
                }
                }
  
                $this->determineCoreSchema( $this->mSchema );
 +              // The schema to be used is now in the search path; no need for explicit qualification
 +              $this->mSchema = '';
  
                return $this->mConn;
        }
         * @param string $db
         * @return bool
         */
 -      function selectDB( $db ) {
 +      public function selectDB( $db ) {
                if ( $this->mDBname !== $db ) {
                        return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
                } else {
                }
        }
  
 -      function makeConnectionString( $vars ) {
 +      /**
 +       * @param string[] $vars
 +       * @return string
 +       */
 +      private function makeConnectionString( $vars ) {
                $s = '';
                foreach ( $vars as $name => $value ) {
                        $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
                return $s;
        }
  
 -      /**
 -       * Closes a database connection, if it is open
 -       * Returns success, true if already closed
 -       * @return bool
 -       */
        protected function closeConnection() {
 -              return pg_close( $this->mConn );
 +              return $this->mConn ? pg_close( $this->mConn ) : true;
        }
  
        public function doQuery( $sql ) {
 +              $conn = $this->getBindingHandle();
 +
                $sql = mb_convert_encoding( $sql, 'UTF-8' );
                // Clear previously left over PQresult
 -              while ( $res = pg_get_result( $this->mConn ) ) {
 +              while ( $res = pg_get_result( $conn ) ) {
                        pg_free_result( $res );
                }
 -              if ( pg_send_query( $this->mConn, $sql ) === false ) {
 +              if ( pg_send_query( $conn, $sql ) === false ) {
                        throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
                }
 -              $this->mLastResult = pg_get_result( $this->mConn );
 +              $this->mLastResult = pg_get_result( $conn );
                $this->mAffectedRows = null;
                if ( pg_result_error( $this->mLastResult ) ) {
                        return false;
                }
        }
  
 -      function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
 +      public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
                if ( $tempIgnore ) {
                        /* Check for constraint violation */
                        if ( $errno === '23505' ) {
                parent::reportQueryError( $error, $errno, $sql, $fname, false );
        }
  
 -      function queryIgnore( $sql, $fname = __METHOD__ ) {
 -              return $this->query( $sql, $fname, true );
 -      }
 -
 -      /**
 -       * @param stdClass|ResultWrapper $res
 -       * @throws DBUnexpectedError
 -       */
 -      function freeResult( $res ) {
 +      public function freeResult( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
                }
        }
  
 -      /**
 -       * @param ResultWrapper|stdClass $res
 -       * @return stdClass
 -       * @throws DBUnexpectedError
 -       */
 -      function fetchObject( $res ) {
 +      public function fetchObject( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
  
                # @todo hashar: not sure if the following test really trigger if the object
                #          fetching failed.
 -              if ( pg_last_error( $this->mConn ) ) {
 +              $conn = $this->getBindingHandle();
 +              if ( pg_last_error( $conn ) ) {
                        throw new DBUnexpectedError(
                                $this,
 -                              'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
 +                              'SQL error: ' . htmlspecialchars( pg_last_error( $conn ) )
                        );
                }
  
                return $row;
        }
  
 -      function fetchRow( $res ) {
 +      public function fetchRow( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
                MediaWiki\suppressWarnings();
                $row = pg_fetch_array( $res );
                MediaWiki\restoreWarnings();
 -              if ( pg_last_error( $this->mConn ) ) {
 +
 +              $conn = $this->getBindingHandle();
 +              if ( pg_last_error( $conn ) ) {
                        throw new DBUnexpectedError(
                                $this,
 -                              'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
 +                              'SQL error: ' . htmlspecialchars( pg_last_error( $conn ) )
                        );
                }
  
                return $row;
        }
  
 -      function numRows( $res ) {
 +      public function numRows( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
                MediaWiki\suppressWarnings();
                $n = pg_num_rows( $res );
                MediaWiki\restoreWarnings();
 -              if ( pg_last_error( $this->mConn ) ) {
 +
 +              $conn = $this->getBindingHandle();
 +              if ( pg_last_error( $conn ) ) {
                        throw new DBUnexpectedError(
                                $this,
 -                              'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
 +                              'SQL error: ' . htmlspecialchars( pg_last_error( $conn ) )
                        );
                }
  
                return $n;
        }
  
 -      function numFields( $res ) {
 +      public function numFields( $res ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
                return pg_num_fields( $res );
        }
  
 -      function fieldName( $res, $n ) {
 +      public function fieldName( $res, $n ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
         *
         * @return int|null
         */
 -      function insertId() {
 +      public function insertId() {
                return $this->mInsertId;
        }
  
 -      /**
 -       * @param mixed $res
 -       * @param int $row
 -       * @return bool
 -       */
 -      function dataSeek( $res, $row ) {
 +      public function dataSeek( $res, $row ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
                return pg_result_seek( $res, $row );
        }
  
 -      function lastError() {
 +      public function lastError() {
                if ( $this->mConn ) {
                        if ( $this->mLastResult ) {
                                return pg_result_error( $this->mLastResult );
                        } else {
                                return pg_last_error();
                        }
 -              } else {
 -                      return 'No database connection';
                }
 +
 +              return $this->getLastPHPError() ?: 'No database connection';
        }
  
 -      function lastErrno() {
 +      public function lastErrno() {
                if ( $this->mLastResult ) {
                        return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
                } else {
                }
        }
  
 -      function affectedRows() {
 +      public function affectedRows() {
                if ( !is_null( $this->mAffectedRows ) ) {
                        // Forced result for simulated queries
                        return $this->mAffectedRows;
         * @param array $options
         * @return int
         */
 -      function estimateRowCount( $table, $vars = '*', $conds = '',
 +      public function estimateRowCount( $table, $vars = '*', $conds = '',
                $fname = __METHOD__, $options = []
        ) {
                $options['EXPLAIN'] = true;
                return $rows;
        }
  
 -      /**
 -       * Returns information about an index
 -       * If errors are explicitly ignored, returns NULL on failure
 -       *
 -       * @param string $table
 -       * @param string $index
 -       * @param string $fname
 -       * @return bool|null
 -       */
 -      function indexInfo( $table, $index, $fname = __METHOD__ ) {
 +      public function indexInfo( $table, $index, $fname = __METHOD__ ) {
                $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
                $res = $this->query( $sql, $fname );
                if ( !$res ) {
                return false;
        }
  
 -      /**
 -       * Returns is of attributes used in index
 -       *
 -       * @since 1.19
 -       * @param string $index
 -       * @param bool|string $schema
 -       * @return array
 -       */
 -      function indexAttributes( $index, $schema = false ) {
 +      public function indexAttributes( $index, $schema = false ) {
                if ( $schema === false ) {
                        $schema = $this->getCoreSchema();
                }
@@@ -480,7 -516,7 +480,7 @@@ __INDEXATTR__
                return $a;
        }
  
 -      function indexUnique( $table, $index, $fname = __METHOD__ ) {
 +      public function indexUnique( $table, $index, $fname = __METHOD__ ) {
                $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
                        " AND indexdef LIKE 'CREATE UNIQUE%(" .
                        $this->strencode( $this->indexName( $index ) ) .
                return $res->numRows() > 0;
        }
  
 -      function selectSQLText(
 +      public function selectSQLText(
                $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        ) {
                // Change the FOR UPDATE option as necessary based on the join conditions. Then pass
         * @param array|string $options String or array. Valid options: IGNORE
         * @return bool Success of insert operation. IGNORE always returns true.
         */
 -      function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
 +      public function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
                if ( !count( $args ) ) {
                        return true;
                }
         * @param array $selectOptions
         * @return bool
         */
 -      function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
 -              $insertOptions = [], $selectOptions = [] ) {
 +      public function nativeInsertSelect(
 +              $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
 +              $insertOptions = [], $selectOptions = []
 +      ) {
                $destTable = $this->tableName( $destTable );
  
                if ( !is_array( $insertOptions ) ) {
                return $res;
        }
  
 -      function tableName( $name, $format = 'quoted' ) {
 -              # Replace reserved words with better ones
 -              switch ( $name ) {
 -                      case 'user':
 -                              return $this->realTableName( 'mwuser', $format );
 -                      case 'text':
 -                              return $this->realTableName( 'pagecontent', $format );
 -                      default:
 -                              return $this->realTableName( $name, $format );
 -              }
 -      }
 +      public function tableName( $name, $format = 'quoted' ) {
 +              // Replace reserved words with better ones
 +              $name = $this->remappedTableName( $name );
  
 -      /* Don't cheat on installer */
 -      function realTableName( $name, $format = 'quoted' ) {
                return parent::tableName( $name, $format );
        }
  
        /**
 -       * Return the next in a sequence, save the value for retrieval via insertId()
 -       *
 -       * @param string $seqName
 -       * @return int|null
 +       * @param string $name
 +       * @return string Value of $name or remapped name if $name is a reserved keyword
 +       * @TODO: dependency inject these...
         */
 -      function nextSequenceValue( $seqName ) {
 +      public function remappedTableName( $name ) {
 +              if ( $name === 'user' ) {
 +                      return 'mwuser';
 +              } elseif ( $name === 'text' ) {
 +                      return 'pagecontent';
 +              }
 +
 +              return $name;
 +      }
 +
 +      /**
 +       * @param string $name
 +       * @param string $format
 +       * @return string Qualified and encoded (if requested) table name
 +       */
 +      public function realTableName( $name, $format = 'quoted' ) {
 +              return parent::tableName( $name, $format );
 +      }
 +
 +      public function nextSequenceValue( $seqName ) {
                $safeseq = str_replace( "'", "''", $seqName );
                $res = $this->query( "SELECT nextval('$safeseq')" );
                $row = $this->fetchRow( $res );
         * @param string $seqName
         * @return int
         */
 -      function currentSequenceValue( $seqName ) {
 +      public function currentSequenceValue( $seqName ) {
                $safeseq = str_replace( "'", "''", $seqName );
                $res = $this->query( "SELECT currval('$safeseq')" );
                $row = $this->fetchRow( $res );
                return $currval;
        }
  
 -      # Returns the size of a text field, or -1 for "unlimited"
 -      function textFieldSize( $table, $field ) {
 +      public function textFieldSize( $table, $field ) {
                $table = $this->tableName( $table );
                $sql = "SELECT t.typname as ftype,a.atttypmod as size
                        FROM pg_class c, pg_attribute a, pg_type t
                return $size;
        }
  
 -      function limitResult( $sql, $limit, $offset = false ) {
 +      public function limitResult( $sql, $limit, $offset = false ) {
                return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
        }
  
 -      function wasDeadlock() {
 +      public function wasDeadlock() {
                return $this->lastErrno() == '40P01';
        }
  
 -      function duplicateTableStructure(
 +      public function duplicateTableStructure(
                $oldName, $newName, $temporary = false, $fname = __METHOD__
        ) {
                $newName = $this->addIdentifierQuotes( $newName );
                        "(LIKE $oldName INCLUDING DEFAULTS)", $fname );
        }
  
 -      function listTables( $prefix = null, $fname = __METHOD__ ) {
 +      public function listTables( $prefix = null, $fname = __METHOD__ ) {
                $eschema = $this->addQuotes( $this->getCoreSchema() );
                $result = $this->query(
                        "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
                return $endArray;
        }
  
 -      function timestamp( $ts = 0 ) {
 +      public function timestamp( $ts = 0 ) {
                $ct = new ConvertibleTimestamp( $ts );
  
                return $ct->getTimestamp( TS_POSTGRES );
  
        /**
         * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
-        * to http://www.php.net/manual/en/ref.pgsql.php
+        * to https://secure.php.net/manual/en/ref.pgsql.php
         *
         * Parsing a postgres array can be a tricky problem, he's my
         * take on this, it handles multi-dimensional arrays plus
         * @param int $offset
         * @return string
         */
 -      function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
 +      private function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
                if ( false === $limit ) {
                        $limit = strlen( $text ) - 1;
                        $output = [];
                return $output;
        }
  
 -      /**
 -       * Return aggregated value function call
 -       * @param array $valuedata
 -       * @param string $valuename
 -       * @return array
 -       */
        public function aggregateValue( $valuedata, $valuename = 'value' ) {
                return $valuedata;
        }
  
 -      /**
 -       * @return string Wikitext of a link to the server software's web site
 -       */
        public function getSoftwareLink() {
                return '[{{int:version-db-postgres-url}} PostgreSQL]';
        }
         * @since 1.19
         * @return string Default schema for the current session
         */
 -      function getCurrentSchema() {
 +      public function getCurrentSchema() {
                $res = $this->query( "SELECT current_schema()", __METHOD__ );
                $row = $this->fetchRow( $res );
  
         * @since 1.19
         * @return array List of actual schemas for the current sesson
         */
 -      function getSchemas() {
 +      public function getSchemas() {
                $res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
                $row = $this->fetchRow( $res );
                $schemas = [];
         * @since 1.19
         * @return array How to search for table names schemas for the current user
         */
 -      function getSearchPath() {
 +      public function getSearchPath() {
                $res = $this->query( "SHOW search_path", __METHOD__ );
                $row = $this->fetchRow( $res );
  
         *
         * @param array $search_path List of schemas to be searched by default
         */
 -      function setSearchPath( $search_path ) {
 +      private function setSearchPath( $search_path ) {
                $this->query( "SET search_path = " . implode( ", ", $search_path ) );
        }
  
         *
         * @param string $desiredSchema
         */
 -      function determineCoreSchema( $desiredSchema ) {
 +      public function determineCoreSchema( $desiredSchema ) {
                $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
                if ( $this->schemaExists( $desiredSchema ) ) {
                        if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
                                $this->mCoreSchema . "\"\n" );
                }
                /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
 -              $this->commit( __METHOD__ );
 +              $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
        }
  
        /**
         * @since 1.19
         * @return string Core schema name
         */
 -      function getCoreSchema() {
 +      public function getCoreSchema() {
                return $this->mCoreSchema;
        }
  
 -      /**
 -       * @return string Version information from the database
 -       */
 -      function getServerVersion() {
 +      public function getServerVersion() {
                if ( !isset( $this->numericVersion ) ) {
 -                      $versionInfo = pg_version( $this->mConn );
 +                      $conn = $this->getBindingHandle();
 +                      $versionInfo = pg_version( $conn );
                        if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
                                // Old client, abort install
                                $this->numericVersion = '7.3 or earlier';
                                $this->numericVersion = $versionInfo['server'];
                        } else {
                                // Bug 16937: broken pgsql extension from PHP<5.3
 -                              $this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' );
 +                              $this->numericVersion = pg_parameter_status( $conn, 'server_version' );
                        }
                }
  
         * @param bool|string $schema
         * @return bool
         */
 -      function relationExists( $table, $types, $schema = false ) {
 +      private function relationExists( $table, $types, $schema = false ) {
                if ( !is_array( $types ) ) {
                        $types = [ $types ];
                }
 -              if ( !$schema ) {
 +              if ( $schema === false ) {
                        $schema = $this->getCoreSchema();
                }
 -              $table = $this->realTableName( $table, 'raw' );
                $etable = $this->addQuotes( $table );
                $eschema = $this->addQuotes( $schema );
                $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
        }
  
        /**
 -       * For backward compatibility, this function checks both tables and
 -       * views.
 +       * For backward compatibility, this function checks both tables and views.
         * @param string $table
         * @param string $fname
         * @param bool|string $schema
         * @return bool
         */
 -      function tableExists( $table, $fname = __METHOD__, $schema = false ) {
 +      public function tableExists( $table, $fname = __METHOD__, $schema = false ) {
                return $this->relationExists( $table, [ 'r', 'v' ], $schema );
        }
  
 -      function sequenceExists( $sequence, $schema = false ) {
 +      public function sequenceExists( $sequence, $schema = false ) {
                return $this->relationExists( $sequence, 'S', $schema );
        }
  
 -      function triggerExists( $table, $trigger ) {
 +      public function triggerExists( $table, $trigger ) {
                $q = <<<SQL
        SELECT 1 FROM pg_class, pg_namespace, pg_trigger
                WHERE relnamespace=pg_namespace.oid AND relkind='r'
@@@ -1093,7 -1133,7 +1093,7 @@@ SQL
                return $rows;
        }
  
 -      function ruleExists( $table, $rule ) {
 +      public function ruleExists( $table, $rule ) {
                $exists = $this->selectField( 'pg_rules', 'rulename',
                        [
                                'rulename' => $rule,
                return $exists === $rule;
        }
  
 -      function constraintExists( $table, $constraint ) {
 +      public function constraintExists( $table, $constraint ) {
                $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
                        "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
                        $this->addQuotes( $this->getCoreSchema() ),
         * @param string $schema
         * @return bool
         */
 -      function schemaExists( $schema ) {
 -              $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
 -                      [ 'nspname' => $schema ], __METHOD__ );
 +      public function schemaExists( $schema ) {
 +              if ( !strlen( $schema ) ) {
 +                      return false; // short-circuit
 +              }
 +
 +              $exists = $this->selectField(
 +                      '"pg_catalog"."pg_namespace"', 1, [ 'nspname' => $schema ], __METHOD__ );
  
                return (bool)$exists;
        }
         * @param string $roleName
         * @return bool
         */
 -      function roleExists( $roleName ) {
 +      public function roleExists( $roleName ) {
                $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
                        [ 'rolname' => $roleName ], __METHOD__ );
  
         * @var string $field
         * @return PostgresField|null
         */
 -      function fieldInfo( $table, $field ) {
 +      public function fieldInfo( $table, $field ) {
                return PostgresField::fromText( $this, $table, $field );
        }
  
         * @param int $index Field number, starting from 0
         * @return string
         */
 -      function fieldType( $res, $index ) {
 +      public function fieldType( $res, $index ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
                return pg_field_type( $res, $index );
        }
  
 -      /**
 -       * @param string $b
 -       * @return Blob
 -       */
 -      function encodeBlob( $b ) {
 +      public function encodeBlob( $b ) {
                return new PostgresBlob( pg_escape_bytea( $b ) );
        }
  
 -      function decodeBlob( $b ) {
 +      public function decodeBlob( $b ) {
                if ( $b instanceof PostgresBlob ) {
                        $b = $b->fetch();
                } elseif ( $b instanceof Blob ) {
                return pg_unescape_bytea( $b );
        }
  
 -      function strencode( $s ) {
 +      public function strencode( $s ) {
                // Should not be called by us
 -
 -              return pg_escape_string( $this->mConn, $s );
 +              return pg_escape_string( $this->getBindingHandle(), $s );
        }
  
 -      /**
 -       * @param string|int|null|bool|Blob $s
 -       * @return string|int
 -       */
 -      function addQuotes( $s ) {
 +      public function addQuotes( $s ) {
 +              $conn = $this->getBindingHandle();
 +
                if ( is_null( $s ) ) {
                        return 'NULL';
                } elseif ( is_bool( $s ) ) {
                        if ( $s instanceof PostgresBlob ) {
                                $s = $s->fetch();
                        } else {
 -                              $s = pg_escape_bytea( $this->mConn, $s->fetch() );
 +                              $s = pg_escape_bytea( $conn, $s->fetch() );
                        }
                        return "'$s'";
                }
  
 -              return "'" . pg_escape_string( $this->mConn, $s ) . "'";
 +              return "'" . pg_escape_string( $conn, $s ) . "'";
        }
  
        /**
                return $ins;
        }
  
 -      /**
 -       * Various select options
 -       *
 -       * @param array $options An associative array of options to be turned into
 -       *   an SQL query, valid keys are listed in the function.
 -       * @return array
 -       */
 -      function makeSelectOptions( $options ) {
 +      public function makeSelectOptions( $options ) {
                $preLimitTail = $postLimitTail = '';
                $startOpts = $useIndex = $ignoreIndex = '';
  
                return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
        }
  
 -      function getDBname() {
 +      public function getDBname() {
                return $this->mDBname;
        }
  
 -      function getServer() {
 +      public function getServer() {
                return $this->mServer;
        }
  
 -      function buildConcat( $stringList ) {
 +      public function buildConcat( $stringList ) {
                return implode( ' || ', $stringList );
        }
  
                return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
        }
  
 -      /**
 -       * @param string $field Field or column to cast
 -       * @return string
 -       * @since 1.28
 -       */
        public function buildStringCast( $field ) {
                return $field . '::text';
        }
                return parent::streamStatementEnd( $sql, $newLine );
        }
  
 -      /**
 -       * Check to see if a named lock is available. This is non-blocking.
 -       * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
 -       *
 -       * @param string $lockName Name of lock to poll
 -       * @param string $method Name of method calling us
 -       * @return bool
 -       * @since 1.20
 -       */
        public function lockIsFree( $lockName, $method ) {
 +              // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
                $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
                $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
                        WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
                return ( $row->lockstatus === 't' );
        }
  
 -      /**
 -       * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
 -       * @param string $lockName
 -       * @param string $method
 -       * @param int $timeout
 -       * @return bool
 -       */
        public function lock( $lockName, $method, $timeout = 5 ) {
 +              // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
                $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
                $loop = new WaitConditionLoop(
                        function () use ( $lockName, $key, $timeout, $method ) {
                return ( $loop->invoke() === $loop::CONDITION_REACHED );
        }
  
 -      /**
 -       * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM
 -       * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
 -       * @param string $lockName
 -       * @param string $method
 -       * @return bool
 -       */
        public function unlock( $lockName, $method ) {
 +              // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
                $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
                $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
                $row = $this->fetchObject( $result );
@@@ -31,17 -31,20 +31,17 @@@ class DatabaseSqlite extends Database 
  
        /** @var string Directory */
        protected $dbDir;
 -
        /** @var string File name for SQLite database file */
        protected $dbPath;
 -
        /** @var string Transaction mode */
        protected $trxMode;
  
        /** @var int The number of rows affected as an integer */
        protected $mAffectedRows;
 -
        /** @var resource */
        protected $mLastResult;
  
 -      /** @var PDO */
 +      /** @var $mConn PDO */
        protected $mConn;
  
        /** @var FSLockManager (hopefully on the same server as the DB) */
         * @param string $dbName
         *
         * @throws DBConnectionError
 -       * @return PDO
 +       * @return bool
         */
        function open( $server, $user, $pass, $dbName ) {
                $this->close();
                }
                $this->openFile( $fileName );
  
 -              return $this->mConn;
 +              return (bool)$this->mConn;
        }
  
        /**
                return false;
        }
  
 +      public function selectDB( $db ) {
 +              return false; // doesn't make sense
 +      }
 +
        /**
         * @return string SQLite DB file path
         * @since 1.25
        }
  
        /**
-        * Attaches external database to our connection, see http://sqlite.org/lang_attach.html
+        * Attaches external database to our connection, see https://sqlite.org/lang_attach.html
         * for details.
         *
         * @param string $name Database name to be used in queries like
         * @param string $table
         * @param string $index
         * @param string $fname
 -       * @return array
 +       * @return array|false
         */
        function indexInfo( $table, $index, $fname = __METHOD__ ) {
                $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
                $res = $this->query( $sql, $fname );
 -              if ( !$res ) {
 -                      return null;
 -              }
 -              if ( $res->numRows() == 0 ) {
 +              if ( !$res || $res->numRows() == 0 ) {
                        return false;
                }
                $info = [];
@@@ -23,7 -23,6 +23,7 @@@
   * @file
   * @ingroup Database
   */
 +use Wikimedia\ScopedCallback;
  
  /**
   * Basic database interface for live and lazy-loaded relation database handles
@@@ -322,6 -321,14 +322,6 @@@ interface IDatabase 
         */
        public function getFlag( $flag );
  
 -      /**
 -       * General read-only accessor
 -       *
 -       * @param string $name
 -       * @return string
 -       */
 -      public function getProperty( $name );
 -
        /**
         * @return string
         */
  
        /**
         * Get the number of fields in a result object
-        * @see http://www.php.net/mysql_num_fields
+        * @see https://secure.php.net/mysql_num_fields
         *
         * @param mixed $res A SQL result
         * @return int
  
        /**
         * Get a field name in a result object
-        * @see http://www.php.net/mysql_field_name
+        * @see https://secure.php.net/mysql_field_name
         *
         * @param mixed $res A SQL result
         * @param int $n
  
        /**
         * Change the position of the cursor in a result object
-        * @see http://www.php.net/mysql_data_seek
+        * @see https://secure.php.net/mysql_data_seek
         *
         * @param mixed $res A SQL result
         * @param int $row
  
        /**
         * Get the last error number
-        * @see http://www.php.net/mysql_errno
+        * @see https://secure.php.net/mysql_errno
         *
         * @return int
         */
  
        /**
         * Get a description of the last error
-        * @see http://www.php.net/mysql_error
+        * @see https://secure.php.net/mysql_error
         *
         * @return string
         */
  
        /**
         * Get the number of rows affected by the last write query
-        * @see http://www.php.net/mysql_affected_rows
+        * @see https://secure.php.net/mysql_affected_rows
         *
         * @return int
         */
  
        /**
         * Returns a wikitext link to the DB's website, e.g.,
-        *   return "[http://www.mysql.com/ MySQL]";
+        *   return "[https://www.mysql.com/ MySQL]";
         * Should at least contain plain text, if for some reason
         * your database has no website.
         *
         *
         * Any implementation of this function should *not* involve reusing
         * sequence numbers created for rolled-back transactions.
-        * See http://bugs.mysql.com/bug.php?id=30767 for details.
+        * See https://bugs.mysql.com/bug.php?id=30767 for details.
         * @param string $seqName
         * @return null|int
         */
         * IDatabase::insert().
         *
         * @param string $b
 -       * @return string
 +       * @return string|Blob
         */
        public function encodeBlob( $b );
  
@@@ -22,7 -22,6 +22,7 @@@
   */
  
  use Psr\Log\LoggerInterface;
 +use Wikimedia\ScopedCallback;
  
  /**
   * An interface for generating database load balancers
@@@ -97,7 -96,7 +97,7 @@@ abstract class LBFactory implements ILB
                $this->errorLogger = isset( $conf['errorLogger'] )
                        ? $conf['errorLogger']
                        : function ( Exception $e ) {
 -                              trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
 +                              trigger_error( E_USER_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
                        };
  
                $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
        /**
         * @see ILBFactory::newExternalLB()
         * @param string $cluster
 -       * @param bool $domain
         * @return LoadBalancer
         */
 -      abstract public function newExternalLB( $cluster, $domain = false );
 +      abstract public function newExternalLB( $cluster );
  
        /**
         * @see ILBFactory::getExternalLB()
         * @param string $cluster
 -       * @param bool $domain
         * @return LoadBalancer
         */
 -      abstract public function getExternalLB( $cluster, $domain = false );
 +      abstract public function getExternalLB( $cluster );
  
        /**
         * Call a method of each tracked load balancer
  
                if ( $failed ) {
                        throw new DBReplicationWaitError(
 +                              null,
                                "Could not wait for replica DBs to catch up to " .
                                implode( ', ', $failed )
                        );
         * @return ScopedCallback|null
         */
        final protected function getScopedPHPBehaviorForCommit() {
-               if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+               if ( PHP_SAPI != 'cli' ) { // https://bugs.php.net/bug.php?id=47540
                        $old = ignore_user_abort( true ); // avoid half-finished operations
                        return new ScopedCallback( function () use ( $old ) {
                                ignore_user_abort( $old );
@@@ -21,7 -21,6 +21,7 @@@
   * @ingroup Database
   */
  use Psr\Log\LoggerInterface;
 +use Wikimedia\ScopedCallback;
  
  /**
   * Database connection, tracking, load balancing, and transaction manager for a cluster
@@@ -190,7 -189,7 +190,7 @@@ class LoadBalancer implements ILoadBala
                $this->errorLogger = isset( $params['errorLogger'] )
                        ? $params['errorLogger']
                        : function ( Exception $e ) {
 -                              trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
 +                              trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
                        };
  
                foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) {
                if ( $i == self::DB_REPLICA ) {
                        $this->mLastError = 'Unknown error'; // reset error string
                        # Try the general server pool if $groups are unavailable.
 -                      $i = in_array( false, $groups, true )
 +                      $i = ( $groups === [ false ] )
                                ? false // don't bother with this if that is what was tried above
                                : $this->getReaderIndex( false, $domain );
                        # Couldn't find a working server in getReaderIndex()?
                        // If all servers were busy, mLastError will contain something sensible
                        throw new DBConnectionError( null, $this->mLastError );
                } else {
 -                      $context['db_server'] = $conn->getProperty( 'mServer' );
 +                      $context['db_server'] = $conn->getServer();
                        $this->connLogger->warning(
                                "Connection error: {last_error} ({db_server})",
                                $context
         * @return ScopedCallback|null
         */
        final protected function getScopedPHPBehaviorForCommit() {
-               if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+               if ( PHP_SAPI != 'cli' ) { // https://bugs.php.net/bug.php?id=47540
                        $old = ignore_user_abort( true ); // avoid half-finished operations
                        return new ScopedCallback( function () use ( $old ) {
                                ignore_user_abort( $old );
@@@ -624,7 -624,7 +624,7 @@@ class WikiPage implements Page, IDBAcce
                        // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
                        // may not find it since a page row UPDATE and revision row INSERT by S2 may have
                        // happened after the first S1 SELECT.
-                       // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
+                       // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
                        $flags = Revision::READ_LOCKING;
                        $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
                } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
                } elseif ( $options['changed'] ) { // bug 50785
                        self::onArticleEdit( $this->mTitle, $revision );
                }
 +
 +              ResourceLoaderWikiModule::invalidateModuleCache(
 +                      $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
 +              );
        }
  
        /**
                }
  
                if ( !$protect ) { // No protection at all means unprotection
 -                      $revCommentMsg = 'unprotectedarticle';
 +                      $revCommentMsg = 'unprotectedarticle-comment';
                        $logAction = 'unprotect';
                } elseif ( $isProtected ) {
 -                      $revCommentMsg = 'modifiedarticleprotection';
 +                      $revCommentMsg = 'modifiedarticleprotection-comment';
                        $logAction = 'modify';
                } else {
 -                      $revCommentMsg = 'protectedarticle';
 +                      $revCommentMsg = 'protectedarticle-comment';
                        $logAction = 'protect';
                }
  
        public function insertProtectNullRevision( $revCommentMsg, array $limit,
                array $expiry, $cascade, $reason, $user = null
        ) {
 -              global $wgContLang;
                $dbw = wfGetDB( DB_MASTER );
  
                // Prepare a null revision to be added to the history
 -              $editComment = $wgContLang->ucfirst(
 -                      wfMessage(
 -                              $revCommentMsg,
 -                              $this->mTitle->getPrefixedText()
 -                      )->inContentLanguage()->text()
 -              );
 +              $editComment = wfMessage(
 +                      $revCommentMsg,
 +                      $this->mTitle->getPrefixedText(),
 +                      $user ? $user->getName() : ''
 +              )->inContentLanguage()->text();
                if ( $reason ) {
                        $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
                }
                // unless they actually try to catch exceptions (which is rare).
  
                // we need to remember the old content so we can use it to generate all deletion updates.
 +              $revision = $this->getRevision();
                try {
                        $content = $this->getContent( Revision::RAW );
                } catch ( Exception $ex ) {
                        $content = null;
                }
  
 +              $fields = Revision::selectFields();
 +              $bitfield = false;
 +
                // Bitfields to further suppress the content
                if ( $suppress ) {
 -                      $bitfield = 0;
 -                      // This should be 15...
 -                      $bitfield |= Revision::DELETED_TEXT;
 -                      $bitfield |= Revision::DELETED_COMMENT;
 -                      $bitfield |= Revision::DELETED_USER;
 -                      $bitfield |= Revision::DELETED_RESTRICTED;
 -                      $deletionFields = [ $dbw->addQuotes( $bitfield ) . ' AS deleted' ];
 -              } else {
 -                      $deletionFields = [ 'rev_deleted AS deleted' ];
 +                      $bitfield = Revision::SUPPRESSED_ALL;
 +                      $fields = array_diff( $fields, [ 'rev_deleted' ] );
                }
  
                // For now, shunt the revision data into the archive table.
                // the rev_deleted field, which is reserved for this purpose.
  
                // Get all of the page revisions
 -              $fields = array_diff( Revision::selectFields(), [ 'rev_deleted' ] );
                $res = $dbw->select(
                        'revision',
 -                      array_merge( $fields, $deletionFields ),
 +                      $fields,
                        [ 'rev_page' => $id ],
                        __METHOD__,
                        'FOR UPDATE'
                                'ar_flags'      => '',
                                'ar_len'        => $row->rev_len,
                                'ar_page_id'    => $id,
 -                              'ar_deleted'    => $row->deleted,
 +                              'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
                                'ar_sha1'       => $row->rev_sha1,
                        ];
                        if ( $wgContentHandlerUseDB ) {
  
                $dbw->endAtomic( __METHOD__ );
  
 -              $this->doDeleteUpdates( $id, $content );
 +              $this->doDeleteUpdates( $id, $content, $revision );
  
                Hooks::run( 'ArticleDeleteComplete', [
                        &$wikiPageBeforeDelete,
         * Do some database updates after deletion
         *
         * @param int $id The page_id value of the page being deleted
 -       * @param Content $content Optional page content to be used when determining
 +       * @param Content|null $content Optional page content to be used when determining
         *   the required updates. This may be needed because $this->getContent()
         *   may already return null when the page proper was deleted.
 +       * @param Revision|null $revision The latest page revision
         */
 -      public function doDeleteUpdates( $id, Content $content = null ) {
 +      public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) {
                try {
                        $countable = $this->isCountable();
                } catch ( Exception $ex ) {
  
                // Clear caches
                WikiPage::onArticleDelete( $this->mTitle );
 +              ResourceLoaderWikiModule::invalidateModuleCache(
 +                      $this->mTitle, $revision, null, wfWikiID()
 +              );
  
                // Reset this object and the Title object
                $this->loadFromRow( false, self::READ_LATEST );
         * Update all the appropriate counts in the category table, given that
         * we've added the categories $added and deleted the categories $deleted.
         *
 +       * This should only be called from deferred updates or jobs to avoid contention.
 +       *
         * @param array $added The names of categories that were added
         * @param array $deleted The names of categories that were deleted
         * @param integer $id Page ID (this should be the original deleted page ID)
         */
        public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
                $id = $id ?: $this->getId();
 +              $ns = $this->getTitle()->getNamespace();
 +
 +              $addFields = [ 'cat_pages = cat_pages + 1' ];
 +              $removeFields = [ 'cat_pages = cat_pages - 1' ];
 +              if ( $ns == NS_CATEGORY ) {
 +                      $addFields[] = 'cat_subcats = cat_subcats + 1';
 +                      $removeFields[] = 'cat_subcats = cat_subcats - 1';
 +              } elseif ( $ns == NS_FILE ) {
 +                      $addFields[] = 'cat_files = cat_files + 1';
 +                      $removeFields[] = 'cat_files = cat_files - 1';
 +              }
 +
                $dbw = wfGetDB( DB_MASTER );
 -              $method = __METHOD__;
 -              // Do this at the end of the commit to reduce lock wait timeouts
 -              $dbw->onTransactionPreCommitOrIdle(
 -                      function () use ( $dbw, $added, $deleted, $id, $method ) {
 -                              $ns = $this->getTitle()->getNamespace();
 -
 -                              $addFields = [ 'cat_pages = cat_pages + 1' ];
 -                              $removeFields = [ 'cat_pages = cat_pages - 1' ];
 -                              if ( $ns == NS_CATEGORY ) {
 -                                      $addFields[] = 'cat_subcats = cat_subcats + 1';
 -                                      $removeFields[] = 'cat_subcats = cat_subcats - 1';
 -                              } elseif ( $ns == NS_FILE ) {
 -                                      $addFields[] = 'cat_files = cat_files + 1';
 -                                      $removeFields[] = 'cat_files = cat_files - 1';
 -                              }
  
 -                              if ( count( $added ) ) {
 -                                      $existingAdded = $dbw->selectFieldValues(
 -                                              'category',
 -                                              'cat_title',
 -                                              [ 'cat_title' => $added ],
 -                                              $method
 -                                      );
 +              if ( count( $added ) ) {
 +                      $existingAdded = $dbw->selectFieldValues(
 +                              'category',
 +                              'cat_title',
 +                              [ 'cat_title' => $added ],
 +                              __METHOD__
 +                      );
  
 -                                      // For category rows that already exist, do a plain
 -                                      // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
 -                                      // to avoid creating gaps in the cat_id sequence.
 -                                      if ( count( $existingAdded ) ) {
 -                                              $dbw->update(
 -                                                      'category',
 -                                                      $addFields,
 -                                                      [ 'cat_title' => $existingAdded ],
 -                                                      $method
 -                                              );
 -                                      }
 +                      // For category rows that already exist, do a plain
 +                      // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
 +                      // to avoid creating gaps in the cat_id sequence.
 +                      if ( count( $existingAdded ) ) {
 +                              $dbw->update(
 +                                      'category',
 +                                      $addFields,
 +                                      [ 'cat_title' => $existingAdded ],
 +                                      __METHOD__
 +                              );
 +                      }
  
 -                                      $missingAdded = array_diff( $added, $existingAdded );
 -                                      if ( count( $missingAdded ) ) {
 -                                              $insertRows = [];
 -                                              foreach ( $missingAdded as $cat ) {
 -                                                      $insertRows[] = [
 -                                                              'cat_title'   => $cat,
 -                                                              'cat_pages'   => 1,
 -                                                              'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
 -                                                              'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
 -                                                      ];
 -                                              }
 -                                              $dbw->upsert(
 -                                                      'category',
 -                                                      $insertRows,
 -                                                      [ 'cat_title' ],
 -                                                      $addFields,
 -                                                      $method
 -                                              );
 -                                      }
 +                      $missingAdded = array_diff( $added, $existingAdded );
 +                      if ( count( $missingAdded ) ) {
 +                              $insertRows = [];
 +                              foreach ( $missingAdded as $cat ) {
 +                                      $insertRows[] = [
 +                                              'cat_title'   => $cat,
 +                                              'cat_pages'   => 1,
 +                                              'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
 +                                              'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
 +                                      ];
                                }
 +                              $dbw->upsert(
 +                                      'category',
 +                                      $insertRows,
 +                                      [ 'cat_title' ],
 +                                      $addFields,
 +                                      __METHOD__
 +                              );
 +                      }
 +              }
  
 -                              if ( count( $deleted ) ) {
 -                                      $dbw->update(
 -                                              'category',
 -                                              $removeFields,
 -                                              [ 'cat_title' => $deleted ],
 -                                              $method
 -                                      );
 -                              }
 +              if ( count( $deleted ) ) {
 +                      $dbw->update(
 +                              'category',
 +                              $removeFields,
 +                              [ 'cat_title' => $deleted ],
 +                              __METHOD__
 +                      );
 +              }
  
 -                              foreach ( $added as $catName ) {
 -                                      $cat = Category::newFromName( $catName );
 -                                      Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
 -                              }
 +              foreach ( $added as $catName ) {
 +                      $cat = Category::newFromName( $catName );
 +                      Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
 +              }
  
 -                              foreach ( $deleted as $catName ) {
 -                                      $cat = Category::newFromName( $catName );
 -                                      Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
 -                              }
 +              foreach ( $deleted as $catName ) {
 +                      $cat = Category::newFromName( $catName );
 +                      Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
 +              }
  
 -                              // Refresh counts on categories that should be empty now, to
 -                              // trigger possible deletion. Check master for the most
 -                              // up-to-date cat_pages.
 -                              if ( count( $deleted ) ) {
 -                                      $rows = $dbw->select(
 -                                              'category',
 -                                              [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
 -                                              [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
 -                                              $method
 -                                      );
 -                                      foreach ( $rows as $row ) {
 -                                              $cat = Category::newFromRow( $row );
 -                                              $cat->refreshCounts();
 -                                      }
 -                              }
 -                      },
 -                      __METHOD__
 -              );
 +              // Refresh counts on categories that should be empty now, to
 +              // trigger possible deletion. Check master for the most
 +              // up-to-date cat_pages.
 +              if ( count( $deleted ) ) {
 +                      $rows = $dbw->select(
 +                              'category',
 +                              [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
 +                              [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
 +                              __METHOD__
 +                      );
 +                      foreach ( $rows as $row ) {
 +                              $cat = Category::newFromRow( $row );
 +                              $cat->refreshCounts();
 +                      }
 +              }
        }
  
        /**
@@@ -22,7 -22,6 +22,7 @@@
   */
  use MediaWiki\Linker\LinkRenderer;
  use MediaWiki\MediaWikiServices;
 +use Wikimedia\ScopedCallback;
  
  /**
   * @defgroup Parser Parser
@@@ -1446,7 -1445,6 +1446,7 @@@ class Parser 
                                $keyword = 'RFC';
                                $urlmsg = 'rfcurl';
                                $cssClass = 'mw-magiclink-rfc';
 +                              $trackingCat = 'magiclink-tracking-rfc';
                                $id = $m[5];
                        } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
                                if ( !$this->mOptions->getMagicPMIDLinks() ) {
                                $keyword = 'PMID';
                                $urlmsg = 'pubmedurl';
                                $cssClass = 'mw-magiclink-pmid';
 +                              $trackingCat = 'magiclink-tracking-pmid';
                                $id = $m[5];
                        } else {
                                throw new MWException( __METHOD__ . ': unrecognised match type "' .
                                        substr( $m[0], 0, 20 ) . '"' );
                        }
                        $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
 +                      $this->addTrackingCategory( $trackingCat );
                        return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
                } elseif ( isset( $m[6] ) && $m[6] !== ''
                        && $this->mOptions->getMagicISBNLinks()
                                ' ' => '',
                                'x' => 'X',
                        ] );
 +                      $this->addTrackingCategory( 'magiclink-tracking-isbn' );
                        return $this->getLinkRenderer()->makeKnownLink(
                                SpecialPage::getTitleFor( 'Booksources', $num ),
                                "ISBN $isbn",
                        return $attrText;
                }
  
 +              // We can't safely check if the expansion for $content resulted in an
 +              // error, because the content could happen to be the error string
 +              // (T149622).
                $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
 -              if ( substr( $content, 0, $errorLen ) === $errorStr ) {
 -                      // See above
 -                      return $content;
 -              }
  
                $marker = self::MARKER_PREFIX . "-$name-"
                        . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
  
                        # HTML names must be case-insensitively unique (bug 10721).
                        # This does not apply to Unicode characters per
-                       # http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
+                       # https://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
                        # @todo FIXME: We may be changing them depending on the current locale.
                        $arrayKey = strtolower( $safeHeadline );
                        if ( $legacyHeadline === false ) {
@@@ -388,6 -388,7 +388,6 @@@ class ResourceLoader implements LoggerA
                                }
                        }
                }
 -
        }
  
        /**
                        // Keep track of their names so that they can be loaded together
                        $this->testModuleNames[$id] = array_keys( $testModules[$id] );
                }
 -
        }
  
        /**
                // back for subsequent output, resulting in invalid GZIP. So we have to wrap
                // the whole thing in our own output buffer to be sure the active buffer
                // doesn't use ob_gzhandler.
-               // See http://bugs.php.net/bug.php?id=36514
+               // See https://bugs.php.net/bug.php?id=36514
                ob_start();
  
                // Find out which modules are missing and instantiate the others
                }
  
                // See RFC 2616 § 3.11 Entity Tags
-               // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
+               // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
                $etag = 'W/"' . $versionHash . '"';
  
                // Try the client-side cache first
  
                $this->errors = [];
                echo $response;
 -
        }
  
        /**
                        header( 'Content-Type: text/javascript; charset=utf-8' );
                }
                // See RFC 2616 § 14.19 ETag
-               // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
+               // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
                header( 'ETag: ' . $etag );
                if ( $context->getDebug() ) {
                        // Do not cache debug responses
         */
        protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
                // See RFC 2616 § 14.26 If-None-Match
-               // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
+               // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
                $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
                // Never send 304s in debug mode
                if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
                        // response (because the gzip header is always there). This is
                        // a problem because 304 responses have to be completely empty
                        // per the HTTP spec, and Firefox behaves buggily when they're not.
-                       // See also http://bugs.php.net/bug.php?id=51579
+                       // See also https://bugs.php.net/bug.php?id=51579
                        // To work around this, we tear down all output buffering before
                        // sending the 304.
                        wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
@@@ -1139,6 -1142,7 +1139,6 @@@ MESSAGE
        protected static function makeLoaderImplementScript(
                $name, $scripts, $styles, $messages, $templates
        ) {
 -
                if ( $scripts instanceof XmlJsCode ) {
                        $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
                } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
diff --combined includes/user/User.php
@@@ -26,7 -26,6 +26,7 @@@ use MediaWiki\Session\Token
  use MediaWiki\Auth\AuthManager;
  use MediaWiki\Auth\AuthenticationResponse;
  use MediaWiki\Auth\AuthenticationRequest;
 +use Wikimedia\ScopedCallback;
  
  /**
   * String Some punctuation to prevent editing from broken text-mangling proxies.
@@@ -321,7 -320,7 +321,7 @@@ class User implements IDBAccessObject 
         * @return string
         */
        public function __toString() {
 -              return $this->getName();
 +              return (string)$this->getName();
        }
  
        /**
  
                // Extensions
                Hooks::run( 'GetBlockedStatus', [ &$this ] );
 -
        }
  
        /**
         * @return bool True if blacklisted.
         */
        public function inDnsBlacklist( $ip, $bases ) {
 -
                $found = false;
-               // @todo FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
+               // @todo FIXME: IPv6 ???  (https://bugs.php.net/bug.php?id=33170)
                if ( IP::isIPv4( $ip ) ) {
                        // Reverse IP, bug 21255
                        $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
@@@ -56,13 -56,13 +56,13 @@@ class UIDGenerator 
                if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
                        MediaWiki\suppressWarnings();
                        if ( wfIsWindows() ) {
-                               // http://technet.microsoft.com/en-us/library/bb490913.aspx
+                               // https://technet.microsoft.com/en-us/library/bb490913.aspx
                                $csv = trim( wfShellExec( 'getmac /NH /FO CSV' ) );
                                $line = substr( $csv, 0, strcspn( $csv, "\n" ) );
                                $info = str_getcsv( $line );
                                $nodeId = isset( $info[0] ) ? str_replace( '-', '', $info[0] ) : '';
                        } elseif ( is_executable( '/sbin/ifconfig' ) ) { // Linux/BSD/Solaris/OS X
-                               // See http://linux.die.net/man/8/ifconfig
+                               // See https://linux.die.net/man/8/ifconfig
                                $m = [];
                                preg_match( '/\s([0-9a-f]{2}(:[0-9a-f]{2}){5})\s/',
                                        wfShellExec( '/sbin/ifconfig -a' ), $m );
        protected function timeWaitUntil( array $time ) {
                do {
                        $ct = self::millitime();
-                       if ( $ct >= $time ) { // http://php.net/manual/en/language.operators.comparison.php
+                       if ( $ct >= $time ) { // https://secure.php.net/manual/en/language.operators.comparison.php
                                return $ct; // current timestamp is higher than $time
                        }
                } while ( ( ( $time[0] - $ct[0] ) * 1000 + ( $time[1] - $ct[1] ) ) <= 10 );
                        $ts = ( 1000 * $sec + $msec ) * 10000 + (int)$offset + $delta;
                        $id_bin = str_pad( decbin( $ts % pow( 2, 60 ) ), 60, '0', STR_PAD_LEFT );
                } elseif ( extension_loaded( 'gmp' ) ) {
 -                      $ts = gmp_add( gmp_mul( (string) $sec, '1000' ), (string) $msec ); // ms
 +                      $ts = gmp_add( gmp_mul( (string)$sec, '1000' ), (string)$msec ); // ms
                        $ts = gmp_add( gmp_mul( $ts, '10000' ), $offset ); // 100ns intervals
 -                      $ts = gmp_add( $ts, (string) $delta );
 +                      $ts = gmp_add( $ts, (string)$delta );
                        $ts = gmp_mod( $ts, gmp_pow( '2', '60' ) ); // wrap around
                        $id_bin = str_pad( gmp_strval( $ts, 2 ), 60, '0', STR_PAD_LEFT );
                } elseif ( extension_loaded( 'bcmath' ) ) {
@@@ -89,7 -89,7 +89,7 @@@ table.toc td 
        display: table-cell;
        /*
        Text decorations are not propagated to the contents of inline blocks and inline tables,
-       according to <http://www.w3.org/TR/css-text-decor-3/#line-decoration>, and 'display: table-cell'
+       according to <https://www.w3.org/TR/css-text-decor-3/#line-decoration>, and 'display: table-cell'
        generates an inline table when used without any parent table-rows and tables.
        */
        text-decoration: inherit;
@@@ -99,8 -99,8 +99,8 @@@
  .tocnumber {
        padding-left: 0;
        padding-right: 0.5em;
 +      color: #222;
  }
 -
  /* @noflip */
  .mw-content-ltr .tocnumber {
        padding-left: 0;
        }() );
  
        /**
 -       * Create an object that can be read from or written to from methods that allow
 +       * Create an object that can be read from or written to via methods that allow
         * interaction both with single and multiple properties at once.
         *
 -       *     @example
 -       *
 -       *     var collection, query, results;
 -       *
 -       *     // Create your address book
 -       *     collection = new mw.Map();
 -       *
 -       *     // This data could be coming from an external source (eg. API/AJAX)
 -       *     collection.set( {
 -       *         'John Doe': 'john@example.org',
 -       *         'Jane Doe': 'jane@example.org',
 -       *         'George van Halen': 'gvanhalen@example.org'
 -       *     } );
 -       *
 -       *     wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
 -       *
 -       *     // You can detect missing keys first
 -       *     if ( !collection.exists( wanted ) ) {
 -       *         // One or more are missing (in this case: "Daniel Jackson")
 -       *         mw.log( 'One or more names were not found in your address book' );
 -       *     }
 -       *
 -       *     // Or just let it give you what it can. Optionally fill in from a default.
 -       *     results = collection.get( wanted, 'nobody@example.com' );
 -       *     mw.log( results['Jane Doe'] ); // "jane@example.org"
 -       *     mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
 -       *
 +       * @private
         * @class mw.Map
         *
         * @constructor
 -       * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
 -       *  empty object.
 -       *  For backwards-compatibility with mw.config, this can also be `true` in which case values
 -       *  are copied to the Window object as global variables (T72470). Values are copied in
 -       *  one direction only. Changes to globals are not reflected in the map.
 +       * @param {boolean} [global=false] Whether to synchronise =values to the global
 +       *  window object (for backwards-compatibility with mw.config; T72470). Values are
 +       *  copied in one direction only. Changes to globals do not reflect in the map.
         */
 -      function Map( values ) {
 -              if ( values === true ) {
 -                      this.values = {};
 +      function Map( global ) {
 +              this.internalValues = {};
 +              if ( global === true ) {
  
                        // Override #set to also set the global variable
                        this.set = function ( selection, value ) {
                                }
                                return false;
                        };
 -
 -                      return;
                }
  
 -              this.values = values || {};
 +              // Deprecated since MediaWiki 1.28
 +              log.deprecate(
 +                      this,
 +                      'values',
 +                      this.internalValues,
 +                      'mw.Map#values is deprecated. Use mw.Map#get() instead.',
 +                      'Map-values'
 +              );
        }
  
        /**
         * @param {Mixed} value
         */
        function setGlobalMapValue( map, key, value ) {
 -              map.values[ key ] = value;
 -              mw.log.deprecate(
 +              map.internalValues[ key ] = value;
 +              log.deprecate(
                                window,
                                key,
                                value,
        }
  
        Map.prototype = {
 +              constructor: Map,
 +
                /**
                 * Get the value of one or more keys.
                 *
                 * @param {Mixed} [fallback=null] Value for keys that don't exist.
                 * @return {Mixed|Object| null} If selection was a string, returns the value,
                 *  If selection was an array, returns an object of key/values.
 -               *  If no selection is passed, the 'values' container is returned. (Beware that,
 +               *  If no selection is passed, the internal container is returned. (Beware that,
                 *  as is the default in JavaScript, the object is returned by reference.)
                 */
                get: function ( selection, fallback ) {
                        }
  
                        if ( typeof selection === 'string' ) {
 -                              if ( !hasOwn.call( this.values, selection ) ) {
 +                              if ( !hasOwn.call( this.internalValues, selection ) ) {
                                        return fallback;
                                }
 -                              return this.values[ selection ];
 +                              return this.internalValues[ selection ];
                        }
  
                        if ( selection === undefined ) {
 -                              return this.values;
 +                              return this.internalValues;
                        }
  
                        // Invalid selection key
  
                        if ( $.isPlainObject( selection ) ) {
                                for ( s in selection ) {
 -                                      this.values[ s ] = selection[ s ];
 +                                      this.internalValues[ s ] = selection[ s ];
                                }
                                return true;
                        }
                        if ( typeof selection === 'string' && arguments.length > 1 ) {
 -                              this.values[ selection ] = value;
 +                              this.internalValues[ selection ] = value;
                                return true;
                        }
                        return false;
  
                        if ( $.isArray( selection ) ) {
                                for ( s = 0; s < selection.length; s++ ) {
 -                                      if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.values, selection[ s ] ) ) {
 +                                      if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.internalValues, selection[ s ] ) ) {
                                                return false;
                                        }
                                }
                                return true;
                        }
 -                      return typeof selection === 'string' && hasOwn.call( this.values, selection );
 +                      return typeof selection === 'string' && hasOwn.call( this.internalValues, selection );
                }
        };
  
                 * @param {string} key Name of property to create in `obj`
                 * @param {Mixed} val The value this property should return when accessed
                 * @param {string} [msg] Optional text to include in the deprecation message
 +               * @param {string} [logName=key] Optional custom name for the feature.
 +               *  This is used instead of `key` in the message and `mw.deprecate` tracking.
                 */
                log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
                        obj[ key ] = val;
 -              } : function ( obj, key, val, msg ) {
 -                      msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
 +              } : function ( obj, key, val, msg, logName ) {
 +                      logName = logName || key;
 +                      msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
                        var logged = new StringSet();
                        function uniqueTrace() {
                                var trace = new Error().stack;
                                        enumerable: true,
                                        get: function () {
                                                if ( uniqueTrace() ) {
 -                                                      mw.track( 'mw.deprecate', key );
 +                                                      mw.track( 'mw.deprecate', logName );
                                                        mw.log.warn( msg );
                                                }
                                                return val;
                                        },
                                        set: function ( newVal ) {
                                                if ( uniqueTrace() ) {
 -                                                      mw.track( 'mw.deprecate', key );
 +                                                      mw.track( 'mw.deprecate', logName );
                                                        mw.log.warn( msg );
                                                }
                                                val = newVal;
                                }
                        }
  
 +                      /**
 +                       * @private
 +                       * @param {string[]} implementations Array containing pieces of JavaScript code in the
 +                       *  form of calls to mw.loader#implement().
 +                       * @param {Function} cb Callback in case of failure
 +                       * @param {Error} cb.err
 +                       */
 +                      function asyncEval( implementations, cb ) {
 +                              if ( !implementations.length ) {
 +                                      return;
 +                              }
 +                              mw.requestIdleCallback( function () {
 +                                      try {
 +                                              $.globalEval( implementations.join( ';' ) );
 +                                      } catch ( err ) {
 +                                              cb( err );
 +                                      }
 +                              } );
 +                      }
 +
                        /**
                         * Make a versioned key for a specific module.
                         *
                                }
                                return {
                                        name: key.slice( 0, index ),
 -                                      version: key.slice( index )
 +                                      version: key.slice( index + 1 )
                                };
                        }
  
                                 * @protected
                                 */
                                work: function () {
 -                                      var q, batch, concatSource, origBatch;
 +                                      var q, batch, implementations, sourceModules;
  
                                        batch = [];
  
  
                                        mw.loader.store.init();
                                        if ( mw.loader.store.enabled ) {
 -                                              concatSource = [];
 -                                              origBatch = batch;
 +                                              implementations = [];
 +                                              sourceModules = [];
                                                batch = $.grep( batch, function ( module ) {
 -                                                      var source = mw.loader.store.get( module );
 -                                                      if ( source ) {
 -                                                              concatSource.push( source );
 +                                                      var implementation = mw.loader.store.get( module );
 +                                                      if ( implementation ) {
 +                                                              implementations.push( implementation );
 +                                                              sourceModules.push( module );
                                                                return false;
                                                        }
                                                        return true;
                                                } );
 -                                              try {
 -                                                      $.globalEval( concatSource.join( ';' ) );
 -                                              } catch ( err ) {
 +                                              asyncEval( implementations, function ( err ) {
                                                        // Not good, the cached mw.loader.implement calls failed! This should
                                                        // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
                                                        // Depending on how corrupt the string is, it is likely that some
                                                        // modules' implement() succeeded while the ones after the error will
                                                        // never run and leave their modules in the 'loading' state forever.
 +                                                      mw.loader.store.stats.failed++;
  
                                                        // Since this is an error not caused by an individual module but by
                                                        // something that infected the implement call itself, don't take any
                                                        // risks and clear everything in this cache.
                                                        mw.loader.store.clear();
 -                                                      // Re-add the ones still pending back to the batch and let the server
 -                                                      // repopulate these modules to the cache.
 -                                                      // This means that at most one module will be useless (the one that had
 -                                                      // the error) instead of all of them.
 +
                                                        mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
 -                                                      origBatch = $.grep( origBatch, function ( module ) {
 +                                                      // Re-add the failed ones that are still pending back to the batch
 +                                                      var failed = $.grep( sourceModules, function ( module ) {
                                                                return registry[ module ].state === 'loading';
                                                        } );
 -                                                      batch = batch.concat( origBatch );
 -                                              }
 +                                                      batchRequest( failed );
 +                                              } );
                                        }
  
                                        batchRequest( batch );
                                        items: {},
  
                                        // Cache hit stats
 -                                      stats: { hits: 0, misses: 0, expired: 0 },
 +                                      stats: { hits: 0, misses: 0, expired: 0, failed: 0 },
  
                                        /**
                                         * Construct a JSON-serializable object representing the content of the store.
                                 *  - this.Raw: The raw value is directly included.
                                 *  - this.Cdata: The raw value is directly included. An exception is
                                 *    thrown if it contains any illegal ETAGO delimiter.
-                                *    See <http://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
+                                *    See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
                                 * @return {string} HTML
                                 */
                                element: function ( name, attrs, contents ) {
@@@ -1,5 -1,5 +1,5 @@@
  # MediaWiki Parser test cases
- # Some taken from http://meta.wikimedia.org/wiki/Parser_testing
+ # Some taken from https://meta.wikimedia.org/wiki/Parser_testing
  # All (C) their respective authors and released under the GPL
  #
  # The syntax should be fairly self-explanatory.
@@@ -524,7 -524,7 +524,7 @@@ http://fr.wikipedia.org/wiki/ð\9f\8d
  !! end
  
  # Note that the html+tidy output removes the spaces after the <li>,
- # which is a bug (http://sourceforge.net/p/tidy/bugs/945/, etc).
+ # which is a bug (https://sourceforge.net/p/tidy/bugs/945/, etc).
  # This is an issue for all tests with lists.  We intentionally do
  # *not* add html+tidy clauses for these, as we don't want to
  # document/test the broken behavior.  (Parsoid matches the non-tidy
@@@ -1230,7 -1230,7 +1230,7 @@@ Text-level semantic html elements in wi
  !! end
  
  # test cases taken from
- # http://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element
+ # https://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element
  !! test
  Ruby markup (W3C-style)
  !! wikitext
@@@ -1293,7 -1293,7 +1293,7 @@@ Non-word characters don't terminate ta
  </p>
  !! end
  
- # There is a tidy bug here: http://sourceforge.net/p/tidy/bugs/946/
+ # There is a tidy bug here: https://sourceforge.net/p/tidy/bugs/946/
  # If the non-word-character tag made it through the sanitizer, tidy
  # would munge it up.
  !! test
@@@ -1420,15 -1420,6 +1420,15 @@@ sed abit
  </span></p>
  !! end
  
 +!! test
 +Don't parse <nowiki><span class="error"></nowiki> (T149622)
 +!! wikitext
 +<nowiki><span class="error"></nowiki>
 +!! html/php
 +<p>&lt;span class="error"&gt;
 +</p>
 +!! end
 +
  !! test
  nowiki 3
  !! wikitext
@@@ -3849,7 -3840,7 +3849,7 @@@ Definition Lists: Hacky use to indent t
  ## All Parsoid only definition list tests have this difference.
  ##
  ## See also: https://phabricator.wikimedia.org/T8569
- ## and http://lists.wikimedia.org/pipermail/wikitext-l/2011-November/000483.html
+ ## and https://lists.wikimedia.org/pipermail/wikitext-l/2011-November/000483.html
  
  !! test
  Table / list interaction: indented table with lists in table contents
@@@ -5203,7 -5194,7 +5203,7 @@@ http://www.example.com/?title=AT%26
  <p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a></p>
  !! end
  
- # According to http://www.w3.org/TR/2011/WD-html5-20110525/Overview.html#parsing-urls a plain
+ # According to https://www.w3.org/TR/2011/WD-html5-20110525/Overview.html#parsing-urls a plain
  # % is actually legal in HTML5. Any change in output would need testing though.
  !! test
  Bug 4781, 5267: %25 in URL
@@@ -5790,7 -5781,7 +5790,7 @@@ Plain ''italic'''s plai
  
  # This should not produce <table></table> as <table><tr><td></td></tr></table>
  # is the bare minimum required by the spec, see:
- # http://www.w3.org/TR/xhtml-modularization/dtd_module_defs.html#a_module_Basic_Tables
+ # https://www.w3.org/TR/xhtml-modularization/dtd_module_defs.html#a_module_Basic_Tables
  # Parsoid team replies: empty table tags are legal in HTML5
  !! test
  A table with no data.
@@@ -19107,7 -19098,7 +19107,7 @@@ parsoid=wt2html,wt2wt,html2htm
  <p><span typeof="mw:Entity">î</span><span typeof="mw:Entity">î</span></p>
  !! end
  
- # See: http://www.w3.org/TR/html5/syntax.html#character-references
+ # See: https://www.w3.org/TR/html5/syntax.html#character-references
  # Note that U+000C (form feed) is not a valid XML character, so
  # it is banned even though allowed in HTML5.
  !! test
@@@ -92,6 -92,7 +92,6 @@@ class HtmlTest extends MediaWikiTestCas
         * @covers Html::expandAttributes
         */
        public function testExpandAttributesSkipsNullAndFalse() {
 -
                # ## EMPTY ########
                $this->assertEmpty(
                        Html::expandAttributes( [ 'foo' => null ] ),
                        Html::expandAttributes( [ 'zero' => 0 ] ),
                        'Number 0 value needs no quotes'
                );
 -
        }
  
        /**
  
        /**
         * List of input element types values introduced by HTML5
-        * Full list at http://www.w3.org/TR/html-markup/input.html
+        * Full list at https://www.w3.org/TR/html-markup/input.html
         */
        public static function provideHtml5InputTypes() {
                $types = [
@@@ -349,7 -349,7 +349,7 @@@ class UploadBaseTest extends MediaWikiT
                        ],
                        [
                                // This currently doesn't seem to work in any browsers, but in case
-                               // http://www.w3.org/TR/css3-images/ is implemented for SVG files
+                               // https://www.w3.org/TR/css3-images/ is implemented for SVG files
                                '<svg xmlns="http://www.w3.org/2000/svg"> <rect width="100" height="100" style="background-image:image(\'sprites.svg#xywh=40,0,20,20\')"/> </svg>',
                                true,
                                true,
                // @codingStandardsIgnoreEnd
        }
  
 +      /**
 +       * @dataProvider provideDetectScriptInSvg
 +       */
 +      public function testDetectScriptInSvg( $svg, $expected, $message ) {
 +              // This only checks some weird cases, most tests are in testCheckSvgScriptCallback() above
 +              $result = $this->upload->detectScriptInSvg( $svg, false );
 +              $this->assertSame( $expected, $result, $message );
 +      }
 +
 +      public static function provideDetectScriptInSvg() {
 +              global $IP;
 +              return [
 +                      [
 +                              "$IP/tests/phpunit/data/upload/buggynamespace-original.svg",
 +                              false,
 +                              'SVG with a weird but valid namespace definition created by Adobe Illustrator'
 +                      ],
 +                      [
 +                              "$IP/tests/phpunit/data/upload/buggynamespace-okay.svg",
 +                              false,
 +                              'SVG with a namespace definition created by Adobe Illustrator and mangled by Inkscape'
 +                      ],
 +                      [
 +                              "$IP/tests/phpunit/data/upload/buggynamespace-okay2.svg",
 +                              false,
 +                              'SVG with a namespace definition created by Adobe Illustrator and mangled by Inkscape (twice)'
 +                      ],
 +                      [
 +                              "$IP/tests/phpunit/data/upload/buggynamespace-bad.svg",
 +                              [ 'uploadscriptednamespace', 'i' ],
 +                              'SVG with a namespace definition using an undefined entity'
 +                      ],
 +                      [
 +                              "$IP/tests/phpunit/data/upload/buggynamespace-evilhtml.svg",
 +                              [ 'uploadscriptednamespace', 'http://www.w3.org/1999/xhtml' ],
 +                              'SVG with an html namespace encoded as an entity'
 +                      ],
 +              ];
 +      }
 +
        /**
         * @dataProvider provideCheckXMLEncodingMissmatch
         */
@@@ -482,11 -442,4 +482,11 @@@ class UploadTestHandler extends UploadB
                );
                return [ $check->wellFormed, $check->filterMatch ];
        }
 +
 +      /**
 +       * Same as parent function, but override visibility to 'public'.
 +       */
 +      public function detectScriptInSvg( $filename, $partial ) {
 +              return parent::detectScriptInSvg( $filename, $partial );
 +      }
  }
@@@ -1,5 -1,4 +1,5 @@@
  <?php
 +use Wikimedia\ScopedCallback;
  
  /**
   * The UnitTest must be either a class that inherits from MediaWikiTestCase
@@@ -108,7 -107,7 +108,7 @@@ class ParserTestTopLevelSuite extends P
                        $testsName = $extensionName . '__' . basename( $fileName, '.txt' );
                        $parserTestClassName = ucfirst( $testsName );
  
-                       // Official spec for class names: http://php.net/manual/en/language.oop5.basic.php
+                       // Official spec for class names: https://secure.php.net/manual/en/language.oop5.basic.php
                        // Prepend 'ParserTest_' to be paranoid about it not starting with a number
                        $parserTestClassName = 'ParserTest_' .
                                preg_replace( '/[^a-zA-Z0-9_\x7f-\xff]/', '_', $parserTestClassName );