From: jenkins-bot Date: Fri, 18 Sep 2015 09:08:27 +0000 (+0000) Subject: Merge "Use User::equals() where applicable in the class" X-Git-Tag: 1.31.0-rc.0~9975 X-Git-Url: http://git.cyclocoop.org//%27http:/jquery.khurshid.com/ifixpng.php/%27?a=commitdiff_plain;h=b60dded47b7c4437a21baea8ee0780243340abb6;hp=5668f2347a65dda85fca5addd7cfc5f61db14717;p=lhc%2Fweb%2Fwiklou.git Merge "Use User::equals() where applicable in the class" --- diff --git a/.jscsrc b/.jscsrc index 6a3c56408b..aaa876c3e2 100644 --- a/.jscsrc +++ b/.jscsrc @@ -1,9 +1,16 @@ { "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/**", diff --git a/.jshintrc b/.jshintrc index b84d276621..b776e8f21a 100644 --- a/.jshintrc +++ b/.jshintrc @@ -22,7 +22,7 @@ "mediaWiki": true, "JSON": true, "OO": true, - "performance": true, + "mwPerformance": true, "jQuery": false, "QUnit": false, "sinon": false 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/Gruntfile.js b/Gruntfile.js index e1e5e4ab01..8dbeb6bfc0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -12,7 +12,7 @@ module.exports = function ( grunt ) { wgScriptPath = process.env.MW_SCRIPT_PATH, karmaProxy = {}; - karmaProxy[wgScriptPath] = wgServer + wgScriptPath; + karmaProxy[ wgScriptPath ] = wgServer + wgScriptPath; grunt.initConfig( { jshint: { @@ -32,6 +32,11 @@ module.exports = function ( grunt ) { ] }, banana: { + options: { + disallowBlankTranslations: false, + disallowDuplicateTranslations: false, + disallowUnusedTranslations: false + }, core: 'languages/i18n/', api: 'includes/api/i18n/', installer: 'includes/installer/i18n/' @@ -87,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 19e3eaa97d..4463c7823e 100644 --- a/RELEASE-NOTES-1.26 +++ b/RELEASE-NOTES-1.26 @@ -22,10 +22,16 @@ production. 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 === -* (T9148) Changes to category membership are now shown in watchlists and recent - changes. This can be configured in user preferences. * (T51506) Now action=info gives estimates of actual watchers for a page. See $wgRCMaxAge, $wgWatchersMaxAge and $wgUnwatchedPageSecret to learn how to configure if needed. @@ -61,6 +67,12 @@ production. 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. @@ -74,6 +86,8 @@ production. * (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 @@ -92,8 +106,22 @@ production. 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 === @@ -111,6 +139,7 @@ changes to languages because of Phabricator reports. * 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 @@ -155,6 +184,9 @@ changes to languages because of Phabricator reports. 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. == Compatibility == diff --git a/autoload.php b/autoload.php index 48e0b8824a..5adfbe5104 100644 --- a/autoload.php +++ b/autoload.php @@ -200,7 +200,6 @@ $wgAutoloadLocalClasses = array( 'CapsCleanup' => __DIR__ . '/maintenance/cleanupCaps.php', 'Category' => __DIR__ . '/includes/Category.php', 'CategoryFinder' => __DIR__ . '/includes/CategoryFinder.php', - 'CategoryMembershipChange' => __DIR__ . '/includes/changes/CategoryMembershipChange.php', 'CategoryPage' => __DIR__ . '/includes/page/CategoryPage.php', 'CategoryPager' => __DIR__ . '/includes/specials/SpecialCategories.php', 'CategoryViewer' => __DIR__ . '/includes/CategoryViewer.php', @@ -544,7 +543,7 @@ $wgAutoloadLocalClasses = array( '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', @@ -725,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', @@ -762,6 +760,15 @@ $wgAutoloadLocalClasses = array( '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', @@ -1012,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', diff --git a/composer.json b/composer.json index af9aeab397..1fe1e5068d 100644 --- a/composer.json +++ b/composer.json @@ -21,13 +21,14 @@ "leafo/lessphp": "0.5.0", "liuggio/statsd-php-client": "1.0.16", "mediawiki/at-ease": "1.0.0", - "oojs/oojs-ui": "0.12.5", + "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/cdb": "1.3.0", "wikimedia/composer-merge-plugin": "1.2.1", - "wikimedia/utfnormal": "1.0.2", + "wikimedia/ip-set": "1.0.1", + "wikimedia/utfnormal": "1.0.3", "wikimedia/wrappedstring": "2.0.0", "zordius/lightncandy": "0.21" }, diff --git a/docs/extension.schema.json b/docs/extension.schema.json index 1d78eccfc9..d11635d020 100644 --- a/docs/extension.schema.json +++ b/docs/extension.schema.json @@ -429,10 +429,6 @@ "type": "object", "description": "ResourceLoader LESS variables" }, - "ResourceLoaderLESSFunctions": { - "type": "object", - "description": "ResourceLoader LESS functions" - }, "ResourceLoaderLESSImportPaths": { "type": "object", "description": "ResourceLoader import paths" @@ -639,6 +635,13 @@ "config": { "type": "object", "description": "Configuration options for this extension", + "properties": { + "_prefix": { + "type": "string", + "default": "wg", + "description": "Prefix to put in front of configuration settings when exporting them to $GLOBALS" + } + }, "patternProperties": { "^[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*$": { "type": ["object", "array", "string", "integer", "null", "boolean"], diff --git a/docs/hooks.txt b/docs/hooks.txt index bf5599778e..54ab46c171 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -1376,6 +1376,9 @@ $auto: The extracted part of the parsed comment before the call to the hook. $post: Boolean, true if there is text after this autocomment $title: An optional title object used to links to sections. Can be null. $local: Boolean indicating whether section links should refer to local page. +$wikiId: String containing the ID (as used by WikiMap) of the wiki from which the + autocomment originated; null for the local wiki. Added in 1.26, should default + to null in handler functions, for backwards compatibility. 'GalleryGetModes': Get list of classes that can render different modes of a gallery. @@ -2448,6 +2451,13 @@ $user: The user having their password expiration reset $oldSessionID: old session id $newSessionID: new session id +'ResourceLoaderForeignApiModules': Called from ResourceLoaderForeignApiModule. +Use this to add dependencies to 'mediawiki.ForeignApi' module when you wish +to override its behavior. See the module docs for more information. +&$dependencies: string[] List of modules that 'mediawiki.ForeignApi' should +depend on +$context: ResourceLoaderContext|null + 'ResourceLoaderGetConfigVars': Called at the end of ResourceLoaderStartUpModule::getConfigSettings(). Use this to export static configuration variables to JavaScript. Things that depend on the current page 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/DefaultSettings.php b/includes/DefaultSettings.php index 34fe7eed40..37429b924b 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -211,7 +211,7 @@ $wgLoadScript = false; /** * The URL path of the skins directory. - * Defaults to "{$wgScriptPath}/skins". + * Defaults to "{$wgResourceBasePath}/skins". * @since 1.3 */ $wgStylePath = false; @@ -226,7 +226,7 @@ $wgLocalStylePath = false; /** * The URL path of the extensions directory. - * Defaults to "{$wgScriptPath}/extensions". + * Defaults to "{$wgResourceBasePath}/extensions". * @since 1.16 */ $wgExtensionAssetsPath = false; @@ -1860,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 */ @@ -3479,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; @@ -3617,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. @@ -3649,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 @@ -3733,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 @@ -4129,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/ + * + * 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) * - * - $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. + * - 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' ); @@ -4503,7 +4489,6 @@ $wgDefaultUserOptions = array( 'gender' => 'unknown', 'hideminor' => 0, 'hidepatrolled' => 0, - 'hidecategorization' => 0, 'imagesize' => 2, 'math' => 1, 'minordefault' => 0, @@ -4535,7 +4520,6 @@ $wgDefaultUserOptions = array( 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, - 'watchlisthidecategorization' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'wllimit' => 250, @@ -7252,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 @@ -7708,6 +7686,7 @@ $wgUseLinkNamespaceDBFields = true; * $wgVirtualRestConfig['modules']['parsoid'] = array( * 'url' => 'http://localhost:8000', * 'prefix' => 'enwiki', + * 'domain' => 'en.wikipedia.org', * ); * * @var array @@ -7718,6 +7697,7 @@ $wgVirtualRestConfig = array( 'global' => array( # Timeout in seconds 'timeout' => 360, + # 'domain' is set to $wgCanonicalServer in Setup.php 'forwardCookies' => false, 'HTTPProxy' => null ) diff --git a/includes/Defines.php b/includes/Defines.php index 38f2d424da..d55bbcf819 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -170,7 +170,6 @@ define( 'RC_EDIT', 0 ); define( 'RC_NEW', 1 ); define( 'RC_LOG', 3 ); define( 'RC_EXTERNAL', 5 ); -define( 'RC_CATEGORIZE', 6 ); /**@}*/ /**@{ diff --git a/includes/EditPage.php b/includes/EditPage.php index f3a41c46b9..05e0ac0ee9 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -527,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 ); @@ -652,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" ); @@ -665,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 ); } diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index c3740a0146..f2e37d557c 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -171,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 ) { @@ -191,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; @@ -206,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 ) { @@ -220,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; @@ -2179,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. @@ -3461,10 +3475,10 @@ function wfResetSessionID() { * @param bool $sessionId */ function wfSetupSession( $sessionId = false ) { - global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgSessionHandler; + global $wgSessionsInObjectCache, $wgSessionHandler; global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly; - if ( $wgSessionsInObjectCache || $wgSessionsInMemcached ) { + if ( $wgSessionsInObjectCache ) { ObjectCacheSessionHandler::install(); } elseif ( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) { # Only set this if $wgSessionHandler isn't null and session.save_handler @@ -3485,7 +3499,7 @@ function wfSetupSession( $sessionId = false ) { session_start(); MediaWiki\restoreWarnings(); - if ( $wgSessionsInObjectCache || $wgSessionsInMemcached ) { + if ( $wgSessionsInObjectCache ) { ObjectCacheSessionHandler::renewCurrentSession(); } } @@ -3954,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 ); } /** @@ -4273,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 c726538c99..a414562436 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -135,10 +135,6 @@ class Hooks { * returning null) is equivalent to returning true. */ public static function run( $event, array $args = array(), $deprecatedVersion = null ) { - $stats = RequestContext::getMain()->getStats(); - $metric = $stats->increment( 'hooks.' . $event ); - $metric->setSampleRate( 0.001 ); - foreach ( self::getHandlers( $event ) as $hook ) { // Turn non-array values into an array. (Can't use casting because of objects.) if ( !is_array( $hook ) ) { @@ -235,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/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 fec8adcbf6..bc5a9570e2 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -855,6 +855,8 @@ class CurlHttpRequest extends MWHttpRequest { class PhpHttpRequest extends MWHttpRequest { + private $fopenErrors = array(); + /** * @param string $url * @return string @@ -865,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(); @@ -917,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 ); @@ -943,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; } @@ -978,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/Linker.php b/includes/Linker.php index d6a4056f02..9b5ff27b3d 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -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 */ @@ -1459,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 ) { @@ -1493,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 @@ -1579,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"; } @@ -2382,6 +2398,7 @@ class Linker { 'title' => $tooltip ) ); } + } /** diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php index 731b62e036..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; } 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 f488aa2485..fbacb2504e 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -283,9 +283,11 @@ class MediaWiki { * /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 ) { + private function tryNormaliseRedirect( Title $title ) { $request = $this->context->getRequest(); $output = $this->context->getOutput(); @@ -495,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 ); @@ -687,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(); @@ -699,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/OutputPage.php b/includes/OutputPage.php index 8d4720b610..552e181553 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -142,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 ); @@ -2012,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(); + } + if ( !is_array( $option ) ) { + $option = array(); } - $this->mVaryHeader[$header] = array_unique( (array)$this->mVaryHeader[$header] ); + $this->mVaryHeader[$header] = array_unique( array_merge( $this->mVaryHeader[$header], $option ) ); } /** @@ -2909,8 +2905,7 @@ class OutputPage extends ContextSource { // Automatically select style/script elements if ( $only === ResourceLoaderModule::TYPE_STYLES ) { - $media = $group === 'print' ? 'print' : 'all'; - $link = Html::linkedStyle( $url, $media ); + $link = Html::linkedStyle( $url ); } else { if ( $context->getRaw() || $isRaw ) { // Startup module can't load itself, needs to use */" + ), array( '→‎autocomment', "/* autocomment */", @@ -166,6 +194,16 @@ class LinkerTest extends MediaWikiLangTestCase { "/* autocomment */", null ), + array( + '→‎autocomment', + "/* autocomment */", + false, false + ), + array( + '→‎autocomment', + "/* autocomment */", + false, false, $wikiId + ), // Linker::formatLinksInComment array( 'abc link def', @@ -191,6 +229,28 @@ class LinkerTest extends MediaWikiLangTestCase { 'abc /subpage def', "abc [[/subpage]] def", ), + array( + 'abc "evil!" def', + "abc [[\"evil!\"]] def", + ), + array( + 'abc [[<script>very evil</script>]] def', + "abc [[]] def", + ), + array( + 'abc [[|]] def', + "abc [[|]] def", + ), + array( + 'abc link def', + "abc [[link]] def", + false, false + ), + array( + 'abc link def', + "abc [[link]] def", + false, false, $wikiId + ) ); } diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php index 85c2220910..f0d905e599 100644 --- a/tests/phpunit/includes/OutputPageTest.php +++ b/tests/phpunit/includes/OutputPageTest.php @@ -142,20 +142,20 @@ class OutputPageTest extends MediaWikiTestCase { array( array( 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ), "" ), array( // Don't condition wrap raw modules (like the startup module) array( 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ), - '' + '' ), // Load module styles only // This also tests the order the modules are put into the url array( array( array( 'test.baz', 'test.foo', 'test.bar' ), ResourceLoaderModule::TYPE_STYLES ), - '' + '' ), // Load private module (only=scripts) array( @@ -180,16 +180,16 @@ class OutputPageTest extends MediaWikiTestCase { // noscript group array( array( 'test.noscript', ResourceLoaderModule::TYPE_STYLES ), - '' + '' ), // Load two modules in separate groups array( array( array( 'test.group.foo', 'test.group.bar' ), ResourceLoaderModule::TYPE_COMBINED ), "\n" . "" ), ); @@ -256,10 +256,89 @@ class OutputPageTest extends MediaWikiTestCase { ) ), ) ); $links = $method->invokeArgs( $out, $args ); - // Strip comments to avoid variation due to wgDBname in WikiID and cache key - $actualHtml = preg_replace( '#/\*[^*]+\*/#', '', implode( "\n", $links['html'] ) ); + $actualHtml = implode( "\n", $links['html'] ); $this->assertEquals( $expectedHtml, $actualHtml ); } + + /** + * @dataProvider provideVaryHeaders + * @covers OutputPage::addVaryHeader + * @covers OutputPage::getVaryHeader + * @covers OutputPage::getXVO + */ + public function testVaryHeaders( $calls, $vary, $xvo ) { + // get rid of default Vary fields + $outputPage = $this->getMockBuilder( 'OutputPage' ) + ->setConstructorArgs( array( new RequestContext() ) ) + ->setMethods( array( 'getCacheVaryCookies' ) ) + ->getMock(); + $outputPage->expects( $this->any() ) + ->method( 'getCacheVaryCookies' ) + ->will( $this->returnValue( array() ) ); + TestingAccessWrapper::newFromObject( $outputPage )->mVaryHeader = array(); + + foreach ( $calls as $call ) { + call_user_func_array( array( $outputPage, 'addVaryHeader' ), $call ); + } + $this->assertEquals( $vary, $outputPage->getVaryHeader(), 'Vary:' ); + $this->assertEquals( $xvo, $outputPage->getXVO(), 'X-Vary-Options:' ); + } + + public function provideVaryHeaders() { + // note: getXVO() automatically adds Vary: Cookie + return array( + array( // single header + array( + array( 'Cookie' ), + ), + 'Vary: Cookie', + 'X-Vary-Options: Cookie', + ), + array( // non-unique headers + array( + array( 'Cookie' ), + array( 'Accept-Language' ), + array( 'Cookie' ), + ), + 'Vary: Cookie, Accept-Language', + 'X-Vary-Options: Cookie,Accept-Language', + ), + array( // two headers with single options + array( + array( 'Cookie', array( 'string-contains=phpsessid' ) ), + array( 'Accept-Language', array( 'string-contains=en' ) ), + ), + 'Vary: Cookie, Accept-Language', + 'X-Vary-Options: Cookie;string-contains=phpsessid,Accept-Language;string-contains=en', + ), + array( // one header with multiple options + array( + array( 'Cookie', array( 'string-contains=phpsessid', 'string-contains=userId' ) ), + ), + 'Vary: Cookie', + 'X-Vary-Options: Cookie;string-contains=phpsessid;string-contains=userId', + ), + array( // Duplicate option + array( + array( 'Cookie', array( 'string-contains=phpsessid' ) ), + array( 'Cookie', array( 'string-contains=phpsessid' ) ), + array( 'Accept-Language', array( 'string-contains=en', 'string-contains=en' ) ), + + + ), + 'Vary: Cookie, Accept-Language', + 'X-Vary-Options: Cookie;string-contains=phpsessid,Accept-Language;string-contains=en', + ), + array( // Same header, different options + array( + array( 'Cookie', array( 'string-contains=phpsessid' ) ), + array( 'Cookie', array( 'string-contains=userId' ) ), + ), + 'Vary: Cookie', + 'X-Vary-Options: Cookie;string-contains=phpsessid;string-contains=userId', + ), + ); + } } /** diff --git a/tests/phpunit/includes/PrefixSearchTest.php b/tests/phpunit/includes/PrefixSearchTest.php index d63541b7e3..afd10e9a04 100644 --- a/tests/phpunit/includes/PrefixSearchTest.php +++ b/tests/phpunit/includes/PrefixSearchTest.php @@ -6,6 +6,11 @@ class PrefixSearchTest extends MediaWikiLangTestCase { public function addDBData() { + if ( !$this->isWikitextNS( NS_MAIN ) ) { + // tests are skipped if NS_MAIN is not wikitext + return; + } + $this->insertPage( 'Sandbox' ); $this->insertPage( 'Bar' ); $this->insertPage( 'Example' ); diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php index c615c460c9..d3dc512bea 100644 --- a/tests/phpunit/includes/SanitizerTest.php +++ b/tests/phpunit/includes/SanitizerTest.php @@ -6,6 +6,11 @@ */ class SanitizerTest extends MediaWikiTestCase { + protected function tearDown() { + MWTidy::destroySingleton(); + parent::tearDown(); + } + /** * @covers Sanitizer::decodeCharReferences */ @@ -93,9 +98,7 @@ class SanitizerTest extends MediaWikiTestCase { * @param bool $escaped Whether sanitizer let the tag in or escape it (ie: '<video>') */ public function testRemovehtmltagsOnHtml5Tags( $tag, $escaped ) { - $this->setMwGlobals( array( - 'wgUseTidy' => false - ) ); + MWTidy::setInstance( false ); if ( $escaped ) { $this->assertEquals( "<$tag>", @@ -157,7 +160,7 @@ class SanitizerTest extends MediaWikiTestCase { * @covers Sanitizer::removeHTMLtags */ public function testRemoveHTMLtags( $input, $output, $msg = null ) { - $GLOBALS['wgUseTidy'] = false; + MWTidy::setInstance( false ); $this->assertEquals( $output, Sanitizer::removeHTMLtags( $input ), $msg ); } @@ -360,5 +363,4 @@ class SanitizerTest extends MediaWikiTestCase { array( '<script>foo</script>', '' ), ); } - } diff --git a/tests/phpunit/includes/TestingAccessWrapper.php b/tests/phpunit/includes/TestingAccessWrapper.php index 84c0f9b5be..63d897198b 100644 --- a/tests/phpunit/includes/TestingAccessWrapper.php +++ b/tests/phpunit/includes/TestingAccessWrapper.php @@ -34,16 +34,42 @@ class TestingAccessWrapper { return $methodReflection->invokeArgs( $this->object, $args ); } - public function __set( $name, $value ) { + /** + * ReflectionClass::getProperty() fails if the private property is defined + * in a parent class. This works more like ReflectionClass::getMethod(). + */ + private function getProperty( $name ) { $classReflection = new ReflectionClass( $this->object ); - $propertyReflection = $classReflection->getProperty( $name ); + try { + return $classReflection->getProperty( $name ); + } catch ( ReflectionException $ex ) { + while ( true ) { + $classReflection = $classReflection->getParentClass(); + if ( !$classReflection ) { + throw $ex; + } + try { + $propertyReflection = $classReflection->getProperty( $name ); + } catch ( ReflectionException $ex2 ) { + continue; + } + if ( $propertyReflection->isPrivate() ) { + return $propertyReflection; + } else { + throw $ex; + } + } + } + } + + public function __set( $name, $value ) { + $propertyReflection = $this->getProperty( $name ); $propertyReflection->setAccessible( true ); $propertyReflection->setValue( $this->object, $value ); } public function __get( $name ) { - $classReflection = new ReflectionClass( $this->object ); - $propertyReflection = $classReflection->getProperty( $name ); + $propertyReflection = $this->getProperty( $name ); $propertyReflection->setAccessible( true ); return $propertyReflection->getValue( $this->object ); } diff --git a/tests/phpunit/includes/TestingAccessWrapperTest.php b/tests/phpunit/includes/TestingAccessWrapperTest.php index 7e5b91a119..fc54afae33 100644 --- a/tests/phpunit/includes/TestingAccessWrapperTest.php +++ b/tests/phpunit/includes/TestingAccessWrapperTest.php @@ -14,18 +14,36 @@ class TestingAccessWrapperTest extends MediaWikiTestCase { function testGetProperty() { $this->assertSame( 1, $this->wrapped->property ); + $this->assertSame( 42, $this->wrapped->privateProperty ); + $this->assertSame( 9000, $this->wrapped->privateParentProperty ); } function testSetProperty() { $this->wrapped->property = 10; $this->assertSame( 10, $this->wrapped->property ); $this->assertSame( 10, $this->raw->getProperty() ); + + $this->wrapped->privateProperty = 11; + $this->assertSame( 11, $this->wrapped->privateProperty ); + $this->assertSame( 11, $this->raw->getPrivateProperty() ); + + $this->wrapped->privateParentProperty = 12; + $this->assertSame( 12, $this->wrapped->privateParentProperty ); + $this->assertSame( 12, $this->raw->getPrivateParentProperty() ); } function testCallMethod() { $this->wrapped->incrementPropertyValue(); $this->assertSame( 2, $this->wrapped->property ); $this->assertSame( 2, $this->raw->getProperty() ); + + $this->wrapped->incrementPrivatePropertyValue(); + $this->assertSame( 43, $this->wrapped->privateProperty ); + $this->assertSame( 43, $this->raw->getPrivateProperty() ); + + $this->wrapped->incrementPrivateParentPropertyValue(); + $this->assertSame( 9001, $this->wrapped->privateParentProperty ); + $this->assertSame( 9001, $this->raw->getPrivateParentProperty() ); } function testCallMethodTwoArgs() { diff --git a/tests/phpunit/includes/WikiMapTest.php b/tests/phpunit/includes/WikiMapTest.php new file mode 100644 index 0000000000..9233416c74 --- /dev/null +++ b/tests/phpunit/includes/WikiMapTest.php @@ -0,0 +1,108 @@ +settings = array( + 'wgServer' => array( + 'enwiki' => 'http://en.example.org', + 'ruwiki' => '//ru.example.org', + ), + 'wgArticlePath' => array( + 'enwiki' => '/w/$1', + 'ruwiki' => '/wiki/$1', + ), + ); + $conf->suffixes = array( 'wiki' ); + $this->setMwGlobals( array( + 'wgConf' => $conf, + ) ); + } + + public function provideGetWiki() { + $enwiki = new WikiReference( 'wiki', 'en', 'http://en.example.org', '/w/$1' ); + $ruwiki = new WikiReference( 'wiki', 'ru', '//ru.example.org', '/wiki/$1' ); + + return array( + 'unknown' => array( false, 'xyzzy' ), + 'enwiki' => array( $enwiki, 'enwiki' ), + 'ruwiki' => array( $ruwiki, 'ruwiki' ), + ); + } + + /** + * @dataProvider provideGetWiki + */ + public function testGetWiki( $expected, $wikiId ) { + $this->assertEquals( $expected, WikiMap::getWiki( $wikiId ) ); + } + + public function provideGetWikiName() { + return array( + 'unknown' => array( 'xyzzy', 'xyzzy' ), + 'enwiki' => array( 'en.example.org', 'enwiki' ), + 'ruwiki' => array( 'ru.example.org', 'ruwiki' ), + ); + } + + /** + * @dataProvider provideGetWikiName + */ + public function testGetWikiName( $expected, $wikiId ) { + $this->assertEquals( $expected, WikiMap::getWikiName( $wikiId ) ); + } + + public function provideMakeForeignLink() { + return array( + 'unknown' => array( false, 'xyzzy', 'Foo' ), + 'enwiki' => array( 'Foo', 'enwiki', 'Foo', ), + 'ruwiki' => array( 'вар', 'ruwiki', 'Фу', 'вар' ), + ); + } + + /** + * @dataProvider provideMakeForeignLink + */ + public function testMakeForeignLink( $expected, $wikiId, $page, $text = null ) { + $this->assertEquals( $expected, WikiMap::makeForeignLink( $wikiId, $page, $text ) ); + } + + public function provideForeignUserLink() { + return array( + 'unknown' => array( false, 'xyzzy', 'Foo' ), + 'enwiki' => array( 'User:Foo', 'enwiki', 'Foo', ), + 'ruwiki' => array( 'вар', 'ruwiki', 'Фу', 'вар' ), + ); + } + + /** + * @dataProvider provideForeignUserLink + */ + public function testForeignUserLink( $expected, $wikiId, $user, $text = null ) { + $this->assertEquals( $expected, WikiMap::foreignUserLink( $wikiId, $user, $text ) ); + } + + public function provideGetForeignURL() { + return array( + 'unknown' => array( false, 'xyzzy', 'Foo' ), + 'enwiki' => array( 'http://en.example.org/w/Foo', 'enwiki', 'Foo', ), + 'ruwiki with fragement' => array( '//ru.example.org/wiki/%D0%A4%D1%83#%D0%B2%D0%B0%D1%80', 'ruwiki', 'Фу', 'вар' ), + ); + } + + /** + * @dataProvider provideGetForeignURL + */ + public function testGetForeignURL( $expected, $wikiId, $page, $fragment = null ) { + $this->assertEquals( $expected, WikiMap::getForeignURL( $wikiId, $page, $fragment ) ); + } + +} + diff --git a/tests/phpunit/includes/WikiReferenceTest.php b/tests/phpunit/includes/WikiReferenceTest.php new file mode 100644 index 0000000000..4fe2e855b6 --- /dev/null +++ b/tests/phpunit/includes/WikiReferenceTest.php @@ -0,0 +1,80 @@ + array( 'foo.bar', 'http://foo.bar' ), + 'https' => array( 'foo.bar', 'http://foo.bar' ), + + // apparently, this is the expected behavior + 'invalid' => array( 'purple kittens', 'purple kittens' ), + ); + } + + /** + * @dataProvider provideGetDisplayName + */ + public function testGetDisplayName( $expected, $canonicalServer ) { + $reference = new WikiReference( 'wiki', 'xx', $canonicalServer, '/wiki/$1' ); + $this->assertEquals( $expected, $reference->getDisplayName() ); + } + + public function testGetCanonicalServer() { + $reference = new WikiReference( 'wiki', 'xx', 'https://acme.com', '/wiki/$1', '//acme.com' ); + $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() ); + } + + public function provideGetCanonicalUrl() { + return array( + 'no fragement' => array( 'https://acme.com/wiki/Foo', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo', null ), + 'empty fragement' => array( 'https://acme.com/wiki/Foo', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo', '' ), + 'fragment' => array( 'https://acme.com/wiki/Foo#Bar', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo', 'Bar' ), + 'double fragment' => array( 'https://acme.com/wiki/Foo#Bar%23Xus', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo', 'Bar#Xus' ), + 'escaped fragement' => array( 'https://acme.com/wiki/Foo%23Bar', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo#Bar', null ), + 'empty path' => array( 'https://acme.com/Foo', 'https://acme.com', '//acme.com', '/$1', 'Foo', null ), + ); + } + + /** + * @dataProvider provideGetCanonicalUrl + */ + public function testGetCanonicalUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) { + $reference = new WikiReference( 'wiki', 'xx', $canonicalServer, $path, $server ); + $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) ); + } + + /** + * @dataProvider provideGetCanonicalUrl + * @note getUrl is an alias for getCanonicalUrl + */ + public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) { + $reference = new WikiReference( 'wiki', 'xx', $canonicalServer, $path, $server ); + $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) ); + } + + public function provideGetFullUrl() { + return array( + 'no fragement' => array( '//acme.com/wiki/Foo', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo', null ), + 'empty fragement' => array( '//acme.com/wiki/Foo', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo', '' ), + 'fragment' => array( '//acme.com/wiki/Foo#Bar', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo', 'Bar' ), + 'double fragment' => array( '//acme.com/wiki/Foo#Bar%23Xus', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo', 'Bar#Xus' ), + 'escaped fragement' => array( '//acme.com/wiki/Foo%23Bar', 'https://acme.com', '//acme.com', '/wiki/$1', 'Foo#Bar', null ), + 'empty path' => array( '//acme.com/Foo', 'https://acme.com', '//acme.com', '/$1', 'Foo', null ), + ); + } + + /** + * @dataProvider provideGetFullUrl + */ + public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) { + $reference = new WikiReference( 'wiki', 'xx', $canonicalServer, $path, $server ); + $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) ); + } + +} + diff --git a/tests/phpunit/includes/api/ApiMainTest.php b/tests/phpunit/includes/api/ApiMainTest.php index ee1a9546d4..94b741dcac 100644 --- a/tests/phpunit/includes/api/ApiMainTest.php +++ b/tests/phpunit/includes/api/ApiMainTest.php @@ -79,4 +79,173 @@ class ApiMainTest extends ApiTestCase { ); } } + + /** + * Test HTTP precondition headers + * + * @covers ApiMain::checkConditionalRequestHeaders + * @dataProvider provideCheckConditionalRequestHeaders + * @param array $headers HTTP headers + * @param array $conditions Return data for ApiBase::getConditionalRequestData + * @param int $status Expected response status + * @param bool $post Request is a POST + */ + public function testCheckConditionalRequestHeaders( $headers, $conditions, $status, $post = false ) { + $request = new FauxRequest( array( 'action' => 'query', 'meta' => 'siteinfo' ), $post ); + $request->setHeaders( $headers ); + $request->response()->statusHeader( 200 ); // Why doesn't it default? + + $api = new ApiMain( $request ); + $priv = TestingAccessWrapper::newFromObject( $api ); + $priv->mInternalMode = false; + + $module = $this->getMockBuilder( 'ApiBase' ) + ->setConstructorArgs( array( $api, 'mock' ) ) + ->setMethods( array( 'getConditionalRequestData' ) ) + ->getMockForAbstractClass(); + $module->expects( $this->any() ) + ->method( 'getConditionalRequestData' ) + ->will( $this->returnCallback( function ( $condition ) use ( $conditions ) { + return isset( $conditions[$condition] ) ? $conditions[$condition] : null; + } ) ); + + $ret = $priv->checkConditionalRequestHeaders( $module ); + + $this->assertSame( $status, $request->response()->getStatusCode() ); + $this->assertSame( $status === 200, $ret ); + } + + public static function provideCheckConditionalRequestHeaders() { + $now = time(); + + return array( + // Non-existing from module is ignored + array( array( 'If-None-Match' => '"foo", "bar"' ), array(), 200 ), + array( array( 'If-Modified-Since' => 'Tue, 18 Aug 2015 00:00:00 GMT' ), array(), 200 ), + + // No headers + array( + array(), + array( + 'etag' => '""', + 'last-modified' => '20150815000000', + ), + 200 + ), + + // Basic If-None-Match + array( array( 'If-None-Match' => '"foo", "bar"' ), array( 'etag' => '"bar"' ), 304 ), + array( array( 'If-None-Match' => '"foo", "bar"' ), array( 'etag' => '"baz"' ), 200 ), + array( array( 'If-None-Match' => '"foo"' ), array( 'etag' => 'W/"foo"' ), 304 ), + array( array( 'If-None-Match' => 'W/"foo"' ), array( 'etag' => '"foo"' ), 304 ), + array( array( 'If-None-Match' => 'W/"foo"' ), array( 'etag' => 'W/"foo"' ), 304 ), + + // Pointless, but supported + array( array( 'If-None-Match' => '*' ), array(), 304 ), + + // Basic If-Modified-Since + array( array( 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ), + array( 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ), 304 ), + array( array( 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ), + array( 'last-modified' => wfTimestamp( TS_MW, $now ) ), 304 ), + array( array( 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ), + array( 'last-modified' => wfTimestamp( TS_MW, $now + 1 ) ), 200 ), + + // If-Modified-Since ignored when If-None-Match is given too + array( array( 'If-None-Match' => '""', 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ), + array( 'etag' => '"x"', 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ), 200 ), + array( array( 'If-None-Match' => '""', 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ), + array( 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ), 304 ), + + // Ignored for POST + array( array( 'If-None-Match' => '"foo", "bar"' ), array( 'etag' => '"bar"' ), 200, true ), + array( array( 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ), + array( 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ), 200, true ), + + // Other date formats allowed by the RFC + array( array( 'If-Modified-Since' => gmdate( 'l, d-M-y H:i:s', $now ) . ' GMT' ), + array( 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ), 304 ), + array( array( 'If-Modified-Since' => gmdate( 'D M j H:i:s Y', $now ) ), + array( 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ), 304 ), + + // Old browser extension to HTTP/1.0 + array( array( 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) . '; length=123' ), + array( 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ), 304 ), + + // Invalid date formats should be ignored + array( array( 'If-Modified-Since' => gmdate( 'Y-m-d H:i:s', $now ) . ' GMT' ), + array( 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ), 200 ), + ); + } + + /** + * Test conditional headers output + * @dataProvider provideConditionalRequestHeadersOutput + * @param array $conditions Return data for ApiBase::getConditionalRequestData + * @param array $headers Expected output headers + * @param bool $isError $isError flag + * @param bool $post Request is a POST + */ + public function testConditionalRequestHeadersOutput( $conditions, $headers, $isError = false, $post = false ) { + $request = new FauxRequest( array( 'action' => 'query', 'meta' => 'siteinfo' ), $post ); + $response = $request->response(); + + $api = new ApiMain( $request ); + $priv = TestingAccessWrapper::newFromObject( $api ); + $priv->mInternalMode = false; + + $module = $this->getMockBuilder( 'ApiBase' ) + ->setConstructorArgs( array( $api, 'mock' ) ) + ->setMethods( array( 'getConditionalRequestData' ) ) + ->getMockForAbstractClass(); + $module->expects( $this->any() ) + ->method( 'getConditionalRequestData' ) + ->will( $this->returnCallback( function ( $condition ) use ( $conditions ) { + return isset( $conditions[$condition] ) ? $conditions[$condition] : null; + } ) ); + $priv->mModule = $module; + + $priv->sendCacheHeaders( $isError ); + + foreach ( array( 'Last-Modified', 'ETag' ) as $header ) { + $this->assertEquals( + isset( $headers[$header] ) ? $headers[$header] : null, + $response->getHeader( $header ), + $header + ); + } + } + + public static function provideConditionalRequestHeadersOutput() { + return array( + array( + array(), + array() + ), + array( + array( 'etag' => '"foo"' ), + array( 'ETag' => '"foo"' ) + ), + array( + array( 'last-modified' => '20150818000102' ), + array( 'Last-Modified' => 'Tue, 18 Aug 2015 00:01:02 GMT' ) + ), + array( + array( 'etag' => '"foo"', 'last-modified' => '20150818000102' ), + array( 'ETag' => '"foo"', 'Last-Modified' => 'Tue, 18 Aug 2015 00:01:02 GMT' ) + ), + array( + array( 'etag' => '"foo"', 'last-modified' => '20150818000102' ), + array(), + true, + ), + array( + array( 'etag' => '"foo"', 'last-modified' => '20150818000102' ), + array(), + false, + true, + ), + ); + } + } diff --git a/tests/phpunit/includes/api/ApiResultTest.php b/tests/phpunit/includes/api/ApiResultTest.php index f894f87b8c..2f31677e70 100644 --- a/tests/phpunit/includes/api/ApiResultTest.php +++ b/tests/phpunit/includes/api/ApiResultTest.php @@ -181,6 +181,19 @@ class ApiResultTest extends MediaWikiTestCase { ); } + ApiResult::setValue( $arr, null, NAN, ApiResult::NO_VALIDATE ); + + try { + ApiResult::setValue( $arr, null, NAN, ApiResult::NO_SIZE_CHECK ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + $this->assertSame( + 'Cannot add non-finite floats to ApiResult', + $ex->getMessage(), + 'Expected exception' + ); + } + $arr = array(); $result2 = new ApiResult( 8388608 ); $result2->addValue( null, 'foo', 'bar' ); @@ -408,6 +421,19 @@ class ApiResultTest extends MediaWikiTestCase { ); } + $result->addValue( null, null, NAN, ApiResult::NO_VALIDATE ); + + try { + $result->addValue( null, null, NAN, ApiResult::NO_SIZE_CHECK ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + $this->assertSame( + 'Cannot add non-finite floats to ApiResult', + $ex->getMessage(), + 'Expected exception' + ); + } + $result->reset(); $result->addParsedLimit( 'foo', 12 ); $this->assertSame( array( @@ -444,6 +470,12 @@ class ApiResultTest extends MediaWikiTestCase { $result->removeValue( null, 'foo' ); $this->assertTrue( $result->addValue( null, 'foo', '1' ) ); + $result = new ApiResult( 10 ); + $obj = new ApiResultTestSerializableObject( 'ok' ); + $obj->foobar = 'foobaz'; + $this->assertTrue( $result->addValue( null, 'foo', $obj ) ); + $this->assertSame( 2, $result->getSize() ); + $result = new ApiResult( 8388608 ); $result2 = new ApiResult( 8388608 ); $result2->addValue( null, 'foo', 'bar' ); @@ -674,6 +706,10 @@ class ApiResultTest extends MediaWikiTestCase { ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key', ), + 'kvpmerge' => array( 'x' => 'a', 'y' => array( 'b' ), 'z' => array( 'c' => 'd' ), + ApiResult::META_TYPE => 'kvp', + ApiResult::META_KVP_MERGE => true, + ), 'emptyDefault' => array( '_dummy' => 1 ), 'emptyAssoc' => array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ), '_dummy' => 1, @@ -858,6 +894,13 @@ class ApiResultTest extends MediaWikiTestCase { ApiResult::META_TYPE => 'assoc', ApiResult::META_KVP_KEY_NAME => 'key', ), + 'kvpmerge' => array( + 'x' => 'a', + 'y' => array( 'b', ApiResult::META_TYPE => 'array' ), + 'z' => array( 'c' => 'd', ApiResult::META_TYPE => 'assoc' ), + ApiResult::META_TYPE => 'assoc', + ApiResult::META_KVP_MERGE => true, + ), 'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ), 'emptyAssoc' => array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ), '_dummy' => 1, @@ -889,6 +932,13 @@ class ApiResultTest extends MediaWikiTestCase { ApiResult::META_TYPE => 'assoc', ApiResult::META_KVP_KEY_NAME => 'key', ), + 'kvpmerge' => (object)array( + 'x' => 'a', + 'y' => array( 'b', ApiResult::META_TYPE => 'array' ), + 'z' => (object)array( 'c' => 'd', ApiResult::META_TYPE => 'assoc' ), + ApiResult::META_TYPE => 'assoc', + ApiResult::META_KVP_MERGE => true, + ), 'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ), 'emptyAssoc' => (object)array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ), '_dummy' => 1, @@ -920,6 +970,13 @@ class ApiResultTest extends MediaWikiTestCase { ApiResult::META_TYPE => 'array', ApiResult::META_KVP_KEY_NAME => 'key', ), + 'kvpmerge' => array( + $kvp( 'name', 'x', 'value', 'a' ), + $kvp( 'name', 'y', 'value', array( 'b', ApiResult::META_TYPE => 'array' ) ), + array( 'name' => 'z', 'c' => 'd', ApiResult::META_TYPE => 'assoc', ApiResult::META_PRESERVE_KEYS => array( 'name' ) ), + ApiResult::META_TYPE => 'array', + ApiResult::META_KVP_MERGE => true, + ), 'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ), 'emptyAssoc' => array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ), '_dummy' => 1, @@ -951,6 +1008,13 @@ class ApiResultTest extends MediaWikiTestCase { ApiResult::META_TYPE => 'array', ApiResult::META_KVP_KEY_NAME => 'key', ), + 'kvpmerge' => array( + $kvp( 'name', 'x', '*', 'a' ), + $kvp( 'name', 'y', '*', array( 'b', ApiResult::META_TYPE => 'array' ) ), + array( 'name' => 'z', 'c' => 'd', ApiResult::META_TYPE => 'assoc', ApiResult::META_PRESERVE_KEYS => array( 'name' ) ), + ApiResult::META_TYPE => 'array', + ApiResult::META_KVP_MERGE => true, + ), 'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ), 'emptyAssoc' => array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ), '_dummy' => 1, @@ -986,6 +1050,13 @@ class ApiResultTest extends MediaWikiTestCase { ApiResult::META_TYPE => 'array', ApiResult::META_KVP_KEY_NAME => 'key', ), + 'kvpmerge' => array( + (object)$kvp( 'name', 'x', 'value', 'a' ), + (object)$kvp( 'name', 'y', 'value', array( 'b', ApiResult::META_TYPE => 'array' ) ), + (object)array( 'name' => 'z', 'c' => 'd', ApiResult::META_TYPE => 'assoc', ApiResult::META_PRESERVE_KEYS => array( 'name' ) ), + ApiResult::META_TYPE => 'array', + ApiResult::META_KVP_MERGE => true, + ), 'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ), 'emptyAssoc' => (object)array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ), '_dummy' => 1, @@ -1025,6 +1096,11 @@ class ApiResultTest extends MediaWikiTestCase { (object)array( 'key' => 'x', 'value' => 'a' ), (object)array( 'key' => 'y', 'value' => 'b' ), ), + 'kvpmerge' => array( + (object)array( 'name' => 'x', 'value' => 'a' ), + (object)array( 'name' => 'y', 'value' => array( 'b' ) ), + (object)array( 'name' => 'z', 'c' => 'd' ), + ), 'emptyDefault' => array(), 'emptyAssoc' => (object)array(), '_dummy' => 1, @@ -1245,17 +1321,6 @@ class ApiResultTest extends MediaWikiTestCase { ), '*' => 'content', ), $result->getData() ); - $result->setRawMode(); - $this->assertSame( array( - 'foo' => array( - 'bar' => array( - '*' => 'content', - ), - ), - '*' => 'content', - '_element' => 'itn', - '_subelements' => array( 'sub' ), - ), $result->getData() ); $arr = array(); ApiResult::setContent( $arr, 'value' ); diff --git a/tests/phpunit/includes/changes/EnhancedChangesListTest.php b/tests/phpunit/includes/changes/EnhancedChangesListTest.php index 01e221f95a..a14a50d2cb 100644 --- a/tests/phpunit/includes/changes/EnhancedChangesListTest.php +++ b/tests/phpunit/includes/changes/EnhancedChangesListTest.php @@ -74,20 +74,6 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { $this->assertEquals( '', $html ); } - public function testCategorizationLineFormatting() { - $html = $this->createCategorizationLine( - $this->getCategorizationChange( '20150629191735', 0, 0 ) - ); - $this->assertNotContains( '(diff | hist)', strip_tags( $html ) ); - } - - public function testCategorizationLineFormattingWithRevision() { - $html = $this->createCategorizationLine( - $this->getCategorizationChange( '20150629191735', 1025, 1024 ) - ); - $this->assertContains( '(diff | hist)', strip_tags( $html ) ); - } - /** * @todo more tests for actual formatting, this is more of a smoke test */ @@ -129,24 +115,6 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { return $recentChange; } - /** - * @return RecentChange - */ - private function getCategorizationChange( $timestamp, $thisId, $lastId ) { - $wikiPage = new WikiPage( Title::newFromText( 'Testpage' ) ); - $wikiPage->doEditContent( new WikitextContent( 'Some random text' ), 'page created' ); - - $wikiPage = new WikiPage( Title::newFromText( 'Category:Foo' ) ); - $wikiPage->doEditContent( new WikitextContent( 'Some random text' ), 'category page created' ); - - $user = $this->getTestUser(); - $recentChange = $this->testRecentChangesHelper->makeCategorizationRecentChange( - $user, 'Category:Foo', $wikiPage->getId(), $thisId, $lastId, $timestamp - ); - - return $recentChange; - } - /** * @return User */ @@ -160,15 +128,4 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { return $user; } - private function createCategorizationLine( $recentChange ) { - $enhancedChangesList = $this->newEnhancedChangesList(); - $cacheEntry = $this->testRecentChangesHelper->getCacheEntry( $recentChange ); - - $reflection = new \ReflectionClass( get_class( $enhancedChangesList ) ); - $method = $reflection->getMethod( 'recentChangesBlockLine' ); - $method->setAccessible( true ); - - return $method->invokeArgs( $enhancedChangesList, array( $cacheEntry ) ); - } - } diff --git a/tests/phpunit/includes/changes/TestRecentChangesHelper.php b/tests/phpunit/includes/changes/TestRecentChangesHelper.php index e59825a9f5..2506087bfa 100644 --- a/tests/phpunit/includes/changes/TestRecentChangesHelper.php +++ b/tests/phpunit/includes/changes/TestRecentChangesHelper.php @@ -97,36 +97,6 @@ class TestRecentChangesHelper { return $change; } - public function getCacheEntry( $recentChange ) { - $rcCacheFactory = new RCCacheEntryFactory( - new RequestContext(), - array( 'diff' => 'diff', 'cur' => 'cur', 'last' => 'last' ) - ); - return $rcCacheFactory->newFromRecentChange( $recentChange, false ); - } - - public function makeCategorizationRecentChange( - User $user, $titleText, $curid, $thisid, $lastid, $timestamp - ) { - - $attribs = array_merge( - $this->getDefaultAttributes( $titleText, $timestamp ), - array( - 'rc_type' => RC_CATEGORIZE, - 'rc_user' => $user->getId(), - 'rc_user_text' => $user->getName(), - 'rc_this_oldid' => $thisid, - 'rc_last_oldid' => $lastid, - 'rc_cur_id' => $curid, - 'rc_comment' => '[[:Testpage]] added to category', - 'rc_old_len' => 0, - 'rc_new_len' => 0, - ) - ); - - return $this->makeRecentChange( $attribs, 0, 0 ); - } - private function getDefaultAttributes( $titleText, $timestamp ) { return array( 'rc_id' => 545, diff --git a/tests/phpunit/includes/content/TextContentHandlerTest.php b/tests/phpunit/includes/content/TextContentHandlerTest.php index 33861f11e3..492fec6b68 100644 --- a/tests/phpunit/includes/content/TextContentHandlerTest.php +++ b/tests/phpunit/includes/content/TextContentHandlerTest.php @@ -4,7 +4,6 @@ * @group ContentHandler */ class TextContentHandlerTest extends MediaWikiLangTestCase { - public function testSupportsDirectEditing() { $handler = new TextContentHandler(); $this->assertTrue( $handler->supportsDirectEditing(), 'direct editing is supported' ); diff --git a/tests/phpunit/includes/content/TextContentTest.php b/tests/phpunit/includes/content/TextContentTest.php index dd61f85b4e..fe263756a0 100644 --- a/tests/phpunit/includes/content/TextContentTest.php +++ b/tests/phpunit/includes/content/TextContentTest.php @@ -27,10 +27,16 @@ class TextContentTest extends MediaWikiLangTestCase { CONTENT_MODEL_JAVASCRIPT, ), 'wgUseTidy' => false, - 'wgAlwaysUseTidy' => false, 'wgCapitalLinks' => true, 'wgHooks' => array(), // bypass hook ContentGetParserOutput that force custom rendering ) ); + + MWTidy::destroySingleton(); + } + + protected function tearDown() { + MWTidy::destroySingleton(); + parent::tearDown(); } public function newContent( $text ) { diff --git a/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php index 05c32a0463..6ee54d3380 100644 --- a/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php +++ b/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php @@ -28,6 +28,13 @@ use TestingAccessWrapper; class LineFormatterTest extends MediaWikiTestCase { + public function setUp() { + if ( !class_exists( 'Monolog\Formatter\LineFormatter' ) ) { + $this->markTestSkipped( 'This test requires monolog to be installed' ); + } + parent::setUp(); + } + /** * @covers LineFormatter::normalizeException */ @@ -41,10 +48,10 @@ class LineFormatterTest extends MediaWikiTestCase { ) ); $out = $fixture->normalizeException( $boom ); - $this->assertContains( '[Exception InvalidArgumentException]', $out ); - $this->assertContains( ', [Exception LengthException]', $out ); - $this->assertContains( ', [Exception LogicException]', $out ); - $this->assertNotContains( '[stacktrace]', $out ); + $this->assertContains( "\n[Exception InvalidArgumentException]", $out ); + $this->assertContains( "\nCaused by: [Exception LengthException]", $out ); + $this->assertContains( "\nCaused by: [Exception LogicException]", $out ); + $this->assertNotContains( "\n #0", $out ); } /** @@ -60,9 +67,9 @@ class LineFormatterTest extends MediaWikiTestCase { ) ); $out = $fixture->normalizeException( $boom ); - $this->assertContains( '[Exception InvalidArgumentException', $out ); - $this->assertContains( ', [Exception LengthException]', $out ); - $this->assertContains( ', [Exception LogicException]', $out ); - $this->assertContains( '[stacktrace]', $out ); + $this->assertContains( "\n[Exception InvalidArgumentException]", $out ); + $this->assertContains( "\nCaused by: [Exception LengthException]", $out ); + $this->assertContains( "\nCaused by: [Exception LogicException]", $out ); + $this->assertContains( "\n #0", $out ); } } diff --git a/tests/phpunit/includes/deferred/LinksUpdateTest.php b/tests/phpunit/includes/deferred/LinksUpdateTest.php index efbfe6f6a9..02f6b2ab2d 100644 --- a/tests/phpunit/includes/deferred/LinksUpdateTest.php +++ b/tests/phpunit/includes/deferred/LinksUpdateTest.php @@ -19,8 +19,7 @@ class LinksUpdateTest extends MediaWikiTestCase { 'externallinks', 'imagelinks', 'templatelinks', - 'iwlinks', - 'recentchanges', + 'iwlinks' ) ); } @@ -42,12 +41,6 @@ class LinksUpdateTest extends MediaWikiTestCase { ); } - public function addDBData() { - $this->insertPage( 'Testing' ); - $this->insertPage( 'Some_other_page' ); - $this->insertPage( 'Template:TestingTemplate' ); - } - protected function makeTitleAndParserOutput( $name, $id ) { $t = Title::newFromText( $name ); $t->mArticleID = $id; # XXX: this is fugly @@ -140,61 +133,6 @@ class LinksUpdateTest extends MediaWikiTestCase { ) ); } - public function testOnAddingAndRemovingCategory_recentChangesRowIsAdded() { - $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' ); - - $title = Title::newFromText( 'Testing' ); - $wikiPage = new WikiPage( $title ); - $wikiPage->doEditContent( new WikitextContent( '[[Category:Foo]]' ), 'added category' ); - - $this->assertRecentChangeByCategorization( - $title, - $wikiPage->getParserOutput( new ParserOptions() ), - Title::newFromText( 'Category:Foo' ), - array( array( 'Foo', '[[:Testing]] added to category' ) ) - ); - - $wikiPage->doEditContent( new WikitextContent( '[[Category:Bar]]' ), 'added category' ); - $this->assertRecentChangeByCategorization( - $title, - $wikiPage->getParserOutput( new ParserOptions() ), - Title::newFromText( 'Category:Foo' ), - array( - array( 'Foo', '[[:Testing]] added to category' ), - array( 'Foo', '[[:Testing]] removed from category' ), - ) - ); - - $this->assertRecentChangeByCategorization( - $title, - $wikiPage->getParserOutput( new ParserOptions() ), - Title::newFromText( 'Category:Bar' ), - array( - array( 'Bar', '[[:Testing]] added to category' ), - ) - ); - } - - public function testOnAddingAndRemovingCategoryToTemplates_embeddingPagesAreIgnored() { - $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' ); - - $templateTitle = Title::newFromText( 'Template:TestingTemplate' ); - $templatePage = new WikiPage( $templateTitle ); - - $wikiPage = new WikiPage( Title::newFromText( 'Testing' ) ); - $wikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' ); - $otherWikiPage = new WikiPage( Title::newFromText( 'Some_other_page' ) ); - $otherWikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' ); - $templatePage->doEditContent( new WikitextContent( '[[Category:Foo]]' ), 'added category' ); - - $this->assertRecentChangeByCategorization( - $templateTitle, - $templatePage->getParserOutput( new ParserOptions() ), - Title::newFromText( 'Foo' ), - array( array( 'Foo', '[[:Template:TestingTemplate]] and 2 pages added to category' ) ) - ); - } - /** * @covers ParserOutput::addInterwikiLink */ @@ -325,26 +263,4 @@ class LinksUpdateTest extends MediaWikiTestCase { $this->assertSelect( $table, $fields, $condition, $expectedRows ); return $update; } - - protected function assertRecentChangeByCategorization( - Title $pageTitle, ParserOutput $parserOutput, Title $categoryTitle, $expectedRows - ) { - $update = new LinksUpdate( $pageTitle, $parserOutput ); - $revision = Revision::newFromTitle( $pageTitle ); - $update->setRevision( $revision ); - $update->beginTransaction(); - $update->doUpdate(); - $update->commitTransaction(); - - $this->assertSelect( - 'recentchanges', - 'rc_title, rc_comment', - array( - 'rc_type' => RC_CATEGORIZE, - 'rc_namespace' => NS_CATEGORY, - 'rc_title' => $categoryTitle->getDBkey() - ), - $expectedRows - ); - } } diff --git a/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php b/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php index 65db7e4963..551d3a7661 100644 --- a/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php +++ b/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php @@ -75,11 +75,11 @@ class MigrateFileRepoLayoutTest extends MediaWikiTestCase { } protected function tearDown() { - foreach ( glob( $this->tmpPrefix . '*' ) as $directory ) { - $this->deleteFilesRecursively( $directory ); - } + foreach ( glob( $this->tmpPrefix . '*' ) as $directory ) { + $this->deleteFilesRecursively( $directory ); + } - unlink( $this->tmpFilepath ); + unlink( $this->tmpFilepath ); parent::tearDown(); } diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php index 22ad6ce0f7..7841f30f48 100644 --- a/tests/phpunit/includes/libs/CSSMinTest.php +++ b/tests/phpunit/includes/libs/CSSMinTest.php @@ -102,12 +102,12 @@ class CSSMinTest extends MediaWikiTestCase { array( 'Without trailing slash', array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux', false ), - 'foo { prop: url(http://example.org/quux/../bar.png); }', + 'foo { prop: url(http://example.org/bar.png); }', ), array( 'With trailing slash on remote (bug 27052)', array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux/', false ), - 'foo { prop: url(http://example.org/quux/../bar.png); }', + 'foo { prop: url(http://example.org/bar.png); }', ), array( 'Guard against stripping double slashes from query', diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php index e96953ee12..57668e505c 100644 --- a/tests/phpunit/includes/libs/IEUrlExtensionTest.php +++ b/tests/phpunit/includes/libs/IEUrlExtensionTest.php @@ -170,4 +170,37 @@ class IEUrlExtensionTest extends PHPUnit_Framework_TestCase { 'Two dots' ); } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testScriptQuery() { + $this->assertEquals( + 'php', + IEUrlExtension::findIE6Extension( 'example.php?foo=a&bar=b' ), + 'Script with query' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testEscapedScriptQuery() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'example%2Ephp?foo=a&bar=b' ), + 'Script with urlencoded dot and query' + ); + } + + /** + * @covers IEUrlExtension::findIE6Extension + */ + public function testEscapedScriptQueryDot() { + $this->assertEquals( + 'y', + IEUrlExtension::findIE6Extension( 'example%2Ephp?foo=a.x&bar=b.y' ), + 'Script with urlencoded dot and query with dot' + ); + } } diff --git a/tests/phpunit/includes/libs/IPSetTest.php b/tests/phpunit/includes/libs/IPSetTest.php deleted file mode 100644 index 5bbacef42d..0000000000 --- a/tests/phpunit/includes/libs/IPSetTest.php +++ /dev/null @@ -1,252 +0,0 @@ - expected (boolean) result against the config dataset. - */ - public static function provideIPSets() { - return array( - array( - 'old_list_subset', - array( - '208.80.152.162', - '10.64.0.123', - '10.64.0.124', - '10.64.0.125', - '10.64.0.126', - '10.64.0.127', - '10.64.0.128', - '10.64.0.129', - '10.64.32.104', - '10.64.32.105', - '10.64.32.106', - '10.64.32.107', - '91.198.174.45', - '91.198.174.46', - '91.198.174.47', - '91.198.174.57', - '2620:0:862:1:A6BA:DBFF:FE30:CFB3', - '91.198.174.58', - '2620:0:862:1:A6BA:DBFF:FE38:FFDA', - '208.80.152.16', - '208.80.152.17', - '208.80.152.18', - '208.80.152.19', - '91.198.174.102', - '91.198.174.103', - '91.198.174.104', - '91.198.174.105', - '91.198.174.106', - '91.198.174.107', - '91.198.174.81', - '2620:0:862:1:26B6:FDFF:FEF5:B2D4', - '91.198.174.82', - '2620:0:862:1:26B6:FDFF:FEF5:ABB4', - '10.20.0.113', - '2620:0:862:102:26B6:FDFF:FEF5:AD9C', - '10.20.0.114', - '2620:0:862:102:26B6:FDFF:FEF5:7C38', - ), - array( - '0.0.0.0' => false, - '255.255.255.255' => false, - '10.64.0.122' => false, - '10.64.0.123' => true, - '10.64.0.124' => true, - '10.64.0.129' => true, - '10.64.0.130' => false, - '91.198.174.81' => true, - '91.198.174.80' => false, - '0::0' => false, - 'ffff:ffff:ffff:ffff:FFFF:FFFF:FFFF:FFFF' => false, - '2001:db8::1234' => false, - '2620:0:862:1:26b6:fdff:fef5:abb3' => false, - '2620:0:862:1:26b6:fdff:fef5:abb4' => true, - '2620:0:862:1:26b6:fdff:fef5:abb5' => false, - ), - ), - array( - 'new_cidr_set', - array( - '208.80.154.0/26', - '2620:0:861:1::/64', - '208.80.154.128/26', - '2620:0:861:2::/64', - '208.80.154.64/26', - '2620:0:861:3::/64', - '208.80.155.96/27', - '2620:0:861:4::/64', - '10.64.0.0/22', - '2620:0:861:101::/64', - '10.64.16.0/22', - '2620:0:861:102::/64', - '10.64.32.0/22', - '2620:0:861:103::/64', - '10.64.48.0/22', - '2620:0:861:107::/64', - '91.198.174.0/25', - '2620:0:862:1::/64', - '10.20.0.0/24', - '2620:0:862:102::/64', - '10.128.0.0/24', - '2620:0:863:101::/64', - '10.2.4.26', - ), - array( - '0.0.0.0' => false, - '255.255.255.255' => false, - '10.2.4.25' => false, - '10.2.4.26' => true, - '10.2.4.27' => false, - '10.20.0.255' => true, - '10.128.0.0' => true, - '10.64.17.55' => true, - '10.64.20.0' => false, - '10.64.27.207' => false, - '10.64.31.255' => false, - '0::0' => false, - 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => false, - '2001:DB8::1' => false, - '2620:0:861:106::45' => false, - '2620:0:862:103::' => false, - '2620:0:862:102:10:20:0:113' => true, - ), - ), - array( - 'empty_set', - array(), - array( - '0.0.0.0' => false, - '255.255.255.255' => false, - '10.2.4.25' => false, - '10.2.4.26' => false, - '10.2.4.27' => false, - '10.20.0.255' => false, - '10.128.0.0' => false, - '10.64.17.55' => false, - '10.64.20.0' => false, - '10.64.27.207' => false, - '10.64.31.255' => false, - '0::0' => false, - 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => false, - '2001:DB8::1' => false, - '2620:0:861:106::45' => false, - '2620:0:862:103::' => false, - '2620:0:862:102:10:20:0:113' => false, - ), - ), - array( - 'edge_cases', - array( - '0.0.0.0', - '255.255.255.255', - '::', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', - '10.10.10.10/25', // host bits intentional - ), - array( - '0.0.0.0' => true, - '255.255.255.255' => true, - '10.2.4.25' => false, - '10.2.4.26' => false, - '10.2.4.27' => false, - '10.20.0.255' => false, - '10.128.0.0' => false, - '10.64.17.55' => false, - '10.64.20.0' => false, - '10.64.27.207' => false, - '10.64.31.255' => false, - '0::0' => true, - 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => true, - '2001:DB8::1' => false, - '2620:0:861:106::45' => false, - '2620:0:862:103::' => false, - '2620:0:862:102:10:20:0:113' => false, - '10.10.9.255' => false, - '10.10.10.0' => true, - '10.10.10.1' => true, - '10.10.10.10' => true, - '10.10.10.126' => true, - '10.10.10.127' => true, - '10.10.10.128' => false, - '10.10.10.177' => false, - '10.10.10.255' => false, - '10.10.11.0' => false, - ), - ), - array( - 'exercise_optimizer', - array( - 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fffd:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fffb:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fffa:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:8000/113', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:0/113', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff7:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff6:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff5:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff4:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff3:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff2:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff1:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffef:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffee:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffec:0/111', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffeb:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffea:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe9:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe8:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe7:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe6:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe5:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe4:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe3:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe2:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe1:0/112', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/110', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/107', - 'ffff:ffff:ffff:ffff:ffff:ffff:ffa0:0/107', - ), - array( - '0.0.0.0' => false, - '255.255.255.255' => false, - '::' => false, - 'ffff:ffff:ffff:ffff:ffff:ffff:ff9f:ffff' => false, - 'ffff:ffff:ffff:ffff:ffff:ffff:ffa0:0' => true, - 'ffff:ffff:ffff:ffff:ffff:ffff:ffc0:1234' => true, - 'ffff:ffff:ffff:ffff:ffff:ffff:ffed:ffff' => true, - 'ffff:ffff:ffff:ffff:ffff:ffff:fff4:4444' => true, - 'ffff:ffff:ffff:ffff:ffff:ffff:fff9:8080' => true, - 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => true, - ), - ), - ); - } - - /** - * Validates IPSet loading and matching code - * - * @covers IPSet - * @dataProvider provideIPSets - */ - public function testIPSet( $desc, array $cfg, array $tests ) { - $ipset = new IPSet( $cfg ); - foreach ( $tests as $ip => $expected ) { - $result = $ipset->match( $ip ); - $this->assertEquals( $expected, $result, "Incorrect match() result for $ip in dataset $desc" ); - } - } -} diff --git a/tests/phpunit/includes/objectcache/BagOStuffTest.php b/tests/phpunit/includes/objectcache/BagOStuffTest.php index fcc15d317b..b684006202 100644 --- a/tests/phpunit/includes/objectcache/BagOStuffTest.php +++ b/tests/phpunit/includes/objectcache/BagOStuffTest.php @@ -1,6 +1,7 @@ + * @group BagOStuff */ class BagOStuffTest extends MediaWikiTestCase { /** @var BagOStuff */ @@ -137,21 +138,25 @@ class BagOStuffTest extends MediaWikiTestCase { public function testGetMulti() { $value1 = array( 'this' => 'is', 'a' => 'test' ); $value2 = array( 'this' => 'is', 'another' => 'test' ); + $value3 = array( 'testing a key that may be encoded when sent to cache backend' ); $key1 = wfMemcKey( 'test1' ); $key2 = wfMemcKey( 'test2' ); + $key3 = wfMemcKey( 'will-%-encode' ); // internally, MemcachedBagOStuffs will encode to will-%25-encode $this->cache->add( $key1, $value1 ); $this->cache->add( $key2, $value2 ); + $this->cache->add( $key3, $value3 ); $this->assertEquals( - $this->cache->getMulti( array( $key1, $key2 ) ), - array( $key1 => $value1, $key2 => $value2 ) + array( $key1 => $value1, $key2 => $value2, $key3 => $value3 ), + $this->cache->getMulti( array( $key1, $key2, $key3 ) ) ); // cleanup $this->cache->delete( $key1 ); $this->cache->delete( $key2 ); + $this->cache->delete( $key3 ); } /** @@ -169,5 +174,12 @@ class BagOStuffTest extends MediaWikiTestCase { $value3 = $this->cache->getScopedLock( $key, 0 ); $this->assertType( 'ScopedCallback', $value3, 'Lock returned callback after release' ); + unset( $value3 ); + + $value1 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' ); + $value2 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' ); + + $this->assertType( 'ScopedCallback', $value1, 'First reentrant call returned lock' ); + $this->assertType( 'ScopedCallback', $value1, 'Second reentrant call returned lock' ); } } diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php index df891f5a4b..96ae3bec64 100644 --- a/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -91,7 +91,7 @@ class MediaWikiParserTest { // enough to cause there to be separate names for different // things, which is good enough for our purposes. $extensionName = basename( dirname( $fileName ) ); - $testsName = $extensionName . '⁄' . basename( $fileName, '.txt' ); + $testsName = $extensionName . '__' . basename( $fileName, '.txt' ); $escapedFileName = strtr( $fileName, array( "'" => "\\'", '\\' => '\\\\' ) ); $parserTestClassName = ucfirst( $testsName ); // Official spec for class names: http://php.net/manual/en/language.oop5.basic.php diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php index c7a310367b..0d2129b729 100644 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -160,10 +160,10 @@ class NewParserTest extends MediaWikiTestCase { $this->djVuSupport = new DjVuSupport(); // Tidy support $this->tidySupport = new TidySupport(); + $tmpGlobals['wgTidyConfig'] = null; $tmpGlobals['wgUseTidy'] = false; - $tmpGlobals['wgAlwaysUseTidy'] = false; $tmpGlobals['wgDebugTidy'] = false; - $tmpGlobals['wgTidyConf'] = $IP . '/includes/tidy.conf'; + $tmpGlobals['wgTidyConf'] = $IP . '/includes/tidy/tidy.conf'; $tmpGlobals['wgTidyOpts'] = ''; $tmpGlobals['wgTidyInternal'] = $this->tidySupport->isInternal(); @@ -185,6 +185,8 @@ class NewParserTest extends MediaWikiTestCase { $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias']; $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias']; + MWTidy::destroySingleton(); + // Restore backends RepoGroup::destroySingleton(); FileBackendGroup::destroySingleton(); @@ -454,6 +456,7 @@ class NewParserTest extends MediaWikiTestCase { $GLOBALS[$var] = $val; } + MWTidy::destroySingleton(); MagicWord::clearCache(); # The entries saved into RepoGroup cache with previous globals will be wrong. diff --git a/tests/phpunit/includes/parser/TagHooksTest.php b/tests/phpunit/includes/parser/TagHooksTest.php index 3605e50f14..4af389852a 100644 --- a/tests/phpunit/includes/parser/TagHooksTest.php +++ b/tests/phpunit/includes/parser/TagHooksTest.php @@ -19,12 +19,6 @@ class TagHookTest extends MediaWikiTestCase { return array( array( "foobar" ), array( "foo\nbar" ), array( "foo\rbar" ) ); } - protected function setUp() { - parent::setUp(); - - $this->setMwGlobals( 'wgAlwaysUseTidy', false ); - } - /** * @dataProvider provideValidNames * @covers Parser::setHook diff --git a/tests/phpunit/includes/parser/TidyTest.php b/tests/phpunit/includes/parser/TidyTest.php index f656a74dbf..5db2908048 100644 --- a/tests/phpunit/includes/parser/TidyTest.php +++ b/tests/phpunit/includes/parser/TidyTest.php @@ -7,8 +7,7 @@ class TidyTest extends MediaWikiTestCase { protected function setUp() { parent::setUp(); - $check = MWTidy::tidy( '' ); - if ( strpos( $check, '