From: jenkins-bot Date: Fri, 26 Jul 2019 22:52:24 +0000 (+0000) Subject: Merge "QueryPage: allow arbitrary sorting" X-Git-Tag: 1.34.0-rc.0~858 X-Git-Url: http://git.cyclocoop.org/%7B%7B%20url_for%28%27admin_vote_add%27%29%20%7D%7D?a=commitdiff_plain;h=63541200513fe472b95d17ea6bda187ef2e63807;hp=2ba42f237449780ab61071132155f7649d723bbb;p=lhc%2Fweb%2Fwiklou.git Merge "QueryPage: allow arbitrary sorting" --- diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index 7a24818f40..16c4639978 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -72,6 +72,8 @@ For notes on 1.33.x and older releases, see HISTORY. configurable via $wgDebugLogFile. * $wgPasswordSalt – This setting, used for migrating exceptionally old, insecure password setups and deprecated since 1.24, is now removed. +* $wgDBOracleDRCP - If you must use persistent connections, set DBO_PERSISTENT + in the 'flags' field for servers in $wgDBServers (or $wgLBFactoryConf). === New user-facing features in 1.34 === * Special:Mute has been added as a quick way for users to block unwanted emails @@ -309,11 +311,24 @@ because of Phabricator reports. deprecated since 1.33. * The static properties mw.Api.errors and mw.Api.warnings, deprecated in 1.29, have been removed. +* ParserOption::getSpeculativeRevIdCallback(), deprecated in 1.28, has been + removed. * The UploadVerification hook, deprecated in 1.28, has been removed. Instead, use the UploadVerifyFile hook. * UploadBase:: and UploadFromChunks::stashFileGetKey() and stashSession(), deprecated in 1.28, have been removed. Instead, please use the getFileKey() method on the response from doStashFile(). +* LBFactory::setDomainPrefix() and LoadBalancer::setDomainPrefix(), deprecated + in 1.33, have been removed. Use setLocalDomainPrefix() instead. +* IDatabase::implicitGroupby(), deprecated in 1.30, has been removed. +* IDatabase::doneWrites(), deprecated in 1.31, has been removed. + Use IDatabase::lastDoneWrites() instead. +* Database::reportConnectionError(), deprecated in 1.32, has been removed. +* LoadBalancer::laggedSlaveUsed(), deprecated in 1.28, has been removed. + Use LoadBalancer::laggedReplicaUsed() instead. +* Database::getProperty(), deprecated in 1.28, has been removed. +* IDatabase::getWikiId(), deprecated in 1.30, has been removed. + Use IDatabase::getDomainID() instead. * … === Deprecations in 1.34 === @@ -396,6 +411,11 @@ because of Phabricator reports. PermissionManager::getUserPermissions() instead. * The LocalisationCacheRecache hook no longer allows purging of message blobs to be prevented. Modifying the $purgeBlobs parameter now has no effect. +* The use of $wgProxyList with IP addresses in the array keys, deprecated in + 1.30, was removed. Instead, $wgProxyList should be an array with IP addresses + as the values, or a string path to a file containing one IP address per line. +* SVGMetadataExtractor::getMetadata has been deprecated. Instead, you should + use SVGReader->getMetadata() directly. === Other changes in 1.34 === * … diff --git a/includes/Autopromote.php b/includes/Autopromote.php index a4130371b8..b17f1ab1c6 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -198,8 +198,7 @@ class Autopromote { case APCOND_IPINRANGE: return IP::isInRange( $user->getRequest()->getIP(), $cond[1] ); case APCOND_BLOCKED: - // @TODO Should partial blocks prevent auto promote? - return (bool)$user->getBlock(); + return $user->getBlock() && $user->getBlock()->isSitewide(); case APCOND_ISBOT: return in_array( 'bot', User::getGroupPermissions( $user->getGroups() ) ); default: diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 6a1f7b53c4..608ef6ae55 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -2052,23 +2052,21 @@ $wgSharedSchema = false; * sent to it. It will be excluded from lag checks in maintenance scripts. * The only way it can receive traffic is if groupLoads is used. * - * - groupLoads: array of load ratios, the key is the query group name. A query may belong - * to several groups, the most specific group defined here is used. - * - * - flags: bit field - * - DBO_DEFAULT -- turns on DBO_TRX only if "cliMode" is off (recommended) - * - DBO_DEBUG -- equivalent of $wgDebugDumpSql - * - DBO_TRX -- wrap entire request in a transaction - * - DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php) - * - DBO_PERSISTENT -- enables persistent database connections - * - DBO_SSL -- uses SSL/TLS encryption in database connections, if available - * - DBO_COMPRESS -- uses internal compression in database connections, - * if available + * - groupLoads: (optional) Array of load ratios, the key is the query group name. A query + * may belong to several groups, the most specific group defined here is used. + * + * - flags: (optional) Bit field of properties: + * - DBO_DEFAULT: Transactionalize web requests and use autocommit otherwise + * - DBO_DEBUG: Equivalent of $wgDebugDumpSql + * - DBO_SSL: Use TLS connection encryption if available + * - DBO_COMPRESS: Use protocol compression with database connections + * - DBO_PERSISTENT: Enables persistent database connections * * - max lag: (optional) Maximum replication lag before a replica DB goes out of rotation * - is static: (optional) Set to true if the dataset is static and no replication is used. * - cliMode: (optional) Connection handles will not assume that requests are short-lived * nor that INSERT..SELECT can be rewritten into a buffered SELECT and INSERT. + * This is what DBO_DEFAULT uses to determine when a web request is present. * [Default: uses value of $wgCommandLineMode] * * These and any other user-defined properties will be assigned to the mLBInfo member @@ -2138,34 +2136,6 @@ $wgDBerrorLog = false; */ $wgDBerrorLogTZ = false; -/** - * Set true to enable Oracle DCRP (supported from 11gR1 onward) - * - * To use this feature set to true and use a datasource defined as - * POOLED (i.e. in tnsnames definition set server=pooled in connect_data - * block). - * - * Starting from 11gR1 you can use DCRP (Database Resident Connection - * Pool) that maintains established sessions and reuses them on new - * connections. - * - * Not completely tested, but it should fall back on normal connection - * in case the pool is full or the datasource is not configured as - * pooled. - * And the other way around; using oci_pconnect on a non pooled - * datasource should produce a normal connection. - * - * When it comes to frequent shortlived DB connections like with MW - * Oracle tends to s***. The problem is the driver connects to the - * database reasonably fast, but establishing a session takes time and - * resources. MW does not rely on session state (as it does not use - * features such as package variables) so establishing a valid session - * is in this case an unwanted overhead that just slows things down. - * - * @warning EXPERIMENTAL! - */ -$wgDBOracleDRCP = false; - /** * Other wikis on this site, can be administered from a single developer account. * @@ -5462,7 +5432,7 @@ $wgAutoConfirmCount = 0; * - [ APCOND_IPINRANGE, range ]: * true if the user has an IP address in the range of the passed parameter * - [ APCOND_BLOCKED ]: - * true if the user is blocked + * true if the user is sitewide blocked * - [ APCOND_ISBOT ]: * true if the user is a bot * - similar constructs can be defined by extensions @@ -6002,9 +5972,8 @@ $wgSecretKey = false; * Big list of banned IP addresses. * * This can have the following formats: - * - An array of addresses, either in the values - * or the keys (for backward compatibility, deprecated since 1.30) - * - A string, in that case this is the path to a file + * - An array of addresses + * - A string, in which case this is the path to a file * containing the list of IP addresses, one per line */ $wgProxyList = []; diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 7b4b502905..1741958681 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -2563,10 +2563,10 @@ function wfWikiID() { * @todo Replace calls to wfGetDB with calls to LoadBalancer::getConnection() * on an injected instance of LoadBalancer. * - * @return \Wikimedia\Rdbms\Database + * @return \Wikimedia\Rdbms\DBConnRef */ function wfGetDB( $db, $groups = [], $wiki = false ) { - return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki ); + return wfGetLB( $wiki )->getMaintenanceConnectionRef( $db, $groups, $wiki ); } /** diff --git a/includes/Revision/RenderedRevision.php b/includes/Revision/RenderedRevision.php index cf1cc947a7..a9132445a5 100644 --- a/includes/Revision/RenderedRevision.php +++ b/includes/Revision/RenderedRevision.php @@ -292,6 +292,7 @@ class RenderedRevision implements SlotRenderingProvider { $this->setRevisionInternal( $rev ); $this->pruneRevisionSensitiveOutput( + $this->revision->getPageId(), $this->revision->getId(), $this->revision->getTimestamp() ); @@ -300,28 +301,38 @@ class RenderedRevision implements SlotRenderingProvider { /** * Prune any output that depends on the revision ID. * + * @param int|bool $actualPageId The actual page id, to check the used speculative page ID + * against; false, to not purge on vary-page-id; true, to purge on vary-page-id + * unconditionally. * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID - * against, or false to not purge on vary-revision-id, or true to purge on + * against,; false, to not purge on vary-revision-id; true, to purge on * vary-revision-id unconditionally. * @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the - * parser output revision timestamp, or false to not purge on vary-revision-timestamp + * parser output revision timestamp; false, to not purge on vary-revision-timestamp; + * true, to purge on vary-revision-timestamp unconditionally. */ - private function pruneRevisionSensitiveOutput( $actualRevId, $actualRevTimestamp ) { + private function pruneRevisionSensitiveOutput( + $actualPageId, + $actualRevId, + $actualRevTimestamp + ) { if ( $this->revisionOutput ) { if ( $this->outputVariesOnRevisionMetaData( $this->revisionOutput, + $actualPageId, $actualRevId, $actualRevTimestamp ) ) { $this->revisionOutput = null; } } else { - $this->saveParseLogger->debug( __METHOD__ . ": no prepared revision output...\n" ); + $this->saveParseLogger->debug( __METHOD__ . ": no prepared revision output" ); } foreach ( $this->slotsOutput as $role => $output ) { if ( $this->outputVariesOnRevisionMetaData( $output, + $actualPageId, $actualRevId, $actualRevTimestamp ) ) { @@ -384,51 +395,58 @@ class RenderedRevision implements SlotRenderingProvider { /** * @param ParserOutput $out - * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID - * against, false to not purge on vary-revision-id, or true to purge on + * @param int|bool $actualPageId The actual page id, to check the used speculative page ID + * against; false, to not purge on vary-page-id; true, to purge on vary-page-id + * unconditionally. + * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID + * against,; false, to not purge on vary-revision-id; true, to purge on * vary-revision-id unconditionally. * @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the - * parser output revision timestamp, false to not purge on vary-revision-timestamp, - * or true to purge on vary-revision-timestamp unconditionally. + * parser output revision timestamp; false, to not purge on vary-revision-timestamp; + * true, to purge on vary-revision-timestamp unconditionally. * @return bool */ private function outputVariesOnRevisionMetaData( ParserOutput $out, + $actualPageId, $actualRevId, $actualRevTimestamp ) { - $method = __METHOD__; + $logger = $this->saveParseLogger; + $varyMsg = __METHOD__ . ": cannot use prepared output for '{title}'"; + $context = [ 'title' => $this->title->getPrefixedText() ]; if ( $out->getFlag( 'vary-revision' ) ) { // If {{PAGEID}} resolved to 0, then that word need to resolve to the actual page ID - $this->saveParseLogger->info( - "$method: Prepared output has vary-revision..." - ); + $logger->info( "$varyMsg (vary-revision)", $context ); return true; - } elseif ( $out->getFlag( 'vary-revision-id' ) + } elseif ( + $out->getFlag( 'vary-revision-id' ) && $actualRevId !== false && ( $actualRevId === true || $out->getSpeculativeRevIdUsed() !== $actualRevId ) ) { - $this->saveParseLogger->info( - "$method: Prepared output has vary-revision-id with wrong ID..." - ); + $logger->info( "$varyMsg (vary-revision-id and wrong ID)", $context ); return true; - } elseif ( $out->getFlag( 'vary-revision-timestamp' ) + } elseif ( + $out->getFlag( 'vary-revision-timestamp' ) && $actualRevTimestamp !== false && ( $actualRevTimestamp === true || $out->getRevisionTimestampUsed() !== $actualRevTimestamp ) ) { - $this->saveParseLogger->info( - "$method: Prepared output has vary-revision-timestamp with wrong timestamp..." - ); + $logger->info( "$varyMsg (vary-revision-timestamp and wrong timestamp)", $context ); + return true; + } elseif ( + $out->getFlag( 'vary-page-id' ) + && $actualPageId !== false + && ( $actualPageId === true || $out->getSpeculativePageIdUsed() !== $actualPageId ) + ) { + $logger->info( "$varyMsg (vary-page-id and wrong ID)", $context ); return true; } elseif ( $out->getFlag( 'vary-revision-exists' ) ) { // If {{REVISIONID}} resolved to '', it now needs to resolve to '-'. // Note that edit stashing always uses '-', which can be used for both // edit filter checks and canonical parser cache. - $this->saveParseLogger->info( - "$method: Prepared output has vary-revision-exists..." - ); + $logger->info( "$varyMsg (vary-revision-exists)", $context ); return true; } elseif ( $out->getFlag( 'vary-revision-sha1' ) && @@ -436,22 +454,18 @@ class RenderedRevision implements SlotRenderingProvider { ) { // If a self-transclusion used the proposed page text, it must match the final // page content after PST transformations and automatically merged edit conflicts - $this->saveParseLogger->info( - "$method: Prepared output has vary-revision-sha1 with wrong SHA-1..." - ); + $logger->info( "$varyMsg (vary-revision-sha1 with wrong SHA-1)" ); return true; - } else { - // NOTE: In the original fix for T135261, the output was discarded if 'vary-user' was - // set for a null-edit. The reason was that the original rendering in that case was - // targeting the user making the null-edit, not the user who made the original edit, - // causing {{REVISIONUSER}} to return the wrong name. - // This case is now expected to be handled by the code in RevisionRenderer that - // constructs the ParserOptions: For a null-edit, setCurrentRevisionCallback is called - // with the old, existing revision. - - $this->saveParseLogger->debug( "$method: Keeping prepared output..." ); - return false; } - } + // NOTE: In the original fix for T135261, the output was discarded if 'vary-user' was + // set for a null-edit. The reason was that the original rendering in that case was + // targeting the user making the null-edit, not the user who made the original edit, + // causing {{REVISIONUSER}} to return the wrong name. + // This case is now expected to be handled by the code in RevisionRenderer that + // constructs the ParserOptions: For a null-edit, setCurrentRevisionCallback is called + // with the old, existing revision. + $logger->debug( __METHOD__ . ": reusing prepared output for '{title}'", $context ); + return false; + } } diff --git a/includes/Revision/RevisionRenderer.php b/includes/Revision/RevisionRenderer.php index 99150c1368..bbda0e5f43 100644 --- a/includes/Revision/RevisionRenderer.php +++ b/includes/Revision/RevisionRenderer.php @@ -130,6 +130,9 @@ class RevisionRenderer { $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) { return $this->getSpeculativeRevId( $dbIndex ); } ); + $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) { + return $this->getSpeculativePageId( $dbIndex ); + } ); if ( !$rev->getId() && $rev->getTimestamp() ) { // This is an unsaved revision with an already determined timestamp. @@ -166,7 +169,8 @@ class RevisionRenderer { // HACK: But don't use a fresh connection in unit tests, since it would not have // the fake tables. This should be handled by the LoadBalancer! $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA - ? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT; + ? 0 + : ILoadBalancer::CONN_TRX_AUTOCOMMIT; $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags ); @@ -178,6 +182,25 @@ class RevisionRenderer { ); } + private function getSpeculativePageId( $dbIndex ) { + // Use a fresh master connection in order to see the latest data, by avoiding + // stale data from REPEATABLE-READ snapshots. + // HACK: But don't use a fresh connection in unit tests, since it would not have + // the fake tables. This should be handled by the LoadBalancer! + $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA + ? 0 + : ILoadBalancer::CONN_TRX_AUTOCOMMIT; + + $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->wikiId, $flags ); + + return 1 + (int)$db->selectField( + 'page', + 'MAX(page_id)', + [], + __METHOD__ + ); + } + /** * This implements the layout for combining the output of multiple slots. * diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index 8a4b6dcfaf..fe5b5b94e9 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -284,7 +284,7 @@ class RevisionStore */ private function getDBConnection( $mode, $groups = [] ) { $lb = $this->getDBLoadBalancer(); - return $lb->getConnection( $mode, $groups, $this->dbDomain ); + return $lb->getConnectionRef( $mode, $groups, $this->dbDomain ); } /** diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 1bb848fb04..9073de1c0e 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -263,6 +263,7 @@ return [ 'LocalServerObjectCache' => function ( MediaWikiServices $services ) : BagOStuff { $cacheId = \ObjectCache::detectLocalServerCache(); + return \ObjectCache::newFromId( $cacheId ); }, @@ -439,7 +440,8 @@ return [ wfUrlProtocols(), $services->getSpecialPageFactory(), $services->getLinkRendererFactory(), - $services->getNamespaceInfo() + $services->getNamespaceInfo(), + LoggerFactory::getInstance( 'Parser' ) ); }, @@ -620,9 +622,10 @@ return [ 'SiteStore' => function ( MediaWikiServices $services ) : SiteStore { $rawSiteStore = new DBSiteStore( $services->getDBLoadBalancer() ); - // TODO: replace wfGetCache with a CacheFactory service. - // TODO: replace wfIsHHVM with a capabilities service. - $cache = wfGetCache( wfIsHHVM() ? CACHE_ACCEL : CACHE_ANYTHING ); + $cache = $services->getLocalServerObjectCache(); + if ( $cache instanceof EmptyBagOStuff ) { + $cache = ObjectCache::getLocalClusterInstance(); + } return new CachingSiteStore( $rawSiteStore, $cache ); }, diff --git a/includes/SiteStatsInit.php b/includes/SiteStatsInit.php index 932e1c3d55..2252f8f4ff 100644 --- a/includes/SiteStatsInit.php +++ b/includes/SiteStatsInit.php @@ -195,8 +195,8 @@ class SiteStatsInit { * @return IDatabase */ private static function getDB( $index, $groups = [] ) { - $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); - - return $lb->getConnection( $index, $groups ); + return MediaWikiServices::getInstance() + ->getDBLoadBalancer() + ->getConnectionRef( $index, $groups ); } } diff --git a/includes/Storage/PageEditStash.php b/includes/Storage/PageEditStash.php index 4671d99f15..a0ef07d651 100644 --- a/includes/Storage/PageEditStash.php +++ b/includes/Storage/PageEditStash.php @@ -271,7 +271,7 @@ class PageEditStash { // This can be used for the initial parse, e.g. for filters or doEditContent(), // but a second parse will be triggered in doEditUpdates() no matter what $logger->info( - "Cache for key '{key}' has 'vary-revision'; post-insertion parse inevitable.", + "Cache for key '{key}' has vary-revision; post-insertion parse inevitable.", $context ); } else { @@ -281,7 +281,9 @@ class PageEditStash { // Similar to the above if we didn't guess the timestamp correctly 'vary-revision-timestamp', // Similar to the above if we didn't guess the content correctly - 'vary-revision-sha1' + 'vary-revision-sha1', + // Similar to the above if we didn't guess page ID correctly + 'vary-page-id' ]; foreach ( $flagsMaybeReparse as $flag ) { if ( $editInfo->output->getFlag( $flag ) ) { diff --git a/includes/Storage/SqlBlobStore.php b/includes/Storage/SqlBlobStore.php index 5260754f5f..d1b688b201 100644 --- a/includes/Storage/SqlBlobStore.php +++ b/includes/Storage/SqlBlobStore.php @@ -207,7 +207,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { */ private function getDBConnection( $index ) { $lb = $this->getDBLoadBalancer(); - return $lb->getConnection( $index, [], $this->dbDomain ); + return $lb->getConnectionRef( $index, [], $this->dbDomain ); } /** diff --git a/includes/api/ApiImportReporter.php b/includes/api/ApiImportReporter.php index 21d9d235dc..be53c67c33 100644 --- a/includes/api/ApiImportReporter.php +++ b/includes/api/ApiImportReporter.php @@ -29,13 +29,13 @@ class ApiImportReporter extends ImportReporter { /** * @param Title $title - * @param Title $origTitle + * @param ForeignTitle $foreignTitle * @param int $revisionCount * @param int $successCount * @param array $pageInfo * @return void */ - public function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) { + public function reportPage( $title, $foreignTitle, $revisionCount, $successCount, $pageInfo ) { // Add a result entry $r = []; @@ -51,7 +51,7 @@ class ApiImportReporter extends ImportReporter { $this->mResultArr[] = $r; // Piggyback on the parent to do the logging - parent::reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ); + parent::reportPage( $title, $foreignTitle, $revisionCount, $successCount, $pageInfo ); } public function getData() { diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index a77136de1b..8389b24c9c 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -593,9 +593,13 @@ class ApiMain extends ApiBase { // Printer may not be initialized if the extractRequestParams() fails for the main module $this->createErrorPrinter(); + // Get desired HTTP code from an ApiUsageException. Don't use codes from other + // exception types, as they are unlikely to be intended as an HTTP code. + $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0; + $failed = false; try { - $this->printResult( $e->getCode() ); + $this->printResult( $httpCode ); } catch ( ApiUsageException $ex ) { // The error printer itself is failing. Try suppressing its request // parameters and redo. @@ -617,10 +621,10 @@ class ApiMain extends ApiBase { $this->mPrinter = null; $this->createErrorPrinter(); $this->mPrinter->forceDefaultParams(); - if ( $e->getCode() ) { + if ( $httpCode ) { $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200 } - $this->printResult( $e->getCode() ); + $this->printResult( $httpCode ); } } diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index fc41e4ea6a..b15b9989a0 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -658,7 +658,7 @@ class ApiUpload extends ApiBase { * @return array */ protected function getApiWarnings() { - $warnings = $this->mUpload->checkWarnings(); + $warnings = UploadBase::makeWarningsSerializable( $this->mUpload->checkWarnings() ); return $this->transformWarnings( $warnings ); } @@ -670,9 +670,8 @@ class ApiUpload extends ApiBase { if ( isset( $warnings['duplicate'] ) ) { $dupes = []; - /** @var File $dupe */ foreach ( $warnings['duplicate'] as $dupe ) { - $dupes[] = $dupe->getName(); + $dupes[] = $dupe['fileName']; } ApiResult::setIndexedTagName( $dupes, 'duplicate' ); $warnings['duplicate'] = $dupes; @@ -681,27 +680,24 @@ class ApiUpload extends ApiBase { if ( isset( $warnings['exists'] ) ) { $warning = $warnings['exists']; unset( $warnings['exists'] ); - /** @var LocalFile $localFile */ $localFile = $warning['normalizedFile'] ?? $warning['file']; - $warnings[$warning['warning']] = $localFile->getName(); + $warnings[$warning['warning']] = $localFile['fileName']; } if ( isset( $warnings['no-change'] ) ) { - /** @var File $file */ $file = $warnings['no-change']; unset( $warnings['no-change'] ); $warnings['nochange'] = [ - 'timestamp' => wfTimestamp( TS_ISO_8601, $file->getTimestamp() ) + 'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] ) ]; } if ( isset( $warnings['duplicate-version'] ) ) { $dupes = []; - /** @var File $dupe */ foreach ( $warnings['duplicate-version'] as $dupe ) { $dupes[] = [ - 'timestamp' => wfTimestamp( TS_ISO_8601, $dupe->getTimestamp() ) + 'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] ) ]; } unset( $warnings['duplicate-version'] ); diff --git a/includes/block/BlockManager.php b/includes/block/BlockManager.php index 68141a178b..c82ed1c258 100644 --- a/includes/block/BlockManager.php +++ b/includes/block/BlockManager.php @@ -293,32 +293,7 @@ class BlockManager { $proxyList = array_map( 'trim', file( $proxyList ) ); } - $resultProxyList = []; - $deprecatedIPEntries = []; - - // backward compatibility: move all ip addresses in keys to values - foreach ( $proxyList as $key => $value ) { - $keyIsIP = IP::isIPAddress( $key ); - $valueIsIP = IP::isIPAddress( $value ); - if ( $keyIsIP && !$valueIsIP ) { - $deprecatedIPEntries[] = $key; - $resultProxyList[] = $key; - } elseif ( $keyIsIP && $valueIsIP ) { - $deprecatedIPEntries[] = $key; - $resultProxyList[] = $key; - $resultProxyList[] = $value; - } else { - $resultProxyList[] = $value; - } - } - - if ( $deprecatedIPEntries ) { - wfDeprecated( - 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' . - implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' ); - } - - $proxyListIPSet = new IPSet( $resultProxyList ); + $proxyListIPSet = new IPSet( $proxyList ); return $proxyListIPSet->match( $ip ); } diff --git a/includes/content/WikitextContent.php b/includes/content/WikitextContent.php index 455eb0de4f..8e5e0a8305 100644 --- a/includes/content/WikitextContent.php +++ b/includes/content/WikitextContent.php @@ -329,7 +329,7 @@ class WikitextContent extends TextContent { * using the global Parser service. * * @param Title $title - * @param int $revId Revision to pass to the parser (default: null) + * @param int|null $revId Revision to pass to the parser (default: null) * @param ParserOptions $options (default: null) * @param bool $generateHtml (default: true) * @param ParserOutput &$output ParserOutput representing the HTML form of the text, diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 501f01a3e9..82fff6b196 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -28,7 +28,6 @@ use Wikimedia\Rdbms\DatabaseDomain; use Wikimedia\Rdbms\Blob; use Wikimedia\Rdbms\ResultWrapper; use Wikimedia\Rdbms\IResultWrapper; -use Wikimedia\Rdbms\DBConnectionError; use Wikimedia\Rdbms\DBUnexpectedError; use Wikimedia\Rdbms\DBExpectedError; @@ -80,101 +79,96 @@ class DatabaseOracle extends Database { return 'oracle'; } - function implicitGroupby() { - return false; - } - function implicitOrderby() { return false; } protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) { if ( !function_exists( 'oci_connect' ) ) { - throw new DBConnectionError( - $this, + throw $this->newExceptionAfterConnectError( "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n " . - "(Note: if you recently installed PHP, you may need to restart your webserver\n " . - "and database)\n" ); + "(Note: if you recently installed PHP, you may need to restart your webserver\n " . + "and database)" + ); } + $this->close(); + if ( $schema !== null ) { - // We use the *database* aspect of $domain for schema, not the domain schema - throw new DBExpectedError( - $this, - __CLASS__ . ": cannot use schema '$schema'; " . - "the database component '$dbName' is actually interpreted as the Oracle schema." + // This uses the *database* aspect of $domain for schema, not the domain schema + throw $this->newExceptionAfterConnectError( + "Got schema '$schema'; not supported. " . + "The database component '$dbName' is actually interpreted as the Oracle schema." ); } - $this->close(); $this->user = $user; $this->password = $password; - if ( !$server ) { - // Backward compatibility (server used to be null and TNS was supplied in dbname) + if ( strlen( $server ) ) { + // Transparent Network Substrate (TNS) endpoint + $this->server = $server; + // Database name, defaulting to the user name + $realDatabase = strlen( $dbName ) ? $dbName : $user; + } else { + // Backward compatibility; $server used to be null and $dbName was the TNS $this->server = $dbName; $realDatabase = $user; - } else { - // $server now holds the TNS endpoint - $this->server = $server; - // $dbName is schema name if different from username - $realDatabase = $dbName ?: $user; - } - - if ( !strlen( $user ) ) { # e.g. the class is being loaded - return null; } - $session_mode = ( $this->flags & DBO_SYSDBA ) ? OCI_SYSDBA : OCI_DEFAULT; - Wikimedia\suppressWarnings(); - if ( $this->flags & DBO_PERSISTENT ) { - $this->conn = oci_pconnect( - $this->user, - $this->password, - $this->server, - $this->defaultCharset, - $session_mode - ); - } elseif ( $this->flags & DBO_DEFAULT ) { - $this->conn = oci_new_connect( - $this->user, - $this->password, - $this->server, - $this->defaultCharset, - $session_mode - ); - } else { - $this->conn = oci_connect( - $this->user, - $this->password, - $this->server, - $this->defaultCharset, - $session_mode - ); - } - Wikimedia\restoreWarnings(); - - if ( $this->user != $realDatabase ) { - // change current schema in session - $this->selectDB( $realDatabase ); - } else { - $this->currentDomain = new DatabaseDomain( - $realDatabase, - null, - $tablePrefix - ); - } + $this->installErrorHandler(); + try { + $this->conn = $this->getFlag( DBO_PERSISTENT ) + ? oci_pconnect( + $this->user, + $this->password, + $this->server, + $this->defaultCharset, + $session_mode + ) + : oci_new_connect( + $this->user, + $this->password, + $this->server, + $this->defaultCharset, + $session_mode + ); + } catch ( Exception $e ) { + $this->restoreErrorHandler(); + throw $this->newExceptionAfterConnectError( $e->getMessage() ); + } + $error = $this->restoreErrorHandler(); if ( !$this->conn ) { - throw new DBConnectionError( $this, $this->lastError() ); + throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() ); } - # removed putenv calls because they interfere with the system globaly - $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); - $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); - $this->doQuery( 'ALTER SESSION SET NLS_NUMERIC_CHARACTERS=\'.,\'' ); - - return (bool)$this->conn; + try { + if ( $this->user != $realDatabase ) { + // Change current schema for the entire session + $this->selectDomain( new DatabaseDomain( + $realDatabase, + $this->currentDomain->getSchema(), + $this->currentDomain->getTablePrefix() + ) ); + } else { + $this->currentDomain = new DatabaseDomain( $realDatabase, null, $tablePrefix ); + } + $set = [ + 'NLS_TIMESTAMP_FORMAT' => 'DD-MM-YYYY HH24:MI:SS.FF6', + 'NLS_TIMESTAMP_TZ_FORMAT' => 'DD-MM-YYYY HH24:MI:SS.FF6', + 'NLS_NUMERIC_CHARACTERS' => '.,' + ]; + foreach ( $set as $var => $val ) { + $this->query( + "ALTER SESSION SET {$var}=" . $this->addQuotes( $val ), + __METHOD__, + self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY + ); + } + } catch ( Exception $e ) { + throw $this->newExceptionAfterConnectError( $e->getMessage() ); + } } /** diff --git a/includes/db/MWLBFactory.php b/includes/db/MWLBFactory.php index 3d404d3c8e..0c17840e4a 100644 --- a/includes/db/MWLBFactory.php +++ b/includes/db/MWLBFactory.php @@ -212,9 +212,6 @@ abstract class MWLBFactory { $flags = DBO_DEFAULT; $flags |= $options->get( 'DebugDumpSql' ) ? DBO_DEBUG : 0; $flags |= $options->get( 'DebugLogFile' ) ? DBO_DEBUG : 0; - if ( $server['type'] === 'oracle' ) { - $flags |= $options->get( 'DBOracleDRCP' ) ? DBO_PERSISTENT : 0; - } $server += [ 'tablePrefix' => $options->get( 'DBprefix' ), diff --git a/includes/export/DumpFileOutput.php b/includes/export/DumpFileOutput.php index 4bec7d4532..d0256fd877 100644 --- a/includes/export/DumpFileOutput.php +++ b/includes/export/DumpFileOutput.php @@ -27,7 +27,10 @@ * @ingroup Dump */ class DumpFileOutput extends DumpOutput { - protected $handle = false, $filename; + /** @var resource|false */ + protected $handle = false; + /** @var string */ + protected $filename; /** * @param string $file @@ -73,7 +76,7 @@ class DumpFileOutput extends DumpOutput { } /** - * @param array $newname + * @param string|string[] $newname * @return string * @throws MWException */ diff --git a/includes/export/WikiExporter.php b/includes/export/WikiExporter.php index fe6dadfe22..3ab88e2927 100644 --- a/includes/export/WikiExporter.php +++ b/includes/export/WikiExporter.php @@ -125,7 +125,7 @@ class WikiExporter { * various row objects and XML output for filtering. Filters * can be chained or used as callbacks. * - * @param DumpOutput &$sink + * @param DumpOutput|DumpFilter &$sink */ public function setOutputSink( &$sink ) { $this->sink =& $sink; @@ -240,7 +240,7 @@ class WikiExporter { * Not called by default (depends on $this->list_authors) * Can be set by Special:Export when not exporting whole history * - * @param array $cond + * @param string $cond */ protected function do_list_authors( $cond ) { $this->author_list = ""; diff --git a/includes/import/WikiImporter.php b/includes/import/WikiImporter.php index 00bb61f7b2..68f5b9b8bf 100644 --- a/includes/import/WikiImporter.php +++ b/includes/import/WikiImporter.php @@ -466,7 +466,7 @@ class WikiImporter { /** * Notify the callback function when a new "" is reached. - * @param Title $title + * @param array $title */ function pageCallback( $title ) { if ( isset( $this->mPageCallback ) ) { diff --git a/includes/import/WikiRevision.php b/includes/import/WikiRevision.php index cae954215a..e36d673499 100644 --- a/includes/import/WikiRevision.php +++ b/includes/import/WikiRevision.php @@ -352,7 +352,7 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision { /** * @since 1.12.2 - * @param array $params + * @param string $params */ public function setParams( $params ) { $this->params = $params; diff --git a/includes/jobqueue/jobs/AssembleUploadChunksJob.php b/includes/jobqueue/jobs/AssembleUploadChunksJob.php index e2914be5e4..9519b7ffd3 100644 --- a/includes/jobqueue/jobs/AssembleUploadChunksJob.php +++ b/includes/jobqueue/jobs/AssembleUploadChunksJob.php @@ -76,7 +76,9 @@ class AssembleUploadChunksJob extends Job { // We can only get warnings like 'duplicate' after concatenating the chunks $status = Status::newGood(); - $status->value = [ 'warnings' => $upload->checkWarnings() ]; + $status->value = [ + 'warnings' => UploadBase::makeWarningsSerializable( $upload->checkWarnings() ) + ]; // We have a new filekey for the fully concatenated file $newFileKey = $upload->getStashFile()->getFileKey(); diff --git a/includes/libs/mime/MimeAnalyzer.php b/includes/libs/mime/MimeAnalyzer.php index 24621748ed..bafe5e3098 100644 --- a/includes/libs/mime/MimeAnalyzer.php +++ b/includes/libs/mime/MimeAnalyzer.php @@ -806,10 +806,10 @@ EOT; // Check for ZIP variants (before getimagesize) $eocdrPos = strpos( $tail, "PK\x05\x06" ); - if ( $eocdrPos !== false ) { + if ( $eocdrPos !== false && $eocdrPos <= strlen( $tail ) - 22 ) { $this->logger->info( __METHOD__ . ": ZIP signature present in $file\n" ); // Check if it really is a ZIP file, make sure the EOCDR is at the end (T40432) - $commentLength = unpack( "n", substr( $tail, $eocdrPos + 20 ) )[0]; + $commentLength = unpack( "n", substr( $tail, $eocdrPos + 20 ) )[1]; if ( $eocdrPos + 22 + $commentLength !== strlen( $tail ) ) { $this->logger->info( __METHOD__ . ": ZIP EOCDR not at end. Not a ZIP file." ); } else { diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php index 8c99532927..e9fd7d90be 100644 --- a/includes/libs/objectcache/BagOStuff.php +++ b/includes/libs/objectcache/BagOStuff.php @@ -115,7 +115,8 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar /** * Get an item with the given key, regenerating and setting it if not found * - * Nothing is stored nor deleted if the callback returns false + * The callback can take $ttl as argument by reference and modify it. + * Nothing is stored nor deleted if the callback returns false. * * @param string $key * @param int $ttl Time-to-live (seconds) @@ -128,10 +129,7 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar $value = $this->get( $key, $flags ); if ( $value === false ) { - if ( !is_callable( $callback ) ) { - throw new InvalidArgumentException( "Invalid cache miss callback provided." ); - } - $value = call_user_func( $callback ); + $value = $callback( $ttl ); if ( $value !== false ) { $this->set( $key, $value, $ttl, $flags ); } diff --git a/includes/libs/objectcache/MediumSpecificBagOStuff.php b/includes/libs/objectcache/MediumSpecificBagOStuff.php index e742432d92..23cf607c7c 100644 --- a/includes/libs/objectcache/MediumSpecificBagOStuff.php +++ b/includes/libs/objectcache/MediumSpecificBagOStuff.php @@ -798,8 +798,8 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * - positive (< 10 years): relative TTL; return UNIX timestamp offset by this value * - positive (>= 10 years): absolute UNIX timestamp; return this value * - * @param int $exptime Absolute TTL or 0 for indefinite - * @return int + * @param int $exptime + * @return int Absolute TTL or 0 for indefinite */ final protected function convertToExpiry( $exptime ) { return $this->expiryIsRelative( $exptime ) @@ -811,11 +811,17 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * Convert an optionally absolute expiry time to a relative time. If an * absolute time is specified which is in the past, use a short expiry time. * + * The input value will be cast to an integer and interpreted as follows: + * - zero: no expiry; return zero (e.g. TTL_INDEFINITE) + * - negative: relative TTL; return a short expiry time (1 second) + * - positive (< 10 years): relative TTL; return this value + * - positive (>= 10 years): absolute UNIX timestamp; return offset to current time + * * @param int $exptime - * @return int + * @return int Relative TTL or 0 for indefinite */ final protected function convertToRelative( $exptime ) { - return $this->expiryIsRelative( $exptime ) + return $this->expiryIsRelative( $exptime ) || !$exptime ? (int)$exptime : max( $exptime - (int)$this->getCurrentTime(), 1 ); } diff --git a/includes/libs/rdbms/database/DBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php index 2c9858add5..f27d042ca9 100644 --- a/includes/libs/rdbms/database/DBConnRef.php +++ b/includes/libs/rdbms/database/DBConnRef.php @@ -140,10 +140,6 @@ class DBConnRef implements IDatabase { throw new DBUnexpectedError( $this, "Database injection is disallowed to enable reuse." ); } - public function implicitGroupby() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - public function implicitOrderby() { return $this->__call( __FUNCTION__, func_get_args() ); } @@ -152,10 +148,6 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } - public function doneWrites() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - public function lastDoneWrites() { return $this->__call( __FUNCTION__, func_get_args() ); } @@ -218,13 +210,6 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } - /** - * @codeCoverageIgnore - */ - public function getWikiID() { - return $this->getDomainID(); - } - public function getType() { if ( $this->conn === null ) { // Avoid triggering a database connection diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index 60062fbc13..8b65397442 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -61,7 +61,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware protected $cliMode; /** @var string Agent name for query profiling */ protected $agent; - /** @var int Bitfield of class DBO_* constants */ + /** @var int Bit field of class DBO_* constants */ protected $flags; /** @var array LoadBalancer tracking information */ protected $lbInfo = []; @@ -217,6 +217,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var float Assume an insert of this many rows or less should be fast to replicate */ private static $SMALL_WRITE_ROWS = 100; + /** @var string[] List of DBO_* flags that can be changed after connection */ + protected static $MUTABLE_FLAGS = [ + 'DBO_DEBUG', + 'DBO_NOBUFFER', + 'DBO_TRX', + 'DBO_DDLMODE', + ]; + /** @var int Bit field of all DBO_* flags that can be changed after connection */ + protected static $DBO_MUTABLE = ( + self::DBO_DEBUG | self::DBO_NOBUFFER | self::DBO_TRX | self::DBO_DDLMODE + ); + /** * @note exceptions for missing libraries/drivers should be thrown in initConnection() * @param array $params Parameters passed from Database::factory() @@ -283,23 +295,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** * Actually connect to the database over the wire (or to local files) * - * @throws InvalidArgumentException * @throws DBConnectionError * @since 1.31 */ protected function doInitConnection() { - if ( strlen( $this->connectionParams['user'] ) ) { - $this->open( - $this->connectionParams['host'], - $this->connectionParams['user'], - $this->connectionParams['password'], - $this->connectionParams['dbname'], - $this->connectionParams['schema'], - $this->connectionParams['tablePrefix'] - ); - } else { - throw new InvalidArgumentException( "No database user provided" ); - } + $this->open( + $this->connectionParams['host'], + $this->connectionParams['user'], + $this->connectionParams['password'], + $this->connectionParams['dbname'], + $this->connectionParams['schema'], + $this->connectionParams['tablePrefix'] + ); } /** @@ -335,7 +342,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas. * - tablePrefix : Optional table prefix that is implicitly added on to all table names * recognized in queries. This can be used in place of schemas for handle site farms. - * - flags : Optional bitfield of DBO_* constants that define connection, protocol, + * - flags : Optional bit field of DBO_* constants that define connection, protocol, * buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT * flag in place UNLESS this this database simply acts as a key/value store. * - driver: Optional name of a specific DB client driver. For MySQL, there is only the @@ -613,10 +620,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $this->lazyMasterHandle; } - public function implicitGroupby() { - return true; - } - public function implicitOrderby() { return true; } @@ -625,10 +628,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $this->lastQuery; } - public function doneWrites() { - return (bool)$this->lastWriteTime; - } - public function lastDoneWrites() { return $this->lastWriteTime ?: false; } @@ -741,24 +740,32 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) { - if ( ( $flag & self::DBO_IGNORE ) ) { - throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" ); + if ( $flag & ~static::$DBO_MUTABLE ) { + throw new DBUnexpectedError( + $this, + "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')' + ); } if ( $remember === self::REMEMBER_PRIOR ) { array_push( $this->priorFlags, $this->flags ); } + $this->flags |= $flag; } public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) { - if ( ( $flag & self::DBO_IGNORE ) ) { - throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" ); + if ( $flag & ~static::$DBO_MUTABLE ) { + throw new DBUnexpectedError( + $this, + "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')' + ); } if ( $remember === self::REMEMBER_PRIOR ) { array_push( $this->priorFlags, $this->flags ); } + $this->flags &= ~$flag; } @@ -776,26 +783,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function getFlag( $flag ) { - return (bool)( $this->flags & $flag ); - } - - /** - * @param string $name Class field name - * @return mixed - * @deprecated Since 1.28 - */ - public function getProperty( $name ) { - return $this->$name; + return ( ( $this->flags & $flag ) === $flag ); } public function getDomainID() { return $this->currentDomain->getId(); } - final public function getWikiID() { - return $this->getDomainID(); - } - /** * Get information about an index into an object * @param string $table Table name @@ -929,7 +923,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $closed = true; // already closed; nothing to do } - $this->conn = false; + $this->conn = null; // Throw any unexpected errors after having disconnected if ( $exception instanceof Exception ) { @@ -1177,7 +1171,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * * @param string $sql Original SQL query * @param string $fname Name of the calling function - * @param int $flags Bitfield of class QUERY_* constants + * @param int $flags Bit field of class QUERY_* constants * @return array An n-tuple of: * - mixed|bool: An object, resource, or true on success; false on failure * - string: The result of calling lastError() @@ -1265,7 +1259,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @param string $commentedSql SQL query with debugging/trace comment * @param bool $isPermWrite Whether the query is a (non-temporary table) write * @param string $fname Name of the calling function - * @param int $flags Bitfield of class QUERY_* constants + * @param int $flags Bit field of class QUERY_* constants * @return array An n-tuple of: * - mixed|bool: An object, resource, or true on success; false on failure * - string: The result of calling lastError() @@ -1570,9 +1564,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $ignore ) { $this->queryLogger->debug( "SQL ERROR (ignored): $error" ); } else { - $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname ); - - throw $exception; + throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname ); } } @@ -1584,19 +1576,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @return DBError */ private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) { - $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ); $this->queryLogger->error( "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}", $this->getLogContext( [ 'method' => __METHOD__, 'errno' => $errno, 'error' => $error, - 'sql1line' => $sql1line, + 'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ), 'fname' => $fname, 'trace' => ( new RuntimeException() )->getTraceAsString() ] ) ); - $this->queryLogger->debug( "SQL ERROR: " . $error . "" ); + if ( $this->wasQueryTimeout( $error, $errno ) ) { $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname ); } elseif ( $this->wasConnectionError( $errno ) ) { @@ -1608,6 +1599,25 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $e; } + /** + * @param string $error + * @return DBConnectionError + */ + final protected function newExceptionAfterConnectError( $error ) { + // Connection was not fully initialized and is not safe for use + $this->conn = null; + + $this->connLogger->error( + "Error connecting to {db_server} as user {db_user}: {error}", + $this->getLogContext( [ + 'error' => $error, + 'trace' => ( new RuntimeException() )->getTraceAsString() + ] ) + ); + + return new DBConnectionError( $this, $error ); + } + public function freeResult( $res ) { } @@ -4297,7 +4307,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware */ protected function replaceLostConnection( $fname ) { $this->closeConnection(); - $this->conn = false; + $this->conn = null; $this->handleSessionLossPreconnect(); @@ -4876,7 +4886,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $this->isOpen() ) { // Open a new connection resource without messing with the old one - $this->conn = false; + $this->conn = null; $this->trxEndCallbacks = []; // don't copy $this->trxSectionCancelCallbacks = []; // don't copy $this->handleSessionLossPreconnect(); // no trx or locks anymore diff --git a/includes/libs/rdbms/database/DatabaseMssql.php b/includes/libs/rdbms/database/DatabaseMssql.php index d06bcb9274..db029a389e 100644 --- a/includes/libs/rdbms/database/DatabaseMssql.php +++ b/includes/libs/rdbms/database/DatabaseMssql.php @@ -59,10 +59,6 @@ class DatabaseMssql extends Database { /** @var string[] */ protected $ignoreErrors = []; - public function implicitGroupby() { - return false; - } - public function implicitOrderby() { return false; } @@ -79,53 +75,50 @@ class DatabaseMssql extends Database { } protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) { - // Test for driver support, to avoid suppressed fatal error if ( !function_exists( 'sqlsrv_connect' ) ) { throw new DBConnectionError( $this, - "Microsoft SQL Server Native (sqlsrv) functions missing. - You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" + "Microsoft SQL Server Native (sqlsrv) functions missing.\n + You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470" ); } $this->close(); + + if ( $schema !== null ) { + throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." ); + } + $this->server = $server; $this->user = $user; $this->password = $password; $connectionInfo = []; - - if ( $dbName != '' ) { + if ( strlen( $dbName ) ) { $connectionInfo['Database'] = $dbName; } - - // Decide which auth scenerio to use - // if we are using Windows auth, then don't add credentials to $connectionInfo if ( !$this->useWindowsAuth ) { $connectionInfo['UID'] = $user; $connectionInfo['PWD'] = $password; } AtEase::suppressWarnings(); - $this->conn = sqlsrv_connect( $server, $connectionInfo ); + $this->conn = sqlsrv_connect( $server, $connectionInfo ) ?: null; AtEase::restoreWarnings(); - if ( $this->conn === false ) { - $error = $this->lastError(); - $this->connLogger->error( - "Error connecting to {db_server}: {error}", - $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] ) - ); - throw new DBConnectionError( $this, $error ); + if ( !$this->conn ) { + throw $this->newExceptionAfterConnectError( $this->lastError() ); } - $this->currentDomain = new DatabaseDomain( - ( $dbName != '' ) ? $dbName : null, - null, - $tablePrefix - ); - - return (bool)$this->conn; + try { + $this->currentDomain = new DatabaseDomain( + strlen( $dbName ) ? $dbName : null, + null, + $tablePrefix + ); + } catch ( Exception $e ) { + throw $this->newExceptionAfterConnectError( $e->getMessage() ); + } } /** diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index 1e3fa845a3..b1a88ed7b9 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -125,7 +125,7 @@ abstract class DatabaseMysqlBase extends Database { $this->close(); if ( $schema !== null ) { - throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" ); + throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." ); } $this->server = $server; @@ -135,23 +135,14 @@ abstract class DatabaseMysqlBase extends Database { $this->installErrorHandler(); try { $this->conn = $this->mysqlConnect( $this->server, $dbName ); - } catch ( Exception $ex ) { + } catch ( Exception $e ) { $this->restoreErrorHandler(); - throw $ex; + throw $this->newExceptionAfterConnectError( $e->getMessage() ); } $error = $this->restoreErrorHandler(); - # Always log connection errors if ( !$this->conn ) { - $error = $error ?: $this->lastError(); - $this->connLogger->error( - "Error connecting to {db_server}: {error}", - $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] ) - ); - $this->connLogger->debug( "DB connection error\n" . - "Server: $server, User: $user, Password: " . - substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); - throw new DBConnectionError( $this, $error ); + throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() ); } try { @@ -160,7 +151,6 @@ abstract class DatabaseMysqlBase extends Database { null, $tablePrefix ); - // Abstract over any insane MySQL defaults $set = [ 'group_concat_max_len = 262144' ]; // Set SQL mode, default is turning them all off, can be overridden or skipped with null @@ -185,11 +175,8 @@ abstract class DatabaseMysqlBase extends Database { ); } } catch ( Exception $e ) { - // Connection was not fully initialized and is not safe for use - $this->conn = false; + throw $this->newExceptionAfterConnectError( $e->getMessage() ); } - - return true; } protected function doSelectDomain( DatabaseDomain $domain ) { @@ -234,7 +221,7 @@ abstract class DatabaseMysqlBase extends Database { * * @param string $realServer * @param string|null $dbName - * @return mixed Raw connection + * @return mixed|null Driver connection handle * @throws DBConnectionError */ abstract protected function mysqlConnect( $realServer, $dbName ); diff --git a/includes/libs/rdbms/database/DatabaseMysqli.php b/includes/libs/rdbms/database/DatabaseMysqli.php index 0f444cd210..ddb39446b4 100644 --- a/includes/libs/rdbms/database/DatabaseMysqli.php +++ b/includes/libs/rdbms/database/DatabaseMysqli.php @@ -54,14 +54,14 @@ class DatabaseMysqli extends DatabaseMysqlBase { /** * @param string $realServer * @param string|null $dbName - * @return bool|mysqli + * @return mysqli|null * @throws DBConnectionError */ protected function mysqlConnect( $realServer, $dbName ) { - # Avoid suppressed fatal error, which is very hard to track down if ( !function_exists( 'mysqli_init' ) ) { - throw new DBConnectionError( $this, "MySQLi functions missing," - . " have you compiled PHP with the --with-mysqli option?\n" ); + throw $this->newExceptionAfterConnectError( + "MySQLi functions missing, have you compiled PHP with the --with-mysqli option?" + ); } // Other than mysql_connect, mysqli_real_connect expects an explicit port @@ -84,7 +84,7 @@ class DatabaseMysqli extends DatabaseMysqlBase { $mysqli = mysqli_init(); $connFlags = 0; - if ( $this->flags & self::DBO_SSL ) { + if ( $this->getFlag( self::DBO_SSL ) ) { $connFlags |= MYSQLI_CLIENT_SSL; $mysqli->ssl_set( $this->sslKeyPath, @@ -94,10 +94,10 @@ class DatabaseMysqli extends DatabaseMysqlBase { $this->sslCiphers ); } - if ( $this->flags & self::DBO_COMPRESS ) { + if ( $this->getFlag( self::DBO_COMPRESS ) ) { $connFlags |= MYSQLI_CLIENT_COMPRESS; } - if ( $this->flags & self::DBO_PERSISTENT ) { + if ( $this->getFlag( self::DBO_PERSISTENT ) ) { $realServer = 'p:' . $realServer; } @@ -122,7 +122,7 @@ class DatabaseMysqli extends DatabaseMysqlBase { return $mysqli; } - return false; + return null; } /** diff --git a/includes/libs/rdbms/database/DatabasePostgres.php b/includes/libs/rdbms/database/DatabasePostgres.php index 840b4280b6..a7ebc86e50 100644 --- a/includes/libs/rdbms/database/DatabasePostgres.php +++ b/includes/libs/rdbms/database/DatabasePostgres.php @@ -31,22 +31,19 @@ use Exception; * @ingroup Database */ class DatabasePostgres extends Database { - /** @var int|bool */ - protected $port; - - /** @var resource */ - protected $lastResultHandle = null; - - /** @var float|string */ - private $numericVersion = null; - /** @var string Connect string to open a PostgreSQL connection */ - private $connectString; + /** @var int|null */ + private $port; /** @var string */ private $coreSchema; /** @var string */ private $tempSchema; /** @var string[] Map of (reserved table name => alternate table name) */ private $keywordTableMap = []; + /** @var float|string */ + private $numericVersion; + + /** @var resource|null */ + private $lastResultHandle; /** * @see Database::__construct() @@ -54,7 +51,7 @@ class DatabasePostgres extends Database { * - keywordTableMap : Map of reserved table names to alternative table names to use */ public function __construct( array $params ) { - $this->port = $params['port'] ?? false; + $this->port = intval( $params['port'] ?? null ); $this->keywordTableMap = $params['keywordTableMap'] ?? []; parent::__construct( $params ); @@ -64,10 +61,6 @@ class DatabasePostgres extends Database { return 'postgres'; } - public function implicitGroupby() { - return false; - } - public function implicitOrderby() { return false; } @@ -87,13 +80,11 @@ class DatabasePostgres extends Database { } protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) { - // Test for Postgres support, to avoid suppressed fatal error if ( !function_exists( 'pg_connect' ) ) { - throw new DBConnectionError( - $this, + throw $this->newExceptionAfterConnectError( "Postgres functions missing, have you compiled PHP with the --with-pgsql\n" . "option? (Note: if you recently installed PHP, you may need to restart your\n" . - "webserver and database)\n" + "webserver and database)" ); } @@ -104,60 +95,47 @@ class DatabasePostgres extends Database { $this->password = $password; $connectVars = [ - // pg_connect() user $user as the default database. Since a database is *required*, - // at least pick a "don't care" database that is more likely to exist. This case - // arrises when LoadBalancer::getConnection( $i, [], '' ) is used. + // pg_connect() user $user as the default database. Since a database is required, + // then pick a "don't care" database that is more likely to exist than that one. 'dbname' => strlen( $dbName ) ? $dbName : 'postgres', 'user' => $user, 'password' => $password ]; - if ( $server != false && $server != '' ) { + if ( strlen( $server ) ) { $connectVars['host'] = $server; } - if ( (int)$this->port > 0 ) { - $connectVars['port'] = (int)$this->port; + if ( $this->port > 0 ) { + $connectVars['port'] = $this->port; } - if ( $this->flags & self::DBO_SSL ) { + if ( $this->getFlag( self::DBO_SSL ) ) { $connectVars['sslmode'] = 'require'; } - - $this->connectString = $this->makeConnectionString( $connectVars ); + $connectString = $this->makeConnectionString( $connectVars ); $this->installErrorHandler(); try { - // Use new connections to let LoadBalancer/LBFactory handle reuse - $this->conn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW ); - } catch ( Exception $ex ) { + $this->conn = pg_connect( $connectString, PGSQL_CONNECT_FORCE_NEW ) ?: null; + } catch ( Exception $e ) { $this->restoreErrorHandler(); - throw $ex; + throw $this->newExceptionAfterConnectError( $e->getMessage() ); } - $phpError = $this->restoreErrorHandler(); + $error = $this->restoreErrorHandler(); if ( !$this->conn ) { - $this->queryLogger->debug( - "DB connection error\n" . - "Server: $server, Database: $dbName, User: $user, Password: " . - substr( $password, 0, 3 ) . "...\n" - ); - $this->queryLogger->debug( $this->lastError() . "\n" ); - throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) ); + throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() ); } try { - // If called from the command-line (e.g. importDump), only show errors. - // No transaction should be open at this point, so the problem of the SET - // effects being rolled back should not be an issue. + // Since no transaction is active at this point, any SET commands should apply + // for the entire session (e.g. will not be reverted on transaction rollback). // See https://www.postgresql.org/docs/8.3/sql-set.html - $variables = []; - if ( $this->cliMode ) { - $variables['client_min_messages'] = 'ERROR'; - } - $variables += [ + $variables = [ 'client_encoding' => 'UTF8', 'datestyle' => 'ISO, YMD', 'timezone' => 'GMT', 'standard_conforming_strings' => 'on', - 'bytea_output' => 'escape' + 'bytea_output' => 'escape', + 'client_min_messages' => 'ERROR' ]; foreach ( $variables as $var => $val ) { $this->query( @@ -166,12 +144,10 @@ class DatabasePostgres extends Database { self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY ); } - $this->determineCoreSchema( $schema ); $this->currentDomain = new DatabaseDomain( $dbName, $schema, $tablePrefix ); } catch ( Exception $e ) { - // Connection was not fully initialized and is not safe for use - $this->conn = false; + throw $this->newExceptionAfterConnectError( $e->getMessage() ); } } @@ -1026,7 +1002,7 @@ __INDEXATTR__; * Values may contain magic keywords like "$user" * @since 1.19 * - * @param array $search_path List of schemas to be searched by default + * @param string[] $search_path List of schemas to be searched by default */ private function setSearchPath( $search_path ) { $this->query( @@ -1066,11 +1042,7 @@ __INDEXATTR__; $this->queryLogger->debug( "Schema \"" . $desiredSchema . "\" already in the search path\n" ); } else { - /** - * Prepend our schema (e.g. 'mediawiki') in front - * of the search path - * Fixes T17816 - */ + // Prepend the desired schema to the search path (T17816) $search_path = $this->getSearchPath(); array_unshift( $search_path, $this->addIdentifierQuotes( $desiredSchema ) ); $this->setSearchPath( $search_path ); diff --git a/includes/libs/rdbms/database/DatabaseSqlite.php b/includes/libs/rdbms/database/DatabaseSqlite.php index 11dda2fb39..83567a5971 100644 --- a/includes/libs/rdbms/database/DatabaseSqlite.php +++ b/includes/libs/rdbms/database/DatabaseSqlite.php @@ -60,6 +60,9 @@ class DatabaseSqlite extends Database { /** @var bool Whether full text is enabled */ private static $fulltextEnabled = null; + /** @var string[] See https://www.sqlite.org/lang_transaction.html */ + private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ]; + /** * Additional params include: * - dbDirectory : directory containing the DB and the lock file directory @@ -77,8 +80,7 @@ class DatabaseSqlite extends Database { $this->dbDir = $p['dbDirectory']; } - // Set a dummy user to make initConnection() trigger open() - parent::__construct( [ 'user' => '@' ] + $p ); + parent::__construct( $p ); $this->trxMode = strtoupper( $p['trxMode'] ?? '' ); @@ -123,22 +125,13 @@ class DatabaseSqlite extends Database { return 'sqlite'; } - /** - * @todo Check if it should be true like parent class - * - * @return bool - */ - public function implicitGroupby() { - return false; - } - protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) { $this->close(); // Note that for SQLite, $server, $user, and $pass are ignored if ( $schema !== null ) { - throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" ); + throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." ); } if ( $this->dbPath !== null ) { @@ -146,59 +139,45 @@ class DatabaseSqlite extends Database { } elseif ( $this->dbDir !== null ) { $path = self::generateFileName( $this->dbDir, $dbName ); } else { - throw new DBExpectedError( $this, __CLASS__ . ": DB path or directory required" ); + throw $this->newExceptionAfterConnectError( "DB path or directory required" ); } - if ( !in_array( $this->trxMode, [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ], true ) ) { - throw new DBExpectedError( - $this, - __CLASS__ . ": invalid transaction mode '{$this->trxMode}'" - ); + if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) { + throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' ); + } elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) { + throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" ); } - if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) { - $error = "SQLite database file not readable"; - $this->connLogger->error( - "Error connecting to {db_server}: {error}", - $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] ) - ); - throw new DBConnectionError( $this, $error ); + $attributes = []; + if ( $this->getFlag( self::DBO_PERSISTENT ) ) { + // Persistent connections can avoid some schema index reading overhead. + // On the other hand, they can cause horrible contention with DBO_TRX. + if ( $this->getFlag( self::DBO_TRX ) ) { + $this->connLogger->warning( __METHOD__ . ": DBO_PERSISTENT mixed with DBO_TRX" ); + } else { + $attributes[PDO::ATTR_PERSISTENT] = true; + } } try { - $conn = new PDO( - "sqlite:$path", - '', - '', - [ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ] - ); - // Set error codes only, don't raise exceptions - $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); + $this->conn = new PDO( "sqlite:$path", null, null, $attributes ); } catch ( PDOException $e ) { - $error = $e->getMessage(); - $this->connLogger->error( - "Error connecting to {db_server}: {error}", - $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] ) - ); - throw new DBConnectionError( $this, $error ); + throw $this->newExceptionAfterConnectError( $e->getMessage() ); } - $this->conn = $conn; $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix ); try { $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY; // Enforce LIKE to be case sensitive, just like MySQL $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags ); - // Apply an optimizations or requirements regarding fsync() usage + // Apply optimizations or requirements regarding fsync() usage $sync = $this->connectionVariables['synchronous'] ?? null; if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) { $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags ); } } catch ( Exception $e ) { - // Connection was not fully initialized and is not safe for use - $this->conn = false; - throw $e; + throw $this->newExceptionAfterConnectError( $e->getMessage() ); } } diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index ef7f1e24f6..7e542218f4 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -87,7 +87,7 @@ interface IDatabase { /** @var int Combine list with OR clauses */ const LIST_OR = 4; - /** @var int Enable debug logging */ + /** @var int Enable debug logging of all SQL queries */ const DBO_DEBUG = 1; /** @var int Disable query buffering (only one result set can be iterated at a time) */ const DBO_NOBUFFER = 2; @@ -238,14 +238,6 @@ interface IDatabase { */ public function setLazyMasterHandle( IDatabase $conn ); - /** - * Returns true if this database does an implicit sort when doing GROUP BY - * - * @return bool - * @deprecated Since 1.30; only use grouped or aggregated fields in the SELECT - */ - public function implicitGroupby(); - /** * Returns true if this database does an implicit order by when the column has an index * For example: SELECT page_title FROM page LIMIT 1 @@ -260,15 +252,6 @@ interface IDatabase { */ public function lastQuery(); - /** - * Returns true if the connection may have been used for write queries. - * Should return true if unsure. - * - * @return bool - * @deprecated Since 1.31; use lastDoneWrites() - */ - public function doneWrites(); - /** * Returns the last time the connection may have been used for write queries. * Should return a timestamp if unsure. @@ -336,13 +319,7 @@ interface IDatabase { /** * Set a flag for this connection * - * @param int $flag DBO_* constants from Defines.php: - * - DBO_DEBUG: output some debug info (same as debug()) - * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) - * - DBO_TRX: automatically start transactions - * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode - * and removes it in command line mode - * - DBO_PERSISTENT: use persistant database connection + * @param int $flag IDatabase::DBO_DEBUG, IDatabase::DBO_NOBUFFER, or IDatabase::DBO_TRX * @param string $remember IDatabase::REMEMBER_* constant [default: REMEMBER_NOTHING] */ public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ); @@ -350,13 +327,7 @@ interface IDatabase { /** * Clear a flag for this connection * - * @param int $flag DBO_* constants from Defines.php: - * - DBO_DEBUG: output some debug info (same as debug()) - * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) - * - DBO_TRX: automatically start transactions - * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode - * and removes it in command line mode - * - DBO_PERSISTENT: use persistant database connection + * @param int $flag IDatabase::DBO_DEBUG, IDatabase::DBO_NOBUFFER, or IDatabase::DBO_TRX * @param string $remember IDatabase::REMEMBER_* constant [default: REMEMBER_NOTHING] */ public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ); @@ -372,11 +343,7 @@ interface IDatabase { /** * Returns a boolean whether the flag $flag is set for this connection * - * @param int $flag DBO_* constants from Defines.php: - * - DBO_DEBUG: output some debug info (same as debug()) - * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) - * - DBO_TRX: automatically start transactions - * - DBO_PERSISTENT: use persistant database connection + * @param int $flag One of the class IDatabase::DBO_* constants * @return bool */ public function getFlag( $flag ); @@ -390,14 +357,6 @@ interface IDatabase { */ public function getDomainID(); - /** - * Alias for getDomainID() - * - * @return string - * @deprecated 1.30 - */ - public function getWikiID(); - /** * Get the type of the DBMS, as it appears in $wgDBtype. * diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php b/includes/libs/rdbms/lbfactory/LBFactory.php index a85e1e544f..4426654ca2 100644 --- a/includes/libs/rdbms/lbfactory/LBFactory.php +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@ -651,14 +651,6 @@ abstract class LBFactory implements ILBFactory { $this->indexAliases = $aliases; } - /** - * @param string $prefix - * @deprecated Since 1.33 - */ - public function setDomainPrefix( $prefix ) { - $this->setLocalDomainPrefix( $prefix ); - } - public function setLocalDomainPrefix( $prefix ) { $this->localDomain = new DatabaseDomain( $this->localDomain->getDatabase(), diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/includes/libs/rdbms/loadbalancer/LoadBalancer.php index 1ef1d09b5f..46d8c068ad 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@ -656,7 +656,7 @@ class LoadBalancer implements ILoadBalancer { $ok = true; // no applicable loads } } finally { - # Restore the old position, as this is not used for lag-protection but for throttling + // Restore the old position; this is used for throttling, not lag-protection $this->waitForPos = $oldPos; } @@ -673,7 +673,7 @@ class LoadBalancer implements ILoadBalancer { $ok = true; for ( $i = 1; $i < $serverCount; $i++ ) { - if ( $this->groupLoads[self::GROUP_GENERIC][$i] > 0 ) { + if ( $this->serverHasLoadInAnyGroup( $i ) ) { $start = microtime( true ); $ok = $this->doWait( $i, true, $timeout ) && $ok; $timeout -= intval( microtime( true ) - $start ); @@ -683,13 +683,27 @@ class LoadBalancer implements ILoadBalancer { } } } finally { - # Restore the old position, as this is not used for lag-protection but for throttling + // Restore the old position; this is used for throttling, not lag-protection $this->waitForPos = $oldPos; } return $ok; } + /** + * @param int $i Specific server index + * @return bool + */ + private function serverHasLoadInAnyGroup( $i ) { + foreach ( $this->groupLoads as $loadsByIndex ) { + if ( ( $loadsByIndex[$i] ?? 0 ) > 0 ) { + return true; + } + } + + return false; + } + /** * @param DBMasterPos|bool $pos */ @@ -1922,15 +1936,6 @@ class LoadBalancer implements ILoadBalancer { return $this->laggedReplicaMode; } - /** - * @return bool - * @since 1.27 - * @deprecated Since 1.28; use laggedReplicaUsed() - */ - public function laggedSlaveUsed() { - return $this->laggedReplicaUsed(); - } - public function getReadOnlyReason( $domain = false, IDatabase $conn = null ) { if ( $this->readOnlyReason !== false ) { return $this->readOnlyReason; @@ -2203,14 +2208,6 @@ class LoadBalancer implements ILoadBalancer { $this->indexAliases = $aliases; } - /** - * @param string $prefix - * @deprecated Since 1.33 - */ - public function setDomainPrefix( $prefix ) { - $this->setLocalDomainPrefix( $prefix ); - } - public function setLocalDomainPrefix( $prefix ) { // Find connections to explicit foreign domains still marked as in-use... $domainsInUse = []; diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php index e8dd8982b5..9e63ffee64 100644 --- a/includes/logging/LogFormatter.php +++ b/includes/logging/LogFormatter.php @@ -153,6 +153,19 @@ class LogFormatter { : self::FOR_PUBLIC; } + /** + * Check if a log item type can be displayed + * @return bool + */ + public function canViewLogType() { + // If the user doesn't have the right permission to view the specific + // log type, return false + $logRestrictions = $this->context->getConfig()->get( 'LogRestrictions' ); + $type = $this->entry->getType(); + return !isset( $logRestrictions[$type] ) + || $this->context->getUser()->isAllowed( $logRestrictions[$type] ); + } + /** * Check if a log item can be displayed * @param int $field LogPage::DELETED_* constant @@ -161,9 +174,10 @@ class LogFormatter { protected function canView( $field ) { if ( $this->audience == self::FOR_THIS_USER ) { return LogEventsList::userCanBitfield( - $this->entry->getDeleted(), $field, $this->context->getUser() ); + $this->entry->getDeleted(), $field, $this->context->getUser() ) && + self::canViewLogType(); } else { - return !$this->entry->isDeleted( $field ); + return !$this->entry->isDeleted( $field ) && self::canViewLogType(); } } diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php index ac332b75db..6ae64261be 100644 --- a/includes/media/SVGMetadataExtractor.php +++ b/includes/media/SVGMetadataExtractor.php @@ -27,9 +27,17 @@ /** * @ingroup Media + * @deprecated since 1.34 */ class SVGMetadataExtractor { - static function getMetadata( $filename ) { + /** + * @param string $filename + * @return array + * @deprecated since 1.34, use SVGReader->getMetadata() directly + */ + public static function getMetadata( $filename ) { + wfDeprecated( __METHOD__, '1.34' ); + $svg = new SVGReader( $filename ); return $svg->getMetadata(); diff --git a/includes/media/SvgHandler.php b/includes/media/SvgHandler.php index bdda674f0d..639132c116 100644 --- a/includes/media/SvgHandler.php +++ b/includes/media/SvgHandler.php @@ -438,8 +438,10 @@ class SvgHandler extends ImageHandler { */ public function getMetadata( $file, $filename ) { $metadata = [ 'version' => self::SVG_METADATA_VERSION ]; + try { - $metadata += SVGMetadataExtractor::getMetadata( $filename ); + $svgReader = new SVGReader( $filename ); + $metadata += $svgReader->getMetadata(); } catch ( Exception $e ) { // @todo SVG specific exceptions // File not found, broken, etc. $metadata['error'] = [ diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index 9226875e2a..6cc63bffb3 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -27,6 +27,7 @@ use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\DBError; use Wikimedia\Rdbms\DBQueryError; use Wikimedia\Rdbms\DBConnectionError; +use Wikimedia\Rdbms\IMaintainableDatabase; use Wikimedia\Rdbms\LoadBalancer; use Wikimedia\ScopedCallback; use Wikimedia\WaitConditionLoop; @@ -165,7 +166,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { * Get a connection to the specified database * * @param int $serverIndex - * @return Database + * @return IMaintainableDatabase * @throws MWException */ protected function getDB( $serverIndex ) { @@ -199,11 +200,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER; if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) { // Keep a separate connection to avoid contention and deadlocks - $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT ); + $db = $lb->getConnectionRef( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT ); } else { // However, SQLite has the opposite behavior due to DB-level locking. // Stock sqlite MediaWiki installs use a separate sqlite cache DB instead. - $db = $lb->getConnection( $index ); + $db = $lb->getConnectionRef( $index ); } } diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php index 5aa1a691b0..a7916c5796 100644 --- a/includes/parser/CoreParserFunctions.php +++ b/includes/parser/CoreParserFunctions.php @@ -1187,39 +1187,44 @@ class CoreParserFunctions { */ public static function pageid( $parser, $title = null ) { $t = Title::newFromText( $title ); - if ( is_null( $t ) ) { + if ( !$t ) { return ''; + } elseif ( !$t->canExist() || $t->isExternal() ) { + return 0; // e.g. special page or interwiki link } - // Use title from parser to have correct pageid after edit + + $parserOutput = $parser->getOutput(); + if ( $t->equals( $parser->getTitle() ) ) { - $t = $parser->getTitle(); - return $t->getArticleID(); - } + // Revision is for the same title that is currently being parsed. + // Use the title from Parser in case a new page ID was injected into it. + $parserOutput->setFlag( 'vary-page-id' ); + $id = $parser->getTitle()->getArticleID(); + if ( $id ) { + $parserOutput->setSpeculativePageIdUsed( $id ); + } - // These can't have ids - if ( !$t->canExist() || $t->isExternal() ) { - return 0; + return $id; } - // Check the link cache, maybe something already looked it up. + // Check the link cache for the title $linkCache = MediaWikiServices::getInstance()->getLinkCache(); $pdbk = $t->getPrefixedDBkey(); $id = $linkCache->getGoodLinkID( $pdbk ); - if ( $id != 0 ) { - $parser->mOutput->addLink( $t, $id ); - return $id; - } - if ( $linkCache->isBadLink( $pdbk ) ) { - $parser->mOutput->addLink( $t, 0 ); + if ( $id != 0 || $linkCache->isBadLink( $pdbk ) ) { + $parserOutput->addLink( $t, $id ); + return $id; } // We need to load it from the DB, so mark expensive if ( $parser->incrementExpensiveFunctionCount() ) { $id = $t->getArticleID(); - $parser->mOutput->addLink( $t, $id ); + $parserOutput->addLink( $t, $id ); + return $id; } + return null; } diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 5821d3f9cf..e5bf94a602 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -26,7 +26,9 @@ use MediaWiki\Linker\LinkRendererFactory; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; use MediaWiki\Special\SpecialPageFactory; +use Psr\Log\NullLogger; use Wikimedia\ScopedCallback; +use Psr\Log\LoggerInterface; /** * @defgroup Parser Parser @@ -294,6 +296,9 @@ class Parser { /** @var NamespaceInfo */ private $nsInfo; + /** @var LoggerInterface */ + private $logger; + /** * TODO Make this a const when HHVM support is dropped (T192166) * @@ -333,11 +338,18 @@ class Parser { * @param SpecialPageFactory|null $spFactory * @param LinkRendererFactory|null $linkRendererFactory * @param NamespaceInfo|null $nsInfo + * @param LoggerInterface|null $logger */ public function __construct( - $svcOptions = null, MagicWordFactory $magicWordFactory = null, - Language $contLang = null, ParserFactory $factory = null, $urlProtocols = null, - SpecialPageFactory $spFactory = null, $linkRendererFactory = null, $nsInfo = null + $svcOptions = null, + MagicWordFactory $magicWordFactory = null, + Language $contLang = null, + ParserFactory $factory = null, + $urlProtocols = null, + SpecialPageFactory $spFactory = null, + $linkRendererFactory = null, + $nsInfo = null, + $logger = null ) { $services = MediaWikiServices::getInstance(); if ( !$svcOptions || is_array( $svcOptions ) ) { @@ -382,6 +394,7 @@ class Parser { $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory(); $this->linkRendererFactory = $linkRendererFactory ?? $services->getLinkRendererFactory(); $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo(); + $this->logger = $logger ?: new NullLogger(); } /** @@ -2770,16 +2783,16 @@ class Parser { $value = wfEscapeWikiText( $subjPage->getPrefixedURL() ); break; case 'pageid': // requested in T25427 - $pageid = $this->getTitle()->getArticleID(); - if ( $pageid == 0 ) { - # 0 means the page doesn't exist in the database, - # which means the user is previewing a new page. - # The vary-revision flag must be set, because the magic word - # will have a different value once the page is saved. - $this->mOutput->setFlag( 'vary-revision' ); - wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision" ); + # Inform the edit saving system that getting the canonical output + # after page insertion requires a parse that used that exact page ID + $this->setOutputFlag( 'vary-page-id', '{{PAGEID}} used' ); + $value = $this->mTitle->getArticleID(); + if ( !$value ) { + $value = $this->mOptions->getSpeculativePageId(); + if ( $value ) { + $this->mOutput->setSpeculativePageIdUsed( $value ); + } } - $value = $pageid ?: null; break; case 'revisionid': if ( @@ -2793,15 +2806,13 @@ class Parser { if ( $this->getRevisionId() || $this->mOptions->getSpeculativeRevId() ) { $value = '-'; } else { - $this->mOutput->setFlag( 'vary-revision-exists' ); - wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-exists" ); + $this->setOutputFlag( 'vary-revision-exists', '{{REVISIONID}} used' ); $value = ''; } } else { # Inform the edit saving system that getting the canonical output after - # revision insertion requires another parse using the actual revision ID - $this->mOutput->setFlag( 'vary-revision-id' ); - wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id" ); + # revision insertion requires a parse that used that exact revision ID + $this->setOutputFlag( 'vary-revision-id', '{{REVISIONID}} used' ); $value = $this->getRevisionId(); if ( $value === 0 ) { $rev = $this->getRevisionObject(); @@ -2836,8 +2847,7 @@ class Parser { case 'revisionuser': # Inform the edit saving system that getting the canonical output after # revision insertion requires a parse that used the actual user ID - $this->mOutput->setFlag( 'vary-user' ); - wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user" ); + $this->setOutputFlag( 'vary-user', '{{REVISIONUSER}} used' ); $value = $this->getRevisionUser(); break; case 'revisionsize': @@ -3007,8 +3017,7 @@ class Parser { if ( $resNow !== $resThen ) { # Inform the edit saving system that getting the canonical output after # revision insertion requires a parse that used an actual revision timestamp - $this->mOutput->setFlag( 'vary-revision-timestamp' ); - wfDebug( __METHOD__ . ": $variable used, setting vary-revision-timestamp" ); + $this->setOutputFlag( 'vary-revision-timestamp', "$variable used" ); } } @@ -3105,8 +3114,10 @@ class Parser { if ( $frame === false ) { $frame = $this->getPreprocessor()->newFrame(); } elseif ( !( $frame instanceof PPFrame ) ) { - wfDebug( __METHOD__ . " called using plain parameters instead of " - . "a PPFrame instance. Creating custom frame.\n" ); + $this->logger->debug( + __METHOD__ . " called using plain parameters instead of " . + "a PPFrame instance. Creating custom frame." + ); $frame = $this->getPreprocessor()->newCustomFrame( $frame ); } @@ -3407,8 +3418,10 @@ class Parser { } } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) { $found = false; # access denied - wfDebug( __METHOD__ . ": template inclusion denied for " . - $title->getPrefixedDBkey() . "\n" ); + $this->logger->debug( + __METHOD__ . + ": template inclusion denied for " . $title->getPrefixedDBkey() + ); } else { list( $text, $title ) = $this->getTemplateDom( $title ); if ( $text !== false ) { @@ -3446,7 +3459,7 @@ class Parser { $this->addTrackingCategory( 'template-loop-category' ); $this->mOutput->addWarning( wfMessage( 'template-loop-warning', wfEscapeWikiText( $titleText ) )->text() ); - wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" ); + $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" ); } } @@ -3740,8 +3753,7 @@ class Parser { $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); if ( $dep['title']->equals( $this->getTitle() ) ) { // Self-transclusion; final result may change based on the new page version - $this->mOutput->setFlag( 'vary-revision' ); - wfDebug( __METHOD__ . ": self transclusion, setting vary-revision" ); + $this->setOutputFlag( 'vary-revision', 'Self transclusion' ); } } } @@ -4685,7 +4697,7 @@ class Parser { '~~~' => $sigText ] ); # The main two signature forms used above are time-sensitive - $this->mOutput->setFlag( 'user-signature' ); + $this->setOutputFlag( 'user-signature', 'User signature detected' ); } # Context links ("pipe tricks"): [[|name]] and [[name (context)|]] @@ -4750,7 +4762,7 @@ class Parser { if ( mb_strlen( $nickname ) > $this->svcOptions->get( 'MaxSigChars' ) ) { $nickname = $username; - wfDebug( __METHOD__ . ": $username has overlong signature.\n" ); + $this->logger->debug( __METHOD__ . ": $username has overlong signature." ); } elseif ( $fancySig !== false ) { # Sig. might contain markup; validate this if ( $this->validateSig( $nickname ) !== false ) { @@ -4759,7 +4771,7 @@ class Parser { } else { # Failed to validate; fall back to the default $nickname = $username; - wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" ); + $this->logger->debug( __METHOD__ . ": $username has bad XML tags in signature." ); } } @@ -5256,7 +5268,8 @@ class Parser { $handlerOptions[$paramName] = $match; } else { // Guess not, consider it as caption. - wfDebug( "$parameterMatch failed parameter validation\n" ); + $this->logger->debug( + "$parameterMatch failed parameter validation" ); $label = $parameterMatch; } } @@ -5642,7 +5655,7 @@ class Parser { * @deprecated since 1.28; use getOutput()->updateCacheExpiry() */ public function disableCache() { - wfDebug( "Parser output marked as uncacheable.\n" ); + $this->logger->debug( "Parser output marked as uncacheable." ); if ( !$this->mOutput ) { throw new MWException( __METHOD__ . " can only be called when actually parsing something" ); @@ -5922,19 +5935,21 @@ class Parser { * @since 1.23 (public since 1.23) */ public function getRevisionObject() { - if ( !is_null( $this->mRevisionObject ) ) { + if ( $this->mRevisionObject ) { return $this->mRevisionObject; } // NOTE: try to get the RevisionObject even if mRevisionId is null. - // This is useful when parsing revision that has not yet been saved. + // This is useful when parsing a revision that has not yet been saved. // However, if we get back a saved revision even though we are in // preview mode, we'll have to ignore it, see below. // NOTE: This callback may be used to inject an OLD revision that was // already loaded, so "current" is a bit of a misnomer. We can't just // skip it if mRevisionId is set. $rev = call_user_func( - $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this + $this->mOptions->getCurrentRevisionCallback(), + $this->getTitle(), + $this ); if ( $this->mRevisionId === null && $rev && $rev->getId() ) { @@ -6448,4 +6463,14 @@ class Parser { OutputPage::setupOOUI(); $this->mOutput->setEnableOOUI( true ); } + + /** + * @param string $flag + * @param string $reason + */ + protected function setOutputFlag( $flag, $reason ) { + $this->mOutput->setFlag( $flag ); + $name = $this->mTitle->getPrefixedText(); + $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" ); + } } diff --git a/includes/parser/ParserFactory.php b/includes/parser/ParserFactory.php index 0446d9c640..3d15e86fd5 100644 --- a/includes/parser/ParserFactory.php +++ b/includes/parser/ParserFactory.php @@ -23,6 +23,8 @@ use MediaWiki\Config\ServiceOptions; use MediaWiki\Linker\LinkRendererFactory; use MediaWiki\MediaWikiServices; use MediaWiki\Special\SpecialPageFactory; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; /** * @since 1.32 @@ -49,6 +51,9 @@ class ParserFactory { /** @var NamespaceInfo */ private $nsInfo; + /** @var LoggerInterface */ + private $logger; + /** * Old parameter list, which we support for backwards compatibility, were: * array $parserConf See $wgParserConf documentation @@ -71,12 +76,18 @@ class ParserFactory { * @param SpecialPageFactory $spFactory * @param LinkRendererFactory $linkRendererFactory * @param NamespaceInfo|LinkRendererFactory|null $nsInfo + * @param LoggerInterface|null $logger * @since 1.32 */ public function __construct( - $svcOptions, MagicWordFactory $magicWordFactory, Language $contLang, - $urlProtocols, SpecialPageFactory $spFactory, $linkRendererFactory, - $nsInfo = null + $svcOptions, + MagicWordFactory $magicWordFactory, + Language $contLang, + $urlProtocols, + SpecialPageFactory $spFactory, + $linkRendererFactory, + $nsInfo = null, + $logger = null ) { // @todo Do we need to retain compat for constructing this class directly? if ( !$nsInfo ) { @@ -107,6 +118,7 @@ class ParserFactory { $this->specialPageFactory = $spFactory; $this->linkRendererFactory = $linkRendererFactory; $this->nsInfo = $nsInfo; + $this->logger = $logger ?: new NullLogger(); } /** @@ -114,8 +126,16 @@ class ParserFactory { * @since 1.32 */ public function create() : Parser { - return new Parser( $this->svcOptions, $this->magicWordFactory, $this->contLang, $this, - $this->urlProtocols, $this->specialPageFactory, $this->linkRendererFactory, - $this->nsInfo ); + return new Parser( + $this->svcOptions, + $this->magicWordFactory, + $this->contLang, + $this, + $this->urlProtocols, + $this->specialPageFactory, + $this->linkRendererFactory, + $this->nsInfo, + $this->logger + ); } } diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index 709f159bb0..5a159fba43 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -62,6 +62,7 @@ class ParserOptions { private static $lazyOptions = [ 'dateformat' => [ __CLASS__, 'initDateFormat' ], 'speculativeRevId' => [ __CLASS__, 'initSpeculativeRevId' ], + 'speculativePageId' => [ __CLASS__, 'initSpeculativePageId' ], ]; /** @@ -117,11 +118,6 @@ class ParserOptions { */ private $mExtraKey = ''; - /** - * @name Option accessors - * @{ - */ - /** * Fetch an option and track that is was accessed * @since 1.30 @@ -856,11 +852,25 @@ class ParserOptions { return $this->getOption( 'speculativeRevId' ); } + /** + * A guess for {{PAGEID}}, calculated using the callback provided via + * setSpeculativeRevPageCallback(). For consistency, the value will be calculated upon the + * first call of this method, and re-used for subsequent calls. + * + * If no callback was defined via setSpeculativePageIdCallback(), this method will return false. + * + * @since 1.34 + * @return int|false + */ + public function getSpeculativePageId() { + return $this->getOption( 'speculativePageId' ); + } + /** * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativeRevId(). * * @param ParserOptions $popt - * @return bool|false + * @return int|false */ private static function initSpeculativeRevId( ParserOptions $popt ) { $cb = $popt->getOption( 'speculativeRevIdCallback' ); @@ -871,27 +881,40 @@ class ParserOptions { } /** - * Callback to generate a guess for {{REVISIONID}} - * @since 1.28 - * @deprecated since 1.32, use getSpeculativeRevId() instead! - * @return callable|null + * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativePageId(). + * + * @param ParserOptions $popt + * @return int|false */ - public function getSpeculativeRevIdCallback() { - return $this->getOption( 'speculativeRevIdCallback' ); + private static function initSpeculativePageId( ParserOptions $popt ) { + $cb = $popt->getOption( 'speculativePageIdCallback' ); + $id = $cb ? $cb() : null; + + // returning null would result in this being re-called every access + return $id ?? false; } /** * Callback to generate a guess for {{REVISIONID}} - * @since 1.28 - * @param callable|null $x New value (null is no change) + * @param callable|null $x New value * @return callable|null Old value + * @since 1.28 */ public function setSpeculativeRevIdCallback( $x ) { $this->setOption( 'speculativeRevId', null ); // reset - return $this->setOptionLegacy( 'speculativeRevIdCallback', $x ); + return $this->setOption( 'speculativeRevIdCallback', $x ); } - /**@}*/ + /** + * Callback to generate a guess for {{PAGEID}} + * @param callable|null $x New value + * @return callable|null Old value + * @since 1.34 + */ + public function setSpeculativePageIdCallback( $x ) { + $this->setOption( 'speculativePageId', null ); // reset + return $this->setOption( 'speculativePageIdCallback', $x ); + } /** * Timestamp used for {{CURRENTDAY}} etc. @@ -1102,6 +1125,8 @@ class ParserOptions { 'templateCallback' => [ Parser::class, 'statelessFetchTemplate' ], 'speculativeRevIdCallback' => null, 'speculativeRevId' => null, + 'speculativePageIdCallback' => null, + 'speculativePageId' => null, ]; Hooks::run( 'ParserOptionsRegister', [ diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index 23e5911574..dcb5444502 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -210,9 +210,17 @@ class ParserOutput extends CacheTime { */ private $mFlags = []; - /** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */ - private $mSpeculativeRevId; + /** @var string[] */ + private static $speculativeFields = [ + 'speculativePageIdUsed', + 'speculativeRevIdUsed', + 'revisionTimestampUsed' + ]; + /** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */ + private $speculativeRevIdUsed; + /** @var int|null Assumed page ID for {{PAGEID}} if no revision is set */ + private $speculativePageIdUsed; /** @var int|null Assumed rev timestamp for {{REVISIONTIMESTAMP}} if no revision is set */ private $revisionTimestampUsed; @@ -440,7 +448,7 @@ class ParserOutput extends CacheTime { * @since 1.28 */ public function setSpeculativeRevIdUsed( $id ) { - $this->mSpeculativeRevId = $id; + $this->speculativeRevIdUsed = $id; } /** @@ -448,7 +456,23 @@ class ParserOutput extends CacheTime { * @since 1.28 */ public function getSpeculativeRevIdUsed() { - return $this->mSpeculativeRevId; + return $this->speculativeRevIdUsed; + } + + /** + * @param int $id + * @since 1.34 + */ + public function setSpeculativePageIdUsed( $id ) { + $this->speculativePageIdUsed = $id; + } + + /** + * @return int|null + * @since 1.34 + */ + public function getSpeculativePageIdUsed() { + return $this->speculativePageIdUsed; } /** @@ -1320,18 +1344,13 @@ class ParserOutput extends CacheTime { $this->mWarnings = self::mergeMap( $this->mWarnings, $source->mWarnings ); // don't use getter $this->mTimestamp = $this->useMaxValue( $this->mTimestamp, $source->getTimestamp() ); - if ( $this->mSpeculativeRevId && $source->mSpeculativeRevId - && $this->mSpeculativeRevId !== $source->mSpeculativeRevId - ) { - wfLogWarning( - 'Inconsistent speculative revision ID encountered while merging parser output!' - ); + foreach ( self::$speculativeFields as $field ) { + if ( $this->$field && $source->$field && $this->$field !== $source->$field ) { + wfLogWarning( __METHOD__ . ": inconsistent '$field' properties!" ); + } + $this->$field = $this->useMaxValue( $this->$field, $source->$field ); } - $this->mSpeculativeRevId = $this->useMaxValue( - $this->mSpeculativeRevId, - $source->getSpeculativeRevIdUsed() - ); $this->mParseStartTime = $this->useEachMinValue( $this->mParseStartTime, $source->mParseStartTime diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 671fe86c7c..7e623b53a7 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -480,7 +480,6 @@ class ResourceLoader implements LoggerAwareInterface { * @return array */ public function getTestModuleNames( $framework = 'all' ) { - /** @todo api siteinfo prop testmodulenames modulenames */ if ( $framework == 'all' ) { return $this->testModuleNames; } elseif ( isset( $this->testModuleNames[$framework] ) diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php index 900395108b..6497543f56 100644 --- a/includes/resourceloader/ResourceLoaderImage.php +++ b/includes/resourceloader/ResourceLoaderImage.php @@ -437,7 +437,8 @@ class ResourceLoaderImage { file_put_contents( $tempFilenameSvg, $svg ); - $metadata = SVGMetadataExtractor::getMetadata( $tempFilenameSvg ); + $svgReader = new SVGReader( $tempFilenameSvg ); + $metadata = $svgReader->getMetadata(); if ( !isset( $metadata['width'] ) || !isset( $metadata['height'] ) ) { unlink( $tempFilenameSvg ); return false; diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 7880f6f249..6b38ee4cf7 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -105,7 +105,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces, 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ), 'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ), - 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ), 'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ), 'wgEnableUploads' => $conf->get( 'EnableUploads' ), 'wgCommentByteLimit' => null, @@ -422,6 +421,14 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { '$VARS.maxQueryLength' => ResourceLoader::encodeJsonForScript( $conf->get( 'ResourceLoaderMaxQueryLength' ) ), + // The client-side module cache can be disabled by site configuration. + // It is also always disabled in debug mode. + '$VARS.storeEnabled' => ResourceLoader::encodeJsonForScript( + $conf->get( 'ResourceLoaderStorageEnabled' ) && !$context->getDebug() + ), + '$VARS.wgLegacyJavaScriptGlobals' => ResourceLoader::encodeJsonForScript( + $conf->get( 'LegacyJavaScriptGlobals' ) + ), '$VARS.storeKey' => ResourceLoader::encodeJsonForScript( $this->getStoreKey() ), '$VARS.storeVary' => ResourceLoader::encodeJsonForScript( $this->getStoreVary( $context ) ), ]; @@ -442,9 +449,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // Perform string replacements for startup.js $pairs = [ - '$VARS.wgLegacyJavaScriptGlobals' => ResourceLoader::encodeJsonForScript( - $conf->get( 'LegacyJavaScriptGlobals' ) - ), '$VARS.configuration' => ResourceLoader::encodeJsonForScript( $this->getConfigSettings( $context ) ), diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index b1040733a0..a2501c40d0 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -412,6 +412,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { return $titleInfo; } + /** @return array */ protected static function fetchTitleInfo( IDatabase $db, array $pages, $fname = __METHOD__ ) { $titleInfo = []; $batch = new LinkBatch; diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index 918c761bca..bbad648b0e 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -275,7 +275,7 @@ abstract class Skin extends ContextSource { // Check, if the page can hold some kind of content, otherwise do nothing $title = $this->getRelevantTitle(); - if ( $title->canExist() ) { + if ( $title->canExist() && $title->canHaveTalkPage() ) { if ( $title->isTalkPage() ) { $titles[] = $title->getSubjectPage(); } else { diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php index 5b15e82f34..fb9dcf56d2 100644 --- a/includes/upload/UploadBase.php +++ b/includes/upload/UploadBase.php @@ -704,6 +704,33 @@ abstract class UploadBase { return $warnings; } + /** + * Convert the warnings array returned by checkWarnings() to something that + * can be serialized. File objects will be converted to an associative array + * with the following keys: + * + * - fileName: The name of the file + * - timestamp: The upload timestamp + * + * @param mixed[] $warnings + * @return mixed[] + */ + public static function makeWarningsSerializable( $warnings ) { + array_walk_recursive( $warnings, function ( &$param, $key ) { + if ( $param instanceof File ) { + $param = [ + 'fileName' => $param->getName(), + 'timestamp' => $param->getTimestamp() + ]; + } elseif ( is_object( $param ) ) { + throw new InvalidArgumentException( + 'UploadBase::makeWarningsSerializable: ' . + 'Unexpected object of class ' . get_class( $param ) ); + } + } ); + return $warnings; + } + /** * Check whether the resulting filename is different from the desired one, * but ignore things like ucfirst() and spaces/underscore things diff --git a/includes/watcheditem/WatchedItemStore.php b/includes/watcheditem/WatchedItemStore.php index d33b6ae303..c3630292fb 100644 --- a/includes/watcheditem/WatchedItemStore.php +++ b/includes/watcheditem/WatchedItemStore.php @@ -383,7 +383,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac /** * @param UserIdentity $user - * @param TitleValue[] $titles + * @param LinkTarget[] $titles * @return bool * @throws MWException */ diff --git a/languages/i18n/ar.json b/languages/i18n/ar.json index 232a14f45e..ee5df2e549 100644 --- a/languages/i18n/ar.json +++ b/languages/i18n/ar.json @@ -77,7 +77,8 @@ "Elbasyouny", "Omar Ghrida", "AHmed Khaled", - "البراء صالح" + "البراء صالح", + "Dyolf77 (WMF)" ] }, "tog-underline": "سطر تحت الوصلات:", @@ -479,7 +480,7 @@ "password-change-forbidden": "لا يمكنك تغيير كلمات السر على هذا الويكي.", "externaldberror": "هناك إما خطأ في دخول قاعدة البيانات الخارجية أو أنه غير مسموح لك بتحديث حسابك الخارجي.", "login": "تسجيل الدخول", - "login-security": "توكيد هويتك", + "login-security": "تأكيد هويتك", "nav-login-createaccount": "دخول / إنشاء حساب", "logout": "تسجيل الخروج", "userlogout": "خروج", @@ -653,7 +654,7 @@ "changeemail-no-info": "يجب تسجيل الدخول للوصول إلى هذه الصفحة مباشرة.", "changeemail-oldemail": "عنوان البريد الإلكتروني الحالي:", "changeemail-newemail": "عنوان البريد الإلكتروني الجديد:", - "changeemail-newemail-help": "هذا الحقل ينبغي أن يترك فارغا في حالة لو كنت تريد إزالة عنوان البريد الإلكتروني الخاص بك. أنت لن تكون قادرا على إعادة ضبط كلمة سر ضائعة ولن تتلقى رسئل بريد إلكتروني من هذه الويكي لو أزيل عنوان البريد الإلكتروني.", + "changeemail-newemail-help": "هذا الحقل يجب أن يترك فارغا لو كنت تريد إزالة عنوان البريد الإلكتروني الخاص بك. لو أزيل عنوان البريد الإلكتروني لن تكون قادرا على إعادة ضبط كلمة سر ضائعة ولن تتلقى رسائل بريد إلكتروني من هذه الويكي.", "changeemail-none": "(لا شيء)", "changeemail-password": "كلمة سر {{SITENAME}} الخاصة بك:", "changeemail-submit": "غيّر البريد الإلكتروني", diff --git a/languages/i18n/as.json b/languages/i18n/as.json index 0a3d39e962..0fd9873e89 100644 --- a/languages/i18n/as.json +++ b/languages/i18n/as.json @@ -618,7 +618,7 @@ "accmailtext": "[[User talk:$1|$1]]-ৰ কাৰণে যাদৃচ্ছিকভাৱে উৎপন্ন কৰা গুপ্তশব্দ $2লৈ পঠোৱা হ'ল । \nএই নতুন একাউন্টত প্ৰৱেশ কৰি ''[[Special:ChangePassword|গুপ্তশব্দ সলনি কৰক]]'' পৃষ্ঠাখনত শব্দতো সলনি কৰি ল’ব পাৰিব ।", "newarticle": "(নতুন)", "newarticletext": "আপুনি বিচৰা প্ৰবন্ধটো বিচাৰি পোৱা নগ'ল।\n\nইচ্ছা কৰিলে আপুনিয়েই এই প্ৰবন্ধটো লিখা আৰম্ভ কৰিব পাৰে। [$1 ইয়াত] সহায় পাব।\n\nআপুনি যদি ইয়ালৈ ভুলতে আহিছে, তেনেহলে আপোনাৰ ব্ৰাওজাৰৰ '''BACK''' বুটামত টিপা মাৰক।", - "anontalkpagetext": "----''এইখন আলোচনা পৃষ্ঠা বেনামী সদস্যৰ বাবে, যিয়ে নিজা একাউণ্ট সৃষ্টি কৰা নাই বা যিয়ে সেই একাউণ্ট ব্যৱহাৰ নকৰে।\nএতেকে আমি তেখেতসকলক আই-পি ঠিকনাৰে চিনাক্ত কৰিবলৈ বাধ্য।\nসেই একেই আই-পি ঠিকনা অনেকেই ব্যৱহাৰ কৰিব পাৰে।\nআপুনি যদি এজন বেনামী সদস্য আৰু যদি আপুনি অনুভৱ কৰে যে আপোনাৰ প্ৰতি অপ্ৰাসঙ্গিক মন্তব্য কৰা হৈছে, তেনেহলে আন বেনামী সদস্যৰ পৰা পৃথক কৰিবলৈ \n[[Special:CreateAccount|একাউণ্ট সৃষ্টি কৰক]] বা [[Special:UserLogin|প্ৰৱেশ কৰক]] ।''", + "anontalkpagetext": "----\nএইখন আলোচনা পৃষ্ঠা বেনামী সদস্যৰ বাবে, যিয়ে নিজা একাউণ্ট সৃষ্টি কৰা নাই বা যিয়ে সেই একাউণ্ট ব্যৱহাৰ নকৰে।\nএতেকে আমি তেখেতসকলক আই-পি ঠিকনাৰে চিনাক্ত কৰিবলৈ বাধ্য।\nসেই একেই আই-পি ঠিকনা অনেকেই ব্যৱহাৰ কৰিব পাৰে।\nআপুনি যদি এজন বেনামী সদস্য আৰু যদি আপুনি অনুভৱ কৰে যে আপোনাৰ প্ৰতি অপ্ৰাসঙ্গিক মন্তব্য কৰা হৈছে, তেনেহলে আন বেনামী সদস্যৰ পৰা পৃথক কৰিবলৈ [[Special:CreateAccount|এ টাএকাউণ্ট সৃষ্টি কৰক]] বা [[Special:UserLogin|প্ৰৱেশ কৰক]]।", "noarticletext": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই ।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}|এই শিৰোনামা সন্ধান কৰিব পাৰে]],\n[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে],\nবা [{{fullurl:{{FULLPAGENAME}}|action=edit}} এই পৃষ্ঠা সৃষ্টি কৰিব পাৰে]", "noarticletext-nopermission": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}|এই শিৰোনামা সন্ধান কৰিব পাৰে]],\nবা [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে], কিন্তু এই পৃষ্ঠা সৃষ্টি কৰিবলৈ আপোনাৰ অনুমতি নাই।", "missing-revision": "\"{{FULLPAGENAME}}\" নামৰ পৃষ্ঠাৰ #$1 সংশোধনৰ অস্তিত্ব নাই।\n\nসাধাৰণতে বিলোপ কৰা এখন পৃষ্ঠাৰ পুৰণা ইতিহাস লিংক অনুসৰণ কৰিলে এনে হয়।\n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} বিলোপন ল'গ]ত অধিক তথ্য পাব।", @@ -737,7 +737,7 @@ "page_first": "প্ৰথম", "page_last": "অন্তিম", "histlegend": "পাৰ্থক্য বাছনি: পাৰ্থক্য চাবলৈ সংকলনবোৰৰ সম্মুখত থকা ৰেডিঅ' বুটামবোৰ বাচনী কৰি এণ্টাৰ টিপক অথবা একেবাৰে তলত দিয়া বুটামতো ক্লিক কৰক
\nলিজেণ্ড: '''({{int:cur}})''' = বৰ্তমানৰ সংকলনৰ লগত পাৰ্থক্য,\n'''({{int:last}})''' = আগৰ সংকলনৰ লগত পাৰ্থক্য, '''{{int:minoreditletter}}'' = অগুৰুত্বপূৰ্ণ সম্পাদনা।", - "history-fieldset-title": "সংশোধিত সংস্কৰণ সন্ধান কৰক", + "history-fieldset-title": "সংশোধিত সংস্কৰণ", "history-show-deleted": "মাথোঁ বিলোপ কৰা", "histfirst": "আটাইতকৈ পুৰণি", "histlast": "শেহতীয়া", @@ -843,7 +843,7 @@ "editundo": "পূৰ্ববত কৰক", "diff-empty": "(কোনো পাৰ্থক্য নাই)", "diff-multi-sameuser": "একেজন সদস্যই কৰা ({{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1 মধ্যৱৰ্তী সংশোধন}} দেখুওৱা হোৱা নাই)", - "diff-multi-otherusers": "({{PLURAL:$2|আন এজন সদস্যই|$2জন সদস্যই}} কৰা ({{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1টা মধ্যৱৰ্তী সংশোধন}} দেখুওৱা হোৱা নাই)", + "diff-multi-otherusers": "({{PLURAL:$2|আন এজন সদস্য|$2জন সদস্য}}ই কৰা {{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1টা মধ্যৱৰ্তী সংশোধন}} দেখুওৱা হোৱা নাই)", "diff-multi-manyusers": "({{PLURAL:$2|এজনতকৈ|$2-জনতকৈ}} অধিক সদস্যৰ দ্বাৰা {{PLURAL:$1|এটা মধ্যৱৰ্তী সংশোধন|$1-টা মধ্যৱৰ্তী সংশোধন}} দেখুওৱা হোৱা নাই)", "difference-missing-revision": "{{PLURAL:$2|এটা সংস্কৰণ|$2 সংস্কৰণসমূহৰ}} সংশোধনৰ পাৰ্থক্য ($1) {{PLURAL:$2| পোৱা নগ’ল}}।\n\n\nসাধাৰণতে বিলোপ কৰা এখন পৃষ্ঠাৰ পুৰণা ইতিহাস লিংক অনুসৰণ কৰিলে এনে হয়।\n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} বিলোপন অভিলেখ] চালে অধিক তথ্য পাব।", "searchresults": "অনুসন্ধানৰ ফলাফল", @@ -1449,7 +1449,7 @@ "filehist-comment": "মন্তব্য", "imagelinks": "ফাইল ব্যৱহাৰ", "linkstoimage": "তলত দিয়া {{PLURAL:$1|পৃষ্ঠাটোৱে|$1 পৃষ্ঠাবোৰে}} এই ফাইলটো ব্যৱহাৰ কৰে:", - "linkstoimage-more": "এই ফাইলৰ লগত $1ৰো বেছি {{PLURAL:$1|পৃষ্ঠা সংযোগ|পৃষ্ঠা সংযোগ}} হৈ আছে ।\nতলৰ তালিকাত {{PLURAL:$1|প্ৰথম পৃষ্ঠা সংযোগ|প্ৰথম $1 পৃষ্ঠা সংযোগ}} দেখুওৱা হৈছে ।\nএখন [[Special:WhatLinksHere/$2|সম্পূৰ্ণ তালিকা]]ও পাব ।", + "linkstoimage-more": "এই ফাইলটো $1ৰো বেছি {{PLURAL:$1|পৃষ্ঠাই ব্যৱহাৰ}} কৰে।\nতলৰ তালিকাত সংযোজিত {{PLURAL:$1|প্ৰথম পৃষ্ঠা|প্ৰথম $1টা পৃষ্ঠা}} দেখুওৱা হৈছে।\n[[Special:WhatLinksHere/$2|সম্পূৰ্ণ তালিকা ইয়াত]] পাব। $1 {{PLURAL:$1|page uses|pages use}} this file.\nThe following list shows the {{PLURAL:$1|first page|first $1 pages}} that use this file only.\nA [[Special:WhatLinksHere/$2|full list]] is available.", "nolinkstoimage": "এই ফাইলটো কোনো পৃষ্ঠাই ব্যৱহাৰ কৰা নাই", "morelinkstoimage": "এই ফাইলৰ [[Special:WhatLinksHere/$1|অধিক সংযোগ]] চাওক ।", "linkstoimage-redirect": "$1 (ফাইল পুনৰ্নিৰ্দেশ) $2", @@ -2622,7 +2622,7 @@ "htmlform-cloner-create": "আৰু যোগ কৰক", "htmlform-cloner-delete": "আঁতৰাওক", "logentry-delete-delete": "$3 পৃষ্ঠাটো $1ৰদ্বাৰা {{GENDER:$2|বিলোপ কৰা হ'ল}}", - "logentry-delete-restore": "$1-এ $3 পৃষ্ঠাটো {{GENDER:$2|পুনৰ্সংৰক্ষণ কৰিলে}}", + "logentry-delete-restore": "$1-এ $3 ($4) পৃষ্ঠাটো {{GENDER:$2|পুনৰ্সংৰক্ষণ কৰিলে}}", "logentry-delete-event": "$3: $4 -ত {{PLURAL:$5|এটা লগ ঘটনা|$5 লগ ঘটনাসমূহ}} -ৰ $1 পৰিৱৰ্তন কৰা দৃশ্যমানতা", "logentry-delete-revision": "পৃষ্ঠা $3ত {{PLURAL:$5|এটা সংশোধন|$5 সংশোধনসমূহ}}ৰ দৃশ্যমানতা $1 {{GENDER:$2|য়ে সলালে}}: $4", "logentry-delete-event-legacy": "$3ত ল'গ ঘটনাসমূহৰ দৃশ্যমানতা $1 {{GENDER:$2|ৰদ্বাৰা সলোৱা হ'ল}}", @@ -2645,7 +2645,7 @@ "logentry-move-move_redir": "পুনৰ্নিৰ্দেশৰে পৃষ্ঠা $3ৰ পৰা $4 $1লৈ স্থানান্তৰ কৰা হ’ল", "logentry-move-move_redir-noredirect": "পুনৰ্নিৰ্দেশ নেৰাকৈ এটা পুনৰ্নিৰ্দেশৰ ওপৰেৰে পৃষ্ঠা $3 -ৰ পৰা $4 $1 স্থানান্তৰ কৰা হল", "logentry-patrol-patrol": "পৃষ্ঠা $3 -ৰ $1 চিহ্নিত সংশোধন $4 নিৰীক্ষণ কৰা হ'ল", - "logentry-patrol-patrol-auto": "পৃষ্ঠা $3 -ৰ $1 চিহ্নিত সংশোধন $4 স্বচালিতভাৱে নিৰীক্ষণ কৰা হ'ল", + "logentry-patrol-patrol-auto": "$1 স্বচালিতভাৱে $3 পৃষ্ঠাৰ $4 নং সংশোধন নিৰীক্ষণ কৰা হ'ল বুলি {{GENDER:$2|চিহ্নিত}} কৰিছে", "logentry-newusers-newusers": "ব্যৱহাৰকাৰী একাউণ্ট $1 সৃষ্টি কৰা হ'ল", "logentry-newusers-create": "ব্যৱহাৰকাৰী একাউণ্ট $1 {{GENDER:$2|সৃষ্টি কৰা হ'ল}}", "logentry-newusers-create2": "$1ৰ দ্বাৰা এটা ব্যৱহাৰকাৰী একাউণ্ট $3 {{GENDER:$2|সৃষ্টি কৰা হ'ল}}", diff --git a/languages/i18n/be.json b/languages/i18n/be.json index 50e7aa878a..dce3652f0b 100644 --- a/languages/i18n/be.json +++ b/languages/i18n/be.json @@ -260,7 +260,7 @@ "helppage-top-gethelp": "Даведка", "mainpage": "Галоўная старонка", "mainpage-description": "Першая старонка", - "policy-url": "Project:Арганізацыйная палітыка", + "policy-url": "Project:Спіс правіл і рэкамендацый", "portal": "Супольнасць", "portal-url": "Project:Супольнасць", "privacy": "Палітыка прыватнасці", @@ -2213,7 +2213,7 @@ "delete-legend": "Выдаліць", "historywarning": "Увага: Старонка, якую вы хочаце выдаліць, мае гісторыю з прыблізна $1 {{PLURAL:$1|праўку|праўкі|правак}}:", "historyaction-submit": "Паказаць", - "confirmdeletetext": "Вы збіраецеся выдаліць старонку разам з усёй яе гісторыяй правак.\nПацвердзіце свой намер зрабіць гэта, сваё разуменне наступстваў, і што Вы робіце гэта ў адпаведнасці з [[{{MediaWiki:Policy-url}}|палітыкай (асноўнымі правіламі)]].", + "confirmdeletetext": "Вы збіраецеся выдаліць старонку разам з усёй яе гісторыяй правак.\nПацвердзіце свой намер зрабіць гэта, сваё разуменне наступстваў, і што Вы робіце гэта ў адпаведнасці з [[{{MediaWiki:Policy-url}}|асноўнымі правіламі праекта]].", "actioncomplete": "Завершана аперацыя", "actionfailed": "Памылка дзеяння", "deletedtext": "Старонка \"$1\" была выдалена.\nЗапісы аб нядаўніх выдаленнях гл. ў $2.", diff --git a/languages/i18n/de.json b/languages/i18n/de.json index fb0fc716cc..e468acccd5 100644 --- a/languages/i18n/de.json +++ b/languages/i18n/de.json @@ -98,7 +98,8 @@ "McDutchie", "Johanna Strodt (WMDE)", "Andi-3", - "1233qwer1234qwer4" + "1233qwer1234qwer4", + "MarkusRost" ] }, "tog-underline": "Links unterstreichen:", @@ -324,7 +325,7 @@ "mainpage": "Hauptseite", "mainpage-description": "Hauptseite", "policy-url": "Project:Richtlinien", - "portal": "Gemeinschaftsportal", + "portal": "Gemeinschafts­portal", "portal-url": "Project:Gemeinschaftsportal", "privacy": "Datenschutz", "privacypage": "Project:Datenschutz", diff --git a/languages/i18n/exif/tt-cyrl.json b/languages/i18n/exif/tt-cyrl.json index 21b54cf87b..17bf742a75 100644 --- a/languages/i18n/exif/tt-cyrl.json +++ b/languages/i18n/exif/tt-cyrl.json @@ -3,7 +3,8 @@ "authors": [ "Don Alessandro", "Ильнар", - "Рашат Якупов" + "Рашат Якупов", + "Ерней" ] }, "exif-imagewidth": "Киңлек", @@ -27,8 +28,8 @@ "exif-colorspace": "Төсләр тирәлеге", "exif-componentsconfiguration": "Төсләр төзелешенең конфигурациясе", "exif-compressedbitsperpixel": "Кысылудан соң төснең тирәнлеге", - "exif-pixelxdimension": "Рәсемнең киңлеге", - "exif-pixelydimension": "Рәсемнең биеклеге", + "exif-pixelxdimension": "Сурәт киңлеге", + "exif-pixelydimension": "Сурәт биеклеге", "exif-usercomment": "Өстәмә җавап", "exif-relatedsoundfile": "Тавыш файлы җавабы", "exif-datetimeoriginal": "Чын вакыты", diff --git a/languages/i18n/io.json b/languages/i18n/io.json index 6e94139cc6..ee0890df7e 100644 --- a/languages/i18n/io.json +++ b/languages/i18n/io.json @@ -1363,7 +1363,7 @@ "statistics-pages-desc": "Omna pagini dil Wiki, inkluzite pagini por facar diskuti, ridirektadi, edc.", "statistics-files": "Adkargita arkivi", "statistics-edits": "Quanto di redakti pos ke {{SITENAME}} kreesis", - "statistics-edits-average": "Mezavalora quanto di redakti per pagino", + "statistics-edits-average": "Mezavalora quanto di redakti po pagino", "statistics-users": "Enrejistrita uzeri", "statistics-users-active": "Aktiva uzeri", "statistics-users-active-desc": "Uzeri qui facis ula agado dum la lasta {{PLURAL:$1|dio|$1 dii}}", @@ -1479,6 +1479,7 @@ "cachedspecial-refresh-now": "Vidar la lasta.", "categories": "Kategorii", "categories-submit": "Montrez", + "categoriespagetext": "La sequanta {{PLURAL:$1|kategorio|kategorii}} existas en ca wiki, e povas uzesar, o ne.\nVidez anke [[Special:WantedCategories|dezirata kategorii]].", "categoriesfrom": "Montrez kategorii komencante en:", "deletedcontributions": "Efacita uzero-kontributaji", "deletedcontributions-title": "Efacita uzero-kontributaji", diff --git a/languages/i18n/nl.json b/languages/i18n/nl.json index 7a7589c975..fa5ee6f90d 100644 --- a/languages/i18n/nl.json +++ b/languages/i18n/nl.json @@ -1059,6 +1059,8 @@ "search-interwiki-more": "(meer)", "search-interwiki-more-results": "meer resultaten", "search-relatedarticle": "Gerelateerd", + "search-invalid-sort-order": "De sorteervolgorde $1 is onbekend, de normale sorteervolgorde is in plaats daarvan toegepast. Geldige sorteervolgorden zijn: $2", + "search-unknown-profile": "Het zoekprofiel $1 is onbekend. Het standaard zoekprofiel zal worden toegepast.", "searchrelated": "gerelateerd", "searchall": "alle", "showingresults": "Hieronder {{PLURAL:$1|staat '''1''' resultaat|staan '''$1''' resultaten}} vanaf #'''$2'''.", diff --git a/languages/i18n/nqo.json b/languages/i18n/nqo.json index a011006e82..c11c94a436 100644 --- a/languages/i18n/nqo.json +++ b/languages/i18n/nqo.json @@ -592,6 +592,11 @@ "editingcomment": "(ߛߌ߰ߘߊ߬ ߞߎߘߊ߫) ߡߊߦߟߍ߬ߡߊ߲ ߦߴߌ ߘߐ߫ $1", "editconflict": "ߝߐߢߐ߲߯ߞߐ ߡߊߦߟߍ߬ߡߊ߲߬: $1", "yourtext": "ߌ ߟߊ߫ ߛߓߍߟߌ", + "storedversion": "ߟߢߊ߬ߟߌ߬ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲", + "editingold": "ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ: ߌ ߦߋ߫ ߟߢߊ߬ߟߌ ߕߎ߬ߡߊ ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߋ߬ ߡߊߦߟߍߡߊ߲ ߞߊ߲߬ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫ ߣߌ߲߬. \nߣߴߌ ߞߵߊ߬ ߟߊߞߎ߲߬ߘߎ߫߸ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߝߋ߲߫-ߋ-ߝߋ߲߫ ߞߍߣߍ߲߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߞߐ߫߸ ߓߣߐ߬ ߘߌ߫ ߞߴߏ߬ ߓߍ߯ ߘߐ߫.", + "unicode-support-fail": "ߊ߬ ߛߓߍߣߍ߲߫ ߦߋ߫ ߞߏ߫ ߞߏ߫ ߌ ߟߊ߫ ߛߏ߲߯ߓߊߟߊ߲ ߘߌ߬ߢߍ߬ߣߍ߲߬ ߕߍ߫ ߎߣߌߞߐߘ ߡߊ߬.ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߞߊ߬ߣߌ߲߬ ߣߍ߲߫߸ ߏ߬ ߘߐ߫ ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߎ߲߬ ߡߊ߫ ߟߊߞߎ߲߬ߘߎ߬ ߡߎߣߎ߲߬.", + "yourdiff": "ߓߐߣߍ߲ߢߐ߲߰ߡߊ ߟߎ߬", + "editpage-cannot-use-custom-model": "ߞߐߜߍ ߣߌ߲߬ ߞߣߐߘߐ ߛߎ߮ߦߊ ߕߍߣߊ߬ ߛߐ߲߬ ߠߊ߫ ߡߊߦߟߍ߬ߡߊ߲߫ ߠߊ߫.", "templatesused": "{{PLURAL:$1|ߞߙߊߞߏ|ߞߙߊߞߏ ߟߎ߫}} ߟߎ߫ ߟߊߓߊ߯ߙߊ߫ ߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫", "templatesusedpreview": "{{PLURAL:$1|ߞߙߊߞߏ|ߞߙߊߞߏ ߟߎ߬}} ߟߋ߬ ߟߊߓߊ߯ߙߊ߫ ߣߍ߲߫ ߢߍߦߋߟߌ ߣߌ߲߬ ߘߐ߫", "template-protected": "(ߊ߬ ߡߊߞߊ߲ߞߊ߲ߣߍ߲߫ ߠߋ߬)", @@ -751,6 +756,8 @@ "search-filter-title-prefix-reset": "ߞߐߜߍ ߓߍ߯ ߢߌߣߌ߲߫", "searchresults-title": "ߣߌ߲߬ \"$1\" ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ", "titlematches": "ߞߐߜߍ ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߢߐ߲߰ߡߊ߬ߣߍ߲߫", + "textmatches": "ߞߐߜߍ ߞߟߏߜߍ ߦߋ߫ ߦߋ߲߬", + "notextmatches": "ߞߐߜߍ ߞߟߏߜߍ߫ ߕߴߦߋ߲߬", "prevn": "ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬ {{PLURAL:$1|$1}}", "nextn": "ߟߊߕߎ߲߰ߠߊ {{PLURAL:$1|$1}}", "prev-page": "ߞߐߜߍ ߢߍߕߊ", @@ -895,6 +902,7 @@ "prefs-changeswatchlist": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߦߌ߬ߘߊ߬", "prefs-pageswatchlist": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߣߍ߲ ߠߎ߬", "prefs-tokenwatchlist": "ߖߐߟߐ߲ߞߐ", + "prefs-diffs": "ߓߐߣߍ߲ߢߐ߲߰ߡߊ ߟߎ߬", "prefs-help-prefershttps": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߣߌ߲߬ ߘߴߊ߬ ߝߏ߲߬ߝߏ߲ ߟߴߌ ߟߊ߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߣߊ߬ߕߐ ߞߊ߲߬.", "userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߤߊߞߍ", "userrights-lookup-user": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ߫ ߛߎߥߊ߲ߘߌ߫", @@ -1122,7 +1130,9 @@ "rcfilters-filter-user-experience-level-unregistered-label": "ߕߐ߯ߛߓߍߓߊߟߌ", "rcfilters-filter-user-experience-level-unregistered-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߊ ߡߍ߲ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫.", "rcfilters-filter-user-experience-level-learner-label": "ߞߊ߬ߙߊ߲߬ߠߊ ߟߎ߬", + "rcfilters-filter-user-experience-level-experienced-label": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ߫ ߖߊߙߌ߲ߒߕߋ", "rcfilters-filter-user-experience-level-experienced-description": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ߬ ߕߐ߯ߛߓߍߣߍ߲ ߡߍ߲ ߠߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬ ߅߀߀ ߞߊ߲߬ ߕߟߋ߬ ߃߀ ߓߊ߯ߙߊ߫ ߣߐ.", + "rcfilters-filtergroup-automated": "ߞߍߒߖߘߍߦߋ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲", "rcfilters-filter-bots-label": "ߓߏߕ", "rcfilters-filter-bots-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߍ߲ ߠߎ߬ ߛߌ߲ߘߌߣߍ߲߫ ߞߍߒߖߘߍߦߋ߫ ߖߐ߯ߙߊ߲ ߠߎ߬ ߘߐ߫.", "rcfilters-filter-humans-label": "ߡߐ߱ (ߓߏߕ ߕߍ߫)", @@ -1136,6 +1146,9 @@ "rcfilters-filter-watchlist-watched-description": "ߊ߬ ߡߊߝߊ߬ߟߋ߲߬ ߌ ߟߊ߫ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߞߐߜߍ ߟߎ߬ ߘߐ߫.", "rcfilters-filter-watchlist-watchednew-label": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ߫ ߞߎߘߊ߫ ߓߘߊ߫ ߡߊߦߟߍ߬ߡߊ߲߫", "rcfilters-filter-watchlist-notwatched-label": "ߊ߬ ߕߍ߫ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߘߐ߫", + "rcfilters-filtergroup-watchlistactivity": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߡߛߍ߬ߞߍ߬ߡߛߍߞߍ", + "rcfilters-filter-watchlistactivity-unseen-label": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߦߋߓߊߟߌ ߟߎ߬", + "rcfilters-filter-watchlistactivity-seen-label": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߠߎ߫ ߦߋ߫", "rcfilters-filtergroup-changetype": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߛߎ߯ߦߊ", "rcfilters-filter-pageedits-label": "ߞߐߜߐ ߡߊߦߟߍ߬ߡߊ߲߫", "rcfilters-filter-pageedits-description": "ߞߐߜߍ ߛߌ߲ߘߟߌ", @@ -1234,6 +1247,7 @@ "uploadwarning-text": "ߞߐߕߐ߮ ߘߎ߰ߟߊ߬ߘߐ߫ ߞߊ߲ߛߓߍߟߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߫ ߖߊ߰ߣߌ߲߬߸ ߞߵߊ߬ ߡߊߝߍߣߍ߲߫ ߕߎ߲߯.", "savefile": "ߞߐߕߐ߮ ߟߊߞߎ߲߬ߘߎ߬", "upload-source": "ߞߐߕߐ߮ ߛߎ߲", + "sourcefilename": "ߞߐߕߐ߮ ߕߐ߮ ߛߎ߲:", "sourceurl": "URL ߛߎ߲:", "destfilename": "ߞߐߕߐ߮ ߕߐ߮ ߞߎ߲߬ߕߋߟߋ߲:", "upload-maxfilesize": "ߞߐߕߐ߮ ߢߊ߲ߞߊ߲ ߞߐߘߊ߲: $1", @@ -1252,6 +1266,7 @@ "upload-dialog-button-upload": "ߟߊ߬ߦߟߍ߬ߟߌ", "upload-form-label-infoform-title": "ߝߊߙߊ߲ߝߊ߯ߛߟߌ", "upload-form-label-infoform-name": "ߕߐ߮", + "upload-form-label-infoform-description": "ߞߊ߲߬ߛߓߍߟߌ", "upload-form-label-usage-title": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ", "upload-form-label-usage-filename": "ߞߐߕߐ߮ ߕߐ߮", "upload-form-label-own-work": "ߒ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߓߊ߯ߙߊ ߟߋ߬", @@ -1287,6 +1302,8 @@ "license-header": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߴߌ ߘߐ߫", "nolicense": "ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬", "listfiles-delete": "ߊ߬ ߖߏ߬ߛߌ߬", + "listfiles_search_for": "ߡߍ߲ߕߊߦߋߕߊ ߕߐ߮ ߢߌߣߌ߲ߠߌ߲:", + "listfiles-userdoesnotexist": "ߟߊ߬ߓߊ߰ߙߊ߬ ߖߊߕߋߘߊ \"$1\" ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲߫ ߕߍ߫.", "imgfile": "ߞߐߕߐ߮", "listfiles": "ߞߐߕߐ߮ ߛߙߍߘߍ", "listfiles_thumb": "ߞߝߊ߬ߟߋ߲ߛߋ߲", diff --git a/languages/i18n/pl.json b/languages/i18n/pl.json index 522fbb7541..f5050381e4 100644 --- a/languages/i18n/pl.json +++ b/languages/i18n/pl.json @@ -1066,6 +1066,7 @@ "search-interwiki-more-results": "Więcej wyników", "search-relatedarticle": "Pokrewne", "search-invalid-sort-order": "Kolejność sortowania $1 jest nierozpoznawana. Zastosowane zostanie domyślne sortowanie. Właściwymi kolejnościami są: $2", + "search-unknown-profile": "Profil wyszukiwania $1 jest nierozpoznawany. Zostanie zastosowany domyślny profil.", "searchrelated": "pokrewne", "searchall": "wszystkie", "showingresults": "Poniżej znajduje się lista {{PLURAL:$1|z '''1''' wynikiem|'''$1''' wyników}}, rozpoczynając od wyniku numer '''$2'''.", diff --git a/resources/Resources.php b/resources/Resources.php index 0c2df6b753..9a7b9e8362 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1740,6 +1740,7 @@ return [ /* MediaWiki Special pages */ 'mediawiki.rcfilters.filters.base.styles' => [ + 'targets' => [ 'desktop', 'mobile' ], 'skinStyles' => [ 'default' => 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less', ], @@ -1753,6 +1754,7 @@ return [ ], ], 'mediawiki.rcfilters.filters.dm' => [ + 'targets' => [ 'desktop', 'mobile' ], 'localBasePath' => "$IP/resources/src/mediawiki.rcfilters", 'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.rcfilters", 'packageFiles' => [ @@ -1783,6 +1785,7 @@ return [ ], ], 'mediawiki.rcfilters.filters.ui' => [ + 'targets' => [ 'desktop', 'mobile' ], 'localBasePath' => "$IP/resources/src/mediawiki.rcfilters", 'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.rcfilters", 'packageFiles' => [ diff --git a/resources/src/mediawiki.special.apisandbox/apisandbox.js b/resources/src/mediawiki.special.apisandbox/apisandbox.js index 395fb8bd82..c2c59609e9 100644 --- a/resources/src/mediawiki.special.apisandbox/apisandbox.js +++ b/resources/src/mediawiki.special.apisandbox/apisandbox.js @@ -289,8 +289,8 @@ // Can't, sorry. }, apiCheckValid: function () { - var ok = this.getValue() !== null || suppressErrors; - this.setIcon( ok ? null : 'alert' ); + var ok = this.getValue() !== null && this.getValue() !== undefined || suppressErrors; + this.info.setIcon( ok ? null : 'alert' ); this.setTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); return $.Deferred().resolve( ok ).promise(); } diff --git a/resources/src/startup/mediawiki.js b/resources/src/startup/mediawiki.js index dbb32e5452..b0355b0019 100644 --- a/resources/src/startup/mediawiki.js +++ b/resources/src/startup/mediawiki.js @@ -432,8 +432,7 @@ * * @property {mw.Map} config */ - // Dummy placeholder later assigned in ResourceLoaderStartUpModule - config: null, + config: new Map( $VARS.wgLegacyJavaScriptGlobals ), /** * Empty object for third-party libraries, for cases where you don't @@ -2180,24 +2179,18 @@ } if ( + !$VARS.storeEnabled || + // Disabled because localStorage quotas are tight and (in Firefox's case) // shared by multiple origins. // See T66721, and . - /Firefox/.test( navigator.userAgent ) || - - // Disabled by configuration. - !mw.config.get( 'wgResourceLoaderStorageEnabled' ) + /Firefox/.test( navigator.userAgent ) ) { // Clear any previous store to free up space. (T66721) this.clear(); this.enabled = false; return; } - if ( mw.config.get( 'debug' ) ) { - // Disable module store in debug mode - this.enabled = false; - return; - } try { // This a string we stored, or `null` if the key does not (yet) exist. diff --git a/resources/src/startup/startup.js b/resources/src/startup/startup.js index da048ffdff..06c6737b26 100644 --- a/resources/src/startup/startup.js +++ b/resources/src/startup/startup.js @@ -113,7 +113,6 @@ if ( !isCompatible( navigator.userAgent ) ) { */ ( function () { /* global mw */ - mw.config = new mw.Map( $VARS.wgLegacyJavaScriptGlobals ); $CODE.registrations(); diff --git a/tests/parser/ParserTestRunner.php b/tests/parser/ParserTestRunner.php index ba850276f2..e3c20a2cee 100644 --- a/tests/parser/ParserTestRunner.php +++ b/tests/parser/ParserTestRunner.php @@ -1302,7 +1302,7 @@ class ParserTestRunner { public function setupDatabase( $nextTeardown = null ) { global $wgDBprefix; - $this->db = wfGetDB( DB_MASTER ); + $this->db = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_MASTER ); $dbType = $this->db->getType(); if ( $dbType == 'oracle' ) { diff --git a/tests/phpunit/MediaWikiCoversValidator.php b/tests/phpunit/MediaWikiCoversValidator.php index a79a139c8a..ce3f2e281d 100644 --- a/tests/phpunit/MediaWikiCoversValidator.php +++ b/tests/phpunit/MediaWikiCoversValidator.php @@ -33,7 +33,7 @@ trait MediaWikiCoversValidator { */ public function testValidCovers() { $methods = get_class_methods( $this ); - $class = get_class( $this ); + $class = static::class; $bad = ''; foreach ( $methods as $method ) { if ( strpos( $method, 'test' ) === 0 ) { diff --git a/tests/phpunit/data/media/zip-comment-overflow.png b/tests/phpunit/data/media/zip-comment-overflow.png new file mode 100644 index 0000000000..710831feb6 Binary files /dev/null and b/tests/phpunit/data/media/zip-comment-overflow.png differ diff --git a/tests/phpunit/data/media/zip-kind-of-valid-2.png b/tests/phpunit/data/media/zip-kind-of-valid-2.png new file mode 100644 index 0000000000..c0e7ff6129 Binary files /dev/null and b/tests/phpunit/data/media/zip-kind-of-valid-2.png differ diff --git a/tests/phpunit/data/media/zip-kind-of-valid.png b/tests/phpunit/data/media/zip-kind-of-valid.png new file mode 100644 index 0000000000..1121af41a9 Binary files /dev/null and b/tests/phpunit/data/media/zip-kind-of-valid.png differ diff --git a/tests/phpunit/data/media/zip-sig-near-end.png b/tests/phpunit/data/media/zip-sig-near-end.png new file mode 100644 index 0000000000..29f3684baf Binary files /dev/null and b/tests/phpunit/data/media/zip-sig-near-end.png differ diff --git a/tests/phpunit/includes/Revision/RevisionStoreTest.php b/tests/phpunit/includes/Revision/RevisionStoreTest.php index 0648bfce6e..83872e3a37 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreTest.php +++ b/tests/phpunit/includes/Revision/RevisionStoreTest.php @@ -12,6 +12,8 @@ use MediaWiki\Revision\RevisionStore; use MediaWiki\Revision\SlotRoleRegistry; use MediaWiki\Revision\SlotRecord; use MediaWiki\Storage\SqlBlobStore; +use Wikimedia\Rdbms\ILoadBalancer; +use Wikimedia\Rdbms\MaintainableDBConnRef; use MediaWikiTestCase; use MWException; use Title; @@ -77,6 +79,17 @@ class RevisionStoreTest extends MediaWikiTestCase { ->disableOriginalConstructor()->getMock(); } + /** + * @param ILoadBalancer $mockLoadBalancer + * @param Database $db + * @return callable + */ + private function getMockDBConnRefCallback( ILoadBalancer $mockLoadBalancer, IDatabase $db ) { + return function ( $i, $g, $domain, $flg ) use ( $mockLoadBalancer, $db ) { + return new MaintainableDBConnRef( $mockLoadBalancer, $db, $i ); + }; + } + /** * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore */ @@ -158,10 +171,14 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); $db = $this->getMockDatabase(); - // Title calls wfGetDB() which uses a regular Connection + // RevisionStore uses getConnectionRef + $mockLoadBalancer->expects( $this->any() ) + ->method( 'getConnectionRef' ) + ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) ); + // Title calls wfGetDB() which uses getMaintenanceConnectionRef $mockLoadBalancer->expects( $this->atLeastOnce() ) - ->method( 'getConnection' ) - ->willReturn( $db ); + ->method( 'getMaintenanceConnectionRef' ) + ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) ); // First call to Title::newFromID, faking no result (db lag?) $db->expects( $this->at( 0 ) ) @@ -192,15 +209,15 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); $db = $this->getMockDatabase(); - // Title calls wfGetDB() which uses a regular Connection + // Title calls wfGetDB() which uses getMaintenanceConnectionRef // Assert that the first call uses a REPLICA and the second falls back to master - $mockLoadBalancer->expects( $this->exactly( 2 ) ) - ->method( 'getConnection' ) - ->willReturn( $db ); - // RevisionStore getTitle uses a ConnectionRef $mockLoadBalancer->expects( $this->atLeastOnce() ) ->method( 'getConnectionRef' ) - ->willReturn( $db ); + ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) ); + // Title calls wfGetDB() which uses getMaintenanceConnectionRef + $mockLoadBalancer->expects( $this->exactly( 2 ) ) + ->method( 'getMaintenanceConnectionRef' ) + ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) ); // First call to Title::newFromID, faking no result (db lag?) $db->expects( $this->at( 0 ) ) @@ -251,14 +268,14 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); $db = $this->getMockDatabase(); - // Title calls wfGetDB() which uses a regular Connection - $mockLoadBalancer->expects( $this->atLeastOnce() ) - ->method( 'getConnection' ) - ->willReturn( $db ); - // RevisionStore getTitle uses a ConnectionRef $mockLoadBalancer->expects( $this->atLeastOnce() ) ->method( 'getConnectionRef' ) - ->willReturn( $db ); + ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) ); + // Title calls wfGetDB() which uses getMaintenanceConnectionRef + // RevisionStore getTitle uses getMaintenanceConnectionRef + $mockLoadBalancer->expects( $this->atLeastOnce() ) + ->method( 'getMaintenanceConnectionRef' ) + ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) ); // First call to Title::newFromID, faking no result (db lag?) $db->expects( $this->at( 0 ) ) @@ -299,15 +316,15 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); $db = $this->getMockDatabase(); - // Title calls wfGetDB() which uses a regular Connection // Assert that the first call uses a REPLICA and the second falls back to master - $mockLoadBalancer->expects( $this->exactly( 2 ) ) - ->method( 'getConnection' ) - ->willReturn( $db ); - // RevisionStore getTitle uses a ConnectionRef + // RevisionStore uses getMaintenanceConnectionRef $mockLoadBalancer->expects( $this->atLeastOnce() ) ->method( 'getConnectionRef' ) - ->willReturn( $db ); + ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) ); + // Title calls wfGetDB() which uses getMaintenanceConnectionRef + $mockLoadBalancer->expects( $this->exactly( 2 ) ) + ->method( 'getMaintenanceConnectionRef' ) + ->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) ); // First call to Title::newFromID, faking no result (db lag?) $db->expects( $this->at( 0 ) ) @@ -368,12 +385,14 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); $db = $this->getMockDatabase(); - // Title calls wfGetDB() which uses a regular Connection + // Title calls wfGetDB() which uses getMaintenanceConnectionRef // Assert that the first call uses a REPLICA and the second falls back to master // RevisionStore getTitle uses getConnectionRef - // Title::newFromID uses getConnection - foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) { + // Title::newFromID uses getMaintenanceConnectionRef + foreach ( [ + 'getConnectionRef', 'getMaintenanceConnectionRef' + ] as $method ) { $mockLoadBalancer->expects( $this->exactly( 2 ) ) ->method( $method ) ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) { diff --git a/tests/phpunit/includes/block/BlockManagerTest.php b/tests/phpunit/includes/block/BlockManagerTest.php index b8f60c4e82..f42777c503 100644 --- a/tests/phpunit/includes/block/BlockManagerTest.php +++ b/tests/phpunit/includes/block/BlockManagerTest.php @@ -160,27 +160,6 @@ class BlockManagerTest extends MediaWikiTestCase { ]; } - /** - * @covers ::isLocallyBlockedProxy - */ - public function testIsLocallyBlockedProxyDeprecated() { - $proxy = '1.2.3.4'; - - $this->hideDeprecated( - 'IP addresses in the keys of $wgProxyList (found the following IP ' . - 'addresses in keys: ' . $proxy . ', please move them to values)' - ); - - $blockManager = TestingAccessWrapper::newFromObject( - $this->getBlockManager( [ - 'wgProxyList' => [ $proxy => 'test' ] - ] ) - ); - - $ip = '1.2.3.4'; - $this->assertTrue( $blockManager->isLocallyBlockedProxy( $ip ) ); - } - /** * @dataProvider provideIsDnsBlacklisted * @covers ::isDnsBlacklisted diff --git a/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php b/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php index 0f23b8cdc1..51ad915d44 100644 --- a/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php +++ b/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php @@ -163,4 +163,40 @@ class MimeAnalyzerTest extends PHPUnit\Framework\TestCase { ]; } + function providePngZipConfusion() { + return [ + [ + 'An invalid ZIP file due to the signature being too close to the ' . + 'end to accomodate an EOCDR', + 'zip-sig-near-end.png', + 'image/png', + ], + [ + 'An invalid ZIP file due to the comment length running beyond the ' . + 'end of the file', + 'zip-comment-overflow.png', + 'image/png', + ], + [ + 'A ZIP file similar to the above, but without either of those two ' . + 'problems. Not a valid ZIP file, but it passes MimeAnalyzer\'s ' . + 'definition of a ZIP file. This is mostly a sanity check of the ' . + 'above two tests.', + 'zip-kind-of-valid.png', + 'application/zip', + ], + [ + 'As above with non-zero comment length', + 'zip-kind-of-valid-2.png', + 'application/zip', + ], + ]; + } + + /** @dataProvider providePngZipConfusion */ + function testPngZipConfusion( $description, $fileName, $expectedType ) { + $file = __DIR__ . '/../../../data/media/' . $fileName; + $actualType = $this->doGuessMimeType( [ $file, 'png' ] ); + $this->assertEquals( $expectedType, $actualType, $description ); + } } diff --git a/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php index 522af43662..4a56fc580f 100644 --- a/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php +++ b/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php @@ -113,6 +113,9 @@ class BagOStuffTest extends MediaWikiTestCase { * @covers MediumSpecificBagOStuff::changeTTL */ public function testChangeTTL() { + $now = 1563892142; + $this->cache->setMockTime( $now ); + $key = $this->cache->makeKey( self::TEST_KEY ); $value = 'meow'; @@ -126,7 +129,7 @@ class BagOStuffTest extends MediaWikiTestCase { $this->assertFalse( $this->cache->changeTTL( $key, 15 ) ); $this->cache->add( $key, $value, 5 ); - $this->assertTrue( $this->cache->changeTTL( $key, time() - 3600 ) ); + $this->assertTrue( $this->cache->changeTTL( $key, $now - 3600 ) ); $this->assertFalse( $this->cache->get( $key ) ); } @@ -134,6 +137,9 @@ class BagOStuffTest extends MediaWikiTestCase { * @covers MediumSpecificBagOStuff::changeTTLMulti */ public function testChangeTTLMulti() { + $now = 1563892142; + $this->cache->setMockTime( $now ); + $key1 = $this->cache->makeKey( 'test-key1' ); $key2 = $this->cache->makeKey( 'test-key2' ); $key3 = $this->cache->makeKey( 'test-key3' ); @@ -166,7 +172,7 @@ class BagOStuffTest extends MediaWikiTestCase { $this->assertEquals( 2, $this->cache->get( $key2 ) ); $this->assertEquals( 3, $this->cache->get( $key3 ) ); - $ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3 ], time() + 86400 ); + $ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3 ], $now + 86400 ); $this->assertTrue( $ok, "Expiry set for all keys" ); $ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3, $key4 ], 300 ); @@ -210,17 +216,28 @@ class BagOStuffTest extends MediaWikiTestCase { * @covers MediumSpecificBagOStuff::getWithSetCallback */ public function testGetWithSetCallback() { + $now = 1563892142; + $this->cache->setMockTime( $now ); $key = $this->cache->makeKey( self::TEST_KEY ); + + $this->assertFalse( $this->cache->get( $key ), "No value" ); + $value = $this->cache->getWithSetCallback( $key, 30, - function () { + function ( &$ttl ) { + $ttl = 10; + return 'hello kitty'; } ); $this->assertEquals( 'hello kitty', $value ); - $this->assertEquals( $value, $this->cache->get( $key ) ); + $this->assertEquals( $value, $this->cache->get( $key ), "Value set" ); + + $now += 11; + + $this->assertFalse( $this->cache->get( $key ), "Value expired" ); } /** diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php index 482ab4b5f5..a775dd70e3 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php @@ -568,62 +568,74 @@ class DatabaseTest extends PHPUnit\Framework\TestCase { public function testFlagSetting() { $db = $this->db; $origTrx = $db->getFlag( DBO_TRX ); - $origSsl = $db->getFlag( DBO_SSL ); + $origNoBuffer = $db->getFlag( DBO_NOBUFFER ); $origTrx ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR ) : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR ); $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) ); - $origSsl - ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR ) - : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR ); - $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) ); + $origNoBuffer + ? $db->clearFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR ) + : $db->setFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR ); + $this->assertEquals( !$origNoBuffer, $db->getFlag( DBO_NOBUFFER ) ); $db->restoreFlags( $db::RESTORE_INITIAL ); $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) ); - $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) ); + $this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) ); $origTrx ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR ) : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR ); - $origSsl - ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR ) - : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR ); + $origNoBuffer + ? $db->clearFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR ) + : $db->setFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR ); $db->restoreFlags(); - $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) ); + $this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) ); $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) ); $db->restoreFlags(); - $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) ); + $this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) ); $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) ); } + public function provideImmutableDBOFlags() { + return [ + [ Database::DBO_IGNORE ], + [ Database::DBO_DEFAULT ], + [ Database::DBO_PERSISTENT ] + ]; + } + /** - * @expectedException UnexpectedValueException + * @expectedException DBUnexpectedError * @covers Wikimedia\Rdbms\Database::setFlag + * @dataProvider provideImmutableDBOFlags + * @param int $flag */ - public function testDBOIgnoreSet() { + public function testDBOCannotSet( $flag ) { $db = $this->getMockBuilder( DatabaseMysqli::class ) ->disableOriginalConstructor() ->setMethods( null ) ->getMock(); - $db->setFlag( Database::DBO_IGNORE ); + $db->setFlag( $flag ); } /** - * @expectedException UnexpectedValueException + * @expectedException DBUnexpectedError * @covers Wikimedia\Rdbms\Database::clearFlag + * @dataProvider provideImmutableDBOFlags + * @param int $flag */ - public function testDBOIgnoreClear() { + public function testDBOCannotClear( $flag ) { $db = $this->getMockBuilder( DatabaseMysqli::class ) ->disableOriginalConstructor() ->setMethods( null ) ->getMock(); - $db->clearFlag( Database::DBO_IGNORE ); + $db->clearFlag( $flag ); } /** diff --git a/tests/phpunit/includes/logging/BlockLogFormatterTest.php b/tests/phpunit/includes/logging/BlockLogFormatterTest.php index b6f8f9cc37..71cf5588f2 100644 --- a/tests/phpunit/includes/logging/BlockLogFormatterTest.php +++ b/tests/phpunit/includes/logging/BlockLogFormatterTest.php @@ -331,6 +331,81 @@ class BlockLogFormatterTest extends LogFormatterTestCase { * @dataProvider provideSuppressBlockLogDatabaseRows */ public function testSuppressBlockLogDatabaseRows( $row, $extra ) { + $this->setMwGlobals( + 'wgGroupPermissions', + [ + 'oversight' => [ + 'viewsuppressed' => true, + 'suppressionlog' => true, + ], + ] + ); + $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] ); + } + + /** + * Provide different rows from the logging table to test + * for backward compatibility. + * Do not change the existing data, just add a new database row + */ + public static function provideSuppressBlockLogDatabaseRowsNonPrivileged() { + return [ + // Current log format + [ + [ + 'type' => 'suppress', + 'action' => 'block', + 'comment' => 'Block comment', + 'user' => 0, + 'user_text' => 'Sysop', + 'namespace' => NS_USER, + 'title' => 'Logtestuser', + 'params' => [ + '5::duration' => 'infinite', + '6::flags' => 'anononly', + ], + ], + [ + 'text' => '(username removed) (log details removed)', + 'api' => [ + 'duration' => 'infinite', + 'flags' => [ 'anononly' ], + ], + ], + ], + + // legacy log + [ + [ + 'type' => 'suppress', + 'action' => 'block', + 'comment' => 'Block comment', + 'user' => 0, + 'user_text' => 'Sysop', + 'namespace' => NS_USER, + 'title' => 'Logtestuser', + 'params' => [ + 'infinite', + 'anononly', + ], + ], + [ + 'legacy' => true, + 'text' => '(username removed) (log details removed)', + 'api' => [ + 'duration' => 'infinite', + 'flags' => [ 'anononly' ], + ], + ], + ], + ]; + } + + /** + * @dataProvider provideSuppressBlockLogDatabaseRowsNonPrivileged + */ + public function testSuppressBlockLogDatabaseRowsNonPrivileged( $row, $extra ) { + $this->user = $this->getTestUser()->getUser(); $this->doTestLogFormatter( $row, $extra ); } @@ -398,6 +473,81 @@ class BlockLogFormatterTest extends LogFormatterTestCase { * @dataProvider provideSuppressReblockLogDatabaseRows */ public function testSuppressReblockLogDatabaseRows( $row, $extra ) { + $this->setMwGlobals( + 'wgGroupPermissions', + [ + 'oversight' => [ + 'viewsuppressed' => true, + 'suppressionlog' => true, + ], + ] + ); + $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] ); + } + + /** + * Provide different rows from the logging table to test + * for backward compatibility. + * Do not change the existing data, just add a new database row + */ + public static function provideSuppressReblockLogDatabaseRowsNonPrivileged() { + return [ + // Current log format + [ + [ + 'type' => 'suppress', + 'action' => 'reblock', + 'comment' => 'Block comment', + 'user' => 0, + 'user_text' => 'Sysop', + 'namespace' => NS_USER, + 'title' => 'Logtestuser', + 'params' => [ + '5::duration' => 'infinite', + '6::flags' => 'anononly', + ], + ], + [ + 'text' => '(username removed) (log details removed)', + 'api' => [ + 'duration' => 'infinite', + 'flags' => [ 'anononly' ], + ], + ], + ], + + // Legacy format + [ + [ + 'type' => 'suppress', + 'action' => 'reblock', + 'comment' => 'Block comment', + 'user' => 0, + 'user_text' => 'Sysop', + 'namespace' => NS_USER, + 'title' => 'Logtestuser', + 'params' => [ + 'infinite', + 'anononly', + ], + ], + [ + 'legacy' => true, + 'text' => '(username removed) (log details removed)', + 'api' => [ + 'duration' => 'infinite', + 'flags' => [ 'anononly' ], + ], + ], + ], + ]; + } + + /** + * @dataProvider provideSuppressReblockLogDatabaseRowsNonPrivileged + */ + public function testSuppressReblockLogDatabaseRowsNonPrivileged( $row, $extra ) { + $this->user = $this->getTestUser()->getUser(); $this->doTestLogFormatter( $row, $extra ); } diff --git a/tests/phpunit/includes/logging/DeleteLogFormatterTest.php b/tests/phpunit/includes/logging/DeleteLogFormatterTest.php index 6648c31c25..f1d58fdf09 100644 --- a/tests/phpunit/includes/logging/DeleteLogFormatterTest.php +++ b/tests/phpunit/includes/logging/DeleteLogFormatterTest.php @@ -409,6 +409,109 @@ class DeleteLogFormatterTest extends LogFormatterTestCase { * @dataProvider provideSuppressRevisionLogDatabaseRows */ public function testSuppressRevisionLogDatabaseRows( $row, $extra ) { + $this->setMwGlobals( + 'wgGroupPermissions', + [ + 'oversight' => [ + 'viewsuppressed' => true, + 'suppressionlog' => true, + ], + ] + ); + $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] ); + } + + /** + * Provide different rows from the logging table to test + * for backward compatibility. + * Do not change the existing data, just add a new database row + */ + public static function provideSuppressRevisionLogDatabaseRowsNonPrivileged() { + return [ + // Current format + [ + [ + 'type' => 'suppress', + 'action' => 'revision', + 'comment' => 'Suppress comment', + 'namespace' => NS_MAIN, + 'title' => 'Page', + 'params' => [ + '4::type' => 'archive', + '5::ids' => [ '1', '3', '4' ], + '6::ofield' => '1', + '7::nfield' => '10', + ], + ], + [ + 'text' => '(username removed) (log details removed)', + 'api' => [ + 'type' => 'archive', + 'ids' => [ '1', '3', '4' ], + 'old' => [ + 'bitmask' => 1, + 'content' => true, + 'comment' => false, + 'user' => false, + 'restricted' => false, + ], + 'new' => [ + 'bitmask' => 10, + 'content' => false, + 'comment' => true, + 'user' => false, + 'restricted' => true, + ], + ], + ], + ], + + // Legacy format + [ + [ + 'type' => 'suppress', + 'action' => 'revision', + 'comment' => 'Suppress comment', + 'namespace' => NS_MAIN, + 'title' => 'Page', + 'params' => [ + 'archive', + '1,3,4', + 'ofield=1', + 'nfield=10', + ], + ], + [ + 'legacy' => true, + 'text' => '(username removed) (log details removed)', + 'api' => [ + 'type' => 'archive', + 'ids' => [ '1', '3', '4' ], + 'old' => [ + 'bitmask' => 1, + 'content' => true, + 'comment' => false, + 'user' => false, + 'restricted' => false, + ], + 'new' => [ + 'bitmask' => 10, + 'content' => false, + 'comment' => true, + 'user' => false, + 'restricted' => true, + ], + ], + ], + ], + ]; + } + + /** + * @dataProvider provideSuppressRevisionLogDatabaseRowsNonPrivileged + */ + public function testSuppressRevisionLogDatabaseRowsNonPrivileged( $row, $extra ) { + $this->user = $this->getTestUser()->getUser(); $this->doTestLogFormatter( $row, $extra ); } @@ -523,6 +626,107 @@ class DeleteLogFormatterTest extends LogFormatterTestCase { * @dataProvider provideSuppressEventLogDatabaseRows */ public function testSuppressEventLogDatabaseRows( $row, $extra ) { + $this->setMwGlobals( + 'wgGroupPermissions', + [ + 'oversight' => [ + 'viewsuppressed' => true, + 'suppressionlog' => true, + ], + ] + ); + $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] ); + } + + /** + * Provide different rows from the logging table to test + * for backward compatibility. + * Do not change the existing data, just add a new database row + */ + public static function provideSuppressEventLogDatabaseRowsNonPrivileged() { + return [ + // Current format + [ + [ + 'type' => 'suppress', + 'action' => 'event', + 'comment' => 'Suppress comment', + 'namespace' => NS_MAIN, + 'title' => 'Page', + 'params' => [ + '4::ids' => [ '1', '3', '4' ], + '5::ofield' => '1', + '6::nfield' => '10', + ], + ], + [ + 'text' => '(username removed) (log details removed)', + 'api' => [ + 'type' => 'logging', + 'ids' => [ '1', '3', '4' ], + 'old' => [ + 'bitmask' => 1, + 'content' => true, + 'comment' => false, + 'user' => false, + 'restricted' => false, + ], + 'new' => [ + 'bitmask' => 10, + 'content' => false, + 'comment' => true, + 'user' => false, + 'restricted' => true, + ], + ], + ], + ], + + // Legacy format + [ + [ + 'type' => 'suppress', + 'action' => 'event', + 'comment' => 'Suppress comment', + 'namespace' => NS_MAIN, + 'title' => 'Page', + 'params' => [ + '1,3,4', + 'ofield=1', + 'nfield=10', + ], + ], + [ + 'legacy' => true, + 'text' => '(username removed) (log details removed)', + 'api' => [ + 'type' => 'logging', + 'ids' => [ '1', '3', '4' ], + 'old' => [ + 'bitmask' => 1, + 'content' => true, + 'comment' => false, + 'user' => false, + 'restricted' => false, + ], + 'new' => [ + 'bitmask' => 10, + 'content' => false, + 'comment' => true, + 'user' => false, + 'restricted' => true, + ], + ], + ], + ], + ]; + } + + /** + * @dataProvider provideSuppressEventLogDatabaseRowsNonPrivileged + */ + public function testSuppressEventLogDatabaseRowsNonPrivileged( $row, $extra ) { + $this->user = $this->getTestUser()->getUser(); $this->doTestLogFormatter( $row, $extra ); } @@ -572,6 +776,65 @@ class DeleteLogFormatterTest extends LogFormatterTestCase { * @dataProvider provideSuppressDeleteLogDatabaseRows */ public function testSuppressDeleteLogDatabaseRows( $row, $extra ) { + $this->setMwGlobals( + 'wgGroupPermissions', + [ + 'oversight' => [ + 'viewsuppressed' => true, + 'suppressionlog' => true, + ], + ] + ); + $this->doTestLogFormatter( $row, $extra, [ 'oversight' ] ); + } + + /** + * Provide different rows from the logging table to test + * for backward compatibility. + * Do not change the existing data, just add a new database row + */ + public static function provideSuppressDeleteLogDatabaseRowsNonPrivileged() { + return [ + // Current format + [ + [ + 'type' => 'suppress', + 'action' => 'delete', + 'comment' => 'delete comment', + 'namespace' => NS_MAIN, + 'title' => 'Page', + 'params' => [], + ], + [ + 'text' => '(username removed) (log details removed)', + 'api' => [], + ], + ], + + // Legacy format + [ + [ + 'type' => 'suppress', + 'action' => 'delete', + 'comment' => 'delete comment', + 'namespace' => NS_MAIN, + 'title' => 'Page', + 'params' => [], + ], + [ + 'legacy' => true, + 'text' => '(username removed) (log details removed)', + 'api' => [], + ], + ], + ]; + } + + /** + * @dataProvider provideSuppressDeleteLogDatabaseRowsNonPrivileged + */ + public function testSuppressDeleteLogDatabaseRowsNonPrivileged( $row, $extra ) { + $this->user = $this->getTestUser()->getUser(); $this->doTestLogFormatter( $row, $extra ); } } diff --git a/tests/phpunit/includes/logging/LogFormatterTestCase.php b/tests/phpunit/includes/logging/LogFormatterTestCase.php index fc2ab916cb..a24065ec50 100644 --- a/tests/phpunit/includes/logging/LogFormatterTestCase.php +++ b/tests/phpunit/includes/logging/LogFormatterTestCase.php @@ -6,11 +6,15 @@ use MediaWiki\Linker\LinkTarget; */ abstract class LogFormatterTestCase extends MediaWikiLangTestCase { - public function doTestLogFormatter( $row, $extra ) { + public function doTestLogFormatter( $row, $extra, $userGroups = [] ) { RequestContext::resetMain(); $row = $this->expandDatabaseRow( $row, $this->isLegacy( $extra ) ); + $context = new RequestContext(); + $context->setUser( $this->getTestUser( $userGroups )->getUser() ); + $formatter = LogFormatter::newFromRow( $row ); + $formatter->setContext( $context ); $this->assertEquals( $extra['text'], diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php deleted file mode 100644 index c84efa1640..0000000000 --- a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php +++ /dev/null @@ -1,201 +0,0 @@ -assertMetadata( $infile, $expected ); - } - - /** - * @dataProvider provideSvgFilesWithXMLMetadata - */ - public function testGetXMLMetadata( $infile, $expected ) { - $r = new XMLReader(); - $this->assertMetadata( $infile, $expected ); - } - - /** - * @dataProvider provideSvgUnits - */ - public function testScaleSVGUnit( $inUnit, $expected ) { - $this->assertEquals( - $expected, - SVGReader::scaleSVGUnit( $inUnit ), - 'SVG unit conversion and scaling failure' - ); - } - - function assertMetadata( $infile, $expected ) { - try { - $data = SVGMetadataExtractor::getMetadata( $infile ); - $this->assertEquals( $expected, $data, 'SVG metadata extraction test' ); - } catch ( MWException $e ) { - if ( $expected === false ) { - $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' ); - } else { - throw $e; - } - } - } - - public static function provideSvgFiles() { - $base = __DIR__ . '/../../data/media'; - - return [ - [ - "$base/Wikimedia-logo.svg", - [ - 'width' => 1024, - 'height' => 1024, - 'originalWidth' => '1024', - 'originalHeight' => '1024', - 'translations' => [], - ] - ], - [ - "$base/QA_icon.svg", - [ - 'width' => 60, - 'height' => 60, - 'originalWidth' => '60', - 'originalHeight' => '60', - 'translations' => [], - ] - ], - [ - "$base/Gtk-media-play-ltr.svg", - [ - 'width' => 60, - 'height' => 60, - 'originalWidth' => '60.0000000', - 'originalHeight' => '60.0000000', - 'translations' => [], - ] - ], - [ - "$base/Toll_Texas_1.svg", - // This file triggered T33719, needs entity expansion in the xmlns checks - [ - 'width' => 385, - 'height' => 385, - 'originalWidth' => '385', - 'originalHeight' => '385.0004883', - 'translations' => [], - ] - ], - [ - "$base/Tux.svg", - [ - 'width' => 512, - 'height' => 594, - 'originalWidth' => '100%', - 'originalHeight' => '100%', - 'title' => 'Tux', - 'translations' => [], - 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg', - ] - ], - [ - "$base/Speech_bubbles.svg", - [ - 'width' => 627, - 'height' => 461, - 'originalWidth' => '17.7cm', - 'originalHeight' => '13cm', - 'translations' => [ - 'de' => SVGReader::LANG_FULL_MATCH, - 'fr' => SVGReader::LANG_FULL_MATCH, - 'nl' => SVGReader::LANG_FULL_MATCH, - 'tlh-ca' => SVGReader::LANG_FULL_MATCH, - 'tlh' => SVGReader::LANG_PREFIX_MATCH - ], - ] - ], - [ - "$base/Soccer_ball_animated.svg", - [ - 'width' => 150, - 'height' => 150, - 'originalWidth' => '150', - 'originalHeight' => '150', - 'animated' => true, - 'translations' => [] - ], - ], - [ - "$base/comma_separated_viewbox.svg", - [ - 'width' => 512, - 'height' => 594, - 'originalWidth' => '100%', - 'originalHeight' => '100%', - 'translations' => [] - ], - ], - ]; - } - - public static function provideSvgFilesWithXMLMetadata() { - $base = __DIR__ . '/../../data/media'; - // phpcs:disable Generic.Files.LineLength - $metadata = ' - - image/svg+xml - - - '; - // phpcs:enable - - $metadata = str_replace( "\r", '', $metadata ); // Windows compat - return [ - [ - "$base/US_states_by_total_state_tax_revenue.svg", - [ - 'height' => 593, - 'metadata' => $metadata, - 'width' => 959, - 'originalWidth' => '958.69', - 'originalHeight' => '592.78998', - 'translations' => [], - ] - ], - ]; - } - - public static function provideSvgUnits() { - return [ - [ '1' , 1 ], - [ '1.1' , 1.1 ], - [ '0.1' , 0.1 ], - [ '.1' , 0.1 ], - [ '1e2' , 100 ], - [ '1E2' , 100 ], - [ '+1' , 1 ], - [ '-1' , -1 ], - [ '-1.1' , -1.1 ], - [ '1e+2' , 100 ], - [ '1e-2' , 0.01 ], - [ '10px' , 10 ], - [ '10pt' , 10 * 1.25 ], - [ '10pc' , 10 * 15 ], - [ '10mm' , 10 * 3.543307 ], - [ '10cm' , 10 * 35.43307 ], - [ '10in' , 10 * 90 ], - [ '10em' , 10 * 16 ], - [ '10ex' , 10 * 12 ], - [ '10%' , 51.2 ], - [ '10 px' , 10 ], - // Invalid values - [ '1e1.1', 10 ], - [ '10bp', 10 ], - [ 'p10', null ], - ]; - } -} diff --git a/tests/phpunit/includes/media/SVGReaderTest.php b/tests/phpunit/includes/media/SVGReaderTest.php new file mode 100644 index 0000000000..7063a575da --- /dev/null +++ b/tests/phpunit/includes/media/SVGReaderTest.php @@ -0,0 +1,203 @@ +assertMetadata( $infile, $expected ); + } + + /** + * @dataProvider provideSvgFilesWithXMLMetadata + */ + public function testGetXMLMetadata( $infile, $expected ) { + $r = new XMLReader(); + $this->assertMetadata( $infile, $expected ); + } + + /** + * @dataProvider provideSvgUnits + */ + public function testScaleSVGUnit( $inUnit, $expected ) { + $this->assertEquals( + $expected, + SVGReader::scaleSVGUnit( $inUnit ), + 'SVG unit conversion and scaling failure' + ); + } + + function assertMetadata( $infile, $expected ) { + try { + $svgReader = new SVGReader( $infile ); + $data = $svgReader->getMetadata(); + + $this->assertEquals( $expected, $data, 'SVG metadata extraction test' ); + } catch ( MWException $e ) { + if ( $expected === false ) { + $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' ); + } else { + throw $e; + } + } + } + + public static function provideSvgFiles() { + $base = __DIR__ . '/../../data/media'; + + return [ + [ + "$base/Wikimedia-logo.svg", + [ + 'width' => 1024, + 'height' => 1024, + 'originalWidth' => '1024', + 'originalHeight' => '1024', + 'translations' => [], + ] + ], + [ + "$base/QA_icon.svg", + [ + 'width' => 60, + 'height' => 60, + 'originalWidth' => '60', + 'originalHeight' => '60', + 'translations' => [], + ] + ], + [ + "$base/Gtk-media-play-ltr.svg", + [ + 'width' => 60, + 'height' => 60, + 'originalWidth' => '60.0000000', + 'originalHeight' => '60.0000000', + 'translations' => [], + ] + ], + [ + "$base/Toll_Texas_1.svg", + // This file triggered T33719, needs entity expansion in the xmlns checks + [ + 'width' => 385, + 'height' => 385, + 'originalWidth' => '385', + 'originalHeight' => '385.0004883', + 'translations' => [], + ] + ], + [ + "$base/Tux.svg", + [ + 'width' => 512, + 'height' => 594, + 'originalWidth' => '100%', + 'originalHeight' => '100%', + 'title' => 'Tux', + 'translations' => [], + 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg', + ] + ], + [ + "$base/Speech_bubbles.svg", + [ + 'width' => 627, + 'height' => 461, + 'originalWidth' => '17.7cm', + 'originalHeight' => '13cm', + 'translations' => [ + 'de' => SVGReader::LANG_FULL_MATCH, + 'fr' => SVGReader::LANG_FULL_MATCH, + 'nl' => SVGReader::LANG_FULL_MATCH, + 'tlh-ca' => SVGReader::LANG_FULL_MATCH, + 'tlh' => SVGReader::LANG_PREFIX_MATCH + ], + ] + ], + [ + "$base/Soccer_ball_animated.svg", + [ + 'width' => 150, + 'height' => 150, + 'originalWidth' => '150', + 'originalHeight' => '150', + 'animated' => true, + 'translations' => [] + ], + ], + [ + "$base/comma_separated_viewbox.svg", + [ + 'width' => 512, + 'height' => 594, + 'originalWidth' => '100%', + 'originalHeight' => '100%', + 'translations' => [] + ], + ], + ]; + } + + public static function provideSvgFilesWithXMLMetadata() { + $base = __DIR__ . '/../../data/media'; + // phpcs:disable Generic.Files.LineLength + $metadata = ' + + image/svg+xml + + + '; + // phpcs:enable + + $metadata = str_replace( "\r", '', $metadata ); // Windows compat + return [ + [ + "$base/US_states_by_total_state_tax_revenue.svg", + [ + 'height' => 593, + 'metadata' => $metadata, + 'width' => 959, + 'originalWidth' => '958.69', + 'originalHeight' => '592.78998', + 'translations' => [], + ] + ], + ]; + } + + public static function provideSvgUnits() { + return [ + [ '1' , 1 ], + [ '1.1' , 1.1 ], + [ '0.1' , 0.1 ], + [ '.1' , 0.1 ], + [ '1e2' , 100 ], + [ '1E2' , 100 ], + [ '+1' , 1 ], + [ '-1' , -1 ], + [ '-1.1' , -1.1 ], + [ '1e+2' , 100 ], + [ '1e-2' , 0.01 ], + [ '10px' , 10 ], + [ '10pt' , 10 * 1.25 ], + [ '10pc' , 10 * 15 ], + [ '10mm' , 10 * 3.543307 ], + [ '10cm' , 10 * 35.43307 ], + [ '10in' , 10 * 90 ], + [ '10em' , 10 * 16 ], + [ '10ex' , 10 * 12 ], + [ '10%' , 51.2 ], + [ '10 px' , 10 ], + // Invalid values + [ '1e1.1', 10 ], + [ '10bp', 10 ], + [ 'p10', null ], + ]; + } +} diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php index 59649153e9..089431e952 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php @@ -311,37 +311,41 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { public static function provideGetContent() { yield 'Bad title' => [ null, '[x]' ]; - yield 'Dead redirect' => [ null, [ - 'text' => 'Dead redirect', - 'title' => 'Dead_redirect', - 'redirect' => 1, - ] ]; - yield 'Bad content model' => [ null, [ - 'text' => 'MediaWiki:Wikitext', - 'ns' => NS_MEDIAWIKI, - 'title' => 'Wikitext', - ] ]; + yield 'No JS content found' => [ null, [ - 'text' => 'MediaWiki:Script.js', + 'text' => 'MediaWiki:Foo.js', 'ns' => NS_MEDIAWIKI, - 'title' => 'Script.js', + 'title' => 'Foo.js', ] ]; - yield 'No CSS content found' => [ null, [ - 'text' => 'MediaWiki:Styles.css', + + yield 'JS content' => [ 'code;', [ + 'text' => 'MediaWiki:Foo.js', 'ns' => NS_MEDIAWIKI, - 'title' => 'Script.css', - ] ]; + 'title' => 'Foo.js', + ], new JavaScriptContent( 'code;' ) ]; + + yield 'CSS content' => [ 'code {}', [ + 'text' => 'MediaWiki:Foo.css', + 'ns' => NS_MEDIAWIKI, + 'title' => 'Foo.css', + ], new CssContent( 'code {}' ) ]; + + yield 'Wikitext content' => [ null, [ + 'text' => 'MediaWiki:Foo', + 'ns' => NS_MEDIAWIKI, + 'title' => 'Foo', + ], new WikitextContent( 'code;' ) ]; } /** * @dataProvider provideGetContent */ - public function testGetContent( $expected, $title ) { + public function testGetContent( $expected, $title, Content $contentObj = null ) { $context = $this->getResourceLoaderContext( [], new EmptyResourceLoader ); $module = $this->getMockBuilder( ResourceLoaderWikiModule::class ) ->setMethods( [ 'getContentObj' ] )->getMock(); $module->method( 'getContentObj' ) - ->willReturn( null ); + ->willReturn( $contentObj ); if ( is_array( $title ) ) { $title += [ 'ns' => NS_MAIN, 'id' => 1, 'len' => 1, 'redirect' => 0 ]; diff --git a/tests/phpunit/includes/user/UserTest.php b/tests/phpunit/includes/user/UserTest.php index bb723158d6..62e8e2364d 100644 --- a/tests/phpunit/includes/user/UserTest.php +++ b/tests/phpunit/includes/user/UserTest.php @@ -1021,18 +1021,6 @@ class UserTest extends MediaWikiTestCase { ] ); $this->assertTrue( User::isLocallyBlockedProxy( $ip ) ); - - $this->hideDeprecated( - 'IP addresses in the keys of $wgProxyList (found the following IP ' . - 'addresses in keys: ' . $blockListEntry . ', please move them to values)' - ); - $this->setMwGlobals( - 'wgProxyList', - [ - $blockListEntry => 'test' - ] - ); - $this->assertTrue( User::isLocallyBlockedProxy( $ip ) ); } /** diff --git a/tests/phpunit/suites/ParserTestTopLevelSuite.php b/tests/phpunit/suites/ParserTestTopLevelSuite.php index 28547d1f84..f318df1dd7 100644 --- a/tests/phpunit/suites/ParserTestTopLevelSuite.php +++ b/tests/phpunit/suites/ParserTestTopLevelSuite.php @@ -1,5 +1,6 @@ getDBLoadBalancer(); + $db = $lb->getConnection( DB_MASTER ); $type = $db->getType(); $prefix = $type === 'oracle' ? MediaWikiTestCase::ORA_DB_PREFIX : MediaWikiTestCase::DB_PREFIX;