From: jenkins-bot Date: Fri, 18 Sep 2015 09:16:13 +0000 (+0000) Subject: Merge "Don't check namespace in SpecialWantedtemplates" X-Git-Tag: 1.31.0-rc.0~9973 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=59ebff658ce912c1b0e7ef8d8f9bfec5a4e17b39;hp=64717f414cdd66a27630b92539cde5456d622141 Merge "Don't check namespace in SpecialWantedtemplates" --- diff --git a/.gitattributes b/.gitattributes index 50ca329f24..09f86a3280 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ *.sh eol=lf +*.icc binary +*.webp binary diff --git a/.jscsrc b/.jscsrc index 98b81db9dc..aaa876c3e2 100644 --- a/.jscsrc +++ b/.jscsrc @@ -1,7 +1,26 @@ { "preset": "wikimedia", + "es3": true, - "disallowQuotedKeysInObjects": null, - "requireSpacesInsideParentheses": null, - "requireSpacesInsideArrayBrackets": null + "requireVarDeclFirst": null, + + "disallowQuotedKeysInObjects": "allButReserved", + "requireDotNotation": { "allExcept": [ "keywords" ] }, + "jsDoc": { + "requireNewlineAfterDescription": true, + "requireParamTypes": true, + "requireReturnTypes": true + }, + + "excludeFiles": [ + "docs/**", + "extensions/**", + "node_modules/**", + "resources/lib/**", + "resources/src/jquery.tipsy/**", + "resources/src/jquery/jquery.farbtastic.js", + "resources/src/mediawiki.libs/**", + "skins/**", + "vendor/**" + ] } diff --git a/.jshintignore b/.jshintignore index 494890c00e..fdde7d054d 100644 --- a/.jshintignore +++ b/.jshintignore @@ -8,7 +8,5 @@ resources/lib/** resources/src/jquery.tipsy/** resources/src/jquery/jquery.farbtastic.js resources/src/mediawiki.libs/** -tests/frontend/node_modules/** - -# github.com/jshint/jshint/issues/729 -tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js +skins/** +vendor/** diff --git a/.jshintrc b/.jshintrc index d72c31d66f..b776e8f21a 100644 --- a/.jshintrc +++ b/.jshintrc @@ -22,6 +22,7 @@ "mediaWiki": true, "JSON": true, "OO": true, + "mwPerformance": true, "jQuery": false, "QUnit": false, "sinon": false diff --git a/.rubocop.yml b/.rubocop.yml index 61ffc1a044..5b6c3f2dc6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,7 @@ AllCops: Exclude: - 'extensions/**/*' + - 'node_modules/**/*' - 'skins/**/*' - 'tests/frontend/node_modules/**/*' - 'vendor/**/*' diff --git a/.travis.yml b/.travis.yml index 512d735fc4..8ba46b5455 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,19 +8,17 @@ # language: php -php: - - hhvm-nightly - - 5.3 - -env: - - dbtype=mysql - - dbtype=postgres - -# TODO: Travis CI's hhvm does not support PostgreSQL at the moment. matrix: - exclude: - - php: hhvm-nightly - env: dbtype=postgres + fast_finish: true + include: + - env: dbtype=mysql + php: 5.3 + - env: dbtype=postgres + php: 5.3 + - env: dbtype=mysql + php: hhvm + - env: dbtype=mysql + php: 7 services: - mysql diff --git a/CREDITS b/CREDITS index 22dee7bdad..44adc4ffb9 100644 --- a/CREDITS +++ b/CREDITS @@ -24,6 +24,7 @@ following names for their contribution to the product. * Bryan Tong Minh * Chad Horohoe * Charles Melbye +* Chris Steipp * church of emacs * Daniel Friesen * Daniel Kinzler @@ -43,6 +44,7 @@ following names for their contribution to the product. * Jack D. Pond * Jack Phoenix * Jackmcbarn +* James Forrester * Jan Paul Posma * Jason Richey * Jeroen De Dauw @@ -91,6 +93,7 @@ following names for their contribution to the product. * Tim Starling * Timo Tijhof * Trevor Parscal +* Tyler Anthony Romeo * Victor Vasiliev * Yesid Carrillo * Yuri Astrakhan @@ -116,7 +119,6 @@ following names for their contribution to the product. * Brianna Laugher * Carlin * Carsten Nielsen -* Chris Steipp * Christian Aistleitner * Christian Neubauer * Conrad Irwin @@ -139,6 +141,7 @@ following names for their contribution to the product. * fomafix * FunPika * Gabriel Wicke +* Geoffrey Mon * Gero Scholz * Gilles van den Hoven * Grunny @@ -227,6 +230,7 @@ following names for their contribution to the product. * Simon Walker * Solitarius * Søren Løvborg +* Southparkfan * Srikanth Lakshmanan * Stefano Codari * Str4nd @@ -235,7 +239,6 @@ following names for their contribution to the product. * The Evil IP address * Tim Landscheidt * Tisane -* Tyler Anthony Romeo * Umherirrender * Van de Bugger * Ville Stadista diff --git a/Gemfile.lock b/Gemfile.lock index 7aa9daeb76..3a695ef75a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GEM builder (3.2.2) childprocess (0.5.6) ffi (~> 1.0, >= 1.0.11) - cucumber (1.3.19) + cucumber (1.3.20) builder (>= 2.1.2) diff-lcs (>= 1.1.3) gherkin (~> 2.12) @@ -26,7 +26,7 @@ GEM faraday-cookie_jar (0.0.6) faraday (>= 0.7.4) http-cookie (~> 1.0.0) - ffi (1.9.8) + ffi (1.9.10) gherkin (2.12.2) multi_json (~> 1.3) headless (1.0.2) @@ -34,7 +34,7 @@ GEM domain_name (~> 0.5) i18n (0.7.0) json (1.8.3) - mediawiki_api (0.3.1) + mediawiki_api (0.4.1) faraday (~> 0.9, >= 0.9.0) faraday-cookie_jar (~> 0.0, >= 0.0.6) mediawiki_selenium (1.2.1) @@ -48,7 +48,7 @@ GEM syntax (~> 1.2, >= 1.2.0) thor (~> 0.19, >= 0.19.1) mime-types (2.6.1) - multi_json (1.11.0) + multi_json (1.11.2) multi_test (0.1.2) multipart-post (2.0.0) netrc (0.10.3) @@ -58,7 +58,7 @@ GEM watir-webdriver (>= 0.6.11) page_navigation (0.9) data_magic (>= 0.14) - parser (2.2.2.5) + parser (2.2.2.6) ast (>= 1.1, < 3.0) powerpack (0.1.1) rainbow (2.0.0) @@ -68,15 +68,15 @@ GEM netrc (~> 0.7) rspec-expectations (2.99.2) diff-lcs (>= 1.1.3, < 2.0) - rubocop (0.31.0) + rubocop (0.32.1) astrolabe (~> 1.3) - parser (>= 2.2.2.1, < 3.0) + parser (>= 2.2.2.5, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) ruby-progressbar (1.7.5) rubyzip (1.1.7) - selenium-webdriver (2.45.0) + selenium-webdriver (2.46.2) childprocess (~> 0.5) multi_json (~> 1.0) rubyzip (~> 1.0) @@ -86,8 +86,8 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.1) - watir-webdriver (0.7.0) - selenium-webdriver (>= 2.45) + watir-webdriver (0.8.0) + selenium-webdriver (>= 2.46.2) websocket (1.2.2) yml_reader (0.5) @@ -97,3 +97,6 @@ PLATFORMS DEPENDENCIES mediawiki_selenium (~> 1.2.1) rubocop + +BUNDLED WITH + 1.10.5 diff --git a/Gruntfile.js b/Gruntfile.js index 573db69afb..8dbeb6bfc0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -12,55 +12,40 @@ module.exports = function ( grunt ) { wgScriptPath = process.env.MW_SCRIPT_PATH, karmaProxy = {}; - karmaProxy[wgScriptPath] = wgServer + wgScriptPath; + karmaProxy[ wgScriptPath ] = wgServer + wgScriptPath; grunt.initConfig( { - pkg: grunt.file.readJSON( 'package.json' ), jshint: { options: { jshintrc: true }, - all: [ - '*.js', - '{includes,languages,resources,tests}/**/*.js' - ] + all: '.' }, jscs: { - all: [ - '<%= jshint.all %>', - // Auto-generated file with JSON (double quotes) - '!tests/qunit/data/mediawiki.jqueryMsg.data.js', - // Skip functions are stored as script files but wrapped in a function when - // executed. node-jscs trips on the would-be "Illegal return statement". - '!resources/src/*-skip.js' - - // Exclude all files ignored by jshint - ].concat( grunt.file.read( '.jshintignore' ).split( '\n' ).reduce( function ( patterns, pattern ) { - // Filter out empty lines - if ( pattern.length && pattern[0] !== '#' ) { - patterns.push( '!' + pattern ); - } - return patterns; - }, [] ) ) + all: '.' }, jsonlint: { all: [ '.jscsrc', - '{languages,maintenance,resources}/**/*.json', - 'package.json' + '**/*.json', + '!{docs/js,extensions,node_modules,skins,vendor}/**' ] }, banana: { + options: { + disallowBlankTranslations: false, + disallowDuplicateTranslations: false, + disallowUnusedTranslations: false + }, core: 'languages/i18n/', api: 'includes/api/i18n/', installer: 'includes/installer/i18n/' }, watch: { files: [ - '<%= jscs.all %>', - '<%= jsonlint.all %>', - '.jshintignore', - '.jshintrc' + '.js*', + '**/*', + '!{docs,extensions,node_modules,skins,vendor}/**' ], tasks: 'test' }, @@ -107,14 +92,14 @@ module.exports = function ( grunt ) { } if ( !process.env.MW_SCRIPT_PATH ) { grunt.log.error( 'Environment variable MW_SCRIPT_PATH must be set.\n' + - 'Set this like $wgScriptPath, e.g. "/w"'); + 'Set this like $wgScriptPath, e.g. "/w"' ); } return !!( process.env.MW_SERVER && process.env.MW_SCRIPT_PATH ); } ); - grunt.registerTask( 'lint', ['jshint', 'jscs', 'jsonlint', 'banana'] ); + grunt.registerTask( 'lint', [ 'jshint', 'jscs', 'jsonlint', 'banana' ] ); grunt.registerTask( 'qunit', [ 'assert-mw-env', 'karma:main' ] ); - grunt.registerTask( 'test', ['lint'] ); + grunt.registerTask( 'test', [ 'lint' ] ); grunt.registerTask( 'default', 'test' ); }; diff --git a/RELEASE-NOTES-1.26 b/RELEASE-NOTES-1.26 index 3693898828..13239de831 100644 --- a/RELEASE-NOTES-1.26 +++ b/RELEASE-NOTES-1.26 @@ -9,48 +9,137 @@ MediaWiki 1.26 is an alpha-quality branch and is not recommended for use in production. === Configuration changes in 1.26 === +* $wgPasswordResetRoutes['email'] = true by default. * $wgEnableParserCache was deprecated, set $wgParserCacheType to CACHE_NONE instead if you want to disable the parser cache. +* New-style continuation is now the default for API action=continue. Clients may + use the 'rawcontinue' parameter to receive raw query-continue data, but the + new style is encouraged as it's harder to implement incorrectly. +* Deprecated API formats dump and wddx have been completely removed. +* (T7645) The "Signature" button on the edit toolbar is now hidden by default + in non-talk namespaces. A new configuration variable, + $wgExtraSignatureNamespaces, controls in which subject (non-talk) namespaces + the "Signature" button on the edit toolbar will be displayed. +* $wgResourceLoaderUseESI was deprecated and removed. This was an experimental + feature that was never enabled by default. +* $wgResourceLoaderExperimentalAsyncLoading was deprecated and removed. + This experimental feature was never enabled by default and is obsolete as of + MediaWiki 1.26, in where ResourceLoader became fully asynchronous. +* $wgMasterWaitTimeout was removed (deprecated in 1.24). +* Fields in ParserOptions are now private. Use the accessors instead. +* Custom LESS functions (defined via $wgResourceLoaderLESSFunctions) + have been removed, after being deprecated in 1.24. +* $wgAlwaysUseTidy has been removed. === New features in 1.26 === +* (T51506) Now action=info gives estimates of actual watchers for a page. + See $wgRCMaxAge, $wgWatchersMaxAge and $wgUnwatchedPageSecret + to learn how to configure if needed. * Change tags can now be hidden in the interface by disabling the associated "tag-" interface message. * ':' (colon) is now invalid in usernames for new accounts. Existing accounts are not affected. * Added a new hook, 'LogException', to log exceptions in nonstandard ways. +* Revive the 'SpecialSearchResultsAppend' hook which occurs after the list of + search results are rendered. The initial use case is to append a "give us + feedback" link beneath the search results. +* Added a new hook, 'RejectParserCacheValue', which allows extensions to + reject an otherwise-successful parser cache lookup. The intent is to allow + extensions to manage the eviction of archaic HTML output from the cache. +* (T68699) The expiration of the UserID and Token login cookies + ($wgExtendedLoginCookieExpiration) can be configured independently of the + expiration of all other cookies ($wgCookieExpiration). +* (T50519) Support for generating JPEG/PNG thumbnails from WebP images added + if ImageMagick is used as image scaler ($wgUseImageMagick = true). Uploading + of WebP images still disabled by default. Add $wgFileExtensions[] = + 'webp'; to LocalSettings.php to enable uploading of WebP images. +* Added new hooks 'EnhancedChangesListModifyLineData' & + 'EnhancedChangesListModifyBlockLineData', to modify the data used to build + lines in enhanced recentchanges and watchlist. +* Caches that need purging ability now use the WANObjectCache interface. + This corresponds to a new $wgMainWANCache setting, which defaults to using + the $wgMainCacheType settings. +* Callers needing fast light-weight data stores use $wgMainStash to select + the store type from $wgObjectCaches. The default is the local database. +* Interface message overrides in the MediaWiki namespace will now be cached in + memcached and APC (if available), rather than memcached and local files. +* Added a new hook, 'RandomPageQuery', to allow modification of the query used + by Special:Random to select random pages. +* $wgTransactionalTimeLimit was added, which controls the request time limit + for potentially slow POST requests that need to be as atomic as possible. +* ResourceLoader now loads all scripts asynchronously. The top-queue and + startup modules are no longer synchronously loaded. +* 'mediawiki.ui.button' styles are no longer unconditionally loaded on every + page. During the deprecation period, the styles will only be loaded on pages + which contain 'mw-ui-button' in their HTML. Starting in 1.28, the styles will + only be loaded if explicitly required. ==== External libraries ==== * Update es5-shim from v4.0.0 to v4.1.5. * Update json2 from revision 2014-02-04 to 2015-05-03. -* Update Sinon.JS from 1.10.3 to 1.15.0. +* Update Sinon.JS from 1.10.3 to 1.15.4. * Upgrade jQuery Client from v1.0.0 to v2.0.0. -* Added mediawiki/at-ease 1.0.0 +* Added mediawiki/at-ease 1.0.0. +* Update QUnit from v1.17.1 to v1.18.0. === Bug fixes in 1.26 === -* (bug 51283) load.php sometimes sends 304 response without full headers +* (T53283) load.php sometimes sends 304 response without full headers * (T65198) Talk page tabs now have a "rel=discussion" attribute +* (T98841) {{msgnw:}} now preserves comments even when subst: is not used. +* (T104142) $wgEmergencyContact and $wgPasswordSender now use their default + value if set to an empty string. === Action API changes in 1.26 === +* New-style continuation is now the default for action=continue. Clients may + use the 'rawcontinue' parameter to receive raw query-continue data, but the + new style is encouraged as it's harder to implement incorrectly. +* Deprecated API formats dump and wddx have been completely removed. * API action=query&list=tags: The displayname can now be boolean false if the tag is meant to be hidden from user interfaces. * action=import no longer allows both the namespace= and rootpage= parameters to be set. If they are both set, the value of rootpage= will be ignored. * prop=revision output in enum mode is now sorted by timestamp rather than revision ID. This usually won't make any difference. +* (T102645) Namespace list from meta=siteinfo&siprop=namespaces is now an array + with formatversion=2. +* Various other output from meta=siteinfo will now always be arrays instead of + sometimes being numerically-indexed objects with formatversion=2. +* When errors about users being blocked are returned, they now include + information about the relevant block. +* (T99926) list=random has higher limits, in line with other API modules. +* list=random's rnredirect parameter is deprecated in favor of a new + rnfilterredir parameter that also allows for listing both redirects and + non-redirects. +* list=random now supports continuation. +* API responses to GET requests may now include ETag and Last-Modified headers, + and will honor corresponding If-None-Match and If-Modified-Since on such + requests. === Action API internal changes in 1.26 === +* New metadata item ApiResult::META_KVP_MERGE to allow for merging the KVP key + into the value when the value is an assoc. +* API action modules may now provide values for the RFC 7232 ETag and + Last-Modified headers. The API will check these against If-None-Match and + If-Modified-Since request headers on GET requests and avoid executing the + module when appropriate. === Languages updated in 1.26 === 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 Bugzilla reports. +changes to languages because of Phabricator reports. +* Languages added: +** ase (American sign language), thanks to translator Icemandeaf +** dty (डोटेली/Doteli), thanks to translators जनक राज भट्ट, बिप्लब आनन्द, + मेश सिंह बोहरा, and राम प्रसाद जोशी +** luz (لئری دوٙمینی / Southern Luri) === Other changes in 1.26 === * ChangeTags::tagDescription() will return false if the interface message for the tag is disabled. * Added PageHistoryPager::doBatchLookups hook. +* Added $wikiId parameter to FormatAutocomments hook. * Added ParserCacheSaveComplete to ParserCache * supportsDirectEditing and supportsDirectApiEditing methods added to ContentHandler, to provide a way for ApiEditPage and EditPage to check @@ -83,7 +172,22 @@ changes to languages because of Bugzilla reports. * wfSuppressWarnings() and wfRestoreWarnings() were split into a separate library, mediawiki/at-ease, and are now deprecated. Callers should use MediaWiki\suppressWarnings() and MediaWiki\restoreWarnings() directly. - +* The Block class constructor now takes an associative array of parameters + instead of many optional positional arguments. Calling the constructor the old + way will issue a deprecation warning. +* The jquery.mwExtension module was deprecated. +* $wgSpecialPageGroups was removed (deprecated in 1.21). +* SpecialPageFactory::setGroup was removed (deprecated in 1.21). +* SpecialPageFactory::getGroup was removed (deprecated in 1.21). +* DatabaseBase::ignoreErrors() is now protected. +* BREAKING CHANGE: mediawiki.legacy.ajax has been removed, following + a lengthy deprecation period. +* The ScopedPHPTimeout class was removed. +* Removed maintenance script fixSlaveDesync.php. +* Watchlist tokens, SpecialResetTokens, and User::getTokenFromOption() + are deprecated. Applications using those can work via the OAuth + extension instead. New tokens types should not be added. +* DatabaseBase::errorCount() was removed (unused). == Compatibility == diff --git a/autoload.php b/autoload.php index 26d954a9d1..5adfbe5104 100644 --- a/autoload.php +++ b/autoload.php @@ -37,14 +37,12 @@ $wgAutoloadLocalClasses = array( 'ApiFileRevert' => __DIR__ . '/includes/api/ApiFileRevert.php', 'ApiFormatBase' => __DIR__ . '/includes/api/ApiFormatBase.php', 'ApiFormatDbg' => __DIR__ . '/includes/api/ApiFormatDbg.php', - 'ApiFormatDump' => __DIR__ . '/includes/api/ApiFormatDump.php', 'ApiFormatFeedWrapper' => __DIR__ . '/includes/api/ApiFormatFeedWrapper.php', 'ApiFormatJson' => __DIR__ . '/includes/api/ApiFormatJson.php', 'ApiFormatNone' => __DIR__ . '/includes/api/ApiFormatNone.php', 'ApiFormatPhp' => __DIR__ . '/includes/api/ApiFormatPhp.php', 'ApiFormatRaw' => __DIR__ . '/includes/api/ApiFormatRaw.php', 'ApiFormatTxt' => __DIR__ . '/includes/api/ApiFormatTxt.php', - 'ApiFormatWddx' => __DIR__ . '/includes/api/ApiFormatWddx.php', 'ApiFormatXml' => __DIR__ . '/includes/api/ApiFormatXml.php', 'ApiFormatXmlRsd' => __DIR__ . '/includes/api/ApiRsd.php', 'ApiFormatYaml' => __DIR__ . '/includes/api/ApiFormatYaml.php', @@ -159,6 +157,9 @@ $wgAutoloadLocalClasses = array( 'BagOStuff' => __DIR__ . '/includes/libs/objectcache/BagOStuff.php', 'BaseDump' => __DIR__ . '/maintenance/backupPrefetch.inc', 'BaseTemplate' => __DIR__ . '/includes/skins/BaseTemplate.php', + 'BatchRowIterator' => __DIR__ . '/includes/utils/BatchRowIterator.php', + 'BatchRowUpdate' => __DIR__ . '/includes/utils/BatchRowUpdate.php', + 'BatchRowWriter' => __DIR__ . '/includes/utils/BatchRowWriter.php', 'BatchedQueryRunner' => __DIR__ . '/maintenance/runBatchedQuery.php', 'BcryptPassword' => __DIR__ . '/includes/password/BcryptPassword.php', 'BenchHttpHttps' => __DIR__ . '/maintenance/benchmarks/bench_HTTP_HTTPS.php', @@ -202,9 +203,9 @@ $wgAutoloadLocalClasses = array( 'CategoryPage' => __DIR__ . '/includes/page/CategoryPage.php', 'CategoryPager' => __DIR__ . '/includes/specials/SpecialCategories.php', 'CategoryViewer' => __DIR__ . '/includes/CategoryViewer.php', - 'CdbException' => __DIR__ . '/includes/CdbCompat.php', - 'CdbReader' => __DIR__ . '/includes/CdbCompat.php', - 'CdbWriter' => __DIR__ . '/includes/CdbCompat.php', + 'CdbException' => __DIR__ . '/includes/compat/CdbCompat.php', + 'CdbReader' => __DIR__ . '/includes/compat/CdbCompat.php', + 'CdbWriter' => __DIR__ . '/includes/compat/CdbCompat.php', 'CgzCopyTransaction' => __DIR__ . '/maintenance/storage/recompressTracked.php', 'ChangePassword' => __DIR__ . '/maintenance/changePassword.php', 'ChangeTags' => __DIR__ . '/includes/changetags/ChangeTags.php', @@ -258,6 +259,7 @@ $wgAutoloadLocalClasses = array( 'ConstantDependency' => __DIR__ . '/includes/cache/CacheDependency.php', 'Content' => __DIR__ . '/includes/content/Content.php', 'ContentHandler' => __DIR__ . '/includes/content/ContentHandler.php', + 'ContentModelLogFormatter' => __DIR__ . '/includes/logging/ContentModelLogFormatter.php', 'ContextSource' => __DIR__ . '/includes/context/ContextSource.php', 'ContribsPager' => __DIR__ . '/includes/specials/SpecialContributions.php', 'ConvertExtensionToRegistration' => __DIR__ . '/maintenance/convertExtensionToRegistration.php', @@ -280,6 +282,7 @@ $wgAutoloadLocalClasses = array( 'CurlHttpRequest' => __DIR__ . '/includes/HttpFunctions.php', 'DBAccessBase' => __DIR__ . '/includes/dao/DBAccessBase.php', 'DBAccessError' => __DIR__ . '/includes/db/LBFactory.php', + 'DBAccessObjectUtils' => __DIR__ . '/includes/dao/DBAccessObjectUtils.php', 'DBConnRef' => __DIR__ . '/includes/db/DBConnRef.php', 'DBConnectionError' => __DIR__ . '/includes/db/DatabaseError.php', 'DBError' => __DIR__ . '/includes/db/DatabaseError.php', @@ -419,6 +422,7 @@ $wgAutoloadLocalClasses = array( 'Field' => __DIR__ . '/includes/db/DatabaseUtility.php', 'File' => __DIR__ . '/includes/filerepo/file/File.php', 'FileBackend' => __DIR__ . '/includes/filebackend/FileBackend.php', + 'FileBackendDBRepoWrapper' => __DIR__ . '/includes/filerepo/FileBackendDBRepoWrapper.php', 'FileBackendError' => __DIR__ . '/includes/filebackend/FileBackend.php', 'FileBackendException' => __DIR__ . '/includes/filebackend/FileBackend.php', 'FileBackendGroup' => __DIR__ . '/includes/filebackend/FileBackendGroup.php', @@ -443,7 +447,6 @@ $wgAutoloadLocalClasses = array( 'FixBug20757' => __DIR__ . '/maintenance/storage/fixBug20757.php', 'FixDoubleRedirects' => __DIR__ . '/maintenance/fixDoubleRedirects.php', 'FixExtLinksProtocolRelative' => __DIR__ . '/maintenance/fixExtLinksProtocolRelative.php', - 'FixSlaveDesync' => __DIR__ . '/maintenance/fixSlaveDesync.php', 'FixTimestamps' => __DIR__ . '/maintenance/fixTimestamps.php', 'FixUserRegistration' => __DIR__ . '/maintenance/fixUserRegistration.php', 'ForeignAPIFile' => __DIR__ . '/includes/filerepo/file/ForeignAPIFile.php', @@ -492,6 +495,7 @@ $wgAutoloadLocalClasses = array( 'HTMLFormField' => __DIR__ . '/includes/htmlform/HTMLFormField.php', 'HTMLFormFieldCloner' => __DIR__ . '/includes/htmlform/HTMLFormFieldCloner.php', 'HTMLFormFieldRequiredOptionsException' => __DIR__ . '/includes/htmlform/HTMLFormFieldRequiredOptionsException.php', + 'HTMLFormFieldWithButton' => __DIR__ . '/includes/htmlform/HTMLFormFieldWithButton.php', 'HTMLHiddenField' => __DIR__ . '/includes/htmlform/HTMLHiddenField.php', 'HTMLInfoField' => __DIR__ . '/includes/htmlform/HTMLInfoField.php', 'HTMLIntField' => __DIR__ . '/includes/htmlform/HTMLIntField.php', @@ -502,11 +506,15 @@ $wgAutoloadLocalClasses = array( 'HTMLSelectField' => __DIR__ . '/includes/htmlform/HTMLSelectField.php', 'HTMLSelectLimitField' => __DIR__ . '/includes/htmlform/HTMLSelectLimitField.php', 'HTMLSelectNamespace' => __DIR__ . '/includes/htmlform/HTMLSelectNamespace.php', + 'HTMLSelectNamespaceWithButton' => __DIR__ . '/includes/htmlform/HTMLSelectNamespaceWithButton.php', 'HTMLSelectOrOtherField' => __DIR__ . '/includes/htmlform/HTMLSelectOrOtherField.php', 'HTMLSubmitField' => __DIR__ . '/includes/htmlform/HTMLSubmitField.php', 'HTMLTagFilter' => __DIR__ . '/includes/htmlform/HTMLTagFilter.php', 'HTMLTextAreaField' => __DIR__ . '/includes/htmlform/HTMLTextAreaField.php', 'HTMLTextField' => __DIR__ . '/includes/htmlform/HTMLTextField.php', + 'HTMLTextFieldWithButton' => __DIR__ . '/includes/htmlform/HTMLTextFieldWithButton.php', + 'HTMLTitleTextField' => __DIR__ . '/includes/htmlform/HTMLTitleTextField.php', + 'HTMLUserTextField' => __DIR__ . '/includes/htmlform/HTMLUserTextField.php', 'HWLDFWordAccumulator' => __DIR__ . '/includes/diff/DairikiDiff.php', 'HashBagOStuff' => __DIR__ . '/includes/libs/objectcache/HashBagOStuff.php', 'HashConfig' => __DIR__ . '/includes/config/HashConfig.php', @@ -528,14 +536,14 @@ $wgAutoloadLocalClasses = array( 'ICacheHelper' => __DIR__ . '/includes/cache/CacheHelper.php', 'IContextSource' => __DIR__ . '/includes/context/IContextSource.php', 'IDBAccessObject' => __DIR__ . '/includes/dao/IDBAccessObject.php', - 'IDatabase' => __DIR__ . '/includes/db/Database.php', + 'IDatabase' => __DIR__ . '/includes/db/IDatabase.php', 'IEContentAnalyzer' => __DIR__ . '/includes/libs/IEContentAnalyzer.php', 'IEUrlExtension' => __DIR__ . '/includes/libs/IEUrlExtension.php', 'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php', 'IORMRow' => __DIR__ . '/includes/db/IORMRow.php', 'IORMTable' => __DIR__ . '/includes/db/IORMTable.php', 'IP' => __DIR__ . '/includes/utils/IP.php', - 'IPSet' => __DIR__ . '/includes/libs/IPSet.php', + 'IPSet' => __DIR__ . '/includes/compat/IPSetCompat.php', 'IPTC' => __DIR__ . '/includes/media/IPTC.php', 'IRCColourfulRCFeedFormatter' => __DIR__ . '/includes/rcfeed/IRCColourfulRCFeedFormatter.php', 'IcuCollation' => __DIR__ . '/includes/Collation.php', @@ -567,6 +575,7 @@ $wgAutoloadLocalClasses = array( 'InstallerOverrides' => __DIR__ . '/mw-config/overrides.php', 'Interwiki' => __DIR__ . '/includes/interwiki/Interwiki.php', 'InvalidPassword' => __DIR__ . '/includes/password/InvalidPassword.php', + 'IteratorDecorator' => __DIR__ . '/includes/utils/iterators/IteratorDecorator.php', 'IuConverter' => __DIR__ . '/languages/classes/LanguageIu.php', 'JSCompilerContext' => __DIR__ . '/includes/libs/jsminplus.php', 'JSMinPlus' => __DIR__ . '/includes/libs/jsminplus.php', @@ -676,7 +685,7 @@ $wgAutoloadLocalClasses = array( 'LoadBalancer' => __DIR__ . '/includes/db/LoadBalancer.php', 'LoadBalancerSingle' => __DIR__ . '/includes/db/LBFactorySingle.php', 'LoadMonitor' => __DIR__ . '/includes/db/LoadMonitor.php', - 'LoadMonitorMySQL' => __DIR__ . '/includes/db/LoadMonitor.php', + 'LoadMonitorMySQL' => __DIR__ . '/includes/db/LoadMonitorMySQL.php', 'LoadMonitorNull' => __DIR__ . '/includes/db/LoadMonitor.php', 'LocalFile' => __DIR__ . '/includes/filerepo/file/LocalFile.php', 'LocalFileDeleteBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php', @@ -715,7 +724,6 @@ $wgAutoloadLocalClasses = array( 'MWOldPassword' => __DIR__ . '/includes/password/MWOldPassword.php', 'MWSaltedPassword' => __DIR__ . '/includes/password/MWSaltedPassword.php', 'MWTidy' => __DIR__ . '/includes/parser/MWTidy.php', - 'MWTidyWrapper' => __DIR__ . '/includes/parser/MWTidy.php', 'MWTimestamp' => __DIR__ . '/includes/MWTimestamp.php', 'MachineReadableRCFeedFormatter' => __DIR__ . '/includes/rcfeed/MachineReadableRCFeedFormatter.php', 'MagicWord' => __DIR__ . '/includes/MagicWord.php', @@ -747,10 +755,23 @@ $wgAutoloadLocalClasses = array( 'MediaWiki\\Logger\\MonologSpi' => __DIR__ . '/includes/debug/logger/MonologSpi.php', 'MediaWiki\\Logger\\Monolog\\LegacyFormatter' => __DIR__ . '/includes/debug/logger/monolog/LegacyFormatter.php', 'MediaWiki\\Logger\\Monolog\\LegacyHandler' => __DIR__ . '/includes/debug/logger/monolog/LegacyHandler.php', + 'MediaWiki\\Logger\\Monolog\\LineFormatter' => __DIR__ . '/includes/debug/logger/monolog/LineFormatter.php', 'MediaWiki\\Logger\\Monolog\\SyslogHandler' => __DIR__ . '/includes/debug/logger/monolog/SyslogHandler.php', 'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . '/includes/debug/logger/monolog/WikiProcessor.php', 'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php', 'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php', + 'MediaWiki\\Tidy\\Html5Depurate' => __DIR__ . '/includes/tidy/Html5Depurate.php', + 'MediaWiki\\Tidy\\RaggettBase' => __DIR__ . '/includes/tidy/RaggettBase.php', + 'MediaWiki\\Tidy\\RaggettExternal' => __DIR__ . '/includes/tidy/RaggettExternal.php', + 'MediaWiki\\Tidy\\RaggettInternalHHVM' => __DIR__ . '/includes/tidy/RaggettInternalHHVM.php', + 'MediaWiki\\Tidy\\RaggettInternalPHP' => __DIR__ . '/includes/tidy/RaggettInternalPHP.php', + 'MediaWiki\\Tidy\\RaggettWrapper' => __DIR__ . '/includes/tidy/RaggettWrapper.php', + 'MediaWiki\\Tidy\\TidyDriverBase' => __DIR__ . '/includes/tidy/TidyDriverBase.php', + 'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php', + 'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php', + 'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php', + 'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php', + 'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php', 'MemCachedClientforWiki' => __DIR__ . '/includes/objectcache/MemcachedClient.php', 'MemcLockManager' => __DIR__ . '/includes/filebackend/lockmanager/MemcLockManager.php', 'MemcachedBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedBagOStuff.php', @@ -765,6 +786,7 @@ $wgAutoloadLocalClasses = array( 'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php', 'MessageContent' => __DIR__ . '/includes/content/MessageContent.php', 'MessageSpecifier' => __DIR__ . '/includes/libs/MessageSpecifier.php', + 'MigrateFileRepoLayout' => __DIR__ . '/maintenance/migrateFileRepoLayout.php', 'MigrateUserGroup' => __DIR__ . '/maintenance/migrateUserGroup.php', 'MimeMagic' => __DIR__ . '/includes/MimeMagic.php', 'MinifyScript' => __DIR__ . '/maintenance/minify.php', @@ -789,6 +811,7 @@ $wgAutoloadLocalClasses = array( 'MultiHttpClient' => __DIR__ . '/includes/libs/MultiHttpClient.php', 'MultiWriteBagOStuff' => __DIR__ . '/includes/objectcache/MultiWriteBagOStuff.php', 'MutableConfig' => __DIR__ . '/includes/config/MutableConfig.php', + 'MutableContext' => __DIR__ . '/includes/context/MutableContext.php', 'MwSql' => __DIR__ . '/maintenance/sql.php', 'MyLocalSettingsGenerator' => __DIR__ . '/mw-config/overrides.php', 'MySQLField' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', @@ -805,6 +828,7 @@ $wgAutoloadLocalClasses = array( 'NewPagesPager' => __DIR__ . '/includes/specials/SpecialNewpages.php', 'NewUsersLogFormatter' => __DIR__ . '/includes/logging/NewUsersLogFormatter.php', 'NolinesImageGallery' => __DIR__ . '/includes/gallery/NolinesImageGallery.php', + 'NotRecursiveIterator' => __DIR__ . '/includes/utils/iterators/NotRecursiveIterator.php', 'NukeNS' => __DIR__ . '/maintenance/nukeNS.php', 'NukePage' => __DIR__ . '/maintenance/nukePage.php', 'NullFileJournal' => __DIR__ . '/includes/filebackend/filejournal/FileJournal.php', @@ -893,6 +917,7 @@ $wgAutoloadLocalClasses = array( 'PoolWorkArticleView' => __DIR__ . '/includes/poolcounter/PoolWorkArticleView.php', 'PopulateBacklinkNamespace' => __DIR__ . '/maintenance/populateBacklinkNamespace.php', 'PopulateCategory' => __DIR__ . '/maintenance/populateCategory.php', + 'PopulateContentModel' => __DIR__ . '/maintenance/populateContentModel.php', 'PopulateFilearchiveSha1' => __DIR__ . '/maintenance/populateFilearchiveSha1.php', 'PopulateImageSha1' => __DIR__ . '/maintenance/populateImageSha1.php', 'PopulateLogSearch' => __DIR__ . '/maintenance/populateLogSearch.php', @@ -929,6 +954,7 @@ $wgAutoloadLocalClasses = array( 'ProfilerXhprof' => __DIR__ . '/includes/profiler/ProfilerXhprof.php', 'Protect' => __DIR__ . '/maintenance/protect.php', 'ProtectAction' => __DIR__ . '/includes/actions/ProtectAction.php', + 'ProtectLogFormatter' => __DIR__ . '/includes/logging/ProtectLogFormatter.php', 'ProtectedPagesPager' => __DIR__ . '/includes/specials/SpecialProtectedpages.php', 'ProtectedTitlesPager' => __DIR__ . '/includes/specials/SpecialProtectedtitles.php', 'ProtectionForm' => __DIR__ . '/includes/ProtectionForm.php', @@ -993,6 +1019,7 @@ $wgAutoloadLocalClasses = array( 'ResourceLoaderEditToolbarModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderEditToolbarModule.php', 'ResourceLoaderFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFileModule.php', 'ResourceLoaderFilePath' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePath.php', + 'ResourceLoaderForeignApiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderForeignApiModule.php', 'ResourceLoaderImage' => __DIR__ . '/includes/resourceloader/ResourceLoaderImage.php', 'ResourceLoaderImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderImageModule.php', 'ResourceLoaderJqueryMsgModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderJqueryMsgModule.php', @@ -1037,19 +1064,21 @@ $wgAutoloadLocalClasses = array( 'RevisionList' => __DIR__ . '/includes/RevisionList.php', 'RevisionListBase' => __DIR__ . '/includes/RevisionList.php', 'RevisiondeleteAction' => __DIR__ . '/includes/actions/RevisiondeleteAction.php', + 'RiffExtractor' => __DIR__ . '/includes/libs/RiffExtractor.php', 'RightsLogFormatter' => __DIR__ . '/includes/logging/RightsLogFormatter.php', 'RollbackAction' => __DIR__ . '/includes/actions/RollbackAction.php', 'RollbackEdits' => __DIR__ . '/maintenance/rollbackEdits.php', + 'RowUpdateGenerator' => __DIR__ . '/includes/utils/RowUpdateGenerator.php', 'RunJobs' => __DIR__ . '/maintenance/runJobs.php', 'RunningStat' => __DIR__ . '/includes/libs/RunningStat.php', 'SQLiteField' => __DIR__ . '/includes/db/DatabaseSqlite.php', 'SVGMetadataExtractor' => __DIR__ . '/includes/media/SVGMetadataExtractor.php', 'SVGReader' => __DIR__ . '/includes/media/SVGMetadataExtractor.php', + 'SamplingStatsdClient' => __DIR__ . '/includes/libs/SamplingStatsdClient.php', 'Sanitizer' => __DIR__ . '/includes/Sanitizer.php', 'SavepointPostgres' => __DIR__ . '/includes/db/DatabasePostgres.php', 'ScopedCallback' => __DIR__ . '/includes/libs/ScopedCallback.php', 'ScopedLock' => __DIR__ . '/includes/filebackend/lockmanager/ScopedLock.php', - 'ScopedPHPTimeout' => __DIR__ . '/includes/libs/ScopedPHPTimeout.php', 'SearchDatabase' => __DIR__ . '/includes/search/SearchDatabase.php', 'SearchDump' => __DIR__ . '/maintenance/dumpIterator.php', 'SearchEngine' => __DIR__ . '/includes/search/SearchEngine.php', @@ -1105,6 +1134,7 @@ $wgAutoloadLocalClasses = array( 'SpecialBookSources' => __DIR__ . '/includes/specials/SpecialBooksources.php', 'SpecialCachedPage' => __DIR__ . '/includes/specials/SpecialCachedPage.php', 'SpecialCategories' => __DIR__ . '/includes/specials/SpecialCategories.php', + 'SpecialChangeContentModel' => __DIR__ . '/includes/specials/SpecialChangeContentModel.php', 'SpecialChangeEmail' => __DIR__ . '/includes/specials/SpecialChangeEmail.php', 'SpecialChangePassword' => __DIR__ . '/includes/specials/SpecialChangePassword.php', 'SpecialComparePages' => __DIR__ . '/includes/specials/SpecialComparePages.php', @@ -1303,7 +1333,7 @@ $wgAutoloadLocalClasses = array( 'UserloginTemplate' => __DIR__ . '/includes/templates/Userlogin.php', 'UserrightsPage' => __DIR__ . '/includes/specials/SpecialUserrights.php', 'UsersPager' => __DIR__ . '/includes/specials/SpecialListusers.php', - 'UtfNormal' => __DIR__ . '/includes/libs/normal/UtfNormal.php', + 'UtfNormal' => __DIR__ . '/includes/compat/normal/UtfNormal.php', 'UzConverter' => __DIR__ . '/languages/classes/LanguageUz.php', 'VFormHTMLForm' => __DIR__ . '/includes/htmlform/VFormHTMLForm.php', 'ValidateRegistrationFile' => __DIR__ . '/maintenance/validateRegistrationFile.php', @@ -1339,6 +1369,7 @@ $wgAutoloadLocalClasses = array( 'WebInstallerUpgrade' => __DIR__ . '/includes/installer/WebInstallerPage.php', 'WebInstallerUpgradeDoc' => __DIR__ . '/includes/installer/WebInstallerPage.php', 'WebInstallerWelcome' => __DIR__ . '/includes/installer/WebInstallerPage.php', + 'WebPHandler' => __DIR__ . '/includes/media/WebP.php', 'WebRequest' => __DIR__ . '/includes/WebRequest.php', 'WebRequestUpload' => __DIR__ . '/includes/WebRequest.php', 'WebResponse' => __DIR__ . '/includes/WebResponse.php', diff --git a/composer.json b/composer.json index fba1a82423..1fe1e5068d 100644 --- a/composer.json +++ b/composer.json @@ -19,30 +19,35 @@ "cssjanus/cssjanus": "1.1.1", "ext-iconv": "*", "leafo/lessphp": "0.5.0", - "liuggio/statsd-php-client": "1.0.12", + "liuggio/statsd-php-client": "1.0.16", "mediawiki/at-ease": "1.0.0", - "oojs/oojs-ui": "0.11.4", + "oojs/oojs-ui": "0.12.8", "php": ">=5.3.3", "psr/log": "1.0.0", - "wikimedia/cdb": "1.0.1", "wikimedia/assert": "0.2.2", - "wikimedia/composer-merge-plugin": "1.1.0", - "wikimedia/utfnormal": "1.0.2", + "wikimedia/cdb": "1.3.0", + "wikimedia/composer-merge-plugin": "1.2.1", + "wikimedia/ip-set": "1.0.1", + "wikimedia/utfnormal": "1.0.3", + "wikimedia/wrappedstring": "2.0.0", "zordius/lightncandy": "0.21" }, "require-dev": { - "jakub-onderka/php-parallel-lint": "~0.8", + "jakub-onderka/php-parallel-lint": "0.9", "justinrainbow/json-schema": "~1.3", "phpunit/phpunit": "3.7.37", - "mediawiki/mediawiki-codesniffer": "0.1.0" + "mediawiki/mediawiki-codesniffer": "0.3.0" }, "suggest": { - "ext-fileinfo": "*", - "ext-intl": "*", - "ext-mbstring": "*", - "ext-wikidiff2": "*", - "ext-apc": "*", - "monolog/monolog": "*" + "ext-fileinfo": "Improved mime magic detection", + "ext-intl": "ICU integration", + "ext-mbstring": "Multibyte string support", + "ext-wikidiff2": "Diff accelerator", + "ext-apc": "Local data and opcode cache", + "monolog/monolog": "Flexible debug logging system", + "pear/mail": "Mail sending support", + "pear/mail_mime": "Mail sending support", + "pear/mail_mime-decode": "Mail sending support" }, "autoload": { "psr-0": { @@ -51,7 +56,7 @@ }, "scripts": { "lint": "parallel-lint --exclude vendor", - "phpcs": "phpcs $PHPCS_ARGS -s --standard=vendor/mediawiki/mediawiki-codesniffer/MediaWiki --ignore=vendor,node_modules --encoding=utf-8 --extensions=php,php5,inc,sample", + "phpcs": "phpcs -p $PHPCS_ARGS", "test": [ "composer lint", "composer phpcs" diff --git a/docs/doxygen_first_page.php b/docs/doxygen_first_page.php index 9949d13366..77ae1dcf36 100644 --- a/docs/doxygen_first_page.php +++ b/docs/doxygen_first_page.php @@ -1,5 +1,5 @@ execute(); \ No newline at end of file +$batch->execute(); diff --git a/docs/sitelist.txt b/docs/sitelist.txt index 48c7ce522a..24e1b9a7f5 100644 --- a/docs/sitelist.txt +++ b/docs/sitelist.txt @@ -44,4 +44,4 @@ The XML elements are used as follows: ** link: Generic URL template, often the document root. ** page_path: (for mediawiki sites) URL template for wiki pages (corresponds to the target wiki's $wgArticlePath setting) ** file_path: (for mediawiki sites) URL pattern for application entry points and resources (corresponds to the target wiki's $wgScriptPath setting). -* forward: Whether using a prefix defined by a localid tag in the URL will cause the request to be redirected to the corresponding page on the target wiki (currently unused). E.g. whether http://wiki.acme.com/wiki/foo:Buzz should be forwarded to http://wiki.foo.com/read/Buzz. (CAVEAT: not yet implement, can be specified but has no effect) \ No newline at end of file +* forward: Whether using a prefix defined by a localid tag in the URL will cause the request to be redirected to the corresponding page on the target wiki (currently unused). E.g. whether http://wiki.acme.com/wiki/foo:Buzz should be forwarded to http://wiki.foo.com/read/Buzz. (CAVEAT: not yet implement, can be specified but has no effect) diff --git a/docs/uidesign/design.html b/docs/uidesign/design.html index 51c1b55204..6ab57d7d4f 100644 --- a/docs/uidesign/design.html +++ b/docs/uidesign/design.html @@ -2,6 +2,7 @@ + diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index b14114d76b..96892d710f 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -124,9 +124,9 @@ class AjaxDispatcher { $result = call_user_func_array( $this->func_name, $this->args ); if ( $result === false || $result === null ) { - wfDebug( __METHOD__ . ' ERROR while dispatching ' - . $this->func_name . "(" . var_export( $this->args, true ) . "): " - . "no data returned\n" ); + wfDebug( __METHOD__ . ' ERROR while dispatching ' . + $this->func_name . "(" . var_export( $this->args, true ) . "): " . + "no data returned\n" ); wfHttpError( 500, 'Internal Error', "{$this->func_name} returned no data" ); @@ -141,9 +141,9 @@ class AjaxDispatcher { wfDebug( __METHOD__ . ' dispatch complete for ' . $this->func_name . "\n" ); } } catch ( Exception $e ) { - wfDebug( __METHOD__ . ' ERROR while dispatching ' - . $this->func_name . "(" . var_export( $this->args, true ) . "): " - . get_class( $e ) . ": " . $e->getMessage() . "\n" ); + wfDebug( __METHOD__ . ' ERROR while dispatching ' . + $this->func_name . "(" . var_export( $this->args, true ) . "): " . + get_class( $e ) . ": " . $e->getMessage() . "\n" ); if ( !headers_sent() ) { wfHttpError( 500, 'Internal Error', diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 2984c33a56..6c2efc296a 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -166,12 +166,12 @@ class AjaxResponse { HttpStatus::header( $n ); } - header ( "Content-Type: " . $this->mContentType ); + header( "Content-Type: " . $this->mContentType ); if ( $this->mLastModified ) { - header ( "Last-Modified: " . $this->mLastModified ); + header( "Last-Modified: " . $this->mLastModified ); } else { - header ( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); + header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); } if ( $this->mCacheDuration ) { @@ -193,20 +193,20 @@ class AjaxResponse { } else { # Let the client do the caching. Cache is not purged. - header ( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" ); - header ( "Cache-Control: s-maxage={$this->mCacheDuration}," . + header( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" ); + header( "Cache-Control: s-maxage={$this->mCacheDuration}," . "public,max-age={$this->mCacheDuration}" ); } } else { # always expired, always modified - header ( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past - header ( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1 - header ( "Pragma: no-cache" ); // HTTP/1.0 + header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past + header( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1 + header( "Pragma: no-cache" ); // HTTP/1.0 } if ( $this->mVary ) { - header ( "Vary: " . $this->mVary ); + header( "Vary: " . $this->mVary ); } } diff --git a/includes/Block.php b/includes/Block.php index d58220144a..c5a16fcea6 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -23,15 +23,16 @@ class Block { /** @var string */ public $mReason; - /** @var bool|string */ + /** @var string */ public $mTimestamp; - /** @var int */ + /** @var bool */ public $mAuto; - /** @var bool|string */ + /** @var string */ public $mExpiry; + /** @var bool */ public $mHideName; /** @var int */ @@ -65,10 +66,10 @@ class Block { protected $blocker; /** @var bool */ - protected $isHardblock = true; + protected $isHardblock; /** @var bool */ - protected $isAutoblocking = true; + protected $isAutoblocking; # TYPE constants const TYPE_USER = 1; @@ -78,55 +79,84 @@ class Block { const TYPE_ID = 5; /** - * @todo FIXME: Don't know what the best format to have for this constructor - * is, but fourteen optional parameters certainly isn't it. - * @param string $address - * @param int $user - * @param int $by - * @param string $reason - * @param mixed $timestamp - * @param int $auto - * @param string $expiry - * @param int $anonOnly - * @param int $createAccount - * @param int $enableAutoblock - * @param int $hideName - * @param int $blockEmail - * @param int $allowUsertalk - * @param string $byText + * Create a new block with specified parameters on a user, IP or IP range. + * + * @param array $options Parameters of the block: + * address string|User Target user name, User object, IP address or IP range + * user int Override target user ID (for foreign users) + * by int User ID of the blocker + * reason string Reason of the block + * timestamp string The time at which the block comes into effect + * auto bool Is this an automatic block? + * expiry string Timestamp of expiration of the block or 'infinity' + * anonOnly bool Only disallow anonymous actions + * createAccount bool Disallow creation of new accounts + * enableAutoblock bool Enable automatic blocking + * hideName bool Hide the target user name + * blockEmail bool Disallow sending emails + * allowUsertalk bool Allow the target to edit its own talk page + * byText string Username of the blocker (for foreign users) + * + * @since 1.26 accepts $options array instead of individual parameters; order + * of parameters above reflects the original order */ - function __construct( $address = '', $user = 0, $by = 0, $reason = '', - $timestamp = 0, $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, - $hideName = 0, $blockEmail = 0, $allowUsertalk = 0, $byText = '' - ) { - if ( $timestamp === 0 ) { - $timestamp = wfTimestampNow(); - } + function __construct( $options = array() ) { + $defaults = array( + 'address' => '', + 'user' => null, + 'by' => null, + 'reason' => '', + 'timestamp' => '', + 'auto' => false, + 'expiry' => '', + 'anonOnly' => false, + 'createAccount' => false, + 'enableAutoblock' => false, + 'hideName' => false, + 'blockEmail' => false, + 'allowUsertalk' => false, + 'byText' => '', + ); - if ( count( func_get_args() ) > 0 ) { - # Soon... :D - # wfDeprecated( __METHOD__ . " with arguments" ); + if ( func_num_args() > 1 || !is_array( $options ) ) { + $options = array_combine( + array_slice( array_keys( $defaults ), 0, func_num_args() ), + func_get_args() + ); + wfDeprecated( __METHOD__ . ' with multiple arguments', '1.26' ); } - $this->setTarget( $address ); - if ( $this->target instanceof User && $user ) { - $this->forcedTargetID = $user; // needed for foreign users + $options += $defaults; + + $this->setTarget( $options['address'] ); + + if ( $this->target instanceof User && $options['user'] ) { + # Needed for foreign users + $this->forcedTargetID = $options['user']; } - if ( $by ) { // local user - $this->setBlocker( User::newFromId( $by ) ); - } else { // foreign user - $this->setBlocker( $byText ); + + if ( $options['by'] ) { + # Local user + $this->setBlocker( User::newFromID( $options['by'] ) ); + } else { + # Foreign user + $this->setBlocker( $options['byText'] ); } - $this->mReason = $reason; - $this->mTimestamp = wfTimestamp( TS_MW, $timestamp ); - $this->mAuto = $auto; - $this->isHardblock( !$anonOnly ); - $this->prevents( 'createaccount', $createAccount ); - $this->mExpiry = wfGetDB( DB_SLAVE )->decodeExpiry( $expiry ); - $this->isAutoblocking( $enableAutoblock ); - $this->mHideName = $hideName; - $this->prevents( 'sendemail', $blockEmail ); - $this->prevents( 'editownusertalk', !$allowUsertalk ); + + $this->mReason = $options['reason']; + $this->mTimestamp = wfTimestamp( TS_MW, $options['timestamp'] ); + $this->mExpiry = wfGetDB( DB_SLAVE )->decodeExpiry( $options['expiry'] ); + + # Boolean settings + $this->mAuto = (bool)$options['auto']; + $this->mHideName = (bool)$options['hideName']; + $this->isHardblock( !$options['anonOnly'] ); + $this->isAutoblocking( (bool)$options['enableAutoblock'] ); + + # Prevention measures + $this->prevents( 'sendemail', (bool)$options['blockEmail'] ); + $this->prevents( 'editownusertalk', !$options['allowUsertalk'] ); + $this->prevents( 'createaccount', (bool)$options['createAccount'] ); $this->mFromMaster = false; } @@ -1109,7 +1139,7 @@ class Block { $blocks = array(); foreach ( $rows as $row ) { $block = self::newFromRow( $row ); - if ( !$block->deleteIfExpired() ) { + if ( !$block->deleteIfExpired() ) { $blocks[] = $block; } } diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index 66079c0179..e2c31a66ff 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -329,7 +329,7 @@ class CategoryViewer extends ContextSource { 'category' => array( 'LEFT JOIN', array( 'cat_title = page_title', 'page_namespace' => NS_CATEGORY - )) + ) ) ) ); diff --git a/includes/CdbCompat.php b/includes/CdbCompat.php deleted file mode 100644 index 0074cc96b0..0000000000 --- a/includes/CdbCompat.php +++ /dev/null @@ -1,45 +0,0 @@ - 'PNGHandler', 'image/gif' => 'GIFHandler', 'image/tiff' => 'TiffHandler', + 'image/webp' => 'WebPHandler', 'image/x-ms-bmp' => 'BmpHandler', 'image/x-bmp' => 'BmpHandler', 'image/x-xcf' => 'XCFHandler', @@ -970,6 +979,14 @@ $wgJpegTran = '/usr/bin/jpegtran'; */ $wgExiv2Command = '/usr/bin/exiv2'; + +/** + * Path to exiftool binary. Used for lossless ICC profile swapping. + * + * @since 1.26 + */ +$wgExiftool = '/usr/bin/exiftool'; + /** * Scalable Vector Graphics (SVG) may be uploaded as images. * Since SVG support is not yet standard in browsers, it is @@ -1004,7 +1021,7 @@ $wgSVGConverterPath = ''; /** * Don't scale a SVG larger than this */ -$wgSVGMaxSize = 2048; +$wgSVGMaxSize = 5120; /** * Don't read SVG metadata beyond this point. @@ -1322,6 +1339,14 @@ $wgUploadThumbnailRenderHttpCustomHost = false; */ $wgUploadThumbnailRenderHttpCustomDomain = false; +/** + * When this variable is true and JPGs use the sRGB ICC profile, swaps it for the more lightweight + * (and free) TinyRGB profile when generating thumbnails. + * + * @since 1.26 + */ +$wgUseTinyRGBForJPGThumbnails = false; + /** * Default parameters for the "" tag */ @@ -1572,7 +1597,8 @@ $wgEnotifRevealEditorAddress = false; /** * Send notification mails on minor edits to watchlist pages. This is enabled - * by default. Does not affect user talk notifications. + * by default. User talk notifications are affected by this, $wgEnotifUserTalk, and + * the nominornewtalk user right. */ $wgEnotifMinorEdits = true; @@ -1597,15 +1623,6 @@ $wgEnotifMaxRecips = 500; */ $wgEnotifUseJobQ = false; -/** - * Use the job queue for user activity updates like updating "last visited" - * fields for email notifications of page changes. This should only be enabled - * if the jobs have a dedicated runner to avoid update lag. - * - * @since 1.26 - */ -$wgActivityUpdatesUseJobQueue = false; - /** * Use real name instead of username in e-mail "from" field. */ @@ -1843,12 +1860,6 @@ $wgDBservers = false; */ $wgLBFactoryConf = array( 'class' => 'LBFactorySimple' ); -/** - * How long to wait for a slave to catch up to the master - * @deprecated since 1.24 - */ -$wgMasterWaitTimeout = 10; - /** * File to log database errors to */ @@ -2061,6 +2072,14 @@ $wgMaxArticleSize = 2048; */ $wgMemoryLimit = "50M"; +/** + * The minimum amount of time that MediaWiki needs for "slow" write request, + * particularly ones with multiple non-atomic writes that *should* be as + * transactional as possible; MediaWiki will call set_time_limit() if needed. + * @since 1.26 + */ +$wgTransactionalTimeLimit = 120; + /** @} */ # end performance hacks } /************************************************************************//** @@ -2151,6 +2170,19 @@ $wgObjectCaches = array( CACHE_ACCEL => array( 'factory' => 'ObjectCache::newAccelerator' ), CACHE_MEMCACHED => array( 'factory' => 'ObjectCache::newMemcached', 'loggroup' => 'memcached' ), + 'db-replicated' => array( + 'class' => 'ReplicatedBagOStuff', + 'readFactory' => array( + 'class' => 'SqlBagOStuff', + 'args' => array( array( 'slaveOnly' => true ) ) + ), + 'writeFactory' => array( + 'class' => 'SqlBagOStuff', + 'args' => array( array( 'slaveOnly' => false ) ) + ), + 'loggroup' => 'SQLBagOStuff' + ), + 'apc' => array( 'class' => 'APCBagOStuff' ), 'xcache' => array( 'class' => 'XCacheBagOStuff' ), 'wincache' => array( 'class' => 'WinCacheBagOStuff' ), @@ -2213,6 +2245,7 @@ $wgWANObjectCaches = array( * lightweight data like hit counters and user activity. Sites with multiple * data-centers should have this use a store that replicates all writes. The * store should have enough consistency for CAS operations to be usable. + * Reads outside of those needed for merge() may be eventually consistent. * * The options are: * - db: Store cache objects in the DB @@ -2221,7 +2254,7 @@ $wgWANObjectCaches = array( * * @since 1.26 */ -$wgMainStash = 'db'; +$wgMainStash = 'db-replicated'; /** * The expiry time for the parser cache, in seconds. @@ -2502,13 +2535,16 @@ $wgInternalServer = false; /** * Cache timeout for the squid, will be sent as s-maxage (without ESI) or * Surrogate-Control (with ESI). Without ESI, you should strip out s-maxage in - * the Squid config. 18000 seconds = 5 hours, more cache hits with 2678400 = 31 - * days + * the Squid config. + * +* 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days. */ $wgSquidMaxage = 18000; /** * Default maximum age for raw CSS/JS accesses + * + * 300 seconds = 5 minutes. */ $wgForcedRawSMaxage = 300; @@ -2797,14 +2833,14 @@ $wgBrowserBlackList = array( * - Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC) * - [...] * - * @link http://en.wikipedia.org/w/index.php?diff=12356041&oldid=12355864 - * @link http://en.wikipedia.org/wiki/Template%3AOS9 + * @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 - * http://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361 + * 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;\)/' ); @@ -3437,8 +3473,8 @@ $wgResourceModuleSkinStyles = array(); $wgResourceLoaderSources = array(); /** - * Default 'remoteBasePath' value for instances of ResourceLoaderFileModule. - * If not set, then $wgScriptPath will be used as a fallback. + * The default 'remoteBasePath' value for instances of ResourceLoaderFileModule. + * Defaults to $wgScriptPath. */ $wgResourceBasePath = null; @@ -3476,13 +3512,6 @@ $wgResourceLoaderMaxage = array( */ $wgResourceLoaderDebug = false; -/** - * Enable embedding of certain resources using Edge Side Includes. This will - * improve performance but only works if there is something in front of the - * web server (e..g a Squid or Varnish server) configured to process the ESI. - */ -$wgResourceLoaderUseESI = false; - /** * Put each statement on its own line when minifying JavaScript. This makes * debugging in non-debug mode a bit easier. @@ -3497,28 +3526,27 @@ $wgResourceLoaderMinifierStatementsOnOwnLine = false; $wgResourceLoaderMinifierMaxLineLength = 1000; /** - * Whether to include the mediawiki.legacy JS library (old wikibits.js), and its - * dependencies. + * Whether to ensure the mediawiki.legacy library is loaded before other modules. + * + * @deprecated since 1.26: Always declare dependencies. */ $wgIncludeLegacyJavaScript = true; /** - * Whether to preload the mediawiki.util module as blocking module in the top - * queue. + * Whether to ensure the mediawiki.util is loaded before other modules. * - * Before MediaWiki 1.19, modules used to load slower/less asynchronous which - * allowed modules to lack dependencies on 'popular' modules that were likely - * loaded already. + * Before MediaWiki 1.19, modules used to load less asynchronous which allowed + * modules to lack dependencies on 'popular' modules that were likely loaded already. * * This setting is to aid scripts during migration by providing mediawiki.util - * unconditionally (which was the most commonly missed dependency). - * It doesn't cover all missing dependencies obviously but should fix most of - * them. + * unconditionally (which was the most commonly missed dependency). It doesn't + * cover all missing dependencies obviously but should fix most of them. * * This should be removed at some point after site/user scripts have been fixed. * Enable this if your wiki has a large amount of user/site scripts that are * lacking dependencies. - * @todo Deprecate + * + * @deprecated since 1.26: Always declare dependencies. */ $wgPreloadJavaScriptMwUtil = false; @@ -3583,13 +3611,6 @@ $wgResourceLoaderValidateJS = true; */ $wgResourceLoaderValidateStaticJS = false; -/** - * If set to true, asynchronous loading of bottom-queue scripts in the "" - * will be enabled. This is an experimental feature that's supposed to make - * JavaScript load faster. - */ -$wgResourceLoaderExperimentalAsyncLoading = false; - /** * Global LESS variables. An associative array binding variable names to * LESS code snippets representing their values. @@ -3615,18 +3636,6 @@ $wgResourceLoaderExperimentalAsyncLoading = false; */ $wgResourceLoaderLESSVars = array(); -/** - * Custom LESS functions. An associative array mapping function name to PHP - * callable. - * - * Changes to LESS functions do not trigger cache invalidation. - * - * @since 1.22 - * @deprecated since 1.24 Questionable usefulness and problematic to support, - * will be removed in the future. - */ -$wgResourceLoaderLESSFunctions = array(); - /** * Default import paths for LESS modules. LESS files referenced in @import * statements will be looked up here first, and relative to the importing file @@ -3699,8 +3708,8 @@ $wgMetaNamespaceTalk = false; * Additional namespaces. If the namespaces defined in Language.php and * Namespace.php are insufficient, you can create new ones here, for example, * to import Help files in other languages. You can also override the namespace - * names of existing namespaces. Extensions developers should use - * $wgCanonicalNamespaceNames. + * names of existing namespaces. Extensions should use the CanonicalNamespaces + * hook or extension.json. * * @warning Once you delete a namespace, the pages in that namespace will * no longer be accessible. If you rename it, then you can access them through @@ -3935,6 +3944,15 @@ $wgTrackingCategories = array(); */ $wgContentNamespaces = array( NS_MAIN ); +/** + * Array of namespaces, in addition to the talk namespaces, where signatures + * (~~~~) are likely to be used. This determines whether to display the + * Signature button on the edit toolbar, and may also be used by extensions. + * For example, "traditional" style wikis, where content and discussion are + * intermixed, could place NS_MAIN and NS_PROJECT namespaces in this array. + */ +$wgExtraSignatureNamespaces = array(); + /** * Max number of redirects to follow when resolving redirects. * 1 means only the first redirect is followed (default behavior). @@ -4086,44 +4104,55 @@ $wgEnableImageWhitelist = true; $wgAllowImageTag = false; /** - * $wgUseTidy: use tidy to make sure HTML output is sane. - * Tidy is a free tool that fixes broken HTML. - * See http://www.w3.org/People/Raggett/tidy/ + * Configuration for HTML postprocessing tool. Set this to a configuration + * array to enable an external tool. Dave Raggett's "HTML Tidy" is typically + * used. See http://www.w3.org/People/Raggett/tidy/ * - * - $wgTidyBin should be set to the path of the binary and - * - $wgTidyConf to the path of the configuration file. - * - $wgTidyOpts can include any number of parameters. - * - $wgTidyInternal controls the use of the PECL extension or the - * libtidy (PHP >= 5) extension to use an in-process tidy library instead - * of spawning a separate program. - * Normally you shouldn't need to override the setting except for - * debugging. To install, use 'pear install tidy' and add a line - * 'extension=tidy.so' to php.ini. + * If this is null and $wgUseTidy is true, the deprecated configuration + * parameters will be used instead. + * + * If this is null and $wgUseTidy is false, a pure PHP fallback will be used. + * + * Keys are: + * - driver: May be: + * - RaggettInternalHHVM: Use the limited-functionality HHVM extension + * - RaggettInternalPHP: Use the PECL extension + * - RaggettExternal: Shell out to an external binary (tidyBin) + * + * - tidyConfigFile: Path to configuration file for any of the Raggett drivers + * - debugComment: True to add a comment to the output with warning messages + * - tidyBin: For RaggettExternal, the path to the tidy binary. + * - tidyCommandLine: For RaggettExternal, additional command line options. */ -$wgUseTidy = false; +$wgTidyConfig = null; /** - * @see $wgUseTidy + * Set this to true to use the deprecated tidy configuration parameters. + * @deprecated use $wgTidyConfig */ -$wgAlwaysUseTidy = false; +$wgUseTidy = false; /** - * @see $wgUseTidy + * The path to the tidy binary. + * @deprecated Use $wgTidyConfig['tidyBin'] */ $wgTidyBin = 'tidy'; /** - * @see $wgUseTidy + * The path to the tidy config file + * @deprecated Use $wgTidyConfig['tidyConfigFile'] */ -$wgTidyConf = $IP . '/includes/tidy.conf'; +$wgTidyConf = $IP . '/includes/tidy/tidy.conf'; /** - * @see $wgUseTidy + * The command line options to the tidy binary + * @deprecated Use $wgTidyConfig['tidyCommandLine'] */ $wgTidyOpts = ''; /** - * @see $wgUseTidy + * Set this to true to use the tidy extension + * @deprecated Use $wgTidyConfig['driver'] */ $wgTidyInternal = extension_loaded( 'tidy' ); @@ -4405,7 +4434,7 @@ $wgPasswordConfig = array( */ $wgPasswordResetRoutes = array( 'username' => true, - 'email' => false, + 'email' => true, ); /** @@ -4524,7 +4553,7 @@ $wgUserrightsInterwikiDelimiter = '@'; /** * This is to let user authenticate using https when they come from http. * Based on an idea by George Herbert on wikitech-l: - * http://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050039.html + * https://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050039.html * @since 1.17 */ $wgSecureLogin = false; @@ -5051,7 +5080,7 @@ $wgAccountCreationThrottle = 0; * There's no administrator override on-wiki, so be careful what you set. :) * May be an array of regexes or a single string for backwards compatibility. * - * @see http://en.wikipedia.org/wiki/Regular_expression + * @see https://en.wikipedia.org/wiki/Regular_expression * * @note Each regex needs a beginning/end delimiter, eg: # or / */ @@ -5249,6 +5278,22 @@ $wgProxyList = array(); */ $wgCookieExpiration = 180 * 86400; +/** + * The identifiers of the login cookies that can have their lifetimes + * extended independently of all other login cookies. + * + * @var string[] + */ +$wgExtendedLoginCookies = array( 'UserID', 'Token' ); + +/** + * Default login cookie lifetime, in seconds. Setting + * $wgExtendLoginCookieExpiration to null will use $wgCookieExpiration to + * calculate the cookie lifetime. As with $wgCookieExpiration, 0 will make + * login cookies session-only. + */ +$wgExtendedLoginCookieExpiration = null; + /** * Set to set an explicit domain on the login cookies eg, "justthis.domain.org" * or ".any.subdomain.net" @@ -5582,7 +5627,7 @@ $wgProfilePerHost = null; * * The host should be running a daemon which can be obtained from MediaWiki * Git at: - * http://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile + * https://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile * * @deprecated set $wgProfiler['udphost'] instead */ @@ -5638,6 +5683,29 @@ $wgAggregateStatsID = false; */ $wgStatsFormatString = "stats/%s - %s 1 1 1 1 %s\n"; +/** + * Destination of statsd metrics. + * + * A host or host:port of a statsd server. Port defaults to 8125. + * + * If not set, statsd metrics will not be collected. + * + * @see wfLogProfilingData + * @since 1.25 + */ +$wgStatsdServer = false; + +/** + * Prefix for metric names sent to wgStatsdServer. + * + * Defaults to "MediaWiki". + * + * @see RequestContext::getStats + * @see BufferingStatsdDataFactory + * @since 1.25 + */ +$wgStatsdMetricPrefix = false; + /** * InfoAction retrieves a list of transclusion links (both to and from). * This number puts a limit on that query in the case of highly transcluded @@ -5970,6 +6038,21 @@ $wgGitRepositoryViewers = array( */ $wgRCMaxAge = 90 * 24 * 3600; +/** + * Page watchers inactive for more than this many seconds are considered inactive. + * Used mainly by action=info. Default: 180 days = about six months. + * @since 1.26 + */ +$wgWatchersMaxAge = 180 * 24 * 3600; + +/** + * If active watchers (per above) are this number or less, do not disclose it. + * Left to 1, prevents unprivileged users from knowing for sure that there are 0. + * Set to -1 if you want to always complement watchers count with this info. + * @since 1.26 + */ +$wgUnwatchedPageSecret = 1; + /** * Filter $wgRCLinkDays by $wgRCMaxAge to avoid showing links for numbers * higher than what will be stored. Note that this is disabled by default @@ -6639,7 +6722,7 @@ $wgJobSerialCommitThreshold = false; * These settings should be global to all wikis. */ $wgJobTypeConf = array( - 'default' => array( 'class' => 'JobQueueDB', 'order' => 'random' ), + 'default' => array( 'class' => 'JobQueueDB', 'order' => 'random', 'claimTTL' => 3600 ), ); /** @@ -6755,6 +6838,7 @@ $wgLogTypes = array( 'suppress', 'tag', 'managetags', + 'contentmodel', ); /** @@ -6830,15 +6914,15 @@ $wgLogNames = array( $wgLogHeaders = array( '' => 'alllogstext', 'block' => 'blocklogtext', - 'protect' => 'protectlogtext', - 'rights' => 'rightslogtext', 'delete' => 'dellogpagetext', - 'upload' => 'uploadlogpagetext', - 'move' => 'movelogpagetext', 'import' => 'importlogpagetext', - 'patrol' => 'patrol-log-header', 'merge' => 'mergelogpagetext', + 'move' => 'movelogpagetext', + 'patrol' => 'patrol-log-header', + 'protect' => 'protectlogtext', + 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', + 'upload' => 'uploadlogpagetext', ); /** @@ -6848,10 +6932,9 @@ $wgLogHeaders = array( * Extensions with custom log types may add to this array. */ $wgLogActions = array( - 'protect/protect' => 'protectedarticle', 'protect/modify' => 'modifiedarticleprotection', + 'protect/protect' => 'protectedarticle', 'protect/unprotect' => 'unprotectedarticle', - 'protect/move_prot' => 'movedarticleprotection', ); /** @@ -6861,34 +6944,36 @@ $wgLogActions = array( * @see LogFormatter */ $wgLogActionsHandlers = array( - 'move/move' => 'MoveLogFormatter', - 'move/move_redir' => 'MoveLogFormatter', + 'block/block' => 'BlockLogFormatter', + 'block/reblock' => 'BlockLogFormatter', + 'block/unblock' => 'BlockLogFormatter', + 'contentmodel/change' => 'ContentModelLogFormatter', 'delete/delete' => 'DeleteLogFormatter', + 'delete/event' => 'DeleteLogFormatter', 'delete/restore' => 'DeleteLogFormatter', 'delete/revision' => 'DeleteLogFormatter', - 'delete/event' => 'DeleteLogFormatter', - 'suppress/revision' => 'DeleteLogFormatter', - 'suppress/event' => 'DeleteLogFormatter', - 'suppress/delete' => 'DeleteLogFormatter', - 'patrol/patrol' => 'PatrolLogFormatter', - 'rights/rights' => 'RightsLogFormatter', - 'rights/autopromote' => 'RightsLogFormatter', - 'upload/upload' => 'UploadLogFormatter', - 'upload/overwrite' => 'UploadLogFormatter', - 'upload/revert' => 'UploadLogFormatter', - 'merge/merge' => 'MergeLogFormatter', - 'tag/update' => 'TagLogFormatter', - 'managetags/create' => 'LogFormatter', - 'managetags/delete' => 'LogFormatter', + 'import/interwiki' => 'LogFormatter', + 'import/upload' => 'LogFormatter', 'managetags/activate' => 'LogFormatter', + 'managetags/create' => 'LogFormatter', 'managetags/deactivate' => 'LogFormatter', - 'block/block' => 'BlockLogFormatter', - 'block/unblock' => 'BlockLogFormatter', - 'block/reblock' => 'BlockLogFormatter', + 'managetags/delete' => 'LogFormatter', + 'merge/merge' => 'MergeLogFormatter', + 'move/move' => 'MoveLogFormatter', + 'move/move_redir' => 'MoveLogFormatter', + 'patrol/patrol' => 'PatrolLogFormatter', + 'protect/move_prot' => 'ProtectLogFormatter', + 'rights/autopromote' => 'RightsLogFormatter', + 'rights/rights' => 'RightsLogFormatter', 'suppress/block' => 'BlockLogFormatter', + 'suppress/delete' => 'DeleteLogFormatter', + 'suppress/event' => 'DeleteLogFormatter', 'suppress/reblock' => 'BlockLogFormatter', - 'import/upload' => 'LogFormatter', - 'import/interwiki' => 'LogFormatter', + 'suppress/revision' => 'DeleteLogFormatter', + 'tag/update' => 'TagLogFormatter', + 'upload/overwrite' => 'UploadLogFormatter', + 'upload/revert' => 'UploadLogFormatter', + 'upload/upload' => 'UploadLogFormatter', ); /** @@ -6914,14 +6999,6 @@ $wgAllowSpecialInclusion = true; */ $wgDisableQueryPageUpdate = false; -/** - * List of special pages, followed by what subtitle they should go under - * at Special:SpecialPages - * - * @deprecated since 1.21 Override SpecialPage::getGroupName instead - */ -$wgSpecialPageGroups = array(); - /** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. @@ -7159,12 +7236,6 @@ $wgAPIPropModules = array(); */ $wgAPIListModules = array(); -/** - * This variable is ignored. To add your module to the API, please add it to $wgAPI*Modules - * @deprecated since 1.21 - */ -$wgAPIGeneratorModules = array(); - /** * Maximum amount of rows to scan in a DB query in the API * The default value is generally fine @@ -7615,6 +7686,7 @@ $wgUseLinkNamespaceDBFields = true; * $wgVirtualRestConfig['modules']['parsoid'] = array( * 'url' => 'http://localhost:8000', * 'prefix' => 'enwiki', + * 'domain' => 'en.wikipedia.org', * ); * * @var array @@ -7625,11 +7697,22 @@ $wgVirtualRestConfig = array( 'global' => array( # Timeout in seconds 'timeout' => 360, + # 'domain' is set to $wgCanonicalServer in Setup.php 'forwardCookies' => false, 'HTTPProxy' => null ) ); +/** + * Controls the percentage of zero-result search queries with suggestions that + * run the suggestion automatically. Must be a number between 0 and 1. This + * can be lowered to reduce query volume at the expense of result quality. + * + * @var float + * @since 1.26 + */ +$wgSearchRunSuggestedQueryPercent = 1; + /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/Defines.php b/includes/Defines.php index 262471eccc..d55bbcf819 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -198,7 +198,7 @@ define( 'LIST_OR', 4 ); /** * Unicode and normalisation related */ -require_once __DIR__ . '/libs/normal/UtfNormalDefines.php'; +require_once __DIR__ . '/compat/normal/UtfNormalDefines.php'; /**@{ * Hook support constants diff --git a/includes/EditPage.php b/includes/EditPage.php index ee67d01d69..05e0ac0ee9 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -167,6 +167,12 @@ class EditPage { */ const AS_PARSE_ERROR = 240; + /** + * Status: when changing the content model is disallowed due to + * $wgContentHandlerUseDB being false + */ + const AS_CANNOT_USE_CUSTOM_MODEL = 241; + /** * HTML id and name for the beginning of the edit form. */ @@ -521,7 +527,10 @@ class EditPage { if ( $permErrors ) { wfDebug( __METHOD__ . ": User can't edit\n" ); // Auto-block user's IP if the account was "hard" blocked - $wgUser->spreadAnyEditBlock(); + $user = $wgUser; + DeferredUpdates::addCallableUpdate( function() use ( $user ) { + $user->spreadAnyEditBlock(); + } ); $this->displayPermissionsError( $permErrors ); @@ -646,6 +655,9 @@ class EditPage { $this->getContextTitle()->getPrefixedText() ) ); $wgOut->addBacklinkSubtitle( $this->getContextTitle() ); + $wgOut->addHTML( $this->editFormPageTop ); + $wgOut->addHTML( $this->editFormTextTop ); + $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) ); $wgOut->addHTML( "
\n" ); @@ -659,13 +671,16 @@ class EditPage { $wgOut->addWikiMsg( 'viewsourcetext' ); } + $wgOut->addHTML( $this->editFormTextBeforeContent ); $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) ); + $wgOut->addHTML( $this->editFormTextAfterContent ); $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ), Linker::formatTemplates( $this->getTemplates() ) ) ); $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' ); + $wgOut->addHTML( $this->editFormTextBottom ); if ( $this->mTitle->exists() ) { $wgOut->returnToMain( null, $this->mTitle ); } @@ -1037,7 +1052,6 @@ class EditPage { $undo = $wgRequest->getInt( 'undo' ); if ( $undo > 0 && $undoafter > 0 ) { - $undorev = Revision::newFromId( $undo ); $oldrev = Revision::newFromId( $undoafter ); @@ -1046,8 +1060,8 @@ class EditPage { # Otherwise, $content will be left as-is. if ( !is_null( $undorev ) && !is_null( $oldrev ) && !$undorev->isDeleted( Revision::DELETED_TEXT ) && - !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - + !$oldrev->isDeleted( Revision::DELETED_TEXT ) + ) { $content = $this->mArticle->getUndoContent( $undorev, $oldrev ); if ( $content === false ) { @@ -1242,9 +1256,9 @@ class EditPage { if ( !$converted ) { //TODO: somehow show a warning to the user! - wfDebug( "Attempt to preload incompatible content: " - . "can't convert " . $content->getModel() - . " to " . $handler->getModelID() ); + wfDebug( "Attempt to preload incompatible content: " . + "can't convert " . $content->getModel() . + " to " . $handler->getModelID() ); return $handler->makeEmptyContent(); } @@ -1362,6 +1376,7 @@ class EditPage { case self::AS_HOOK_ERROR: return false; + case self::AS_CANNOT_USE_CUSTOM_MODEL: case self::AS_PARSE_ERROR: $wgOut->addWikiText( '
' . $status->getWikiText() . '
' ); return true; @@ -1544,6 +1559,7 @@ class EditPage { */ function internalAttemptSave( &$result, $bot = false ) { global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize; + global $wgContentHandlerUseDB; $status = Status::newGood(); @@ -1664,11 +1680,19 @@ class EditPage { } } - if ( $this->contentModel !== $this->mTitle->getContentModel() - && !$wgUser->isAllowed( 'editcontentmodel' ) - ) { - $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); - return $status; + $changingContentModel = false; + if ( $this->contentModel !== $this->mTitle->getContentModel() ) { + if ( !$wgContentHandlerUseDB ) { + $status->fatal( 'editpage-cannot-use-custom-model' ); + $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL; + return $status; + } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) { + $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); + return $status; + + } + $changingContentModel = true; + $oldContentModel = $this->mTitle->getContentModel(); } if ( $this->changeTags ) { @@ -1928,7 +1952,7 @@ class EditPage { $this->summary, $flags, false, - null, + $wgUser, $content->getDefaultFormat() ); @@ -1967,9 +1991,39 @@ class EditPage { } ); } + // If the content model changed, add a log entry + if ( $changingContentModel ) { + $this->addContentModelChangeLogEntry( + $wgUser, + $oldContentModel, + $this->contentModel, + $this->summary + ); + } + return $status; } + /** + * @param Title $title + * @param string $oldModel + * @param string $newModel + * @param string $reason + */ + protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) { + $log = new ManualLogEntry( 'contentmodel', 'change' ); + $log->setPerformer( $user ); + $log->setTarget( $this->mTitle ); + $log->setComment( $reason ); + $log->setParameters( array( + '4::oldmodel' => $oldModel, + '5::newmodel' => $newModel + ) ); + $logid = $log->insert(); + $log->publish( $logid ); + } + + /** * Register the change of watch status */ @@ -2500,7 +2554,7 @@ class EditPage { $wgOut->addHTML( $this->editFormTextBeforeContent ); if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) { - $wgOut->addHTML( EditPage::getEditToolbar() ); + $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) ); } if ( $this->blankArticle ) { @@ -3400,7 +3454,7 @@ HTML $this->deletedSinceEdit = false; - if ( $this->mTitle->isDeletedQuick() ) { + if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) { $this->lastDelete = $this->getLastDelete(); if ( $this->lastDelete ) { $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); @@ -3641,13 +3695,18 @@ HTML * Shows a bulletin board style toolbar for common editing functions. * It can be disabled in the user preferences. * + * @param $title Title object for the page being edited (optional) * @return string */ - static function getEditToolbar() { + static function getEditToolbar( $title = null ) { global $wgContLang, $wgOut; global $wgEnableUploads, $wgForeignFileRepos; $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos ); + $showSignature = true; + if ( $title ) { + $showSignature = MWNamespace::wantSignatures( $title->getNamespace() ); + } /** * $toolarray is an array of arrays each of which includes the @@ -3715,13 +3774,13 @@ HTML 'sample' => wfMessage( 'nowiki_sample' )->text(), 'tip' => wfMessage( 'nowiki_tip' )->text(), ), - array( + $showSignature ? array( 'id' => 'mw-editbutton-signature', 'open' => '--~~~~', 'close' => '', 'sample' => '', 'tip' => wfMessage( 'sig_tip' )->text(), - ), + ) : false, array( 'id' => 'mw-editbutton-hr', 'open' => "\n----\n", diff --git a/includes/Export.php b/includes/Export.php index 0d55d7dbc1..adab21c32c 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -874,7 +874,7 @@ class XmlDumpWriter { } global $wgContLang; - $prefix = str_replace( '_', ' ', $wgContLang->getNsText( $title->getNamespace() ) ); + $prefix = $wgContLang->getFormattedNsText( $title->getNamespace() ); if ( $prefix !== '' ) { $prefix .= ':'; diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 6d74af2da1..bcd6db2026 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -297,7 +297,7 @@ class FileDeleteForm { if ( $wgUser->isAllowed( 'editinterface' ) ) { $title = wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(); - $link = Linker::link( + $link = Linker::linkKnown( $title, wfMessage( 'filedelete-edit-reasonlist' )->escaped(), array(), diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 7a9b27bbf5..f2e37d557c 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -24,7 +24,6 @@ if ( !defined( 'MEDIAWIKI' ) ) { die( "This file is part of MediaWiki, it is not a valid entry point" ); } -use Liuggio\StatsdClient\StatsdClient; use Liuggio\StatsdClient\Sender\SocketSender; use MediaWiki\Logger\LoggerFactory; @@ -172,6 +171,7 @@ if ( !function_exists( 'hash_equals' ) ) { * * @param string $ext Name of the extension to load * @param string|null $path Absolute path of where to find the extension.json file + * @since 1.25 */ function wfLoadExtension( $ext, $path = null ) { if ( !$path ) { @@ -192,6 +192,7 @@ function wfLoadExtension( $ext, $path = null ) { * * @see wfLoadExtension * @param string[] $exts Array of extension names to load + * @since 1.25 */ function wfLoadExtensions( array $exts ) { global $wgExtensionDirectory; @@ -207,6 +208,7 @@ function wfLoadExtensions( array $exts ) { * @see wfLoadExtension * @param string $skin Name of the extension to load * @param string|null $path Absolute path of where to find the skin.json file + * @since 1.25 */ function wfLoadSkin( $skin, $path = null ) { if ( !$path ) { @@ -221,6 +223,7 @@ function wfLoadSkin( $skin, $path = null ) { * * @see wfLoadExtensions * @param string[] $skins Array of extension names to load + * @since 1.25 */ function wfLoadSkins( array $skins ) { global $wgStyleDirectory; @@ -402,12 +405,17 @@ function wfRandomString( $length = 32 ) { * * ;:@&=$-_.+!*'(), * + * RFC 1738 says ~ is unsafe, however RFC 3986 considers it an unreserved + * character which should not be encoded. More importantly, google chrome + * always converts %7E back to ~, and converting it in this function can + * cause a redirect loop (T105265). + * * But + is not safe because it's used to indicate a space; &= are only safe in * paths and not in queries (and we don't distinguish here); ' seems kind of * scary; and urlencode() doesn't touch -_. to begin with. Plus, although / * is reserved, we don't care. So the list we unescape is: * - * ;:@$!*(),/ + * ;:@$!*(),/~ * * However, IIS7 redirects fail when the url contains a colon (Bug 22709), * so no fancy : for IIS7. @@ -426,7 +434,7 @@ function wfUrlencode( $s ) { } if ( is_null( $needle ) ) { - $needle = array( '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F' ); + $needle = array( '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F', '%7E' ); if ( !isset( $_SERVER['SERVER_SOFTWARE'] ) || ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/7' ) === false ) ) { @@ -437,7 +445,7 @@ function wfUrlencode( $s ) { $s = urlencode( $s ); $s = str_ireplace( $needle, - array( ';', '@', '$', '!', '*', '(', ')', ',', '/', ':' ), + array( ';', '@', '$', '!', '*', '(', ')', ',', '/', '~', ':' ), $s ); @@ -1248,13 +1256,17 @@ function wfLogProfilingData() { $profiler->logData(); $config = $context->getConfig(); - if ( $config->has( 'StatsdServer' ) ) { - $statsdServer = explode( ':', $config->get( 'StatsdServer' ) ); - $statsdHost = $statsdServer[0]; - $statsdPort = isset( $statsdServer[1] ) ? $statsdServer[1] : 8125; - $statsdSender = new SocketSender( $statsdHost, $statsdPort ); - $statsdClient = new StatsdClient( $statsdSender ); - $statsdClient->send( $context->getStats()->getBuffer() ); + if ( $config->get( 'StatsdServer' ) ) { + try { + $statsdServer = explode( ':', $config->get( 'StatsdServer' ) ); + $statsdHost = $statsdServer[0]; + $statsdPort = isset( $statsdServer[1] ) ? $statsdServer[1] : 8125; + $statsdSender = new SocketSender( $statsdHost, $statsdPort ); + $statsdClient = new SamplingStatsdClient( $statsdSender, true, false ); + $statsdClient->send( $context->getStats()->getBuffer() ); + } catch ( Exception $ex ) { + MWExceptionHandler::logException( $ex ); + } } # Profiling must actually be enabled... @@ -1416,7 +1428,7 @@ function wfGetLangObj( $langcode = false ) { * * This function replaces all old wfMsg* functions. * - * @param string|string[] $key Message key, or array of keys + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, or a MessageSpecifier * @param mixed $params,... Normal message parameters * @return Message * @@ -1756,7 +1768,7 @@ function wfMsgExt( $key, $options ) { } if ( in_array( 'escape', $options, true ) ) { - $string = htmlspecialchars ( $string ); + $string = htmlspecialchars( $string ); } elseif ( in_array( 'escapenoentities', $options, true ) ) { $string = Sanitizer::escapeHtmlAllowEntities( $string ); } @@ -2171,14 +2183,24 @@ function wfResetOutputBuffers( $resetGzipEncoding = true ) { $wgDisableOutputCompression = true; } while ( $status = ob_get_status() ) { - if ( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) { - // Probably from zlib.output_compression or other - // PHP-internal setting which can't be removed. - // + if ( isset( $status['flags'] ) ) { + $flags = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_REMOVABLE; + $deleteable = ( $status['flags'] & $flags ) === $flags; + } elseif ( isset( $status['del'] ) ) { + $deleteable = $status['del']; + } else { + // Guess that any PHP-internal setting can't be removed. + $deleteable = $status['type'] !== 0; /* PHP_OUTPUT_HANDLER_INTERNAL */ + } + if ( !$deleteable ) { // Give up, and hope the result doesn't break // output behavior. break; } + if ( $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier' ) { + // Unit testing barrier to prevent this function from breaking PHPUnit. + break; + } if ( !ob_end_clean() ) { // Could not remove output buffer handler; abort now // to avoid getting in some kind of infinite loop. @@ -2758,7 +2780,7 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $useLogPipe = false; if ( is_executable( '/bin/bash' ) ) { - $time = intval ( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); + $time = intval( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); if ( isset( $limits['walltime'] ) ) { $wallTime = intval( $limits['walltime'] ); } elseif ( isset( $limits['time'] ) ) { @@ -2766,8 +2788,8 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), } else { $wallTime = intval( $wgMaxShellWallClockTime ); } - $mem = intval ( isset( $limits['memory'] ) ? $limits['memory'] : $wgMaxShellMemory ); - $filesize = intval ( isset( $limits['filesize'] ) ? $limits['filesize'] : $wgMaxShellFileSize ); + $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" ) . ' ' . @@ -3453,15 +3475,17 @@ function wfResetSessionID() { * @param bool $sessionId */ function wfSetupSession( $sessionId = false ) { - global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgCookiePath, $wgCookieDomain, - $wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler; - if ( $wgSessionsInObjectCache || $wgSessionsInMemcached ) { + global $wgSessionsInObjectCache, $wgSessionHandler; + global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly; + + if ( $wgSessionsInObjectCache ) { ObjectCacheSessionHandler::install(); } elseif ( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) { # Only set this if $wgSessionHandler isn't null and session.save_handler # hasn't already been set to the desired value (that causes errors) ini_set( 'session.save_handler', $wgSessionHandler ); } + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly ); session_cache_limiter( 'private, must-revalidate' ); @@ -3470,9 +3494,14 @@ function wfSetupSession( $sessionId = false ) { } else { wfFixSessionID(); } + MediaWiki\suppressWarnings(); session_start(); MediaWiki\restoreWarnings(); + + if ( $wgSessionsInObjectCache ) { + ObjectCacheSessionHandler::renewCurrentSession(); + } } /** @@ -3495,7 +3524,7 @@ function wfGetPrecompiledData( $name ) { } /** - * Get a cache key + * Make a cache key for the local wiki. * * @param string $args,... * @return string @@ -3505,12 +3534,13 @@ function wfMemcKey( /*...*/ ) { $prefix = $wgCachePrefix === false ? wfWikiID() : $wgCachePrefix; $args = func_get_args(); $key = $prefix . ':' . implode( ':', $args ); - $key = str_replace( ' ', '_', $key ); - return $key; + return strtr( $key, ' ', '_' ); } /** - * Get a cache key for a foreign DB + * Make a cache key for a foreign DB. + * + * Must match what wfMemcKey() would produce in context of the foreign wiki. * * @param string $db * @param string $prefix @@ -3520,11 +3550,29 @@ function wfMemcKey( /*...*/ ) { function wfForeignMemcKey( $db, $prefix /*...*/ ) { $args = array_slice( func_get_args(), 2 ); if ( $prefix ) { + // Match wfWikiID() logic $key = "$db-$prefix:" . implode( ':', $args ); } else { $key = $db . ':' . implode( ':', $args ); } - return str_replace( ' ', '_', $key ); + return strtr( $key, ' ', '_' ); +} + +/** + * Make a cache key with database-agnostic prefix. + * + * Doesn't have a wiki-specific namespace. Uses a generic 'global' prefix + * instead. Must have a prefix as otherwise keys that use a database name + * in the first segment will clash with wfMemcKey/wfForeignMemcKey. + * + * @since 1.26 + * @param string $args,... + * @return string + */ +function wfGlobalCacheKey( /*...*/ ) { + $args = func_get_args(); + $key = 'global:' . implode( ':', $args ); + return strtr( $key, ' ', '_' ); } /** @@ -3814,9 +3862,9 @@ function wfStripIllegalFilenameChars( $name ) { } /** - * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit; + * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit * - * @return int Value the memory limit was set to. + * @return int Resulting value of the memory limit. */ function wfMemoryLimit() { global $wgMemoryLimit; @@ -3840,6 +3888,26 @@ function wfMemoryLimit() { return $memlimit; } +/** + * Set PHP's time limit to the larger of php.ini or $wgTransactionalTimeLimit + * + * @return int Prior time limit + * @since 1.26 + */ +function wfTransactionalTimeLimit() { + global $wgTransactionalTimeLimit; + + $timeLimit = ini_get( 'max_execution_time' ); + // Note that CLI scripts use 0 + if ( $timeLimit > 0 && $wgTransactionalTimeLimit > $timeLimit ) { + set_time_limit( $wgTransactionalTimeLimit ); + } + + ignore_user_abort( true ); // ignore client disconnects + + return $timeLimit; +} + /** * Converts shorthand byte notation to integer form * @@ -3900,13 +3968,13 @@ function wfBCP47( $code ) { } /** - * Get a cache object. + * Get a specific cache object. * - * @param int $inputType Cache type, one of the CACHE_* constants. + * @param int|string $cacheType A CACHE_* constants, or other key in $wgObjectCaches * @return BagOStuff */ -function wfGetCache( $inputType ) { - return ObjectCache::getInstance( $inputType ); +function wfGetCache( $cacheType ) { + return ObjectCache::getInstance( $cacheType ); } /** @@ -4219,3 +4287,28 @@ function wfThumbIsStandard( File $file, array $params ) { return true; } + +/** + * Merges two (possibly) 2 dimensional arrays into the target array ($baseArray). + * + * Values that exist in both values will be combined with += (all values of the array + * of $newValues will be added to the values of the array of $baseArray, while values, + * that exists in both, the value of $baseArray will be used). + * + * @param array $baseArray The array where you want to add the values of $newValues to + * @param array $newValues An array with new values + * @return array The combined array + * @since 1.26 + */ +function wfArrayPlus2d( array $baseArray, array $newValues ) { + // First merge items that are in both arrays + foreach ( $baseArray as $name => &$groupVal ) { + if ( isset( $newValues[$name] ) ) { + $groupVal += $newValues[$name]; + } + } + // Now add items that didn't exist yet + $baseArray += $newValues; + + return $baseArray; +} diff --git a/includes/Hooks.php b/includes/Hooks.php index 036d65c71e..a414562436 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -231,22 +231,25 @@ class Hooks { } /** - * Handle PHP errors issued inside a hook. Catch errors that have to do with - * a function expecting a reference, and let all others pass through. - * - * This REALLY should be protected... but it's public for compatibility + * Handle PHP errors issued inside a hook. Catch errors that have to do + * with a function expecting a reference, and pass all others through to + * MWExceptionHandler::handleError() for default processing. * * @since 1.18 * * @param int $errno Error number (unused) * @param string $errstr Error message * @throws MWHookException If the error has to do with the function signature - * @return bool Always returns false + * @return bool */ public static function hookErrorHandler( $errno, $errstr ) { if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) { throw new MWHookException( $errstr, $errno ); } - return false; + + // Delegate unhandled errors to the default MW handler + return call_user_func_array( + 'MWExceptionHandler::handleError', func_get_args() + ); } } diff --git a/includes/Html.php b/includes/Html.php index 235096ddde..62ae0b8591 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -822,6 +822,47 @@ class Html { return self::element( 'textarea', self::getTextInputAttributes( $attribs ), $spacedValue ); } + /** + * Helper for Html::namespaceSelector(). + * @param array $params See Html::namespaceSelector() + * @return array + */ + public static function namespaceSelectorOptions( array $params = array() ) { + global $wgContLang; + + $options = array(); + + if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { + $params['exclude'] = array(); + } + + if ( isset( $params['all'] ) ) { + // add an option that would let the user select all namespaces. + // Value is provided by user, the name shown is localized for the user. + $options[$params['all']] = wfMessage( 'namespacesall' )->text(); + } + // Add all namespaces as options (in the content language) + $options += $wgContLang->getFormattedNamespaces(); + + $optionsOut = array(); + // Filter out namespaces below 0 and massage labels + foreach ( $options as $nsId => $nsName ) { + if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { + continue; + } + if ( $nsId === NS_MAIN ) { + // For other namespaces use the namespace prefix as label, but for + // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") + $nsName = wfMessage( 'blanknamespace' )->text(); + } elseif ( is_int( $nsId ) ) { + $nsName = $wgContLang->convertNamespace( $nsId ); + } + $optionsOut[ $nsId ] = $nsName; + } + + return $optionsOut; + } + /** * Build a drop-down box for selecting a namespace * @@ -841,8 +882,6 @@ class Html { public static function namespaceSelector( array $params = array(), array $selectAttribs = array() ) { - global $wgContLang; - ksort( $selectAttribs ); // Is a namespace selected? @@ -859,37 +898,16 @@ class Html { $params['selected'] = ''; } - if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { - $params['exclude'] = array(); - } if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) { $params['disable'] = array(); } // Associative array between option-values and option-labels - $options = array(); - - if ( isset( $params['all'] ) ) { - // add an option that would let the user select all namespaces. - // Value is provided by user, the name shown is localized for the user. - $options[$params['all']] = wfMessage( 'namespacesall' )->text(); - } - // Add all namespaces as options (in the content language) - $options += $wgContLang->getFormattedNamespaces(); + $options = self::namespaceSelectorOptions( $params ); - // Convert $options to HTML and filter out namespaces below 0 + // Convert $options to HTML $optionsHtml = array(); foreach ( $options as $nsId => $nsName ) { - if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { - continue; - } - if ( $nsId === NS_MAIN ) { - // For other namespaces use the namespace prefix as label, but for - // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") - $nsName = wfMessage( 'blanknamespace' )->text(); - } elseif ( is_int( $nsId ) ) { - $nsName = $wgContLang->convertNamespace( $nsId ); - } $optionsHtml[] = self::element( 'option', array( 'disabled' => in_array( $nsId, $params['disable'] ), diff --git a/includes/HtmlFormatter.php b/includes/HtmlFormatter.php index b2926d17bc..221cefbb3a 100644 --- a/includes/HtmlFormatter.php +++ b/includes/HtmlFormatter.php @@ -63,7 +63,15 @@ class HtmlFormatter { */ public function getDoc() { if ( !$this->doc ) { - $html = mb_convert_encoding( $this->html, 'HTML-ENTITIES', 'UTF-8' ); + // DOMDocument::loadHTML apparently isn't very good with encodings, so + // convert input to ASCII by encoding everything above 128 as entities. + if ( function_exists( 'mb_convert_encoding' ) ) { + $html = mb_convert_encoding( $this->html, 'HTML-ENTITIES', 'UTF-8' ); + } else { + $html = preg_replace_callback( '/[\x{80}-\x{10ffff}]/u', function ( $m ) { + return '&#' . UtfNormal\Utils::utf8ToCodepoint( $m[0] ) . ';'; + }, $this->html ); + } // Workaround for bug that caused spaces before references // to disappear during processing: @@ -244,7 +252,14 @@ class HtmlFormatter { ) ); } $html = $replacements->replace( $html ); - $html = mb_convert_encoding( $html, 'UTF-8', 'HTML-ENTITIES' ); + + if ( function_exists( 'mb_convert_encoding' ) ) { + // Just in case the conversion in getDoc() above used named + // entities that aren't known to html_entity_decode(). + $html = mb_convert_encoding( $html, 'UTF-8', 'HTML-ENTITIES' ); + } else { + $html = html_entity_decode( $html, ENT_COMPAT, 'utf-8' ); + } return $html; } diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index 825cd064e8..bc5a9570e2 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -25,6 +25,8 @@ * @defgroup HTTP HTTP */ +use MediaWiki\Logger\LoggerFactory; + /** * Various HTTP related functions * @ingroup HTTP @@ -73,11 +75,14 @@ class Http { $req = MWHttpRequest::factory( $url, $options, $caller ); $status = $req->execute(); - $content = false; if ( $status->isOK() ) { - $content = $req->getContent(); + return $req->getContent(); + } else { + $errors = $status->getErrorsByType( 'error' ); + $logger = LoggerFactory::getInstance( 'http' ); + $logger->warning( $status->getWikiText(), array( 'caller' => $caller ) ); + return false; } - return $content; } /** @@ -252,7 +257,7 @@ class MWHttpRequest { $this->parsedUrl = wfParseUrl( $this->url ); if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { - $this->status = Status::newFatal( 'http-invalid-url' ); + $this->status = Status::newFatal( 'http-invalid-url', $url ); } else { $this->status = Status::newGood( 100 ); // continue } @@ -850,6 +855,8 @@ class CurlHttpRequest extends MWHttpRequest { class PhpHttpRequest extends MWHttpRequest { + private $fopenErrors = array(); + /** * @param string $url * @return string @@ -860,6 +867,60 @@ class PhpHttpRequest extends MWHttpRequest { return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port']; } + /** + * Returns an array with a 'capath' or 'cafile' key that is suitable to be merged into the 'ssl' sub-array of a + * stream context options array. Uses the 'caInfo' option of the class if it is provided, otherwise uses the system + * default CA bundle if PHP supports that, or searches a few standard locations. + * @return array + * @throws DomainException + */ + protected function getCertOptions() { + $certOptions = array(); + $certLocations = array(); + if ( $this->caInfo ) { + $certLocations = array( 'manual' => $this->caInfo ); + } elseif ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) { + // Default locations, based on + // https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/ + // PHP 5.5 and older doesn't have any defaults, so we try to guess ourselves. PHP 5.6+ gets the CA location + // from OpenSSL as long as it is not set manually, so we should leave capath/cafile empty there. + $certLocations = array_filter( array( + getenv( 'SSL_CERT_DIR' ), + getenv( 'SSL_CERT_PATH' ), + '/etc/pki/tls/certs/ca-bundle.crt', # Fedora et al + '/etc/ssl/certs', # Debian et al + '/etc/pki/tls/certs/ca-bundle.trust.crt', + '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem', + '/System/Library/OpenSSL', # OSX + ) ); + } + + foreach( $certLocations as $key => $cert ) { + if ( is_dir( $cert ) ) { + $certOptions['capath'] = $cert; + break; + } elseif ( is_file( $cert ) ) { + $certOptions['cafile'] = $cert; + break; + } elseif ( $key === 'manual' ) { + // fail more loudly if a cert path was manually configured and it is not valid + throw new DomainException( "Invalid CA info passed: $cert" ); + } + } + + return $certOptions; + } + + /** + * Custom error handler for dealing with fopen() errors. fopen() tends to fire multiple errors in succession, and the last one + * is completely useless (something like "fopen: failed to open stream") so normal methods of handling errors programmatically + * like get_last_error() don't work. + */ + public function errorHandler( $errno, $errstr ) { + $n = count( $this->fopenErrors ) + 1; + $this->fopenErrors += array( "errno$n" => $errno, "errstr$n" => $errstr ); + } + public function execute() { parent::execute(); @@ -912,16 +973,16 @@ class PhpHttpRequest extends MWHttpRequest { } if ( $this->sslVerifyHost ) { - $options['ssl']['CN_match'] = $this->parsedUrl['host']; + // PHP 5.6.0 deprecates CN_match, in favour of peer_name which + // actually checks SubjectAltName properly. + if ( version_compare( PHP_VERSION, '5.6.0', '>=' ) ) { + $options['ssl']['peer_name'] = $this->parsedUrl['host']; + } else { + $options['ssl']['CN_match'] = $this->parsedUrl['host']; + } } - if ( is_dir( $this->caInfo ) ) { - $options['ssl']['capath'] = $this->caInfo; - } elseif ( is_file( $this->caInfo ) ) { - $options['ssl']['cafile'] = $this->caInfo; - } elseif ( $this->caInfo ) { - throw new MWException( "Invalid CA info passed: {$this->caInfo}" ); - } + $options['ssl'] += $this->getCertOptions(); $context = stream_context_create( $options ); @@ -938,11 +999,25 @@ class PhpHttpRequest extends MWHttpRequest { } do { $reqCount++; - MediaWiki\suppressWarnings(); + $this->fopenErrors = array(); + set_error_handler( array( $this, 'errorHandler' ) ); $fh = fopen( $url, "r", false, $context ); - MediaWiki\restoreWarnings(); + restore_error_handler(); if ( !$fh ) { + // HACK for instant commons. + // If we are contacting (commons|upload).wikimedia.org + // try again with CN_match for en.wikipedia.org + // as php does not handle SubjectAltName properly + // prior to "peer_name" option in php 5.6 + if ( isset( $options['ssl']['CN_match'] ) + && ( $options['ssl']['CN_match'] === 'commons.wikimedia.org' + || $options['ssl']['CN_match'] === 'upload.wikimedia.org' ) + ) { + $options['ssl']['CN_match'] = 'en.wikipedia.org'; + $context = stream_context_create( $options ); + continue; + } break; } @@ -973,6 +1048,10 @@ class PhpHttpRequest extends MWHttpRequest { $this->setStatus(); if ( $fh === false ) { + if ( $this->fopenErrors ) { + LoggerFactory::getInstance( 'http' )->warning( __CLASS__ + . ': error opening connection: {errstr1}', $this->fopenErrors ); + } $this->status->fatal( 'http-request-error' ); return $this->status; } diff --git a/includes/Import.php b/includes/Import.php index 214bc4e793..6a0bfd093b 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -394,9 +394,9 @@ class WikiImporter { $countKey = 'title_' . $title->getPrefixedText(); $countable = $page->isCountable( $editInfo ); if ( array_key_exists( $countKey, $this->countableCache ) && - $countable != $this->countableCache[ $countKey ] ) { + $countable != $this->countableCache[$countKey] ) { DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( - 'articles' => ( (int)$countable - (int)$this->countableCache[ $countKey ] ) + 'articles' => ( (int)$countable - (int)$this->countableCache[$countKey] ) ) ) ); } } @@ -611,7 +611,7 @@ class WikiImporter { $tag = $this->reader->localName; if ( $tag == 'namespace' ) { - $this->foreignNamespaces[ $this->nodeAttribute( 'key' ) ] = + $this->foreignNamespaces[$this->nodeAttribute( 'key' )] = $this->nodeContents(); } elseif ( in_array( $tag, $normalFields ) ) { $siteInfo[$tag] = $this->nodeContents(); diff --git a/includes/Linker.php b/includes/Linker.php index 4a1aa872d8..9b5ff27b3d 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -77,7 +77,7 @@ class Linker { wfDeprecated( __METHOD__, '1.25' ); $title = urldecode( $title ); - $title = str_replace( '_', ' ', $title ); + $title = strtr( $title, '_', ' ' ); return self::getLinkAttributesInternal( $title, $class ); } @@ -1276,9 +1276,11 @@ class Linker { * @param string $comment * @param Title|null $title Title object (to generate link to the section in autocomment) or null * @param bool $local Whether section links should refer to local page + * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to. For use with external changes. + * * @return mixed|string */ - public static function formatComment( $comment, $title = null, $local = false ) { + public static function formatComment( $comment, $title = null, $local = false, $wikiId = null ) { # Sanitize text a bit: $comment = str_replace( "\n", " ", $comment ); @@ -1286,8 +1288,8 @@ class Linker { $comment = Sanitizer::escapeHtmlAllowEntities( $comment ); # Render autocomments and make links: - $comment = self::formatAutocomments( $comment, $title, $local ); - $comment = self::formatLinksInComment( $comment, $title, $local ); + $comment = self::formatAutocomments( $comment, $title, $local, $wikiId ); + $comment = self::formatLinksInComment( $comment, $title, $local, $wikiId ); return $comment; } @@ -1304,9 +1306,11 @@ class Linker { * @param string $comment Comment text * @param Title|null $title An optional title object used to links to sections * @param bool $local Whether section links should refer to local page - * @return string Formatted comment + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. + * + * @return string Formatted comment (wikitext) */ - private static function formatAutocomments( $comment, $title = null, $local = false ) { + private static function formatAutocomments( $comment, $title = null, $local = false, $wikiId = null ) { // @todo $append here is something of a hack to preserve the status // quo. Someone who knows more about bidi and such should decide // (1) what sane rendering even *is* for an LTR edit summary on an RTL @@ -1320,7 +1324,7 @@ class Linker { // zero-width assertions optional, so wrap them in a non-capturing // group. '!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!', - function ( $match ) use ( $title, $local, &$append ) { + function ( $match ) use ( $title, $local, $wikiId, &$append ) { global $wgLang; // Ensure all match positions are defined @@ -1330,7 +1334,7 @@ class Linker { $auto = $match[2]; $post = $match[3] !== ''; $comment = null; - Hooks::run( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) ); + Hooks::run( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local, $wikiId ) ); if ( $comment === null ) { $link = ''; if ( $title ) { @@ -1349,9 +1353,7 @@ class Linker { $title->getDBkey(), $section ); } if ( $sectionTitle ) { - $link = Linker::link( $sectionTitle, - $wgLang->getArrow(), array(), array(), - 'noclasses' ); + $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' ); } else { $link = ''; } @@ -1384,7 +1386,7 @@ class Linker { * @param string $comment Text to format links in * @param Title|null $title An optional title object used to links to sections * @param bool $local Whether section links should refer to local page - * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. * * @return string */ @@ -1414,10 +1416,9 @@ class Linker { # fix up urlencoded title texts (copied from Parser::replaceInternalLinks) if ( strpos( $match[1], '%' ) !== false ) { - $match[1] = str_replace( - array( '<', '>' ), - array( '<', '>' ), - rawurldecode( $match[1] ) + $match[1] = strtr( + rawurldecode( $match[1] ), + array( '<' => '<', '>' => '>' ) ); } @@ -1460,22 +1461,9 @@ class Linker { $newTarget = clone ( $title ); $newTarget->setFragment( '#' . $target->getFragment() ); $target = $newTarget; - - } - - if ( $wikiId !== null ) { - $thelink = Linker::makeExternalLink( - WikiMap::getForeignURL( $wikiId, $target->getFullText() ), - $linkText . $inside, - /* escape = */ false // Already escaped - ) . $trail; - } else { - $thelink = Linker::link( - $target, - $linkText . $inside - ) . $trail; } + $thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail; } } if ( $thelink ) { @@ -1494,6 +1482,32 @@ class Linker { ); } + /** + * Generates a link to the given Title + * + * @note This is only public for technical reasons. It's not intended for use outside Linker. + * + * @param Title $title + * @param string $text + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. + * @param string|string[] $options See the $options parameter in Linker::link. + * + * @return string HTML link + */ + public static function makeCommentLink( Title $title, $text, $wikiId = null, $options = array() ) { + if ( $wikiId !== null && !$title->isExternal() ) { + $link = Linker::makeExternalLink( + WikiMap::getForeignURL( $wikiId, $title->getPrefixedText(), $title->getFragment() ), + $text, + /* escape = */ false // Already escaped + ); + } else { + $link = Linker::link( $title, $text, array(), array(), $options ); + } + + return $link; + } + /** * @param Title $contextTitle * @param string $target @@ -1580,17 +1594,18 @@ class Linker { * @param string $comment * @param Title|null $title Title object (to generate link to section in autocomment) or null * @param bool $local Whether section links should refer to local page + * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to. For use with external changes. * * @return string */ - public static function commentBlock( $comment, $title = null, $local = false ) { + public static function commentBlock( $comment, $title = null, $local = false, $wikiId = null ) { // '*' used to be the comment inserted by the software way back // in antiquity in case none was provided, here for backwards // compatibility, acc. to brion -ævar if ( $comment == '' || $comment == '*' ) { return ''; } else { - $formatted = self::formatComment( $comment, $title, $local ); + $formatted = self::formatComment( $comment, $title, $local, $wikiId ); $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped(); return " $formatted"; } @@ -1705,8 +1720,7 @@ class Linker { } /** - * Generate a table of contents from a section tree - * Currently unused. + * Generate a table of contents from a section tree. * * @param array $tree Return value of ParserOutput::getSections() * @param string|Language|bool $lang Language for the toc title, defaults to user language @@ -2384,6 +2398,7 @@ class Linker { 'title' => $tooltip ) ); } + } /** diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php index bd68551453..8ca205ab42 100644 --- a/includes/MWNamespace.php +++ b/includes/MWNamespace.php @@ -210,6 +210,8 @@ class MWNamespace { if ( $namespaces === null || $rebuild ) { global $wgExtraNamespaces, $wgCanonicalNamespaceNames; $namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames; + // Add extension namespaces + $namespaces += ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' ); if ( is_array( $wgExtraNamespaces ) ) { $namespaces += $wgExtraNamespaces; } @@ -296,6 +298,18 @@ class MWNamespace { return $index == NS_MAIN || in_array( $index, $wgContentNamespaces ); } + /** + * Might pages in this namespace require the use of the Signature button on + * the edit toolbar? + * + * @param int $index Index to check + * @return bool + */ + public static function wantSignatures( $index ) { + global $wgExtraSignatureNamespaces; + return self::isTalk( $index ) || in_array( $index, $wgExtraSignatureNamespaces ); + } + /** * Can pages in a namespace be watched? * diff --git a/includes/MWTimestamp.php b/includes/MWTimestamp.php index f2bd6ba569..d28f88e504 100644 --- a/includes/MWTimestamp.php +++ b/includes/MWTimestamp.php @@ -56,7 +56,7 @@ class MWTimestamp { * * @since 1.20 * - * @param bool|string $timestamp Timestamp to set, or false for current time + * @param bool|string|int|float $timestamp Timestamp to set, or false for current time */ public function __construct( $timestamp = false ) { $this->setTimestamp( $timestamp ); @@ -74,6 +74,7 @@ class MWTimestamp { * @throws TimestampException */ public function setTimestamp( $ts = false ) { + $m = array(); $da = array(); $strtime = ''; @@ -87,9 +88,9 @@ class MWTimestamp { # TS_EXIF } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) { # TS_MW - } elseif ( preg_match( '/^-?\d{1,13}$/D', $ts ) ) { + } elseif ( preg_match( '/^(-?\d{1,13})(\.\d+)?$/D', $ts, $m ) ) { # TS_UNIX - $strtime = "@$ts"; // http://php.net/manual/en/datetime.formats.compound.php + $strtime = "@{$m[1]}"; // http://php.net/manual/en/datetime.formats.compound.php } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) { # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6 $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 186821de39..2c7ba91bf2 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -718,9 +718,6 @@ class MagicWordArray { private $regex; - /** @todo Unused? */ - private $matches; - /** * @param array $names */ @@ -953,10 +950,12 @@ class MagicWordArray { if ( $regex === '' ) { continue; } - preg_match_all( $regex, $text, $matches, PREG_SET_ORDER ); - foreach ( $matches as $m ) { - list( $name, $param ) = $this->parseMatch( $m ); - $found[$name] = $param; + $matches = array(); + if ( preg_match_all( $regex, $text, $matches, PREG_SET_ORDER ) ) { + foreach ( $matches as $m ) { + list( $name, $param ) = $this->parseMatch( $m ); + $found[$name] = $param; + } } $text = preg_replace( $regex, '', $text ); } diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 7a0d7b7ce3..fbacb2504e 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -239,63 +239,109 @@ class MediaWiki { } throw new BadTitleError(); } - // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant - } elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted() - && ( $request->getVal( 'title' ) === null - || $title->getPrefixedDBkey() != $request->getVal( 'title' ) ) - && !count( $request->getValueNames( array( 'action', 'title' ) ) ) - && Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) ) - ) { - if ( $title->isSpecialPage() ) { - list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); - if ( $name ) { - $title = SpecialPage::getTitleFor( $name, $subpage ); - } - } - $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); - // Redirect to canonical url, make it a 301 to allow caching - if ( $targetUrl == $request->getFullRequestURL() ) { - $message = "Redirect loop detected!\n\n" . - "This means the wiki got confused about what page was " . - "requested; this sometimes happens when moving a wiki " . - "to a new server or changing the server configuration.\n\n"; - - if ( $this->config->get( 'UsePathInfo' ) ) { - $message .= "The wiki is trying to interpret the page " . - "title from the URL path portion (PATH_INFO), which " . - "sometimes fails depending on the web server. Try " . - "setting \"\$wgUsePathInfo = false;\" in your " . - "LocalSettings.php, or check that \$wgArticlePath " . - "is correct."; + // Handle any other redirects. + // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant + } elseif ( !$this->tryNormaliseRedirect( $title ) ) { + + // Special pages + if ( NS_SPECIAL == $title->getNamespace() ) { + // Actions that need to be made when we have a special pages + SpecialPageFactory::executePath( $title, $this->context ); + } else { + // ...otherwise treat it as an article view. The article + // may still be a wikipage redirect to another article or URL. + $article = $this->initializeArticle(); + if ( is_object( $article ) ) { + $this->performAction( $article, $requestTitle ); + } elseif ( is_string( $article ) ) { + $output->redirect( $article ); } else { - $message .= "Your web server was detected as possibly not " . - "supporting URL path components (PATH_INFO) correctly; " . - "check your LocalSettings.php for a customized " . - "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . - "to true."; + throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()" + . " returned neither an object nor a URL" ); } - throw new HttpError( 500, $message ); - } else { - $output->setSquidMaxage( 1200 ); - $output->redirect( $targetUrl, '301' ); } - // Special pages - } elseif ( NS_SPECIAL == $title->getNamespace() ) { - // Actions that need to be made when we have a special pages - SpecialPageFactory::executePath( $title, $this->context ); - } else { - // ...otherwise treat it as an article view. The article - // may be a redirect to another article or URL. - $article = $this->initializeArticle(); - if ( is_object( $article ) ) { - $this->performAction( $article, $requestTitle ); - } elseif ( is_string( $article ) ) { - $output->redirect( $article ); + } + } + + /** + * Handle redirects for uncanonical title requests. + * + * Handles: + * - Redirect loops. + * - No title in URL. + * - $wgUsePathInfo URLs. + * - URLs with a variant. + * - Other non-standard URLs (as long as they have no extra query parameters). + * + * Behaviour: + * - Normalise title values: + * /wiki/Foo%20Bar -> /wiki/Foo_Bar + * - Normalise empty title: + * /wiki/ -> /wiki/Main + * /w/index.php?title= -> /wiki/Main + * - Normalise non-standard title urls: + * /w/index.php?title=Foo_Bar -> /wiki/Foo_Bar + * - Don't redirect anything with query parameters other than 'title' or 'action=view'. + * + * @param Title $title + * @return bool True if a redirect was set. + * @throws HttpError + */ + private function tryNormaliseRedirect( Title $title ) { + $request = $this->context->getRequest(); + $output = $this->context->getOutput(); + + if ( $request->getVal( 'action', 'view' ) != 'view' + || $request->wasPosted() + || count( $request->getValueNames( array( 'action', 'title' ) ) ) + || !Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) ) + ) { + return false; + } + + if ( $title->isSpecialPage() ) { + list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); + if ( $name ) { + $title = SpecialPage::getTitleFor( $name, $subpage ); + } + } + // Redirect to canonical url, make it a 301 to allow caching + $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); + + if ( $targetUrl != $request->getFullRequestURL() ) { + $output->setSquidMaxage( 1200 ); + $output->redirect( $targetUrl, '301' ); + return true; + } + + // If there is no title, or the title is in a non-standard encoding, we demand + // a redirect. If cgi somehow changed the 'title' query to be non-standard while + // the url is standard, the server is misconfigured. + if ( $request->getVal( 'title' ) === null + || $title->getPrefixedDBkey() != $request->getVal( 'title' ) + ) { + $message = "Redirect loop detected!\n\n" . + "This means the wiki got confused about what page was " . + "requested; this sometimes happens when moving a wiki " . + "to a new server or changing the server configuration.\n\n"; + + if ( $this->config->get( 'UsePathInfo' ) ) { + $message .= "The wiki is trying to interpret the page " . + "title from the URL path portion (PATH_INFO), which " . + "sometimes fails depending on the web server. Try " . + "setting \"\$wgUsePathInfo = false;\" in your " . + "LocalSettings.php, or check that \$wgArticlePath " . + "is correct."; } else { - throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()" - . " returned neither an object nor a URL" ); + $message .= "Your web server was detected as possibly not " . + "supporting URL path components (PATH_INFO) correctly; " . + "check your LocalSettings.php for a customized " . + "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . + "to true."; } + throw new HttpError( 500, $message ); } + return false; } /** @@ -319,9 +365,8 @@ class MediaWiki { $this->context->setWikiPage( $article->getPage() ); } - // NS_MEDIAWIKI has no redirects. - // It is also used for CSS/JS, so performance matters here... - if ( $title->getNamespace() == NS_MEDIAWIKI ) { + // Skip some unnecessary code if the content model doesn't support redirects + if ( !ContentHandler::getForTitle( $title )->supportsRedirects() ) { return $article; } @@ -452,36 +497,39 @@ class MediaWiki { public function doPreOutputCommit() { // Either all DBs should commit or none ignore_user_abort( true ); - wfGetLBFactory()->commitMasterChanges(); + + // Commit all changes and record ChronologyProtector positions + $factory = wfGetLBFactory(); + $factory->commitMasterChanges(); + $factory->shutdown(); + + wfDebug( __METHOD__ . ' completed; all transactions committed' ); } /** * This function does work that can be done *after* the * user gets the HTTP response so they don't block on it * + * This manages deferred updates, job insertion, + * final commit, and the logging of profiling data + * * @param string $mode Use 'fast' to always skip job running * @since 1.26 */ public function doPostOutputShutdown( $mode = 'normal' ) { - // Show profiling data if enabled + // Show visible profiling data if enabled (which cannot be post-send) Profiler::instance()->logDataPageOutputOnly(); $that = $this; $callback = function () use ( $that, $mode ) { try { - // Assure deferred updates are not in the main transaction - wfGetLBFactory()->commitMasterChanges(); - // Run jobs occasionally, if enabled - if ( $mode === 'normal' ) { - $that->triggerJobs(); - } - // Do deferred updates and job insertion and final commit - $that->restInPeace(); + $that->restInPeace( $mode ); } catch ( Exception $e ) { MWExceptionHandler::handleException( $e ); } }; + // Defer everything else... if ( function_exists( 'register_postsend_function' ) ) { // https://github.com/facebook/hhvm/issues/1230 register_postsend_function( $callback ); @@ -644,8 +692,12 @@ class MediaWiki { /** * Ends this task peacefully + * @param string $mode Use 'fast' to always skip job running */ - public function restInPeace() { + public function restInPeace( $mode = 'fast' ) { + // Assure deferred updates are not in the main transaction + wfGetLBFactory()->commitMasterChanges(); + // Ignore things like master queries/connections on GET requests // as long as they are in deferred updates (which catch errors). Profiler::instance()->getTransactionProfiler()->resetExpectations(); @@ -656,6 +708,12 @@ class MediaWiki { // Make sure any lazy jobs are pushed JobQueueGroup::pushLazyJobs(); + // Now that everything specific to this request is done, + // try to occasionally run jobs (if enabled) from the queues + if ( $mode === 'normal' ) { + $this->triggerJobs(); + } + // Log profiling data, e.g. in the database or UDP wfLogProfilingData(); diff --git a/includes/Message.php b/includes/Message.php index 329d97afac..54abfd151b 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -156,7 +156,7 @@ * * @since 1.17 */ -class Message implements MessageSpecifier { +class Message implements MessageSpecifier, Serializable { /** * In which language to get this message. True, which is the default, @@ -226,8 +226,9 @@ class Message implements MessageSpecifier { /** * @since 1.17 * - * @param string|string[] $key Message key or array of message keys to try and use the first - * non-empty message for. + * @param string|string[]|MessageSpecifier $key Message key, or array of + * message keys to try and use the first non-empty message for, or a + * MessageSpecifier to copy from. * @param array $params Message parameters. * @param Language $language Optional language of the message, defaults to $wgLang. * @@ -236,6 +237,16 @@ class Message implements MessageSpecifier { public function __construct( $key, $params = array(), Language $language = null ) { global $wgLang; + if ( $key instanceof MessageSpecifier ) { + if ( $params ) { + throw new InvalidArgumentException( + '$params must be empty if $key is a MessageSpecifier' + ); + } + $params = $key->getParams(); + $key = $key->getKey(); + } + if ( !is_string( $key ) && !is_array( $key ) ) { throw new InvalidArgumentException( '$key must be a string or an array' ); } @@ -252,6 +263,41 @@ class Message implements MessageSpecifier { $this->language = $language ?: $wgLang; } + /** + * @see Serializable::serialize() + * @since 1.26 + * @return string + */ + public function serialize() { + return serialize( array( + 'interface' => $this->interface, + 'language' => $this->language->getCode(), + 'key' => $this->key, + 'keysToTry' => $this->keysToTry, + 'parameters' => $this->parameters, + 'format' => $this->format, + 'useDatabase' => $this->useDatabase, + 'title' => $this->title, + ) ); + } + + /** + * @see Serializable::unserialize() + * @since 1.26 + * @param string $serialized + */ + public function unserialize( $serialized ) { + $data = unserialize( $serialized ); + $this->interface = $data['interface']; + $this->key = $data['key']; + $this->keysToTry = $data['keysToTry']; + $this->parameters = $data['parameters']; + $this->format = $data['format']; + $this->useDatabase = $data['useDatabase']; + $this->language = Language::factory( $data['language'] ); + $this->title = $data['title']; + } + /** * @since 1.24 * @@ -327,7 +373,7 @@ class Message implements MessageSpecifier { * * @since 1.17 * - * @param string|string[] $key Message key or array of keys. + * @param string|string[]|MessageSpecifier $key * @param mixed $param,... Parameters as strings. * * @return Message diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 0d907b7e31..2b240c3b02 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -617,12 +617,14 @@ class MimeMagic { /** * Guess the MIME type from the file contents. * + * @todo Remove $ext param + * * @param string $file * @param mixed $ext * @return bool|string * @throws MWException */ - private function doGuessMimeType( $file, $ext ) { // TODO: remove $ext param + private function doGuessMimeType( $file, $ext ) { // Read a chunk of the file MediaWiki\suppressWarnings(); $f = fopen( $file, 'rb' ); @@ -693,7 +695,7 @@ class MimeMagic { } /* Look for WebP */ - if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 8 ), "WEBPVP8 ", 8 ) == 0 ) { + if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 7 ), "WEBPVP8", 7 ) == 0 ) { wfDebug( __METHOD__ . ": recognized file as image/webp\n" ); return "image/webp"; } diff --git a/includes/MovePage.php b/includes/MovePage.php index 9891106022..2cd9698c0f 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -305,8 +305,8 @@ class MovePage { __METHOD__, array( 'IGNORE' ) ); - # Update the protection log - $log = new LogPage( 'protect' ); + + // Build comment for log $comment = wfMessage( 'prot_1movedto2', $this->oldTitle->getPrefixedText(), @@ -315,14 +315,6 @@ class MovePage { if ( $reason ) { $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } - // @todo FIXME: $params? - $logId = $log->addEntry( - 'move_prot', - $this->newTitle, - $comment, - array( $this->oldTitle->getPrefixedText() ), - $user - ); // reread inserted pr_ids for log relation $insertedPrIds = $dbw->select( @@ -335,7 +327,18 @@ class MovePage { foreach ( $insertedPrIds as $prid ) { $logRelationsValues[] = $prid->pr_id; } - $log->addRelations( 'pr_id', $logRelationsValues, $logId ); + + // Update the protection log + $logEntry = new ManualLogEntry( 'protect', 'move_prot' ); + $logEntry->setTarget( $this->newTitle ); + $logEntry->setComment( $comment ); + $logEntry->setPerformer( $user ); + $logEntry->setParameters( array( + '4::oldtitle' => $this->oldTitle->getPrefixedText(), + ) ); + $logEntry->setRelations( array( 'pr_id' => $logRelationsValues ) ); + $logId = $logEntry->insert(); + $logEntry->publish( $logId ); } // Update *_from_namespace fields as needed @@ -416,6 +419,13 @@ class MovePage { $redirectContent = null; } + // Figure out whether the content model is no longer the default + $oldDefault = ContentHandler::getDefaultModelFor( $this->oldTitle ); + $contentModel = $this->oldTitle->getContentModel(); + $newDefault = ContentHandler::getDefaultModelFor( $nt ); + $defaultContentModelChanging = ( $oldDefault !== $newDefault + && $oldDefault === $contentModel ); + // bug 57084: log_page should be the ID of the *moved* page $oldid = $this->oldTitle->getArticleID(); $logTitle = clone $this->oldTitle; @@ -493,6 +503,16 @@ class MovePage { $newpage->doEditUpdates( $nullRevision, $user, array( 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ) ); + // If the default content model changes, we need to populate rev_content_model + if ( $defaultContentModelChanging ) { + $dbw->update( + 'revision', + array( 'rev_content_model' => $contentModel ), + array( 'rev_page' => $nt->getArticleID(), 'rev_content_model IS NULL' ), + __METHOD__ + ); + } + if ( !$moveOverRedirect ) { WikiPage::onArticleCreate( $nt ); } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index b3720a4c03..552e181553 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -21,6 +21,7 @@ */ use MediaWiki\Logger\LoggerFactory; +use WrappedString\WrappedString; /** * This class should be covered by a general architecture document which does @@ -141,9 +142,6 @@ class OutputPage extends ContextSource { /** @var string Inline CSS styles. Use addInlineStyle() sparingly */ protected $mInlineStyles = ''; - /** @todo Unused? */ - private $mLinkColours; - /** * @var string Used by skin template. * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle ); @@ -617,7 +615,8 @@ class OutputPage extends ContextSource { $module = $resourceLoader->getModule( $val ); if ( $module instanceof ResourceLoaderModule && $module->isPositionDefault() ) { - $warning = __METHOD__ . ': style module should define its position explicitly: ' . $val . ' ' . get_class( $module ); + $warning = __METHOD__ . ': style module should define its position explicitly: ' . + $val . ' ' . get_class( $module ); wfDebugLog( 'resourceloader', $warning ); wfLogWarning( $warning ); } @@ -1816,6 +1815,11 @@ class OutputPage extends ContextSource { } } + // enable OOUI if requested via ParserOutput + if ( $parserOutput->getEnableOOUI() ) { + $this->enableOOUI(); + } + // Link flags are ignored for now, but may in the future be // used to mark individual language links. $linkFlags = array(); @@ -2005,21 +2009,20 @@ class OutputPage extends ContextSource { * Add an HTTP header that will influence on the cache * * @param string $header Header name - * @param array|null $option - * @todo FIXME: Document the $option parameter; it appears to be for - * X-Vary-Options but what format is acceptable? + * @param string[]|null $option Options for X-Vary-Options. Possible options are: + * - "string-contains=$XXX" varies on whether the header value as a string + * contains $XXX as a substring. + * - "list-contains=$XXX" varies on whether the header value as a + * comma-separated list contains $XXX as one of the list items. */ - public function addVaryHeader( $header, $option = null ) { + public function addVaryHeader( $header, array $option = null ) { if ( !array_key_exists( $header, $this->mVaryHeader ) ) { - $this->mVaryHeader[$header] = (array)$option; - } elseif ( is_array( $option ) ) { - if ( is_array( $this->mVaryHeader[$header] ) ) { - $this->mVaryHeader[$header] = array_merge( $this->mVaryHeader[$header], $option ); - } else { - $this->mVaryHeader[$header] = $option; - } + $this->mVaryHeader[$header] = array(); } - $this->mVaryHeader[$header] = array_unique( (array)$this->mVaryHeader[$header] ); + if ( !is_array( $option ) ) { + $option = array(); + } + $this->mVaryHeader[$header] = array_unique( array_merge( $this->mVaryHeader[$header], $option ) ); } /** @@ -2281,14 +2284,14 @@ class OutputPage extends ContextSource { if ( $this->mArticleBodyOnly ) { echo $this->mBodytext; } else { - $sk = $this->getSkin(); // add skin specific modules $modules = $sk->getDefaultModules(); - // enforce various default modules for all skins + // Enforce various default modules for all skins $coreModules = array( - // keep this list as small as possible + // Keep this list as small as possible + 'site', 'mediawiki.page.startup', 'mediawiki.user', ); @@ -2695,16 +2698,14 @@ class OutputPage extends ContextSource { } $ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n"; + $ret .= $this->getInlineHeadScripts() . "\n"; + $ret .= $this->buildCssLinks() . "\n"; + $ret .= $this->getExternalHeadScripts() . "\n"; foreach ( $this->getHeadLinksArray() as $item ) { $ret .= $item . "\n"; } - // No newline after buildCssLinks since makeResourceLoaderLink did that already - $ret .= $this->buildCssLinks(); - - $ret .= $this->getHeadScripts() . "\n"; - foreach ( $this->mHeadItems as $item ) { $ret .= $item . "\n"; } @@ -2761,23 +2762,22 @@ class OutputPage extends ContextSource { } /** - * @todo Document + * Construct neccecary html and loader preset states to load modules on a page. + * + * Use getHtmlFromLoaderLinks() to convert this array to HTML. + * * @param array|string $modules One or more module names * @param string $only ResourceLoaderModule TYPE_ class constant - * @param bool $useESI - * @param array $extraQuery Array with extra query parameters to add to each - * request. array( param => value ). - * @param bool $loadCall If true, output an (asynchronous) mw.loader.load() - * call rather than a " tags to put in "". + * + * @return string HTML fragment + */ + function getInlineHeadScripts() { + $links = array(); + + // Client profile classes for . Allows for easy hiding/showing of UI components. + // Must be done synchronously on every page to avoid flashes of wrong content. + // Note: This class distinguishes MediaWiki-supported JavaScript from the rest. + // The "rest" includes browsers that support JavaScript but not supported by our runtime. + // For the performance benefit of the majority, this is added unconditionally here and is + // then fixed up by the startup module for unsupported browsers. + $links[] = Html::inlineScript( + 'document.documentElement.className = document.documentElement.className' + . '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );' + ); // Load config before anything else $links[] = ResourceLoader::makeInlineScript( @@ -3004,12 +3026,6 @@ class OutputPage extends ContextSource { // Separate user.tokens as otherwise caching will be allowed (T84960) $links[] = $this->makeResourceLoaderLink( 'user.tokens', ResourceLoaderModule::TYPE_COMBINED ); - // Scripts and messages "only" requests marked for top inclusion - $links[] = $this->makeResourceLoaderLink( - $this->getModuleScripts( true, 'top' ), - ResourceLoaderModule::TYPE_SCRIPTS - ); - // Modules requests - let the client calculate dependencies and batch requests as it likes // Only load modules that have marked themselves for loading at the top $modules = $this->getModules( true, 'top' ); @@ -3019,37 +3035,35 @@ class OutputPage extends ContextSource { ); } - if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { - $links[] = $this->getScriptsForBottomQueue( true ); - } + // "Scripts only" modules marked for top inclusion + $links[] = $this->makeResourceLoaderLink( + $this->getModuleScripts( true, 'top' ), + ResourceLoaderModule::TYPE_SCRIPTS + ); return self::getHtmlFromLoaderLinks( $links ); } /** - * JS stuff to put at the 'bottom', which can either be the bottom of the - * "" or the bottom of the "" depending on - * $wgResourceLoaderExperimentalAsyncLoading: modules marked with position - * 'bottom', legacy scripts ($this->mScripts), user preferences, site JS - * and user JS. + * JS stuff to put at the 'bottom', which goes at the bottom of the ``. + * These are modules marked with position 'bottom', legacy scripts ($this->mScripts), + * site JS, and user JS. * - * @param bool $inHead If true, this HTML goes into the "", - * if false it goes into the "". + * @param bool $unused Previously used to let this method change its output based + * on whether it was called by getExternalHeadScripts() or getBottomScripts(). * @return string */ - function getScriptsForBottomQueue( $inHead ) { + function getScriptsForBottomQueue( $unused = null ) { // Scripts "only" requests marked for bottom inclusion // If we're in the , use load() calls rather than " + ); } /** @@ -1382,11 +1409,13 @@ MESSAGE; * @return string */ public static function makeConfigSetScript( array $configuration ) { - return Xml::encodeJsCall( - 'mw.config.set', - array( $configuration ), - ResourceLoader::inDebugMode() - ); + if ( ResourceLoader::inDebugMode() ) { + return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), true ); + } + + $config = RequestContext::getMain()->getConfig(); + $js = Xml::encodeJsCall( 'mw.config.set', array( $configuration ), false ); + return self::applyFilter( 'minify-js', $js, $config ); } /** @@ -1448,7 +1477,7 @@ MESSAGE; * @param string $source Name of the ResourceLoader source * @param ResourceLoaderContext $context * @param array $extraQuery - * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative) + * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too. */ public function createLoaderURL( $source, ResourceLoaderContext $context, $extraQuery = array() @@ -1456,9 +1485,7 @@ MESSAGE; $query = self::createLoaderQuery( $context, $extraQuery ); $script = $this->getLoadScript( $source ); - // Prevent the IE6 extension check from being triggered (bug 28840) - // by appending a character that's invalid in Windows extensions ('*') - return wfExpandUrl( wfAppendQuery( $script, $query ) . '&*', PROTO_RELATIVE ); + return wfAppendQuery( $script, $query ); } /** @@ -1474,7 +1501,7 @@ MESSAGE; * @param bool $printable Printable mode * @param bool $handheld Handheld mode * @param array $extraQuery Extra query parameters to add - * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative) + * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too. */ public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null, $printable = false, @@ -1486,9 +1513,7 @@ MESSAGE; $only, $printable, $handheld, $extraQuery ); - // Prevent the IE6 extension check from being triggered (bug 28840) - // by appending a character that's invalid in Windows extensions ('*') - return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE ); + return wfAppendQuery( $wgLoadScript, $query ); } /** @@ -1600,9 +1625,6 @@ MESSAGE; $less->setPreserveComments( true ); $less->setVariables( self::getLessVars( $config ) ); $less->setImportDir( $config->get( 'ResourceLoaderLESSImportPaths' ) ); - foreach ( $config->get( 'ResourceLoaderLESSFunctions' ) as $name => $func ) { - $less->registerFunction( $name, $func ); - } return $less; } diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index cee70350c2..2e1752a6b1 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -59,24 +59,26 @@ class ResourceLoaderContext { $this->resourceLoader = $resourceLoader; $this->request = $request; - // Interpret request // List of modules $modules = $request->getVal( 'modules' ); $this->modules = $modules ? self::expandModuleNames( $modules ) : array(); + // Various parameters - $this->skin = $request->getVal( 'skin' ); $this->user = $request->getVal( 'user' ); $this->debug = $request->getFuzzyBool( - 'debug', $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' ) + 'debug', + $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' ) ); - $this->only = $request->getVal( 'only' ); - $this->version = $request->getVal( 'version' ); + $this->only = $request->getVal( 'only', null ); + $this->version = $request->getVal( 'version', null ); $this->raw = $request->getFuzzyBool( 'raw' ); + // Image requests $this->image = $request->getVal( 'image' ); $this->variant = $request->getVal( 'variant' ); $this->format = $request->getVal( 'format' ); + $this->skin = $request->getVal( 'skin' ); $skinnames = Skin::getSkinNames(); // If no skin is specified, or we don't recognize the skin, use the default skin if ( !$this->skin || !isset( $skinnames[$this->skin] ) ) { @@ -157,7 +159,7 @@ class ResourceLoaderContext { public function getLanguage() { if ( $this->language === null ) { // Must be a valid language code after this point (bug 62849) - $this->language = RequestContext::sanitizeLangCode( $this->request->getVal( 'lang' ) ); + $this->language = RequestContext::sanitizeLangCode( $this->getRequest()->getVal( 'lang' ) ); } return $this->language; } @@ -167,7 +169,7 @@ class ResourceLoaderContext { */ public function getDirection() { if ( $this->direction === null ) { - $this->direction = $this->request->getVal( 'dir' ); + $this->direction = $this->getRequest()->getVal( 'dir' ); if ( !$this->direction ) { // Determine directionality based on user language (bug 6100) $this->direction = Language::factory( $this->getLanguage() )->getDir(); @@ -177,7 +179,7 @@ class ResourceLoaderContext { } /** - * @return string|null + * @return string */ public function getSkin() { return $this->skin; @@ -305,21 +307,21 @@ class ResourceLoaderContext { * @return bool */ public function shouldIncludeScripts() { - return is_null( $this->getOnly() ) || $this->getOnly() === 'scripts'; + return $this->getOnly() === null || $this->getOnly() === 'scripts'; } /** * @return bool */ public function shouldIncludeStyles() { - return is_null( $this->getOnly() ) || $this->getOnly() === 'styles'; + return $this->getOnly() === null || $this->getOnly() === 'styles'; } /** * @return bool */ public function shouldIncludeMessages() { - return is_null( $this->getOnly() ); + return $this->getOnly() === null; } /** diff --git a/includes/resourceloader/ResourceLoaderEditToolbarModule.php b/includes/resourceloader/ResourceLoaderEditToolbarModule.php index d0273c2cfe..f3fae0e607 100644 --- a/includes/resourceloader/ResourceLoaderEditToolbarModule.php +++ b/includes/resourceloader/ResourceLoaderEditToolbarModule.php @@ -65,15 +65,10 @@ class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule { } /** - * @param ResourceLoaderContext $context - * @return array + * @return bool */ - public function getDefinitionSummary( ResourceLoaderContext $context ) { - $summary = parent::getDefinitionSummary( $context ); - $summary[] = array( - 'lessVars' => $this->getLessVars( $context ), - ); - return $summary; + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index e6c9bd0439..112ebc0052 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -152,6 +152,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { */ protected $localFileRefs = array(); + /** + * @var array Place where readStyleFile() tracks file dependencies for non-existent files. + * Used in tests to detect missing dependencies. + */ + protected $missingLocalFileRefs = array(); + /* Methods */ /** @@ -514,19 +520,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } /** - * Helper method to gather file mtimes for getDefinitionSummary. + * Disable module content versioning. + * + * This class uses getDefinitionSummary() instead, to avoid filesystem overhead + * involved with building the full module content inside a startup request. + * + * @return bool + */ + public function enableModuleContentVersion() { + return false; + } + + /** + * Helper method to gather file hashes for getDefinitionSummary. * - * Last modified timestamps are calculated from the highest last modified - * timestamp of this module's constituent files as well as the files it - * depends on. This function is context-sensitive, only performing - * calculations on files relevant to the given language, skin and debug - * mode. + * This function is context-sensitive, only computing hashes of files relevant to the + * given language, skin, etc. * * @see ResourceLoaderModule::getFileDependencies * @param ResourceLoaderContext $context * @return array */ - protected function getFileMtimes( ResourceLoaderContext $context ) { + protected function getFileHashes( ResourceLoaderContext $context ) { $files = array(); // Flatten style files into $files @@ -565,13 +580,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { // entry point Less file we already know about. $files = array_values( array_unique( $files ) ); - // Don't max() because older files are significant. - // While the associated file names are significant, that is already taken care of by the - // definition summary. Avoid creating an array keyed by file path here because those are - // absolute file paths. Including that would needlessly cause global cache invalidation - // when the MediaWiki installation path changes (which is quite common in cases like - // Wikimedia where the installation path reflects the MediaWiki branch name). - return array_map( array( __CLASS__, 'safeFilemtime' ), $files ); + // Don't include keys or file paths here, only the hashes. Including that would needlessly + // cause global cache invalidation when files move or if e.g. the MediaWiki path changes. + // Any significant ordering is already detected by the definition summary. + return array_map( array( __CLASS__, 'safeFileHash' ), $files ); } /** @@ -585,6 +597,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $options = array(); foreach ( array( + // The following properties are omitted because they don't affect the module reponse: + // - localBasePath (Per T104950; Changes when absolute directory name changes. If + // this affects 'scripts' and other file paths, getFileHashes accounts for that.) + // - remoteBasePath (Per T104950) + // - dependencies (provided via startup module) + // - targets + // - group (provided via startup module) + // - position (only used by OutputPage) 'scripts', 'debugScripts', 'loaderScripts', @@ -592,17 +612,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { 'languageScripts', 'skinScripts', 'skinStyles', - 'dependencies', 'messages', - 'targets', 'templates', - 'group', - 'position', 'skipFunction', - // FIXME: localBasePath includes the MediaWiki installation path and - // needlessly causes cache invalidation. - 'localBasePath', - 'remoteBasePath', 'debugRaw', 'raw', ) as $member ) { @@ -611,7 +623,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $summary[] = array( 'options' => $options, - 'fileMtimes' => $this->getFileMTimes( $context ), + 'fileHashes' => $this->getFileHashes( $context ), 'msgBlobMtime' => $this->getMsgBlobMtime( $context->getLanguage() ), ); return $summary; @@ -911,10 +923,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $localDir = dirname( $localPath ); $remoteDir = dirname( $remotePath ); // Get and register local file references - $this->localFileRefs = array_merge( - $this->localFileRefs, - CSSMin::getLocalFileReferences( $style, $localDir ) - ); + $localFileRefs = CSSMin::getAllLocalFileReferences( $style, $localDir ); + foreach ( $localFileRefs as $file ) { + if ( file_exists( $file ) ) { + $this->localFileRefs[] = $file; + } else { + $this->missingLocalFileRefs[] = $file; + } + } return CSSMin::remap( $style, $localDir, $remoteDir, true ); diff --git a/includes/resourceloader/ResourceLoaderForeignApiModule.php b/includes/resourceloader/ResourceLoaderForeignApiModule.php new file mode 100644 index 0000000000..7ed08317eb --- /dev/null +++ b/includes/resourceloader/ResourceLoaderForeignApiModule.php @@ -0,0 +1,33 @@ +dependencies; + Hooks::run( 'ResourceLoaderForeignApiModules', array( &$dependencies, $context ) ); + return $dependencies; + } +} diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php index bf68fdda94..2338c902ba 100644 --- a/includes/resourceloader/ResourceLoaderImage.php +++ b/includes/resourceloader/ResourceLoaderImage.php @@ -54,15 +54,16 @@ class ResourceLoaderImage { $this->variants = $variants; // Expand shorthands: - // array( "en,de,fr" => "foo.svg" ) → array( "en" => "foo.svg", "de" => "foo.svg", "fr" => "foo.svg" ) + // array( "en,de,fr" => "foo.svg" ) + // → array( "en" => "foo.svg", "de" => "foo.svg", "fr" => "foo.svg" ) if ( is_array( $this->descriptor ) && isset( $this->descriptor['lang'] ) ) { foreach ( array_keys( $this->descriptor['lang'] ) as $langList ) { if ( strpos( $langList, ',' ) !== false ) { $this->descriptor['lang'] += array_fill_keys( explode( ',', $langList ), - $this->descriptor['lang'][ $langList ] + $this->descriptor['lang'][$langList] ); - unset( $this->descriptor['lang'][ $langList ] ); + unset( $this->descriptor['lang'][$langList] ); } } } @@ -75,11 +76,15 @@ class ResourceLoaderImage { } ); $extensions = array_unique( $extensions ); if ( count( $extensions ) !== 1 ) { - throw new InvalidArgumentException( "File type for different image files of '$name' not the same" ); + throw new InvalidArgumentException( + "File type for different image files of '$name' not the same" + ); } $ext = $extensions[0]; if ( !isset( self::$fileTypes[$ext] ) ) { - throw new InvalidArgumentException( "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg)" ); + throw new InvalidArgumentException( + "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg)" + ); } $this->extension = $ext; } @@ -121,10 +126,10 @@ class ResourceLoaderImage { $desc = $this->descriptor; if ( is_string( $desc ) ) { return $this->basePath . '/' . $desc; - } elseif ( isset( $desc['lang'][ $context->getLanguage() ] ) ) { - return $this->basePath . '/' . $desc['lang'][ $context->getLanguage() ]; - } elseif ( isset( $desc[ $context->getDirection() ] ) ) { - return $this->basePath . '/' . $desc[ $context->getDirection() ]; + } elseif ( isset( $desc['lang'][$context->getLanguage()] ) ) { + return $this->basePath . '/' . $desc['lang'][$context->getLanguage()]; + } elseif ( isset( $desc[$context->getDirection()] ) ) { + return $this->basePath . '/' . $desc[$context->getDirection()]; } else { return $this->basePath . '/' . $desc['default']; } diff --git a/includes/resourceloader/ResourceLoaderImageModule.php b/includes/resourceloader/ResourceLoaderImageModule.php index 2caca8741e..8de87f2ef5 100644 --- a/includes/resourceloader/ResourceLoaderImageModule.php +++ b/includes/resourceloader/ResourceLoaderImageModule.php @@ -86,9 +86,11 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { * // List of image files and their options * 'images' => array( * [theme name] => array( - * [file path string], - * [file path string] => array( - * 'name' => [image name string, defaults to file name], + * [icon name] => array( + * 'file' => [file path string or array whose values are file path strings + * and whose keys are 'default', 'ltr', 'rtl', a single + * language code like 'en', or a list of language codes like + * 'en,de,ar'], * 'variants' => [array of variant name strings, variants * available for this image], * ), @@ -132,20 +134,30 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $prefix = isset( $options['prefix'] ) && $options['prefix']; $selector = isset( $options['selector'] ) && $options['selector']; - $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] ) && $options['selectorWithoutVariant']; - $selectorWithVariant = isset( $options['selectorWithVariant'] ) && $options['selectorWithVariant']; + $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] ) + && $options['selectorWithoutVariant']; + $selectorWithVariant = isset( $options['selectorWithVariant'] ) + && $options['selectorWithVariant']; if ( $selectorWithoutVariant && !$selectorWithVariant ) { - throw new InvalidArgumentException( "Given 'selectorWithoutVariant' but no 'selectorWithVariant'." ); + throw new InvalidArgumentException( + "Given 'selectorWithoutVariant' but no 'selectorWithVariant'." + ); } if ( $selectorWithVariant && !$selectorWithoutVariant ) { - throw new InvalidArgumentException( "Given 'selectorWithVariant' but no 'selectorWithoutVariant'." ); + throw new InvalidArgumentException( + "Given 'selectorWithVariant' but no 'selectorWithoutVariant'." + ); } if ( $selector && $selectorWithVariant ) { - throw new InvalidArgumentException( "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given." ); + throw new InvalidArgumentException( + "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given." + ); } if ( !$prefix && !$selector && !$selectorWithVariant ) { - throw new InvalidArgumentException( "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given." ); + throw new InvalidArgumentException( + "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given." + ); } foreach ( $options as $member => $option ) { @@ -229,23 +241,23 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $this->loadFromDefinition(); $this->imageObjects = array(); } - if ( !isset( $this->imageObjects[ $skin ] ) ) { - $this->imageObjects[ $skin ] = array(); - if ( !isset( $this->images[ $skin ] ) ) { - $this->images[ $skin ] = isset( $this->images[ 'default' ] ) ? - $this->images[ 'default' ] : + if ( !isset( $this->imageObjects[$skin] ) ) { + $this->imageObjects[$skin] = array(); + if ( !isset( $this->images[$skin] ) ) { + $this->images[$skin] = isset( $this->images['default'] ) ? + $this->images['default'] : array(); } - foreach ( $this->images[ $skin ] as $name => $options ) { + foreach ( $this->images[$skin] as $name => $options ) { $fileDescriptor = is_string( $options ) ? $options : $options['file']; $allowedVariants = array_merge( is_array( $options ) && isset( $options['variants'] ) ? $options['variants'] : array(), $this->getGlobalVariants( $context ) ); - if ( isset( $this->variants[ $skin ] ) ) { + if ( isset( $this->variants[$skin] ) ) { $variantConfig = array_intersect_key( - $this->variants[ $skin ], + $this->variants[$skin], array_fill_keys( $allowedVariants, true ) ); } else { @@ -259,11 +271,11 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $this->localBasePath, $variantConfig ); - $this->imageObjects[ $skin ][ $image->getName() ] = $image; + $this->imageObjects[$skin][$image->getName()] = $image; } } - return $this->imageObjects[ $skin ]; + return $this->imageObjects[$skin]; } /** @@ -278,21 +290,21 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $this->loadFromDefinition(); $this->globalVariants = array(); } - if ( !isset( $this->globalVariants[ $skin ] ) ) { - $this->globalVariants[ $skin ] = array(); - if ( !isset( $this->variants[ $skin ] ) ) { - $this->variants[ $skin ] = isset( $this->variants[ 'default' ] ) ? - $this->variants[ 'default' ] : + if ( !isset( $this->globalVariants[$skin] ) ) { + $this->globalVariants[$skin] = array(); + if ( !isset( $this->variants[$skin] ) ) { + $this->variants[$skin] = isset( $this->variants['default'] ) ? + $this->variants['default'] : array(); } - foreach ( $this->variants[ $skin ] as $name => $config ) { + foreach ( $this->variants[$skin] as $name => $config ) { if ( isset( $config['global'] ) && $config['global'] ) { - $this->globalVariants[ $skin ][] = $name; + $this->globalVariants[$skin][] = $name; } } } - return $this->globalVariants[ $skin ]; + return $this->globalVariants[$skin]; } /** diff --git a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php index b90578120f..f9dfbdc2d4 100644 --- a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php +++ b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php @@ -49,14 +49,18 @@ class ResourceLoaderJqueryMsgModule extends ResourceLoaderFileModule { } /** - * @param ResourceLoaderContext $context - * @return array|null + * @param ResourceLoaderContext $context + * @return array + */ + public function getScriptURLsForDebug( ResourceLoaderContext $context ) { + // Bypass file module urls + return ResourceLoaderModule::getScriptURLsForDebug( $context ); + } + + /** + * @return bool */ - public function getDefinitionSummary( ResourceLoaderContext $context ) { - $summary = parent::getDefinitionSummary( $context ); - $summary[] = array( - 'sanitizerData' => Sanitizer::getRecognizedTagData() - ); - return $summary; + public function enableModuleContentVersion() { + return true; } } diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php index be15008ef9..27c74d7457 100644 --- a/includes/resourceloader/ResourceLoaderLanguageDataModule.php +++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php @@ -63,11 +63,10 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule { } /** - * @param ResourceLoaderContext $context - * @return string Hash + * @return bool */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( serialize( $this->getData( $context ) ) ); + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php index 827a573feb..081c728cb4 100644 --- a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php +++ b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php @@ -32,7 +32,6 @@ class ResourceLoaderLanguageNamesModule extends ResourceLoaderModule { protected $targets = array( 'desktop', 'mobile' ); - /** * @param ResourceLoaderContext $context * @return array @@ -69,11 +68,10 @@ class ResourceLoaderLanguageNamesModule extends ResourceLoaderModule { } /** - * @param ResourceLoaderContext $context - * @return string Hash + * @return bool */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( serialize( $this->getData( $context ) ) ); + public function enableModuleContentVersion() { + return true; } } diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index ec7ed7059d..1243f23131 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -293,9 +293,9 @@ abstract class ResourceLoaderModule { /** * Whether the position returned by getPosition() is a default value or comes from the module - * definition. This method is meant to be short-lived, and is only useful until classes added via - * addModuleStyles with a default value define an explicit position. See getModuleStyles() in - * OutputPage for the related migration warning. + * definition. This method is meant to be short-lived, and is only useful until classes added + * via addModuleStyles with a default value define an explicit position. See getModuleStyles() + * in OutputPage for the related migration warning. * * @return bool * @since 1.26 @@ -386,16 +386,21 @@ abstract class ResourceLoaderModule { } $dbr = wfGetDB( DB_SLAVE ); - $deps = $dbr->selectField( 'module_deps', 'md_deps', array( + $deps = $dbr->selectField( 'module_deps', + 'md_deps', + array( 'md_module' => $this->getName(), 'md_skin' => $skin, - ), __METHOD__ + ), + __METHOD__ ); + if ( !is_null( $deps ) ) { $this->fileDeps[$skin] = (array)FormatJson::decode( $deps, true ); } else { $this->fileDeps[$skin] = array(); } + return $this->fileDeps[$skin]; } @@ -421,10 +426,13 @@ abstract class ResourceLoaderModule { } $dbr = wfGetDB( DB_SLAVE ); - $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array( + $msgBlobMtime = $dbr->selectField( 'msg_resource', + 'mr_timestamp', + array( 'mr_resource' => $this->getName(), 'mr_lang' => $lang - ), __METHOD__ + ), + __METHOD__ ); // If no blob was found, but the module does have messages, that means we need // to regenerate it. Return NOW @@ -458,9 +466,9 @@ abstract class ResourceLoaderModule { // Cache this expensive operation. This calls builds the scripts, styles, and messages // content which typically involves filesystem and/or database access. if ( !array_key_exists( $contextHash, $this->contents ) ) { - $this->contents[ $contextHash ] = $this->buildContent( $context ); + $this->contents[$contextHash] = $this->buildContent( $context ); } - return $this->contents[ $contextHash ]; + return $this->contents[$contextHash]; } /** @@ -472,6 +480,8 @@ abstract class ResourceLoaderModule { */ final protected function buildContent( ResourceLoaderContext $context ) { $rl = $context->getResourceLoader(); + $stats = RequestContext::getMain()->getStats(); + $statStart = microtime( true ); // Only include properties that are relevant to this context (e.g. only=scripts) // and that are non-empty (e.g. don't include "templates" for modules without @@ -527,7 +537,8 @@ abstract class ResourceLoaderModule { $stylePairs[$media] = array(); foreach ( $style as $cssText ) { if ( is_string( $cssText ) ) { - $stylePairs[$media][] = $rl->filter( 'minify-css', $cssText ); + $stylePairs[$media][] = + $rl->filter( 'minify-css', $cssText ); } } } elseif ( is_string( $style ) ) { @@ -559,6 +570,11 @@ abstract class ResourceLoaderModule { $content['templates'] = $templates; } + $statTiming = microtime( true ) - $statStart; + $statName = strtr( $this->getName(), '.', '_' ); + $stats->timing( "resourceloader_build.all", 1000 * $statTiming ); + $stats->timing( "resourceloader_build.$statName", 1000 * $statTiming ); + return $content; } @@ -585,32 +601,61 @@ abstract class ResourceLoaderModule { * @return string Hash (should use ResourceLoader::makeHash) */ public function getVersionHash( ResourceLoaderContext $context ) { + // The startup module produces a manifest with versions representing the entire module. + // Typically, the request for the startup module itself has only=scripts. That must apply + // only to the startup module content, and not to the module version computed here. + $context = new DerivativeResourceLoaderContext( $context ); + $context->setModules( array() ); + // Version hash must cover all resources, regardless of startup request itself. + $context->setOnly( null ); + // Compute version hash based on content, not debug urls. + $context->setDebug( false ); + // Cache this somewhat expensive operation. Especially because some classes // (e.g. startup module) iterate more than once over all modules to get versions. $contextHash = $context->getHash(); if ( !array_key_exists( $contextHash, $this->versionHash ) ) { - $summary = $this->getDefinitionSummary( $context ); - if ( !isset( $summary['_cacheEpoch'] ) ) { - throw new Exception( 'getDefinitionSummary must call parent method' ); - } - $str = json_encode( $summary ); + if ( $this->enableModuleContentVersion() ) { + // Detect changes directly + $str = json_encode( $this->getModuleContent( $context ) ); + } else { + // Infer changes based on definition and other metrics + $summary = $this->getDefinitionSummary( $context ); + if ( !isset( $summary['_cacheEpoch'] ) ) { + throw new LogicException( 'getDefinitionSummary must call parent method' ); + } + $str = json_encode( $summary ); - $mtime = $this->getModifiedTime( $context ); - if ( $mtime !== null ) { - // Support: MediaWiki 1.25 and earlier - $str .= strval( $mtime ); - } + $mtime = $this->getModifiedTime( $context ); + if ( $mtime !== null ) { + // Support: MediaWiki 1.25 and earlier + $str .= strval( $mtime ); + } - $mhash = $this->getModifiedHash( $context ); - if ( $mhash !== null ) { - // Support: MediaWiki 1.25 and earlier - $str .= strval( $mhash ); + $mhash = $this->getModifiedHash( $context ); + if ( $mhash !== null ) { + // Support: MediaWiki 1.25 and earlier + $str .= strval( $mhash ); + } } - $this->versionHash[ $contextHash ] = ResourceLoader::makeHash( $str ); + $this->versionHash[$contextHash] = ResourceLoader::makeHash( $str ); } - return $this->versionHash[ $contextHash ]; + return $this->versionHash[$contextHash]; + } + + /** + * Whether to generate version hash based on module content. + * + * If a module requires database or file system access to build the module + * content, consider disabling this in favour of manually tracking relevant + * aspects in getDefinitionSummary(). See getVersionHash() for how this is used. + * + * @return bool + */ + public function enableModuleContentVersion() { + return false; } /** @@ -748,8 +793,13 @@ abstract class ResourceLoaderModule { protected function validateScriptFile( $fileName, $contents ) { if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) { // Try for cache hit - // Use CACHE_ANYTHING since filtering is very slow compared to DB queries - $key = wfMemcKey( 'resourceloader', 'jsparse', self::$parseCacheVersion, md5( $contents ) ); + // Use CACHE_ANYTHING since parsing JS is much slower than a DB query + $key = wfMemcKey( + 'resourceloader', + 'jsparse', + self::$parseCacheVersion, + md5( $contents ) + ); $cache = wfGetCache( CACHE_ANYTHING ); $cacheEntry = $cache->get( $key ); if ( is_string( $cacheEntry ) ) { @@ -763,7 +813,8 @@ abstract class ResourceLoaderModule { } catch ( Exception $e ) { // We'll save this to cache to avoid having to validate broken JS over and over... $err = $e->getMessage(); - $result = "mw.log.error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");"; + $result = "mw.log.error(" . + Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");"; } $cache->set( $key, $result ); @@ -784,16 +835,30 @@ abstract class ResourceLoaderModule { } /** - * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist - * but returns 1 instead. - * @param string $filename File name + * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist. + * Defaults to 1. + * + * @param string $filePath File path * @return int UNIX timestamp */ - protected static function safeFilemtime( $filename ) { + protected static function safeFilemtime( $filePath ) { MediaWiki\suppressWarnings(); - $mtime = filemtime( $filename ) ?: 1; + $mtime = filemtime( $filePath ) ?: 1; MediaWiki\restoreWarnings(); - return $mtime; } + + /** + * Safe version of sha1_file(), which doesn't throw a PHP warning if the file doesn't exist. + * Defaults to empty string. + * + * @param string $filePath File path + * @return string Hash + */ + protected static function safeFileHash( $filePath ) { + MediaWiki\suppressWarnings(); + $hash = sha1_file( $filePath ) ?: ''; + MediaWiki\restoreWarnings(); + return $hash; + } } diff --git a/includes/resourceloader/ResourceLoaderOOUIImageModule.php b/includes/resourceloader/ResourceLoaderOOUIImageModule.php index ebbeb0153e..8493f9fdc3 100644 --- a/includes/resourceloader/ResourceLoaderOOUIImageModule.php +++ b/includes/resourceloader/ResourceLoaderOOUIImageModule.php @@ -50,7 +50,7 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule { array_walk( $data['images'], function ( &$value ) use ( $fixPath ) { if ( is_string( $value['file'] ) ) { $fixPath( $value['file'] ); - } else if ( is_array( $value['file'] ) ) { + } elseif ( is_array( $value['file'] ) ) { array_walk_recursive( $value['file'], $fixPath ); } } ); @@ -69,7 +69,9 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule { if ( !isset( $definition[$key] ) ) { $definition[$key] = $data[$key]; } elseif ( $definition[$key] !== $data[$key] ) { - throw new Exception( "Mismatched OOUI theme definitions are not supported: trying to load $key of $theme theme" ); + throw new Exception( + "Mismatched OOUI theme definitions are not supported: trying to load $key of $theme theme" + ); } break; } diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php index 19e0baeb5e..380b7a53f3 100644 --- a/includes/resourceloader/ResourceLoaderSiteModule.php +++ b/includes/resourceloader/ResourceLoaderSiteModule.php @@ -47,13 +47,4 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule { } return $pages; } - - /** - * Get group name - * - * @return string - */ - public function getGroup() { - return 'site'; - } } diff --git a/includes/resourceloader/ResourceLoaderSkinModule.php b/includes/resourceloader/ResourceLoaderSkinModule.php index 980b7fed1e..911d953415 100644 --- a/includes/resourceloader/ResourceLoaderSkinModule.php +++ b/includes/resourceloader/ResourceLoaderSkinModule.php @@ -44,13 +44,13 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule { '(min-resolution: 1.5dppx), ' . '(min-resolution: 144dpi)' ][] = '.mw-wiki-logo { background-image: ' . - CSSMin::buildUrlValue( $logoHD['1.5x'] ) .';' . + CSSMin::buildUrlValue( $logoHD['1.5x'] ) . ';' . 'background-size: 135px auto; }'; } if ( isset( $logoHD['2x'] ) ) { $styles[ '(-webkit-min-device-pixel-ratio: 2), ' . - '(min--moz-device-pixel-ratio: 2),'. + '(min--moz-device-pixel-ratio: 2),' . '(min-resolution: 2dppx), ' . '(min-resolution: 192dpi)' ][] = '.mw-wiki-logo { background-image: ' . diff --git a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php index 03f2124537..8170cb1c98 100644 --- a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php +++ b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php @@ -54,11 +54,10 @@ class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule { } /** - * @param ResourceLoaderContext $context - * @return string Hash + * @return bool */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( serialize( $this->getData() ) ); + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 8dbed8e889..c704c1a8ef 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -88,6 +88,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgContentNamespaces' => MWNamespace::getContentNamespaces(), 'wgSiteName' => $conf->get( 'Sitename' ), 'wgDBname' => $conf->get( 'DBname' ), + 'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ), 'wgAvailableSkins' => Skin::getSkinNames(), 'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ), // MediaWiki sets cookies to have this prefix by default @@ -100,6 +101,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ), 'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ), 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ), + 'wgResourceLoaderLegacyModules' => self::getLegacyModules(), ); Hooks::run( 'ResourceLoaderGetConfigVars', array( &$vars ) ); @@ -187,6 +189,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $resourceLoader = $context->getResourceLoader(); $target = $context->getRequest()->getVal( 'target', 'desktop' ); + // Bypass target filter if this request is from a unit test context. To prevent misuse in + // production, this is only allowed if testing is enabled server-side. + $byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test'; $out = ''; $registryData = array(); @@ -195,7 +200,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { foreach ( $resourceLoader->getModuleNames() as $name ) { $module = $resourceLoader->getModule( $name ); $moduleTargets = $module->getTargets(); - if ( !in_array( $target, $moduleTargets ) ) { + if ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) ) { continue; } @@ -268,7 +273,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } // Register modules - $out .= ResourceLoader::makeLoaderRegisterScript( $registrations ); + $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations ); return $out; } @@ -289,6 +294,20 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { return array( 'jquery', 'mediawiki' ); } + public static function getLegacyModules() { + global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil; + + $legacyModules = array(); + if ( $wgIncludeLegacyJavaScript ) { + $legacyModules[] = 'mediawiki.legacy.wikibits'; + } + if ( $wgPreloadJavaScriptMwUtil ) { + $legacyModules[] = 'mediawiki.util'; + } + + return $legacyModules; + } + /** * Get the load URL of the startup modules. * @@ -321,40 +340,25 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { */ public function getScript( ResourceLoaderContext $context ) { global $IP; + if ( $context->getOnly() !== 'scripts' ) { + return '/* Requires only=script */'; + } $out = file_get_contents( "$IP/resources/src/startup.js" ); - if ( $context->getOnly() === 'scripts' ) { - // Startup function - $configuration = $this->getConfigSettings( $context ); - $registrations = $this->getModuleRegistrations( $context ); + $pairs = array_map( function ( $value ) { + $value = FormatJson::encode( $value, ResourceLoader::inDebugMode(), FormatJson::ALL_OK ); // Fix indentation - $registrations = str_replace( "\n", "\n\t", trim( $registrations ) ); - $mwMapJsCall = Xml::encodeJsCall( - 'mw.Map', - array( $this->getConfig()->get( 'LegacyJavaScriptGlobals' ) ) - ); - $mwConfigSetJsCall = Xml::encodeJsCall( - 'mw.config.set', - array( $configuration ), - ResourceLoader::inDebugMode() - ); - - $out .= "var startUp = function () {\n" . - "\tmw.config = new " . - $mwMapJsCall . "\n" . - "\t$registrations\n" . - "\t" . $mwConfigSetJsCall . - "};\n"; - - // Conditional script injection - $scriptTag = Html::linkedScript( self::getStartupModulesUrl( $context ) ); - $out .= "if ( isCompatible() ) {\n" . - "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) . - "\n}"; - } - - return $out; + $value = str_replace( "\n", "\n\t", $value ); + return $value; + }, array( + '$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), + '$VARS.configuration' => $this->getConfigSettings( $context ), + '$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ), + ) ); + $pairs['$CODE.registrations()'] = str_replace( "\n", "\n\t", trim( $this->getModuleRegistrations( $context ) ) ); + + return strtr( $out, $pairs ); } /** @@ -404,7 +408,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // Pre-populate versionHash with something because the loop over all modules below includes // the startup module (this module). // See ResourceLoaderModule::getVersionHash() for usage of this cache. - $this->versionHash[ $context->getHash() ] = null; + $this->versionHash[$context->getHash()] = null; return $rl->getCombinedVersion( $context, $rl->getModuleNames() ); } diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php index d0f7d4476b..65d770e23f 100644 --- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php +++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php @@ -30,11 +30,10 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { protected $origin = self::ORIGIN_CORE_INDIVIDUAL; /** - * @param ResourceLoaderContext $context - * @return array|int|mixed + * @return bool */ - public function getModifiedTime( ResourceLoaderContext $context ) { - return wfTimestamp( TS_UNIX, $context->getUserObj()->getTouched() ); + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderUserDefaultsModule.php b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php index 2fd35adcd1..eba61edcfb 100644 --- a/includes/resourceloader/ResourceLoaderUserDefaultsModule.php +++ b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php @@ -26,18 +26,13 @@ */ class ResourceLoaderUserDefaultsModule extends ResourceLoaderModule { - /* Protected Members */ - protected $targets = array( 'desktop', 'mobile' ); - /* Methods */ - /** - * @param ResourceLoaderContext $context - * @return string Hash + * @return bool */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( serialize( User::getDefaultOptions() ) ); + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index aba0fa6bc6..0847109cf5 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -40,11 +40,10 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { } /** - * @param ResourceLoaderContext $context - * @return int + * @return bool */ - public function getModifiedTime( ResourceLoaderContext $context ) { - return wfTimestamp( TS_UNIX, $context->getUserObj()->getTouched() ); + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 264af5bafe..0023de2757 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -149,7 +149,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { */ protected function getContent( $titleText ) { $title = Title::newFromText( $titleText ); - if ( !$title || $title->isRedirect() ) { + if ( !$title ) { return null; } @@ -224,6 +224,20 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { return $styles; } + /** + * Disable module content versioning. + * + * This class does not support generating content outside of a module + * request due to foreign database support. + * + * See getDefinitionSummary() for meta-data versioning. + * + * @return bool + */ + public function enableModuleContentVersion() { + return false; + } + /** * @param ResourceLoaderContext $context * @return array @@ -277,7 +291,6 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { $pages = $this->getPages( $context ); $key = implode( '|', array_keys( $pages ) ); if ( !isset( $this->titleInfo[$key] ) ) { - $this->titleInfo[$key] = array(); $batch = new LinkBatch; foreach ( $pages as $titleText => $options ) { diff --git a/includes/revisiondelete/RevDelList.php b/includes/revisiondelete/RevDelList.php index 837557cb53..7aac3b8925 100644 --- a/includes/revisiondelete/RevDelList.php +++ b/includes/revisiondelete/RevDelList.php @@ -251,7 +251,7 @@ abstract class RevDelList extends RevisionListBase { if ( !$field ) { throw new MWException( "Bad log URL param type!" ); } - // Put things hidden from sysops in the oversight log + // Put things hidden from sysops in the suppression log if ( ( $params['newBits'] | $params['oldBits'] ) & $this->getSuppressBit() ) { $logType = 'suppress'; } else { diff --git a/includes/search/SearchHighlighter.php b/includes/search/SearchHighlighter.php index 5087e8d585..7d5d38f22b 100644 --- a/includes/search/SearchHighlighter.php +++ b/includes/search/SearchHighlighter.php @@ -218,7 +218,7 @@ class SearchHighlighter { } // calc by how much to extend existing snippets - $targetchars = intval( ( $contextchars * $contextlines ) / count ( $snippets ) ); + $targetchars = intval( ( $contextchars * $contextlines ) / count( $snippets ) ); } foreach ( $snippets as $index => $line ) { diff --git a/includes/search/SearchMySQL.php b/includes/search/SearchMySQL.php index fa2423b7e6..246f115546 100644 --- a/includes/search/SearchMySQL.php +++ b/includes/search/SearchMySQL.php @@ -439,7 +439,7 @@ class SearchMySQL extends SearchDatabase { $sql = "SHOW GLOBAL VARIABLES LIKE 'ft\\_min\\_word\\_len'"; $dbr = wfGetDB( DB_SLAVE ); - $result = $dbr->query( $sql ); + $result = $dbr->query( $sql, __METHOD__ ); $row = $result->fetchObject(); $result->free(); diff --git a/includes/search/SearchPostgres.php b/includes/search/SearchPostgres.php index bda10b0bda..71e3b63505 100644 --- a/includes/search/SearchPostgres.php +++ b/includes/search/SearchPostgres.php @@ -186,7 +186,7 @@ class SearchPostgres extends SearchDatabase { function update( $pageid, $title, $text ) { ## We don't want to index older revisions $sql = "UPDATE pagecontent SET textvector = NULL WHERE textvector IS NOT NULL and old_id IN " . - "(SELECT rev_text_id FROM revision WHERE rev_page = " . intval( $pageid ) . + "(SELECT DISTINCT rev_text_id FROM revision WHERE rev_page = " . intval( $pageid ) . " ORDER BY rev_text_id DESC OFFSET 1)"; $this->db->query( $sql ); return true; diff --git a/includes/search/SearchResultSet.php b/includes/search/SearchResultSet.php index 0a05eef159..8d18b0e6ed 100644 --- a/includes/search/SearchResultSet.php +++ b/includes/search/SearchResultSet.php @@ -60,6 +60,33 @@ class SearchResultSet { return null; } + /** + * Some search modes will run an alternative query that it thinks gives + * a better result than the provided search. Returns true if this has + * occured. + * + * @return bool + */ + function hasRewrittenQuery() { + return false; + } + + /** + * @return string|null The search the query was internally rewritten to, + * or null when the result of the original query was returned. + */ + function getQueryAfterRewrite() { + return null; + } + + /** + * @return string|null Same as self::getQueryAfterRewrite(), but in HTML + * and with changes highlighted. Null when the query was not rewritten. + */ + function getQueryAfterRewriteSnippet() { + return null; + } + /** * Some search modes return a suggested alternate term if there are * no exact hits. Returns true if there is one on this set. diff --git a/includes/site/CachingSiteStore.php b/includes/site/CachingSiteStore.php index 9243f12b8b..077dbc0efd 100644 --- a/includes/site/CachingSiteStore.php +++ b/includes/site/CachingSiteStore.php @@ -168,9 +168,11 @@ class CachingSiteStore implements SiteStore { } /** - * Purges the internal and external cache of the site list, forcing the list + * Purges the internal and external cache of the site list, forcing the list. * of sites to be reloaded. * + * Only use this for testing, as APC is typically used and is per-server + * * @since 1.25 */ public function reset() { @@ -182,6 +184,8 @@ class CachingSiteStore implements SiteStore { /** * Clears the list of sites stored. * + * Only use this for testing, as APC is typically used and is per-server. + * * @see SiteStore::clear() * * @return bool Success diff --git a/includes/site/DBSiteStore.php b/includes/site/DBSiteStore.php index f167584e70..1193bd65f6 100644 --- a/includes/site/DBSiteStore.php +++ b/includes/site/DBSiteStore.php @@ -153,7 +153,11 @@ class DBSiteStore implements SiteStore { protected function loadSites() { $this->sites = new SiteList(); - foreach ( $this->sitesTable->select() as $siteRow ) { + $siteRows = $this->sitesTable->select( null, array(), array( + 'ORDER BY' => 'site_global_key' + ) ); + + foreach ( $siteRows as $siteRow ) { $this->sites[] = $this->siteFromRow( $siteRow ); } diff --git a/includes/site/SiteSQLStore.php b/includes/site/SiteSQLStore.php index d77f07be5c..e3230fff84 100644 --- a/includes/site/SiteSQLStore.php +++ b/includes/site/SiteSQLStore.php @@ -41,7 +41,7 @@ class SiteSQLStore extends CachingSiteStore { */ public static function newInstance( ORMTable $sitesTable = null, BagOStuff $cache = null ) { if ( $cache === null ) { - $cache = wfGetMainCache(); + $cache = wfGetCache( wfIsHHVM() ? CACHE_ACCEL : CACHE_ANYTHING ); } $siteStore = new DBSiteStore(); diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index 6c5fbcd338..4e6201cd73 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -180,8 +180,7 @@ abstract class Skin extends ContextSource { * @return array Array of modules with helper keys for easy overriding */ public function getDefaultModules() { - global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax, - $wgAjaxWatch, $wgEnableAPI, $wgEnableWriteAPI; + global $wgUseAjax, $wgAjaxWatch, $wgEnableAPI, $wgEnableWriteAPI; $out = $this->getOutput(); $user = $out->getUser(); @@ -191,7 +190,7 @@ abstract class Skin extends ContextSource { 'mediawiki.page.ready', ), // modules that exist for legacy reasons - 'legacy' => array(), + 'legacy' => ResourceLoaderStartUpModule::getLegacyModules(), // modules relating to search functionality 'search' => array(), // modules relating to functionality relating to watching an article @@ -199,27 +198,17 @@ abstract class Skin extends ContextSource { // modules which relate to the current users preferences 'user' => array(), ); - if ( $wgIncludeLegacyJavaScript ) { - $modules['legacy'][] = 'mediawiki.legacy.wikibits'; - } - - if ( $wgPreloadJavaScriptMwUtil ) { - $modules['legacy'][] = 'mediawiki.util'; - } // Add various resources if required - if ( $wgUseAjax ) { - $modules['legacy'][] = 'mediawiki.legacy.ajax'; - - if ( $wgEnableAPI ) { - if ( $wgEnableWriteAPI && $wgAjaxWatch && $user->isLoggedIn() - && $user->isAllowed( 'writeapi' ) - ) { - $modules['watch'][] = 'mediawiki.page.watch.ajax'; - } - - $modules['search'][] = 'mediawiki.searchSuggest'; + if ( $wgUseAjax && $wgEnableAPI ) { + if ( $wgEnableWriteAPI && $wgAjaxWatch && $user->isLoggedIn() + && $user->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' ) + && $this->getRelevantTitle()->canExist() + ) { + $modules['watch'][] = 'mediawiki.page.watch.ajax'; } + + $modules['search'][] = 'mediawiki.searchSuggest'; } if ( $user->getBoolOption( 'editsectiononrightclick' ) ) { @@ -248,8 +237,8 @@ abstract class Skin extends ContextSource { $titles[] = $user->getTalkPage(); } - // Other tab link - if ( $title->isSpecialPage() ) { + // Check, if the page can hold some kind of content, otherwise do nothing + if ( !$title->canExist() ) { // nothing } elseif ( $title->isTalkPage() ) { $titles[] = $title->getSubjectPage(); @@ -650,7 +639,7 @@ abstract class Skin extends ContextSource { } return $this->msg( 'retrievedfrom' ) - ->rawParams( '' . $url . '' ) + ->rawParams( '' . $url . '' ) ->parse(); } diff --git a/includes/skins/SkinFallbackTemplate.php b/includes/skins/SkinFallbackTemplate.php index 312769f931..cd5e43c67b 100644 --- a/includes/skins/SkinFallbackTemplate.php +++ b/includes/skins/SkinFallbackTemplate.php @@ -87,7 +87,7 @@ class SkinFallbackTemplate extends BaseTemplate { if ( file_exists( "$IP/skins/$skin/skin.json" ) ) { return "wfLoadSkin( '$skin' );"; } else { - return "require_once \"\$IP/skins/$skin/$skin.php\";"; + return "require_once \"\$IP/skins/$skin/$skin.php\";"; } } diff --git a/includes/skins/SkinTemplate.php b/includes/skins/SkinTemplate.php index ae78b2c3b2..baf9d95478 100644 --- a/includes/skins/SkinTemplate.php +++ b/includes/skins/SkinTemplate.php @@ -51,12 +51,22 @@ class SkinTemplate extends Skin { * @param OutputPage $out */ function setupSkinUserCss( OutputPage $out ) { - $out->addModuleStyles( array( + $moduleStyles = array( 'mediawiki.legacy.shared', 'mediawiki.legacy.commonPrint', - 'mediawiki.ui.button', 'mediawiki.sectionAnchor' - ) ); + ); + if ( $out->isSyndicated() ) { + $moduleStyles[] = 'mediawiki.feedlink'; + } + + // Deprecated since 1.26: Unconditional loading of mediawiki.ui.button + // on every page is deprecated. Express a dependency instead. + if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) { + $moduleStyles[] = 'mediawiki.ui.button'; + } + + $out->addModuleStyles( $moduleStyles ); } /** diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index b9132358a6..23bd394ccb 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -434,7 +434,8 @@ abstract class ChangesListSpecialPage extends SpecialPage { $legend .= Html::element( 'dt', array( 'class' => $cssClass ), $context->msg( $letter )->text() ) . "\n" . - Html::rawElement( 'dd', array(), + Html::rawElement( 'dd', + array( 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ), $context->msg( $label )->parse() ) . "\n"; } diff --git a/includes/specialpage/FormSpecialPage.php b/includes/specialpage/FormSpecialPage.php index 90567617fa..42c59806b4 100644 --- a/includes/specialpage/FormSpecialPage.php +++ b/includes/specialpage/FormSpecialPage.php @@ -96,7 +96,11 @@ abstract class FormSpecialPage extends SpecialPage { $this->getMessagePrefix() ); $form->setSubmitCallback( array( $this, 'onSubmit' ) ); - $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' ); + if ( $this->getDisplayFormat() !== 'ooui' ) { + // No legend and wrapper by default in OOUI forms, but can be set manually + // from alterForm() + $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' ); + } $headerMsg = $this->msg( $this->getMessagePrefix() . '-text' ); if ( !$headerMsg->isDisabled() ) { diff --git a/includes/specialpage/QueryPage.php b/includes/specialpage/QueryPage.php index 92409cd652..3c8b74201b 100644 --- a/includes/specialpage/QueryPage.php +++ b/includes/specialpage/QueryPage.php @@ -70,7 +70,7 @@ abstract class QueryPage extends SpecialPage { array( 'DeadendPagesPage', 'Deadendpages' ), array( 'DoubleRedirectsPage', 'DoubleRedirects' ), array( 'FileDuplicateSearchPage', 'FileDuplicateSearch' ), - array( 'ListDuplicatedFilesPage', 'ListDuplicatedFiles'), + array( 'ListDuplicatedFilesPage', 'ListDuplicatedFiles' ), array( 'LinkSearchPage', 'LinkSearch' ), array( 'ListredirectsPage', 'Listredirects' ), array( 'LonelyPagesPage', 'Lonelypages' ), @@ -141,7 +141,7 @@ abstract class QueryPage extends SpecialPage { * @return array * @since 1.18 */ - function getQueryInfo() { + public function getQueryInfo() { return null; } @@ -178,7 +178,7 @@ abstract class QueryPage extends SpecialPage { * @return bool * @since 1.18 */ - function usesTimestamps() { + public function usesTimestamps() { return false; } @@ -198,7 +198,7 @@ abstract class QueryPage extends SpecialPage { * * @return bool */ - function isExpensive() { + public function isExpensive() { return $this->getConfig()->get( 'DisableQueryPages' ); } @@ -219,7 +219,7 @@ abstract class QueryPage extends SpecialPage { * * @return bool */ - function isCached() { + public function isCached() { return $this->isExpensive() && $this->getConfig()->get( 'MiserMode' ); } @@ -294,7 +294,7 @@ abstract class QueryPage extends SpecialPage { * @throws DBError|Exception * @return bool|int */ - function recache( $limit, $ignoreErrors = true ) { + public function recache( $limit, $ignoreErrors = true ) { if ( !$this->isCacheable() ) { return 0; } @@ -370,7 +370,7 @@ abstract class QueryPage extends SpecialPage { * @return ResultWrapper * @since 1.18 */ - function reallyDoQuery( $limit, $offset = false ) { + public function reallyDoQuery( $limit, $offset = false ) { $fname = get_class( $this ) . "::reallyDoQuery"; $dbr = $this->getRecacheDB(); $query = $this->getQueryInfo(); @@ -421,7 +421,7 @@ abstract class QueryPage extends SpecialPage { * @param int|bool $limit * @return ResultWrapper */ - function doQuery( $offset = false, $limit = false ) { + public function doQuery( $offset = false, $limit = false ) { if ( $this->isCached() && $this->isCacheable() ) { return $this->fetchFromCache( $limit, $offset ); } else { @@ -436,7 +436,7 @@ abstract class QueryPage extends SpecialPage { * @return ResultWrapper * @since 1.18 */ - function fetchFromCache( $limit, $offset = false ) { + public function fetchFromCache( $limit, $offset = false ) { $dbr = wfGetDB( DB_SLAVE ); $options = array(); if ( $limit !== false ) { @@ -470,12 +470,24 @@ abstract class QueryPage extends SpecialPage { return $this->cachedTimestamp; } + /** + * Returns limit and offset, as returned by $this->getRequest()->getLimitOffset(). + * Subclasses may override this to further restrict or modify limit and offset. + * + * @since 1.26 + * + * @return int[] list( $limit, $offset ) + */ + protected function getLimitOffset() { + return $this->getRequest()->getLimitOffset(); + } + /** * This is the actual workhorse. It does everything needed to make a * real, honest-to-gosh query page. * @param string $par */ - function execute( $par ) { + public function execute( $par ) { $user = $this->getUser(); if ( !$this->userCanExecute( $user ) ) { $this->displayRestrictionError(); @@ -495,7 +507,7 @@ abstract class QueryPage extends SpecialPage { $out->setSyndicated( $this->isSyndicated() ); if ( $this->limit == 0 && $this->offset == 0 ) { - list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset(); + list( $this->limit, $this->offset ) = $this->getLimitOffset(); } // @todo Use doQuery() diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php index df1fa613f3..9129ee5d42 100644 --- a/includes/specialpage/RedirectSpecialPage.php +++ b/includes/specialpage/RedirectSpecialPage.php @@ -149,11 +149,11 @@ abstract class SpecialRedirectToSpecial extends RedirectSpecialPage { * - limit, offset: Useful for linking to history of one's own user page or * user talk page. For example, this would be a link to "the last edit to your * user talk page in the year 2010": - * http://en.wikipedia.org/wiki/Special:MyPage?offset=20110000000000&limit=1&action=history + * https://en.wikipedia.org/wiki/Special:MyPage?offset=20110000000000&limit=1&action=history * * - feed: would allow linking to the current user's RSS feed for their user * talk page: - * http://en.wikipedia.org/w/index.php?title=Special:MyTalk&action=history&feed=rss + * https://en.wikipedia.org/w/index.php?title=Special:MyTalk&action=history&feed=rss * * - preloadtitle: Can be used to provide a default section title for a * preloaded new comment on one's own talk page. @@ -168,7 +168,7 @@ abstract class SpecialRedirectToSpecial extends RedirectSpecialPage { * - redlink: Affects the message the user sees if their talk page/user talk * page does not currently exist. Avoids confusion for newbies with no user * pages over why they got a "permission error" following this link: - * http://en.wikipedia.org/w/index.php?title=Special:MyPage&redlink=1 + * https://en.wikipedia.org/w/index.php?title=Special:MyPage&redlink=1 * * - debug: determines whether the debug parameter is passed to load.php, * which disables reformatting and allows scripts to be debugged. Useful diff --git a/includes/specialpage/SpecialPage.php b/includes/specialpage/SpecialPage.php index a7a43b0e30..65a4eb9abf 100644 --- a/includes/specialpage/SpecialPage.php +++ b/includes/specialpage/SpecialPage.php @@ -662,7 +662,6 @@ class SpecialPage { */ public function getFinalGroupName() { $name = $this->getName(); - $specialPageGroups = $this->getConfig()->get( 'SpecialPageGroups' ); // Allow overbidding the group from the wiki side $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage(); @@ -671,18 +670,6 @@ class SpecialPage { } else { // Than use the group from this object $group = $this->getGroupName(); - - // Group '-' is used as default to have the chance to determine, - // if the special pages overrides this method, - // if not overridden, $wgSpecialPageGroups is checked for b/c - if ( $group === '-' && isset( $specialPageGroups[$name] ) ) { - $group = $specialPageGroups[$name]; - } - } - - // never give '-' back, change to 'other' - if ( $group === '-' ) { - $group = 'other'; } return $group; @@ -697,8 +684,16 @@ class SpecialPage { * @since 1.21 */ protected function getGroupName() { - // '-' used here to determine, if this group is overridden or has a hardcoded 'other' - // Needed for b/c in getFinalGroupName - return '-'; + return 'other'; + } + + /** + * Call wfTransactionalTimeLimit() if this request was POSTed + * @since 1.26 + */ + protected function useTransactionalTimeLimit() { + if ( $this->getRequest()->wasPosted() ) { + wfTransactionalTimeLimit(); + } } } diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index dedfcb6afc..e794a5dfdc 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -218,7 +218,7 @@ class SpecialPageFactory { global $wgSpecialPages; global $wgDisableInternalSearch, $wgEmailAuthentication; global $wgEnableEmail, $wgEnableJavaScriptTest; - global $wgPageLanguageUseDB; + global $wgPageLanguageUseDB, $wgContentHandlerUseDB; if ( !is_array( self::$list ) ) { @@ -244,6 +244,9 @@ class SpecialPageFactory { if ( $wgPageLanguageUseDB ) { self::$list['PageLanguage'] = 'SpecialPageLanguage'; } + if ( $wgContentHandlerUseDB ) { + self::$list['ChangeContentModel'] = 'SpecialChangeContentModel'; + } self::$list['Activeusers'] = 'SpecialActiveUsers'; @@ -260,14 +263,13 @@ class SpecialPageFactory { } /** - * Initialise and return the list of special page aliases. Returns an object with - * properties which can be accessed $obj->pagename - each property name is an - * alias, with the value being the canonical name of the special page. All - * registered special pages are guaranteed to map to themselves. - * @return object + * Initialise and return the list of special page aliases. Returns an array where + * the key is an alias, and the value is the canonical name of the special page. + * All registered special pages are guaranteed to map to themselves. + * @return array */ - private static function getAliasListObject() { - if ( !is_object( self::$aliases ) ) { + private static function getAliasList() { + if ( is_null( self::$aliases ) ) { global $wgContLang; $aliases = $wgContLang->getSpecialPageAliases(); $pageList = self::getPageList(); @@ -310,9 +312,6 @@ class SpecialPageFactory { } } } - - // Cast to object: func()[$key] doesn't work, but func()->$key does - self::$aliases = (object)self::$aliases; } return self::$aliases; @@ -332,8 +331,9 @@ class SpecialPageFactory { $caseFoldedAlias = $wgContLang->caseFold( $bits[0] ); $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); - if ( isset( self::getAliasListObject()->$caseFoldedAlias ) ) { - $name = self::getAliasListObject()->$caseFoldedAlias; + $aliases = self::getAliasList(); + if ( isset( $aliases[$caseFoldedAlias] ) ) { + $name = $aliases[$caseFoldedAlias]; } else { return array( null, null ); } @@ -347,34 +347,6 @@ class SpecialPageFactory { return array( $name, $par ); } - /** - * Add a page to a certain display group for Special:SpecialPages - * - * @param SpecialPage|string $page - * @param string $group - * @deprecated since 1.21 Override SpecialPage::getGroupName - */ - public static function setGroup( $page, $group ) { - wfDeprecated( __METHOD__, '1.21' ); - - global $wgSpecialPageGroups; - $name = is_object( $page ) ? $page->getName() : $page; - $wgSpecialPageGroups[$name] = $group; - } - - /** - * Get the group that the special page belongs in on Special:SpecialPage - * - * @param SpecialPage $page - * @return string - * @deprecated since 1.21 Use SpecialPage::getFinalGroupName - */ - public static function getGroup( &$page ) { - wfDeprecated( __METHOD__, '1.21' ); - - return $page->getFinalGroupName(); - } - /** * Check if a given name exist as a special page or as a special page alias * @@ -572,7 +544,6 @@ class SpecialPageFactory { $context->setTitle( $page->getPageTitle( $par ) ); } } elseif ( !$page->isIncludable() ) { - return false; } @@ -638,7 +609,7 @@ class SpecialPageFactory { public static function getLocalNameFor( $name, $subpage = false ) { global $wgContLang; $aliases = $wgContLang->getSpecialPageAliases(); - $aliasList = self::getAliasListObject(); + $aliasList = self::getAliasList(); // Find the first alias that maps back to $name if ( isset( $aliases[$name] ) ) { @@ -646,8 +617,8 @@ class SpecialPageFactory { foreach ( $aliases[$name] as $alias ) { $caseFoldedAlias = $wgContLang->caseFold( $alias ); $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); - if ( isset( $aliasList->$caseFoldedAlias ) && - $aliasList->$caseFoldedAlias === $name + if ( isset( $aliasList[$caseFoldedAlias] ) && + $aliasList[$caseFoldedAlias] === $name ) { $name = $alias; $found = true; diff --git a/includes/specials/SpecialAllMessages.php b/includes/specials/SpecialAllMessages.php index 6a86af2d87..762658c523 100644 --- a/includes/specials/SpecialAllMessages.php +++ b/includes/specials/SpecialAllMessages.php @@ -130,7 +130,7 @@ class AllMessagesTablePager extends TablePager { if ( $prefix !== null ) { $this->displayPrefix = $prefix->getDBkey(); - $this->prefix = '/^' . preg_quote( $this->displayPrefix ) . '/i'; + $this->prefix = '/^' . preg_quote( $this->displayPrefix, '/' ) . '/i'; } else { $this->displayPrefix = false; $this->prefix = false; @@ -206,7 +206,7 @@ class AllMessagesTablePager extends TablePager { Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) . ' ' . - $this->getLimitSelect() . + $this->getLimitSelect( array( 'id' => 'mw-table_pager_limit_label' ) ) . ' @@ -392,10 +392,10 @@ class AllMessagesTablePager extends TablePager { ); } - return $title . ' ' - . $this->msg( 'parentheses' )->rawParams( $talk )->escaped() - . ' ' - . $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); + return $title . ' ' . + $this->msg( 'parentheses' )->rawParams( $talk )->escaped() . + ' ' . + $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); case 'am_default' : case 'am_actual' : @@ -445,7 +445,7 @@ class AllMessagesTablePager extends TablePager { } elseif ( $field === 'am_title' ) { return array( 'class' => $field ); } else { - return array( 'lang' => $this->langcode, 'dir' => $this->lang->getDir(), 'class' => $field ); + return array( 'lang' => wfBCP47( $this->langcode ), 'dir' => $this->lang->getDir(), 'class' => $field ); } } diff --git a/includes/specials/SpecialAllPages.php b/includes/specials/SpecialAllPages.php index 74b1f7bb10..c4a67c0cf1 100644 --- a/includes/specials/SpecialAllPages.php +++ b/includes/specials/SpecialAllPages.php @@ -25,6 +25,7 @@ * Implements Special:Allpages * * @ingroup SpecialPage + * @todo Rewrite using IndexPager */ class SpecialAllPages extends IncludableSpecialPage { @@ -179,6 +180,7 @@ class SpecialAllPages extends IncludableSpecialPage { $toList = $this->getNamespaceKeyAndText( $namespace, $to ); $namespaces = $this->getContext()->getLanguage()->getNamespaces(); $n = 0; + $prevTitle = null; if ( !$fromList || !$toList ) { $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock(); @@ -191,15 +193,13 @@ class SpecialAllPages extends IncludableSpecialPage { list( , $toKey, $to ) = $toList; $dbr = wfGetDB( DB_SLAVE ); - $conds = array( - 'page_namespace' => $namespace, - 'page_title >= ' . $dbr->addQuotes( $fromKey ) - ); - + $filterConds = array( 'page_namespace' => $namespace ); if ( $hideredirects ) { - $conds['page_is_redirect'] = 0; + $filterConds['page_is_redirect'] = 0; } + $conds = $filterConds; + $conds[] = 'page_title >= ' . $dbr->addQuotes( $fromKey ); if ( $toKey !== "" ) { $conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey ); } @@ -234,6 +234,35 @@ class SpecialAllPages extends IncludableSpecialPage { } else { $out = ''; } + + if ( $fromKey !== '' && !$this->including() ) { + # Get the first title from previous chunk + $prevConds = $filterConds; + $prevConds[] = 'page_title < ' . $dbr->addQuotes( $fromKey ); + $prevKey = $dbr->selectField( + 'page', + 'page_title', + $prevConds, + __METHOD__, + array( 'ORDER BY' => 'page_title DESC', 'OFFSET' => $this->maxPerPage - 1 ) + ); + + if ( $prevKey === false ) { + # The previous chunk is not complete, need to link to the very first title + # available in the database + $prevKey = $dbr->selectField( + 'page', + 'page_title', + $prevConds, + __METHOD__, + array( 'ORDER BY' => 'page_title' ) + ); + } + + if ( $prevKey !== false ) { + $prevTitle = Title::makeTitle( $namespace, $prevKey ); + } + } } if ( $this->including() ) { @@ -241,44 +270,6 @@ class SpecialAllPages extends IncludableSpecialPage { return; } - if ( $from == '' ) { - // First chunk; no previous link. - $prevTitle = null; - } else { - # Get the last title from previous chunk - $dbr = wfGetDB( DB_SLAVE ); - $res_prev = $dbr->select( - 'page', - 'page_title', - array( 'page_namespace' => $namespace, 'page_title < ' . $dbr->addQuotes( $from ) ), - __METHOD__, - array( 'ORDER BY' => 'page_title DESC', - 'LIMIT' => $this->maxPerPage, 'OFFSET' => ( $this->maxPerPage - 1 ) - ) - ); - - # Get first title of previous complete chunk - if ( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) { - $pt = $dbr->fetchObject( $res_prev ); - $prevTitle = Title::makeTitle( $namespace, $pt->page_title ); - } else { - # The previous chunk is not complete, need to link to the very first title - # available in the database - $options = array( 'LIMIT' => 1 ); - if ( !$dbr->implicitOrderby() ) { - $options['ORDER BY'] = 'page_title'; - } - $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', - array( 'page_namespace' => $namespace ), __METHOD__, $options ); - # Show the previous link if it s not the current requested chunk - if ( $from != $reallyFirstPage_title ) { - $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title ); - } else { - $prevTitle = null; - } - } - } - $self = $this->getPageTitle(); $topLinks = array( @@ -287,7 +278,7 @@ class SpecialAllPages extends IncludableSpecialPage { $bottomLinks = array(); # Do we put a previous link ? - if ( $prevTitle && $pt = $prevTitle->getText() ) { + if ( $prevTitle ) { $query = array( 'from' => $prevTitle->getText() ); if ( $namespace ) { @@ -300,7 +291,7 @@ class SpecialAllPages extends IncludableSpecialPage { $prevLink = Linker::linkKnown( $self, - $this->msg( 'prevpage', $pt )->escaped(), + $this->msg( 'prevpage', $prevTitle->getText() )->escaped(), array(), $query ); diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php index b0830327be..2da24a8ed6 100644 --- a/includes/specials/SpecialAncientpages.php +++ b/includes/specials/SpecialAncientpages.php @@ -32,7 +32,7 @@ class AncientPagesPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -40,7 +40,7 @@ class AncientPagesPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'page', 'revision' ), 'fields' => array( @@ -56,7 +56,7 @@ class AncientPagesPage extends QueryPage { ); } - function usesTimestamps() { + public function usesTimestamps() { return true; } diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index 752edc35d4..cd6cc76e8e 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -97,7 +97,6 @@ class SpecialBlock extends FormSpecialPage { protected function alterForm( HTMLForm $form ) { $form->setWrapperLegendMsg( 'blockip-legend' ); $form->setHeaderText( '' ); - $form->setSubmitCallback( array( __CLASS__, 'processUIForm' ) ); $form->setSubmitDestructive(); $msg = $this->alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit'; @@ -394,7 +393,7 @@ class SpecialBlock extends FormSpecialPage { # Link to edit the block dropdown reasons, if applicable if ( $user->isAllowed( 'editinterface' ) ) { - $links[] = Linker::link( + $links[] = Linker::linkKnown( $this->msg( 'ipbreason-dropdown' )->inContentLanguage()->getTitle(), $this->msg( 'ipb-edit-dropdown' )->escaped(), array(), @@ -597,17 +596,8 @@ class SpecialBlock extends FormSpecialPage { } /** - * Submit callback for an HTMLForm object, will simply pass - * @param array $data - * @param HTMLForm $form - * @return bool|string - */ - public static function processUIForm( array $data, HTMLForm $form ) { - return self::processForm( $data, $form->getContext() ); - } - - /** - * Given the form data, actually implement a block + * Given the form data, actually implement a block. This is also called from ApiBlock. + * * @param array $data * @param IContextSource $context * @return bool|string @@ -672,8 +662,8 @@ class SpecialBlock extends FormSpecialPage { if ( $data['HideUser'] ) { if ( !$performer->isAllowed( 'hideuser' ) ) { # this codepath is unreachable except by a malicious user spoofing forms, - # or by race conditions (user has oversight and sysop, loads block form, - # and is de-oversighted before submission); so need to fail completely + # or by race conditions (user has hideuser and block rights, loads block form, + # and loses hideuser rights before submission); so need to fail completely # rather than just silently disable hiding return array( 'badaccess-group0' ); } @@ -797,7 +787,7 @@ class SpecialBlock extends FormSpecialPage { $logParams['5::duration'] = $data['Expiry']; $logParams['6::flags'] = self::blockLogFlags( $data, $type ); - # Make log entry, if the name is hidden, put it in the oversight log + # Make log entry, if the name is hidden, put it in the suppression log $log_type = $data['HideUser'] ? 'suppress' : 'block'; $logEntry = new ManualLogEntry( $log_type, $logaction ); $logEntry->setTarget( Title::makeTitle( NS_USER, $target ) ); @@ -962,11 +952,11 @@ class SpecialBlock extends FormSpecialPage { /** * Process the form on POST submission. * @param array $data + * @param HTMLForm $form * @return bool|array True for success, false for didn't-try, array of errors on failure */ - public function onSubmit( array $data ) { - // This isn't used since we need that HTMLForm that's passed in the - // second parameter. See alterForm for the real function + public function onSubmit( array $data, HTMLForm $form = null ) { + return self::processForm( $data, $form->getContext() ); } /** diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index 4dd313bce8..9defaba7f1 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -65,6 +65,9 @@ class SpecialBlockList extends SpecialPage { return; } + # setup BlockListPager here to get the actual default Limit + $pager = $this->getBlockListPager(); + # Just show the block list $fields = array( 'Target' => array( @@ -77,11 +80,11 @@ class SpecialBlockList extends SpecialPage { ), 'Options' => array( 'type' => 'multiselect', - 'options' => array( - $this->msg( 'blocklist-userblocks' )->text() => 'userblocks', - $this->msg( 'blocklist-tempblocks' )->text() => 'tempblocks', - $this->msg( 'blocklist-addressblocks' )->text() => 'addressblocks', - $this->msg( 'blocklist-rangeblocks' )->text() => 'rangeblocks', + 'options-messages' => array( + 'blocklist-userblocks' => 'userblocks', + 'blocklist-tempblocks' => 'tempblocks', + 'blocklist-addressblocks' => 'addressblocks', + 'blocklist-rangeblocks' => 'rangeblocks', ), 'flatlist' => true, ), @@ -96,7 +99,7 @@ class SpecialBlockList extends SpecialPage { $lang->formatNum( 500 ) => 500, ), 'name' => 'limit', - 'default' => 50, + 'default' => $pager->getLimit(), ), ); $context = new DerivativeContext( $this->getContext() ); @@ -109,10 +112,14 @@ class SpecialBlockList extends SpecialPage { $form->prepareForm(); $form->displayForm( '' ); - $this->showList(); + $this->showList( $pager ); } - function showList() { + /** + * Setup a new BlockListPager instance. + * @return BlockListPager + */ + protected function getBlockListPager() { $conds = array(); # Is the user allowed to see hidden blocks? if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { @@ -163,12 +170,20 @@ class SpecialBlockList extends SpecialPage { $conds[] = "ipb_range_end = ipb_range_start"; } + return new BlockListPager( $this, $conds ); + } + + /** + * Show the list of blocked accounts matching the actual filter. + * @param BlockListPager $pager The BlockListPager instance for this page + */ + protected function showList( BlockListPager $pager ) { + $out = $this->getOutput(); + # Check for other blocks, i.e. global/tor blocks $otherBlockLink = array(); Hooks::run( 'OtherBlockLogLink', array( &$otherBlockLink, $this->target ) ); - $out = $this->getOutput(); - # Show additional header for the local block only when other blocks exists. # Not necessary in a standard installation without such extensions enabled if ( count( $otherBlockLink ) ) { @@ -177,7 +192,6 @@ class SpecialBlockList extends SpecialPage { ); } - $pager = new BlockListPager( $this, $conds ); if ( $pager->getNumRows() ) { $out->addParserOutputContent( $pager->getFullOutput() ); } elseif ( $this->target ) { @@ -249,7 +263,7 @@ class BlockListPager extends TablePager { function formatValue( $name, $value ) { static $msg = null; if ( $msg === null ) { - $msg = array( + $keys = array( 'anononlyblock', 'createaccountblock', 'noautoblockblock', @@ -258,17 +272,22 @@ class BlockListPager extends TablePager { 'unblocklink', 'change-blocklink', ); - $msg = array_combine( $msg, array_map( array( $this, 'msg' ), $msg ) ); + + foreach ( $keys as $key ) { + $msg[$key] = $this->msg( $key )->escaped(); + } } /** @var $row object */ $row = $this->mCurrentRow; + $language = $this->getLanguage(); + $formatted = ''; switch ( $name ) { case 'ipb_timestamp': - $formatted = $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ); + $formatted = htmlspecialchars( $language->userTimeAndDate( $value, $this->getUser() ) ); break; case 'ipb_target': @@ -294,7 +313,10 @@ class BlockListPager extends TablePager { break; case 'ipb_expiry': - $formatted = $this->getLanguage()->formatExpiry( $value, /* User preference timezone */true ); + $formatted = htmlspecialchars( $language->formatExpiry( + $value, + /* User preference timezone */true + ) ); if ( $this->getUser()->isAllowed( 'block' ) ) { if ( $row->ipb_auto ) { $links[] = Linker::linkKnown( @@ -317,7 +339,7 @@ class BlockListPager extends TablePager { 'span', array( 'class' => 'mw-blocklist-actions' ), $this->msg( 'parentheses' )->rawParams( - $this->getLanguage()->pipeList( $links ) )->escaped() + $language->pipeList( $links ) )->escaped() ); } break; @@ -355,7 +377,7 @@ class BlockListPager extends TablePager { $properties[] = $msg['blocklist-nousertalk']; } - $formatted = $this->getLanguage()->commaList( $properties ); + $formatted = $language->commaList( $properties ); break; default: @@ -431,16 +453,12 @@ class BlockListPager extends TablePager { $lb->setCaller( __METHOD__ ); foreach ( $result as $row ) { - # Usernames and titles are in fact related by a simple substitution of space -> underscore - # The last few lines of Title::secureAndSplit() tell the story. - $name = str_replace( ' ', '_', $row->ipb_address ); - $lb->add( NS_USER, $name ); - $lb->add( NS_USER_TALK, $name ); + $lb->add( NS_USER, $row->ipb_address ); + $lb->add( NS_USER_TALK, $row->ipb_address ); if ( isset( $row->by_user_name ) ) { - $username = str_replace( ' ', '_', $row->by_user_name ); - $lb->add( NS_USER, $username ); - $lb->add( NS_USER_TALK, $username ); + $lb->add( NS_USER, $row->by_user_name ); + $lb->add( NS_USER_TALK, $row->by_user_name ); } } diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php index 1bbdbeab93..701f75f047 100644 --- a/includes/specials/SpecialBrokenRedirects.php +++ b/includes/specials/SpecialBrokenRedirects.php @@ -32,7 +32,7 @@ class BrokenRedirectsPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -48,7 +48,7 @@ class BrokenRedirectsPage extends QueryPage { return $this->msg( 'brokenredirectstext' )->parseAsBlock(); } - function getQueryInfo() { + public function getQueryInfo() { $dbr = wfGetDB( DB_SLAVE ); return array( diff --git a/includes/specials/SpecialChangeContentModel.php b/includes/specials/SpecialChangeContentModel.php new file mode 100644 index 0000000000..cce5da5c12 --- /dev/null +++ b/includes/specials/SpecialChangeContentModel.php @@ -0,0 +1,223 @@ +getRequest()->getVal( 'pagetitle', $par ); + $title = Title::newFromText( $par ); + if ( $title ) { + $this->title = $title; + $this->par = $title->getPrefixedText(); + } else { + $this->par = ''; + } + } + + protected function getDisplayFormat() { + return 'ooui'; + } + + protected function alterForm( HTMLForm $form ) { + if ( !$this->title ) { + $form->setMethod( 'GET' ); + } + } + + public function validateTitle( $title ) { + if ( !$title ) { + // No form input yet + return true; + } + + // Already validated by HTMLForm, but if not, throw + // and exception instead of a fatal + $titleObj = Title::newFromTextThrow( $title ); + + $this->oldRevision = Revision::newFromTitle( $titleObj ) ?: false; + + if ( $this->oldRevision ) { + $oldContent = $this->oldRevision->getContent(); + if ( !$oldContent->getContentHandler()->supportsDirectEditing() ) { + return $this->msg( 'changecontentmodel-nodirectediting' ) + ->params( ContentHandler::getLocalizedName( $oldContent->getModel() ) ) + ->escaped(); + } + } + + return true; + } + + protected function getFormFields() { + $that = $this; + $fields = array( + 'pagetitle' => array( + 'type' => 'title', + 'creatable' => true, + 'name' => 'pagetitle', + 'default' => $this->par, + 'label-message' => 'changecontentmodel-title-label', + 'validation-callback' => array( $this, 'validateTitle' ), + ), + ); + if ( $this->title ) { + $fields['pagetitle']['readonly'] = true; + $fields += array( + 'model' => array( + 'type' => 'select', + 'name' => 'model', + 'options' => $this->getOptionsForTitle( $this->title ), + 'label-message' => 'changecontentmodel-model-label' + ), + 'reason' => array( + 'type' => 'text', + 'name' => 'reason', + 'validation-callback' => function( $reason ) use ( $that ) { + $match = EditPage::matchSummarySpamRegex( $reason ); + if ( $match ) { + return $that->msg( 'spamprotectionmatch', $match )->parse(); + } + + return true; + }, + 'label-message' => 'changecontentmodel-reason-label', + ), + ); + } + + return $fields; + } + + private function getOptionsForTitle( Title $title = null ) { + $models = ContentHandler::getContentModels(); + $options = array(); + foreach ( $models as $model ) { + $handler = ContentHandler::getForModelID( $model ); + if ( !$handler->supportsDirectEditing() ) { + continue; + } + if ( $title ) { + if ( $title->getContentModel() === $model ) { + continue; + } + if ( !$handler->canBeUsedOn( $title ) ) { + continue; + } + } + $options[ContentHandler::getLocalizedName( $model )] = $model; + } + + return $options; + } + + public function onSubmit( array $data ) { + global $wgContLang; + + if ( $data['pagetitle'] === '' ) { + // Initial form view of special page, pass + return false; + } + + // At this point, it has to be a POST request. This is enforced by HTMLForm, + // but lets be safe verify that. + if ( !$this->getRequest()->wasPosted() ) { + throw new RuntimeException( "Form submission was not POSTed" ); + } + + $this->title = Title::newFromText( $data['pagetitle' ] ); + $user = $this->getUser(); + // Check permissions and make sure the user has permission to edit the specific page + $errors = $this->title->getUserPermissionsErrors( 'editcontentmodel', $user ); + $errors = wfMergeErrorArrays( $errors, $this->title->getUserPermissionsErrors( 'edit', $user ) ); + if ( $errors ) { + $out = $this->getOutput(); + $wikitext = $out->formatPermissionsErrorMessage( $errors ); + // Hack to get our wikitext parsed + return Status::newFatal( new RawMessage( '$1', array( $wikitext ) ) ); + } + + $page = WikiPage::factory( $this->title ); + if ( $this->oldRevision === null ) { + $this->oldRevision = $page->getRevision() ?: false; + } + $oldModel = $this->title->getContentModel(); + if ( $this->oldRevision ) { + $oldContent = $this->oldRevision->getContent(); + try { + $newContent = ContentHandler::makeContent( + $oldContent->getNativeData(), $this->title, $data['model'] + ); + } catch ( MWException $e ) { + return Status::newFatal( + $this->msg( 'changecontentmodel-cannot-convert' ) + ->params( + $this->title->getPrefixedText(), + ContentHandler::getLocalizedName( $data['model'] ) + ) + ); + } + } else { + // Page doesn't exist, create an empty content object + $newContent = ContentHandler::getForModelID( $data['model'] )->makeEmptyContent(); + } + $flags = $this->oldRevision ? EDIT_UPDATE : EDIT_NEW; + if ( $user->isAllowed( 'bot' ) ) { + $flags |= EDIT_FORCE_BOT; + } + + $log = new ManualLogEntry( 'contentmodel', 'change' ); + $log->setPerformer( $user ); + $log->setTarget( $this->title ); + $log->setComment( $data['reason'] ); + $log->setParameters( array( + '4::oldmodel' => $oldModel, + '5::newmodel' => $data['model'] + ) ); + + $formatter = LogFormatter::newFromEntry( $log ); + $formatter->setContext( RequestContext::newExtraneousContext( $this->title ) ); + $reason = $formatter->getPlainActionText(); + if ( $data['reason'] !== '' ) { + $reason .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $data['reason']; + } + # Truncate for whole multibyte characters. + $reason = $wgContLang->truncate( $reason, 255 ); + + $status = $page->doEditContent( + $newContent, + $reason, + $flags, + $this->oldRevision ? $this->oldRevision->getId() : false, + $user + ); + if ( !$status->isOK() ) { + return $status; + } + + $logid = $log->insert(); + $log->publish( $logid ); + + return $status; + } + + public function onSuccess() { + $out = $this->getOutput(); + $out->setPageTitle( $this->msg( 'changecontentmodel-success-title' ) ); + $out->addWikiMsg( 'changecontentmodel-success-text', $this->title ); + } +} diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php index babb315483..c2aa704ab1 100644 --- a/includes/specials/SpecialChangeEmail.php +++ b/includes/specials/SpecialChangeEmail.php @@ -107,7 +107,7 @@ class SpecialChangeEmail extends FormSpecialPage { } protected function getDisplayFormat() { - return 'vform'; + return 'ooui'; } protected function alterForm( HTMLForm $form ) { diff --git a/includes/specials/SpecialComparePages.php b/includes/specials/SpecialComparePages.php index da1a54cd04..0f8b7291b1 100644 --- a/includes/specials/SpecialComparePages.php +++ b/includes/specials/SpecialComparePages.php @@ -50,10 +50,12 @@ class SpecialComparePages extends SpecialPage { $this->setHeaders(); $this->outputHeader(); + # Form (.mw-searchInput enables suggestions) $form = new HTMLForm( array( 'Page1' => array( 'type' => 'text', 'name' => 'page1', + 'cssclass' => 'mw-searchInput', 'label-message' => 'compare-page1', 'size' => '40', 'section' => 'page1', @@ -70,6 +72,7 @@ class SpecialComparePages extends SpecialPage { 'Page2' => array( 'type' => 'text', 'name' => 'page2', + 'cssclass' => 'mw-searchInput', 'label-message' => 'compare-page2', 'size' => '40', 'section' => 'page2', diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php index b6ab112b34..147f67e8d2 100644 --- a/includes/specials/SpecialConfirmemail.php +++ b/includes/specials/SpecialConfirmemail.php @@ -43,6 +43,10 @@ class EmailConfirmation extends UnlistedSpecialPage { * @throws UserNotLoggedIn */ function execute( $code ) { + // Ignore things like master queries/connections on GET requests. + // It's very convenient to just allow formless link usage. + Profiler::instance()->getTransactionProfiler()->resetExpectations(); + $this->setHeaders(); $this->checkReadOnly(); @@ -120,7 +124,7 @@ class EmailConfirmation extends UnlistedSpecialPage { * @param string $code Confirmation code */ function attemptConfirm( $code ) { - $user = User::newFromConfirmationCode( $code ); + $user = User::newFromConfirmationCode( $code, User::READ_LATEST ); if ( !is_object( $user ) ) { $this->getOutput()->addWikiMsg( 'confirmemail_invalid' ); @@ -151,6 +155,10 @@ class EmailInvalidation extends UnlistedSpecialPage { } function execute( $code ) { + // Ignore things like master queries/connections on GET requests. + // It's very convenient to just allow formless link usage. + Profiler::instance()->getTransactionProfiler()->resetExpectations(); + $this->setHeaders(); $this->checkReadOnly(); $this->checkPermissions(); @@ -164,7 +172,7 @@ class EmailInvalidation extends UnlistedSpecialPage { * @param string $code Confirmation code */ function attemptInvalidate( $code ) { - $user = User::newFromConfirmationCode( $code ); + $user = User::newFromConfirmationCode( $code, User::READ_LATEST ); if ( !is_object( $user ) ) { $this->getOutput()->addWikiMsg( 'confirmemail_invalid' ); diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index 8c7f0c89c1..44352a78b5 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -477,7 +477,7 @@ class DeletedContributionsPage extends SpecialPage { if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $nt->getText() ) ) ) { # Block / Change block / Unblock links if ( $this->getUser()->isAllowed( 'block' ) ) { - if ( $userObj->isBlocked() ) { + if ( $userObj->isBlocked() && $userObj->getBlock()->getType() !== Block::TYPE_AUTO ) { $tools[] = Linker::linkKnown( # Change block link SpecialPage::getTitleFor( 'Block', $nt->getDBkey() ), $this->msg( 'change-blocklink' )->escaped() diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php index c364f70fa9..6d40985bbd 100644 --- a/includes/specials/SpecialDoubleRedirects.php +++ b/includes/specials/SpecialDoubleRedirects.php @@ -32,7 +32,7 @@ class DoubleRedirectsPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -99,7 +99,7 @@ class DoubleRedirectsPage extends QueryPage { return $retval; } - function getQueryInfo() { + public function getQueryInfo() { return $this->reallyGetQueryInfo(); } @@ -156,7 +156,6 @@ class DoubleRedirectsPage extends QueryPage { $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->escaped(), array(), array( - 'redirect' => 'no', 'action' => 'edit' ) ); diff --git a/includes/specials/SpecialEditTags.php b/includes/specials/SpecialEditTags.php index f41a1f1ded..d2b2e70846 100644 --- a/includes/specials/SpecialEditTags.php +++ b/includes/specials/SpecialEditTags.php @@ -123,7 +123,7 @@ class SpecialEditTags extends UnlistedSpecialPage { // Either submit or create our form if ( $this->isAllowed && $this->submitClicked ) { - $this->submit( $request ); + $this->submit(); } else { $this->showForm(); } @@ -349,20 +349,18 @@ class SpecialEditTags extends UnlistedSpecialPage { protected function getTagSelect( $selectedTags, $label ) { $result = array(); $result[0] = Xml::label( $label, 'mw-edittags-tag-list' ); - $result[1] = Xml::openElement( 'select', array( - 'name' => 'wpTagList[]', - 'id' => 'mw-edittags-tag-list', - 'multiple' => 'multiple', - 'size' => '8', - ) ); + + $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags ); + $select->setAttribute( 'multiple', 'multiple' ); + $select->setAttribute( 'size', '8' ); $tags = ChangeTags::listExplicitlyDefinedTags(); $tags = array_unique( array_merge( $tags, $selectedTags ) ); - foreach ( $tags as $tag ) { - $result[1] .= Xml::option( $tag, $tag, in_array( $tag, $selectedTags ) ); - } - $result[1] .= Xml::closeElement( 'select' ); + // Values of $tags are also used as