From: jenkins-bot Date: Thu, 12 Oct 2017 21:55:35 +0000 (+0000) Subject: Merge "Skin: Make skins aware of their registered skin name" X-Git-Tag: 1.31.0-rc.0~1793 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=a8379682a46a428320c88702c800a6107c015137;hp=a0947c9507065a83afe52b078f0f6d1c6163875e Merge "Skin: Make skins aware of their registered skin name" --- diff --git a/RELEASE-NOTES-1.30 b/RELEASE-NOTES-1.30 index c4c56e833f..bec7b86b34 100644 --- a/RELEASE-NOTES-1.30 +++ b/RELEASE-NOTES-1.30 @@ -25,6 +25,8 @@ section). to plain class names, using the 'factory' key in the module description array. This allows dependency injection to be used for ResourceLoader modules. * $wgExceptionHooks has been removed. +* (T163562) $wgRangeContributionsCIDRLimit was introduced to control the size + of IP ranges that can be queried at Special:Contributions. * (T45547) $wgUsePigLatinVariant added (off by default). * (T152540) MediaWiki now supports a section ID escaping style that allows to display non-Latin characters verbatim on many modern browsers. This is controlled by the @@ -44,6 +46,8 @@ section). * (T37247) Output from Parser::parse() will now be wrapped in a div with class="mw-parser-output" by default. This may be changed or disabled using ParserOptions::setWrapOutputClass(). +* (T163562) Added ability to search for contributions within an IP ranges + at Special:Contributions. * Added 'ChangeTagsAllowedAdd' hook, enabling extensions to allow software- specific tags to be added by users. * Added a 'ParserOptionsRegister' hook to allow extensions to register @@ -66,6 +70,12 @@ section). ** This is currently gated by $wgCommentTableSchemaMigrationStage. Most wikis can set this to MIGRATION_NEW and run maintenance/migrateComments.php as soon as any necessary extensions are updated. +* (T138166) Added ability for users to prohibit other users from sending them + emails with Special:Emailuser. Can be enabled by setting + $wgEnableUserEmailBlacklist to true. +* (T67297) $wgBrowserBlacklist is deprecated, and changing it will have no effect. + Instead, users using browsers that do not support Unicode will be unable to edit + and should upgrade to a modern browser instead. === External library changes in 1.30 === @@ -188,7 +198,23 @@ changes to languages because of Phabricator reports. RunningStat\RunningStat should be used instead. * MWMemcached and MemCachedClientforWiki classes (deprecated in 1.27) were removed. The MemcachedClient class should be used instead. -* EditPage::isOouiEnabled() is deprecated and will always return true. +* EditPage underwent some refactoring and deprecations: + * EditPage::isOouiEnabled() is deprecated and will always return true. + * EditPage::getSummaryInput() and ::getSummaryInputOOUI() are deprecated. Please + use ::getSummaryInputWidget() instead. + * EditPage::getCheckboxes() and ::getCheckboxesOOUI() are deprecated. Please + use ::getCheckboxesWidget() instead. + * Creating an EditPage instance without calling EditPage::setContextTitle() should + be avoided and will be deprecated in a future release. + * EditPage::safeUnicodeInput() and ::safeUnicodeOutput() are deprecated and no-ops. + * EditPage::$isCssJsSubpage, ::$isCssSubpage, and ::$isJsSubpage are deprecated. The + corresponding methods from Title should be used instead. + * EditPage::$isWrongCaseCssJsPage is deprecated. There is no replacement. + * EditPage::$mArticle and ::$mTitle are deprecated for public usage. The getters + ::getArticle() and ::getTitle() should be used instead. + * Trying to control or fake EditPage context by overriding $wgUser, $wgRequest, $wgOut, + and $wgLang is no longer supported and won't work. The IContextSource returned from + EditPage::getContext() must be modified instead. * Parser::getRandomString() (deprecated in 1.26) was removed. * Parser::uniqPrefix() (deprecated in 1.26) was removed. * Parser::extractTagsAndParams() now only accepts three arguments. The fourth, @@ -197,6 +223,23 @@ changes to languages because of Phabricator reports. PRIMARY KEYs for increased maintainability: categorylinks, imagelinks, iwlinks, langlinks, log_search, module_deps, objectcache, pagelinks, query_cache, site_stats, templatelinks, text, transcache, user_former_groups, user_properties. +* IDatabase::nextSequenceValue() is no longer needed by any database backends + (formerly it was needed by PostgreSQL and Oracle), and is now deprecated. +* (T146591) The lc_lang_key index on the l10n_cache table has been changed into a + PRIMARY KEY. +* (T157227) bot_password.bp_user, change_tag.ct_log_id, change_tag.ct_rev_id, + page_restrictions.pr_user, tag_summary.ts_log_id, tag_summary.ts_rev_id and + user_properties.up_user have all been made unsigned on MySQL. +* DB_SLAVE is deprecated. DB_REPLICA should be used instead. +* wfUsePHP() is deprecated. +* wfFixSessionID() was removed. +* wfShellExec() and related functions are deprecated, use Shell::command(). This also + slightly changes the behavior of how execution time limits are calculated when only + some of defaults are overridden per-call. When in doubt, always override both wall + clock and CPU time. +* (T138166) SpecialEmailUser::getTarget() now requires a second argument, the sending + user object. Using the method without the second argument is deprecated. +* (T67297) Browsers that don't support Unicode will have their edits rejected. == Compatibility == MediaWiki 1.30 requires PHP 5.5.9 or later. There is experimental support for diff --git a/RELEASE-NOTES-1.31 b/RELEASE-NOTES-1.31 new file mode 100644 index 0000000000..57cbec456d --- /dev/null +++ b/RELEASE-NOTES-1.31 @@ -0,0 +1,97 @@ +== MediaWiki 1.31 == + +THIS IS NOT A RELEASE YET + +MediaWiki 1.31 is an alpha-quality branch and is not recommended for use in +production. + +=== Configuration changes in 1.31 === +* $wgEnableAPI and $wgEnableWriteAPI are now deprecated and will be removed in + a future version. The API is now considered to be stable, secure and + essential. + +=== New features in 1.31 === +* … + +=== External library changes in 1.31 === + +==== Upgraded external libraries ==== +* … + +==== New external libraries ==== +* … + +==== Removed and replaced external libraries ==== +* … + +=== Bug fixes in 1.31 === +* … + +=== Action API changes in 1.31 === +* … + +=== Action API internal changes in 1.31 === +* … + +=== Languages updated in 1.31 === +MediaWiki supports over 350 languages. Many localisations are updated +regularly. Below only new and removed languages are listed, as well as +changes to languages because of Phabricator reports. + +* … + +=== Other changes in 1.31 === +* MessageBlobStore::insertMessageBlob() (deprecated in 1.27) was removed. +* The global function wfBCP47 was renamed to LanguageCode::bcp47. +* The global function wfBCP47 is now deprecated. + +== Compatibility == +MediaWiki 1.31 requires PHP 5.5.9 or later. There is experimental support for +HHVM 3.6.5 or later. + +MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used, +but support for them is somewhat less mature. There is experimental support for +Oracle and Microsoft SQL Server. + +The supported versions are: + +* MySQL 5.0.3 or later +* PostgreSQL 8.3 or later +* SQLite 3.3.7 or later +* Oracle 9.0.1 or later +* Microsoft SQL Server 2005 (9.00.1399) + +== Upgrading == +1.31 has several database changes since 1.30, and will not work without schema +updates. Note that due to changes to some very large tables like the revision +table, the schema update may take quite long (minutes on a medium sized site, +many hours on a large site). + +Don't forget to always back up your database before upgrading! + +See the file UPGRADE for more detailed upgrade instructions, including +important information when upgrading from versions prior to 1.11. + +For notes on 1.30.x and older releases, see HISTORY. + +== Online documentation == +Documentation for both end-users and site administrators is available on +MediaWiki.org, and is covered under the GNU Free Documentation License (except +for pages that explicitly state that their contents are in the public domain): + + https://www.mediawiki.org/wiki/Special:MyLanguage/Documentation + +== Mailing list == +A mailing list is available for MediaWiki user support and discussion: + + https://lists.wikimedia.org/mailman/listinfo/mediawiki-l + +A low-traffic announcements-only list is also available: + + https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce + +It's highly recommended that you sign up for one of these lists if you're +going to run a public MediaWiki, so you can be notified of security fixes. + +== IRC help == +There's usually someone online in #mediawiki on irc.freenode.net. diff --git a/SECURITY b/SECURITY new file mode 100644 index 0000000000..5c6a288b11 --- /dev/null +++ b/SECURITY @@ -0,0 +1,3 @@ +MediaWiki takes security very seriously. If you believe you have found a +security issue, see +for information on how to responsibly report it. diff --git a/autoload.php b/autoload.php index 3a2ae10007..0a2ecf01c5 100644 --- a/autoload.php +++ b/autoload.php @@ -421,8 +421,8 @@ $wgAutoloadLocalClasses = [ 'EditAction' => __DIR__ . '/includes/actions/EditAction.php', 'EditCLI' => __DIR__ . '/maintenance/edit.php', 'EditPage' => __DIR__ . '/includes/EditPage.php', - 'EditWatchlistCheckboxSeriesField' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php', - 'EditWatchlistNormalHTMLForm' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php', + 'EditWatchlistCheckboxSeriesField' => __DIR__ . '/includes/specials/formfields/EditWatchlistCheckboxSeriesField.php', + 'EditWatchlistNormalHTMLForm' => __DIR__ . '/includes/specials/forms/EditWatchlistNormalHTMLForm.php', 'EmailConfirmation' => __DIR__ . '/includes/specials/SpecialConfirmemail.php', 'EmailInvalidation' => __DIR__ . '/includes/specials/SpecialEmailInvalidate.php', 'EmailNotification' => __DIR__ . '/includes/mail/EmailNotification.php', @@ -437,6 +437,7 @@ $wgAutoloadLocalClasses = [ 'EraseArchivedFile' => __DIR__ . '/maintenance/eraseArchivedFile.php', 'ErrorPageError' => __DIR__ . '/includes/exception/ErrorPageError.php', 'EtcdConfig' => __DIR__ . '/includes/config/EtcdConfig.php', + 'EtcdConfigParseError' => __DIR__ . '/includes/config/EtcdConfigParseError.php', 'EventRelayer' => __DIR__ . '/includes/libs/eventrelayer/EventRelayer.php', 'EventRelayerGroup' => __DIR__ . '/includes/EventRelayerGroup.php', 'EventRelayerKafka' => __DIR__ . '/includes/libs/eventrelayer/EventRelayerKafka.php', @@ -632,7 +633,7 @@ $wgAutoloadLocalClasses = [ 'ImageQueryPage' => __DIR__ . '/includes/specialpage/ImageQueryPage.php', 'ImportImages' => __DIR__ . '/maintenance/importImages.php', 'ImportLogFormatter' => __DIR__ . '/includes/logging/ImportLogFormatter.php', - 'ImportReporter' => __DIR__ . '/includes/specials/SpecialImport.php', + 'ImportReporter' => __DIR__ . '/includes/specials/helpers/ImportReporter.php', 'ImportSiteScripts' => __DIR__ . '/maintenance/importSiteScripts.php', 'ImportSites' => __DIR__ . '/maintenance/importSites.php', 'ImportSource' => __DIR__ . '/includes/import/ImportSource.php', @@ -745,8 +746,8 @@ $wgAutoloadLocalClasses = [ 'Languages' => __DIR__ . '/maintenance/language/languages.inc', 'LayeredParameterizedPassword' => __DIR__ . '/includes/password/LayeredParameterizedPassword.php', 'LegacyLogFormatter' => __DIR__ . '/includes/logging/LogFormatter.php', - 'License' => __DIR__ . '/includes/Licenses.php', - 'Licenses' => __DIR__ . '/includes/Licenses.php', + 'License' => __DIR__ . '/includes/specials/helpers/License.php', + 'Licenses' => __DIR__ . '/includes/specials/formfields/Licenses.php', 'LinkBatch' => __DIR__ . '/includes/cache/LinkBatch.php', 'LinkCache' => __DIR__ . '/includes/cache/LinkCache.php', 'LinkFilter' => __DIR__ . '/includes/LinkFilter.php', @@ -903,6 +904,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php', 'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php', 'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php', + 'MediaWiki\\ProcOpenError' => __DIR__ . '/includes/exception/ProcOpenError.php', 'MediaWiki\\Search\\ParserOutputSearchDataExtractor' => __DIR__ . '/includes/search/ParserOutputSearchDataExtractor.php', 'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/services/CannotReplaceActiveServiceException.php', 'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/services/ContainerDisabledException.php', @@ -927,6 +929,10 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Session\\SessionProviderInterface' => __DIR__ . '/includes/session/SessionProviderInterface.php', 'MediaWiki\\Session\\Token' => __DIR__ . '/includes/session/Token.php', 'MediaWiki\\Session\\UserInfo' => __DIR__ . '/includes/session/UserInfo.php', + 'MediaWiki\\ShellDisabledError' => __DIR__ . '/includes/exception/ShellDisabledError.php', + 'MediaWiki\\Shell\\Command' => __DIR__ . '/includes/shell/Command.php', + 'MediaWiki\\Shell\\Result' => __DIR__ . '/includes/shell/Result.php', + 'MediaWiki\\Shell\\Shell' => __DIR__ . '/includes/shell/Shell.php', 'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php', 'MediaWiki\\Tidy\\BalanceActiveFormattingElements' => __DIR__ . '/includes/tidy/Balancer.php', 'MediaWiki\\Tidy\\BalanceElement' => __DIR__ . '/includes/tidy/Balancer.php', @@ -1042,6 +1048,7 @@ $wgAutoloadLocalClasses = [ 'OldLocalFile' => __DIR__ . '/includes/filerepo/file/OldLocalFile.php', 'OracleInstaller' => __DIR__ . '/includes/installer/OracleInstaller.php', 'OracleUpdater' => __DIR__ . '/includes/installer/OracleUpdater.php', + 'OrderedStreamingForkController' => __DIR__ . '/includes/OrderedStreamingForkController.php', 'OrphanStats' => __DIR__ . '/maintenance/storage/orphanStats.php', 'Orphans' => __DIR__ . '/maintenance/orphans.php', 'OutputPage' => __DIR__ . '/includes/OutputPage.php', @@ -1116,6 +1123,7 @@ $wgAutoloadLocalClasses = [ 'PopulateFilearchiveSha1' => __DIR__ . '/maintenance/populateFilearchiveSha1.php', 'PopulateImageSha1' => __DIR__ . '/maintenance/populateImageSha1.php', 'PopulateInterwiki' => __DIR__ . '/maintenance/populateInterwiki.php', + 'PopulateIpChanges' => __DIR__ . '/maintenance/populateIpChanges.php', 'PopulateLogSearch' => __DIR__ . '/maintenance/populateLogSearch.php', 'PopulateLogUsertext' => __DIR__ . '/maintenance/populateLogUsertext.php', 'PopulatePPSortKey' => __DIR__ . '/maintenance/populatePPSortKey.php', @@ -1127,7 +1135,7 @@ $wgAutoloadLocalClasses = [ 'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php', 'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php', 'Preferences' => __DIR__ . '/includes/Preferences.php', - 'PreferencesForm' => __DIR__ . '/includes/Preferences.php', + 'PreferencesForm' => __DIR__ . '/includes/specials/forms/PreferencesForm.php', 'PrefixSearch' => __DIR__ . '/includes/PrefixSearch.php', 'PreprocessDump' => __DIR__ . '/maintenance/preprocessDump.php', 'Preprocessor' => __DIR__ . '/includes/parser/Preprocessor.php', @@ -1157,6 +1165,7 @@ $wgAutoloadLocalClasses = [ 'PurgeAction' => __DIR__ . '/includes/actions/PurgeAction.php', 'PurgeChangedFiles' => __DIR__ . '/maintenance/purgeChangedFiles.php', 'PurgeChangedPages' => __DIR__ . '/maintenance/purgeChangedPages.php', + 'PurgeExpiredUserrights' => __DIR__ . '/maintenance/purgeExpiredUserrights.php', 'PurgeJobUtils' => __DIR__ . '/includes/jobqueue/utils/PurgeJobUtils.php', 'PurgeList' => __DIR__ . '/maintenance/purgeList.php', 'PurgeModuleDeps' => __DIR__ . '/maintenance/purgeModuleDeps.php', @@ -1524,14 +1533,14 @@ $wgAutoloadLocalClasses = [ 'UploadChunkVerificationException' => __DIR__ . '/includes/upload/UploadFromChunks.php', 'UploadChunkZeroLengthFileException' => __DIR__ . '/includes/upload/UploadFromChunks.php', 'UploadDumper' => __DIR__ . '/maintenance/dumpUploads.php', - 'UploadForm' => __DIR__ . '/includes/specials/SpecialUpload.php', + 'UploadForm' => __DIR__ . '/includes/specials/forms/UploadForm.php', 'UploadFromChunks' => __DIR__ . '/includes/upload/UploadFromChunks.php', 'UploadFromFile' => __DIR__ . '/includes/upload/UploadFromFile.php', 'UploadFromStash' => __DIR__ . '/includes/upload/UploadFromStash.php', 'UploadFromUrl' => __DIR__ . '/includes/upload/UploadFromUrl.php', 'UploadLogFormatter' => __DIR__ . '/includes/logging/UploadLogFormatter.php', 'UploadSourceAdapter' => __DIR__ . '/includes/import/UploadSourceAdapter.php', - 'UploadSourceField' => __DIR__ . '/includes/specials/SpecialUpload.php', + 'UploadSourceField' => __DIR__ . '/includes/specials/formfields/UploadSourceField.php', 'UploadStash' => __DIR__ . '/includes/upload/UploadStash.php', 'UploadStashBadPathException' => __DIR__ . '/includes/upload/UploadStash.php', 'UploadStashCleanup' => __DIR__ . '/maintenance/cleanupUploadStash.php', @@ -1628,6 +1637,7 @@ $wgAutoloadLocalClasses = [ 'Wikimedia\\Rdbms\\DBExpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBExpectedError.php', 'Wikimedia\\Rdbms\\DBMasterPos' => __DIR__ . '/includes/libs/rdbms/database/position/DBMasterPos.php', 'Wikimedia\\Rdbms\\DBQueryError' => __DIR__ . '/includes/libs/rdbms/exception/DBQueryError.php', + 'Wikimedia\\Rdbms\\DBQueryTimeoutError' => __DIR__ . '/includes/libs/rdbms/exception/DBQueryTimeoutError.php', 'Wikimedia\\Rdbms\\DBReadOnlyError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyError.php', 'Wikimedia\\Rdbms\\DBReplicationWaitError' => __DIR__ . '/includes/libs/rdbms/exception/DBReplicationWaitError.php', 'Wikimedia\\Rdbms\\DBTransactionError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionError.php', @@ -1666,6 +1676,7 @@ $wgAutoloadLocalClasses = [ 'Wikimedia\\Rdbms\\MssqlResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php', 'Wikimedia\\Rdbms\\MySQLField' => __DIR__ . '/includes/libs/rdbms/field/MySQLField.php', 'Wikimedia\\Rdbms\\MySQLMasterPos' => __DIR__ . '/includes/libs/rdbms/database/position/MySQLMasterPos.php', + 'Wikimedia\\Rdbms\\NextSequenceValue' => __DIR__ . '/includes/libs/rdbms/database/utils/NextSequenceValue.php', 'Wikimedia\\Rdbms\\PostgresBlob' => __DIR__ . '/includes/libs/rdbms/encasing/PostgresBlob.php', 'Wikimedia\\Rdbms\\PostgresField' => __DIR__ . '/includes/libs/rdbms/field/PostgresField.php', 'Wikimedia\\Rdbms\\ResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php', diff --git a/composer.json b/composer.json index a7983a0dbc..1de39e4399 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "ext-xml": "*", "liuggio/statsd-php-client": "1.0.18", "mediawiki/at-ease": "1.1.0", - "oojs/oojs-ui": "0.22.5", + "oojs/oojs-ui": "0.23.3", "oyejorge/less.php": "1.7.0.14", "php": ">=5.5.9", "psr/log": "1.0.2", @@ -54,7 +54,7 @@ "jakub-onderka/php-parallel-lint": "0.9.2", "jetbrains/phpstorm-stubs": "dev-master#1b9906084d6635456fcf3f3a01f0d7d5b99a578a", "justinrainbow/json-schema": "~5.2", - "mediawiki/mediawiki-codesniffer": "0.11.0", + "mediawiki/mediawiki-codesniffer": "13.0.0", "monolog/monolog": "~1.22.1", "nikic/php-parser": "2.1.0", "nmred/kafka-php": "0.1.5", diff --git a/docs/deferred.txt b/docs/deferred.txt index b8ec76bdb0..9a62fda92a 100644 --- a/docs/deferred.txt +++ b/docs/deferred.txt @@ -30,7 +30,7 @@ Currently there are a few different types of jobs: htmlCacheUpdate Clear caches when a template is changed to ensure that changes can be seen. - Each job clears $wgUpdateRowsPerJob pages (500 by default). + Each job clears $wgUpdateRowsPerJob pages (300 by default). enotifNotify Used to send mail using the job queue. diff --git a/docs/hooks.txt b/docs/hooks.txt index 8912b82b70..a19e9fc0e2 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -996,13 +996,10 @@ $special: ChangesListSpecialPage instance 'ChangesListSpecialPageQuery': Called when building SQL query on pages inheriting from ChangesListSpecialPage (in core: RecentChanges, RecentChangesLinked and Watchlist). - Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure. - Instead, use sub-classes of those classes, in conjunction with the ChangesListSpecialPageStructuredFilters hook. - This hook can be used to implement filters that do not implement that structure, or custom behavior that is not an individual filter. $name: name of the special page, e.g. 'Watchlist' @@ -1017,11 +1014,16 @@ $opts: FormOptions for this request filters for pages inheriting from ChangesListSpecialPage (in core: RecentChanges, RecentChangesLinked, and Watchlist). Generally, you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects. - When constructing them, you specify which group they belong to. You can reuse existing groups (accessed through $special->getFilterGroup), or create your own (ChangesListBooleanFilterGroup or ChangesListStringOptionsFilterGroup). If you create new groups, you must register them with $special->registerFilterGroup. +Note that this is called regardless of whether the user is currently using +the new (structured) or old (unstructured) filter UI. If you want your boolean +filter to show on both the new and old UI, specify all the supported fields. +These include showHide, label, and description. +See the constructor of each ChangesList* class for documentation of supported +fields. $special: ChangesListSpecialPage instance 'ChangeTagAfterDelete': Called after a change tag has been deleted (that is, diff --git a/docs/uidesign/monospace.html b/docs/uidesign/monospace.html index f2b988e219..cdaf580aa5 100644 --- a/docs/uidesign/monospace.html +++ b/docs/uidesign/monospace.html @@ -2,16 +2,25 @@ @@ -33,45 +42,138 @@ font besides just "monospace", those browsers will no longer treat it as monospace and use 0.8 x 16px = 13px instead.

+Additionally, it seems that textareas have their own font-size set in Chrome +(but not Firefox and other browsers), making them unaffected by this behavior. +

+

Below are various rendering:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
<pre><textarea>
 font-family: monospace;
 
+
+ +
 font-family: "Courier New";
 
+
+ +
 font-family: Courier;
 
+
+ +
 font-family: monospace, monospace;
 
+
+ +
 font-family: monospace, "Courier New";
 
+
+ +
 font-family: monospace, Courier;
 
+
+ +
 font-family: monospace, Verdana;
 
+
+ +
 font-family: monospace, DOESNOTEXISTREALLY;
 
