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