From: jenkins-bot Date: Tue, 22 Sep 2015 04:57:47 +0000 (+0000) Subject: Merge "registration: Allow extensions to specify which MW core versions they require" X-Git-Tag: 1.31.0-rc.0~9925 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/exercices/modifier.php?a=commitdiff_plain;h=35f61c7c4b38388dc498f898193f1dda0461e0b3;hp=cef1f31167177ca05c8981d61d1203172329345d;p=lhc%2Fweb%2Fwiklou.git Merge "registration: Allow extensions to specify which MW core versions they require" --- 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/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 5b85bbcc7e..19309a80d6 100644 --- a/RELEASE-NOTES-1.26 +++ b/RELEASE-NOTES-1.26 @@ -27,6 +27,9 @@ production. MediaWiki 1.26, in where ResourceLoader became fully asynchronous. * $wgMasterWaitTimeout was removed (deprecated in 1.24). * Fields in ParserOptions are now private. Use the accessors instead. +* Custom LESS functions (defined via $wgResourceLoaderLESSFunctions) + have been removed, after being deprecated in 1.24. +* $wgAlwaysUseTidy has been removed. === New features in 1.26 === * (T51506) Now action=info gives estimates of actual watchers for a page. @@ -66,6 +69,10 @@ production. 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. @@ -79,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 @@ -97,10 +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 === @@ -118,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 @@ -162,6 +184,12 @@ 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. +* DatabaseBase::errorCount() was removed (unused). +* $wgDeferredUpdateList was removed. +* DeferredUpdates::addHTMLCacheUpdate() was removed. == Compatibility == diff --git a/autoload.php b/autoload.php index b97e190bbf..bdcbe5a6e0 100644 --- a/autoload.php +++ b/autoload.php @@ -149,6 +149,7 @@ $wgAutoloadLocalClasses = array( 'AutoLoader' => __DIR__ . '/includes/AutoLoader.php', 'AutoloadGenerator' => __DIR__ . '/includes/utils/AutoloadGenerator.php', 'Autopromote' => __DIR__ . '/includes/Autopromote.php', + 'AvroValidator' => __DIR__ . '/includes/utils/AvroValidator.php', 'BacklinkCache' => __DIR__ . '/includes/cache/BacklinkCache.php', 'BacklinkJobUtils' => __DIR__ . '/includes/jobqueue/utils/BacklinkJobUtils.php', 'BackupDumper' => __DIR__ . '/maintenance/backup.inc', @@ -725,7 +726,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', @@ -755,6 +755,9 @@ $wgAutoloadLocalClasses = array( 'MediaWiki\\Logger\\LegacySpi' => __DIR__ . '/includes/debug/logger/LegacySpi.php', 'MediaWiki\\Logger\\LoggerFactory' => __DIR__ . '/includes/debug/logger/LoggerFactory.php', 'MediaWiki\\Logger\\MonologSpi' => __DIR__ . '/includes/debug/logger/MonologSpi.php', + 'MediaWiki\\Logger\\Monolog\\AvroFormatter' => __DIR__ . '/includes/debug/logger/monolog/AvroFormatter.php', + 'MediaWiki\\Logger\\Monolog\\BufferHandler' => __DIR__ . '/includes/debug/logger/monolog/BufferHandler.php', + 'MediaWiki\\Logger\\Monolog\\KafkaHandler' => __DIR__ . '/includes/debug/logger/monolog/KafkaHandler.php', 'MediaWiki\\Logger\\Monolog\\LegacyFormatter' => __DIR__ . '/includes/debug/logger/monolog/LegacyFormatter.php', 'MediaWiki\\Logger\\Monolog\\LegacyHandler' => __DIR__ . '/includes/debug/logger/monolog/LegacyHandler.php', 'MediaWiki\\Logger\\Monolog\\LineFormatter' => __DIR__ . '/includes/debug/logger/monolog/LineFormatter.php', @@ -762,7 +765,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', diff --git a/composer.json b/composer.json index adfe01b8b0..cce664a048 100644 --- a/composer.json +++ b/composer.json @@ -19,17 +19,17 @@ "composer/semver": "0.1.0", "cssjanus/cssjanus": "1.1.1", "ext-iconv": "*", - "leafo/lessphp": "0.5.0", "liuggio/statsd-php-client": "1.0.16", - "mediawiki/at-ease": "1.0.0", - "oojs/oojs-ui": "0.12.6", + "oyejorge/less.php": "1.7.0.5", + "mediawiki/at-ease": "1.1.0", + "oojs/oojs-ui": "0.12.8.1", "php": ">=5.3.3", "psr/log": "1.0.0", "wikimedia/assert": "0.2.2", - "wikimedia/cdb": "1.0.1", + "wikimedia/cdb": "1.3.0", "wikimedia/composer-merge-plugin": "1.2.1", "wikimedia/ip-set": "1.0.1", - "wikimedia/utfnormal": "1.0.2", + "wikimedia/utfnormal": "1.0.3", "wikimedia/wrappedstring": "2.0.0", "zordius/lightncandy": "0.21" }, @@ -46,9 +46,11 @@ "ext-wikidiff2": "Diff accelerator", "ext-apc": "Local data and opcode cache", "monolog/monolog": "Flexible debug logging system", + "nmred/kafka-php": "Send debug log events to kafka", "pear/mail": "Mail sending support", "pear/mail_mime": "Mail sending support", - "pear/mail_mime-decode": "Mail sending support" + "pear/mail_mime-decode": "Mail sending support", + "wikimedia/avro": "Binary serialization format used with kafka" }, "autoload": { "psr-0": { diff --git a/docs/extension.schema.json b/docs/extension.schema.json index ece38fe9fb..218a19c3b9 100644 --- a/docs/extension.schema.json +++ b/docs/extension.schema.json @@ -439,10 +439,6 @@ "type": "object", "description": "ResourceLoader LESS variables" }, - "ResourceLoaderLESSFunctions": { - "type": "object", - "description": "ResourceLoader LESS functions" - }, "ResourceLoaderLESSImportPaths": { "type": "object", "description": "ResourceLoader import paths" @@ -649,6 +645,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 5e2269abb7..f5999a5f47 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -662,7 +662,8 @@ $oldPageID: the page ID of the revision when archived (may be null) 'ArticleRevisionVisibilitySet': Called when changing visibility of one or more revisions of an article. -&$title: Title object of the article +$title: Title object of the article +$ids: Ids to set the visibility for 'ArticleRollbackComplete': After an article rollback is completed. $wikiPage: the WikiPage that was edited @@ -1271,20 +1272,21 @@ $user: User being checked $confirmed: Whether or not the email address is confirmed 'EmailUser': Before sending email from one user to another. -$to: address of receiving user -$from: address of sending user +$to: MailAddress object of receiving user +$from: MailAddress object of sending user $subject: subject of the mail $text: text of the mail +&$error: Out-param for an error 'EmailUserCC': Before sending the copy of the email to the author. -$to: address of receiving user -$from: address of sending user +$to: MailAddress object of receiving user +$from: MailAddress object of sending user $subject: subject of the mail $text: text of the mail 'EmailUserComplete': After sending email from one user to another. -$to: address of receiving user -$from: address of sending user +$to: MailAddress object of receiving user +$from: MailAddress object of sending user $subject: subject of the mail $text: text of the mail @@ -1376,6 +1378,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. 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 c0aad5df04..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; @@ -3473,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; @@ -3636,18 +3636,6 @@ $wgResourceLoaderValidateStaticJS = 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 @@ -4116,44 +4104,55 @@ $wgEnableImageWhitelist = true; $wgAllowImageTag = false; /** - * $wgUseTidy: use tidy to make sure HTML output is sane. - * Tidy is a free tool that fixes broken HTML. - * See http://www.w3.org/People/Raggett/tidy/ + * Configuration for HTML postprocessing tool. Set this to a configuration + * array to enable an external tool. Dave Raggett's "HTML Tidy" is typically + * used. See http://www.w3.org/People/Raggett/tidy/ * - * - $wgTidyBin should be set to the path of the binary and - * - $wgTidyConf to the path of the configuration file. - * - $wgTidyOpts can include any number of parameters. - * - $wgTidyInternal controls the use of the PECL extension or the - * libtidy (PHP >= 5) extension to use an in-process tidy library instead - * of spawning a separate program. - * Normally you shouldn't need to override the setting except for - * debugging. To install, use 'pear install tidy' and add a line - * 'extension=tidy.so' to php.ini. + * If this is null and $wgUseTidy is true, the deprecated configuration + * parameters will be used instead. + * + * If this is null and $wgUseTidy is false, a pure PHP fallback will be used. + * + * Keys are: + * - driver: May be: + * - RaggettInternalHHVM: Use the limited-functionality HHVM extension + * - RaggettInternalPHP: Use the PECL extension + * - RaggettExternal: Shell out to an external binary (tidyBin) + * + * - tidyConfigFile: Path to configuration file for any of the Raggett drivers + * - debugComment: True to add a comment to the output with warning messages + * - tidyBin: For RaggettExternal, the path to the tidy binary. + * - tidyCommandLine: For RaggettExternal, additional command line options. */ -$wgUseTidy = false; +$wgTidyConfig = null; /** - * @see $wgUseTidy + * Set this to true to use the deprecated tidy configuration parameters. + * @deprecated use $wgTidyConfig */ -$wgAlwaysUseTidy = false; +$wgUseTidy = false; /** - * @see $wgUseTidy + * The path to the tidy binary. + * @deprecated Use $wgTidyConfig['tidyBin'] */ $wgTidyBin = 'tidy'; /** - * @see $wgUseTidy + * The path to the tidy config file + * @deprecated Use $wgTidyConfig['tidyConfigFile'] */ -$wgTidyConf = $IP . '/includes/tidy.conf'; +$wgTidyConf = $IP . '/includes/tidy/tidy.conf'; /** - * @see $wgUseTidy + * The command line options to the tidy binary + * @deprecated Use $wgTidyConfig['tidyCommandLine'] */ $wgTidyOpts = ''; /** - * @see $wgUseTidy + * Set this to true to use the tidy extension + * @deprecated Use $wgTidyConfig['driver'] */ $wgTidyInternal = extension_loaded( 'tidy' ); @@ -7237,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 diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 5e8b2c3aeb..58927dc6df 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. @@ -4274,3 +4288,28 @@ function wfThumbIsStandard( File $file, array $params ) { return true; } + +/** + * Merges two (possibly) 2 dimensional arrays into the target array ($baseArray). + * + * Values that exist in both values will be combined with += (all values of the array + * of $newValues will be added to the values of the array of $baseArray, while values, + * that exists in both, the value of $baseArray will be used). + * + * @param array $baseArray The array where you want to add the values of $newValues to + * @param array $newValues An array with new values + * @return array The combined array + * @since 1.26 + */ +function wfArrayPlus2d( array $baseArray, array $newValues ) { + // First merge items that are in both arrays + foreach ( $baseArray as $name => &$groupVal ) { + if ( isset( $newValues[$name] ) ) { + $groupVal += $newValues[$name]; + } + } + // Now add items that didn't exist yet + $baseArray += $newValues; + + return $baseArray; +} diff --git a/includes/Hooks.php b/includes/Hooks.php index 036d65c71e..a414562436 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -231,22 +231,25 @@ class Hooks { } /** - * Handle PHP errors issued inside a hook. Catch errors that have to do with - * a function expecting a reference, and let all others pass through. - * - * This REALLY should be protected... but it's public for compatibility + * Handle PHP errors issued inside a hook. Catch errors that have to do + * with a function expecting a reference, and pass all others through to + * MWExceptionHandler::handleError() for default processing. * * @since 1.18 * * @param int $errno Error number (unused) * @param string $errstr Error message * @throws MWHookException If the error has to do with the function signature - * @return bool Always returns false + * @return bool */ public static function hookErrorHandler( $errno, $errstr ) { if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) { throw new MWHookException( $errstr, $errno ); } - return false; + + // Delegate unhandled errors to the default MW handler + return call_user_func_array( + 'MWExceptionHandler::handleError', func_get_args() + ); } } diff --git a/includes/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 1c794853a0..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(); @@ -926,13 +982,7 @@ class PhpHttpRequest extends MWHttpRequest { } } - 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 ); @@ -949,9 +999,10 @@ 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. @@ -997,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/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/OutputPage.php b/includes/OutputPage.php index 073762a035..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 ee2b278d4c..f0d905e599 100644 --- a/tests/phpunit/includes/OutputPageTest.php +++ b/tests/phpunit/includes/OutputPageTest.php @@ -259,6 +259,86 @@ class OutputPageTest extends MediaWikiTestCase { $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/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 affb0fa9a5..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' ); @@ -1289,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/RecentChangeTest.php b/tests/phpunit/includes/changes/RecentChangeTest.php index 60235b8f12..4d1a936eaf 100644 --- a/tests/phpunit/includes/changes/RecentChangeTest.php +++ b/tests/phpunit/includes/changes/RecentChangeTest.php @@ -10,8 +10,8 @@ class RecentChangeTest extends MediaWikiTestCase { protected $user_comment; protected $context; - public function __construct() { - parent::__construct(); + public function setUp() { + parent::setUp(); $this->title = Title::newFromText( 'SomeTitle' ); $this->target = Title::newFromText( 'TestTarget' ); @@ -21,6 +21,26 @@ class RecentChangeTest extends MediaWikiTestCase { $this->context = RequestContext::newExtraneousContext( $this->title ); } + /** + * @covers RecentChange::newFromRow + * @covers RecentChange::loadFromRow + */ + public function testNewFromRow() { + $row = new stdClass(); + $row->rc_foo = 'AAA'; + $row->rc_timestamp = '20150921134808'; + $row->rc_deleted = 'bar'; + + $rc = RecentChange::newFromRow( $row ); + + $expected = array( + 'rc_foo' => 'AAA', + 'rc_timestamp' => '20150921134808', + 'rc_deleted' => 'bar', + ); + $this->assertEquals( $expected, $rc->getAttributes() ); + } + /** * The testIrcMsgForAction* tests are supposed to cover the hacky * LogFormatter::getIRCActionText / bug 34508 @@ -59,291 +79,6 @@ class RecentChangeTest extends MediaWikiTestCase { * - undo */ - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypeBlock() { - $sep = $this->context->msg( 'colon-separator' )->text(); - - # block/block - $this->assertIRCComment( - $this->context->msg( 'blocklogentry', 'SomeTitle', 'duration', '(flags)' )->plain() - . $sep . $this->user_comment, - 'block', 'block', - array( - '5::duration' => 'duration', - '6::flags' => 'flags', - ), - $this->user_comment - ); - # block/block - legacy - $this->assertIRCComment( - $this->context->msg( 'blocklogentry', 'SomeTitle', 'duration', '(flags)' )->plain() - . $sep . $this->user_comment, - 'block', 'block', - array( - 'duration', - 'flags', - ), - $this->user_comment, - '', - true - ); - # block/unblock - $this->assertIRCComment( - $this->context->msg( 'unblocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment, - 'block', 'unblock', - array(), - $this->user_comment - ); - # block/reblock - $this->assertIRCComment( - $this->context->msg( 'reblock-logentry', 'SomeTitle', 'duration', '(flags)' )->plain() - . $sep . $this->user_comment, - 'block', 'reblock', - array( - '5::duration' => 'duration', - '6::flags' => 'flags', - ), - $this->user_comment - ); - } - - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypeDelete() { - $sep = $this->context->msg( 'colon-separator' )->text(); - - # delete/delete - $this->assertIRCComment( - $this->context->msg( 'deletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, - 'delete', 'delete', - array(), - $this->user_comment - ); - - # delete/restore - $this->assertIRCComment( - $this->context->msg( 'undeletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, - 'delete', 'restore', - array(), - $this->user_comment - ); - } - - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypeNewusers() { - $this->assertIRCComment( - 'New user account', - 'newusers', 'newusers', - array() - ); - $this->assertIRCComment( - 'New user account', - 'newusers', 'create', - array() - ); - $this->assertIRCComment( - 'created new account SomeTitle', - 'newusers', 'create2', - array() - ); - $this->assertIRCComment( - 'Account created automatically', - 'newusers', 'autocreate', - array() - ); - } - - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypeMove() { - $move_params = array( - '4::target' => $this->target->getPrefixedText(), - '5::noredir' => 0, - ); - $sep = $this->context->msg( 'colon-separator' )->text(); - - # move/move - $this->assertIRCComment( - $this->context->msg( '1movedto2', 'SomeTitle', 'TestTarget' ) - ->plain() . $sep . $this->user_comment, - 'move', 'move', - $move_params, - $this->user_comment - ); - - # move/move_redir - $this->assertIRCComment( - $this->context->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' ) - ->plain() . $sep . $this->user_comment, - 'move', 'move_redir', - $move_params, - $this->user_comment - ); - } - - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypePatrol() { - # patrol/patrol - $this->assertIRCComment( - $this->context->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(), - 'patrol', 'patrol', - array( - '4::curid' => '777', - '5::previd' => '666', - '6::auto' => 0, - ) - ); - } - - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypeProtect() { - $protectParams = array( - '[edit=sysop] (indefinite) ‎[move=sysop] (indefinite)' - ); - $sep = $this->context->msg( 'colon-separator' )->text(); - - # protect/protect - $this->assertIRCComment( - $this->context->msg( 'protectedarticle', 'SomeTitle ' . $protectParams[0] ) - ->plain() . $sep . $this->user_comment, - 'protect', 'protect', - $protectParams, - $this->user_comment - ); - - # protect/unprotect - $this->assertIRCComment( - $this->context->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, - 'protect', 'unprotect', - array(), - $this->user_comment - ); - - # protect/modify - $this->assertIRCComment( - $this->context->msg( 'modifiedarticleprotection', 'SomeTitle ' . $protectParams[0] ) - ->plain() . $sep . $this->user_comment, - 'protect', 'modify', - $protectParams, - $this->user_comment - ); - - # protect/move_prot - $this->assertIRCComment( - $this->context->msg( 'movedarticleprotection', 'SomeTitle', 'OldTitle' ) - ->plain() . $sep . $this->user_comment, - 'protect', 'move_prot', - array( - '4::oldtitle' => 'OldTitle' - ), - $this->user_comment - ); - } - - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypeUpload() { - $sep = $this->context->msg( 'colon-separator' )->text(); - - # upload/upload - $this->assertIRCComment( - $this->context->msg( 'uploadedimage', 'SomeTitle' )->plain() . $sep . $this->user_comment, - 'upload', 'upload', - array(), - $this->user_comment - ); - - # upload/overwrite - $this->assertIRCComment( - $this->context->msg( 'overwroteimage', 'SomeTitle' )->plain() . $sep . $this->user_comment, - 'upload', 'overwrite', - array(), - $this->user_comment - ); - } - - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypeMerge() { - $sep = $this->context->msg( 'colon-separator' )->text(); - - # merge/merge - $this->assertIRCComment( - $this->context->msg( 'pagemerge-logentry', 'SomeTitle', 'Dest', 'timestamp' )->plain() - . $sep . $this->user_comment, - 'merge', 'merge', - array( - '4::dest' => 'Dest', - '5::mergepoint' => 'timestamp', - ), - $this->user_comment - ); - } - - /** - * @covers LogFormatter::getIRCActionText - */ - public function testIrcMsgForLogTypeImport() { - $sep = $this->context->msg( 'colon-separator' )->text(); - - # import/upload - $msg = $this->context->msg( 'import-logentry-upload', 'SomeTitle' )->plain() . - $sep . - $this->user_comment; - $this->assertIRCComment( - $msg, - 'import', 'upload', - array(), - $this->user_comment - ); - - # import/interwiki - $msg = $this->context->msg( 'import-logentry-interwiki', 'SomeTitle' )->plain() . - $sep . - $this->user_comment; - $this->assertIRCComment( - $msg, - 'import', 'interwiki', - array(), - $this->user_comment - ); - } - - /** - * @todo Emulate these edits somehow and extract - * raw edit summary from RecentChange object - * -- - */ - /* - public function testIrcMsgForBlankingAES() { - // $this->context->msg( 'autosumm-blank', .. ); - } - - public function testIrcMsgForReplaceAES() { - // $this->context->msg( 'autosumm-replace', .. ); - } - - public function testIrcMsgForRollbackAES() { - // $this->context->msg( 'revertpage', .. ); - } - - public function testIrcMsgForUndoAES() { - // $this->context->msg( 'undo-summary', .. ); - } - */ - /** * @covers RecentChange::parseParams */ @@ -390,35 +125,50 @@ class RecentChangeTest extends MediaWikiTestCase { } /** - * @param string $expected Expected IRC text without colors codes - * @param string $type Log type (move, delete, suppress, patrol ...) - * @param string $action A log type action - * @param array $params - * @param string $comment (optional) A comment for the log action - * @param string $msg (optional) A message for PHPUnit :-) + * 50 mins and 100 mins are used here as the tests never take that long! + * @return array */ - protected function assertIRCComment( $expected, $type, $action, $params, - $comment = null, $msg = '', $legacy = false - ) { - $logEntry = new ManualLogEntry( $type, $action ); - $logEntry->setPerformer( $this->user ); - $logEntry->setTarget( $this->title ); - if ( $comment !== null ) { - $logEntry->setComment( $comment ); - } - $logEntry->setParameters( $params ); - $logEntry->setLegacy( $legacy ); - - $formatter = LogFormatter::newFromEntry( $logEntry ); - $formatter->setContext( $this->context ); + public function provideIsInRCLifespan() { + return array( + array( 6000, time() - 3000, 0, true ), + array( 3000, time() - 6000, 0, false ), + array( 6000, time() - 3000, 6000, true ), + array( 3000, time() - 6000, 6000, true ), + ); + } - // Apply the same transformation as done in IRCColourfulRCFeedFormatter::getLine for rc_comment - $ircRcComment = IRCColourfulRCFeedFormatter::cleanupForIRC( $formatter->getIRCActionComment() ); + /** + * @covers RecentChange::isInRCLifespan + * @dataProvider provideIsInRCLifespan + */ + public function testIsInRCLifespan( $maxAge, $timestamp, $tolerance, $expected ) { + $this->setMwGlobals( 'wgRCMaxAge', $maxAge ); + $this->assertEquals( $expected, RecentChange::isInRCLifespan( $timestamp, $tolerance ) ); + } - $this->assertEquals( - $expected, - $ircRcComment, - $msg + public function provideRCTypes() { + return array( + array( RC_EDIT, 'edit' ), + array( RC_NEW, 'new' ), + array( RC_LOG, 'log' ), + array( RC_EXTERNAL, 'external' ), ); } + + /** + * @dataProvider provideRCTypes + * @covers RecentChange::parseFromRCType + */ + public function testParseFromRCType( $rcType, $type ) { + $this->assertEquals( $type, RecentChange::parseFromRCType( $rcType ) ); + } + + /** + * @dataProvider provideRCTypes + * @covers RecentChange::parseToRCType + */ + public function testParseToRCType( $rcType, $type ) { + $this->assertEquals( $rcType, RecentChange::parseToRCType( $type ) ); + } + } 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/AvroFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php new file mode 100644 index 0000000000..4c6d25e0ca --- /dev/null +++ b/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php @@ -0,0 +1,64 @@ +markTestSkipped( 'Avro is required for the AvroFormatterTest' ); + } + parent::setUp(); + } + + public function testSchemaNotAvailable() { + $formatter = new AvroFormatter( array() ); + $this->setExpectedException( 'PHPUnit_Framework_Error_Notice', "The schema for channel 'marty' is not available" ); + $formatter->format( array( 'channel' => 'marty' ) ); + } + + public function testSchemaNotAvailableReturnValue() { + $formatter = new AvroFormatter( array() ); + $noticeEnabled = PHPUnit_Framework_Error_Notice::$enabled; + // disable conversion of notices + PHPUnit_Framework_Error_Notice::$enabled = false; + // have to keep the user notice from being output + wfSuppressWarnings(); + $res = $formatter->format( array( 'channel' => 'marty' ) ); + wfRestoreWarnings(); + PHPUnit_Framework_Error_Notice::$enabled = $noticeEnabled; + $this->assertNull( $res ); + } + + public function testDoesSomethingWhenSchemaAvailable() { + $formatter = new AvroFormatter( array( 'string' => array( 'type' => 'string' ) ) ); + $res = $formatter->format( array( + 'channel' => 'string', + 'context' => 'better to be', + ) ); + $this->assertNotNull( $res ); + // basically just tell us if avro changes its string encoding + $this->assertEquals( base64_decode( 'GGJldHRlciB0byBiZQ==' ), $res ); + } +} diff --git a/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php b/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php new file mode 100644 index 0000000000..272f6e4827 --- /dev/null +++ b/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php @@ -0,0 +1,204 @@ +markTestSkipped( 'Monolog is required for the KafkaHandlerTest' ); + } + parent::setUp(); + } + + public function topicNamingProvider() { + return array( + array( array(), 'monolog_foo' ), + array( array( 'alias' => array( 'foo' => 'bar' ) ), 'bar' ) + ); + } + + /** + * @dataProvider topicNamingProvider + */ + public function testTopicNaming( $options, $expect ) { + $produce = $this->getMockBuilder( 'Kafka\Produce' ) + ->disableOriginalConstructor() + ->getMock(); + $produce->expects($this->any()) + ->method('getAvailablePartitions') + ->will($this->returnValue( array( 'A' ) ) ); + $produce->expects($this->once()) + ->method( 'setMessages' ) + ->with( $expect, $this->anything(), $this->anything() ); + + $handler = new KafkaHandler( $produce, $options ); + $handler->handle( array( + 'channel' => 'foo', + 'level' => Logger::EMERGENCY, + 'extra' => array(), + ) ); + } + + public function swallowsExceptionsWhenRequested() { + return array( + // defaults to false + array( array(), true ), + // also try false explicitly + array( array( 'swallowExceptions' => false ), true ), + // turn it on + array( array( 'swallowExceptions' => true ), false ), + ); + } + + /** + * @dataProvider swallowsExceptionsWhenRequested + */ + public function testGetAvailablePartitionsException( $options, $expectException ) { + $produce = $this->getMockBuilder( 'Kafka\Produce' ) + ->disableOriginalConstructor() + ->getMock(); + $produce->expects( $this->any() ) + ->method( 'getAvailablePartitions' ) + ->will( $this->throwException( new \Kafka\Exception ) ); + + if ( $expectException ) { + $this->setExpectedException( 'Kafka\Exception' ); + } + + $handler = new KafkaHandler( $produce, $options ); + $handler->handle( array( + 'channel' => 'foo', + 'level' => Logger::EMERGENCY, + 'extra' => array(), + ) ); + + if ( !$expectException ) { + $this->assertTrue( true, 'no exception was thrown' ); + } + } + + /** + * @dataProvider swallowsExceptionsWhenRequested + */ + public function testSendException( $options, $expectException ) { + $produce = $this->getMockBuilder( 'Kafka\Produce' ) + ->disableOriginalConstructor() + ->getMock(); + $produce->expects( $this->any() ) + ->method( 'getAvailablePartitions' ) + ->will( $this->returnValue( array( 'A' ) ) ); + $produce->expects( $this->any() ) + ->method( 'send' ) + ->will( $this->throwException( new \Kafka\Exception ) ); + + if ( $expectException ) { + $this->setExpectedException( 'Kafka\Exception' ); + } + + $handler = new KafkaHandler( $produce, $options ); + $handler->handle( array( + 'channel' => 'foo', + 'level' => Logger::EMERGENCY, + 'extra' => array(), + ) ); + + if ( !$expectException ) { + $this->assertTrue( true, 'no exception was thrown' ); + } + } + + public function testHandlesNullFormatterResult() { + $produce = $this->getMockBuilder( 'Kafka\Produce' ) + ->disableOriginalConstructor() + ->getMock(); + $produce->expects( $this->any() ) + ->method( 'getAvailablePartitions' ) + ->will( $this->returnValue( array( 'A' ) ) ); + $mockMethod = $produce->expects( $this->exactly( 2 ) ) + ->method( 'setMessages' ); + // evil hax + \TestingAccessWrapper::newFromObject( $mockMethod )->matcher->parametersMatcher = + new \PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters( array( + array( $this->anything(), $this->anything(), array( 'words' ) ), + array( $this->anything(), $this->anything(), array( 'lines' ) ) + ) ); + + $formatter = $this->getMock( 'Monolog\Formatter\FormatterInterface' ); + $formatter->expects( $this->any() ) + ->method( 'format' ) + ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) ); + + $handler = new KafkaHandler( $produce, array() ); + $handler->setFormatter( $formatter ); + for ( $i = 0; $i < 3; ++$i ) { + $handler->handle( array( + 'channel' => 'foo', + 'level' => Logger::EMERGENCY, + 'extra' => array(), + ) ); + } + } + + + public function testBatchHandlesNullFormatterResult() { + $produce = $this->getMockBuilder( 'Kafka\Produce' ) + ->disableOriginalConstructor() + ->getMock(); + $produce->expects( $this->any() ) + ->method( 'getAvailablePartitions' ) + ->will( $this->returnValue( array( 'A' ) ) ); + $produce->expects( $this->once() ) + ->method( 'setMessages' ) + ->with( $this->anything(), $this->anything(), array( 'words', 'lines' ) ); + + $formatter = $this->getMock( 'Monolog\Formatter\FormatterInterface' ); + $formatter->expects( $this->any() ) + ->method( 'format' ) + ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) ); + + $handler = new KafkaHandler( $produce, array() ); + $handler->setFormatter( $formatter ); + $handler->handleBatch( array( + array( + 'channel' => 'foo', + 'level' => Logger::EMERGENCY, + 'extra' => array(), + ), + array( + 'channel' => 'foo', + 'level' => Logger::EMERGENCY, + 'extra' => array(), + ), + array( + 'channel' => 'foo', + 'level' => Logger::EMERGENCY, + 'extra' => array(), + ), + ) ); + } +} diff --git a/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php index f12cf5bd80..6ee54d3380 100644 --- a/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php +++ b/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php @@ -48,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 ); } /** @@ -67,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/filebackend/FileBackendTest.php b/tests/phpunit/includes/filebackend/FileBackendTest.php index aaa93efbb5..2e4942f0d8 100644 --- a/tests/phpunit/includes/filebackend/FileBackendTest.php +++ b/tests/phpunit/includes/filebackend/FileBackendTest.php @@ -2392,6 +2392,56 @@ class FileBackendTest extends MediaWikiTestCase { "Scoped unlocking of files succeeded with OK status ($backendName)." ); } + public function testReadAffinity() { + $be = TestingAccessWrapper::newFromObject( + new FileBackendMultiWrite( array( + 'name' => 'localtesting', + 'wikiId' => wfWikiId() . mt_rand(), + 'backends' => array( + array( // backend 0 + 'name' => 'multitesting0', + 'class' => 'MemoryFileBackend', + 'isMultiMaster' => false, + 'readAffinity' => true + ), + array( // backend 1 + 'name' => 'multitesting1', + 'class' => 'MemoryFileBackend', + 'isMultiMaster' => true + ) + ) + ) ) + ); + + $this->assertEquals( + 1, + $be->getReadIndexFromParams( array( 'latest' => 1 ) ), + 'Reads with "latest" flag use backend 1' + ); + $this->assertEquals( + 0, + $be->getReadIndexFromParams( array( 'latest' => 0 ) ), + 'Reads without "latest" flag use backend 0' + ); + + $p = 'container/test-cont/file.txt'; + $be->backends[0]->quickCreate( array( + 'dst' => "mwstore://multitesting0/$p", 'content' => 'cattitude' ) ); + $be->backends[1]->quickCreate( array( + 'dst' => "mwstore://multitesting1/$p", 'content' => 'princess of power' ) ); + + $this->assertEquals( + 'cattitude', + $be->getFileContents( array( 'src' => "mwstore://localtesting/$p" ) ), + "Non-latest read came from backend 0" + ); + $this->assertEquals( + 'princess of power', + $be->getFileContents( array( 'src' => "mwstore://localtesting/$p", 'latest' => 1 ) ), + "Latest read came from backend1" + ); + } + // helper function private function listToArray( $iter ) { return is_array( $iter ) ? $iter : iterator_to_array( $iter ); 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/logging/LogFormatterTest.php b/tests/phpunit/includes/logging/LogFormatterTest.php index 515990e64a..844c9afbc0 100644 --- a/tests/phpunit/includes/logging/LogFormatterTest.php +++ b/tests/phpunit/includes/logging/LogFormatterTest.php @@ -1,4 +1,5 @@ recache( $wgLang->getCode() ); $this->user = User::newFromName( 'Testuser' ); - $this->title = Title::newMainPage(); + $this->title = Title::newFromText( 'SomeTitle' ); + $this->target = Title::newFromText( 'TestTarget' ); $this->context = new RequestContext(); $this->context->setUser( $this->user ); $this->context->setTitle( $this->title ); $this->context->setLanguage( $wgLang ); + + $this->user_comment = ''; } protected function tearDown() { @@ -292,4 +306,309 @@ class LogFormatterTest extends MediaWikiLangTestCase { array( '4:user-link:key', 'foo', array( 'key' => 'Foo' ) ), ); } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeBlock() { + $sep = $this->context->msg( 'colon-separator' )->text(); + + # block/block + $this->assertIRCComment( + $this->context->msg( 'blocklogentry', 'SomeTitle', 'duration', '(flags)' )->plain() + . $sep . $this->user_comment, + 'block', 'block', + array( + '5::duration' => 'duration', + '6::flags' => 'flags', + ), + $this->user_comment + ); + # block/block - legacy + $this->assertIRCComment( + $this->context->msg( 'blocklogentry', 'SomeTitle', 'duration', '(flags)' )->plain() + . $sep . $this->user_comment, + 'block', 'block', + array( + 'duration', + 'flags', + ), + $this->user_comment, + '', + true + ); + # block/unblock + $this->assertIRCComment( + $this->context->msg( 'unblocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'block', 'unblock', + array(), + $this->user_comment + ); + # block/reblock + $this->assertIRCComment( + $this->context->msg( 'reblock-logentry', 'SomeTitle', 'duration', '(flags)' )->plain() + . $sep . $this->user_comment, + 'block', 'reblock', + array( + '5::duration' => 'duration', + '6::flags' => 'flags', + ), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeDelete() { + $sep = $this->context->msg( 'colon-separator' )->text(); + + # delete/delete + $this->assertIRCComment( + $this->context->msg( 'deletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'delete', 'delete', + array(), + $this->user_comment + ); + + # delete/restore + $this->assertIRCComment( + $this->context->msg( 'undeletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'delete', 'restore', + array(), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeNewusers() { + $this->assertIRCComment( + 'New user account', + 'newusers', 'newusers', + array() + ); + $this->assertIRCComment( + 'New user account', + 'newusers', 'create', + array() + ); + $this->assertIRCComment( + 'created new account SomeTitle', + 'newusers', 'create2', + array() + ); + $this->assertIRCComment( + 'Account created automatically', + 'newusers', 'autocreate', + array() + ); + } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeMove() { + $move_params = array( + '4::target' => $this->target->getPrefixedText(), + '5::noredir' => 0, + ); + $sep = $this->context->msg( 'colon-separator' )->text(); + + # move/move + $this->assertIRCComment( + $this->context->msg( '1movedto2', 'SomeTitle', 'TestTarget' ) + ->plain() . $sep . $this->user_comment, + 'move', 'move', + $move_params, + $this->user_comment + ); + + # move/move_redir + $this->assertIRCComment( + $this->context->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' ) + ->plain() . $sep . $this->user_comment, + 'move', 'move_redir', + $move_params, + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypePatrol() { + # patrol/patrol + $this->assertIRCComment( + $this->context->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(), + 'patrol', 'patrol', + array( + '4::curid' => '777', + '5::previd' => '666', + '6::auto' => 0, + ) + ); + } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeProtect() { + $protectParams = array( + '[edit=sysop] (indefinite) ‎[move=sysop] (indefinite)' + ); + $sep = $this->context->msg( 'colon-separator' )->text(); + + # protect/protect + $this->assertIRCComment( + $this->context->msg( 'protectedarticle', 'SomeTitle ' . $protectParams[0] ) + ->plain() . $sep . $this->user_comment, + 'protect', 'protect', + $protectParams, + $this->user_comment + ); + + # protect/unprotect + $this->assertIRCComment( + $this->context->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'protect', 'unprotect', + array(), + $this->user_comment + ); + + # protect/modify + $this->assertIRCComment( + $this->context->msg( 'modifiedarticleprotection', 'SomeTitle ' . $protectParams[0] ) + ->plain() . $sep . $this->user_comment, + 'protect', 'modify', + $protectParams, + $this->user_comment + ); + + # protect/move_prot + $this->assertIRCComment( + $this->context->msg( 'movedarticleprotection', 'SomeTitle', 'OldTitle' ) + ->plain() . $sep . $this->user_comment, + 'protect', 'move_prot', + array( + '4::oldtitle' => 'OldTitle' + ), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeUpload() { + $sep = $this->context->msg( 'colon-separator' )->text(); + + # upload/upload + $this->assertIRCComment( + $this->context->msg( 'uploadedimage', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'upload', 'upload', + array(), + $this->user_comment + ); + + # upload/overwrite + $this->assertIRCComment( + $this->context->msg( 'overwroteimage', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'upload', 'overwrite', + array(), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeMerge() { + $sep = $this->context->msg( 'colon-separator' )->text(); + + # merge/merge + $this->assertIRCComment( + $this->context->msg( 'pagemerge-logentry', 'SomeTitle', 'Dest', 'timestamp' )->plain() + . $sep . $this->user_comment, + 'merge', 'merge', + array( + '4::dest' => 'Dest', + '5::mergepoint' => 'timestamp', + ), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionComment + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeImport() { + $sep = $this->context->msg( 'colon-separator' )->text(); + + # import/upload + $msg = $this->context->msg( 'import-logentry-upload', 'SomeTitle' )->plain() . + $sep . + $this->user_comment; + $this->assertIRCComment( + $msg, + 'import', 'upload', + array(), + $this->user_comment + ); + + # import/interwiki + $msg = $this->context->msg( 'import-logentry-interwiki', 'SomeTitle' )->plain() . + $sep . + $this->user_comment; + $this->assertIRCComment( + $msg, + 'import', 'interwiki', + array(), + $this->user_comment + ); + } + + /** + * @param string $expected Expected IRC text without colors codes + * @param string $type Log type (move, delete, suppress, patrol ...) + * @param string $action A log type action + * @param array $params + * @param string $comment (optional) A comment for the log action + * @param string $msg (optional) A message for PHPUnit :-) + */ + protected function assertIRCComment( $expected, $type, $action, $params, + $comment = null, $msg = '', $legacy = false + ) { + $logEntry = new ManualLogEntry( $type, $action ); + $logEntry->setPerformer( $this->user ); + $logEntry->setTarget( $this->title ); + if ( $comment !== null ) { + $logEntry->setComment( $comment ); + } + $logEntry->setParameters( $params ); + $logEntry->setLegacy( $legacy ); + + $formatter = LogFormatter::newFromEntry( $logEntry ); + $formatter->setContext( $this->context ); + + // Apply the same transformation as done in IRCColourfulRCFeedFormatter::getLine for rc_comment + $ircRcComment = IRCColourfulRCFeedFormatter::cleanupForIRC( $formatter->getIRCActionComment() ); + + $this->assertEquals( + $expected, + $ircRcComment, + $msg + ); + } + } diff --git a/tests/phpunit/includes/objectcache/BagOStuffTest.php b/tests/phpunit/includes/objectcache/BagOStuffTest.php index f5814e4483..b684006202 100644 --- a/tests/phpunit/includes/objectcache/BagOStuffTest.php +++ b/tests/phpunit/includes/objectcache/BagOStuffTest.php @@ -138,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 ); } /** diff --git a/tests/phpunit/includes/objectcache/MultiWriteBagOStuffTest.php b/tests/phpunit/includes/objectcache/MultiWriteBagOStuffTest.php new file mode 100644 index 0000000000..2b66181c9f --- /dev/null +++ b/tests/phpunit/includes/objectcache/MultiWriteBagOStuffTest.php @@ -0,0 +1,55 @@ +cache1 = new HashBagOStuff(); + $this->cache2 = new HashBagOStuff(); + $this->cache = new MultiWriteBagOStuff( array( + 'caches' => array( $this->cache1, $this->cache2 ), + 'replication' => 'async' + ) ); + } + + public function testSetImmediate() { + $key = wfRandomString(); + $value = wfRandomString(); + $this->cache->set( $key, $value ); + + // Set in tier 1 + $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); + // Set in tier 2 + $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); + } + + public function testSetDelayed() { + $key = wfRandomString(); + $value = wfRandomString(); + + // XXX: DeferredUpdates bound to transactions in CLI mode + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + $this->cache->set( $key, $value ); + + // Set in tier 1 + $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' ); + // Not yet set in tier 2 + $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' ); + + $dbw->commit(); + + // Set in tier 2 + $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); + } +} 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..df7da98c62 100644 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -106,7 +106,6 @@ class NewParserTest extends MediaWikiTestCase { $tmpGlobals['wgAdaptiveMessageCache'] = true; $tmpGlobals['wgUseDatabaseMessages'] = true; $tmpGlobals['wgLocaltimezone'] = 'UTC'; - $tmpGlobals['wgDeferredUpdateList'] = array(); $tmpGlobals['wgGroupPermissions'] = array( '*' => array( 'createaccount' => true, @@ -160,10 +159,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 +184,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 +455,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, '