+
+ +
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index 2adbc80f3b..75fcff3654 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -56,6 +56,7 @@ class AjaxDispatcher { /** * Load up our object with user supplied data + * @param Config $config */ function __construct( Config $config ) { $this->config = $config; diff --git a/includes/Block.php b/includes/Block.php index 0b17e93f3f..5a4c43e6ae 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -493,7 +493,6 @@ class Block { } $row = $this->getDatabaseArray( $dbw ); - $row['ipb_id'] = $dbw->nextSequenceValue( "ipblocks_ipb_id_seq" ); $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] ); $affected = $dbw->affectedRows(); @@ -1330,7 +1329,7 @@ class Block { * which in turn gives User::getName(). * * @param string|int|User|null $target - * @return array( User|String|null, Block::TYPE_ constant|null ) + * @return array [ User|String|null, Block::TYPE_ constant|null ] */ public static function parseTarget( $target ) { # We may have been through this before @@ -1397,7 +1396,7 @@ class Block { * Get the target and target type for this particular Block. Note that for autoblocks, * this returns the unredacted name; frontend functions need to call $block->getRedactedName() * in this situation. - * @return array( User|String, Block::TYPE_ constant ) + * @return array [ User|String, Block::TYPE_ constant ] * @todo FIXME: This should be an integral part of the Block member variables */ public function getTargetAndType() { diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index 9d692d71b3..f36c75800b 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -629,7 +629,7 @@ class CategoryViewer extends ContextSource { * @return string HTML */ private function pagingLinks( $first, $last, $type = '' ) { - $prevLink = $this->msg( 'prev-page' )->text(); + $prevLink = $this->msg( 'prev-page' )->escaped(); $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); if ( $first != '' ) { @@ -638,13 +638,13 @@ class CategoryViewer extends ContextSource { unset( $prevQuery["{$type}from"] ); $prevLink = $linkRenderer->makeKnownLink( $this->addFragmentToTitle( $this->title, $type ), - $prevLink, + new HtmlArmor( $prevLink ), [], $prevQuery ); } - $nextLink = $this->msg( 'next-page' )->text(); + $nextLink = $this->msg( 'next-page' )->escaped(); if ( $last != '' ) { $lastQuery = $this->query; @@ -652,7 +652,7 @@ class CategoryViewer extends ContextSource { unset( $lastQuery["{$type}until"] ); $nextLink = $linkRenderer->makeKnownLink( $this->addFragmentToTitle( $this->title, $type ), - $nextLink, + new HtmlArmor( $nextLink ), [], $lastQuery ); diff --git a/includes/CommentStore.php b/includes/CommentStore.php index 0c86c1e871..0d679d37db 100644 --- a/includes/CommentStore.php +++ b/includes/CommentStore.php @@ -29,6 +29,26 @@ use Wikimedia\Rdbms\IDatabase; */ class CommentStore { + /** + * Maximum length of a comment in UTF-8 characters. Longer comments will be truncated. + * @note This must be at least 255 and not greater than floor( MAX_COMMENT_LENGTH / 4 ). + */ + const COMMENT_CHARACTER_LIMIT = 1000; + + /** + * Maximum length of a comment in bytes. Longer comments will be truncated. + * @note This value is determined by the size of the underlying database field, + * currently BLOB in MySQL/MariaDB. + */ + const MAX_COMMENT_LENGTH = 65535; + + /** + * Maximum length of serialized data in bytes. Longer data will result in an exception. + * @note This value is determined by the size of the underlying database field, + * currently BLOB in MySQL/MariaDB. + */ + const MAX_DATA_LENGTH = 65535; + /** * Define fields that use temporary tables for transitional purposes * @var array Keys are '$key', values are arrays with four fields: @@ -68,15 +88,21 @@ class CommentStore { /** @var array|null Cache for `self::getJoin()` */ protected $joinCache = null; + /** @var Language Language to use for comment truncation */ + protected $lang; + /** * @param string $key A key such as "rev_comment" identifying the comment * field being fetched. + * @param Language $lang Language to use for comment truncation. Defaults + * to $wgContLang. */ - public function __construct( $key ) { - global $wgCommentTableSchemaMigrationStage; + public function __construct( $key, Language $lang = null ) { + global $wgCommentTableSchemaMigrationStage, $wgContLang; $this->key = $key; $this->stage = $wgCommentTableSchemaMigrationStage; + $this->lang = $lang ?: $wgContLang; } /** @@ -355,25 +381,18 @@ class CommentStore { * @return CommentStoreComment */ public function createComment( IDatabase $dbw, $comment, array $data = null ) { - global $wgContLang; - - if ( !$comment instanceof CommentStoreComment ) { - if ( $data !== null ) { - foreach ( $data as $k => $v ) { - if ( substr( $k, 0, 1 ) === '_' ) { - throw new InvalidArgumentException( 'Keys in $data beginning with "_" are reserved' ); - } - } - } - if ( $comment instanceof Message ) { - $message = clone $comment; - $text = $message->inLanguage( $wgContLang ) // Avoid $wgForceUIMsgAsContentMsg - ->setInterfaceMessageFlag( true ) - ->text(); - $comment = new CommentStoreComment( null, $text, $message, $data ); - } else { - $comment = new CommentStoreComment( null, $comment, null, $data ); + $comment = CommentStoreComment::newUnsavedComment( $comment, $data ); + + # Truncate comment in a Unicode-sensitive manner + $comment->text = $this->lang->truncate( $comment->text, self::MAX_COMMENT_LENGTH ); + if ( mb_strlen( $comment->text, 'UTF-8' ) > self::COMMENT_CHARACTER_LIMIT ) { + $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this->lang )->escaped(); + if ( mb_strlen( $ellipsis ) >= self::COMMENT_CHARACTER_LIMIT ) { + // WTF? + $ellipsis = '...'; } + $maxLength = self::COMMENT_CHARACTER_LIMIT - mb_strlen( $ellipsis, 'UTF-8' ); + $comment->text = mb_substr( $comment->text, 0, $maxLength, 'UTF-8' ) . $ellipsis; } if ( $this->stage > MIGRATION_OLD && !$comment->id ) { @@ -386,6 +405,11 @@ class CommentStore { } if ( $dbData !== null ) { $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK ); + $len = strlen( $dbData ); + if ( $len > self::MAX_DATA_LENGTH ) { + $max = self::MAX_DATA_LENGTH; + throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" ); + } } $hash = self::hash( $comment->text, $dbData ); @@ -400,11 +424,9 @@ class CommentStore { __METHOD__ ); if ( !$comment->id ) { - $comment->id = $dbw->nextSequenceValue( 'comment_comment_id_seq' ); $dbw->insert( 'comment', [ - 'comment_id' => $comment->id, 'comment_hash' => $hash, 'comment_text' => $comment->text, 'comment_data' => $dbData, @@ -432,7 +454,7 @@ class CommentStore { $comment = $this->createComment( $dbw, $comment, $data ); if ( $this->stage <= MIGRATION_WRITE_BOTH ) { - $fields[$this->key] = $comment->text; + $fields[$this->key] = $this->lang->truncate( $comment->text, 255 ); } if ( $this->stage >= MIGRATION_WRITE_BOTH ) { @@ -459,7 +481,7 @@ class CommentStore { } /** - * Prepare for the insertion of a row with a comment + * Insert a comment in preparation for a row that references it * * @note It's recommended to include both the call to this method and the * row insert in the same transaction. @@ -478,7 +500,7 @@ class CommentStore { } /** - * Prepare for the insertion of a row with a comment and temporary table + * Insert a comment in a temporary table in preparation for a row that references it * * This is currently needed for "rev_comment" and "img_description". In the * future that requirement will be removed. diff --git a/includes/CommentStoreComment.php b/includes/CommentStoreComment.php index afc1374223..7ed86d66c7 100644 --- a/includes/CommentStoreComment.php +++ b/includes/CommentStoreComment.php @@ -20,8 +20,6 @@ * @file */ -use Wikimedia\Rdbms\IDatabase; - /** * CommentStoreComment represents a comment stored by CommentStore. The fields * should be considered read-only. @@ -42,7 +40,7 @@ class CommentStoreComment { public $data; /** - * @private For use by CommentStore only + * @private For use by CommentStore only. Use self::newUnsavedComment() instead. * @param int|null $id * @param string $text * @param Message|null $message @@ -54,4 +52,39 @@ class CommentStoreComment { $this->message = $message ?: new RawMessage( '$1', [ $text ] ); $this->data = $data; } + + /** + * Create a new, unsaved CommentStoreComment + * + * @param string|Message|CommentStoreComment $comment Comment text or Message object. + * A CommentStoreComment is also accepted here, in which case it is returned unchanged. + * @param array|null $data Structured data to store. Keys beginning with '_' are reserved. + * Ignored if $comment is a CommentStoreComment. + * @return CommentStoreComment + */ + public static function newUnsavedComment( $comment, array $data = null ) { + global $wgContLang; + + if ( $comment instanceof CommentStoreComment ) { + return $comment; + } + + if ( $data !== null ) { + foreach ( $data as $k => $v ) { + if ( substr( $k, 0, 1 ) === '_' ) { + throw new InvalidArgumentException( 'Keys in $data beginning with "_" are reserved' ); + } + } + } + + if ( $comment instanceof Message ) { + $message = clone $comment; + $text = $message->inLanguage( $wgContLang ) // Avoid $wgForceUIMsgAsContentMsg + ->setInterfaceMessageFlag( true ) + ->text(); + return new CommentStoreComment( null, $text, $message, $data ); + } else { + return new CommentStoreComment( null, $comment, null, $data ); + } + } } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index cf3e569b2a..bd944d24d8 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -75,7 +75,7 @@ $wgConfigRegistry = [ * MediaWiki version number * @since 1.2 */ -$wgVersion = '1.30.0-alpha'; +$wgVersion = '1.31.0-alpha'; /** * Name of the site. It must be changed in LocalSettings.php @@ -1603,6 +1603,13 @@ $wgEnableEmail = true; */ $wgEnableUserEmail = true; +/** + * Set to true to enable user-to-user e-mail blacklist. + * + * @since 1.30 + */ +$wgEnableUserEmailBlacklist = false; + /** * If true put the sending user's email in a Reply-To header * instead of From (false). ($wgPasswordSender will be used as From.) @@ -2973,46 +2980,9 @@ $wgAllUnicodeFixes = false; $wgLegacyEncoding = false; /** - * Browser Blacklist for unicode non compliant browsers. Contains a list of - * regexps : "/regexp/" matching problematic browsers. These browsers will - * be served encoded unicode in the edit box instead of real unicode. + * @deprecated since 1.30, does nothing */ -$wgBrowserBlackList = [ - /** - * Netscape 2-4 detection - * The minor version may contain strings such as "Gold" or "SGoldC-SGI" - * Lots of non-netscape user agents have "compatible", so it's useful to check for that - * with a negative assertion. The [UIN] identifier specifies the level of security - * in a Netscape/Mozilla browser, checking for it rules out a number of fakers. - * The language string is unreliable, it is missing on NS4 Mac. - * - * Reference: http://www.psychedelix.com/agents/index.shtml - */ - '/^Mozilla\/2\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', - '/^Mozilla\/3\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', - '/^Mozilla\/4\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', - - /** - * MSIE on Mac OS 9 is teh sux0r, converts þ to , ð to , - * Þ to and Ð to - * - * Known useragents: - * - Mozilla/4.0 (compatible; MSIE 5.0; Mac_PowerPC) - * - Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC) - * - Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC) - * - [...] - * - * @link https://en.wikipedia.org/w/index.php?diff=12356041&oldid=12355864 - * @link https://en.wikipedia.org/wiki/Template%3AOS9 - */ - '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/', - - /** - * Google wireless transcoder, seems to eat a lot of chars alive - * https://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361 - */ - '/^Mozilla\/4\.0 \(compatible; MSIE 6.0; Windows NT 5.0; Google Wireless Transcoder;\)/' -]; +$wgBrowserBlackList = []; /** * If set to true, the MediaWiki 1.4 to 1.5 schema conversion will @@ -3394,8 +3364,8 @@ $wgExperimentalHtmlIds = false; /** * How should section IDs be encoded? * This array can contain 1 or 2 elements, each of them can be one of: - * - 'html5' is modern HTML5 style encoding with minimal escaping. Allows to - * display Unicode characters in many browsers' address bars. + * - 'html5' is modern HTML5 style encoding with minimal escaping. Displays Unicode + * characters in most browsers' address bars. * - 'legacy' is old MediaWiki-style encoding, e.g. 啤酒 turns into .E5.95.A4.E9.85.92 * - 'html5-legacy' corresponds to DEPRECATED $wgExperimentalHtmlIds mode. DO NOT use * it for anything but migration off that mode (see below). @@ -4920,6 +4890,7 @@ $wgDefaultUserOptions = [ 'previewontop' => 1, 'rcdays' => 7, 'rcenhancedfilters' => 0, + 'rcenhancedfilters-disable' => 0, 'rclimit' => 50, 'rows' => 25, // @deprecated since 1.29 No longer used in core 'showhiddencats' => 0, @@ -6175,8 +6146,8 @@ $wgTrxProfilerLimits = [ 'writes' => 0, 'readQueryTime' => 5 ], - // Deferred updates that run after HTTP response is sent - 'PostSend' => [ + // Deferred updates that run after HTTP response is sent for GET requests + 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'maxAffected' => 1000, @@ -6184,6 +6155,12 @@ $wgTrxProfilerLimits = [ 'masterConns' => 0, 'writes' => 0, ], + // Deferred updates that run after HTTP response is sent for POST requests + 'PostSend-POST' => [ + 'readQueryTime' => 5, + 'writeQueryTime' => 1, + 'maxAffected' => 1000 + ], // Background job runner 'JobRunner' => [ 'readQueryTime' => 30, @@ -6830,20 +6807,30 @@ $wgRCWatchCategoryMembership = false; $wgUseRCPatrol = true; /** - * Whether to show the new experimental views (like namespaces, tags, and users) in - * RecentChanges filters + * Whether a preference is displayed for structured change filters. + * If false, no preference is displayed and structured change filters are disabled. + * If true, structured change filters are *enabled* by default, and a preference is displayed + * that lets users disable them. + * + * Temporary variable during development and will be removed. + * + * @since 1.30 */ -$wgStructuredChangeFiltersEnableExperimentalViews = false; +$wgStructuredChangeFiltersShowPreference = false; /** - * Whether to allow users to use the experimental live update feature in the new RecentChanges UI + * Whether to enable RCFilters app on Special:Watchlist + * + * Temporary variable during development and will be removed. */ -$wgStructuredChangeFiltersEnableLiveUpdate = false; +$wgStructuredChangeFiltersOnWatchlist = false; /** - * Whether to enable RCFilters app on Special:Watchlist + * Polling rate, in seconds, used by the 'live update' and 'view newest' features + * of the RCFilters app on SpecialRecentChanges and Special:Watchlist. + * 0 to disable completely. */ -$wgStructuredChangeFiltersOnWatchlist = false; +$wgStructuredChangeFiltersLiveUpdatePollingRate = 3; /** * Use new page patrolling to check new pages on Special:Newpages @@ -7966,6 +7953,8 @@ $wgExemptFromUserRobotsControl = null; * machine-readable data via api.php * * See https://www.mediawiki.org/wiki/API + * + * @deprecated since 1.31 */ $wgEnableAPI = true; @@ -7973,6 +7962,8 @@ $wgEnableAPI = true; * Allow the API to be used to perform write operations * (page edits, rollback, etc.) when an authorised user * accesses it + * + * @deprecated since 1.31 */ $wgEnableWriteAPI = true; @@ -8727,6 +8718,18 @@ $wgCSPFalsePositiveUrls = [ 'https://ad.lkqd.net/vpaid/vpaid.js' => true, ]; +/** + * Shortest CIDR limits that can be checked in any individual range check + * at Special:Contributions. + * + * @var array + * @since 1.30 + */ +$wgRangeContributionsCIDRLimit = [ + 'IPv4' => 16, + 'IPv6' => 32, +]; + /** * The following variables define 3 user experience levels: * diff --git a/includes/Defines.php b/includes/Defines.php index 8ac84e5ab5..ca603e7684 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -31,6 +31,9 @@ use Wikimedia\Rdbms\IDatabase; */ # Obsolete aliases +/** + * @deprecated since 1.28 + */ define( 'DB_SLAVE', -1 ); /**@{ diff --git a/includes/EditPage.php b/includes/EditPage.php index 40913bbd12..3fc12ced8c 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -40,6 +40,11 @@ use Wikimedia\ScopedCallback; * headaches, which may be fatal. */ class EditPage { + /** + * Used for Unicode support checks + */ + const UNICODE_CHECK = 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ'; + /** * Status: Article successfully updated */ @@ -177,6 +182,11 @@ class EditPage { */ const AS_CANNOT_USE_CUSTOM_MODEL = 241; + /** + * Status: edit rejected because browser doesn't support Unicode. + */ + const AS_UNICODE_NOT_SUPPORTED = 242; + /** * HTML id and name for the beginning of the edit form. */ @@ -203,12 +213,18 @@ class EditPage { */ const POST_EDIT_COOKIE_DURATION = 1200; - /** @var Article */ + /** + * @deprecated for public usage since 1.30 use EditPage::getArticle() + * @var Article + */ public $mArticle; /** @var WikiPage */ private $page; - /** @var Title */ + /** + * @deprecated for public usage since 1.30 use EditPage::getTitle() + * @var Title + */ public $mTitle; /** @var null|Title */ @@ -220,16 +236,28 @@ class EditPage { /** @var bool */ public $isConflict = false; - /** @var bool */ + /** + * @deprecated since 1.30 use Title::isCssJsSubpage() + * @var bool + */ public $isCssJsSubpage = false; - /** @var bool */ + /** + * @deprecated since 1.30 use Title::isCssSubpage() + * @var bool + */ public $isCssSubpage = false; - /** @var bool */ + /** + * @deprecated since 1.30 use Title::isJsSubpage() + * @var bool + */ public $isJsSubpage = false; - /** @var bool */ + /** + * @deprecated since 1.30 + * @var bool + */ public $isWrongCaseCssJsPage = false; /** @var bool New page or new section */ @@ -413,6 +441,11 @@ class EditPage { */ private $isOldRev = false; + /** + * @var string|null What the user submitted in the 'wpUnicodeCheck' field + */ + private $unicodeCheck; + /** * @param Article $article */ @@ -469,6 +502,10 @@ class EditPage { */ public function getContextTitle() { if ( is_null( $this->mContextTitle ) ) { + wfDebugLog( + 'GlobalTitleFail', + __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.' + ); global $wgTitle; return $wgTitle; } else { @@ -482,6 +519,7 @@ class EditPage { * @deprecated since 1.30 */ public function isOouiEnabled() { + wfDeprecated( __METHOD__, '1.30' ); return true; } @@ -511,6 +549,7 @@ class EditPage { * @deprecated since 1.29, call edit directly */ public function submit() { + wfDeprecated( __METHOD__, '1.29' ); $this->edit(); } @@ -526,7 +565,6 @@ class EditPage { * the newly-edited page. */ public function edit() { - global $wgOut, $wgRequest, $wgUser; // Allow extensions to modify/prevent this form or submission if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) { return; @@ -534,13 +572,14 @@ class EditPage { wfDebug( __METHOD__ . ": enter\n" ); + $request = $this->context->getRequest(); // If they used redlink=1 and the page exists, redirect to the main article - if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) { - $wgOut->redirect( $this->mTitle->getFullURL() ); + if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) { + $this->context->getOutput()->redirect( $this->mTitle->getFullURL() ); return; } - $this->importFormData( $wgRequest ); + $this->importFormData( $request ); $this->firsttime = false; if ( wfReadOnly() && $this->save ) { @@ -569,9 +608,8 @@ class EditPage { wfDebug( __METHOD__ . ": User can't edit\n" ); // Auto-block user's IP if the account was "hard" blocked if ( !wfReadOnly() ) { - $user = $wgUser; - DeferredUpdates::addCallableUpdate( function () use ( $user ) { - $user->spreadAnyEditBlock(); + DeferredUpdates::addCallableUpdate( function () { + $this->context->getUser()->spreadAnyEditBlock(); } ); } $this->displayPermissionsError( $permErrors ); @@ -608,10 +646,11 @@ class EditPage { $this->isConflict = false; // css / js subpages of user pages get a special treatment + // The following member variables are deprecated since 1.30, + // the functions should be used instead. $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); $this->isCssSubpage = $this->mTitle->isCssSubpage(); $this->isJsSubpage = $this->mTitle->isJsSubpage(); - // @todo FIXME: Silly assignment. $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage(); # Show applicable editing introductions @@ -656,15 +695,14 @@ class EditPage { * @return array */ protected function getEditPermissionErrors( $rigor = 'secure' ) { - global $wgUser; - - $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor ); + $user = $this->context->getUser(); + $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor ); # Can this title be created? if ( !$this->mTitle->exists() ) { $permErrors = array_merge( $permErrors, wfArrayDiff2( - $this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ), + $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ), $permErrors ) ); @@ -701,13 +739,12 @@ class EditPage { * @throws PermissionsError */ protected function displayPermissionsError( array $permErrors ) { - global $wgRequest, $wgOut; - - if ( $wgRequest->getBool( 'redlink' ) ) { + $out = $this->context->getOutput(); + if ( $this->context->getRequest()->getBool( 'redlink' ) ) { // The edit page was reached via a red link. // Redirect to the article page and let them click the edit tab if // they really want a permission error. - $wgOut->redirect( $this->mTitle->getFullURL() ); + $out->redirect( $this->mTitle->getFullURL() ); return; } @@ -722,7 +759,7 @@ class EditPage { $this->displayViewSourcePage( $content, - $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) + $out->formatPermissionsErrorMessage( $permErrors, 'edit' ) ); } @@ -732,29 +769,28 @@ class EditPage { * @param string $errorMessage additional wikitext error message to display */ protected function displayViewSourcePage( Content $content, $errorMessage = '' ) { - global $wgOut; - - Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] ); + $out = $this->context->getOutput(); + Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setPageTitle( $this->context->msg( + $out->setRobotPolicy( 'noindex,nofollow' ); + $out->setPageTitle( $this->context->msg( 'viewsource-title', $this->getContextTitle()->getPrefixedText() ) ); - $wgOut->addBacklinkSubtitle( $this->getContextTitle() ); - $wgOut->addHTML( $this->editFormPageTop ); - $wgOut->addHTML( $this->editFormTextTop ); + $out->addBacklinkSubtitle( $this->getContextTitle() ); + $out->addHTML( $this->editFormPageTop ); + $out->addHTML( $this->editFormTextTop ); if ( $errorMessage !== '' ) { - $wgOut->addWikiText( $errorMessage ); - $wgOut->addHTML( "
\n" ); + $out->addWikiText( $errorMessage ); + $out->addHTML( "
\n" ); } # If the user made changes, preserve them when showing the markup # (This happens when a user is blocked during edit, for instance) if ( !$this->firsttime ) { $text = $this->textbox1; - $wgOut->addWikiMsg( 'viewyourtext' ); + $out->addWikiMsg( 'viewyourtext' ); } else { try { $text = $this->toEditText( $content ); @@ -763,20 +799,20 @@ class EditPage { # (e.g. for an old revision with a different model) $text = $content->serialize(); } - $wgOut->addWikiMsg( 'viewsourcetext' ); + $out->addWikiMsg( 'viewsourcetext' ); } - $wgOut->addHTML( $this->editFormTextBeforeContent ); + $out->addHTML( $this->editFormTextBeforeContent ); $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] ); - $wgOut->addHTML( $this->editFormTextAfterContent ); + $out->addHTML( $this->editFormTextAfterContent ); - $wgOut->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) ); + $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) ); - $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' ); + $out->addModules( 'mediawiki.action.edit.collapsibleFooter' ); - $wgOut->addHTML( $this->editFormTextBottom ); + $out->addHTML( $this->editFormTextBottom ); if ( $this->mTitle->exists() ) { - $wgOut->returnToMain( null, $this->mTitle ); + $out->returnToMain( null, $this->mTitle ); } } @@ -786,24 +822,32 @@ class EditPage { * @return bool */ protected function previewOnOpen() { - global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces; - if ( $wgRequest->getVal( 'preview' ) == 'yes' ) { + $config = $this->context->getConfig(); + $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' ); + $request = $this->context->getRequest(); + if ( $config->get( 'RawHtml' ) ) { + // If raw HTML is enabled, disable preview on open + // since it has to be posted with a token for + // security reasons + return false; + } + if ( $request->getVal( 'preview' ) == 'yes' ) { // Explicit override from request return true; - } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) { + } elseif ( $request->getVal( 'preview' ) == 'no' ) { // Explicit override from request return false; } elseif ( $this->section == 'new' ) { // Nothing *to* preview for new sections return false; - } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) - && $wgUser->getOption( 'previewonfirst' ) + } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() ) + && $this->context->getUser()->getOption( 'previewonfirst' ) ) { // Standard preference behavior return true; } elseif ( !$this->mTitle->exists() - && isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) - && $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] + && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] ) + && $previewOnOpenNamespaces[$this->mTitle->getNamespace()] ) { // Categories are special return true; @@ -850,8 +894,6 @@ class EditPage { * @throws ErrorPageError */ public function importFormData( &$request ) { - global $wgContLang, $wgUser; - # Section edit can come from either the form or a link $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); @@ -865,7 +907,7 @@ class EditPage { # These fields need to be checked for encoding. # Also remove trailing whitespace, but don't remove _initial_ # whitespace from the text boxes. This may be significant formatting. - $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' ); + $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) ); if ( !$request->getCheck( 'wpTextbox2' ) ) { // Skip this if wpTextbox2 has input, it indicates that we came // from a conflict page with raw page text, not a custom form @@ -876,8 +918,9 @@ class EditPage { } } - # Truncate for whole multibyte characters - $this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 ); + $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' ); + + $this->summary = $request->getText( 'wpSummary' ); # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for @@ -889,7 +932,7 @@ class EditPage { # currently doing double duty as both edit summary and section title. Right now this # is just to allow API edits to work around this limitation, but this should be # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312). - $this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 ); + $this->sectiontitle = $request->getText( 'wpSectionTitle' ); $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle ); $this->edittime = $request->getVal( 'wpEdittime' ); @@ -961,14 +1004,15 @@ class EditPage { $this->minoredit = $request->getCheck( 'wpMinoredit' ); $this->watchthis = $request->getCheck( 'wpWatchthis' ); + $user = $this->context->getUser(); # Don't force edit summaries when a user is editing their own user or talk page if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) - && $this->mTitle->getText() == $wgUser->getName() + && $this->mTitle->getText() == $user->getName() ) { $this->allowBlankSummary = true; } else { $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) - || !$wgUser->getOption( 'forceeditsummary' ); + || !$user->getOption( 'forceeditsummary' ); } $this->autoSumm = $request->getText( 'wpAutoSummary' ); @@ -1084,7 +1128,6 @@ class EditPage { * @return bool If the requested section is valid */ public function initialiseForm() { - global $wgUser; $this->edittime = $this->page->getTimestamp(); $this->editRevId = $this->page->getLatest(); @@ -1094,19 +1137,20 @@ class EditPage { } $this->textbox1 = $this->toEditText( $content ); + $user = $this->context->getUser(); // activate checkboxes if user wants them to be always active # Sort out the "watch" checkbox - if ( $wgUser->getOption( 'watchdefault' ) ) { + if ( $user->getOption( 'watchdefault' ) ) { # Watch all edits $this->watchthis = true; - } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { # Watch creations $this->watchthis = true; - } elseif ( $wgUser->isWatched( $this->mTitle ) ) { + } elseif ( $user->isWatched( $this->mTitle ) ) { # Already watched $this->watchthis = true; } - if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) { + if ( $user->getOption( 'minordefault' ) && !$this->isNew ) { $this->minoredit = true; } if ( $this->textbox1 === false ) { @@ -1123,10 +1167,12 @@ class EditPage { * @since 1.21 */ protected function getContentObject( $def_content = null ) { - global $wgOut, $wgRequest, $wgUser, $wgContLang; + global $wgContLang; $content = false; + $user = $this->context->getUser(); + $request = $this->context->getRequest(); // For message page not locally set, use the i18n message. // For other non-existent articles, use preload text if any. if ( !$this->mTitle->exists() || $this->section == 'new' ) { @@ -1138,10 +1184,10 @@ class EditPage { } if ( $content === false ) { # If requested, preload some text. - $preload = $wgRequest->getVal( 'preload', + $preload = $request->getVal( 'preload', // Custom preload text for new sections $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' ); - $params = $wgRequest->getArray( 'preloadparams', [] ); + $params = $request->getArray( 'preloadparams', [] ); $content = $this->getPreloadedContent( $preload, $params ); } @@ -1149,15 +1195,15 @@ class EditPage { } else { if ( $this->section != '' ) { // Get section edit text (returns $def_text for invalid sections) - $orig = $this->getOriginalContent( $wgUser ); + $orig = $this->getOriginalContent( $user ); $content = $orig ? $orig->getSection( $this->section ) : null; if ( !$content ) { $content = $def_content; } } else { - $undoafter = $wgRequest->getInt( 'undoafter' ); - $undo = $wgRequest->getInt( 'undo' ); + $undoafter = $request->getInt( 'undoafter' ); + $undo = $request->getInt( 'undo' ); if ( $undo > 0 && $undoafter > 0 ) { $undorev = Revision::newFromId( $undo ); @@ -1177,8 +1223,8 @@ class EditPage { $undoMsg = 'failure'; } else { $oldContent = $this->page->getContent( Revision::RAW ); - $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); - $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts ); + $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); + $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts ); if ( $newContent->getModel() !== $oldContent->getModel() ) { // The undo may change content // model if its reverting the top @@ -1231,14 +1277,15 @@ class EditPage { $undoMsg = 'norev'; } + $out = $this->context->getOutput(); // Messages: undo-success, undo-failure, undo-norev, undo-nochange $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}"; - $this->editFormPageTop .= $wgOut->parse( "
" . + $this->editFormPageTop .= $out->parse( "
" . $this->context->msg( 'undo-' . $undoMsg )->plain() . '
', true, /* interface */true ); } if ( $content === false ) { - $content = $this->getOriginalContent( $wgUser ); + $content = $this->getOriginalContent( $user ); } } } @@ -1364,8 +1411,6 @@ class EditPage { * @since 1.21 */ protected function getPreloadedContent( $preload, $params = [] ) { - global $wgUser; - if ( !empty( $this->mPreloadContent ) ) { return $this->mPreloadContent; } @@ -1376,9 +1421,10 @@ class EditPage { return $handler->makeEmptyContent(); } + $user = $this->context->getUser(); $title = Title::newFromText( $preload ); # Check for existence to avoid getting MediaWiki:Noarticletext - if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) { + if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) { // TODO: somehow show a warning to the user! return $handler->makeEmptyContent(); } @@ -1387,14 +1433,14 @@ class EditPage { if ( $page->isRedirect() ) { $title = $page->getRedirectTarget(); # Same as before - if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) { + if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) { // TODO: somehow show a warning to the user! return $handler->makeEmptyContent(); } $page = WikiPage::factory( $title ); } - $parserOptions = ParserOptions::newFromUser( $wgUser ); + $parserOptions = ParserOptions::newFromUser( $user ); $content = $page->getContent( Revision::RAW ); if ( !$content ) { @@ -1428,10 +1474,10 @@ class EditPage { * @private */ public function tokenOk( &$request ) { - global $wgUser; $token = $request->getVal( 'wpEditToken' ); - $this->mTokenOk = $wgUser->matchEditToken( $token ); - $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token ); + $user = $this->context->getUser(); + $this->mTokenOk = $user->matchEditToken( $token ); + $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token ); return $this->mTokenOk; } @@ -1460,7 +1506,7 @@ class EditPage { $val = 'restored'; } - $response = RequestContext::getMain()->getRequest()->response(); + $response = $this->context->getRequest()->response(); $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION ); } @@ -1471,10 +1517,8 @@ class EditPage { * @return Status The resulting status object. */ public function attemptSave( &$resultDetails = false ) { - global $wgUser; - # Allow bots to exempt some edits from bot flagging - $bot = $wgUser->isAllowed( 'bot' ) && $this->bot; + $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot; $status = $this->internalAttemptSave( $resultDetails, $bot ); Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] ); @@ -1486,9 +1530,7 @@ class EditPage { * Log when a page was successfully saved after the edit conflict view */ private function incrementResolvedConflicts() { - global $wgRequest; - - if ( $wgRequest->getText( 'mode' ) !== 'conflict' ) { + if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) { return; } @@ -1506,8 +1548,6 @@ class EditPage { * @return bool False, if output is done, true if rest of the form should be displayed */ private function handleStatus( Status $status, $resultDetails ) { - global $wgUser, $wgOut; - /** * @todo FIXME: once the interface for internalAttemptSave() is made * nicer, this should use the message in $status @@ -1523,9 +1563,11 @@ class EditPage { } } + $out = $this->context->getOutput(); + // "wpExtraQueryRedirect" is a hidden input to modify // after save URL and is not used by actual edit form - $request = RequestContext::getMain()->getRequest(); + $request = $this->context->getRequest(); $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' ); switch ( $status->value ) { @@ -1546,7 +1588,8 @@ class EditPage { case self::AS_CANNOT_USE_CUSTOM_MODEL: case self::AS_PARSE_ERROR: - $wgOut->addWikiText( '
' . "\n" . $status->getWikiText() . '
' ); + case self::AS_UNICODE_NOT_SUPPORTED: + $out->addWikiText( '
' . "\n" . $status->getWikiText() . '
' ); return true; case self::AS_SUCCESS_NEW_ARTICLE: @@ -1559,7 +1602,7 @@ class EditPage { } } $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : ''; - $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor ); + $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor ); return false; case self::AS_SUCCESS_UPDATE: @@ -1587,7 +1630,7 @@ class EditPage { } } - $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor ); + $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor ); return false; case self::AS_SPAM_ERROR: @@ -1595,7 +1638,7 @@ class EditPage { return false; case self::AS_BLOCKED_PAGE_FOR_USER: - throw new UserBlockedError( $wgUser->getBlock() ); + throw new UserBlockedError( $this->context->getUser()->getBlock() ); case self::AS_IMAGE_REDIRECT_ANON: case self::AS_IMAGE_REDIRECT_LOGGED: @@ -1649,7 +1692,7 @@ class EditPage { // Run new style post-section-merge edit filter if ( !Hooks::run( 'EditFilterMergedContent', - [ $this->mArticle->getContext(), $content, $status, $this->summary, + [ $this->context, $content, $status, $this->summary, $user, $this->minoredit ] ) ) { # Error messages etc. could be handled within the hook... @@ -1734,10 +1777,8 @@ class EditPage { * time. */ public function internalAttemptSave( &$result, $bot = false ) { - global $wgUser, $wgRequest, $wgMaxArticleSize; - global $wgContentHandlerUseDB; - $status = Status::newGood(); + $user = $this->context->getUser(); if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) { wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); @@ -1746,11 +1787,18 @@ class EditPage { return $status; } - $spam = $wgRequest->getText( 'wpAntispam' ); + if ( $this->unicodeCheck !== self::UNICODE_CHECK ) { + $status->fatal( 'unicode-support-fail' ); + $status->value = self::AS_UNICODE_NOT_SUPPORTED; + return $status; + } + + $request = $this->context->getRequest(); + $spam = $request->getText( 'wpAntispam' ); if ( $spam !== '' ) { wfDebugLog( 'SimpleAntiSpam', - $wgUser->getName() . + $user->getName() . ' editing "' . $this->mTitle->getPrefixedText() . '" submitted bogus field "' . @@ -1779,9 +1827,9 @@ class EditPage { # Check image redirect if ( $this->mTitle->getNamespace() == NS_FILE && $textbox_content->isRedirect() && - !$wgUser->isAllowed( 'upload' ) + !$user->isAllowed( 'upload' ) ) { - $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; + $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; $status->setResult( false, $code ); return $status; @@ -1806,7 +1854,7 @@ class EditPage { } if ( $match !== false ) { $result['spam'] = $match; - $ip = $wgRequest->getIP(); + $ip = $request->getIP(); $pdbk = $this->mTitle->getPrefixedDBkey(); $match = str_replace( "\n", '', $match ); wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); @@ -1829,10 +1877,10 @@ class EditPage { return $status; } - if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { + if ( $user->isBlockedFrom( $this->mTitle, false ) ) { // Auto-block user's IP if the account was "hard" blocked if ( !wfReadOnly() ) { - $wgUser->spreadAnyEditBlock(); + $user->spreadAnyEditBlock(); } # Check block state against master, thus 'false'. $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER ); @@ -1840,15 +1888,17 @@ class EditPage { } $this->contentLength = strlen( $this->textbox1 ); - if ( $this->contentLength > $wgMaxArticleSize * 1024 ) { + $config = $this->context->getConfig(); + $maxArticleSize = $config->get( 'MaxArticleSize' ); + if ( $this->contentLength > $maxArticleSize * 1024 ) { // Error will be displayed by showEditForm() $this->tooBig = true; $status->setResult( false, self::AS_CONTENT_TOO_BIG ); return $status; } - if ( !$wgUser->isAllowed( 'edit' ) ) { - if ( $wgUser->isAnon() ) { + if ( !$user->isAllowed( 'edit' ) ) { + if ( $user->isAnon() ) { $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON ); return $status; } else { @@ -1860,19 +1910,19 @@ class EditPage { $changingContentModel = false; if ( $this->contentModel !== $this->mTitle->getContentModel() ) { - if ( !$wgContentHandlerUseDB ) { + if ( !$config->get( 'ContentHandlerUseDB' ) ) { $status->fatal( 'editpage-cannot-use-custom-model' ); $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL; return $status; - } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) { + } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) { $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); return $status; } // Make sure the user can edit the page under the new content model too $titleWithNewContentModel = clone $this->mTitle; $titleWithNewContentModel->setContentModel( $this->contentModel ); - if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $wgUser ) - || !$titleWithNewContentModel->userCan( 'edit', $wgUser ) + if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $user ) + || !$titleWithNewContentModel->userCan( 'edit', $user ) ) { $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); return $status; @@ -1884,7 +1934,7 @@ class EditPage { if ( $this->changeTags ) { $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange( - $this->changeTags, $wgUser ); + $this->changeTags, $user ); if ( !$changeTagsStatus->isOK() ) { $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR; return $changeTagsStatus; @@ -1896,8 +1946,8 @@ class EditPage { $status->value = self::AS_READ_ONLY_PAGE; return $status; } - if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) - || ( $changingContentModel && $wgUser->pingLimiter( 'editcontentmodel' ) ) + if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 ) + || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) ) ) { $status->fatal( 'actionthrottledtext' ); $status->value = self::AS_RATE_LIMITED; @@ -1918,7 +1968,7 @@ class EditPage { if ( $new ) { // Late check for create permission, just in case *PARANOIA* - if ( !$this->mTitle->userCan( 'create', $wgUser ) ) { + if ( !$this->mTitle->userCan( 'create', $user ) ) { $status->fatal( 'nocreatetext' ); $status->value = self::AS_NO_CREATE_PERMISSION; wfDebug( __METHOD__ . ": no create permission\n" ); @@ -1942,7 +1992,7 @@ class EditPage { return $status; } - if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) { + if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) { return $status; } @@ -1978,7 +2028,7 @@ class EditPage { ) { $this->isConflict = true; if ( $this->section == 'new' ) { - if ( $this->page->getUserText() == $wgUser->getName() && + if ( $this->page->getUserText() == $user->getName() && $this->page->getComment() == $this->newSectionSummary() ) { // Probably a duplicate submission of a new comment. @@ -1994,7 +2044,7 @@ class EditPage { } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER, $this->mTitle->getArticleID(), - $wgUser->getId(), $this->edittime + $user->getId(), $this->edittime ) ) { # Suppress edit conflict with self, except for section edits where merging is required. @@ -2064,7 +2114,7 @@ class EditPage { return $status; } - if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) { + if ( !$this->runPostMergeFilters( $content, $status, $user ) ) { return $status; } @@ -2085,7 +2135,7 @@ class EditPage { return $status; } } elseif ( !$this->allowBlankSummary - && !$content->equals( $this->getOriginalContent( $wgUser ) ) + && !$content->equals( $this->getOriginalContent( $user ) ) && !$content->isRedirect() && md5( $this->summary ) == $this->autoSumm ) { @@ -2139,7 +2189,7 @@ class EditPage { // Check for length errors again now that the section is merged in $this->contentLength = strlen( $this->toEditText( $content ) ); - if ( $this->contentLength > $wgMaxArticleSize * 1024 ) { + if ( $this->contentLength > $maxArticleSize * 1024 ) { $this->tooBig = true; $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); return $status; @@ -2155,7 +2205,7 @@ class EditPage { $this->summary, $flags, false, - $wgUser, + $user, $content->getDefaultFormat(), $this->changeTags, $this->undidRev @@ -2179,7 +2229,7 @@ class EditPage { $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' ); if ( $result['nullEdit'] ) { // We don't know if it was a null edit until now, so increment here - $wgUser->pingLimiter( 'linkpurge' ); + $user->pingLimiter( 'linkpurge' ); } $result['redirect'] = $content->isRedirect(); @@ -2188,7 +2238,7 @@ class EditPage { // If the content model changed, add a log entry if ( $changingContentModel ) { $this->addContentModelChangeLogEntry( - $wgUser, + $user, $new ? false : $oldContentModel, $this->contentModel, $this->summary @@ -2222,13 +2272,11 @@ class EditPage { * Register the change of watch status */ protected function updateWatchlist() { - global $wgUser; - - if ( !$wgUser->isLoggedIn() ) { + $user = $this->context->getUser(); + if ( !$user->isLoggedIn() ) { return; } - $user = $wgUser; $title = $this->mTitle; $watch = $this->watchthis; // Do this in its own transaction to reduce contention... @@ -2342,29 +2390,30 @@ class EditPage { } public function setHeaders() { - global $wgOut, $wgUser, $wgAjaxEditStash; + $out = $this->context->getOutput(); - $wgOut->addModules( 'mediawiki.action.edit' ); - $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' ); + $out->addModules( 'mediawiki.action.edit' ); + $out->addModuleStyles( 'mediawiki.action.edit.styles' ); - if ( $wgUser->getOption( 'showtoolbar' ) ) { + $user = $this->context->getUser(); + if ( $user->getOption( 'showtoolbar' ) ) { // The addition of default buttons is handled by getEditToolbar() which // has its own dependency on this module. The call here ensures the module // is loaded in time (it has position "top") for other modules to register // buttons (e.g. extensions, gadgets, user scripts). - $wgOut->addModules( 'mediawiki.toolbar' ); + $out->addModules( 'mediawiki.toolbar' ); } - if ( $wgUser->getOption( 'uselivepreview' ) ) { - $wgOut->addModules( 'mediawiki.action.edit.preview' ); + if ( $user->getOption( 'uselivepreview' ) ) { + $out->addModules( 'mediawiki.action.edit.preview' ); } - if ( $wgUser->getOption( 'useeditwarning' ) ) { - $wgOut->addModules( 'mediawiki.action.edit.editWarning' ); + if ( $user->getOption( 'useeditwarning' ) ) { + $out->addModules( 'mediawiki.action.edit.editWarning' ); } # Enabled article-related sidebar, toplinks, etc. - $wgOut->setArticleRelated( true ); + $out->setArticleRelated( true ); $contextTitle = $this->getContextTitle(); if ( $this->isConflict ) { @@ -2387,12 +2436,12 @@ class EditPage { if ( $displayTitle === false ) { $displayTitle = $contextTitle->getPrefixedText(); } - $wgOut->setPageTitle( $this->context->msg( $msg, $displayTitle ) ); + $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) ); # Transmit the name of the message to JavaScript for live preview # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys - $wgOut->addJsConfigVars( [ + $out->addJsConfigVars( [ 'wgEditMessage' => $msg, - 'wgAjaxEditStash' => $wgAjaxEditStash, + 'wgAjaxEditStash' => $this->context->getConfig()->get( 'AjaxEditStash' ), ] ); } @@ -2400,16 +2449,16 @@ class EditPage { * Show all applicable editing introductions */ protected function showIntro() { - global $wgOut, $wgUser; if ( $this->suppressIntro ) { return; } + $out = $this->context->getOutput(); $namespace = $this->mTitle->getNamespace(); if ( $namespace == NS_MEDIAWIKI ) { # Show a warning if editing an interface message - $wgOut->wrapWikiMsg( "
\n$1\n
", 'editinginterface' ); + $out->wrapWikiMsg( "
\n$1\n
", 'editinginterface' ); # If this is a default message (but not css or js), # show a hint that it is translatable on translatewiki.net if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS ) @@ -2417,7 +2466,7 @@ class EditPage { ) { $defaultMessageText = $this->mTitle->getDefaultMessageText(); if ( $defaultMessageText !== false ) { - $wgOut->wrapWikiMsg( "
\n$1\n
", + $out->wrapWikiMsg( "
\n$1\n
", 'translateinterface' ); } } @@ -2429,11 +2478,11 @@ class EditPage { # there must be a description url to show a hint to shared repo if ( $descUrl ) { if ( !$this->mTitle->exists() ) { - $wgOut->wrapWikiMsg( "
\n$1\n
", [ + $out->wrapWikiMsg( "
\n$1\n
", [ 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl ] ); } else { - $wgOut->wrapWikiMsg( "
\n$1\n
", [ + $out->wrapWikiMsg( "
\n$1\n
", [ 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl ] ); } @@ -2449,12 +2498,12 @@ class EditPage { $ip = User::isIP( $username ); $block = Block::newFromTarget( $user, $user ); if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist - $wgOut->wrapWikiMsg( "
\n$1\n
", + $out->wrapWikiMsg( "
\n$1\n
", [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] ); } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { # Show log extract if the user is currently blocked LogEventsList::showLogExtract( - $wgOut, + $out, 'block', MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(), '', @@ -2474,8 +2523,8 @@ class EditPage { $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl( $this->context->msg( 'helppage' )->inContentLanguage()->text() ) ); - if ( $wgUser->isLoggedIn() ) { - $wgOut->wrapWikiMsg( + if ( $this->context->getUser()->isLoggedIn() ) { + $out->wrapWikiMsg( // Suppress the external link icon, consider the help url an internal one "
\n$1\n
", [ @@ -2484,7 +2533,7 @@ class EditPage { ] ); } else { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( // Suppress the external link icon, consider the help url an internal one "
\n$1\n
", [ @@ -2498,7 +2547,7 @@ class EditPage { if ( !$this->mTitle->exists() ) { $dbr = wfGetDB( DB_REPLICA ); - LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle, + LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle, '', [ 'lim' => 10, @@ -2519,9 +2568,8 @@ class EditPage { if ( $this->editintro ) { $title = Title::newFromText( $this->editintro ); if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) { - global $wgOut; // Added using template syntax, to take 's into account. - $wgOut->addWikiTextTitleTidy( + $this->context->getOutput()->addWikiTextTitleTidy( '
{{:' . $title->getFullText() . '}}
', $this->mTitle ); @@ -2593,7 +2641,7 @@ class EditPage { } /** - * Send the edit form and related headers to $wgOut + * Send the edit form and related headers to OutputPage * @param callable|null $formCallback That takes an OutputPage parameter; will be called * during form output near the top, for captchas and the like. * @@ -2601,8 +2649,6 @@ class EditPage { * use the EditPage::showEditForm:fields hook instead. */ public function showEditForm( $formCallback = null ) { - global $wgOut, $wgUser; - # need to parse the preview early so that we know which templates are used, # otherwise users with "show preview after edit box" will get a blank list # we parse this near the beginning so that setHeaders can do the title @@ -2612,9 +2658,11 @@ class EditPage { $previewOutput = $this->getPreviewText(); } + $out = $this->context->getOutput(); + // Avoid PHP 7.1 warning of passing $this by reference $editPage = $this; - Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$wgOut ] ); + Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] ); $this->setHeaders(); @@ -2627,19 +2675,20 @@ class EditPage { // We use $this->section to much before this and getVal('wgSection') directly in other places // at this point we can't reset $this->section to '' to fallback to non-section editing. // Someone is welcome to try refactoring though - $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' ); + $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' ); return; } $this->showHeader(); - $wgOut->addHTML( $this->editFormPageTop ); + $out->addHTML( $this->editFormPageTop ); - if ( $wgUser->getOption( 'previewontop' ) ) { + $user = $this->context->getUser(); + if ( $user->getOption( 'previewontop' ) ) { $this->displayPreviewArea( $previewOutput, true ); } - $wgOut->addHTML( $this->editFormTextTop ); + $out->addHTML( $this->editFormTextTop ); $showToolbar = true; if ( $this->wasDeletedSinceLastEdit() ) { @@ -2648,14 +2697,14 @@ class EditPage { // Add an confirmation checkbox and explanation. $showToolbar = false; } else { - $wgOut->wrapWikiMsg( "
\n$1\n
", + $out->wrapWikiMsg( "
\n$1\n
", 'deletedwhileediting' ); } } // @todo add EditForm plugin interface and use it here! // search for textarea1 and textarea2, and allow EditForm to override all uses. - $wgOut->addHTML( Html::openElement( + $out->addHTML( Html::openElement( 'form', [ 'class' => 'mw-editform', @@ -2669,11 +2718,14 @@ class EditPage { if ( is_callable( $formCallback ) ) { wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' ); - call_user_func_array( $formCallback, [ &$wgOut ] ); + call_user_func_array( $formCallback, [ &$out ] ); } + // Add a check for Unicode support + $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) ); + // Add an empty field to trip up spambots - $wgOut->addHTML( + $out->addHTML( Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] ) . Html::rawElement( 'label', @@ -2694,7 +2746,7 @@ class EditPage { // Avoid PHP 7.1 warning of passing $this by reference $editPage = $this; - Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$wgOut ] ); + Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] ); // Put these up at the top to ensure they aren't lost on early form submission $this->showFormBeforeText(); @@ -2708,7 +2760,7 @@ class EditPage { $key = $comment === '' ? 'confirmrecreate-noreason' : 'confirmrecreate'; - $wgOut->addHTML( + $out->addHTML( '
' . $this->context->msg( $key, $username, "$comment" )->parse() . Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false, @@ -2720,7 +2772,7 @@ class EditPage { # When the summary is hidden, also hide them on preview/show changes if ( $this->nosummary ) { - $wgOut->addHTML( Html::hidden( 'nosummary', true ) ); + $out->addHTML( Html::hidden( 'nosummary', true ) ); } # If a blank edit summary was previously provided, and the appropriate @@ -2731,15 +2783,15 @@ class EditPage { # For a bit more sophisticated detection of blank summaries, hash the # automatic one and pass that in the hidden field wpAutoSummary. if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) { - $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) ); + $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) ); } if ( $this->undidRev ) { - $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) ); + $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) ); } if ( $this->selfRedirect ) { - $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) ); + $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) ); } if ( $this->hasPresetSummary ) { @@ -2750,29 +2802,29 @@ class EditPage { } $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); - $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) ); + $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) ); - $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) ); - $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) ); + $out->addHTML( Html::hidden( 'oldid', $this->oldid ) ); + $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) ); - $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) ); - $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) ); + $out->addHTML( Html::hidden( 'format', $this->contentFormat ) ); + $out->addHTML( Html::hidden( 'model', $this->contentModel ) ); - $wgOut->enableOOUI(); + $out->enableOOUI(); if ( $this->section == 'new' ) { $this->showSummaryInput( true, $this->summary ); - $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) ); + $out->addHTML( $this->getSummaryPreview( true, $this->summary ) ); } - $wgOut->addHTML( $this->editFormTextBeforeContent ); + $out->addHTML( $this->editFormTextBeforeContent ); - if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) { - $wgOut->addHTML( self::getEditToolbar( $this->mTitle ) ); + if ( !$this->mTitle->isCssJsSubpage() && $showToolbar && $user->getOption( 'showtoolbar' ) ) { + $out->addHTML( self::getEditToolbar( $this->mTitle ) ); } if ( $this->blankArticle ) { - $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) ); + $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) ); } if ( $this->isConflict ) { @@ -2790,7 +2842,7 @@ class EditPage { $this->showContentForm(); } - $wgOut->addHTML( $this->editFormTextAfterContent ); + $out->addHTML( $this->editFormTextAfterContent ); $this->showStandardInputs(); @@ -2800,17 +2852,17 @@ class EditPage { $this->showEditTools(); - $wgOut->addHTML( $this->editFormTextAfterTools . "\n" ); + $out->addHTML( $this->editFormTextAfterTools . "\n" ); - $wgOut->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) ); + $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) ); - $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ], + $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ], Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) ); - $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ], + $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ], self::getPreviewLimitReport( $this->mParserOutput ) ) ); - $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' ); + $out->addModules( 'mediawiki.action.edit.collapsibleFooter' ); if ( $this->isConflict ) { try { @@ -2823,7 +2875,7 @@ class EditPage { $this->contentFormat, $ex->getMessage() ); - $wgOut->addWikiText( '
' . $msg->text() . '
' ); + $out->addWikiText( '
' . $msg->text() . '
' ); } } @@ -2837,14 +2889,14 @@ class EditPage { } else { $mode = 'text'; } - $wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) ); + $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) ); // Marker for detecting truncated form data. This must be the last // parameter sent in order to be of use, so do not move me. - $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) ); - $wgOut->addHTML( $this->editFormTextBottom . "\n\n" ); + $out->addHTML( Html::hidden( 'wpUltimateParam', true ) ); + $out->addHTML( $this->editFormTextBottom . "\n\n" ); - if ( !$wgUser->getOption( 'previewontop' ) ) { + if ( !$user->getOption( 'previewontop' ) ) { $this->displayPreviewArea( $previewOutput, false ); } } @@ -2891,11 +2943,10 @@ class EditPage { } protected function showHeader() { - global $wgOut, $wgUser; - global $wgAllowUserCss, $wgAllowUserJs; - + $out = $this->context->getOutput(); + $user = $this->context->getUser(); if ( $this->isConflict ) { - $this->addExplainConflictHeader( $wgOut ); + $this->addExplainConflictHeader( $out ); $this->editRevId = $this->page->getLatest(); } else { if ( $this->section != '' && $this->section != 'new' ) { @@ -2907,46 +2958,42 @@ class EditPage { } } - $buttonLabel = $this->context->msg( $this->getSaveButtonLabel() )->text(); + $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text(); if ( $this->missingComment ) { - $wgOut->wrapWikiMsg( "
\n$1\n
", 'missingcommenttext' ); + $out->wrapWikiMsg( "
\n$1\n
", 'missingcommenttext' ); } if ( $this->missingSummary && $this->section != 'new' ) { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( "
\n$1\n
", [ 'missingsummary', $buttonLabel ] ); } if ( $this->missingSummary && $this->section == 'new' ) { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( "
\n$1\n
", [ 'missingcommentheader', $buttonLabel ] ); } if ( $this->blankArticle ) { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( "
\n$1\n
", [ 'blankarticle', $buttonLabel ] ); } if ( $this->selfRedirect ) { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( "
\n$1\n
", [ 'selfredirect', $buttonLabel ] ); } if ( $this->hookError !== '' ) { - $wgOut->addWikiText( $this->hookError ); - } - - if ( !$this->checkUnicodeCompliantBrowser() ) { - $wgOut->addWikiMsg( 'nonunicodebrowser' ); + $out->addWikiText( $this->hookError ); } if ( $this->section != 'new' ) { @@ -2954,13 +3001,13 @@ class EditPage { if ( $revision ) { // Let sysop know that this will make private content public if saved - if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) { - $wgOut->wrapWikiMsg( + if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) { + $out->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( "\n", 'rev-deleted-text-view' ); @@ -2968,26 +3015,26 @@ class EditPage { if ( !$revision->isCurrent() ) { $this->mArticle->setOldSubtitle( $revision->getId() ); - $wgOut->addWikiMsg( 'editingold' ); + $out->addWikiMsg( 'editingold' ); $this->isOldRev = true; } } elseif ( $this->mTitle->exists() ) { // Something went wrong - $wgOut->wrapWikiMsg( "
\n$1\n
\n", + $out->wrapWikiMsg( "
\n$1\n
\n", [ 'missing-revision', $this->oldid ] ); } } } if ( wfReadOnly() ) { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( "
\n$1\n
", [ 'readonlywarning', wfReadOnlyReason() ] ); - } elseif ( $wgUser->isAnon() ) { + } elseif ( $user->isAnon() ) { if ( $this->formtype != 'preview' ) { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( "
\n$1\n
", [ 'anoneditwarning', // Log-in link @@ -3001,33 +3048,35 @@ class EditPage { ] ); } else { - $wgOut->wrapWikiMsg( "
\n$1
", + $out->wrapWikiMsg( "
\n$1
", 'anonpreviewwarning' ); } } else { - if ( $this->isCssJsSubpage ) { + if ( $this->mTitle->isCssJsSubpage() ) { # Check the skin exists - if ( $this->isWrongCaseCssJsPage ) { - $wgOut->wrapWikiMsg( + if ( $this->isWrongCaseCssJsPage() ) { + $out->wrapWikiMsg( "
\n$1\n
", [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ] ); } - if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) { - $wgOut->wrapWikiMsg( '
$1
', - $this->isCssSubpage ? 'usercssispublic' : 'userjsispublic' + if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) { + $isCssSubpage = $this->mTitle->isCssSubpage(); + $out->wrapWikiMsg( '
$1
', + $isCssSubpage ? 'usercssispublic' : 'userjsispublic' ); if ( $this->formtype !== 'preview' ) { - if ( $this->isCssSubpage && $wgAllowUserCss ) { - $wgOut->wrapWikiMsg( + $config = $this->context->getConfig(); + if ( $isCssSubpage && $config->get( 'AllowUserCss' ) ) { + $out->wrapWikiMsg( "
\n$1\n
", [ 'usercssyoucanpreview' ] ); } - if ( $this->isJsSubpage && $wgAllowUserJs ) { - $wgOut->wrapWikiMsg( + if ( $this->mTitle->isJsSubpage() && $config->get( 'AllowUserJs' ) ) { + $out->wrapWikiMsg( "
\n$1\n
", [ 'userjsyoucanpreview' ] ); @@ -3064,9 +3113,51 @@ class EditPage { ]; } + /** + * Standard summary input and label (wgSummary), abstracted so EditPage + * subclasses may reorganize the form. + * Note that you do not need to worry about the label's for=, it will be + * inferred by the id given to the input. You can remove them both by + * passing [ 'id' => false ] to $userInputAttrs. + * + * @deprecated since 1.30 Use getSummaryInputWidget() instead + * @param string $summary The value of the summary input + * @param string $labelText The html to place inside the label + * @param array $inputAttrs Array of attrs to use on the input + * @param array $spanLabelAttrs Array of attrs to use on the span inside the label + * @return array An array in the format [ $label, $input ] + */ + public function getSummaryInput( $summary = "", $labelText = null, + $inputAttrs = null, $spanLabelAttrs = null + ) { + wfDeprecated( __METHOD__, '1.30' ); + $inputAttrs = $this->getSummaryInputAttributes( $inputAttrs ); + $inputAttrs += Linker::tooltipAndAccesskeyAttribs( 'summary' ); + + $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [ + 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary', + 'id' => "wpSummaryLabel" + ]; + + $label = null; + if ( $labelText ) { + $label = Xml::tags( + 'label', + $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null, + $labelText + ); + $label = Xml::tags( 'span', $spanLabelAttrs, $label ); + } + + $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs ); + + return [ $label, $input ]; + } + /** * Builds a standard summary input with a label. * + * @deprecated since 1.30 Use getSummaryInputWidget() instead * @param string $summary The value of the summary input * @param string $labelText The html to place inside the label * @param array $inputAttrs Array of attrs to use on the input @@ -3074,6 +3165,20 @@ class EditPage { * @return OOUI\FieldLayout OOUI FieldLayout with Label and Input */ function getSummaryInputOOUI( $summary = "", $labelText = null, $inputAttrs = null ) { + wfDeprecated( __METHOD__, '1.30' ); + $this->getSummaryInputWidget( $summary, $labelText, $inputAttrs ); + } + + /** + * Builds a standard summary input with a label. + * + * @param string $summary The value of the summary input + * @param string $labelText The html to place inside the label + * @param array $inputAttrs Array of attrs to use on the input + * + * @return OOUI\FieldLayout OOUI FieldLayout with Label and Input + */ + function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) { $inputAttrs = OOUI\Element::configFromHtmlAttributes( $this->getSummaryInputAttributes( $inputAttrs ) ); @@ -3107,8 +3212,6 @@ class EditPage { * @param string $summary The text of the summary to display */ protected function showSummaryInput( $isSubjectPreview, $summary = "" ) { - global $wgOut; - # Add a class if 'missingsummary' is triggered to allow styling of the summary line $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; if ( $isSubjectPreview ) { @@ -3122,7 +3225,7 @@ class EditPage { } $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse(); - $wgOut->addHTML( $this->getSummaryInputOOUI( + $this->context->getOutput()->addHTML( $this->getSummaryInputWidget( $summary, $labelText, [ 'class' => $summaryClass ] @@ -3159,21 +3262,15 @@ class EditPage { } protected function showFormBeforeText() { - global $wgOut; - - $wgOut->addHTML( Html::hidden( 'wpSection', htmlspecialchars( $this->section ) ) ); - $wgOut->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) ); - $wgOut->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) ); - $wgOut->addHTML( Html::hidden( 'editRevId', $this->editRevId ) ); - $wgOut->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop ) ); - - if ( !$this->checkUnicodeCompliantBrowser() ) { - $wgOut->addHTML( Html::hidden( 'safemode', '1' ) ); - } + $out = $this->context->getOutput(); + $out->addHTML( Html::hidden( 'wpSection', htmlspecialchars( $this->section ) ) ); + $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) ); + $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) ); + $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) ); + $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) ); } protected function showFormAfterText() { - global $wgOut, $wgUser; /** * To make it harder for someone to slip a user a page * which submits an edit form to the wiki without their @@ -3186,7 +3283,11 @@ class EditPage { * include the constant suffix to prevent editing from * broken text-mangling proxies. */ - $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" ); + $this->context->getOutput()->addHTML( + "\n" . + Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) . + "\n" + ); } /** @@ -3260,18 +3361,14 @@ class EditPage { } protected function showTextbox( $text, $name, $customAttribs = [] ) { - global $wgOut, $wgUser; + $wikitext = $this->addNewLineAtEnd( $text ); - $wikitext = $this->safeUnicodeOutput( $text ); - $wikitext = $this->addNewLineAtEnd( $wikitext ); + $attribs = $this->buildTextboxAttribs( $name, $customAttribs, $this->context->getUser() ); - $attribs = $this->buildTextboxAttribs( $name, $customAttribs, $wgUser ); - - $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) ); + $this->context->getOutput()->addHTML( Html::textarea( $name, $wikitext, $attribs ) ); } protected function displayPreviewArea( $previewOutput, $isOnTop = false ) { - global $wgOut; $classes = []; if ( $isOnTop ) { $classes[] = 'ontop'; @@ -3283,7 +3380,8 @@ class EditPage { $attribs['style'] = 'display: none;'; } - $wgOut->addHTML( Xml::openElement( 'div', $attribs ) ); + $out = $this->context->getOutput(); + $out->addHTML( Xml::openElement( 'div', $attribs ) ); if ( $this->formtype == 'preview' ) { $this->showPreview( $previewOutput ); @@ -3292,10 +3390,10 @@ class EditPage { $pageViewLang = $this->mTitle->getPageViewLanguage(); $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(), 'class' => 'mw-content-' . $pageViewLang->getDir() ]; - $wgOut->addHTML( Html::rawElement( 'div', $attribs ) ); + $out->addHTML( Html::rawElement( 'div', $attribs ) ); } - $wgOut->addHTML( '
' ); + $out->addHTML( '
' ); if ( $this->formtype == 'diff' ) { try { @@ -3307,26 +3405,26 @@ class EditPage { $this->contentFormat, $ex->getMessage() ); - $wgOut->addWikiText( '
' . $msg->text() . '
' ); + $out->addWikiText( '
' . $msg->text() . '
' ); } } } /** - * Append preview output to $wgOut. + * Append preview output to OutputPage. * Includes category rendering if this is a category page. * * @param string $text The HTML to be output for the preview. */ protected function showPreview( $text ) { - global $wgOut; if ( $this->mArticle instanceof CategoryPage ) { $this->mArticle->openShowCategory(); } # This hook seems slightly odd here, but makes things more # consistent for extensions. - Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] ); - $wgOut->addHTML( $text ); + $out = $this->context->getOutput(); + Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] ); + $out->addHTML( $text ); if ( $this->mArticle instanceof CategoryPage ) { $this->mArticle->closeShowCategory(); } @@ -3340,7 +3438,7 @@ class EditPage { * save and then make a comparison. */ public function showDiff() { - global $wgUser, $wgContLang, $wgOut; + global $wgContLang; $oldtitlemsg = 'currentrev'; # if message does not exist, show diff against the preloaded default @@ -3370,8 +3468,9 @@ class EditPage { if ( $newContent ) { Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] ); - $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); - $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts ); + $user = $this->context->getUser(); + $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); + $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts ); } if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) { @@ -3386,7 +3485,7 @@ class EditPage { $newContent = $oldContent->getContentHandler()->makeEmptyContent(); } - $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() ); + $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context ); $de->setContent( $oldContent, $newContent ); $difftext = $de->getDiff( $oldtitle, $newtitle ); @@ -3395,7 +3494,7 @@ class EditPage { $difftext = ''; } - $wgOut->addHTML( '
' . $difftext . '
' ); + $this->context->getOutput()->addHTML( '
' . $difftext . '
' ); } /** @@ -3404,8 +3503,7 @@ class EditPage { protected function showHeaderCopyrightWarning() { $msg = 'editpage-head-copy-warn'; if ( !$this->context->msg( $msg )->isDisabled() ) { - global $wgOut; - $wgOut->wrapWikiMsg( "
\n$1\n
", + $this->context->getOutput()->wrapWikiMsg( "
\n$1\n
", 'editpage-head-copy-warn' ); } } @@ -3422,10 +3520,10 @@ class EditPage { $msg = 'editpage-tos-summary'; Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] ); if ( !$this->context->msg( $msg )->isDisabled() ) { - global $wgOut; - $wgOut->addHTML( '
' ); - $wgOut->addWikiMsg( $msg ); - $wgOut->addHTML( '
' ); + $out = $this->context->getOutput(); + $out->addHTML( '
' ); + $out->addWikiMsg( $msg ); + $out->addHTML( '
' ); } } @@ -3434,8 +3532,7 @@ class EditPage { * characters not present on most keyboards for copying/pasting. */ protected function showEditTools() { - global $wgOut; - $wgOut->addHTML( '
' . + $this->context->getOutput()->addHTML( '
' . $this->context->msg( 'edittools' )->inContentLanguage()->parse() . '
' ); } @@ -3529,28 +3626,28 @@ class EditPage { } protected function showStandardInputs( &$tabindex = 2 ) { - global $wgOut; - $wgOut->addHTML( "
\n" ); + $out = $this->context->getOutput(); + $out->addHTML( "
\n" ); if ( $this->section != 'new' ) { $this->showSummaryInput( false, $this->summary ); - $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) ); + $out->addHTML( $this->getSummaryPreview( false, $this->summary ) ); } - $checkboxes = $this->getCheckboxesOOUI( + $checkboxes = $this->getCheckboxesWidget( $tabindex, [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] ); $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] ); - $wgOut->addHTML( "
" . $checkboxesHTML . "
\n" ); + $out->addHTML( "
" . $checkboxesHTML . "
\n" ); // Show copyright warning. - $wgOut->addWikiText( $this->getCopywarn() ); - $wgOut->addHTML( $this->editFormTextAfterWarn ); + $out->addWikiText( $this->getCopywarn() ); + $out->addHTML( $this->editFormTextAfterWarn ); - $wgOut->addHTML( "
\n" ); - $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" ); + $out->addHTML( "
\n" ); + $out->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" ); $cancel = $this->getCancelLink(); if ( $cancel !== '' ) { @@ -3570,13 +3667,13 @@ class EditPage { $this->context->msg( 'word-separator' )->escaped() . $this->context->msg( 'newwindow' )->parse(); - $wgOut->addHTML( " {$cancel}\n" ); - $wgOut->addHTML( " {$edithelp}\n" ); - $wgOut->addHTML( "
\n" ); + $out->addHTML( " {$cancel}\n" ); + $out->addHTML( " {$edithelp}\n" ); + $out->addHTML( "
\n" ); - Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] ); + Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] ); - $wgOut->addHTML( "
\n" ); + $out->addHTML( "
\n" ); } /** @@ -3584,27 +3681,26 @@ class EditPage { * If you want to use another entry point to this function, be careful. */ protected function showConflict() { - global $wgOut; - + $out = $this->context->getOutput(); // Avoid PHP 7.1 warning of passing $this by reference $editPage = $this; - if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$wgOut ] ) ) { + if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) { $this->incrementConflictStats(); - $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); + $out->wrapWikiMsg( '

$1

', "yourdiff" ); $content1 = $this->toEditContent( $this->textbox1 ); $content2 = $this->toEditContent( $this->textbox2 ); $handler = ContentHandler::getForModelID( $this->contentModel ); - $de = $handler->createDifferenceEngine( $this->mArticle->getContext() ); + $de = $handler->createDifferenceEngine( $this->context ); $de->setContent( $content2, $content1 ); $de->showDiff( $this->context->msg( 'yourtext' )->parse(), $this->context->msg( 'storedversion' )->text() ); - $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); + $out->wrapWikiMsg( '

$1

', "yourtext" ); $this->showTextbox2(); } } @@ -3734,10 +3830,10 @@ class EditPage { * @return string */ public function getPreviewText() { - global $wgOut, $wgRawHtml, $wgLang; - global $wgAllowUserCss, $wgAllowUserJs; + $out = $this->context->getOutput(); + $config = $this->context->getConfig(); - if ( $wgRawHtml && !$this->mTokenOk ) { + if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) { // Could be an offsite preview attempt. This is very unsafe if // HTML is enabled, as it could be an attack. $parsedNote = ''; @@ -3745,7 +3841,7 @@ class EditPage { // Do not put big scary notice, if previewing the empty // string, which happens when you initially edit // a category page, due to automatic preview-on-open. - $parsedNote = $wgOut->parse( "
" . + $parsedNote = $out->parse( "
" . $this->context->msg( 'session_fail_preview_html' )->text() . "
", true, /* interface */true ); } @@ -3768,7 +3864,8 @@ class EditPage { # provide a anchor link to the editform $continueEditing = '' . - '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . + '[[#' . self::EDITFORM_ID . '|' . + $this->context->getLanguage()->getArrow() . ' ' . $this->context->msg( 'continue-editing' )->text() . ']]'; if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { @@ -3799,12 +3896,12 @@ class EditPage { if ( $content->getModel() == CONTENT_MODEL_CSS ) { $format = 'css'; - if ( $level === 'user' && !$wgAllowUserCss ) { + if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) { $format = false; } } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) { $format = 'js'; - if ( $level === 'user' && !$wgAllowUserJs ) { + if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) { $format = false; } } else { @@ -3833,7 +3930,7 @@ class EditPage { $parserOutput = $parserResult['parserOutput']; $previewHTML = $parserResult['html']; $this->mParserOutput = $parserOutput; - $wgOut->addParserOutputMetadata( $parserOutput ); + $out->addParserOutputMetadata( $parserOutput ); if ( count( $parserOutput->getWarnings() ) ) { $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); @@ -3859,7 +3956,7 @@ class EditPage { $previewhead = "
\n" . '

' . $this->context->msg( 'preview' )->escaped() . "

" . - $wgOut->parse( $note, true, /* interface */true ) . $conflict . "
\n"; + $out->parse( $note, true, /* interface */true ) . $conflict . "
\n"; $pageViewLang = $this->mTitle->getPageViewLanguage(); $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(), @@ -3879,7 +3976,7 @@ class EditPage { * @return ParserOptions */ protected function getPreviewParserOptions() { - $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() ); + $parserOptions = $this->page->makeParserOptions( $this->context ); $parserOptions->setIsPreview( true ); $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' ); $parserOptions->enableLimitReport(); @@ -3896,11 +3993,11 @@ class EditPage { * - html: The HTML to be displayed */ protected function doPreviewParse( Content $content ) { - global $wgUser; + $user = $this->context->getUser(); $parserOptions = $this->getPreviewParserOptions(); - $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); + $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions ); $scopedCallback = $parserOptions->setupFakeRevision( - $this->mTitle, $pstContent, $wgUser ); + $this->mTitle, $pstContent, $user ); $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions ); ScopedCallback::consume( $scopedCallback ); $parserOutput->setEditSectionTokens( false ); // no section edit links @@ -4086,12 +4183,12 @@ class EditPage { * where bool indicates the checked status of the checkbox * @return array */ - protected function getCheckboxesDefinition( $checked ) { - global $wgUser; + public function getCheckboxesDefinition( $checked ) { $checkboxes = []; + $user = $this->context->getUser(); // don't show the minor edit checkbox if it's a new page or section - if ( !$this->isNew && $wgUser->isAllowed( 'minoredit' ) ) { + if ( !$this->isNew && $user->isAllowed( 'minoredit' ) ) { $checkboxes['wpMinoredit'] = [ 'id' => 'wpMinoredit', 'label-message' => 'minoredit', @@ -4103,7 +4200,7 @@ class EditPage { ]; } - if ( $wgUser->isLoggedIn() ) { + if ( $user->isLoggedIn() ) { $checkboxes['wpWatchthis'] = [ 'id' => 'wpWatchthis', 'label-message' => 'watchthis', @@ -4125,13 +4222,13 @@ class EditPage { * Returns an array of html code of the following checkboxes old style: * minor and watch * + * @deprecated since 1.30 Use getCheckboxesWidget() or getCheckboxesDefinition() instead * @param int &$tabindex Current tabindex * @param array $checked See getCheckboxesDefinition() * @return array */ public function getCheckboxes( &$tabindex, $checked ) { - global $wgUseMediaWikiUIEverywhere; - + wfDeprecated( __METHOD__, '1.30' ); $checkboxes = []; $checkboxesDef = $this->getCheckboxesDefinition( $checked ); @@ -4166,10 +4263,6 @@ class EditPage { ' ' . Xml::tags( 'label', $labelAttribs, $label ); - if ( $wgUseMediaWikiUIEverywhere ) { - $checkboxHtml = Html::rawElement( 'div', [ 'class' => 'mw-ui-checkbox' ], $checkboxHtml ); - } - $checkboxes[ $legacyName ] = $checkboxHtml; } @@ -4180,21 +4273,35 @@ class EditPage { } /** - * Returns an array of html code of the following checkboxes: - * minor and watch + * Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and + * any other added by extensions. * + * @deprecated since 1.30 Use getCheckboxesWidget() or getCheckboxesDefinition() instead * @param int &$tabindex Current tabindex * @param array $checked Array of checkbox => bool, where bool indicates the checked * status of the checkbox * - * @return array + * @return array Associative array of string keys to OOUI\FieldLayout instances */ public function getCheckboxesOOUI( &$tabindex, $checked ) { + wfDeprecated( __METHOD__, '1.30' ); + return $this->getCheckboxesWidget( $tabindex, $checked ); + } + + /** + * Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and + * any other added by extensions. + * + * @param int &$tabindex Current tabindex + * @param array $checked Array of checkbox => bool, where bool indicates the checked + * status of the checkbox + * + * @return array Associative array of string keys to OOUI\FieldLayout instances + */ + public function getCheckboxesWidget( &$tabindex, $checked ) { $checkboxes = []; $checkboxesDef = $this->getCheckboxesDefinition( $checked ); - $origTabindex = $tabindex; - foreach ( $checkboxesDef as $name => $options ) { $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name; @@ -4207,9 +4314,6 @@ class EditPage { if ( isset( $options['title-message'] ) ) { $title = $this->context->msg( $options['title-message'] )->text(); } - if ( isset( $options['label-id'] ) ) { - $labelAttribs['id'] = $options['label-id']; - } $checkboxes[ $legacyName ] = new OOUI\FieldLayout( new OOUI\CheckboxInputWidget( [ @@ -4233,7 +4337,19 @@ class EditPage { // Backwards-compatibility hack to run the EditPageBeforeEditChecks hook. It's important, // people have used it for the weirdest things completely unrelated to checkboxes... // And if we're gonna run it, might as well allow its legacy checkboxes to be shown. - $legacyCheckboxes = $this->getCheckboxes( $origTabindex, $checked ); + $legacyCheckboxes = []; + if ( !$this->isNew ) { + $legacyCheckboxes['minor'] = ''; + } + $legacyCheckboxes['watch'] = ''; + // Copy new-style checkboxes into an old-style structure + foreach ( $checkboxes as $name => $oouiLayout ) { + $legacyCheckboxes[$name] = (string)$oouiLayout; + } + // Avoid PHP 7.1 warning of passing $this by reference + $ep = $this; + Hooks::run( 'EditPageBeforeEditChecks', [ &$ep, &$legacyCheckboxes, &$tabindex ], '1.29' ); + // Copy back any additional old-style checkboxes into the new-style structure foreach ( $legacyCheckboxes as $name => $html ) { if ( $html && !isset( $checkboxes[$name] ) ) { $checkboxes[$name] = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $html ) ] ); @@ -4246,11 +4362,12 @@ class EditPage { /** * Get the message key of the label for the button to save the page * + * @since 1.30 * @return string */ - private function getSaveButtonLabel() { + protected function getSubmitButtonLabel() { $labelAsPublish = - $this->mArticle->getContext()->getConfig()->get( 'EditSubmitButtonLabelPublish' ); + $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' ); // Can't use $this->isNew as that's also true if we're adding a new section to an extant page $newPage = !$this->mTitle->exists(); @@ -4275,7 +4392,7 @@ class EditPage { public function getEditButtons( &$tabindex ) { $buttons = []; - $buttonLabel = $this->context->msg( $this->getSaveButtonLabel() )->text(); + $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text(); $attribs = [ 'name' => 'wpSave', @@ -4344,18 +4461,17 @@ class EditPage { * they have attempted to edit a nonexistent section. */ public function noSuchSectionPage() { - global $wgOut; - - $wgOut->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) ); + $out = $this->context->getOutput(); + $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) ); $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock(); // Avoid PHP 7.1 warning of passing $this by reference $editPage = $this; Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] ); - $wgOut->addHTML( $res ); + $out->addHTML( $res ); - $wgOut->returnToMain( false, $this->mTitle ); + $out->returnToMain( false, $this->mTitle ); } /** @@ -4364,177 +4480,69 @@ class EditPage { * @param string|array|bool $match Text (or array of texts) which triggered one or more filters */ public function spamPageWithContent( $match = false ) { - global $wgOut, $wgLang; $this->textbox2 = $this->textbox1; if ( is_array( $match ) ) { - $match = $wgLang->listToText( $match ); + $match = $this->context->getLanguage()->listToText( $match ); } - $wgOut->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) ); + $out = $this->context->getOutput(); + $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) ); - $wgOut->addHTML( '
' ); - $wgOut->addWikiMsg( 'spamprotectiontext' ); + $out->addHTML( '
' ); + $out->addWikiMsg( 'spamprotectiontext' ); if ( $match ) { - $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); + $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); } - $wgOut->addHTML( '
' ); + $out->addHTML( '
' ); - $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); + $out->wrapWikiMsg( '

$1

', "yourdiff" ); $this->showDiff(); - $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); + $out->wrapWikiMsg( '

$1

', "yourtext" ); $this->showTextbox2(); - $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] ); - } - - /** - * Check if the browser is on a blacklist of user-agents known to - * mangle UTF-8 data on form submission. Returns true if Unicode - * should make it through, false if it's known to be a problem. - * @return bool - */ - private function checkUnicodeCompliantBrowser() { - global $wgBrowserBlackList, $wgRequest; - - $currentbrowser = $wgRequest->getHeader( 'User-Agent' ); - if ( $currentbrowser === false ) { - // No User-Agent header sent? Trust it by default... - return true; - } - - foreach ( $wgBrowserBlackList as $browser ) { - if ( preg_match( $browser, $currentbrowser ) ) { - return false; - } - } - return true; + $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] ); } /** * Filter an input field through a Unicode de-armoring process if it * came from an old browser with known broken Unicode editing issues. * + * @deprecated since 1.30, does nothing + * * @param WebRequest $request * @param string $field * @return string */ protected function safeUnicodeInput( $request, $field ) { - $text = rtrim( $request->getText( $field ) ); - return $request->getBool( 'safemode' ) - ? $this->unmakeSafe( $text ) - : $text; + return rtrim( $request->getText( $field ) ); } /** * Filter an output field through a Unicode armoring process if it is * going to an old browser with known broken Unicode editing issues. * + * @deprecated since 1.30, does nothing + * * @param string $text * @return string */ protected function safeUnicodeOutput( $text ) { - return $this->checkUnicodeCompliantBrowser() - ? $text - : $this->makeSafe( $text ); - } - - /** - * A number of web browsers are known to corrupt non-ASCII characters - * in a UTF-8 text editing environment. To protect against this, - * detected browsers will be served an armored version of the text, - * with non-ASCII chars converted to numeric HTML character references. - * - * Preexisting such character references will have a 0 added to them - * to ensure that round-trips do not alter the original data. - * - * @param string $invalue - * @return string - */ - private function makeSafe( $invalue ) { - // Armor existing references for reversibility. - $invalue = strtr( $invalue, [ "&#x" => "�" ] ); - - $bytesleft = 0; - $result = ""; - $working = 0; - $valueLength = strlen( $invalue ); - for ( $i = 0; $i < $valueLength; $i++ ) { - $bytevalue = ord( $invalue[$i] ); - if ( $bytevalue <= 0x7F ) { // 0xxx xxxx - $result .= chr( $bytevalue ); - $bytesleft = 0; - } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx - $working = $working << 6; - $working += ( $bytevalue & 0x3F ); - $bytesleft--; - if ( $bytesleft <= 0 ) { - $result .= "&#x" . strtoupper( dechex( $working ) ) . ";"; - } - } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx - $working = $bytevalue & 0x1F; - $bytesleft = 1; - } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx - $working = $bytevalue & 0x0F; - $bytesleft = 2; - } else { // 1111 0xxx - $working = $bytevalue & 0x07; - $bytesleft = 3; - } - } - return $result; - } - - /** - * Reverse the previously applied transliteration of non-ASCII characters - * back to UTF-8. Used to protect data from corruption by broken web browsers - * as listed in $wgBrowserBlackList. - * - * @param string $invalue - * @return string - */ - private function unmakeSafe( $invalue ) { - $result = ""; - $valueLength = strlen( $invalue ); - for ( $i = 0; $i < $valueLength; $i++ ) { - if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) { - $i += 3; - $hexstring = ""; - do { - $hexstring .= $invalue[$i]; - $i++; - } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) ); - - // Do some sanity checks. These aren't needed for reversibility, - // but should help keep the breakage down if the editor - // breaks one of the entities whilst editing. - if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) { - $codepoint = hexdec( $hexstring ); - $result .= UtfNormal\Utils::codepointToUtf8( $codepoint ); - } else { - $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 ); - } - } else { - $result .= substr( $invalue, $i, 1 ); - } - } - // reverse the transform that we made for reversibility reasons. - return strtr( $result, [ "�" => "&#x" ] ); + return $text; } /** * @since 1.29 */ protected function addEditNotices() { - global $wgOut; - + $out = $this->context->getOutput(); $editNotices = $this->mTitle->getEditNotices( $this->oldid ); if ( count( $editNotices ) ) { - $wgOut->addHTML( implode( "\n", $editNotices ) ); + $out->addHTML( implode( "\n", $editNotices ) ); } else { $msg = $this->context->msg( 'editnotice-notext' ); if ( !$msg->isDisabled() ) { - $wgOut->addHTML( + $out->addHTML( '
' . $msg->parseAsBlock() . '
' @@ -4547,10 +4555,8 @@ class EditPage { * @since 1.29 */ protected function addTalkPageText() { - global $wgOut; - if ( $this->mTitle->isTalkPage() ) { - $wgOut->addWikiMsg( 'talkpagetext' ); + $this->context->getOutput()->addWikiMsg( 'talkpagetext' ); } } @@ -4558,26 +4564,27 @@ class EditPage { * @since 1.29 */ protected function addLongPageWarningHeader() { - global $wgMaxArticleSize, $wgOut, $wgLang; - if ( $this->contentLength === false ) { $this->contentLength = strlen( $this->textbox1 ); } - if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) { - $wgOut->wrapWikiMsg( "
\n$1\n
", + $out = $this->context->getOutput(); + $lang = $this->context->getLanguage(); + $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' ); + if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) { + $out->wrapWikiMsg( "
\n$1\n
", [ 'longpageerror', - $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ), - $wgLang->formatNum( $wgMaxArticleSize ) + $lang->formatNum( round( $this->contentLength / 1024, 3 ) ), + $lang->formatNum( $maxArticleSize ) ] ); } else { if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) { - $wgOut->wrapWikiMsg( "
\n$1\n
", + $out->wrapWikiMsg( "
\n$1\n
", [ 'longpage-hint', - $wgLang->formatSize( strlen( $this->textbox1 ) ), + $lang->formatSize( strlen( $this->textbox1 ) ), strlen( $this->textbox1 ) ] ); @@ -4589,8 +4596,7 @@ class EditPage { * @since 1.29 */ protected function addPageProtectionWarningHeaders() { - global $wgOut; - + $out = $this->context->getOutput(); if ( $this->mTitle->isProtected( 'edit' ) && MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ] ) { @@ -4601,7 +4607,7 @@ class EditPage { # Then it must be protected based on static groups (regular) $noticeMsg = 'protectedpagewarning'; } - LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '', + LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '', [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] ); } if ( $this->mTitle->isCascadeProtected() ) { @@ -4617,10 +4623,10 @@ class EditPage { } } $notice .= '
'; - $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] ); + $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] ); } if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { - LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '', + LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '', [ 'lim' => 1, 'showIfEmpty' => false, 'msgKey' => [ 'titleprotectedwarning' ], @@ -4635,7 +4641,7 @@ class EditPage { protected function addExplainConflictHeader( OutputPage $out ) { $out->wrapWikiMsg( "
\n$1\n
", - [ 'explainconflict', $this->context->msg( $this->getSaveButtonLabel() )->text() ] + [ 'explainconflict', $this->context->msg( $this->getSubmitButtonLabel() )->text() ] ); } @@ -4658,7 +4664,6 @@ class EditPage { ]; // The following classes can be used here: - // * mw-editfont-default // * mw-editfont-monospace // * mw-editfont-sans-serif // * mw-editfont-serif diff --git a/includes/Feed.php b/includes/Feed.php index f76a634d3f..bc7747fe72 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -139,7 +139,7 @@ class FeedItem { */ public function getLanguage() { global $wgLanguageCode; - return wfBCP47( $wgLanguageCode ); + return LanguageCode::bcp47( $wgLanguageCode ); } /** diff --git a/includes/FormOptions.php b/includes/FormOptions.php index 725a512980..53c8d3bf7c 100644 --- a/includes/FormOptions.php +++ b/includes/FormOptions.php @@ -246,6 +246,9 @@ class FormOptions implements ArrayAccess { /** * @see validateBounds() + * @param string $name + * @param int $min + * @param int $max */ public function validateIntBounds( $name, $min, $max ) { $this->validateBounds( $name, $min, $max ); diff --git a/includes/GitInfo.php b/includes/GitInfo.php index 4351acc0e9..3c600ed984 100644 --- a/includes/GitInfo.php +++ b/includes/GitInfo.php @@ -191,8 +191,14 @@ class GitInfo { } else { // If not a SHA1 it may be a ref: $refFile = "{$this->basedir}/{$head}"; + $packedRefs = "{$this->basedir}/packed-refs"; + $headRegex = preg_quote( $head, '/' ); if ( is_readable( $refFile ) ) { $sha1 = rtrim( file_get_contents( $refFile ) ); + } elseif ( is_readable( $packedRefs ) && + preg_match( "/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ), $matches ) + ) { + $sha1 = $matches[1]; } } $this->cache['headSHA1'] = $sha1; diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 49159ed3df..d53e98dbe1 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -26,8 +26,10 @@ if ( !defined( 'MEDIAWIKI' ) ) { use Liuggio\StatsdClient\Sender\SocketSender; use MediaWiki\Logger\LoggerFactory; +use MediaWiki\ProcOpenError; use MediaWiki\Session\SessionManager; use MediaWiki\MediaWikiServices; +use MediaWiki\Shell\Shell; use Wikimedia\ScopedCallback; use Wikimedia\Rdbms\DBReplicationWaitError; @@ -193,11 +195,15 @@ function wfArrayDiff2_cmp( $a, $b ) { } else { reset( $a ); reset( $b ); - while ( ( list( , $valueA ) = each( $a ) ) && ( list( , $valueB ) = each( $b ) ) ) { + while ( key( $a ) !== null && key( $b ) !== null ) { + $valueA = current( $a ); + $valueB = current( $b ); $cmp = strcmp( $valueA, $valueB ); if ( $cmp !== 0 ) { return $cmp; } + next( $a ); + next( $b ); } return 0; } @@ -2237,64 +2243,12 @@ function wfIniGetBool( $setting ) { * @param string $args,... strings to escape and glue together, * or a single array of strings parameter * @return string + * @deprecated since 1.30 use MediaWiki\Shell::escape() */ function wfEscapeShellArg( /*...*/ ) { $args = func_get_args(); - if ( count( $args ) === 1 && is_array( reset( $args ) ) ) { - // If only one argument has been passed, and that argument is an array, - // treat it as a list of arguments - $args = reset( $args ); - } - $first = true; - $retVal = ''; - foreach ( $args as $arg ) { - if ( !$first ) { - $retVal .= ' '; - } else { - $first = false; - } - - if ( wfIsWindows() ) { - // Escaping for an MSVC-style command line parser and CMD.EXE - // @codingStandardsIgnoreStart For long URLs - // Refs: - // * https://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html - // * https://technet.microsoft.com/en-us/library/cc723564.aspx - // * T15518 - // * CR r63214 - // Double the backslashes before any double quotes. Escape the double quotes. - // @codingStandardsIgnoreEnd - $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); - $arg = ''; - $iteration = 0; - foreach ( $tokens as $token ) { - if ( $iteration % 2 == 1 ) { - // Delimiter, a double quote preceded by zero or more slashes - $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"'; - } elseif ( $iteration % 4 == 2 ) { - // ^ in $token will be outside quotes, need to be escaped - $arg .= str_replace( '^', '^^', $token ); - } else { // $iteration % 4 == 0 - // ^ in $token will appear inside double quotes, so leave as is - $arg .= $token; - } - $iteration++; - } - // Double the backslashes before the end of the string, because - // we will soon add a quote - $m = []; - if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) { - $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] ); - } - - // Add surrounding quotes - $retVal .= '"' . $arg . '"'; - } else { - $retVal .= escapeshellarg( $arg ); - } - } - return $retVal; + return call_user_func_array( Shell::class . '::escape', $args ); } /** @@ -2302,18 +2256,11 @@ function wfEscapeShellArg( /*...*/ ) { * * @return bool|string False or 'disabled' * @since 1.22 + * @deprecated since 1.30 use MediaWiki\Shell::isDisabled() */ function wfShellExecDisabled() { - static $disabled = null; - if ( is_null( $disabled ) ) { - if ( !function_exists( 'proc_open' ) ) { - wfDebug( "proc_open() is disabled\n" ); - $disabled = 'disabled'; - } else { - $disabled = false; - } - } - return $disabled; + wfDeprecated( __FUNCTION__, '1.30' ); + return Shell::isDisabled() ? 'disabled' : false; } /** @@ -2337,221 +2284,40 @@ function wfShellExecDisabled() { * method. Set this to a string for an alternative method to profile from * * @return string Collected stdout as a string + * @deprecated since 1.30 use class MediaWiki\Shell\Shell */ function wfShellExec( $cmd, &$retval = null, $environ = [], $limits = [], $options = [] ) { - global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime, - $wgMaxShellWallClockTime, $wgShellCgroup; - - $disabled = wfShellExecDisabled(); - if ( $disabled ) { + if ( Shell::isDisabled() ) { $retval = 1; + // Backwards compatibility be upon us... return 'Unable to run external programs, proc_open() is disabled.'; } - $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr']; - $profileMethod = isset( $options['profileMethod'] ) ? $options['profileMethod'] : wfGetCaller(); - - $envcmd = ''; - foreach ( $environ as $k => $v ) { - if ( wfIsWindows() ) { - /* Surrounding a set in quotes (method used by wfEscapeShellArg) makes the quotes themselves - * appear in the environment variable, so we must use carat escaping as documented in - * https://technet.microsoft.com/en-us/library/cc723564.aspx - * Note however that the quote isn't listed there, but is needed, and the parentheses - * are listed there but doesn't appear to need it. - */ - $envcmd .= "set $k=" . preg_replace( '/([&|()<>^"])/', '^\\1', $v ) . '&& '; - } else { - /* Assume this is a POSIX shell, thus required to accept variable assignments before the command - * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_09_01 - */ - $envcmd .= "$k=" . escapeshellarg( $v ) . ' '; - } - } if ( is_array( $cmd ) ) { - $cmd = wfEscapeShellArg( $cmd ); + $cmd = Shell::escape( $cmd ); } - $cmd = $envcmd . $cmd; + $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr']; + $profileMethod = isset( $options['profileMethod'] ) ? $options['profileMethod'] : wfGetCaller(); - $useLogPipe = false; - if ( is_executable( '/bin/bash' ) ) { - $time = intval( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); - if ( isset( $limits['walltime'] ) ) { - $wallTime = intval( $limits['walltime'] ); - } elseif ( isset( $limits['time'] ) ) { - $wallTime = $time; - } else { - $wallTime = intval( $wgMaxShellWallClockTime ); - } - $mem = intval( isset( $limits['memory'] ) ? $limits['memory'] : $wgMaxShellMemory ); - $filesize = intval( isset( $limits['filesize'] ) ? $limits['filesize'] : $wgMaxShellFileSize ); - - if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) { - $cmd = '/bin/bash ' . escapeshellarg( "$IP/includes/limit.sh" ) . ' ' . - escapeshellarg( $cmd ) . ' ' . - escapeshellarg( - "MW_INCLUDE_STDERR=" . ( $includeStderr ? '1' : '' ) . ';' . - "MW_CPU_LIMIT=$time; " . - 'MW_CGROUP=' . escapeshellarg( $wgShellCgroup ) . '; ' . - "MW_MEM_LIMIT=$mem; " . - "MW_FILE_SIZE_LIMIT=$filesize; " . - "MW_WALL_CLOCK_LIMIT=$wallTime; " . - "MW_USE_LOG_PIPE=yes" - ); - $useLogPipe = true; - } elseif ( $includeStderr ) { - $cmd .= ' 2>&1'; - } - } elseif ( $includeStderr ) { - $cmd .= ' 2>&1'; - } - wfDebug( "wfShellExec: $cmd\n" ); - - // Don't try to execute commands that exceed Linux's MAX_ARG_STRLEN. - // Other platforms may be more accomodating, but we don't want to be - // accomodating, because very long commands probably include user - // input. See T129506. - if ( strlen( $cmd ) > SHELL_MAX_ARG_STRLEN ) { - throw new Exception( __METHOD__ . - '(): total length of $cmd must not exceed SHELL_MAX_ARG_STRLEN' ); - } - - $desc = [ - 0 => [ 'file', 'php://stdin', 'r' ], - 1 => [ 'pipe', 'w' ], - 2 => [ 'file', 'php://stderr', 'w' ] ]; - if ( $useLogPipe ) { - $desc[3] = [ 'pipe', 'w' ]; - } - $pipes = null; - $scoped = Profiler::instance()->scopedProfileIn( __FUNCTION__ . '-' . $profileMethod ); - $proc = proc_open( $cmd, $desc, $pipes ); - if ( !$proc ) { - wfDebugLog( 'exec', "proc_open() failed: $cmd" ); + try { + $result = Shell::command( [] ) + ->unsafeParams( (array)$cmd ) + ->environment( $environ ) + ->limits( $limits ) + ->includeStderr( $includeStderr ) + ->profileMethod( $profileMethod ) + ->execute(); + } catch ( ProcOpenError $ex ) { $retval = -1; return ''; } - $outBuffer = $logBuffer = ''; - $emptyArray = []; - $status = false; - $logMsg = false; - /* According to the documentation, it is possible for stream_select() - * to fail due to EINTR. I haven't managed to induce this in testing - * despite sending various signals. If it did happen, the error - * message would take the form: - * - * stream_select(): unable to select [4]: Interrupted system call (max_fd=5) - * - * where [4] is the value of the macro EINTR and "Interrupted system - * call" is string which according to the Linux manual is "possibly" - * localised according to LC_MESSAGES. - */ - $eintr = defined( 'SOCKET_EINTR' ) ? SOCKET_EINTR : 4; - $eintrMessage = "stream_select(): unable to select [$eintr]"; - - $running = true; - $timeout = null; - $numReadyPipes = 0; - - while ( $running === true || $numReadyPipes !== 0 ) { - if ( $running ) { - $status = proc_get_status( $proc ); - // If the process has terminated, switch to nonblocking selects - // for getting any data still waiting to be read. - if ( !$status['running'] ) { - $running = false; - $timeout = 0; - } - } - - $readyPipes = $pipes; + $retval = $result->getExitCode(); - // Clear last error - // @codingStandardsIgnoreStart Generic.PHP.NoSilencedErrors.Discouraged - @trigger_error( '' ); - $numReadyPipes = @stream_select( $readyPipes, $emptyArray, $emptyArray, $timeout ); - if ( $numReadyPipes === false ) { - // @codingStandardsIgnoreEnd - $error = error_get_last(); - if ( strncmp( $error['message'], $eintrMessage, strlen( $eintrMessage ) ) == 0 ) { - continue; - } else { - trigger_error( $error['message'], E_USER_WARNING ); - $logMsg = $error['message']; - break; - } - } - foreach ( $readyPipes as $fd => $pipe ) { - $block = fread( $pipe, 65536 ); - if ( $block === '' ) { - // End of file - fclose( $pipes[$fd] ); - unset( $pipes[$fd] ); - if ( !$pipes ) { - break 2; - } - } elseif ( $block === false ) { - // Read error - $logMsg = "Error reading from pipe"; - break 2; - } elseif ( $fd == 1 ) { - // From stdout - $outBuffer .= $block; - } elseif ( $fd == 3 ) { - // From log FD - $logBuffer .= $block; - if ( strpos( $block, "\n" ) !== false ) { - $lines = explode( "\n", $logBuffer ); - $logBuffer = array_pop( $lines ); - foreach ( $lines as $line ) { - wfDebugLog( 'exec', $line ); - } - } - } - } - } - - foreach ( $pipes as $pipe ) { - fclose( $pipe ); - } - - // Use the status previously collected if possible, since proc_get_status() - // just calls waitpid() which will not return anything useful the second time. - if ( $running ) { - $status = proc_get_status( $proc ); - } - - if ( $logMsg !== false ) { - // Read/select error - $retval = -1; - proc_close( $proc ); - } elseif ( $status['signaled'] ) { - $logMsg = "Exited with signal {$status['termsig']}"; - $retval = 128 + $status['termsig']; - proc_close( $proc ); - } else { - if ( $status['running'] ) { - $retval = proc_close( $proc ); - } else { - $retval = $status['exitcode']; - proc_close( $proc ); - } - if ( $retval == 127 ) { - $logMsg = "Possibly missing executable file"; - } elseif ( $retval >= 129 && $retval <= 192 ) { - $logMsg = "Probably exited with signal " . ( $retval - 128 ); - } - } - - if ( $logMsg !== false ) { - wfDebugLog( 'exec', "$logMsg: $cmd" ); - } - - return $outBuffer; + return $result->getStdout(); } /** @@ -2569,6 +2335,7 @@ function wfShellExec( $cmd, &$retval = null, $environ = [], * @param array $limits Optional array with limits(filesize, memory, time, walltime) * this overwrites the global wgMaxShell* limits. * @return string Collected stdout and stderr as a string + * @deprecated since 1.30 use class MediaWiki\Shell\Shell */ function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits = [] ) { return wfShellExec( $cmd, $retval, $environ, $limits, @@ -2584,6 +2351,7 @@ function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits = * @see $wgShellLocale */ function wfInitShellLocale() { + wfDeprecated( __FUNCTION__, '1.30' ); } /** @@ -2609,7 +2377,7 @@ function wfShellWikiCmd( $script, array $parameters = [], array $options = [] ) } $cmd[] = $script; // Escape each parameter for shell - return wfEscapeShellArg( array_merge( $cmd, $parameters ) ); + return Shell::escape( array_merge( $cmd, $parameters ) ); } /** @@ -2654,7 +2422,7 @@ function wfMerge( $old, $mine, $yours, &$result ) { fclose( $yourtextFile ); # Check for a conflict - $cmd = wfEscapeShellArg( $wgDiff3, '-a', '--overlap-only', $mytextName, + $cmd = Shell::escape( $wgDiff3, '-a', '--overlap-only', $mytextName, $oldtextName, $yourtextName ); $handle = popen( $cmd, 'r' ); @@ -2666,7 +2434,7 @@ function wfMerge( $old, $mine, $yours, &$result ) { pclose( $handle ); # Merge differences - $cmd = wfEscapeShellArg( $wgDiff3, '-a', '-e', '--merge', $mytextName, + $cmd = Shell::escape( $wgDiff3, '-a', '-e', '--merge', $mytextName, $oldtextName, $yourtextName ); $handle = popen( $cmd, 'r' ); $result = ''; @@ -2730,7 +2498,7 @@ function wfDiff( $before, $after, $params = '-u' ) { fclose( $newtextFile ); // Get the diff of the two files - $cmd = "$wgDiff " . $params . ' ' . wfEscapeShellArg( $oldtextName, $newtextName ); + $cmd = "$wgDiff " . $params . ' ' . Shell::escape( $oldtextName, $newtextName ); $h = popen( $cmd, 'r' ); if ( !$h ) { @@ -2781,6 +2549,9 @@ function wfDiff( $before, $after, $params = '-u' ) { * @see perldoc -f use * * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float + * + * @deprecated since 1.30 + * * @throws MWException */ function wfUsePHP( $req_ver ) { @@ -2809,7 +2580,7 @@ function wfUsePHP( $req_ver ) { * * @see perldoc -f use * - * @deprecated since 1.26, use the "requires' property of extension.json + * @deprecated since 1.26, use the "requires" property of extension.json * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float * @throws MWException */ @@ -2916,14 +2687,6 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, return Wikimedia\base_convert( $input, $sourceBase, $destBase, $pad, $lowercase, $engine ); } -/** - * @deprecated since 1.27, PHP's session generation isn't used with - * MediaWiki\Session\SessionManager - */ -function wfFixSessionID() { - wfDeprecated( __FUNCTION__, '1.27' ); -} - /** * Reset the session id * @@ -3403,30 +3166,15 @@ function wfShorthandToInteger( $string = '', $default = -1 ) { /** * Get the normalised IETF language tag * See unit test for examples. + * See mediawiki.language.bcp47 for the JavaScript implementation. + * + * @deprecated since 1.31, use LanguageCode::bcp47() directly. * * @param string $code The language code. * @return string The language code which complying with BCP 47 standards. */ function wfBCP47( $code ) { - $codeSegment = explode( '-', $code ); - $codeBCP = []; - foreach ( $codeSegment as $segNo => $seg ) { - // when previous segment is x, it is a private segment and should be lc - if ( $segNo > 0 && strtolower( $codeSegment[( $segNo - 1 )] ) == 'x' ) { - $codeBCP[$segNo] = strtolower( $seg ); - // ISO 3166 country code - } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) { - $codeBCP[$segNo] = strtoupper( $seg ); - // ISO 15924 script code - } elseif ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) ) { - $codeBCP[$segNo] = ucfirst( strtolower( $seg ) ); - // Use lowercase for other cases - } else { - $codeBCP[$segNo] = strtolower( $seg ); - } - } - $langCode = implode( '-', $codeBCP ); - return $langCode; + return LanguageCode::bcp47( $code ); } /** diff --git a/includes/Hooks.php b/includes/Hooks.php index f4f86be68b..c22dc97f46 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -108,17 +108,89 @@ class Hooks { } } + /** + * @param string $event Event name + * @param array|callable $hook + * @param array $args Array of parameters passed to hook functions + * @param string|null $deprecatedVersion [optional] + * @param string &$fname [optional] Readable name of hook [returned] + * @return null|string|bool + */ + private static function callHook( $event, $hook, array $args, $deprecatedVersion = null, + &$fname = null + ) { + // Turn non-array values into an array. (Can't use casting because of objects.) + if ( !is_array( $hook ) ) { + $hook = [ $hook ]; + } + + if ( !array_filter( $hook ) ) { + // Either array is empty or it's an array filled with null/false/empty. + return null; + } + + if ( is_array( $hook[0] ) ) { + // First element is an array, meaning the developer intended + // the first element to be a callback. Merge it in so that + // processing can be uniform. + $hook = array_merge( $hook[0], array_slice( $hook, 1 ) ); + } + + /** + * $hook can be: a function, an object, an array of $function and + * $data, an array of just a function, an array of object and + * method, or an array of object, method, and data. + */ + if ( $hook[0] instanceof Closure ) { + $fname = "hook-$event-closure"; + $callback = array_shift( $hook ); + } elseif ( is_object( $hook[0] ) ) { + $object = array_shift( $hook ); + $method = array_shift( $hook ); + + // If no method was specified, default to on$event. + if ( $method === null ) { + $method = "on$event"; + } + + $fname = get_class( $object ) . '::' . $method; + $callback = [ $object, $method ]; + } elseif ( is_string( $hook[0] ) ) { + $fname = $callback = array_shift( $hook ); + } else { + throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" ); + } + + // Run autoloader (workaround for call_user_func_array bug) + // and throw error if not callable. + if ( !is_callable( $callback ) ) { + throw new MWException( 'Invalid callback ' . $fname . ' in hooks for ' . $event . "\n" ); + } + + // mark hook as deprecated, if deprecation version is specified + if ( $deprecatedVersion !== null ) { + wfDeprecated( "$event hook (used in $fname)", $deprecatedVersion ); + } + + // Call the hook. + $hook_args = array_merge( $hook, $args ); + return call_user_func_array( $callback, $hook_args ); + } + /** * Call hook functions defined in Hooks::register and $wgHooks. * - * For a certain hook event, fetch the array of hook events and + * For the given hook event, fetch the array of hook events and * process them. Determine the proper callback for each hook and * then call the actual hook using the appropriate arguments. * Finally, process the return value and return/throw accordingly. * + * For hook event that are not abortable through a handler's return value, + * use runWithoutAbort() instead. + * * @param string $event Event name * @param array $args Array of parameters passed to hook functions - * @param string|null $deprecatedVersion Optionally, mark hook as deprecated with version number + * @param string|null $deprecatedVersion [optional] Mark hook as deprecated with version number * @return bool True if no handler aborted the hook * * @throws Exception @@ -130,61 +202,11 @@ class Hooks { */ public static function run( $event, array $args = [], $deprecatedVersion = null ) { foreach ( self::getHandlers( $event ) as $hook ) { - // Turn non-array values into an array. (Can't use casting because of objects.) - if ( !is_array( $hook ) ) { - $hook = [ $hook ]; - } - - if ( !array_filter( $hook ) ) { - // Either array is empty or it's an array filled with null/false/empty. + $retval = self::callHook( $event, $hook, $args, $deprecatedVersion ); + if ( $retval === null ) { continue; - } elseif ( is_array( $hook[0] ) ) { - // First element is an array, meaning the developer intended - // the first element to be a callback. Merge it in so that - // processing can be uniform. - $hook = array_merge( $hook[0], array_slice( $hook, 1 ) ); - } - - /** - * $hook can be: a function, an object, an array of $function and - * $data, an array of just a function, an array of object and - * method, or an array of object, method, and data. - */ - if ( $hook[0] instanceof Closure ) { - $func = "hook-$event-closure"; - $callback = array_shift( $hook ); - } elseif ( is_object( $hook[0] ) ) { - $object = array_shift( $hook ); - $method = array_shift( $hook ); - - // If no method was specified, default to on$event. - if ( $method === null ) { - $method = "on$event"; - } - - $func = get_class( $object ) . '::' . $method; - $callback = [ $object, $method ]; - } elseif ( is_string( $hook[0] ) ) { - $func = $callback = array_shift( $hook ); - } else { - throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" ); - } - - // Run autoloader (workaround for call_user_func_array bug) - // and throw error if not callable. - if ( !is_callable( $callback ) ) { - throw new MWException( 'Invalid callback ' . $func . ' in hooks for ' . $event . "\n" ); } - // mark hook as deprecated, if deprecation version is specified - if ( $deprecatedVersion !== null ) { - wfDeprecated( "$event hook (used in $func)", $deprecatedVersion ); - } - - // Call the hook. - $hook_args = array_merge( $hook, $args ); - $retval = call_user_func_array( $callback, $hook_args ); - // Process the return value. if ( is_string( $retval ) ) { // String returned means error. @@ -197,4 +219,26 @@ class Hooks { return true; } + + /** + * Call hook functions defined in Hooks::register and $wgHooks. + * + * @param string $event Event name + * @param array $args Array of parameters passed to hook functions + * @param string|null $deprecatedVersion [optional] Mark hook as deprecated with version number + * @return bool Always true + * @throws MWException If a callback is invalid, unknown + * @throws UnexpectedValueException If a callback returns an abort value. + * @since 1.30 + */ + public static function runWithoutAbort( $event, array $args = [], $deprecatedVersion = null ) { + foreach ( self::getHandlers( $event ) as $hook ) { + $fname = null; + $retval = self::callHook( $event, $hook, $args, $deprecatedVersion, $fname ); + if ( $retval !== null && $retval !== true ) { + throw new UnexpectedValueException( "Invalid return from $fname for unabortable $event." ); + } + } + return true; + } } diff --git a/includes/Licenses.php b/includes/Licenses.php deleted file mode 100644 index 6467777bff..0000000000 --- a/includes/Licenses.php +++ /dev/null @@ -1,210 +0,0 @@ - - * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason - * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later - */ - -/** - * A License class for use on Special:Upload - */ -class Licenses extends HTMLFormField { - /** @var string */ - protected $msg; - - /** @var array */ - protected $licenses = []; - - /** @var string */ - protected $html; - /**#@-*/ - - /** - * @param array $params - */ - public function __construct( $params ) { - parent::__construct( $params ); - - $this->msg = empty( $params['licenses'] ) - ? wfMessage( 'licenses' )->inContentLanguage()->plain() - : $params['licenses']; - $this->selected = null; - - $this->makeLicenses(); - } - - /** - * @private - */ - protected function makeLicenses() { - $levels = []; - $lines = explode( "\n", $this->msg ); - - foreach ( $lines as $line ) { - if ( strpos( $line, '*' ) !== 0 ) { - continue; - } else { - list( $level, $line ) = $this->trimStars( $line ); - - if ( strpos( $line, '|' ) !== false ) { - $obj = new License( $line ); - $this->stackItem( $this->licenses, $levels, $obj ); - } else { - if ( $level < count( $levels ) ) { - $levels = array_slice( $levels, 0, $level ); - } - if ( $level == count( $levels ) ) { - $levels[$level - 1] = $line; - } elseif ( $level > count( $levels ) ) { - $levels[] = $line; - } - } - } - } - } - - /** - * @param string $str - * @return array - */ - protected function trimStars( $str ) { - $numStars = strspn( $str, '*' ); - return [ $numStars, ltrim( substr( $str, $numStars ), ' ' ) ]; - } - - /** - * @param array &$list - * @param array $path - * @param mixed $item - */ - protected function stackItem( &$list, $path, $item ) { - $position =& $list; - if ( $path ) { - foreach ( $path as $key ) { - $position =& $position[$key]; - } - } - $position[] = $item; - } - - /** - * @param array $tagset - * @param int $depth - */ - protected function makeHtml( $tagset, $depth = 0 ) { - foreach ( $tagset as $key => $val ) { - if ( is_array( $val ) ) { - $this->html .= $this->outputOption( - $key, '', - [ - 'disabled' => 'disabled', - 'style' => 'color: GrayText', // for MSIE - ], - $depth - ); - $this->makeHtml( $val, $depth + 1 ); - } else { - $this->html .= $this->outputOption( - $val->text, $val->template, - [ 'title' => '{{' . $val->template . '}}' ], - $depth - ); - } - } - } - - /** - * @param string $message - * @param string $value - * @param null|array $attribs - * @param int $depth - * @return string - */ - protected function outputOption( $message, $value, $attribs = null, $depth = 0 ) { - $msgObj = $this->msg( $message ); - $text = $msgObj->exists() ? $msgObj->text() : $message; - $attribs['value'] = $value; - if ( $value === $this->selected ) { - $attribs['selected'] = 'selected'; - } - - $val = str_repeat( /*   */ "\xc2\xa0", $depth * 2 ) . $text; - return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n"; - } - - /**#@-*/ - - /** - * Accessor for $this->licenses - * - * @return array - */ - public function getLicenses() { - return $this->licenses; - } - - /** - * Accessor for $this->html - * - * @param bool $value - * - * @return string - */ - public function getInputHTML( $value ) { - $this->selected = $value; - - $this->html = $this->outputOption( wfMessage( 'nolicense' )->text(), '', - (bool)$this->selected ? null : [ 'selected' => 'selected' ] ); - $this->makeHtml( $this->getLicenses() ); - - $attribs = [ - 'name' => $this->mName, - 'id' => $this->mID - ]; - if ( !empty( $this->mParams['disabled'] ) ) { - $attibs['disabled'] = 'disabled'; - } - - return Html::rawElement( 'select', $attribs, $this->html ); - } -} - -/** - * A License class for use on Special:Upload (represents a single type of license). - */ -class License { - /** @var string */ - public $template; - - /** @var string */ - public $text; - - /** - * @param string $str License name?? - */ - function __construct( $str ) { - list( $text, $template ) = explode( '|', strrev( $str ), 2 ); - - $this->template = strrev( $template ); - $this->text = strrev( $text ); - } -} diff --git a/includes/Linker.php b/includes/Linker.php index d55f0e0bb5..403b10a149 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -76,7 +76,7 @@ class Linker { * @since 1.18 Method exists since 1.16 as non-static, made static in 1.18. * @deprecated since 1.28, use MediaWiki\Linker\LinkRenderer instead * - * @param Title $target Can currently only be a Title, but this may + * @param LinkTarget $target Can currently only be a LinkTarget, but this may * change to support Images, literal URLs, etc. * @param string $html The HTML contents of the element, i.e., * the link text. This is raw HTML and will not be escaped. If null, @@ -107,8 +107,8 @@ class Linker { public static function link( $target, $html = null, $customAttribs = [], $query = [], $options = [] ) { - if ( !$target instanceof Title ) { - wfWarn( __METHOD__ . ': Requires $target to be a Title object.', 2 ); + if ( !$target instanceof LinkTarget ) { + wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 ); return "$html"; } @@ -1175,7 +1175,7 @@ class Linker { $sectionTitle = Title::newFromText( '#' . $section ); } else { $sectionTitle = Title::makeTitleSafe( $title->getNamespace(), - $title->getDBkey(), $section ); + $title->getDBkey(), Sanitizer::decodeCharReferences( $section ) ); } if ( $sectionTitle ) { $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' ); @@ -1291,9 +1291,7 @@ class Linker { if ( $target->getText() == '' && !$target->isExternal() && !$local && $title ) { - $newTarget = clone $title; - $newTarget->setFragment( '#' . $target->getFragment() ); - $target = $newTarget; + $target = $title->createFragmentTarget( $target->getFragment() ); } $thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail; @@ -1321,7 +1319,7 @@ class Linker { * * @note This is only public for technical reasons. It's not intended for use outside Linker. * - * @param Title $title + * @param LinkTarget $linkTarget * @param string $text * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), * as used by WikiMap. @@ -1330,23 +1328,23 @@ class Linker { * @return string HTML link */ public static function makeCommentLink( - Title $title, $text, $wikiId = null, $options = [] + LinkTarget $linkTarget, $text, $wikiId = null, $options = [] ) { - if ( $wikiId !== null && !$title->isExternal() ) { + if ( $wikiId !== null && !$linkTarget->isExternal() ) { $link = self::makeExternalLink( WikiMap::getForeignURL( $wikiId, - $title->getNamespace() === 0 - ? $title->getDBkey() - : MWNamespace::getCanonicalName( $title->getNamespace() ) . ':' - . $title->getDBkey(), - $title->getFragment() + $linkTarget->getNamespace() === 0 + ? $linkTarget->getDBkey() + : MWNamespace::getCanonicalName( $linkTarget->getNamespace() ) . ':' + . $linkTarget->getDBkey(), + $linkTarget->getFragment() ), $text, /* escape = */ false // Already escaped ); } else { - $link = self::link( $title, $text, [], [], $options ); + $link = self::link( $linkTarget, $text, [], [], $options ); } return $link; @@ -1539,10 +1537,16 @@ class Linker { if ( $sectionIndex !== false ) { $classes .= " tocsection-$sectionIndex"; } - return "\n
  • ' . - $tocnumber . ' ' . - $tocline . ''; + + // \n
  • + // $tocnumber $tocline + return "\n" . Html::openElement( 'li', [ 'class' => $classes ] ) + . Html::rawElement( 'a', + [ 'href' => "#$anchor" ], + Html::element( 'span', [ 'class' => 'tocnumber' ], $tocnumber ) + . ' ' + . Html::rawElement( 'span', [ 'class' => 'toctext' ], $tocline ) + ); } /** @@ -1624,14 +1628,16 @@ class Linker { $link, $fallbackAnchor = false ) { $anchorEscaped = htmlspecialchars( $anchor ); - $ret = "$html" - . $link - . ""; + $fallback = ''; if ( $fallbackAnchor !== false && $fallbackAnchor !== $anchor ) { $fallbackAnchor = htmlspecialchars( $fallbackAnchor ); - $ret = "
    $ret"; + $fallback = ""; } + $ret = "$html" + . $link + . ""; + return $ret; } diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php index 97dba26b95..f2f98ba29c 100644 --- a/includes/MWNamespace.php +++ b/includes/MWNamespace.php @@ -38,6 +38,15 @@ class MWNamespace { */ private static $alwaysCapitalizedNamespaces = [ NS_SPECIAL, NS_USER, NS_MEDIAWIKI ]; + /** @var string[]|null Canonical namespaces cache */ + private static $canonicalNamespaces = null; + + /** @var array|false Canonical namespaces index cache */ + private static $namespaceIndexes = false; + + /** @var int[]|null Valid namespaces cache */ + private static $validNamespaces = null; + /** * Throw an exception when trying to get the subject or talk page * for a given namespace where it does not make sense. @@ -57,6 +66,19 @@ class MWNamespace { return true; } + /** + * Clear internal caches + * + * For use in unit testing when namespace configuration is changed. + * + * @since 1.31 + */ + public static function clearCaches() { + self::$canonicalNamespaces = null; + self::$namespaceIndexes = false; + self::$validNamespaces = null; + } + /** * Can pages in the given namespace be moved? * @@ -200,23 +222,28 @@ class MWNamespace { * (English) names. * * @param bool $rebuild Rebuild namespace list (default = false). Used for testing. + * Deprecated since 1.31, use self::clearCaches() instead. * * @return array * @since 1.17 */ public static function getCanonicalNamespaces( $rebuild = false ) { - static $namespaces = null; - if ( $namespaces === null || $rebuild ) { + if ( $rebuild ) { + self::clearCaches(); + } + + if ( self::$canonicalNamespaces === null ) { global $wgExtraNamespaces, $wgCanonicalNamespaceNames; - $namespaces = [ NS_MAIN => '' ] + $wgCanonicalNamespaceNames; + self::$canonicalNamespaces = [ NS_MAIN => '' ] + $wgCanonicalNamespaceNames; // Add extension namespaces - $namespaces += ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' ); + self::$canonicalNamespaces += + ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' ); if ( is_array( $wgExtraNamespaces ) ) { - $namespaces += $wgExtraNamespaces; + self::$canonicalNamespaces += $wgExtraNamespaces; } - Hooks::run( 'CanonicalNamespaces', [ &$namespaces ] ); + Hooks::run( 'CanonicalNamespaces', [ &self::$canonicalNamespaces ] ); } - return $namespaces; + return self::$canonicalNamespaces; } /** @@ -242,15 +269,14 @@ class MWNamespace { * @return int */ public static function getCanonicalIndex( $name ) { - static $xNamespaces = false; - if ( $xNamespaces === false ) { - $xNamespaces = []; + if ( self::$namespaceIndexes === false ) { + self::$namespaceIndexes = []; foreach ( self::getCanonicalNamespaces() as $i => $text ) { - $xNamespaces[strtolower( $text )] = $i; + self::$namespaceIndexes[strtolower( $text )] = $i; } } - if ( array_key_exists( $name, $xNamespaces ) ) { - return $xNamespaces[$name]; + if ( array_key_exists( $name, self::$namespaceIndexes ) ) { + return self::$namespaceIndexes[$name]; } else { return null; } @@ -262,19 +288,17 @@ class MWNamespace { * @return array */ public static function getValidNamespaces() { - static $mValidNamespaces = null; - - if ( is_null( $mValidNamespaces ) ) { + if ( is_null( self::$validNamespaces ) ) { foreach ( array_keys( self::getCanonicalNamespaces() ) as $ns ) { if ( $ns >= 0 ) { - $mValidNamespaces[] = $ns; + self::$validNamespaces[] = $ns; } } // T109137: sort numerically - sort( $mValidNamespaces, SORT_NUMERIC ); + sort( self::$validNamespaces, SORT_NUMERIC ); } - return $mValidNamespaces; + return self::$validNamespaces; } /** diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 6e7799a308..93c8a71c9e 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -59,10 +59,10 @@ class MagicWord { /**#@-*/ - /** @var int */ + /** @var string */ public $mId; - /** @var array */ + /** @var string[] */ public $mSynonyms; /** @var bool */ @@ -92,7 +92,10 @@ class MagicWord { /** @var bool */ private $mFound = false; + /** @var bool */ public static $mVariableIDsInitialised = false; + + /** @var string[] */ public static $mVariableIDs = [ '!', 'currentmonth', @@ -174,7 +177,9 @@ class MagicWord { 'cascadingsources', ]; - /* Array of caching hints for ParserCache */ + /** Array of caching hints for ParserCache + * @var array [ string => int ] + */ public static $mCacheTTLs = [ 'currentmonth' => 86400, 'currentmonth1' => 86400, @@ -216,6 +221,7 @@ class MagicWord { 'numberingroup' => 3600, ]; + /** @var string[] */ public static $mDoubleUnderscoreIDs = [ 'notoc', 'nogallery', @@ -232,17 +238,30 @@ class MagicWord { 'nocontentconvert', ]; + /** @var string[] */ public static $mSubstIDs = [ 'subst', 'safesubst', ]; + /** @var array [ string => MagicWord ] */ public static $mObjects = []; + + /** @var MagicWordArray */ public static $mDoubleUnderscoreArray = null; /**#@-*/ - public function __construct( $id = 0, $syn = [], $cs = false ) { + /** + * Create a new MagicWord object + * + * Use factory instead: MagicWord::get + * + * @param string $id The internal name of the magic word + * @param string[]|string $syn synonyms for the magic word + * @param bool $cs If magic word is case sensitive + */ + public function __construct( $id = null, $syn = [], $cs = false ) { $this->mId = $id; $this->mSynonyms = (array)$syn; $this->mCaseSensitive = $cs; @@ -251,7 +270,7 @@ class MagicWord { /** * Factory: creates an object representing an ID * - * @param int $id + * @param string $id The internal name of the magic word * * @return MagicWord */ @@ -267,7 +286,7 @@ class MagicWord { /** * Get an array of parser variable IDs * - * @return array + * @return string[] */ public static function getVariableIDs() { if ( !self::$mVariableIDsInitialised ) { @@ -280,7 +299,7 @@ class MagicWord { /** * Get an array of parser substitution modifier IDs - * @return array + * @return string[] */ public static function getSubstIDs() { return self::$mSubstIDs; @@ -289,7 +308,7 @@ class MagicWord { /** * Allow external reads of TTL array * - * @param int $id + * @param string $id * @return int */ public static function getCacheTTL( $id ) { @@ -324,7 +343,7 @@ class MagicWord { /** * Initialises this object with an ID * - * @param int $id + * @param string $id * @throws MWException */ public function load( $id ) { @@ -630,7 +649,7 @@ class MagicWord { } /** - * @return array + * @return string[] */ public function getSynonyms() { return $this->mSynonyms; @@ -650,7 +669,7 @@ class MagicWord { * Adds all the synonyms of this MagicWord to an array, to allow quick * lookup in a list of magic words * - * @param array &$array + * @param string[] &$array * @param string $value */ public function addToArray( &$array, $value ) { @@ -668,7 +687,7 @@ class MagicWord { } /** - * @return int + * @return string */ public function getId() { return $this->mId; diff --git a/includes/MagicWordArray.php b/includes/MagicWordArray.php index 5856e21b71..4010ec7585 100644 --- a/includes/MagicWordArray.php +++ b/includes/MagicWordArray.php @@ -203,7 +203,9 @@ class MagicWordArray { */ public function parseMatch( $m ) { reset( $m ); - while ( list( $key, $value ) = each( $m ) ) { + while ( ( $key = key( $m ) ) !== null ) { + $value = current( $m ); + next( $m ); if ( $key === 0 || $value === '' ) { continue; } diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 7b59ee93dc..43de4ba375 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -712,10 +712,11 @@ class MediaWiki { MWExceptionHandler::rollbackMasterChangesAndLog( $e ); } + $blocksHttpClient = true; // Defer everything else if possible... - $callback = function () use ( $mode ) { + $callback = function () use ( $mode, &$blocksHttpClient ) { try { - $this->restInPeace( $mode ); + $this->restInPeace( $mode, $blocksHttpClient ); } catch ( Exception $e ) { // If this is post-send, then displaying errors can cause broken HTML MWExceptionHandler::rollbackMasterChangesAndLog( $e ); @@ -725,9 +726,11 @@ class MediaWiki { if ( function_exists( 'register_postsend_function' ) ) { // https://github.com/facebook/hhvm/issues/1230 register_postsend_function( $callback ); + $blocksHttpClient = false; } else { if ( function_exists( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); + $blocksHttpClient = false; } else { // Either all DB and deferred updates should happen or none. // The latter should not be cancelled due to client disconnect. @@ -870,8 +873,9 @@ class MediaWiki { /** * Ends this task peacefully * @param string $mode Use 'fast' to always skip job running + * @param bool $blocksHttpClient Whether this blocks an HTTP response to a client */ - public function restInPeace( $mode = 'fast' ) { + public function restInPeace( $mode = 'fast', $blocksHttpClient = true ) { $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); // Assure deferred updates are not in the main transaction $lbFactory->commitMasterChanges( __METHOD__ ); @@ -880,15 +884,17 @@ class MediaWiki { $trxProfiler = Profiler::instance()->getTransactionProfiler(); $trxProfiler->resetExpectations(); $trxProfiler->setExpectations( - $this->config->get( 'TrxProfilerLimits' )['PostSend'], + $this->context->getRequest()->hasSafeMethod() + ? $this->config->get( 'TrxProfilerLimits' )['PostSend-GET'] + : $this->config->get( 'TrxProfilerLimits' )['PostSend-POST'], __METHOD__ ); // Important: this must be the last deferred update added (T100085, T154425) DeferredUpdates::addCallableUpdate( [ JobQueueGroup::class, 'pushLazyJobs' ] ); - // Do any deferred jobs - DeferredUpdates::doUpdates( 'enqueue' ); + // Do any deferred jobs; preferring to run them now if a client will not wait on them + DeferredUpdates::doUpdates( $blocksHttpClient ? 'enqueue' : 'run' ); // Now that everything specific to this request is done, // try to occasionally run jobs (if enabled) from the queues diff --git a/includes/MovePage.php b/includes/MovePage.php index 39dc6424db..2f8255ba38 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -442,7 +442,6 @@ class MovePage { private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true, array $changeTags = [] ) { - global $wgContLang; if ( $nt->exists() ) { $moveOverRedirect = true; $logType = 'move_redir'; @@ -520,8 +519,6 @@ class MovePage { if ( $reason ) { $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } - # Truncate for whole multibyte characters. - $comment = $wgContLang->truncate( $comment, 255 ); $dbw = wfGetDB( DB_MASTER ); diff --git a/includes/OrderedStreamingForkController.php b/includes/OrderedStreamingForkController.php new file mode 100644 index 0000000000..ff29cb510d --- /dev/null +++ b/includes/OrderedStreamingForkController.php @@ -0,0 +1,216 @@ +workCallback = $workCallback; + $this->input = $input; + $this->output = $output; + } + + /** + * @inheritDoc + */ + public function start() { + if ( $this->procsToStart > 0 ) { + $status = parent::start(); + if ( $status === 'child' ) { + $this->consume(); + } + } else { + $status = 'parent'; + $this->consumeNoFork(); + } + return $status; + } + + /** + * @param int $numProcs + * @return string + */ + protected function forkWorkers( $numProcs ) { + $this->prepareEnvironment(); + + $childSockets = []; + // Create the child processes + for ( $i = 0; $i < $numProcs; $i++ ) { + $sockets = stream_socket_pair( STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP ); + // Do the fork + $pid = pcntl_fork(); + if ( $pid === -1 || $pid === false ) { + echo "Error creating child processes\n"; + exit( 1 ); + } + + if ( !$pid ) { + $this->initChild(); + $this->childNumber = $i; + $this->input = $sockets[0]; + $this->output = $sockets[0]; + fclose( $sockets[1] ); + return 'child'; + } else { + // This is the parent process + $this->children[$pid] = true; + fclose( $sockets[0] ); + $childSockets[] = $sockets[1]; + } + } + $this->feedChildren( $childSockets ); + foreach ( $childSockets as $socket ) { + // if a child has already shutdown the sockets will be closed, + // closing a second time would raise a warning. + if ( is_resource( $socket ) ) { + fclose( $socket ); + } + } + return 'parent'; + } + + /** + * Child worker process. Reads work from $this->input and writes the + * result of that work to $this->output when completed. + */ + protected function consume() { + while ( !feof( $this->input ) ) { + $line = trim( fgets( $this->input ) ); + if ( $line ) { + list( $id, $data ) = json_decode( $line ); + $result = call_user_func( $this->workCallback, $data ); + fwrite( $this->output, json_encode( [ $id, $result ] ) . "\n" ); + } + } + } + + /** + * Special cased version of self::consume() when no forking occurs + */ + protected function consumeNoFork() { + while ( !feof( $this->input ) ) { + $line = trim( fgets( $this->input ) ); + if ( $line ) { + $result = call_user_func( $this->workCallback, $line ); + fwrite( $this->output, "$result\n" ); + } + } + } + + /** + * Reads lines of work from $this->input and farms them out to + * the provided socket. + * + * @param resource[] $sockets + */ + protected function feedChildren( array $sockets ) { + $used = []; + $id = 0; + $this->nextOutputId = 0; + + while ( !feof( $this->input ) ) { + $data = fgets( $this->input ); + if ( $used ) { + do { + $this->updateAvailableSockets( $sockets, $used, $sockets ? 0 : 5 ); + } while ( !$sockets ); + } + $data = trim( $data ); + if ( !$data ) { + continue; + } + $socket = array_pop( $sockets ); + fwrite( $socket, json_encode( [ $id++, $data ] ) . "\n" ); + $used[] = $socket; + } + while ( $used ) { + $this->updateAvailableSockets( $sockets, $used, 5 ); + } + } + + /** + * Moves sockets from $used to $sockets when they are available + * for more work + * + * @param resource[] &$sockets List of sockets that are waiting for work + * @param resource[] &$used List of sockets currently performing work + * @param int $timeout The number of seconds to block waiting. 0 for + * non-blocking operation. + */ + protected function updateAvailableSockets( &$sockets, &$used, $timeout ) { + $read = $used; + $write = $except = []; + stream_select( $read, $write, $except, $timeout ); + foreach ( $read as $socket ) { + $line = fgets( $socket ); + list( $id, $data ) = json_decode( trim( $line ) ); + $this->receive( (int)$id, $data ); + $sockets[] = $socket; + $idx = array_search( $socket, $used ); + unset( $used[$idx] ); + } + } + + /** + * @param int $id + * @param string $data + */ + protected function receive( $id, $data ) { + if ( $id !== $this->nextOutputId ) { + $this->delayedOutputData[$id] = $data; + return; + } + fwrite( $this->output, $data . "\n" ); + $this->nextOutputId = $id + 1; + while ( isset( $this->delayedOutputData[$this->nextOutputId] ) ) { + fwrite( $this->output, $this->delayedOutputData[$this->nextOutputId] . "\n" ); + unset( $this->delayedOutputData[$this->nextOutputId] ); + $this->nextOutputId++; + } + } +} diff --git a/includes/OutputPage.php b/includes/OutputPage.php index dd21194bdb..20b2c3c32d 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -139,6 +139,9 @@ class OutputPage extends ContextSource { /** @var array Array of elements in "". Parser might add its own headers! */ protected $mHeadItems = []; + /** @var array Additional classes; there are also classes from other sources */ + protected $mAdditionalBodyClasses = []; + /** @var array */ protected $mModules = []; @@ -705,6 +708,16 @@ class OutputPage extends ContextSource { return isset( $this->mHeadItems[$name] ); } + /** + * Add a class to the element + * + * @since 1.30 + * @param string|string[] $classes One or more classes to add + */ + public function addBodyClasses( $classes ) { + $this->mAdditionalBodyClasses = array_merge( $this->mAdditionalBodyClasses, (array)$classes ); + } + /** * @deprecated since 1.28 Obsolete - wgUseETag experiment was removed. * @param string $tag @@ -1839,7 +1852,7 @@ class OutputPage extends ContextSource { // Avoid PHP 7.1 warning of passing $this by reference $outputPage = $this; Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] ); - Hooks::run( 'OutputPageParserOutput', [ &$outputPage, $parserOutput ] ); + Hooks::runWithoutAbort( 'OutputPageParserOutput', [ &$outputPage, $parserOutput ] ); // This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata // so that extensions may modify ParserOutput to toggle TOC. @@ -1877,7 +1890,7 @@ class OutputPage extends ContextSource { $text = $parserOutput->getText(); // Avoid PHP 7.1 warning of passing $this by reference $outputPage = $this; - Hooks::run( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] ); + Hooks::runWithoutAbort( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] ); $this->addHTML( $text ); } @@ -2013,7 +2026,7 @@ class OutputPage extends ContextSource { } $age = time() - wfTimestamp( TS_UNIX, $mtime ); - $adaptiveTTL = max( .9 * $age, $minTTL ); + $adaptiveTTL = max( 0.9 * $age, $minTTL ); $adaptiveTTL = min( $adaptiveTTL, $maxTTL ); $this->lowerCdnMaxage( (int)$adaptiveTTL ); @@ -2187,7 +2200,7 @@ class OutputPage extends ContextSource { // IE and some other browsers use BCP 47 standards in // their Accept-Language header, like "zh-CN" or "zh-Hant". // We should handle these too. - $variantBCP47 = wfBCP47( $variant ); + $variantBCP47 = LanguageCode::bcp47( $variant ); if ( $variantBCP47 !== $variant ) { $aloption[] = 'substr=' . $variantBCP47; } @@ -2423,7 +2436,7 @@ class OutputPage extends ContextSource { $outputPage = $this; // Hook that allows last minute changes to the output page, e.g. // adding of CSS or Javascript by extensions. - Hooks::run( 'BeforePageDisplay', [ &$outputPage, &$sk ] ); + Hooks::runWithoutAbort( 'BeforePageDisplay', [ &$outputPage, &$sk ] ); try { $sk->outputPage(); @@ -2435,7 +2448,7 @@ class OutputPage extends ContextSource { try { // This hook allows last minute changes to final overall output by modifying output buffer - Hooks::run( 'AfterFinalPageOutput', [ $this ] ); + Hooks::runWithoutAbort( 'AfterFinalPageOutput', [ $this ] ); } catch ( Exception $e ) { ob_end_clean(); // bug T129657 throw $e; @@ -2910,20 +2923,19 @@ class OutputPage extends ContextSource { $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) ); $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) ); - $min = ResourceLoader::inDebugMode() ? '' : '.min'; // Use an IE conditional comment to serve the script only to old IE $pieces[] = ''; $pieces[] = Html::closeElement( 'head' ); - $bodyClasses = []; + $bodyClasses = $this->mAdditionalBodyClasses; $bodyClasses[] = 'mediawiki'; # Classes for LTR/RTL directionality support @@ -3425,7 +3437,7 @@ class OutputPage extends ContextSource { foreach ( $variants as $variant ) { $tags["variant-$variant"] = Html::element( 'link', [ 'rel' => 'alternate', - 'hreflang' => wfBCP47( $variant ), + 'hreflang' => LanguageCode::bcp47( $variant ), 'href' => $this->getTitle()->getLocalURL( [ 'variant' => $variant ] ) ] diff --git a/includes/PHPVersionCheck.php b/includes/PHPVersionCheck.php index e9e271ce49..a862903d52 100644 --- a/includes/PHPVersionCheck.php +++ b/includes/PHPVersionCheck.php @@ -29,7 +29,7 @@ */ class PHPVersionCheck { /* @var string The number of the MediaWiki version used */ - var $mwVersion = '1.30'; + var $mwVersion = '1.31'; var $functionsExtensionsMapping = array( 'mb_substr' => 'mbstring', 'utf8_encode' => 'xml', diff --git a/includes/PageProps.php b/includes/PageProps.php index dac756ed75..ff8deee373 100644 --- a/includes/PageProps.php +++ b/includes/PageProps.php @@ -242,6 +242,8 @@ class PageProps { private function getGoodIDs( $titles ) { $result = []; if ( is_array( $titles ) ) { + ( new LinkBatch( $titles ) )->execute(); + foreach ( $titles as $title ) { $pageID = $title->getArticleID(); if ( $pageID > 0 ) { diff --git a/includes/Pingback.php b/includes/Pingback.php index bd1b2a25b4..c3393bcc1c 100644 --- a/includes/Pingback.php +++ b/includes/Pingback.php @@ -228,6 +228,7 @@ class Pingback { * * The schema for the data is located at: * + * @return bool */ public function sendPingback() { if ( !$this->acquireLock() ) { diff --git a/includes/Preferences.php b/includes/Preferences.php index c64e8a8ae1..ba90121a83 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -1,7 +1,5 @@ $name ) { - $display = wfBCP47( $code ) . ' - ' . $name; + $display = LanguageCode::bcp47( $code ) . ' - ' . $name; $options[$display] = $code; } $defaultPreferences['language'] = [ @@ -396,7 +394,7 @@ class Preferences { $options = []; foreach ( $variantArray as $code => $name ) { - $display = wfBCP47( $code ) . ' - ' . $name; + $display = LanguageCode::bcp47( $code ) . ' - ' . $name; $options[$display] = $code; } @@ -556,6 +554,22 @@ class Preferences { 'label-message' => 'tog-ccmeonemails', 'disabled' => $disableEmailPrefs, ]; + + if ( $config->get( 'EnableUserEmailBlacklist' ) + && !$disableEmailPrefs + && !(bool)$user->getOption( 'disablemail' ) + ) { + $lookup = CentralIdLookup::factory(); + $ids = $user->getOption( 'email-blacklist', [] ); + $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : []; + + $defaultPreferences['email-blacklist'] = [ + 'type' => 'usersmultiselect', + 'label-message' => 'email-blacklist-label', + 'section' => 'personal/email', + 'default' => implode( "\n", $names ), + ]; + } } if ( $config->get( 'EnotifWatchlist' ) ) { @@ -611,7 +625,6 @@ class Preferences { $defaultPreferences['skin'] = [ 'type' => 'radio', 'options' => $skinOptions, - 'label' => ' ', 'section' => 'rendering/skin', ]; } @@ -681,7 +694,6 @@ class Preferences { $defaultPreferences['date'] = [ 'type' => 'radio', 'options' => $dateOptions, - 'label' => ' ', 'section' => 'rendering/dateformat', ]; } @@ -835,7 +847,6 @@ class Preferences { $context->msg( 'editfont-monospace' )->text() => 'monospace', $context->msg( 'editfont-sansserif' )->text() => 'sans-serif', $context->msg( 'editfont-serif' )->text() => 'serif', - $context->msg( 'editfont-default' )->text() => 'default', ] ]; } @@ -923,6 +934,12 @@ class Preferences { $defaultPreferences['rcfilters-wl-saved-queries'] = [ 'type' => 'api', ]; + $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [ + 'type' => 'api', + ]; + $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [ + 'type' => 'api', + ]; $defaultPreferences['rcfilters-rclimit'] = [ 'type' => 'api', ]; @@ -958,6 +975,15 @@ class Preferences { 'label-message' => 'tog-shownumberswatching', ]; } + + if ( $config->get( 'StructuredChangeFiltersShowPreference' ) ) { + $defaultPreferences['rcenhancedfilters-disable'] = [ + 'type' => 'toggle', + 'section' => 'rc/opt-out', + 'label-message' => 'rcfilters-preference-label', + 'help-message' => 'rcfilters-preference-help', + ]; + } } /** @@ -1356,7 +1382,7 @@ class Preferences { $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() ); # Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save' $htmlForm->setSubmitTooltip( 'preferences-save' ); - $htmlForm->setSubmitID( 'prefsubmit' ); + $htmlForm->setSubmitID( 'prefcontrol' ); $htmlForm->setSubmitCallback( [ 'Preferences', 'tryFormSubmit' ] ); return $htmlForm; @@ -1626,123 +1652,3 @@ class Preferences { return $timeZoneList; } } - -/** Some tweaks to allow js prefs to work */ -class PreferencesForm extends HTMLForm { - // Override default value from HTMLForm - protected $mSubSectionBeforeFields = false; - - private $modifiedUser; - - /** - * @param User $user - */ - public function setModifiedUser( $user ) { - $this->modifiedUser = $user; - } - - /** - * @return User - */ - public function getModifiedUser() { - if ( $this->modifiedUser === null ) { - return $this->getUser(); - } else { - return $this->modifiedUser; - } - } - - /** - * Get extra parameters for the query string when redirecting after - * successful save. - * - * @return array - */ - public function getExtraSuccessRedirectParameters() { - return []; - } - - /** - * @param string $html - * @return string - */ - function wrapForm( $html ) { - $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html ); - - return parent::wrapForm( $html ); - } - - /** - * @return string - */ - function getButtons() { - $attrs = [ 'id' => 'mw-prefs-restoreprefs' ]; - - if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) { - return ''; - } - - $html = parent::getButtons(); - - if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) { - $t = $this->getTitle()->getSubpage( 'reset' ); - - $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); - $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(), - Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) ); - - $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html ); - } - - return $html; - } - - /** - * Separate multi-option preferences into multiple preferences, since we - * have to store them separately - * @param array $data - * @return array - */ - function filterDataForSubmit( $data ) { - foreach ( $this->mFlatFields as $fieldname => $field ) { - if ( $field instanceof HTMLNestedFilterable ) { - $info = $field->mParams; - $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; - foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) { - $data["$prefix$key"] = $value; - } - unset( $data[$fieldname] ); - } - } - - return $data; - } - - /** - * Get the whole body of the form. - * @return string - */ - function getBody() { - return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' ); - } - - /** - * Get the "" for a given section key. Normally this is the - * prefs-$key message but we'll allow extensions to override it. - * @param string $key - * @return string - */ - function getLegend( $key ) { - $legend = parent::getLegend( $key ); - Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] ); - return $legend; - } - - /** - * Get the keys of each top level preference section. - * @return array of section keys - */ - function getPreferenceSections() { - return array_keys( array_filter( $this->mFieldTree, 'is_array' ) ); - } -} diff --git a/includes/Revision.php b/includes/Revision.php index ff4a284386..dd3ee782fd 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -362,7 +362,7 @@ class Revision implements IDBAccessObject { $row = self::fetchFromConds( $db, $conditions, $flags ); if ( $row ) { $rev = new Revision( $row ); - $rev->mWiki = $db->getWikiID(); + $rev->mWiki = $db->getDomainID(); return $rev; } @@ -571,168 +571,184 @@ class Revision implements IDBAccessObject { * @throws MWException * @access private */ - function __construct( $row ) { + public function __construct( $row ) { if ( is_object( $row ) ) { - $this->mId = intval( $row->rev_id ); - $this->mPage = intval( $row->rev_page ); - $this->mTextId = intval( $row->rev_text_id ); - $this->mComment = CommentStore::newKey( 'rev_comment' ) - // Legacy because $row probably came from self::selectFields() - ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text; - $this->mUser = intval( $row->rev_user ); - $this->mMinorEdit = intval( $row->rev_minor_edit ); - $this->mTimestamp = $row->rev_timestamp; - $this->mDeleted = intval( $row->rev_deleted ); - - if ( !isset( $row->rev_parent_id ) ) { - $this->mParentId = null; - } else { - $this->mParentId = intval( $row->rev_parent_id ); - } - - if ( !isset( $row->rev_len ) ) { - $this->mSize = null; - } else { - $this->mSize = intval( $row->rev_len ); - } - - if ( !isset( $row->rev_sha1 ) ) { - $this->mSha1 = null; - } else { - $this->mSha1 = $row->rev_sha1; - } + $this->constructFromDbRowObject( $row ); + } elseif ( is_array( $row ) ) { + $this->constructFromRowArray( $row ); + } else { + throw new MWException( 'Revision constructor passed invalid row format.' ); + } + $this->mUnpatrolled = null; + } - if ( isset( $row->page_latest ) ) { - $this->mCurrent = ( $row->rev_id == $row->page_latest ); - $this->mTitle = Title::newFromRow( $row ); - } else { - $this->mCurrent = false; - $this->mTitle = null; - } + /** + * @param object $row + */ + private function constructFromDbRowObject( $row ) { + $this->mId = intval( $row->rev_id ); + $this->mPage = intval( $row->rev_page ); + $this->mTextId = intval( $row->rev_text_id ); + $this->mComment = CommentStore::newKey( 'rev_comment' ) + // Legacy because $row probably came from self::selectFields() + ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text; + $this->mUser = intval( $row->rev_user ); + $this->mMinorEdit = intval( $row->rev_minor_edit ); + $this->mTimestamp = $row->rev_timestamp; + $this->mDeleted = intval( $row->rev_deleted ); + + if ( !isset( $row->rev_parent_id ) ) { + $this->mParentId = null; + } else { + $this->mParentId = intval( $row->rev_parent_id ); + } - if ( !isset( $row->rev_content_model ) ) { - $this->mContentModel = null; # determine on demand if needed - } else { - $this->mContentModel = strval( $row->rev_content_model ); - } + if ( !isset( $row->rev_len ) ) { + $this->mSize = null; + } else { + $this->mSize = intval( $row->rev_len ); + } - if ( !isset( $row->rev_content_format ) ) { - $this->mContentFormat = null; # determine on demand if needed - } else { - $this->mContentFormat = strval( $row->rev_content_format ); - } + if ( !isset( $row->rev_sha1 ) ) { + $this->mSha1 = null; + } else { + $this->mSha1 = $row->rev_sha1; + } - // Lazy extraction... - $this->mText = null; - if ( isset( $row->old_text ) ) { - $this->mTextRow = $row; - } else { - // 'text' table row entry will be lazy-loaded - $this->mTextRow = null; - } + if ( isset( $row->page_latest ) ) { + $this->mCurrent = ( $row->rev_id == $row->page_latest ); + $this->mTitle = Title::newFromRow( $row ); + } else { + $this->mCurrent = false; + $this->mTitle = null; + } - // Use user_name for users and rev_user_text for IPs... - $this->mUserText = null; // lazy load if left null - if ( $this->mUser == 0 ) { - $this->mUserText = $row->rev_user_text; // IP user - } elseif ( isset( $row->user_name ) ) { - $this->mUserText = $row->user_name; // logged-in user - } - $this->mOrigUserText = $row->rev_user_text; - } elseif ( is_array( $row ) ) { - // Build a new revision to be saved... - global $wgUser; // ugh - - # if we have a content object, use it to set the model and type - if ( !empty( $row['content'] ) ) { - // @todo when is that set? test with external store setup! check out insertOn() [dk] - if ( !empty( $row['text_id'] ) ) { - throw new MWException( "Text already stored in external store (id {$row['text_id']}), " . - "can't serialize content object" ); - } + if ( !isset( $row->rev_content_model ) ) { + $this->mContentModel = null; # determine on demand if needed + } else { + $this->mContentModel = strval( $row->rev_content_model ); + } - $row['content_model'] = $row['content']->getModel(); - # note: mContentFormat is initializes later accordingly - # note: content is serialized later in this method! - # also set text to null? - } + if ( !isset( $row->rev_content_format ) ) { + $this->mContentFormat = null; # determine on demand if needed + } else { + $this->mContentFormat = strval( $row->rev_content_format ); + } - $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null; - $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null; - $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null; - $this->mUserText = isset( $row['user_text'] ) - ? strval( $row['user_text'] ) : $wgUser->getName(); - $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId(); - $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0; - $this->mTimestamp = isset( $row['timestamp'] ) - ? strval( $row['timestamp'] ) : wfTimestampNow(); - $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0; - $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null; - $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null; - $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null; - - $this->mContentModel = isset( $row['content_model'] ) - ? strval( $row['content_model'] ) : null; - $this->mContentFormat = isset( $row['content_format'] ) - ? strval( $row['content_format'] ) : null; - - // Enforce spacing trimming on supplied text - $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; - $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; + // Lazy extraction... + $this->mText = null; + if ( isset( $row->old_text ) ) { + $this->mTextRow = $row; + } else { + // 'text' table row entry will be lazy-loaded $this->mTextRow = null; + } - $this->mTitle = isset( $row['title'] ) ? $row['title'] : null; - - // if we have a Content object, override mText and mContentModel - if ( !empty( $row['content'] ) ) { - if ( !( $row['content'] instanceof Content ) ) { - throw new MWException( '`content` field must contain a Content object.' ); - } - - $handler = $this->getContentHandler(); - $this->mContent = $row['content']; + // Use user_name for users and rev_user_text for IPs... + $this->mUserText = null; // lazy load if left null + if ( $this->mUser == 0 ) { + $this->mUserText = $row->rev_user_text; // IP user + } elseif ( isset( $row->user_name ) ) { + $this->mUserText = $row->user_name; // logged-in user + } + $this->mOrigUserText = $row->rev_user_text; + } - $this->mContentModel = $this->mContent->getModel(); - $this->mContentHandler = null; + /** + * @param array $row + * + * @throws MWException + */ + private function constructFromRowArray( array $row ) { + // Build a new revision to be saved... + global $wgUser; // ugh - $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() ); - } elseif ( $this->mText !== null ) { - $handler = $this->getContentHandler(); - $this->mContent = $handler->unserializeContent( $this->mText ); + # if we have a content object, use it to set the model and type + if ( !empty( $row['content'] ) ) { + if ( !( $row['content'] instanceof Content ) ) { + throw new MWException( '`content` field must contain a Content object.' ); } - // If we have a Title object, make sure it is consistent with mPage. - if ( $this->mTitle && $this->mTitle->exists() ) { - if ( $this->mPage === null ) { - // if the page ID wasn't known, set it now - $this->mPage = $this->mTitle->getArticleID(); - } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) { - // Got different page IDs. This may be legit (e.g. during undeletion), - // but it seems worth mentioning it in the log. - wfDebug( "Page ID " . $this->mPage . " mismatches the ID " . - $this->mTitle->getArticleID() . " provided by the Title object." ); - } + // @todo when is that set? test with external store setup! check out insertOn() [dk] + if ( !empty( $row['text_id'] ) ) { + throw new MWException( "Text already stored in external store (id {$row['text_id']}), " . + "can't serialize content object" ); } - $this->mCurrent = false; + $row['content_model'] = $row['content']->getModel(); + # note: mContentFormat is initializes later accordingly + # note: content is serialized later in this method! + # also set text to null? + } + + $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null; + $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null; + $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null; + $this->mUserText = isset( $row['user_text'] ) + ? strval( $row['user_text'] ) : $wgUser->getName(); + $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId(); + $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0; + $this->mTimestamp = isset( $row['timestamp'] ) + ? strval( $row['timestamp'] ) : wfTimestampNow(); + $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0; + $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null; + $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null; + $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null; + + $this->mContentModel = isset( $row['content_model'] ) + ? strval( $row['content_model'] ) : null; + $this->mContentFormat = isset( $row['content_format'] ) + ? strval( $row['content_format'] ) : null; + + // Enforce spacing trimming on supplied text + $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; + $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; + $this->mTextRow = null; + + $this->mTitle = isset( $row['title'] ) ? $row['title'] : null; + + // if we have a Content object, override mText and mContentModel + if ( !empty( $row['content'] ) ) { + $handler = $this->getContentHandler(); + $this->mContent = $row['content']; - // If we still have no length, see it we have the text to figure it out - if ( !$this->mSize && $this->mContent !== null ) { - $this->mSize = $this->mContent->getSize(); - } + $this->mContentModel = $this->mContent->getModel(); + $this->mContentHandler = null; - // Same for sha1 - if ( $this->mSha1 === null ) { - $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText ); + $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() ); + } elseif ( $this->mText !== null ) { + $handler = $this->getContentHandler(); + $this->mContent = $handler->unserializeContent( $this->mText ); + } + + // If we have a Title object, make sure it is consistent with mPage. + if ( $this->mTitle && $this->mTitle->exists() ) { + if ( $this->mPage === null ) { + // if the page ID wasn't known, set it now + $this->mPage = $this->mTitle->getArticleID(); + } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) { + // Got different page IDs. This may be legit (e.g. during undeletion), + // but it seems worth mentioning it in the log. + wfDebug( "Page ID " . $this->mPage . " mismatches the ID " . + $this->mTitle->getArticleID() . " provided by the Title object." ); } + } - // force lazy init - $this->getContentModel(); - $this->getContentFormat(); - } else { - throw new MWException( 'Revision constructor passed invalid row format.' ); + $this->mCurrent = false; + + // If we still have no length, see it we have the text to figure it out + if ( !$this->mSize && $this->mContent !== null ) { + $this->mSize = $this->mContent->getSize(); } - $this->mUnpatrolled = null; + + // Same for sha1 + if ( $this->mSha1 === null ) { + $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText ); + } + + // force lazy init + $this->getContentModel(); + $this->getContentFormat(); } /** @@ -1401,7 +1417,7 @@ class Revision implements IDBAccessObject { * * @param IDatabase $dbw (master connection) * @throws MWException - * @return int + * @return int The revision ID */ public function insertOn( $dbw ) { global $wgDefaultExternalStore, $wgContentHandlerUseDB; @@ -1442,10 +1458,8 @@ class Revision implements IDBAccessObject { # Record the text (or external storage URL) to the text table if ( $this->mTextId === null ) { - $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' ); $dbw->insert( 'text', [ - 'old_id' => $old_id, 'old_text' => $data, 'old_flags' => $flags, ], __METHOD__ @@ -1458,11 +1472,7 @@ class Revision implements IDBAccessObject { } # Record the edit in revisions - $rev_id = $this->mId !== null - ? $this->mId - : $dbw->nextSequenceValue( 'revision_rev_id_seq' ); $row = [ - 'rev_id' => $rev_id, 'rev_page' => $this->mPage, 'rev_text_id' => $this->mTextId, 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0, @@ -1478,6 +1488,9 @@ class Revision implements IDBAccessObject { ? self::base36Sha1( $this->mText ) : $this->mSha1, ]; + if ( $this->mId !== null ) { + $row['rev_id'] = $this->mId; + } list( $commentFields, $commentCallback ) = CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment ); @@ -1508,7 +1521,7 @@ class Revision implements IDBAccessObject { $dbw->insert( 'revision', $row, __METHOD__ ); if ( $this->mId === null ) { - // Only if nextSequenceValue() was called + // Only if auto-increment was used $this->mId = $dbw->insertId(); } $commentCallback( $this->mId ); @@ -1521,6 +1534,16 @@ class Revision implements IDBAccessObject { ); } + // Insert IP revision into ip_changes for use when querying for a range. + if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) { + $ipcRow = [ + 'ipc_rev_id' => $this->mId, + 'ipc_rev_timestamp' => $row['rev_timestamp'], + 'ipc_hex' => IP::toHex( $row['rev_user_text'] ), + ]; + $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ ); + } + // Avoid PHP 7.1 warning of passing $this by reference $revision = $this; Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] ); @@ -1706,7 +1729,7 @@ class Revision implements IDBAccessObject { * @return Revision|null Revision or null on error */ public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) { - global $wgContentHandlerUseDB, $wgContLang; + global $wgContentHandlerUseDB; $fields = [ 'page_latest', 'page_namespace', 'page_title', 'rev_text_id', 'rev_len', 'rev_sha1' ]; @@ -1733,9 +1756,6 @@ class Revision implements IDBAccessObject { $user = $wgUser; } - // Truncate for whole multibyte characters - $summary = $wgContLang->truncate( $summary, 255 ); - $row = [ 'page' => $pageId, 'user_text' => $user->getName(), @@ -1937,7 +1957,7 @@ class Revision implements IDBAccessObject { $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); return $cache->getWithSetCallback( // Page/rev IDs passed in from DB to reflect history merges - $cache->makeGlobalKey( 'revision', $db->getWikiID(), $pageId, $revId ), + $cache->makeGlobalKey( 'revision', $db->getDomainID(), $pageId, $revId ), $cache::TTL_WEEK, function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) { $setOpts += Database::getCacheSetOptions( $db ); diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index ed09701d4b..4c996771e8 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -824,7 +824,7 @@ class Sanitizer { || $attribute === 'aria-labelledby' || $attribute === 'aria-owns' ) { - $value = self::escapeIdReferenceList( $value, 'noninitial' ); + $value = self::escapeIdReferenceList( $value ); } // RDFa and microdata properties allow URLs, URIs and/or CURIs. @@ -1203,8 +1203,6 @@ class Sanitizer { global $wgExperimentalHtmlIds; $options = (array)$options; - $id = self::decodeCharReferences( $id ); - if ( $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) { $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id ); $id = trim( $id, '_' ); @@ -1284,7 +1282,6 @@ class Sanitizer { $mode = $wgFragmentMode[self::ID_PRIMARY]; $id = self::escapeIdInternal( $id, $mode ); - $id = self::urlEscapeId( $id, $mode ); return $id; } @@ -1302,23 +1299,6 @@ class Sanitizer { global $wgExternalInterwikiFragmentMode; $id = self::escapeIdInternal( $id, $wgExternalInterwikiFragmentMode ); - $id = self::urlEscapeId( $id, $wgExternalInterwikiFragmentMode ); - - return $id; - } - - /** - * Helper for escapeIdFor*() functions. URL-escapes the ID if needed. - * - * @param string $id String to escape - * @param string $mode One of modes from $wgFragmentMode - * @return string - */ - private static function urlEscapeId( $id, $mode ) { - if ( $mode === 'html5' ) { - $id = urlencode( $id ); - $id = str_replace( '%3A', ':', $id ); - } return $id; } @@ -1331,8 +1311,6 @@ class Sanitizer { * @return string */ private static function escapeIdInternal( $id, $mode ) { - $id = self::decodeCharReferences( $id ); - switch ( $mode ) { case 'html5': $id = str_replace( ' ', '_', $id ); @@ -1366,7 +1344,7 @@ class Sanitizer { * Given a string containing a space delimited list of ids, escape each id * to match ids escaped by the escapeId() function. * - * @todo wfDeprecated() uses of $options in 1.31, remove completely in 1.32 + * @todo remove $options completely in 1.32 * * @since 1.27 * @@ -1375,6 +1353,9 @@ class Sanitizer { * @return string */ static function escapeIdReferenceList( $referenceString, $options = [] ) { + if ( $options ) { + wfDeprecated( __METHOD__ . ' with $options', '1.31' ); + } # Explode the space delimited list string into an array of tokens $references = preg_split( '/\s+/', "{$referenceString}", -1, PREG_SPLIT_NO_EMPTY ); diff --git a/includes/TemplateParser.php b/includes/TemplateParser.php index 2759ff9baa..2293dabbd9 100644 --- a/includes/TemplateParser.php +++ b/includes/TemplateParser.php @@ -38,6 +38,13 @@ class TemplateParser { */ protected $forceRecompile = false; + /** + * @var int Compilation flags passed to LightnCandy + */ + // Do not add more flags here without discussion. + // If you do add more flags, be sure to update unit tests as well. + protected $compileFlags = LightnCandy::FLAG_ERROR_EXCEPTION; + /** * @param string $templateDir * @param bool $forceRecompile @@ -47,6 +54,18 @@ class TemplateParser { $this->forceRecompile = $forceRecompile; } + /** + * Enable/disable the use of recursive partials. + * @param bool $enable + */ + public function enableRecursivePartials( $enable ) { + if ( $enable ) { + $this->compileFlags = $this->compileFlags | LightnCandy::FLAG_RUNTIMEPARTIAL; + } else { + $this->compileFlags = $this->compileFlags & ~LightnCandy::FLAG_RUNTIMEPARTIAL; + } + } + /** * Constructs the location of the the source Mustache template * @param string $templateName The name of the template @@ -73,11 +92,13 @@ class TemplateParser { * @throws RuntimeException */ protected function getTemplate( $templateName ) { + $templateKey = $templateName . '|' . $this->compileFlags; + // If a renderer has already been defined for this template, reuse it - if ( isset( $this->renderers[$templateName] ) && - is_callable( $this->renderers[$templateName] ) + if ( isset( $this->renderers[$templateKey] ) && + is_callable( $this->renderers[$templateKey] ) ) { - return $this->renderers[$templateName]; + return $this->renderers[$templateKey]; } $filename = $this->getTemplateFilename( $templateName ); @@ -90,7 +111,7 @@ class TemplateParser { $fileContents = file_get_contents( $filename ); // Generate a quick hash for cache invalidation - $fastHash = md5( $fileContents ); + $fastHash = md5( $this->compileFlags . '|' . $fileContents ); // Fetch a secret key for building a keyed hash of the PHP code $config = MediaWikiServices::getInstance()->getMainConfig(); @@ -127,7 +148,7 @@ class TemplateParser { if ( !is_callable( $renderer ) ) { throw new RuntimeException( "Requested template, {$templateName}, is not callable" ); } - $this->renderers[$templateName] = $renderer; + $this->renderers[$templateKey] = $renderer; return $renderer; } @@ -168,9 +189,7 @@ class TemplateParser { return LightnCandy::compile( $code, [ - // Do not add more flags here without discussion. - // If you do add more flags, be sure to update unit tests as well. - 'flags' => LightnCandy::FLAG_ERROR_EXCEPTION, + 'flags' => $this->compileFlags, 'basedir' => $this->templateDir, 'fileext' => '.mustache', ] diff --git a/includes/Title.php b/includes/Title.php index 0687a15589..3da6ab9552 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -253,6 +253,9 @@ class Title implements LinkTarget { * Create a new Title from text, such as what one would find in a link. De- * codes any HTML entities in the text. * + * Title objects returned by this method are guaranteed to be valid, and + * thus return true from the isValid() method. + * * @param string|int|null $text The link text; spaces, prefixes, and an * initial ':' indicating the main namespace are accepted. * @param int $defaultNamespace The namespace to use if none is specified @@ -284,6 +287,9 @@ class Title implements LinkTarget { * * The exception subclasses encode detailed information about why the title is invalid. * + * Title objects returned by this method are guaranteed to be valid, and + * thus return true from the isValid() method. + * * @see Title::newFromText * * @since 1.25 @@ -500,10 +506,19 @@ class Title implements LinkTarget { /** * Create a new Title from a namespace index and a DB key. - * It's assumed that $ns and $title are *valid*, for instance when - * they came directly from the database or a special page name. - * For convenience, spaces are converted to underscores so that - * eg user_text fields can be used directly. + * + * It's assumed that $ns and $title are safe, for instance when + * they came directly from the database or a special page name, + * not from user input. + * + * No validation is applied. For convenience, spaces are normalized + * to underscores, so that e.g. user_text fields can be used directly. + * + * @note This method may return Title objects that are "invalid" + * according to the isValid() method. This is usually caused by + * configuration changes: e.g. a namespace that was once defined is + * no longer configured, or a character that was once allowed in + * titles is now forbidden. * * @param int $ns The namespace of the article * @param string $title The unprefixed database key form @@ -529,6 +544,10 @@ class Title implements LinkTarget { * The parameters will be checked for validity, which is a bit slower * than makeTitle() but safer for user-provided data. * + * Title objects returned by makeTitleSafe() are guaranteed to be valid, + * that is, they return true from the isValid() method. If no valid Title + * can be constructed from the input, this method returns null. + * * @param int $ns The namespace of the article * @param string $title Database key form * @param string $fragment The link fragment (after the "#") @@ -536,6 +555,9 @@ class Title implements LinkTarget { * @return Title|null The new object, or null on an error */ public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { + // NOTE: ideally, this would just call makeTitle() and then isValid(), + // but presently, that means more overhead on a potential performance hotspot. + if ( !MWNamespace::exists( $ns ) ) { return null; } @@ -777,6 +799,36 @@ class Title implements LinkTarget { } } + /** + * Returns true if the title is valid, false if it is invalid. + * + * Valid titles can be round-tripped via makeTitleSafe() and newFromText(). + * Invalid titles may get returned from makeTitle(), and it may be useful to + * allow them to exist, e.g. in order to process log entries about pages in + * namespaces that belong to extensions that are no longer installed. + * + * @note This method is relatively expensive. When constructing Title + * objects that need to be valid, use an instantiator method that is guaranteed + * to return valid titles, such as makeTitleSafe() or newFromText(). + * + * @return bool + */ + public function isValid() { + $ns = $this->getNamespace(); + + if ( !MWNamespace::exists( $ns ) ) { + return false; + } + + try { + $parser = MediaWikiServices::getInstance()->getTitleParser(); + $parser->parseTitle( $this->getDBkey(), $ns ); + return true; + } catch ( MalformedTitleException $ex ) { + return false; + } + } + /** * Determine whether the object refers to a page within * this project (either this wiki or a wiki with a local @@ -1356,7 +1408,7 @@ class Title implements LinkTarget { * get the talk page, if it is a subject page get the talk page * * @since 1.25 - * @throws MWException + * @throws MWException If the page doesn't have an other page * @return Title */ public function getOtherPage() { @@ -1366,6 +1418,9 @@ class Title implements LinkTarget { if ( $this->isTalkPage() ) { return $this->getSubjectPage(); } else { + if ( !$this->canHaveTalkPage() ) { + throw new MWException( "{$this->getPrefixedText()} does not have an other page" ); + } return $this->getTalkPage(); } } @@ -1742,7 +1797,7 @@ class Title implements LinkTarget { * @see self::getLocalURL for the arguments. * @param array|string $query * @param string $proto Protocol type to use in URL - * @return String. A url suitable to use in an HTTP location header. + * @return string A url suitable to use in an HTTP location header. */ public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) { $target = $this; @@ -1920,6 +1975,8 @@ class Title implements LinkTarget { * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment * * @see self::getLocalURL for the arguments. + * @param string $query + * @param string|bool $query2 * @return string The URL * @since 1.18 */ diff --git a/includes/WatchedItemStore.php b/includes/WatchedItemStore.php index 69a9df2d57..60d8b7699c 100644 --- a/includes/WatchedItemStore.php +++ b/includes/WatchedItemStore.php @@ -766,7 +766,7 @@ class WatchedItemStore implements StatsdAwareInterface { ); if ( count( $watchersChunks ) > 1 ) { $factory->commitAndWaitForReplication( - __METHOD__, $ticket, [ 'wiki' => $dbw->getWikiID() ] + __METHOD__, $ticket, [ 'domain' => $dbw->getDomainID() ] ); } } diff --git a/includes/WikiMap.php b/includes/WikiMap.php index 4f3c461699..8bb37b5c88 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\DatabaseDomain; /** * Helper tools for dealing with other locally-hosted wikis. @@ -239,4 +240,22 @@ class WikiMap { return false; } + + /** + * Get the wiki ID of a database domain + * + * This is like DatabaseDomain::getId() without encoding (for legacy reasons) + * + * @param string|DatabaseDomain $domain + * @return string + */ + public static function getWikiIdFromDomain( $domain ) { + if ( !( $domain instanceof DatabaseDomain ) ) { + $domain = DatabaseDomain::newFromId( $domain ); + } + + return strlen( $domain->getTablePrefix() ) + ? "{$domain->getDatabase()}-{$domain->getTablePrefix()}" + : $domain->getDatabase(); + } } diff --git a/includes/Xml.php b/includes/Xml.php index 16a5a9ddec..0091513125 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -493,7 +493,8 @@ class Xml { } /** - * Build a drop-down box from a textual list. + * Build a drop-down box from a textual list. This is a wrapper + * for Xml::listDropDownOptions() plus the XmlSelect class. * * @param string $name Name and id for the drop-down * @param string $list Correctly formatted text (newline delimited) to be @@ -507,60 +508,91 @@ class Xml { public static function listDropDown( $name = '', $list = '', $other = '', $selected = '', $class = '', $tabindex = null ) { - $optgroup = false; + $options = self::listDropDownOptions( $list, [ 'other' => $other ] ); + + $xmlSelect = new XmlSelect( $name, $name, $selected ); + $xmlSelect->addOptions( $options ); + + if ( $class ) { + $xmlSelect->setAttribute( 'class', $class ); + } + if ( $tabindex ) { + $xmlSelect->setAttribute( 'tabindex', $tabindex ); + } - $options = self::option( $other, 'other', $selected === 'other' ); + return $xmlSelect->getHTML(); + } + /** + * Build options for a drop-down box from a textual list. + * + * The result of this function can be passed to XmlSelect::addOptions() + * (to render a plain ` and on the