From: jenkins-bot Date: Tue, 8 Nov 2016 21:32:00 +0000 (+0000) Subject: Merge "Update weblinks in comments from HTTP to HTTPS" X-Git-Tag: 1.31.0-rc.0~4920 X-Git-Url: http://git.cyclocoop.org/%22.%28%24lien.?a=commitdiff_plain;h=69ae945e8d39972a07bea89ddb64bc0189b43ac2;hp=-c;p=lhc%2Fweb%2Fwiklou.git Merge "Update weblinks in comments from HTTP to HTTPS" --- 69ae945e8d39972a07bea89ddb64bc0189b43ac2 diff --combined includes/DefaultSettings.php index 68e4f5d5be,7cdd7c317d..9d8ccf89c4 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.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+', * ]; @@@ -7148,7 -7145,7 +7148,7 @@@ * - 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 index e5589447ee,6706db7e53..725a512980 --- a/includes/FormOptions.php +++ b/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; /* @} */ /** @@@ -123,8 -120,6 +123,8 @@@ return self::FLOAT; } elseif ( is_string( $data ) ) { return self::STRING; + } elseif ( is_array( $data ) ) { + return self::ARR; } else { throw new MWException( 'Unsupported datatype' ); } @@@ -362,9 -357,6 +362,9 @@@ break; case self::INTNULL: $value = $r->getIntOrNull( $name ); + break; + case self::ARR: + $value = $r->getArray( $name ); break; default: throw new MWException( 'Unsupported datatype' ); @@@ -378,7 -370,7 +378,7 @@@ /** @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 */ /* @{ */ /** diff --combined includes/GlobalFunctions.php index 15a73cfc30,ec097029eb..b3ccc5675a --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.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; @@@ -1670,9 -1668,7 +1670,9 @@@ 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 = [ '"' => '"', '&' => '&', "'" => ''', '<' => '<', '=' => '=', '>' => '>', '[' => '[', ']' => ']', @@@ -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. diff --combined includes/MediaWikiServices.php index a0c00c6c07,4e80ba1c08..7c9363ca19 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@@ -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' ) { @@@ -192,6 -190,7 +192,6 @@@ } else { $oldInstance->destroy(); } - } /** @@@ -297,7 -296,7 +297,7 @@@ 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() ); } @@@ -533,14 -532,6 +533,14 @@@ return $this->getService( 'CryptRand' ); } + /** + * @since 1.28 + * @return CryptHKDF + */ + public function getCryptHKDF() { + return $this->getService( 'CryptHKDF' ); + } + /** * @since 1.28 * @return MediaHandlerFactory @@@ -565,14 -556,6 +565,14 @@@ 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 index 12df3a5880,4d8f836836..76bfaa26f9 --- a/includes/OutputPage.php +++ b/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; @@@ -1224,8 -1224,8 +1224,8 @@@ /** * 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; @@@ -1234,7 -1234,7 +1234,7 @@@ /** * 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; @@@ -1713,6 -1713,7 +1713,6 @@@ $popts->setTidy( $oldTidy ); $this->addParserOutput( $parserOutput ); - } /** @@@ -1781,9 -1782,7 +1781,9 @@@ } // 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. @@@ -1932,36 -1931,6 +1932,36 @@@ $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 * @@@ -2197,26 -2166,22 +2197,26 @@@ # 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 @@@ -2425,14 -2390,10 +2425,14 @@@ /** * 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 @@@ -2725,13 -2686,15 +2725,13 @@@ $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, @@@ -2810,8 -2773,8 +2810,8 @@@ // 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' ] ); } @@@ -2966,14 -2929,12 +2966,14 @@@ } } - $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 ); } @@@ -3695,7 -3656,7 +3695,7 @@@ 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 index 8deee5c8e1,80d9ceb350..ce2f930a81 --- a/includes/api/ApiMain.php +++ b/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) */ @@@ -719,7 -719,7 +719,7 @@@ $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" ); } @@@ -1475,7 -1475,7 +1475,7 @@@ 'ip' => $request->getIP(), 'userAgent' => $this->getUserAgent(), 'wiki' => wfWikiID(), - 'timeSpentBackend' => (int) round( $time * 1000 ), + 'timeSpentBackend' => (int)round( $time * 1000 ), 'hadError' => $e !== null, 'errorCodes' => [], 'params' => [], diff --combined includes/htmlform/fields/HTMLFloatField.php index 5dbccfd43e,1eb332605b..d1250c06f7 --- a/includes/htmlform/fields/HTMLFloatField.php +++ b/includes/htmlform/fields/HTMLFloatField.php @@@ -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(); diff --combined includes/htmlform/fields/HTMLIntField.php index 41916edc21,f9ee2b2c91..c87a778a4a --- a/includes/htmlform/fields/HTMLIntField.php +++ b/includes/htmlform/fields/HTMLIntField.php @@@ -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 diff --combined includes/jobqueue/JobQueueDB.php index 84b2a0b2dd,5ec11274d4..0a8ae7fd51 --- a/includes/jobqueue/JobQueueDB.php +++ b/includes/jobqueue/JobQueueDB.php @@@ -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__ @@@ -123,7 -122,7 +123,7 @@@ return $count; } - $dbr = $this->getSlaveDB(); + $dbr = $this->getReplicaDB(); try { $count = (int)$dbr->selectField( 'job', 'COUNT(*)', [ 'job_cmd' => $this->type, "job_token != {$dbr->addQuotes( '' )}" ], @@@ -154,7 -153,7 +154,7 @@@ return $count; } - $dbr = $this->getSlaveDB(); + $dbr = $this->getReplicaDB(); try { $count = (int)$dbr->selectField( 'job', 'COUNT(*)', [ @@@ -324,7 -323,7 +324,7 @@@ $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 @@@ -398,7 -397,7 +398,7 @@@ $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. @@@ -566,7 -565,7 +566,7 @@@ * @return Iterator */ protected function getJobIterator( array $conds ) { - $dbr = $this->getSlaveDB(); + $dbr = $this->getReplicaDB(); try { return new MappedIterator( $dbr->select( 'job', self::selectFields(), $conds ), @@@ -594,7 -593,7 +594,7 @@@ } 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 @@@ -611,7 -610,7 +611,7 @@@ } 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' ] ); @@@ -737,7 -736,7 +737,7 @@@ * @throws JobQueueConnectionError * @return DBConnRef */ - protected function getSlaveDB() { + protected function getReplicaDB() { try { return $this->getDB( DB_REPLICA ); } catch ( DBConnectionError $e ) { diff --combined includes/jobqueue/JobRunner.php index 0469eebc66,79bfab33d0..990f112a60 --- a/includes/jobqueue/JobRunner.php +++ b/includes/jobqueue/JobRunner.php @@@ -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; } diff --combined includes/libs/filebackend/FileBackend.php index 5764cc3397,97f148c6fa..15f13b9b89 --- a/includes/libs/filebackend/FileBackend.php +++ b/includes/libs/filebackend/FileBackend.php @@@ -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 ); diff --combined includes/libs/filebackend/SwiftFileBackend.php index 1593457f43,12eda7349e..08cb388e2a --- a/includes/libs/filebackend/SwiftFileBackend.php +++ b/includes/libs/filebackend/SwiftFileBackend.php @@@ -176,6 -176,7 +176,6 @@@ class SwiftFileBackend extends FileBack return isset( $params['headers'] ) ? $this->getCustomHeaders( $params['headers'] ) : []; - } /** @@@ -1208,7 -1209,7 +1208,7 @@@ $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 ) . @@@ -1304,7 -1305,7 +1304,7 @@@ /** * 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. diff --combined includes/libs/rdbms/database/Database.php index ee4524fae8,0826355b04..3d35d76ca3 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@@ -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; @@@ -383,7 -382,7 +383,7 @@@ } 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 ); }; } @@@ -609,11 -608,6 +609,11 @@@ return !!( $this->mFlags & $flag ); } + /** + * @param string $name Class field name + * @return mixed + * @deprecated Since 1.28 + */ public function getProperty( $name ) { return $this->$name; } @@@ -657,22 -651,14 +657,22 @@@ 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( '!\[\]!', '', $this->mPHPError ); $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error ); return $error; - } else { - return false; } + + return false; } /** @@@ -787,11 -773,8 +787,11 @@@ * @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 + ); } /** @@@ -898,7 -881,7 +898,7 @@@ 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 ) { @@@ -1721,9 -1704,9 +1721,9 @@@ } 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; @@@ -1745,35 -1728,29 +1745,35 @@@ # 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() { @@@ -2851,7 -2828,7 +2851,7 @@@ $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)." ); } @@@ -3272,7 -3249,7 +3272,7 @@@ $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)." ); } @@@ -3391,28 -3368,6 +3391,28 @@@ 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 @@@ -3467,11 -3422,8 +3467,11 @@@ } 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; } diff --combined includes/libs/rdbms/database/DatabaseMysqlBase.php index 76208c4c46,3e24ce6116..668443ba76 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@@ -94,7 -94,7 +94,7 @@@ abstract class DatabaseMysqlBase extend /** * @return string */ - function getType() { + public function getType() { return 'mysql'; } @@@ -106,7 -106,7 +106,7 @@@ * @throws Exception|DBConnectionError * @return bool */ - function open( $server, $user, $password, $dbName ) { + public function open( $server, $user, $password, $dbName ) { # Close/unset connection handle $this->close(); @@@ -237,7 -237,7 +237,7 @@@ * @param ResultWrapper|resource $res * @throws DBUnexpectedError */ - function freeResult( $res ) { + public function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@@ -262,7 -262,7 +262,7 @@@ * @return stdClass|bool * @throws DBUnexpectedError */ - function fetchObject( $res ) { + public function fetchObject( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@@ -274,7 -274,7 +274,7 @@@ // 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, @@@ -298,7 -298,7 +298,7 @@@ * @return array|bool * @throws DBUnexpectedError */ - function fetchRow( $res ) { + public function fetchRow( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@@ -310,7 -310,7 +310,7 @@@ // 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, @@@ -345,7 -345,7 +345,7 @@@ // 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; } @@@ -362,7 -362,7 +362,7 @@@ * @param ResultWrapper|resource $res * @return int */ - function numFields( $res ) { + public function numFields( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@@ -383,7 -383,7 +383,7 @@@ * @param int $n * @return string */ - function fieldName( $res, $n ) { + public function fieldName( $res, $n ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@@ -428,7 -428,7 +428,7 @@@ * @param int $row * @return bool */ - function dataSeek( $res, $row ) { + public function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@@ -448,7 -448,7 +448,7 @@@ /** * @return string */ - function lastError() { + public function lastError() { if ( $this->mConn ) { # Even if it's non-zero, it can still be invalid MediaWiki\suppressWarnings(); @@@ -482,7 -482,7 +482,7 @@@ * @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 ); } @@@ -518,7 -518,7 +518,7 @@@ 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 @@@ -534,7 -534,7 +534,7 @@@ * @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 ) { @@@ -569,10 -569,10 +569,10 @@@ * @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 ); @@@ -598,7 -598,7 +598,7 @@@ * @param string $s * @return string */ - function strencode( $s ) { + public function strencode( $s ) { return $this->mysqlRealEscapeString( $s ); } @@@ -638,7 -638,7 +638,7 @@@ return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`'; } - function getLag() { + public function getLag() { if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) { return $this->getLagFromPtHeartbeat(); } else { @@@ -768,7 -768,7 +768,7 @@@ 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 @@@ -786,7 -786,7 +786,7 @@@ 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" ); } @@@ -839,7 -839,7 +839,7 @@@ * * @return MySQLMasterPos|bool */ - function getReplicaPos() { + public function getReplicaPos() { $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ ); $row = $this->fetchObject( $res ); @@@ -867,7 -867,7 +867,7 @@@ * * @return MySQLMasterPos|bool */ - function getMasterPos() { + public function getMasterPos() { $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ ); $row = $this->fetchObject( $res ); @@@ -1013,7 -1013,7 +1013,7 @@@ /** * 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 @@@ -1034,7 -1034,7 +1034,7 @@@ } 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; } @@@ -1108,9 -1108,7 +1108,9 @@@ * @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' ); } @@@ -1164,7 -1162,7 +1164,7 @@@ * * @return int */ - function getServerUptime() { + public function getServerUptime() { $vars = $this->getMysqlStatus( 'Uptime' ); return (int)$vars['Uptime']; @@@ -1175,7 -1173,7 +1175,7 @@@ * * @return bool */ - function wasDeadlock() { + public function wasDeadlock() { return $this->lastErrno() == 1213; } @@@ -1184,11 -1182,11 +1184,11 @@@ * * @return bool */ - function wasLockTimeout() { + public function wasLockTimeout() { return $this->lastErrno() == 1205; } - function wasErrorReissuable() { + public function wasErrorReissuable() { return $this->lastErrno() == 2013 || $this->lastErrno() == 2006; } @@@ -1197,15 -1195,37 +1197,15 @@@ * * @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 @@@ -1213,9 -1233,7 +1213,9 @@@ * @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 ); @@@ -1231,7 -1249,7 +1231,7 @@@ * @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 = []; @@@ -1267,7 -1285,7 +1267,7 @@@ * @param string $which * @return array */ - function getMysqlStatus( $which = "%" ) { + private function getMysqlStatus( $which = "%" ) { $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); $status = []; diff --combined includes/libs/rdbms/database/DatabasePostgres.php index d4d3aa85dc,e66fa0d9ba..41aa5e07f2 --- a/includes/libs/rdbms/database/DatabasePostgres.php +++ b/includes/libs/rdbms/database/DatabasePostgres.php @@@ -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( @@@ -83,6 -90,10 +83,6 @@@ ); } - if ( !strlen( $user ) ) { # e.g. the class is being loaded - return null; - } - $this->mServer = $server; $this->mUser = $user; $this->mPassword = $password; @@@ -108,8 -119,7 +108,8 @@@ $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; @@@ -118,11 -128,10 +118,11 @@@ $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 ) ); } @@@ -143,8 -152,6 +143,8 @@@ } $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; } @@@ -155,7 -162,7 +155,7 @@@ * @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 { @@@ -163,11 -170,7 +163,11 @@@ } } - function makeConnectionString( $vars ) { + /** + * @param string[] $vars + * @return string + */ + private function makeConnectionString( $vars ) { $s = ''; foreach ( $vars as $name => $value ) { $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' "; @@@ -176,22 -179,25 +176,22 @@@ 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; @@@ -221,7 -227,7 +221,7 @@@ } } - 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' ) { @@@ -239,7 -245,15 +239,7 @@@ 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; } @@@ -251,7 -265,12 +251,7 @@@ } } - /** - * @param ResultWrapper|stdClass $res - * @return stdClass - * @throws DBUnexpectedError - */ - function fetchObject( $res ) { + public function fetchObject( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@@ -262,56 -281,51 +262,56 @@@ # @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; } @@@ -319,7 -333,7 +319,7 @@@ return pg_num_fields( $res ); } - function fieldName( $res, $n ) { + public function fieldName( $res, $n ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@@ -333,11 -347,16 +333,11 @@@ * * @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; } @@@ -345,19 -364,19 +345,19 @@@ 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 { @@@ -365,7 -384,7 +365,7 @@@ } } - function affectedRows() { + public function affectedRows() { if ( !is_null( $this->mAffectedRows ) ) { // Forced result for simulated queries return $this->mAffectedRows; @@@ -391,7 -410,7 +391,7 @@@ * @param array $options * @return int */ - function estimateRowCount( $table, $vars = '*', $conds = '', + public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [] ) { $options['EXPLAIN'] = true; @@@ -408,7 -427,16 +408,7 @@@ 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 ) { @@@ -423,7 -451,15 +423,7 @@@ 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 ) ) . @@@ -493,7 -529,7 +493,7 @@@ 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 @@@ -535,7 -571,7 +535,7 @@@ * @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; } @@@ -661,10 -697,8 +661,10 @@@ * @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 ) ) { @@@ -726,38 -760,30 +726,38 @@@ 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 ); @@@ -772,7 -798,7 +772,7 @@@ * @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 ); @@@ -781,7 -807,8 +781,7 @@@ 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 @@@ -798,15 -825,15 +798,15 @@@ 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 ); @@@ -816,7 -843,7 +816,7 @@@ "(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 ); @@@ -833,7 -860,7 +833,7 @@@ return $endArray; } - function timestamp( $ts = 0 ) { + public function timestamp( $ts = 0 ) { $ct = new ConvertibleTimestamp( $ts ); return $ct->getTimestamp( TS_POSTGRES ); @@@ -841,7 -868,7 +841,7 @@@ /** * 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 @@@ -857,7 -884,7 +857,7 @@@ * @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 = []; @@@ -884,10 -911,19 +884,10 @@@ 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]'; } @@@ -899,7 -935,7 +899,7 @@@ * @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 ); @@@ -916,7 -952,7 +916,7 @@@ * @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 = []; @@@ -935,7 -971,7 +935,7 @@@ * @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 ); @@@ -951,7 -987,7 +951,7 @@@ * * @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 ) ); } @@@ -969,7 -1005,7 +969,7 @@@ * * @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() ) ) { @@@ -997,7 -1033,7 +997,7 @@@ $this->mCoreSchema . "\"\n" ); } /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */ - $this->commit( __METHOD__ ); + $this->commit( __METHOD__, self::FLUSHING_INTERNAL ); } /** @@@ -1006,14 -1042,16 +1006,14 @@@ * @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'; @@@ -1022,7 -1060,7 +1022,7 @@@ $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' ); } } @@@ -1037,13 -1075,14 +1037,13 @@@ * @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 " @@@ -1056,21 -1095,22 +1056,21 @@@ } /** - * 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 = <<selectField( 'pg_rules', 'rulename', [ 'rulename' => $rule, @@@ -1105,7 -1145,7 +1105,7 @@@ 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() ), @@@ -1126,13 -1166,9 +1126,13 @@@ * @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; } @@@ -1142,7 -1178,7 +1142,7 @@@ * @param string $roleName * @return bool */ - function roleExists( $roleName ) { + public function roleExists( $roleName ) { $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1, [ 'rolname' => $roleName ], __METHOD__ ); @@@ -1154,7 -1190,7 +1154,7 @@@ * @var string $field * @return PostgresField|null */ - function fieldInfo( $table, $field ) { + public function fieldInfo( $table, $field ) { return PostgresField::fromText( $this, $table, $field ); } @@@ -1164,7 -1200,7 +1164,7 @@@ * @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; } @@@ -1172,11 -1208,15 +1172,11 @@@ 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 ) { @@@ -1186,14 -1226,17 +1186,14 @@@ 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 ) ) { @@@ -1202,12 -1245,12 +1202,12 @@@ 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 ) . "'"; } /** @@@ -1232,7 -1275,14 +1232,7 @@@ 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 = ''; @@@ -1267,15 -1317,15 +1267,15 @@@ 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 ); } @@@ -1287,6 -1337,11 +1287,6 @@@ 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'; } @@@ -1304,8 -1359,16 +1304,8 @@@ 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 ); @@@ -1314,8 -1377,14 +1314,8 @@@ 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 ) { @@@ -1334,8 -1403,14 +1334,8 @@@ 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 ); diff --combined includes/libs/rdbms/database/DatabaseSqlite.php index 8236abfc54,90b861db10..7317d54070 --- a/includes/libs/rdbms/database/DatabaseSqlite.php +++ b/includes/libs/rdbms/database/DatabaseSqlite.php @@@ -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) */ @@@ -142,7 -145,7 +142,7 @@@ * @param string $dbName * * @throws DBConnectionError - * @return PDO + * @return bool */ function open( $server, $user, $pass, $dbName ) { $this->close(); @@@ -153,7 -156,7 +153,7 @@@ } $this->openFile( $fileName ); - return $this->mConn; + return (bool)$this->mConn; } /** @@@ -196,10 -199,6 +196,10 @@@ return false; } + public function selectDB( $db ) { + return false; // doesn't make sense + } + /** * @return string SQLite DB file path * @since 1.25 @@@ -268,7 -267,7 +268,7 @@@ } /** - * 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 @@@ -495,12 -494,15 +495,12 @@@ * @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 = []; diff --combined includes/libs/rdbms/database/IDatabase.php index f33dfc05a5,e3f5bc223e..48d76c4023 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@@ -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 */ @@@ -386,7 -393,7 +386,7 @@@ /** * 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 @@@ -395,7 -402,7 +395,7 @@@ /** * 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 @@@ -419,7 -426,7 +419,7 @@@ /** * 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 @@@ -428,7 -435,7 +428,7 @@@ /** * Get the last error number - * @see http://www.php.net/mysql_errno + * @see https://secure.php.net/mysql_errno * * @return int */ @@@ -436,7 -443,7 +436,7 @@@ /** * Get a description of the last error - * @see http://www.php.net/mysql_error + * @see https://secure.php.net/mysql_error * * @return string */ @@@ -455,7 -462,7 +455,7 @@@ /** * 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 */ @@@ -463,7 -470,7 +463,7 @@@ /** * 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. * @@@ -1088,7 -1095,7 +1088,7 @@@ * * 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 */ @@@ -1633,7 -1640,7 +1633,7 @@@ * IDatabase::insert(). * * @param string $b - * @return string + * @return string|Blob */ public function encodeBlob( $b ); diff --combined includes/libs/rdbms/lbfactory/LBFactory.php index 4fb5b383df,a4f4bff114..15a5c0d78f --- a/includes/libs/rdbms/lbfactory/LBFactory.php +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@@ -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; @@@ -153,16 -152,18 +153,16 @@@ /** * @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 @@@ -355,7 -356,6 +355,7 @@@ if ( $failed ) { throw new DBReplicationWaitError( + null, "Could not wait for replica DBs to catch up to " . implode( ', ', $failed ) ); @@@ -555,7 -555,7 +555,7 @@@ * @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 ); diff --combined includes/libs/rdbms/loadbalancer/LoadBalancer.php index a86e1decf3,4d2b746070..d42fed9590 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@@ -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 ) { @@@ -553,7 -552,7 +553,7 @@@ 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()? @@@ -887,7 -886,7 +887,7 @@@ // 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 @@@ -1529,7 -1528,7 +1529,7 @@@ * @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 ); diff --combined includes/page/WikiPage.php index dd78d19852,7aba860718..a5ac78f10d --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@@ -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 ) { @@@ -2399,10 -2399,6 +2399,10 @@@ } elseif ( $options['changed'] ) { // bug 50785 self::onArticleEdit( $this->mTitle, $revision ); } + + ResourceLoaderWikiModule::invalidateModuleCache( + $this->mTitle, $options['oldrevision'], $revision, wfWikiID() + ); } /** @@@ -2485,13 -2481,13 +2485,13 @@@ } 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'; } @@@ -2675,14 -2671,16 +2675,14 @@@ 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; } @@@ -2914,7 -2912,6 +2914,7 @@@ // 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 ) { @@@ -2924,13 -2921,17 +2924,13 @@@ $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. @@@ -2941,9 -2942,10 +2941,9 @@@ // 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' @@@ -2966,7 -2968,7 +2966,7 @@@ '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 ) { @@@ -3009,7 -3011,7 +3009,7 @@@ $dbw->endAtomic( __METHOD__ ); - $this->doDeleteUpdates( $id, $content ); + $this->doDeleteUpdates( $id, $content, $revision ); Hooks::run( 'ArticleDeleteComplete', [ &$wikiPageBeforeDelete, @@@ -3056,12 -3058,11 +3056,12 @@@ * 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 ) { @@@ -3089,9 -3090,6 +3089,9 @@@ // 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 ); @@@ -3549,103 -3547,107 +3549,103 @@@ * 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(); + } + } } /** diff --combined includes/parser/Parser.php index eafccd936b,d427cf9a6f..3f703e3cb5 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@@ -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() ) { @@@ -1455,14 -1453,12 +1455,14 @@@ $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() @@@ -1476,7 -1472,6 +1476,7 @@@ ' ' => '', 'x' => 'X', ] ); + $this->addTrackingCategory( 'magiclink-tracking-isbn' ); return $this->getLinkRenderer()->makeKnownLink( SpecialPage::getTitleFor( 'Booksources', $num ), "ISBN $isbn", @@@ -3805,10 -3800,11 +3805,10 @@@ 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; @@@ -4217,7 -4213,7 +4217,7 @@@ # 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 ) { diff --combined includes/resourceloader/ResourceLoader.php index b05b51b7fb,5ab71b19df..c9dacbcae9 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@@ -388,6 -388,7 +388,6 @@@ class ResourceLoader implements LoggerA } } } - } /** @@@ -427,6 -428,7 +427,6 @@@ // Keep track of their names so that they can be loaded together $this->testModuleNames[$id] = array_keys( $testModules[$id] ); } - } /** @@@ -668,7 -670,7 +668,7 @@@ // 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 @@@ -714,7 -716,7 +714,7 @@@ } // 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 @@@ -777,6 -779,7 +777,6 @@@ $this->errors = []; echo $response; - } /** @@@ -821,7 -824,7 +821,7 @@@ 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 @@@ -846,7 -849,7 +846,7 @@@ */ 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 ) ) { @@@ -856,7 -859,7 +856,7 @@@ // 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 index 2a59d5dacd,4a34371e16..273d555eff --- a/includes/user/User.php +++ b/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(); } /** @@@ -1658,6 -1657,7 +1658,6 @@@ // Extensions Hooks::run( 'GetBlockedStatus', [ &$this ] ); - } /** @@@ -1689,8 -1689,9 +1689,8 @@@ * @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 ) ) ); diff --combined includes/utils/UIDGenerator.php index 1fd830e4e9,b9f51b6bbf..c6d1a54dd2 --- a/includes/utils/UIDGenerator.php +++ b/includes/utils/UIDGenerator.php @@@ -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 ); @@@ -517,7 -517,7 +517,7 @@@ 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 ); @@@ -554,9 -554,9 +554,9 @@@ $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' ) ) { diff --combined resources/src/mediawiki.skinning/content.css index 022fa28e54,e5c1583331..d4c93fd266 --- a/resources/src/mediawiki.skinning/content.css +++ b/resources/src/mediawiki.skinning/content.css @@@ -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 , and 'display: table-cell' + according to , 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; diff --combined resources/src/mediawiki/mediawiki.js index da4c769805,ab3f103d94..9c8fe70bde --- a/resources/src/mediawiki/mediawiki.js +++ b/resources/src/mediawiki/mediawiki.js @@@ -66,20 -66,48 +66,20 @@@ }() ); /** - * 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 ) { @@@ -97,16 -125,11 +97,16 @@@ } 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' + ); } /** @@@ -119,8 -142,8 +119,8 @@@ * @param {Mixed} value */ function setGlobalMapValue( map, key, value ) { - map.values[ key ] = value; - mw.log.deprecate( + map.internalValues[ key ] = value; + log.deprecate( window, key, value, @@@ -130,8 -153,6 +130,8 @@@ } Map.prototype = { + constructor: Map, + /** * Get the value of one or more keys. * @@@ -141,7 -162,7 +141,7 @@@ * @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 ) { @@@ -160,14 -181,14 +160,14 @@@ } 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 @@@ -186,12 -207,12 +186,12 @@@ 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; @@@ -208,13 -229,13 +208,13 @@@ 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 ); } }; @@@ -454,14 -475,11 +454,14 @@@ * @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; @@@ -480,14 -498,14 +480,14 @@@ 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; @@@ -1662,26 -1680,6 +1662,26 @@@ } } + /** + * @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. * @@@ -1707,7 -1705,7 +1707,7 @@@ } return { name: key.slice( 0, index ), - version: key.slice( index ) + version: key.slice( index + 1 ) }; } @@@ -1735,7 -1733,7 +1735,7 @@@ * @protected */ work: function () { - var q, batch, concatSource, origBatch; + var q, batch, implementations, sourceModules; batch = []; @@@ -1765,37 -1763,39 +1765,37 @@@ 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 ); @@@ -2185,7 -2185,7 +2185,7 @@@ 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. @@@ -2505,7 -2505,7 +2505,7 @@@ * - 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 . + * See . * @return {string} HTML */ element: function ( name, attrs, contents ) { diff --combined tests/parser/parserTests.txt index 103acc609e,6166cc21c3..a18e2193fd --- a/tests/parser/parserTests.txt +++ b/tests/parser/parserTests.txt @@@ -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/🍠!! end # Note that the html+tidy output removes the spaces after the
  • , - # 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

    !! 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

    !! end +!! test +Don't parse (T149622) +!! wikitext + +!! html/php +

    <span class="error"> +

    +!! 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

    http://www.example.com/?title=AT%26T

    !! 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
    as
    # 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

    îî

    !! 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 diff --combined tests/phpunit/includes/HtmlTest.php index 95c7a610b1,c701ff9e57..e2ee193416 --- a/tests/phpunit/includes/HtmlTest.php +++ b/tests/phpunit/includes/HtmlTest.php @@@ -92,6 -92,7 +92,6 @@@ class HtmlTest extends MediaWikiTestCas * @covers Html::expandAttributes */ public function testExpandAttributesSkipsNullAndFalse() { - # ## EMPTY ######## $this->assertEmpty( Html::expandAttributes( [ 'foo' => null ] ), @@@ -189,6 -190,7 +189,6 @@@ Html::expandAttributes( [ 'zero' => 0 ] ), 'Number 0 value needs no quotes' ); - } /** @@@ -445,7 -447,7 +445,7 @@@ /** * 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 = [ diff --combined tests/phpunit/includes/upload/UploadBaseTest.php index a44926b208,d81ddd3882..6be272fb61 --- a/tests/phpunit/includes/upload/UploadBaseTest.php +++ b/tests/phpunit/includes/upload/UploadBaseTest.php @@@ -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 ' ', true, true, @@@ -397,46 -397,6 +397,46 @@@ // @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 ); + } } diff --combined tests/phpunit/suites/ParserTestTopLevelSuite.php index 5a7fc4804f,187d07e010..5d5d693571 --- a/tests/phpunit/suites/ParserTestTopLevelSuite.php +++ b/tests/phpunit/suites/ParserTestTopLevelSuite.php @@@ -1,5 -1,4 +1,5 @@@