From: jenkins-bot Date: Sun, 21 Jul 2019 18:11:12 +0000 (+0000) Subject: Merge "Exclude redirects from Special:Fewestrevisions" X-Git-Tag: 1.34.0-rc.0~925 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=bd5a37aacf600bdd5f3a6e7998f92bd1d9326a8a;hp=95438dd70ade9ae00b6e835078eeccd3c28d9be4 Merge "Exclude redirects from Special:Fewestrevisions" --- diff --git a/.phpcs.xml b/.phpcs.xml index 8f3bd8c527..9f11ebcf5c 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -179,6 +179,7 @@ */maintenance/storage/recompressTracked\.php */maintenance/storage/trackBlobs\.php + */tests/phpunit/unit/includes/GlobalFunctions/*\.php */tests/phpunit/includes/GlobalFunctions/*\.php */tests/phpunit/maintenance/*\.php */tests/phpunit/integration/includes/GlobalFunctions/*\.php diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index 5e49fc7773..57d635e5ef 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -36,6 +36,9 @@ For notes on 1.33.x and older releases, see HISTORY. * $wgEnableSpecialMute (T218265) - This configuration controls whether Special:Mute is available and whether to include a link to it on emails originating from Special:Email. +* editmyuserjsredirect user right – users without this right now cannot edit JS + redirects in their userspace unless the target of the redirect is also in + their userspace. By default, this right is given to everyone. ==== Changed configuration ==== * $wgUseCdn, $wgCdnServers, $wgCdnServersNoPurge, and $wgCdnMaxAge – These four @@ -52,20 +55,33 @@ For notes on 1.33.x and older releases, see HISTORY. * Introduced $wgVerifyMimeTypeIE to allow disabling the MSIE 6/7 file type detection heuristic on upload, which is more conservative than the checks that were changed above. +* $wgSkipSkin — Setting this instead of $wgSkipSkins, deprecated in 1.23, is now + hard-deprecated. +* $wgLocalInterwiki — Setting this instead of $wgLocalInterwikis, deprecated in + 1.23, is now hard-deprecated. +* $wgProfileOnly — Setting this, deprecated in 1.23, is now hard-deprecated. + Instead, set the log file in $wgDebugLogGroups['profileoutput']. * … ==== Removed configuration ==== * $wgWikiDiff2MovedParagraphDetectionCutoff — If you still want a custom change size threshold, please specify in php.ini, using the configuration variable wikidiff2.moved_paragraph_detection_cutoff. +* $wgDebugPrintHttpHeaders - The default of including HTTP headers in the + debug log channel is no longer configurable. The debug log itself remains + configurable via $wgDebugLogFile. === New user-facing features in 1.34 === * Special:Mute has been added as a quick way for users to block unwanted emails from other users originating from Special:EmailUser. === New developer features in 1.34 === +* The ImgAuthModifyHeaders hook was added to img_auth.php to allow modification + of headers in private wikis. * Language::formatTimePeriod now supports the new 'avoidhours' option to output strings like "5 days ago" instead of "5 days 13 hours ago". +* (T220163) Added SpecialMuteModifyFormFields hook to allow extensions + to add fields to Special:Mute. === External library changes in 1.34 === @@ -74,7 +90,7 @@ For notes on 1.33.x and older releases, see HISTORY. ==== Changed external libraries ==== * Updated Mustache from 1.0.0 to v3.0.1. -* Updated OOUI from v0.31.3 to v0.33.1. +* Updated OOUI from v0.31.3 to v0.33.2. * Updated composer/semver from 1.4.2 to 1.5.0. * Updated composer/spdx-licenses from 1.4.0 to 1.5.1 (dev-only). * Updated mediawiki/codesniffer from 25.0.0 to 26.0.0 (dev-only). @@ -82,9 +98,10 @@ For notes on 1.33.x and older releases, see HISTORY. * Updated wikimedia/at-ease from 1.2.0 to 2.0.0. * Updated wikimedia/remex-html from 2.0.1 to 2.0.3. * Updated monolog/monolog from 1.22.1 to 1.24.0 (dev-only). -* Updated wikimedia/object-factory from 1.0.0 to 2.0.0. +* Updated wikimedia/object-factory from 1.0.0 to 2.1.0. * Updated wikimedia/timestamp from 2.2.0 to 3.0.0. * Updated wikimedia/xmp-reader from 0.6.2 to 0.6.3. +* Updated mediawiki/mediawiki-phan-config from 0.6.0 to 0.6.1 (dev-only). * … ==== Removed external libraries ==== @@ -218,6 +235,8 @@ because of Phabricator reports. specified, deprecated in 1.30, have been removed. * BufferingStatsdDataFactory::getBuffer(), deprecated in 1.30, has been removed. * The constant DB_SLAVE, deprecated in 1.28, has been removed. Use DB_REPLICA. +* The constants NS_IMAGE and NS_IMAGE_TALK, deprecated in 1.14, have been + removed. Use NS_FILE and NS_FILE_TALK respectively. * Replacer, DoubleReplacer, HashtableReplacer and RegexlikeReplacer (deprecated in 1.32) have been removed. Closures should be used instead. * OutputPage::addWikiText(), ::addWikiTextWithTitle(), ::addWikiTextTitleTidy(), @@ -265,6 +284,32 @@ because of Phabricator reports. in JavaScript, use mw.log.deprecate() instead. * The 'user.groups' module, deprecated in 1.28, was removed. Use the 'user' module instead. +* The ability to override User::$mRights has been removed. Use + PermissionManager::addTemporaryUserRights() instead. +* Previously, when iterating ResultWrapper with foreach() or a similar + construct, the range of the index was 1..numRows. This has been fixed to be + 0..(numRows-1). +* The ChangePasswordForm hook, deprecated in 1.27, has been removed. Use the + AuthChangeFormFields hook or security levels instead. +* WikiMap::getWikiIdFromDomain(), deprecated in 1.33, has been removed. + Use WikiMap::getWikiIdFromDbDomain() instead. +* The config variables $wgHtml5, $wgJsMimeType, and $wgXhtmlDefaultNamespace, + which were deprecated and ignored by core since 1.22, are no longer set to any + value, and SkinTemplate no longer emits a 'jsmimetype' key. Any extensions not + updated since 2013 to cope with this deprecation may now break. +* (T222637) Passing ResourceLoaderModule objects to ResourceLoader::register() + or $wgResourceModules is no longer supported. + Use the 'class' or 'factory' option of the array format instead. +* The parameter $lang of the functions generateTOC and tocList in Linker and + DummyLinker must be in type Language when present. Other types are + deprecated since 1.33. +* The static properties mw.Api.errors and mw.Api.warnings, deprecated in 1.29, + have 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(). * … === Deprecations in 1.34 === @@ -342,6 +387,10 @@ because of Phabricator reports. template option 'searchaction' instead. * LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have been deprecated. +* User::getRights() and User::$mRights have been deprecated. Use + PermissionManager::getUserPermissions() instead. +* The LocalisationCacheRecache hook no longer allows purging of message blobs + to be prevented. Modifying the $purgeBlobs parameter now has no effect. === Other changes in 1.34 === * … diff --git a/autoload.php b/autoload.php index 218c244a53..5410bb8a4b 100644 --- a/autoload.php +++ b/autoload.php @@ -968,6 +968,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Widget\\TitlesMultiselectWidget' => __DIR__ . '/includes/widget/TitlesMultiselectWidget.php', 'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php', 'MediaWiki\\Widget\\UsersMultiselectWidget' => __DIR__ . '/includes/widget/UsersMultiselectWidget.php', + 'MediumSpecificBagOStuff' => __DIR__ . '/includes/libs/objectcache/MediumSpecificBagOStuff.php', 'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php', 'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php', 'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php', @@ -1560,6 +1561,7 @@ $wgAutoloadLocalClasses = [ 'UploadStashWrongOwnerException' => __DIR__ . '/includes/upload/exception/UploadStashWrongOwnerException.php', 'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/exception/UploadStashZeroLengthFileException.php', 'UppercaseCollation' => __DIR__ . '/includes/collation/UppercaseCollation.php', + 'UppercaseTitlesForUnicodeTransition' => __DIR__ . '/maintenance/uppercaseTitlesForUnicodeTransition.php', 'User' => __DIR__ . '/includes/user/User.php', 'UserArray' => __DIR__ . '/includes/user/UserArray.php', 'UserArrayFromResult' => __DIR__ . '/includes/user/UserArrayFromResult.php', diff --git a/composer.json b/composer.json index f7b72f5bba..dc6d091353 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "ext-xml": "*", "guzzlehttp/guzzle": "6.3.3", "liuggio/statsd-php-client": "1.0.18", - "oojs/oojs-ui": "0.33.1", + "oojs/oojs-ui": "0.33.3", "pear/mail": "1.4.1", "pear/mail_mime": "1.10.2", "pear/net_smtp": "1.8.1", @@ -43,7 +43,7 @@ "wikimedia/html-formatter": "1.0.2", "wikimedia/ip-set": "2.0.1", "wikimedia/less.php": "1.8.0", - "wikimedia/object-factory": "2.0.0", + "wikimedia/object-factory": "2.1.0", "wikimedia/password-blacklist": "0.1.4", "wikimedia/php-session-serializer": "1.0.7", "wikimedia/purtle": "1.0.7", @@ -76,7 +76,7 @@ "wikimedia/avro": "1.8.0", "wikimedia/testing-access-wrapper": "~1.0", "wmde/hamcrest-html-matchers": "^0.1.0", - "mediawiki/mediawiki-phan-config": "0.6.0", + "mediawiki/mediawiki-phan-config": "0.6.1", "symfony/yaml": "3.4.28", "johnkary/phpunit-speedtrap": "^1.0 | ^2.0" }, diff --git a/docs/extension.schema.v1.json b/docs/extension.schema.v1.json index 86fa1b3da8..9ce016f063 100644 --- a/docs/extension.schema.v1.json +++ b/docs/extension.schema.v1.json @@ -730,6 +730,30 @@ "SkinOOUIThemes": { "type": "object" }, + "OOUIThemePaths": { + "type": "object", + "description": "Map of custom OOUI theme names to paths to load them from. Same format as ResourceLoaderOOUIModule::$builtinThemePaths.", + "patternProperties": { + "^[A-Za-z]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "scripts": { + "type": "string", + "description": "Path to script file." + }, + "styles": { + "type": "string", + "description": "Path to style files. '{module}' will be replaced with the module's name." + }, + "images": { + "type": [ "string", "null" ], + "description": "Path to images (optional). '{module}' will be replaced with the module's name." + } + } + } + } + }, "PasswordPolicy": { "type": "object", "description": "Password policies" diff --git a/docs/extension.schema.v2.json b/docs/extension.schema.v2.json index c1db2b6e4a..9d874f47f4 100644 --- a/docs/extension.schema.v2.json +++ b/docs/extension.schema.v2.json @@ -801,6 +801,30 @@ "type": "object", "description": "Map of skin names to OOUI themes to use. Same format as ResourceLoaderOOUIModule::$builtinSkinThemeMap." }, + "OOUIThemePaths": { + "type": "object", + "description": "Map of custom OOUI theme names to paths to load them from. Same format as ResourceLoaderOOUIModule::$builtinThemePaths.", + "patternProperties": { + "^[A-Za-z]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "scripts": { + "type": "string", + "description": "Path to script file." + }, + "styles": { + "type": "string", + "description": "Path to style files. '{module}' will be replaced with the module's name." + }, + "images": { + "type": [ "string", "null" ], + "description": "Path to images (optional). '{module}' will be replaced with the module's name." + } + } + } + } + }, "PasswordPolicy": { "type": "object", "description": "Password policies" diff --git a/docs/hooks.txt b/docs/hooks.txt index 1e5072f003..8e274ed06e 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -944,12 +944,6 @@ No return data is accepted; this hook is for auditing only. $req: AuthenticationRequest object describing the change (and target user) $status: StatusValue with the result of the action -'ChangePasswordForm': DEPRECATED since 1.27! Use AuthChangeFormFields or -security levels. For extensions that need to add a field to the ChangePassword -form via the Preferences form. -&$extraFields: An array of arrays that hold fields like would be passed to the - pretty function. - 'ChangesListInitRows': Batch process change list rows prior to rendering. $changesList: ChangesList instance $rows: The data that will be rendered. May be a \Wikimedia\Rdbms\IResultWrapper @@ -1816,7 +1810,7 @@ $page: ImagePage object $page: ImagePage object &$toc: Array of
  • strings -'ImgAuthBeforeStream': executed before file is streamed to user, but only when +'ImgAuthBeforeStream': Executed before file is streamed to user, but only when using img_auth.php. &$title: the Title object of the file as it would appear for the upload page &$path: the original file and path name when img_auth was invoked by the web @@ -1829,6 +1823,14 @@ using img_auth.php. $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. +'ImgAuthModifyHeaders': Executed just before a file is streamed to a user via +img_auth.php, allowing headers to be modified beforehand. +$title: LinkTarget object +&$headers: HTTP headers ( name => value, names are case insensitive ). + Two headers get special handling: If-Modified-Since (value must be + a valid HTTP date) and Range (must be of the form "bytes=(\d*-\d*)") + will be honored when streaming the file. + 'ImportHandleLogItemXMLTag': When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader: XMLReader object @@ -2093,8 +2095,6 @@ cache. $cache: The LocalisationCache object $code: language code &$alldata: The localisation data from core and extensions -&$purgeBlobs: whether to purge/update the message blobs via - MessageBlobStore::clear() 'LocalisationCacheRecacheFallback': Called for each language when merging fallback data into the cache. @@ -3200,6 +3200,10 @@ $request: WebRequest object for getting the value provided by the current user &$oldTitle: old title (object) &$newTitle: new title (object) +'SpecialMuteModifyFormFields': Add more fields to Special:Mute +$sp: SpecialPage object, for context +&$fields: Current HTMLForm fields descriptors + 'SpecialNewpagesConditions': Called when building sql query for Special:NewPages. &$special: NewPagesPager object (subclass of ReverseChronologicalPager) @@ -3565,14 +3569,6 @@ $props: (array|null) File properties, as returned by MessageSpecifier instance (you might want to use ApiMessage to provide machine -readable details for the API). -'UploadVerification': DEPRECATED since 1.28! Use UploadVerifyFile instead. -Additional chances to reject an uploaded file. -$saveName: (string) destination file name -$tempName: (string) filesystem path to the temporary file for checks -&$error: (string) output: message key for message to show if upload canceled by - returning false. May also be an array, where the first element is the message - key and the remaining elements are used as parameters to the message. - 'UploadVerifyFile': extra file verification, based on MIME type, etc. Preferred in most cases over UploadVerification. $upload: (object) an instance of UploadBase, with all info about the upload diff --git a/docs/pageupdater.txt b/docs/pageupdater.txt index 54eb91a9e5..fd084c0587 100644 --- a/docs/pageupdater.txt +++ b/docs/pageupdater.txt @@ -161,11 +161,11 @@ Calling prepareUpdate() with the same parameters again has no effect. Calling it again with mismatching parameters, or calling it with parameters mismatching the ones prepareContent() was called with, triggers a LogicException. -- getSecondaryDataUpdtes() returns DataUpdates that represent derived data for the revision. +- getSecondaryDataUpdates() returns DataUpdates that represent derived data for the revision. These may be used to update such data, e.g. in ApiPurge, RefreshLinksJob, and the refreshLinks script. -- doUpdates() triggers the updates defined by getSecondaryDataUpdtes(), and also causes +- doUpdates() triggers the updates defined by getSecondaryDataUpdates(), and also causes updates to cached artifacts in the ParserCache, the CDN layer, etc. This is primarily used by PageUpdater, but also by PageArchive during undeletion, and when importing revisions from XML. doUpdates() can only be called after prepareUpdate() was used to diff --git a/img_auth.php b/img_auth.php index 1434125af9..914014d85f 100644 --- a/img_auth.php +++ b/img_auth.php @@ -138,12 +138,13 @@ function wfImageAuthMain() { $headers = []; // extra HTTP headers to send + $title = Title::makeTitleSafe( NS_FILE, $name ); + if ( !$publicWiki ) { // For private wikis, run extra auth checks and set cache control headers - $headers[] = 'Cache-Control: private'; - $headers[] = 'Vary: Cookie'; + $headers['Cache-Control'] = 'private'; + $headers['Vary'] = 'Cookie'; - $title = Title::makeTitleSafe( NS_FILE, $name ); if ( !$title instanceof Title ) { // files have valid titles wfForbidden( 'img-auth-accessdenied', 'img-auth-badtitle', $name ); return; @@ -167,19 +168,22 @@ function wfImageAuthMain() { } } - $options = []; // HTTP header options if ( isset( $_SERVER['HTTP_RANGE'] ) ) { - $options['range'] = $_SERVER['HTTP_RANGE']; + $headers['Range'] = $_SERVER['HTTP_RANGE']; } if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { - $options['if-modified-since'] = $_SERVER['HTTP_IF_MODIFIED_SINCE']; + $headers['If-Modified-Since'] = $_SERVER['HTTP_IF_MODIFIED_SINCE']; } if ( $request->getCheck( 'download' ) ) { - $headers[] = 'Content-Disposition: attachment'; + $headers['Content-Disposition'] = 'attachment'; } + // Allow modification of headers before streaming a file + Hooks::run( 'ImgAuthModifyHeaders', [ $title->getTitleValue(), &$headers ] ); + // Stream the requested file + list( $headers, $options ) = HTTPFileStreamer::preprocessHeaders( $headers ); wfDebugLog( 'img_auth', "Streaming `" . $filename . "`." ); $repo->streamFileWithStatus( $filename, $headers, $options ); } diff --git a/includes/CategoryFinder.php b/includes/CategoryFinder.php index 7446b59025..720abc3b46 100644 --- a/includes/CategoryFinder.php +++ b/includes/CategoryFinder.php @@ -213,14 +213,14 @@ class CategoryFinder { /* WHERE */ [ 'cl_from' => $this->next ], __METHOD__ . '-1' ); - foreach ( $res as $o ) { - $k = $o->cl_to; + foreach ( $res as $row ) { + $k = $row->cl_to; # Update parent tree - if ( !isset( $this->parents[$o->cl_from] ) ) { - $this->parents[$o->cl_from] = []; + if ( !isset( $this->parents[$row->cl_from] ) ) { + $this->parents[$row->cl_from] = []; } - $this->parents[$o->cl_from][$k] = $o; + $this->parents[$row->cl_from][$k] = $row; # Ignore those we already have if ( in_array( $k, $this->deadend ) ) { @@ -245,9 +245,9 @@ class CategoryFinder { /* WHERE */ [ 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ], __METHOD__ . '-2' ); - foreach ( $res as $o ) { - $id = $o->page_id; - $name = $o->page_title; + foreach ( $res as $row ) { + $id = $row->page_id; + $name = $row->page_title; $this->name2id[$name] = $id; $this->next[] = $id; unset( $layer[$name] ); diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 9b5a38ba02..107c546724 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -2416,11 +2416,11 @@ $wgObjectCaches = [ 'class' => ReplicatedBagOStuff::class, 'readFactory' => [ 'class' => SqlBagOStuff::class, - 'args' => [ [ 'slaveOnly' => true ] ] + 'args' => [ [ 'replicaOnly' => true ] ] ], 'writeFactory' => [ 'class' => SqlBagOStuff::class, - 'args' => [ [ 'slaveOnly' => false ] ] + 'args' => [ [ 'replicaOnly' => false ] ] ], 'loggroup' => 'SQLBagOStuff', 'reportDupes' => false @@ -2492,11 +2492,35 @@ $wgWANObjectCaches = [ $wgEnableWANCacheReaper = false; /** - * Main object stash type. This should be a fast storage system for storing - * lightweight data like hit counters and user activity. Sites with multiple - * data-centers should have this use a store that replicates all writes. The - * store should have enough consistency for CAS operations to be usable. - * Reads outside of those needed for merge() may be eventually consistent. + * The object store type of the main stash. + * + * This store should be a very fast storage system optimized for holding lightweight data + * like incrementable hit counters and current user activity. The store should replicate the + * dataset among all data-centers. Any add(), merge(), lock(), and unlock() operations should + * maintain "best effort" linearizability; as long as connectivity is strong, latency is low, + * and there is no eviction pressure prompted by low free space, those operations should be + * linearizable. In terms of PACELC (https://en.wikipedia.org/wiki/PACELC_theorem), the store + * should act as a PA/EL distributed system for these operations. One optimization for these + * operations is to route them to a "primary" data-center (e.g. one that serves HTTP POST) for + * synchronous execution and then replicate to the others asynchronously. This means that at + * least calls to these operations during HTTP POST requests would quickly return. + * + * All other operations, such as get(), set(), delete(), changeTTL(), incr(), and decr(), + * should be synchronous in the local data-center, replicating asynchronously to the others. + * This behavior can be overriden by the use of the WRITE_SYNC and READ_LATEST flags. + * + * The store should *preferably* have eventual consistency to handle network partitions. + * + * Modules that rely on the stash should be prepared for: + * - add(), merge(), lock(), and unlock() to be slower than other write operations, + * at least in "secondary" data-centers (e.g. one that only serves HTTP GET/HEAD) + * - Other write operations to have race conditions accross data-centers + * - Read operations to have race conditions accross data-centers + * - Consistency to be either eventual (with Last-Write-Wins) or just "best effort" + * + * In general, this means avoiding updates during idempotent HTTP requests (GET/HEAD) and + * avoiding assumptions of true linearizability (e.g. accepting anomalies). Modules that need + * these kind of guarantees should use other storage mediums. * * The options are: * - db: Store cache objects in the DB @@ -3253,33 +3277,6 @@ $wgOverrideUcfirstCharacters = []; */ $wgMimeType = 'text/html'; -/** - * Previously used as content type in HTML script tags. This is now ignored since - * HTML5 doesn't require a MIME type for script tags (javascript is the default). - * It was also previously used by RawAction to determine the ctype query parameter - * value that will result in a javascript response. - * @deprecated since 1.22 - */ -$wgJsMimeType = null; - -/** - * The default xmlns attribute. The option to define this has been removed. - * The value of this variable is no longer used by core and is set to a fixed - * value in Setup.php for compatibility with extensions that depend on the value - * of this variable being set. Such a dependency however is deprecated. - * @deprecated since 1.22 - */ -$wgXhtmlDefaultNamespace = null; - -/** - * Previously used to determine if we should output an HTML5 doctype. - * This is no longer used as we always output HTML5 now. For compatibility with - * extensions that still check the value of this config it's value is now forced - * to true by Setup.php. - * @deprecated since 1.22 - */ -$wgHtml5 = true; - /** * Defines the value of the version attribute in the <html> tag, if any. * @@ -5176,6 +5173,7 @@ $wgGroupPermissions['user']['minoredit'] = true; $wgGroupPermissions['user']['editmyusercss'] = true; $wgGroupPermissions['user']['editmyuserjson'] = true; $wgGroupPermissions['user']['editmyuserjs'] = true; +$wgGroupPermissions['user']['editmyuserjsredirect'] = true; $wgGroupPermissions['user']['purge'] = true; $wgGroupPermissions['user']['sendemail'] = true; $wgGroupPermissions['user']['applychangetags'] = true; @@ -6306,11 +6304,6 @@ $wgShowDebug = false; */ $wgDebugTimestamps = false; -/** - * Print HTTP headers for every request in the debug information. - */ -$wgDebugPrintHttpHeaders = true; - /** * Show the contents of $wgHooks in Special:Version */ @@ -6499,14 +6492,6 @@ $wgStatsdSamplingRates = [ */ $wgPageInfoTransclusionLimit = 50; -/** - * Set this to an integer to only do synchronous site_stats updates - * one every *this many* updates. The other requests go into pending - * delta values in $wgMemc. Make sure that $wgMemc is a global cache. - * If set to -1, updates *only* go to $wgMemc (useful for daemons). - */ -$wgSiteStatsAsyncFactor = false; - /** * Parser test suite files to be run by parserTests.php when no specific * filename is passed to it. @@ -8300,6 +8285,13 @@ $wgCrossSiteAJAXdomains = []; */ $wgCrossSiteAJAXdomainExceptions = []; +/** + * Enable the experimental REST API. + * + * This will be removed once the REST API is stable and used by clients. + */ +$wgEnableRestAPI = false; + /** @} */ # End AJAX and API } /************************************************************************//** diff --git a/includes/Defines.php b/includes/Defines.php index 648e493b91..d818226974 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -73,22 +73,6 @@ define( 'NS_HELP', 12 ); define( 'NS_HELP_TALK', 13 ); define( 'NS_CATEGORY', 14 ); define( 'NS_CATEGORY_TALK', 15 ); - -/** - * NS_IMAGE and NS_IMAGE_TALK are the pre-v1.14 names for NS_FILE and - * NS_FILE_TALK respectively, and are kept for compatibility. - * - * When writing code that should be compatible with older MediaWiki - * versions, either stick to the old names or define the new constants - * yourself, if they're not defined already. - * - * @deprecated since 1.14 - */ -define( 'NS_IMAGE', NS_FILE ); -/** - * @deprecated since 1.14 - */ -define( 'NS_IMAGE_TALK', NS_FILE_TALK ); /**@}*/ /**@{ diff --git a/includes/DevelopmentSettings.php b/includes/DevelopmentSettings.php index d2f26b30c6..dac9d65ea0 100644 --- a/includes/DevelopmentSettings.php +++ b/includes/DevelopmentSettings.php @@ -57,3 +57,6 @@ unset( $logDir ); // Disable rate-limiting to allow integration tests to run unthrottled // in CI and for devs locally (T225796) $wgRateLimits = []; + +// Disable legacy javascript globals in CI and for devs (T72470) +$wgLegacyJavaScriptGlobals = true; diff --git a/includes/DummyLinker.php b/includes/DummyLinker.php index e46c45e6ea..00d66bf13c 100644 --- a/includes/DummyLinker.php +++ b/includes/DummyLinker.php @@ -345,11 +345,11 @@ class DummyLinker { return Linker::tocLineEnd(); } - public function tocList( $toc, $lang = null ) { + public function tocList( $toc, Language $lang = null ) { return Linker::tocList( $toc, $lang ); } - public function generateTOC( $tree, $lang = null ) { + public function generateTOC( $tree, Language $lang = null ) { return Linker::generateTOC( $tree, $lang ); } diff --git a/includes/EditPage.php b/includes/EditPage.php index d27ef9c7a9..f288327831 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -25,6 +25,7 @@ use MediaWiki\EditPage\TextboxBuilder; use MediaWiki\EditPage\TextConflictHelper; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; use Wikimedia\ScopedCallback; /** @@ -1222,8 +1223,8 @@ class EditPage { # the revisions exist and they were not deleted. # Otherwise, $content will be left as-is. if ( !is_null( $undorev ) && !is_null( $oldrev ) && - !$undorev->isDeleted( Revision::DELETED_TEXT ) && - !$oldrev->isDeleted( Revision::DELETED_TEXT ) + !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) && + !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT ) ) { if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev ) || !$this->isSupportedContentModel( $oldrev->getContentModel() ) @@ -1245,7 +1246,7 @@ class EditPage { } if ( $undoMsg === null ) { - $oldContent = $this->page->getContent( Revision::RAW ); + $oldContent = $this->page->getContent( RevisionRecord::RAW ); $popts = ParserOptions::newFromUserAndLang( $user, MediaWikiServices::getInstance()->getContentLanguage() ); $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts ); @@ -1371,7 +1372,7 @@ class EditPage { $handler = ContentHandler::getForModelID( $this->contentModel ); return $handler->makeEmptyContent(); } - $content = $revision->getContent( Revision::FOR_THIS_USER, $user ); + $content = $revision->getContent( RevisionRecord::FOR_THIS_USER, $user ); return $content; } @@ -1405,7 +1406,7 @@ class EditPage { */ protected function getCurrentContent() { $rev = $this->page->getRevision(); - $content = $rev ? $rev->getContent( Revision::RAW ) : null; + $content = $rev ? $rev->getContent( RevisionRecord::RAW ) : null; if ( $content === false || $content === null ) { $handler = ContentHandler::getForModelID( $this->contentModel ); @@ -1496,7 +1497,7 @@ class EditPage { } $parserOptions = ParserOptions::newFromUser( $user ); - $content = $page->getContent( Revision::RAW ); + $content = $page->getContent( RevisionRecord::RAW ); if ( !$content ) { // TODO: somehow show a warning to the user! @@ -3139,12 +3140,12 @@ ERROR; if ( $revision ) { // Let sysop know that this will make private content public if saved - if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) { + if ( !$revision->userCan( RevisionRecord::DELETED_TEXT, $user ) ) { $out->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); - } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { + } elseif ( $revision->isDeleted( RevisionRecord::DELETED_TEXT ) ) { $out->wrapWikiMsg( "\n", 'rev-deleted-text-view' diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index 59efc98b1c..8efae4f7d6 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -21,6 +21,8 @@ * @ingroup Feed */ +use MediaWiki\Storage\RevisionRecord; + /** * Helper functions for feeds * @@ -68,7 +70,7 @@ class FeedUtils { return self::formatDiffRow( $titleObj, $row->rc_last_oldid, $row->rc_this_oldid, $timestamp, - $row->rc_deleted & Revision::DELETED_COMMENT + $row->rc_deleted & RevisionRecord::DELETED_COMMENT ? wfMessage( 'rev-deleted-comment' )->escaped() : CommentStore::getStore()->getComment( 'rc_comment', $row )->text, $actiontext diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 5f17ad8627..7b4b502905 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -2521,6 +2521,7 @@ function wfForeignMemcKey( $db, $prefix, ...$args ) { * @return string */ function wfGlobalCacheKey( ...$args ) { + wfDeprecated( __METHOD__, '1.30' ); return ObjectCache::getLocalClusterInstance()->makeGlobalKey( ...$args ); } @@ -2756,30 +2757,27 @@ function wfStripIllegalFilenameChars( $name ) { } /** - * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit + * Raise PHP's memory limit (if needed). * - * @return int Resulting value of the memory limit. + * @internal For use by Setup.php */ -function wfMemoryLimit() { - global $wgMemoryLimit; - $memlimit = wfShorthandToInteger( ini_get( 'memory_limit' ) ); - if ( $memlimit != -1 ) { - $conflimit = wfShorthandToInteger( $wgMemoryLimit ); - if ( $conflimit == -1 ) { +function wfMemoryLimit( $newLimit ) { + $oldLimit = wfShorthandToInteger( ini_get( 'memory_limit' ) ); + // If the INI config is already unlimited, there is nothing larger + if ( $oldLimit != -1 ) { + $newLimit = wfShorthandToInteger( $newLimit ); + if ( $newLimit == -1 ) { wfDebug( "Removing PHP's memory limit\n" ); Wikimedia\suppressWarnings(); - ini_set( 'memory_limit', $conflimit ); + ini_set( 'memory_limit', $newLimit ); Wikimedia\restoreWarnings(); - return $conflimit; - } elseif ( $conflimit > $memlimit ) { - wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" ); + } elseif ( $newLimit > $oldLimit ) { + wfDebug( "Raising PHP's memory limit to $newLimit bytes\n" ); Wikimedia\suppressWarnings(); - ini_set( 'memory_limit', $conflimit ); + ini_set( 'memory_limit', $newLimit ); Wikimedia\restoreWarnings(); - return $conflimit; } } - return $memlimit; } /** diff --git a/includes/Html.php b/includes/Html.php index d0f9fc64e9..c4b57af978 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -154,8 +154,7 @@ class Html { * Returns an HTML link element in a string styled as a button * (when $wgUseMediaWikiUIEverywhere is enabled). * - * @param string $contents The raw HTML contents of the element: *not* - * escaped! + * @param string $text The text of the element. Will be escaped (not raw HTML) * @param array $attrs Associative array of attributes, e.g., [ * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for * further documentation. @@ -163,10 +162,10 @@ class Html { * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers * @return string Raw HTML */ - public static function linkButton( $contents, array $attrs, array $modifiers = [] ) { + public static function linkButton( $text, array $attrs, array $modifiers = [] ) { return self::element( 'a', self::buttonAttributes( $attrs, $modifiers ), - $contents + $text ); } diff --git a/includes/Linker.php b/includes/Linker.php index f3d492f829..db3e2f5f03 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -21,6 +21,7 @@ */ use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; /** * Some internal bits split of from Skin.php. These functions are used @@ -1093,15 +1094,15 @@ class Linker { * @return string HTML fragment */ public static function revUserLink( $rev, $isPublic = false ) { - if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) && $isPublic ) { $link = wfMessage( 'rev-deleted-user' )->escaped(); - } elseif ( $rev->userCan( Revision::DELETED_USER ) ) { - $link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ), - $rev->getUserText( Revision::FOR_THIS_USER ) ); + } elseif ( $rev->userCan( RevisionRecord::DELETED_USER ) ) { + $link = self::userLink( $rev->getUser( RevisionRecord::FOR_THIS_USER ), + $rev->getUserText( RevisionRecord::FOR_THIS_USER ) ); } else { $link = wfMessage( 'rev-deleted-user' )->escaped(); } - if ( $rev->isDeleted( Revision::DELETED_USER ) ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) { return '' . $link . ''; } return $link; @@ -1116,12 +1117,12 @@ class Linker { * @return string HTML */ public static function revUserTools( $rev, $isPublic = false, $useParentheses = true ) { - if ( $rev->userCan( Revision::DELETED_USER ) && - ( !$rev->isDeleted( Revision::DELETED_USER ) || !$isPublic ) + if ( $rev->userCan( RevisionRecord::DELETED_USER ) && + ( !$rev->isDeleted( RevisionRecord::DELETED_USER ) || !$isPublic ) ) { - $userId = $rev->getUser( Revision::FOR_THIS_USER ); - $userText = $rev->getUserText( Revision::FOR_THIS_USER ); - if ( $userId && $userText ) { + $userId = $rev->getUser( RevisionRecord::FOR_THIS_USER ); + $userText = $rev->getUserText( RevisionRecord::FOR_THIS_USER ); + if ( $userId || (string)$userText !== '' ) { $link = self::userLink( $userId, $userText ) . self::userToolLinks( $userId, $userText, false, 0, null, $useParentheses ); @@ -1132,7 +1133,7 @@ class Linker { $link = wfMessage( 'rev-deleted-user' )->escaped(); } - if ( $rev->isDeleted( Revision::DELETED_USER ) ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) { return ' ' . $link . ''; } return $link; @@ -1571,18 +1572,18 @@ class Linker { public static function revComment( Revision $rev, $local = false, $isPublic = false, $useParentheses = true ) { - if ( $rev->getComment( Revision::RAW ) == "" ) { + if ( $rev->getComment( RevisionRecord::RAW ) == "" ) { return ""; } - if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) && $isPublic ) { $block = " " . wfMessage( 'rev-deleted-comment' )->escaped() . ""; - } elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) { - $block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ), + } elseif ( $rev->userCan( RevisionRecord::DELETED_COMMENT ) ) { + $block = self::commentBlock( $rev->getComment( RevisionRecord::FOR_THIS_USER ), $rev->getTitle(), $local, null, $useParentheses ); } else { $block = " " . wfMessage( 'rev-deleted-comment' )->escaped() . ""; } - if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) { return " $block"; } return $block; @@ -1667,16 +1668,11 @@ class Linker { * * @since 1.16.3 * @param string $toc Html of the Table Of Contents - * @param string|Language|bool|null $lang Language for the toc title, defaults to user language. - * The types string and bool are deprecated. + * @param Language|null $lang Language for the toc title, defaults to user language * @return string Full html of the TOC */ - public static function tocList( $toc, $lang = null ) { + public static function tocList( $toc, Language $lang = null ) { $lang = $lang ?? RequestContext::getMain()->getLanguage(); - if ( !$lang instanceof Language ) { - wfDeprecated( __METHOD__ . ' with type other than Language for $lang', '1.33' ); - $lang = wfGetLangObj( $lang ); - } $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped(); @@ -1709,11 +1705,10 @@ class Linker { * * @since 1.16.3. $lang added in 1.17 * @param array $tree Return value of ParserOutput::getSections() - * @param string|Language|bool|null $lang Language for the toc title, defaults to user language. - * The types string and bool are deprecated. + * @param Language|null $lang Language for the toc title, defaults to user language * @return string HTML fragment */ - public static function generateTOC( $tree, $lang = null ) { + public static function generateTOC( $tree, Language $lang = null ) { $toc = ''; $lastLevel = 0; foreach ( $tree as $section ) { @@ -1881,10 +1876,10 @@ class Linker { $editCount = 0; $moreRevs = false; foreach ( $res as $row ) { - if ( $rev->getUserText( Revision::RAW ) != $row->rev_user_text ) { + if ( $rev->getUserText( RevisionRecord::RAW ) != $row->rev_user_text ) { if ( $verify && - ( $row->rev_deleted & Revision::DELETED_TEXT - || $row->rev_deleted & Revision::DELETED_USER + ( $row->rev_deleted & RevisionRecord::DELETED_TEXT + || $row->rev_deleted & RevisionRecord::DELETED_USER ) ) { // If the user or the text of the revision we might rollback // to is deleted in some way we can't rollback. Similar to @@ -2113,7 +2108,7 @@ class Linker { return ''; } - if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { + if ( !$rev->userCan( RevisionRecord::DELETED_RESTRICTED, $user ) ) { return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops } $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()-> @@ -2136,7 +2131,7 @@ class Linker { ]; } return self::revDeleteLink( $query, - $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide ); + $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ), $canHide ); } /** diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php index defcb656de..a04b29cb34 100644 --- a/includes/Permissions/PermissionManager.php +++ b/includes/Permissions/PermissionManager.php @@ -23,6 +23,8 @@ use Action; use Exception; use Hooks; use MediaWiki\Linker\LinkTarget; +use MediaWiki\Revision\RevisionLookup; +use MediaWiki\Revision\RevisionRecord; use MediaWiki\Session\SessionManager; use MediaWiki\Special\SpecialPageFactory; use MediaWiki\User\UserIdentity; @@ -32,6 +34,7 @@ use RequestContext; use SpecialPage; use Title; use User; +use Wikimedia\ScopedCallback; use WikiPage; /** @@ -54,6 +57,9 @@ class PermissionManager { /** @var SpecialPageFactory */ private $specialPageFactory; + /** @var RevisionLookup */ + private $revisionLookup; + /** @var string[] List of pages names anonymous user may see */ private $whitelistRead; @@ -84,6 +90,12 @@ class PermissionManager { /** @var string[][] Cached user rights */ private $usersRights = null; + /** + * Temporary user rights, valid for the current request only. + * @var string[][][] userid => override group => rights + */ + private $temporaryUserRights = []; + /** @var string[] Cached rights for isEveryoneAllowed */ private $cachedRights = []; @@ -123,6 +135,7 @@ class PermissionManager { 'editmyusercss', 'editmyuserjson', 'editmyuserjs', + 'editmyuserjsredirect', 'editmywatchlist', 'editsemiprotected', 'editsitecss', @@ -177,6 +190,7 @@ class PermissionManager { /** * @param SpecialPageFactory $specialPageFactory + * @param RevisionLookup $revisionLookup * @param string[] $whitelistRead * @param string[] $whitelistReadRegexp * @param bool $emailConfirmToEdit @@ -188,6 +202,7 @@ class PermissionManager { */ public function __construct( SpecialPageFactory $specialPageFactory, + RevisionLookup $revisionLookup, $whitelistRead, $whitelistReadRegexp, $emailConfirmToEdit, @@ -198,6 +213,7 @@ class PermissionManager { NamespaceInfo $nsInfo ) { $this->specialPageFactory = $specialPageFactory; + $this->revisionLookup = $revisionLookup; $this->whitelistRead = $whitelistRead; $this->whitelistReadRegexp = $whitelistReadRegexp; $this->emailConfirmToEdit = $emailConfirmToEdit; @@ -1127,6 +1143,20 @@ class PermissionManager { && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) { $errors[] = [ 'mycustomjsprotected', $action ]; + } elseif ( + $page->isUserJsConfigPage() + && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' ) + ) { + // T207750 - do not allow users to edit a redirect if they couldn't edit the target + $rev = $this->revisionLookup->getRevisionByTitle( $page ); + $content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null; + $target = $content ? $content->getUltimateRedirectTarget() : null; + if ( $target && ( + !$target->inNamespace( NS_USER ) + || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() ) + ) ) { + $errors[] = [ 'mycustomjsredirectprotected', $action ]; + } } } else { // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for @@ -1223,7 +1253,11 @@ class PermissionManager { ); } } - return $this->usersRights[ $user->getId() ]; + $rights = $this->usersRights[ $user->getId() ]; + foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) { + $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) ); + } + return $rights; } /** @@ -1391,6 +1425,29 @@ class PermissionManager { return $this->allRights; } + /** + * Add temporary user rights, only valid for the current scope. + * This is meant for making it possible to programatically trigger certain actions that + * the user wouldn't be able to trigger themselves; e.g. allow users without the bot right + * to make bot-flagged actions through certain special pages. + * Returns a "scope guard" variable; whenever that variable goes out of scope or is consumed + * via ScopedCallback::consume(), the temporary rights are revoked. + * + * @since 1.34 + * + * @param UserIdentity $user + * @param string|string[] $rights + * @return ScopedCallback + */ + public function addTemporaryUserRights( UserIdentity $user, $rights ) { + $userId = $user->getId(); + $nextKey = count( $this->temporaryUserRights[$userId] ?? [] ); + $this->temporaryUserRights[$userId][$nextKey] = (array)$rights; + return new ScopedCallback( function () use ( $userId, $nextKey ) { + unset( $this->temporaryUserRights[$userId][$nextKey] ); + } ); + } + /** * Overrides user permissions cache * diff --git a/includes/Rest/BasicAccess/BasicAuthorizerBase.php b/includes/Rest/BasicAccess/BasicAuthorizerBase.php new file mode 100644 index 0000000000..7aefe255b0 --- /dev/null +++ b/includes/Rest/BasicAccess/BasicAuthorizerBase.php @@ -0,0 +1,28 @@ +createRequestAuthorizer( $request, $handler )->authorize(); + } + + /** + * Create a BasicRequestAuthorizer to authorize the request. + * + * @param RequestInterface $request + * @param Handler $handler + * @return BasicRequestAuthorizer + */ + abstract protected function createRequestAuthorizer( RequestInterface $request, + Handler $handler ) : BasicRequestAuthorizer; +} diff --git a/includes/Rest/BasicAccess/BasicAuthorizerInterface.php b/includes/Rest/BasicAccess/BasicAuthorizerInterface.php new file mode 100644 index 0000000000..64143d4f4b --- /dev/null +++ b/includes/Rest/BasicAccess/BasicAuthorizerInterface.php @@ -0,0 +1,28 @@ +request = $request; + $this->handler = $handler; + } + + /** + * @see BasicAuthorizerInterface::authorize() + * @return string|null If the request is denied, the string error code. If + * the request is allowed, null. + */ + public function authorize() { + if ( $this->handler->needsReadAccess() && !$this->isReadAllowed() ) { + return 'rest-read-denied'; + } + if ( $this->handler->needsWriteAccess() && !$this->isWriteAllowed() ) { + return 'rest-write-denied'; + } + return null; + } + + /** + * Check if the current user is allowed to read from the wiki + * + * @return bool + */ + abstract protected function isReadAllowed(); + + /** + * Check if the current user is allowed to write to the wiki + * + * @return bool + */ + abstract protected function isWriteAllowed(); +} diff --git a/includes/Rest/BasicAccess/MWBasicAuthorizer.php b/includes/Rest/BasicAccess/MWBasicAuthorizer.php new file mode 100644 index 0000000000..43014f1379 --- /dev/null +++ b/includes/Rest/BasicAccess/MWBasicAuthorizer.php @@ -0,0 +1,33 @@ +user = $user; + $this->permissionManager = $permissionManager; + } + + protected function createRequestAuthorizer( RequestInterface $request, + Handler $handler + ): BasicRequestAuthorizer { + return new MWBasicRequestAuthorizer( $request, $handler, $this->user, + $this->permissionManager ); + } +} diff --git a/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php b/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php new file mode 100644 index 0000000000..8c459c63f4 --- /dev/null +++ b/includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php @@ -0,0 +1,42 @@ +user = $user; + $this->permissionManager = $permissionManager; + } + + protected function isReadAllowed() { + return $this->permissionManager->isEveryoneAllowed( 'read' ) + || $this->isAllowed( 'read' ); + } + + protected function isWriteAllowed() { + return $this->isAllowed( 'writeapi' ); + } + + private function isAllowed( $action ) { + return $this->permissionManager->userHasRight( $this->user, $action ); + } +} diff --git a/includes/Rest/BasicAccess/StaticBasicAuthorizer.php b/includes/Rest/BasicAccess/StaticBasicAuthorizer.php new file mode 100644 index 0000000000..c4dcda1426 --- /dev/null +++ b/includes/Rest/BasicAccess/StaticBasicAuthorizer.php @@ -0,0 +1,30 @@ +value = $value; + } + + public function authorize( RequestInterface $request, Handler $handler ) { + return $this->value; + } +} diff --git a/includes/Rest/EntryPoint.php b/includes/Rest/EntryPoint.php index 795999a55c..a14c1a1294 100644 --- a/includes/Rest/EntryPoint.php +++ b/includes/Rest/EntryPoint.php @@ -4,6 +4,7 @@ namespace MediaWiki\Rest; use ExtensionRegistry; use MediaWiki\MediaWikiServices; +use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer; use RequestContext; use Title; use WebResponse; @@ -31,17 +32,27 @@ class EntryPoint { $services = MediaWikiServices::getInstance(); $conf = $services->getMainConfig(); + if ( !$conf->get( 'EnableRestAPI' ) ) { + wfHttpError( 403, 'Access Denied', + 'Set $wgEnableRestAPI to true to enable the experimental REST API' ); + return; + } + $request = new RequestFromGlobals( [ 'cookiePrefix' => $conf->get( 'CookiePrefix' ) ] ); + $authorizer = new MWBasicAuthorizer( RequestContext::getMain()->getUser(), + $services->getPermissionManager() ); + global $IP; $router = new Router( [ "$IP/includes/Rest/coreRoutes.json" ], ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ), $conf->get( 'RestPath' ), $services->getLocalServerObjectCache(), - new ResponseFactory + new ResponseFactory, + $authorizer ); $entryPoint = new self( diff --git a/includes/Rest/Handler.php b/includes/Rest/Handler.php index cee403fa2f..c05d8e774a 100644 --- a/includes/Rest/Handler.php +++ b/includes/Rest/Handler.php @@ -95,6 +95,35 @@ abstract class Handler { return null; } + /** + * Indicates whether this route requires read rights. + * + * The handler should override this if it does not need to read from the + * wiki. This is uncommon, but may be useful for login and other account + * management APIs. + * + * @return bool + */ + public function needsReadAccess() { + return true; + } + + /** + * Indicates whether this route requires write access. + * + * The handler should override this if the route does not need to write to + * the database. + * + * This should return true for routes that may require synchronous database writes. + * Modules that do not need such writes should also not rely on master database access, + * since only read queries are needed and each master DB is a single point of failure. + * + * @return bool + */ + public function needsWriteAccess() { + return true; + } + /** * Execute the handler. This is called after parameter validation. The * return value can either be a Response or any type accepted by diff --git a/includes/Rest/Handler/HelloHandler.php b/includes/Rest/Handler/HelloHandler.php index 6e119dd651..34faee26d3 100644 --- a/includes/Rest/Handler/HelloHandler.php +++ b/includes/Rest/Handler/HelloHandler.php @@ -12,4 +12,8 @@ class HelloHandler extends SimpleHandler { public function run( $name ) { return [ 'message' => "Hello, $name!" ]; } + + public function needsWriteAccess() { + return false; + } } diff --git a/includes/Rest/Router.php b/includes/Rest/Router.php index 5ba3d08c5c..14b4c9cb89 100644 --- a/includes/Rest/Router.php +++ b/includes/Rest/Router.php @@ -4,6 +4,7 @@ namespace MediaWiki\Rest; use AppendIterator; use BagOStuff; +use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface; use MediaWiki\Rest\PathTemplateMatcher\PathMatcher; use Wikimedia\ObjectFactory; @@ -40,21 +41,27 @@ class Router { /** @var ResponseFactory */ private $responseFactory; + /** @var BasicAuthorizerInterface */ + private $basicAuth; + /** * @param string[] $routeFiles List of names of JSON files containing routes * @param array $extraRoutes Extension route array * @param string $rootPath The base URL path * @param BagOStuff $cacheBag A cache in which to store the matcher trees * @param ResponseFactory $responseFactory + * @param BasicAuthorizerInterface $basicAuth */ public function __construct( $routeFiles, $extraRoutes, $rootPath, - BagOStuff $cacheBag, ResponseFactory $responseFactory + BagOStuff $cacheBag, ResponseFactory $responseFactory, + BasicAuthorizerInterface $basicAuth ) { $this->routeFiles = $routeFiles; $this->extraRoutes = $extraRoutes; $this->rootPath = $rootPath; $this->cacheBag = $cacheBag; $this->responseFactory = $responseFactory; + $this->basicAuth = $basicAuth; } /** @@ -189,7 +196,9 @@ class Router { * @return false|string */ private function getRelativePath( $path ) { - if ( substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0 ) { + if ( strlen( $this->rootPath ) > strlen( $path ) || + substr_compare( $path, $this->rootPath, 0, strlen( $this->rootPath ) ) !== 0 + ) { return false; } return substr( $path, strlen( $this->rootPath ) ); @@ -254,6 +263,10 @@ class Router { * @return ResponseInterface */ private function executeHandler( $handler ): ResponseInterface { + $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler ); + if ( $authResult ) { + return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] ); + } $response = $handler->execute(); if ( !( $response instanceof ResponseInterface ) ) { $response = $this->responseFactory->createFromReturnValue( $response ); diff --git a/includes/Revision/RenderedRevision.php b/includes/Revision/RenderedRevision.php index 4acb9c0a64..cf1cc947a7 100644 --- a/includes/Revision/RenderedRevision.php +++ b/includes/Revision/RenderedRevision.php @@ -430,6 +430,16 @@ class RenderedRevision implements SlotRenderingProvider { "$method: Prepared output has vary-revision-exists..." ); return true; + } elseif ( + $out->getFlag( 'vary-revision-sha1' ) && + $out->getRevisionUsedSha1Base36() !== $this->revision->getSha1() + ) { + // 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..." + ); + 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 diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index ec1c08c2da..8a4b6dcfaf 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -445,7 +445,7 @@ class RevisionStore */ public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) { // TODO: pass in a DBTransactionContext instead of a database connection. - $this->checkDatabaseWikiId( $dbw ); + $this->checkDatabaseDomain( $dbw ); $slotRoles = $rev->getSlotRoles(); @@ -1073,7 +1073,7 @@ class RevisionStore $minor, User $user ) { - $this->checkDatabaseWikiId( $dbw ); + $this->checkDatabaseDomain( $dbw ); $pageId = $title->getArticleID(); @@ -2247,32 +2247,14 @@ class RevisionStore * @param IDatabase $db * @throws MWException */ - private function checkDatabaseWikiId( IDatabase $db ) { - $storeWiki = $this->dbDomain; - $dbWiki = $db->getDomainID(); - - if ( $dbWiki === $storeWiki ) { - return; - } - - $storeWiki = $storeWiki ?: $this->loadBalancer->getLocalDomainID(); - // @FIXME: when would getDomainID() be false here? - $dbWiki = $dbWiki ?: wfWikiID(); - - if ( $dbWiki === $storeWiki ) { - return; - } - - // HACK: counteract encoding imposed by DatabaseDomain - $storeWiki = str_replace( '?h', '-', $storeWiki ); - $dbWiki = str_replace( '?h', '-', $dbWiki ); - - if ( $dbWiki === $storeWiki ) { + private function checkDatabaseDomain( IDatabase $db ) { + $dbDomain = $db->getDomainID(); + $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain ); + if ( $dbDomain === $storeDomain ) { return; } - throw new MWException( "RevisionStore for $storeWiki " - . "cannot be used with a DB connection for $dbWiki" ); + throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" ); } /** @@ -2288,7 +2270,7 @@ class RevisionStore * @return object|false data row as a raw object */ private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) { - $this->checkDatabaseWikiId( $db ); + $this->checkDatabaseDomain( $db ); $revQuery = $this->getQueryInfo( [ 'page', 'user' ] ); $options = []; @@ -2608,7 +2590,7 @@ class RevisionStore * of the corresponding revision. */ public function listRevisionSizes( IDatabase $db, array $revIds ) { - $this->checkDatabaseWikiId( $db ); + $this->checkDatabaseDomain( $db ); $revLens = []; if ( !$revIds ) { @@ -2745,7 +2727,7 @@ class RevisionStore * @return int */ private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) { - $this->checkDatabaseWikiId( $db ); + $this->checkDatabaseDomain( $db ); if ( $rev->getPageId() === null ) { return 0; @@ -2804,7 +2786,7 @@ class RevisionStore * @return int */ public function countRevisionsByPageId( IDatabase $db, $id ) { - $this->checkDatabaseWikiId( $db ); + $this->checkDatabaseDomain( $db ); $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ], @@ -2853,7 +2835,7 @@ class RevisionStore * @return bool True if the given user was the only one to edit since the given timestamp */ public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) { - $this->checkDatabaseWikiId( $db ); + $this->checkDatabaseDomain( $db ); if ( !$userId ) { return false; diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 96baf1469e..1bb848fb04 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -91,20 +91,13 @@ return [ }, 'BlockManager' => function ( MediaWikiServices $services ) : BlockManager { - $config = $services->getMainConfig(); $context = RequestContext::getMain(); return new BlockManager( + new ServiceOptions( + BlockManager::$constructorOptions, $services->getMainConfig() + ), $context->getUser(), - $context->getRequest(), - $config->get( 'ApplyIpBlocksToXff' ), - $config->get( 'CookieSetOnAutoblock' ), - $config->get( 'CookieSetOnIpBlock' ), - $config->get( 'DnsBlacklistUrls' ), - $config->get( 'EnableDnsBlacklist' ), - $config->get( 'ProxyList' ), - $config->get( 'ProxyWhitelist' ), - $config->get( 'SecretKey' ), - $config->get( 'SoftBlockRanges' ) + $context->getRequest() ); }, @@ -472,6 +465,7 @@ return [ $config = $services->getMainConfig(); return new PermissionManager( $services->getSpecialPageFactory(), + $services->getRevisionLookup(), $config->get( 'WhitelistRead' ), $config->get( 'WhitelistReadRegexp' ), $config->get( 'EmailConfirmToEdit' ), diff --git a/includes/Setup.php b/includes/Setup.php index 641f1f9030..420298504f 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -55,7 +55,7 @@ if ( ini_get( 'mbstring.func_overload' ) ) { // Start the autoloader, so that extensions can derive classes from core files require_once "$IP/includes/AutoLoader.php"; -// Load up some global defines +// Load global constants require_once "$IP/includes/Defines.php"; // Load default settings @@ -89,9 +89,17 @@ if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) { die( 1 ); } +/** + * Changes to the PHP environment that don't vary on configuration. + */ + // Install a header callback MediaWiki\HeaderCallback::register(); +// Set the encoding used by reading HTTP input, writing HTTP output. +// This is also the default for mbstring functions. +mb_internal_encoding( 'UTF-8' ); + /** * Load LocalSettings.php */ @@ -128,8 +136,6 @@ ExtensionRegistry::getInstance()->loadFromQueue(); // Don't let any other extensions load ExtensionRegistry::getInstance()->finish(); -mb_internal_encoding( 'UTF-8' ); - // Set the configured locale on all requests for consisteny putenv( "LC_ALL=$wgShellLocale" ); setlocale( LC_ALL, $wgShellLocale ); @@ -387,6 +393,8 @@ $wgDefaultUserOptions['watchlistdays'] = min( unset( $rcMaxAgeDays ); if ( $wgSkipSkin ) { + // Hard deprecated in 1.34. + wfDeprecated( '$wgSkipSkin – use $wgSkipSkins instead', '1.23' ); $wgSkipSkins[] = $wgSkipSkin; } @@ -394,6 +402,8 @@ $wgSkipSkins[] = 'fallback'; $wgSkipSkins[] = 'apioutput'; if ( $wgLocalInterwiki ) { + // Hard deprecated in 1.34. + wfDeprecated( '$wgLocalInterwiki – use $wgLocalInterwikis instead', '1.23' ); array_unshift( $wgLocalInterwikis, $wgLocalInterwiki ); } @@ -579,12 +589,6 @@ if ( $wgUseFileCache || $wgUseCdn ) { $wgDebugToolbar = false; } -// We always output HTML5 since 1.22, overriding these is no longer supported -// we set them here for extensions that depend on its value. -$wgHtml5 = true; -$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml'; -$wgJsMimeType = 'text/javascript'; - // Blacklisted file extensions shouldn't appear on the "allowed" list $wgFileExtensions = array_values( array_diff( $wgFileExtensions, $wgFileBlacklist ) ); @@ -622,6 +626,11 @@ if ( $wgCookieSecure === 'detect' ) { } if ( $wgProfileOnly ) { + // Hard deprecated in 1.34. + wfDeprecated( + '$wgProfileOnly set the log file in $wgDebugLogGroups[\'profileoutput\'] instead', + '1.23' + ); $wgDebugLogGroups['profileoutput'] = $wgDebugLogFile; $wgDebugLogFile = ''; } @@ -754,7 +763,9 @@ Profiler::instance()->scopedProfileOut( $ps_default2 ); $ps_misc = Profiler::instance()->scopedProfileIn( $fname . '-misc' ); // Raise the memory limit if it's too low -wfMemoryLimit(); +// Note, this makes use of wfDebug, and thus should not be before +// MWDebug::init() is called. +wfMemoryLimit( $wgMemoryLimit ); /** * Set up the timezone, suppressing the pseudo-security warning in PHP 5.1+ @@ -812,13 +823,9 @@ if ( $wgCommandLineMode ) { } } else { $debug = "\n\nStart request {$wgRequest->getMethod()} {$wgRequest->getRequestURL()}\n"; - - if ( $wgDebugPrintHttpHeaders ) { - $debug .= "HTTP HEADERS:\n"; - - foreach ( $wgRequest->getAllHeaders() as $name => $value ) { - $debug .= "$name: $value\n"; - } + $debug .= "HTTP HEADERS:\n"; + foreach ( $wgRequest->getAllHeaders() as $name => $value ) { + $debug .= "$name: $value\n"; } wfDebug( $debug ); } diff --git a/includes/SiteStats.php b/includes/SiteStats.php index e3cb617e3f..cf3a1ebdf7 100644 --- a/includes/SiteStats.php +++ b/includes/SiteStats.php @@ -52,14 +52,14 @@ class SiteStats { $config = MediaWikiServices::getInstance()->getMainConfig(); $lb = self::getLB(); - $dbr = $lb->getConnection( DB_REPLICA ); + $dbr = $lb->getConnectionRef( DB_REPLICA ); wfDebug( __METHOD__ . ": reading site_stats from replica DB\n" ); $row = self::doLoadFromDB( $dbr ); if ( !self::isRowSane( $row ) && $lb->hasOrMadeRecentMasterChanges() ) { // Might have just been initialized during this request? Underflow? wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" ); - $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) ); + $row = self::doLoadFromDB( $lb->getConnectionRef( DB_MASTER ) ); } if ( !self::isRowSane( $row ) ) { @@ -76,7 +76,7 @@ class SiteStats { SiteStatsInit::doAllAndCommit( $dbr ); } - $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) ); + $row = self::doLoadFromDB( $lb->getConnectionRef( DB_MASTER ) ); } if ( !self::isRowSane( $row ) ) { @@ -155,7 +155,7 @@ class SiteStats { $cache->makeKey( 'SiteStats', 'groupcounts', $group ), $cache::TTL_HOUR, function ( $oldValue, &$ttl, array &$setOpts ) use ( $group, $fname ) { - $dbr = self::getLB()->getConnection( DB_REPLICA ); + $dbr = self::getLB()->getConnectionRef( DB_REPLICA ); $setOpts += Database::getCacheSetOptions( $dbr ); return (int)$dbr->selectField( @@ -206,7 +206,7 @@ class SiteStats { $cache->makeKey( 'SiteStats', 'page-in-namespace', $ns ), $cache::TTL_HOUR, function ( $oldValue, &$ttl, array &$setOpts ) use ( $ns, $fname ) { - $dbr = self::getLB()->getConnection( DB_REPLICA ); + $dbr = self::getLB()->getConnectionRef( DB_REPLICA ); $setOpts += Database::getCacheSetOptions( $dbr ); return (int)$dbr->selectField( diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index b4d6f052f6..5d847b6a83 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -79,7 +79,7 @@ use WikiPage; * * DerivedPageDataUpdater instances are designed to be cached inside a WikiPage instance, * and re-used by callback code over the course of an update operation. It's a stepping stone - * one the way to a more complete refactoring of WikiPage. + * on the way to a more complete refactoring of WikiPage. * * When using a DerivedPageDataUpdater, the following life cycle must be observed: * grabCurrentRevision (optional), prepareContent (optional), prepareUpdate (required @@ -343,14 +343,6 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { } } - /** - * @return bool|string - */ - private function getWikiId() { - // TODO: get from RevisionStore - return false; - } - /** * Checks whether this DerivedPageDataUpdater can be re-used for running updates targeting * the given revision. @@ -580,7 +572,6 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { */ public function isContentDeleted() { if ( $this->revision ) { - // XXX: if that revision is the current revision, this should be skipped return $this->revision->isDeleted( RevisionRecord::DELETED_TEXT ); } else { // If the content has not been saved yet, it cannot have been deleted yet. @@ -1082,6 +1073,11 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { * See DataUpdate::getCauseAction(). (default 'unknown') * - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent(). * (string, default 'unknown') + * - known-revision-output: a combined canonical ParserOutput for the revision, perhaps + * from some cache. The caller is responsible for ensuring that the ParserOutput indeed + * matched the $rev and $options. This mechanism is intended as a temporary stop-gap, + * for the time until caches have been changed to store RenderedRevision states instead + * of ParserOutput objects. (default: null) (since 1.33) */ public function prepareUpdate( RevisionRecord $revision, array $options = [] ) { Assert::parameter( @@ -1228,14 +1224,17 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { if ( $this->renderedRevision ) { $this->renderedRevision->updateRevision( $revision ); } else { - // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions // NOTE: the revision is either new or current, so we can bypass audience checks. $this->renderedRevision = $this->revisionRenderer->getRenderedRevision( $this->revision, null, null, - [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ] + [ + 'use-master' => $this->useMaster(), + 'audience' => RevisionRecord::RAW, + 'known-revision-output' => $options['known-revision-output'] ?? null + ] ); // XXX: Since we presumably are dealing with the current revision, @@ -1574,7 +1573,10 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { // TODO: In the wiring, register a listener for this on the new PageEventEmitter ResourceLoaderWikiModule::invalidateModuleCache( - $title, $oldLegacyRevision, $legacyRevision, $this->getWikiId() ?: wfWikiID() + $title, + $oldLegacyRevision, + $legacyRevision, + $this->loadbalancerFactory->getLocalDomainID() ); $this->doTransition( 'done' ); diff --git a/includes/Storage/PageEditStash.php b/includes/Storage/PageEditStash.php index 2285f4a953..4671d99f15 100644 --- a/includes/Storage/PageEditStash.php +++ b/includes/Storage/PageEditStash.php @@ -109,7 +109,7 @@ class PageEditStash { // the stash request finishes parsing. For the lock acquisition below, there is not much // need to duplicate parsing of the same content/user/summary bundle, so try to avoid // blocking at all here. - $dbw = $this->lb->getConnection( DB_MASTER ); + $dbw = $this->lb->getConnectionRef( DB_MASTER ); if ( !$dbw->lock( $key, $fname, 0 ) ) { // De-duplicate requests on the same key return self::ERROR_BUSY; @@ -269,23 +269,28 @@ class PageEditStash { if ( $editInfo->output->getFlag( 'vary-revision' ) ) { // This can be used for the initial parse, e.g. for filters or doEditContent(), - // but a second parse will be triggered in doEditUpdates(). This is not optimal. + // 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.", - $context - ); - } elseif ( $editInfo->output->getFlag( 'vary-revision-id' ) ) { - // Similar to the above if we didn't guess the ID correctly. - $logger->debug( - "Cache for key '{key}' has vary_revision_id; post-insertion parse possible.", - $context - ); - } elseif ( $editInfo->output->getFlag( 'vary-revision-timestamp' ) ) { - // Similar to the above if we didn't guess the timestamp correctly. - $logger->debug( - "Cache for key '{key}' has vary_revision_timestamp; post-insertion parse possible.", + "Cache for key '{key}' has 'vary-revision'; post-insertion parse inevitable.", $context ); + } else { + static $flagsMaybeReparse = [ + // Similar to the above if we didn't guess the ID correctly + 'vary-revision-id', + // 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' + ]; + foreach ( $flagsMaybeReparse as $flag ) { + if ( $editInfo->output->getFlag( $flag ) ) { + $logger->debug( + "Cache for key '{key}' has $flag; post-insertion parse possible.", + $context + ); + } + } } return $editInfo; @@ -357,7 +362,8 @@ class PageEditStash { * @return string|null TS_MW timestamp or null */ private function lastEditTime( User $user ) { - $db = $this->lb->getConnection( DB_REPLICA ); + $db = $this->lb->getConnectionRef( DB_REPLICA ); + $actorQuery = ActorMigration::newMigration()->getWhere( $db, 'rc_user', $user, false ); $time = $db->selectField( [ 'recentchanges' ] + $actorQuery['tables'], diff --git a/includes/Title.php b/includes/Title.php index 6e75102c92..12d66415ac 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -23,6 +23,7 @@ */ use MediaWiki\Permissions\PermissionManager; +use MediaWiki\Storage\RevisionRecord; use Wikimedia\Assert\Assert; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IDatabase; @@ -1770,6 +1771,7 @@ class Title implements LinkTarget, IDBAccessObject { if ( !MediaWikiServices::getInstance()->getNamespaceInfo()-> hasSubpages( $this->mNamespace ) + || strtok( $this->getText(), '/' ) === false ) { return $this->getText(); } @@ -1887,7 +1889,12 @@ class Title implements LinkTarget, IDBAccessObject { * @since 1.20 */ public function getSubpage( $text ) { - return self::makeTitleSafe( $this->mNamespace, $this->getText() . '/' . $text ); + return self::makeTitleSafe( + $this->mNamespace, + $this->getText() . '/' . $text, + '', + $this->mInterwiki + ); } /** @@ -2294,34 +2301,6 @@ class Title implements LinkTarget, IDBAccessObject { ->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors ); } - /** - * Add the resulting error code to the errors array - * - * @param array $errors List of current errors - * @param array|string|MessageSpecifier|false $result Result of errors - * - * @return array List of errors - */ - private function resultToError( $errors, $result ) { - if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { - // A single array representing an error - $errors[] = $result; - } elseif ( is_array( $result ) && is_array( $result[0] ) ) { - // A nested array representing multiple errors - $errors = array_merge( $errors, $result ); - } elseif ( $result !== '' && is_string( $result ) ) { - // A string representing a message-id - $errors[] = [ $result ]; - } elseif ( $result instanceof MessageSpecifier ) { - // A message specifier representing an error - $errors[] = [ $result ]; - } elseif ( $result === false ) { - // a generic "We don't want them to do that" - $errors[] = [ 'badaccess-group0' ]; - } - return $errors; - } - /** * Get a filtered list of all restriction types supported by this wiki. * @param bool $exists True to get all restriction types that apply to @@ -2949,7 +2928,7 @@ class Title implements LinkTarget, IDBAccessObject { $this->mHasSubpages = false; $subpages = $this->getSubpages( 1 ); if ( $subpages instanceof TitleArray ) { - $this->mHasSubpages = (bool)$subpages->count(); + $this->mHasSubpages = (bool)$subpages->current(); } } @@ -3969,17 +3948,18 @@ class Title implements LinkTarget, IDBAccessObject { if ( $old->getId() === $new->getId() ) { return ( $old_cmp === '>' && $new_cmp === '<' ) ? [] : - [ $old->getUserText( Revision::RAW ) ]; + [ $old->getUserText( RevisionRecord::RAW ) ]; } elseif ( $old->getId() === $new->getParentId() ) { if ( $old_cmp === '>=' && $new_cmp === '<=' ) { - $authors[] = $old->getUserText( Revision::RAW ); - if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) { - $authors[] = $new->getUserText( Revision::RAW ); + $authors[] = $oldUserText = $old->getUserText( RevisionRecord::RAW ); + $newUserText = $new->getUserText( RevisionRecord::RAW ); + if ( $oldUserText != $newUserText ) { + $authors[] = $newUserText; } } elseif ( $old_cmp === '>=' ) { - $authors[] = $old->getUserText( Revision::RAW ); + $authors[] = $old->getUserText( RevisionRecord::RAW ); } elseif ( $new_cmp === '<=' ) { - $authors[] = $new->getUserText( Revision::RAW ); + $authors[] = $new->getUserText( RevisionRecord::RAW ); } return $authors; } @@ -4290,7 +4270,7 @@ class Title implements LinkTarget, IDBAccessObject { * Get the timestamp when this page was updated since the user last saw it. * * @param User|null $user - * @return string|null + * @return string|bool|null String timestamp, false if not watched, null if nothing is unseen */ public function getNotificationTimestamp( $user = null ) { global $wgUser; diff --git a/includes/TitleArrayFromResult.php b/includes/TitleArrayFromResult.php index ee60f7b967..80fdf9ddf1 100644 --- a/includes/TitleArrayFromResult.php +++ b/includes/TitleArrayFromResult.php @@ -41,7 +41,7 @@ class TitleArrayFromResult extends TitleArray implements Countable { } /** - * @param bool|IResultWrapper $row + * @param bool|stdClass $row * @return void */ protected function setCurrent( $row ) { diff --git a/includes/WikiMap.php b/includes/WikiMap.php index 23b0e3edb2..f2641f40f4 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -286,15 +286,6 @@ class WikiMap { : (string)$domain->getDatabase(); } - /** - * @param string $domain - * @return string - * @deprecated Since 1.33; use getWikiIdFromDbDomain() - */ - public static function getWikiIdFromDomain( $domain ) { - return self::getWikiIdFromDbDomain( $domain ); - } - /** * @return DatabaseDomain Database domain of the current wiki * @since 1.33 @@ -311,7 +302,7 @@ class WikiMap { * @since 1.33 */ public static function isCurrentWikiDbDomain( $domain ) { - return self::getCurrentWikiDbDomain()->equals( DatabaseDomain::newFromId( $domain ) ); + return self::getCurrentWikiDbDomain()->equals( $domain ); } /** diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index 4df2f563c7..958ec06f47 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -148,10 +148,17 @@ class HistoryAction extends FormlessAction { $out = $this->getOutput(); $request = $this->getRequest(); - /** - * Allow client caching. - */ - if ( $out->checkLastModified( $this->page->getTouched() ) ) { + // Allow client-side HTTP caching of the history page. + // But, always ignore this cache if the (logged-in) user has this page on their watchlist + // and has one or more unseen revisions. Otherwise, we might be showing stale update markers. + // The Last-Modified for the history page does not change when user's markers are cleared, + // so going from "some unseen" to "all seen" would not clear the cache. + // But, when all of the revisions are marked as seen, then only way for new unseen revision + // markers to appear, is for the page to be edited, which updates page_touched/Last-Modified. + if ( + !$this->hasUnseenRevisionMarkers() && + $out->checkLastModified( $this->page->getTouched() ) + ) { return null; // Client cache fresh and headers sent, nothing more to do. } @@ -305,6 +312,16 @@ class HistoryAction extends FormlessAction { return null; } + /** + * @return bool Page is watched by and has unseen revision for the user + */ + private function hasUnseenRevisionMarkers() { + return ( + $this->getContext()->getConfig()->get( 'ShowUpdatedMarker' ) && + $this->getTitle()->getNotificationTimestamp( $this->getUser() ) + ); + } + /** * Fetch an array of revisions, specified by a given limit, offset and * direction. This is now only used by the feeds. It was previously diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index f8ba08c3ba..279c13bd04 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -23,6 +23,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; use Wikimedia\Rdbms\Database; /** @@ -543,7 +544,7 @@ class InfoAction extends FormlessAction { $batch = new LinkBatch; if ( $firstRev ) { - $firstRevUser = $firstRev->getUserText( Revision::FOR_THIS_USER ); + $firstRevUser = $firstRev->getUserText( RevisionRecord::FOR_THIS_USER ); if ( $firstRevUser !== '' ) { $firstRevUserTitle = Title::makeTitle( NS_USER, $firstRevUser ); $batch->addObj( $firstRevUserTitle ); @@ -552,7 +553,7 @@ class InfoAction extends FormlessAction { } if ( $lastRev ) { - $lastRevUser = $lastRev->getUserText( Revision::FOR_THIS_USER ); + $lastRevUser = $lastRev->getUserText( RevisionRecord::FOR_THIS_USER ); if ( $lastRevUser !== '' ) { $lastRevUserTitle = Title::makeTitle( NS_USER, $lastRevUser ); $batch->addObj( $lastRevUserTitle ); diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index e2fc265f96..519da617ba 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -20,6 +20,8 @@ * @ingroup Actions */ +use MediaWiki\Storage\RevisionRecord; + /** * User interface for the rollback action * @@ -167,8 +169,8 @@ class RollbackAction extends FormAction { $this->getOutput()->addHTML( $this->msg( 'rollback-success' ) ->rawParams( $old, $new ) - ->params( $current->getUserText( Revision::FOR_THIS_USER, $user ) ) - ->params( $target->getUserText( Revision::FOR_THIS_USER, $user ) ) + ->params( $current->getUserText( RevisionRecord::FOR_THIS_USER, $user ) ) + ->params( $target->getUserText( RevisionRecord::FOR_THIS_USER, $user ) ) ->parseAsBlock() ); diff --git a/includes/actions/pagers/HistoryPager.php b/includes/actions/pagers/HistoryPager.php index c9c1b51a99..c5c090d21b 100644 --- a/includes/actions/pagers/HistoryPager.php +++ b/includes/actions/pagers/HistoryPager.php @@ -22,6 +22,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\Revision\RevisionRecord; /** * @ingroup Pager @@ -123,7 +124,6 @@ class HistoryPager extends ReverseChronologicalPager { */ function formatRow( $row ) { if ( $this->lastRow ) { - $latest = ( $this->counter == 1 && $this->mIsFirst ); $firstInList = $this->counter == 1; $this->counter++; @@ -131,8 +131,7 @@ class HistoryPager extends ReverseChronologicalPager { ? $this->getTitle()->getNotificationTimestamp( $this->getUser() ) : false; - $s = $this->historyLine( - $this->lastRow, $row, $notifTimestamp, $latest, $firstInList ); + $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList ); } else { $s = ''; } @@ -185,34 +184,40 @@ class HistoryPager extends ReverseChronologicalPager { $s .= Html::hidden( 'type', 'revision' ) . "\n"; // Button container stored in $this->buttons for re-use in getEndBody() - $this->buttons = Html::openElement( 'div', [ 'class' => 'mw-history-compareselectedversions' ] ); - $className = 'historysubmit mw-history-compareselectedversions-button'; - $attrs = [ 'class' => $className ] - + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' ); - $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(), - $attrs - ) . "\n"; - - $user = $this->getUser(); - $actionButtons = ''; - if ( $user->isAllowed( 'deleterevision' ) ) { - $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' ); - } - if ( $this->showTagEditUI ) { - $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' ); - } - if ( $actionButtons ) { - $this->buttons .= Xml::tags( 'div', [ 'class' => - 'mw-history-revisionactions' ], $actionButtons ); - } + $this->buttons = ''; + if ( $this->getNumRows() > 0 ) { + $this->buttons .= Html::openElement( + 'div', [ 'class' => 'mw-history-compareselectedversions' ] ); + $className = 'historysubmit mw-history-compareselectedversions-button'; + $attrs = [ 'class' => $className ] + + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' ); + $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(), + $attrs + ) . "\n"; + + $user = $this->getUser(); + $actionButtons = ''; + if ( $user->isAllowed( 'deleterevision' ) ) { + $actionButtons .= $this->getRevisionButton( + 'revisiondelete', 'showhideselectedversions' ); + } + if ( $this->showTagEditUI ) { + $actionButtons .= $this->getRevisionButton( + 'editchangetags', 'history-edit-tags' ); + } + if ( $actionButtons ) { + $this->buttons .= Xml::tags( 'div', [ 'class' => + 'mw-history-revisionactions' ], $actionButtons ); + } - if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) { - $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML(); - } + if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) { + $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML(); + } - $this->buttons .= ''; + $this->buttons .= ''; - $s .= $this->buttons; + $s .= $this->buttons; + } $s .= '